[
  {
    "path": ".babelrc",
    "content": "{}"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [prayag17] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\nlfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry\npolar: # Replace with a single Polar username\nbuy_me_a_coffee: # Replace with a single Buy Me a Coffee username\nthanks_dev: # Replace with a single thanks.dev username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Desktop (please complete the following information):**\n - OS: [e.g. Windows]\n - Version [e.g. v0.0.6-dev]\n - Jellyfin Version [e.g. 10.9]\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/release.yaml",
    "content": "changelog:\n  categories:\n    - title: 🏕 Features\n      labels:\n        - '*'\n      exclude:\n        labels:\n          - dependencies\n    - title: 👒 Dependencies\n      labels:\n        - dependencies\n"
  },
  {
    "path": ".github/workflows/continuous-integration.yml",
    "content": "# @format\n\nname: Continuous Integration\non:\n     push:\n          branches:\n               - main\n     pull_request:\n          types: [opened, synchronize, reopened, ready_for_review]\n\njobs:\n     build_web:\n          runs-on: ubuntu-latest\n          steps:\n               - name: Checkout repository\n                 uses: actions/checkout@v6\n\n               - uses: pnpm/action-setup@v4\n                 name: Install pnpm\n                 with:\n                      version: 10\n                      run_install: false\n\n               - name: Get pnpm store directory\n                 shell: bash\n                 run: |\n                      echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n\n               - uses: actions/cache@v5\n                 name: Setup pnpm cache\n                 with:\n                      path: ${{ env.STORE_PATH }}\n                      key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n                      restore-keys: |\n                           ${{ runner.os }}-pnpm-store-\n\n               - name: Sync node version and setup cache\n                 uses: actions/setup-node@v6\n                 with:\n                      node-version: \"lts/*\"\n                      cache: \"pnpm\"\n\n               - name: Install dependencies\n                 run: pnpm install --no-frozen-lockfile\n\n               # - name: Lint\n               #   run: pnpm run lint\n\n               - name: Build\n                 run: pnpm run build\n\n     build:\n          needs: build_web\n          # Prevent forked PRs from running the build job which is expensive\n          if: github.repository == 'prayag17/Blink'\n          strategy:\n               fail-fast: false\n               matrix:\n                    platform: [macos-latest, ubuntu-22.04, windows-latest]\n          runs-on: ${{ matrix.platform }}\n          environment: TAURI\n          steps:\n               - name: Checkout repository\n                 uses: actions/checkout@v6\n\n               - name: Install dependencies (ubuntu only)\n                 if: matrix.platform == 'ubuntu-22.04'\n                 # You can remove libayatana-appindicator3-dev if you don't use the system tray feature.\n                 run: |\n                      sudo apt-get update\n                      sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev librsvg2-dev\n\n               - name: Rust setup\n                 uses: dtolnay/rust-toolchain@stable\n\n               - name: Rust cache\n                 uses: swatinem/rust-cache@v2\n                 with:\n                      workspaces: \"./src-tauri -> target\"\n\n               - uses: pnpm/action-setup@v4\n                 name: Install pnpm\n                 with:\n                      version: 10\n                      run_install: false\n\n               - name: Get pnpm store directory\n                 shell: bash\n                 run: |\n                      echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n\n               - uses: actions/cache@v5\n                 name: Setup pnpm cache\n                 with:\n                      path: ${{ env.STORE_PATH }}\n                      key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n                      restore-keys: |\n                           ${{ runner.os }}-pnpm-store-\n\n               - name: Sync node version and setup cache\n                 uses: actions/setup-node@v6\n                 with:\n                      node-version: \"lts/*\"\n                      cache: \"pnpm\"\n\n               - name: Install dependencies\n                 run: pnpm install --no-frozen-lockfile\n\n               - name: Build app\n                 run: pnpm run tauri build --debug\n                 env:\n                      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n                      TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}\n                      TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}\n                      TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}\n                      TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}\n\n                      # TODO: Remove the below command after upstream issue is fixed\n                      # Related issue: https://github.com/tauri-apps/tauri/issues/10702\n                      WEBKIT_DISABLE_DMABUF_RENDERER: 1 # Disable DMABUF renderer on Wayland\n\n               - name: Upload build Artifacts\n                 uses: actions/upload-artifact@v6\n                 with:\n                      name: Blink-${{ matrix.platform }}-dev${{ github.sha }}\n                      path: src-tauri/target/debug/bundle/*\n\n     # Create or update nightly release\n     nightly_release:\n          needs: build\n          if: github.repository == 'prayag17/Blink' && github.event_name == 'push' && github.ref == 'refs/heads/main'\n          runs-on: ubuntu-latest\n          permissions:\n               contents: write     # Required for creating/updating releases\n          steps:\n               - name: Checkout repository\n                 uses: actions/checkout@v6.0.1\n\n               - name: Download all artifacts\n                 uses: actions/download-artifact@v7.0.0\n                 with:\n                    path: artifacts\n                    \n               - name: Prepare assets\n                 run: |\n                   mkdir -p release_assets\n                   find artifacts -type f \\( -name \"*.exe\" -o -name \"*.msi\" -o -name \"*.dmg\" -o -name \"*.deb\" -o -name \"*.AppImage\" -o -name \"*.rpm\" -o -name \"*.tar.gz\" \\) | while read -r file; do\n                     # Extract platform from artifact name (e.g. Blink-ubuntu-22.04-devSHA)\n                     artifact_name=$(echo \"$file\" | cut -d'/' -f2)\n                     platform=$(echo \"$artifact_name\" | sed -E 's/Blink-(.*)-dev.*/\\1/' | sed 's/-/_/g')\n                     filename=$(basename \"$file\")\n                     cp \"$file\" \"release_assets/${platform}-${filename}\"\n                   done\n\n               - name: Delete old nightly release\n                 run: gh release delete nightly --yes --repo ${{ github.repository }} || true\n                 env:\n                    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n               - name: Create nightly release\n                 uses: softprops/action-gh-release@v2.5.0\n                 with:\n                    tag_name: nightly\n                    name: Nightly Build (${{ github.sha }})\n                    body: |\n                      Nightly build from the latest main branch.\n                      \n                      **Last commit:** ${{ github.event.head_commit.message }}\n                      **SHA:** ${{ github.sha }}\n                    prerelease: true\n                    files: release_assets/*\n                    fail_on_unmatched_files: true\n\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "# @format\n\nname: Release\non:\n     push:\n          tags:\n               - \"v*\"\n     workflow_dispatch:\n\njobs:\n     release:\n          strategy:\n               fail-fast: false\n               matrix:\n                    platform: [macos-latest, ubuntu-22.04, windows-latest]\n          runs-on: ${{ matrix.platform }}\n          environment: TAURI\n          steps:\n               - name: Checkout repository\n                 uses: actions/checkout@v6\n\n               - name: Install dependencies (ubuntu only)\n                 if: matrix.platform == 'ubuntu-22.04'\n                 # You can remove libayatana-appindicator3-dev if you don't use the system tray feature.\n                 run: |\n                      sudo apt-get update\n                      sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev librsvg2-dev\n\n               - name: Rust setup\n                 uses: dtolnay/rust-toolchain@stable\n                 with:\n                      # Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.\n                      targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}\n\n               - name: Rust cache\n                 uses: swatinem/rust-cache@v2\n                 with:\n                      workspaces: \"./src-tauri -> target\"\n\n               - uses: pnpm/action-setup@v4\n                 name: Install pnpm\n                 with:\n                      version: 10\n                      run_install: false\n\n               - name: Get pnpm store directory\n                 shell: bash\n                 run: |\n                      echo \"STORE_PATH=$(pnpm store path --silent)\" >> $GITHUB_ENV\n\n               - uses: actions/cache@v5\n                 name: Setup pnpm cache\n                 with:\n                      path: ${{ env.STORE_PATH }}\n                      key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}\n                      restore-keys: |\n                           ${{ runner.os }}-pnpm-store-\n\n               - name: Sync node version and setup cache\n                 uses: actions/setup-node@v6\n                 with:\n                      node-version: \"lts/*\"\n                      cache: \"pnpm\" # Set this to npm, yarn or pnpm.\n\n               - name: Install app dependencies and build web\n                 # Remove `&& yarn build` if you build your frontend in `beforeBuildCommand`\n                 run: pnpm install --no-frozen-lockfile && pnpm run build # Change this to npm, yarn or pnpm.\n\n               - name: Build the app\n                 uses: tauri-apps/tauri-action@v0\n\n                 env:\n                      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n                      TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}\n                      TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}\n                      TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}\n                      TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}\n                 with:\n                      tagName: ${{ github.ref_name }} # This only works if your workflow triggers on new tags.\n                      releaseName: \"App Name v__VERSION__\" # tauri-action replaces \\_\\_VERSION\\_\\_ with the app version.\n                      releaseBody: \"See the assets to download and install this version.\"\n                      releaseDraft: true\n                      prerelease: false\n                      args: ${{ matrix.platform == 'macos-latest' && '--target universal-apple-darwin' || '' }}\n"
  },
  {
    "path": ".github/workflows/winget-releaser.yml",
    "content": "name: Publish Releases to WinGet\non:\n  release:\n    types: [published]\njobs:\n  publish-alpha:\n    runs-on: ubuntu-latest\n    if: contains(github.event.release.tag_name, '-alpha')\n    steps:\n      - uses: vedantmgoyal9/winget-releaser@main\n        with:\n          identifier: prayag17.Blink.Alpha\n          version: ${{ github.event.release.tag_name }}\n          installers-regex: '\\.exe$'\n          token: ${{ secrets.WINGET_TOKEN }}\n\n  publish-stable:\n    runs-on: ubuntu-latest\n    if: \"!contains(github.event.release.tag_name, '-alpha')\"\n    steps:\n      - uses: vedantmgoyal9/winget-releaser@main\n        with:\n          identifier: prayag17.Blink\n          version: ${{ github.event.release.tag_name }}\n          installers-regex: '\\.exe$'\n          token: ${{ secrets.WINGET_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "*dist\n*build\n__pycache__\n*.mkv\n*.mp4\n*mpv*\nsh-agent*\n.pytest_cache\n\n### react ###\n.DS_*\n*.log\nlogs\n**/*.backup.*\n**/*.back.*\n\nnode_modules\nbower_components\n\n*.sublime*\n\npsd\nthumb\nsketch\n\n### Rust ###\n# Generated by Cargo\n# will have compiled files and executables\ndebug/\ntarget/\n\n# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries\n# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html\n# Cargo.lock\n\n# These are backup files generated by rustfmt\n**/*.rs.bk\n\n# MSVC Windows builds of rustc generate these, which store debugging information\n*.pdb\n\ngenerateBanner\n# Million Lint\n.million\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n    \"recommendations\": [\n        \"biomejs.biome\",\n        \"github.vscode-github-actions\",\n        \"swellaby.rust-pack\",\n        \"1yib.rust-bundle\",\n        \"sibiraj-s.vscode-scss-formatter\",\n        \"tauri-apps.tauri-vscode\"\n    ]\n}"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n    \"version\": \"0.2.0\",\n    \"configurations\": [\n        {\n            \"type\": \"lldb\",\n            \"request\": \"launch\",\n            \"name\": \"Tauri Development Debug\",\n            \"cargo\": {\n                \"args\": [\n                    \"build\",\n                    \"--manifest-path=./src-tauri/Cargo.toml\",\n                    \"--no-default-features\"\n                ]\n            },\n            // task for the `beforeDevCommand` if used, must be configured in `.vscode/tasks.json`\n            \"preLaunchTask\": \"ui:dev\"\n        },\n        {\n            \"type\": \"lldb\",\n            \"request\": \"launch\",\n            \"name\": \"Tauri Production Debug\",\n            \"cargo\": {\n                \"args\": [\n                    \"build\",\n                    \"--release\",\n                    \"--manifest-path=./src-tauri/Cargo.toml\"\n                ]\n            },\n            // task for the `beforeBuildCommand` if used, must be configured in `.vscode/tasks.json`\n            \"preLaunchTask\": \"ui:build\"\n        }\n    ]\n}"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n\t\"editor.defaultFormatter\": \"biomejs.biome\",\n\t\"editor.codeActionsOnSave\": {\n\t\t\"quickfix.biome\": \"always\",\n\t\t\"source.organizeImports.biome\": \"explicit\"\n\t},\n\t\"[github-actions-workflow]\": {\n\t\t\"editor.defaultFormatter\": \"esbenp.prettier-vscode\"\n\t},\n\t\"[json]\": {\n\t\t\"editor.defaultFormatter\": \"biomejs.biome\"\n\t},\n\t\"[typescriptreact]\": {\n\t\t\"editor.defaultFormatter\": \"biomejs.biome\"\n\t},\n\t\"[typescript]\": {\n\t\t\"editor.defaultFormatter\": \"biomejs.biome\"\n\t},\n\t\"[javascriptreact]\": {\n\t\t\"editor.defaultFormatter\": \"biomejs.biome\"\n\t},\n\t\"[javascript]\": {\n\t\t\"editor.defaultFormatter\": \"biomejs.biome\"\n\t},\n\t\"rust-analyzer.linkedProjects\": [\"./src-tauri/Cargo.toml\"],\n\t\"typescript.tsdk\": \"node_modules/typescript/lib\",\n\t\"[scss]\": {\n\t\t\"editor.defaultFormatter\": \"vscode.css-language-features\"\n\t},\n\t\"millionLint.theme\": \"million\",\n\t\"millionLint.disableOutdatedVersionMessage\": true\n}\n"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n    // See https://go.microsoft.com/fwlink/?LinkId=733558\n    // for the documentation about the tasks.json format\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n        {\n            \"label\": \"ui:dev\",\n            \"type\": \"shell\",\n            // `dev` keeps running in the background\n            // ideally you should also configure a `problemMatcher`\n            // see https://code.visualstudio.com/docs/editor/tasks#_can-a-background-task-be-used-as-a-prelaunchtask-in-launchjson\n            \"isBackground\": true,\n            // change this to your `beforeDevCommand`:\n            \"command\": \"yarn\",\n            \"args\": [\n                \"dev\"\n            ]\n        },\n        {\n            \"label\": \"ui:build\",\n            \"type\": \"shell\",\n            // change this to your `beforeBuildCommand`:\n            \"command\": \"yarn\",\n            \"args\": [\n                \"build\"\n            ]\n        }\n    ]\n}"
  },
  {
    "path": "CHANGELOG.md",
    "content": "- # Alpha 3 (11/12/2024)\n    - refactor: prepare for alpha 3\n    - refactor: cleanup\n    - refactor: remove unused imports\n    - fix: series page not fetching correct season\n    - feat: add key binding for macos users #435\n    - refactor: remove jellyPlayer naming from code #460\n    - feat: add playlist playback support\n    - refactor: improve server add process\n    - feat: show current time while scrubbing video progress #339\n    - style: increase the sharpness of card images\n    - feat: add skip recap button\n    - refactor: cleanup video player\n    - feat: add chapter navigation buttons to video player #457\n    - feat: allow changing audio track from video player\n    - feat: allow changing chapters from a chapter list #457\n    - refactor: cleanup search route\n    - refactor: cleanup some components\n    - refactor: unmount card image if not inView\n    - refactor: cleanup person route\n    - style: replace noto-sans with plus-jakarta-sans\n    - refactor: cleanup login routes\n    - fix: library items not of same size #380\n    - refactor: cleanup item.tsx\n    - fix: series page breaking if season is not loaded\n    - fix: do not display missing items in series\n    - refactor: move more files to ts\n    - refactor: move more files to ts\n    - refactor: move more files to ts\n    - refactor: move more files to typescript\n    - refactor: move more components to ts\n    - refactore: cleanup __root.tsx\n    - chore(deps): update dependencies\n    - fix: collections page not rendering #447\n    - fix: quick connect not working #445\n    - fix: playback error if subtitle is not set\n    - fix: index not being assigned proper value #420\n    - fix: synced lyrics not working\n    - Merge pull request #446 from prayag17/renovate/node-22.x-lockfile\n    - refactor: use saved lyrics\n    - refactor: change GenreView to genreFilter\n    - fix(deps): update dependency @types/node to v22.10.1\n    - Merge pull request #393 from prayag17/renovate/tanstack-router-monorepo\n    - Merge pull request #424 from prayag17/renovate/tanstack-query-monorepo\n    - fix(deps): update tanstack-router monorepo\n    - Merge pull request #426 from prayag17/renovate/node-22.x-lockfile\n    - fix(deps): update dependency @types/node to v22.10.0\n    - Merge pull request #437 from prayag17/dependabot/cargo/src-tauri/rustls-0.23.18\n    - fix(deps): update tanstack-query monorepo to v5.61.4\n    - Merge pull request #438 from prayag17/renovate/axios-1.x-lockfile\n    - Merge pull request #439 from prayag17/renovate/vitejs-plugin-react-4.x-lockfile\n    - chore(deps): update dependency @vitejs/plugin-react to v4.3.4\n    - refactore: move more components to typescript\n    - fix: episode and series not playing if episode is not present in nextUp and continueWatching\n    - feat: show current episode in PlayButton\n    - fix: episode indexing for absolute numbering indexNumbers in PlayButton\n    - fix: unable to read Id of undefined in PlayButton\n    - fix(deps): update dependency axios to v1.7.8\n    - chore(deps): bump rustls from 0.23.16 to 0.23.18 in /src-tauri\n    - fix(ci): not uploading debug builds\n    - refactor(ci): build debug builds in CI\n    - Merge pull request #425 from prayag17/renovate/serde_json-1.x-lockfile\n    - Merge pull request #436 from sambartik/macos-universal-dmg\n    - chore(deps): update dependencies\n    - refactor: convert more files to typescript\n    - fix: improper handling of undefined api leading to crashes\n    - refactor: move all buttons to tsx #338\n    - Add universal dmg for macOS in release job\n    - fix: audio queue not moving after 2 changes #182\n    - fix: playing track from album not updating the queue #182\n    - refactor: use MediaSegments api for intro/outro skipping\n    - fix: trickplay images not showing up\n    - refactor: improve login page UI #338 #202\n    - chore(release): update latest.json\n    - fix(deps): update rust crate serde_json to v1.0.133\n\n- # 0.0.7-dev (20/11/2024)\n    - chore(deps): update dependencies\n    - refactor: change UI of login/list and fix Api not being defined in context\n    - fix: player/audio route not able to interact with audioPlayer\n    - fix: audio player info not visible\n    - fix: audio player not playing audio\n    - fix: toggling ssubtitles not working\n    - fix: subtitles not being rendered #401\n    - fix: hero carousel indicators not scrolling\n    - fix: vite build error for safari13\n    - fix: blink not launching or rendering on opening\n    - Merge branch 'main' of https://github.com/prayag17/JellyPlayer\n    - chore(deps): update dependencies\n    - Merge pull request #410 from prayag17/renovate/tauri-plugin-notification-2.x-lockfile\n    - Merge pull request #409 from Blackspirits/patch-1\n    - chore(deps): update dependencies\n    - (origin/renovate/tauri-plugin-notification-2.x-lockfile) fix(deps): update rust crate tauri-plugin-notification to v2.0.1  \n    - feat: allow downloading images from ImageViewer\n    - Update index.tsx\n    - feat: implement basic image viewer #338 #240\n    - fix: react infinite depth error\n    - fix: vite config\n    - refactor: remove @million/js and @million/lint\n    - refactor: remove Cargo.lock from .gitignore #400\n    - chore(deps): update dependencies\n    - refactor: code cleanup\n    - fix: bug_report showing extra fields\n    - refactor: remove redundant calls for getCurrentUser\n    - feat: show queue item from currently playing item in queueButton\n    - chore(deps): update dependencies\n    - feat: remember library filters #192\n    - Merge pull request #370 from prayag17/renovate/tauri-apps-plugin-clipboard-manager-2.x\n    - fix(deps): update dependency @tauri-apps/plugin-clipboard-manager to v2.1.0-beta.6\n    - Merge pull request #381 from prayag17/renovate/wavesurfer.js-7.x-lockfile\n    - Merge pull request #342 from prayag17/renovate/tanstack-router-monorepo\n    - Merge pull request #368 from prayag17/renovate/tauri-monorepo\n    - chore(release): update latest.json\n\n- # 0.0.6-dev (9/7/2024)\n    > **Note** : This is an unstable release\n    - feat: add video player slider bubble theming #337\n    - feat: add trickplay support #338 #337\n    - fix(ci): update env variables\n    - fix(ci): fix ubuntu build failing\n    - chore(deps): update js and rust deps\n    - feat: add chapter markers #337\n    - feat: add subtitle support #337\n    - fix(ci): invalid tauri build command\n    - fix(ci):workflow repo name\n    - feat: remember last search query\n    - feat: add new audioPlayer route and allow reordering queue\n    - chore(deps): update deps\n    - chore(deps): cleanup unused dependencies and update dependencies\n    - refactor: replace wavesurfer with html audio player\n    - feat: add new album page\n    - fix: quick connect not working\n    - chore: change tauri updater signing keys\n    - feat: show upNext card for episode items in player\n    - fix: login page not working\n    - Merge pull request #332 from pypp/feat/double-click-fullscreen\n    - Merge pull request #333 from pypp/feat/scrollwheel-volume-control\n    - fix: removed subtitleRenderer state that was added while merging\n    - fix: fixed bug caused by merge econflicts\n    - Merge remote-tracking branch 'upstream/main' into feat/scrollwheel-volume-control\n    - fix: fixed fucntion to use the new API\n    - Merge remote-tracking branch 'upstream/main' into feat/double-click-fullscreen\n    - Merge branch 'main' of https://github.com/pypp/JellyPlayer into feat/double-click-fullscreen\n    - fix(deps): update dependency babel-plugin-react-compiler to v0.0.0\n    - fix(deps): update tauri monorepo\n    - chore(deps): update dependencies\n    - feat: allow toggling subtitles on and off from player\n    - fix: playNext and playPrevious logic\n    - feat: add intro-skipper\n    - refactor: cleanup unused variables in playbackStore\n    - chore(deps): update packages\n    - refactor: support vtt and ass subtitles\n    - refactor: add sorting options to library page for Series and BoxSets\n    - style: update hero-carousel styling\n    - chore(deps): update deps\n    - fix(deps): update dependency framer-motion to v11.2.9\n    - refactor: move most routing logic to tanstack\n    - refactor: implement new login logic and verify context.api status\n    - chore(deps): update dependencies\n    - refactor: move to tanstack router\n    - fix: videoPlayer not restoring playback position when queueItem is updated\n    - fix: Slight UI regression when changing servers on latest version #288\n    - fix: eac3 codec support\n    - refactor: queueItem playback\n    - fix(deps): update rust crate tauri to 1.6.2 (#298)\n    - fix: episode playback\n    - feat: add queueButton\n    - Merge branch 'main' of https://github.com/prayag17/JellyPlayer\n    - chore(deps): fix vite 5.2 bundle\n    - fix(deps): update dependency @types/react to v18.3.1 (#295)\n    - fix: scrollTrigger in libraryView\n    - style: improve styling\n    - fix: backdrop flashing\n    - feat: use tanstack virtual for rendering library items\n    - style: improve login notice ui\n    - refactor: fetch mediaSource for series\n    - refactor: implement new playback method for playbutton (wip)\n    - ui: improve glass texture\n    - ui: implement new appBar #284\n    - feat: display sdr and hdr only videoRange\n    - style: improve hdr10 logo spacing\n    - chore: update title-movie screenshot in README.md\n    - chore: update title-movie.png\n    - fix: hdr10 icon height not matching\n    - style: update mediainfo icons\n    - style: update mediainfo icons\n    - fix: no subtitle option not selecting\n    - feat: use brand logo for hdr10plus\n    - fix: default subtitle not being selected properly\n    - style: display backdrops in boxset\n    - fix: videoPlayer not exiting if subtitles are disabled\n    - chore: add kitsu icon to iconLink\n    - fix: carousel not rendering\n    - chore: update readme\n    - chore: update screenshots\n    - fix: improve hdr10+ detection\n    - feat: add quality and audio brand labels\n    - style: improve page-margin\n    - fix(player): unable to play media without subtitles\n    - fix: album not displaying if no parent backdrop is found\n    - chore: add screenshots and some features of JellyPlayer #242\n    - chore: upload screenshots\n    - fix(nsis): nsis setup images not displaying correctly\n    - style: Improve titlePage styling\n    - style: change cardText styling\n    - style: Improve overall app component paddings and margins and rollback vite to 5.1.6\n    - chore(deps): update dependency vite to v5.2.6 (#286)\n    - fix(deps): update material-ui monorepo (#270)\n    - fix(deps): update dependency @types/node to v20.11.29 (#292)\n    - fix(deps): update dependency @types/react to v18.2.67 (#293)\n    - fix(deps): update tanstack-query monorepo to v5.28.6 (#271)\n    - fix(deps): update dependency @types/react to v18.2.67 (#272)\n    - fix(deps): update dependency @types/node to v20.11.29 (#274)\n    - fix(deps): update dependency framer-motion to v11.0.15 (#278)\n    - fix(deps): update dependency typescript to v5.4.3 (#287)\n    - fix(deps): update dependency material-symbols to v0.17.1 (#289)\n    - chore(deps): update dependency @biomejs/biome to v1.6.2 (#290)\n    - fix(deps): update rust crate log to ^0.4.21 (#276)\n    - chore: update latest.json\n\n- # v0.0.5-dev\n    > **Note** : This is an unstable release\n    - feat(web): Add new About page (#258) by @prayag17\n    - feat(web): Allow managing servers from settings dialog by @prayag17\n    - feat(search): Add Episode to search by @prayag17\n    - feat(settings-server): Add a refretch button by @prayag17\n    - feat(web): Add a global query fetch/mutation status by @prayag17\n    - fix(server): Use redirect instead of relaunch when changing or deleting a server by @prayag17\n    - refactor(player): Rewrite and improve video playback by @prayag17\n    - feat(web): Use season backdrop in series page if available by @prayag17\n    - feat(player): Allow selecting subtitles during playback by @prayag17\n    - feat(ui): Add new episode layout by @prayag17"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "![Banner](https://github.com/user-attachments/assets/cf3ffbe3-3b48-4fab-bd7e-f011928286fa)\r\n<div align=\"center\">\r\n<img alt=\"GitHub Release\" src=\"https://img.shields.io/github/v/release/prayag17/Blink?sort=date&display_name=tag&style=for-the-badge&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iODc4IiBoZWlnaHQ9IjEwMTIiIHZpZXdCb3g9IjAgMCA4NzggMTAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik00MzguODA5IDUwNkwzNS40MDQ4IDczOC45MDZMNDM4LjgwOSA5NzEuODEyTDg0Mi4yMTQgNzM4LjkwNlYyNzMuMDk0TDQzOC44MDkgNDAuMTg4NUwzNS40MDQ4IDI3My4wOTRMNDM4LjgwOSA1MDZaTTQzOC44MDkgMEw4NzcuMDE4IDI1M1Y3NTlMNDM4LjgwOSAxMDEyTDAuNjAwNTg2IDc1OVY3MTguODEyVjI5My4xODhWMjUzTDQzOC44MDkgMFoiIGZpbGw9IndoaXRlIi8%2BCjwvc3ZnPgo%3D&labelColor=000&link=https%3A%2F%2Fgithub.com%2Fprayag17%Blink%2Freleases%2Flatest\">\r\n<img alt=\"GitHub Repo stars\" src=\"https://img.shields.io/github/stars/prayag17/Blink?style=for-the-badge&logo=github&labelColor=000&link=https%3A%2F%2Fgithub.com%2Fprayag17%Blink%2Fstargazers\">\r\n<img alt=\"GitHub License\" src=\"https://img.shields.io/github/license/prayag17/Blink?style=for-the-badge&labelColor=000\">  \r\n<img alt=\"GitHub Actions Workflow Status\" src=\"https://img.shields.io/github/actions/workflow/status/prayag17/Blink/continuous-integration.yml?style=for-the-badge&logo=github&label=CI&labelColor=000&link=https%3A%2F%2Fgithub.com%2Fprayag17%Blink%2Factions%2Fworkflows%2Fcontinuous-integration.yml\">\r\n</div>\r\n\r\n### \r\n\r\n> [!IMPORTANT]\r\n> **JellyPlayer** has been renamed to **Blink** to avoid confusion with first party Jellyfin apps\r\n\r\n## 📝 Prerequisites\r\n\r\n- Nodejs (22.14.0)\r\n- Rust (>=1.85.0)\r\n- Visual Studio C++ Build tools\r\n- [pnpm](https://pnpm.io/)\r\n\r\n## ℹ️ Getting started\r\n\r\n- Install Nodejs, pnpm and Rust.\r\n  > **Note** : Install Rust from <https://www.rust-lang.org/learn/get-started>\r\n- install depencies using pnpm:\r\n\r\n  ```shell\r\n  pnpm install\r\n  ```\r\n\r\n## 🛠️ Development\r\n\r\n- Running the app:\r\n\r\n  ```shell\r\n  pnpm run tauri dev\r\n  ```\r\n\r\n- Building the app:\r\n\r\n  ```shell\r\n  pnpm run tauri build\r\n  ```\r\n\r\n- other commands can be found inside the `\"scripts\"` inside [package.json](https://github.com/prayag17/Blink/blob/main/package.json)\r\n\r\n## 💻 Contribution\r\n\r\n- Checkout `issues` to see currently worked on features and bugs\r\n- Add features or fix bugs\r\n- Create a pull request\r\n\r\n## ✨ Features\r\n\r\n- Play any media supported by the system (DirectPlay most files on windows, mac and linux)\r\n- Clean and minimal UI.\r\n- Multi Jellyfin server support\r\n- Cross Platform\r\n- Mediainfo recognition (DolbyVision, DolbyAtoms, Dts, Hdr10+, and more...)\r\n- Sort/Filter library items\r\n- Queue playback support \r\n- Intro Skip button using [jumoog/Intro-Skipper](https://github.com/jumoog/intro-skipper) plugin\r\n- Mediasegment support\r\n\r\n## 📷 Screenshots\r\n\r\n- Home\r\n  <img width=\"2560\" height=\"1600\" alt=\"Screenshot 2026-02-10 212208\" src=\"https://github.com/user-attachments/assets/036c2339-e8f1-4c8d-a908-af0ece9bb1f3\" />\r\n- Library\r\n  <img width=\"2560\" height=\"1600\" alt=\"Screenshot 2026-02-10 212723\" src=\"https://github.com/user-attachments/assets/6520a325-3cf3-47a0-9056-b27aded4bb8c\" />\r\n- Music Player Detailed\r\n  <img width=\"2560\" height=\"1600\" alt=\"Screenshot 2026-02-10 213017\" src=\"https://github.com/user-attachments/assets/00a89efa-0a4c-4272-868e-f52c9fcfeaac\" />\r\n- Item Page\r\n  <img width=\"2560\" height=\"1600\" alt=\"Screenshot 2026-02-10 212653\" src=\"https://github.com/user-attachments/assets/5d281f8c-48ba-4787-b8ca-a4b86ccab5c1\" />\r\n- Album / Music Player\r\n  <img width=\"2560\" height=\"1600\" alt=\"Screenshot 2026-02-10 212949\" src=\"https://github.com/user-attachments/assets/d99aaea5-e242-4b3d-a5c5-d2868313728b\" />\r\n- Search Dialog\r\n  <img width=\"2560\" height=\"1600\" alt=\"Screenshot 2026-02-10 212749\" src=\"https://github.com/user-attachments/assets/4ad87072-86ad-4510-95a4-567d6d11e997\" />\r\n- Video Player\r\n  <img width=\"2560\" height=\"1600\" alt=\"Screenshot 2026-02-10 213049\" src=\"https://github.com/user-attachments/assets/effcca22-3650-46e1-9a86-b3b434851f2f\" />\r\n\r\n\r\n## 📃 Roadmap\r\n\r\n- Checkout [GitHub Project](https://github.com/users/prayag17/projects/3)\r\n  \r\n\r\n## 🎊 Special thanks to\r\n\r\n- [@ferferga](https://github.com/ferferga) for helping in development behind the scenes.\r\n- All contributors of [jellyfin/jellyfin-vue](https://github.com/jellyfin/jellyfin-vue).\r\n- And also [@jellyfin](https://github.com/jellyfin/) for creating the main service\r\n\r\n"
  },
  {
    "path": "biome.json",
    "content": "{\n\t\"$schema\": \"https://biomejs.dev/schemas/2.3.13/schema.json\",\n\t\"assist\": { \"actions\": { \"source\": { \"organizeImports\": \"on\" } } },\n\t\"linter\": {\n\t\t\"enabled\": true,\n\t\t\"rules\": {\n\t\t\t\"recommended\": true,\n\t\t\t\"style\": {\n\t\t\t\t\"noNonNullAssertion\": \"off\",\n\t\t\t\t\"useEnumInitializers\": \"off\",\n\t\t\t\t\"noParameterAssign\": \"error\",\n\t\t\t\t\"useAsConstAssertion\": \"error\",\n\t\t\t\t\"useDefaultParameterLast\": \"error\",\n\t\t\t\t\"useSelfClosingElements\": \"error\",\n\t\t\t\t\"useSingleVarDeclarator\": \"error\",\n\t\t\t\t\"noUnusedTemplateLiteral\": \"error\",\n\t\t\t\t\"useNumberNamespace\": \"error\",\n\t\t\t\t\"noInferrableTypes\": \"error\",\n\t\t\t\t\"noUselessElse\": \"error\"\n\t\t\t},\n\t\t\t\"a11y\": {\n\t\t\t\t\"useKeyWithClickEvents\": \"off\",\n\t\t\t\t\"useMediaCaption\": \"off\",\n\t\t\t\t\"noStaticElementInteractions\": \"off\"\n\t\t\t},\n\t\t\t\"correctness\": {\n\t\t\t\t\"useExhaustiveDependencies\": \"off\",\n\t\t\t\t\"noUnusedImports\": \"off\"\n\t\t\t},\n\t\t\t\"suspicious\": {\n\t\t\t\t\"noExplicitAny\": \"off\"\n\t\t\t},\n\t\t\t\"nursery\": {}\n\t\t}\n\t},\n\t\"files\": {\n\t\t\"includes\": [\n\t\t\t\"**/src/**/*.ts\",\n\t\t\t\"**/src/**/*.tsx\",\n\t\t\t\"**/src/**/*.jsx\",\n\t\t\t\"**/src/**/*.js\",\n\t\t\t\"**/*.json\",\n\t\t\t\"**/*.scss\"\n\t\t],\n\t\t\"ignoreUnknown\": true\n\t},\n\t\"formatter\": {\n\t\t\"formatWithErrors\": true\n\t}\n}\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n  <!-- <script src=\"http://localhost:8097\"></script> -->\n  <title>Blink</title>\n  <style>\n    .app-loading {\n      position: fixed;\n      top: 50%;\n      left: 50%;\n      transform: translate(-50%,-50%);\n      width: 10em;\n      height: 10em;\n      background: rgb(180 180 180);\n      z-index: 100000;\n      mask-image: url(\"./src/assets/icon-outline.png\");\n      mask-size: contain;\n    }\n\n    .app-loading.hidden {\n      display: none;\n    }\n\n    \n    .app-loading-anim{\n      width: 150%;\n      height: 100%;\n      position: absolute;\n      top: 0;\n      left: 0;\n      background: linear-gradient(to right,transparent,rgb(255 255 255),transparent);\n      transform: scaleX(0);\n      animation: loading 0.75s linear infinite both;\n        transform-origin: left;\n    }\n\n    @keyframes loading {\n      0%{\n        transform: translate(-100%);\n      }\n      50%{\n        transform: translate(0%);\n      }\n      100%{\n        transform: translate(100%);\n      }\n    }\n  </style>\n</head>\n\n<body>\n  <div class=\"app-loading\">\n    <!-- <img src=\"./src/assets/icon-outline.svg\" class=\"app-loading-logo\" /> -->\n    <div class=\"app-loading-anim\"></div>\n  </div>\n  <div id=\"root\"></div>\n  <svg style=\"width: 0; height: 0; position: absolute\" aria-hidden=\"true\" focusable=\"false\">\n    <linearGradient id=\"clr-gradient-default\" x2=\"1\" y2=\"1\">\n      <stop offset=\"0%\" stop-color=\"hsl(337, 96%, 56%)\" />\n      <stop offset=\"100%\" stop-color=\"hsl(273, 100%, 36%)\" />\n    </linearGradient>\n  </svg>\n  <script type=\"module\" src=\"/src/main.tsx\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "latest.json",
    "content": "{\n\t\"version\": \"0.0.7-dev\",\n\t\"notes\": \"![Banner](https://i.imgur.com/rnhjwsl.png)\\n- chore(deps): update dependencies\\n- refactor: change UI of login/list and fix Api not being defined in context\\n- fix: player/audio route not able to interact with audioPlayer\\n- fix: audio player info not visible\\n- fix: audio player not playing audio\\n- fix: toggling ssubtitles not working\\n- fix: subtitles not being rendered #401\\n- fix: hero carousel indicators not scrolling\\n- fix: vite build error for safari13\\n- fix: blink not launching or rendering on opening\\n- Merge branch 'main' of https://github.com/prayag17/JellyPlayer\\n- chore(deps): update dependencies\\n- Merge pull request #410 from prayag17/renovate/tauri-plugin-notification-2.x-lockfile\\n- Merge pull request #409 from Blackspirits/patch-1\\n- chore(deps): update dependencies\\n- fix(deps): update rust crate tauri-plugin-notification to v2.0.1\\n- feat: allow downloading images from ImageViewer\\n- Update index.tsx\\n- feat: implement basic image viewer #338 #240\\n- fix: react infinite depth error\\n- fix: vite config\\n- refactor: remove @million/js and @million/lint\\n- refactor: remove Cargo.lock from .gitignore #400\\n- chore(deps): update dependencies\\n- refactor: code cleanup\\n- fix: bug_report showing extra fields\\n- refactor: remove redundant calls for getCurrentUser\\n- feat: show queue item from currently playing item in queueButton\\n- chore(deps): update dependencies\\n- feat: remember library filters #192\\n- Merge pull request #370 from prayag17/renovate/tauri-apps-plugin-clipboard-manager-2.x\\n- fix(deps): update dependency @tauri-apps/plugin-clipboard-manager to v2.1.0-beta.6\\n- Merge pull request #381 from prayag17/renovate/wavesurfer.js-7.x-lockfile\\n- Merge pull request #342 from prayag17/renovate/tanstack-router-monorepo\\n- Merge pull request #368 from prayag17/renovate/tauri-monorepo\\n- chore(release): update latest.json\",\n\t\"pub_date\": \"2024-11-20T07:04:53.965Z\",\n\t\"platforms\": {\n\t\t\"darwin-aarch64\": {\n\t\t\t\"signature\": \"dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSNkxEL1dFMElZL2RHaC9wc3VMQzJxeFRYaHVVN0xEZW91WWgwZkluYkYrdW5hUWl2UFlSeEUyRk93dm13WUdmQVZETWhmNW9MZ2lPUFdOdERzbGFibkpPNHJWMU1BYlFrPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzMyMDg1ODYyCWZpbGU6QmxpbmsuYXBwLnRhci5negpjK0VIaW9oVzlrUU92UWZudzcvRGlZNExwM0NOdmpHVGd1QUc2WWZ0bnRkU2NsWkdqQUJrR01ucVVRTktvR0RaV0l0L09KQ2luZHNUdjMvTGVlM2RCUT09Cg==\",\n\t\t\t\"url\": \"https://github.com/prayag17/Blink/releases/download/v0.0.7-dev/Blink_aarch64.app.tar.gz\"\n\t\t},\n\t\t\"linux-x86_64\": {\n\t\t\t\"signature\": \"dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSNkxEL1dFMElZL1hKZGoxZFUvZzl5SmNnUEFRTGQ2TmFNMlphK25lTU9MTjJEM0JYQWt0Sy91Um9yRmk4QTl6dEtUSmRuRmFNODRwajJ2YW4rTGJzYVZjcmhFb1IxZkFnPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzMyMDg2MTQxCWZpbGU6QmxpbmtfMC4wLjctZGV2X2FtZDY0LkFwcEltYWdlCndKZURUdjF0bkNna1VCVDNEMFNodEZBZGkzZFBKVWx2cThSNE5najBmd2U5Rzh5SDFIbUxqRHlKOWdxMnY2QU9NdTRiQmtWY2VVaVlhNjRrNDVHMUFRPT0K\",\n\t\t\t\"url\": \"https://github.com/prayag17/Blink/releases/download/v0.0.7-dev/Blink_0.0.7-dev_amd64.AppImage\"\n\t\t},\n\t\t\"windows-x86_64\": {\n\t\t\t\"signature\": \"dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSNkxEL1dFMElZL1MwRVJ0UXBUUEYvd01JbXhBalFwQ0w5bEdkZkdlblZNeW1EMXQxajhYeVd0cERWcnhWOWFOaXV4Q0hDU1VRTEhabXF1ekFIbzB0KzRmL0pxdEIzMEFrPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzMyMDg2Mjg5CWZpbGU6QmxpbmtfMC4wLjctZGV2X3g2NC1zZXR1cC5uc2lzLnppcApJMmRORUFhV3NKV3BCTTliQkIwcWtSZStKTzEwY002QndCWmxKb0ZRd2p2K0cvek90dmdsbmQ0MHhNOVJsWnE3Y29qQmNDckdxdUNabWpTWjJ3d0xBUT09Cg==\",\n\t\t\t\"url\": \"https://github.com/prayag17/Blink/releases/download/v0.0.7-dev/Blink_0.0.7-dev_x64-setup.nsis.zip\"\n\t\t}\n\t}\n}"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"blink\",\n\t\"private\": true,\n\t\"version\": \"1.0.0-alpha.4\",\n\t\"type\": \"module\",\n\t\"scripts\": {\n\t\t\"dev\": \"vite --force && tsr watch\",\n\t\t\"build\": \"vite build\",\n\t\t\"preview\": \"vite preview\",\n\t\t\"tauri\": \"tauri\",\n\t\t\"lint\": \"biome check src/**/*\",\n\t\t\"lint:fix\": \"biome check --apply src/**/*\",\n\t\t\"tanstack-router-dev\": \"tsr watch\"\n\t},\n\t\"dependencies\": {\n\t\t\"@dnd-kit/core\": \"^6.3.1\",\n\t\t\"@dnd-kit/modifiers\": \"^9.0.0\",\n\t\t\"@dnd-kit/sortable\": \"^10.0.0\",\n\t\t\"@dnd-kit/utilities\": \"^3.2.2\",\n\t\t\"@emotion/react\": \"^11.14.0\",\n\t\t\"@emotion/styled\": \"^11.14.1\",\n\t\t\"@fontsource-variable/jetbrains-mono\": \"^5.2.8\",\n\t\t\"@fontsource-variable/noto-sans\": \"^5.2.10\",\n\t\t\"@fontsource-variable/plus-jakarta-sans\": \"^5.2.8\",\n\t\t\"@fontsource-variable/space-grotesk\": \"^5.2.10\",\n\t\t\"@jellyfin/sdk\": \"0.13.0\",\n\t\t\"@mui/lab\": \"7.0.1-beta.21\",\n\t\t\"@mui/material\": \"7.3.7\",\n\t\t\"@popperjs/core\": \"^2.11.8\",\n\t\t\"@react-spring/web\": \"^10.0.3\",\n\t\t\"@tanem/react-nprogress\": \"^5.0.59\",\n\t\t\"@tanstack/react-form\": \"^1.28.0\",\n\t\t\"@tanstack/react-query\": \"^5.90.20\",\n\t\t\"@tanstack/react-query-devtools\": \"^5.91.3\",\n\t\t\"@tanstack/react-router\": \"^1.158.4\",\n\t\t\"@tanstack/react-router-devtools\": \"^1.158.4\",\n\t\t\"@tanstack/react-virtual\": \"^3.13.18\",\n\t\t\"@tauri-apps/api\": \"2.10.1\",\n\t\t\"@tauri-apps/plugin-clipboard-manager\": \"2.3.2\",\n\t\t\"@tauri-apps/plugin-dialog\": \"2.6.0\",\n\t\t\"@tauri-apps/plugin-global-shortcut\": \"~2.3.1\",\n\t\t\"@tauri-apps/plugin-notification\": \"2.3.3\",\n\t\t\"@tauri-apps/plugin-process\": \"2.3.1\",\n\t\t\"@tauri-apps/plugin-shell\": \"~2.3.5\",\n\t\t\"@tauri-apps/plugin-store\": \"github:tauri-apps/tauri-plugin-store#v2\",\n\t\t\"@tauri-apps/plugin-updater\": \"2.10.0\",\n\t\t\"@tauri-apps/plugin-upload\": \"2.4.0\",\n\t\t\"@tauri-apps/plugin-window-state\": \"~2.4.1\",\n\t\t\"@types/node\": \"^25.2.1\",\n\t\t\"@types/react\": \"^19.2.13\",\n\t\t\"@types/react-dom\": \"^19.2.3\",\n\t\t\"axios\": \"^1.13.4\",\n\t\t\"axios-tauri-api-adapter\": \"^2.0.6\",\n\t\t\"blurhash\": \"^2.0.5\",\n\t\t\"blurhash-wasm\": \"^0.2.0\",\n\t\t\"immer\": \"^11.1.3\",\n\t\t\"jassub\": \"^2.4.1\",\n\t\t\"libpgs\": \"^0.8.1\",\n\t\t\"lodash\": \"^4.17.23\",\n\t\t\"material-symbols\": \"^0.40.2\",\n\t\t\"motion\": \"^12.33.0\",\n\t\t\"notistack\": \"^3.0.2\",\n\t\t\"react\": \"^19.2.4\",\n\t\t\"react-blurhash\": \"^0.3.0\",\n\t\t\"react-dnd\": \"^16.0.1\",\n\t\t\"react-dnd-html5-backend\": \"^16.0.1\",\n\t\t\"react-dom\": \"^19.2.4\",\n\t\t\"react-error-boundary\": \"^6.1.0\",\n\t\t\"react-intersection-observer\": \"^10.0.2\",\n\t\t\"react-markdown\": \"^10.1.0\",\n\t\t\"react-multi-carousel\": \"^2.8.6\",\n\t\t\"react-player\": \"^3.4.0\",\n\t\t\"remark-gfm\": \"^4.0.1\",\n\t\t\"typescript\": \"^5.9.3\",\n\t\t\"vite-plugin-svgr\": \"^4.5.0\",\n\t\t\"vite-plugin-top-level-await\": \"^1.6.0\",\n\t\t\"wavesurfer.js\": \"^7.12.1\",\n\t\t\"zod\": \"^4.3.6\",\n\t\t\"zustand\": \"5.0.11\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@babel/plugin-proposal-optional-chaining-assign\": \"^7.27.1\",\n\t\t\"@biomejs/biome\": \"^2.3.14\",\n\t\t\"@tanstack/router-cli\": \"^1.158.4\",\n\t\t\"@tanstack/router-devtools\": \"^1.158.4\",\n\t\t\"@tanstack/router-vite-plugin\": \"^1.158.4\",\n\t\t\"@tauri-apps/cli\": \"2.10.0\",\n\t\t\"@types/lodash\": \"^4.17.23\",\n\t\t\"@vitejs/plugin-react\": \"^5.1.3\",\n\t\t\"babel-plugin-react-compiler\": \"19.1.0-rc.3\",\n\t\t\"eslint-plugin-react-hooks\": \"7.0.1\",\n\t\t\"prop-types\": \"^15.8.1\",\n\t\t\"sass\": \"^1.97.3\",\n\t\t\"vite\": \"^7.3.1\",\n\t\t\"vite-plugin-wasm\": \"^3.5.0\"\n\t},\n\t\"license\": \"GPL-3.0-only\"\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "onlyBuiltDependencies:\n  - '@parcel/watcher'\n  - core-js\n  - esbuild\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n\t\"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n\t\"extends\": [\"config:base\", \":disableDependencyDashboard\"],\n\t\"labels\": [\"dependencies\"]\n}\n"
  },
  {
    "path": "src/components/Slider/index.tsx",
    "content": "import React, {\n\ttype ReactNode,\n\tuseCallback,\n\tuseEffect,\n\tuseRef,\n\tuseState,\n} from \"react\";\n\nimport { useSpring } from \"@react-spring/web\";\nimport { debounce } from \"lodash\";\n\nimport \"./style.scss\";\nimport { IconButton } from \"@mui/material\";\n\nconst Slider = ({\n\tcontent,\n\tcurrentSlide,\n}: { content: ReactNode[]; currentSlide: number }) => {\n\tconst containerRef = useRef<HTMLDivElement>(null);\n\tconst [scrollPos, setScrollPos] = useState({ isStart: true, isEnd: false });\n\n\tconst handleScroll = useCallback(() => {\n\t\tconst scrollWidth = containerRef.current?.scrollWidth ?? 0;\n\t\tconst clientWidth =\n\t\t\tcontainerRef.current?.getBoundingClientRect().width ?? 0;\n\t\tconst scrolledPostion = containerRef.current?.scrollLeft ?? 0;\n\t\tif (content.length === 0) {\n\t\t\tsetScrollPos({ isStart: true, isEnd: true });\n\t\t} else if (clientWidth >= scrollWidth) {\n\t\t\tsetScrollPos({ isStart: true, isEnd: true });\n\t\t} else if (\n\t\t\tscrolledPostion >=\n\t\t\t(containerRef.current?.scrollWidth ?? 0) - clientWidth\n\t\t) {\n\t\t\tsetScrollPos({ isStart: false, isEnd: true });\n\t\t} else if (scrolledPostion > 0) {\n\t\t\tsetScrollPos({ isStart: false, isEnd: false });\n\t\t} else {\n\t\t\tsetScrollPos({ isStart: true, isEnd: false });\n\t\t}\n\t}, [content]);\n\n\tconst debouncedScroll = useCallback(\n\t\t() => debounce(handleScroll, 50),\n\t\t[handleScroll],\n\t);\n\tuseEffect(() => {\n\t\tconst handleResize = () => {\n\t\t\tdebouncedScroll();\n\t\t};\n\t\twindow.addEventListener(\"resize\", handleResize, { passive: true });\n\t\treturn () => window.removeEventListener(\"resize\", handleResize);\n\t}, [debouncedScroll]);\n\n\tuseEffect(() => {\n\t\thandleScroll();\n\t}, [handleScroll, content]);\n\n\tconst onScroll = () => {\n\t\tdebouncedScroll();\n\t};\n\n\tconst [, setX] = useSpring(() => ({\n\t\tfrom: { x: 0 },\n\t\tto: { x: 0 },\n\t\tonChange: (results) => {\n\t\t\tif (containerRef.current) {\n\t\t\t\tcontainerRef.current.scrollLeft = results.value.x;\n\t\t\t}\n\t\t},\n\t}));\n\n\tconst slideLeft = useCallback(() => {\n\t\tconst clientWidth =\n\t\t\tcontainerRef.current?.getBoundingClientRect().width ?? 0;\n\t\tconst cardWidth =\n\t\t\tcontainerRef.current?.firstElementChild?.getBoundingClientRect().width ??\n\t\t\t0;\n\t\tconst scrollPosition = containerRef.current?.scrollLeft ?? 0;\n\t\tconst visibleItems = Math.floor(clientWidth / cardWidth);\n\t\tconst scrollOffset = scrollPosition % cardWidth;\n\n\t\tconst newX = Math.max(\n\t\t\tscrollPosition - scrollOffset - visibleItems * cardWidth,\n\t\t\t0,\n\t\t);\n\t\tsetX({\n\t\t\tfrom: { x: scrollPosition },\n\t\t\tto: { x: newX },\n\t\t\tonChange: (results) => {\n\t\t\t\tif (containerRef.current) {\n\t\t\t\t\tcontainerRef.current.scrollLeft = results.value.x;\n\t\t\t\t}\n\t\t\t},\n\t\t\treset: true,\n\t\t\tconfig: { friction: 60, tension: 1000, velocity: 2 },\n\t\t});\n\n\t\tif (newX === 0) {\n\t\t\tsetScrollPos({ isStart: true, isEnd: false });\n\t\t} else {\n\t\t\tsetScrollPos({ isStart: false, isEnd: false });\n\t\t}\n\t}, [setX]);\n\n\tconst slideRight = useCallback(() => {\n\t\tconst clientWidth =\n\t\t\tcontainerRef.current?.getBoundingClientRect().width ?? 0;\n\t\tconst cardWidth =\n\t\t\tcontainerRef.current?.firstElementChild?.getBoundingClientRect().width ??\n\t\t\t0;\n\t\tconst scrollPosition = containerRef.current?.scrollLeft ?? 0;\n\t\tconst visibleItems = Math.floor(clientWidth / cardWidth);\n\t\tconst scrollOffset = scrollPosition % cardWidth;\n\n\t\tconst newX = Math.min(\n\t\t\tscrollPosition - scrollOffset + visibleItems * cardWidth,\n\t\t\tcontainerRef.current?.scrollWidth ?? 0 - clientWidth,\n\t\t);\n\t\tsetX({\n\t\t\tfrom: { x: scrollPosition },\n\t\t\tto: { x: newX },\n\t\t\tonChange: (results) => {\n\t\t\t\tif (containerRef.current) {\n\t\t\t\t\tcontainerRef.current.scrollLeft = results.value.x;\n\t\t\t\t}\n\t\t\t},\n\t\t\treset: true,\n\t\t\tconfig: { friction: 60, tension: 500, velocity: 2 },\n\t\t});\n\n\t\tif (newX >= (containerRef.current?.scrollWidth ?? 0) - clientWidth) {\n\t\t\tsetScrollPos({ isStart: false, isEnd: true });\n\t\t} else {\n\t\t\tsetScrollPos({ isStart: false, isEnd: false });\n\t\t}\n\t}, [setX]);\n\n\treturn (\n\t\t<div className=\"slider\">\n\t\t\t<IconButton\n\t\t\t\tclassName=\"slider-button left\"\n\t\t\t\tonClick={slideLeft}\n\t\t\t\tdisabled={scrollPos.isStart}\n\t\t\t>\n\t\t\t\t<span className=\"material-symbols-rounded\">chevron_left</span>\n\t\t\t</IconButton>\n\t\t\t<div className=\"slider-track\" ref={containerRef} onScroll={onScroll}>\n\t\t\t\t{content?.map((item, index) => (\n\t\t\t\t\t<div\n\t\t\t\t\t\t//biome-ignore lint/suspicious/noArrayIndexKey: item does not have a unique key\n\t\t\t\t\t\tkey={index}\n\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\tcurrentSlide === index\n\t\t\t\t\t\t\t\t? \"slider-track-item active\"\n\t\t\t\t\t\t\t\t: \"slider-track-item\"\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\t{item}\n\t\t\t\t\t</div>\n\t\t\t\t))}\n\t\t\t</div>\n\t\t\t<IconButton\n\t\t\t\tclassName=\"slider-button right\"\n\t\t\t\tonClick={slideRight}\n\t\t\t\tdisabled={scrollPos.isEnd}\n\t\t\t>\n\t\t\t\t<span className=\"material-symbols-rounded\">chevron_right</span>\n\t\t\t</IconButton>\n\t\t</div>\n\t);\n};\nexport default Slider;"
  },
  {
    "path": "src/components/Slider/style.scss",
    "content": ".slider {\n    position: relative;\n\n    &-track {\n        overflow-y: hidden;\n        overflow-x: auto;\n        white-space: nowrap;\n        position: relative;\n        overscroll-behavior-x: contain;\n        margin: 0.5em 1em;\n        &::-webkit-scrollbar {\n                display: none;\n            }\n\n        &-item {\n            display: inline-block;\n            // align-self: top;\n            padding: 0.5em 0;\n        }\n    }\n\n    &-button {\n        position: absolute !important;\n        bottom: 1.3em;\n\n        &.left {\n            left: -1.2em;\n        }\n\n        &.right {\n            right: -1.2em;\n        }\n    }\n}"
  },
  {
    "path": "src/components/addServerDialog/index.tsx",
    "content": "import { LoadingButton } from \"@mui/lab\";\nimport {\n\tBox,\n\tButton,\n\tDialog,\n\tInputBase,\n\tStack,\n\tTypography,\n} from \"@mui/material\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { enqueueSnackbar } from \"notistack\";\nimport React, { useState } from \"react\";\nimport { setServer } from \"@/utils/storage/servers\";\nimport { jellyfin } from \"@/utils/store/api\";\n\ntype AddServerDialogProps = {\n\topen: boolean;\n\tsetAddServerDialog: (value: boolean) => void;\n\tsideEffect?: () => Promise<unknown>;\n\thideBackdrop?: boolean;\n};\n\nexport default function AddServerDialog(props: AddServerDialogProps) {\n\tconst { open, setAddServerDialog, sideEffect, hideBackdrop } = props;\n\n\tconst [serverIp, setServerIp] = useState(\"\");\n\tconst addServer = useMutation({\n\t\tmutationKey: [\"add-server\"],\n\t\tmutationFn: async () => {\n\t\t\tconst servers =\n\t\t\t\tawait jellyfin.discovery.getRecommendedServerCandidates(serverIp);\n\t\t\tconst bestServer = jellyfin.discovery.findBestServer(servers);\n\t\t\treturn bestServer;\n\t\t},\n\t\tonSuccess: async (bestServer) => {\n\t\t\tif (bestServer) {\n\t\t\t\tawait setServer(bestServer.systemInfo?.Id ?? \"\", bestServer);\n\t\t\t\tsetAddServerDialog(false);\n\t\t\t\tenqueueSnackbar(\n\t\t\t\t\t\"Client added successfully. You might need to refresh client list.\",\n\t\t\t\t\t{\n\t\t\t\t\t\tvariant: \"success\",\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tif (sideEffect) {\n\t\t\t\t\tawait sideEffect();\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\tonError: (err) => {\n\t\t\tconsole.error(err);\n\t\t\tenqueueSnackbar(`${err}`, { variant: \"error\" });\n\t\t\tenqueueSnackbar(\"Something went wrong\", { variant: \"error\" });\n\t\t},\n\t\tonSettled: async (bestServer) => {\n\t\t\tif (!bestServer) {\n\t\t\t\tenqueueSnackbar(\"Provided server address is not a Jellyfin server.\", {\n\t\t\t\t\tvariant: \"error\",\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t});\n\n\treturn (\n\t\t<Dialog\n\t\t\topen={open}\n\t\t\tonClose={() => setAddServerDialog(false)}\n\t\t\thideBackdrop={Boolean(hideBackdrop)}\n\t\t\tfullWidth\n\t\t\tmaxWidth=\"sm\"\n\t\t\tdisableScrollLock={true}\n\t\t\tPaperProps={{\n\t\t\t\tsx: {\n\t\t\t\t\tbackgroundColor: \"rgba(20, 20, 30, 0.7)\",\n\t\t\t\t\tbackdropFilter: \"blur(24px) saturate(180%)\",\n\t\t\t\t\tbackgroundImage:\n\t\t\t\t\t\t\"linear-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0))\",\n\t\t\t\t\tborder: \"1px solid rgba(255, 255, 255, 0.08)\",\n\t\t\t\t\tboxShadow: \"0 20px 50px rgba(0,0,0,0.5)\",\n\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t},\n\t\t\t}}\n\t\t>\n\t\t\t<Stack p={4} spacing={3}>\n\t\t\t\t<Stack spacing={1} alignItems=\"center\">\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tfontSize: \"48px\",\n\t\t\t\t\t\t\tcolor: \"var(--mui-palette-primary-main)\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\tadd_to_queue\n\t\t\t\t\t</span>\n\t\t\t\t\t<Typography variant=\"h5\" fontWeight=\"bold\" textAlign=\"center\">\n\t\t\t\t\t\tConnect to Server\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<Typography variant=\"body2\" color=\"text.secondary\" textAlign=\"center\">\n\t\t\t\t\t\tEnter your Jellyfin server address to continue\n\t\t\t\t\t</Typography>\n\t\t\t\t</Stack>\n\n\t\t\t\t<Box\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tp: \"2px 16px\",\n\t\t\t\t\t\tbgcolor: \"rgba(0,0,0,0.2)\",\n\t\t\t\t\t\tborderRadius: 2,\n\t\t\t\t\t\tborder: \"1px solid\",\n\t\t\t\t\t\tborderColor: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\tgap: 2,\n\t\t\t\t\t\t\"&:focus-within\": {\n\t\t\t\t\t\t\tborderColor: \"var(--mui-palette-primary-main)\",\n\t\t\t\t\t\t\tbgcolor: \"rgba(0,0,0,0.3)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\ttransition: \"border-color 0.2s, background-color 0.2s\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<span className=\"material-symbols-rounded\" style={{ opacity: 0.5 }}>\n\t\t\t\t\t\tdns\n\t\t\t\t\t</span>\n\t\t\t\t\t<InputBase\n\t\t\t\t\t\tplaceholder=\"https://jellyfin.example.com\"\n\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\tvalue={serverIp}\n\t\t\t\t\t\tonChange={(e) => setServerIp(e.target.value)}\n\t\t\t\t\t\tsx={{ py: 1.5 }}\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\t\t\tif (e.key === \"Enter\") {\n\t\t\t\t\t\t\t\taddServer.mutate();\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t</Box>\n\n\t\t\t\t<Stack direction=\"row\" spacing={2} justifyContent=\"flex-end\" pt={1}>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tonClick={() => setAddServerDialog(false)}\n\t\t\t\t\t\tcolor=\"inherit\"\n\t\t\t\t\t\tvariant=\"text\"\n\t\t\t\t\t\tsx={{ borderRadius: 2 }}\n\t\t\t\t\t>\n\t\t\t\t\t\tCancel\n\t\t\t\t\t</Button>\n\t\t\t\t\t<LoadingButton\n\t\t\t\t\t\tloading={addServer.isPending}\n\t\t\t\t\t\tonClick={() => addServer.mutate()}\n\t\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\t\tcolor=\"primary\"\n\t\t\t\t\t\tsx={{ borderRadius: 2, px: 3 }}\n\t\t\t\t\t\tdisabled={!serverIp}\n\t\t\t\t\t>\n\t\t\t\t\t\tConnect\n\t\t\t\t\t</LoadingButton>\n\t\t\t\t</Stack>\n\t\t\t</Stack>\n\t\t</Dialog>\n\t);\n}"
  },
  {
    "path": "src/components/albumMusicTrack/albumMusicTrack.scss",
    "content": ".item-info-track{\n    display: grid;\n        grid-template-columns: 3em 60% 6em 1fr;\n        gap: 1em;\n        padding: 1em;\n        align-items: center;\n        cursor: pointer;\n        border-radius: $border-radius-default;\n        transition: background $transition-time-fast;\n        &.header {\n            border-bottom: 1px solid rgb(255 255 255 / 0.2);\n            color: rgb(255 255 255 / 0.7);\n            border-radius: 0 !important;\n        }\n        .index-container {\n            justify-self: center;\n            position: relative;\n            .material-symbols-rounded {\n                position: absolute;\n                top: 50%;\n                left: 50%;\n                transform: translate(-50%, -50%);\n                font-size: 2em !important;\n                opacity: 0;\n                transition: none !important;\n                color: $clr-accent-default;\n            }\n        }\n        &-info {\n            display: flex;\n            flex-direction: column;\n            gap: 0.1em\n        }\n        &:hover {\n            background: rgb(255 255 255 / 0.1) !important;\n            .index-container {\n                // font-size: 0em;\n                .index {\n                    opacity: 0;\n                    transition: opacity 0;\n                }\n                .material-symbols-rounded {\n                    opacity: 1;\n                    font-size: 2.2em !important;\n                }\n            }\n        }\n        &.playing {\n            background: rgb(0 0 0 / 0.4) !important;\n            .item-info-track-info-name, .index {\n                color: $clr-accent-default;\n                font-weight: 600;\n            }\n        }\n        &-container {\n            display: flex;\n            flex-direction: column;\n            gap: 0.4em\n        }\n}"
  },
  {
    "path": "src/components/albumMusicTrack/index.tsx",
    "content": "import {\n\tgenerateAudioStreamUrl,\n\tplayAudio,\n\tuseAudioPlayback,\n} from \"@/utils/store/audioPlayback\";\nimport type {\n\tBaseItemDto,\n\tBaseItemDtoQueryResult,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { Typography } from \"@mui/material\";\nimport React from \"react\";\n\nimport { getRuntimeMusic } from \"@/utils/date/time\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport { setQueue } from \"@/utils/store/queue\";\nimport { useSnackbar } from \"notistack\";\nimport lyricsIcon from \"../../assets/icons/lyrics.svg\";\nimport LikeButton from \"../buttons/likeButton\";\n\nimport \"./albumMusicTrack.scss\";\n\ntype AlbumMusicTrackProps = {\n\ttrack: BaseItemDto;\n\t/**\n\t * This is the index of the track in the musicTracks array (not to be confused with IndexNumber property of the track)\n\t */\n\ttrackIndex: number;\n\tmusicTracks: BaseItemDtoQueryResult | null;\n};\n\nexport default function AlbumMusicTrack(props: AlbumMusicTrackProps) {\n\tconst { track, musicTracks, trackIndex } = props;\n\tconst [currentPlayingItem] = useAudioPlayback((state) => [state.item]);\n\n\tconst user = useCentralStore((state) => state.currentUser);\n\tconst api = useApiInContext((s) => s.api);\n\n\tconst { enqueueSnackbar } = useSnackbar();\n\n\tconst handlePlayback = (\n\t\tindex: number,\n\t\titem: BaseItemDto,\n\t\tqueue: BaseItemDto[],\n\t) => {\n\t\tif (!user?.Id || !api) {\n\t\t\tconsole.error(\"User not logged in\");\n\t\t\tenqueueSnackbar(\"You need to be logged in to play music\", {\n\t\t\t\tvariant: \"error\",\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tif (item.Id) {\n\t\t\tconst url = generateAudioStreamUrl(\n\t\t\t\titem.Id,\n\t\t\t\tuser.Id,\n\t\t\t\tapi.deviceInfo.id,\n\t\t\t\tapi.basePath,\n\t\t\t\tapi.accessToken,\n\t\t\t);\n\t\t\tplayAudio(url, item);\n\t\t\tsetQueue(queue, index);\n\t\t}\n\t};\n\treturn (\n\t\t<div\n\t\t\tclassName={\n\t\t\t\tcurrentPlayingItem?.Id === track.Id\n\t\t\t\t\t? \"item-info-track playing\"\n\t\t\t\t\t: \"item-info-track\"\n\t\t\t}\n\t\t\tkey={track.Id}\n\t\t\tonClick={() => {\n\t\t\t\tif (musicTracks?.Items) {\n\t\t\t\t\thandlePlayback(trackIndex, track, musicTracks.Items);\n\t\t\t\t}\n\t\t\t}}\n\t\t>\n\t\t\t<div className=\"index-container\">\n\t\t\t\t<span className=\"material-symbols-rounded fill \">play_arrow</span>\n\t\t\t\t<Typography className=\"index\">{track.IndexNumber ?? \"-\"}</Typography>\n\t\t\t</div>\n\t\t\t<div className=\"item-info-track-info\">\n\t\t\t\t<Typography className=\"item-info-track-info-name\">\n\t\t\t\t\t{track.Name}\n\t\t\t\t</Typography>\n\t\t\t\t<Typography\n\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\topacity: 0.6,\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\tgap: \"0.6em\",\n\t\t\t\t\t}}\n\t\t\t\t\tfontWeight={300}\n\t\t\t\t>\n\t\t\t\t\t{track.HasLyrics && <img src={lyricsIcon} alt=\"lyrics\" />}\n\t\t\t\t\t{track.Artists?.join(\", \")}\n\t\t\t\t</Typography>\n\t\t\t</div>\n\t\t\t<Typography>{getRuntimeMusic(track.RunTimeTicks ?? 0)}</Typography>\n\t\t\t<div className=\"flex flex-align-center\">\n\t\t\t\t<LikeButton\n\t\t\t\t\titemId={track.Id}\n\t\t\t\t\tisFavorite={track.UserData?.IsFavorite}\n\t\t\t\t\tqueryKey={[\"item\", \"musicTracks\"]}\n\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\titemName={track.Name}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t</div>\n\t);\n}"
  },
  {
    "path": "src/components/alphaSelector/alphaSelector.scss",
    "content": ".alpha-selector {\n  position: sticky;\n  top: 80px;\n  right: 0;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 4px;\n  padding: 4px;\n  margin-left: auto;\n  user-select: none;\n  z-index: 2;\n}\n\n.alpha-letter {\n  font-size: 12px;\n  line-height: 1;\n  padding: 6px 8px;\n  border-radius: 8px;\n  background: transparent;\n  color: rgba(255,255,255,0.7);\n  border: none;\n  cursor: pointer;\n}\n\n.alpha-letter.active {\n  background: rgba(255,255,255,0.12);\n  color: #fff;\n}\n\n.alpha-letter:hover {\n  background: rgba(255,255,255,0.08);\n}\n"
  },
  {
    "path": "src/components/alphaSelector/index.tsx",
    "content": "import { getRouteApi } from \"@tanstack/react-router\";\nimport React, { useMemo } from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { useLibraryStateStore } from \"@/utils/store/libraryState\";\nimport \"./alphaSelector.scss\";\n\nconst libraryRoute = getRouteApi(\"/_api/library/$id\");\n\nexport const AlphaSelector = React.memo(function AlphaSelector() {\n\tconst { id: currentLibraryId } = libraryRoute.useParams();\n\tconst { nameStartsWith } = useLibraryStateStore(\n\t\tuseShallow((s) => ({\n\t\t\tnameStartsWith: s.libraries[currentLibraryId || \"\"]?.nameStartsWith,\n\t\t})),\n\t);\n\tconst updateLibrary = useLibraryStateStore((s) => s.updateLibrary);\n\n\tconst letters = useMemo(() => {\n\t\tconst arr = Array.from({ length: 26 }, (_, i) =>\n\t\t\tString.fromCharCode(65 + i),\n\t\t);\n\t\treturn [\"All\", ...arr];\n\t}, []);\n\n\tconst handleSelect = (letter: string) => {\n\t\tconst value = letter === \"All\" ? undefined : letter;\n\t\tif (currentLibraryId)\n\t\t\tupdateLibrary(currentLibraryId, { nameStartsWith: value });\n\t};\n\n\treturn (\n\t\t<div className=\"alpha-selector\">\n\t\t\t{letters.map((l) => {\n\t\t\t\tconst active = (nameStartsWith || \"All\") === l;\n\t\t\t\treturn (\n\t\t\t\t\t<button\n\t\t\t\t\t\tkey={l}\n\t\t\t\t\t\tclassName={active ? \"alpha-letter active\" : \"alpha-letter\"}\n\t\t\t\t\t\tonClick={() => handleSelect(l)}\n\t\t\t\t\t\taria-label={`Filter by ${l}`}\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{l}\n\t\t\t\t\t</button>\n\t\t\t\t);\n\t\t\t})}\n\t\t</div>\n\t);\n});\n\nexport default AlphaSelector;\n"
  },
  {
    "path": "src/components/appBar/appBar.scss",
    "content": "\n.appBar {\n\t// Limit transitions to cheaper properties; avoid animating box-shadow directly\n\ttransition: background-color $transition-time-fast;\n\tpadding: 0.8em ($page-margin - 1.7em);\n\tz-index: 999 !important;\n\t.flex {\n\t\t// Avoid animating padding; it causes layout reflow on each frame.\n\t\ttransition: background-color $transition-time-fast;\n\t\t// Avoid using will-change: scroll-position; it can degrade performance when misused\n\t\t// will-change: opacity, transform;\n\t\tborder-radius: 1000px;\n\t\tposition: relative;\n\n\t\t// Pseudo-element handles elevation shadow; we animate its opacity\n\t\t&::after {\n\t\t\tcontent: \"\";\n\t\t\tposition: absolute;\n\t\t\tinset: -0.35em;\n\t\t\tpointer-events: none;\n\t\t\tborder-radius: inherit;\n\t\t\t\n\t\t\t@include glass-effect;\n\n\t\t\topacity: 0;\n\t\t\tz-index: -1;\n\t\t\ttransition: all $transition-time-fast;\n\t\t}\n\t}\n\n\t// Inner content wrapper that we can scale without causing layout reflow\n\t.flex-inner {\n\t\ttransform-origin: top center;\n\t\ttransition: transform $transition-time-fast;\n\t}\n\t&.elevated{\n\t\t.flex {\n\t\t\t&::after {\n\t\t\t\topacity: 1;\n\t\t\t}\n\t\t}\n\n\t\t// Apply a subtle scale to simulate shrink without layout changes\n\t\t.flex-inner {\n\t\t\ttransform: scaleY(0.95);\n\t\t}\n\t}\n\t&-avatar {\n\t\tbackground: rgb(255 255 255 / 0.1) !important;\n\t\t&-icon {\n\t\t\tcolor: white;\n\t\t}\n\t}\n\t&-text {\n\t\topacity: 0.7;\n\t\ttransition: opacity $transition-time-fast;\n\t\tposition: relative;\n\t\tcolor: white;\n\t\ttext-decoration: none;\n\t\t&::after {\n\t\t\tcontent: \"\";\n\t\t\twidth: 100%;\n\t\t\theight: 1.2px;\n\t\t\tbackground: white;\n\t\t\topacity: 0.7;\n\t\t\tposition: absolute;\n\t\t\tbottom: 4.6px;\n\t\t\tleft: 0;\n\t\t\ttransform: scaleX(0);\n\t\t\ttransition: transform $transition-time-fast,\n\t\t\tbackground $transition-time-fast,\n\t\t\topacity $transition-time-fast;\n\t\t}\n\t\t&:hover {\n\t\t\topacity: 0.8;\n\t\t\t&::after {\n\t\t\t\ttransform: scaleX(0.25);\n\t\t\t\topacity: 0.8;\n\t\t\t}\n\t\t}\n\t\t&.active {\n\t\t\topacity: 1;\n\t\t\tcolor: $clr-accent-default;\n\t\t\t&::after {\n\t\t\t\ttransform: scaleX(0.5);\n\t\t\t\topacity: 1;\n\t\t\t\tbackground: $clr-accent-default;\n\t\t\t}\n\t\t}\n\t}\n}\n\n.library-drawer {\n\t// Ensure the drawer paper itself gets the styling\n\t&.MuiDrawer-paper {\n\t\twidth: 280px;\n\t\tmargin: 16px;\n\t\theight: calc(100% - 32px) !important; // float in the middle\n\t\tborder-radius: 24px;\n\t\tbackground: rgba(20, 20, 25, 0.75); // Darker, substantial base\n\t\tbackdrop-filter: blur(24px) saturate(180%);\n\t\tborder: 1px solid rgba(255, 255, 255, 0.06);\n\t\tbox-shadow: \n\t\t\t0 24px 48px rgba(0, 0, 0, 0.4),\n\t\t\t0 4px 8px rgba(0, 0, 0, 0.2);\n\t\toverflow: hidden; // Contain scrollbars\n\t}\n\n\t.MuiList-root {\n\t\tpadding: 8px; // Reduced from 16px\n\t\tgap: 2px; // Reduced gap\n\t}\n\n\t.MuiDivider-root {\n\t\tborder-color: rgba(255, 255, 255, 0.1);\n\t\tmargin: 4px 12px; // Reduced margins\n\t}\n\n\t// Navigation Items\n\t.MuiListItemButton-root {\n\t\tborder-radius: 12px !important; // Slightly smaller radius\n\t\tpadding: 8px 12px; // Reduced padding (was 12px 16px)\n\t\tmargin-bottom: 2px;\n\t\ttransition: all 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);\n\t\tcolor: rgba(255, 255, 255, 0.6);\n\t\t\n\t\t&:hover {\n\t\t\tbackground: rgba(255, 255, 255, 0.08);\n\t\t\tcolor: #fff;\n\t\t\ttransform: scale(1.02);\n\t\t}\n\n\t\t.material-symbols-rounded {\n\t\t\tmargin-right: 12px; // Reduced spacing\n\t\t\tfont-size: 22px; // Slightly smaller icons usually look better with less padding\n\t\t\ttransition: all 0.2s ease;\n\t\t\tcolor: inherit;\n\t\t}\n\n\t\t.MuiListItemText-primary {\n\t\t\tfont-weight: 600;\n\t\t\tfont-size: 0.9rem; // Slightly smaller text\n\t\t\tletter-spacing: 0.02em;\n\t\t}\n\t}\n\n\t// Active State\n\t.library-drawer-item.active {\n\t\t.MuiListItemButton-root {\n\t\t\tbackground: linear-gradient(90deg, rgba($clr-accent-default, 0.2), rgba($clr-accent-default, 0.05));\n\t\t\tcolor: $clr-accent-default;\n\t\t\t\n\t\t\t.material-symbols-rounded {\n\t\t\t\tfont-variation-settings: 'FILL' 1;\n\t\t\t\tcolor: $clr-accent-default;\n\t\t\t}\n\n\t\t\t// Active Indicator Bar\n\t\t\t&::before {\n\t\t\t\tcontent: '';\n\t\t\t\tposition: absolute;\n\t\t\t\tleft: 0;\n\t\t\t\ttop: 50%;\n\t\t\t\ttransform: translateY(-50%);\n\t\t\t\theight: 24px;\n\t\t\t\twidth: 4px;\n\t\t\t\tbackground: $clr-accent-default;\n\t\t\t\tborder-radius: 0 4px 4px 0;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Custom Scrollbar for the drawer content\n\t&::-webkit-scrollbar {\n\t\twidth: 4px;\n\t}\n\t&::-webkit-scrollbar-track {\n\t\tbackground: transparent;\n\t}\n\t&::-webkit-scrollbar-thumb {\n\t\tbackground: rgba(255, 255, 255, 0.1);\n\t\tborder-radius: 4px;\n\t\t&:hover {\n\t\t\tbackground: rgba(255, 255, 255, 0.2);\n\t\t}\n\t}\n}\n\n.appLoading {\n\topacity: 0;\n}\n"
  },
  {
    "path": "src/components/appBar/appBar.tsx",
    "content": "import MuiAppBar from \"@mui/material/AppBar\";\nimport IconButton from \"@mui/material/IconButton\";\nimport useScrollTrigger from \"@mui/material/useScrollTrigger\";\nimport { useLocation, useNavigate } from \"@tanstack/react-router\";\nimport React, { useCallback, useMemo, useState } from \"react\";\n\nimport \"./appBar.scss\";\n\nimport { useShallow } from \"zustand/shallow\";\n// removed search placeholder; library state now managed via zustand\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport useHeaderStore from \"@/utils/store/header\";\nimport useSearchStore from \"@/utils/store/search\";\nimport BackButton from \"../buttons/backButton\";\nimport { UserAvatarMenu } from \"../userAvatarMenu\";\nimport { NavigationDrawer } from \"./navigationDrawer\";\n\nconst MemoizeBackButton = React.memo(BackButton);\n\nconst HIDDEN_PATHS = [\n\t\"/login\",\n\t\"/setup\",\n\t\"/server\",\n\t\"/player\",\n\t\"/error\",\n\t\"/settings\",\n\t\"/library\",\n\t\"/search\",\n];\n\nexport const AppBar = () => {\n\tconst navigate = useNavigate();\n\tconst location = useLocation();\n\n\tconst toggleSearchDialog = useSearchStore(\n\t\tuseShallow((s) => s.toggleSearchDialog),\n\t);\n\n\tconst display = !HIDDEN_PATHS.some(\n\t\t(path) =>\n\t\t\t(location.pathname.startsWith(path) &&\n\t\t\t\tlocation.pathname !== \"/player/audio\") ||\n\t\t\tlocation.pathname === \"/\",\n\t);\n\n\tconst trigger = useScrollTrigger({\n\t\tdisableHysteresis: true,\n\t\tthreshold: 20,\n\t});\n\n\tconst [showDrawer, setShowDrawer] = useState(false);\n\n\tconst appBarStyling = useMemo(() => {\n\t\treturn {\n\t\t\tbackgroundColor: \"transparent\",\n\t\t\tpaddingRight: \"0 !important\",\n\t\t};\n\t}, []);\n\n\tconst _handleNavigateToSearch = useCallback(\n\t\t() => navigate({ to: \"/search\", search: { query: \"\" } }),\n\t\t[navigate],\n\t);\n\n\tconst handleDrawerClose = useCallback(() => {\n\t\tsetShowDrawer(false);\n\t}, []);\n\n\tconst handleDrawerOpen = useCallback(() => {\n\t\tsetShowDrawer(true);\n\t}, []);\n\n\tconst handleNavigateToHome = useCallback(() => navigate({ to: \"/home\" }), []);\n\tconst handleNavigateToFavorite = useCallback(() => {\n\t\tnavigate({ to: \"/favorite\" });\n\t}, []);\n\n\tuseHeaderStore(useShallow((s) => ({ pageTitle: s.pageTitle })));\n\n\tif (!display) {\n\t\treturn null;\n\t}\n\tif (display) {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<MuiAppBar\n\t\t\t\t\tstyle={appBarStyling}\n\t\t\t\t\tclassName={\n\t\t\t\t\t\ttrigger\n\t\t\t\t\t\t\t? \"appBar flex flex-row flex-justify-spaced-between elevated\"\n\t\t\t\t\t\t\t: \"appBar flex flex-row flex-justify-spaced-between\"\n\t\t\t\t\t}\n\t\t\t\t\televation={0}\n\t\t\t\t\tcolor=\"transparent\"\n\t\t\t\t>\n\t\t\t\t\t<div className=\"flex flex-row\" style={{ gap: \"0.6em\" }}>\n\t\t\t\t\t\t<IconButton onClick={handleDrawerOpen}>\n\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">menu</span>\n\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t<MemoizeBackButton />\n\t\t\t\t\t\t<IconButton onClick={handleNavigateToHome}>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\t\t\tlocation.pathname === \"/home\"\n\t\t\t\t\t\t\t\t\t\t? \"material-symbols-rounded fill\"\n\t\t\t\t\t\t\t\t\t\t: \"material-symbols-rounded\"\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\thome\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div className=\"flex flex-row\" style={{ gap: \"0.6em\" }}>\n\t\t\t\t\t\t<IconButton onClick={toggleSearchDialog}>\n\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">search</span>\n\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t<IconButton onClick={handleNavigateToFavorite}>\n\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">favorite</span>\n\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t<UserAvatarMenu />\n\t\t\t\t\t</div>\n\t\t\t\t</MuiAppBar>\n\t\t\t\t<NavigationDrawer open={showDrawer} onClose={handleDrawerClose} />\n\t\t\t</>\n\t\t);\n\t}\n};"
  },
  {
    "path": "src/components/appBar/backOnly.tsx",
    "content": "import AppBar from \"@mui/material/AppBar\";\nimport IconButton from \"@mui/material/IconButton\";\nimport Toolbar from \"@mui/material/Toolbar\";\nimport { useLocation, useRouter } from \"@tanstack/react-router\";\nimport React from \"react\";\nimport QuickConnectButton from \"../buttons/quickConnectButton\";\n\nexport default function AppBarBackOnly() {\n\tconst { history } = useRouter();\n\tconst location = useLocation();\n\n\tconst handleBack = () => {\n\t\thistory.go(-1);\n\t};\n\n\tconst hideQuickConnect = location.pathname === \"/setup/server/list\";\n\n\treturn (\n\t\t<AppBar elevation={0} color=\"transparent\" position=\"fixed\">\n\t\t\t<Toolbar>\n\t\t\t\t<IconButton\n\t\t\t\t\tsize=\"large\"\n\t\t\t\t\tedge=\"start\"\n\t\t\t\t\t//@ts-ignore\n\t\t\t\t\tcolor=\"white\"\n\t\t\t\t\taria-label=\"back\"\n\t\t\t\t\tdisabled={history.length <= 1}\n\t\t\t\t\tonClick={handleBack}\n\t\t\t\t\tsx={{ mr: 2 }}\n\t\t\t\t>\n\t\t\t\t\t<span className=\"material-symbols-rounded\">arrow_back</span>\n\t\t\t\t</IconButton>\n\t\t\t\t{!hideQuickConnect && (\n\t\t\t\t\t<QuickConnectButton\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tmarginLeft: \"auto\",\n\t\t\t\t\t\t\tflex: \"none !important\",\n\t\t\t\t\t\t\tborderRadius: \"10px\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tcolor=\"secondary\"\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</Toolbar>\n\t\t</AppBar>\n\t);\n};\n"
  },
  {
    "path": "src/components/appBar/navigationDrawer.tsx",
    "content": "import { getUserViewsApi } from \"@jellyfin/sdk/lib/utils/api/user-views-api\";\nimport { Divider, Drawer, List, ListItem, ListItemButton } from \"@mui/material\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport React, { useMemo } from \"react\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport ListItemLink from \"../listItemLink\";\nimport { getTypeIcon } from \"../utils/iconsCollection\";\n\ninterface NavigationDrawerProps {\n    open: boolean;\n    onClose: () => void;\n}\n\nexport const NavigationDrawer = ({ open, onClose }: NavigationDrawerProps) => {\n    const api = useApiInContext((s) => s.api);\n    const [user] = useCentralStore((s) => [s.currentUser]);\n\n    const libraries = useQuery({\n        queryKey: [\"libraries\"],\n        queryFn: async () => {\n            if (!user?.Id || !api?.accessToken) {\n                return;\n            }\n            const libs = await getUserViewsApi(api).getUserViews({\n                userId: user.Id,\n            });\n            return libs.data;\n        },\n        enabled: !!user?.Id && !!api?.accessToken,\n        networkMode: \"always\",\n    });\n\n    const drawerPaperProps = useMemo(() => {\n        return {\n            className: \"glass library-drawer\",\n            elevation: 6,\n        };\n    }, []);\n\n    return (\n        <Drawer\n            open={open}\n            slotProps={{ paper: drawerPaperProps }}\n            className=\"library-drawer\"\n            onClose={onClose}\n        >\n            <List>\n                <ListItem>\n                    <ListItemButton onClick={onClose}>\n                        <span className=\"material-symbols-rounded\">menu_open</span>\n                        <div style={{ marginLeft: \"8px\" }}>Close</div>\n                    </ListItemButton>\n                </ListItem>\n            </List>\n            <Divider variant=\"middle\" />\n            <List>\n                <ListItemLink\n                    className=\"library-drawer-item\"\n                    to=\"/home\"\n                    icon=\"home\"\n                    primary=\"Home\"\n                    onClick={onClose}\n                />\n                {libraries.isSuccess &&\n                    libraries.data?.Items?.map((library) => (\n                        <ListItemLink\n                            className=\"library-drawer-item\"\n                            key={library.Id}\n                            to=\"/library/$id\"\n                            params={{ id: library.Id ?? \"\" }}\n                            icon={\n                                library.CollectionType &&\n                                getTypeIcon(library.CollectionType)\n                            }\n                            primary={library.Name ?? \"Library\"}\n                            onClick={onClose}\n                        />\n                    ))}\n            </List>\n            <Divider variant=\"middle\" />\n            <List>\n                <ListItemLink\n                    to=\"/settings/preferences\"\n                    icon=\"settings\"\n                    primary=\"Settings\"\n                    className=\"library-drawer-item\"\n                    onClick={onClose}\n                />\n                <ListItemLink\n                    to=\"/settings/changeServer\"\n                    icon=\"dns\"\n                    primary=\"Change Server\"\n                    className=\"library-drawer-item\"\n                    onClick={onClose}\n                />\n                <ListItemLink\n                    to=\"/settings/about\"\n                    icon=\"info\"\n                    primary=\"About\"\n                    className=\"library-drawer-item\"\n                    onClick={onClose}\n                />\n            </List>\n        </Drawer>\n    );\n};\n"
  },
  {
    "path": "src/components/avatar/avatar.module.scss",
    "content": "\n.avatar-image {\n\taspect-ratio: 1;\n\theight: 100%;\n\tposition: absolute;\n\tinset: 0;\n\tbackground-size: cover;\n\tbackground-position: center;\n\n\t&-container {\n\t\twidth: fit-content;\n\t\taspect-ratio: 1;\n\t\tposition: relative;\n\t\tborder-radius: 100vh;\n\t\toverflow: hidden;\n\t}\n\t&-icon {\n\t\theight: 100%;\n\t\tpadding: 0.2em;\n\t\tbackground: rgb(255 255 255 / 0.1);\n\t\tcolor: white;\n\t\tfont-size: 7em !important;\n\t}\n}\n"
  },
  {
    "path": "src/components/avatar/avatar.tsx",
    "content": "import React from \"react\";\n\nimport \"./avatar.module.scss\";\nimport { useApiInContext } from \"@/utils/store/api\";\n\nexport const AvatarImage = ({ userId }: { userId: string }) => {\n\tconst api = useApiInContext((s) => s.api);\n\tif (!api) return null;\n\n\treturn (\n\t\t<div className=\"avatar-image-container\">\n\t\t\t<div\n\t\t\t\tclassName=\"avatar-image\"\n\t\t\t\tstyle={{\n\t\t\t\t\tbackgroundImage: `url('${api.basePath}/Users/${userId}/Images/Primary')`,\n\t\t\t\t}}\n\t\t\t/>\n\n\t\t\t<div className=\"avatar-image-icon-container\">\n\t\t\t\t<span className=\"material-symbols-rounded avatar-image-icon\">\n\t\t\t\t\taccount_circle\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t</div>\n\t);\n};"
  },
  {
    "path": "src/components/backdrop/index.tsx",
    "content": "import { AnimatePresence, motion } from \"motion/react\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport BlurhashCanvas from \"../blurhash-canvas\";\n\nexport default function Backdrop() {\n\t// const [backdropLoading, setBackdropLoading] = useState(true);\n\n\tconst { backdropHash } = useBackdropStore(\n\t\tuseShallow((state) => ({\n\t\t\tbackdropHash: state.backdropHash,\n\t\t})),\n\t);\n\n\tif (!backdropHash) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t<AnimatePresence mode=\"sync\">\n\t\t\t<motion.div\n\t\t\t\tinitial={{ opacity: 0 }}\n\t\t\t\tanimate={{ opacity: 1 }}\n\t\t\t\tkey={backdropHash}\n\t\t\t\texit={{ opacity: 0 }}\n\t\t\t\ttransition={{\n\t\t\t\t\tduration: 2,\n\t\t\t\t\tease: \"easeInOut\",\n\t\t\t\t}}\n\t\t\t\tclassName=\"app-backdrop-container\"\n\t\t\t>\n\t\t\t\t<BlurhashCanvas\n\t\t\t\t\tblurhashString={backdropHash}\n\t\t\t\t\twidth={300}\n\t\t\t\t\theight={150}\n\t\t\t\t\tcanvasProps={{\n\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\tdisplay: \"block\",\n\t\t\t\t\t\t\twidth: \"100vw\",\n\t\t\t\t\t\t\theight: \"100vh\",\n\t\t\t\t\t\t\tmargin: \"0 auto\",\n\t\t\t\t\t\t\tfilter: \"brightness(0.8) contrast(1.6) saturate(1.6)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</motion.div>\n\t\t</AnimatePresence>\n\t);\n}"
  },
  {
    "path": "src/components/blurhash-canvas/index.tsx",
    "content": "import * as blurhash from \"blurhash-wasm\";\nimport React, { useEffect } from \"react\";\n\ntype BlurhashCanvasProps = {\n\tcanvasProps?: React.CanvasHTMLAttributes<HTMLCanvasElement>;\n\tblurhashString: string;\n\twidth?: number;\n\theight?: number;\n\t// punch?: number;\n\tonLoad?: () => void;\n\tonError?: (error: Error) => void;\n};\n\nconst BlurhashCanvas = ({\n\tcanvasProps,\n\tblurhashString,\n\twidth,\n\theight,\n\t// punch,\n\tonLoad,\n\tonError,\n}: BlurhashCanvasProps) => {\n\tconst canvasRef = React.useRef<HTMLCanvasElement>(null);\n\tuseEffect(() => {\n\t\tconst canvas = canvasRef.current;\n\t\tif (!canvas) return;\n\t\tconst ctx = canvas.getContext(\"2d\");\n\t\tif (!ctx) return;\n\t\tconst drawBlurhash = async () => {\n\t\t\ttry {\n\t\t\t\tconst imageDataBuffer = blurhash.decode(\n\t\t\t\t\tblurhashString,\n\t\t\t\t\twidth || 32,\n\t\t\t\t\theight || 32,\n\t\t\t\t\t// punch || 1,\n\t\t\t\t);\n\t\t\t\t// canvas.width = width || 32;\n\t\t\t\t// canvas.height = height || 32;\n\t\t\t\tif (!imageDataBuffer) {\n\t\t\t\t\tthrow new Error(\"Failed to decode blurhash\");\n\t\t\t\t}\n\t\t\t\tconst imageData = new ImageData(\n\t\t\t\t\tnew Uint8ClampedArray(imageDataBuffer),\n\t\t\t\t\twidth || 32,\n\t\t\t\t\theight || 32,\n\t\t\t\t);\n\t\t\t\t// imageData.data.set(imageDataBuffer);\n\t\t\t\tctx.putImageData(imageData, 0, 0);\n\t\t\t\tif (onLoad) onLoad();\n\t\t\t} catch (error) {\n\t\t\t\tif (onError) onError(error as Error);\n\t\t\t\telse console.error(\"Error decoding blurhash:\", error);\n\t\t\t}\n\t\t};\n\t\tdrawBlurhash();\n\t}, [blurhash, width, height, onLoad, onError]);\n\treturn <canvas ref={canvasRef} {...canvasProps} />;\n};\n\nexport default BlurhashCanvas;\n"
  },
  {
    "path": "src/components/buttons/backButton.tsx",
    "content": "import { IconButton } from \"@mui/material\";\nimport { useRouter } from \"@tanstack/react-router\";\nimport React from \"react\";\n\nexport default function BackButton() {\n\tconst { history } = useRouter();\n\tconst handleClick = () => {\n\t\thistory.back();\n\t};\n\n\treturn (\n\t\t<IconButton onClick={handleClick} disabled={history.length === 0}>\n\t\t\t<span className=\"material-symbols-rounded\">arrow_back</span>\n\t\t</IconButton>\n\t);\n}"
  },
  {
    "path": "src/components/buttons/likeButton.tsx",
    "content": "import type { UserItemDataDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport { getUserLibraryApi } from \"@jellyfin/sdk/lib/utils/api/user-library-api\";\nimport { pink } from \"@mui/material/colors\";\nimport IconButton from \"@mui/material/IconButton\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { useSnackbar } from \"notistack\";\nimport React from \"react\";\nimport { useApiInContext } from \"@/utils/store/api\";\n\nexport default function LikeButton({\n\titemId,\n\tisFavorite,\n\tqueryKey,\n\tuserId,\n\titemName,\n}: {\n\titemId: string | undefined;\n\tisFavorite: boolean | undefined;\n\tqueryKey: string[] | undefined;\n\tuserId: string | undefined;\n\titemName?: string | null;\n}) {\n\tconst api = useApiInContext((s) => s.api);\n\tconst queryClient = useQueryClient();\n\tconst { enqueueSnackbar } = useSnackbar();\n\n\tconst handleLiking = async () => {\n\t\tlet result: UserItemDataDto = null!;\n\t\tif (!api) return;\n\t\tif (!userId) return;\n\t\tif (!itemId) return;\n\t\tif (!itemName) return;\n\t\tif (isFavorite) {\n\t\t\tresult = (\n\t\t\t\tawait getUserLibraryApi(api).unmarkFavoriteItem({\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\titemId: itemId,\n\t\t\t\t})\n\t\t\t).data;\n\t\t} else if (!isFavorite) {\n\t\t\tresult = (\n\t\t\t\tawait getUserLibraryApi(api).markFavoriteItem({\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\titemId: itemId,\n\t\t\t\t})\n\t\t\t).data;\n\t\t}\n\t\treturn result; // We need to return the result so that the onSettled function can invalidate the query\n\t};\n\tconst mutation = useMutation({\n\t\tmutationFn: handleLiking,\n\t\tonError: (error) => {\n\t\t\tenqueueSnackbar(`An error occured while updating \"${itemName}\"`, {\n\t\t\t\tvariant: \"error\",\n\t\t\t});\n\t\t\tconsole.error(error);\n\t\t},\n\t\tonSettled: async () => {\n\t\t\treturn await queryClient.invalidateQueries({\n\t\t\t\tqueryKey,\n\t\t\t});\n\t\t},\n\t\tmutationKey: [\"likeButton\", itemId],\n\t});\n\n\treturn (\n\t\t<IconButton\n\t\t\tonClick={(e) => {\n\t\t\t\tif (!mutation.isPending) {\n\t\t\t\t\tmutation.mutate();\n\t\t\t\t\te.stopPropagation();\n\t\t\t\t}\n\t\t\t}}\n\t\t\tstyle={{\n\t\t\t\topacity: mutation.isPending ? 0.5 : 1,\n\t\t\t\ttransition: \"opacity 250ms\",\n\t\t\t}}\n\t\t>\n\t\t\t<span\n\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\tstyle={\n\t\t\t\t\tisFavorite\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t//@ts-expect-error\n\t\t\t\t\t\t\t\t\"--fill\": mutation.isPending ? 0 : 1,\n\t\t\t\t\t\t\t\tcolor: mutation.isPending ? \"white\" : pink.A700,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t//@ts-expect-error\n\t\t\t\t\t\t\t\t\"--fill\": mutation.isPending ? 1 : 0,\n\t\t\t\t\t\t\t\tcolor: mutation.isPending ? pink.A700 : \"white\",\n\t\t\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t>\n\t\t\t\tfavorite\n\t\t\t</span>\n\t\t</IconButton>\n\t);\n}\n"
  },
  {
    "path": "src/components/buttons/markPlayedButton.tsx",
    "content": "import React from \"react\";\n\nimport IconButton from \"@mui/material/IconButton\";\n\nimport { green } from \"@mui/material/colors\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\n\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { getPlaystateApi } from \"@jellyfin/sdk/lib/utils/api/playstate-api\";\nimport { useSnackbar } from \"notistack\";\n\nexport default function MarkPlayedButton({\n\titemId,\n\tisPlayed,\n\tqueryKey,\n\tuserId,\n\titemName,\n}: {\n\titemId?: string;\n\tisPlayed?: boolean;\n\tqueryKey?: string[];\n\tuserId?: string;\n\titemName?: string | null;\n}) {\n\tconst api = useApiInContext((s) => s.api);\n\n\tconst queryClient = useQueryClient();\n\tconst { enqueueSnackbar } = useSnackbar();\n\n\tconst handleMarking = async () => {\n\t\tlet result = null;\n\t\tif (!api) return;\n\t\tif (!api) return;\n\t\tif (!userId) return;\n\t\tif (!itemId) return;\n\t\tif (!itemName) return;\n\n\t\tif (!isPlayed) {\n\t\t\tresult = await getPlaystateApi(api).markPlayedItem({\n\t\t\t\tuserId: userId,\n\t\t\t\titemId: itemId,\n\t\t\t});\n\t\t} else if (isPlayed) {\n\t\t\tresult = await getPlaystateApi(api).markUnplayedItem({\n\t\t\t\tuserId: userId,\n\t\t\t\titemId: itemId,\n\t\t\t});\n\t\t}\n\t\treturn result; // We need to return the result so that the onSettled function can invalidate the query\n\t};\n\tconst mutation = useMutation({\n\t\tmutationFn: handleMarking,\n\t\tonError: (error) => {\n\t\t\tenqueueSnackbar(`${error}`, {\n\t\t\t\tvariant: \"error\",\n\t\t\t});\n\t\t\tenqueueSnackbar(`An error occured while updating \"${itemName}\"`, {\n\t\t\t\tvariant: \"error\",\n\t\t\t});\n\t\t\tconsole.error(error);\n\t\t},\n\t\tonSettled: async () => {\n\t\t\treturn await queryClient.invalidateQueries({\n\t\t\t\tqueryKey: queryKey,\n\t\t\t});\n\t\t},\n\t\tmutationKey: [\"markPlayedButton\", itemId],\n\t});\n\treturn (\n\t\t<IconButton\n\t\t\tonClick={(e) => {\n\t\t\t\tif (!mutation.isPending) {\n\t\t\t\t\tmutation.mutate();\n\t\t\t\t\te.stopPropagation();\n\t\t\t\t}\n\t\t\t}}\n\t\t\tstyle={{\n\t\t\t\topacity: mutation.isPending ? 0.5 : 1,\n\t\t\t\ttransition: \"opacity 250ms\",\n\t\t\t}}\n\t\t>\n\t\t\t<div\n\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\tstyle={{\n\t\t\t\t\tcolor: isPlayed\n\t\t\t\t\t\t? mutation.isPending\n\t\t\t\t\t\t\t? \"white\"\n\t\t\t\t\t\t\t: green.A700\n\t\t\t\t\t\t: mutation.isPending\n\t\t\t\t\t\t\t? green.A700\n\t\t\t\t\t\t\t: \"white\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\tdone\n\t\t\t</div>\n\t\t</IconButton>\n\t);\n}\n"
  },
  {
    "path": "src/components/buttons/playButton.tsx",
    "content": "import {\n\ttype BaseItemDto,\n\tBaseItemKind,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { getTvShowsApi } from \"@jellyfin/sdk/lib/utils/api/tv-shows-api\";\nimport type { SxProps } from \"@mui/material\";\nimport Button, { type ButtonProps } from \"@mui/material/Button\";\nimport Fab from \"@mui/material/Fab\";\nimport LinearProgress from \"@mui/material/LinearProgress\";\nimport Typography from \"@mui/material/Typography\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { useNavigate } from \"@tanstack/react-router\";\nimport { useSnackbar } from \"notistack\";\nimport React, { type MouseEvent, memo } from \"react\"; // Import memo\nimport type PlayResult from \"@//utils/types/playResult\";\nimport { getRuntimeCompact } from \"@/utils/date/time\";\nimport { getNextEpisode, getPlaybackInfo } from \"@/utils/methods/playback\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { generateAudioStreamUrl, playAudio } from \"@/utils/store/audioPlayback\";\nimport { usePhotosPlayback } from \"@/utils/store/photosPlayback\";\nimport { initializePlayback } from \"@/utils/store/playback\";\nimport { setQueue } from \"@/utils/store/queue\";\n\ntype PlayButtonProps = {\n\titem: BaseItemDto;\n\tuserId: string | undefined;\n\titemType: BaseItemKind;\n\tcurrentAudioTrack?: number | \"auto\";\n\tcurrentVideoTrack?: number;\n\tcurrentSubTrack?: number | \"nosub\";\n\tclassName?: string;\n\tsx?: SxProps;\n\tbuttonProps?: ButtonProps;\n\ticonOnly?: boolean;\n\taudio?: boolean;\n\tsize?: \"small\" | \"large\" | \"medium\";\n\tplaylistItem?: boolean;\n\tplaylistItemId?: string;\n\ttrackIndex?: number;\n};\n\n// Memoized LinearProgress component\nconst MemoizedLinearProgress = memo(({ value }: { value: number }) => (\n\t<LinearProgress\n\t\tvariant=\"determinate\"\n\t\tvalue={value}\n\t\tsx={{\n\t\t\tposition: \"absolute\",\n\t\t\ttop: 0,\n\t\t\tleft: 0,\n\t\t\tright: 0,\n\t\t\tbottom: 0,\n\t\t\theight: \"100%\",\n\t\t\tbackground: \"transparent\",\n\t\t\topacity: 0.2,\n\t\t\tzIndex: 0,\n\t\t\tmixBlendMode: \"difference\",\n\t\t}}\n\t\t//@ts-expect-error\n\t\tcolor=\"white\"\n\t/>\n));\n\nconst PlayButton = ({\n\titem,\n\tuserId,\n\titemType,\n\tcurrentAudioTrack,\n\tcurrentSubTrack,\n\t// currentVideoTrack,\n\tclassName,\n\tsx,\n\tbuttonProps,\n\ticonOnly,\n\taudio = false,\n\tsize = \"large\",\n\tplaylistItem,\n\tplaylistItemId = \"\",\n}: PlayButtonProps) => {\n\tconst api = useApiInContext((s) => s.api);\n\tconst navigate = useNavigate();\n\t// const setPlaybackDataLoading = usePlaybackDataLoadStore(\n\t// \t(state) => state.setisPending,\n\t// );\n\tconst playPhotos = usePhotosPlayback((s) => s.playPhotos);\n\n\tconst { enqueueSnackbar } = useSnackbar();\n\n\tconst itemQuery = useMutation({\n\t\tmutationKey: [\"playButton\", item?.Id, userId],\n\t\tmutationFn: async (currentEpisodeId?: string) => {\n\t\t\tif (!api) {\n\t\t\t\tthrow new Error(\"API is not available\");\n\t\t\t}\n\t\t\tif (!userId) {\n\t\t\t\tthrow new Error(\"User ID is not available\");\n\t\t\t}\n\t\t\tif (!item.Id) {\n\t\t\t\tthrow new Error(\"Item ID is not available\");\n\t\t\t}\n\n\t\t\treturn await getPlaybackInfo(api, userId, item, {\n\t\t\t\tcurrentAudioTrack,\n\t\t\t\tcurrentSubTrack,\n\t\t\t\tcurrentEpisodeId,\n\t\t\t\tplaylistItem,\n\t\t\t\tplaylistItemId,\n\t\t\t});\n\t\t},\n\t\tonSuccess: async (result: PlayResult | null) => {\n\t\t\tif (!api) {\n\t\t\t\tconsole.error(\"API is not available\");\n\t\t\t\tenqueueSnackbar(\"API is not available\", { variant: \"error\" });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!userId) {\n\t\t\t\tconsole.error(\"User ID is not available\");\n\t\t\t\tenqueueSnackbar(\"User ID is not available\", { variant: \"error\" });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!result?.item?.Items?.length) {\n\t\t\t\tconsole.error(\"No items found\");\n\t\t\t\tenqueueSnackbar(\"No items found\", { variant: \"error\" });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!result?.item?.Items?.[0]?.Id) {\n\t\t\t\tconsole.error(\"No item ID found\");\n\t\t\t\tenqueueSnackbar(\"No item ID found\", { variant: \"error\" });\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (audio) {\n\t\t\t\t// Album/Individual audio track playback\n\t\t\t\tconst playbackUrl = generateAudioStreamUrl(\n\t\t\t\t\tresult?.item?.Items?.[0].Id,\n\t\t\t\t\tuserId,\n\t\t\t\t\tapi?.deviceInfo.id,\n\t\t\t\t\tapi.basePath,\n\t\t\t\t\tapi.accessToken,\n\t\t\t\t);\n\t\t\t\tplayAudio(playbackUrl, result?.item?.Items?.[0], undefined);\n\t\t\t\tsetQueue(result?.item?.Items ?? [], 0);\n\t\t\t} else if (item.Type === \"Photo\") {\n\t\t\t\tconst index = result?.item?.Items?.map((i) => i.Id).indexOf(item.Id);\n\t\t\t\tif (result?.item?.Items && index) {\n\t\t\t\t\tplayPhotos(result?.item?.Items, index);\n\t\t\t\t\tnavigate({ to: \"/player/photos\" });\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif (!result?.mediaSource?.MediaSources?.[0]?.Id) {\n\t\t\t\t\tconsole.error(\"No media source ID found\");\n\t\t\t\t\tenqueueSnackbar(\"No media source ID found\", { variant: \"error\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst episodeIndex = result.episodeIndex;\n\t\t\t\tconst queue = result?.item?.Items ?? [];\n\n\t\t\t\tlet playItemValue = queue[episodeIndex];\n\t\t\t\tif (itemType === BaseItemKind.Movie) {\n\t\t\t\t\tplayItemValue = queue[0];\n\t\t\t\t}\n\n\t\t\t\tif (!playItemValue) {\n\t\t\t\t\tconsole.error(\"No item to play found\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst startPosition = playItemValue.UserData?.PlaybackPositionTicks;\n\n\t\t\t\tinitializePlayback({\n\t\t\t\t\tapi,\n\t\t\t\t\tuserId,\n\t\t\t\t\titem: playItemValue,\n\t\t\t\t\tmediaSource: result.mediaSource,\n\t\t\t\t\tmediaSegments: result.mediaSegments,\n\t\t\t\t\tqueueItems: queue,\n\t\t\t\t\tqueueIndex: episodeIndex,\n\t\t\t\t\tstartPositionTicks: startPosition,\n\t\t\t\t\taudioStreamIndex:\n\t\t\t\t\t\tcurrentAudioTrack === \"auto\" ? undefined : currentAudioTrack,\n\t\t\t\t\tsubtitleStreamIndex:\n\t\t\t\t\t\tcurrentSubTrack === \"nosub\" ? -1 : currentSubTrack,\n\t\t\t\t});\n\t\t\t\tnavigate({ to: \"/player\" });\n\t\t\t}\n\t\t},\n\t\tonSettled: () => {\n\t\t\t// setPlaybackDataLoading(false);\n\t\t},\n\t\tonError: (error) => {\n\t\t\tconsole.error(error);\n\t\t\tenqueueSnackbar(`${error}`, {\n\t\t\t\tvariant: \"error\",\n\t\t\t});\n\t\t},\n\t});\n\tconst handleClick = (\n\t\te: MouseEvent<HTMLAnchorElement | MouseEvent>,\n\t\tcurrentEpisodeId?: string | undefined,\n\t) => {\n\t\te.stopPropagation();\n\t\titemQuery.mutate(currentEpisodeId);\n\t};\n\tconst currentEpisode = useQuery({\n\t\tqueryKey: [\"playButton\", \"currentEpisode\", item?.Id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) {\n\t\t\t\tthrow new Error(\"API is not available\");\n\t\t\t}\n\t\t\tif (!userId) {\n\t\t\t\tthrow new Error(\"User ID is not available\");\n\t\t\t}\n\t\t\tif (!item.Id) {\n\t\t\t\tthrow new Error(\"Item ID is not available\");\n\t\t\t}\n\t\t\tconst seasons = await getTvShowsApi(api).getSeasons({\n\t\t\t\tseriesId: item.Id,\n\t\t\t\tuserId: userId,\n\t\t\t\tenableUserData: true,\n\t\t\t});\n\n\t\t\tconst currentSeason = seasons.data.Items?.find((season) => {\n\t\t\t\treturn season.UserData?.Played !== true;\n\t\t\t});\n\n\t\t\tconst episode = await getNextEpisode(\n\t\t\t\tapi,\n\t\t\t\tuserId,\n\t\t\t\titem.Id,\n\t\t\t\tcurrentSeason?.IndexNumber ?? 0,\n\t\t\t);\n\t\t\tif (episode) {\n\t\t\t\treturn { Items: [episode] };\n\t\t\t}\n\t\t\treturn { Items: [] };\n\t\t},\n\t\tenabled: itemType === BaseItemKind.Series,\n\t});\n\n\tif (iconOnly) {\n\t\treturn (\n\t\t\t//@ts-expect-error\n\t\t\t<Fab\n\t\t\t\tcolor=\"primary\"\n\t\t\t\taria-label=\"Play\"\n\t\t\t\tclassName={className}\n\t\t\t\tonClick={(e) => {\n\t\t\t\t\tif (item.Type === \"Episode\") {\n\t\t\t\t\t\thandleClick(e, item.Id);\n\t\t\t\t\t} else if (itemType === BaseItemKind.Series) {\n\t\t\t\t\t\thandleClick(e, currentEpisode.data?.Items?.[0]?.Id);\n\t\t\t\t\t} else {\n\t\t\t\t\t\thandleClick(e);\n\t\t\t\t\t}\n\t\t\t\t}}\n\t\t\t\tsx={sx}\n\t\t\t\tsize={size}\n\t\t\t\tdisabled={\n\t\t\t\t\titemType === BaseItemKind.Series &&\n\t\t\t\t\t(!currentEpisode.data ||\n\t\t\t\t\t\t!currentEpisode.data.Items ||\n\t\t\t\t\t\tcurrentEpisode.data.Items.length === 0)\n\t\t\t\t}\n\t\t\t\t{...buttonProps}\n\t\t\t>\n\t\t\t\t<span\n\t\t\t\t\tclassName=\"material-symbols-rounded em-4 fill\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tfontSize: \"3em\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\tplay_arrow\n\t\t\t\t</span>\n\t\t\t</Fab>\n\t\t);\n\t}\n\n\tif (itemType === BaseItemKind.Series) {\n\t\treturn (\n\t\t\t<div\n\t\t\t\tclassName=\"play-button\"\n\t\t\t\tstyle={{\n\t\t\t\t\twidth: \"auto\",\n\t\t\t\t\tposition: \"relative\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Button\n\t\t\t\t\tloading={currentEpisode.isPending}\n\t\t\t\t\tclassName={className ?? \"play-button\"}\n\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\tconst episodeId = currentEpisode.data?.Items?.[0]?.Id;\n\t\t\t\t\t\tif (episodeId) {\n\t\t\t\t\t\t\thandleClick(e, episodeId);\n\t\t\t\t\t\t}\n\t\t\t\t\t}}\n\t\t\t\t\tstartIcon={\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"material-symbols-rounded fill\"\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tzIndex: 1,\n\t\t\t\t\t\t\t\tfontSize: \"2em\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tplay_arrow\n\t\t\t\t\t\t</span>\n\t\t\t\t\t}\n\t\t\t\t\t{...buttonProps}\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tposition: \"relative\",\n\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t}}\n\t\t\t\t\t//@ts-expect-error - white color is a custom color in the theme which mui's types don't know about\n\t\t\t\t\tcolor=\"white\"\n\t\t\t\t\tsize={size}\n\t\t\t\t\tdisabled={\n\t\t\t\t\t\tcurrentEpisode.isPending ||\n\t\t\t\t\t\t!currentEpisode.data ||\n\t\t\t\t\t\t!currentEpisode.data.Items ||\n\t\t\t\t\t\tcurrentEpisode.data.Items.length === 0 ||\n\t\t\t\t\t\t!currentEpisode.data.Items[0].Id\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t{currentEpisode.isPending ? (\n\t\t\t\t\t\t\"Loading...\"\n\t\t\t\t\t) : !currentEpisode.data ||\n\t\t\t\t\t\t!currentEpisode.data.Items ||\n\t\t\t\t\t\tcurrentEpisode.data.Items.length === 0 ? (\n\t\t\t\t\t\t\"No episodes to watch found\"\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\tWatch S{currentEpisode.data.Items[0].ParentIndexNumber ?? 1}E\n\t\t\t\t\t\t\t{currentEpisode.data.Items[0]?.IndexNumber ?? 1}\n\t\t\t\t\t\t\t<MemoizedLinearProgress\n\t\t\t\t\t\t\t\t//@ts-expect-error\n\t\t\t\t\t\t\t\tvalue={\n\t\t\t\t\t\t\t\t\t100 >\n\t\t\t\t\t\t\t\t\t\t(currentEpisode.data.Items[0].UserData?.PlayedPercentage ??\n\t\t\t\t\t\t\t\t\t\t\t100) &&\n\t\t\t\t\t\t\t\t\t(currentEpisode.data.Items[0].UserData?.PlayedPercentage ??\n\t\t\t\t\t\t\t\t\t\t0) > 0\n\t\t\t\t\t\t\t\t\t\t? currentEpisode.data.Items[0].UserData?.PlayedPercentage\n\t\t\t\t\t\t\t\t\t\t: 0\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</>\n\t\t\t\t\t)}\n\t\t\t\t</Button>\n\t\t\t\t{(currentEpisode.data?.Items?.[0]?.UserData?.PlaybackPositionTicks ??\n\t\t\t\t\t0) > 0 && (\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\topacity: 0.8,\n\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\tbottom: \"-1.8em\",\n\t\t\t\t\t\t\tleft: \"50%\",\n\t\t\t\t\t\t\ttransform: \"translate(-50%)\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{getRuntimeCompact(\n\t\t\t\t\t\t\t(currentEpisode.data?.Items?.[0]?.RunTimeTicks ?? 0) -\n\t\t\t\t\t\t\t\t(currentEpisode.data?.Items?.[0]?.UserData\n\t\t\t\t\t\t\t\t\t?.PlaybackPositionTicks ?? 0),\n\t\t\t\t\t\t)}{\" \"}\n\t\t\t\t\t\tleft\n\t\t\t\t\t</Typography>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t);\n\t}\n\treturn (\n\t\t<div\n\t\t\tclassName=\"play-button\"\n\t\t\tstyle={{\n\t\t\t\twidth: \"auto\",\n\t\t\t\tposition: \"relative\",\n\t\t\t}}\n\t\t>\n\t\t\t<Button\n\t\t\t\tclassName={className ?? \"play-button\"}\n\t\t\t\tvariant=\"contained\"\n\t\t\t\tonClick={(e) =>\n\t\t\t\t\titem.Type === \"Episode\" ? handleClick(e, item.Id) : handleClick(e)\n\t\t\t\t}\n\t\t\t\tstartIcon={\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName=\"material-symbols-rounded fill\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tzIndex: 1,\n\t\t\t\t\t\t\tfontSize: \"2em\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\tplay_arrow\n\t\t\t\t\t</span>\n\t\t\t\t}\n\t\t\t\t{...buttonProps}\n\t\t\t\tsx={{\n\t\t\t\t\tposition: \"relative\",\n\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t}}\n\t\t\t\t//@ts-expect-error - white color is a custom color in the theme which mui's types don't know about\n\t\t\t\tcolor=\"white\"\n\t\t\t\tsize={size}\n\t\t\t>\n\t\t\t\t{item.UserData?.PlaybackPositionTicks\n\t\t\t\t\t? \"Continue Watching\"\n\t\t\t\t\t: item?.Type === \"MusicAlbum\" ||\n\t\t\t\t\t\t\titem?.Type === \"Audio\" ||\n\t\t\t\t\t\t\titem?.Type === \"AudioBook\" ||\n\t\t\t\t\t\t\titem?.Type === \"Playlist\" ||\n\t\t\t\t\t\t\taudio\n\t\t\t\t\t\t? \"Play Now\"\n\t\t\t\t\t\t: \"Watch Now\"}\n\t\t\t\t<MemoizedLinearProgress\n\t\t\t\t\t//@ts-expect-error\n\t\t\t\t\tvalue={\n\t\t\t\t\t\t100 > (item.UserData?.PlayedPercentage ?? 100) &&\n\t\t\t\t\t\t(item.UserData?.PlayedPercentage ?? 0) > 0\n\t\t\t\t\t\t\t? item.UserData?.PlayedPercentage\n\t\t\t\t\t\t\t: 0\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t</Button>\n\t\t\t{(item.UserData?.PlaybackPositionTicks ?? 0) > 0 && (\n\t\t\t\t<Typography\n\t\t\t\t\tsx={{\n\t\t\t\t\t\topacity: 0.8,\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\tbottom: \"-1.8em\",\n\t\t\t\t\t\tleft: \"50%\",\n\t\t\t\t\t\ttransform: \"translate(-50%)\",\n\t\t\t\t\t}}\n\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t>\n\t\t\t\t\t{getRuntimeCompact(\n\t\t\t\t\t\t(item.RunTimeTicks ?? 0) -\n\t\t\t\t\t\t\t(item.UserData?.PlaybackPositionTicks ?? 0),\n\t\t\t\t\t)}{\" \"}\n\t\t\t\t\tleft\n\t\t\t\t</Typography>\n\t\t\t)}\n\t\t</div>\n\t);\n};\n\nexport default PlayButton;\n"
  },
  {
    "path": "src/components/buttons/playNextButton.tsx",
    "content": "import { useApiInContext } from \"@/utils/store/api\";\nimport { playItemFromQueue } from \"@/utils/store/playback\";\nimport useQueue from \"@/utils/store/queue\";\nimport { getUserApi } from \"@jellyfin/sdk/lib/utils/api/user-api\";\nimport { IconButton } from \"@mui/material\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport React from \"react\";\n\nconst PlayNextButton = () => {\n\tconst api = useApiInContext((s) => s.api);\n\n\tconst user = useQuery({\n\t\tqueryKey: [\"user\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return;\n\t\t\tconst result = await getUserApi(api).getCurrentUser();\n\t\t\treturn result.data;\n\t\t},\n\t});\n\tconst handlePlayNext = useMutation({\n\t\tmutationKey: [\"playNextButton\"],\n\t\tmutationFn: () => playItemFromQueue(\"next\", user.data?.Id, api),\n\t\tonError: (error) => console.error(error),\n\t});\n\tconst [queueItems, currentItemIndex] = useQueue((state) => [\n\t\tstate.tracks,\n\t\tstate.currentItemIndex,\n\t]);\n\treturn (\n\t\t<IconButton\n\t\t\tdisabled={queueItems?.length === currentItemIndex + 1}\n\t\t\tonClick={() => handlePlayNext.mutate()}\n\t\t>\n\t\t\t<span className=\"material-symbols-rounded\">skip_next</span>\n\t\t</IconButton>\n\t);\n};\n\nexport default PlayNextButton;"
  },
  {
    "path": "src/components/buttons/playPreviousButtom.tsx",
    "content": "import { useApiInContext } from \"@/utils/store/api\";\nimport { playItemFromQueue } from \"@/utils/store/playback\";\nimport useQueue from \"@/utils/store/queue\";\nimport { getUserApi } from \"@jellyfin/sdk/lib/utils/api/user-api\";\nimport { IconButton } from \"@mui/material\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport React from \"react\";\n\nconst PlayPreviousButton = () => {\n\tconst api = useApiInContext((s) => s.api);\n\tconst user = useQuery({\n\t\tqueryKey: [\"user\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return;\n\t\t\tconst result = await getUserApi(api).getCurrentUser();\n\t\t\treturn result.data;\n\t\t},\n\t});\n\tconst handlePlayNext = useMutation({\n\t\tmutationKey: [\"playPreviousButton\"],\n\t\tmutationFn: () => playItemFromQueue(\"previous\", user.data?.Id, api),\n\t\tonError: (error) => [console.error(error)],\n\t});\n\tconst [currentItemIndex] = useQueue((state) => [state.currentItemIndex]);\n\treturn (\n\t\t<IconButton\n\t\t\tdisabled={currentItemIndex === 0}\n\t\t\tonClick={() => handlePlayNext.mutate()}\n\t\t>\n\t\t\t<span className=\"material-symbols-rounded\">skip_previous</span>\n\t\t</IconButton>\n\t);\n};\n\nexport default PlayPreviousButton;"
  },
  {
    "path": "src/components/buttons/queueButton.scss",
    "content": ".queue{\n    &-item{\n        --image-size: 5em;\n        &.episode {\n                --image-size: 8em !important;\n            }\n        width: 32em;\n        padding: 1em;\n        display: grid !important;\n        opacity: 1 !important;\n        gap: 1em;\n        height: 5em;\n        grid-template-columns: 3em var(--image-size) 1fr;\n        justify-items: center;\n        align-items: center;\n        &-image{\n            max-height: 100%;\n            max-width: 100%;\n            object-fit: cover;\n            border-radius: $border-radius_04;\n            overflow: hidden;\n            &-container {\n                aspect-ratio: 1;\n                overflow: hidden;\n                height: 100%;\n                display: flex;\n                justify-content: center;\n                align-items: center;    \n                position: relative;\n                width: var(--image-size);\n            }\n            &-icon {\n                width: 100%;\n                height: 100%;\n                display: grid;\n                place-items: center;\n                background: rgb(255 255 255 / 0.2);\n                border-radius: 10px;\n                .material-symbols-rounded {\n                    font-size: 2em;\n                }\n                \n            }\n        }\n        &-info{\n            display: flex;\n            flex-direction: column;\n            gap: 0.2em;\n            width: 100%;\n            overflow: hidden;\n        }\n    }\n    &-item.Mui-disabled {\n        .queue-item-image,.queue-item-image-icon{\n            opacity: 0.5 !important;\n        }\n    }\n    \n}"
  },
  {
    "path": "src/components/buttons/queueButton.tsx",
    "content": "import { getUserApi } from \"@jellyfin/sdk/lib/utils/api/user-api\";\nimport {\n\tBox,\n\tDrawer,\n\tIconButton,\n\tList,\n\tTooltip,\n\tTypography,\n} from \"@mui/material\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport React, { useMemo, useState } from \"react\";\nimport { playItemFromQueue } from \"@/utils/store/playback\";\nimport useQueue, {\n\tclearUpcoming,\n\tremoveFromQueue,\n\treorderQueue,\n\tshuffleUpcoming,\n} from \"@/utils/store/queue\";\nimport \"./queueButton.scss\";\nimport {\n\tclosestCenter,\n\tDndContext,\n\ttype DragEndEvent,\n\tKeyboardSensor,\n\tPointerSensor,\n\tuseSensor,\n\tuseSensors,\n} from \"@dnd-kit/core\";\nimport { restrictToVerticalAxis } from \"@dnd-kit/modifiers\";\nimport {\n\tarrayMove,\n\tSortableContext,\n\tsortableKeyboardCoordinates,\n\tuseSortable,\n\tverticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport QueueListItem from \"../queueListItem\";\n\nfunction SortableQueueItem({\n\titem,\n\tonDelete,\n\tonPlay,\n\tindex,\n}: {\n\titem: BaseItemDto;\n\tonDelete: () => void;\n\tonPlay: () => void;\n\tindex: number;\n}) {\n\tconst {\n\t\tattributes,\n\t\tlisteners,\n\t\tsetNodeRef,\n\t\ttransform,\n\t\ttransition,\n\t\tisDragging,\n\t} = useSortable({ id: item.Id || \"\" });\n\n\tconst style = {\n\t\ttransform: CSS.Transform.toString(transform),\n\t\ttransition,\n\t\topacity: isDragging ? 0.5 : 1,\n\t\tposition: \"relative\" as const,\n\t\tzIndex: isDragging ? 2 : 1,\n\t};\n\n\treturn (\n\t\t<div ref={setNodeRef} style={style}>\n\t\t\t<QueueListItem\n\t\t\t\tqueueItem={item}\n\t\t\t\tonDelete={onDelete}\n\t\t\t\tonPlay={onPlay}\n\t\t\t\tdragHandleProps={{ ...attributes, ...listeners }}\n\t\t\t\tisEpisode={item.Type === \"Episode\"}\n\t\t\t\tindex={index}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n\nconst QueueButton = () => {\n    const api = useApiInContext((s) => s.api);\n\n    const [queueItems, currentItemIndex] = useQueue((state) => [\n\t\t\t\t\tstate.tracks,\n\t\t\t\t\tstate.currentItemIndex,\n\t\t\t\t]);\n\n    const [open, setOpen] = useState(false);\n\n\tconst user = useQuery({\n\t\tqueryKey: [\"currentUser\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) throw new Error(\"API not available\");\n\t\t\treturn (await getUserApi(api).getCurrentUser()).data;\n\t\t},\n\t\tenabled: !!api,\n\t});\n\n    const sensors = useSensors(\n\t\t\t\t\tuseSensor(PointerSensor),\n\t\t\t\t\tuseSensor(KeyboardSensor, {\n\t\t\t\t\t\tcoordinateGetter: sortableKeyboardCoordinates,\n\t\t\t\t\t}),\n\t\t\t\t);\n\n\t\t\t\tconst upcomingItems = useMemo(() => {\n\t\t\t\t\treturn queueItems ? queueItems.slice(currentItemIndex + 1) : [];\n\t\t\t\t}, [queueItems, currentItemIndex]);\n\n\t\t\t\tconst upcomingIds = useMemo(() => {\n\t\t\t\t\treturn upcomingItems.map((item) => item.Id || \"\");\n\t\t\t\t}, [upcomingItems]);\n\n\t\t\t\tconst handleDragEnd = (event: DragEndEvent) => {\n\t\t\t\t\tconst { active, over } = event;\n\n\t\t\t\t\tif (active.id !== over?.id && queueItems) {\n\t\t\t\t\t\tconst oldIndex = upcomingItems.findIndex(\n\t\t\t\t\t\t\t(item) => item.Id === active.id,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst newIndex = upcomingItems.findIndex(\n\t\t\t\t\t\t\t(item) => item.Id === over?.id,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tif (oldIndex !== -1 && newIndex !== -1) {\n\t\t\t\t\t\t\tconst newUpcoming = arrayMove(upcomingItems, oldIndex, newIndex);\n\t\t\t\t\t\t\tconst newQueue = [\n\t\t\t\t\t\t\t\t...queueItems.slice(0, currentItemIndex + 1),\n\t\t\t\t\t\t\t\t...newUpcoming,\n\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\treorderQueue(newQueue);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t};\n\n\t\t\t\tconst handleDelete = (index: number) => {\n\t\t\t\t\tremoveFromQueue(index);\n\t\t\t\t};\n\n\t\t\t\tconst handlePlay = (index: number) => {\n\t\t\t\t\tplayItemFromQueue(index, user.data?.Id, api);\n\t\t\t\t};\n\n\t\t\t\tconst handleClear = () => {\n\t\t\t\t\tclearUpcoming();\n\t\t\t\t};\n\n\t\t\t\tconst handleShuffle = () => {\n\t\t\t\t\tshuffleUpcoming();\n\t\t\t\t};\n\n    return (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<Drawer\n\t\t\t\t\t\t\tanchor=\"right\"\n\t\t\t\t\t\t\topen={open}\n\t\t\t\t\t\t\tonClose={() => setOpen(false)}\n\t\t\t\t\t\t\tPaperProps={{\n\t\t\t\t\t\t\t\tclassName: \"glass\",\n\t\t\t\t\t\t\t\tsx: {\n\t\t\t\t\t\t\t\t\twidth: 450,\n\t\t\t\t\t\t\t\t\tmaxWidth: \"100%\",\n\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\t\t\t\t\tborderLeft: \"1px solid rgba(255, 255, 255, 0.08)\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tModalProps={{\n\t\t\t\t\t\t\t\tBackdropProps: {\n\t\t\t\t\t\t\t\t\tsx: {\n\t\t\t\t\t\t\t\t\t\tbackgroundColor: \"rgba(0, 0, 0, 0.2)\",\n\t\t\t\t\t\t\t\t\t\tbackdropFilter: \"blur(4px)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Box\n\t\t\t\t\t\t\t\tp={3}\n\t\t\t\t\t\t\t\tdisplay=\"flex\"\n\t\t\t\t\t\t\t\tjustifyContent=\"space-between\"\n\t\t\t\t\t\t\t\talignItems=\"center\"\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\tposition: \"sticky\",\n\t\t\t\t\t\t\t\t\ttop: 0,\n\t\t\t\t\t\t\t\t\tzIndex: 20,\n\t\t\t\t\t\t\t\t\tborderBottom: \"1px solid\",\n\t\t\t\t\t\t\t\t\tborderColor: \"rgba(255, 255, 255, 0.08)\",\n\t\t\t\t\t\t\t\t\tbackgroundColor: \"rgba(20, 20, 30, 0.4)\",\n\t\t\t\t\t\t\t\t\tbackdropFilter: \"blur(12px)\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Box display=\"flex\" alignItems=\"center\" gap={1}>\n\t\t\t\t\t\t\t\t\t<Typography variant=\"h5\" fontWeight=\"bold\">\n\t\t\t\t\t\t\t\t\t\tPlay Queue\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t\t\t\t\t\tcolor=\"text.secondary\"\n\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\tml: 1,\n\t\t\t\t\t\t\t\t\t\t\tfontWeight: 600,\n\t\t\t\t\t\t\t\t\t\t\tbgcolor: \"rgba(255, 255, 255, 0.08)\",\n\t\t\t\t\t\t\t\t\t\t\tpx: 1,\n\t\t\t\t\t\t\t\t\t\t\tpy: 0.5,\n\t\t\t\t\t\t\t\t\t\t\tborderRadius: 2,\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{queueItems?.length || 0}\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t</Box>\n\t\t\t\t\t\t\t\t<Box display=\"flex\" gap={0.5}>\n\t\t\t\t\t\t\t\t\t<Tooltip title=\"Shuffle Upcoming\">\n\t\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t\t<IconButton\n\t\t\t\t\t\t\t\t\t\t\t\tonClick={handleShuffle}\n\t\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t\t\tdisabled={upcomingItems.length < 2}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">\n\t\t\t\t\t\t\t\t\t\t\t\t\tshuffle\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t\t<Tooltip title=\"Clear Upcoming\">\n\t\t\t\t\t\t\t\t\t\t<span>\n\t\t\t\t\t\t\t\t\t\t\t<IconButton\n\t\t\t\t\t\t\t\t\t\t\t\tonClick={handleClear}\n\t\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t\t\tdisabled={upcomingItems.length === 0}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">\n\t\t\t\t\t\t\t\t\t\t\t\t\tclear_all\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t\t\t\t<IconButton onClick={() => setOpen(false)} size=\"small\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">close</span>\n\t\t\t\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t\t\t</Box>\n\t\t\t\t\t\t\t</Box>\n\n\t\t\t\t\t\t\t<Box\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\toverflowY: \"auto\",\n\t\t\t\t\t\t\t\t\tflex: 1,\n\t\t\t\t\t\t\t\t\tp: 2,\n\t\t\t\t\t\t\t\t\t\"&::-webkit-scrollbar\": { width: 8 },\n\t\t\t\t\t\t\t\t\t\"&::-webkit-scrollbar-track\": { background: \"transparent\" },\n\t\t\t\t\t\t\t\t\t\"&::-webkit-scrollbar-thumb\": {\n\t\t\t\t\t\t\t\t\t\tbackground: \"rgba(128,128,128,0.2)\",\n\t\t\t\t\t\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"&::-webkit-scrollbar-thumb:hover\": {\n\t\t\t\t\t\t\t\t\t\tbackground: \"rgba(128,128,128,0.3)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{queueItems && queueItems.length > 0 ? (\n\t\t\t\t\t\t\t\t\t<List disablePadding>\n\t\t\t\t\t\t\t\t\t\t{/* Current Item */}\n\t\t\t\t\t\t\t\t\t\t{queueItems[currentItemIndex] && (\n\t\t\t\t\t\t\t\t\t\t\t<Box sx={{ mb: 4 }}>\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"overline\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisplay: \"block\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.6,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmb: 1,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tfontWeight: 600,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tletterSpacing: 1.2,\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\tNOW PLAYING\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t<Box\n\t\t\t\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tbgcolor: \"background.paper\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tborderRadius: 3,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tboxShadow: 4,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tborder: \"1px solid\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tborderColor: \"divider\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<QueueListItem\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tqueueItem={queueItems[currentItemIndex]}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tactive={true}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tisEpisode={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tqueueItems[currentItemIndex].Type === \"Episode\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tindex={currentItemIndex + 1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t</Box>\n\t\t\t\t\t\t\t\t\t\t\t</Box>\n\t\t\t\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t\t\t\t{/* Upcoming Items */}\n\t\t\t\t\t\t\t\t\t\t{upcomingItems.length > 0 && (\n\t\t\t\t\t\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"overline\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisplay: \"block\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.6,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tmb: 1,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tfontWeight: 600,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tletterSpacing: 1.2,\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\tNEXT UP\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t<DndContext\n\t\t\t\t\t\t\t\t\t\t\t\t\tsensors={sensors}\n\t\t\t\t\t\t\t\t\t\t\t\t\tcollisionDetection={closestCenter}\n\t\t\t\t\t\t\t\t\t\t\t\t\tonDragEnd={handleDragEnd}\n\t\t\t\t\t\t\t\t\t\t\t\t\tmodifiers={[restrictToVerticalAxis]}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<SortableContext\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titems={upcomingIds}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstrategy={verticalListSortingStrategy}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Box\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tgap: 1,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{upcomingItems.map((item, index) => (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<SortableQueueItem\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tkey={item.Id || index}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\titem={item}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonDelete={() =>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thandleDelete(currentItemIndex + 1 + index)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonPlay={() =>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\thandlePlay(currentItemIndex + 1 + index)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tindex={currentItemIndex + 1 + index + 1}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</Box>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</SortableContext>\n\t\t\t\t\t\t\t\t\t\t\t\t</DndContext>\n\t\t\t\t\t\t\t\t\t\t\t</>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</List>\n\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t<Box\n\t\t\t\t\t\t\t\t\t\tp={4}\n\t\t\t\t\t\t\t\t\t\ttextAlign=\"center\"\n\t\t\t\t\t\t\t\t\t\tdisplay=\"flex\"\n\t\t\t\t\t\t\t\t\t\tflexDirection=\"column\"\n\t\t\t\t\t\t\t\t\t\talignItems=\"center\"\n\t\t\t\t\t\t\t\t\t\tgap={2}\n\t\t\t\t\t\t\t\t\t\tsx={{ opacity: 0.5 }}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\t\t\t\tstyle={{ fontSize: 48 }}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tqueue_music\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t<Typography>Your queue is empty</Typography>\n\t\t\t\t\t\t\t\t\t</Box>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</Box>\n\t\t\t\t\t\t</Drawer>\n\t\t\t\t\t\t<IconButton onClick={() => setOpen(true)}>\n\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">playlist_play</span>\n\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t</>\n\t\t\t\t);\n};\n\nexport default QueueButton;"
  },
  {
    "path": "src/components/buttons/quickConnectButton.tsx",
    "content": "import { getQuickConnectApi } from \"@jellyfin/sdk/lib/utils/api/quick-connect-api\";\nimport { getUserApi } from \"@jellyfin/sdk/lib/utils/api/user-api\";\nimport { LoadingButton, type LoadingButtonProps } from \"@mui/lab\";\nimport {\n\tButton,\n\tCheckbox,\n\tDialog,\n\tDialogActions,\n\tDialogContent,\n\tDialogContentText,\n\tFormControlLabel,\n\tSlide,\n\tTooltip,\n\tTypography,\n} from \"@mui/material\";\nimport type { TransitionProps } from \"@mui/material/transitions\";\nimport { skipToken, useMutation, useQuery } from \"@tanstack/react-query\";\nimport { useNavigate } from \"@tanstack/react-router\";\nimport { writeText } from \"@tauri-apps/plugin-clipboard-manager\";\nimport { useSnackbar } from \"notistack\";\nimport React, { useCallback, useState } from \"react\";\nimport useInterval from \"@/utils/hooks/useInterval\";\nimport { saveUser } from \"@/utils/storage/user\";\nimport { useApiInContext } from \"@/utils/store/api\";\n\nconst Transition = React.forwardRef(function Transition(\n\tprops: TransitionProps & {\n\t\tchildren: React.ReactElement<any, any>;\n\t},\n\tref: React.Ref<unknown>,\n) {\n\treturn (\n\t\t<Slide\n\t\t\tdirection=\"up\"\n\t\t\tmountOnEnter\n\t\t\tunmountOnExit\n\t\t\tref={ref}\n\t\t\ttimeout={{ enter: 500, exit: 1500 }}\n\t\t\t{...props}\n\t\t/>\n\t);\n});\n\nconst QuickConnectButton = (props: LoadingButtonProps) => {\n\tconst api = useApiInContext((s) => s.api);\n\tconst createApi = useApiInContext((s) => s.createApi);\n\t// if (!api) {\n\t// \tconsole.error(\n\t// \t\t\"Unable to display quick connect button, api is not available\",\n\t// \t);\n\t// \treturn null;\n\t// }\n\n\tconst headers = {\n\t\t\"X-Emby-Authorization\": `MediaBrowser Client=\"${api?.clientInfo.name}\", Device=\"${api?.deviceInfo.name}\", DeviceId=\"${api?.deviceInfo.id}\", Version=\"${api?.clientInfo.version}\"`,\n\t};\n\n\tconst navigate = useNavigate();\n\n\tconst quickConnectEnabled = useQuery({\n\t\tqueryKey: [\"quick-connect-button\", \"check-quick-connect-enabled\"],\n\t\tqueryFn: api\n\t\t\t? async () => await getQuickConnectApi(api).getQuickConnectEnabled()\n\t\t\t: skipToken,\n\t\tenabled: Boolean(api),\n\t});\n\n\tconst { enqueueSnackbar } = useSnackbar();\n\tconst [quickConnectCode, setQuickConnectCode] = useState<string | null>();\n\tconst [quickConnectSecret, setQuickConnectSecret] = useState<string | null>();\n\tconst [checkForQuickConnect, setCheckForQuickConnect] = useState(false);\n\tconst [rememberUser, setRememberUser] = useState(true);\n\tconst authenticateUser = useMutation({\n\t\tmutationKey: [\"quick-connect-button\", \"authenticate-user\"],\n\t\tmutationFn: async () => {\n\t\t\tif (api && quickConnectSecret) {\n\t\t\t\treturn await getUserApi(api).authenticateWithQuickConnect(\n\t\t\t\t\t{ quickConnectDto: { Secret: quickConnectSecret } },\n\t\t\t\t\t{ headers },\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t\tonSuccess: (result) => {\n\t\t\tif (api && result?.data?.AccessToken && result.data.User?.Name) {\n\t\t\t\tsetCheckForQuickConnect(false);\n\t\t\t\t// saveUser({ username, password, rememberMe: rememberUser });\n\t\t\t\tenqueueSnackbar(`Logged in as ${result.data.User?.Name}!`, {\n\t\t\t\t\tvariant: \"success\",\n\t\t\t\t});\n\t\t\t\tcreateApi(api.basePath, result.data.AccessToken);\n\t\t\t\tif (rememberUser) {\n\t\t\t\t\tsaveUser(result.data.User?.Name, result.data.AccessToken);\n\t\t\t\t}\n\t\t\t\tnavigate({ to: \"/home\", replace: true });\n\t\t\t}\n\t\t},\n\t\tonError: (error) => {\n\t\t\tenqueueSnackbar(\"Error authenticating user.\", { variant: \"error\" });\n\t\t\tconsole.error(error);\n\t\t},\n\t});\n\tconst checkQuickConnectStatus = useMutation({\n\t\tmutationKey: [\"quick-connect-button\", \"check-quick-connect-status\"],\n\t\tmutationFn: async (secret: string) =>\n\t\t\tapi &&\n\t\t\t(\n\t\t\t\tawait getQuickConnectApi(api).getQuickConnectState(\n\t\t\t\t\t{ secret },\n\t\t\t\t\t{ headers },\n\t\t\t\t)\n\t\t\t).data,\n\t\tonSuccess: (result) => {\n\t\t\tif (result?.Authenticated) {\n\t\t\t\tsetQuickConnectCode(null);\n\t\t\t\tinitQuickConnect.reset();\n\t\t\t\tcheckQuickConnectStatus.reset();\n\t\t\t\tauthenticateUser.mutate();\n\t\t\t}\n\t\t},\n\t});\n\tconst initQuickConnect = useMutation({\n\t\tmutationKey: [\"quick-connect-button\", \"initiate-connection\"],\n\t\tmutationFn: async () => {\n\t\t\tif (!quickConnectEnabled.data?.data) {\n\t\t\t\tenqueueSnackbar(\"Quick Connect is not enabled on server.\", {\n\t\t\t\t\tvariant: \"error\",\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (api) {\n\t\t\t\treturn await getQuickConnectApi(api).initiateQuickConnect({ headers });\n\t\t\t}\n\t\t},\n\t\tonSuccess: (result) => {\n\t\t\tsetQuickConnectCode(result?.data.Code);\n\t\t\tsetQuickConnectSecret(result?.data.Secret);\n\t\t\tsetCheckForQuickConnect(true);\n\t\t},\n\t\tonError: (error) => {\n\t\t\tenqueueSnackbar(\"Error initiating Quick Connect.\", { variant: \"error\" });\n\t\t\tconsole.error(error);\n\t\t},\n\t});\n\n\tconst handleQuickConnectClose = useCallback(() => {\n\t\tsetQuickConnectCode(null);\n\t\tsetCheckForQuickConnect(false);\n\t\tinitQuickConnect.reset();\n\t\tcheckQuickConnectStatus.reset();\n\t}, []);\n\n\tuseInterval(\n\t\t() => {\n\t\t\tif (quickConnectSecret) {\n\t\t\t\tconsole.log(checkForQuickConnect);\n\t\t\t\tcheckQuickConnectStatus.mutate(quickConnectSecret);\n\t\t\t}\n\t\t},\n\t\tcheckForQuickConnect ? 1500 : null,\n\t);\n\n\treturn (\n\t\t<div style={{ marginLeft: \"auto\" }}>\n\t\t\t{/* @ts-ignore */}\n\t\t\t<LoadingButton\n\t\t\t\t{...props}\n\t\t\t\tdisabled={\n\t\t\t\t\tquickConnectEnabled.isPending ||\n\t\t\t\t\tBoolean(!quickConnectEnabled.data?.data)\n\t\t\t\t}\n\t\t\t\tloading={Boolean(\n\t\t\t\t\tinitQuickConnect.isPending ||\n\t\t\t\t\t\tcheckQuickConnectStatus.isPending ||\n\t\t\t\t\t\t(quickConnectCode && !checkQuickConnectStatus.data?.Authenticated),\n\t\t\t\t)}\n\t\t\t\tvariant={props.variant ?? \"contained\"}\n\t\t\t\tonClick={initQuickConnect.mutate}\n\t\t\t>\n\t\t\t\t{quickConnectEnabled.data?.data\n\t\t\t\t\t? \"Use Quick Connect\"\n\t\t\t\t\t: \"Quick Connect Disabled\"}\n\t\t\t</LoadingButton>\n\t\t\t<Dialog\n\t\t\t\topen={Boolean(quickConnectCode)}\n\t\t\t\tonClose={handleQuickConnectClose}\n\t\t\t\tfullWidth\n\t\t\t\tmaxWidth=\"xs\"\n\t\t\t\tTransitionComponent={Transition}\n\t\t\t\tPaperProps={{ className: \"glass\", sx: { borderRadius: \"24px\" } }}\n\t\t\t>\n\t\t\t\t<DialogContent\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tvariant=\"h5\"\n\t\t\t\t\t\tmb={2}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\tQuick Connect Code:\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<DialogContentText>\n\t\t\t\t\t\tUse this code in the Quick Connect tab in your server to login.\n\t\t\t\t\t</DialogContentText>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"flex flex-row\"\n\t\t\t\t\t\tstyle={{ gap: \"1em\", alignItems: \"center\", marginTop: \"1em\" }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Tooltip title=\"Click to copy\" arrow placement=\"top\">\n\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\tvariant=\"h3\"\n\t\t\t\t\t\t\t\tcolor=\"textPrimary\"\n\t\t\t\t\t\t\t\ttextAlign=\"center\"\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\tbackground: \"rgb(255 255 255 / 0.1)\",\n\t\t\t\t\t\t\t\t\twidth: \"fit-content\",\n\t\t\t\t\t\t\t\t\tpadding: \"0.4em\",\n\t\t\t\t\t\t\t\t\tborderRadius: \"10px\",\n\t\t\t\t\t\t\t\t\tcursor: \"pointer\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tonClick={async () => {\n\t\t\t\t\t\t\t\t\tquickConnectCode && (await writeText(quickConnectCode));\n\t\t\t\t\t\t\t\t\tenqueueSnackbar(\"Quick Connect Code copied!\", {\n\t\t\t\t\t\t\t\t\t\tvariant: \"info\",\n\t\t\t\t\t\t\t\t\t\tkey: \"copiedText\",\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{quickConnectCode}\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t</div>\n\t\t\t\t</DialogContent>\n\t\t\t\t<DialogActions\n\t\t\t\t\tclassName=\"flex flex-row\"\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tpadding: \"0em 1em 1em 1em\",\n\t\t\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<FormControlLabel\n\t\t\t\t\t\tcontrol={\n\t\t\t\t\t\t\t<Checkbox\n\t\t\t\t\t\t\t\tchecked={rememberUser}\n\t\t\t\t\t\t\t\tonChange={(e) => setRememberUser(e.target.checked)}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlabel=\"Remember device\"\n\t\t\t\t\t/>\n\t\t\t\t\t<Button variant=\"contained\" onClick={handleQuickConnectClose}>\n\t\t\t\t\t\tClose\n\t\t\t\t\t</Button>\n\t\t\t\t</DialogActions>\n\t\t\t</Dialog>\n\t\t</div>\n\t);\n};\n\nexport default QuickConnectButton;"
  },
  {
    "path": "src/components/buttons/trailerButton.tsx",
    "content": "import type { MediaUrl } from \"@jellyfin/sdk/lib/generated-client\";\nimport { Dialog, IconButton } from \"@mui/material\";\nimport React, { useState } from \"react\";\nimport ReactPlayer from \"react-player\";\n\ntype TrailerButtonType = {\n\ttrailerItem: MediaUrl[];\n\tdisabled: boolean;\n};\n\nconst TrailerButton = (props: TrailerButtonType) => {\n\tconst [dialog, setDialog] = useState(false);\n\treturn (\n\t\t<div>\n\t\t\t<IconButton\n\t\t\t\tdisabled={props.disabled ?? false}\n\t\t\t\tonClick={() => setDialog(true)}\n\t\t\t>\n\t\t\t\t<span className=\"material-symbols-rounded\">theaters</span>\n\t\t\t</IconButton>\n\t\t\t<Dialog\n\t\t\t\tmaxWidth=\"md\"\n\t\t\t\tfullWidth\n\t\t\t\topen={dialog}\n\t\t\t\tPaperProps={{\n\t\t\t\t\tclassName: \"glass\",\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t},\n\t\t\t\t}}\n\t\t\t\tonClose={() => setDialog(false)}\n\t\t\t>\n\t\t\t\t{props.trailerItem[0]?.Url && (\n\t\t\t\t\t<ReactPlayer\n\t\t\t\t\t\tplaying={false}\n\t\t\t\t\t\turl={props.trailerItem[0]?.Url}\n\t\t\t\t\t\twidth=\"100%\"\n\t\t\t\t\t\theight=\"auto\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\taspectRatio: \"16/9\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tcontrols\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</Dialog>\n\t\t</div>\n\t);\n};\n\nexport default TrailerButton;\n"
  },
  {
    "path": "src/components/card/card.scss",
    "content": ".card {\n\theight: 100%;\n\toverflow: visible !important;\n\talign-items: flex-start;\n\tbackground: transparent !important;\n\tmargin-right: 1.5em;\n\tmargin-bottom: .8em;\n\t&-box {\n\t\tdisplay: flex;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tposition: relative;\n\t\toverflow: visible;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t}\n\t&-image {\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tobject-fit: cover;\n\t\ttransition: all $transition-time-fast;\n\t\tposition: absolute;\n\t\tz-index: 1;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\t&-container {\n\t\t\tposition: relative;\n\t\t\twidth: 100%;\n\t\t\tbox-shadow: 0 4px 8px rgb(0 0 0 / 0.2);\n\t\t\tborder-radius: $border-radius-default;\n\t\t\toverflow: hidden;\n\t\t\theight: auto;\n\t\t\tz-index: 0;\n\n\t\t\t&.thumb{\n\t\t\t\taspect-ratio: 1.777777;\n\t\t\t}\n\t\t\t&.portrait{\n\t\t\t\taspect-ratio: 0.666666;\n\t\t\t}\n\t\t\t&.square {\n\t\t\t\taspect-ratio: 1;\n\t\t\t}\n\t\t}\n\n\t\t&-icon {\n\t\t\tfont-size: 5.4em !important;\n\t\t\t&-container {\n\t\t\t\tbackground: linear-gradient(45deg, rgb(255 255 255 / 0.05), rgb(255 255 255 / 0.15));\n\t\t\t\theight: 100%;\n\t\t\t\twidth: 100%;\n\t\t\t\tposition: absolute;\n\t\t\t\tz-index: 0;\n\t\t\t\ttop: 0;\n\t\t\t\tleft: 0;\n\t\t\t\ttransition: filter $transition-time-default;\n\t\t\t\t.material-symbols-rounded {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 50%;\n\t\t\t\t\tleft: 50%;\n\t\t\t\t\ttransform: translate(-50%, -50%);\n\t\t\t\t\tcolor: rgb(255 255 255 / 0.5);\n\t\t\t\t\tfont-size: 4em !important;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t&-blurhash {\n\t\t\tposition: absolute;\n\t\t\tz-index: 1;\n\t\t\ttop: 0;\n\t\t\tleft: 0;\n\t\t\twidth: 100% !important;\n\t\t\theight: 100% !important;\n\t\t\toverflow: hidden;\n\t\t}\n\t}\n\t&-overlay {\n\t\tposition: absolute;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\tz-index: 10;\n\t\topacity: 0;\n\t\ttransition: all $transition-time-default;\n\t\tdisplay: flex;\n\t\talign-items: flex-end;\n\t\tjustify-content: flex-end;\n\t\tpadding: 0.5em;\n\t\tbackground: rgba(20, 20, 30, 0.6);\n\t\tbackdrop-filter: blur(12px);\n\t}\n\t&:hover,\n\t&:focus,\n\t&:focus-within {\n\t\t.card-overlay {\n\t\t\topacity: 1;\n\t\t}\n\t}\n\t&:hover {\n\t\tcursor: pointer;\n\t}\n\n\t&:focus {\n\t\toutline: none;\n\t}\n\t&-play-button {\n\t\tposition: absolute !important;\n\t\ttop: 50%;\n\t\tleft: 50%;\n\t\ttransform: translate(-50%, -50%);\n\t\tz-index: 1;\n\t}\n\t&-indicator {\n\t\tposition: absolute;\n\t\ttop: 0.4em;\n\t\tright: 0.4em;\n\t\tz-index: 2;\n\t\tpadding: 0.2em 0.75em;\n\t\tbackground: rgb(20 20 20 / 0.5);\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tbackdrop-filter: blur(5px);\n\t\ttransition: opacity 250ms;\n\t\tborder-radius: 100px;\n\t\tbox-shadow: 0 0 5px rgb(0 0 0 / 0.2);\n\t}\n\t&-episode {\n\t\t.card-box {\n\t\t\theight: unset !important;\n\t\t}\n\t}\n\n\t&-actor {\n\t\ttransition: all $transition-time-default;\n\t\tborder-radius: 10px;\n\t\t// padding: 1em !important;\n\t\t&:hover {\n\t\t\tbackground: rgb(255 255 255 / 0.1) !important;\n\t\t}\n\t\t.card-image-container {\n\t\t\tborder-radius: 100% !important;\n\t\t}\n\t}\n\n\t&-progress{\n\t\t// width: attr('data-progress');\n\t\theight: 100%;\n\t\tbackground: white;\n\t\ttransition: width $transition-time-default ease-in-out;\n\t\t&-container{\n\t\t\t$padding: 12px;\n\t\t\tposition: absolute;\n\t\t\tbottom: $padding;\n\t\t\tleft: $padding;\n\t\t\tright: $padding;\n\t\t\theight: 4.5px;\n\t\t\tbackground: rgb(255 255 255 / 0.5);\n\t\t\tbackdrop-filter: blur(10px);\n\t\t\tbox-shadow: 0 0 10px rgb(20 20 20 / 0.5);\n\t\t\tz-index: 1;\n\t\t\toverflow: hidden;\n\t\t\tbox-sizing: content-box;\n\t\t\tborder-radius: 10px;\n\t\t}\n\t}\n\n\t&-text-container {\n\t\tdisplay: flex !important;\n\t\tflex-direction: column;\n\t}\n}\n"
  },
  {
    "path": "src/components/card/card.tsx",
    "content": "/** @format */\n\nimport {\n\ttype BaseItemDto,\n\tBaseItemKind,\n\ttype ImageType,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { useNavigate } from \"@tanstack/react-router\";\nimport React, { memo, type Ref, useCallback, useState } from \"react\";\nimport { ErrorBoundary } from \"react-error-boundary\";\nimport { useInView } from \"react-intersection-observer\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport LikeButton from \"../buttons/likeButton\";\nimport MarkPlayedButton from \"../buttons/markPlayedButton\";\nimport PlayButton from \"../buttons/playButton\";\nimport { getTypeIcon } from \"../utils/iconsCollection\";\nimport \"./card.scss\";\n\ninterface CardProps {\n\titem: BaseItemDto;\n\tcardTitle: string | undefined | null;\n\tcardCaption?: string | null | number;\n\timageType?: ImageType;\n\tcardType: \"square\" | \"thumb\" | \"portrait\";\n\tqueryKey?: string[];\n\tuserId?: string;\n\tseriesId?: string | null;\n\thideText?: boolean;\n\tonClick?: () => void;\n\tdisableOverlay?: boolean;\n\toverrideIcon?: any;\n\tskipInView?: boolean;\n}\n\nconst CardContent = ({\n\titem,\n\tcardTitle,\n\tcardCaption,\n\timageType = \"Primary\",\n\tcardType = \"square\",\n\tqueryKey,\n\tuserId,\n\tseriesId,\n\thideText = false,\n\tonClick,\n\tdisableOverlay = false,\n\toverrideIcon,\n\tinView,\n\tforwardedRef,\n}: CardProps & {\n\tinView: boolean;\n\tforwardedRef?: Ref<HTMLDivElement>;\n}) => {\n\tconst api = useApiInContext((s) => s.api);\n\tconst navigate = useNavigate();\n\n\tconst defaultOnClick = useCallback(() => {\n\t\tif (item?.Id) {\n\t\t\tswitch (item?.Type) {\n\t\t\t\tcase BaseItemKind.BoxSet:\n\t\t\t\t\tnavigate({ to: \"/boxset/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tcase BaseItemKind.Episode:\n\t\t\t\t\tnavigate({ to: \"/episode/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tcase BaseItemKind.MusicAlbum:\n\t\t\t\t\tnavigate({ to: \"/album/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tcase BaseItemKind.MusicArtist:\n\t\t\t\t\tnavigate({ to: \"/artist/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tcase BaseItemKind.Person:\n\t\t\t\t\tnavigate({ to: \"/person/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tcase BaseItemKind.Series:\n\t\t\t\t\tnavigate({ to: \"/series/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tcase BaseItemKind.Playlist:\n\t\t\t\t\tnavigate({ to: \"/playlist/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tnavigate({ to: \"/item/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}, [item?.Id, item?.Type, navigate]);\n\n\treturn (\n\t\t<div\n\t\t\tclassName=\"card\"\n\t\t\tref={forwardedRef}\n\t\t\tonClick={onClick || defaultOnClick}\n\t\t>\n\t\t\t<div className={`card-image-container ${cardType}`}>\n\t\t\t\t<ErrorBoundary fallback={<></>}>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"card-indicator check\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\topacity: item.UserData?.Played ? 1 : 0,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div className=\"material-symbols-rounded\">done</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName={\"card-indicator text\"}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\topacity: item.UserData?.UnplayedItemCount ? 1 : 0,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tpadding: \"0.1em 0.4em\",\n\t\t\t\t\t\t\t\tfontWeight: 400,\n\t\t\t\t\t\t\t\tfontSize: \"0.875rem\",\n\t\t\t\t\t\t\t\tlineHeight: 1.57,\n\t\t\t\t\t\t\t\tfontFamily: \"inherit\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.UserData?.UnplayedItemCount}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t</ErrorBoundary>\n\t\t\t\t<div className=\"card-image-icon-container\">\n\t\t\t\t\t{overrideIcon\n\t\t\t\t\t\t? getTypeIcon(overrideIcon)\n\t\t\t\t\t\t: getTypeIcon(item.Type ?? \"universal\")}\n\t\t\t\t</div>\n\t\t\t\t{inView && (\n\t\t\t\t\t<img\n\t\t\t\t\t\talt={item.Name ?? \"blink\"}\n\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\tapi\n\t\t\t\t\t\t\t\t? overrideIcon === \"User\"\n\t\t\t\t\t\t\t\t\t? `${api?.basePath}/Users/${item.Id}/Images/Primary`\n\t\t\t\t\t\t\t\t\t: getImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t(seriesId ? item.SeriesId : (item.AlbumId ?? item.Id)) ??\n\t\t\t\t\t\t\t\t\t\t\t\t\"\",\n\t\t\t\t\t\t\t\t\t\t\timageType,\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\tquality: 90,\n\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: cardType === \"thumb\" ? 560 : 320,\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t: \"\"\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\tdisplay: \"block\",\n\t\t\t\t\t\t\ttransition: \"opacity 0.3s ease-in-out\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tloading=\"lazy\"\n\t\t\t\t\t\tonLoad={(e) => {\n\t\t\t\t\t\t\te.currentTarget.style.opacity = \"1\";\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"card-image\"\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t\t{inView && !disableOverlay && (\n\t\t\t\t\t<div className=\"card-overlay\">\n\t\t\t\t\t\t<PlayButton\n\t\t\t\t\t\t\titem={item}\n\t\t\t\t\t\t\tuserId={userId}\n\t\t\t\t\t\t\titemType={item.Type ?? \"Movie\"}\n\t\t\t\t\t\t\tclassName=\"card-play-button\"\n\t\t\t\t\t\t\ticonOnly\n\t\t\t\t\t\t\taudio={\n\t\t\t\t\t\t\t\titem.Type === BaseItemKind.MusicAlbum ||\n\t\t\t\t\t\t\t\titem.Type === BaseItemKind.Audio ||\n\t\t\t\t\t\t\t\titem.Type === BaseItemKind.AudioBook ||\n\t\t\t\t\t\t\t\titem.Type === BaseItemKind.Playlist\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tplaylistItem={item.Type === BaseItemKind.Playlist}\n\t\t\t\t\t\t\tplaylistItemId={item.Id}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\titemId={item.Id}\n\t\t\t\t\t\t\titemName={item.Name ?? \"\"}\n\t\t\t\t\t\t\tisFavorite={item.UserData?.IsFavorite}\n\t\t\t\t\t\t\tqueryKey={queryKey}\n\t\t\t\t\t\t\tuserId={userId}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<MarkPlayedButton\n\t\t\t\t\t\t\titemId={item.Id}\n\t\t\t\t\t\t\titemName={item.Name ?? \"\"}\n\t\t\t\t\t\t\tisPlayed={item.UserData?.Played}\n\t\t\t\t\t\t\tqueryKey={queryKey}\n\t\t\t\t\t\t\tuserId={userId}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t\t{(item.UserData?.PlaybackPositionTicks ?? -1) > 0 && (\n\t\t\t\t\t<div className=\"card-progress-container\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"card-progress\"\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\twidth: `${item.UserData?.PlayedPercentage}%`,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t\t<div\n\t\t\t\tclassName=\"card-text-container\"\n\t\t\t\tstyle={{ display: hideText ? \"none\" : \"block\", marginLeft: \"0.2em\" }}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tmarginTop: \"8px\",\n\t\t\t\t\t\topacity: 0.9,\n\t\t\t\t\t\tfontSize: \"0.875rem\",\n\t\t\t\t\t\tfontWeight: 500,\n\t\t\t\t\t\twhiteSpace: \"nowrap\",\n\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t\ttextOverflow: \"ellipsis\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{cardTitle}\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\topacity: 0.6,\n\t\t\t\t\t\tfontSize: \"0.75rem\",\n\t\t\t\t\t\twhiteSpace: \"nowrap\",\n\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t\ttextOverflow: \"ellipsis\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{cardCaption}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n};\n\nconst CardWithInView = (props: CardProps) => {\n\tconst { ref, inView } = useInView({\n\t\tthreshold: 0.1,\n\t});\n\n\treturn <CardContent {...props} inView={inView} forwardedRef={ref} />;\n};\n\nconst CardComponent = (props: CardProps) => {\n\tif (props.skipInView) {\n\t\treturn <CardContent {...props} inView={true} />;\n\t}\n\treturn <CardWithInView {...props} />;\n};\n\nexport const Card = memo(CardComponent);"
  },
  {
    "path": "src/components/cardScroller/cardScroller.scss",
    "content": "@use \"@/styles/variables.scss\" as *;\n\n.card-scroller-container {\n    margin-bottom: 2.5em;\n    position: relative;\n    \n    // Ensure the container handles overflow if needed, but usually Carousel handles it\n}\n\n.card-scroller {\n    // Override carousel default outline\n    &:focus {\n        outline: none;\n    }\n\n\t&-slide {\n\t\tpadding-right: 0.5em; // Gap between cards (handled by slide padding instead of gap for carousel)\n        // With react-multi-carousel, better to rely on their gap logic or item class\n\t}\n    \n    &-item {\n        padding-right: 0.6em; // Consistent gap\n    }\n\n\t&-header-container {\n\t\tdisplay: flex;\n\t\twidth: 100%;\n\t\tjustify-content: space-between;\n\t\talign-items: center;\n        margin-bottom: 1em;\n        min-height: 40px;\n\t}\n\n\t&-heading {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: flex-start;\n\t\tgap: 0.5em;\n\t\tposition: relative;\n        font-weight: 600 !important;\n        \n\t\t&-decoration {\n\t\t\twidth: 4px;\n\t\t\theight: 24px;\n\t\t\tbackground: $clr-accent-default;\n            border-radius: 4px;\n\t\t}\n\t}\n\n    &-controls {\n        display: flex;\n        gap: 0.5em;\n\n        .scroller-nav-btn {\n            background-color: rgba(255, 255, 255, 0.05);\n            border: 1px solid rgba(255, 255, 255, 0.05);\n            color: rgba(255, 255, 255, 0.7);\n            transition: all 0.2s ease;\n            width: 36px;\n            height: 36px;\n\n            &:hover {\n                background-color: rgba(255, 255, 255, 0.1);\n                color: #fff;\n                border-color: rgba(255, 255, 255, 0.2);\n            }\n\n            // Disabled state\n            &.Mui-disabled {\n                background-color: transparent;\n                border-color: transparent;\n                color: rgba(255, 255, 255, 0.1);\n            }\n\n            .material-symbols-rounded {\n                font-size: 1.5em;\n            }\n        }\n    }\n}\n\n.hidden-decoration {\n\t.card-scroller-heading-decoration {\n\t\tdisplay: none;\n\t}\n}\n"
  },
  {
    "path": "src/components/cardScroller/cardScroller.tsx",
    "content": "import React, { type ReactNode, useRef, useState } from \"react\";\nimport Carousel from \"react-multi-carousel\";\nimport \"react-multi-carousel/lib/styles.css\";\n\nimport IconButton from \"@mui/material/IconButton\";\nimport Typography from \"@mui/material/Typography\";\n\nimport \"./cardScroller.scss\";\n\n// Custom Header + Button Group component\nconst ScrollerHeader = ({\n\tonNext,\n\tonPrevious,\n\ttitle,\n\theadingProps,\n\tdisableDecoration,\n\tcurrentSlide = 0,\n\tisNextDisabled = false,\n}: any) => {\n\t// Basic disable logic for visual feedback\n\tconst isFirst = currentSlide === 0;\n\n\treturn (\n\t\t<div\n\t\t\tclassName={`card-scroller-header-container ${disableDecoration ? \"hidden-decoration\" : \"\"}`}\n\t\t>\n\t\t\t<Typography\n\t\t\t\tvariant=\"h5\"\n\t\t\t\tcolor=\"textPrimary\"\n\t\t\t\tclassName=\"card-scroller-heading\"\n\t\t\t\t{...headingProps}\n\t\t\t>\n\t\t\t\t<div className=\"card-scroller-heading-decoration\" />\n\t\t\t\t{title}\n\t\t\t</Typography>\n\n\t\t\t<div className=\"card-scroller-controls\">\n\t\t\t\t<IconButton\n\t\t\t\t\tonClick={onPrevious}\n\t\t\t\t\tdisabled={isFirst}\n\t\t\t\t\tsize=\"small\"\n\t\t\t\t\tclassName=\"scroller-nav-btn\"\n\t\t\t\t>\n\t\t\t\t\t<span className=\"material-symbols-rounded\">chevron_left</span>\n\t\t\t\t</IconButton>\n\t\t\t\t<IconButton\n\t\t\t\t\tonClick={onNext}\n\t\t\t\t\tdisabled={isNextDisabled}\n\t\t\t\t\tsize=\"small\"\n\t\t\t\t\tclassName=\"scroller-nav-btn\"\n\t\t\t\t>\n\t\t\t\t\t<span className=\"material-symbols-rounded\">chevron_right</span>\n\t\t\t\t</IconButton>\n\t\t\t</div>\n\t\t</div>\n\t);\n};\n\ntype CardScrollerProps = {\n\tchildren: ReactNode;\n\tdisplayCards: number;\n\ttitle: string;\n\theadingProps?: object;\n\tdisableDecoration?: boolean;\n\tboxProps?: object;\n};\n\nexport default function CardScroller({\n\tchildren,\n\tdisplayCards,\n\ttitle,\n\theadingProps,\n\tdisableDecoration = false,\n\tboxProps,\n}: CardScrollerProps) {\n\tconst carouselRef = useRef<any>(null);\n\tconst [currentSlide, setCurrentSlide] = useState(0);\n\tconst [slidesToShow, setSlidesToShow] = useState(displayCards);\n\tconst totalItems = React.Children.count(children);\n\n\tconst responsive = {\n\t\tsuperLargeDesktop: {\n\t\t\tbreakpoint: { max: 4000, min: 3000 },\n\t\t\titems: displayCards + 1,\n\t\t\tslidesToSlide: displayCards + 1,\n\t\t\tpartialVisibilityGutter: 40,\n\t\t},\n\t\tdesktop: {\n\t\t\tbreakpoint: { max: 3000, min: 925 },\n\t\t\titems: displayCards,\n\t\t\tslidesToSlide: displayCards,\n\t\t\tpartialVisibilityGutter: 30,\n\t\t},\n\t\ttablet: {\n\t\t\tbreakpoint: { max: 925, min: 600 },\n\t\t\titems: displayCards - 3,\n\t\t\tslidesToSlide: displayCards - 3,\n\t\t\tpartialVisibilityGutter: 20,\n\t\t},\n\t\tmobile: {\n\t\t\tbreakpoint: { max: 600, min: 424 },\n\t\t\titems: displayCards - 5,\n\t\t\tslidesToSlide: displayCards - 5,\n\t\t\tpartialVisibilityGutter: 10,\n\t\t},\n\t\tsmallScreen: {\n\t\t\tbreakpoint: { max: 424, min: 0 },\n\t\t\titems: 1,\n\t\t\tslidesToSlide: 1,\n\t\t\tpartialVisibilityGutter: 10,\n\t\t},\n\t};\n\n\tconst handleNext = () => {\n\t\tif (carouselRef.current) {\n\t\t\tcarouselRef.current.next();\n\t\t}\n\t};\n\n\tconst handlePrevious = () => {\n\t\tif (carouselRef.current) {\n\t\t\tcarouselRef.current.previous();\n\t\t}\n\t};\n\n\treturn (\n\t\t<div {...boxProps} className=\"card-scroller-container\">\n\t\t\t<ScrollerHeader\n\t\t\t\ttitle={title}\n\t\t\t\theadingProps={headingProps}\n\t\t\t\tdisableDecoration={disableDecoration}\n\t\t\t\tonNext={handleNext}\n\t\t\t\tonPrevious={handlePrevious}\n\t\t\t\tcurrentSlide={currentSlide}\n\t\t\t\tisNextDisabled={currentSlide + slidesToShow >= totalItems}\n\t\t\t/>\n\t\t\t<Carousel\n\t\t\t\tref={carouselRef}\n\t\t\t\tswipeable\n\t\t\t\tdraggable\n\t\t\t\tresponsive={responsive}\n\t\t\t\tarrows={false}\n\t\t\t\tclassName=\"card-scroller\"\n\t\t\t\tcustomTransition=\"transform 400ms ease-in-out\"\n\t\t\t\ttransitionDuration={400}\n\t\t\t\tcontainerClass=\"card-scroller-track\"\n\t\t\t\titemClass=\"card-scroller-item\"\n\t\t\t\tbeforeChange={(nextSlide, state) => {\n\t\t\t\t\tsetCurrentSlide(nextSlide);\n\t\t\t\t\tsetSlidesToShow(state.slidesToShow);\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</Carousel>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/components/carousel/carousel.scss",
    "content": ".carousel {\n\tposition: relative;\n\toverflow: visible;\n\theight: 65vh;\n\tmargin-bottom: 3em;\n\tdisplay: grid;\n\tgrid-template-areas: \"main sidebar\";\n\tgrid-template-columns: 1fr 340px;\n\tgap: 1.5em;\n\n\t&-sidebar {\n\t\toverflow-y: auto;\n\t\theight: 100%;\n\t\tposition: relative;\n\t\t@include glass-effect($bg-color: rgba(10, 10, 14, 0.4));\n\t\tborder-radius: 24px;\n\t\tgrid-area: sidebar;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tpadding: 12px;\n\t\tgap: 8px;\n\t\t\n\t\t// Ensure programmatic scroll respects padding\n\t\tscroll-padding-block: 24px;\n\n\t\t// Hide scrollbar but keep functionality\n\t\tscrollbar-width: none; \n\t\t-ms-overflow-style: none; \n\t\t&::-webkit-scrollbar {\n\t\t\twidth: 0;\n\t\t\theight: 0;\n\t\t\tdisplay: none; \n\t\t}\n\t}\n\t\n\t&-button {\n\t\tposition: absolute !important;\n\t\ttop: 50%;\n\t\ttransform: translateY(-50%);\n\t\tz-index: 100;\n\t\t\n\t\t&.right {\n\t\t\tright: 0.5em;\n\t\t}\n\t\t\n\t\t&.left {\n\t\t\tleft: 0.5em;\n\t\t}\n\t}\n\t\n\t&-ticker{\n\t\toverflow: hidden;\n\t\theight: 72px;\n\t\tdisplay: flex;\n\t\tpadding: 8px;\n\t\talign-items: center;\n\t\tgap: 12px;\n\t\ttransition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n\t\tposition: relative;\n\t\tborder-radius: 12px; // Slightly smaller radius to work with the bar\n\t\tbackground: transparent;\n\t\tborder: 1px solid transparent;\n\n\t\t&:hover {\n\t\t\tbackground: rgba(255, 255, 255, 0.05);\n\t\t\tcursor: pointer;\n\t\t}\n\n\t\t&.active {\n\t\t\tbackground: rgba(255, 255, 255, 0.08); // Subtle background\n\t\t\t\n\t\t\t// Vertical Progress Bar\n\t\t\t&::after {\n\t\t\t\tcontent: '';\n\t\t\t\tposition: absolute;\n\t\t\t\tleft: 0;\n\t\t\t\tbottom: 0;\n\t\t\t\twidth: 4px;\n\t\t\t\tbackground-color: var(--mui-palette-primary-main, #fff);\n\t\t\t\theight: 0%;\n\t\t\t\tanimation: ticker-v-progress 8s linear;\n\t\t\t}\n\t\t}\n\n\t\t&-image {\n\t\t\theight: 56px;\n\t\t\twidth: 100px;\n\t\t\tflex-shrink: 0;\n\t\t\tborder-radius: 8px;\n\t\t\tobject-fit: cover;\n\t\t\tbox-shadow: 0 2px 8px rgba(0,0,0,0.2);\n\n\t\t\t&.placeholder {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\tbackground: rgb(100 100 100 / 0.1);\n\t\t\t\tcolor: rgb(200 200 200 / 0.7);\n\t\t\t\tfont-size: 2rem;\n\t\t\t}\n\t\t}\n\t\t\n\t\t& > div {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tjustify-content: center;\n\t\t\tmin-width: 0; // flexible text truncation\n\t\t\tpadding-left: 4px; // compensate for visual weight of potential bar\n\t\t\t\n\t\t\t.carousel-ticker-title {\n\t\t\t\twhite-space: nowrap;\n\t\t\t\toverflow: hidden;\n\t\t\t\ttext-overflow: ellipsis;\n\t\t\t\tfont-weight: 500; // Slightly lighter\n\t\t\t\tfont-size: 0.95rem;\n\t\t\t\tline-height: 1.2;\n\t\t\t\tmargin-bottom: 2px;\n\t\t\t\topacity: 0.9;\n\t\t\t}\n\t\t\t.carousel-ticker-year {\n\t\t\t\tfont-size: 0.8rem;\n\t\t\t\topacity: 0.6;\n\t\t\t}\n\t\t}\n\t}\n\t\n\t// Pause progress when carousel is paused\n\t&.paused {\n\t\t.carousel-ticker.active::after {\n\t\t\tanimation-play-state: paused;\n\t\t}\n\t}\n\n\t@keyframes ticker-v-progress {\n\t\tfrom { height: 0%; }\n\t\tto { height: 100%; }\n\t}\n\t\n\t&-indicator {\n\t\taspect-ratio: 1.77777;\n\t\toverflow: hidden;\n\t\tborder-radius: 2px;\n\t\tpadding: 2px;\n\t\ttransition: opacity $transition-time-fast;\n\t\topacity: 0.5;\n\t\twidth: auto;\n\t\t&:hover {\n\t\t\tcursor: pointer;\n\t\t\topacity: 0.8;\n\t\t}\n\t\t\n\t\t&-container {\n\t\t\tdisplay: flex;\n\t\t\tgap: 1em;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t}\n\t\t&-image {\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\tobject-fit: cover;\n\t\t\tborder-radius: 4px;\n\t\t}\n\t\t\n\t\t&-icon {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\tbackground: rgb(155 155 155 / 0.4);\n\t\t\tborder-radius: 4px;\n\t\t}\n\t\t&.active {\n\t\t\topacity: 1;\n\t\t\t\n\t\t\t.material-symbols-rounded {\n\t\t\t\t\n\t\t\t\t--fill: 1;\n\t\t\t}\n\t\t}\n\t}\n}\n.carousel-ticker-wrapper {\nscroll-margin-block: 24px;\n}\n\n"
  },
  {
    "path": "src/components/carousel/index.tsx",
    "content": "import { AnimatePresence } from \"motion/react\";\nimport React, { useCallback, useEffect, useState } from \"react\";\n\nimport { useCarouselStore } from \"../../utils/store/carousel\";\nimport \"./carousel.scss\";\nimport {\n\ttype BaseItemDto,\n\tBaseItemKind,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport CarouselSlide from \"../carouselSlide\";\nimport CarouselTickers from \"./tickers\";\n\n// Memoize CarouselSlide to prevent unnecessary re-renders\nconst MemoizedCarouselSlide = React.memo(CarouselSlide);\n\nconst Carousel = ({\n\tcontent,\n\tonChange,\n}: {\n\tcontent: BaseItemDto[];\n\tonChange: (currentSlide: number) => void;\n}) => {\n\tconst [currentSlide, setCurrentSlide] = useState(0);\n\tconst [isPaused, setIsPaused] = useState(false);\n\tconst sidebarRef = React.useRef<HTMLDivElement>(null);\n\tconst tickerRefs = React.useRef<(HTMLDivElement | null)[]>([]);\n\tconst currentSlideRef = React.useRef(currentSlide);\n\n\tconst [setDirection] = useCarouselStore((state) => [state.setDirection]);\n\n\tuseEffect(() => {\n\t\tcurrentSlideRef.current = currentSlide;\n\t}, [currentSlide]);\n\n\tuseEffect(() => {\n\t\tonChange(currentSlide);\n\n\t\t// Auto-scroll sidebar to keep active item in view\n\t\tconst activeTicker = tickerRefs.current[currentSlide];\n\t\tconst sidebar = sidebarRef.current;\n\n\t\tif (activeTicker && sidebar) {\n\t\t\tconst sidebarTop = sidebar.scrollTop;\n\t\t\tconst sidebarBottom = sidebarTop + sidebar.clientHeight;\n\t\t\tconst activeTickerTop = activeTicker.offsetTop;\n\t\t\tconst activeTickerBottom =\n\t\t\t\tactiveTicker.offsetTop + activeTicker.offsetHeight;\n\n\t\t\t// If element is below the visible area\n\t\t\tif (activeTickerBottom > sidebarBottom) {\n\t\t\t\tsidebar.scrollTo({\n\t\t\t\t\ttop: activeTickerBottom - sidebar.clientHeight + 24, // 24px padding\n\t\t\t\t\tbehavior: \"smooth\",\n\t\t\t\t});\n\t\t\t}\n\t\t\t// If element is above the visible area\n\t\t\telse if (activeTickerTop < sidebarTop) {\n\t\t\t\tsidebar.scrollTo({\n\t\t\t\t\ttop: activeTickerTop - 24, // 24px padding\n\t\t\t\t\tbehavior: \"smooth\",\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}, [currentSlide, content, onChange]);\n\n\t// Autoplay functionality\n\tuseEffect(() => {\n\t\tif (isPaused || content.length === 0) return;\n\n\t\tconst timer = setInterval(() => {\n\t\t\tsetDirection(\"right\");\n\t\t\tsetCurrentSlide((prev) => (prev + 1) % content.length);\n\t\t}, 8000);\n\n\t\treturn () => clearInterval(timer);\n\t}, [isPaused, content.length, setDirection, currentSlide]);\n\n\tconst api = useApiInContext((s) => s.api);\n\n\tconst handleTickerClick = useCallback(\n\t\t(index: number) => {\n\t\t\tif (index === currentSlideRef.current) return;\n\t\t\tsetDirection(index > currentSlideRef.current ? \"right\" : \"left\");\n\t\t\tsetCurrentSlide(index);\n\t\t},\n\t\t[setDirection],\n\t);\n\n\tif (!api) return null;\n\n\treturn (\n\t\t<div\n\t\t\tclassName={`carousel ${isPaused ? \"paused\" : \"\"}`}\n\t\t\tonMouseEnter={() => setIsPaused(true)}\n\t\t\tonMouseLeave={() => setIsPaused(false)}\n\t\t>\n\t\t\t<AnimatePresence mode=\"sync\">\n\t\t\t\t<MemoizedCarouselSlide\n\t\t\t\t\titem={content[currentSlide]}\n\t\t\t\t\tkey={content[currentSlide].Id}\n\t\t\t\t/>\n\t\t\t</AnimatePresence>\n\t\t\t<div className=\"carousel-sidebar\" ref={sidebarRef}>\n\t\t\t\t{content.map((item, index) => (\n\t\t\t\t\t<div\n\t\t\t\t\t\tkey={item.Id}\n\t\t\t\t\t\tref={(el) => {\n\t\t\t\t\t\t\ttickerRefs.current[index] = el;\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tclassName=\"carousel-ticker-wrapper\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<CarouselTickers\n\t\t\t\t\t\t\timageUrl={\n\t\t\t\t\t\t\t\titem.ImageTags?.Thumb\n\t\t\t\t\t\t\t\t\t? getImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\titem.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"Thumb\",\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\tquality: 90,\n\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 360,\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tisActive={index === currentSlide}\n\t\t\t\t\t\t\titemName={item.Name ?? \"Unknown\"}\n\t\t\t\t\t\t\titemYear={\n\t\t\t\t\t\t\t\titem.Type === BaseItemKind.Series && item.EndDate\n\t\t\t\t\t\t\t\t\t? `${item.ProductionYear ?? \"\"} - ${new Date(item.EndDate).getFullYear().toString()}`\n\t\t\t\t\t\t\t\t\t: (item.ProductionYear?.toString() ?? \"\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tonTickerClick={handleTickerClick}\n\t\t\t\t\t\t\tindex={index}\n\t\t\t\t\t\t\titemType={item.Type as BaseItemKind}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t))}\n\t\t\t</div>\n\t\t</div>\n\t);\n};\n\nexport default React.memo(Carousel);\n"
  },
  {
    "path": "src/components/carousel/tickers.tsx",
    "content": "import type { BaseItemKind } from \"@jellyfin/sdk/lib/generated-client\";\nimport { Typography } from \"@mui/material\";\nimport React from \"react\";\nimport { getTypeIcon } from \"../utils/iconsCollection\";\n\ntype CarouselTickersProps = {\n\timageUrl?: string | undefined | null;\n\titemName: string;\n\titemYear: string;\n\tonTickerClick: (index: number) => void;\n\tindex: number;\n\tisActive: boolean;\n\titemType: BaseItemKind;\n};\n\nconst CarouselTickers = React.memo(\n\t({\n\t\timageUrl,\n\t\titemName,\n\t\titemYear,\n\t\tonTickerClick,\n\t\tindex,\n\t\tisActive,\n\t\titemType,\n\t}: CarouselTickersProps) => {\n\t\tconst handleClick = React.useCallback(() => {\n\t\t\tonTickerClick(index);\n\t\t}, [index, onTickerClick]);\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tclassName={`carousel-ticker${isActive ? \" active\" : \"\"}`}\n\t\t\t\tonClick={handleClick}\n\t\t\t>\n\t\t\t\t{imageUrl ? (\n\t\t\t\t\t<img\n\t\t\t\t\t\tsrc={imageUrl}\n\t\t\t\t\t\talt={itemName}\n\t\t\t\t\t\tclassName=\"carousel-ticker-image\"\n\t\t\t\t\t/>\n\t\t\t\t) : (\n\t\t\t\t\t<div className=\"carousel-ticker-image placeholder material-symbols-rounded\">\n\t\t\t\t\t\t{\" \"}\n\t\t\t\t\t\t{getTypeIcon(itemType)}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t\t<div>\n\t\t\t\t\t<Typography variant=\"subtitle1\" className=\"carousel-ticker-title\">\n\t\t\t\t\t\t{itemName}\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<Typography variant=\"caption\" className=\"carousel-ticker-year\">\n\t\t\t\t\t\t{itemYear}\n\t\t\t\t\t</Typography>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t);\n\t},\n);\nexport default CarouselTickers;\n"
  },
  {
    "path": "src/components/carouselSlide/index.tsx",
    "content": "import {\n\ttype BaseItemDto,\n\tBaseItemKind,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport Button from \"@mui/material/Button\";\nimport Chip from \"@mui/material/Chip\";\nimport { green, red, yellow } from \"@mui/material/colors\";\nimport Stack from \"@mui/material/Stack\";\nimport Typography from \"@mui/material/Typography\";\nimport { useNavigate } from \"@tanstack/react-router\";\nimport { motion } from \"motion/react\";\nimport React from \"react\";\nimport { endsAt, getRuntime } from \"@/utils/date/time\";\n\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useCarouselStore } from \"@/utils/store/carousel\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport LikeButton from \"../buttons/likeButton\";\nimport MarkPlayedButton from \"../buttons/markPlayedButton\";\nimport PlayButton from \"../buttons/playButton\";\nimport { getTypeIcon } from \"../utils/iconsCollection\";\n\nconst textVariants = {\n\tinitial: (direction: string) => ({\n\t\tx: direction === \"right\" ? 40 : -40,\n\t\topacity: 0,\n\t}),\n\tanimate: {\n\t\tx: 0,\n\t\topacity: 1,\n\t\ttransition: {\n\t\t\tduration: 0.35,\n\t\t\tease: [0.2, 0, 0, 1],\n\t\t},\n\t},\n\texit: (direction: string) => ({\n\t\tx: direction === \"right\" ? -40 : 40,\n\t\topacity: 0,\n\t\ttransition: {\n\t\t\tduration: 0.2,\n\t\t\tease: \"easeInOut\",\n\t\t},\n\t}),\n};\n\nconst CarouselSlide = ({ item }: { item: BaseItemDto }) => {\n\tconst api = useApiInContext((s) => s.api);\n\tconst navigate = useNavigate();\n\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst handleMoreInfo = () => {\n\t\tif (item.Id) {\n\t\t\tswitch (item.Type) {\n\t\t\t\tcase BaseItemKind.BoxSet:\n\t\t\t\t\tnavigate({ to: \"/boxset/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tcase BaseItemKind.Episode:\n\t\t\t\t\tnavigate({ to: \"/episode/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tcase BaseItemKind.MusicAlbum:\n\t\t\t\t\tnavigate({ to: \"/album/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tcase BaseItemKind.MusicArtist:\n\t\t\t\t\tnavigate({ to: \"/artist/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tcase BaseItemKind.Person:\n\t\t\t\t\tnavigate({ to: \"/person/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tcase BaseItemKind.Series:\n\t\t\t\t\tnavigate({ to: \"/series/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tcase BaseItemKind.Playlist:\n\t\t\t\t\tnavigate({ to: \"/playlist/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tnavigate({ to: \"/item/$id\", params: { id: item.Id } });\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t};\n\n\tconst [animationDirection] = useCarouselStore((state) => [state.direction]);\n\n\treturn (\n\t\t<div className=\"hero-carousel-slide\">\n\t\t\t<motion.div\n\t\t\t\tclassName=\"hero-carousel-background-container\"\n\t\t\t\tinitial={{ opacity: 0 }}\n\t\t\t\tanimate={{ opacity: 1 }}\n\t\t\t\texit={{ opacity: 0 }}\n\t\t\t\ttransition={{ duration: 0.4, ease: \"easeInOut\" }}\n\t\t\t>\n\t\t\t\t{item.BackdropImageTags?.length ? (\n\t\t\t\t\t<img\n\t\t\t\t\t\talt={item.Name ?? \"item\"}\n\t\t\t\t\t\tclassName=\"hero-carousel-background-image\"\n\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\titem.ParentBackdropItemId\n\t\t\t\t\t\t\t\t? `${api?.basePath}/Items/${item.ParentBackdropItemId}/Images/Backdrop?quality=80`\n\t\t\t\t\t\t\t\t: `${api?.basePath}/Items/${item.Id}/Images/Backdrop?quality=80&fillHeight=1400`\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonLoad={(e) => {\n\t\t\t\t\t\t\te.currentTarget.style.opacity = \"1\";\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tloading=\"eager\"\n\t\t\t\t\t/>\n\t\t\t\t) : (\n\t\t\t\t\t<div className=\"hero-carousel-background-icon-container\">\n\t\t\t\t\t\t{getTypeIcon(item.Type ?? \"Movie\")}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</motion.div>\n\t\t\t<motion.div\n\t\t\t\tclassName=\"hero-carousel-detail\"\n\t\t\t\tinitial=\"initial\"\n\t\t\t\tanimate=\"animate\"\n\t\t\t\texit=\"exit\"\n\t\t\t\tvariants={{\n\t\t\t\t\tanimate: {\n\t\t\t\t\t\ttransition: {\n\t\t\t\t\t\t\tstaggerChildren: 0.05,\n\t\t\t\t\t\t\tdelayChildren: 0.05,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\texit: {\n\t\t\t\t\t\ttransition: {\n\t\t\t\t\t\t\tstaggerChildren: 0.02,\n\t\t\t\t\t\t\tstaggerDirection: -1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{/* @ts-ignore */}\n\t\t\t\t<Typography\n\t\t\t\t\tcomponent={motion.div}\n\t\t\t\t\tcustom={animationDirection}\n\t\t\t\t\tvariants={textVariants}\n\t\t\t\t\tkey={item.Id}\n\t\t\t\t\tvariant=\"h2\"\n\t\t\t\t\tclassName=\"hero-carousel-text\"\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tmb: \"5px\",\n\t\t\t\t\t}}\n\t\t\t\t\tfontWeight={600}\n\t\t\t\t\toverflow=\"visible\"\n\t\t\t\t>\n\t\t\t\t\t{!item.ImageTags?.Logo ? (\n\t\t\t\t\t\titem.Name\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\talt={item.Name ?? \"Item\"}\n\t\t\t\t\t\t\tclassName=\"hero-carousel-text-logo\"\n\t\t\t\t\t\t\tsrc={`${api?.basePath}/Items/${item.Id}/Images/Logo?quality=90&tag=${item.ImageTags.Logo}`}\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t\ttransition: \"opacity 0.2s\",\n\t\t\t\t\t\t\t\tobjectFit: \"contain\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tonLoad={(e) => {\n\t\t\t\t\t\t\t\te.currentTarget.style.opacity = \"1\";\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t</Typography>\n\t\t\t\t{/* @ts-ignore */}\n\t\t\t\t<Stack\n\t\t\t\t\tcomponent={motion.div}\n\t\t\t\t\tcustom={animationDirection}\n\t\t\t\t\tvariants={textVariants}\n\t\t\t\t\tdirection=\"row\"\n\t\t\t\t\tgap={2}\n\t\t\t\t\tclassName=\"hero-carousel-info\"\n\t\t\t\t\tmt={1}\n\t\t\t\t\tjustifyItems=\"flex-start\"\n\t\t\t\t\talignItems=\"center\"\n\t\t\t\t>\n\t\t\t\t\t{item.PremiereDate && (\n\t\t\t\t\t\t<Typography style={{ opacity: \"0.8\" }} variant=\"subtitle2\">\n\t\t\t\t\t\t\t{item.ProductionYear ?? \"\"}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t)}\n\t\t\t\t\t{item.OfficialRating && (\n\t\t\t\t\t\t<Chip variant=\"filled\" size=\"small\" label={item.OfficialRating} />\n\t\t\t\t\t)}\n\n\t\t\t\t\t{item.CommunityRating && (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\tgap: \"0.25em\",\n\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tclassName=\"hero-carousel-info-rating\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded fill\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t// fontSize: \"2.2em\",\n\t\t\t\t\t\t\t\t\tcolor: yellow[400],\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tstar\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\topacity: \"0.8\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{Math.round(item.CommunityRating * 10) / 10}\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t\t{item.CriticRating && (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\tgap: \"0.25em\",\n\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tclassName=\"hero-carousel-info-rating\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded fill\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tcolor: item.CriticRating > 50 ? green[400] : red[400],\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{item.CriticRating > 50 ? \"thumb_up\" : \"thumb_down\"}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\topacity: \"0.8\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{item.CriticRating}\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\n\t\t\t\t\t{item.RunTimeTicks && (\n\t\t\t\t\t\t<Typography style={{ opacity: \"0.8\" }} variant=\"subtitle2\">\n\t\t\t\t\t\t\t{getRuntime(item.RunTimeTicks)}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t)}\n\t\t\t\t\t{item.RunTimeTicks && (\n\t\t\t\t\t\t<Typography style={{ opacity: \"0.8\" }} variant=\"subtitle2\">\n\t\t\t\t\t\t\t{endsAt(\n\t\t\t\t\t\t\t\titem.RunTimeTicks - (item.UserData?.PlaybackPositionTicks ?? 0),\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t)}\n\t\t\t\t\t<Typography variant=\"subtitle2\" style={{ opacity: 0.8 }}>\n\t\t\t\t\t\t{item.Genres?.slice(0, 4).join(\" / \")}\n\t\t\t\t\t</Typography>\n\t\t\t\t</Stack>\n\t\t\t\t{/* @ts-ignore */}\n\t\t\t\t<Typography\n\t\t\t\t\tcomponent={motion.div}\n\t\t\t\t\tcustom={animationDirection}\n\t\t\t\t\tvariants={textVariants}\n\t\t\t\t\tclassName=\"hero-carousel-overview\"\n\t\t\t\t\tvariant=\"body1\"\n\t\t\t\t\tfontWeight={400}\n\t\t\t\t>\n\t\t\t\t\t{item.Overview}\n\t\t\t\t</Typography>\n\n\t\t\t\t{/* @ts-ignore */}\n\t\t\t\t<Stack\n\t\t\t\t\tdirection=\"row\"\n\t\t\t\t\tgap={2}\n\t\t\t\t\twidth=\"100%\"\n\t\t\t\t\tclassName=\"hero-carousel-button-container\"\n\t\t\t\t\talignItems=\"center\"\n\t\t\t\t\tcomponent={motion.div}\n\t\t\t\t\tcustom={animationDirection}\n\t\t\t\t\tvariants={textVariants}\n\t\t\t\t>\n\t\t\t\t\t<PlayButton\n\t\t\t\t\t\titem={item}\n\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\titemType={item.Type ?? \"Movie\"}\n\t\t\t\t\t\tbuttonProps={{\n\t\t\t\t\t\t\tsize: \"large\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t\taudio={\n\t\t\t\t\t\t\titem.Type === BaseItemKind.MusicAlbum ||\n\t\t\t\t\t\t\titem.Type === BaseItemKind.Audio ||\n\t\t\t\t\t\t\titem.Type === BaseItemKind.AudioBook ||\n\t\t\t\t\t\t\titem.Type === BaseItemKind.Playlist\n\t\t\t\t\t\t}\n\t\t\t\t\t\tplaylistItem={item.Type === BaseItemKind.Playlist}\n\t\t\t\t\t\tplaylistItemId={item.Id}\n\t\t\t\t\t/>\n\n\t\t\t\t\t<Button\n\t\t\t\t\t\tsize=\"large\"\n\t\t\t\t\t\t//@ts-expect-error\n\t\t\t\t\t\tcolor=\"white\"\n\t\t\t\t\t\tvariant=\"outlined\"\n\t\t\t\t\t\tendIcon={\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tfontSize: \"2em\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tchevron_right\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t\tonClick={handleMoreInfo}\n\t\t\t\t\t>\n\t\t\t\t\t\tMore info\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Stack direction=\"row\" gap={1}>\n\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\titemId={item.Id}\n\t\t\t\t\t\t\tqueryKey={[\"home\", \"latestMedia\"]}\n\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\tisFavorite={item.UserData?.IsFavorite}\n\t\t\t\t\t\t\titemName={item.Name}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<MarkPlayedButton\n\t\t\t\t\t\t\titemId={item.Id}\n\t\t\t\t\t\t\tqueryKey={[\"home\", \"latestMedia\"]}\n\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\tisPlayed={item.UserData?.Played}\n\t\t\t\t\t\t\titemName={item.Name}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</Stack>\n\t\t\t\t</Stack>\n\t\t\t</motion.div>\n\t\t</div>\n\t);\n};\n\nexport default CarouselSlide;\n"
  },
  {
    "path": "src/components/circularPageLoadingAnimation/index.tsx",
    "content": "// Write a react component that included an MUI circluar progress element utilizing NProgrss\n\nimport { CircularProgress } from \"@mui/material\";\nimport { useNProgress } from \"@tanem/react-nprogress\";\nimport React from \"react\";\n\nconst CircularPageLoadingAnimation = () => {\n\tconst { progress } = useNProgress({\n\t\tisAnimating: true,\n\t});\n\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\tpointerEvents: \"none\",\n\t\t\t\tposition: \"absolute\",\n\t\t\t\ttop: \"50%\",\n\t\t\t\tleft: \"50%\",\n\t\t\t\ttransform: \"translate(-50%, -50%)\",\n\t\t\t\tzIndex: \"10001\",\n\t\t\t}}\n\t\t>\n\t\t\t<CircularProgress\n\t\t\t\t// variant=\"indeterminate\"\n\t\t\t\tsx={{\n\t\t\t\t\ttransitionDuration: \"0.3s\",\n\t\t\t\t\ttransform: `scale(${progress})`,\n\t\t\t\t}}\n\t\t\t/>\n\t\t</div>\n\t);\n};\n\nexport default CircularPageLoadingAnimation;\n"
  },
  {
    "path": "src/components/filtersDialog/index.tsx",
    "content": "import { getGenresApi } from \"@jellyfin/sdk/lib/utils/api/genres-api\";\nimport { getMusicGenresApi } from \"@jellyfin/sdk/lib/utils/api/music-genres-api\";\nimport {\n\tAccordion,\n\tAccordionDetails,\n\tAccordionSummary,\n\tButton,\n\tCheckbox,\n\tDialog,\n\tDialogActions,\n\tDialogContent,\n\tDialogTitle,\n\tFormControlLabel,\n\tFormGroup,\n\tIconButton,\n\tStack,\n\tTooltip,\n\tTypography,\n} from \"@mui/material\";\nimport { useQuery, useSuspenseQuery } from \"@tanstack/react-query\";\nimport { getRouteApi } from \"@tanstack/react-router\";\nimport React, { useMemo, useRef, useState } from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport type { FILTERS } from \"@/utils/constants/library\";\nimport { getLibraryQueryOptions } from \"@/utils/queries/library\";\nimport { useLibraryStateStore } from \"@/utils/store/libraryState\";\n\nconst route = getRouteApi(\"/_api/library/$id\");\n\nconst FILTER_LABELS: Record<FILTERS, string> = {\n\tisPlayed: \"Played\",\n\tisUnPlayed: \"Unplayed\",\n\tisResumable: \"Resumable\",\n\tisFavorite: \"Favorite\",\n\thasSubtitles: \"Has Subtitles\",\n\thasTrailer: \"Has Trailer\",\n\thasSpecialFeature: \"Special Feature\",\n\thasThemeSong: \"Theme Song\",\n\thasThemeVideo: \"Theme Video\",\n\tisSD: \"SD\",\n\tisHD: \"HD\",\n\tis4K: \"4K\",\n\tis3D: \"3D\",\n};\n\ninterface FiltersDialogProps {\n\topen: boolean;\n\tonClose: () => void;\n}\n\nexport const FiltersDialog: React.FC<FiltersDialogProps> = React.memo(\n\t({ open, onClose }) => {\n\t\tconst { id: currentLibraryId } = route.useParams();\n\t\tconst { api, user } = route.useRouteContext();\n\t\tconst { routeFilters, routeVideoTypes, routeGenreIds } =\n\t\t\tuseLibraryStateStore(\n\t\t\t\tuseShallow((s) => {\n\t\t\t\t\tconst slice = s.libraries[currentLibraryId || \"\"];\n\t\t\t\t\treturn {\n\t\t\t\t\t\trouteFilters: slice?.filters,\n\t\t\t\t\t\trouteVideoTypes: slice?.videoTypesState,\n\t\t\t\t\t\trouteGenreIds: slice?.genreIds,\n\t\t\t\t\t};\n\t\t\t\t}),\n\t\t\t);\n\t\tconst updateLibrary = useLibraryStateStore((s) => s.updateLibrary);\n\t\tconst initial = useMemo(\n\t\t\t() => ({ ...(routeFilters || {}) }),\n\t\t\t[routeFilters],\n\t\t);\n\t\tconst [localFilters, setLocalFilters] =\n\t\t\tuseState<Record<string, boolean | undefined>>(initial);\n\t\tconst initialVideo = useMemo(\n\t\t\t() => ({ ...(routeVideoTypes || {}) }),\n\t\t\t[routeVideoTypes],\n\t\t);\n\t\tconst [localVideoTypes, setLocalVideoTypes] =\n\t\t\tuseState<Record<string, boolean>>(initialVideo);\n\t\tconst initialGenres = useMemo(\n\t\t\t() => [...(routeGenreIds || [])] as string[],\n\t\t\t[routeGenreIds],\n\t\t);\n\t\tconst [localGenreIds, setLocalGenreIds] = useState<string[]>(initialGenres);\n\t\tconst dirty = useMemo(\n\t\t\t() =>\n\t\t\t\tJSON.stringify(initial) !== JSON.stringify(localFilters) ||\n\t\t\t\tJSON.stringify(initialVideo) !== JSON.stringify(localVideoTypes) ||\n\t\t\t\tJSON.stringify(initialGenres) !== JSON.stringify(localGenreIds),\n\t\t\t[\n\t\t\t\tinitial,\n\t\t\t\tlocalFilters,\n\t\t\t\tinitialVideo,\n\t\t\t\tlocalVideoTypes,\n\t\t\t\tinitialGenres,\n\t\t\t\tlocalGenreIds,\n\t\t\t],\n\t\t);\n\t\tconst debounceRef = useRef<number | null>(null);\n\t\tconst currentLibrary = useSuspenseQuery(\n\t\t\tgetLibraryQueryOptions(api, user?.Id, currentLibraryId),\n\t\t);\n\t\tconst collectionType = currentLibrary.data.CollectionType;\n\t\tconst isVideoCollection = [\"movies\", \"tvshows\", \"boxsets\"].includes(\n\t\t\tString(collectionType || \"\").toLowerCase(),\n\t\t);\n\n\t\t// Fetch genres for this library (music uses music-genres API)\n\t\tconst { data: genresData } = useQuery({\n\t\t\tqueryKey: [\"library\", \"genres\", currentLibraryId, collectionType],\n\t\t\tqueryFn: async () => {\n\t\t\t\tif (!api || !user?.Id || !currentLibraryId) return { Items: [] } as any;\n\t\t\t\tif (String(collectionType || \"\").toLowerCase() === \"music\") {\n\t\t\t\t\tconst res = await getMusicGenresApi(api).getMusicGenres({\n\t\t\t\t\t\tparentId: currentLibraryId,\n\t\t\t\t\t\tuserId: user.Id,\n\t\t\t\t\t});\n\t\t\t\t\treturn res.data;\n\t\t\t\t}\n\t\t\t\tconst res = await getGenresApi(api).getGenres({\n\t\t\t\t\tparentId: currentLibraryId,\n\t\t\t\t\tuserId: user.Id,\n\t\t\t\t});\n\t\t\t\treturn res.data;\n\t\t\t},\n\t\t\tstaleTime: 10 * 60 * 1000,\n\t\t\tenabled: !!open && !!api && !!user?.Id && !!currentLibraryId,\n\t\t});\n\n\t\tconst handleToggle = (key: FILTERS) => {\n\t\t\tsetLocalFilters((prev) => {\n\t\t\t\tconst next = { ...prev };\n\t\t\t\t// isHD special: undefined if unchecked instead of false\n\t\t\t\tif (key === \"isHD\") {\n\t\t\t\t\tif (next[key]) delete next[key];\n\t\t\t\t\telse next[key] = true;\n\t\t\t\t} else {\n\t\t\t\t\tnext[key] = !next[key];\n\t\t\t\t}\n\t\t\t\treturn next;\n\t\t\t});\n\t\t};\n\n\t\tconst apply = () => {\n\t\t\tif (!currentLibraryId) return;\n\t\t\tif (debounceRef.current) window.clearTimeout(debounceRef.current);\n\t\t\tdebounceRef.current = window.setTimeout(() => {\n\t\t\t\tupdateLibrary(currentLibraryId, {\n\t\t\t\t\tfilters: localFilters as any,\n\t\t\t\t\tvideoTypesState: localVideoTypes as any,\n\t\t\t\t\tgenreIds: localGenreIds as any,\n\t\t\t\t});\n\t\t\t\tonClose();\n\t\t\t}, 40);\n\t\t};\n\n\t\tconst clearAll = () => {\n\t\t\tsetLocalFilters({});\n\t\t\tsetLocalVideoTypes({});\n\t\t\tsetLocalGenreIds([]);\n\t\t};\n\n\t\tconst restore = () => setLocalFilters(initial);\n\t\tconst restoreVideo = () => setLocalVideoTypes(initialVideo);\n\t\tconst restoreGenres = () => setLocalGenreIds(initialGenres);\n\n\t\treturn (\n\t\t\t<Dialog\n\t\t\t\topen={open}\n\t\t\t\tonClose={onClose}\n\t\t\t\tfullWidth\n\t\t\t\tmaxWidth=\"sm\"\n\t\t\t\tkeepMounted\n\t\t\t\tPaperProps={{ className: \"glass-dialog-paper\" }}\n\t\t\t>\n\t\t\t\t<DialogTitle>Filters</DialogTitle>\n\t\t\t\t<DialogContent\n\t\t\t\t\tdividers\n\t\t\t\t\tsx={{ borderColor: \"rgba(255, 255, 255, 0.1)\" }}\n\t\t\t\t>\n\t\t\t\t\t<Stack spacing={1.25}>\n\t\t\t\t\t\t<Accordion\n\t\t\t\t\t\t\tdisableGutters\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\tbackgroundColor: \"transparent\",\n\t\t\t\t\t\t\t\tboxShadow: \"none\",\n\t\t\t\t\t\t\t\tm: 0,\n\t\t\t\t\t\t\t\t\"&:before\": { display: \"none\" },\n\t\t\t\t\t\t\t\t\"&:not(:last-of-type)\": {\n\t\t\t\t\t\t\t\t\tborderBottom: \"1px solid rgba(255,255,255,0.12)\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<AccordionSummary\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\tminHeight: 40,\n\t\t\t\t\t\t\t\t\t\"& .MuiAccordionSummary-content\": { my: 0 },\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\texpandIcon={\n\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">expand_more</span>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Typography variant=\"subtitle2\">General</Typography>\n\t\t\t\t\t\t\t</AccordionSummary>\n\t\t\t\t\t\t\t<AccordionDetails sx={{ px: 1.5, py: 1.25 }}>\n\t\t\t\t\t\t\t\t<FormGroup>\n\t\t\t\t\t\t\t\t\t{(\n\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\"isPlayed\",\n\t\t\t\t\t\t\t\t\t\t\t\"isUnPlayed\",\n\t\t\t\t\t\t\t\t\t\t\t\"isResumable\",\n\t\t\t\t\t\t\t\t\t\t\t\"isFavorite\",\n\t\t\t\t\t\t\t\t\t\t] as FILTERS[]\n\t\t\t\t\t\t\t\t\t).map((k) => (\n\t\t\t\t\t\t\t\t\t\t<FormControlLabel\n\t\t\t\t\t\t\t\t\t\t\tkey={k}\n\t\t\t\t\t\t\t\t\t\t\tcontrol={\n\t\t\t\t\t\t\t\t\t\t\t\t<Checkbox\n\t\t\t\t\t\t\t\t\t\t\t\t\tchecked={!!localFilters[k]}\n\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={() => handleToggle(k)}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tlabel={FILTER_LABELS[k]}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</FormGroup>\n\t\t\t\t\t\t\t</AccordionDetails>\n\t\t\t\t\t\t</Accordion>\n\n\t\t\t\t\t\t{isVideoCollection && (\n\t\t\t\t\t\t\t<Accordion\n\t\t\t\t\t\t\t\tdisableGutters\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\tbackgroundColor: \"transparent\",\n\t\t\t\t\t\t\t\t\tboxShadow: \"none\",\n\t\t\t\t\t\t\t\t\tm: 0,\n\t\t\t\t\t\t\t\t\t\"&:before\": { display: \"none\" },\n\t\t\t\t\t\t\t\t\t\"&:not(:last-of-type)\": {\n\t\t\t\t\t\t\t\t\t\tborderBottom: \"1px solid rgba(255,255,255,0.12)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<AccordionSummary\n\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\tminHeight: 40,\n\t\t\t\t\t\t\t\t\t\t\"& .MuiAccordionSummary-content\": { my: 0 },\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\texpandIcon={\n\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">\n\t\t\t\t\t\t\t\t\t\t\texpand_more\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle2\">Video features</Typography>\n\t\t\t\t\t\t\t\t</AccordionSummary>\n\t\t\t\t\t\t\t\t<AccordionDetails sx={{ px: 1.5, py: 1.25 }}>\n\t\t\t\t\t\t\t\t\t<FormGroup>\n\t\t\t\t\t\t\t\t\t\t{(\n\t\t\t\t\t\t\t\t\t\t\t[\n\t\t\t\t\t\t\t\t\t\t\t\t\"hasSubtitles\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"hasTrailer\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"hasSpecialFeature\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"hasThemeSong\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"hasThemeVideo\",\n\t\t\t\t\t\t\t\t\t\t\t] as FILTERS[]\n\t\t\t\t\t\t\t\t\t\t).map((k) => (\n\t\t\t\t\t\t\t\t\t\t\t<FormControlLabel\n\t\t\t\t\t\t\t\t\t\t\t\tkey={k}\n\t\t\t\t\t\t\t\t\t\t\t\tcontrol={\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Checkbox\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tchecked={!!localFilters[k]}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={() => handleToggle(k)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tlabel={FILTER_LABELS[k]}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</FormGroup>\n\t\t\t\t\t\t\t\t</AccordionDetails>\n\t\t\t\t\t\t\t</Accordion>\n\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t{isVideoCollection && (\n\t\t\t\t\t\t\t<Accordion\n\t\t\t\t\t\t\t\tdisableGutters\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\tbackgroundColor: \"transparent\",\n\t\t\t\t\t\t\t\t\tboxShadow: \"none\",\n\t\t\t\t\t\t\t\t\tm: 0,\n\t\t\t\t\t\t\t\t\t\"&:before\": { display: \"none\" },\n\t\t\t\t\t\t\t\t\t\"&:not(:last-of-type)\": {\n\t\t\t\t\t\t\t\t\t\tborderBottom: \"1px solid rgba(255,255,255,0.12)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<AccordionSummary\n\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\tminHeight: 40,\n\t\t\t\t\t\t\t\t\t\t\"& .MuiAccordionSummary-content\": { my: 0 },\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\texpandIcon={\n\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">\n\t\t\t\t\t\t\t\t\t\t\texpand_more\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle2\">Resolution</Typography>\n\t\t\t\t\t\t\t\t</AccordionSummary>\n\t\t\t\t\t\t\t\t<AccordionDetails sx={{ px: 1.5, py: 1.25 }}>\n\t\t\t\t\t\t\t\t\t<FormGroup>\n\t\t\t\t\t\t\t\t\t\t{([\"isSD\", \"isHD\", \"is4K\", \"is3D\"] as FILTERS[]).map(\n\t\t\t\t\t\t\t\t\t\t\t(k) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<FormControlLabel\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={k}\n\t\t\t\t\t\t\t\t\t\t\t\t\tcontrol={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Checkbox\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tchecked={!!localFilters[k]}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={() => handleToggle(k)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tlabel={FILTER_LABELS[k]}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</FormGroup>\n\t\t\t\t\t\t\t\t</AccordionDetails>\n\t\t\t\t\t\t\t</Accordion>\n\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t{isVideoCollection && (\n\t\t\t\t\t\t\t<Accordion\n\t\t\t\t\t\t\t\tdisableGutters\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\tbackgroundColor: \"transparent\",\n\t\t\t\t\t\t\t\t\tboxShadow: \"none\",\n\t\t\t\t\t\t\t\t\tm: 0,\n\t\t\t\t\t\t\t\t\t\"&:before\": { display: \"none\" },\n\t\t\t\t\t\t\t\t\t\"&:not(:last-of-type)\": {\n\t\t\t\t\t\t\t\t\t\tborderBottom: \"1px solid rgba(255,255,255,0.12)\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<AccordionSummary\n\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\tminHeight: 40,\n\t\t\t\t\t\t\t\t\t\t\"& .MuiAccordionSummary-content\": { my: 0 },\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\texpandIcon={\n\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">\n\t\t\t\t\t\t\t\t\t\t\texpand_more\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle2\">Media type</Typography>\n\t\t\t\t\t\t\t\t</AccordionSummary>\n\t\t\t\t\t\t\t\t<AccordionDetails sx={{ px: 1.5, py: 1.25 }}>\n\t\t\t\t\t\t\t\t\t<FormGroup>\n\t\t\t\t\t\t\t\t\t\t{([\"BluRay\", \"Dvd\", \"Iso\", \"VideoFile\"] as const).map(\n\t\t\t\t\t\t\t\t\t\t\t(k) => (\n\t\t\t\t\t\t\t\t\t\t\t\t<FormControlLabel\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={k}\n\t\t\t\t\t\t\t\t\t\t\t\t\tcontrol={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Checkbox\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tchecked={!!(localVideoTypes as any)[k]}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={() =>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetLocalVideoTypes((prev) => ({\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t...(prev || {}),\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[k]: !(prev as any)?.[k],\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}))\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tlabel={k}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</FormGroup>\n\t\t\t\t\t\t\t\t</AccordionDetails>\n\t\t\t\t\t\t\t</Accordion>\n\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t{/* Genres (all collection types) */}\n\t\t\t\t\t\t{(genresData?.Items?.length || 0) > 0 && (\n\t\t\t\t\t\t\t<Accordion\n\t\t\t\t\t\t\t\tdisableGutters\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\tbackgroundColor: \"transparent\",\n\t\t\t\t\t\t\t\t\tboxShadow: \"none\",\n\t\t\t\t\t\t\t\t\tm: 0,\n\t\t\t\t\t\t\t\t\t\"&:before\": { display: \"none\" },\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<AccordionSummary\n\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\tminHeight: 40,\n\t\t\t\t\t\t\t\t\t\t\"& .MuiAccordionSummary-content\": { my: 0 },\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\texpandIcon={\n\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">\n\t\t\t\t\t\t\t\t\t\t\texpand_more\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle2\">Genres</Typography>\n\t\t\t\t\t\t\t\t</AccordionSummary>\n\t\t\t\t\t\t\t\t<AccordionDetails sx={{ px: 1.5, py: 1.25 }}>\n\t\t\t\t\t\t\t\t\t<FormGroup>\n\t\t\t\t\t\t\t\t\t\t{(genresData?.Items || []).map((g: any) => (\n\t\t\t\t\t\t\t\t\t\t\t<FormControlLabel\n\t\t\t\t\t\t\t\t\t\t\t\tkey={g.Id}\n\t\t\t\t\t\t\t\t\t\t\t\tcontrol={\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Checkbox\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tchecked={localGenreIds.includes(g.Id)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tonChange={() =>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsetLocalGenreIds((prev) =>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tprev.includes(g.Id)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? prev.filter((id) => id !== g.Id)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: [...prev, g.Id],\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\tlabel={g.Name}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</FormGroup>\n\t\t\t\t\t\t\t\t</AccordionDetails>\n\t\t\t\t\t\t\t</Accordion>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Stack>\n\t\t\t\t</DialogContent>\n\t\t\t\t<DialogActions>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tonClick={clearAll}\n\t\t\t\t\t\tcolor=\"inherit\"\n\t\t\t\t\t\tdisabled={Object.keys(localFilters).length === 0}\n\t\t\t\t\t>\n\t\t\t\t\t\tClear\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\trestore();\n\t\t\t\t\t\t\trestoreVideo();\n\t\t\t\t\t\t\trestoreGenres();\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tcolor=\"inherit\"\n\t\t\t\t\t\tdisabled={!dirty}\n\t\t\t\t\t>\n\t\t\t\t\t\tReset\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button onClick={onClose}>Cancel</Button>\n\t\t\t\t\t<Button onClick={apply} disabled={!dirty} variant=\"contained\">\n\t\t\t\t\t\tApply\n\t\t\t\t\t</Button>\n\t\t\t\t</DialogActions>\n\t\t\t</Dialog>\n\t\t);\n\t},\n);\n(FiltersDialog as any).displayName = \"FiltersDialog\";\n\nexport const FiltersDialogTrigger: React.FC = () => {\n\tconst [open, setOpen] = React.useState(false);\n\tconst { id: currentLibraryId } = route.useParams();\n\tconst { filters, videoTypesState, genreIds } = useLibraryStateStore(\n\t\tuseShallow((s) => {\n\t\t\tconst slice = s.libraries[currentLibraryId || \"\"];\n\t\t\treturn {\n\t\t\t\tfilters: slice?.filters,\n\t\t\t\tvideoTypesState: slice?.videoTypesState,\n\t\t\t\tgenreIds: slice?.genreIds,\n\t\t\t};\n\t\t}),\n\t);\n\tconst activeCount = useMemo(() => {\n\t\tconst a = Object.values(filters || {}).filter((v) => v === true).length;\n\t\tconst b = Object.values(videoTypesState || {}).filter(\n\t\t\t(v) => v === true,\n\t\t).length;\n\t\tconst c = (genreIds || []).length;\n\t\treturn a + b + c;\n\t}, [filters, videoTypesState, genreIds]);\n\treturn (\n\t\t<>\n\t\t\t<Tooltip\n\t\t\t\ttitle={activeCount ? `${activeCount} active filters` : \"Filters\"}\n\t\t\t>\n\t\t\t\t<IconButton\n\t\t\t\t\tonClick={() => setOpen(true)}\n\t\t\t\t\taria-label=\"Open filters dialog\"\n\t\t\t\t>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\tstyle={{ position: \"relative\" }}\n\t\t\t\t\t>\n\t\t\t\t\t\tfilter_list\n\t\t\t\t\t\t{activeCount > 0 && (\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\t\t\ttop: -2,\n\t\t\t\t\t\t\t\t\tright: -4,\n\t\t\t\t\t\t\t\t\tbackground: \"#ff4081\",\n\t\t\t\t\t\t\t\t\tcolor: \"#fff\",\n\t\t\t\t\t\t\t\t\tfontSize: 10,\n\t\t\t\t\t\t\t\t\tborderRadius: 8,\n\t\t\t\t\t\t\t\t\tlineHeight: 1,\n\t\t\t\t\t\t\t\t\tpadding: \"2px 4px\",\n\t\t\t\t\t\t\t\t\tfontFamily: \"Plus Jakarta Sans Variable\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{activeCount}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</span>\n\t\t\t\t</IconButton>\n\t\t\t</Tooltip>\n\t\t\t<FiltersDialog open={open} onClose={() => setOpen(false)} />\n\t\t</>\n\t);\n};\n\nexport default FiltersDialog;\n"
  },
  {
    "path": "src/components/iconLink/index.tsx",
    "content": "import { Link, Typography } from \"@mui/material\";\nimport React, { memo } from \"react\";\n\nimport anidbIcon from \"../../assets/icons/anidb.png\";\nimport anilistIcon from \"../../assets/icons/anilist.svg\";\nimport audioDBIcon from \"../../assets/icons/audioDB.png\";\nimport imdbIcon from \"../../assets/icons/imdb.svg\";\nimport kitsuIcon from \"../../assets/icons/kitsu.svg\";\nimport musicBrainzIcon from \"../../assets/icons/musicbrainz.svg\";\nimport tvDbIcon from \"../../assets/icons/the-tvdb.svg\";\nimport tmdbIcon from \"../../assets/icons/themoviedatabase.svg\";\nimport traktIcon from \"../../assets/icons/trakt.svg\";\nimport tvMazeIcon from \"../../assets/icons/tvmaze.png\";\n\nconst knownIcons = [\n\t\"imdb\",\n\t\"themoviedb\",\n\t\"trakt\",\n\t\"musicbrainz\",\n\t\"thetvdb\",\n\t\"anidb\",\n\t\"anilist\",\n\t\"tvmaze\",\n\t\"theaudiodb\",\n\t\"kitsu\",\n];\n\nconst IconLink = memo(({ name, url }: { name: string; url: string }) => {\n\treturn (\n\t\t<Link target=\"_blank\" href={url} className=\"item-detail-link\">\n\t\t\t{name.toLocaleLowerCase() === \"imdb\" && <img src={imdbIcon} alt=\"IMDb\" />}\n\t\t\t{name.toLocaleLowerCase() === \"themoviedb\" && (\n\t\t\t\t<img src={tmdbIcon} alt=\"TheMovieDb\" />\n\t\t\t)}\n\t\t\t{name.toLocaleLowerCase() === \"trakt\" && (\n\t\t\t\t<img src={traktIcon} alt=\"Trakt\" />\n\t\t\t)}\n\t\t\t{name.toLocaleLowerCase() === \"musicbrainz\" && (\n\t\t\t\t<img src={musicBrainzIcon} alt=\"MusicBrainz\" />\n\t\t\t)}\n\t\t\t{name.toLocaleLowerCase() === \"thetvdb\" && (\n\t\t\t\t<img src={tvDbIcon} alt=\"TheTVDB\" />\n\t\t\t)}\n\t\t\t{name.toLocaleLowerCase() === \"anidb\" && (\n\t\t\t\t<img src={anidbIcon} alt=\"AniDB\" />\n\t\t\t)}\n\t\t\t{name.toLocaleLowerCase() === \"anilist\" && (\n\t\t\t\t<img src={anilistIcon} alt=\"AniList\" />\n\t\t\t)}\n\t\t\t{name.toLocaleLowerCase() === \"tvmaze\" && (\n\t\t\t\t<img src={tvMazeIcon} alt=\"TVMaze\" />\n\t\t\t)}\n\t\t\t{name.toLocaleLowerCase() === \"theaudiodb\" && (\n\t\t\t\t<img src={audioDBIcon} alt=\"TheAudioDB\" />\n\t\t\t)}\n\t\t\t{name.toLocaleLowerCase() === \"kitsu\" && (\n\t\t\t\t<img src={kitsuIcon} alt=\"Kitsu\" />\n\t\t\t)}\n\t\t\t{!knownIcons.includes(name.toLocaleLowerCase()) && (\n\t\t\t\t<Typography>{name}</Typography>\n\t\t\t)}\n\t\t</Link>\n\t);\n});\n\nexport default IconLink;\n"
  },
  {
    "path": "src/components/itemBackdrop/index.tsx",
    "content": "import {\n\tAnimatePresence,\n\ttype HTMLMotionProps,\n\tmotion,\n\tuseScroll,\n\tuseTransform,\n} from \"motion/react\";\nimport React, { type RefObject, useEffect, useState } from \"react\";\n\ninterface ItemBackdropProps {\n\ttargetRef: RefObject<HTMLElement | null>;\n\tbackdropSrc?: string;\n\tfallbackSrc: string;\n\talt: string;\n\tdistance?: number; // parallax distance\n\tclassName?: string;\n\tonLoad?: (e: React.SyntheticEvent<HTMLImageElement>) => void;\n\tmotionProps?: HTMLMotionProps<\"img\">;\n}\n\nconst MotionImg = motion.img;\n\nconst animationProps = {\n\tinitial: { opacity: 0 },\n\tanimate: { opacity: 1 },\n\texit: { opacity: 0 },\n\ttransition: { duration: 0.5 },\n};\n\n/**\n * ItemBackdrop delays applying motion-driven parallax until after mount\n * to avoid hydration timing errors (\"Target ref is defined but not hydrated\").\n * It accepts a scroll container ref from the parent; if the ref is not yet\n * attached, it renders a static img to prevent motion from touching an\n * unhydrated DOM node.\n */\nconst ItemBackdropContent = React.memo(function ItemBackdropContent({\n\ttargetRef,\n\tsrc,\n\talt,\n\tdistance,\n\tclassName,\n\tonLoad,\n\tmotionProps,\n}: {\n\ttargetRef: RefObject<HTMLElement | null>;\n\tsrc: string;\n\talt: string;\n\tdistance: number;\n\tclassName?: string;\n\tonLoad?: (e: React.SyntheticEvent<HTMLImageElement>) => void;\n\tmotionProps?: HTMLMotionProps<\"img\">;\n}) {\n\tconst { scrollYProgress } = useScroll({\n\t\ttarget: targetRef,\n\t\toffset: [\"start start\", \"60vh start\"],\n\t});\n\tconst y = useTransform(scrollYProgress, [0, 1], [-distance, distance]);\n\treturn (\n\t\t<MotionImg\n\t\t\talt={alt}\n\t\t\tsrc={src}\n\t\t\tclassName={className ?? \"item-hero-backdrop\"}\n\t\t\tonLoad={(e) => {\n\t\t\t\tonLoad?.(e);\n\t\t\t}}\n\t\t\tstyle={{ y }}\n\t\t\t{...motionProps}\n\t\t/>\n\t);\n});\n\nexport function ItemBackdrop({\n\ttargetRef,\n\tbackdropSrc,\n\tfallbackSrc,\n\talt,\n\tdistance = 50,\n\tclassName,\n\tonLoad,\n\tmotionProps,\n}: ItemBackdropProps) {\n\tconst src = backdropSrc || fallbackSrc;\n\tconst [ready, setReady] = useState(false);\n\t// Poll a few animation frames until the target ref hydrates to avoid premature motion initialization.\n\tuseEffect(() => {\n\t\tif (ready) return;\n\t\tlet frame = 0;\n\t\tlet raf: number;\n\t\tconst tick = () => {\n\t\t\tif (targetRef.current || frame > 5) {\n\t\t\t\tsetReady(true);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tframe++;\n\t\t\traf = requestAnimationFrame(tick);\n\t\t};\n\t\ttick();\n\t\treturn () => cancelAnimationFrame(raf);\n\t}, [targetRef, ready]);\n\n\tconst finalMotionProps = React.useMemo(\n\t\t() => ({\n\t\t\t...animationProps,\n\t\t\t...motionProps,\n\t\t}),\n\t\t[motionProps],\n\t);\n\n\tif (!ready) {\n\t\treturn (\n\t\t\t<img\n\t\t\t\talt={alt}\n\t\t\t\tsrc={src}\n\t\t\t\tclassName={className ?? \"item-hero-backdrop\"}\n\t\t\t\tonLoad={(e) => {\n\t\t\t\t\te.currentTarget.style.opacity = \"1\";\n\t\t\t\t\tonLoad?.(e);\n\t\t\t\t}}\n\t\t\t/>\n\t\t);\n\t}\n\n\treturn (\n\t\t<AnimatePresence>\n\t\t\t<ItemBackdropContent\n\t\t\t\tkey={src}\n\t\t\t\ttargetRef={targetRef}\n\t\t\t\tsrc={src}\n\t\t\t\talt={alt}\n\t\t\t\tdistance={distance}\n\t\t\t\tclassName={className}\n\t\t\t\tonLoad={onLoad}\n\t\t\t\tmotionProps={finalMotionProps}\n\t\t\t/>\n\t\t</AnimatePresence>\n\t);\n}\n\nexport default ItemBackdrop;\n"
  },
  {
    "path": "src/components/itemHeader/index.tsx",
    "content": "import type { Api } from \"@jellyfin/sdk\";\nimport type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport Chip from \"@mui/material/Chip\";\nimport { green, red, yellow } from \"@mui/material/colors\";\nimport Stack from \"@mui/material/Stack\";\nimport Typography from \"@mui/material/Typography\";\nimport { Link } from \"@tanstack/react-router\";\nimport React, { type ReactNode, type RefObject } from \"react\";\nimport { Blurhash } from \"react-blurhash\";\nimport heroBg from \"@/assets/herobg.png\";\nimport ultraHdIcon from \"@/assets/icons/4k.svg\";\nimport dolbyAtmosIcon from \"@/assets/icons/dolby-atmos.svg\";\nimport dolbyDigitalIcon from \"@/assets/icons/dolby-digital.svg\";\nimport dolbyTrueHDIcon from \"@/assets/icons/dolby-truehd.svg\";\nimport dolbyVisionIcon from \"@/assets/icons/dolby-vision.svg\";\nimport dolbyVisionAtmosIcon from \"@/assets/icons/dolby-vision-atmos.png\";\nimport dtsIcon from \"@/assets/icons/dts.svg\";\nimport dtsHdMaIcon from \"@/assets/icons/dts-hd-ma.svg\";\nimport hdIcon from \"@/assets/icons/hd.svg\";\nimport hdrIcon from \"@/assets/icons/hdr.svg\";\nimport hdr10Icon from \"@/assets/icons/hdr10.svg\";\nimport hdr10PlusIcon from \"@/assets/icons/hdr10-plus.svg\";\nimport sdIcon from \"@/assets/icons/sd.svg\";\nimport sdrIcon from \"@/assets/icons/sdr.svg\";\nimport ItemBackdrop from \"@/components/itemBackdrop\";\nimport { getTypeIcon } from \"@/components/utils/iconsCollection\";\nimport { endsAt, getRuntime } from \"@/utils/date/time\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport type MediaQualityInfo from \"@/utils/types/mediaQualityInfo\";\nimport \"./itemHeader.scss\";\n\ninterface ItemHeaderProps {\n\titem: BaseItemDto;\n\tapi: Api | undefined;\n\tmediaQualityInfo?: MediaQualityInfo;\n\tscrollTargetRef?: RefObject<HTMLDivElement | null>;\n\tchildren?: ReactNode;\n\tbackdropSrc?: string | null;\n}\n\nconst ItemHeader = ({\n\titem,\n\tapi,\n\tmediaQualityInfo,\n\tscrollTargetRef,\n\tchildren,\n\tbackdropSrc,\n}: ItemHeaderProps) => {\n\tconst isEpisode = item.Type === \"Episode\";\n\tconst backdropTag =\n\t\tisEpisode && item.ParentBackdropImageTags?.length\n\t\t\t? item.ParentBackdropImageTags[0]\n\t\t\t: item.BackdropImageTags?.[0];\n\tconst backdropId =\n\t\tisEpisode && item.ParentBackdropImageTags?.length\n\t\t\t? (item.ParentBackdropItemId ?? item.Id)\n\t\t\t: item.Id;\n\n\tconst parentLogoImageTag =\n\t\t(item as any).ParentLogoImageTag || (item as any).ParentLogoImageTags?.[0];\n\tconst logoTag =\n\t\tisEpisode && parentLogoImageTag ? parentLogoImageTag : item.ImageTags?.Logo;\n\tconst logoId =\n\t\tisEpisode && parentLogoImageTag ? (item.SeriesId ?? item.Id) : item.Id;\n\n\tconst logo =\n\t\tlogoTag ? (\n\t\t\t<img\n\t\t\t\talt={item.Name ?? \"\"}\n\t\t\t\tsrc={\n\t\t\t\t\tapi &&\n\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(logoId ?? \"\", \"Logo\", {\n\t\t\t\t\t\tquality: 90,\n\t\t\t\t\t\tfillWidth: 592,\n\t\t\t\t\t\tfillHeight: 592,\n\t\t\t\t\t\ttag: logoTag,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tonLoad={(e) => {\n\t\t\t\t\te.currentTarget.style.opacity = \"1\";\n\t\t\t\t}}\n\t\t\t\tclassName=\"item-hero-logo\"\n\t\t\t/>\n\t\t) : (\n\t\t\t<Typography mb={2} fontWeight={200} variant=\"h2\">\n\t\t\t\t{isEpisode ? item.SeriesName : item.Name}\n\t\t\t</Typography>\n\t\t);\n\n\treturn (\n\t\t<div className=\"item-hero\">\n\t\t\t<div className=\"item-hero-backdrop-container\">\n\t\t\t\t{scrollTargetRef && (\n\t\t\t\t\t<ItemBackdrop\n\t\t\t\t\t\ttargetRef={scrollTargetRef}\n\t\t\t\t\t\talt={item.Name ?? \"\"}\n\t\t\t\t\t\tbackdropSrc={\n\t\t\t\t\t\t\tbackdropSrc ??\n\t\t\t\t\t\t\t(backdropTag\n\t\t\t\t\t\t\t\t? api &&\n\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\tbackdropId ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\"Backdrop\",\n\t\t\t\t\t\t\t\t\t\t{ tag: backdropTag },\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t: undefined)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tfallbackSrc={heroBg}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t\t<div\n\t\t\t\tclassName=\"item-hero-image-container\"\n\t\t\t\tstyle={{\n\t\t\t\t\taspectRatio: item.PrimaryImageAspectRatio ?? 1,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{item.ImageTags?.Primary ? (\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<Blurhash\n\t\t\t\t\t\t\thash={\n\t\t\t\t\t\t\t\titem.ImageBlurHashes?.Primary?.[item.ImageTags.Primary] ?? \"\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tclassName=\"item-hero-image-blurhash\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\talt={item.Name ?? \"\"}\n\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\titem.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tquality: 90,\n\t\t\t\t\t\t\t\t\t\ttag: item.ImageTags.Primary,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tonLoad={(e) => {\n\t\t\t\t\t\t\t\te.currentTarget.style.opacity = \"1\";\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tclassName=\"item-hero-image\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t) : (\n\t\t\t\t\t<div className=\"item-hero-image-icon\">\n\t\t\t\t\t\t{getTypeIcon(item.Type ?? \"Movie\")}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t\t<div className=\"item-hero-detail flex flex-column\">\n\t\t\t\t{isEpisode && logoId ? (\n\t\t\t\t\t<Link\n\t\t\t\t\t\tto=\"/series/$id\"\n\t\t\t\t\t\tparams={{ id: logoId }}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\ttextDecoration: \"none\",\n\t\t\t\t\t\t\tcolor: \"inherit\",\n\t\t\t\t\t\t\twidth: \"fit-content\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{logo}\n\t\t\t\t\t</Link>\n\t\t\t\t) : (\n\t\t\t\t\tlogo\n\t\t\t\t)}\n\t\t\t\t{isEpisode && (\n\t\t\t\t\t<Typography mb={2} fontWeight={200} variant=\"h4\">\n\t\t\t\t\t\t{item.ParentIndexNumber !== undefined &&\n\t\t\t\t\t\titem.IndexNumber !== undefined\n\t\t\t\t\t\t\t? `S${item.ParentIndexNumber}:E${item.IndexNumber} - `\n\t\t\t\t\t\t\t: \"\"}\n\t\t\t\t\t\t{item.Name}\n\t\t\t\t\t</Typography>\n\t\t\t\t)}\n\t\t\t\t<Stack\n\t\t\t\t\tdirection=\"row\"\n\t\t\t\t\tgap={2}\n\t\t\t\t\tjustifyItems=\"flex-start\"\n\t\t\t\t\talignItems=\"center\"\n\t\t\t\t>\n\t\t\t\t\t{mediaQualityInfo?.isUHD && (\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tsrc={ultraHdIcon}\n\t\t\t\t\t\t\talt=\"ultra hd\"\n\t\t\t\t\t\t\tclassName=\"item-hero-mediaInfo badge\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t\t{mediaQualityInfo?.isHD && (\n\t\t\t\t\t\t<img src={hdIcon} alt=\"hd\" className=\"item-hero-mediaInfo badge\" />\n\t\t\t\t\t)}\n\t\t\t\t\t{mediaQualityInfo?.isSD && (\n\t\t\t\t\t\t<img src={sdIcon} alt=\"sd\" className=\"item-hero-mediaInfo badge\" />\n\t\t\t\t\t)}\n\t\t\t\t\t{mediaQualityInfo?.isSDR && (\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tsrc={sdrIcon}\n\t\t\t\t\t\t\talt=\"sdr\"\n\t\t\t\t\t\t\tclassName=\"item-hero-mediaInfo badge\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t\t{mediaQualityInfo?.isHDR &&\n\t\t\t\t\t\t!mediaQualityInfo?.isHDR10 &&\n\t\t\t\t\t\t!mediaQualityInfo?.isHDR10Plus && (\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc={hdrIcon}\n\t\t\t\t\t\t\t\talt=\"hdr\"\n\t\t\t\t\t\t\t\tclassName=\"item-hero-mediaInfo badge\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t{mediaQualityInfo?.isHDR10 && (\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tsrc={hdr10Icon}\n\t\t\t\t\t\t\talt=\"hdr10\"\n\t\t\t\t\t\t\tclassName=\"item-hero-mediaInfo badge\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t\t{item.PremiereDate && (\n\t\t\t\t\t\t<Typography style={{ opacity: \"0.8\" }} variant=\"subtitle2\">\n\t\t\t\t\t\t\t{item.ProductionYear ?? \"\"}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t)}\n\t\t\t\t\t{item.OfficialRating && (\n\t\t\t\t\t\t<Chip variant=\"filled\" size=\"small\" label={item.OfficialRating} />\n\t\t\t\t\t)}\n\n\t\t\t\t\t{item.CommunityRating && (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\tgap: \"0.25em\",\n\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tclassName=\"hero-carousel-info-rating\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded fill\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t// fontSize: \"2.2em\",\n\t\t\t\t\t\t\t\t\tcolor: yellow[400],\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tstar\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\topacity: \"0.8\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{Math.round(item.CommunityRating * 10) / 10}\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t\t{item.CriticRating && (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\tgap: \"0.25em\",\n\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tclassName=\"hero-carousel-info-rating\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded fill\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tcolor: item.CriticRating > 50 ? green[400] : red[400],\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{item.CriticRating > 50 ? \"thumb_up\" : \"thumb_down\"}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\topacity: \"0.8\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{item.CriticRating}\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\n\t\t\t\t\t{item.RunTimeTicks && (\n\t\t\t\t\t\t<Typography style={{ opacity: \"0.8\" }} variant=\"subtitle2\">\n\t\t\t\t\t\t\t{getRuntime(item.RunTimeTicks)}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t)}\n\t\t\t\t\t{item.RunTimeTicks && (\n\t\t\t\t\t\t<Typography style={{ opacity: \"0.8\" }} variant=\"subtitle2\">\n\t\t\t\t\t\t\t{endsAt(\n\t\t\t\t\t\t\t\titem.RunTimeTicks - (item.UserData?.PlaybackPositionTicks ?? 0),\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t)}\n\t\t\t\t\t<Typography variant=\"subtitle2\" style={{ opacity: 0.8 }}>\n\t\t\t\t\t\t{item.Genres?.slice(0, 4).join(\" / \")}\n\t\t\t\t\t</Typography>\n\t\t\t\t</Stack>\n\t\t\t\t{mediaQualityInfo && (\n\t\t\t\t\t<Stack\n\t\t\t\t\t\tdirection=\"row\"\n\t\t\t\t\t\tgap={2}\n\t\t\t\t\t\tjustifyItems=\"flex-start\"\n\t\t\t\t\t\talignItems=\"center\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{mediaQualityInfo?.isHDR10Plus && (\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc={hdr10PlusIcon}\n\t\t\t\t\t\t\t\talt=\"hdr10+\"\n\t\t\t\t\t\t\t\tclassName=\"item-hero-mediaInfo\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{mediaQualityInfo.isDts && (\n\t\t\t\t\t\t\t<img src={dtsIcon} alt=\"dts\" className=\"item-hero-mediaInfo\" />\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{mediaQualityInfo.isDtsHDMA && (\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc={dtsHdMaIcon}\n\t\t\t\t\t\t\t\talt=\"dts-hd ma\"\n\t\t\t\t\t\t\t\tclassName=\"item-hero-mediaInfo\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{mediaQualityInfo.isAtmos && mediaQualityInfo.isDolbyVision && (\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc={dolbyVisionAtmosIcon}\n\t\t\t\t\t\t\t\talt=\"dolby vision atmos\"\n\t\t\t\t\t\t\t\tclassName=\"item-hero-mediaInfo\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{mediaQualityInfo.isAtmos && !mediaQualityInfo.isDolbyVision && (\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc={dolbyAtmosIcon}\n\t\t\t\t\t\t\t\talt=\"dolby atmos\"\n\t\t\t\t\t\t\t\tclassName=\"item-hero-mediaInfo\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{mediaQualityInfo.isDolbyVision && !mediaQualityInfo.isAtmos && (\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc={dolbyVisionIcon}\n\t\t\t\t\t\t\t\talt=\"dolby vision\"\n\t\t\t\t\t\t\t\tclassName=\"item-hero-mediaInfo\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{mediaQualityInfo.isTrueHD && (\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc={dolbyTrueHDIcon}\n\t\t\t\t\t\t\t\talt=\"dolby truehd\"\n\t\t\t\t\t\t\t\tclassName=\"item-hero-mediaInfo\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{mediaQualityInfo.isDD && (\n\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\tsrc={dolbyDigitalIcon}\n\t\t\t\t\t\t\t\talt=\"dolby digital\"\n\t\t\t\t\t\t\t\tclassName=\"item-hero-mediaInfo\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</Stack>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t\t{children}\n\t\t</div>\n\t);\n};\n\nexport default ItemHeader;\n"
  },
  {
    "path": "src/components/itemHeader/itemHeader.scss",
    "content": "@import \"../../styles/variables.scss\";\n\n$cardWidth: 18%;\n\n.item-hero {\n    height: 60vh;\n    gap: 2em;\n    row-gap: 1.2em;\n    position: relative;\n    align-items: flex-end;\n    display: grid;\n    grid-template-columns: $cardWidth 1fr;\n    grid-template-rows: 1fr auto;\n    &-image {\n        opacity: 0;\n        width: 100%;\n        object-fit: contain;\n        transition: opacity $transition-time-default;\n        &-container {\n            height: fit-content;\n            max-height: 100%;\n            overflow: hidden;\n            border-radius: $border-radius-default;\n            box-shadow: $shadow-card;\n            flex-grow: 1;\n            flex-shrink: 0;\n            position: relative;\n            width: 100%;\n        }\n        &-blurhash {\n            position: absolute !important;\n            width: 100% !important;\n            height: 100% !important;\n            z-index: -1;\n        }\n        &-icon {\n            background: linear-gradient(45deg, rgb(255 255 255 / 0.05), rgb(255 255 255 / 0.15));\n            height: 100%;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            .material-symbols-rounded{\n                font-size: 4em;\n            }\n        }\n    }\n    &-logo {\n        max-width: 34rem;\n        max-height: 10rem;\n        opacity: 0;\n        transition: opacity $transition-time-default;\n        object-position: bottom;\n        margin-bottom: 2em;\n    }\n    &-detail {\n        width: 100%;\n        align-items: flex-start;\n        gap: 0.2em;\n        justify-content: end;\n    }\n    &-buttons {\n        &-container {\n            align-items: center;\n            justify-content: space-between;\n            width: 100%;\n            grid-column: 1/3;\n            display: grid;\n            grid-template-columns: inherit;\n            gap: 2em;\n        }\n    }\n    &-backdrop {\n        width: 100vw;\n        height: 100vh;\n        object-fit: cover;\n        object-position: top;\n        opacity: 0;\n        position: absolute;\n        top: 0;\n        left: 0;\n        transition: opacity $transition-time-default;\n        &-container {\n            filter: brightness(0.8);\n            position: absolute;\n            top: -4.4em;\n            left: -$page-margin;\n            width: 100vw;\n            height: calc(64vh + 4.4em);\n            z-index: -1;\n            mask-image: linear-gradient(to top, transparent, black);\n            -webkit-mask-image: linear-gradient(\n            to top,\n            transparent,\n            black\n            );\n        }\n    }\n    & .play-button {\n        width: 100% !important;\n    }\n}\n"
  },
  {
    "path": "src/components/layouts/artist/albumArtist.scss",
    "content": "\n.album {\n\t&-image {\n\t\taspect-ratio: 1;\n\t\tborder-radius: 20px;\n\t\toverflow: hidden;\n\t\twidth: 16em;\n\t\theight: 16em;\n\t\tbox-shadow: $shadow-card-image;\n\t\tposition: relative;\n\t\tflex-shrink: 0;\n\t\t&-icon {\n\t\t\tcolor: rgb(255 255 255 / 0.5);\n\t\t\t\tfont-variation-settings: \"FILL\" 1, \"wght\" 300, \"GRAD\" 25, \"opsz\" 40;\n\n\n\t\t\t&-container {\n\t\t\t\tbackground: $clr-background-card-icon;\n\t\t\t\tposition: absolute;\n\t\t\t\twidth: 100%;\n\t\t\t\theight: 100%;\n\t\t\t\ttop: 0;\n\t\t\t\tleft: 0;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\tz-index: 1;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/components/layouts/artist/artistAlbum.tsx",
    "content": "import React from \"react\";\n\nimport Typography from \"@mui/material/Typography\";\n\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport {\n\ttype BaseItemDto,\n\tSortOrder,\n\ttype UserDto,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport LikeButton from \"../../buttons/likeButton\";\nimport PlayButton from \"../../buttons/playButton\";\n\nimport \"./albumArtist.scss\";\nimport AlbumMusicTrack from \"@/components/albumMusicTrack\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { Link } from \"@tanstack/react-router\";\n\ntype ArtistAlbumProps = {\n\tuser: UserDto;\n\talbum: BaseItemDto;\n\tboxProps?: object;\n};\n\nexport const ArtistAlbum = ({ user, album, boxProps }: ArtistAlbumProps) => {\n\tconst api = useApiInContext((s) => s.api);\n\tconst albumTracks = useQuery({\n\t\tqueryKey: [\"artist\", \"album\", album.Id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user.Id,\n\t\t\t\tparentId: album.Id,\n\t\t\t\tsortOrder: [SortOrder.Ascending],\n\t\t\t\tsortBy: [\"IndexNumber\"],\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\n\t\tnetworkMode: \"always\",\n\t});\n\n\treturn (\n\t\t<div style={{ marginBottom: \"3em\" }} {...boxProps}>\n\t\t\t<div\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\tflexDirection: \"row\",\n\t\t\t\t\tgap: \"1em\",\n\t\t\t\t\tmarginBottom: \"1em\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{!!album.ImageTags?.Primary && (\n\t\t\t\t\t<div className=\"album-image\">\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\talt={album.Name ?? \"Album Image\"}\n\t\t\t\t\t\t\tsrc={`${api?.basePath}/Items/${album.Id}/Images/Primary?fillHeight=532&fillWidth=532&quality=96`}\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\t\tobjectFit: \"cover\",\n\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t\ttransition: \"opacity 250ms\",\n\t\t\t\t\t\t\t\tposition: \"relative\",\n\t\t\t\t\t\t\t\tzIndex: 2,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tonLoad={(e) => {\n\t\t\t\t\t\t\t\te.currentTarget.style.opacity = \"1\";\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<div className=\"album-image-icon-container\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded album-image-icon\"\n\t\t\t\t\t\t\t\tstyle={{ fontSize: \"8em\" }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\talbum\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"flex flex-column\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\talignItems: \"flex-start\",\n\t\t\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\t\t\tpadding: \"1em 0\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<div className=\"flex flex-column\">\n\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\tvariant=\"h5\"\n\t\t\t\t\t\t\tfontWeight={300}\n\t\t\t\t\t\t\tstyle={{ opacity: \"0.6\" }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{album.ProductionYear}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\tto=\"/album/$id\"\n\t\t\t\t\t\t\tparams={{ id: album.Id ?? \"\" }}\n\t\t\t\t\t\t\tstyle={{ color: \"white\", textDecoration: \"none\" }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Typography variant=\"h4\" color=\"white\">\n\t\t\t\t\t\t\t\t{album.Name}\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t</Link>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\" flex flex-row\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tgap: \"1em\",\n\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<PlayButton\n\t\t\t\t\t\t\taudio\n\t\t\t\t\t\t\titem={album}\n\t\t\t\t\t\t\titemType={album.Type ?? \"MusicAlbum\"}\n\t\t\t\t\t\t\tuserId={user.Id}\n\t\t\t\t\t\t\tbuttonProps={{\n\t\t\t\t\t\t\t\t//@ts-ignore\n\t\t\t\t\t\t\t\tcolor: \"white\",\n\t\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\tcolor: \"black \",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\n\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\titemName={album.Name}\n\t\t\t\t\t\t\tkey={album.Id}\n\t\t\t\t\t\t\tqueryKey={[\n\t\t\t\t\t\t\t\t\"item\",\n\t\t\t\t\t\t\t\talbum.ParentBackdropItemId ?? \"\",\n\t\t\t\t\t\t\t\t\"artist\",\n\t\t\t\t\t\t\t\t\"discography\",\n\t\t\t\t\t\t\t]}\n\t\t\t\t\t\t\tisFavorite={album.UserData?.IsFavorite}\n\t\t\t\t\t\t\titemId={album.Id}\n\t\t\t\t\t\t\tuserId={user.Id}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t{albumTracks.isSuccess && (albumTracks.data?.Items?.length ?? 0) > 0 && (\n\t\t\t\t<div>\n\t\t\t\t\t<div className=\"item-info-track header\">\n\t\t\t\t\t\t<span className=\"material-symbols-rounded index\">tag</span>\n\t\t\t\t\t\t<Typography variant=\"subtitle1\">Title</Typography>\n\t\t\t\t\t\t<Typography variant=\"subtitle1\">Duration</Typography>\n\t\t\t\t\t</div>\n\t\t\t\t\t{albumTracks.data?.Items?.map((track, index) => (\n\t\t\t\t\t\t<AlbumMusicTrack\n\t\t\t\t\t\t\ttrack={track}\n\t\t\t\t\t\t\ttrackIndex={index}\n\t\t\t\t\t\t\tmusicTracks={albumTracks.data}\n\t\t\t\t\t\t\tkey={track.Id}\n\t\t\t\t\t\t/>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t);\n};\n"
  },
  {
    "path": "src/components/layouts/homeSection/latestMediaSection.tsx",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport React from \"react\";\nimport { Card } from \"../../card/card\";\nimport CardScroller from \"../../cardScroller/cardScroller\";\nimport { CardsSkeleton } from \"../../skeleton/cards\";\n\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport {\n\ttype BaseItemDto,\n\tBaseItemKind,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { getUserLibraryApi } from \"@jellyfin/sdk/lib/utils/api/user-library-api\";\n\n/**\n * @description Latest Media Section\n */\nexport const LatestMediaSection = ({\n\tlatestMediaLib,\n}: { latestMediaLib: BaseItemDto }) => {\n\tconst api = useApiInContext((s) => s.api);\n\tconst user = useCentralStore((s) => s.currentUser);\n\tconst fetchLatestMedia = async (library: BaseItemDto) => {\n\t\tif (!api || !user?.Id) return null;\n\t\tconst media = await getUserLibraryApi(api).getLatestMedia({\n\t\t\tuserId: user.Id,\n\t\t\tparentId: library.Id,\n\t\t\tlimit: 16,\n\t\t\tfields: [\"PrimaryImageAspectRatio\", \"ParentId\"],\n\t\t});\n\t\treturn media.data;\n\t};\n\tconst data = useQuery({\n\t\tqueryKey: [\"home\", \"latestMedia\", latestMediaLib.Id],\n\t\tqueryFn: () => fetchLatestMedia(latestMediaLib),\n\t\tenabled: !!user?.Id,\n\t\trefetchOnMount: true,\n\t});\n\tif (data.isPending) {\n\t\treturn <CardsSkeleton />;\n\t}\n\tif (data.isSuccess && (data.data?.length ?? 0) >= 1) {\n\t\treturn (\n\t\t\t<CardScroller displayCards={7} title={`Latest ${latestMediaLib.Name}`}>\n\t\t\t\t{data.data?.map((item) => {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\tkey={item.Id}\n\t\t\t\t\t\t\titem={item}\n\t\t\t\t\t\t\tseriesId={item.SeriesId}\n\t\t\t\t\t\t\tcardTitle={\n\t\t\t\t\t\t\t\titem.Type === BaseItemKind.Episode ? item.SeriesName : item.Name\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\t\t\tcardCaption={\n\t\t\t\t\t\t\t\titem.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t? `S${item.ParentIndexNumber ?? 0}:E${\n\t\t\t\t\t\t\t\t\t\t\titem.IndexNumber ?? 0\n\t\t\t\t\t\t\t\t\t\t} - ${item.Name ?? \"Unknown\"}`\n\t\t\t\t\t\t\t\t\t: item.Type === BaseItemKind.Series\n\t\t\t\t\t\t\t\t\t\t? `${item.ProductionYear} - ${\n\t\t\t\t\t\t\t\t\t\t\t\titem.EndDate\n\t\t\t\t\t\t\t\t\t\t\t\t\t? new Date(item.EndDate).toLocaleString([], {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tyear: \"numeric\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t\t\t: \"Present\"\n\t\t\t\t\t\t\t\t\t\t\t}`\n\t\t\t\t\t\t\t\t\t\t: item.ProductionYear\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcardType={\n\t\t\t\t\t\t\t\titem.Type === BaseItemKind.MusicAlbum ||\n\t\t\t\t\t\t\t\titem.Type === BaseItemKind.Audio\n\t\t\t\t\t\t\t\t\t? \"square\"\n\t\t\t\t\t\t\t\t\t: \"portrait\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tqueryKey={[\"home\", \"latestMedia\", latestMediaLib.Id ?? \"\"]}\n\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t/>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</CardScroller>\n\t\t);\n\t}\n\n\treturn <></>;\n};\n"
  },
  {
    "path": "src/components/libraryHeader/index.tsx",
    "content": "import type { BaseItemKind } from \"@jellyfin/sdk/lib/generated-client\";\nimport { ItemSortBy, SortOrder } from \"@jellyfin/sdk/lib/generated-client\";\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport {\n\tAppBar,\n\tChip,\n\tIconButton,\n\tMenuItem,\n\tTextField,\n\tTypography,\n\tuseScrollTrigger,\n} from \"@mui/material\";\nimport { useQueryClient, useSuspenseQuery } from \"@tanstack/react-query\";\nimport { getRouteApi, useNavigate } from \"@tanstack/react-router\";\nimport type { ChangeEvent } from \"react\";\nimport React, { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { AVAILABLE_VIEWS, SORT_BY_OPTIONS } from \"@/utils/constants/library\";\nimport { getLibraryQueryOptions } from \"@/utils/queries/library\";\n// import removed: header reads name/count from Zustand slice\nimport { useLibraryStateStore } from \"@/utils/store/libraryState\";\nimport useSearchStore from \"@/utils/store/search\";\nimport { NavigationDrawer } from \"../appBar/navigationDrawer\";\nimport BackButton from \"../buttons/backButton\";\nimport { FiltersDialogTrigger } from \"../filtersDialog\";\nimport { UserAvatarMenu } from \"../userAvatarMenu\";\nimport \"./libraryHeader.scss\";\n\nconst route = getRouteApi(\"/_api/library/$id\");\n\nconst MemoizeBackButton = React.memo(BackButton);\n\nexport const LibraryHeader = () => {\n\tconst { id: currentLibraryId } = route.useParams();\n\tconst { api, user } = route.useRouteContext();\n\tconst navigate = useNavigate();\n\tconst queryClient = useQueryClient();\n\n\t// Zustand library state (selected via shallow comparator)\n\tconst {\n\t\tcurrentViewType,\n\t\tstoreSortBy,\n\t\tstoreSortAscending,\n\t\tfilters,\n\t\tvts,\n\t\tnameStartsWith,\n\t\tgenreIds,\n\t\tlibraryNameFromStore,\n\t\titemsTotalCount,\n\t} = useLibraryStateStore(\n\t\tuseShallow((s) => {\n\t\t\tconst slice = s.libraries[currentLibraryId || \"\"];\n\t\t\treturn {\n\t\t\t\tcurrentViewType: slice?.currentViewType,\n\t\t\t\tstoreSortBy: slice?.sortBy,\n\t\t\t\tstoreSortAscending: slice?.sortAscending,\n\t\t\t\tfilters: slice?.filters,\n\t\t\t\tvts: slice?.videoTypesState,\n\t\t\t\tnameStartsWith: slice?.nameStartsWith,\n\t\t\t\tgenreIds: slice?.genreIds,\n\t\t\t\tlibraryNameFromStore: slice?.libraryName,\n\t\t\t\titemsTotalCount: slice?.itemsTotalCount,\n\t\t\t};\n\t\t}),\n\t);\n\tconst { initLibrary, updateLibrary } = useLibraryStateStore(\n\t\tuseShallow((s) => ({\n\t\t\tinitLibrary: s.initLibrary,\n\t\t\tupdateLibrary: s.updateLibrary,\n\t\t})),\n\t);\n\n\tuseEffect(() => {\n\t\tif (!currentLibraryId) return;\n\t\t// If the slice isn't initialized, these fields will be undefined\n\t\tif (\n\t\t\tcurrentViewType === undefined &&\n\t\t\tstoreSortBy === undefined &&\n\t\t\tstoreSortAscending === undefined\n\t\t) {\n\t\t\tinitLibrary(currentLibraryId, {\n\t\t\t\tsortBy: ItemSortBy.Name,\n\t\t\t\tsortAscending: true,\n\t\t\t});\n\t\t}\n\t}, [\n\t\tcurrentLibraryId,\n\t\tcurrentViewType,\n\t\tstoreSortBy,\n\t\tstoreSortAscending,\n\t\tinitLibrary,\n\t]);\n\tconst currentLibrary = useSuspenseQuery(\n\t\tgetLibraryQueryOptions(api, user?.Id, currentLibraryId),\n\t);\n\tconst scrollTrigger = useScrollTrigger({\n\t\tdisableHysteresis: true,\n\t\tthreshold: 20,\n\t});\n\n\tconst compatibleViews = useMemo(\n\t\t() =>\n\t\t\tAVAILABLE_VIEWS.filter((v) =>\n\t\t\t\tv.compatibleCollectionTypes.includes(\n\t\t\t\t\tcurrentLibrary.data.CollectionType as any,\n\t\t\t\t),\n\t\t\t),\n\t\t[currentLibrary.data.CollectionType],\n\t);\n\t// Values from store\n\tconst sortBy = (storeSortBy ?? ItemSortBy.Name) as string;\n\tconst sortAscending = storeSortAscending ?? true;\n\tconst effectiveViewType = currentViewType as any;\n\tconst effectiveSortByRaw = sortBy;\n\tconst effectiveSortAscending = sortAscending;\n\n\tconst normalizedViewType = useMemo(() => {\n\t\tconst values = compatibleViews.map((v) => v.value);\n\t\tif (effectiveViewType && values.includes(effectiveViewType as any))\n\t\t\treturn effectiveViewType as any;\n\t\treturn values[0] as BaseItemKind | \"Artist\";\n\t}, [effectiveViewType, compatibleViews]);\n\n\tconst normalizedSortBy = useMemo(() => {\n\t\tconst compatibleOptions = SORT_BY_OPTIONS.filter(\n\t\t\t(option) =>\n\t\t\t\toption.compatibleViewTypes?.includes(normalizedViewType as any) ||\n\t\t\t\toption.compatibleCollectionTypes?.includes(\n\t\t\t\t\tcurrentLibrary.data.CollectionType as any,\n\t\t\t\t),\n\t\t);\n\t\tconst compatibleValues = compatibleOptions.map((opt) =>\n\t\t\tArray.isArray(opt.value) ? opt.value.join(\",\") : String(opt.value),\n\t\t);\n\t\tconst current = String(effectiveSortByRaw ?? \"\");\n\t\tif (current && compatibleValues.includes(current)) return current;\n\t\treturn compatibleValues[0] ?? String(ItemSortBy.Name);\n\t}, [\n\t\teffectiveSortByRaw,\n\t\tnormalizedViewType,\n\t\tcurrentLibrary.data.CollectionType,\n\t]);\n\n\tconst handleChangeViewType = (e: ChangeEvent<HTMLInputElement>) => {\n\t\tconst next = e.target.value as unknown as BaseItemKind | \"Artist\";\n\t\tif (currentLibraryId)\n\t\t\tupdateLibrary(currentLibraryId, { currentViewType: next });\n\t};\n\tconst handleChangeSortBy = (e: ChangeEvent<HTMLInputElement>) => {\n\t\tconst next = e.target.value as unknown as string;\n\t\tif (currentLibraryId) updateLibrary(currentLibraryId, { sortBy: next });\n\t};\n\tconst handleSortAscendingToggle = () => {\n\t\tif (currentLibraryId)\n\t\t\tupdateLibrary(currentLibraryId, {\n\t\t\t\tsortAscending: !effectiveSortAscending,\n\t\t\t});\n\t};\n\n\t// (Video type multi-select temporarily removed; logic cleaned to reduce overhead.)\n\n\t// Prefetch helpers to reduce perceived latency on sort changes\n\t// filters, vts, nameStartsWith, genreIds are already selected above\n\t// Prefetch handler will be attached on menu items below\n\tconst prefetchItems = useCallback(\n\t\tasync (sortByValue: string, asc: boolean) => {\n\t\t\tif (!api || !user?.Id || !currentLibraryId || !normalizedViewType) return;\n\t\t\tconst key = [\n\t\t\t\t\"library\",\n\t\t\t\t\"items\",\n\t\t\t\tcurrentLibraryId,\n\t\t\t\t{\n\t\t\t\t\tcurrentViewType: normalizedViewType,\n\t\t\t\t\tsortAscending: asc,\n\t\t\t\t\tsortBy: sortByValue,\n\t\t\t\t\tfilters,\n\t\t\t\t\tvideoTypesState: vts,\n\t\t\t\t\tnameStartsWith,\n\t\t\t\t\tgenreIds,\n\t\t\t\t},\n\t\t\t] as const;\n\t\t\tawait queryClient.prefetchQuery({\n\t\t\t\tqueryKey: key,\n\t\t\t\tqueryFn: async () => {\n\t\t\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\t\t\tuserId: user.Id!,\n\t\t\t\t\t\tparentId: currentLibraryId,\n\t\t\t\t\t\trecursive: true,\n\t\t\t\t\t\tincludeItemTypes: [normalizedViewType as any],\n\t\t\t\t\t\tsortOrder: [asc ? SortOrder.Ascending : SortOrder.Descending],\n\t\t\t\t\t\tsortBy: String(sortByValue).split(\",\") as any,\n\t\t\t\t\t\tenableUserData: true,\n\t\t\t\t\t\tnameStartsWith: nameStartsWith || undefined,\n\t\t\t\t\t\tgenreIds: genreIds && genreIds.length > 0 ? genreIds : undefined,\n\t\t\t\t\t});\n\t\t\t\t\treturn result.data;\n\t\t\t\t},\n\t\t\t\tstaleTime: 5_000,\n\t\t\t});\n\t\t},\n\t\t[\n\t\t\tapi,\n\t\t\tuser?.Id,\n\t\t\tcurrentLibraryId,\n\t\t\tnormalizedViewType,\n\t\t\tfilters,\n\t\t\tvts,\n\t\t\tnameStartsWith,\n\t\t\tgenreIds,\n\t\t\tqueryClient,\n\t\t],\n\t);\n\n\tconst disableSortOption = useMemo(\n\t\t() =>\n\t\t\t!normalizedViewType ||\n\t\t\t!SORT_BY_OPTIONS.some(\n\t\t\t\t(option) =>\n\t\t\t\t\toption.compatibleViewTypes?.includes(normalizedViewType as any) ||\n\t\t\t\t\toption.compatibleCollectionTypes?.includes(\n\t\t\t\t\t\tcurrentLibrary.data.CollectionType as any,\n\t\t\t\t\t),\n\t\t\t),\n\t\t[normalizedViewType, currentLibrary.data.CollectionType],\n\t);\n\n\tconst appBarStyling = useMemo(\n\t\t() => ({ backgroundColor: \"transparent\", paddingRight: \"0 !important\" }),\n\t\t[],\n\t);\n\n\t// Read values from Zustand: name set by route, count set by grid\n\tconst libraryName = libraryNameFromStore ?? currentLibrary.data.Name;\n\tconst totalCount = itemsTotalCount;\n\n\tconst toggleSearchDialog = useSearchStore(\n\t\tuseShallow((s) => s.toggleSearchDialog),\n\t);\n\tconst handleNavigateToHome = useCallback(\n\t\t() => navigate({ to: \"/home\" }),\n\t\t[navigate],\n\t);\n\tconst handleNavigateToFavorite = useCallback(\n\t\t() => navigate({ to: \"/favorite\" }),\n\t\t[navigate],\n\t);\n\n\tconst [showDrawer, setShowDrawer] = useState(false);\n\n\tconst handleDrawerClose = useCallback(() => {\n\t\tsetShowDrawer(false);\n\t}, []);\n\n\tconst handleDrawerOpen = useCallback(() => {\n\t\tsetShowDrawer(true);\n\t}, []);\n\n\treturn (\n\t\t<>\n\t\t\t<AppBar\n\t\t\t\tclassName={\n\t\t\t\t\tscrollTrigger\n\t\t\t\t\t\t? \"library-header scrolling flex flex-row\"\n\t\t\t\t\t\t: \"library-header flex flex-row\"\n\t\t\t\t}\n\t\t\t\tstyle={appBarStyling}\n\t\t\t\televation={0}\n\t\t\t\tcolor=\"transparent\"\n\t\t\t>\n\t\t\t\t<div className=\"flex flex-row\" style={{ gap: \"0.6em\" }}>\n\t\t\t\t\t<IconButton onClick={handleDrawerOpen}>\n\t\t\t\t\t\t<div className=\"material-symbols-rounded\">menu</div>\n\t\t\t\t\t</IconButton>\n\t\t\t\t\t<MemoizeBackButton />\n\t\t\t\t\t<IconButton onClick={handleNavigateToHome}>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\t\tlocation.pathname === \"/home\"\n\t\t\t\t\t\t\t\t\t? \"material-symbols-rounded fill\"\n\t\t\t\t\t\t\t\t\t: \"material-symbols-rounded\"\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\thome\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</IconButton>\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"flex flex-row library-header-center\"\n\t\t\t\t\tstyle={{ gap: \"0.6em\" }}\n\t\t\t\t>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"library-title-count flex flex-row flex-align-center\"\n\t\t\t\t\t\ttitle={`${libraryName}${typeof totalCount === \"number\" ? ` · ${totalCount.toLocaleString()} items` : \"\"}`}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Typography variant=\"subtitle1\" noWrap align=\"center\">\n\t\t\t\t\t\t\t{libraryName}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t{typeof totalCount === \"number\" && (\n\t\t\t\t\t\t\t<Chip label={totalCount} sx={{ px: 0.5, ml: 1 }} />\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t\t<TextField\n\t\t\t\t\t\tselect\n\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\tvalue={String(normalizedViewType)}\n\t\t\t\t\t\tonChange={handleChangeViewType}\n\t\t\t\t\t\tSelectProps={{\n\t\t\t\t\t\t\tMenuProps: {\n\t\t\t\t\t\t\t\tPaperProps: {\n\t\t\t\t\t\t\t\t\tclassName: \"glass-menu-paper\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<MenuItem disabled value=\"\">\n\t\t\t\t\t\t\t<Typography variant=\"overline\" color=\"textSecondary\">\n\t\t\t\t\t\t\t\tView\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t{compatibleViews.map((v) => (\n\t\t\t\t\t\t\t<MenuItem key={String(v.value)} value={String(v.value)}>\n\t\t\t\t\t\t\t\t{v.title}\n\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</TextField>\n\t\t\t\t\t<IconButton\n\t\t\t\t\t\tonClick={handleSortAscendingToggle}\n\t\t\t\t\t\taria-label=\"Toggle sort order\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\ttransform: sortAscending ? \"rotateX(0deg)\" : \"rotateX(180deg)\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tsort\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</IconButton>\n\t\t\t\t\t<TextField\n\t\t\t\t\t\tselect\n\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\tvalue={normalizedSortBy}\n\t\t\t\t\t\tonChange={handleChangeSortBy}\n\t\t\t\t\t\tdisabled={disableSortOption}\n\t\t\t\t\t\tSelectProps={{\n\t\t\t\t\t\t\tMenuProps: {\n\t\t\t\t\t\t\t\tPaperProps: {\n\t\t\t\t\t\t\t\t\tclassName: \"glass-menu-paper\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<MenuItem disabled value=\"\">\n\t\t\t\t\t\t\t<Typography variant=\"overline\" color=\"textSecondary\">\n\t\t\t\t\t\t\t\tSort By\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t{SORT_BY_OPTIONS.map((option) => {\n\t\t\t\t\t\t\tconst isCompatible =\n\t\t\t\t\t\t\t\toption.compatibleViewTypes?.includes(currentViewType as any) ||\n\t\t\t\t\t\t\t\toption.compatibleCollectionTypes?.includes(\n\t\t\t\t\t\t\t\t\tcurrentLibrary.data.CollectionType as any,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (!isCompatible) return null;\n\t\t\t\t\t\t\tconst valueStr = Array.isArray(option.value)\n\t\t\t\t\t\t\t\t? option.value.join(\",\")\n\t\t\t\t\t\t\t\t: option.value;\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\t\t\tkey={\n\t\t\t\t\t\t\t\t\t\tArray.isArray(option.value)\n\t\t\t\t\t\t\t\t\t\t\t? option.value.join(\",\")\n\t\t\t\t\t\t\t\t\t\t\t: option.value\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tvalue={valueStr}\n\t\t\t\t\t\t\t\t\tonMouseEnter={() =>\n\t\t\t\t\t\t\t\t\t\tprefetchItems(String(valueStr), sortAscending)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{option.title}\n\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</TextField>\n\t\t\t\t\t<FiltersDialogTrigger />\n\t\t\t\t</div>\n\t\t\t\t<div className=\"flex flex-row\" style={{ gap: \"0.6em\" }}>\n\t\t\t\t\t<IconButton onClick={toggleSearchDialog}>\n\t\t\t\t\t\t<div className=\"material-symbols-rounded\">search</div>\n\t\t\t\t\t</IconButton>\n\t\t\t\t\t<IconButton onClick={handleNavigateToFavorite}>\n\t\t\t\t\t\t<div className=\"material-symbols-rounded\">favorite</div>\n\t\t\t\t\t</IconButton>\n\t\t\t\t\t<UserAvatarMenu />\n\t\t\t\t</div>\n\t\t\t</AppBar>\n\t\t\t<NavigationDrawer open={showDrawer} onClose={handleDrawerClose} />\n\t\t</>\n\t);\n};\n"
  },
  {
    "path": "src/components/libraryHeader/libraryHeader.scss",
    "content": ".library-header {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    inset: 1em 2em auto 2em !important;\n    border-radius: 9999px;\n    margin: 0 auto;\n    width: fit-content;\n    position: sticky;\n    width: calc(100vw - 4em) !important;\n    z-index: 10;\n    padding: 0.25em !important;\n    transition: all 0.2s ease-in-out;\n    \n    &::after {\n        content: \"\";\n        position: absolute;\n        inset: -0.35em;\n        pointer-events: none;\n        border-radius: inherit;\n        @include glass-effect;\n        opacity: 0;\n        z-index: -1;\n        transition: all 150ms;\n    }\n    &.scrolling {\n        &::after {\n            opacity: 1;\n        }\n    }\n}\n\n"
  },
  {
    "path": "src/components/libraryItemsGrid/index.tsx",
    "content": "import {\n\ttype BaseItemDto,\n\tBaseItemKind,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { useSuspenseQuery } from \"@tanstack/react-query\";\nimport { getRouteApi } from \"@tanstack/react-router\";\nimport { useWindowVirtualizer } from \"@tanstack/react-virtual\";\nimport React, {\n\tuseCallback,\n\tuseEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport \"./libraryItemsGrid.scss\";\nimport { useShallow } from \"zustand/shallow\";\nimport { getLibraryQueryOptions } from \"@/utils/queries/library\";\nimport { getLibraryItemsQueryOptions } from \"@/utils/queries/libraryItems\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport { useLibraryStateStore } from \"@/utils/store/libraryState\";\n// @ts-expect-error: Vite worker import\nimport BackdropWorker from \"@/utils/workers/backdrop.worker?worker\";\nimport { Card } from \"../card/card\";\n\nconst libraryRoute = getRouteApi(\"/_api/library/$id\");\n\n/**\n * Virtualized library items grid.\n * Virtualizes vertical scrolling only; items are arranged into responsive columns.\n * Assumptions:\n *  - Card aspect ratio handled by CSS; we approximate a fixed height for virtualization.\n *  - Width changes trigger re-measure via ResizeObserver.\n * Future improvements:\n *  - Dynamic row height based on item type.\n *  - Horizontal virtualization for extremely wide displays.\n */\nconst LibraryItemsGrid = () => {\n\tconst api = useApiInContext((s) => s.api);\n\tconst user = useCentralStore((s) => s.currentUser);\n\tconst { id: currentLibraryId } = libraryRoute.useParams();\n\tconst {\n\t\tcurrentViewType,\n\t\tsortAscending,\n\t\tsortBy,\n\t\tfilters,\n\t\tvideoTypesState,\n\t\tnameStartsWith,\n\t\tgenreIds,\n\t} = useLibraryStateStore(\n\t\tuseShallow((s) => {\n\t\t\tconst slice = s.libraries[currentLibraryId || \"\"];\n\t\t\treturn {\n\t\t\t\tcurrentViewType: slice?.currentViewType,\n\t\t\t\tsortAscending: slice?.sortAscending ?? true,\n\t\t\t\tsortBy: slice?.sortBy ?? \"Name\",\n\t\t\t\tfilters: slice?.filters,\n\t\t\t\tvideoTypesState: slice?.videoTypesState,\n\t\t\t\tnameStartsWith: slice?.nameStartsWith,\n\t\t\t\tgenreIds: slice?.genreIds,\n\t\t\t};\n\t\t}),\n\t);\n\tconst updateLibrary = useLibraryStateStore((s) => s.updateLibrary);\n\tconst currentLibrary = useSuspenseQuery(\n\t\tgetLibraryQueryOptions(api, user?.Id, currentLibraryId),\n\t);\n\n\tuseEffect(() => {\n\t\tif (currentLibraryId && !currentViewType && currentLibrary.data?.Type) {\n\t\t\tupdateLibrary(currentLibraryId, {\n\t\t\t\tcurrentViewType: currentLibrary.data.Type as BaseItemKind,\n\t\t\t});\n\t\t}\n\t}, [\n\t\tcurrentLibrary.data?.Type,\n\t\tcurrentLibraryId,\n\t\tcurrentViewType,\n\t\tupdateLibrary,\n\t]);\n\n\tconst items = useSuspenseQuery(\n\t\tgetLibraryItemsQueryOptions(api, user?.Id, currentLibraryId, {\n\t\t\tcurrentViewType,\n\t\t\tsortAscending,\n\t\t\tsortBy,\n\t\t\tfilters,\n\t\t\tvideoTypesState,\n\t\t\tnameStartsWith,\n\t\t\tgenreIds,\n\t\t\tcollectionType: currentLibrary.data.CollectionType as any,\n\t\t}),\n\t);\n\t// --- Backdrop ---\n\tconst setBackdrop = useBackdropStore(useShallow((s) => s.setBackdrop));\n\tconst backdropItems = useMemo<BaseItemDto[]>(() => {\n\t\tif (!items.isSuccess || !items.data?.Items) return [];\n\t\treturn items.data.Items.filter(\n\t\t\t(item) => Object.keys(item.ImageBlurHashes?.Backdrop ?? {}).length > 0,\n\t\t);\n\t}, [items.isSuccess, items.data?.Items]);\n\n\tuseEffect(() => {\n\t\tconst worker = new BackdropWorker();\n\n\t\tworker.onmessage = (event: MessageEvent) => {\n\t\t\tif (event.data.type === \"UPDATE_BACKDROP\") {\n\t\t\t\tconst schedule =\n\t\t\t\t\t(window as any).requestIdleCallback || window.requestAnimationFrame;\n\t\t\t\tschedule(() => setBackdrop(event.data.payload));\n\t\t\t}\n\t\t};\n\n\t\tif (backdropItems.length > 0) {\n\t\t\tworker.postMessage({\n\t\t\t\ttype: \"SET_BACKDROP_ITEMS\",\n\t\t\t\tpayload: backdropItems,\n\t\t\t});\n\t\t\tworker.postMessage({ type: \"START\" });\n\t\t}\n\n\t\treturn () => {\n\t\t\tworker.postMessage({ type: \"STOP\" });\n\t\t\tworker.terminate();\n\t\t};\n\t}, [backdropItems, setBackdrop]);\n\t// --- Virtualization setup ---\n\tconst parentRef = useRef<HTMLDivElement | null>(null);\n\tconst [containerWidth, setContainerWidth] = useState<number>(0);\n\n\t// Determine column count based on container width (simple heuristic)\n\tconst [columns, setColumns] = useState(1);\n\tconst measureColumns = useCallback(() => {\n\t\tif (!parentRef.current) return;\n\t\tconst width = parentRef.current.clientWidth;\n\t\tsetContainerWidth(width);\n\t\t// Base desired approximate card width (including gap) 180px\n\t\tconst ideal = Math.max(1, Math.floor(width / 180));\n\t\tsetColumns(ideal);\n\t}, []);\n\n\tuseEffect(() => {\n\t\tmeasureColumns();\n\t\tif (!parentRef.current) return;\n\n\t\tconst ro = new ResizeObserver(() => measureColumns());\n\t\tro.observe(parentRef.current);\n\t\treturn () => ro.disconnect();\n\t}, [measureColumns]);\n\n\tconst itemCount = items.data?.Items?.length ?? 0;\n\t// Compute number of virtual rows\n\tconst rowCount = Math.ceil(itemCount / columns);\n\n\t// Estimated row height (depends on card type). Using 260px as base (portrait taller)\n\t// Compute card width minus gaps\n\tconst cardGap = 16; // px gap between cards\n\tconst cardWidth = useMemo(() => {\n\t\tif (columns <= 0) return 180;\n\t\tconst totalGap = (columns - 1) * cardGap;\n\t\treturn Math.floor((containerWidth - totalGap) / columns);\n\t}, [columns, containerWidth]);\n\n\t// Approximate tallest card height (portrait ratio ~ 2:3 => height ≈ width / 0.666) + text/footer area\n\t// We add a small buffer (20px) for hover overlays and progress bars\n\tconst estimatedTallCardHeight = useMemo(\n\t\t() => Math.round(cardWidth / 0.666666 + 90),\n\t\t[cardWidth],\n\t);\n\n\tconst estimateRowHeight = useCallback(\n\t\t() => estimatedTallCardHeight,\n\t\t[estimatedTallCardHeight],\n\t);\n\n\tconst virtualizer = useWindowVirtualizer({\n\t\tcount: rowCount,\n\t\testimateSize: estimateRowHeight,\n\t\toverscan: 6,\n\t\tscrollMargin: parentRef.current?.offsetTop ?? 0,\n\t});\n\n\tconst virtualItems = virtualizer.getVirtualItems();\n\n\t// Update count in Zustand so header can display it without extra fetches\n\tuseEffect(() => {\n\t\tconst total = items.data?.TotalRecordCount;\n\t\tif (currentLibraryId && typeof total === \"number\") {\n\t\t\tupdateLibrary(currentLibraryId, { itemsTotalCount: total });\n\t\t}\n\t}, [items.data?.TotalRecordCount, currentLibraryId, updateLibrary]);\n\n\treturn (\n\t\t<div\n\t\t\tref={parentRef}\n\t\t\tclassName=\"library-items-container virtualized\"\n\t\t\tstyle={{ position: \"relative\" }}\n\t\t>\n\t\t\t{items.isSuccess && itemCount === 0 ? (\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\tminHeight: 240,\n\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\topacity: 0.8,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<div style={{ textAlign: \"center\" }}>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\tstyle={{ fontSize: 48, marginBottom: 8 }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tsentiment_dissatisfied\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div style={{ fontSize: 16 }}>No items to show</div>\n\t\t\t\t\t\t<div style={{ fontSize: 13, marginTop: 6 }}>\n\t\t\t\t\t\t\tTry adjusting filters, sort, or the A–Z selector.\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t) : (\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tposition: \"relative\",\n\t\t\t\t\t\theight: virtualizer.getTotalSize(),\n\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{virtualItems.map((row) => {\n\t\t\t\t\t\tconst startIndex = row.index * columns;\n\t\t\t\t\t\tconst endIndex = Math.min(startIndex + columns, itemCount);\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={row.key}\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\t\t\ttransform: `translateY(${\n\t\t\t\t\t\t\t\t\t\trow.start - virtualizer.options.scrollMargin\n\t\t\t\t\t\t\t\t\t}px)`,\n\t\t\t\t\t\t\t\t\tleft: 0,\n\t\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\tflexDirection: \"row\",\n\t\t\t\t\t\t\t\t\talignItems: \"flex-start\",\n\t\t\t\t\t\t\t\t\tgap: `${cardGap}px`,\n\t\t\t\t\t\t\t\t\t// Allow natural height; virtualizer will update size after measurement.\n\t\t\t\t\t\t\t\t\tpaddingBottom: \"0.6em\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tdata-index={row.index}\n\t\t\t\t\t\t\t\tref={(el) => {\n\t\t\t\t\t\t\t\t\tif (el) virtualizer.measureElement(el);\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{items.data?.Items?.slice(startIndex, endIndex).map(\n\t\t\t\t\t\t\t\t\t(item: BaseItemDto, localIndex: number) => {\n\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\tkey={item?.Id ?? localIndex}\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\twidth: `${cardWidth}px`,\n\t\t\t\t\t\t\t\t\t\t\t\t\tflex: \"0 0 auto\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Override default card margins to avoid compounding with gaps\n\t\t\t\t\t\t\t\t\t\t\t\t\tmargin: 0,\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\t\t\t\t\titem={item}\n\t\t\t\t\t\t\t\t\t\t\t\t\tseriesId={item?.SeriesId}\n\t\t\t\t\t\t\t\t\t\t\t\t\tskipInView\n\t\t\t\t\t\t\t\t\t\t\t\t\tcardTitle={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem?.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? item.SeriesName\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: item?.Name\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\tcardCaption={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem?.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: item?.Type === BaseItemKind.Series\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? `${item.ProductionYear} - ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\titem.EndDate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? new Date(item.EndDate).toLocaleString(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t[],\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tyear: \"numeric\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"Present\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}`\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: item?.ProductionYear\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tdisableOverlay={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem?.Type === BaseItemKind.Person ||\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem?.Type === BaseItemKind.Genre ||\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem?.Type === BaseItemKind.MusicGenre ||\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem?.Type === BaseItemKind.Studio\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tcardType={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem?.Type === BaseItemKind.MusicAlbum ||\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem?.Type === BaseItemKind.Audio ||\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem?.Type === BaseItemKind.Genre ||\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem?.Type === BaseItemKind.MusicGenre ||\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem?.Type === BaseItemKind.Studio ||\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem?.Type === BaseItemKind.Playlist\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? \"square\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"portrait\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t\t\t\t\t\t// Inline style overrides to neutralize original margins for virtualization\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t);\n};\nexport default LibraryItemsGrid;\n"
  },
  {
    "path": "src/components/libraryItemsGrid/libraryItemsGrid.scss",
    "content": "// Overrides for virtualized library items layout to prevent horizontal overflow\n.library-items-container.virtualized {\n  overflow-x: hidden;\n  box-sizing: border-box;\n  width: 100%;\n  display: block !important; // override grid from route styles\n  padding: 0 !important; // override padding from route styles\n  position: relative; // required for absolutely positioned rows\n  gap: 0 !important;\n  justify-items: normal !important;\n  align-content: stretch !important;\n  > .card {\n    max-width: none !important;\n  }\n  // Ensure no unintended padding expands width\n  padding-right: 0;\n  padding-left: 0;\n  .card {\n    margin-right: 0 !important;\n    margin-bottom: 0.6em !important; // keep a consistent vertical rhythm\n  }\n  .card-image-container {\n    // Remove box shadow spread from affecting layout width calculation\n    box-sizing: border-box;\n  }\n}\n\n// When page has audio-playing class, avoid extra bottom padding causing scrollbars\n.audio-playing .library-items-container.virtualized {\n  padding-bottom: 8em; // mimic original intent without affecting width\n}\n"
  },
  {
    "path": "src/components/listItemLink/index.tsx",
    "content": "import {\n\tListItem,\n\tListItemButton,\n\ttype ListItemProps,\n\tListItemText,\n} from \"@mui/material\";\nimport { createLink, Link } from \"@tanstack/react-router\";\nimport React, { forwardRef, type ReactNode } from \"react\";\n\ninterface MUIListItemLinkProps extends ListItemProps<\"a\"> {\n\ticon?: ReactNode;\n\tprimary: string;\n\tclassName?: string;\n}\n\nconst CreatedListItemLink = forwardRef<HTMLAnchorElement, MUIListItemLinkProps>(\n\t(props, ref) => (\n\t\t<ListItem component=\"a\" ref={ref} {...props}>\n\t\t\t<ListItemButton\n\t\t\t\tsx={{\n\t\t\t\t\tcolor: \"inherit\",\n\t\t\t\t\ttextDecoration: \"none\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<div className=\"material-symbols-rounded\">{props.icon}</div>\n\t\t\t\t<ListItemText primary={props.primary} />\n\t\t\t</ListItemButton>\n\t\t</ListItem>\n\t),\n);\n\nconst ListItemLink = createLink(CreatedListItemLink);\n\nexport default ListItemLink;\n\n/* export default function ListItemLink(props: ListItemLinkProps) {\n\tconst { icon, primary, to, className } = props;\n\n\treturn (\n\t\t<li>\n\t\t\t{/* @ts-expect-error /*}\n\t\t\t<ListItem\n\t\t\t\tcomponent={Link}\n\t\t\t\tactiveClassName=\"active\"\n\t\t\t\tclassName={className}\n\t\t\t\tto={to}\n\t\t\t>\n\t\t\t\t<ListItemButton\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tborderRadius: \"100px\",\n\t\t\t\t\t\tgap: \"0.85em\",\n\t\t\t\t\t\tcolor: \"white\",\n\t\t\t\t\t\ttextDecoration: \"none\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<div className=\"material-symbols-rounded\">{icon}</div>\n\t\t\t\t\t<ListItemText primary={primary} />\n\t\t\t\t</ListItemButton>\n\t\t\t</ListItem>\n\t\t</li>\n\t);\n}\n */\n"
  },
  {
    "path": "src/components/musicTrack/index.tsx",
    "content": "import Typography from \"@mui/material/Typography\";\nimport React from \"react\";\nimport { getRuntimeMusic } from \"../../utils/date/time\";\nimport { useAudioPlayback } from \"../../utils/store/audioPlayback\";\nimport LikeButton from \"../buttons/likeButton\";\nimport PlayButton from \"../buttons/playButton\";\nimport \"./musicTrack.scss\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport { Link } from \"@tanstack/react-router\";\nimport lyrics_icon from \"../../assets/icons/lyrics.svg\";\n\n/**\n * @description Music Track element displayed in ArtistTitlePage and Songs library\n */\nconst MusicTrack = ({\n\titem,\n\tqueryKey,\n\tuserId,\n\tplaylistItem = false,\n\tplaylistItemId,\n\ttrackIndex,\n\tclassName = \"\",\n}: {\n\titem: BaseItemDto;\n\tqueryKey: string[];\n\tuserId?: string | undefined;\n\tplaylistItem?: boolean;\n\tplaylistItemId?: string;\n\ttrackIndex?: number;\n\tclassName?: string;\n}) => {\n\tconst api = useApiInContext((s) => s.api);\n\tconst [currentTrackItem] = useAudioPlayback((state) => [state.item]);\n\n\treturn (\n\t\t<div className={`music-track ${className}`}>\n\t\t\t<div className=\"music-track-image\">\n\t\t\t\t<div className=\"music-track-icon\">\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\tstyle={{ fontSize: \"2em !important\" }}\n\t\t\t\t\t>\n\t\t\t\t\t\tmusic_note\n\t\t\t\t\t</span>\n\t\t\t\t</div>\n\t\t\t\t<img\n\t\t\t\t\talt={item.Name ?? \"Music Track\"}\n\t\t\t\t\tsrc={\n\t\t\t\t\t\tapi &&\n\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t(Object.keys(item.ImageTags ?? {}).length === 0\n\t\t\t\t\t\t\t\t? item.AlbumId\n\t\t\t\t\t\t\t\t: item.Id) ?? \"\",\n\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tquality: 70,\n\t\t\t\t\t\t\t\tfillHeight: 128,\n\t\t\t\t\t\t\t\tfillWidth: 128,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\tobjectFit: \"cover\",\n\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t}}\n\t\t\t\t\tonLoad={(e) => {\n\t\t\t\t\t\te.currentTarget.style.opacity = \"1\";\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t\t<div className=\"music-track-image-overlay\">\n\t\t\t\t\t<PlayButton\n\t\t\t\t\t\titem={item}\n\t\t\t\t\t\titemType=\"Audio\"\n\t\t\t\t\t\tuserId={userId}\n\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\ticonOnly\n\t\t\t\t\t\taudio\n\t\t\t\t\t\tplaylistItem={playlistItem}\n\t\t\t\t\t\tplaylistItemId={playlistItemId}\n\t\t\t\t\t\ttrackIndex={trackIndex}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div className=\"music-track-info\">\n\t\t\t\t<Typography\n\t\t\t\t\tvariant=\"subtitle1\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tcolor:\n\t\t\t\t\t\t\titem.Id === currentTrackItem?.Id ? \"hsl(337, 96%, 56%)\" : \"white\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{item.Name}\n\t\t\t\t</Typography>\n\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\topacity: 0.8,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{item.HasLyrics && (\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tsrc={lyrics_icon}\n\t\t\t\t\t\t\talt=\"Lyrics\"\n\t\t\t\t\t\t\tclassName=\"music-track-info-lyrics\"\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t\t{item.ArtistItems?.map((artist, index) => (\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tstyle={{ display: \"inline-flex\", alignItems: \"center\" }}\n\t\t\t\t\t\t\tkey={artist.Id}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\tfontWeight=\"400\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\ttextDecoration: \"none\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\tstyle={{ textDecoration: \"none\", color: \"white\" }}\n\t\t\t\t\t\t\t\t\tto=\"/artist/$id\"\n\t\t\t\t\t\t\t\t\tparams={{ id: artist.Id ?? \"\" }}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{artist.Name}\n\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t{index !== (item.ArtistItems?.length ?? 0) - 1 && (\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\twhiteSpace: \"pre\",\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t,{\" \"}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</span>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t{getRuntimeMusic(item.RunTimeTicks ?? 0)}\n\t\t\t</Typography>\n\t\t\t<LikeButton\n\t\t\t\titemId={item.Id}\n\t\t\t\tisFavorite={item.UserData?.IsFavorite}\n\t\t\t\tqueryKey={queryKey}\n\t\t\t\tuserId={userId}\n\t\t\t\titemName={item.Name}\n\t\t\t/>\n\t\t</div>\n\t);\n};\n\nexport default MusicTrack;\n"
  },
  {
    "path": "src/components/musicTrack/musicTrack.scss",
    "content": "\n.music-track {\n\tdisplay: grid;\n\tgrid-template-columns: 4.5em 80% 1fr 1fr;\n\tgap: 0.5em;\n\talign-items: center;\n\tjustify-items: center;\n\twidth: 100%;\n\tcursor: pointer;\n\t&-icon {\n\t\tposition: absolute;\n\t\ttop: 0;\n\t\tleft: 0;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tbackground: rgb(255 255 255 / 0.09);\n\t\tz-index: -1;\n\t}\n\t&-image {\n\t\tmargin: 0.4em;\n\t\tborder-radius: $border-radius_04;\n\t\toverflow: hidden;\n\t\taspect-ratio: 1;\n\t\tposition: relative;\n\t\twidth: 4em;\n\t\tbox-shadow: 0 0 10px rgb(0 0 0 / 0.2);\n\t\t\ttransition: opacity $transition-time-default;\n\t\t&-overlay {\n\t\t\tposition: absolute;\n\t\t\ttop: 0;\n\t\t\tleft: 0;\n\t\t\tbackdrop-filter: blur(10px);\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\tdisplay: flex;\n\t\t\tjustify-content: center;\n\t\t\talign-items: center;\n\t\t\topacity: 0;\n\t\t\ttransition: opacity $transition-time-fast;\n\t\t}\n\t}\n\t&-info {\n\t\tjustify-self: start;\n\t\t&-lyrics {\n\t\t\t\tmargin-right: 0.4em;\n\t\t\t}\n\t}\n\n\t&:hover {\n\t\t.music-track-image-overlay {\n\t\t\topacity: 1;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/components/nProgress/index.tsx",
    "content": "import { LinearProgress } from \"@mui/material\";\nimport { useNProgress } from \"@tanem/react-nprogress\";\nimport { useIsFetching, useIsMutating } from \"@tanstack/react-query\";\nimport { useRouterState } from \"@tanstack/react-router\";\nimport React, { useMemo } from \"react\";\nimport { createPortal } from \"react-dom\";\n\nexport default function NProgress() {\n\tconst isQueryFetching = useIsFetching();\n\tconst isMutating = useIsMutating();\n\tconst routeIsLoading = useRouterState().isLoading;\n\n\tconst isAnimating = useMemo(() => {\n\t\tif (isQueryFetching || isMutating || routeIsLoading) {\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}, [isQueryFetching, isMutating, routeIsLoading]);\n\n\tconst { animationDuration, isFinished, progress } = useNProgress({\n\t\tisAnimating,\n\t});\n\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\tpointerEvents: \"none\",\n\t\t\t\ttransition: \"opacity 250ms linear\",\n\t\t\t\tposition: \"fixed\",\n\t\t\t\ttop: \"0\",\n\t\t\t\tleft: \"0\",\n\t\t\t\tright: \"0\",\n\t\t\t\tzIndex: \"10001\",\n\t\t\t\topacity: isFinished ? 0 : 1,\n\t\t\t}}\n\t\t>\n\t\t\t{isAnimating && (\n\t\t\t\t<LinearProgress\n\t\t\t\t\tsx={{\n\t\t\t\t\t\theight: 3,\n\t\t\t\t\t\ttransitionDuration: animationDuration,\n\t\t\t\t\t}}\n\t\t\t\t\tvalue={progress * 100}\n\t\t\t\t\tvariant=\"determinate\"\n\t\t\t\t/>\n\t\t\t)}\n\t\t</div>\n\t);\n};\n\n"
  },
  {
    "path": "src/components/notices/emptyNotice/emptyNotice.tsx",
    "content": "\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\nimport { yellow } from \"@mui/material/colors\";\nimport React from \"react\";\n\nexport const EmptyNotice = ({ extraMsg }: { extraMsg?: string }) => {\n\treturn (\n\t\t<Box\n\t\t\tsx={{\n\t\t\t\twidth: \"100%\",\n\t\t\t\theight: \"100%\",\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\tjustifyContent: \"center\",\n\t\t\t\talignItems: \"center\",\n\t\t\t\tflexFlow: \"column\",\n\t\t\t\tposition: \"static\",\n\t\t\t\ttop: 0,\n\t\t\t\tleft: 0,\n\t\t\t}}\n\t\t>\n\t\t\t<div\n\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\tstyle={{\n\t\t\t\t\tfontSize: \"10em\",\n\t\t\t\t\tcolor: yellow[800],\n\t\t\t\t\tfontVariationSettings: '\"FILL\" 1, \"wght\" 400, \"GRAD\" 25, \"opsz\" 60',\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\tsentiment_dissatisfied\n\t\t\t</div>\n\t\t\t<Typography variant=\"h4\" fontWeight={300}>\n\t\t\t\tNo results found\n\t\t\t</Typography>\n\t\t\t<Typography variant=\"subtitle1\" fontWeight={300} style={{ opacity: 0.5 }}>\n\t\t\t\t{extraMsg}\n\t\t\t</Typography>\n\t\t</Box>\n\t);\n};\n"
  },
  {
    "path": "src/components/notices/errorNotice/errorNotice.tsx",
    "content": "import Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\nimport { red } from \"@mui/material/colors\";\nimport React from \"react\";\n\nexport const ErrorNotice = ({ error }: { error?: Error }) => {\n\tconsole.error(error);\n\treturn (\n\t\t<Box\n\t\t\tsx={{\n\t\t\t\twidth: \"100%\",\n\t\t\t\theight: \"100vh\",\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\tjustifyContent: \"center\",\n\t\t\t\talignItems: \"center\",\n\t\t\t\tflexFlow: \"column\",\n\t\t\t\t// opacity: 0.2,\n\t\t\t}}\n\t\t>\n\t\t\t<div\n\t\t\t\tclassName=\"material-symbols-rounded animate-icon\"\n\t\t\t\tstyle={{\n\t\t\t\t\tfontSize: \"10em\",\n\t\t\t\t\tcolor: red[800],\n\t\t\t\t\t//@ts-ignore\n\t\t\t\t\t\"--clr\": \"rgb(198 40 40 / 30%)\",\n\t\t\t\t\tfontVariationSettings: '\"FILL\" 1, \"wght\" 400, \"GRAD\" 25, \"opsz\" 60',\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\terror\n\t\t\t</div>\n\t\t\t<Typography variant=\"h3\" color=\"white\" fontWeight={300}>\n\t\t\t\tSomething went wrong.\n\t\t\t</Typography>\n\t\t\t<Typography\n\t\t\t\tfontFamily=\"JetBrains Mono Variable\"\n\t\t\t\tfontWeight={100}\n\t\t\t\tvariant=\"h5\"\n\t\t\t\tstyle={{\n\t\t\t\t\tpadding: \"0.5em\",\n\t\t\t\t\tbackground: \"rgb(0 0 0 / 1)\",\n\t\t\t\t\tborderRadius: \"8px\",\n\t\t\t\t\tborder: \"2px dashed rgb(255 255 255 / 0.5)\",\n\t\t\t\t\tmaxWidth: \"40em\",\n\t\t\t\t\tmaxHeight: \"20em\",\n\t\t\t\t\toverflow: \"auto\",\n\t\t\t\t\tmarginTop: \"1em\",\n\t\t\t\t\t// opacity: 0.6,\n\t\t\t\t}}\n\t\t\t\tcolor=\"gray\"\n\t\t\t>\n\t\t\t\t{JSON.stringify(error?.stack)}\n\t\t\t</Typography>\n\t\t</Box>\n\t);\n};\n"
  },
  {
    "path": "src/components/outroCard/index.tsx",
    "content": "import { useApiInContext } from \"@/utils/store/api\";\nimport { playItemFromQueue } from \"@/utils/store/playback\";\nimport useQueue from \"@/utils/store/queue\";\nimport { Button, Typography } from \"@mui/material\";\nimport React from \"react\";\nimport { getTypeIcon } from \"../utils/iconsCollection\";\n\nimport \"./outroCard.scss\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport { useMutation } from \"@tanstack/react-query\";\n\nconst OutroCard = (props: { handleShowCredits: () => void }) => {\n\tconst api = useApiInContext((s) => s.api);\n\tconst [nextItemIndex, queueItems] = useQueue((s) => [\n\t\ts.currentItemIndex,\n\t\ts.tracks,\n\t]);\n\tconst item = queueItems?.[nextItemIndex + 1];\n\tconst user = useCentralStore((s) => s.currentUser);\n\tconst handlePlayNext = useMutation({\n\t\tmutationKey: [\"playNextButton\"],\n\t\tmutationFn: () => playItemFromQueue(\"next\", user?.Id, api),\n\t\tonError: (error) => [console.error(error)],\n\t});\n\tif (!item || !item.Type) return null;\n\treturn (\n\t\t<div className=\"outro-card\">\n\t\t\t<Typography variant=\"h4\">Up Next</Typography>\n\t\t\t<div className=\"outro-card-content\">\n\t\t\t\t{item?.ImageTags?.Primary ? (\n\t\t\t\t\t<img\n\t\t\t\t\t\tclassName=\"outro-card-content-image\"\n\t\t\t\t\t\talt={item.Name ?? \"Cover\"}\n\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\titem.Id ?? \"\",\n\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\ttag: item.ImageTags.Primary,\n\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t) : (\n\t\t\t\t\t<div className=\"outro-card-content-image icon\">\n\t\t\t\t\t\t{getTypeIcon(item.Type)}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t\t<Typography variant=\"h5\" className=\"outro-card-content-title\">\n\t\t\t\t\t{item.Name}\n\t\t\t\t</Typography>\n\t\t\t\t<Typography variant=\"subtitle2\" className=\"outro-card-content-overview\">\n\t\t\t\t\t{item.Overview}\n\t\t\t\t</Typography>\n\t\t\t\t<div className=\"outro-card-content-buttons\">\n\t\t\t\t\t<Button\n\t\t\t\t\t\tonClick={handlePlayNext.mutate}\n\t\t\t\t\t\t//@ts-ignore\n\t\t\t\t\t\tcolor=\"white\"\n\t\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\t\tstartIcon={\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded fill\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tzIndex: 1,\n\t\t\t\t\t\t\t\t\tfontSize: \"2em\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tplay_arrow\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\tPlay Now\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tonClick={props.handleShowCredits}\n\t\t\t\t\t\t//@ts-ignore\n\t\t\t\t\t\tcolor=\"white\"\n\t\t\t\t\t\tvariant=\"outlined\"\n\t\t\t\t\t>\n\t\t\t\t\t\tWatch Credits\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n};\n\nexport default OutroCard;"
  },
  {
    "path": "src/components/outroCard/outroCard.scss",
    "content": ".outro-card {\n    position: absolute;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    z-index: 1000;\n    display: flex;\n    flex-direction: column;\n    padding: 0 5vw 2em 5vw;\n    gap: 1.2em;\n    background: linear-gradient(to top, rgb(10 10 10), rgb(10 10 10 / 0.7) 50%, transparent);\n    padding-top: 25vh;\n\n    &-content {\n        display: grid;\n        grid-template-areas: \"img title\"\n            \"img info\"\n            \"img buttons\";\n        grid-template-columns: 20em 1fr;\n        align-items: center;\n        gap: 1em;\n\n        &-image {\n            grid-area: img;\n            aspect-ratio: 16/9;\n            width: 20em;\n            object-fit: cover;\n            border-radius: $border-radius-default;\n        }\n\n        &-title {\n            grid-area: title;\n        }\n\n        &-overview {\n            grid-area: info;\n            display: -webkit-box;\n            text-overflow: ellipsis;\n            overflow: hidden;\n            -webkit-line-clamp: 3;\n            -webkit-box-orient: vertical;\n        }\n\n        &-buttons {\n            grid-area: buttons;\n            display: flex;\n            gap: 1em;\n        }\n    }\n}"
  },
  {
    "path": "src/components/playback/audioPlayer/audioPlayer.scss",
    "content": "$image-size: 4em;\n\n.audio-player {\n\tposition: fixed;\n\tbottom: 1rem;\n\tleft: 50%;\n\ttransform: translateX(-50%);\n\twidth: 95%;\n\tmax-width: 1200px;\n\tz-index: 100;\n\tdisplay: grid;\n\tpadding: 0.75em 1.5em;\n\tgap: 1.5em;\n\tbox-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);\n\tgrid-template-columns: 25% 1fr 25%;\n    @include glass-effect($blur: 12px, $bg-color: rgba(18, 18, 18, 0.85));\n\t// background: rgba(18, 18, 18, 0.85) !important;\n\t// backdrop-filter: blur(12px);\n\t// -webkit-backdrop-filter: blur(12px);\n\t// border: 1px solid rgba(255, 255, 255, 0.1);\n\tborder-radius: 1rem;\n\talign-items: center;\n\n\t@media (max-width: 900px) {\n\t\tgrid-template-columns: 1fr auto;\n\t\tpadding: 0.5em 1em;\n\t\tbottom: 0;\n\t\twidth: 100%;\n\t\tborder-radius: 0;\n\t\tborder-bottom: none;\n\t\tborder-left: none;\n\t\tborder-right: none;\n\t\t\n\t\t.audio-player-controls {\n\t\t\tdisplay: none; // Hide controls on mobile for now or adjust\n\t\t}\n\t}\n\n\t#waveform {\n\t\twidth: 100%;\n\t\ttransform: scaleY(0);\n\t\ttransition: transform $transition-time-default;\n\t\t&[data-show=\"true\"] {\n\t\t\ttransform: scaleY(1) !important;\n\t\t}\n\t}\n\t&-controls {\n\t\tdisplay: flex;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 0.5em;\n\t\tpadding: 0.5em 0;\n\t\tflex-flow: column;\n\t}\n\t&-info {\n\t\tdisplay: grid;\n\t\tgap: 1em;\n\t\tgrid-template-columns: minmax(0, $image-size) minmax(0, 1fr);\n\t\twidth: fit-content;\n\t\talign-items: center;\n\t\tflex-grow: 1;\n\t\twidth: 100%;\n\t\toverflow: hidden;\n\n\t\t&-text {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\toverflow: hidden;\n\t\t\tmin-width: 0;\n\t\t}\n\t}\n\t&-image {\n\t\twidth: $image-size;\n\t\taspect-ratio: 1;\n\t\tobject-fit: cover;\n\t\tz-index: 1;\n\t\tflex-shrink: 0;\n\t\tflex-grow: 1;\n\t\t\n\t\tposition: relative;\n\t\t&-container {\n\t\t\tborder-radius: $border-radius_04;\n\t\t\tposition: relative;\n\t\t\toverflow: hidden;\n\t\t\theight: fit-content;\n\t\t\taspect-ratio: 1;\n\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t}\n\t\t&-icon {\n\t\t\tposition: absolute;\n\t\t\tleft: 50%;\n\t\t\ttop: 50%;\n\t\t\ttransform: translate(-50%, -50%);\n\t\t\tfont-size: 2em !important;\n\t\t\tz-index: 0;\n\t\t}\n\t}\n\t&-track {\n\t\tgrid-template-columns: $image-size 60% 0.5fr 0.5fr !important;\n\t}\n\t&-playlist{\n\t\tbackground: $clr-background-dark !important;\n\t\tborder: 1.2px solid rgb(255 255 255 / 0.2);\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 1em;\n\t\t// Magic scrollbar\n\t\t&::-webkit-scrollbar {\n\t\t\t&-thumb {\n\t\t\t\theight: 150px;\n\t\t\t\tmax-height: 33%;\n\t\t\t}\n\t\t\t&-track {\n\t\t\t\tmargin-top: 20px;\n\t\t\t\tmargin-bottom: 20px;\n\t\t\t\tbackground: transparent !important;\n\t\t\t}\n\t\t}\n\t\t\n\t\t&-track{\n\t\t\tdisplay: grid;\n\t\t\tgap: 1em;\n\t\t\tgrid-template-columns: 4em 1fr 4em;\n\t\t\tcursor: pointer;\n\t\t\t&.active{\n\t\t\t\tcolor: $clr-accent-default;\n\t\t\t}\n\t\t\t&-image{\n\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\tborder: 1.2px solid rgb(255 255 255 / 0.1);\n\t\t\t\toverflow: hidden;\n\t\t\t\tborder-radius:5px;\n\t\t\t\twidth: 100%;\n\t\t\t\taspect-ratio: 1;\n\t\t\t\theight: 4em;\n\t\t\t}\n\t\t}\n\t\t\n\t}\n}\n"
  },
  {
    "path": "src/components/playback/audioPlayer/components/LyricsPanel.tsx",
    "content": "import type { Api } from \"@jellyfin/sdk\";\nimport type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport { getLyricsApi } from \"@jellyfin/sdk/lib/utils/api/lyrics-api\";\nimport { Typography } from \"@mui/material\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { secToTicks } from \"@/utils/date/time\";\n\ninterface LyricsPanelProps {\n\titem: BaseItemDto | undefined | null;\n\tapi: Api | undefined;\n\tcurrentTime: number;\n}\n\nconst LyricsPanel = ({ item, api, currentTime }: LyricsPanelProps) => {\n\tconst lyricsContainer = useRef<HTMLDivElement | null>(null);\n\n\tconst lyrics = useQuery({\n\t\tqueryKey: [\"player\", \"audio\", item?.Id, \"lyrics\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!item?.Id || !api) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst response = await getLyricsApi(api).getLyrics({\n\t\t\t\t\titemId: item?.Id,\n\t\t\t\t});\n\t\t\t\treturn response.data;\n\t\t\t} catch (e) {\n\t\t\t\tconsole.error(\"Failed to fetch lyrics\", e);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t},\n\t\tenabled: Boolean(item?.Id) && Boolean(api),\n\t});\n\n\tuseEffect(() => {\n\t\tif (lyrics.data && lyricsContainer.current) {\n\t\t\tconst currentLyric = lyricsContainer.current.querySelector(\n\t\t\t\t\"[data-active-lyric='true']\",\n\t\t\t);\n\t\t\tif (currentLyric) {\n\t\t\t\tcurrentLyric.scrollIntoView({\n\t\t\t\t\tblock: \"center\",\n\t\t\t\t\tinline: \"nearest\",\n\t\t\t\t\tbehavior: \"smooth\",\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}, [currentTime, lyrics.data]);\n\n\tif (lyrics.isLoading) {\n\t\treturn (\n\t\t\t<div className=\"audio-lyrics-status\">\n\t\t\t\t<Typography>Loading lyrics...</Typography>\n\t\t\t</div>\n\t\t);\n\t}\n\n\tif (lyrics.isError || !lyrics.data || !lyrics.data.Lyrics?.length) {\n\t\treturn (\n\t\t\t<div className=\"audio-lyrics-status\">\n\t\t\t\t<Typography>No lyrics available</Typography>\n\t\t\t</div>\n\t\t);\n\t}\n\n\tconst hasSyncedLyrics = (lyrics.data.Lyrics[0].Start ?? -1) >= 0;\n\n\treturn (\n\t\t<div className=\"audio-lyrics\" data-has-synced-lyrics={hasSyncedLyrics}>\n\t\t\t<div className=\"audio-lyrics-container\" ref={lyricsContainer}>\n\t\t\t\t{lyrics.data.Lyrics.map((lyric, index) => {\n\t\t\t\t\tconst isActive =\n\t\t\t\t\t\thasSyncedLyrics &&\n\t\t\t\t\t\tsecToTicks(currentTime) >= (lyric.Start ?? 0) &&\n\t\t\t\t\t\tsecToTicks(currentTime) <\n\t\t\t\t\t\t\t(lyrics.data?.Lyrics?.[index + 1]?.Start ??\n\t\t\t\t\t\t\t\tNumber.POSITIVE_INFINITY);\n\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\tvariant=\"body1\"\n\t\t\t\t\t\t\tcomponent=\"p\"\n\t\t\t\t\t\t\tclassName={`audio-lyrics-line ${isActive ? \"active\" : \"\"}`}\n\t\t\t\t\t\t\tkey={`${lyric.Text}-${index}`}\n\t\t\t\t\t\t\tdata-active-lyric={isActive}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{lyric.Text}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</div>\n\t\t\t{!hasSyncedLyrics && (\n\t\t\t\t<div className=\"audio-lyrics-unsynced-notice\">\n\t\t\t\t\t<span className=\"material-symbols-rounded\">info</span>\n\t\t\t\t\t<Typography variant=\"caption\">Synced lyrics not available</Typography>\n\t\t\t\t</div>\n\t\t\t)}\n\t\t</div>\n\t);\n};\n\nexport default React.memo(LyricsPanel);\n"
  },
  {
    "path": "src/components/playback/audioPlayer/components/PlayerActions.tsx",
    "content": "import IconButton from \"@mui/material/IconButton\";\nimport type { ReactNode } from \"react\";\nimport React from \"react\";\nimport QueueButton from \"@/components/buttons/queueButton\";\n\ninterface PlayerActionsProps {\n\tonNavigate: () => void;\n\tonClose: () => void;\n\tchildren?: ReactNode;\n}\n\nconst PlayerActions = ({\n\tonNavigate,\n\tonClose,\n\tchildren,\n}: PlayerActionsProps) => {\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\talignItems: \"center\",\n\t\t\t\tjustifyContent: \"flex-end\",\n\t\t\t}}\n\t\t>\n\t\t\t<IconButton onClick={onNavigate}>\n\t\t\t\t<span className=\"material-symbols-rounded\">info</span>\n\t\t\t</IconButton>\n\t\t\t<QueueButton />\n\t\t\t{children}\n\t\t\t<IconButton onClick={onClose}>\n\t\t\t\t<div className=\"material-symbols-rounded\">close</div>\n\t\t\t</IconButton>\n\t\t</div>\n\t);\n};\n\nexport default PlayerActions;\n"
  },
  {
    "path": "src/components/playback/audioPlayer/components/PlayerControls.tsx",
    "content": "import Fab from \"@mui/material/Fab\";\nimport IconButton from \"@mui/material/IconButton\";\nimport React from \"react\";\nimport PlayNextButton from \"@/components/buttons/playNextButton\";\nimport PlayPreviousButton from \"@/components/buttons/playPreviousButtom\";\n\ninterface PlayerControlsProps {\n\tplaying: boolean;\n\tonPlayPause: () => void;\n\tsize?: \"small\" | \"medium\" | \"large\";\n\tonRewind?: () => void;\n\tonForward?: () => void;\n}\n\nconst PlayerControls = ({\n\tplaying,\n\tonPlayPause,\n\tsize = \"small\",\n\tonRewind,\n\tonForward,\n}: PlayerControlsProps) => {\n\treturn (\n\t\t<div style={{ display: \"flex\", gap: \"1em\", alignItems: \"center\" }}>\n\t\t\t<PlayPreviousButton />\n\t\t\t{onRewind && (\n\t\t\t\t<IconButton onClick={onRewind}>\n\t\t\t\t\t<span className=\"material-symbols-rounded\">fast_rewind</span>\n\t\t\t\t</IconButton>\n\t\t\t)}\n\t\t\t<div\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: \"inline-flex\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\tposition: \"relative\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Fab size={size} onClick={onPlayPause}>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tfontSize: size === \"large\" ? \"2.4em\" : \"2em\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{playing ? \"pause\" : \"play_arrow\"}\n\t\t\t\t\t</div>\n\t\t\t\t</Fab>\n\t\t\t</div>\n\t\t\t{onForward && (\n\t\t\t\t<IconButton onClick={onForward}>\n\t\t\t\t\t<span className=\"material-symbols-rounded\">fast_forward</span>\n\t\t\t\t</IconButton>\n\t\t\t)}\n\t\t\t<PlayNextButton />\n\t\t</div>\n\t);\n};\n\nexport default React.memo(PlayerControls);\n"
  },
  {
    "path": "src/components/playback/audioPlayer/components/PlayerInfo.tsx",
    "content": "import type { Api } from \"@jellyfin/sdk\";\nimport type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport Typography from \"@mui/material/Typography\";\nimport React from \"react\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\n\ninterface PlayerInfoProps {\n\titem: BaseItemDto | undefined | null;\n\tapi: Api | undefined;\n\ttrackName?: string;\n}\n\nconst PlayerInfo = ({ item, api, trackName }: PlayerInfoProps) => {\n\tif (!api || !item) return null;\n\n\treturn (\n\t\t<div className=\"audio-player-info\">\n\t\t\t<div className=\"audio-player-image-container\">\n\t\t\t\t<img\n\t\t\t\t\talt={trackName ?? \"track\"}\n\t\t\t\t\tclassName=\"audio-player-image\"\n\t\t\t\t\tsrc={getImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t(!item?.ImageTags?.Primary ? item?.AlbumId : item.Id) ?? \"\",\n\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tquality: 85,\n\t\t\t\t\t\t\tfillHeight: 462,\n\t\t\t\t\t\t\tfillWidth: 462,\n\t\t\t\t\t\t},\n\t\t\t\t\t)}\n\t\t\t\t/>\n\t\t\t\t<span className=\"material-symbols-rounded audio-player-image-icon\">\n\t\t\t\t\tmusic_note\n\t\t\t\t</span>\n\t\t\t</div>\n\t\t\t<div className=\"audio-player-info-text\">\n\t\t\t\t<Typography\n\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t}}\n\t\t\t\t\tnoWrap\n\t\t\t\t>\n\t\t\t\t\t{item?.Name}\n\t\t\t\t</Typography>\n\t\t\t\t<Typography\n\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t}}\n\t\t\t\t\tnoWrap\n\t\t\t\t>\n\t\t\t\t\tby {item?.Artists?.map((artist) => artist).join(\",\")}\n\t\t\t\t</Typography>\n\t\t\t</div>\n\t\t</div>\n\t);\n};\n\nexport default PlayerInfo;\n"
  },
  {
    "path": "src/components/playback/audioPlayer/components/PlayerProgress.tsx",
    "content": "import Slider from \"@mui/material/Slider\";\nimport Typography from \"@mui/material/Typography\";\nimport React, { useEffect, useState } from \"react\";\nimport { getRuntimeMusic } from \"@/utils/date/time\";\n\ninterface PlayerProgressProps {\n\tprogress: number;\n\tduration: number;\n\tonSeek?: (value: number) => void;\n\tonSeekCommit: (value: number) => void;\n}\n\nconst PlayerProgress = ({\n\tprogress,\n\tduration,\n\tonSeek,\n\tonSeekCommit,\n}: PlayerProgressProps) => {\n\tconst [isScrubbing, setIsScrubbing] = useState(false);\n\tconst [sliderValue, setSliderValue] = useState(progress);\n\n\tuseEffect(() => {\n\t\tif (!isScrubbing) {\n\t\t\tsetSliderValue(progress);\n\t\t}\n\t}, [progress, isScrubbing]);\n\n\tconst handleChange = (_: Event, value: number | number[]) => {\n\t\tsetIsScrubbing(true);\n\t\tconst newValue = Array.isArray(value) ? value[0] : value;\n\t\tsetSliderValue(newValue);\n\t\tonSeek?.(newValue);\n\t};\n\n\tconst handleChangeCommitted = (_: unknown, value: number | number[]) => {\n\t\tsetIsScrubbing(false);\n\t\tconst newValue = Array.isArray(value) ? value[0] : value;\n\t\tonSeekCommit(newValue);\n\t};\n\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\twidth: \"100%\",\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\tgap: \"1em\",\n\t\t\t\talignItems: \"center\",\n\t\t\t\tjustifyContent: \"center\",\n\t\t\t}}\n\t\t>\n\t\t\t<Typography\n\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\tfontWeight={300}\n\t\t\t\tstyle={{\n\t\t\t\t\topacity: 0.8,\n\t\t\t\t\tminWidth: \"3.5em\",\n\t\t\t\t\ttextAlign: \"right\",\n\t\t\t\t\tfontVariantNumeric: \"tabular-nums\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{getRuntimeMusic(sliderValue)}\n\t\t\t</Typography>\n\t\t\t<Slider\n\t\t\t\tvalue={sliderValue}\n\t\t\t\tstep={1}\n\t\t\t\tsize=\"small\"\n\t\t\t\tmax={duration}\n\t\t\t\tonChange={handleChange}\n\t\t\t\tonChangeCommitted={handleChangeCommitted}\n\t\t\t/>\n\t\t\t<Typography\n\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\tfontWeight={300}\n\t\t\t\tstyle={{\n\t\t\t\t\topacity: 0.8,\n\t\t\t\t\tminWidth: \"3.5em\",\n\t\t\t\t\ttextAlign: \"left\",\n\t\t\t\t\tfontVariantNumeric: \"tabular-nums\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{getRuntimeMusic(duration)}\n\t\t\t</Typography>\n\t\t</div>\n\t);\n};\n\nexport default PlayerProgress;\n"
  },
  {
    "path": "src/components/playback/audioPlayer/components/PlayerVolume.tsx",
    "content": "import IconButton from \"@mui/material/IconButton\";\nimport Slider from \"@mui/material/Slider\";\nimport React from \"react\";\n\ninterface PlayerVolumeProps {\n\tvolume: number;\n\tisMuted: boolean;\n\tonVolumeChange: (value: number) => void;\n\tonMuteToggle: () => void;\n}\n\nconst PlayerVolume = ({\n\tvolume,\n\tisMuted,\n\tonVolumeChange,\n\tonMuteToggle,\n}: PlayerVolumeProps) => {\n\tconst [localVolume, setLocalVolume] = React.useState(volume * 100);\n\n\tReact.useEffect(() => {\n\t\tsetLocalVolume(volume * 100);\n\t}, [volume]);\n\n\treturn (\n\t\t<>\n\t\t\t<IconButton onClick={onMuteToggle}>\n\t\t\t\t<div className=\"material-symbols-rounded\">\n\t\t\t\t\t{isMuted ? \"volume_mute\" : \"volume_up\"}\n\t\t\t\t</div>\n\t\t\t</IconButton>\n\t\t\t<Slider\n\t\t\t\tvalue={isMuted ? 0 : localVolume}\n\t\t\t\tstep={1}\n\t\t\t\tmax={100}\n\t\t\t\tonChange={(_, newVal) => {\n\t\t\t\t\tconst newVolume = Array.isArray(newVal) ? newVal[0] : newVal;\n\t\t\t\t\tsetLocalVolume(newVolume);\n\t\t\t\t\tonVolumeChange(newVolume / 100);\n\t\t\t\t}}\n\t\t\t\tvalueLabelDisplay=\"auto\"\n\t\t\t\tvalueLabelFormat={(value) => Math.floor(value)}\n\t\t\t\tsize=\"small\"\n\t\t\t\tsx={{\n\t\t\t\t\tmr: 1,\n\t\t\t\t\tml: 1,\n\t\t\t\t\twidth: \"10em\",\n\t\t\t\t\t\"& .MuiSlider-valueLabel\": {\n\t\t\t\t\t\tlineHeight: 1.2,\n\t\t\t\t\t\tfontSize: 24,\n\t\t\t\t\t\tbackground: \"rgb(0 0 0 / 0.5)\",\n\t\t\t\t\t\tbackdropFilter: \"blur(5px)\",\n\t\t\t\t\t\tpadding: 1,\n\t\t\t\t\t\tborderRadius: \"10px\",\n\t\t\t\t\t\tborder: \"1px solid rgb(255 255 255 / 0.15)\",\n\t\t\t\t\t\tboxShadow: \"0 0 10px rgb(0 0 0 / 0.4) \",\n\t\t\t\t\t\ttransform: \"translatey(-120%) scale(0)\",\n\t\t\t\t\t\t\"&:before\": {\n\t\t\t\t\t\t\tdisplay: \"none\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"&.MuiSlider-valueLabelOpen\": {\n\t\t\t\t\t\t\ttransform: \"translateY(-120%) scale(1)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"& > *\": {\n\t\t\t\t\t\t\ttransform: \"rotate(0deg)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}}\n\t\t\t/>\n\t\t</>\n\t);\n};\n\nexport default PlayerVolume;\n"
  },
  {
    "path": "src/components/playback/audioPlayer/components/QueuePanel.tsx",
    "content": "import {\n\tclosestCenter,\n\tDndContext,\n\ttype DragEndEvent,\n\tDragOverlay,\n\ttype DragStartEvent,\n\ttype DropAnimation,\n\tdefaultDropAnimationSideEffects,\n\tKeyboardSensor,\n\tPointerSensor,\n\tuseSensor,\n\tuseSensors,\n} from \"@dnd-kit/core\";\nimport {\n\trestrictToVerticalAxis,\n\trestrictToWindowEdges,\n} from \"@dnd-kit/modifiers\";\nimport {\n\tarrayMove,\n\tSortableContext,\n\tsortableKeyboardCoordinates,\n\tuseSortable,\n\tverticalListSortingStrategy,\n} from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport {\n\tBox,\n\tIconButton,\n\tMenu,\n\tMenuItem,\n\tTooltip,\n\tTypography,\n} from \"@mui/material\";\nimport React, { useMemo, useState } from \"react\";\nimport QueueListItem from \"@/components/queueListItem\";\nimport { stopPlayback } from \"@/utils/store/audioPlayback\";\nimport useQueue, {\n\tclearQueue,\n\tremoveFromQueue,\n\tsetQueue,\n\tshuffleUpcoming,\n} from \"@/utils/store/queue\";\n\ninterface QueuePanelProps {\n\tqueue: BaseItemDto[];\n\tcurrentTrackIndex: number;\n}\n\nconst dropAnimation: DropAnimation = {\n\tsideEffects: defaultDropAnimationSideEffects({\n\t\tstyles: {\n\t\t\tactive: {\n\t\t\t\topacity: \"0.5\",\n\t\t\t},\n\t\t},\n\t}),\n};\n\nfunction SortableQueueItem({\n\titem,\n\tonDelete,\n\tindex,\n\tisActive,\n}: {\n\titem: BaseItemDto;\n\tonDelete: () => void;\n\tindex: number;\n\tisActive: boolean;\n}) {\n\tconst {\n\t\tattributes,\n\t\tlisteners,\n\t\tsetNodeRef,\n\t\ttransform,\n\t\ttransition,\n\t\tisDragging,\n\t} = useSortable({ id: item.Id || \"\" });\n\n\tconst style = {\n\t\ttransform: CSS.Translate.toString(transform),\n\t\ttransition,\n\t\topacity: isDragging ? 0.3 : 1,\n\t\tposition: \"relative\" as const,\n\t\tzIndex: isDragging ? 999 : \"auto\",\n\t};\n\n\treturn (\n\t\t<div ref={setNodeRef} style={style} className=\"sortable-queue-item\">\n\t\t\t<QueueListItem\n\t\t\t\tqueueItem={item}\n\t\t\t\tonDelete={onDelete}\n\t\t\t\tdragHandleProps={{ ...attributes, ...listeners }}\n\t\t\t\tisEpisode={item.Type === \"Episode\"}\n\t\t\t\tindex={index}\n\t\t\t\tclassName={isActive ? \"active\" : \"\"}\n\t\t\t\tsx={{\n\t\t\t\t\tbgcolor: isActive\n\t\t\t\t\t\t? \"rgba(255, 255, 255, 0.1) !important\"\n\t\t\t\t\t\t: \"transparent\",\n\t\t\t\t\tborderRadius: 2,\n\t\t\t\t\tmb: 1,\n\t\t\t\t\ttransition: \"background-color 0.2s\",\n\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\tbgcolor: \"rgba(255, 255, 255, 0.05)\",\n\t\t\t\t\t},\n\t\t\t\t\t\".MuiListItemText-primary\": {\n\t\t\t\t\t\tcolor: isActive ? \"var(--primary-color, #90caf9)\" : \"inherit\",\n\t\t\t\t\t\tfontWeight: isActive ? 600 : 500,\n\t\t\t\t\t\tfontSize: \"1rem\",\n\t\t\t\t\t},\n\t\t\t\t\t\".MuiListItemText-secondary\": {\n\t\t\t\t\t\tfontSize: \"0.85rem\",\n\t\t\t\t\t\topacity: 0.7,\n\t\t\t\t\t},\n\t\t\t\t}}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n\nconst QueuePanel = ({ queue, currentTrackIndex }: QueuePanelProps) => {\n\tconst [activeId, setActiveId] = useState<string | null>(null);\n\n\tconst sensors = useSensors(\n\t\tuseSensor(PointerSensor, {\n\t\t\tactivationConstraint: {\n\t\t\t\tdistance: 8,\n\t\t\t},\n\t\t}),\n\t\tuseSensor(KeyboardSensor, {\n\t\t\tcoordinateGetter: sortableKeyboardCoordinates,\n\t\t}),\n\t);\n\n\tconst activeItem = useMemo(\n\t\t() => queue.find((item) => item.Id === activeId),\n\t\t[queue, activeId],\n\t);\n\n\tconst handleDragStart = (event: DragStartEvent) => {\n\t\tsetActiveId(String(event.active.id));\n\t};\n\n\tconst handleDragEnd = (event: DragEndEvent) => {\n\t\tconst { active, over } = event;\n\t\tsetActiveId(null);\n\n\t\tif (active.id !== over?.id) {\n\t\t\tconst oldIndex = queue.map((item) => item.Id).indexOf(String(active.id));\n\t\t\tconst newIndex = queue.map((item) => item.Id).indexOf(String(over?.id));\n\n\t\t\tif (oldIndex !== -1 && newIndex !== -1) {\n\t\t\t\tconst newQueue = arrayMove(queue, oldIndex, newIndex);\n\t\t\t\tconst currentTrackId = queue[currentTrackIndex]?.Id;\n\t\t\t\tconst newCurrentTrackIndex = newQueue.findIndex(\n\t\t\t\t\t(item) => item.Id === currentTrackId,\n\t\t\t\t);\n\n\t\t\t\tsetQueue(\n\t\t\t\t\tnewQueue,\n\t\t\t\t\tnewCurrentTrackIndex !== -1 ? newCurrentTrackIndex : 0,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t};\n\n\tconst [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);\n\tconst open = Boolean(anchorEl);\n\tconst handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n\t\tsetAnchorEl(event.currentTarget);\n\t};\n\tconst handleClose = () => {\n\t\tsetAnchorEl(null);\n\t};\n\n\tconst handleClearQueue = () => {\n\t\tstopPlayback();\n\t\tclearQueue();\n\t\thandleClose();\n\t};\n\n\treturn (\n\t\t<DndContext\n\t\t\tsensors={sensors}\n\t\t\tcollisionDetection={closestCenter}\n\t\t\tonDragStart={handleDragStart}\n\t\t\tonDragEnd={handleDragEnd}\n\t\t\tmodifiers={[restrictToVerticalAxis, restrictToWindowEdges]}\n\t\t>\n\t\t\t<div\n\t\t\t\tclassName=\"audio-queue-container\"\n\t\t\t\tstyle={{\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\theight: \"100%\",\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Box\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\tmb: 2,\n\t\t\t\t\t\tpx: 1,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Typography variant=\"h6\" fontWeight={700}>\n\t\t\t\t\t\tQueue\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<Box sx={{ display: \"flex\", gap: 1 }}>\n\t\t\t\t\t\t<Tooltip title=\"Shuffle Upcoming\">\n\t\t\t\t\t\t\t<IconButton onClick={() => shuffleUpcoming()} size=\"small\">\n\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">shuffle</span>\n\t\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t</Tooltip>\n\t\t\t\t\t\t<IconButton onClick={handleClick} size=\"small\">\n\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">more_vert</span>\n\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t</Box>\n\t\t\t\t\t<Menu\n\t\t\t\t\t\tanchorEl={anchorEl}\n\t\t\t\t\t\topen={open}\n\t\t\t\t\t\tonClose={handleClose}\n\t\t\t\t\t\ttransformOrigin={{ horizontal: \"right\", vertical: \"top\" }}\n\t\t\t\t\t\tanchorOrigin={{ horizontal: \"right\", vertical: \"bottom\" }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<MenuItem onClick={handleClearQueue}>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\tstyle={{ marginRight: \"0.5em\", fontSize: \"1.2em\" }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tclear_all\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\tClear Queue\n\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t</Menu>\n\t\t\t\t</Box>\n\n\t\t\t\t<SortableContext\n\t\t\t\t\titems={queue.map((track) => track.Id ?? \"\")}\n\t\t\t\t\tstrategy={verticalListSortingStrategy}\n\t\t\t\t>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"audio-queue\"\n\t\t\t\t\t\tstyle={{ display: \"flex\", flexDirection: \"column\", gap: \"0px\" }}\n\t\t\t\t\t>\n\t\t\t\t\t\t{queue.map((track, index) => (\n\t\t\t\t\t\t\t<SortableQueueItem\n\t\t\t\t\t\t\t\tkey={track.Id}\n\t\t\t\t\t\t\t\titem={track}\n\t\t\t\t\t\t\t\tindex={index}\n\t\t\t\t\t\t\t\tonDelete={() => removeFromQueue(index)}\n\t\t\t\t\t\t\t\tisActive={currentTrackIndex === index}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</div>\n\t\t\t\t</SortableContext>\n\n\t\t\t\t<DragOverlay\n\t\t\t\t\tdropAnimation={dropAnimation}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tleft: \"1.5rem\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{activeItem ? (\n\t\t\t\t\t\t<QueueListItem\n\t\t\t\t\t\t\tqueueItem={activeItem}\n\t\t\t\t\t\t\tisEpisode={activeItem.Type === \"Episode\"}\n\t\t\t\t\t\t\tdragHandleProps={{}} // Pass empty object to render handle\n\t\t\t\t\t\t\tonDelete={() => {}} // Pass empty function to render delete button space\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\tbgcolor: \"rgba(40, 40, 40, 0.9)\",\n\t\t\t\t\t\t\t\tbackdropFilter: \"blur(10px)\",\n\t\t\t\t\t\t\t\tborderRadius: 2,\n\t\t\t\t\t\t\t\tboxShadow: \"0 8px 32px rgba(0,0,0,0.5)\",\n\t\t\t\t\t\t\t\toutline: \"1px solid rgba(255, 255, 255, 0.1)\",\n\t\t\t\t\t\t\t\tmb: 1,\n\t\t\t\t\t\t\t\t\".MuiListItemText-primary\": {\n\t\t\t\t\t\t\t\t\tfontWeight: 500,\n\t\t\t\t\t\t\t\t\tfontSize: \"1rem\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\".MuiListItemText-secondary\": {\n\t\t\t\t\t\t\t\t\tfontSize: \"0.85rem\",\n\t\t\t\t\t\t\t\t\topacity: 0.7,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : null}\n\t\t\t\t</DragOverlay>\n\t\t\t</div>\n\t\t</DndContext>\n\t);\n};\n\nexport default React.memo(QueuePanel);\n"
  },
  {
    "path": "src/components/playback/audioPlayer/components/StatsPanel.tsx",
    "content": "import type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport { Box, Paper, Typography } from \"@mui/material\";\nimport React, { useEffect, useState } from \"react\";\n\ninterface StatsPanelProps {\n\titem: BaseItemDto | undefined | null;\n\taudioRef: React.RefObject<HTMLAudioElement> | null;\n}\n\nconst StatRow = ({\n\tlabel,\n\tvalue,\n}: {\n\tlabel: string;\n\tvalue: string | number | undefined | null;\n}) => (\n\t<Box\n\t\tsx={{\n\t\t\tdisplay: \"flex\",\n\t\t\tjustifyContent: \"space-between\",\n\t\t\tmb: 1,\n\t\t\tborderBottom: \"1px solid rgba(255,255,255,0.1)\",\n\t\t\tpb: 0.5,\n\t\t}}\n\t>\n\t\t<Typography variant=\"body2\" sx={{ opacity: 0.7, fontFamily: \"monospace\" }}>\n\t\t\t{label}\n\t\t</Typography>\n\t\t<Typography\n\t\t\tvariant=\"body2\"\n\t\t\tsx={{ fontFamily: \"monospace\", fontWeight: 600 }}\n\t\t>\n\t\t\t{value || \"-\"}\n\t\t</Typography>\n\t</Box>\n);\n\nconst StatsPanel: React.FC<StatsPanelProps> = ({ item, audioRef }) => {\n\tconst [buffered, setBuffered] = useState<string>(\"0%\");\n\n\tuseEffect(() => {\n\t\tconst player = audioRef?.current;\n\t\tif (!player) return;\n\n\t\tconst updateStats = () => {\n\t\t\tif (player.buffered.length > 0) {\n\t\t\t\tconst bufferedEnd = player.buffered.end(player.buffered.length - 1);\n\t\t\t\tconst duration = player.duration;\n\t\t\t\tif (duration > 0) {\n\t\t\t\t\tsetBuffered(`${Math.round((bufferedEnd / duration) * 100)}%`);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tconst interval = setInterval(updateStats, 1000);\n\t\treturn () => clearInterval(interval);\n\t}, [audioRef]);\n\n\tconst mediaSource = item?.MediaSources?.[0];\n\tconst mediaStream = mediaSource?.MediaStreams?.find(\n\t\t(s) => s.Type === \"Audio\",\n\t);\n\n\treturn (\n\t\t<Box sx={{ p: 2 }}>\n\t\t\t<Typography variant=\"h6\" sx={{ mb: 2 }}>\n\t\t\t\tStats for Nerds\n\t\t\t</Typography>\n\n\t\t\t<Paper\n\t\t\t\tsx={{\n\t\t\t\t\tp: 2,\n\t\t\t\t\tbackground: \"rgba(0,0,0,0.2)\",\n\t\t\t\t\tbackdropFilter: \"blur(10px)\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Typography\n\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\tsx={{ mb: 1, opacity: 0.5, textTransform: \"uppercase\" }}\n\t\t\t\t>\n\t\t\t\t\tMedia Source\n\t\t\t\t</Typography>\n\t\t\t\t<StatRow label=\"Container\" value={mediaSource?.Container} />\n\t\t\t\t<StatRow\n\t\t\t\t\tlabel=\"Bitrate\"\n\t\t\t\t\tvalue={\n\t\t\t\t\t\tmediaSource?.Bitrate\n\t\t\t\t\t\t\t? `${Math.round(mediaSource.Bitrate / 1000)} kbps`\n\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t<StatRow\n\t\t\t\t\tlabel=\"Size\"\n\t\t\t\t\tvalue={\n\t\t\t\t\t\tmediaSource?.Size\n\t\t\t\t\t\t\t? `${(mediaSource.Size / 1024 / 1024).toFixed(2)} MB`\n\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t\t<StatRow label=\"Path\" value={mediaSource?.Path} />\n\n\t\t\t\t<Box sx={{ mt: 3 }}>\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\tsx={{ mb: 1, opacity: 0.5, textTransform: \"uppercase\" }}\n\t\t\t\t\t>\n\t\t\t\t\t\tAudio Stream\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<StatRow label=\"Codec\" value={mediaStream?.Codec} />\n\t\t\t\t\t<StatRow label=\"Channels\" value={mediaStream?.Channels} />\n\t\t\t\t\t<StatRow\n\t\t\t\t\t\tlabel=\"Sample Rate\"\n\t\t\t\t\t\tvalue={\n\t\t\t\t\t\t\tmediaStream?.SampleRate\n\t\t\t\t\t\t\t\t? `${mediaStream.SampleRate} Hz`\n\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t\t<StatRow label=\"Bit Depth\" value={mediaStream?.BitDepth} />\n\t\t\t\t\t<StatRow label=\"Language\" value={mediaStream?.Language} />\n\t\t\t\t</Box>\n\n\t\t\t\t<Box sx={{ mt: 3 }}>\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\tsx={{ mb: 1, opacity: 0.5, textTransform: \"uppercase\" }}\n\t\t\t\t\t>\n\t\t\t\t\t\tPlayer\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<StatRow label=\"Buffered\" value={buffered} />\n\t\t\t\t\t<StatRow\n\t\t\t\t\t\tlabel=\"Volume\"\n\t\t\t\t\t\tvalue={\n\t\t\t\t\t\t\taudioRef?.current\n\t\t\t\t\t\t\t\t? `${Math.round(audioRef.current.volume * 100)}%`\n\t\t\t\t\t\t\t\t: undefined\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\t\t\t\t\t<StatRow\n\t\t\t\t\t\tlabel=\"Playback Rate\"\n\t\t\t\t\t\tvalue={audioRef?.current?.playbackRate}\n\t\t\t\t\t/>\n\t\t\t\t</Box>\n\t\t\t</Paper>\n\t\t</Box>\n\t);\n};\n\nexport default React.memo(StatsPanel);\n"
  },
  {
    "path": "src/components/playback/audioPlayer/index.tsx",
    "content": "import { useLocation, useNavigate } from \"@tanstack/react-router\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport React, { type SyntheticEvent, useEffect, useRef, useState } from \"react\";\nimport { secToTicks, ticksToSec } from \"@/utils/date/time\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport {\n\tsetAudioRef,\n\tsetIsMuted,\n\tsetVolume,\n\tuseAudioPlayback,\n} from \"@/utils/store/audioPlayback\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport { playItemFromQueue } from \"@/utils/store/playback\";\nimport useQueue from \"@/utils/store/queue\";\nimport PlayerActions from \"./components/PlayerActions\";\nimport PlayerControls from \"./components/PlayerControls\";\nimport PlayerInfo from \"./components/PlayerInfo\";\nimport PlayerProgress from \"./components/PlayerProgress\";\nimport PlayerVolume from \"./components/PlayerVolume\";\n\nimport \"./audioPlayer.scss\";\n\nconst AudioPlayer = () => {\n\tconst api = useApiInContext((s) => s.api);\n\tconst user = useCentralStore((s) => s.currentUser);\n\tconst navigate = useNavigate();\n\tconst location = useLocation();\n\t\n\tconst audioRef = useRef<HTMLAudioElement | null>(null);\n\tconst [url, display, item, volume, isMuted] = useAudioPlayback((state) => [\n\t\tstate.url,\n\t\tstate.display,\n\t\tstate.item,\n\t\tstate.player.volume,\n\t\tstate.player.isMuted,\n\t]);\n\t\n\tconst [tracks, currentTrack] = useQueue((state) => [\n\t\tstate.tracks,\n\t\tstate.currentItemIndex,\n\t]);\n\t\n\tconst [playing, setPlaying] = useState(false);\n\tconst [progress, setProgress] = useState(0);\n\t\n\t// Sync volume and ref\n\tuseEffect(() => {\n\t\tif (display) {\n\t\t\tsetAudioRef(audioRef);\n\n\t\t\tif (audioRef.current) {\n\t\t\t\taudioRef.current.volume = isMuted ? 0 : volume;\n\t\t\t}\n\t\t}\n\t}, [display, url, tracks?.[currentTrack]?.Id]);\n\t\n\t// Update volume when state changes\n\tuseEffect(() => {\n\t\tif (audioRef.current) {\n\t\t\taudioRef.current.volume = isMuted ? 0 : volume;\n\t\t}\n\t}, [volume, isMuted]);\n\t\n\t// Media Session Metadata\n\tuseEffect(() => {\n\t\tif (\"mediaSession\" in navigator && item) {\n\t\t\tconst imageUrl = item.ImageTags?.Primary\n\t\t\t\t? api &&\n\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(item.Id ?? \"\", \"Primary\", {\n\t\t\t\t\t\ttag: item.ImageTags.Primary,\n\t\t\t\t\t})\n\t\t\t\t: undefined;\n\n\t\t\tnavigator.mediaSession.metadata = new MediaMetadata({\n\t\t\t\ttitle: item.Name ?? \"Unknown Title\",\n\t\t\t\tartist: item.Artists?.join(\", \") ?? \"Unknown Artist\",\n\t\t\t\talbum: item.Album ?? \"Unknown Album\",\n\t\t\t\tartwork: imageUrl\n\t\t\t\t\t? [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tsrc: imageUrl,\n\t\t\t\t\t\t\t\tsizes: \"512x512\",\n\t\t\t\t\t\t\t\ttype: \"image/jpeg\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t]\n\t\t\t\t\t: [],\n\t\t\t});\n\t\t}\n\t}, [item, api]);\n\t\n\t// Media Session Action Handlers\n\tuseEffect(() => {\n\t\tif (\"mediaSession\" in navigator) {\n\t\t\tnavigator.mediaSession.setActionHandler(\"play\", () => {\n\t\t\t\taudioRef.current?.play();\n\t\t\t});\n\t\t\tnavigator.mediaSession.setActionHandler(\"pause\", () => {\n\t\t\t\taudioRef.current?.pause();\n\t\t\t});\n\t\t\tnavigator.mediaSession.setActionHandler(\"previoustrack\", () => {\n\t\t\t\tif (user?.Id && api) {\n\t\t\t\t\tplayItemFromQueue(\"previous\", user.Id, api);\n\t\t\t\t}\n\t\t\t});\n\t\t\tnavigator.mediaSession.setActionHandler(\"nexttrack\", () => {\n\t\t\t\tif (user?.Id && api) {\n\t\t\t\t\tplayItemFromQueue(\"next\", user.Id, api);\n\t\t\t\t}\n\t\t\t});\n\t\t\tnavigator.mediaSession.setActionHandler(\"seekto\", (details) => {\n\t\t\t\tif (audioRef.current && details.seekTime !== undefined) {\n\t\t\t\t\taudioRef.current.currentTime = details.seekTime;\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t}, [api, user?.Id]);\n\t\n\tconst updatePositionState = () => {\n\t\tif (\n\t\t\t\"mediaSession\" in navigator &&\n\t\t\taudioRef.current &&\n\t\t\t!Number.isNaN(audioRef.current.duration)\n\t\t) {\n\t\t\ttry {\n\t\t\t\tnavigator.mediaSession.setPositionState({\n\t\t\t\t\tduration: audioRef.current.duration,\n\t\t\t\t\tplaybackRate: audioRef.current.playbackRate,\n\t\t\t\t\tposition: audioRef.current.currentTime,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\"Error updating media session position state:\", error);\n\t\t\t}\n\t\t}\n\t};\n\t\n\tconst handlePlayPause = () => {\n\t\tif (audioRef.current) {\n\t\t\tif (audioRef.current.paused) {\n\t\t\t\taudioRef.current.play();\n\t\t\t\tsetPlaying(true);\n\t\t\t} else {\n\t\t\t\taudioRef.current.pause();\n\t\t\t\tsetPlaying(false);\n\t\t\t}\n\t\t}\n\t};\n\t\n\tconst handleTimeUpdate = (e: SyntheticEvent<HTMLAudioElement>) => {\n\t\tsetProgress(secToTicks(e.currentTarget.currentTime));\n\t};\n\t\n\tconst handleEnded = () => {\n\t\tsetPlaying(false);\n\t\tif (tracks?.[currentTrack + 1]?.Id && api && user?.Id) {\n\t\t\tplayItemFromQueue(\"next\", user.Id, api);\n\t\t}\n\t};\n\t\n\tconst handleSeekCommit = (value: number) => {\n\t\tif (audioRef.current) {\n\t\t\taudioRef.current.currentTime = ticksToSec(value);\n\t\t\tsetProgress(value);\n\t\t}\n\t};\n\t\n\tconst handleVolumeChange = (newVolume: number) => {\n\t\tsetVolume(newVolume);\n\t};\n\t\n\tconst handleMuteToggle = () => {\n\t\tsetIsMuted(!isMuted);\n\t};\n\t\n\tconst handleClose = () => {\n\t\tuseAudioPlayback.setState(useAudioPlayback.getInitialState());\n\t};\n\t\n\tconst handleNavigate = () => {\n\t\tnavigate({ to: \"/player/audio\" });\n\t};\n\t\n\t// Listen to play/pause events directly from audio element to keep state in sync\n\t// (e.g. if paused by media keys)\n\tconst onPlay = () => {\n\t\tsetPlaying(true);\n\t\tupdatePositionState();\n\t\tif (\"mediaSession\" in navigator) {\n\t\t\tnavigator.mediaSession.playbackState = \"playing\";\n\t\t}\n\t};\n\tconst onPause = () => {\n\t\tsetPlaying(false);\n\t\tupdatePositionState();\n\t\tif (\"mediaSession\" in navigator) {\n\t\t\tnavigator.mediaSession.playbackState = \"paused\";\n\t\t}\n\t};\n\t\n\treturn (\n\t\t<AnimatePresence mode=\"sync\">\n\t\t\t{display && (\n\t\t\t\t<motion.div\n\t\t\t\t\tinitial={{ transform: \"translate(-50%, 150%)\" }}\n\t\t\t\t\tanimate={{\n\t\t\t\t\t\ttransform:\n\t\t\t\t\t\t\tlocation.pathname === \"/player/audio\"\n\t\t\t\t\t\t\t\t? \"translate(-50%, 150%)\"\n\t\t\t\t\t\t\t\t: \"translate(-50%, 0%)\",\n\t\t\t\t\t}}\n\t\t\t\t\texit={{ transform: \"translate(-50%, 150%)\" }}\n\t\t\t\t\ttransition={{ duration: 0.3, ease: \"circOut\" }}\n\t\t\t\t\tclassName=\"audio-player glass\"\n\t\t\t\t>\n\t\t\t\t\t<PlayerInfo\n\t\t\t\t\t\titem={item}\n\t\t\t\t\t\tapi={api}\n\t\t\t\t\t\ttrackName={tracks?.[currentTrack]?.Name ?? \"Unknown Track\"}\n\t\t\t\t\t/>\n\n\t\t\t\t\t<audio\n\t\t\t\t\t\tautoPlay\n\t\t\t\t\t\tsrc={url}\n\t\t\t\t\t\tref={audioRef}\n\t\t\t\t\t\tkey={item?.Id ?? \"noItem\"}\n\t\t\t\t\t\tonTimeUpdate={handleTimeUpdate}\n\t\t\t\t\t\tonEnded={handleEnded}\n\t\t\t\t\t\tonPlay={onPlay}\n\t\t\t\t\t\tonPause={onPause}\n\t\t\t\t\t\tonSeeked={updatePositionState}\n\t\t\t\t\t\tonLoadedMetadata={updatePositionState}\n\t\t\t\t\t\tid=\"audio-player\"\n\t\t\t\t\t/>\n\n\t\t\t\t\t<div className=\"audio-player-controls\">\n\t\t\t\t\t\t<PlayerControls playing={playing} onPlayPause={handlePlayPause} />\n\t\t\t\t\t\t<PlayerProgress\n\t\t\t\t\t\t\tprogress={progress}\n\t\t\t\t\t\t\tduration={item?.RunTimeTicks ?? 1}\n\t\t\t\t\t\t\tonSeekCommit={handleSeekCommit}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<PlayerActions onNavigate={handleNavigate} onClose={handleClose}>\n\t\t\t\t\t\t<PlayerVolume\n\t\t\t\t\t\t\tvolume={volume}\n\t\t\t\t\t\t\tisMuted={isMuted}\n\t\t\t\t\t\t\tonVolumeChange={handleVolumeChange}\n\t\t\t\t\t\t\tonMuteToggle={handleMuteToggle}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</PlayerActions>\n\t\t\t\t</motion.div>\n\t\t\t)}\n\t\t</AnimatePresence>\n\t);\n};\n\nexport default AudioPlayer;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/EndsAtDisplay.tsx",
    "content": "import { Typography } from \"@mui/material\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { endsAt } from \"@/utils/date/time\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst EndsAtDisplay = () => {\n\tconst { currentTime, itemDuration } = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tcurrentTime: state.playerState.currentTime,\n\t\t\titemDuration:\n\t\t\t\tstate.metadata.itemDuration ?? state.metadata.item?.RunTimeTicks,\n\t\t})),\n\t);\n\n\tif (!itemDuration) return <Typography variant=\"subtitle1\">--:--</Typography>;\n\n\tconst remaining = itemDuration - (currentTime ?? 0);\n\n\treturn (\n\t\t<Typography variant=\"subtitle1\">\n\t\t\t{endsAt(remaining)}\n\t\t</Typography>\n\t);\n};\n\nexport default EndsAtDisplay;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/ErrorDisplay.tsx",
    "content": "import { Button, Typography } from \"@mui/material\";\nimport React from \"react\";\n\ninterface ErrorDisplayProps {\n\terror: any;\n\tonRetry?: () => void;\n\tonExit?: () => void;\n}\n\nconst ErrorDisplay = ({ error, onRetry, onExit }: ErrorDisplayProps) => {\n\tif (!error) return null;\n\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\tposition: \"absolute\",\n\t\t\t\ttop: 0,\n\t\t\t\tleft: 0,\n\t\t\t\twidth: \"100%\",\n\t\t\t\theight: \"100%\",\n\t\t\t\tbackgroundColor: \"rgba(0, 0, 0, 0.8)\",\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\tflexDirection: \"column\",\n\t\t\t\talignItems: \"center\",\n\t\t\t\tjustifyContent: \"center\",\n\t\t\t\tzIndex: 100,\n\t\t\t\tcolor: \"white\",\n\t\t\t\tgap: \"1rem\",\n\t\t\t}}\n\t\t>\n\t\t\t<Typography variant=\"h5\" color=\"error\">\n\t\t\t\tPlayback Error\n\t\t\t</Typography>\n\t\t\t<Typography variant=\"body1\">\n\t\t\t\t{error.message || \"An unknown error occurred during playback.\"}\n\t\t\t</Typography>\n\t\t\t<div style={{ display: \"flex\", gap: \"1rem\" }}>\n\t\t\t\t{onRetry && (\n\t\t\t\t\t<Button variant=\"contained\" color=\"primary\" onClick={onRetry}>\n\t\t\t\t\t\tRetry\n\t\t\t\t\t</Button>\n\t\t\t\t)}\n\t\t\t\t{onExit && (\n\t\t\t\t\t<Button variant=\"outlined\" color=\"secondary\" onClick={onExit}>\n\t\t\t\t\t\tExit\n\t\t\t\t\t</Button>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t);\n};\n\nexport default ErrorDisplay;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/LoadingIndicator.tsx",
    "content": "import { CircularProgress } from \"@mui/material\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst LoadingIndicator = () => {\n\tconst { isBuffering } = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tisBuffering: state.playerState.isBuffering,\n\t\t})),\n\t);\n\tif (!isBuffering) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\tzIndex: 2,\n\t\t\t\tposition: \"absolute\",\n\t\t\t\theight: \"100vh\",\n\t\t\t\twidth: \"100vw\",\n\t\t\t\ttop: 0,\n\t\t\t\tleft: 0,\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\tjustifyContent: \"center\",\n\t\t\t\talignItems: \"center\",\n\t\t\t}}\n\t\t>\n\t\t\t<CircularProgress size={72} thickness={1.4} />\n\t\t</div>\n\t);\n};\nexport default LoadingIndicator;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/ProgressDisplay.tsx",
    "content": "import { Typography } from \"@mui/material\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport ticksDisplay from \"@/utils/methods/ticksDisplay\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst ProgressDisplay = () => {\n\tconst { currentTime, itemDuration, isUserSeeking, seekValue } =\n\t\tusePlaybackStore(\n\t\t\tuseShallow((state) => ({\n\t\t\t\tcurrentTime: state.playerState.currentTime,\n\t\t\t\titemDuration:\n\t\t\t\t\tstate.metadata.itemDuration ?? state.metadata.item?.RunTimeTicks,\n\t\t\t\tisUserSeeking: state.playerState.isUserSeeking,\n\t\t\t\tseekValue: state.playerState.seekValue,\n\t\t\t})),\n\t\t);\n\treturn (\n\t\t<div className=\"video-player-osd-controls-timeline-text\">\n\t\t\t<Typography>\n\t\t\t\t{ticksDisplay(isUserSeeking ? (seekValue ?? 0) : (currentTime ?? 0))}\n\t\t\t</Typography>\n\t\t\t<Typography>{ticksDisplay(itemDuration ?? 0)}</Typography>\n\t\t</div>\n\t);\n};\n\nexport default ProgressDisplay;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/StatsForNerds.tsx",
    "content": "import { IconButton, Paper, Typography } from \"@mui/material\";\nimport React, { type RefObject, useEffect, useState } from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\ninterface StatsForNerdsProps {\n\tplayerRef: RefObject<HTMLVideoElement>;\n}\n\nconst StatsForNerds = ({ playerRef }: StatsForNerdsProps) => {\n\tconst {\n\t\tshowStatsForNerds,\n\t\ttoggleShowStatsForNerds,\n\t\titemId,\n\t\tmediaSourceId,\n\t\tplaysessionId,\n\t\tvolume,\n\t\tisBuffering,\n\t\tplaybackStream,\n\t\tmediaSource,\n\t} = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tshowStatsForNerds: state.playerState.showStatsForNerds,\n\t\t\ttoggleShowStatsForNerds: state.toggleShowStatsForNerds,\n\t\t\titemId: state.metadata.item?.Id,\n\t\t\tmediaSourceId: state.mediaSource.id,\n\t\t\tplaysessionId: state.playsessionId,\n\t\t\tvolume: state.playerState.volume,\n\t\t\tisBuffering: state.playerState.isBuffering,\n\t\t\tplaybackStream: state.playbackStream,\n\t\t\tmediaSource: state.mediaSource,\n\t\t})),\n\t);\n\n\tconst [videoStats, setVideoStats] = useState<{\n\t\tresolution: string;\n\t\tdroppedFrames: number;\n\t\ttotalFrames: number;\n\t\tbuffered: string;\n\t}>({\n\t\tresolution: \"-\",\n\t\tdroppedFrames: 0,\n\t\ttotalFrames: 0,\n\t\tbuffered: \"-\",\n\t});\n\n\tuseEffect(() => {\n\t\tif (!showStatsForNerds) return;\n\n\t\tconst interval = setInterval(() => {\n\t\t\tif (playerRef.current) {\n\t\t\t\tconst internalPlayer = playerRef.current as HTMLVideoElement;\n\t\t\t\tif (internalPlayer) {\n\t\t\t\t\tconst quality = internalPlayer.getVideoPlaybackQuality?.();\n\n\t\t\t\t\tlet bufferedEnd = 0;\n\t\t\t\t\tif (internalPlayer.buffered.length > 0) {\n\t\t\t\t\t\t// Find the buffered range that covers the current time\n\t\t\t\t\t\tfor (let i = 0; i < internalPlayer.buffered.length; i++) {\n\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\tinternalPlayer.buffered.start(i) <=\n\t\t\t\t\t\t\t\t\tinternalPlayer.currentTime &&\n\t\t\t\t\t\t\t\tinternalPlayer.buffered.end(i) >= internalPlayer.currentTime\n\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\tbufferedEnd = internalPlayer.buffered.end(i);\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tsetVideoStats({\n\t\t\t\t\t\tresolution:\n\t\t\t\t\t\t\tinternalPlayer.videoWidth && internalPlayer.videoHeight\n\t\t\t\t\t\t\t\t? `${internalPlayer.videoWidth}x${internalPlayer.videoHeight}`\n\t\t\t\t\t\t\t\t: \"-\",\n\t\t\t\t\t\tdroppedFrames: quality?.droppedVideoFrames ?? 0,\n\t\t\t\t\t\ttotalFrames: quality?.totalVideoFrames ?? 0,\n\t\t\t\t\t\tbuffered: `${(bufferedEnd - internalPlayer.currentTime).toFixed(2)}s`,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}, 1000);\n\n\t\treturn () => clearInterval(interval);\n\t}, [showStatsForNerds, playerRef]);\n\n\tif (!showStatsForNerds) return null;\n\n\treturn (\n\t\t<Paper\n\t\t\tclassName=\"glass\"\n\t\t\tsx={{\n\t\t\t\tposition: \"absolute\",\n\t\t\t\ttop: \"2em\",\n\t\t\t\tleft: \"2em\",\n\t\t\t\tpadding: \"1em\",\n\t\t\t\tcolor: \"white\",\n\t\t\t\tzIndex: 100,\n\t\t\t\tmaxWidth: \"400px\",\n\t\t\t\tfontSize: \"0.8rem\",\n\t\t\t\tfontFamily: \"monospace\",\n\t\t\t}}\n\t\t>\n\t\t\t<div\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tmarginBottom: \"0.5em\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Typography variant=\"subtitle2\" fontWeight=\"bold\">\n\t\t\t\t\tStats for Nerds\n\t\t\t\t</Typography>\n\t\t\t\t<IconButton\n\t\t\t\t\tsize=\"small\"\n\t\t\t\t\tonClick={toggleShowStatsForNerds}\n\t\t\t\t\tsx={{ color: \"white\" }}\n\t\t\t\t>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\tstyle={{ fontSize: \"1.2rem\" }}\n\t\t\t\t\t>\n\t\t\t\t\t\tclose\n\t\t\t\t\t</span>\n\t\t\t\t</IconButton>\n\t\t\t</div>\n\n\t\t\t<div\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: \"grid\",\n\t\t\t\t\tgridTemplateColumns: \"auto 1fr\",\n\t\t\t\t\tgap: \"0.5em 1em\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<span>Video ID:</span> <span>{itemId}</span>\n\t\t\t\t<span>Media Source ID:</span> <span>{mediaSourceId}</span>\n\t\t\t\t<span>Play Session ID:</span> <span>{playsessionId}</span>\n\t\t\t\t<span>Playback Method:</span>{\" \"}\n\t\t\t\t<span>{mediaSource.playMethod}</span>\n\t\t\t\t<span>Container:</span> <span>{mediaSource.container}</span>\n\t\t\t\t<span>Video Codec:</span> <span>{mediaSource.videoCodec}</span>\n\t\t\t\t<span>Audio Codec:</span> <span>{mediaSource.audioCodec}</span>\n\t\t\t\t<span>Bitrate:</span>{\" \"}\n\t\t\t\t<span>\n\t\t\t\t\t{mediaSource.bitrate\n\t\t\t\t\t\t? `${(mediaSource.bitrate / 1000000).toFixed(2)} Mbps`\n\t\t\t\t\t\t: \"-\"}\n\t\t\t\t</span>\n\t\t\t\t<span>Stream URL:</span>{\" \"}\n\t\t\t\t<span style={{ wordBreak: \"break-all\" }}>{playbackStream}</span>\n\t\t\t\t<span>Resolution:</span> <span>{videoStats.resolution}</span>\n\t\t\t\t<span>Volume:</span> <span>{Math.round(volume * 100)}%</span>\n\t\t\t\t<span>Buffer Health:</span> <span>{videoStats.buffered}</span>\n\t\t\t\t<span>Dropped Frames:</span>{\" \"}\n\t\t\t\t<span>\n\t\t\t\t\t{videoStats.droppedFrames} / {videoStats.totalFrames}\n\t\t\t\t</span>\n\t\t\t\t<span>Is Buffering:</span> <span>{isBuffering ? \"Yes\" : \"No\"}</span>\n\t\t\t</div>\n\t\t</Paper>\n\t);\n};\n\nexport default StatsForNerds;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/VolumeChangeOverlay.tsx",
    "content": "import { LinearProgress } from \"@mui/material\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst VolumeChangeOverlay = () => {\n\tconst { playerVolume, showVolumeIndicator } = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tplayerVolume: state.playerState.volume,\n\t\t\tshowVolumeIndicator: state.isVolumeInidcatorVisible,\n\t\t})),\n\t);\n\treturn (\n\t\t<AnimatePresence>\n\t\t\t{showVolumeIndicator && (\n\t\t\t\t<motion.div\n\t\t\t\t\tinitial={{ opacity: 0 }}\n\t\t\t\t\tanimate={{ opacity: 1 }}\n\t\t\t\t\texit={{ opacity: 0 }}\n\t\t\t\t\tclassName=\"video-volume-indicator glass\"\n\t\t\t\t>\n\t\t\t\t\t<div className=\"material-symbols-rounded\">\n\t\t\t\t\t\t{playerVolume > 0.7 ? \"volume_up\" : \"volume_down\"}\n\t\t\t\t\t</div>\n\t\t\t\t\t<LinearProgress\n\t\t\t\t\t\tstyle={{ width: \"100%\" }}\n\t\t\t\t\t\tvalue={playerVolume * 100}\n\t\t\t\t\t\tvariant=\"determinate\"\n\t\t\t\t\t/>\n\t\t\t\t</motion.div>\n\t\t\t)}\n\t\t</AnimatePresence>\n\t);\n};\nexport default VolumeChangeOverlay;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/bubbleSlider/index.tsx",
    "content": "import type { TrickplayInfo } from \"@jellyfin/sdk/lib/generated-client\";\nimport { Slider, Tooltip, Typography } from \"@mui/material\";\nimport type { Instance } from \"@popperjs/core\";\nimport React, {\n\ttype MouseEvent,\n\tuseCallback,\n\tuseEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { ticksToMs } from \"@/utils/date/time\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst ticksDisplay = (ticks: number) => {\n\tconst time = Math.round(ticks / 10000);\n\tlet formatedTime = \"\";\n\tlet timeSec = Math.floor(time / 1000);\n\tlet timeMin = Math.floor(timeSec / 60);\n\ttimeSec -= timeMin * 60;\n\ttimeSec = timeSec === 0 ? 0o0 : timeSec;\n\tconst timeHr = Math.floor(timeMin / 60);\n\ttimeMin -= timeHr * 60;\n\tformatedTime = `${timeHr.toLocaleString([], {\n\t\tminimumIntegerDigits: 2,\n\t\tuseGrouping: false,\n\t})}:${timeMin.toLocaleString([], {\n\t\tminimumIntegerDigits: 2,\n\t\tuseGrouping: false,\n\t})}:${timeSec.toLocaleString([], {\n\t\tminimumIntegerDigits: 2,\n\t\tuseGrouping: false,\n\t})}`;\n\treturn formatedTime;\n};\n\nconst BubbleSlider = () => {\n\tconst api = useApiInContext((state) => state.api);\n\tconst {\n\t\tseekValue,\n\t\tcurrentTime,\n\t\tisUserSeeking,\n\t\t// setIsUserSeeking,\n\t\t// setSeekValue,\n\t\t// seekTo,\n\t\titemDuration,\n\t\titemChapters,\n\t\tmediaSource,\n\t\titemTrickplay,\n\t\titemId,\n\t\thandleStartSeek,\n\t\thandleStopSeek,\n\t} = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tseekValue: state.playerState.seekValue,\n\t\t\tcurrentTime: state.playerState.currentTime,\n\t\t\tisUserSeeking: state.playerState.isUserSeeking,\n\t\t\t// setIsUserSeeking: state.setIsUserSeeking,\n\t\t\t// setSeekValue: state.setSeekValue,\n\t\t\t// seekTo: state.seekTo,\n\t\t\titemDuration: state.metadata.item.RunTimeTicks,\n\t\t\titemChapters: state.metadata.item.Chapters,\n\t\t\tmediaSource: state.mediaSource,\n\t\t\titemTrickplay: state.metadata.item.Trickplay,\n\t\t\titemId: state.metadata.item.Id,\n\t\t\thandleStartSeek: state.handleStartSeek,\n\t\t\thandleStopSeek: state.handleStopSeek,\n\t\t})),\n\t);\n\n\tconst positionRef = useRef<{ x: number; y: number }>({\n\t\tx: 0,\n\t\ty: 0,\n\t});\n\tconst popperRef = useRef<Instance>(null);\n\tconst areaRef = useRef<HTMLDivElement>(null);\n\tconst rafRef = useRef<number | null>(null);\n\n\tconst [hoverProgress, setHoverProgress] = useState(0);\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\tif (rafRef.current) {\n\t\t\t\tcancelAnimationFrame(rafRef.current);\n\t\t\t}\n\t\t};\n\t}, []);\n\n\tconst handleSliderHover = useCallback(\n\t\t(event: MouseEvent) => {\n\t\t\tconst clientX = event.clientX;\n\t\t\tconst clientY = event.clientY;\n\n\t\t\tif (\n\t\t\t\tpositionRef.current.x === clientX &&\n\t\t\t\tpositionRef.current.y === clientY\n\t\t\t) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tpositionRef.current = { x: clientX, y: clientY };\n\n\t\t\tif (rafRef.current) return;\n\n\t\t\trafRef.current = requestAnimationFrame(() => {\n\t\t\t\tif (areaRef.current) {\n\t\t\t\t\tconst rect = areaRef.current.getBoundingClientRect();\n\t\t\t\t\tconst width = rect.width;\n\t\t\t\t\tconst distX = clientX - rect.left;\n\t\t\t\t\tconst percentageCovered = distX / width;\n\t\t\t\t\tsetHoverProgress(percentageCovered * (itemDuration ?? 0));\n\t\t\t\t\tif (popperRef.current != null) {\n\t\t\t\t\t\tpopperRef.current.update();\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\trafRef.current = null;\n\t\t\t});\n\t\t},\n\t\t[itemDuration],\n\t);\n\n\tconst chapterMarks = useMemo(() => {\n\t\tconst marks: { value: number }[] = [];\n\t\titemChapters?.forEach((val) => {\n\t\t\tmarks.push({ value: val.StartPositionTicks ?? 0 });\n\t\t});\n\t\treturn marks;\n\t}, [itemChapters]);\n\n\tconst sliderDisplayFormat = (value: number) => {\n\t\tconst currentChapter = itemChapters?.filter((chapter, index) => {\n\t\t\tif (index + 1 === itemChapters?.length) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (\n\t\t\t\t(itemChapters?.[index + 1]?.StartPositionTicks ?? value) - value >= 0 &&\n\t\t\t\t(chapter.StartPositionTicks ?? value) - value < 0\n\t\t\t) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\n\t\tlet trickplayResolution: TrickplayInfo | undefined;\n\t\tconst trickplayResolutions = mediaSource.id\n\t\t\t? itemTrickplay?.[mediaSource.id]\n\t\t\t: null;\n\t\tif (trickplayResolutions) {\n\t\t\tlet bestWidth: number | undefined;\n\t\t\tconst maxWidth = window.screen.width * window.devicePixelRatio * 0.2;\n\t\t\tfor (const [_, trickInfo] of Object.entries(trickplayResolutions)) {\n\t\t\t\tif (\n\t\t\t\t\t!bestWidth ||\n\t\t\t\t\t(trickInfo.Width &&\n\t\t\t\t\t\t((trickInfo.Width < bestWidth && bestWidth > maxWidth) ||\n\t\t\t\t\t\t\t(trickInfo.Width > bestWidth && bestWidth <= maxWidth)))\n\t\t\t\t) {\n\t\t\t\t\tbestWidth = trickInfo.Width;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (bestWidth) {\n\t\t\t\ttrickplayResolution = trickplayResolutions[bestWidth];\n\t\t\t}\n\t\t}\n\t\tlet trickplayStyle: React.CSSProperties | undefined;\n\n\t\tif (\n\t\t\ttrickplayResolution?.TileWidth &&\n\t\t\ttrickplayResolution.TileHeight &&\n\t\t\ttrickplayResolution.Width &&\n\t\t\ttrickplayResolution.Height\n\t\t) {\n\t\t\tconst currentTrickplayImage = trickplayResolution?.Interval\n\t\t\t\t? Math.floor(ticksToMs(value) / trickplayResolution?.Interval)\n\t\t\t\t: 0;\n\t\t\tconst trickplayImageSize =\n\t\t\t\ttrickplayResolution?.TileWidth * trickplayResolution?.TileHeight; // this gives the area of a single tile\n\n\t\t\tconst trickplayImageOffset = currentTrickplayImage % trickplayImageSize; // this gives the tile index inside a trickplay image\n\t\t\tconst index = Math.floor(currentTrickplayImage / trickplayImageSize); // this gives the index of trickplay image\n\n\t\t\tconst imageOffsetX =\n\t\t\t\ttrickplayImageOffset % trickplayResolution?.TileWidth; // this gives the x coordinate of tile in trickplay image\n\t\t\tconst imageOffsetY = Math.floor(\n\t\t\t\ttrickplayImageOffset / trickplayResolution?.TileWidth,\n\t\t\t); // this gives the y coordinate of tile in trickplay image\n\t\t\tconst backgroundOffsetX = -(imageOffsetX * trickplayResolution?.Width);\n\t\t\tconst backgroundOffsetY = -(imageOffsetY * trickplayResolution?.Height);\n\n\t\t\tconst imgUrlParamsObject: Record<string, string> = {\n\t\t\t\tapi_key: String(api?.accessToken),\n\t\t\t\tMediaSourceId: mediaSource.id ?? \"\",\n\t\t\t};\n\t\t\tconst imgUrlParams = new URLSearchParams(imgUrlParamsObject).toString();\n\n\t\t\tconst _imageAspectRatio =\n\t\t\t\ttrickplayResolution.Width / trickplayResolution.Height;\n\n\t\t\ttrickplayStyle = {\n\t\t\t\tbackgroundImage: `url(${api?.basePath}/Videos/${itemId}/Trickplay/${trickplayResolution.Width}/${index}.jpg?${imgUrlParams})`,\n\t\t\t\tbackgroundPositionX: `${backgroundOffsetX}px`,\n\t\t\t\tbackgroundPositionY: `${backgroundOffsetY}px`,\n\t\t\t\twidth: `${trickplayResolution.Width}px`,\n\t\t\t\theight: `${trickplayResolution.Height}px`,\n\t\t\t\tborderBottom: \"1px solid rgba(255,255,255,0.1)\",\n\t\t\t\tborderRadius: 0,\n\t\t\t};\n\t\t}\n\n\t\tconst chapterName = currentChapter?.[0]?.Name;\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tclassName=\"flex flex-column video-osb-bubble\"\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{trickplayStyle && (\n\t\t\t\t\t<div className=\"video-osd-trickplayBubble\" style={trickplayStyle} />\n\t\t\t\t)}\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tpadding: \"8px 12px\",\n\t\t\t\t\t\ttextAlign: \"center\",\n\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\tboxSizing: \"border-box\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{chapterName && (\n\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\tfontWeight: \"bold\",\n\t\t\t\t\t\t\t\tmb: 0.5,\n\t\t\t\t\t\t\t\tcolor: \"white\",\n\t\t\t\t\t\t\t\ttextShadow: \"0 2px 4px rgba(0,0,0,0.5)\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{chapterName}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t)}\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tvariant=\"body2\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tcolor: \"rgba(255,255,255,0.9)\",\n\t\t\t\t\t\t\tfontVariantNumeric: \"tabular-nums\",\n\t\t\t\t\t\t\tfontWeight: 500,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{ticksDisplay(value)}\n\t\t\t\t\t</Typography>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t);\n\t};\n\n\tconst virtualAnchorEl = useMemo(() => {\n\t\treturn {\n\t\t\tgetBoundingClientRect: () => {\n\t\t\t\treturn new DOMRect(\n\t\t\t\t\tpositionRef.current.x,\n\t\t\t\t\tareaRef.current?.getBoundingClientRect().y ?? 0,\n\t\t\t\t\t0,\n\t\t\t\t\t0,\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\t}, []);\n\n\treturn (\n\t\t<Tooltip\n\t\t\t// title={sliderDisplayFormat(\n\t\t\t// \tpositionRef.current.x - areaRef.current!.getBoundingClientRect().x,\n\t\t\t// )}\n\t\t\ttitle={sliderDisplayFormat(hoverProgress)}\n\t\t\tplacement=\"top\"\n\t\t\tslotProps={{\n\t\t\t\tpopper: {\n\t\t\t\t\tpopperRef,\n\t\t\t\t\tanchorEl: virtualAnchorEl,\n\t\t\t\t\t// disablePortal: true,\n\t\t\t\t},\n\t\t\t\ttooltip: {\n\t\t\t\t\tclassName: \"glass\",\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\twidth: \"auto\",\n\t\t\t\t\t\tmaxWidth: \"none\",\n\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t\tpadding: \"0\",\n\t\t\t\t\t\tborderRadius: \"12px\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}}\n\t\t>\n\t\t\t<Slider\n\t\t\t\tvalue={isUserSeeking ? seekValue : currentTime}\n\t\t\t\tmax={itemDuration ?? 0}\n\t\t\t\tstep={1}\n\t\t\t\tonChange={(_, newValue) => {\n\t\t\t\t\t// setIsUserSeeking(true);\n\t\t\t\t\tArray.isArray(newValue)\n\t\t\t\t\t\t? handleStartSeek(newValue[0])\n\t\t\t\t\t\t: handleStartSeek(newValue);\n\t\t\t\t}}\n\t\t\t\tonChangeCommitted={(_, newValue) => {\n\t\t\t\t\t// setIsUserSeeking(false);\n\t\t\t\t\t// Array.isArray(newValue)\n\t\t\t\t\t// \t? setSeekValue(newValue[0])\n\t\t\t\t\t// \t: setSeekValue(newValue);\n\t\t\t\t\tif (Array.isArray(newValue)) {\n\t\t\t\t\t\thandleStopSeek(newValue[0]);\n\t\t\t\t\t} else {\n\t\t\t\t\t\thandleStopSeek(newValue);\n\t\t\t\t\t}\n\t\t\t\t}}\n\t\t\t\tsx={{\n\t\t\t\t\t\"& .MuiSlider-thumb\": {\n\t\t\t\t\t\twidth: 14,\n\t\t\t\t\t\theight: 14,\n\t\t\t\t\t\ttransition: \"0.1s ease-in-out\",\n\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\"&.Mui-active\": {\n\t\t\t\t\t\t\twidth: 20,\n\t\t\t\t\t\t\theight: 20,\n\t\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t\"&:hover .MuiSlider-thumb\": {\n\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t},\n\t\t\t\t\t\"& .MuiSlider-rail\": {\n\t\t\t\t\t\topacity: 0.28,\n\t\t\t\t\t\tbackground: \"white\",\n\t\t\t\t\t},\n\t\t\t\t}}\n\t\t\t\tmarks={chapterMarks}\n\t\t\t\tvalueLabelDisplay=\"off\"\n\t\t\t\tref={areaRef}\n\t\t\t\tonMouseMove={handleSliderHover}\n\t\t\t/>\n\t\t</Tooltip>\n\t);\n};\n\nexport default BubbleSlider;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/buttons/CaptionsButton.tsx",
    "content": "import { IconButton } from \"@mui/material\";\nimport React, { useTransition } from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { toggleSubtitleTrack, usePlaybackStore } from \"@/utils/store/playback\";\n\nconst CaptionsButton = () => {\n\tconst { subtitle } = usePlaybackStore(\n\t\tuseShallow((state) => ({ subtitle: state.mediaSource.subtitle })),\n\t);\n\n\tconst [subtitleIsChanging, startSubtitleChange] = useTransition();\n\n\treturn (\n\t\t<IconButton\n\t\t\tdisabled={subtitle?.allTracks?.length === 0 || subtitleIsChanging}\n\t\t\tonClick={() => startSubtitleChange(toggleSubtitleTrack)}\n\t\t>\n\t\t\t<span className={\"material-symbols-rounded\"}>\n\t\t\t\t{subtitle?.enable ? \"closed_caption\" : \"closed_caption_disabled\"}\n\t\t\t</span>\n\t\t</IconButton>\n\t);\n};\n\nexport default CaptionsButton;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/buttons/ChaptersListButton.tsx",
    "content": "import { IconButton, Menu, MenuItem } from \"@mui/material\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { ticksToSec } from \"@/utils/date/time\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst ChaptersListButton = () => {\n\tconst { itemChapters, seekTo } = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\titemChapters: state.metadata.item?.Chapters,\n\t\t\tseekTo: state.seekTo,\n\t\t})),\n\t);\n\tconst [showChapterList, setShowChapterList] =\n\t\tReact.useState<null | HTMLElement>(null);\n\tconst handleShowChapterList = (event: React.MouseEvent<HTMLElement>) => {\n\t\tsetShowChapterList(event.currentTarget);\n\t};\n\n\treturn (\n\t\t<>\n\t\t\t<IconButton\n\t\t\t\tdisabled={itemChapters?.length === 0}\n\t\t\t\tonClick={handleShowChapterList}\n\t\t\t>\n\t\t\t\t<span className=\"material-symbols-rounded\">list</span>\n\t\t\t</IconButton>\n\t\t\t<Menu\n\t\t\t\topen={Boolean(showChapterList)}\n\t\t\t\tanchorEl={showChapterList}\n\t\t\t\tonClose={() => setShowChapterList(null)}\n\t\t\t\tanchorOrigin={{\n\t\t\t\t\tvertical: \"top\",\n\t\t\t\t\thorizontal: \"center\",\n\t\t\t\t}}\n\t\t\t\ttransformOrigin={{\n\t\t\t\t\tvertical: \"bottom\",\n\t\t\t\t\thorizontal: \"center\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{itemChapters?.length &&\n\t\t\t\t\titemChapters?.map((chapter) => (\n\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\tkey={chapter.Name}\n\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\tseekTo(ticksToSec(chapter.StartPositionTicks ?? 0));\n\t\t\t\t\t\t\t\tsetShowChapterList(null); // Close the chapter list menu\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{chapter.Name}\n\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t))}\n\t\t\t</Menu>\n\t\t</>\n\t);\n};\n\nexport default ChaptersListButton;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/buttons/ForwardButton.tsx",
    "content": "import { IconButton } from \"@mui/material\";\nimport React, { useEffect } from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst ForwardButton = () => {\n\tconst { seekForward } = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tseekForward: state.seekForward,\n\t\t})),\n\t);\n\n\tuseEffect(() => {\n\t\tnavigator.mediaSession.setActionHandler(\"seekforward\", (details) => {\n\t\t\tif (details.seekOffset) {\n\t\t\t\tseekForward(details.seekOffset);\n\t\t\t} else {\n\t\t\t\tseekForward(10); // Default to 10 seconds\n\t\t\t}\n\t\t});\n\t}, [seekForward]);\n\n\treturn (\n\t\t<IconButton onClick={() => seekForward(15)}>\n\t\t\t<span className=\"material-symbols-rounded fill\">fast_forward</span>\n\t\t</IconButton>\n\t);\n};\n\nexport default ForwardButton;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/buttons/FullscreenButton.tsx",
    "content": "import { IconButton } from \"@mui/material\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst FullscreenButton = () => {\n\tconst { isPlayerFullscreen, toggleIsPlayerFullscreen } = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tisPlayerFullscreen: state.playerState.isPlayerFullscreen,\n\t\t\ttoggleIsPlayerFullscreen: state.toggleIsPlayerFullscreen,\n\t\t})),\n\t);\n\n\treturn (\n\t\t<IconButton onClick={toggleIsPlayerFullscreen}>\n\t\t\t<span className=\"material-symbols-rounded fill\">\n\t\t\t\t{isPlayerFullscreen ? \"fullscreen_exit\" : \"fullscreen\"}\n\t\t\t</span>\n\t\t</IconButton>\n\t);\n};\n\nexport default FullscreenButton;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/buttons/NextChapterButton.tsx",
    "content": "import { IconButton } from \"@mui/material\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst NextChapterButton = () => {\n\tconst { seekToNextChapter } = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tseekToNextChapter: state.seekToNextChapter,\n\t\t})),\n\t);\n\treturn (\n\t\t<IconButton onClick={() => seekToNextChapter()}>\n\t\t\t<span className=\"material-symbols-rounded fill\">last_page</span>\n\t\t</IconButton>\n\t);\n};\n\nexport default NextChapterButton;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/buttons/PlayPauseButton.tsx",
    "content": "import { IconButton } from \"@mui/material\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst PlayPauseButton = () => {\n\tconst { isPlayerPlaying, toggleIsPlaying } = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tisPlayerPlaying: state.playerState.isPlayerPlaying,\n\t\t\ttoggleIsPlaying: state.toggleIsPlaying,\n\t\t})),\n\t);\n\treturn (\n\t\t<IconButton onClick={toggleIsPlaying}>\n\t\t\t<span className=\"material-symbols-rounded fill\">\n\t\t\t\t{isPlayerPlaying ? \"pause\" : \"play_arrow\"}\n\t\t\t</span>\n\t\t</IconButton>\n\t);\n};\n\nexport default PlayPauseButton;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/buttons/PrevChapterButton.tsx",
    "content": "import { IconButton } from \"@mui/material\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst PrevChapterButton = () => {\n\tconst { seekToPrevChapter } = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tseekToPrevChapter: state.seekToPrevChapter,\n\t\t})),\n\t);\n\n\treturn (\n\t\t<IconButton onClick={seekToPrevChapter}>\n\t\t\t<span className=\"material-symbols-rounded fill\">first_page</span>\n\t\t</IconButton>\n\t);\n};\n\nexport default PrevChapterButton;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/buttons/RewindButton.tsx",
    "content": "import { IconButton } from \"@mui/material\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst RewindButton = () => {\n\tconst { seekBackward } = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tseekBackward: state.seekBackward,\n\t\t})),\n\t);\n\n\tReact.useEffect(() => {\n\t\tnavigator.mediaSession.setActionHandler(\"seekbackward\", (details) => {\n\t\t\tif (details.seekOffset) {\n\t\t\t\tseekBackward(details.seekOffset);\n\t\t\t} else {\n\t\t\t\tseekBackward(10); // Default to 10 seconds\n\t\t\t}\n\t\t});\n\t}, [seekBackward]);\n\n\treturn (\n\t\t<IconButton\n\t\t\tonClick={() => {\n\t\t\t\tseekBackward(15);\n\t\t\t}}\n\t\t>\n\t\t\t<span className=\"material-symbols-rounded fill\">fast_rewind</span>\n\t\t</IconButton>\n\t);\n};\n\nexport default RewindButton;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/buttons/SkipSegmentButton.tsx",
    "content": "import { Box, Button } from \"@mui/material\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\n\nconst SkipSegmentButton = () => {\n\tconst {\n\t\tmediaSegments,\n\t\tcurrentSegmentIndex,\n\t\tskipSegment,\n\t\tactiveSegmentId,\n\t\tisUserHovering,\n\t\tisPlayerPlaying,\n\t\tisUserSeeking,\n\t} = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tmediaSegments: state.metadata.mediaSegments,\n\t\t\tcurrentSegmentIndex: state.nextSegmentIndex - 1,\n\t\t\tskipSegment: state.skipSegment,\n\t\t\tactiveSegmentId: state.activeSegmentId,\n\t\t\tisUserHovering: state.playerState.isUserHovering,\n\t\t\tisPlayerPlaying: state.playerState.isPlayerPlaying,\n\t\t\tisUserSeeking: state.playerState.isUserSeeking,\n\t\t})),\n\t);\n\n\tconst shouldShow =\n\t\tmediaSegments?.Items?.length &&\n\t\tcurrentSegmentIndex >= 0 &&\n\t\tactiveSegmentId &&\n\t\tmediaSegments?.Items?.[currentSegmentIndex].Type !== \"Outro\";\n\n\treturn (\n\t\t<AnimatePresence>\n\t\t\t{shouldShow && (\n\t\t\t\t<motion.div\n\t\t\t\t\tinitial={{ opacity: 0, x: 20, scale: 0.9 }}\n\t\t\t\t\tanimate={{ opacity: 1, x: 0, scale: 1 }}\n\t\t\t\t\texit={{ opacity: 0, x: 20, scale: 0.9 }}\n\t\t\t\t\ttransition={{\n\t\t\t\t\t\ttype: \"spring\",\n\t\t\t\t\t\tstiffness: 300,\n\t\t\t\t\t\tdamping: 30,\n\t\t\t\t\t}}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\tright: \"3em\",\n\t\t\t\t\t\tzIndex: 10000,\n\t\t\t\t\t\tbottom:\n\t\t\t\t\t\t\tisUserHovering || !isPlayerPlaying || isUserSeeking ? \"20vh\" : \"6em\",\n\t\t\t\t\t\ttransition: \"bottom 0.3s ease-in-out\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tonClick={skipSegment}\n\t\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\t\tstartIcon={\n\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">skip_next</span>\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tbgcolor: \"rgba(20, 20, 20, 0.6)\",\n\t\t\t\t\t\t\tbackdropFilter: \"blur(16px) saturate(180%)\",\n\t\t\t\t\t\t\tcolor: \"white\",\n\t\t\t\t\t\t\tborder: \"1px solid rgba(255, 255, 255, 0.1)\",\n\t\t\t\t\t\t\tborderRadius: \"12px\",\n\t\t\t\t\t\t\tpadding: \"12px 24px\",\n\t\t\t\t\t\t\ttextTransform: \"none\",\n\t\t\t\t\t\t\tfontSize: \"1rem\",\n\t\t\t\t\t\t\tfontWeight: 600,\n\t\t\t\t\t\t\tboxShadow: \"0 8px 32px 0 rgba(0, 0, 0, 0.2)\",\n\t\t\t\t\t\t\ttransition: \"all 0.2s ease\",\n\t\t\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\t\t\tbgcolor: \"rgba(255, 255, 255, 0.1)\",\n\t\t\t\t\t\t\t\ttransform: \"scale(1.02)\",\n\t\t\t\t\t\t\t\tboxShadow: \"0 8px 32px 0 rgba(0, 0, 0, 0.3)\",\n\t\t\t\t\t\t\t\tborder: \"1px solid rgba(255, 255, 255, 0.2)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\"&:active\": {\n\t\t\t\t\t\t\t\ttransform: \"scale(0.98)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\tSkip {mediaSegments?.Items?.[currentSegmentIndex].Type}\n\t\t\t\t\t</Button>\n\t\t\t\t</motion.div>\n\t\t\t)}\n\t\t</AnimatePresence>\n\t);\n};\n\nconst SkipSegmentButtonMemo = React.memo(SkipSegmentButton);\n\nexport default SkipSegmentButtonMemo;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/controls.scss",
    "content": ".video-player {\n    background: black;\n    position: absolute;\n    width: 100vw;\n    height: 100vh;\n    top: 0;\n    left: 0;\n    &-osd {\n        position: absolute;\n        width: 100vw;\n        height: 100vh;\n        top: 0;\n        left: 0;\n        cursor: none;\n        display: flex;\n        flex-direction: column;\n        align-self: stretch;\n        justify-content: space-between;\n        padding: 2em 3em;\n        background: linear-gradient(\n            to bottom,\n            rgba(0, 0, 0, 0.8) 0%,\n            rgba(0, 0, 0, 0) 25%,\n            rgba(0, 0, 0, 0) 75%,\n            rgba(0, 0, 0, 0.9) 100%\n        );\n        &.hovering {\n            cursor: default;\n        }\n        &-controls {\n            display: flex;\n            flex-direction: column;\n            gap: 1em;\n\n            &-timeline-text {\n                display: flex;\n                align-items: stretch;\n                justify-content: space-between;\n                margin-top: 0.5em;\n                font-weight: 500;\n                opacity: 0.9;\n            }\n            &-progress {\n                display: flex;\n                flex-direction: column;\n                gap: 0.2em;\n                align-items: stretch;\n                justify-content: center;\n                margin-bottom: 0.5em;\n                &-bubble {\n                    width: 14em;\n                    top: 0;\n                    left: 0;\n                    transform-origin: bottom center;\n                    transform: translate(var(--left, 0), var(--top, 0));\n                    position: fixed;\n                }\n            }\n            &-buttons {\n                display: flex;\n                gap: 0.5em;\n                align-items: center;\n            }\n        }\n        .MuiSlider-valueLabel {\n            padding: 0;\n            background: transparent;\n            overflow: hidden;\n            border-radius: $border-radius-default;\n            border: 1.2px solid rgb(255 255 255 / 0.1);\n        }\n    }\n}"
  },
  {
    "path": "src/components/playback/videoPlayer/controls.tsx",
    "content": "import { getPlaystateApi } from \"@jellyfin/sdk/lib/utils/api/playstate-api\";\nimport { IconButton, Slider, Typography } from \"@mui/material\";\nimport { WebviewWindow as appWindow } from \"@tauri-apps/api/webviewWindow\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport React, {\n\ttype MouseEvent,\n\tuseCallback,\n\tuseEffect,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport PlayNextButton from \"@/components/buttons/playNextButton\";\nimport PlayPreviousButton from \"@/components/buttons/playPreviousButtom\";\nimport QueueButton from \"@/components/buttons/queueButton\";\nimport BubbleSlider from \"@/components/playback/videoPlayer/bubbleSlider\";\nimport { secToTicks } from \"@/utils/date/time\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\nimport CaptionsButton from \"./buttons/CaptionsButton\";\nimport ChaptersListButton from \"./buttons/ChaptersListButton\";\nimport ForwardButton from \"./buttons/ForwardButton\";\nimport FullscreenButton from \"./buttons/FullscreenButton\";\nimport NextChapterButton from \"./buttons/NextChapterButton\";\nimport PlayPauseButton from \"./buttons/PlayPauseButton\";\nimport PrevChapterButton from \"./buttons/PrevChapterButton\";\nimport RewindButton from \"./buttons/RewindButton\";\nimport EndsAtDisplay from \"./EndsAtDisplay\";\nimport VideoPlayerSettingsMenu from \"./settingsMenu\";\n\nimport \"./controls.scss\";\nimport { clearQueue } from \"@/utils/store/queue\";\nimport ProgressDisplay from \"./ProgressDisplay\";\n\n/**\n * Constant for the volume change interval when using the mouse wheel.\n * This value determines how much the volume will change with each scroll step.\n */\nconst VOLUME_SCROLL_INTERVAL = 0.02;\n\ntype VideoPlayerControlsProps = {\n\t// isVisible: boolean;\n\tonHover?: (event: MouseEvent<HTMLDivElement>) => void;\n\tonLeave?: (event: MouseEvent<HTMLDivElement>) => void;\n};\n\nconst VideoPlayerControls = ({\n\t// isVisible,\n\tonHover,\n\tonLeave,\n}: VideoPlayerControlsProps) => {\n\tconst playerOSDRef = useRef<HTMLDivElement>(null);\n\n\tconst api = useApiInContext((state) => state.api);\n\tconst {\n\t\tmediaSourceId,\n\t\titemId,\n\t\titemName,\n\t\tepisodeTitle,\n\t\tplaysessionId,\n\t\tisPlayerPlaying,\n\t\ttoggleIsPlaying,\n\t\ttoggleIsPlayerFullscreen,\n\t\tisUserSeeking,\n\t\t// seekValue,\n\t\tisPlayerMuted,\n\t\tvolume,\n\t\tsetVolume,\n\t\ttoggleIsPlayerMuted,\n\t\tincreaseVolumeByStep,\n\t\tdecreaseVolumeByStep,\n\t\tgetCurrentTime,\n\t\tisUserHovering,\n\t} = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tmediaSourceId: state.mediaSource.id,\n\t\t\titemChapters: state.metadata.item.Chapters,\n\t\t\titemId: state.metadata.item.Id,\n\t\t\titemName: state.metadata.itemName,\n\t\t\tepisodeTitle: state.metadata.episodeTitle,\n\t\t\t// mediaSegments: state.metadata.mediaSegments,\n\t\t\tisPlayerPlaying: state.playerState.isPlayerPlaying,\n\t\t\tplaysessionId: state.playsessionId,\n\t\t\ttoggleIsPlaying: state.toggleIsPlaying,\n\t\t\ttoggleIsPlayerFullscreen: state.toggleIsPlayerFullscreen,\n\t\t\tisUserSeeking: state.playerState.isUserSeeking,\n\t\t\tseekValue: state.playerState.seekValue,\n\t\t\tisPlayerMuted: state.playerState.isPlayerMuted,\n\t\t\tvolume: state.playerState.volume,\n\t\t\tsetVolume: state.setVolume,\n\t\t\ttoggleIsPlayerMuted: state.toggleIsPlayerMuted,\n\t\t\tincreaseVolumeByStep: state.increaseVolumeByStep,\n\t\t\tdecreaseVolumeByStep: state.decreaseVolumeByStep,\n\t\t\tgetCurrentTime: state.getCurrentTime,\n\t\t\tisUserHovering: state.playerState.isUserHovering,\n\t\t})),\n\t);\n\n\t// Volume control with mouse wheel\n\tuseEffect(() => {\n\t\tconst handleMouseWheel = (event: WheelEvent) => {\n\t\t\tif (event.deltaY < 0) {\n\t\t\t\tincreaseVolumeByStep(VOLUME_SCROLL_INTERVAL);\n\t\t\t} else if (event.deltaY > 0) {\n\t\t\t\tdecreaseVolumeByStep(VOLUME_SCROLL_INTERVAL);\n\t\t\t}\n\t\t};\n\n\t\tconst currentRef = playerOSDRef.current;\n\t\t// attach the event listener\n\t\tcurrentRef?.addEventListener(\"wheel\", handleMouseWheel);\n\n\t\t// remove the event listener\n\t\treturn () => {\n\t\t\tcurrentRef?.removeEventListener(\"wheel\", handleMouseWheel);\n\t\t};\n\t}, [increaseVolumeByStep, decreaseVolumeByStep]);\n\n\tconst [clickTimeout, setClickTimeout] = useState<NodeJS.Timeout | null>(null);\n\tconst [showVolumeControl, setShowVolumeControl] = useState(false);\n\n\tconst [settingsMenuRef, setSettingsMenuRef] =\n\t\tuseState<HTMLButtonElement | null>(null);\n\n\t// const creditInfo = mediaSegments?.Items?.find(\n\t// \t(segment) => segment.Type === \"Outro\",\n\t// );\n\n\t// Credits and Next Episode Card\n\t// const [forceShowCredits, setForceShowCredits] = useState(false);\n\t// const showUpNextCard = useMemo(() => {\n\t// \tif (queue?.[currentQueueItemIndex]?.Id === queue?.[queue.length - 1]?.Id) {\n\t// \t\treturn false; // Check if the current playing episode is last episode in queue\n\t// \t}\n\t// \tif (creditInfo) {\n\t// \t\tif (\n\t// \t\t\tcurrentTime >= (creditInfo.StartTicks ?? currentTime + 1) &&\n\t// \t\t\tcurrentTime < (creditInfo.EndTicks ?? 0)\n\t// \t\t)\n\t// \t\t\treturn true;\n\t// \t}\n\t// \tif (\n\t// \t\tMath.ceil(ticksToSec(itemDuration) - ticksToSec(currentTime)) <= 30 &&\n\t// \t\tMath.ceil(ticksToSec(itemDuration) - ticksToSec(currentTime)) > 0\n\t// \t) {\n\t// \t\treturn true;\n\t// \t}\n\t// \treturn false;\n\t// }, [currentTime, creditInfo, itemDuration, queue, currentQueueItemIndex]);\n\n\tconst handleExitPlayer = useCallback(async () => {\n\t\tappWindow.getCurrent().setFullscreen(false);\n\n\t\thistory.back();\n\t\tif (!api) {\n\t\t\tthrow Error(\"API is not available, cannot report playback stopped.\");\n\t\t}\n\t\t// Report Jellyfin server: Playback has ended/stopped\n\t\tgetPlaystateApi(api).reportPlaybackStopped({\n\t\t\tplaybackStopInfo: {\n\t\t\t\tFailed: false,\n\t\t\t\tItemId: itemId,\n\t\t\t\tMediaSourceId: mediaSourceId,\n\t\t\t\tPlaySessionId: playsessionId,\n\t\t\t\tPositionTicks: secToTicks(getCurrentTime() ?? 0),\n\t\t\t},\n\t\t});\n\t\tusePlaybackStore.setState(usePlaybackStore.getInitialState());\n\t\tclearQueue();\n\t}, [api, mediaSourceId, playsessionId, itemId]);\n\n\treturn (\n\t\t<motion.div\n\t\t\tclassName={\n\t\t\t\tisUserHovering || isUserSeeking || !isPlayerPlaying\n\t\t\t\t\t? \"video-player-osd hovering\"\n\t\t\t\t\t: \"video-player-osd\"\n\t\t\t}\n\t\t\tonMouseEnter={onHover}\n\t\t\tonMouseLeave={onLeave}\n\t\t\tref={playerOSDRef}\n\t\t\tinitial={{\n\t\t\t\topacity: 0,\n\t\t\t}}\n\t\t\tanimate={{\n\t\t\t\topacity: isUserHovering || isUserSeeking || !isPlayerPlaying ? 1 : 0,\n\t\t\t}}\n\t\t\tstyle={{\n\t\t\t\tzIndex: 2,\n\t\t\t}}\n\t\t\tonClick={(event) => {\n\t\t\t\tif (event.currentTarget !== event.target) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tif (event.detail === 1) {\n\t\t\t\t\tsetClickTimeout(\n\t\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\t\ttoggleIsPlaying();\n\t\t\t\t\t\t}, 200),\n\t\t\t\t\t);\n\t\t\t\t} else if (event.detail === 2 && clickTimeout) {\n\t\t\t\t\tclearTimeout(clickTimeout);\n\t\t\t\t\ttoggleIsPlayerFullscreen();\n\t\t\t\t}\n\t\t\t}}\n\t\t>\n\t\t\t<div className=\"video-player-osd-header flex flex-justify-spaced-between flex-align-center\">\n\t\t\t\t<IconButton onClick={handleExitPlayer}>\n\t\t\t\t\t<span className=\"material-symbols-rounded\">arrow_back</span>\n\t\t\t\t</IconButton>\n\t\t\t\t<IconButton onClick={(e) => setSettingsMenuRef(e.currentTarget)}>\n\t\t\t\t\t<span className=\"material-symbols-rounded\">settings</span>\n\t\t\t\t</IconButton>\n\t\t\t\t<VideoPlayerSettingsMenu\n\t\t\t\t\tsettingsMenuRef={settingsMenuRef}\n\t\t\t\t\thandleClose={() => setSettingsMenuRef(null)}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t\t<div className=\"video-player-osd-info\">\n\t\t\t\t<div>\n\t\t\t\t\t<Typography variant=\"h4\" fontWeight={500} mb={2}>\n\t\t\t\t\t\t{String(itemName)}\n\t\t\t\t\t</Typography>\n\t\t\t\t\t{episodeTitle && (\n\t\t\t\t\t\t<Typography variant=\"h6\" fontWeight={300} mt={1}>\n\t\t\t\t\t\t\t{String(episodeTitle)}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t\t<div className=\"video-player-osd-controls\">\n\t\t\t\t\t<div className=\"video-player-osd-controls-timeline\">\n\t\t\t\t\t\t<BubbleSlider />\n\t\t\t\t\t\t<ProgressDisplay />\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"flex flex-row flex-justify-spaced-between\">\n\t\t\t\t\t\t<div className=\"video-player-osd-controls-buttons\">\n\t\t\t\t\t\t\t<PlayPreviousButton />\n\t\t\t\t\t\t\t<RewindButton />\n\t\t\t\t\t\t\t<PrevChapterButton />\n\n\t\t\t\t\t\t\t<PlayPauseButton />\n\n\t\t\t\t\t\t\t<NextChapterButton />\n\n\t\t\t\t\t\t\t<ForwardButton />\n\t\t\t\t\t\t\t<PlayNextButton />\n\n\t\t\t\t\t\t\t<EndsAtDisplay />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"video-player-osd-controls-buttons\"\n\t\t\t\t\t\t\tonMouseLeave={() => setShowVolumeControl(false)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<AnimatePresence>\n\t\t\t\t\t\t\t\t{showVolumeControl && (\n\t\t\t\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\t\t\t\tinitial={{ width: 0, opacity: 0, marginRight: 0 }}\n\t\t\t\t\t\t\t\t\t\tanimate={{ width: 100, opacity: 1, marginRight: 10 }}\n\t\t\t\t\t\t\t\t\t\texit={{ width: 0, opacity: 0, marginRight: 0 }}\n\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<Slider\n\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t\tvalue={isPlayerMuted ? 0 : volume}\n\t\t\t\t\t\t\t\t\t\t\tmax={1}\n\t\t\t\t\t\t\t\t\t\t\tstep={0.01}\n\t\t\t\t\t\t\t\t\t\t\tonChange={(_, newValue) => {\n\t\t\t\t\t\t\t\t\t\t\t\tconst val = Array.isArray(newValue)\n\t\t\t\t\t\t\t\t\t\t\t\t\t? newValue[0]\n\t\t\t\t\t\t\t\t\t\t\t\t\t: newValue;\n\t\t\t\t\t\t\t\t\t\t\t\tsetVolume(val);\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\tsx={{ width: 100, color: \"white\" }}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</motion.div>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</AnimatePresence>\n\t\t\t\t\t\t\t<IconButton\n\t\t\t\t\t\t\t\tonClick={toggleIsPlayerMuted}\n\t\t\t\t\t\t\t\tonMouseEnter={() => setShowVolumeControl(true)}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">\n\t\t\t\t\t\t\t\t\t{isPlayerMuted\n\t\t\t\t\t\t\t\t\t\t? \"volume_off\"\n\t\t\t\t\t\t\t\t\t\t: volume < 0.4\n\t\t\t\t\t\t\t\t\t\t\t? \"volume_down\"\n\t\t\t\t\t\t\t\t\t\t\t: \"volume_up\"}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t\t<QueueButton />\n\n\t\t\t\t\t\t\t<ChaptersListButton />\n\n\t\t\t\t\t\t\t<CaptionsButton />\n\t\t\t\t\t\t\t<FullscreenButton />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</motion.div>\n\t);\n};\n\nexport default VideoPlayerControls;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/settingsMenu.tsx",
    "content": "import {\n\tMenuItem,\n\tPopover,\n\tSwitch,\n\tTextField,\n\tTypography,\n} from \"@mui/material\";\nimport { toNumber } from \"lodash\";\nimport React, {\n\ttype ChangeEventHandler,\n\tuseCallback,\n\tuseMemo,\n\tuseTransition,\n} from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport {\n\tchangeAudioTrack,\n\tchangeSubtitleTrack,\n\tusePlaybackStore,\n} from \"@/utils/store/playback\";\n\ntype VideoPlayerSettingsMenuProps = {\n\tsettingsMenuRef: HTMLButtonElement | null;\n\thandleClose: () => void;\n};\n\nconst VideoPlayerSettingsMenu = ({\n\tsettingsMenuRef,\n\thandleClose,\n}: VideoPlayerSettingsMenuProps) => {\n\tconst settingsMenuOpen = useMemo(() => {\n\t\treturn Boolean(settingsMenuRef);\n\t}, [settingsMenuRef]);\n\n\tconst api = useApiInContext((state) => state.api);\n\n\tconst {\n\t\tmediaSource,\n\t\tsetIsBuffering,\n\t\tshowStatsForNerds,\n\t\ttoggleShowStatsForNerds,\n\t} = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tmediaSource: state.mediaSource,\n\t\t\tsetIsBuffering: state.setIsBuffering,\n\t\t\tshowStatsForNerds: state.playerState.showStatsForNerds,\n\t\t\ttoggleShowStatsForNerds: state.toggleShowStatsForNerds,\n\t\t})),\n\t);\n\n\tconst [subtitleIsChanging, startSubtitleChange] = useTransition();\n\tconst handleSubtitleChange: ChangeEventHandler<\n\t\tHTMLInputElement | HTMLTextAreaElement\n\t> = useCallback(\n\t\t(e) => {\n\t\t\tstartSubtitleChange(() => {\n\t\t\t\tif (mediaSource.subtitle.allTracks) {\n\t\t\t\t\tchangeSubtitleTrack(\n\t\t\t\t\t\ttoNumber(e.target.value),\n\t\t\t\t\t\tmediaSource.subtitle.allTracks,\n\t\t\t\t\t);\n\t\t\t\t\thandleClose();\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\t\t[mediaSource.subtitle?.allTracks],\n\t);\n\n\tconst [audioTackIsChanging, startAudioTrackChange] = useTransition();\n\tconst handleAudioTrackChange: ChangeEventHandler<\n\t\tHTMLInputElement | HTMLTextAreaElement\n\t> = (e) => {\n\t\t// setPlaying(false);\n\t\tstartAudioTrackChange(() => {\n\t\t\tif (api && mediaSource.audio.allTracks) {\n\t\t\t\tchangeAudioTrack(toNumber(e.target.value), api);\n\t\t\t\thandleClose();\n\t\t\t\tsetIsBuffering(true);\n\t\t\t}\n\t\t});\n\n\t\t// setPlaying(true);\n\t};\n\n\treturn (\n\t\t<Popover\n\t\t\tanchorEl={settingsMenuRef}\n\t\t\topen={settingsMenuOpen}\n\t\t\tonClose={handleClose}\n\t\t\tslotProps={{\n\t\t\t\tpaper: {\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tmaxHeight: \"20em\",\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\t\tgap: \"1em\",\n\t\t\t\t\t\twidth: \"24em\",\n\t\t\t\t\t\tpadding: \"1em\",\n\t\t\t\t\t\tborderRadius: \"12px\",\n\t\t\t\t\t},\n\t\t\t\t\tclassName: \"glass\",\n\t\t\t\t},\n\t\t\t}}\n\t\t\tanchorOrigin={{\n\t\t\t\tvertical: \"bottom\",\n\t\t\t\thorizontal: \"right\",\n\t\t\t}}\n\t\t\ttransformOrigin={{\n\t\t\t\tvertical: \"top\",\n\t\t\t\thorizontal: \"right\",\n\t\t\t}}\n\t\t>\n\t\t\t<TextField\n\t\t\t\tselect\n\t\t\t\tlabel=\"Audio\"\n\t\t\t\tvariant=\"outlined\"\n\t\t\t\tvalue={mediaSource.audio?.track}\n\t\t\t\tonChange={handleAudioTrackChange}\n\t\t\t\tfullWidth\n\t\t\t\tdisabled={audioTackIsChanging}\n\t\t\t>\n\t\t\t\t{mediaSource.audio?.allTracks?.map((sub) => (\n\t\t\t\t\t<MenuItem key={sub.Index} value={sub.Index}>\n\t\t\t\t\t\t{sub.DisplayTitle}\n\t\t\t\t\t</MenuItem>\n\t\t\t\t))}\n\t\t\t</TextField>\n\t\t\t<TextField\n\t\t\t\tselect\n\t\t\t\tlabel=\"Subtitles\"\n\t\t\t\tvariant=\"outlined\"\n\t\t\t\tvalue={mediaSource.subtitle?.track}\n\t\t\t\tonChange={handleSubtitleChange}\n\t\t\t\tfullWidth\n\t\t\t\tdisabled={mediaSource.subtitle?.track === -2 || subtitleIsChanging}\n\t\t\t>\n\t\t\t\t<MenuItem key={-1} value={-1}>\n\t\t\t\t\tNo Subtitle\n\t\t\t\t</MenuItem>\n\t\t\t\t{mediaSource.subtitle?.allTracks?.map((sub) => (\n\t\t\t\t\t<MenuItem key={sub.Index} value={sub.Index}>\n\t\t\t\t\t\t{sub.DisplayTitle}\n\t\t\t\t\t</MenuItem>\n\t\t\t\t))}\n\t\t\t</TextField>\n\t\t\t<MenuItem onClick={toggleShowStatsForNerds}>\n\t\t\t\t<Typography flexGrow={1}>Stats for Nerds</Typography>\n\t\t\t\t<Switch checked={showStatsForNerds} />\n\t\t\t</MenuItem>\n\t\t</Popover>\n\t);\n};\n\nexport default VideoPlayerSettingsMenu;\n"
  },
  {
    "path": "src/components/playback/videoPlayer/upNextFlyout.scss",
    "content": ".video-player-up_next_flyout {\n    position: absolute;\n    bottom: 2em;\n    right: 2em;\n    width: 42em;\n    height: fit-content;\n    display: flex;\n    gap: 10px;\n    align-items: stretch;\n    justify-content: center;\n    background-color: rgba(60, 60, 60,1);\n    padding: 10px;\n    border-radius: $border-radius-default + 10px;\n    z-index: 10;\n    transition: bottom 0.3s ease-in-out;\n    \n    &.floating {\n        bottom: 18vh\n    }\n    \n    &-thumbnail {\n        max-width: 100%;\n        max-height: 100%;\n        width: 15em;\n        object-fit: cover;\n        border-radius: $border-radius-default;\n    }\n    &-details {\n        display: flex;\n        flex-direction: column;\n        gap: 5px;\n        overflow: hidden;\n        justify-content: flex-start;\n        align-items: flex-start;\n        // height: 100%;\n    }\n    &-button{\n        // justify-self: flex-end;\n        margin-top: auto;\n    }\n}"
  },
  {
    "path": "src/components/playback/videoPlayer/upNextFlyout.tsx",
    "content": "import { Box, Button, IconButton, Typography } from \"@mui/material\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { usePlaybackStore } from \"@/utils/store/playback\";\nimport useQueue from \"@/utils/store/queue\";\n\nimport \"./upNextFlyout.scss\";\n\nconst UpNextFlyout = () => {\n\tconst {\n\t\tactiveSegmentId,\n\t\tcurrentSegmemtIndex,\n\t\tmediaSegments,\n\t\tskipSegment,\n\t\tisUserHovering,\n\t\tisUserSeeking,\n\t\tisPlayerPlaying,\n\t} = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\tactiveSegmentId: state.activeSegmentId,\n\t\t\tcurrentSegmemtIndex: state.nextSegmentIndex - 1,\n\t\t\tmediaSegments: state.metadata.mediaSegments,\n\t\t\tskipSegment: state.skipSegment,\n\t\t\tisUserHovering: state.playerState.isUserHovering,\n\t\t\tisUserSeeking: state.playerState.isUserSeeking,\n\t\t\tisPlayerPlaying: state.playerState.isPlayerPlaying,\n\t\t})),\n\t);\n\n\tconst { nextItemIndex, tracks } = useQueue(\n\t\tuseShallow((state) => ({\n\t\t\tnextItemIndex: state.currentItemIndex + 1,\n\t\t\ttracks: state.tracks,\n\t\t})),\n\t);\n\n\tconst [isHidden, setIsHidden] = useState(false);\n\n\tuseEffect(() => {\n\t\tsetIsHidden(false);\n\t}, [activeSegmentId]);\n\n\tconst areControlsVisible = useMemo(() => {\n\t\treturn isUserHovering || isUserSeeking || !isPlayerPlaying;\n\t}, [isUserHovering, isUserSeeking, isPlayerPlaying]);\n\n\tconst api = useApiInContext((s) => s.api);\n\n\tif (\n\t\t!api ||\n\t\t!activeSegmentId ||\n\t\tmediaSegments?.Items?.[currentSegmemtIndex].Type !== \"Outro\" ||\n\t\tisHidden\n\t) {\n\t\treturn null;\n\t}\n\n\tconst item = tracks?.[nextItemIndex];\n\tif (!item || !item.Id) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t<AnimatePresence>\n\t\t\t<motion.div\n\t\t\t\tclassName={\n\t\t\t\t\tareControlsVisible\n\t\t\t\t\t\t? \"video-player-up_next_flyout floating\"\n\t\t\t\t\t\t: \"video-player-up_next_flyout\"\n\t\t\t\t}\n\t\t\t\tinitial={{ opacity: 0, y: 50, scale: 0.95 }}\n\t\t\t\tanimate={{ opacity: 1, y: 0, scale: 1 }}\n\t\t\t\texit={{ opacity: 0, y: 20, scale: 0.95 }}\n\t\t\t\ttransition={{ duration: 0.4, ease: [0.4, 0, 0.2, 1] }}\n\t\t\t\tstyle={{\n\t\t\t\t\tbackground: \"rgba(20, 20, 20, 0.6)\",\n\t\t\t\t\tbackdropFilter: \"blur(16px) saturate(180%)\",\n\t\t\t\t\tborder: \"1px solid rgba(255, 255, 255, 0.1)\",\n\t\t\t\t\tboxShadow: \"0 8px 32px 0 rgba(0, 0, 0, 0.3)\",\n\t\t\t\t\tborderRadius: \"24px\",\n\t\t\t\t\tpadding: \"16px\",\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\tgap: \"16px\",\n\t\t\t\t\tmaxWidth: \"500px\",\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Box\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tposition: \"relative\",\n\t\t\t\t\t\twidth: \"160px\",\n\t\t\t\t\t\tminWidth: \"160px\",\n\t\t\t\t\t\tborderRadius: \"8px\",\n\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t\taspectRatio: \"16/9\",\n\t\t\t\t\t\tboxShadow: \"0 4px 12px rgba(0,0,0,0.3)\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<img\n\t\t\t\t\t\tsrc={getImageUrlsApi(api).getItemImageUrlById(item.Id, \"Primary\", {\n\t\t\t\t\t\t\ttag: item.ImageTags?.Primary,\n\t\t\t\t\t\t\tquality: 90,\n\t\t\t\t\t\t\tfillWidth: 320,\n\t\t\t\t\t\t})}\n\t\t\t\t\t\talt=\"next item thumbnail\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\tobjectFit: \"cover\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t<Box\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\tinset: 0,\n\t\t\t\t\t\t\tbackground:\n\t\t\t\t\t\t\t\t\"linear-gradient(to top, rgba(0,0,0,0.6) 0%, transparent 100%)\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t</Box>\n\n\t\t\t\t<Box\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\t\tflex: 1,\n\t\t\t\t\t\tminWidth: 0,\n\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tvariant=\"overline\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tcolor: \"primary.main\",\n\t\t\t\t\t\t\tfontWeight: 700,\n\t\t\t\t\t\t\tlineHeight: 1,\n\t\t\t\t\t\t\tmb: 0.5,\n\t\t\t\t\t\t\tletterSpacing: 1.2,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\tUP NEXT\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tvariant=\"h6\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tfontWeight: 600,\n\t\t\t\t\t\t\tlineHeight: 1.2,\n\t\t\t\t\t\t\tmb: 0.5,\n\t\t\t\t\t\t\tfontSize: \"1.1rem\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tnoWrap\n\t\t\t\t\t>\n\t\t\t\t\t\t{item.SeriesName || item.Name}\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tvariant=\"body2\"\n\t\t\t\t\t\tcolor=\"text.secondary\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tmb: 2,\n\t\t\t\t\t\t\tdisplay: \"-webkit-box\",\n\t\t\t\t\t\t\tWebkitLineClamp: 1,\n\t\t\t\t\t\t\tWebkitBoxOrient: \"vertical\",\n\t\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t\t\topacity: 0.8,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{item.SeriesName\n\t\t\t\t\t\t\t? `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`\n\t\t\t\t\t\t\t: item.Overview || \"\"}\n\t\t\t\t\t</Typography>\n\n\t\t\t\t\t<Box sx={{ display: \"flex\", gap: 1, mt: \"auto\" }}>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tonClick={skipSegment}\n\t\t\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\t\t\tcolor=\"primary\"\n\t\t\t\t\t\t\tstartIcon={\n\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded fill\">\n\t\t\t\t\t\t\t\t\tplay_arrow\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\tborderRadius: \"8px\",\n\t\t\t\t\t\t\t\ttextTransform: \"none\",\n\t\t\t\t\t\t\t\tfontWeight: 600,\n\t\t\t\t\t\t\t\tboxShadow:\n\t\t\t\t\t\t\t\t\t\"0 4px 12px rgba(var(--mui-palette-primary-mainChannel), 0.3)\",\n\t\t\t\t\t\t\t\tflex: 1,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tPlay Now\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<IconButton\n\t\t\t\t\t\t\tonClick={() => setIsHidden(true)}\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\tborderRadius: \"8px\",\n\t\t\t\t\t\t\t\tborder: \"1px solid rgba(255,255,255,0.1)\",\n\t\t\t\t\t\t\t\tcolor: \"text.secondary\",\n\t\t\t\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.1)\",\n\t\t\t\t\t\t\t\t\tcolor: \"white\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">close</span>\n\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t</Box>\n\t\t\t\t</Box>\n\t\t\t</motion.div>\n\t\t</AnimatePresence>\n\t);\n};\n\nexport default UpNextFlyout;\n"
  },
  {
    "path": "src/components/queueListItem/index.tsx",
    "content": "import type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport {\n\tBox,\n\tIconButton,\n\tListItem,\n\tListItemAvatar,\n\tListItemText,\n} from \"@mui/material\";\nimport React from \"react\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { getTypeIcon } from \"../utils/iconsCollection\";\n\ntype QueueListItemProps = {\n\tqueueItem: BaseItemDto;\n\tactive?: boolean;\n\tonDelete?: () => void;\n\tonPlay?: () => void;\n\tdragHandleProps?: any;\n\tisEpisode?: boolean;\n\tindex?: number;\n\tclassName?: string;\n\tsx?: any;\n};\n\nconst QueueListItem = ({\n\tqueueItem,\n\tactive,\n\tonDelete,\n\tonPlay,\n\tdragHandleProps,\n\tisEpisode,\n\tclassName,\n\tsx,\n}: QueueListItemProps) => {\n\tconst api = useApiInContext((s) => s.api);\n\n\tconst imageUrl =\n\t\tapi &&\n\t\tqueueItem.Id &&\n\t\t(queueItem.ImageTags?.Primary\n\t\t\t? getImageUrlsApi(api).getItemImageUrlById(queueItem.Id, \"Primary\", {\n\t\t\t\t\ttag: queueItem.ImageTags.Primary,\n\t\t\t\t})\n\t\t\t: queueItem.AlbumPrimaryImageTag?.[0]\n\t\t\t\t? getImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\tqueueItem.AlbumId || \"\",\n\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t{ tag: queueItem.AlbumPrimaryImageTag[0] },\n\t\t\t\t\t)\n\t\t\t\t: null);\n\n\tconst primaryText = queueItem.SeriesName || queueItem.Name;\n\n\tlet secondaryText = \"\";\n\tif (isEpisode) {\n\t\tconst season = queueItem.ParentIndexNumber;\n\t\tconst episode = queueItem.IndexNumber;\n\t\tconst episodeEnd = queueItem.IndexNumberEnd;\n\t\tconst episodeString = episodeEnd\n\t\t\t? `S${season}:E${episode}-${episodeEnd}`\n\t\t\t: `S${season}:E${episode}`;\n\n\t\tsecondaryText = `${episodeString} - ${queueItem.Name}`;\n\t} else {\n\t\tsecondaryText = queueItem.Artists?.join(\", \") || \"\";\n\t}\n\n\treturn (\n\t\t<ListItem\n\t\t\tcomponent=\"div\"\n\t\t\tclassName={className}\n\t\t\tsecondaryAction={\n\t\t\t\tonDelete && (\n\t\t\t\t\t<IconButton\n\t\t\t\t\t\tedge=\"end\"\n\t\t\t\t\t\taria-label=\"delete\"\n\t\t\t\t\t\tonClick={onDelete}\n\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\ttransition: \"opacity 0.2s\",\n\t\t\t\t\t\t\t\".MuiListItem-root:hover &\": { opacity: 0.7 },\n\t\t\t\t\t\t\t\"&:hover\": { opacity: \"1 !important\", bgcolor: \"action.hover\" },\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span className=\"material-symbols-rounded\" style={{ fontSize: 20 }}>\n\t\t\t\t\t\t\tclose\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</IconButton>\n\t\t\t\t)\n\t\t\t}\n\t\t\tsx={{\n\t\t\t\topacity: active ? 1 : 1,\n\t\t\t\tbgcolor: active ? \"rgba(255, 255, 255, 0.1)\" : \"rgba(0,0,0,0.2)\",\n\t\t\t\tborderRadius: 3,\n\t\t\t\tmb: 0,\n\t\t\t\ttransition: \"all 0.2s ease\",\n\t\t\t\tborder: \"1px solid\",\n\t\t\t\tborderColor: active\n\t\t\t\t\t? \"rgba(255, 255, 255, 0.2)\"\n\t\t\t\t\t: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\"&:hover\": {\n\t\t\t\t\tbgcolor: active\n\t\t\t\t\t\t? \"rgba(255, 255, 255, 0.15)\"\n\t\t\t\t\t\t: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\ttransform: active ? \"none\" : \"translateY(-2px)\",\n\t\t\t\t\tborderColor: active\n\t\t\t\t\t\t? \"rgba(255, 255, 255, 0.3)\"\n\t\t\t\t\t\t: \"rgba(255,255,255,0.2)\",\n\t\t\t\t\tboxShadow: active ? \"none\" : \"0 4px 12px rgba(0,0,0,0.2)\",\n\t\t\t\t},\n\t\t\t\tpr: onDelete ? 6 : 2,\n\t\t\t\t...sx,\n\t\t\t}}\n\t\t>\n\t\t\t{dragHandleProps && (\n\t\t\t\t<div\n\t\t\t\t\t{...dragHandleProps}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tcursor: \"grab\",\n\t\t\t\t\t\tmarginRight: 12,\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\ttransition: \"opacity 0.2s\",\n\t\t\t\t\t}}\n\t\t\t\t\tclassName=\"drag-handle\"\n\t\t\t\t>\n\t\t\t\t\t<span className=\"material-symbols-rounded\">drag_indicator</span>\n\t\t\t\t</div>\n\t\t\t)}\n\n\t\t\t<ListItemAvatar sx={{ minWidth: 64 }}>\n\t\t\t\t<Box\n\t\t\t\t\tonClick={onPlay}\n\t\t\t\t\tsx={{\n\t\t\t\t\t\twidth: 48,\n\t\t\t\t\t\theight: 48,\n\t\t\t\t\t\tborderRadius: 2,\n\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t\tposition: \"relative\",\n\t\t\t\t\t\tboxShadow: \"0 2px 8px rgba(0,0,0,0.2)\",\n\t\t\t\t\t\tcursor: onPlay ? \"pointer\" : \"default\",\n\t\t\t\t\t\t\"&:hover .play-overlay\": {\n\t\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\t},\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{imageUrl ? (\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tsrc={imageUrl}\n\t\t\t\t\t\t\talt={queueItem.Name || \"\"}\n\t\t\t\t\t\t\tstyle={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\t\tbackground: \"#2a2a2a\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{queueItem.Type && getTypeIcon(queueItem.Type)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t\t{active && (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\t\tinset: 0,\n\t\t\t\t\t\t\t\tbackground: \"rgba(0,0,0,0.4)\",\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\t\tbackdropFilter: \"blur(2px)\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\tstyle={{ fontSize: \"1.5rem\", color: \"white\" }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tequalizer\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t\t{!active && onPlay && (\n\t\t\t\t\t\t<Box\n\t\t\t\t\t\t\tclassName=\"play-overlay\"\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\t\tinset: 0,\n\t\t\t\t\t\t\t\tbackground: \"rgba(0,0,0,0.4)\",\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t\ttransition: \"opacity 0.2s\",\n\t\t\t\t\t\t\t\tbackdropFilter: \"blur(1px)\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\tstyle={{ fontSize: \"2rem\", color: \"white\" }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tplay_arrow\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</Box>\n\t\t\t\t\t)}\n\t\t\t\t</Box>\n\t\t\t</ListItemAvatar>\n\n\t\t\t<ListItemText\n\t\t\t\tprimary={primaryText}\n\t\t\t\tsecondary={secondaryText}\n\t\t\t\tslotProps={{\n\t\t\t\t\tprimary: {\n\t\t\t\t\t\tnoWrap: true,\n\t\t\t\t\t\tvariant: \"body2\",\n\t\t\t\t\t\tfontWeight: active ? 700 : 500,\n\t\t\t\t\t\tsx: { mb: 0.5 },\n\t\t\t\t\t},\n\t\t\t\t\tsecondary: {\n\t\t\t\t\t\tnoWrap: true,\n\t\t\t\t\t\tvariant: \"caption\",\n\t\t\t\t\t\tsx: { opacity: 0.7 },\n\t\t\t\t\t},\n\t\t\t\t}}\n\t\t\t/>\n\t\t</ListItem>\n\t);\n};\n\nexport default QueueListItem;\n"
  },
  {
    "path": "src/components/queueTrack/index.tsx",
    "content": "import { useSortable } from \"@dnd-kit/sortable\";\nimport { CSS } from \"@dnd-kit/utilities\";\nimport type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport { IconButton, Typography } from \"@mui/material\";\nimport React from \"react\";\nimport { getRuntimeMusic } from \"@/utils/date/time\";\nimport useQueue from \"@/utils/store/queue\";\n\ntype Props = {\n\ttrack: BaseItemDto;\n\tindex: number;\n\tonRemove?: () => void;\n};\nconst QueueTrack = (props: Props) => {\n\tconst { track, index, onRemove } = props;\n\tconst [currentItemIndex] = useQueue((s) => [s.currentItemIndex]);\n\n\tconst { attributes, listeners, setNodeRef, transform, transition } =\n\t\tuseSortable({\n\t\t\tid: track.Id ?? \"\",\n\t\t});\n\n\tconst style = {\n\t\t// opacity: isDragging ? 0.5 : 1,\n\t\ttransform: CSS.Transform.toString(transform),\n\t\ttransition,\n\t};\n\n\treturn (\n\t\t<div\n\t\t\tref={setNodeRef}\n\t\t\tstyle={style}\n\t\t\tclassName={\n\t\t\t\tcurrentItemIndex === index\n\t\t\t\t\t? \"audio-queue-track active\"\n\t\t\t\t\t: \"audio-queue-track\"\n\t\t\t}\n\t\t>\n\t\t\t<span {...attributes} {...listeners} className=\"material-symbols-rounded\">\n\t\t\t\tdrag_handle\n\t\t\t</span>\n\t\t\t<div className=\"audio-queue-track-info\">\n\t\t\t\t<Typography className=\"audio-queue-track-info-name\">\n\t\t\t\t\t{track.Name}\n\t\t\t\t</Typography>\n\t\t\t\t<Typography fontWeight={300} className=\"opacity-07\" variant=\"subtitle2\">\n\t\t\t\t\t{track.Artists?.join(\", \")}\n\t\t\t\t</Typography>\n\t\t\t</div>\n\t\t\t<div style={{ display: \"flex\", alignItems: \"center\", gap: \"0.5em\" }}>\n\t\t\t\t<Typography className=\"opacity-07\">\n\t\t\t\t\t{getRuntimeMusic(track.RunTimeTicks ?? 0)}\n\t\t\t\t</Typography>\n\t\t\t\t{onRemove && (\n\t\t\t\t\t<IconButton size=\"small\" onClick={onRemove}>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\tstyle={{ fontSize: \"1.2em\" }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tclose\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</IconButton>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t);\n};\n\nexport default QueueTrack;"
  },
  {
    "path": "src/components/routerLoading/index.tsx",
    "content": "import { CircularProgress } from \"@mui/material\";\nimport { useRouterState } from \"@tanstack/react-router\";\nimport React from \"react\";\n\nexport default function RouterLoading() {\n\tconst routeLoading = useRouterState().isLoading;\n\tif (!routeLoading) return null;\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\twidth: \"100vw\",\n\t\t\t\theight: \"100vh\",\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\tjustifyContent: \"center\",\n\t\t\t\talignItems: \"center\",\n\t\t\t\tbackground: \"rgb(0 0 0 / 0.35)\",\n\t\t\t\tposition: \"fixed\",\n\t\t\t\tzIndex: 10000,\n\t\t\t}}\n\t\t>\n\t\t\t<CircularProgress size={72} thickness={2} />\n\t\t</div>\n\t);\n}"
  },
  {
    "path": "src/components/search/index.tsx",
    "content": "import { ItemSortBy } from \"@jellyfin/sdk/lib/generated-client\";\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport { getSearchApi } from \"@jellyfin/sdk/lib/utils/api/search-api\";\nimport {\n\tBox,\n\tButton,\n\tDialog,\n\tGrow,\n\tIconButton,\n\tInputBase,\n\tLinearProgress,\n\tStack,\n\tTypography,\n\tuseTheme,\n} from \"@mui/material\";\nimport type { TransitionProps } from \"@mui/material/transitions\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useNavigate } from \"@tanstack/react-router\";\nimport { register } from \"@tauri-apps/plugin-global-shortcut\";\nimport React, {\n\ttype ChangeEvent,\n\tuseCallback,\n\tuseEffect,\n\tuseMemo,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport useDebounce from \"@/utils/hooks/useDebounce\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport useSearchStore from \"@/utils/store/search\";\nimport SearchItem from \"./item\";\n\nconst Transition = React.forwardRef(function Transition(\n\tprops: TransitionProps & {\n\t\tchildren: React.ReactElement<any, any>;\n\t},\n\tref: React.Ref<unknown>,\n) {\n\treturn <Grow ref={ref} {...props} />;\n});\n\nconst Search = () => {\n\tconst theme = useTheme();\n\tconst { isOpen, handleClose, toggleSearchDialog } = useSearchStore(\n\t\tuseShallow((s) => ({\n\t\t\tisOpen: s.showSearchDialog,\n\t\t\thandleClose: s.toggleSearchDialog,\n\t\t\ttoggleSearchDialog: s.toggleSearchDialog,\n\t\t})),\n\t);\n\tconst api = useApiInContext((s) => s.api);\n\tconst userId = useCentralStore((s) => s.currentUser?.Id || \"\");\n\tconst suggestions = useQuery({\n\t\tqueryKey: [\"search\", \"suggestions\"],\n\t\tqueryFn: async () =>\n\t\t\tapi &&\n\t\t\t(\n\t\t\t\tawait getItemsApi(api).getItems({\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\tlimit: 5,\n\t\t\t\t\tsortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random],\n\t\t\t\t\tincludeItemTypes: [\"Movie\", \"Series\", \"MusicArtist\"],\n\t\t\t\t\tenableImages: true,\n\t\t\t\t\trecursive: true,\n\t\t\t\t})\n\t\t\t).data,\n\t});\n\n\tconst [searchTerm, setSearchTerm] = useState(\"\");\n\tconst debouncedSearchTerm = useDebounce(searchTerm, 300);\n\tconst inputRef = useRef<HTMLInputElement>(null);\n\n\tuseEffect(() => {\n\t\tif (isOpen) {\n\t\t\tsetTimeout(() => {\n\t\t\t\tinputRef.current?.focus();\n\t\t\t}, 100);\n\t\t}\n\t}, [isOpen]);\n\n\tconst handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {\n\t\tsetSearchTerm(e.target.value);\n\t};\n\n\tconst searchResults = useQuery({\n\t\tqueryKey: [\"search\", debouncedSearchTerm],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tif (debouncedSearchTerm.trim() === \"\") {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\treturn (\n\t\t\t\tawait getSearchApi(api).getSearchHints({\n\t\t\t\t\tsearchTerm: debouncedSearchTerm,\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\tlimit: 10,\n\t\t\t\t\tincludeItemTypes: [\n\t\t\t\t\t\t\"Movie\",\n\t\t\t\t\t\t\"Series\",\n\t\t\t\t\t\t\"MusicAlbum\",\n\t\t\t\t\t\t\"Person\",\n\t\t\t\t\t\t\"Audio\",\n\t\t\t\t\t\t\"Photo\",\n\t\t\t\t\t\t\"PhotoAlbum\",\n\t\t\t\t\t\t\"Playlist\",\n\t\t\t\t\t],\n\t\t\t\t\t// enableImages: true,\n\t\t\t\t})\n\t\t\t).data;\n\t\t},\n\t});\n\n\tconst showSuggestions = useMemo(\n\t\t() =>\n\t\t\t(debouncedSearchTerm.length === 0 || searchResults.isLoading) &&\n\t\t\t(suggestions.data?.Items?.length ?? 0) > 0,\n\t\t[\n\t\t\tdebouncedSearchTerm.length,\n\t\t\tsearchResults.isLoading,\n\t\t\tsuggestions.data?.Items?.length,\n\t\t],\n\t);\n\n\tconst navigate = useNavigate();\n\tconst handleClearSearch = useCallback(() => {\n\t\tsetSearchTerm(\"\");\n\t}, []);\n\tconst handleAdvancedSearch = useCallback(() => {\n\t\tnavigate({ to: \"/search\", search: { query: debouncedSearchTerm } });\n\t\ttoggleSearchDialog();\n\t}, [navigate, toggleSearchDialog, debouncedSearchTerm]);\n\n\tuseEffect(() => {\n\t\tasync function registerglobalShortcut() {\n\t\t\tawait register(\"CommandOrControl+K\", (e) => {\n\t\t\t\tif (e.state === \"Pressed\") {\n\t\t\t\t\ttoggleSearchDialog();\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\t\tregisterglobalShortcut();\n\t}, []);\n\n\treturn (\n\t\t<Dialog\n\t\t\topen={isOpen}\n\t\t\tslotProps={{\n\t\t\t\ttransition: Transition,\n\t\t\t\tpaper: {\n\t\t\t\t\tclassName: \"glass-panel\",\n\t\t\t\t\tsx: {\n\t\t\t\t\t\twillChange: \"opacity, transform, backdrop-filter\",\n\t\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}}\n\t\t\tonClose={handleClose}\n\t\t\tfullWidth\n\t\t\tmaxWidth=\"sm\"\n\t\t>\n\t\t\t<Box sx={{ position: \"relative\" }}>\n\t\t\t\t<Stack\n\t\t\t\t\tdirection=\"row\"\n\t\t\t\t\talignItems=\"center\"\n\t\t\t\t\tspacing={1.5}\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tp: \"16px 24px\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tcolor: \"var(--mui-palette-text-secondary)\",\n\t\t\t\t\t\t\tfontSize: \"1.75rem\",\n\t\t\t\t\t\t\topacity: 0.7,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\tsearch\n\t\t\t\t\t</span>\n\t\t\t\t\t<InputBase\n\t\t\t\t\t\tinputRef={inputRef}\n\t\t\t\t\t\tplaceholder=\"Search movies, shows, and more...\"\n\t\t\t\t\t\tonChange={handleSearchChange}\n\t\t\t\t\t\tvalue={searchTerm}\n\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tfontSize: \"1.25rem\",\n\t\t\t\t\t\t\tfontWeight: 400,\n\t\t\t\t\t\t\tcolor: \"text.primary\",\n\t\t\t\t\t\t\t\"& input::placeholder\": {\n\t\t\t\t\t\t\t\tcolor: \"text.disabled\",\n\t\t\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t{searchTerm && (\n\t\t\t\t\t\t<IconButton\n\t\t\t\t\t\t\tonClick={handleClearSearch}\n\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\tcolor: \"text.secondary\",\n\t\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.15)\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\tstyle={{ fontSize: \"1.25rem\" }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tclose\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t)}\n\t\t\t\t</Stack>\n\t\t\t\t{searchResults.isLoading && (\n\t\t\t\t\t<LinearProgress\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\theight: 2,\n\t\t\t\t\t\t\tbackgroundColor: \"transparent\",\n\t\t\t\t\t\t\t\"& .MuiLinearProgress-bar\": {\n\t\t\t\t\t\t\t\tbackgroundImage: `linear-gradient(90deg, ${theme.palette.primary.main}, ${theme.palette.secondary.main})`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\tbottom: 0,\n\t\t\t\t\t\t\tleft: 0,\n\t\t\t\t\t\t\tright: 0,\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t\t<Box\n\t\t\t\t\tsx={{\n\t\t\t\t\t\theight: \"1px\",\n\t\t\t\t\t\tbgcolor: \"divider\",\n\t\t\t\t\t\topacity: 0.1,\n\t\t\t\t\t\tmx: 2,\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</Box>\n\n\t\t\t<Stack\n\t\t\t\tspacing={1}\n\t\t\t\tsx={{\n\t\t\t\t\tp: 1,\n\t\t\t\t\tmaxHeight: \"55vh\",\n\t\t\t\t\toverflowY: \"auto\",\n\t\t\t\t\tminHeight:\n\t\t\t\t\t\tshowSuggestions ||\n\t\t\t\t\t\t(debouncedSearchTerm &&\n\t\t\t\t\t\t\tsearchResults.data?.SearchHints?.length !== 0)\n\t\t\t\t\t\t\t? \"auto\"\n\t\t\t\t\t\t\t: \"150px\",\n\t\t\t\t\t\"&::-webkit-scrollbar\": {\n\t\t\t\t\t\twidth: \"8px\",\n\t\t\t\t\t},\n\t\t\t\t\t\"&::-webkit-scrollbar-thumb\": {\n\t\t\t\t\t\tbackground: \"rgba(255,255,255,0.1)\",\n\t\t\t\t\t\tborderRadius: \"4px\",\n\t\t\t\t\t},\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{debouncedSearchTerm.length > 0 &&\n\t\t\t\t\tsearchResults.data?.SearchHints &&\n\t\t\t\t\tsearchResults.data.SearchHints.length > 0 && (\n\t\t\t\t\t\t<Box>\n\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t\t\t\tfontWeight={600}\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\tpx: 2,\n\t\t\t\t\t\t\t\t\tpy: 1.5,\n\t\t\t\t\t\t\t\t\tcolor: \"text.secondary\",\n\t\t\t\t\t\t\t\t\ttextTransform: \"uppercase\",\n\t\t\t\t\t\t\t\t\tletterSpacing: \"0.05em\",\n\t\t\t\t\t\t\t\t\tdisplay: \"block\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tResults\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t<Stack spacing={0.5} sx={{ px: 1 }}>\n\t\t\t\t\t\t\t\t{searchResults.data.SearchHints.map((item) => (\n\t\t\t\t\t\t\t\t\t<SearchItem\n\t\t\t\t\t\t\t\t\t\tkey={item.Id}\n\t\t\t\t\t\t\t\t\t\titemName={item.Name || \"Untitled\"}\n\t\t\t\t\t\t\t\t\t\timageUrl={\n\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\titem.PrimaryImageTag &&\n\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrl(item, \"Primary\", {\n\t\t\t\t\t\t\t\t\t\t\t\tmaxWidth: 90,\n\t\t\t\t\t\t\t\t\t\t\t\tmaxHeight: 140,\n\t\t\t\t\t\t\t\t\t\t\t\ttag: item.PrimaryImageTag,\n\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\titemYear={\n\t\t\t\t\t\t\t\t\t\t\titem.Type === \"Series\"\n\t\t\t\t\t\t\t\t\t\t\t\t? `${item.ProductionYear?.toString()} - ${new Date(item.EndDate ?? \"\").getFullYear().toString()}`\n\t\t\t\t\t\t\t\t\t\t\t\t: item.ProductionYear?.toString()\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\titemType={item.Type}\n\t\t\t\t\t\t\t\t\t\titemId={item.Id ?? \"\"}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</Stack>\n\t\t\t\t\t\t</Box>\n\t\t\t\t\t)}\n\n\t\t\t\t{showSuggestions && (\n\t\t\t\t\t<Box>\n\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t\t\tfontWeight={600}\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\tpx: 2,\n\t\t\t\t\t\t\t\tpy: 1.5,\n\t\t\t\t\t\t\t\tcolor: \"text.secondary\",\n\t\t\t\t\t\t\t\ttextTransform: \"uppercase\",\n\t\t\t\t\t\t\t\tletterSpacing: \"0.05em\",\n\t\t\t\t\t\t\t\tdisplay: \"block\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tSuggestions\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t<Stack spacing={0.5} sx={{ px: 1 }}>\n\t\t\t\t\t\t\t{suggestions.data?.Items?.map((item) => (\n\t\t\t\t\t\t\t\t<SearchItem\n\t\t\t\t\t\t\t\t\tkey={item.Id}\n\t\t\t\t\t\t\t\t\titemName={item.Name || \"Untitled\"}\n\t\t\t\t\t\t\t\t\timageUrl={\n\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\titem.ImageTags?.Primary &&\n\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrl(item, \"Primary\", {\n\t\t\t\t\t\t\t\t\t\t\tmaxWidth: 90,\n\t\t\t\t\t\t\t\t\t\t\tmaxHeight: 140,\n\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\titemYear={\n\t\t\t\t\t\t\t\t\t\titem.Type === \"Series\"\n\t\t\t\t\t\t\t\t\t\t\t? `${item.ProductionYear?.toString()} - ${new Date(item.EndDate ?? \"\").getFullYear().toString()}`\n\t\t\t\t\t\t\t\t\t\t\t: item.ProductionYear?.toString()\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\titemType={item.Type}\n\t\t\t\t\t\t\t\t\titemId={item.Id ?? \"\"}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t</Box>\n\t\t\t\t)}\n\n\t\t\t\t{searchResults.isSuccess &&\n\t\t\t\t\tsearchResults.data?.SearchHints?.length === 0 && (\n\t\t\t\t\t\t<Stack\n\t\t\t\t\t\t\talignItems=\"center\"\n\t\t\t\t\t\t\tjustifyContent=\"center\"\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\t\tminHeight: \"200px\",\n\t\t\t\t\t\t\t\tcolor: \"text.secondary\",\n\t\t\t\t\t\t\t\tgap: 2,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\tstyle={{ fontSize: \"48px\", opacity: 0.2 }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tsearch_off\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<Typography variant=\"body1\">\n\t\t\t\t\t\t\t\tNo results found for \"{debouncedSearchTerm}\"\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t)}\n\t\t\t</Stack>\n\n\t\t\t<Stack\n\t\t\t\tdirection=\"row\"\n\t\t\t\tjustifyContent=\"flex-end\"\n\t\t\t\tsx={{\n\t\t\t\t\tp: 1.5,\n\t\t\t\t\tmt: 1,\n\t\t\t\t\tbackgroundImage:\n\t\t\t\t\t\t\"linear-gradient(to top, rgba(0,0,0,0.2), transparent)\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Button\n\t\t\t\t\tonClick={handleAdvancedSearch}\n\t\t\t\t\tendIcon={\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\tstyle={{ fontSize: \"1.25rem\" }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tarrow_forward\n\t\t\t\t\t\t</span>\n\t\t\t\t\t}\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tcolor: \"text.secondary\",\n\t\t\t\t\t\ttextTransform: \"none\",\n\t\t\t\t\t\tfontWeight: 500,\n\t\t\t\t\t\tpadding: \"6px 16px\",\n\t\t\t\t\t\tborderRadius: \"8px\",\n\t\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\t\tcolor: \"primary.main\",\n\t\t\t\t\t\t\tbackgroundColor: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\t\t},\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\tAdvanced Search\n\t\t\t\t</Button>\n\t\t\t</Stack>\n\t\t</Dialog>\n\t);\n};\n\nexport default Search;\n"
  },
  {
    "path": "src/components/search/item.tsx",
    "content": "import { BaseItemKind } from \"@jellyfin/sdk/lib/generated-client\";\nimport { Box, Button, Stack, Typography } from \"@mui/material\";\nimport { useNavigate } from \"@tanstack/react-router\";\nimport React from \"react\";\nimport { useShallow } from \"zustand/react/shallow\";\nimport useSearchStore from \"@/utils/store/search\";\nimport { getTypeIcon } from \"../utils/iconsCollection\";\n\ntype SearchItemProps = {\n\titemName: string;\n\timageUrl?: string | null;\n\titemYear?: string;\n\titemType?: BaseItemKind;\n\titemId: string;\n};\n\nconst SearchItem = (props: SearchItemProps) => {\n\tconst { itemName, imageUrl, itemYear, itemType, itemId } = props;\n\tconst toggleSearchDialog = useSearchStore(\n\t\tuseShallow((s) => s.toggleSearchDialog),\n\t);\n\tconst navigate = useNavigate();\n\tconst handleOnClick = () => {\n\t\ttoggleSearchDialog();\n\t\tswitch (itemType) {\n\t\t\tcase BaseItemKind.BoxSet:\n\t\t\t\tnavigate({ to: \"/boxset/$id\", params: { id: itemId } });\n\t\t\t\tbreak;\n\t\t\tcase BaseItemKind.Episode:\n\t\t\t\tnavigate({ to: \"/episode/$id\", params: { id: itemId } });\n\t\t\t\tbreak;\n\t\t\tcase BaseItemKind.MusicAlbum:\n\t\t\t\tnavigate({ to: \"/album/$id\", params: { id: itemId } });\n\t\t\t\tbreak;\n\t\t\tcase BaseItemKind.MusicArtist:\n\t\t\t\tnavigate({ to: \"/artist/$id\", params: { id: itemId } });\n\t\t\t\tbreak;\n\t\t\tcase BaseItemKind.Person:\n\t\t\t\tnavigate({ to: \"/person/$id\", params: { id: itemId } });\n\t\t\t\tbreak;\n\t\t\tcase BaseItemKind.Series:\n\t\t\t\tnavigate({ to: \"/series/$id\", params: { id: itemId } });\n\t\t\t\tbreak;\n\t\t\tcase BaseItemKind.Playlist:\n\t\t\t\tnavigate({ to: \"/playlist/$id\", params: { id: itemId } });\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tnavigate({ to: \"/item/$id\", params: { id: itemId } });\n\t\t\t\tbreak;\n\t\t}\n\t};\n\n\treturn (\n\t\t<Button\n\t\t\tonClick={handleOnClick}\n\t\t\tfullWidth\n\t\t\tsx={{\n\t\t\t\tjustifyContent: \"flex-start\",\n\t\t\t\ttextAlign: \"left\",\n\t\t\t\tp: 1,\n\t\t\t\tborderRadius: 2,\n\t\t\t\ttextTransform: \"none\",\n\t\t\t\tcolor: \"text.primary\",\n\t\t\t\t\"&:hover\": {\n\t\t\t\t\tbgcolor: \"action.hover\",\n\t\t\t\t},\n\t\t\t}}\n\t\t>\n\t\t\t<Box\n\t\t\t\tsx={{\n\t\t\t\t\twidth: 50,\n\t\t\t\t\theight: 75,\n\t\t\t\t\tmr: 2,\n\t\t\t\t\tborderRadius: 1,\n\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\tbgcolor: \"action.selected\",\n\t\t\t\t\tflexShrink: 0,\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{imageUrl ? (\n\t\t\t\t\t<Box\n\t\t\t\t\t\tcomponent=\"img\"\n\t\t\t\t\t\tsrc={imageUrl}\n\t\t\t\t\t\talt={itemName}\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\tobjectFit: \"cover\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t) : (\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tfontSize: \"2rem\",\n\t\t\t\t\t\t\tcolor: \"var(--mui-palette-text-secondary)\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{getTypeIcon(itemType ?? \"unknown\")}\n\t\t\t\t\t</span>\n\t\t\t\t)}\n\t\t\t</Box>\n\t\t\t<Stack sx={{ minWidth: 0, flex: 1 }}>\n\t\t\t\t<Typography variant=\"body1\" noWrap fontWeight={500}>\n\t\t\t\t\t{itemName}\n\t\t\t\t</Typography>\n\t\t\t\t<Stack direction=\"row\" spacing={1} alignItems=\"center\">\n\t\t\t\t\t{itemYear && (\n\t\t\t\t\t\t<Typography variant=\"caption\" color=\"text.secondary\">\n\t\t\t\t\t\t\t{itemYear}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t)}\n\t\t\t\t\t{itemYear && itemType && (\n\t\t\t\t\t\t<Typography variant=\"caption\" color=\"text.secondary\">\n\t\t\t\t\t\t\t•\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t)}\n\t\t\t\t\t{itemType && (\n\t\t\t\t\t\t<Typography variant=\"caption\" color=\"text.secondary\">\n\t\t\t\t\t\t\t{itemType}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t)}\n\t\t\t\t</Stack>\n\t\t\t</Stack>\n\t\t</Button>\n\t);\n};\n\nexport default SearchItem;\n"
  },
  {
    "path": "src/components/settingOption/index.tsx",
    "content": "import { getSetting, setSetting } from \"@/utils/storage/settings\";\nimport { FormControlLabel, Switch, Typography } from \"@mui/material\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport React, { useState } from \"react\";\n\nconst SettingOption = ({\n\tsetting,\n}: { setting: { key: string; name: string; description: string } }) => {\n\tconst initValue = useQuery({\n\t\tqueryKey: [\"setting\", setting.key],\n\t\tqueryFn: async () => {\n\t\t\tconst result = await getSetting(setting.key);\n\t\t\treturn Boolean(result);\n\t\t},\n\t});\n\tconst [settingVal, setSettingVal] = useState(initValue.data ?? false);\n\treturn (\n\t\t<FormControlLabel\n\t\t\tcontrol={\n\t\t\t\t<Switch\n\t\t\t\t\tcolor=\"primary\"\n\t\t\t\t\tchecked={Boolean(settingVal)}\n\t\t\t\t\tonChange={(_, checked) => {\n\t\t\t\t\t\tsetSetting(setting.key, checked);\n\t\t\t\t\t\tsetSettingVal(Boolean(checked));\n\t\t\t\t\t}}\n\t\t\t\t\tdisabled={initValue.isLoading}\n\t\t\t\t\tname={setting.name}\n\t\t\t\t/>\n\t\t\t}\n\t\t\tlabel={\n\t\t\t\t<div className=\"settings-option-info\">\n\t\t\t\t\t<Typography variant=\"subtitle1\" fontWeight={400}>\n\t\t\t\t\t\t{setting.name}\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t\tclassName=\"settings-option-info-caption\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{setting.description}\n\t\t\t\t\t</Typography>\n\t\t\t\t</div>\n\t\t\t}\n\t\t\tlabelPlacement=\"start\"\n\t\t\tclassName=\"settings-option\"\n\t\t/>\n\t);\n};\n\nexport default SettingOption;"
  },
  {
    "path": "src/components/settingOptionSelect/index.tsx",
    "content": "import type { CultureDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport {\n\tFormControlLabel,\n\tMenuItem,\n\tTextField,\n\tTypography,\n} from \"@mui/material\";\nimport React, { useState } from \"react\";\n\nconst SettingOptionSelect = ({\n\tsetting,\n\toptions,\n\tuserValue,\n}: {\n\tsetting: { name: string; description: string };\n\toptions: CultureDto[];\n\tuserValue: string | null;\n}) => {\n\tconst [value, setValue] = useState(\n\t\tuserValue === \"\" ? \"anyLanguage\" : userValue,\n\t);\n\treturn (\n\t\t<FormControlLabel\n\t\t\tvalue={value}\n\t\t\tcontrol={\n\t\t\t\t<TextField select onChange={(e) => setValue(e.target.value)}>\n\t\t\t\t\t<MenuItem key={\"anyLanguage\"} value={\"anyLanguage\"}>\n\t\t\t\t\t\tAny Language\n\t\t\t\t\t</MenuItem>\n\t\t\t\t\t{options.map((option) => (\n\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\tkey={option.ThreeLetterISOLanguageName}\n\t\t\t\t\t\t\tvalue={option.ThreeLetterISOLanguageName ?? \"none\"}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{option.DisplayName}\n\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t))}\n\t\t\t\t</TextField>\n\t\t\t}\n\t\t\tlabel={\n\t\t\t\t<div className=\"settings-option-info\">\n\t\t\t\t\t<Typography variant=\"subtitle1\" fontWeight={400}>\n\t\t\t\t\t\t{setting.name}\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t\tclassName=\"settings-option-info-caption\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{setting.description}\n\t\t\t\t\t</Typography>\n\t\t\t\t</div>\n\t\t\t}\n\t\t\tlabelPlacement=\"start\"\n\t\t\tclassName=\"settings-option\"\n\t\t/>\n\t);\n};\n\nexport default SettingOptionSelect;\n"
  },
  {
    "path": "src/components/showMoreText/index.tsx",
    "content": "import Button from \"@mui/material/Button\";\nimport Typography from \"@mui/material/Typography\";\nimport React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nconst ShowMoreText = ({\n\tcontent,\n\tcollapsedLines,\n\textraProps,\n}: {\n\tcontent: string;\n\tcollapsedLines: number;\n\textraProps?: Record<string, any>;\n}) => {\n\tconst [displayFull, setDisplayFull] = useState(false);\n\tconst [isOverflowing, setIsOverflowing] = useState(false);\n\tconst textRef = useRef<HTMLDivElement>(null);\n\n\tuseEffect(() => {\n\t\tif (textRef.current) {\n\t\t\tif (textRef.current.offsetHeight < textRef.current.scrollHeight) {\n\t\t\t\tsetIsOverflowing(true);\n\t\t\t} else {\n\t\t\t\tsetIsOverflowing(false);\n\t\t\t}\n\t\t\t// textRef.current.\n\t\t}\n\t}, []);\n\treturn (\n\t\t<div\n\t\t\tclassName=\"flex flex-column\"\n\t\t\t{...extraProps}\n\t\t\tstyle={{\n\t\t\t\talignItems: \"flex-end\",\n\t\t\t\tgap: \"1em\",\n\t\t\t\topacity: 0.8,\n\t\t\t}}\n\t\t>\n\t\t\t<Typography\n\t\t\t\tref={textRef}\n\t\t\t\tkey={displayFull ? \"full\" : \"collapsed\"}\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: \"-webkit-box\",\n\t\t\t\t\ttextOverflow: \"ellipsis\",\n\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\tWebkitLineClamp: displayFull ? \"none\" : (collapsedLines ?? 2),\n\t\t\t\t\tWebkitBoxOrient: \"vertical\",\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t}}\n\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\tfontWeight={300}\n\t\t\t>\n\t\t\t\t{content}\n\t\t\t</Typography>\n\t\t\t{content.length > 0 && isOverflowing && (\n\t\t\t\t<Button\n\t\t\t\t\t//@ts-ignore\n\t\t\t\t\tcolor=\"white\"\n\t\t\t\t\tvariant=\"outlined\"\n\t\t\t\t\tonClick={() => setDisplayFull((state) => !state)}\n\t\t\t\t\t// key={displayFull}\n\t\t\t\t>\n\t\t\t\t\t{displayFull ? \"Show less\" : \"Show more\"}\n\t\t\t\t</Button>\n\t\t\t)}\n\t\t</div>\n\t);\n};\nexport default ShowMoreText;\n"
  },
  {
    "path": "src/components/skeleton/cards.tsx",
    "content": "import Skeleton from \"@mui/material/Skeleton\";\nimport Typography from \"@mui/material/Typography\";\nimport React from \"react\";\n\nexport const CardsSkeleton = () => {\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\tflexFlow: \"column\",\n\t\t\t\tmarginBottom: \"2em\",\n\t\t\t}}\n\t\t>\n\t\t\t<Typography variant=\"h3\" sx={{ mb: \"0.15em\" }}>\n\t\t\t\t<Skeleton width=\"35%\" />\n\t\t\t</Typography>\n\t\t\t<div className=\"flex flex-align-center\" style={{ height: \"17em\" }}>\n\t\t\t\t{Array.from({ length: 12 }).map((_, index) => (\n\t\t\t\t\t<Skeleton\n\t\t\t\t\t\theight=\"26em\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tflex: \"1 0 12.45%\",\n\t\t\t\t\t\t\tmarginRight: \"1.5em\",\n\t\t\t\t\t\t\tborderRadius: \"10px\",\n\t\t\t\t\t\t\tanimationDelay: `${index * 0.25}s`,\n\t\t\t\t\t\t}}\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noArrayIndexKey: This is a skeleton component\n\t\t\t\t\t\tkey={index}\n\t\t\t\t\t/>\n\t\t\t\t))}\n\t\t\t</div>\n\t\t</div>\n\t);\n};\n"
  },
  {
    "path": "src/components/skeleton/carousel.tsx",
    "content": "import Skeleton from \"@mui/material/Skeleton\";\nimport Typography from \"@mui/material/Typography\";\nimport React from \"react\";\nexport const CarouselSkeleton = () => {\n\treturn (\n\t\t<div\n\t\t\tclassName=\"hero-carousel hero-carousel-skeleton\"\n\t\t\tstyle={{\n\t\t\t\theight: \"70vh\",\n\t\t\t\tposition: \"relative\",\n\t\t\t\twidth: \"100vw\",\n\t\t\t\tmarginLeft: \"-4.4em\",\n\t\t\t}}\n\t\t>\n\t\t\t<div className=\"hero-carousel-detail\">\n\t\t\t\t<Typography variant=\"h3\" className=\"hero-carousel-text\">\n\t\t\t\t\t<Skeleton\n\t\t\t\t\t\tvariant=\"text\"\n\t\t\t\t\t\tsx={{ fontSize: \"8rem\" }}\n\t\t\t\t\t\twidth={300}\n\t\t\t\t\t\t// animation=\"wave\"\n\t\t\t\t\t/>\n\t\t\t\t</Typography>\n\t\t\t\t<Typography\n\t\t\t\t\tvariant=\"body1\"\n\t\t\t\t\tclassName=\"hero-carousel-text\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\t\tpaddingBottom: \"4.4em\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Skeleton\n\t\t\t\t\t\tvariant=\"text\"\n\t\t\t\t\t\tsx={{ fontSize: \"2rem\" }}\n\t\t\t\t\t\twidth={400}\n\t\t\t\t\t\t// animation=\"wave\"\n\t\t\t\t\t/>\n\t\t\t\t\t<Skeleton\n\t\t\t\t\t\tvariant=\"text\"\n\t\t\t\t\t\tsx={{ fontSize: \"2rem\" }}\n\t\t\t\t\t\twidth={400}\n\t\t\t\t\t\t// animation=\"wave\"\n\t\t\t\t\t/>\n\t\t\t\t\t<Skeleton\n\t\t\t\t\t\tvariant=\"text\"\n\t\t\t\t\t\tsx={{ fontSize: \"2rem\" }}\n\t\t\t\t\t\twidth={400}\n\t\t\t\t\t\t// animation=\"wave\"\n\t\t\t\t\t/>\n\t\t\t\t</Typography>\n\t\t\t</div>\n\t\t</div>\n\t);\n};\n"
  },
  {
    "path": "src/components/skeleton/episode.tsx",
    "content": "import { Divider, Skeleton, Typography } from \"@mui/material\";\nimport React from \"react\";\n\nconst EpisodeSkeleton = () => {\n    return Array.from(new Array(8)).map((_, index) => {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t// biome-ignore lint/suspicious/noArrayIndexKey: this is a skeleton\n\t\t\t\t\t\t\tkey={index}\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div className=\"item-detail-episode\">\n\t\t\t\t\t\t\t\t<Typography variant=\"h6\">\n\t\t\t\t\t\t\t\t\t<Skeleton\n\t\t\t\t\t\t\t\t\t\tvariant=\"circular\"\n\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\tanimationDelay: `${index * 0.25}s`,\n\t\t\t\t\t\t\t\t\t\t\tanimationDuration: \"1.4s\",\n\t\t\t\t\t\t\t\t\t\t\tanimationName: \"pulse\",\n\t\t\t\t\t\t\t\t\t\t\topacity: 0.1,\n\t\t\t\t\t\t\t\t\t\t\taspectRatio: 1,\n\t\t\t\t\t\t\t\t\t\t\twidth: \"3rem\",\n\t\t\t\t\t\t\t\t\t\t\theight: \"3rem\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t<div className=\"item-detail-episode-image-container\">\n\t\t\t\t\t\t\t\t\t<Skeleton\n\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-episode-image\"\n\t\t\t\t\t\t\t\t\t\tvariant=\"rectangular\"\n\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\tanimationDelay: `${index * 0.2}s`,\n\t\t\t\t\t\t\t\t\t\t\tanimationDuration: \"1.4s\",\n\t\t\t\t\t\t\t\t\t\t\tanimationName: \"pulse\",\n\t\t\t\t\t\t\t\t\t\t\topacity: 0.1,\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div className=\"item-detail-episode-info\">\n\t\t\t\t\t\t\t\t\t<Typography variant=\"h4\">\n\t\t\t\t\t\t\t\t\t\t<Skeleton\n\t\t\t\t\t\t\t\t\t\t\tvariant=\"text\"\n\t\t\t\t\t\t\t\t\t\t\twidth=\"10em\"\n\t\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\t\tanimationDelay: `${index * 0.2}s`,\n\t\t\t\t\t\t\t\t\t\t\t\tanimationDuration: \"1.4s\",\n\t\t\t\t\t\t\t\t\t\t\t\tanimationName: \"pulse\",\n\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.1,\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t<Skeleton\n\t\t\t\t\t\t\t\t\t\tvariant=\"rounded\"\n\t\t\t\t\t\t\t\t\t\twidth=\"5em\"\n\t\t\t\t\t\t\t\t\t\tstyle={{ borderRadius: \"100px\" }}\n\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\tanimationDelay: `${index * 0.2}s`,\n\t\t\t\t\t\t\t\t\t\t\tanimationDuration: \"1.4s\",\n\t\t\t\t\t\t\t\t\t\t\tanimationName: \"pulse\",\n\t\t\t\t\t\t\t\t\t\t\topacity: 0.1,\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<Skeleton\n\t\t\t\t\t\t\t\t\t\t\twidth=\"100%\"\n\t\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\t\tanimationDelay: `${index * 0.2}s`,\n\t\t\t\t\t\t\t\t\t\t\t\tanimationDuration: \"1.4s\",\n\t\t\t\t\t\t\t\t\t\t\t\tanimationName: \"pulse\",\n\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.1,\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t<Skeleton\n\t\t\t\t\t\t\t\t\t\t\twidth=\"100%\"\n\t\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\t\tanimationDelay: `${index * 0.2}s`,\n\t\t\t\t\t\t\t\t\t\t\t\tanimationDuration: \"1.4s\",\n\t\t\t\t\t\t\t\t\t\t\t\tanimationName: \"pulse\",\n\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.1,\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{index !== 7 && <Divider orientation=\"horizontal\" flexItem />}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t);\n\t\t\t\t});\n}\n\nexport default EpisodeSkeleton"
  },
  {
    "path": "src/components/skeleton/item.tsx",
    "content": "import React from \"react\";\n\nimport { Skeleton } from \"@mui/material\";\nimport { motion } from \"motion/react\";\n\nconst ItemSkeleton = () => {\n\treturn (\n\t\t<motion.div\n\t\t\tclassName=\"item item-default scrollY padded-top flex flex-column\"\n\t\t\tstyle={{\n\t\t\t\twidth: \"100vw\",\n\t\t\t}}\n\t\t\tinitial={{\n\t\t\t\topacity: 0,\n\t\t\t}}\n\t\t\tanimate={{\n\t\t\t\topacity: 1,\n\t\t\t}}\n\t\t\texit={{\n\t\t\t\topacity: 0,\n\t\t\t}}\n\t\t>\n\t\t\t<div className=\"item-hero\">\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"item-hero-image-container\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\taspectRatio: 0.6666,\n\t\t\t\t\t\tboxShadow: \"0 0 black\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Skeleton\n\t\t\t\t\t\tvariant=\"rounded\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\theight: \"50vh\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"item-hero-detail flex flex-column\"\n\t\t\t\t\tstyle={\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t// width: \"50vw\",\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t<Skeleton variant=\"text\" sx={{ fontSize: \"4rem\" }} width=\"50%\" />\n\t\t\t\t\t{/* </Typography> */}\n\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"item-hero-buttons-container flex flex-row\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\twidth: \"70%\",\n\t\t\t\t\t\t\tgap: \"1em\",\n\t\t\t\t\t\t\tjustifyContent: \"flex-start\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Skeleton\n\t\t\t\t\t\t\twidth=\"10rem\"\n\t\t\t\t\t\t\theight=\"3rem\"\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\tfontSize: \"3rem\",\n\t\t\t\t\t\t\t\tborderRadius: \"3em\",\n\t\t\t\t\t\t\t\ttransform: \"scale(1)\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<div className=\"flex flex-row\" style={{ gap: \"1em\" }}>\n\t\t\t\t\t\t\t<Skeleton\n\t\t\t\t\t\t\t\twidth=\"3rem\"\n\t\t\t\t\t\t\t\theight=\"3rem\"\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\tfontSize: \"3rem\",\n\t\t\t\t\t\t\t\t\tborderRadius: \"3em\",\n\t\t\t\t\t\t\t\t\ttransform: \"scale(1)\",\n\t\t\t\t\t\t\t\t\taspectRatio: \"1\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<Skeleton\n\t\t\t\t\t\t\t\twidth=\"3rem\"\n\t\t\t\t\t\t\t\theight=\"3rem\"\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\tfontSize: \"3rem\",\n\t\t\t\t\t\t\t\t\tborderRadius: \"3em\",\n\t\t\t\t\t\t\t\t\ttransform: \"scale(1)\",\n\t\t\t\t\t\t\t\t\taspectRatio: \"1\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div className=\"item-detail\">\n\t\t\t\t<div style={{ width: \"100%\" }}>\n\t\t\t\t\t<Skeleton variant=\"text\" />\n\t\t\t\t\t<Skeleton variant=\"text\" />\n\t\t\t\t\t<Skeleton variant=\"text\" />\n\t\t\t\t</div>\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Skeleton\n\t\t\t\t\t\tvariant=\"rounded\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tfontSize: \"2.4rem\",\n\t\t\t\t\t\t\tmb: 1,\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t<Skeleton\n\t\t\t\t\t\tvariant=\"rounded\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tfontSize: \"2.4rem\",\n\t\t\t\t\t\t\tmb: 1,\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t<Skeleton\n\t\t\t\t\t\tvariant=\"rounded\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tfontSize: \"2.4rem\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</motion.div>\n\t);\n};\n\nexport default ItemSkeleton;\n"
  },
  {
    "path": "src/components/skeleton/libraryItems.tsx",
    "content": "import { Skeleton } from \"@mui/material\";\nimport React from \"react\";\n\nconst LibraryItemsSkeleton = () => {\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\twidth: \"100%\",\n\t\t\t\toverflow: \"visible\",\n\t\t\t\tflexWrap: \"wrap\",\n\t\t\t\talignItems: \"center\",\n\t\t\t\tjustifyContent: \"center\",\n\t\t\t}}\n\t\t>\n\t\t\t{Array.from(new Array(18)).map((item, index) => (\n\t\t\t\t<Skeleton\n\t\t\t\t\tkey={index}\n\t\t\t\t\tvariant=\"rounded\"\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tflexBasis: 185,\n\t\t\t\t\t\theight: \"auto\",\n\t\t\t\t\t\taspectRatio: 0.6666,\n\t\t\t\t\t\t// background: \"red\",\n\t\t\t\t\t\tmarginRight: \"1.5em\",\n\t\t\t\t\t\tmarginBottom: \"1.5em\",\n\t\t\t\t\t\tflexShrink: 1,\n\t\t\t\t\t\tflexGrow: 0,\n\t\t\t\t\t\tborderRadius: \"10px\",\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t))}\n\t\t</div>\n\t);\n};\n\nexport default LibraryItemsSkeleton;"
  },
  {
    "path": "src/components/skeleton/seasonSelector.tsx",
    "content": "import Divider from \"@mui/material/Divider\";\nimport Grid2 from \"@mui/material/Grid\";\nimport Skeleton from \"@mui/material/Skeleton\";\nimport Typography from \"@mui/material/Typography\";\nimport React from \"react\";\nimport EpisodeSkeleton from \"./episode\";\n\nexport const SeasonSelectorSkeleton = () => {\n\treturn (\n\t\t<Grid2\n\t\t\tcontainer\n\t\t\tcolumns={{\n\t\t\t\txs: 2,\n\t\t\t\tsm: 3,\n\t\t\t\tmd: 4,\n\t\t\t}}\n\t\t\tsx={{ width: \"100%\" }}\n\t\t>\n\t\t\t<div\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\tgap: \"1em\",\n\t\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Typography variant=\"h3\">\n\t\t\t\t\t<Skeleton width={320} variant=\"text\" animation=\"wave\" />\n\t\t\t\t</Typography>\n\t\t\t\t<Skeleton animation=\"wave\" height={64} width={128} />\n\t\t\t</div>\n\t\t\t<Divider\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: \"block\",\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<div\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\t\tpaddingBottom: \" 0.5em\",\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<div className=\"item-detail-episode-container\">\n\t\t\t\t<EpisodeSkeleton />\n\t\t\t</div>\n\t\t</Grid2>\n\t);\n};\n"
  },
  {
    "path": "src/components/tagChip/index.tsx",
    "content": "import { Typography } from \"@mui/material\";\nimport { Link, type LinkProps } from \"@tanstack/react-router\";\nimport React from \"react\";\n\nimport \"./tagChip.scss\";\n\nconst TagChip = ({\n\tlabel,\n\tlinkProps,\n}: { label: string; linkProps?: LinkProps }) => {\n\treturn (\n\t\t<Link className=\"tag\" to=\"/search\" search={{ query: label }} {...linkProps}>\n\t\t\t<Typography variant=\"subtitle2\" fontWeight={300}>\n\t\t\t\t{label}\n\t\t\t</Typography>\n\t\t</Link>\n\t);\n};\n\nexport default TagChip;"
  },
  {
    "path": "src/components/tagChip/tagChip.scss",
    "content": ".tag {\n    display: flex;\n    padding: 0.4em 0.8em;\n    color: white;\n    text-decoration: none;\n    gap: 0.2em;\n    border: 1px solid rgb(255 255 255 / 0.3);\n    border-radius: 4em;\n    .MuiTypography-root {\n        white-space: nowrap;\n        transition: $transition-time-fast;\n    }\n    &:hover {\n        border-color: rgb(255 255 255 / 0.8) !important;\n        .MuiTypography-root {\n            font-weight: 400;\n        }\n    }\n}"
  },
  {
    "path": "src/components/updater/index.tsx",
    "content": "import { LoadingButton } from \"@mui/lab\";\nimport {\n\tBox,\n\tButton,\n\tCircularProgress,\n\tDialog,\n\tDialogActions,\n\tDialogContent,\n\tStack,\n\tTypography,\n} from \"@mui/material\";\nimport { relaunch } from \"@tauri-apps/plugin-process\";\nimport { open } from \"@tauri-apps/plugin-shell\";\nimport { check, type Update } from \"@tauri-apps/plugin-updater\";\nimport { useSnackbar } from \"notistack\";\nimport React, { useCallback, useLayoutEffect, useState } from \"react\";\n\nexport default function Updater() {\n\tconst [updateDialog, setUpdateDialog] = useState(false);\n\tconst [updateDialogButton, setUpdateDialogButton] = useState(false);\n\tconst [updateInfo, setUpdateInfo] = useState<Update | undefined>(undefined);\n\n\tuseLayoutEffect(() => {\n\t\tasync function checkForUpdates() {\n\t\t\ttry {\n\t\t\t\tconst update = await check();\n\t\t\t\tif (update) {\n\t\t\t\t\t// console.log(update);\n\t\t\t\t\tsetUpdateInfo(update);\n\t\t\t\t\tsetUpdateDialog(true);\n\n\t\t\t\t\tconsole.info(`Update found : ${update.version}, ${update.date}`);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(error);\n\t\t\t}\n\t\t}\n\t\tcheckForUpdates();\n\t}, []);\n\tconst { enqueueSnackbar } = useSnackbar();\n\tconst [downloadProgress, setDownloadProgress] = useState(0);\n\tconst [downloadSize, setDownloadSize] = useState(0);\n\tconst handleUpdate = useCallback(async () => {\n\t\ttry {\n\t\t\tsetUpdateDialogButton(true);\n\t\t\tawait updateInfo?.downloadAndInstall((e) => {\n\t\t\t\tswitch (e.event) {\n\t\t\t\t\tcase \"Started\":\n\t\t\t\t\t\tsetDownloadSize(e.data.contentLength ?? 0);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"Progress\":\n\t\t\t\t\t\tsetDownloadProgress((s) => s + e.data.chunkLength);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"Finished\":\n\t\t\t\t\t\tsetDownloadProgress(downloadSize);\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t});\n\t\t\tenqueueSnackbar(\n\t\t\t\t\"Update has been installed! You need to relaunch Blink.\",\n\t\t\t\t{\n\t\t\t\t\tvariant: \"success\",\n\t\t\t\t},\n\t\t\t);\n\t\t\tawait relaunch();\n\t\t} catch (error) {\n\t\t\tconsole.error(error);\n\t\t\tenqueueSnackbar(`Failed to update Blink. ${error}`, { variant: \"error\" });\n\t\t}\n\t\tsetUpdateDialogButton(false);\n\t}, [enqueueSnackbar, updateInfo, downloadSize]);\n\n\tif (!updateInfo) return null;\n\n\treturn (\n\t\t<Dialog\n\t\t\topen={updateDialog}\n\t\t\tmaxWidth=\"xs\"\n\t\t\tfullWidth\n\t\t\tslotProps={{\n\t\t\t\tpaper: {\n\t\t\t\t\tclassName: \"glass\",\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tborderRadius: \"24px\",\n\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tbackdrop: {\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\tbackdropFilter: \"blur(5px)\",\n\t\t\t\t\t\tbackgroundColor: \"rgba(0, 0, 0, 0.5)\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}}\n\t\t>\n\t\t\t<Box\n\t\t\t\tsx={{\n\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\ttop: 0,\n\t\t\t\t\tleft: 0,\n\t\t\t\t\tright: 0,\n\t\t\t\t\theight: \"140px\",\n\t\t\t\t\topacity: 0.5,\n\t\t\t\t\tbackground: (theme) =>\n\t\t\t\t\t\t`linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.secondary.main} 100%)`,\n\t\t\t\t\tzIndex: 0,\n\t\t\t\t\tfilter: \"blur(40px)\",\n\t\t\t\t\ttransform: \"translateY(-50%) scale(1.5)\",\n\t\t\t\t}}\n\t\t\t/>\n\n\t\t\t<DialogContent\n\t\t\t\tsx={{ position: \"relative\", zIndex: 1, pt: 5, pb: 3, px: 3 }}\n\t\t\t>\n\t\t\t\t<Stack alignItems=\"center\" spacing={3}>\n\t\t\t\t\t<Box\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\twidth: 72,\n\t\t\t\t\t\t\theight: 72,\n\t\t\t\t\t\t\tborderRadius: \"50%\",\n\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\tbackground: (theme) =>\n\t\t\t\t\t\t\t\t`linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.secondary.main} 100%)`,\n\t\t\t\t\t\t\tboxShadow: (theme) =>\n\t\t\t\t\t\t\t\t`0 10px 25px -5px ${theme.palette.primary.main}80`,\n\t\t\t\t\t\t\tmb: 1,\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\tstyle={{ fontSize: \"36px\", color: \"white\" }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\trocket_launch\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</Box>\n\n\t\t\t\t\t<Stack spacing={1} textAlign=\"center\">\n\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\tvariant=\"h5\"\n\t\t\t\t\t\t\tfontWeight=\"900\"\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\tbackground: (theme) =>\n\t\t\t\t\t\t\t\t\t`linear-gradient(135deg, ${theme.palette.primary.light} 0%, ${theme.palette.secondary.light} 100%)`,\n\t\t\t\t\t\t\t\tbackgroundClip: \"text\",\n\t\t\t\t\t\t\t\tWebkitBackgroundClip: \"text\",\n\t\t\t\t\t\t\t\tcolor: \"transparent\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tUpdate Available\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\tvariant=\"body2\"\n\t\t\t\t\t\t\tcolor=\"text.secondary\"\n\t\t\t\t\t\t\tsx={{ maxWidth: \"25ch\" }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tA new version of Blink is ready for you.\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t</Stack>\n\n\t\t\t\t\t<Stack\n\t\t\t\t\t\tdirection=\"row\"\n\t\t\t\t\t\talignItems=\"center\"\n\t\t\t\t\t\tjustifyContent=\"space-between\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.03)\",\n\t\t\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\t\t\tp: 2.5,\n\t\t\t\t\t\t\tborder: \"1px solid rgba(255,255,255,0.05)\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Stack>\n\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t\t\t\tcolor=\"text.secondary\"\n\t\t\t\t\t\t\t\tsx={{ mb: 0.5 }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tCurrent\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t<Typography variant=\"subtitle1\" fontWeight=\"bold\">\n\t\t\t\t\t\t\t\tv{updateInfo.currentVersion}\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t</Stack>\n\n\t\t\t\t\t\t<Box\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\t\twidth: 32,\n\t\t\t\t\t\t\t\theight: 32,\n\t\t\t\t\t\t\t\tborderRadius: \"50%\",\n\t\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tfontSize: \"16px\",\n\t\t\t\t\t\t\t\t\tcolor: \"rgba(255,255,255,0.5)\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tarrow_forward\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</Box>\n\n\t\t\t\t\t\t<Stack alignItems=\"flex-end\">\n\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t\t\t\tcolor=\"primary.main\"\n\t\t\t\t\t\t\t\tsx={{ mb: 0.5 }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tNew Version\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\tvariant=\"subtitle1\"\n\t\t\t\t\t\t\t\tfontWeight=\"bold\"\n\t\t\t\t\t\t\t\tcolor=\"primary.main\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tv{updateInfo.version}\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t</Stack>\n\t\t\t\t\t</Stack>\n\n\t\t\t\t\t<Button\n\t\t\t\t\t\tonClick={() =>\n\t\t\t\t\t\t\topen(\n\t\t\t\t\t\t\t\t`https://github.com/prayag17/Blink/releases/v${updateInfo.version}`,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\ttextTransform: \"none\",\n\t\t\t\t\t\t\tcursor: \"pointer\",\n\t\t\t\t\t\t\tcolor: \"text.secondary\",\n\t\t\t\t\t\t\tborderRadius: \"100px\",\n\t\t\t\t\t\t\tpx: 2,\n\t\t\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\t\t\tcolor: \"primary.main\",\n\t\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tendIcon={\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\tstyle={{ fontSize: 16 }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\topen_in_new\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\tSee what's new\n\t\t\t\t\t</Button>\n\t\t\t\t</Stack>\n\t\t\t</DialogContent>\n\n\t\t\t<DialogActions\n\t\t\t\tsx={{\n\t\t\t\t\tp: 3,\n\t\t\t\t\tpt: 0,\n\t\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\t\tgap: 1.5,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Button\n\t\t\t\t\tcolor=\"inherit\"\n\t\t\t\t\tsize=\"large\"\n\t\t\t\t\tonClick={() => setUpdateDialog(false)}\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tborderRadius: \"14px\",\n\t\t\t\t\t\tflex: 1,\n\t\t\t\t\t\tcolor: \"text.secondary\",\n\t\t\t\t\t\theight: 48,\n\t\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\t\t\tcolor: \"text.primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\tLater\n\t\t\t\t</Button>\n\t\t\t\t<Button\n\t\t\t\t\tsize=\"large\"\n\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\tloading={updateDialogButton}\n\t\t\t\t\tloadingIndicator={\n\t\t\t\t\t\t<CircularProgress\n\t\t\t\t\t\t\tsize={24}\n\t\t\t\t\t\t\tcolor=\"inherit\"\n\t\t\t\t\t\t\tvalue={downloadProgress / downloadSize}\n\t\t\t\t\t\t/>\n\t\t\t\t\t}\n\t\t\t\t\tdisableElevation\n\t\t\t\t\tonClick={handleUpdate}\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tborderRadius: \"14px\",\n\t\t\t\t\t\tflex: 1,\n\t\t\t\t\t\theight: 48,\n\t\t\t\t\t\tbackground: (theme) =>\n\t\t\t\t\t\t\t`linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.secondary.main} 100%)`,\n\t\t\t\t\t\tboxShadow: (theme) =>\n\t\t\t\t\t\t\t`0 8px 20px -5px ${theme.palette.primary.main}60`,\n\t\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\t\tbackground: (theme) =>\n\t\t\t\t\t\t\t\t`linear-gradient(135deg, ${theme.palette.primary.main} 0%, ${theme.palette.secondary.main} 100%)`,\n\t\t\t\t\t\t\topacity: 0.9,\n\t\t\t\t\t\t\tboxShadow: (theme) =>\n\t\t\t\t\t\t\t\t`0 12px 25px -5px ${theme.palette.primary.main}80`,\n\t\t\t\t\t\t},\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\tUpdate\n\t\t\t\t</Button>\n\t\t\t</DialogActions>\n\t\t</Dialog>\n\t);\n}"
  },
  {
    "path": "src/components/userAvatarMenu/index.tsx",
    "content": "import {\n\tAvatar,\n\tDivider,\n\tIconButton,\n\tListItemIcon,\n\tMenu,\n\tMenuItem,\n} from \"@mui/material\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { useNavigate, useRouter } from \"@tanstack/react-router\";\nimport React, {\n\ttype MouseEventHandler,\n\tuseCallback,\n\tuseMemo,\n\tuseState,\n} from \"react\";\nimport { delUser } from \"@/utils/storage/user\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useCentralStore } from \"@/utils/store/central\";\n\nexport const UserAvatarMenu = () => {\n\tconst api = useApiInContext((s) => s.api);\n\tconst createApi = useApiInContext((s) => s.createApi);\n\tconst [user, resetCurrentUser] = useCentralStore((s) => [\n\t\ts.currentUser,\n\t\ts.resetCurrentUser,\n\t]);\n\tconst navigate = useNavigate();\n\tconst router = useRouter();\n\tconst queryClient = useQueryClient();\n\n\tconst [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);\n\tconst openMenu = Boolean(anchorEl);\n\n\tconst handleMenuOpen: MouseEventHandler<HTMLButtonElement> = useCallback(\n\t\t(event) => {\n\t\t\tsetAnchorEl(event.currentTarget);\n\t\t},\n\t\t[],\n\t);\n\n\tconst handleMenuClose = useCallback(() => {\n\t\tsetAnchorEl(null);\n\t}, []);\n\n\tconst handleLogout = useCallback(async () => {\n\t\tconsole.log(\"Logging out user...\");\n\t\tawait api?.logout();\n\t\tcreateApi(api?.basePath ?? \"\", undefined);\n\t\tresetCurrentUser();\n\t\tdelUser();\n\t\tsessionStorage.removeItem(\"accessToken\");\n\t\tqueryClient.clear();\n\t\tawait router.invalidate();\n\t\tsetAnchorEl(null);\n\t\tnavigate({ to: \"/login\", replace: true });\n\t}, [api, createApi, navigate, queryClient, resetCurrentUser, router]);\n\n\tconst menuButtonSx = useMemo(() => ({ p: 0 }), []);\n\tconst menuStyle = useMemo(() => ({ mt: 2 }), []);\n\n\treturn (\n\t\t<div>\n\t\t\t<IconButton sx={menuButtonSx} onClick={handleMenuOpen}>\n\t\t\t\t{!!user?.Id &&\n\t\t\t\t\t(user?.PrimaryImageTag === undefined ? (\n\t\t\t\t\t\t<Avatar className=\"appBar-avatar\" alt={user?.Name ?? \"image\"}>\n\t\t\t\t\t\t\t<span className=\"material-symbols-rounded appBar-avatar-icon\">\n\t\t\t\t\t\t\t\taccount_circle\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</Avatar>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<Avatar\n\t\t\t\t\t\t\tclassName=\"appBar-avatar\"\n\t\t\t\t\t\t\tsrc={`${api?.basePath}/Users/${user?.Id}/Images/Primary`}\n\t\t\t\t\t\t\talt={user?.Name ?? \"image\"}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span className=\"material-symbols-rounded appBar-avatar-icon\">\n\t\t\t\t\t\t\t\taccount_circle\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</Avatar>\n\t\t\t\t\t))}\n\t\t\t</IconButton>\n\t\t\t<Menu\n\t\t\t\tanchorEl={anchorEl}\n\t\t\t\topen={openMenu}\n\t\t\t\tonClose={handleMenuClose}\n\t\t\t\tsx={menuStyle}\n\t\t\t\tdisableScrollLock\n\t\t\t\tPaperProps={{ className: \"glass\" }}\n\t\t\t>\n\t\t\t\t<MenuItem\n\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\thandleLogout();\n\t\t\t\t\t\thandleMenuClose();\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<ListItemIcon>\n\t\t\t\t\t\t<span className=\"material-symbols-rounded\">logout</span>\n\t\t\t\t\t</ListItemIcon>\n\t\t\t\t\tLogout\n\t\t\t\t</MenuItem>\n\t\t\t\t<Divider />\n\t\t\t\t<MenuItem\n\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\tnavigate({ to: \"/settings/preferences\" });\n\t\t\t\t\t\thandleMenuClose();\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<ListItemIcon>\n\t\t\t\t\t\t<span className=\"material-symbols-rounded\">tune</span>\n\t\t\t\t\t</ListItemIcon>\n\t\t\t\t\tPreferences\n\t\t\t\t</MenuItem>\n\t\t\t\t<MenuItem\n\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\tnavigate({ to: \"/settings/about\" });\n\t\t\t\t\t\thandleMenuClose();\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<ListItemIcon>\n\t\t\t\t\t\t<span className=\"material-symbols-rounded\">info</span>\n\t\t\t\t\t</ListItemIcon>\n\t\t\t\t\tAbout\n\t\t\t\t</MenuItem>\n\t\t\t</Menu>\n\t\t</div>\n\t);\n};\n"
  },
  {
    "path": "src/components/utils/easterEgg.tsx",
    "content": "import React, { useCallback, useMemo } from \"react\";\n\nimport { Dialog, Slide } from \"@mui/material\";\n// import { useKonamiEasterEgg } from \"../../utils/misc/konami\";\n\nexport const EasterEgg = () => {\n\t// const [easterEgg, setEasterEgg] = useKonamiEasterEgg();\n\n\t// // Memoize the onClose function\n\t// const handleClose = useCallback(() => {\n\t// \tsetEasterEgg(false);\n\t// }, [setEasterEgg]);\n\n\t// // Memoize the sx object\n\t// const dialogSx = useMemo(\n\t// \t() => ({\n\t// \t\tbackground: \"black\",\n\t// \t}),\n\t// \t[],\n\t// );\n\n\t// return (\n\t// \t<>\n\t// \t\t<Dialog open={easterEgg} onClose={handleClose} sx={dialogSx}>\n\t// \t\t\t<iframe\n\t// \t\t\t\twidth=\"560\"\n\t// \t\t\t\theight=\"315\"\n\t// \t\t\t\tsrc=\"https://www.youtube.com/embed/dQw4w9WgXcQ?autoplay=1&disablekb=1\"\n\t// \t\t\t\ttitle=\"EasterEgg\"\n\t// \t\t\t\tallow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n\t// \t\t\t/>\n\t// \t\t</Dialog>\n\t// \t\t<Slide direction=\"up\" in={easterEgg} mountOnEnter unmountOnExit>\n\t// \t\t\t<img\n\t// \t\t\t\tsrc=\"https://i.gifer.com/PYh.gif\"\n\t// \t\t\t\tloading=\"lazy\"\n\t// \t\t\t\twidth={320}\n\t// \t\t\t\theight={320}\n\t// \t\t\t\talt=\"nyan cat gif\"\n\t// \t\t\t\tstyle={{\n\t// \t\t\t\t\tzIndex: \"99999999\",\n\t// \t\t\t\t\tposition: \"fixed\",\n\t// \t\t\t\t\tbottom: 0,\n\t// \t\t\t\t\tleft: 0,\n\t// \t\t\t\t\tobjectFit: \"cover\",\n\t// \t\t\t\t}}\n\t// \t\t\t/>\n\t// \t\t</Slide>\n\t// \t</>\n\t// );\n\treturn <></>;\n};\n"
  },
  {
    "path": "src/components/utils/iconsCollection.tsx",
    "content": "import {\n\tBaseItemKind,\n\ttype CollectionType,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport React from \"react\";\n\nexport const getTypeIcon = (\n\ticon: BaseItemKind | CollectionType | \"Home\" | \"User\" | \"universal\",\n) => {\n\tswitch (icon) {\n\t\tcase \"Home\":\n\t\t\treturn <div className=\"material-symbols-rounded\">home</div>;\n\t\tcase BaseItemKind.Audio:\n\t\t\treturn <div className=\"material-symbols-rounded\">mic</div>;\n\t\tcase BaseItemKind.AudioBook:\n\t\t\treturn <div className=\"material-symbols-rounded\">speech_to_text</div>;\n\t\tcase BaseItemKind.Book:\n\t\t\treturn <div className=\"material-symbols-rounded\">book</div>;\n\t\tcase \"boxsets\":\n\t\tcase BaseItemKind.BoxSet:\n\t\t\treturn <div className=\"material-symbols-rounded\">video_library</div>;\n\t\tcase \"livetv\":\n\t\tcase BaseItemKind.TvProgram:\n\t\tcase BaseItemKind.TvChannel:\n\t\tcase BaseItemKind.Program:\n\t\tcase BaseItemKind.Recording:\n\t\tcase BaseItemKind.LiveTvChannel:\n\t\tcase BaseItemKind.LiveTvProgram:\n\t\tcase BaseItemKind.ChannelFolderItem:\n\t\tcase BaseItemKind.Channel:\n\t\t\treturn (\n\t\t\t\t<div className=\"material-symbols-rounded\">settings_input_antenna</div>\n\t\t\t);\n\t\tcase \"tvshows\":\n\t\tcase BaseItemKind.Season:\n\t\tcase BaseItemKind.Series:\n\t\tcase BaseItemKind.Episode:\n\t\t\treturn <div className=\"material-symbols-rounded\">tv_gen</div>;\n\t\tcase \"playlists\":\n\t\tcase BaseItemKind.Playlist:\n\t\tcase BaseItemKind.PlaylistsFolder:\n\t\tcase BaseItemKind.ManualPlaylistsFolder:\n\t\t\treturn <div className=\"material-symbols-rounded\">queue_music</div>;\n\t\tcase \"movies\":\n\t\tcase BaseItemKind.Movie:\n\t\t\treturn <div className=\"material-symbols-rounded\">movie</div>;\n\t\tcase BaseItemKind.MusicAlbum:\n\t\t\treturn <div className=\"material-symbols-rounded\">album</div>;\n\t\tcase BaseItemKind.MusicArtist:\n\t\t\treturn <div className=\"material-symbols-rounded\">artist</div>;\n\t\tcase BaseItemKind.Genre:\n\t\tcase BaseItemKind.MusicGenre:\n\t\t\treturn <div className=\"material-symbols-rounded\">domino_mask</div>;\n\t\tcase \"musicvideos\":\n\t\tcase BaseItemKind.MusicVideo:\n\t\t\treturn <div className=\"material-symbols-rounded\">music_video</div>;\n\t\tcase \"User\":\n\t\tcase BaseItemKind.Person:\n\t\t\treturn <div className=\"material-symbols-rounded\">person</div>;\n\t\tcase BaseItemKind.Photo:\n\t\t\treturn <div className=\"material-symbols-rounded\">image</div>;\n\t\tcase \"photos\":\n\t\tcase BaseItemKind.PhotoAlbum:\n\t\t\treturn <div className=\"material-symbols-rounded\">photo_library</div>;\n\t\tcase \"universal\":\n\t\tcase BaseItemKind.Studio:\n\t\t\treturn <div className=\"material-symbols-rounded\">category</div>;\n\t\tcase \"trailers\":\n\t\tcase BaseItemKind.Trailer:\n\t\t\treturn <div className=\"material-symbols-rounded\">smart_display</div>;\n\t\tcase BaseItemKind.Video:\n\t\t\treturn <div className=\"material-symbols-rounded\">theaters</div>;\n\t\tcase \"music\":\n\t\t\treturn <div className=\"material-symbols-rounded\">library_music</div>;\n\t\tcase \"books\":\n\t\t\treturn <div className=\"material-symbols-rounded\">library_books</div>;\n\t\tcase \"folders\":\n\t\t\treturn <div className=\"material-symbols-rounded\">folder</div>;\n\t\tdefault:\n\t\t\treturn <div className=\"material-symbols-rounded\">description</div>;\n\t}\n};\n"
  },
  {
    "path": "src/global.d.ts",
    "content": "// Fix image imports\ndeclare module \"*.png\" {\n\tconst value: any;\n\texport = value;\n}\ndeclare module \"*.svg\" {\n\tconst value: any;\n\texport = value;\n}\ndeclare module \"*.jpg\" {\n\tconst value: any;\n\texport = value;\n}"
  },
  {
    "path": "src/main.tsx",
    "content": "import { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { createRouter, RouterProvider } from \"@tanstack/react-router\";\nimport React, { useEffect } from \"react\";\nimport ReactDOM from \"react-dom/client\";\n// Import the generated route tree\nimport { routeTree } from \"./routeTree.gen\";\nimport {\n\tApiProvider,\n\tinitializeApi,\n\tuseApiInContext,\n} from \"./utils/store/api\";\nimport { CentralProvider, useCentralStore } from \"./utils/store/central\";\n\nconst queryClient = new QueryClient({\n\tdefaultOptions: {\n\t\tqueries: {\n\t\t\tnetworkMode: \"always\",\n\t\t},\n\t\tmutations: {\n\t\t\tnetworkMode: \"always\",\n\t\t},\n\t},\n});\n\n//TODO: Need help implementing modal routes in TanStack Router\n// const SearchMask = createRouteMask({\n// \trouteTree,\n// \tfrom: \"/searchModal,\n// \tto: \"/search\",\n// \tparams: true,\n// });\n\n// Create a new router instance\nconst router = createRouter({\n\trouteTree,\n\tcontext: {\n\t\tapi: undefined!,\n\t\tcreateApi: undefined!,\n\t\tuser: undefined,\n\t\tjellyfinSDK: undefined!,\n\t\tfetchCurrentUser: undefined!,\n\t\tqueryClient,\n\t},\n\tdefaultPreload: false,\n\tdefaultPreloadStaleTime: 0,\n\tdefaultViewTransition: true,\n\tscrollRestoration: true,\n\tscrollRestorationBehavior: \"auto\",\n});\n\n// Register the router instance for type safety\ndeclare module \"@tanstack/react-router\" {\n\tinterface Register {\n\t\trouter: typeof router;\n\t}\n}\n\nfunction ProviderWrapper() {\n\tconst [api, createApi, jellyfinSDK] = useApiInContext((s) => [\n\t\ts.api,\n\t\ts.createApi,\n\t\ts.jellyfin,\n\t]);\n\tconst [user, fetchCurrentUser] = useCentralStore((s) => [\n\t\ts.currentUser,\n\t\ts.fetchCurrentUser,\n\t]);\n\tuseEffect(() => {\n\t\tif (api?.accessToken && !user?.Id) {\n\t\t\tfetchCurrentUser(api);\n\t\t}\n\t}, [api, user, fetchCurrentUser]);\n\tuseEffect(() => {\n\t\tif (api) {\n\t\t\tconsole.log(`API is set - Route ${window.location.pathname}`);\n\t\t\tconsole.log(api);\n\t\t} else {\n\t\t\tconsole.log(`API is not set - Route ${window.location.pathname}`);\n\t\t}\n\t\trouter.invalidate(); // This is a hack to force the router to re-evaluate the routes and re-run the beforeLoad functions\n\t}, [api?.accessToken]);\n\treturn (\n\t\t<RouterProvider\n\t\t\trouter={router}\n\t\t\tcontext={{\n\t\t\t\tapi,\n\t\t\t\tcreateApi,\n\t\t\t\tuser,\n\t\t\t\tjellyfinSDK,\n\t\t\t\tfetchCurrentUser,\n\t\t\t\t// queryClient,\n\t\t\t}}\n\t\t/>\n\t);\n}\n\nconst init = async () => {\n\tconst initialApi = await initializeApi();\n\n\tReactDOM.createRoot(document.getElementById(\"root\")!).render(\n\t\t<React.StrictMode>\n\t\t\t<QueryClientProvider client={queryClient}>\n\t\t\t\t<CentralProvider>\n\t\t\t\t\t<ApiProvider initialApi={initialApi}>\n\t\t\t\t\t\t<ProviderWrapper />\n\t\t\t\t\t</ApiProvider>\n\t\t\t\t</CentralProvider>\n\t\t\t</QueryClientProvider>\n\t\t</React.StrictMode>,\n\t);\n};\n\ninit();\n\ndocument.querySelector(\".app-loading\")!.classList.toggle(\"hidden\");\n"
  },
  {
    "path": "src/palette.module.scss",
    "content": "\n@use \"./styles/variables.scss\" as *;\n\n:export {\n\tclrAccentDefault: $clr-accent-default;\n\tclrSecondaryDefault: $clr-secondary-default;\n\tclrBackgroundDefault: $clr-background-default;\n\n\tclrBackgroundDark: $clr-background-dark;\n\tclrBackgroundDarkOpacity0_8: $clr-background-dark-opacity-0_8;\n\tclrBackgroundLight: $clr-background-light;\n\n\tborderRadiusDefault: $border-radius-default;\n}\n"
  },
  {
    "path": "src/routeTree.gen.ts",
    "content": "/* eslint-disable */\n\n// @ts-nocheck\n\n// noinspection JSUnusedGlobalSymbols\n\n// This file was automatically generated by TanStack Router.\n// You should NOT make any changes in this file as it will be overwritten.\n// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.\n\nimport { Route as rootRouteImport } from './routes/__root'\nimport { Route as ApiRouteImport } from './routes/_api'\nimport { Route as IndexRouteImport } from './routes/index'\nimport { Route as ErrorCodeRouteImport } from './routes/error/$code'\nimport { Route as ApiSettingsRouteImport } from './routes/_api/settings'\nimport { Route as ApiLoginRouteImport } from './routes/_api/login'\nimport { Route as ApiSearchIndexRouteImport } from './routes/_api/search/index'\nimport { Route as ApiPlayerIndexRouteImport } from './routes/_api/player/index'\nimport { Route as ApiHomeIndexRouteImport } from './routes/_api/home/index'\nimport { Route as ApiFavoriteIndexRouteImport } from './routes/_api/favorite/index'\nimport { Route as SetupServerListRouteImport } from './routes/setup/server.list'\nimport { Route as SetupServerErrorRouteImport } from './routes/setup/server.error'\nimport { Route as SetupServerAddRouteImport } from './routes/setup/server.add'\nimport { Route as ApiSettingsPreferencesRouteImport } from './routes/_api/settings/preferences'\nimport { Route as ApiSettingsAboutRouteImport } from './routes/_api/settings/about'\nimport { Route as ApiSeriesIdRouteImport } from './routes/_api/series/$id'\nimport { Route as ApiPlaylistIdRouteImport } from './routes/_api/playlist/$id'\nimport { Route as ApiPlayerPhotosRouteImport } from './routes/_api/player/photos'\nimport { Route as ApiPlayerAudioRouteImport } from './routes/_api/player/audio'\nimport { Route as ApiPersonIdRouteImport } from './routes/_api/person/$id'\nimport { Route as ApiLoginManualRouteImport } from './routes/_api/login/manual'\nimport { Route as ApiLoginListRouteImport } from './routes/_api/login/list'\nimport { Route as ApiLibraryIdRouteImport } from './routes/_api/library/$id'\nimport { Route as ApiItemIdRouteImport } from './routes/_api/item/$id'\nimport { Route as ApiEpisodeIdRouteImport } from './routes/_api/episode/$id'\nimport { Route as ApiBoxsetIdRouteImport } from './routes/_api/boxset/$id'\nimport { Route as ApiArtistIdRouteImport } from './routes/_api/artist/$id'\nimport { Route as ApiAlbumIdRouteImport } from './routes/_api/album/$id'\nimport { Route as ApiSettingsChangeServerIndexRouteImport } from './routes/_api/settings/changeServer/index'\nimport { Route as ApiLoginUserIdUserNameRouteImport } from './routes/_api/login/$userId.$userName'\n\nconst ApiRoute = ApiRouteImport.update({\n  id: '/_api',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst IndexRoute = IndexRouteImport.update({\n  id: '/',\n  path: '/',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst ErrorCodeRoute = ErrorCodeRouteImport.update({\n  id: '/error/$code',\n  path: '/error/$code',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst ApiSettingsRoute = ApiSettingsRouteImport.update({\n  id: '/settings',\n  path: '/settings',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiLoginRoute = ApiLoginRouteImport.update({\n  id: '/login',\n  path: '/login',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiSearchIndexRoute = ApiSearchIndexRouteImport.update({\n  id: '/search/',\n  path: '/search/',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiPlayerIndexRoute = ApiPlayerIndexRouteImport.update({\n  id: '/player/',\n  path: '/player/',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiHomeIndexRoute = ApiHomeIndexRouteImport.update({\n  id: '/home/',\n  path: '/home/',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiFavoriteIndexRoute = ApiFavoriteIndexRouteImport.update({\n  id: '/favorite/',\n  path: '/favorite/',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst SetupServerListRoute = SetupServerListRouteImport.update({\n  id: '/setup/server/list',\n  path: '/setup/server/list',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst SetupServerErrorRoute = SetupServerErrorRouteImport.update({\n  id: '/setup/server/error',\n  path: '/setup/server/error',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst SetupServerAddRoute = SetupServerAddRouteImport.update({\n  id: '/setup/server/add',\n  path: '/setup/server/add',\n  getParentRoute: () => rootRouteImport,\n} as any)\nconst ApiSettingsPreferencesRoute = ApiSettingsPreferencesRouteImport.update({\n  id: '/preferences',\n  path: '/preferences',\n  getParentRoute: () => ApiSettingsRoute,\n} as any)\nconst ApiSettingsAboutRoute = ApiSettingsAboutRouteImport.update({\n  id: '/about',\n  path: '/about',\n  getParentRoute: () => ApiSettingsRoute,\n} as any)\nconst ApiSeriesIdRoute = ApiSeriesIdRouteImport.update({\n  id: '/series/$id',\n  path: '/series/$id',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiPlaylistIdRoute = ApiPlaylistIdRouteImport.update({\n  id: '/playlist/$id',\n  path: '/playlist/$id',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiPlayerPhotosRoute = ApiPlayerPhotosRouteImport.update({\n  id: '/player/photos',\n  path: '/player/photos',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiPlayerAudioRoute = ApiPlayerAudioRouteImport.update({\n  id: '/player/audio',\n  path: '/player/audio',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiPersonIdRoute = ApiPersonIdRouteImport.update({\n  id: '/person/$id',\n  path: '/person/$id',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiLoginManualRoute = ApiLoginManualRouteImport.update({\n  id: '/manual',\n  path: '/manual',\n  getParentRoute: () => ApiLoginRoute,\n} as any)\nconst ApiLoginListRoute = ApiLoginListRouteImport.update({\n  id: '/list',\n  path: '/list',\n  getParentRoute: () => ApiLoginRoute,\n} as any)\nconst ApiLibraryIdRoute = ApiLibraryIdRouteImport.update({\n  id: '/library/$id',\n  path: '/library/$id',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiItemIdRoute = ApiItemIdRouteImport.update({\n  id: '/item/$id',\n  path: '/item/$id',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiEpisodeIdRoute = ApiEpisodeIdRouteImport.update({\n  id: '/episode/$id',\n  path: '/episode/$id',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiBoxsetIdRoute = ApiBoxsetIdRouteImport.update({\n  id: '/boxset/$id',\n  path: '/boxset/$id',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiArtistIdRoute = ApiArtistIdRouteImport.update({\n  id: '/artist/$id',\n  path: '/artist/$id',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiAlbumIdRoute = ApiAlbumIdRouteImport.update({\n  id: '/album/$id',\n  path: '/album/$id',\n  getParentRoute: () => ApiRoute,\n} as any)\nconst ApiSettingsChangeServerIndexRoute =\n  ApiSettingsChangeServerIndexRouteImport.update({\n    id: '/changeServer/',\n    path: '/changeServer/',\n    getParentRoute: () => ApiSettingsRoute,\n  } as any)\nconst ApiLoginUserIdUserNameRoute = ApiLoginUserIdUserNameRouteImport.update({\n  id: '/$userId/$userName',\n  path: '/$userId/$userName',\n  getParentRoute: () => ApiLoginRoute,\n} as any)\n\nexport interface FileRoutesByFullPath {\n  '/': typeof IndexRoute\n  '/login': typeof ApiLoginRouteWithChildren\n  '/settings': typeof ApiSettingsRouteWithChildren\n  '/error/$code': typeof ErrorCodeRoute\n  '/album/$id': typeof ApiAlbumIdRoute\n  '/artist/$id': typeof ApiArtistIdRoute\n  '/boxset/$id': typeof ApiBoxsetIdRoute\n  '/episode/$id': typeof ApiEpisodeIdRoute\n  '/item/$id': typeof ApiItemIdRoute\n  '/library/$id': typeof ApiLibraryIdRoute\n  '/login/list': typeof ApiLoginListRoute\n  '/login/manual': typeof ApiLoginManualRoute\n  '/person/$id': typeof ApiPersonIdRoute\n  '/player/audio': typeof ApiPlayerAudioRoute\n  '/player/photos': typeof ApiPlayerPhotosRoute\n  '/playlist/$id': typeof ApiPlaylistIdRoute\n  '/series/$id': typeof ApiSeriesIdRoute\n  '/settings/about': typeof ApiSettingsAboutRoute\n  '/settings/preferences': typeof ApiSettingsPreferencesRoute\n  '/setup/server/add': typeof SetupServerAddRoute\n  '/setup/server/error': typeof SetupServerErrorRoute\n  '/setup/server/list': typeof SetupServerListRoute\n  '/favorite/': typeof ApiFavoriteIndexRoute\n  '/home/': typeof ApiHomeIndexRoute\n  '/player/': typeof ApiPlayerIndexRoute\n  '/search/': typeof ApiSearchIndexRoute\n  '/login/$userId/$userName': typeof ApiLoginUserIdUserNameRoute\n  '/settings/changeServer/': typeof ApiSettingsChangeServerIndexRoute\n}\nexport interface FileRoutesByTo {\n  '/': typeof IndexRoute\n  '/login': typeof ApiLoginRouteWithChildren\n  '/settings': typeof ApiSettingsRouteWithChildren\n  '/error/$code': typeof ErrorCodeRoute\n  '/album/$id': typeof ApiAlbumIdRoute\n  '/artist/$id': typeof ApiArtistIdRoute\n  '/boxset/$id': typeof ApiBoxsetIdRoute\n  '/episode/$id': typeof ApiEpisodeIdRoute\n  '/item/$id': typeof ApiItemIdRoute\n  '/library/$id': typeof ApiLibraryIdRoute\n  '/login/list': typeof ApiLoginListRoute\n  '/login/manual': typeof ApiLoginManualRoute\n  '/person/$id': typeof ApiPersonIdRoute\n  '/player/audio': typeof ApiPlayerAudioRoute\n  '/player/photos': typeof ApiPlayerPhotosRoute\n  '/playlist/$id': typeof ApiPlaylistIdRoute\n  '/series/$id': typeof ApiSeriesIdRoute\n  '/settings/about': typeof ApiSettingsAboutRoute\n  '/settings/preferences': typeof ApiSettingsPreferencesRoute\n  '/setup/server/add': typeof SetupServerAddRoute\n  '/setup/server/error': typeof SetupServerErrorRoute\n  '/setup/server/list': typeof SetupServerListRoute\n  '/favorite': typeof ApiFavoriteIndexRoute\n  '/home': typeof ApiHomeIndexRoute\n  '/player': typeof ApiPlayerIndexRoute\n  '/search': typeof ApiSearchIndexRoute\n  '/login/$userId/$userName': typeof ApiLoginUserIdUserNameRoute\n  '/settings/changeServer': typeof ApiSettingsChangeServerIndexRoute\n}\nexport interface FileRoutesById {\n  __root__: typeof rootRouteImport\n  '/': typeof IndexRoute\n  '/_api': typeof ApiRouteWithChildren\n  '/_api/login': typeof ApiLoginRouteWithChildren\n  '/_api/settings': typeof ApiSettingsRouteWithChildren\n  '/error/$code': typeof ErrorCodeRoute\n  '/_api/album/$id': typeof ApiAlbumIdRoute\n  '/_api/artist/$id': typeof ApiArtistIdRoute\n  '/_api/boxset/$id': typeof ApiBoxsetIdRoute\n  '/_api/episode/$id': typeof ApiEpisodeIdRoute\n  '/_api/item/$id': typeof ApiItemIdRoute\n  '/_api/library/$id': typeof ApiLibraryIdRoute\n  '/_api/login/list': typeof ApiLoginListRoute\n  '/_api/login/manual': typeof ApiLoginManualRoute\n  '/_api/person/$id': typeof ApiPersonIdRoute\n  '/_api/player/audio': typeof ApiPlayerAudioRoute\n  '/_api/player/photos': typeof ApiPlayerPhotosRoute\n  '/_api/playlist/$id': typeof ApiPlaylistIdRoute\n  '/_api/series/$id': typeof ApiSeriesIdRoute\n  '/_api/settings/about': typeof ApiSettingsAboutRoute\n  '/_api/settings/preferences': typeof ApiSettingsPreferencesRoute\n  '/setup/server/add': typeof SetupServerAddRoute\n  '/setup/server/error': typeof SetupServerErrorRoute\n  '/setup/server/list': typeof SetupServerListRoute\n  '/_api/favorite/': typeof ApiFavoriteIndexRoute\n  '/_api/home/': typeof ApiHomeIndexRoute\n  '/_api/player/': typeof ApiPlayerIndexRoute\n  '/_api/search/': typeof ApiSearchIndexRoute\n  '/_api/login/$userId/$userName': typeof ApiLoginUserIdUserNameRoute\n  '/_api/settings/changeServer/': typeof ApiSettingsChangeServerIndexRoute\n}\nexport interface FileRouteTypes {\n  fileRoutesByFullPath: FileRoutesByFullPath\n  fullPaths:\n    | '/'\n    | '/login'\n    | '/settings'\n    | '/error/$code'\n    | '/album/$id'\n    | '/artist/$id'\n    | '/boxset/$id'\n    | '/episode/$id'\n    | '/item/$id'\n    | '/library/$id'\n    | '/login/list'\n    | '/login/manual'\n    | '/person/$id'\n    | '/player/audio'\n    | '/player/photos'\n    | '/playlist/$id'\n    | '/series/$id'\n    | '/settings/about'\n    | '/settings/preferences'\n    | '/setup/server/add'\n    | '/setup/server/error'\n    | '/setup/server/list'\n    | '/favorite/'\n    | '/home/'\n    | '/player/'\n    | '/search/'\n    | '/login/$userId/$userName'\n    | '/settings/changeServer/'\n  fileRoutesByTo: FileRoutesByTo\n  to:\n    | '/'\n    | '/login'\n    | '/settings'\n    | '/error/$code'\n    | '/album/$id'\n    | '/artist/$id'\n    | '/boxset/$id'\n    | '/episode/$id'\n    | '/item/$id'\n    | '/library/$id'\n    | '/login/list'\n    | '/login/manual'\n    | '/person/$id'\n    | '/player/audio'\n    | '/player/photos'\n    | '/playlist/$id'\n    | '/series/$id'\n    | '/settings/about'\n    | '/settings/preferences'\n    | '/setup/server/add'\n    | '/setup/server/error'\n    | '/setup/server/list'\n    | '/favorite'\n    | '/home'\n    | '/player'\n    | '/search'\n    | '/login/$userId/$userName'\n    | '/settings/changeServer'\n  id:\n    | '__root__'\n    | '/'\n    | '/_api'\n    | '/_api/login'\n    | '/_api/settings'\n    | '/error/$code'\n    | '/_api/album/$id'\n    | '/_api/artist/$id'\n    | '/_api/boxset/$id'\n    | '/_api/episode/$id'\n    | '/_api/item/$id'\n    | '/_api/library/$id'\n    | '/_api/login/list'\n    | '/_api/login/manual'\n    | '/_api/person/$id'\n    | '/_api/player/audio'\n    | '/_api/player/photos'\n    | '/_api/playlist/$id'\n    | '/_api/series/$id'\n    | '/_api/settings/about'\n    | '/_api/settings/preferences'\n    | '/setup/server/add'\n    | '/setup/server/error'\n    | '/setup/server/list'\n    | '/_api/favorite/'\n    | '/_api/home/'\n    | '/_api/player/'\n    | '/_api/search/'\n    | '/_api/login/$userId/$userName'\n    | '/_api/settings/changeServer/'\n  fileRoutesById: FileRoutesById\n}\nexport interface RootRouteChildren {\n  IndexRoute: typeof IndexRoute\n  ApiRoute: typeof ApiRouteWithChildren\n  ErrorCodeRoute: typeof ErrorCodeRoute\n  SetupServerAddRoute: typeof SetupServerAddRoute\n  SetupServerErrorRoute: typeof SetupServerErrorRoute\n  SetupServerListRoute: typeof SetupServerListRoute\n}\n\ndeclare module '@tanstack/react-router' {\n  interface FileRoutesByPath {\n    '/_api': {\n      id: '/_api'\n      path: ''\n      fullPath: '/'\n      preLoaderRoute: typeof ApiRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/': {\n      id: '/'\n      path: '/'\n      fullPath: '/'\n      preLoaderRoute: typeof IndexRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/error/$code': {\n      id: '/error/$code'\n      path: '/error/$code'\n      fullPath: '/error/$code'\n      preLoaderRoute: typeof ErrorCodeRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/_api/settings': {\n      id: '/_api/settings'\n      path: '/settings'\n      fullPath: '/settings'\n      preLoaderRoute: typeof ApiSettingsRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/login': {\n      id: '/_api/login'\n      path: '/login'\n      fullPath: '/login'\n      preLoaderRoute: typeof ApiLoginRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/search/': {\n      id: '/_api/search/'\n      path: '/search'\n      fullPath: '/search/'\n      preLoaderRoute: typeof ApiSearchIndexRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/player/': {\n      id: '/_api/player/'\n      path: '/player'\n      fullPath: '/player/'\n      preLoaderRoute: typeof ApiPlayerIndexRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/home/': {\n      id: '/_api/home/'\n      path: '/home'\n      fullPath: '/home/'\n      preLoaderRoute: typeof ApiHomeIndexRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/favorite/': {\n      id: '/_api/favorite/'\n      path: '/favorite'\n      fullPath: '/favorite/'\n      preLoaderRoute: typeof ApiFavoriteIndexRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/setup/server/list': {\n      id: '/setup/server/list'\n      path: '/setup/server/list'\n      fullPath: '/setup/server/list'\n      preLoaderRoute: typeof SetupServerListRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/setup/server/error': {\n      id: '/setup/server/error'\n      path: '/setup/server/error'\n      fullPath: '/setup/server/error'\n      preLoaderRoute: typeof SetupServerErrorRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/setup/server/add': {\n      id: '/setup/server/add'\n      path: '/setup/server/add'\n      fullPath: '/setup/server/add'\n      preLoaderRoute: typeof SetupServerAddRouteImport\n      parentRoute: typeof rootRouteImport\n    }\n    '/_api/settings/preferences': {\n      id: '/_api/settings/preferences'\n      path: '/preferences'\n      fullPath: '/settings/preferences'\n      preLoaderRoute: typeof ApiSettingsPreferencesRouteImport\n      parentRoute: typeof ApiSettingsRoute\n    }\n    '/_api/settings/about': {\n      id: '/_api/settings/about'\n      path: '/about'\n      fullPath: '/settings/about'\n      preLoaderRoute: typeof ApiSettingsAboutRouteImport\n      parentRoute: typeof ApiSettingsRoute\n    }\n    '/_api/series/$id': {\n      id: '/_api/series/$id'\n      path: '/series/$id'\n      fullPath: '/series/$id'\n      preLoaderRoute: typeof ApiSeriesIdRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/playlist/$id': {\n      id: '/_api/playlist/$id'\n      path: '/playlist/$id'\n      fullPath: '/playlist/$id'\n      preLoaderRoute: typeof ApiPlaylistIdRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/player/photos': {\n      id: '/_api/player/photos'\n      path: '/player/photos'\n      fullPath: '/player/photos'\n      preLoaderRoute: typeof ApiPlayerPhotosRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/player/audio': {\n      id: '/_api/player/audio'\n      path: '/player/audio'\n      fullPath: '/player/audio'\n      preLoaderRoute: typeof ApiPlayerAudioRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/person/$id': {\n      id: '/_api/person/$id'\n      path: '/person/$id'\n      fullPath: '/person/$id'\n      preLoaderRoute: typeof ApiPersonIdRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/login/manual': {\n      id: '/_api/login/manual'\n      path: '/manual'\n      fullPath: '/login/manual'\n      preLoaderRoute: typeof ApiLoginManualRouteImport\n      parentRoute: typeof ApiLoginRoute\n    }\n    '/_api/login/list': {\n      id: '/_api/login/list'\n      path: '/list'\n      fullPath: '/login/list'\n      preLoaderRoute: typeof ApiLoginListRouteImport\n      parentRoute: typeof ApiLoginRoute\n    }\n    '/_api/library/$id': {\n      id: '/_api/library/$id'\n      path: '/library/$id'\n      fullPath: '/library/$id'\n      preLoaderRoute: typeof ApiLibraryIdRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/item/$id': {\n      id: '/_api/item/$id'\n      path: '/item/$id'\n      fullPath: '/item/$id'\n      preLoaderRoute: typeof ApiItemIdRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/episode/$id': {\n      id: '/_api/episode/$id'\n      path: '/episode/$id'\n      fullPath: '/episode/$id'\n      preLoaderRoute: typeof ApiEpisodeIdRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/boxset/$id': {\n      id: '/_api/boxset/$id'\n      path: '/boxset/$id'\n      fullPath: '/boxset/$id'\n      preLoaderRoute: typeof ApiBoxsetIdRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/artist/$id': {\n      id: '/_api/artist/$id'\n      path: '/artist/$id'\n      fullPath: '/artist/$id'\n      preLoaderRoute: typeof ApiArtistIdRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/album/$id': {\n      id: '/_api/album/$id'\n      path: '/album/$id'\n      fullPath: '/album/$id'\n      preLoaderRoute: typeof ApiAlbumIdRouteImport\n      parentRoute: typeof ApiRoute\n    }\n    '/_api/settings/changeServer/': {\n      id: '/_api/settings/changeServer/'\n      path: '/changeServer'\n      fullPath: '/settings/changeServer/'\n      preLoaderRoute: typeof ApiSettingsChangeServerIndexRouteImport\n      parentRoute: typeof ApiSettingsRoute\n    }\n    '/_api/login/$userId/$userName': {\n      id: '/_api/login/$userId/$userName'\n      path: '/$userId/$userName'\n      fullPath: '/login/$userId/$userName'\n      preLoaderRoute: typeof ApiLoginUserIdUserNameRouteImport\n      parentRoute: typeof ApiLoginRoute\n    }\n  }\n}\n\ninterface ApiLoginRouteChildren {\n  ApiLoginListRoute: typeof ApiLoginListRoute\n  ApiLoginManualRoute: typeof ApiLoginManualRoute\n  ApiLoginUserIdUserNameRoute: typeof ApiLoginUserIdUserNameRoute\n}\n\nconst ApiLoginRouteChildren: ApiLoginRouteChildren = {\n  ApiLoginListRoute: ApiLoginListRoute,\n  ApiLoginManualRoute: ApiLoginManualRoute,\n  ApiLoginUserIdUserNameRoute: ApiLoginUserIdUserNameRoute,\n}\n\nconst ApiLoginRouteWithChildren = ApiLoginRoute._addFileChildren(\n  ApiLoginRouteChildren,\n)\n\ninterface ApiSettingsRouteChildren {\n  ApiSettingsAboutRoute: typeof ApiSettingsAboutRoute\n  ApiSettingsPreferencesRoute: typeof ApiSettingsPreferencesRoute\n  ApiSettingsChangeServerIndexRoute: typeof ApiSettingsChangeServerIndexRoute\n}\n\nconst ApiSettingsRouteChildren: ApiSettingsRouteChildren = {\n  ApiSettingsAboutRoute: ApiSettingsAboutRoute,\n  ApiSettingsPreferencesRoute: ApiSettingsPreferencesRoute,\n  ApiSettingsChangeServerIndexRoute: ApiSettingsChangeServerIndexRoute,\n}\n\nconst ApiSettingsRouteWithChildren = ApiSettingsRoute._addFileChildren(\n  ApiSettingsRouteChildren,\n)\n\ninterface ApiRouteChildren {\n  ApiLoginRoute: typeof ApiLoginRouteWithChildren\n  ApiSettingsRoute: typeof ApiSettingsRouteWithChildren\n  ApiAlbumIdRoute: typeof ApiAlbumIdRoute\n  ApiArtistIdRoute: typeof ApiArtistIdRoute\n  ApiBoxsetIdRoute: typeof ApiBoxsetIdRoute\n  ApiEpisodeIdRoute: typeof ApiEpisodeIdRoute\n  ApiItemIdRoute: typeof ApiItemIdRoute\n  ApiLibraryIdRoute: typeof ApiLibraryIdRoute\n  ApiPersonIdRoute: typeof ApiPersonIdRoute\n  ApiPlayerAudioRoute: typeof ApiPlayerAudioRoute\n  ApiPlayerPhotosRoute: typeof ApiPlayerPhotosRoute\n  ApiPlaylistIdRoute: typeof ApiPlaylistIdRoute\n  ApiSeriesIdRoute: typeof ApiSeriesIdRoute\n  ApiFavoriteIndexRoute: typeof ApiFavoriteIndexRoute\n  ApiHomeIndexRoute: typeof ApiHomeIndexRoute\n  ApiPlayerIndexRoute: typeof ApiPlayerIndexRoute\n  ApiSearchIndexRoute: typeof ApiSearchIndexRoute\n}\n\nconst ApiRouteChildren: ApiRouteChildren = {\n  ApiLoginRoute: ApiLoginRouteWithChildren,\n  ApiSettingsRoute: ApiSettingsRouteWithChildren,\n  ApiAlbumIdRoute: ApiAlbumIdRoute,\n  ApiArtistIdRoute: ApiArtistIdRoute,\n  ApiBoxsetIdRoute: ApiBoxsetIdRoute,\n  ApiEpisodeIdRoute: ApiEpisodeIdRoute,\n  ApiItemIdRoute: ApiItemIdRoute,\n  ApiLibraryIdRoute: ApiLibraryIdRoute,\n  ApiPersonIdRoute: ApiPersonIdRoute,\n  ApiPlayerAudioRoute: ApiPlayerAudioRoute,\n  ApiPlayerPhotosRoute: ApiPlayerPhotosRoute,\n  ApiPlaylistIdRoute: ApiPlaylistIdRoute,\n  ApiSeriesIdRoute: ApiSeriesIdRoute,\n  ApiFavoriteIndexRoute: ApiFavoriteIndexRoute,\n  ApiHomeIndexRoute: ApiHomeIndexRoute,\n  ApiPlayerIndexRoute: ApiPlayerIndexRoute,\n  ApiSearchIndexRoute: ApiSearchIndexRoute,\n}\n\nconst ApiRouteWithChildren = ApiRoute._addFileChildren(ApiRouteChildren)\n\nconst rootRouteChildren: RootRouteChildren = {\n  IndexRoute: IndexRoute,\n  ApiRoute: ApiRouteWithChildren,\n  ErrorCodeRoute: ErrorCodeRoute,\n  SetupServerAddRoute: SetupServerAddRoute,\n  SetupServerErrorRoute: SetupServerErrorRoute,\n  SetupServerListRoute: SetupServerListRoute,\n}\nexport const routeTree = rootRouteImport\n  ._addFileChildren(rootRouteChildren)\n  ._addFileTypes<FileRouteTypes>()\n"
  },
  {
    "path": "src/routes/__root.tsx",
    "content": "// import { EasterEgg } from \"@/components/utils/easterEgg\";\nimport { CssBaseline } from \"@mui/material\";\nimport { ThemeProvider } from \"@mui/material/styles\";\nimport { createRootRouteWithContext, Outlet } from \"@tanstack/react-router\";\nimport { SnackbarProvider } from \"notistack\";\nimport React from \"react\";\nimport { AppBar } from \"@/components/appBar/appBar\";\n// import NProgress from \"@/components/nProgress\";\nimport AudioPlayer from \"@/components/playback/audioPlayer\";\n\nimport \"../styles/global.scss\";\nimport { theme } from \"@/theme\";\n\n// Fonts\nimport \"@fontsource-variable/jetbrains-mono\";\nimport \"@fontsource-variable/noto-sans\";\nimport \"@fontsource-variable/plus-jakarta-sans\";\n\nimport \"material-symbols/rounded.scss\";\nimport type { Api, Jellyfin } from \"@jellyfin/sdk\";\nimport type { UserDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport type { QueryClient } from \"@tanstack/react-query\";\nimport { ReactQueryDevtools } from \"@tanstack/react-query-devtools\";\nimport { TanStackRouterDevtools } from \"@tanstack/react-router-devtools\";\n// import { TanStackRouterDevtools } from \"@tanstack/react-router-devtools\";\nimport { DndProvider } from \"react-dnd\";\nimport { HTML5Backend } from \"react-dnd-html5-backend\";\nimport Backdrop from \"@/components/backdrop\";\nimport { ErrorNotice } from \"@/components/notices/errorNotice/errorNotice\";\nimport RouterLoading from \"@/components/routerLoading\";\nimport Search from \"@/components/search\";\nimport Updater from \"@/components/updater\";\nimport { EasterEgg } from \"@/components/utils/easterEgg\";\n\ntype ApiContext = {\n\tapi: Api | undefined;\n\tcreateApi: (serverAddress: string, accessToken: string | undefined) => void;\n\tuser: UserDto | null | undefined;\n\tjellyfinSDK: Jellyfin;\n\tfetchCurrentUser: (api: Api | undefined) => Promise<void>;\n\tqueryClient: QueryClient;\n};\n\nexport const Route = createRootRouteWithContext<ApiContext>()({\n\tcomponent: () => {\n\t\treturn (\n\t\t\t<DndProvider backend={HTML5Backend}>\n\t\t\t\t<ThemeProvider theme={theme}>\n\t\t\t\t\t<SnackbarProvider maxSnack={5}>\n\t\t\t\t\t\t<CssBaseline />\n\t\t\t\t\t\t<EasterEgg />\n\t\t\t\t\t\t{/* <NProgress /> */}\n\n\t\t\t\t\t\t<RouterLoading />\n\n\t\t\t\t\t\t<Updater />\n\t\t\t\t\t\t<Backdrop />\n\t\t\t\t\t\t<Search />\n\t\t\t\t\t\t<AppBar />\n\t\t\t\t\t\t<AudioPlayer />\n\t\t\t\t\t\t<Outlet />\n\t\t\t\t\t\t<ReactQueryDevtools />\n\t\t\t\t\t\t<TanStackRouterDevtools />\n\t\t\t\t\t</SnackbarProvider>\n\t\t\t\t</ThemeProvider>\n\t\t\t</DndProvider>\n\t\t);\n\t},\n\terrorComponent: ErrorNotice,\n});"
  },
  {
    "path": "src/routes/_api/album/$id.tsx",
    "content": "import { BaseItemKind, SortOrder } from \"@jellyfin/sdk/lib/generated-client\";\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport { getLibraryApi } from \"@jellyfin/sdk/lib/utils/api/library-api\";\nimport { getUserLibraryApi } from \"@jellyfin/sdk/lib/utils/api/user-library-api\";\nimport Box from \"@mui/material/Box\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport Typography from \"@mui/material/Typography\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport React, { useLayoutEffect, useMemo } from \"react\";\nimport LikeButton from \"@/components/buttons/likeButton\";\nimport PlayButton from \"@/components/buttons/playButton\";\nimport { Card } from \"@/components/card/card\";\nimport CardScroller from \"@/components/cardScroller/cardScroller\";\nimport { ErrorNotice } from \"@/components/notices/errorNotice/errorNotice\";\nimport { getTypeIcon } from \"@/components/utils/iconsCollection\";\nimport { getRuntimeCompact } from \"@/utils/date/time\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport \"./album.scss\";\n\nimport { Chip } from \"@mui/material\";\nimport { createFileRoute, Link } from \"@tanstack/react-router\";\nimport { createPortal } from \"react-dom\";\nimport AlbumMusicTrack from \"@/components/albumMusicTrack\";\nimport ShowMoreText from \"@/components/showMoreText\";\nimport TagChip from \"@/components/tagChip\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useCentralStore } from \"@/utils/store/central\";\n\nexport const Route = createFileRoute(\"/_api/album/$id\")({\n\tcomponent: MusicAlbumTitlePage,\n});\n\nfunction MusicAlbumTitlePage() {\n\tconst { id } = Route.useParams();\n\tconst api = useApiInContext((s) => s.api);\n\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst item = useQuery({\n\t\tqueryKey: [\"item\", id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getUserLibraryApi(api).getItem({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\titemId: id,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: !!user?.Id,\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst similarItems = useQuery({\n\t\tqueryKey: [\"item\", id, \"similarItem\"],\n\t\tqueryFn: async () => {\n\t\t\tif (api && item.data?.Id) {\n\t\t\t\tconst result = await getLibraryApi(api).getSimilarAlbums({\n\t\t\t\t\tuserId: user?.Id,\n\t\t\t\t\titemId: item.data?.Id,\n\t\t\t\t\tlimit: 16,\n\t\t\t\t});\n\t\t\t\treturn result.data;\n\t\t\t}\n\t\t\treturn null;\n\t\t},\n\t\tenabled: item.isSuccess,\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst musicTracks = useQuery({\n\t\tqueryKey: [\"item\", \"musicTracks\", id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tparentId: item.data?.Id,\n\t\t\t\tsortOrder: [SortOrder.Ascending],\n\t\t\t\tsortBy: [\"IndexNumber\", \"ParentIndexNumber\"],\n\t\t\t\tfields: [\"MediaSources\"],\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: item.isSuccess && item.data?.Type === BaseItemKind.MusicAlbum,\n\t\tnetworkMode: \"always\",\n\t});\n\n\tconst setBackdrop = useBackdropStore((state) => state.setBackdrop);\n\tconst lastBackdropRef = React.useRef<string | undefined>(undefined);\n\n\tconst allDiscs = useMemo(() => {\n\t\tconst discs: number[] = [];\n\t\tif (musicTracks.data?.Items) {\n\t\t\tfor (const track of musicTracks.data.Items) {\n\t\t\t\tif (\n\t\t\t\t\ttrack.ParentIndexNumber &&\n\t\t\t\t\t!discs.includes(track.ParentIndexNumber)\n\t\t\t\t) {\n\t\t\t\t\tdiscs.push(track.ParentIndexNumber);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn discs;\n\t}, [musicTracks.data]);\n\n\tuseLayoutEffect(() => {\n\t\tif (item.isSuccess && item.data) {\n\t\t\tconst backdropItemId = item.data.ParentBackdropItemId;\n\t\t\tif (backdropItemId && lastBackdropRef.current !== backdropItemId) {\n\t\t\t\tconst hash =\n\t\t\t\t\titem.data.ImageBlurHashes?.Backdrop?.[\n\t\t\t\t\t\titem.data.ParentBackdropImageTags?.[0] ?? \"\"\n\t\t\t\t\t];\n\t\t\t\tif (hash) {\n\t\t\t\t\tsetBackdrop(hash);\n\t\t\t\t\tlastBackdropRef.current = backdropItemId;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, [item.isSuccess, item.data, setBackdrop]);\n\n\tif (item.isPending || similarItems.isPending) {\n\t\treturn (\n\t\t\t<Box\n\t\t\t\tsx={{\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\theight: \"100vh\",\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<CircularProgress />\n\t\t\t</Box>\n\t\t);\n\t}\n\tif (item.isSuccess && item.data && similarItems.isSuccess) {\n\t\treturn (\n\t\t\t<div key={id} className=\"scrollY padded-top item item-album\">\n\t\t\t\t<div className=\"item-info\">\n\t\t\t\t\t<Typography className=\"item-info-name\" variant=\"h3\">\n\t\t\t\t\t\t{item.data?.Name}\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<div className=\"flex flex-align-center item-info-album-info\">\n\t\t\t\t\t\t{/* @ts-ignore - Using Link as component  */}\n\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\tto=\"/artist/$id\"\n\t\t\t\t\t\t\tparams={{ id: item.data.AlbumArtists?.[0].Id ?? \"\" }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Chip\n\t\t\t\t\t\t\t\t// component={Link}\n\t\t\t\t\t\t\t\tclassName=\"flex flex-align-center\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tcolor: \"white\",\n\t\t\t\t\t\t\t\t\ttextDecoration: \"none\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\ticon={\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded fill\"\n\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\tpaddingLeft: \"0.25em\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tartist\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlabel={item.data.AlbumArtist}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t<Typography className=\"opacity-07\">\n\t\t\t\t\t\t\t{item.data.ProductionYear}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t<Typography className=\"opacity-07\">\n\t\t\t\t\t\t\t{(musicTracks.data?.TotalRecordCount ?? 0) > 1\n\t\t\t\t\t\t\t\t? `${musicTracks.data?.TotalRecordCount} songs`\n\t\t\t\t\t\t\t\t: \"Single\"}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t<Typography className=\"opacity-07\">\n\t\t\t\t\t\t\t{getRuntimeCompact(item.data.CumulativeRunTimeTicks ?? 0)}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"item-info-buttons\">\n\t\t\t\t\t\t<PlayButton\n\t\t\t\t\t\t\titem={item.data}\n\t\t\t\t\t\t\taudio\n\t\t\t\t\t\t\titemType={item.data.Type ?? \"Audio\"}\n\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\titemId={item.data.Id}\n\t\t\t\t\t\t\tisFavorite={item.data.UserData?.IsFavorite}\n\t\t\t\t\t\t\tqueryKey={[\"item\", \"musicTracks\"]}\n\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\titemName={item.data.Name}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"item-info-track-container \">\n\t\t\t\t\t\t{allDiscs.length > 1 ? (\n\t\t\t\t\t\t\tallDiscs.map((disc) => (\n\t\t\t\t\t\t\t\t<div className=\"item-info-disc-container\" key={disc}>\n\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\tclassName=\"item-info-disc-header flex flex-row flex-align-center\"\n\t\t\t\t\t\t\t\t\t\tstyle={{ marginBottom: \"1em\" }}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\t\t\t\tstyle={{ fontSize: \"2em\" }}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\talbum\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t<Typography variant=\"h5\">Disc {disc}</Typography>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t<div className=\"item-info-track header\">\n\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded index\">tag</span>\n\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">Title</Typography>\n\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">Duration</Typography>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t{musicTracks.data?.Items?.map(\n\t\t\t\t\t\t\t\t\t\t(track, index) =>\n\t\t\t\t\t\t\t\t\t\t\ttrack.ParentIndexNumber === disc && (\n\t\t\t\t\t\t\t\t\t\t\t\t<AlbumMusicTrack\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={track.Id}\n\t\t\t\t\t\t\t\t\t\t\t\t\ttrack={track}\n\t\t\t\t\t\t\t\t\t\t\t\t\ttrackIndex={index}\n\t\t\t\t\t\t\t\t\t\t\t\t\tmusicTracks={musicTracks.data}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<div className=\"item-info-track header\">\n\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded index\">tag</span>\n\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">Title</Typography>\n\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">Duration</Typography>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t{musicTracks.data?.Items?.map((track, index) => (\n\t\t\t\t\t\t\t\t\t<AlbumMusicTrack\n\t\t\t\t\t\t\t\t\t\tkey={track.Id}\n\t\t\t\t\t\t\t\t\t\ttrack={track}\n\t\t\t\t\t\t\t\t\t\ttrackIndex={index}\n\t\t\t\t\t\t\t\t\t\tmusicTracks={musicTracks.data}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t{createPortal(\n\t\t\t\t\t<div className=\"item-info-sidebar\">\n\t\t\t\t\t\t<div className=\"item-info-sidebar-image-container\">\n\t\t\t\t\t\t\t{item.data.ImageTags?.Primary ? (\n\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\tclassName=\"item-info-sidebar-image\"\n\t\t\t\t\t\t\t\t\talt={item.data.Name ?? \"Album\"}\n\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\titem.data.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\ttag: item.data.ImageTags.Primary,\n\t\t\t\t\t\t\t\t\t\t\t\tquality: 90,\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<div className=\"item-info-sidebar-icon\">\n\t\t\t\t\t\t\t\t\t{getTypeIcon(\"MusicAlbum\")}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"flex flex-align-center\"\n\t\t\t\t\t\t\tstyle={{ gap: \"1em\", flexWrap: \"wrap\" }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.data.GenreItems?.map((genre) => (\n\t\t\t\t\t\t\t\t<TagChip label={genre.Name ?? \"genre\"} key={genre.Id} />\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"item-info-sidebar-overview\">\n\t\t\t\t\t\t\t<ShowMoreText\n\t\t\t\t\t\t\t\tcontent={item.data.Overview ?? \"\"}\n\t\t\t\t\t\t\t\tcollapsedLines={4}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>,\n\t\t\t\t\tdocument.body,\n\t\t\t\t)}\n\t\t\t\t{(similarItems.data?.TotalRecordCount ?? 0) > 0 && (\n\t\t\t\t\t<CardScroller\n\t\t\t\t\t\ttitle=\"You might also like\"\n\t\t\t\t\t\tdisplayCards={5}\n\t\t\t\t\t\tdisableDecoration\n\t\t\t\t\t>\n\t\t\t\t\t\t{similarItems.data?.Items?.map((similar) => {\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\tkey={similar.Id}\n\t\t\t\t\t\t\t\t\titem={similar}\n\t\t\t\t\t\t\t\t\tseriesId={similar.SeriesId}\n\t\t\t\t\t\t\t\t\tcardTitle={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? similar.SeriesName\n\t\t\t\t\t\t\t\t\t\t\t: similar.Name\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\t\t\t\t\tcardCaption={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? `S${similar.ParentIndexNumber}:E${similar.IndexNumber} - ${similar.Name}`\n\t\t\t\t\t\t\t\t\t\t\t: similar.Type === BaseItemKind.Series\n\t\t\t\t\t\t\t\t\t\t\t\t? `${similar.ProductionYear} - ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsimilar.EndDate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? new Date(similar.EndDate).toLocaleString([], {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tyear: \"numeric\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"Present\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t}`\n\t\t\t\t\t\t\t\t\t\t\t\t: similar.ProductionYear\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tcardType={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.MusicAlbum ||\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Audio\n\t\t\t\t\t\t\t\t\t\t\t? \"square\"\n\t\t\t\t\t\t\t\t\t\t\t: \"portrait\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id, \"similarItem\"]}\n\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</CardScroller>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t);\n\t}\n\tif (item.isError || similarItems.isError) {\n\t\treturn <ErrorNotice />;\n\t}\n}\n\nexport default MusicAlbumTitlePage;\n"
  },
  {
    "path": "src/routes/_api/album/album.scss",
    "content": "$border-radius: 20px;\n.item-album {\n\twidth: 70vw;\n\tpadding-right: 2.2em;\n\t.item-info {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 1.2em;\n\t\t\t&-container {\n\t\t\tgrid-template-columns: 1fr 25%;\n\t\t\tdisplay: grid;\n\t\t\tgap: 3em;\n\t\t}\n\t\t\n\t\t&-album-info {\n\t\t\tgap: 1.2em;\n\t\t}\n\t\t&-buttons {\n\t\t\tdisplay: flex;\n\t\t\tgap: 1em;\n\t\t}\n\t\t&-disc-header{\n\t\t\tpadding: 1em 2em;\n\t\t\tbackground: hsla(0, 0, 100%, 10%);\n\t\t\tborder-radius: 100px;\n\t\t\tgap: 1em;\n\t\t\tmargin-top: 1em;\n\t\t}\n\t}\n}\n\n.item-info-sidebar {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1em;\n\tposition: fixed;\n\t// max-height: 100vh;\n\ttop: 0;\n\tright: 0;\n\twidth: 30vw;\n\tpadding: 4.4em;\n\tpadding-left: 2.2em;\n\t&-image {\n\t\twidth: 100%;\n\t\tobject-fit: cover;\n\t\tborder-radius: $border-radius-default;\n\t}\n\t&-artist {\n\t\tdisplay: flex;\n\t\tgap: 1em;\n\t\talign-items: center;\n\t\tpadding: 1em 0;\n\t\tcolor: rgb(255 255 255 / 0.7);\n\t\ttransition: $transition-time-default;\n\t\tcursor: pointer;\n\t\ttext-decoration: none;\n\t\t&:hover {\n\t\t\tcolor: rgb(255 255 255)\n\t\t}\n\t\t&-image {\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\tobject-fit: cover;\n\t\t\tz-index: 1;\n\t\t\tposition: relative;\n\t\t\t&-container {\n\t\t\t\twidth: 3em;\n\t\t\t\taspect-ratio: 1;\n\t\t\t\tborder-radius: 100%;\n\t\t\t\toverflow: hidden;\n\t\t\t\tposition: relative;\n\t\t\t\t.material-symbols-rounded {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0;\n\t\t\t\t\tright: 0;\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\theight: 100%;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t\tz-index: 0;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t&-container {\n\t\t\t// max-height: 20em;/\n\t\t\toverflow-y: auto;\n\t\t\toverflow-x: hidden;\n\t\t}\n\t}\n\t&-overview {\n\t\toverflow-y: auto;\n\t\tmax-height: 28vh;\n\t\tpadding-bottom: 2px;\n\t}\n}\n\n.item-detail-album {\n\t&-track {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: 4% 1fr 80% 1fr;\n\t\talign-items: center;\n\t\tjustify-items: center;\n\t\tgap: 0.5em;\n\t\twidth: 100%;\n\t\tpadding: 0.25em 0;\n\t\tbackground: rgb(0 0 0 / 0.4);\n\t\ttransition: background $transition-time-default;\n\t\tbackdrop-filter: blur(5px);\n\t\t&:first-child {\n\t\t\tborder-radius: $border-radius $border-radius 8px 8px;\n\t\t\tcursor: default !important;\n\t\t}\n\t\t&:last-child {\n\t\t\tborder-radius: 8px 8px $border-radius $border-radius;\n\t\t}\n\t\tborder-radius: 8px;\n\t\t&s {\n\t\t\tbackground: transparent !important;\n\t\t\tbox-shadow: none !important;\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tgap: 5px;\n\t\t}\n\t\t&:hover {\n\t\t\tbackground: rgb(0 0 0 / 0.5);\n\t\t\tcursor: pointer;\n\t\t}\n\t\t&.playing {\n\t\t\tcolor: $clr-accent-default;\n\t\t}\n\t}\n}\n$cardWidth: 22%;\n.item-album.item {\n\n\t.item-detail {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: 1fr 35%;\n\t\tjustify-items: center;\n\t\talign-items: start;\n\t\tgap: 3em;\n\t\t&-cast {\n\t\t\tmax-height: 50vh;\n\t\t\toverflow-y: auto;\n\t\t\toverflow-x: hidden;\n\t\t\tmask-image: linear-gradient(to top, transparent, black 1.2em);\n\t\t\t-webkit-mask-image: linear-gradient(to top, transparent, black 1.2em);\n\t\t\t&-title {\n\t\t\t\tposition: sticky;\n\t\t\t\ttop: 0;\n\t\t\t\tbackground: black;\n\t\t\t\tborder: 1px solid rgb(255 255 255 / 0.2);\n\t\t\t\tpadding: 0.35em;\n\t\t\t\tborder-radius: 10px;\n\t\t\t\tz-index: 1;\n\t\t\t}\n\t\t\t&-container {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\tgap: 1em;\n\t\t\t}\n\t\t\t&-grid {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgap: 1em;\n\t\t\t\tmargin-bottom: 1em;\n\t\t\t\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n\t\t\t}\n\t\t\t&-card {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: 6em 1fr;\n\t\t\t\tgap: 0.5em;\n\t\t\t\talign-items: center;\n\t\t\t\tcolor: white !important;\n\t\t\t\ttext-decoration: none;\n\t\t\t\tpadding: 0.5em;\n\t\t\t\tbackground: rgb(255 255 255 / 0);\n\t\t\t\ttransition: background $transition-time-default;\n\t\t\t\tborder-radius: $border-radius-default;\n\t\t\t\t&:hover {\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t}\n\t\t\t\t&-image,\n\t\t\t\t&-icon {\n\t\t\t\t\twidth: 6em;\n\t\t\t\t\theight: 6em;\n\t\t\t\t\tobject-fit: cover;\n\t\t\t\t\tborder-radius: 100%;\n\t\t\t\t\tbox-shadow: $shadow-card-image;\n\t\t\t\t}\n\t\t\t\t&-icon {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tfont-size: 1em;\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t\t.material-symbols-rounded {\n\t\t\t\t\t\tfont-size: 3em;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t&-link {\n\t\t\tcolor: white;\n\t\t\ttext-decoration-color: rgb(255 255 255 / 0.5);\n\t\t\t&:hover {\n\t\t\t\ttext-decoration-color: white;\n\t\t\t}\n\t\t}\n\t\t&-episodes-container {\n\t\t\tdisplay: grid;\n\t\t\tgrid-template-columns: repeat(4, minmax(0, 1fr));\n\t\t\tgap: 0.25em;\n\t\t\tmargin-top: 0.5em;\n\t\t\talign-items: start;\n\t\t\tjustify-items: center;\n\t\t}\n\t}\n}\n@media (max-width: 1320px) {\n\t.item-album .item-detail {\n\t\t&-cast {\n\t\t\t&-grid {\n\t\t\t\tgrid-template-columns: 1fr;\n\t\t\t}\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/routes/_api/artist/$id.tsx",
    "content": "import {\n\tBaseItemKind,\n\tItemFields,\n\tSortOrder,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport { getUserLibraryApi } from \"@jellyfin/sdk/lib/utils/api/user-library-api\";\nimport Box from \"@mui/material/Box\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport Divider from \"@mui/material/Divider\";\nimport Tab from \"@mui/material/Tab\";\nimport Tabs from \"@mui/material/Tabs\";\nimport Typography from \"@mui/material/Typography\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { AnimatePresence, motion, useScroll } from \"motion/react\";\nimport React, {\n\ttype ReactNode,\n\tuseEffect,\n\tuseLayoutEffect,\n\tuseRef,\n\tuseState,\n} from \"react\";\n\nimport LikeButton from \"@/components/buttons/likeButton\";\nimport { Card } from \"@/components/card/card\";\nimport ItemHeader from \"@/components/itemHeader\";\nimport { ArtistAlbum } from \"@/components/layouts/artist/artistAlbum\";\nimport MusicTrack from \"@/components/musicTrack\";\nimport { ErrorNotice } from \"@/components/notices/errorNotice/errorNotice\";\nimport ShowMoreText from \"@/components/showMoreText\";\nimport useParallax from \"@/utils/hooks/useParallax\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport \"./artist.scss\";\n\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport IconLink from \"@/components/iconLink\";\nimport { useCentralStore } from \"@/utils/store/central\";\n\n\ntype TabPanelProp = {\n\tchildren: ReactNode;\n\tindex: number;\n\tvalue: number;\n};\n\nfunction TabPanel(props: TabPanelProp) {\n\tconst { children, value, index, ...other } = props;\n\n\treturn (\n\t\t<div\n\t\t\trole=\"tabpanel\"\n\t\t\thidden={value !== index}\n\t\t\tid={`full-width-tabpanel-${index}`}\n\t\t\taria-labelledby={`full-width-tab-${index}`}\n\t\t\t{...other}\n\t\t\tstyle={{ marginTop: \"1em\" }}\n\t\t>\n\t\t\t{value === index && <Box>{children}</Box>}\n\t\t</div>\n\t);\n}\n\nexport const Route = createFileRoute(\"/_api/artist/$id\")({\n\tcomponent: ArtistTitlePage,\n});\n\nfunction ArtistTitlePage() {\n\tconst { id } = Route.useParams();\n\tconst api = Route.useRouteContext().api;\n\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst item = useQuery({\n\t\tqueryKey: [\"item\", id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getUserLibraryApi(api).getItem({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\titemId: id,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: !!user?.Id,\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst artistDiscography = useQuery({\n\t\tqueryKey: [\"item\", id, \"artist\", \"discography\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\talbumArtistIds: [id],\n\t\t\t\tsortBy: [\"PremiereDate\", \"ProductionYear\", \"SortName\"],\n\t\t\t\tsortOrder: [SortOrder.Descending],\n\t\t\t\trecursive: true,\n\t\t\t\tincludeItemTypes: [BaseItemKind.MusicAlbum],\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tfields: [ItemFields.Overview],\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tconst artistSongs = useQuery({\n\t\tqueryKey: [\"item\", id, \"artist\", \"songs\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tartistIds: [id],\n\t\t\t\tsortBy: [\"PremiereDate\", \"ProductionYear\", \"SortName\"],\n\t\t\t\tsortOrder: [SortOrder.Ascending],\n\t\t\t\trecursive: true,\n\t\t\t\tincludeItemTypes: [BaseItemKind.Audio],\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tfields: [ItemFields.Overview],\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tconst artistAppearances = useQuery({\n\t\tqueryKey: [\"item\", id, \"artist\", \"appearences\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tcontributingArtistIds: [id],\n\t\t\t\texcludeItemIds: [id],\n\t\t\t\tsortBy: [\"PremiereDate\", \"ProductionYear\", \"SortName\"],\n\t\t\t\tsortOrder: [SortOrder.Descending],\n\t\t\t\trecursive: true,\n\t\t\t\tincludeItemTypes: [BaseItemKind.MusicAlbum],\n\t\t\t\tuserId: user?.Id,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tconst artistTabs = [\n\t\t{ title: \"Discography\", data: artistDiscography },\n\t\t{ title: \"Songs\", data: artistSongs },\n\t\t{ title: \"Appearances\", data: artistAppearances },\n\t];\n\tconst [activeArtistTab, setActiveArtistTab] = useState(0);\n\n\tuseLayoutEffect(() => {\n\t\tif (\n\t\t\tartistDiscography.isSuccess &&\n\t\t\tartistSongs.isSuccess &&\n\t\t\tartistAppearances.isSuccess &&\n\t\t\tartistDiscography.data &&\n\t\t\tartistSongs.data &&\n\t\t\tartistAppearances.data\n\t\t) {\n\t\t\tif (artistDiscography.data.TotalRecordCount !== 0) {\n\t\t\t\tsetActiveArtistTab(0);\n\t\t\t} else if (artistSongs.data.TotalRecordCount !== 0) {\n\t\t\t\tsetActiveArtistTab(1);\n\t\t\t} else if (artistAppearances.data.TotalRecordCount !== 0) {\n\t\t\t\tsetActiveArtistTab(2);\n\t\t\t}\n\t\t}\n\t}, [\n\t\tartistDiscography.isSuccess,\n\t\tartistSongs.isSuccess,\n\t\tartistAppearances.isSuccess,\n\t]);\n\n\tconst [animationDirection, setAnimationDirection] = useState(\"forward\");\n\n\tconst { setBackdrop } = useBackdropStore();\n\tuseEffect(() => {\n\t\tif (item.isSuccess && item.data) {\n\t\t\tsetBackdrop(`${api?.basePath}/Items/${item.data.Id}/Images/Backdrop`);\n\t\t}\n\t}, [item.isSuccess]);\n\n\tconst scrollTargetRef = useRef<HTMLDivElement | null>(null);\n\n\tif (item.isPending) {\n\t\treturn (\n\t\t\t<Box\n\t\t\t\tsx={{\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\theight: \"100vh\",\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<CircularProgress />\n\t\t\t</Box>\n\t\t);\n\t}\n\tif (item.isSuccess && item.data) {\n\t\treturn (\n\t\t\t<motion.div\n\t\t\t\tkey={id}\n\t\t\t\tinitial={{\n\t\t\t\t\topacity: 0,\n\t\t\t\t}}\n\t\t\t\tanimate={{\n\t\t\t\t\topacity: 1,\n\t\t\t\t}}\n\t\t\t\ttransition={{\n\t\t\t\t\tduration: 0.25,\n\t\t\t\t\tease: \"easeInOut\",\n\t\t\t\t}}\n\t\t\t\tclassName=\"scrollY item item-artist\"\n\t\t\t>\n\t\t\t\t<div ref={scrollTargetRef} />\n\t\t\t\t<ItemHeader\n\t\t\t\t\titem={item.data}\n\t\t\t\t\tapi={api}\n\t\t\t\t\tscrollTargetRef={scrollTargetRef}\n\t\t\t\t>\n\t\t\t\t\t<div className=\"item-hero-buttons-container\">\n\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\titemName={item.data.Name}\n\t\t\t\t\t\t\titemId={item.data.Id}\n\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\tisFavorite={item.data.UserData?.IsFavorite}\n\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t</ItemHeader>\n\t\t\t\t<div className=\"item-detail\">\n\t\t\t\t\t<div style={{ width: \"100%\", overflow: \"hidden\" }}>\n\t\t\t\t\t\t<ShowMoreText\n\t\t\t\t\t\t\tcontent={item.data.Overview ?? \"\"}\n\t\t\t\t\t\t\tcollapsedLines={4}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\tgap: \"0.6em\",\n\t\t\t\t\t\t\t\talignSelf: \"end\",\n\t\t\t\t\t\t\t\tmarginTop: \"1em\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.data.ExternalUrls?.map((url) => (\n\t\t\t\t\t\t\t\t<IconLink\n\t\t\t\t\t\t\t\t\turl={url.Url ?? \"\"}\n\t\t\t\t\t\t\t\t\tname={url.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\tkey={url.Url}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{item.data.PremiereDate && (\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<Typography variant=\"h5\">Born</Typography>\n\t\t\t\t\t\t\t\t<Typography sx={{ opacity: 0.8 }}>\n\t\t\t\t\t\t\t\t\t{new Date(item.data.PremiereDate).toDateString()}\n\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{item.data.EndDate && (\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<Typography variant=\"h5\" mt={2}>\n\t\t\t\t\t\t\t\t\tDeath\n\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t<Typography sx={{ opacity: 0.8 }}>\n\t\t\t\t\t\t\t\t\t{new Date(item.data.EndDate).toDateString()}\n\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"item-detail-artist-container\">\n\t\t\t\t\t<div className=\"item-detail-artist-header\">\n\t\t\t\t\t\t<Tabs\n\t\t\t\t\t\t\tvariant=\"scrollable\"\n\t\t\t\t\t\t\tvalue={activeArtistTab}\n\t\t\t\t\t\t\tonChange={(_, newVal) => {\n\t\t\t\t\t\t\t\tif (newVal > activeArtistTab) {\n\t\t\t\t\t\t\t\t\tsetAnimationDirection(\"forward\");\n\t\t\t\t\t\t\t\t} else if (newVal < activeArtistTab) {\n\t\t\t\t\t\t\t\t\tsetAnimationDirection(\"backwards\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tsetActiveArtistTab(newVal);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{artistTabs.map((tab) => {\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<Tab\n\t\t\t\t\t\t\t\t\t\tkey={tab.title}\n\t\t\t\t\t\t\t\t\t\tlabel={tab.title}\n\t\t\t\t\t\t\t\t\t\tdisabled={tab.data.data?.TotalRecordCount === 0}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t</Tabs>\n\t\t\t\t\t\t<Divider />\n\t\t\t\t\t</div>\n\t\t\t\t\t<AnimatePresence>\n\t\t\t\t\t\t<TabPanel value={activeArtistTab} index={0}>\n\t\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\t\tkey={activeArtistTab}\n\t\t\t\t\t\t\t\tinitial={{\n\t\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t\t\ttransform:\n\t\t\t\t\t\t\t\t\t\tanimationDirection === \"forward\"\n\t\t\t\t\t\t\t\t\t\t\t? \"translate(30px)\"\n\t\t\t\t\t\t\t\t\t\t\t: \"translate(-30px)\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tanimate={{\n\t\t\t\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\t\t\t\ttransform: \"translate(0px)\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\ttransition={{\n\t\t\t\t\t\t\t\t\tduration: 0.2,\n\t\t\t\t\t\t\t\t\tease: \"anticipate\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{user &&\n\t\t\t\t\t\t\t\t\tartistDiscography.data?.Items?.map((tabitem) => {\n\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t<ArtistAlbum\n\t\t\t\t\t\t\t\t\t\t\t\tkey={tabitem.Id}\n\t\t\t\t\t\t\t\t\t\t\t\tuser={user}\n\t\t\t\t\t\t\t\t\t\t\t\talbum={tabitem}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t</motion.div>\n\t\t\t\t\t\t</TabPanel>\n\t\t\t\t\t\t<TabPanel value={activeArtistTab} index={1}>\n\t\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\t\tkey={activeArtistTab}\n\t\t\t\t\t\t\t\tinitial={{\n\t\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t\t\ttransform:\n\t\t\t\t\t\t\t\t\t\tanimationDirection === \"forward\"\n\t\t\t\t\t\t\t\t\t\t\t? \"translate(30px)\"\n\t\t\t\t\t\t\t\t\t\t\t: \"translate(-30px)\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tanimate={{\n\t\t\t\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\t\t\t\ttransform: \"translate(0px)\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\ttransition={{\n\t\t\t\t\t\t\t\t\tduration: 0.2,\n\t\t\t\t\t\t\t\t\tease: \"anticipate\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{artistSongs.isSuccess &&\n\t\t\t\t\t\t\t\t\tartistSongs.data?.Items?.map((tabitem) => {\n\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t<MusicTrack\n\t\t\t\t\t\t\t\t\t\t\t\titem={tabitem}\n\t\t\t\t\t\t\t\t\t\t\t\tkey={tabitem.Id}\n\t\t\t\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id, \"artist\", \"songs\"]}\n\t\t\t\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t</motion.div>\n\t\t\t\t\t\t</TabPanel>\n\t\t\t\t\t\t<TabPanel value={activeArtistTab} index={2}>\n\t\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\t\tkey={activeArtistTab}\n\t\t\t\t\t\t\t\tinitial={{\n\t\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t\t\ttransform:\n\t\t\t\t\t\t\t\t\t\tanimationDirection === \"forward\"\n\t\t\t\t\t\t\t\t\t\t\t? \"translate(30px)\"\n\t\t\t\t\t\t\t\t\t\t\t: \"translate(-30px)\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tanimate={{\n\t\t\t\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\t\t\t\ttransform: \"translate(0px)\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\ttransition={{\n\t\t\t\t\t\t\t\t\tduration: 0.2,\n\t\t\t\t\t\t\t\t\tease: \"anticipate\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tclassName=\"grid\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{artistAppearances.isSuccess &&\n\t\t\t\t\t\t\t\t\tartistAppearances.data?.Items?.map((tabitem) => {\n\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\t\t\t\tkey={tabitem.Id}\n\t\t\t\t\t\t\t\t\t\t\t\titem={tabitem}\n\t\t\t\t\t\t\t\t\t\t\t\tcardTitle={tabitem.Name}\n\t\t\t\t\t\t\t\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\t\t\t\t\t\t\t\tcardCaption={tabitem.AlbumArtist}\n\t\t\t\t\t\t\t\t\t\t\t\tcardType={\"square\"}\n\t\t\t\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id, \"artist\", \"appearences\"]}\n\t\t\t\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t</motion.div>\n\t\t\t\t\t\t</TabPanel>\n\t\t\t\t\t</AnimatePresence>\n\t\t\t\t</div>\n\t\t\t</motion.div>\n\t\t);\n\t}\n\tif (item.isError) {\n\t\treturn <ErrorNotice />;\n\t}\n}\n\nexport default ArtistTitlePage;\n"
  },
  {
    "path": "src/routes/_api/artist/artist.scss",
    "content": "$cardWidth: 22%;\n\n.item-artist.item {\n\tgap: 1em;\n\n\t.item-detail {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: 1fr 35%;\n\t\tjustify-items: center;\n\t\talign-items: start;\n\t\tgap: 3em;\n\n\t\t&-cast {\n\t\t\t&-container {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\tgap: 1em;\n\t\t\t}\n\t\t\t&-grid {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: repeat(5, minmax(0, 1fr));\n\t\t\t\tgap: 1em;\n\t\t\t\tmargin-bottom: 1em;\n\t\t\t}\n\t\t\t&-card {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: 6em 1fr;\n\t\t\t\tgap: 0.5em;\n\t\t\t\talign-items: center;\n\t\t\t\tcolor: white !important;\n\t\t\t\ttext-decoration: none;\n\t\t\t\tpadding: 0.5em;\n\t\t\t\tbackground: rgb(255 255 255 / 0);\n\t\t\t\ttransition: background $transition-time-default;\n\t\t\t\tborder-radius: $border-radius-default;\n\t\t\t\t&:hover {\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t}\n\t\t\t\t&-image,\n\t\t\t\t&-icon {\n\t\t\t\t\twidth: 6em;\n\t\t\t\t\theight: 6em;\n\t\t\t\t\tobject-fit: cover;\n\t\t\t\t\tborder-radius: 100%;\n\t\t\t\t\tbox-shadow: $shadow-card-image;\n\t\t\t\t}\n\t\t\t\t&-icon {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tfont-size: 1em;\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t\t.material-symbols-rounded {\n\t\t\t\t\t\tfont-size: 3em;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t&-link {\n\t\t\tcolor: white;\n\t\t\ttext-decoration-color: rgb(255 255 255 / 0.5);\n\t\t\t&:hover {\n\t\t\t\ttext-decoration-color: white;\n\t\t\t}\n\t\t}\n\t}\n}\n.item-detail-artist {\n\t&-container {\n\t\tposition: relative;\n\t\twidth: 100%;\n\t}\n\t&-header {\n\t\twidth: 100%;\n\t}\n\t&-cards {\n\t\tdisplay: grid;\n\t\talign-items: start;\n\t}\n}\n\n.grid {\n\tdisplay: grid;\n\tgrid-template-columns: repeat(7, minmax(0, 1fr));\n\n\talign-items: start;\n}\n"
  },
  {
    "path": "src/routes/_api/boxset/$id.tsx",
    "content": "import {\n\tBaseItemKind,\n\tItemFields,\n\tLocationType,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport { getLibraryApi } from \"@jellyfin/sdk/lib/utils/api/library-api\";\nimport { getUserLibraryApi } from \"@jellyfin/sdk/lib/utils/api/user-library-api\";\nimport Box from \"@mui/material/Box\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { motion } from \"motion/react\";\nimport React, { useLayoutEffect, useRef } from \"react\";\nimport LikeButton from \"@/components/buttons/likeButton\";\nimport MarkPlayedButton from \"@/components/buttons/markPlayedButton\";\nimport PlayButton from \"@/components/buttons/playButton\";\nimport { Card } from \"@/components/card/card\";\nimport CardScroller from \"@/components/cardScroller/cardScroller\";\nimport ItemHeader from \"@/components/itemHeader\";\nimport { ErrorNotice } from \"@/components/notices/errorNotice/errorNotice\";\nimport ShowMoreText from \"@/components/showMoreText\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport \"./boxset.scss\";\n\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport IconLink from \"@/components/iconLink\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useCentralStore } from \"@/utils/store/central\";\n\nexport const Route = createFileRoute(\"/_api/boxset/$id\")({\n\tcomponent: BoxSetTitlePage,\n});\n\nfunction BoxSetTitlePage() {\n\tconst { id } = Route.useParams();\n\tconst api = Route.useRouteContext().api;\n\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst item = useQuery({\n\t\tqueryKey: [\"item\", id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getUserLibraryApi(api).getItem({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\titemId: id,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: !!user?.Id,\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst collectionItems = useQuery({\n\t\tqueryKey: [\"item\", id, \"collection\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tparentId: id,\n\t\t\t\tfields: [ItemFields.SeasonUserData, ItemFields.Overview],\n\t\t\t\texcludeLocationTypes: [LocationType.Virtual],\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst similarItems = useQuery({\n\t\tqueryKey: [\"item\", \"similarItem\", id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getLibraryApi(api).getSimilarItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\titemId: id,\n\t\t\t\tlimit: 16,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst setBackdrop = useBackdropStore((s) => s.setBackdrop);\n\n\tuseLayoutEffect(() => {\n\t\tif (api && item.isSuccess) {\n\t\t\tif (item.data?.BackdropImageTags?.length) {\n\t\t\t\tsetBackdrop(\n\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\titem.data.Id ?? \"\",\n\t\t\t\t\t\t\"Backdrop\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttag: item.data.BackdropImageTags[0],\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tsetBackdrop(\"\");\n\t\t\t}\n\t\t}\n\t}, [item.isSuccess]);\n\n\tconst scrollTargetRef = useRef<HTMLDivElement | null>(null);\n\n\tif (item.isPending || similarItems.isPending) {\n\t\treturn (\n\t\t\t<Box\n\t\t\t\tsx={{\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\theight: \"100vh\",\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<CircularProgress />\n\t\t\t</Box>\n\t\t);\n\t}\n\tif (item.isSuccess && item.data && similarItems.isSuccess) {\n\t\treturn (\n\t\t\t<motion.div\n\t\t\t\tkey={id}\n\t\t\t\tinitial={{\n\t\t\t\t\topacity: 0,\n\t\t\t\t}}\n\t\t\t\tanimate={{\n\t\t\t\t\topacity: 1,\n\t\t\t\t}}\n\t\t\t\ttransition={{\n\t\t\t\t\tduration: 0.25,\n\t\t\t\t\tease: \"easeInOut\",\n\t\t\t\t}}\n\t\t\t\tclassName=\"scrollY item item-boxset padded-top\"\n\t\t\t>\n\t\t\t\t<div ref={scrollTargetRef} />\n\t\t\t\t<ItemHeader\n\t\t\t\t\titem={item.data}\n\t\t\t\t\tapi={api}\n\t\t\t\t\tscrollTargetRef={scrollTargetRef}\n\t\t\t\t>\n\t\t\t\t\t<div className=\"item-hero-buttons-container flex flex-row\">\n\t\t\t\t\t\t<div className=\"flex flex-row fullWidth\">\n\t\t\t\t\t\t\t<PlayButton\n\t\t\t\t\t\t\t\titem={item.data}\n\t\t\t\t\t\t\t\titemType={item.data.Type ?? \"BoxSet\"}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\tbuttonProps={{\n\t\t\t\t\t\t\t\t\tfullWidth: true,\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"flex flex-row\" style={{ gap: \"1em\" }}>\n\t\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\t\titemName={item.data.Name}\n\t\t\t\t\t\t\t\titemId={item.data.Id}\n\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\tisFavorite={item.data.UserData?.IsFavorite}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<MarkPlayedButton\n\t\t\t\t\t\t\t\titemName={item.data.Name}\n\t\t\t\t\t\t\t\titemId={item.data.Id}\n\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\tisPlayed={item.data.UserData?.Played}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</ItemHeader>\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"item-detail\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tmarginBottom: \"2em\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<ShowMoreText content={item.data.Overview ?? \"\"} collapsedLines={4} />\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\tgap: \"0.6em\",\n\t\t\t\t\t\t\t\talignSelf: \"end\",\n\t\t\t\t\t\t\t\tmarginTop: \"1em\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.data.ExternalUrls?.map((url) => (\n\t\t\t\t\t\t\t\t<IconLink\n\t\t\t\t\t\t\t\t\turl={url.Url ?? \"\"}\n\t\t\t\t\t\t\t\t\tname={url.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\tkey={url.Url}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t{(collectionItems.data?.TotalRecordCount ?? 0) > 0 && (\n\t\t\t\t\t<CardScroller title=\"\" displayCards={7} disableDecoration>\n\t\t\t\t\t\t{collectionItems.data?.Items?.map((similar) => {\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\tkey={similar.Id}\n\t\t\t\t\t\t\t\t\titem={similar}\n\t\t\t\t\t\t\t\t\tseriesId={similar.SeriesId}\n\t\t\t\t\t\t\t\t\tcardTitle={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? similar.SeriesName\n\t\t\t\t\t\t\t\t\t\t\t: similar.Name\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\t\t\t\t\tcardCaption={similar.ProductionYear}\n\t\t\t\t\t\t\t\t\tcardType={\"portrait\"}\n\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</CardScroller>\n\t\t\t\t)}\n\t\t\t\t{similarItems.data?.TotalRecordCount && (\n\t\t\t\t\t<CardScroller\n\t\t\t\t\t\ttitle=\"You might also like\"\n\t\t\t\t\t\tdisplayCards={7}\n\t\t\t\t\t\tdisableDecoration\n\t\t\t\t\t>\n\t\t\t\t\t\t{similarItems.data?.Items?.map((similar) => {\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\tkey={similar.Id}\n\t\t\t\t\t\t\t\t\titem={similar}\n\t\t\t\t\t\t\t\t\tseriesId={similar.SeriesId}\n\t\t\t\t\t\t\t\t\tcardTitle={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? similar.SeriesName\n\t\t\t\t\t\t\t\t\t\t\t: similar.Name\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\t\t\t\t\tcardCaption={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? `S${similar.ParentIndexNumber}:E${similar.IndexNumber} - ${similar.Name}`\n\t\t\t\t\t\t\t\t\t\t\t: similar.Type === BaseItemKind.Series\n\t\t\t\t\t\t\t\t\t\t\t\t? `${similar.ProductionYear} - ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsimilar.EndDate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? new Date(similar.EndDate).toLocaleString([], {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tyear: \"numeric\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"Present\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t}`\n\t\t\t\t\t\t\t\t\t\t\t\t: similar.ProductionYear\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tcardType={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.MusicAlbum ||\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Audio\n\t\t\t\t\t\t\t\t\t\t\t? \"square\"\n\t\t\t\t\t\t\t\t\t\t\t: \"portrait\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id, \"similarItem\"]}\n\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</CardScroller>\n\t\t\t\t)}\n\t\t\t</motion.div>\n\t\t);\n\t}\n\tif (item.isError || similarItems.isError) {\n\t\treturn <ErrorNotice />;\n\t}\n}\n\nexport default BoxSetTitlePage;\n"
  },
  {
    "path": "src/routes/_api/boxset/boxset.scss",
    "content": "$cardWidth: 18%;\t\n.item-boxset.item {\n\tgap: 1em;\n\n\t.item-detail {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: 1fr 35%;\n\t\tjustify-items: center;\n\t\talign-items: start;\n\t\tgap: 3em;\n\n\t\t&-cast {\n\t\t\t&-container {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\tgap: 1em;\n\t\t\t}\n\t\t\t&-grid {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: repeat(5, minmax(0, 1fr));\n\t\t\t\tgap: 1em;\n\t\t\t\tmargin-bottom: 1em;\n\t\t\t}\n\t\t\t&-card {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: 6em 1fr;\n\t\t\t\tgap: 0.5em;\n\t\t\t\talign-items: center;\n\t\t\t\tcolor: white !important;\n\t\t\t\ttext-decoration: none;\n\t\t\t\tpadding: 0.5em;\n\t\t\t\tbackground: rgb(255 255 255 / 0);\n\t\t\t\ttransition: background $transition-time-default;\n\t\t\t\tborder-radius: $border-radius-default;\n\t\t\t\t&:hover {\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t}\n\t\t\t\t&-image,\n\t\t\t\t&-icon {\n\t\t\t\t\twidth: 6em;\n\t\t\t\t\theight: 6em;\n\t\t\t\t\tobject-fit: cover;\n\t\t\t\t\tborder-radius: 100%;\n\t\t\t\t\tbox-shadow: $shadow-card-image;\n\t\t\t\t}\n\t\t\t\t&-icon {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tfont-size: 1em;\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t\t.material-symbols-rounded {\n\t\t\t\t\t\tfont-size: 3em;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t&-link {\n\t\t\tcolor: white;\n\t\t\ttext-decoration-color: rgb(255 255 255 / 0.5);\n\t\t\t&:hover {\n\t\t\t\ttext-decoration-color: white;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/routes/_api/episode/$id.tsx",
    "content": "import {\n\tBaseItemKind,\n\tMediaStreamType,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { getUserLibraryApi } from \"@jellyfin/sdk/lib/utils/api/user-library-api\";\nimport Box from \"@mui/material/Box\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport TextField from \"@mui/material/TextField\";\nimport Typography from \"@mui/material/Typography\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { motion } from \"motion/react\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport LikeButton from \"@/components/buttons/likeButton\";\nimport MarkPlayedButton from \"@/components/buttons/markPlayedButton\";\nimport PlayButton from \"@/components/buttons/playButton\";\nimport TrailerButton from \"@/components/buttons/trailerButton\";\n// import useParallax from \"@/utils/hooks/useParallax\";\nimport { ErrorNotice } from \"@/components/notices/errorNotice/errorNotice\";\nimport ShowMoreText from \"@/components/showMoreText\";\nimport { getTypeIcon } from \"@/components/utils/iconsCollection\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport \"./episode.scss\";\n\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport { createFileRoute, Link } from \"@tanstack/react-router\";\nimport { Card } from \"@/components/card/card\";\nimport CardScroller from \"@/components/cardScroller/cardScroller\";\nimport IconLink from \"@/components/iconLink\";\nimport ItemHeader from \"@/components/itemHeader\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport type MediaQualityInfo from \"@/utils/types/mediaQualityInfo\";\n\nexport const Route = createFileRoute(\"/_api/episode/$id\")({\n\tcomponent: EpisodeTitlePage,\n});\n\nfunction EpisodeTitlePage() {\n\tconst { id } = Route.useParams();\n\tconst api = Route.useRouteContext().api;\n\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst item = useQuery({\n\t\tqueryKey: [\"item\", id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getUserLibraryApi(api).getItem({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\titemId: id,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: !!user?.Id,\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst upcomingEpisodes = useQuery({\n\t\tqueryKey: [\"item\", id, \"episode\", \"upcomingEpisodes\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tparentId: item.data?.ParentId ?? \"\",\n\t\t\t\tstartIndex: item.data?.IndexNumber ?? 0,\n\t\t\t\texcludeLocationTypes: [\"Virtual\"],\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: item.isSuccess && item.data?.Type === BaseItemKind.Episode,\n\t\tnetworkMode: \"always\",\n\t});\n\n\tconst setBackdrop = useBackdropStore((state) => state.setBackdrop);\n\n\tconst [selectedVideoTrack, setSelectedVideoTrack] = useState(0);\n\tconst [selectedAudioTrack, setSelectedAudioTrack] = useState(0);\n\tconst [selectedSubtitleTrack, setSelectedSubtitleTrack] = useState(0);\n\n\tconst videoTracks = useMemo(() => {\n\t\tconst result = item.data?.MediaStreams?.filter((source) => {\n\t\t\tif (source.Type === MediaStreamType.Video) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t\tsetSelectedVideoTrack(result?.find((track) => track.IsDefault)?.Index ?? 0);\n\t\treturn result ?? [];\n\t}, [item.data?.Id]);\n\tconst audioTracks = useMemo(() => {\n\t\tconst result = item.data?.MediaStreams?.filter((source) => {\n\t\t\tif (source.Type === MediaStreamType.Audio) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t\tsetSelectedAudioTrack(\n\t\t\titem.data?.MediaSources?.[0].DefaultAudioStreamIndex ?? 0,\n\t\t);\n\t\treturn result ?? [];\n\t}, [item.data?.Id]);\n\tconst subtitleTracks = useMemo(() => {\n\t\tconst result = item.data?.MediaStreams?.filter((source) => {\n\t\t\tif (source.Type === MediaStreamType.Subtitle) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t\tsetSelectedSubtitleTrack(\n\t\t\tresult?.find((track) => track.IsDefault)?.Index ?? -1,\n\t\t);\n\t\treturn result ?? [];\n\t}, [item.data?.Id]);\n\n\tconst directors = useMemo(() => {\n\t\treturn item.data?.People?.filter((itm) => itm.Type === \"Director\") ?? [];\n\t}, [item.data?.Id]);\n\tconst writers = useMemo(() => {\n\t\treturn item.data?.People?.filter((itm) => itm.Type === \"Writer\") ?? [];\n\t}, [item.data?.Id]);\n\tconst actors = useMemo(() => {\n\t\treturn item.data?.People?.filter((itm) => itm.Type === \"Actor\") ?? [];\n\t}, [item.data?.Id]);\n\tconst producers = useMemo(() => {\n\t\treturn item.data?.People?.filter((itm) => itm.Type === \"Producer\") ?? [];\n\t}, [item.data?.Id]);\n\n\tconst mediaQualityInfo = useMemo<MediaQualityInfo>(() => {\n\t\tconst checkAtmos = audioTracks.filter((audio) =>\n\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"atmos\"),\n\t\t);\n\t\tconst checkDolbyVision = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"dv\") ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"dolby vision\") ||\n\t\t\t\t!!video.VideoDoViTitle ||\n\t\t\t\tvideo.VideoRangeType === \"DOVI\",\n\t\t);\n\t\tconst checkDD = audioTracks.filter(\n\t\t\t(audio) =>\n\t\t\t\t(audio.DisplayTitle?.toLocaleLowerCase().includes(\"dd\") &&\n\t\t\t\t\t!audio.DisplayTitle?.toLocaleLowerCase().includes(\"ddp\")) ||\n\t\t\t\t(audio.DisplayTitle?.toLocaleLowerCase().includes(\"dolby digital\") &&\n\t\t\t\t\t!audio.DisplayTitle?.toLocaleLowerCase().includes(\"dolby digital+\")),\n\t\t);\n\t\tconst checkDDP = audioTracks.filter(\n\t\t\t(audio) =>\n\t\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"ddp\") ||\n\t\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"dolby digital+\"),\n\t\t);\n\t\tconst checkDts = audioTracks.filter(\n\t\t\t(audio) =>\n\t\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"dts\") &&\n\t\t\t\t!audio.DisplayTitle?.toLocaleLowerCase().includes(\"dts-hd ma\") &&\n\t\t\t\t!audio.DisplayTitle?.toLocaleLowerCase().includes(\"dts-hd.ma\"),\n\t\t);\n\t\tconst checkDtsHDMA = audioTracks.filter(\n\t\t\t(audio) =>\n\t\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"dts-hd ma\") ||\n\t\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"dts-hd.ma\"),\n\t\t);\n\t\tconst checkUHD = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"4k\") ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"2160p\") ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"uhd\"),\n\t\t);\n\t\tconst checkHD = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\t(video.DisplayTitle?.toLocaleLowerCase().includes(\"hd\") &&\n\t\t\t\t\t!video.DisplayTitle?.toLocaleLowerCase().includes(\"hdr\")) ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"1080p\") ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"fhd\"),\n\t\t);\n\t\tconst checkSD = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\t(video.DisplayTitle?.toLocaleLowerCase().includes(\"sd\") &&\n\t\t\t\t\t!video.DisplayTitle?.toLocaleLowerCase().includes(\"sdr\")) ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"720p\"),\n\t\t);\n\t\tconst checkSDR = videoTracks.filter(\n\t\t\t(video) => video.VideoRangeType === \"SDR\",\n\t\t);\n\t\tconst checkHDR = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\tvideo.VideoRange === \"HDR\" ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"dv\"),\n\t\t);\n\t\tconst checkHDR10 = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\tvideo.VideoRangeType === \"HDR10\" &&\n\t\t\t\t!video.DisplayTitle?.toLocaleLowerCase().includes(\"hdr10+\"),\n\t\t);\n\t\tconst checkHDR10Plus = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\tvideo.VideoRangeType === \"HDR10Plus\" ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"hdr10+\"),\n\t\t);\n\t\tconst checkTrueHD = audioTracks.filter((audio) =>\n\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"truehd\"),\n\t\t);\n\t\tconst checkIMAX = videoTracks.filter((video) =>\n\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"imax\"),\n\t\t);\n\t\treturn {\n\t\t\tisAtmos: checkAtmos.length > 0,\n\t\t\tisDolbyVision: checkDolbyVision.length > 0,\n\t\t\tisDts: checkDts.length > 0,\n\t\t\tisDtsHDMA: checkDtsHDMA.length > 0,\n\t\t\tisDD: checkDD.length > 0,\n\t\t\tisDDP: checkDDP.length > 0,\n\t\t\tisUHD: checkUHD.length > 0,\n\t\t\tisHD: checkHD.length > 0,\n\t\t\tisSD: checkSD.length > 0,\n\t\t\tisSDR: checkSDR.length > 0,\n\t\t\tisHDR: checkHDR.length > 0,\n\t\t\tisHDR10: checkHDR10.length > 0,\n\t\t\tisHDR10Plus: checkHDR10Plus.length > 0,\n\t\t\tisTrueHD: checkTrueHD.length > 0,\n\t\t\tisIMAX: checkIMAX.length > 0,\n\t\t};\n\t}, [item.data?.Id]);\n\n\tconst lastBackdropRef = useRef<string | undefined>(undefined);\n\tuseEffect(() => {\n\t\tif (!api || !item.isSuccess || !item.data) return;\n\t\tconst tag = item.data.ParentBackdropImageTags?.[0];\n\t\t// Prefer blurhash if available; fallback to URL\n\t\tconst hash = item.data.ImageBlurHashes?.Backdrop?.[tag ?? \"\"];\n\n\t\tif (hash && lastBackdropRef.current !== hash) {\n\t\t\tlastBackdropRef.current = hash;\n\t\t\tsetBackdrop(hash);\n\t\t}\n\t}, [api, item.isSuccess, item.data, setBackdrop]);\n\n\tconst containerRef = useRef<HTMLDivElement | null>(null);\n\n\tif (item.isPending) {\n\t\treturn (\n\t\t\t<Box\n\t\t\t\tsx={{\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\theight: \"100vh\",\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<CircularProgress />\n\t\t\t</Box>\n\t\t);\n\t}\n\tif (item.isSuccess && item.data) {\n\t\treturn (\n\t\t\t<motion.div\n\t\t\t\tkey={id}\n\t\t\t\tinitial={{\n\t\t\t\t\topacity: 0,\n\t\t\t\t}}\n\t\t\t\tanimate={{\n\t\t\t\t\topacity: 1,\n\t\t\t\t}}\n\t\t\t\ttransition={{\n\t\t\t\t\tduration: 0.25,\n\t\t\t\t\tease: \"easeInOut\",\n\t\t\t\t}}\n\t\t\t\tclassName=\"scrollY padded-top flex flex-column item item-episode\"\n\t\t\t\tref={containerRef}\n\t\t\t>\n\t\t\t\t<ItemHeader\n\t\t\t\t\titem={item.data}\n\t\t\t\t\tapi={api}\n\t\t\t\t\tmediaQualityInfo={mediaQualityInfo}\n\t\t\t\t\tscrollTargetRef={containerRef}\n\t\t\t\t>\n\t\t\t\t\t<div className=\"item-hero-buttons-container\">\n\t\t\t\t\t\t<div className=\"flex flex-row\">\n\t\t\t\t\t\t\t<PlayButton\n\t\t\t\t\t\t\t\titem={item.data}\n\t\t\t\t\t\t\t\titemType=\"Episode\"\n\t\t\t\t\t\t\t\tcurrentVideoTrack={selectedVideoTrack ?? 0}\n\t\t\t\t\t\t\t\tcurrentAudioTrack={selectedAudioTrack ?? 0}\n\t\t\t\t\t\t\t\tcurrentSubTrack={selectedSubtitleTrack ?? \"nosub\"}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"flex flex-row\" style={{ gap: \"1em\" }}>\n\t\t\t\t\t\t\t{item.data.RemoteTrailers && (\n\t\t\t\t\t\t\t\t<TrailerButton\n\t\t\t\t\t\t\t\t\ttrailerItem={item.data.RemoteTrailers}\n\t\t\t\t\t\t\t\t\tdisabled={item.data.RemoteTrailers?.length === 0}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\t\titemName={item.data.Name}\n\t\t\t\t\t\t\t\titemId={item.data.Id}\n\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\tisFavorite={item.data.UserData?.IsFavorite}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<MarkPlayedButton\n\t\t\t\t\t\t\t\titemName={item.data.Name}\n\t\t\t\t\t\t\t\titemId={item.data.Id}\n\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\tisPlayed={item.data.UserData?.Played}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</ItemHeader>\n\t\t\t\t<div className=\"item-detail\">\n\t\t\t\t\t<div className=\"fullWidth\">\n\t\t\t\t\t\t<ShowMoreText\n\t\t\t\t\t\t\tcontent={item.data.Overview ?? \"\"}\n\t\t\t\t\t\t\tcollapsedLines={4}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"fullWidth\">\n\t\t\t\t\t\t{videoTracks.length > 0 && (\n\t\t\t\t\t\t\t<TextField\n\t\t\t\t\t\t\t\tlabel=\"Video\"\n\t\t\t\t\t\t\t\tselect\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\t\tmarginBottom: \"1em\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tvalue={selectedVideoTrack}\n\t\t\t\t\t\t\t\tvariant=\"filled\"\n\t\t\t\t\t\t\t\tonChange={(e) => setSelectedVideoTrack(Number(e.target.value))}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{videoTracks.map((track) => (\n\t\t\t\t\t\t\t\t\t<MenuItem key={track.Index} value={track.Index}>\n\t\t\t\t\t\t\t\t\t\t{track.DisplayTitle}\n\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</TextField>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{audioTracks.length > 0 && (\n\t\t\t\t\t\t\t<TextField\n\t\t\t\t\t\t\t\tlabel=\"Audio\"\n\t\t\t\t\t\t\t\tselect\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\t\tmarginBottom: \"1em\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tvalue={selectedAudioTrack}\n\t\t\t\t\t\t\t\tvariant=\"filled\"\n\t\t\t\t\t\t\t\tonChange={(e) => setSelectedAudioTrack(Number(e.target.value))}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{audioTracks.map((track) => (\n\t\t\t\t\t\t\t\t\t<MenuItem key={track.Index} value={track.Index}>\n\t\t\t\t\t\t\t\t\t\t{track.DisplayTitle}\n\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</TextField>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{subtitleTracks.length > 0 && (\n\t\t\t\t\t\t\t<TextField\n\t\t\t\t\t\t\t\tlabel=\"Subtitle\"\n\t\t\t\t\t\t\t\tselect\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tvalue={selectedSubtitleTrack}\n\t\t\t\t\t\t\t\tvariant=\"filled\"\n\t\t\t\t\t\t\t\tonChange={(e) =>\n\t\t\t\t\t\t\t\t\tsetSelectedSubtitleTrack(Number(e.target.value))\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<MenuItem key={-1} value={-1}>\n\t\t\t\t\t\t\t\t\tNo Subtitle\n\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t{subtitleTracks.map((track) => (\n\t\t\t\t\t\t\t\t\t<MenuItem key={track.Index} value={track.Index}>\n\t\t\t\t\t\t\t\t\t\t{track.DisplayTitle}\n\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</TextField>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\tgap: \"0.6em\",\n\t\t\t\t\t\t\t\talignSelf: \"end\",\n\t\t\t\t\t\t\t\tmarginTop: \"1em\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.data.ExternalUrls?.map((url) => (\n\t\t\t\t\t\t\t\t<IconLink\n\t\t\t\t\t\t\t\t\tkey={url.Url}\n\t\t\t\t\t\t\t\t\turl={url.Url ?? \"\"}\n\t\t\t\t\t\t\t\t\tname={url.Name ?? \"\"}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t{(item.data.People?.length ?? 0) > 0 && (\n\t\t\t\t\t<div className=\"item-detail-cast\">\n\t\t\t\t\t\t<Typography variant=\"h5\" mb={2}>\n\t\t\t\t\t\t\tCast & Crew\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t{actors.length > 0 && (\n\t\t\t\t\t\t\t<div className=\"item-detail-cast-container\">\n\t\t\t\t\t\t\t\t<Typography variant=\"h6\">Actors</Typography>\n\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-grid\">\n\t\t\t\t\t\t\t\t\t{actors.map((actor) => (\n\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card\"\n\t\t\t\t\t\t\t\t\t\t\tkey={actor.Id}\n\t\t\t\t\t\t\t\t\t\t\tto=\"/person/$id\"\n\t\t\t\t\t\t\t\t\t\t\tparams={{\n\t\t\t\t\t\t\t\t\t\t\t\tid: actor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{actor.PrimaryImageTag ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\talt={actor.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Person\")}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-text\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Role}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{writers.length > 0 && (\n\t\t\t\t\t\t\t<div className=\"item-detail-cast-container\">\n\t\t\t\t\t\t\t\t<Typography variant=\"h6\">Writers</Typography>\n\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-grid\">\n\t\t\t\t\t\t\t\t\t{writers.map((actor) => (\n\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card\"\n\t\t\t\t\t\t\t\t\t\t\tkey={actor.Id}\n\t\t\t\t\t\t\t\t\t\t\tto=\"/person/$id\"\n\t\t\t\t\t\t\t\t\t\t\tparams={{\n\t\t\t\t\t\t\t\t\t\t\t\tid: actor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{actor.PrimaryImageTag ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\talt={actor.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Person\")}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-text\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Role}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{directors.length > 0 && (\n\t\t\t\t\t\t\t<div className=\"item-detail-cast-container\">\n\t\t\t\t\t\t\t\t<Typography variant=\"h6\">Directors</Typography>\n\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-grid\">\n\t\t\t\t\t\t\t\t\t{directors.map((actor) => (\n\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card\"\n\t\t\t\t\t\t\t\t\t\t\tkey={actor.Id}\n\t\t\t\t\t\t\t\t\t\t\tto=\"/person/$id\"\n\t\t\t\t\t\t\t\t\t\t\tparams={{\n\t\t\t\t\t\t\t\t\t\t\t\tid: actor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{actor.PrimaryImageTag ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\talt={actor.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Person\")}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-text\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Role}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{producers.length > 0 && (\n\t\t\t\t\t\t\t<div className=\"item-detail-cast-container\">\n\t\t\t\t\t\t\t\t<Typography variant=\"h6\">Producers</Typography>\n\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-grid\">\n\t\t\t\t\t\t\t\t\t{producers.map((actor) => (\n\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card\"\n\t\t\t\t\t\t\t\t\t\t\tkey={actor.Id}\n\t\t\t\t\t\t\t\t\t\t\tto=\"/person/$id\"\n\t\t\t\t\t\t\t\t\t\t\tparams={{\n\t\t\t\t\t\t\t\t\t\t\t\tid: actor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{actor.PrimaryImageTag ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\talt={actor.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Person\")}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-text\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Role}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\n\t\t\t\t{upcomingEpisodes.isSuccess &&\n\t\t\t\t\t(upcomingEpisodes.data?.Items?.length ?? 0) > 0 && (\n\t\t\t\t\t\t<CardScroller\n\t\t\t\t\t\t\ttitle=\"Upcoming Episodes\"\n\t\t\t\t\t\t\tdisplayCards={4}\n\t\t\t\t\t\t\tdisableDecoration\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{upcomingEpisodes.data?.Items?.map((episode) => {\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\t\tkey={episode.Id}\n\t\t\t\t\t\t\t\t\t\titem={episode}\n\t\t\t\t\t\t\t\t\t\tcardTitle={episode.SeriesName}\n\t\t\t\t\t\t\t\t\t\timageType=\"Primary\"\n\t\t\t\t\t\t\t\t\t\tcardCaption={`S${episode.ParentIndexNumber}:E${episode.IndexNumber} - ${episode.Name}`}\n\t\t\t\t\t\t\t\t\t\tcardType=\"thumb\"\n\t\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id, \"episode\", \"upcomingEpisodes\"]}\n\t\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t</CardScroller>\n\t\t\t\t\t)}\n\t\t\t</motion.div>\n\t\t);\n\t}\n\tif (item.isError) {\n\t\treturn <ErrorNotice />;\n\t}\n}\n\nexport default EpisodeTitlePage;\n"
  },
  {
    "path": "src/routes/_api/episode/episode.scss",
    "content": "$cardWidth: 30%;\n.item-episode.item {\n\tgap: 1em;\n\t\n\t.item-detail {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: 1fr 35%;\n\t\tjustify-items: center;\n\t\talign-items: start;\n\t\tgap: 3em;\n\t\t\n\t\t&-cast {\n\t\t\t&-container {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\tgap: 1em;\n\t\t\t}\n\t\t\t&-grid {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: repeat(5, minmax(0, 1fr));\n\t\t\t\tgap: 1em;\n\t\t\t\tmargin-bottom: 1em;\n\t\t\t}\n\t\t\t&-card {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: 6em 1fr;\n\t\t\t\tgap: 0.5em;\n\t\t\t\talign-items: center;\n\t\t\t\tcolor: white !important;\n\t\t\t\ttext-decoration: none;\n\t\t\t\tpadding: 0.5em;\n\t\t\t\tbackground: rgb(255 255 255 / 0);\n\t\t\t\ttransition: background $transition-time-default;\n\t\t\t\tborder-radius: $border-radius-default;\n\t\t\t\t&:hover {\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t}\n\t\t\t\t&-image,\n\t\t\t\t&-icon {\n\t\t\t\t\twidth: 6em;\n\t\t\t\t\theight: 6em;\n\t\t\t\t\tobject-fit: cover;\n\t\t\t\t\tborder-radius: 100%;\n\t\t\t\t\tbox-shadow: $shadow-card-image;\n\t\t\t\t}\n\t\t\t\t&-icon {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tfont-size: 1em;\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t\t.material-symbols-rounded {\n\t\t\t\t\t\tfont-size: 3em;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t&-link {\n\t\t\tcolor: white;\n\t\t\ttext-decoration-color: rgb(255 255 255 / 0.5);\n\t\t\t&:hover {\n\t\t\t\ttext-decoration-color: white;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/routes/_api/favorite/favorite.scss",
    "content": "@use \"@/styles/variables.scss\" as *;\n\n.favorite-page {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n    width: 100%;\n    overflow-y: auto;\n    padding-bottom: 2em;\n\n    .favorite-header {\n        padding: 2em 2em 0 2em;\n        display: flex;\n        flex-direction: column;\n        gap: 0.5em;\n        position: relative;\n        z-index: 2;\n\n        .header-content {\n            display: flex;\n            align-items: flex-end;\n            justify-content: space-between;\n            \n            h1 {\n                font-size: 2.2rem;\n                font-weight: 600;\n                margin: 0;\n                color: rgba(255, 255, 255, 0.95);\n                letter-spacing: -0.02em;\n            }\n        }\n\n        .MuiTabs-root {\n            min-height: 40px;\n            margin-left: -12px;\n            margin-top: 1em;\n            \n            .MuiTabs-indicator {\n                display: none; // Hide default indicator\n            }\n\n            .MuiTabs-scroller {\n                overflow: visible !important; // Allow shadows/glows to show\n            }\n\n            .MuiTab-root {\n                text-transform: none;\n                font-size: 0.9rem;\n                font-weight: 500;\n                color: rgba(255, 255, 255, 0.6);\n                padding: 8px 20px;\n                min-height: 36px;\n                min-width: fit-content;\n                border-radius: 100px;\n                margin-right: 8px;\n                transition: all $transition-time-fast ease;\n                background-color: transparent;\n                border: 1px solid rgba(255, 255, 255, 0.1);\n\n                &:first-of-type {\n                    margin-left: 12px;\n                }\n\n                &:hover {\n                    color: rgba(255, 255, 255, 0.9);\n                    background-color: rgba(255, 255, 255, 0.05);\n                    border-color: rgba(255, 255, 255, 0.2);\n                }\n\n                &.Mui-selected {\n                    color: #fff;\n                    font-weight: 600;\n                    background-color: $clr-accent-default;\n                    border-color: $clr-accent-default;\n                    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);\n                }\n            }\n        }\n    }\n\n    .favorite-content {\n        padding: 0.5em 0;\n        flex: 1;\n\n        // For Overview (Scrollers)\n        .overview-section {\n            padding: 0 2em; // Align with header\n            display: flex;\n            flex-direction: column;\n            gap: 1.5em;\n\n            // Override card scroller buttons to fit the new padding\n            .card-scroller-button {\n                width: 2em !important;\n                \n                &.left {\n                    left: -2em !important;\n                    border-radius: 4px !important;\n                }\n                \n                &.right {\n                    right: -2em !important;\n                    border-radius: 4px !important;\n                }\n                \n                .material-symbols-rounded {\n                    font-size: 2em;\n                }\n            }\n        }\n\n\n        // For Grid Views\n        .grid-section {\n            padding: 1em 2em;\n            display: grid;\n            grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));\n            gap: 1em;\n            \n            // Adjust for different card types if needed\n            &.grid-landscape {\n                grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));\n            }\n        }\n\n        .empty-state {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            justify-content: center;\n            height: 300px;\n            color: rgba(255, 255, 255, 0.4);\n            \n            h3 {\n                margin-top: 1em;\n                font-weight: 400;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/routes/_api/favorite/index.tsx",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport React, { type SyntheticEvent, useEffect, useState } from \"react\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport \"./favorite.scss\";\n\nimport {\n\ttype BaseItemDto,\n\tBaseItemKind,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport { Fade, Tab, Tabs, Typography } from \"@mui/material\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { useShallow } from \"zustand/react/shallow\";\nimport { Card } from \"@/components/card/card\";\nimport CardScroller from \"@/components/cardScroller/cardScroller\";\nimport { useCentralStore } from \"@/utils/store/central\";\n\nexport const Route = createFileRoute(\"/_api/favorite/\")({\n\tcomponent: FavoritePage,\n});\n\nfunction FavoritePage() {\n\tconst api = Route.useRouteContext().api;\n\tconst [setBackdrop] = useBackdropStore(\n\t\tuseShallow((state) => [state.setBackdrop]),\n\t);\n\tconst [tabValue, setTabValue] = useState(\"overview\");\n\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst movies = useQuery({\n\t\tqueryKey: [\"favorite\", \"movies\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tisFavorite: true,\n\t\t\t\tincludeItemTypes: [BaseItemKind.Movie],\n\t\t\t\tenableImages: true,\n\t\t\t\trecursive: true,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tconst series = useQuery({\n\t\tqueryKey: [\"favorite\", \"series\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tisFavorite: true,\n\t\t\t\tincludeItemTypes: [BaseItemKind.Series],\n\t\t\t\trecursive: true,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tconst musicAlbum = useQuery({\n\t\tqueryKey: [\"favorite\", \"musicAlbum\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tisFavorite: true,\n\t\t\t\tincludeItemTypes: [BaseItemKind.MusicAlbum],\n\t\t\t\trecursive: true,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tconst audio = useQuery({\n\t\tqueryKey: [\"favorite\", \"audio\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tisFavorite: true,\n\t\t\t\tincludeItemTypes: [BaseItemKind.Audio],\n\t\t\t\trecursive: true,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tconst book = useQuery({\n\t\tqueryKey: [\"favorite\", \"book\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tisFavorite: true,\n\t\t\t\tincludeItemTypes: [BaseItemKind.Book],\n\t\t\t\trecursive: true,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tconst musicArtists = useQuery({\n\t\tqueryKey: [\"favorite\", \"musicArtist\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tisFavorite: true,\n\t\t\t\tincludeItemTypes: [BaseItemKind.MusicArtist],\n\t\t\t\trecursive: true,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tconst person = useQuery({\n\t\tqueryKey: [\"favorite\", \"person\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tisFavorite: true,\n\t\t\t\tincludeItemTypes: [BaseItemKind.Person],\n\t\t\t\trecursive: true,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tuseEffect(() => {\n\t\t// Set a backdrop from one of the favorites if available?\n\t\t// For now, clear it or maybe pick random from movies\n\t\tif (movies.data?.Items && movies.data.Items.length > 0) {\n\t\t\tconst randomMovie =\n\t\t\t\tmovies.data.Items[Math.floor(Math.random() * movies.data.Items.length)];\n\t\t\tif (randomMovie.BackdropImageTags?.[0]) {\n\t\t\t\tsetBackdrop(\n\t\t\t\t\trandomMovie.ImageBlurHashes?.Backdrop?.[\n\t\t\t\t\t\trandomMovie.BackdropImageTags[0]\n\t\t\t\t\t] || \"\",\n\t\t\t\t);\n\t\t\t}\n\t\t} else {\n\t\t\tsetBackdrop(\"\");\n\t\t}\n\t}, [movies.data, setBackdrop]);\n\n\tconst handleTabChange = (_event: SyntheticEvent, newValue: string) => {\n\t\tsetTabValue(newValue);\n\t};\n\n\tconst sections = [\n\t\t{\n\t\t\tid: \"movies\",\n\t\t\ttitle: \"Movies\",\n\t\t\tquery: movies,\n\t\t\trenderCard: (item: BaseItemDto) => (\n\t\t\t\t<Card\n\t\t\t\t\tkey={item.Id}\n\t\t\t\t\titem={item}\n\t\t\t\t\tcardTitle={item.Name}\n\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\tcardCaption={item.ProductionYear}\n\t\t\t\t\tcardType=\"portrait\"\n\t\t\t\t\tqueryKey={[\"favorite\", \"movies\"]}\n\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t/>\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tid: \"series\",\n\t\t\ttitle: \"TV Shows\",\n\t\t\tquery: series,\n\t\t\trenderCard: (item: BaseItemDto) => (\n\t\t\t\t<Card\n\t\t\t\t\tkey={item.Id}\n\t\t\t\t\titem={item}\n\t\t\t\t\tcardTitle={item.Name}\n\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\tcardCaption={`${item.ProductionYear} - ${item.EndDate ? new Date(item.EndDate).getFullYear() : \"Present\"}`}\n\t\t\t\t\tcardType=\"portrait\"\n\t\t\t\t\tqueryKey={[\"favorite\", \"series\"]}\n\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t/>\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tid: \"albums\",\n\t\t\ttitle: \"Albums\",\n\t\t\tquery: musicAlbum,\n\t\t\trenderCard: (item: BaseItemDto) => (\n\t\t\t\t<Card\n\t\t\t\t\tkey={item.Id}\n\t\t\t\t\titem={item}\n\t\t\t\t\tseriesId={item.SeriesId}\n\t\t\t\t\tcardTitle={item.Name}\n\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\tcardCaption={item.AlbumArtist}\n\t\t\t\t\tcardType={\"square\"}\n\t\t\t\t\tqueryKey={[\"favorite\", \"musicAlbum\"]}\n\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t/>\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tid: \"artists\",\n\t\t\ttitle: \"Artists\",\n\t\t\tquery: musicArtists,\n\t\t\trenderCard: (item: BaseItemDto) => (\n\t\t\t\t<Card\n\t\t\t\t\tkey={item.Id}\n\t\t\t\t\titem={item}\n\t\t\t\t\tcardTitle={item.Name}\n\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\tdisableOverlay\n\t\t\t\t\tcardType={\"square\"}\n\t\t\t\t\tqueryKey={[\"favorite\", \"musicArtist\"]}\n\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t/>\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tid: \"audio\",\n\t\t\ttitle: \"Songs\",\n\t\t\tquery: audio,\n\t\t\trenderCard: (item: BaseItemDto) => (\n\t\t\t\t<Card\n\t\t\t\t\tkey={item.Id}\n\t\t\t\t\titem={item}\n\t\t\t\t\tcardTitle={item.Name}\n\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\tcardCaption={item.ProductionYear}\n\t\t\t\t\tcardType=\"square\"\n\t\t\t\t\tqueryKey={[\"favorite\", \"audio\"]}\n\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t/>\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tid: \"books\",\n\t\t\ttitle: \"Books\",\n\t\t\tquery: book,\n\t\t\trenderCard: (item: BaseItemDto) => (\n\t\t\t\t<Card\n\t\t\t\t\tkey={item.Id}\n\t\t\t\t\titem={item}\n\t\t\t\t\tcardTitle={item.Name}\n\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\tcardCaption={item.ProductionYear}\n\t\t\t\t\tdisableOverlay\n\t\t\t\t\tcardType={\"portrait\"}\n\t\t\t\t\tqueryKey={[\"favorite\", \"book\"]}\n\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t/>\n\t\t\t),\n\t\t},\n\t\t{\n\t\t\tid: \"person\",\n\t\t\ttitle: \"People\",\n\t\t\tquery: person,\n\t\t\trenderCard: (item: BaseItemDto) => (\n\t\t\t\t<Card\n\t\t\t\t\tkey={item.Id}\n\t\t\t\t\titem={item}\n\t\t\t\t\tcardTitle={item.Name}\n\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\tdisableOverlay\n\t\t\t\t\tcardType={\"square\"}\n\t\t\t\t\tqueryKey={[\"favorite\", \"person\"]}\n\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t/>\n\t\t\t),\n\t\t},\n\t];\n\n\tconst availableSections = sections.filter(\n\t\t(s) => s.query.isSuccess && (s.query.data?.Items?.length ?? 0) > 0,\n\t);\n\n\treturn (\n\t\t<div className=\"favorite-page scrollY\">\n\t\t\t<div className=\"favorite-header\">\n\t\t\t\t<div className=\"header-content\">\n\t\t\t\t\t<Typography variant=\"h1\" component=\"h1\">\n\t\t\t\t\t\tYour Favorites\n\t\t\t\t\t</Typography>\n\t\t\t\t</div>\n\t\t\t\t<Tabs\n\t\t\t\t\tvalue={tabValue}\n\t\t\t\t\tonChange={handleTabChange}\n\t\t\t\t\tvariant=\"scrollable\"\n\t\t\t\t\tscrollButtons=\"auto\"\n\t\t\t\t\taria-label=\"favorite sections\"\n\t\t\t\t>\n\t\t\t\t\t<Tab label=\"Overview\" value=\"overview\" />\n\t\t\t\t\t{availableSections.map((section) => (\n\t\t\t\t\t\t<Tab key={section.id} label={section.title} value={section.id} />\n\t\t\t\t\t))}\n\t\t\t\t</Tabs>\n\t\t\t</div>\n\n\t\t\t<div className=\"favorite-content\">\n\t\t\t\t{tabValue === \"overview\" && (\n\t\t\t\t\t<Fade in={tabValue === \"overview\"}>\n\t\t\t\t\t\t<div className=\"overview-section\">\n\t\t\t\t\t\t\t{availableSections.map((section) => (\n\t\t\t\t\t\t\t\t<CardScroller\n\t\t\t\t\t\t\t\t\tkey={section.id}\n\t\t\t\t\t\t\t\t\ttitle={section.title}\n\t\t\t\t\t\t\t\t\tdisplayCards={7}\n\t\t\t\t\t\t\t\t\tdisableDecoration\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{section.query.data?.Items?.map(section.renderCard)}\n\t\t\t\t\t\t\t\t</CardScroller>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t{availableSections.length === 0 && (\n\t\t\t\t\t\t\t\t<div className=\"empty-state\">\n\t\t\t\t\t\t\t\t\t<Typography variant=\"h5\">No favorites yet</Typography>\n\t\t\t\t\t\t\t\t\t<Typography variant=\"body1\">\n\t\t\t\t\t\t\t\t\t\tMark items as favorite to see them here.\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</Fade>\n\t\t\t\t)}\n\n\t\t\t\t{availableSections.map(\n\t\t\t\t\t(section) =>\n\t\t\t\t\t\ttabValue === section.id && (\n\t\t\t\t\t\t\t<Fade key={section.id} in={tabValue === section.id}>\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tclassName={`grid-section ${[\"movies\", \"series\", \"books\"].includes(section.id) ? \"grid-landscape\" : \"\"}`}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{section.query.data?.Items?.map(section.renderCard)}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</Fade>\n\t\t\t\t\t\t),\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n\nexport default FavoritePage;\n"
  },
  {
    "path": "src/routes/_api/home/home.scss",
    "content": "\n.hero-carousel {\n\tposition: relative;\n\toverflow: visible;\n\tisolation: isolate;\n\theight: 100%;\n\twidth: 100%;\n\n\t&-slide {\n\t\tposition: relative;\n\t\theight: 100%;\n\t\twidth: 100%;\n\t\toverflow: hidden;\n\t\tmargin: 0;\n\t\tborder: none;\n\t\tborder-radius: 20px;\n\t\tgrid-area: main;\n\t\tborder: 1.2px solid rgb(255 255 255 / 0.1);\n\t\tbox-shadow: 0 0 20px rgba(0,0,0,0.2);\n\t}\n\n\t&-background {\n\t\t&-container {\n\t\t\tposition: absolute;\n\t\t\tz-index: 0;\n\t\t\ttop: 0;\n\t\t\tleft: 0;\n\t\t\tright: 0;\n\t\t\tbottom: 0;\n\t\t\theight: 100%;\n\t\t\t\n\t\t\t// Gradient overlay to make text readable\n\t\t\t&::after {\n\t\t\t\tcontent: \"\";\n\t\t\t\tposition: absolute;\n\t\t\t\tinset: 0;\n\t\t\t\tbackground: linear-gradient(\n\t\t\t\t\tto top right,\n\t\t\t\t\trgba(0, 0, 0, 0.9) 0%,\n\t\t\t\t\trgba(0, 0, 0, 0.4) 50%,\n\t\t\t\t\ttransparent 100%\n\t\t\t\t);\n\t\t\t\tz-index: 2;\n\t\t\t}\n\t\t\t\n\t\t}\n\n\t\t&-image {\n\t\t\tposition: absolute;\n\t\t\tbackground-size: cover;\n\t\t\tz-index: 1;\n\t\t\tright: 0;\n\t\t\ttop: 0;\n\t\t\tbottom: 0;\n\t\t\tleft: 0;\n\t\t\tbackground-position: center;\n\t\t\tobject-fit: cover;\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\ttransition: opacity 0.5s;\n\t\t}\n\n\t\t&-icon-container {\n\t\t\tposition: absolute;\n\t\t\tbackground: url(\"/src/assets/herobg.png\"), rgb(0 0 0 /0.3);\n\t\t\tz-index: 0;\n\t\t\ttop: 0;\n\t\t\tleft: 0;\n\t\t\tright: 0;\n\t\t\tbottom: 0;\n\t\t\tbackground-blend-mode: overlay;\n\t\t\theight: 100%;\n\n\t\t\t.material-symbols-rounded {\n\t\t\t\tposition: absolute;\n\t\t\t\tfont-size: 15em !important;\n\t\t\t\tleft: 50%;\n\t\t\t\ttop: 50%;\n\t\t\t\ttransform: translate(-50%, -50%);\n\t\t\t\tfont-variation-settings: \"FILL\" 0, \"wght\" 300, \"GRAD\" 50, \"opsz\" 60;\n\t\t\t\topacity: 0.5;\n\t\t\t}\n\t\t}\n\t}\n\n\t&-text {\n\t\tdisplay: -webkit-box;\n\t\toverflow: hidden;\n\t\tmargin-bottom: 0.5em;\n\t\ttext-shadow: 0 0 10px rgba(0,0,0,0.5), 0 0 20px rgba(0,0,0,0.3);\n\n\t\t&-logo {\n\t\t\tmax-width: 25vw;\n\t\t\tmax-height: 20vh;\n\t\t\tobject-fit: contain;\n\t\t\tobject-position: left bottom;\n\t\t\tfilter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));\n\t\t}\n\t}\n\n\t&-overview {\n\t\toverflow: hidden;\n\t\tmargin-bottom: 0.5em;\n\t\ttext-shadow: 0 1px 2px rgba(0,0,0,0.8);\n\t\twhite-space: nowrap;\n\t\ttext-overflow: ellipsis;\n\t\tdisplay: block;\n\t\tfont-size: 1.1rem;\n\t\topacity: 0.9;\n\t\tmax-width: 800px;\n\t}\n\n\t&-detail {\n\t\tposition: absolute;\n\t\tz-index: 3;\n\t\tdisplay: flex;\n\t\tgap: 1.5em;\n\t\theight: 100%;\n\t\twidth: 100%;\n\t\tmax-width: 1400px;\n\t\tpadding: 4rem;\n\t\tpadding-bottom: 5rem;\n\t\tpadding-left: 5rem;\n\t\tflex-direction: column;\n\t\tjustify-content: flex-end;\n\t\talign-items: flex-start;\n\t\tleft: 0;\n\t\tbottom: 0;\n\t}\n\t\n\t&-info {\n\t\ttext-shadow: 0 1px 2px rgba(0,0,0,0.8);\n\t}\n}\n\n.backdropContainer {\n\tbackground: linear-gradient(\n\t45deg,\n\t\t$clr-background-default,\n\t\t$clr-background-light\n\t);\n}\n\n@media (max-width: 1140px) {\n\t.carousel {\n\t\theight: 60vh !important;\n\t}\n\t.hero-carousel {\n\t\t&-detail {\n\t\t\twidth: 100%;\n\t\t\tbackground: linear-gradient(to top, rgba(0,0,0,0.9), transparent);\n\t\t}\n\t\t\n\t\t&-background-container::after {\n\t\t\tbackground: linear-gradient(to top, rgba(0,0,0,0.8), transparent);\n\t\t}\n\n\t\t&-text {\n\t\t\t&-logo {\n\t\t\t\tmax-width: 60vw;\n\t\t\t\tmax-height: 10vh;\n\t\t\t}\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/routes/_api/home/index.tsx",
    "content": "import { useSuspenseQuery } from \"@tanstack/react-query\";\nimport React, { useCallback, useMemo } from \"react\";\n\nimport \"./home.scss\";\n\nimport type { Api } from \"@jellyfin/sdk\";\nimport {\n\ttype BaseItemDto,\n\tBaseItemKind,\n\tItemFields,\n\ttype UserDto,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport { getTvShowsApi } from \"@jellyfin/sdk/lib/utils/api/tv-shows-api\";\nimport { getUserViewsApi } from \"@jellyfin/sdk/lib/utils/api/user-views-api\";\nimport Typography from \"@mui/material/Typography\";\nimport { createFileRoute, useNavigate } from \"@tanstack/react-router\";\nimport { ErrorBoundary } from \"react-error-boundary\";\nimport { useShallow } from \"zustand/shallow\";\n// Custom Components\nimport { Card } from \"@/components/card/card\";\nimport CardScroller from \"@/components/cardScroller/cardScroller\";\nimport Carousel from \"@/components/carousel\";\nimport CircularPageLoadingAnimation from \"@/components/circularPageLoadingAnimation\";\nimport { LatestMediaSection } from \"@/components/layouts/homeSection/latestMediaSection\";\nimport { ErrorNotice } from \"@/components/notices/errorNotice/errorNotice\";\nimport { getLatestItemsQueryOptions } from \"@/utils/queries/items\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport { useCentralStore } from \"@/utils/store/central\";\n\nexport const Route = createFileRoute(\"/_api/home/\")({\n\tcomponent: Home,\n\tpendingComponent: () => <CircularPageLoadingAnimation />,\n\tloader: ({ context: { api, user, queryClient } }) => {\n\t\tif (!api || !user?.Id) return null;\n\t\tqueryClient.ensureQueryData(getLatestItemsQueryOptions(api, user?.Id));\n\t},\n});\n\nfunction Home() {\n\tconst api = useApiInContext((s) => s.api);\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tif (!api || !user?.Id) {\n\t\treturn <CircularPageLoadingAnimation />;\n\t}\n\n\treturn <HomeContent api={api} user={user} />;\n}\n\nfunction HomeContent({ api, user }: { api: Api; user: UserDto }) {\n\tconst libraries = useSuspenseQuery({\n\t\tqueryKey: [\"libraries\"],\n\t\tqueryFn: async () => {\n\t\t\tconst libs = await getUserViewsApi(api).getUserViews({\n\t\t\t\tuserId: user.Id,\n\t\t\t});\n\t\t\treturn libs.data;\n\t\t},\n\t\tnetworkMode: \"always\",\n\t});\n\n\tconst latestMedia = useSuspenseQuery(\n\t\tgetLatestItemsQueryOptions(api, user.Id),\n\t);\n\n\tconst resumeItemsVideo = useSuspenseQuery({\n\t\tqueryKey: [\"home\", \"resume\", \"video\"],\n\t\tqueryFn: async () => {\n\t\t\tconst resumeItems = await getItemsApi(api).getResumeItems({\n\t\t\t\tuserId: user.Id,\n\t\t\t\tlimit: 10,\n\t\t\t\tmediaTypes: [\"Video\"],\n\t\t\t\tenableUserData: true,\n\t\t\t\tfields: [ItemFields.MediaStreams, ItemFields.MediaSources],\n\t\t\t});\n\t\t\treturn resumeItems.data;\n\t\t},\n\t});\n\n\tconst resumeItemsAudio = useSuspenseQuery({\n\t\tqueryKey: [\"home\", \"resume\", \"audio\"],\n\t\tqueryFn: async () => {\n\t\t\tconst resumeItems = await getItemsApi(api).getResumeItems({\n\t\t\t\tuserId: user.Id,\n\t\t\t\tlimit: 10,\n\t\t\t\tmediaTypes: [\"Audio\"],\n\t\t\t\tenableUserData: true,\n\t\t\t\tfields: [ItemFields.MediaStreams, ItemFields.MediaSources],\n\t\t\t});\n\t\t\treturn resumeItems.data;\n\t\t},\n\t});\n\n\tconst upNextItems = useSuspenseQuery({\n\t\tqueryKey: [\"home\", \"upNext\"],\n\t\tqueryFn: async () => {\n\t\t\tconst upNext = await getTvShowsApi(api).getNextUp({\n\t\t\t\tuserId: user.Id,\n\t\t\t\tfields: [\n\t\t\t\t\tItemFields.PrimaryImageAspectRatio,\n\t\t\t\t\tItemFields.MediaStreams,\n\t\t\t\t\tItemFields.MediaSources,\n\t\t\t\t\tItemFields.ParentId,\n\t\t\t\t],\n\t\t\t\tenableResumable: false,\n\t\t\t\tenableRewatching: false,\n\t\t\t\tdisableFirstEpisode: false,\n\t\t\t\tlimit: 10,\n\t\t\t});\n\t\t\treturn upNext.data;\n\t\t},\n\t});\n\n\tconst excludeTypes = [\"boxsets\", \"playlists\", \"livetv\", \"channels\"];\n\tconst latestMediaLibs = useMemo(() => {\n\t\tif (libraries.data?.Items?.length) {\n\t\t\treturn libraries.data?.Items?.reduce(\n\t\t\t\t(filteredItems: BaseItemDto[], currentItem) => {\n\t\t\t\t\tif (currentItem.Type && !excludeTypes.includes(currentItem.Type)) {\n\t\t\t\t\t\tfilteredItems.push(currentItem);\n\t\t\t\t\t}\n\t\t\t\t\treturn filteredItems;\n\t\t\t\t},\n\t\t\t\t[],\n\t\t\t);\n\t\t}\n\t}, [libraries.data?.Items?.length]);\n\n\tconst navigate = useNavigate();\n\n\tconst setBackdrop = useBackdropStore(useShallow((s) => s.setBackdrop));\n\n\tconst setBackdropForItem = useCallback(\n\t\t(item: BaseItemDto) => {\n\t\t\tif (item?.ParentBackdropImageTags) {\n\t\t\t\tsetBackdrop(\n\t\t\t\t\titem.ImageBlurHashes?.Backdrop?.[item.ParentBackdropImageTags[0]],\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tif (!item?.BackdropImageTags) {\n\t\t\t\t\tsetBackdrop(undefined);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsetBackdrop(\n\t\t\t\t\titem.ImageBlurHashes?.Backdrop?.[item.BackdropImageTags[0]],\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t\t[setBackdrop],\n\t);\n\n\tconst handleOnChangeCarousel = useCallback(\n\t\t(now: number) => {\n\t\t\tif (api && latestMedia.isSuccess && (latestMedia.data?.length ?? 0) > 0) {\n\t\t\t\tconst currentItem = latestMedia.data?.[now];\n\t\t\t\tif (!currentItem) return;\n\n\t\t\t\t// Extract this logic to a separate function\n\t\t\t\tsetBackdropForItem(currentItem);\n\t\t\t}\n\t\t},\n\t\t// Only depend on what you need\n\t\t[latestMedia.isSuccess, api, setBackdrop],\n\t);\n\n\treturn (\n\t\t<main\n\t\t\tclassName=\"scrollY home padded-top\"\n\t\t\tstyle={{\n\t\t\t\tflexGrow: 1,\n\t\t\t\tposition: \"relative\",\n\t\t\t}}\n\t\t>\n\t\t\t<ErrorBoundary FallbackComponent={ErrorNotice}>\n\t\t\t\t{latestMedia.data?.length && (\n\t\t\t\t\t<Carousel\n\t\t\t\t\t\tcontent={latestMedia.data}\n\t\t\t\t\t\tonChange={handleOnChangeCarousel}\n\t\t\t\t\t/>\n\t\t\t\t)}\n\t\t\t</ErrorBoundary>\n\n\t\t\t<ErrorBoundary\n\t\t\t\tfallback={\n\t\t\t\t\t<div className=\"error\">\n\t\t\t\t\t\t<Typography>Error with Carousel</Typography>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t>\n\t\t\t\t<CardScroller displayCards={4} title=\"Libraries\">\n\t\t\t\t\t{libraries.data?.Items?.map((item) => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\tkey={item.Id}\n\t\t\t\t\t\t\t\titem={item}\n\t\t\t\t\t\t\t\tcardTitle={item.Name}\n\t\t\t\t\t\t\t\timageType=\"Primary\"\n\t\t\t\t\t\t\t\tcardType=\"thumb\"\n\t\t\t\t\t\t\t\tdisableOverlay\n\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\tif (item.Id) {\n\t\t\t\t\t\t\t\t\t\tnavigate({ to: \"/library/$id\", params: { id: item.Id } });\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\toverrideIcon={item.CollectionType}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t\t</CardScroller>\n\t\t\t</ErrorBoundary>\n\t\t\t<ErrorBoundary\n\t\t\t\tfallback={\n\t\t\t\t\t<div className=\"error\">\n\t\t\t\t\t\t<Typography>Error with Libraries</Typography>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t>\n\t\t\t\t{upNextItems.isSuccess && upNextItems.data?.TotalRecordCount && (\n\t\t\t\t\t<CardScroller displayCards={4} title=\"Up Next\">\n\t\t\t\t\t\t{upNextItems.data?.Items?.map((item) => {\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\tkey={item.Id}\n\t\t\t\t\t\t\t\t\titem={item}\n\t\t\t\t\t\t\t\t\tcardTitle={\n\t\t\t\t\t\t\t\t\t\titem.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? item.SeriesName\n\t\t\t\t\t\t\t\t\t\t\t: item.Name\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\timageType={\n\t\t\t\t\t\t\t\t\t\titem.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? \"Primary\"\n\t\t\t\t\t\t\t\t\t\t\t: item.ImageTags?.Thumb\n\t\t\t\t\t\t\t\t\t\t\t\t? \"Thumb\"\n\t\t\t\t\t\t\t\t\t\t\t\t: \"Backdrop\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tcardCaption={\n\t\t\t\t\t\t\t\t\t\titem.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? item.ParentIndexNumber === 0\n\t\t\t\t\t\t\t\t\t\t\t\t? `${item.SeasonName} - ${item.Name}`\n\t\t\t\t\t\t\t\t\t\t\t\t: item.IndexNumberEnd\n\t\t\t\t\t\t\t\t\t\t\t\t\t? `${item.IndexNumber}-${item.IndexNumberEnd}. ${item.Name}`\n\t\t\t\t\t\t\t\t\t\t\t\t\t: `${item.IndexNumber}. ${item.Name}`\n\t\t\t\t\t\t\t\t\t\t\t: item.Type === BaseItemKind.Series\n\t\t\t\t\t\t\t\t\t\t\t\t? `${item.ProductionYear} - ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem.EndDate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? new Date(item.EndDate).toLocaleString([], {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tyear: \"numeric\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"Present\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t}`\n\t\t\t\t\t\t\t\t\t\t\t\t: item.ProductionYear\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tcardType=\"thumb\"\n\t\t\t\t\t\t\t\t\tqueryKey={[\"home\", \"upNext\"]}\n\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</CardScroller>\n\t\t\t\t)}\n\t\t\t</ErrorBoundary>\n\t\t\t<ErrorBoundary\n\t\t\t\tfallback={\n\t\t\t\t\t<div className=\"error\">\n\t\t\t\t\t\t<Typography>Error with resumeItemsVideo</Typography>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t>\n\t\t\t\t{resumeItemsVideo.isSuccess &&\n\t\t\t\t!resumeItemsVideo.data?.TotalRecordCount ? (\n\t\t\t\t\t<></>\n\t\t\t\t) : (\n\t\t\t\t\t<CardScroller displayCards={4} title=\"Continue Watching\">\n\t\t\t\t\t\t{resumeItemsVideo.data?.Items?.map((item) => {\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\tkey={item.Id}\n\t\t\t\t\t\t\t\t\titem={item}\n\t\t\t\t\t\t\t\t\tcardTitle={\n\t\t\t\t\t\t\t\t\t\titem.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? item.SeriesName\n\t\t\t\t\t\t\t\t\t\t\t: item.Name\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\timageType={\n\t\t\t\t\t\t\t\t\t\titem.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? \"Primary\"\n\t\t\t\t\t\t\t\t\t\t\t: item.ImageTags?.Thumb\n\t\t\t\t\t\t\t\t\t\t\t\t? \"Thumb\"\n\t\t\t\t\t\t\t\t\t\t\t\t: \"Backdrop\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tcardCaption={\n\t\t\t\t\t\t\t\t\t\titem.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`\n\t\t\t\t\t\t\t\t\t\t\t: item.Type === BaseItemKind.Series\n\t\t\t\t\t\t\t\t\t\t\t\t? `${item.ProductionYear} - ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem.EndDate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? new Date(item.EndDate).toLocaleString([], {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tyear: \"numeric\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"Present\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t}`\n\t\t\t\t\t\t\t\t\t\t\t\t: item.ProductionYear\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tcardType=\"thumb\"\n\t\t\t\t\t\t\t\t\tqueryKey={[\"home\", \"resume\", \"video\"]}\n\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</CardScroller>\n\t\t\t\t)}\n\t\t\t</ErrorBoundary>\n\t\t\t<ErrorBoundary\n\t\t\t\tfallback={\n\t\t\t\t\t<div className=\"error\">\n\t\t\t\t\t\t<Typography>Error with resumeItemsAudio</Typography>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t>\n\t\t\t\t{resumeItemsAudio.isSuccess &&\n\t\t\t\t!resumeItemsAudio.data?.TotalRecordCount ? (\n\t\t\t\t\t<></>\n\t\t\t\t) : (\n\t\t\t\t\t<CardScroller displayCards={4} title=\"Continue Listening\">\n\t\t\t\t\t\t{resumeItemsAudio.data?.Items?.map((item) => {\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\tkey={item.Id}\n\t\t\t\t\t\t\t\t\titem={item}\n\t\t\t\t\t\t\t\t\tcardTitle={item.Name}\n\t\t\t\t\t\t\t\t\timageType=\"Primary\"\n\t\t\t\t\t\t\t\t\tcardCaption={item.ProductionYear}\n\t\t\t\t\t\t\t\t\tcardType=\"thumb\"\n\t\t\t\t\t\t\t\t\tqueryKey={[\"home\", \"resume\", \"audio\"]}\n\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</CardScroller>\n\t\t\t\t)}\n\t\t\t</ErrorBoundary>\n\t\t\t<ErrorBoundary\n\t\t\t\tfallback={\n\t\t\t\t\t<div className=\"error\">\n\t\t\t\t\t\t<Typography>Error with LatestMediaSections</Typography>\n\t\t\t\t\t</div>\n\t\t\t\t}\n\t\t\t>\n\t\t\t\t{latestMediaLibs?.map((lib) => {\n\t\t\t\t\treturn <LatestMediaSection key={lib.Id} latestMediaLib={lib} />;\n\t\t\t\t})}\n\t\t\t</ErrorBoundary>\n\t\t</main>\n\t);\n}\n\nexport default Home;\n"
  },
  {
    "path": "src/routes/_api/item/$id.tsx",
    "content": "import {\n\tBaseItemKind,\n\tMediaStreamType,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { getLibraryApi } from \"@jellyfin/sdk/lib/utils/api/library-api\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport TextField from \"@mui/material/TextField\";\nimport Typography from \"@mui/material/Typography\";\nimport { useQuery, useSuspenseQuery } from \"@tanstack/react-query\";\nimport { motion } from \"motion/react\";\nimport React, { useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { Card } from \"@/components/card/card\";\nimport CardScroller from \"@/components/cardScroller/cardScroller\";\nimport ItemHeader from \"@/components/itemHeader\";\n\nimport \"./item.scss\";\n\nimport { createFileRoute, Link } from \"@tanstack/react-router\";\nimport { useShallow } from \"zustand/shallow\";\nimport LikeButton from \"@/components/buttons/likeButton\";\nimport MarkPlayedButton from \"@/components/buttons/markPlayedButton\";\nimport PlayButton from \"@/components/buttons/playButton\";\nimport TrailerButton from \"@/components/buttons/trailerButton\";\nimport IconLink from \"@/components/iconLink\";\nimport ShowMoreText from \"@/components/showMoreText\";\nimport ItemSkeleton from \"@/components/skeleton/item\";\nimport { getTypeIcon } from \"@/components/utils/iconsCollection\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { getItemQueryOptions } from \"@/utils/queries/items\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport type MediaQualityInfo from \"@/utils/types/mediaQualityInfo\";\n\nexport const Route = createFileRoute(\"/_api/item/$id\")({\n\tcomponent: ItemDetail,\n\tpendingComponent: () => <ItemSkeleton />,\n\t// pendingMs: 5000,\n\tloader: async ({ context: { queryClient, api, user }, params }) => {\n\t\tif (!api || !user?.Id) return null;\n\t\tconst itemPromise = queryClient.ensureQueryData(\n\t\t\tgetItemQueryOptions(params.id, api, user?.Id),\n\t\t);\n\t\tqueryClient.prefetchQuery({\n\t\t\tqueryKey: [\"item\", params.id, \"similarItem\"],\n\t\t\tqueryFn: async () =>\n\t\t\t\t(\n\t\t\t\t\tawait getLibraryApi(api).getSimilarItems({\n\t\t\t\t\t\tuserId: user?.Id,\n\t\t\t\t\t\titemId: params.id,\n\t\t\t\t\t\tlimit: 16,\n\t\t\t\t\t})\n\t\t\t\t).data,\n\t\t});\n\t\tawait itemPromise;\n\t},\n});\n\nfunction ItemDetail() {\n\tconst { id } = Route.useParams();\n\n\tconst api = Route.useRouteContext().api;\n\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst item = useSuspenseQuery(getItemQueryOptions(id, api, user?.Id));\n\n\tconst similarItems = useQuery({\n\t\tqueryKey: [\"item\", id, \"similarItem\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api || !user?.Id) return null;\n\t\t\treturn (\n\t\t\t\tawait getLibraryApi(api).getSimilarItems({\n\t\t\t\t\tuserId: user?.Id,\n\t\t\t\t\titemId: id,\n\t\t\t\t\tlimit: 16,\n\t\t\t\t})\n\t\t\t).data;\n\t\t},\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst [selectedVideoTrack, setSelectedVideoTrack] = useState(0);\n\tconst [selectedAudioTrack, setSelectedAudioTrack] = useState(0);\n\tconst [selectedSubtitleTrack, setSelectedSubtitleTrack] = useState(0);\n\n\tconst videoTracks = useMemo(() => {\n\t\tconst result = item.data?.MediaStreams?.filter((source) => {\n\t\t\tif (source.Type === MediaStreamType.Video) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t\tsetSelectedVideoTrack(\n\t\t\tresult?.find((track) => track.IsDefault)?.Index ??\n\t\t\t\tresult?.[0]?.Index ??\n\t\t\t\t0,\n\t\t);\n\t\treturn result ?? [];\n\t}, [item.data?.Id]);\n\tconst audioTracks = useMemo(() => {\n\t\tconst result = item.data?.MediaStreams?.filter((source) => {\n\t\t\tif (source.Type === MediaStreamType.Audio) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t\tsetSelectedAudioTrack(item.data.MediaSources?.[0].DefaultAudioStreamIndex ?? 0);\n\t\treturn result ?? [];\n\t}, [item.data?.Id]);\n\tconst subtitleTracks = useMemo(() => {\n\t\tconst result = item.data.MediaStreams?.filter((source) => {\n\t\t\tif (source.Type === MediaStreamType.Subtitle) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\treturn false;\n\t\t});\n\t\tsetSelectedSubtitleTrack(\n\t\t\titem.data.MediaSources?.[0].DefaultSubtitleStreamIndex ?? -1,\n\t\t);\n\t\treturn result ?? [];\n\t}, [item.data?.Id]);\n\n\tconst directors = useMemo(() => {\n\t\treturn item.data?.People?.filter((itm) => itm.Type === \"Director\") ?? [];\n\t}, [item.data?.Id]);\n\tconst writers = useMemo(() => {\n\t\treturn item.data?.People?.filter((itm) => itm.Type === \"Writer\") ?? [];\n\t}, [item.data?.Id]);\n\tconst actors = useMemo(() => {\n\t\treturn item.data?.People?.filter((itm) => itm.Type === \"Actor\") ?? [];\n\t}, [item.data?.Id]);\n\tconst producers = useMemo(() => {\n\t\treturn item.data?.People?.filter((itm) => itm.Type === \"Producer\") ?? [];\n\t}, [item.data?.Id]);\n\n\tconst mediaQualityInfo = useMemo<MediaQualityInfo>(() => {\n\t\tconst checkAtmos = audioTracks.filter((audio) =>\n\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"atmos\"),\n\t\t);\n\t\tconst checkDolbyVision = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"dv\") ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"dolby vision\") ||\n\t\t\t\t!!video.VideoDoViTitle ||\n\t\t\t\tvideo.VideoRangeType === \"DOVI\",\n\t\t);\n\t\tconst checkDD = audioTracks.filter(\n\t\t\t(audio) =>\n\t\t\t\t(audio.DisplayTitle?.toLocaleLowerCase().includes(\"dd\") &&\n\t\t\t\t\t!audio.DisplayTitle?.toLocaleLowerCase().includes(\"ddp\")) ||\n\t\t\t\t(audio.DisplayTitle?.toLocaleLowerCase().includes(\"dolby digital\") &&\n\t\t\t\t\t!audio.DisplayTitle?.toLocaleLowerCase().includes(\"dolby digital+\")),\n\t\t);\n\t\tconst checkDDP = audioTracks.filter(\n\t\t\t(audio) =>\n\t\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"ddp\") ||\n\t\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"dolby digital+\"),\n\t\t);\n\t\tconst checkDts = audioTracks.filter(\n\t\t\t(audio) =>\n\t\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"dts\") &&\n\t\t\t\t!audio.DisplayTitle?.toLocaleLowerCase().includes(\"dts-hd ma\") &&\n\t\t\t\t!audio.DisplayTitle?.toLocaleLowerCase().includes(\"dts-hd.ma\"),\n\t\t);\n\t\tconst checkDtsHDMA = audioTracks.filter(\n\t\t\t(audio) =>\n\t\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"dts-hd ma\") ||\n\t\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"dts-hd.ma\"),\n\t\t);\n\t\tconst checkUHD = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"4k\") ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"2160p\") ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"uhd\"),\n\t\t);\n\t\tconst checkHD = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\t(video.DisplayTitle?.toLocaleLowerCase().includes(\"hd\") &&\n\t\t\t\t\t!video.DisplayTitle?.toLocaleLowerCase().includes(\"hdr\")) ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"1080p\") ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"fhd\"),\n\t\t);\n\t\tconst checkSD = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\t(video.DisplayTitle?.toLocaleLowerCase().includes(\"sd\") &&\n\t\t\t\t\t!video.DisplayTitle?.toLocaleLowerCase().includes(\"sdr\")) ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"720p\"),\n\t\t);\n\t\tconst checkSDR = videoTracks.filter(\n\t\t\t(video) => video.VideoRangeType === \"SDR\",\n\t\t);\n\t\tconst checkHDR = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\tvideo.VideoRange === \"HDR\" ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"dv\"),\n\t\t);\n\t\tconst checkHDR10 = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\tvideo.VideoRangeType === \"HDR10\" &&\n\t\t\t\t!video.DisplayTitle?.toLocaleLowerCase().includes(\"hdr10+\"),\n\t\t);\n\t\tconst checkHDR10Plus = videoTracks.filter(\n\t\t\t(video) =>\n\t\t\t\tvideo.VideoRangeType === \"HDR10Plus\" ||\n\t\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"hdr10+\"),\n\t\t);\n\t\tconst checkTrueHD = audioTracks.filter((audio) =>\n\t\t\taudio.DisplayTitle?.toLocaleLowerCase().includes(\"truehd\"),\n\t\t);\n\t\tconst checkIMAX = videoTracks.filter((video) =>\n\t\t\tvideo.DisplayTitle?.toLocaleLowerCase().includes(\"imax\"),\n\t\t);\n\t\treturn {\n\t\t\tisAtmos: checkAtmos.length > 0,\n\t\t\tisDolbyVision: checkDolbyVision.length > 0,\n\t\t\tisDts: checkDts.length > 0,\n\t\t\tisDtsHDMA: checkDtsHDMA.length > 0,\n\t\t\tisDD: checkDD.length > 0,\n\t\t\tisDDP: checkDDP.length > 0,\n\t\t\tisUHD: checkUHD.length > 0,\n\t\t\tisHD: checkHD.length > 0,\n\t\t\tisSD: checkSD.length > 0,\n\t\t\tisSDR: checkSDR.length > 0,\n\t\t\tisHDR: checkHDR.length > 0,\n\t\t\tisHDR10: checkHDR10.length > 0,\n\t\t\tisHDR10Plus: checkHDR10Plus.length > 0,\n\t\t\tisTrueHD: checkTrueHD.length > 0,\n\t\t\tisIMAX: checkIMAX.length > 0,\n\t\t};\n\t}, [item.data?.Id]);\n\n\tconst setBackdrop = useBackdropStore(useShallow((state) => state.setBackdrop));\n\n\tuseLayoutEffect(() => {\n\t\tif (api && item.data) {\n\t\t\tif (item.data?.BackdropImageTags?.length) {\n\t\t\t\tsetBackdrop(\n\t\t\t\t\titem.data.ImageBlurHashes?.Backdrop?.[\n\t\t\t\t\t\titem.data.BackdropImageTags?.[0]\n\t\t\t\t\t],\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tsetBackdrop(\"\");\n\t\t\t}\n\t\t}\n\t}, [item.data.Id]);\n\n\t// Provide a scroll target ref to the backdrop component.\n\tconst scrollTargetRef = useRef<HTMLDivElement | null>(null);\n\n\t\treturn (\n\t\t\t<motion.div\n\t\t\t\tkey={id}\n\t\t\t\tinitial={{\n\t\t\t\t\topacity: 0,\n\t\t\t\t}}\n\t\t\t\tanimate={{\n\t\t\t\t\topacity: 1,\n\t\t\t\t}}\n\t\t\t\ttransition={{\n\t\t\t\t\tduration: 0.35,\n\t\t\t\t\tease: \"easeInOut\",\n\t\t\t\t}}\n\t\t\t\tclassName=\"scrollY padded-top flex flex-column item item-default\"\n\t\t\t\tref={scrollTargetRef}\n\t\t\t>\n\t\t\t\t<ItemHeader\n\t\t\t\t\titem={item.data}\n\t\t\t\t\tapi={api}\n\t\t\t\t\tmediaQualityInfo={mediaQualityInfo}\n\t\t\t\t\tscrollTargetRef={scrollTargetRef}\n\t\t\t\t>\n\t\t\t\t\t<div className=\"item-hero-buttons-container\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"flex flex-row\"\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<PlayButton\n\t\t\t\t\t\t\t\titem={item.data}\n\t\t\t\t\t\t\t\titemType={item.data.Type ?? \"Movie\"}\n\t\t\t\t\t\t\t\taudio={item.data.Type === \"Audio\"}\n\t\t\t\t\t\t\t\tcurrentVideoTrack={selectedVideoTrack}\n\t\t\t\t\t\t\t\tcurrentAudioTrack={selectedAudioTrack}\n\t\t\t\t\t\t\t\tcurrentSubTrack={selectedSubtitleTrack}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\tbuttonProps={{\n\t\t\t\t\t\t\t\t\tfullWidth: true,\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"flex flex-row\" style={{ gap: \"1em\" }}>\n\t\t\t\t\t\t\t{item.data.RemoteTrailers && (\n\t\t\t\t\t\t\t\t<TrailerButton\n\t\t\t\t\t\t\t\t\ttrailerItem={item.data.RemoteTrailers}\n\t\t\t\t\t\t\t\t\tdisabled={item.data.RemoteTrailers?.length === 0}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\t\titemName={item.data.Name}\n\t\t\t\t\t\t\t\titemId={item.data.Id}\n\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\tisFavorite={item.data.UserData?.IsFavorite}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<MarkPlayedButton\n\t\t\t\t\t\t\t\titemName={item.data.Name}\n\t\t\t\t\t\t\t\titemId={item.data.Id}\n\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\tisPlayed={item.data.UserData?.Played}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</ItemHeader>\n\t\t\t\t<div className=\"item-detail\">\n\t\t\t\t\t<div style={{ width: \"100%\" }}>\n\t\t\t\t\t\t{(item.data.Taglines?.length ?? 0) > 0 && (\n\t\t\t\t\t\t\t<Typography variant=\"h5\" mb={2} fontWeight={300}>\n\t\t\t\t\t\t\t\t{item.data.Taglines?.[0]}\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t<ShowMoreText\n\t\t\t\t\t\t\tcontent={item.data.Overview ?? \"\"}\n\t\t\t\t\t\t\tcollapsedLines={4}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{videoTracks.length > 0 && (\n\t\t\t\t\t\t\t<TextField\n\t\t\t\t\t\t\t\tlabel=\"Video\"\n\t\t\t\t\t\t\t\tselect\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\t\tmarginBottom: \"1em\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tvalue={selectedVideoTrack}\n\t\t\t\t\t\t\t\tvariant=\"filled\"\n\t\t\t\t\t\t\t\tonChange={(e) => setSelectedVideoTrack(Number(e.target.value))}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{videoTracks.map((track) => (\n\t\t\t\t\t\t\t\t\t<MenuItem key={track.Index} value={track.Index}>\n\t\t\t\t\t\t\t\t\t\t{track.DisplayTitle}\n\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</TextField>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{audioTracks.length > 0 && (\n\t\t\t\t\t\t\t<TextField\n\t\t\t\t\t\t\t\tlabel=\"Audio\"\n\t\t\t\t\t\t\t\tselect\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\t\tmarginBottom: \"1em\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tvalue={selectedAudioTrack}\n\t\t\t\t\t\t\t\tvariant=\"filled\"\n\t\t\t\t\t\t\t\tonChange={(e) => setSelectedAudioTrack(Number(e.target.value))}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{audioTracks.map((track) => (\n\t\t\t\t\t\t\t\t\t<MenuItem key={track.Index} value={track.Index}>\n\t\t\t\t\t\t\t\t\t\t{track.DisplayTitle}\n\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</TextField>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{subtitleTracks.length > 0 && (\n\t\t\t\t\t\t\t<TextField\n\t\t\t\t\t\t\t\tlabel=\"Subtitle\"\n\t\t\t\t\t\t\t\tselect\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tvalue={selectedSubtitleTrack}\n\t\t\t\t\t\t\t\tvariant=\"filled\"\n\t\t\t\t\t\t\t\tSelectProps={{\n\t\t\t\t\t\t\t\t\tMenuProps: {\n\t\t\t\t\t\t\t\t\t\tdisablePortal: true,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\tonChange={(e) =>\n\t\t\t\t\t\t\t\t\tsetSelectedSubtitleTrack(Number(e.target.value))\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<MenuItem key={-1} value={-1}>\n\t\t\t\t\t\t\t\t\tNo Subtitle\n\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t{subtitleTracks.map((track) => (\n\t\t\t\t\t\t\t\t\t<MenuItem key={track.Index} value={track.Index}>\n\t\t\t\t\t\t\t\t\t\t{track.DisplayTitle}\n\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t</TextField>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\tgap: \"0.6em\",\n\t\t\t\t\t\t\t\talignSelf: \"end\",\n\t\t\t\t\t\t\t\tmarginTop: \"1em\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.data.ExternalUrls?.map(\n\t\t\t\t\t\t\t\t(url) =>\n\t\t\t\t\t\t\t\t\turl.Url &&\n\t\t\t\t\t\t\t\t\turl.Name && (\n\t\t\t\t\t\t\t\t\t\t<IconLink key={url.Name} url={url.Url} name={url.Name} />\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t{(item.data.People?.length ?? 0) > 0 && (\n\t\t\t\t\t<div className=\"item-detail-cast\">\n\t\t\t\t\t\t<Typography variant=\"h5\" mb={2}>\n\t\t\t\t\t\t\tCast & Crew\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t{actors.length > 0 && (\n\t\t\t\t\t\t\t<div className=\"item-detail-cast-container\">\n\t\t\t\t\t\t\t\t<Typography variant=\"h6\">Actors</Typography>\n\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-grid\">\n\t\t\t\t\t\t\t\t\t{actors.map((actor) => (\n\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card\"\n\t\t\t\t\t\t\t\t\t\t\tkey={actor.Id}\n\t\t\t\t\t\t\t\t\t\t\tto=\"/person/$id\"\n\t\t\t\t\t\t\t\t\t\t\tparams={{\n\t\t\t\t\t\t\t\t\t\t\t\tid: actor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{actor.PrimaryImageTag ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\talt={actor.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Person\")}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-text\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Role}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{writers.length > 0 && (\n\t\t\t\t\t\t\t<div className=\"item-detail-cast-container\">\n\t\t\t\t\t\t\t\t<Typography variant=\"h6\">Writers</Typography>\n\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-grid\">\n\t\t\t\t\t\t\t\t\t{writers.map((actor) => (\n\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card\"\n\t\t\t\t\t\t\t\t\t\t\tkey={actor.Id}\n\t\t\t\t\t\t\t\t\t\t\tto=\"/person/$id\"\n\t\t\t\t\t\t\t\t\t\t\tparams={{\n\t\t\t\t\t\t\t\t\t\t\t\tid: actor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{actor.PrimaryImageTag ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\talt={actor.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Person\")}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-text\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Role}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{directors.length > 0 && (\n\t\t\t\t\t\t\t<div className=\"item-detail-cast-container\">\n\t\t\t\t\t\t\t\t<Typography variant=\"h6\">Directors</Typography>\n\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-grid\">\n\t\t\t\t\t\t\t\t\t{directors.map((actor) => (\n\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card\"\n\t\t\t\t\t\t\t\t\t\t\tto=\"/person/$id\"\n\t\t\t\t\t\t\t\t\t\t\tkey={actor.Id}\n\t\t\t\t\t\t\t\t\t\t\tparams={{\n\t\t\t\t\t\t\t\t\t\t\t\tid: actor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{actor.PrimaryImageTag ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\talt={actor.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Person\")}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-text\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Role}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{producers.length > 0 && (\n\t\t\t\t\t\t\t<div className=\"item-detail-cast-container\">\n\t\t\t\t\t\t\t\t<Typography variant=\"h6\">Producers</Typography>\n\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-grid\">\n\t\t\t\t\t\t\t\t\t{producers.map((actor) => (\n\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card\"\n\t\t\t\t\t\t\t\t\t\t\tkey={actor.Id}\n\t\t\t\t\t\t\t\t\t\t\tto=\"/person/$id\"\n\t\t\t\t\t\t\t\t\t\t\tparams={{\n\t\t\t\t\t\t\t\t\t\t\t\tid: actor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t{actor.PrimaryImageTag ? (\n\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\talt={actor.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Person\")}\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-text\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Role}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t\t{(similarItems.data?.TotalRecordCount ?? 0) > 0 && (\n\t\t\t\t\t<CardScroller\n\t\t\t\t\t\ttitle=\"You might also like\"\n\t\t\t\t\t\tdisplayCards={7}\n\t\t\t\t\t\tdisableDecoration\n\t\t\t\t\t>\n\t\t\t\t\t\t{similarItems.data?.Items?.map((similar) => {\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\tkey={similar.Id}\n\t\t\t\t\t\t\t\t\titem={similar}\n\t\t\t\t\t\t\t\t\tseriesId={similar.SeriesId}\n\t\t\t\t\t\t\t\t\tcardTitle={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? similar.SeriesName\n\t\t\t\t\t\t\t\t\t\t\t: similar.Name\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\t\t\t\t\tcardCaption={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? `S${similar.ParentIndexNumber}:E${similar.IndexNumber} - ${similar.Name}`\n\t\t\t\t\t\t\t\t\t\t\t: similar.Type === BaseItemKind.Series\n\t\t\t\t\t\t\t\t\t\t\t\t? `${similar.ProductionYear} - ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsimilar.EndDate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? new Date(similar.EndDate).toLocaleString([], {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tyear: \"numeric\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"Present\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t}`\n\t\t\t\t\t\t\t\t\t\t\t\t: similar.ProductionYear\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tcardType={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.MusicAlbum ||\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Audio\n\t\t\t\t\t\t\t\t\t\t\t? \"square\"\n\t\t\t\t\t\t\t\t\t\t\t: \"portrait\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id, \"similarItem\"]}\n\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</CardScroller>\n\t\t\t\t)}\n\t\t\t</motion.div>\n\t\t);\n}\n\nexport default ItemDetail;\n"
  },
  {
    "path": "src/routes/_api/item/item.scss",
    "content": "$cardWidth: 18%;\n\n.item-default.item {\n\t\n\t.item-detail {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: 1fr 35%;\n\t\tjustify-items: center;\n\t\talign-items: start;\n\t\tgap: 3em;\n\t\t\n\t\t&-cast {\n\t\t\t&-container {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\tgap: 1em;\n\t\t\t}\n\t\t\t&-grid {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: repeat(5, minmax(0, 1fr));\n\t\t\t\tgap: 1em;\n\t\t\t\tmargin-bottom: 1em;\n\t\t\t}\n\t\t\t&-card {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: 6em 1fr;\n\t\t\t\tgap: 0.5em;\n\t\t\t\talign-items: center;\n\t\t\t\tcolor: white !important;\n\t\t\t\ttext-decoration: none;\n\t\t\t\tpadding: 0.5em;\n\t\t\t\tbackground: rgb(255 255 255 / 0);\n\t\t\t\ttransition: background $transition-time-default;\n\t\t\t\tborder-radius: $border-radius-default;\n\t\t\t\t&:hover {\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t}\n\t\t\t\t&-image,\n\t\t\t\t&-icon {\n\t\t\t\t\twidth: 6em;\n\t\t\t\t\theight: 6em;\n\t\t\t\t\tobject-fit: cover;\n\t\t\t\t\tborder-radius: 100%;\n\t\t\t\t\tbox-shadow: $shadow-card-image;\n\t\t\t\t}\n\t\t\t\t&-icon {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tfont-size: 1em;\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t\t.material-symbols-rounded {\n\t\t\t\t\t\tfont-size: 3em;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t&-link {\n\t\t\tcolor: white;\n\t\t\ttext-decoration-color: rgb(255 255 255 / 0.5);\n\t\t\t&:hover {\n\t\t\t\ttext-decoration-color: white;\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Global item-detail\n.item-detail-link{\n\tmargin-top: 0.85em !important;\n\t&:hover > img{\n\t\tfilter: brightness(1);\n\t}\n\t& > img {\n\t\twidth: 2em;\n\t\taspect-ratio: 1;\n\t\tfilter: brightness(0.5);\n\t\ttransition: filter $transition-time-default;\n\t}\n}\n\n.item {\n\tgap: 1.5em;\n\t&-hero{\n\t\t// margin-bottom: 1em;\n\t\t&-image{\n\t\t\tbox-sizing: border-box;\n\t\t\t&-container{\n\t\t\t\tbox-sizing: content-box;\n\t\t\t\tbox-shadow: 0px 10px 20px -10px rgb(0 0 0 / 0.5) !important;\n\t\t\t}\n\t\t}\n\t\t&-mediaInfo{\n\t\t\tmargin-top: 1em;\n\t\t\theight: 1.1em;\n\t\t\topacity: 0.8;\n\t\t\t&.badge{\n\t\t\t\tmargin: 0;\n\t\t\t\theight: 1.2em;\n\t\t\t}\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/routes/_api/library/$id.tsx",
    "content": "import { BaseItemKind, ItemSortBy } from \"@jellyfin/sdk/lib/generated-client\";\nimport { useSuspenseQuery } from \"@tanstack/react-query\";\nimport React, { Suspense, useEffect } from \"react\";\nimport \"./library.scss\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { useShallow } from \"zustand/shallow\";\nimport { AlphaSelector } from \"@/components/alphaSelector\";\nimport { LibraryHeader } from \"@/components/libraryHeader\";\nimport LibraryItemsGrid from \"@/components/libraryItemsGrid\";\nimport LibraryItemsSkeleton from \"@/components/skeleton/libraryItems\";\nimport { getDefaultSortByForCollectionType } from \"@/utils/constants/library\";\nimport { getLibraryQueryOptions } from \"@/utils/queries/library\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport useHeaderStore from \"@/utils/store/header\";\nimport { useLibraryStateStore } from \"@/utils/store/libraryState\";\n\n// type SortByObject = { title: string; value: string };\n// type ViewObject = { title: string; value: BaseItemKind | \"Artist\" };\n// type allowedFilters = [\"movies\", \"tvshows\", \"music\", \"books\"];\n\nexport const Route = createFileRoute(\"/_api/library/$id\")({\n\tcomponent: Library,\n\tloader: async ({ context: { queryClient, api, user }, params }) => {\n\t\tif (!api || !user?.Id) return;\n\t\tconst library = await queryClient.ensureQueryData(\n\t\t\tgetLibraryQueryOptions(api, user?.Id, params.id),\n\t\t);\n\n\t\tconst collectionType = library.CollectionType;\n\t\tconst detectedViewType: BaseItemKind | \"Artist\" =\n\t\t\tcollectionType === \"music\"\n\t\t\t\t? BaseItemKind.MusicAlbum\n\t\t\t\t: collectionType === \"movies\"\n\t\t\t\t\t? BaseItemKind.Movie\n\t\t\t\t\t: collectionType === \"tvshows\"\n\t\t\t\t\t\t? BaseItemKind.Series\n\t\t\t\t\t\t: collectionType === \"boxsets\"\n\t\t\t\t\t\t\t? BaseItemKind.BoxSet\n\t\t\t\t\t\t\t: collectionType === \"books\"\n\t\t\t\t\t\t\t\t? BaseItemKind.Book\n\t\t\t\t\t\t\t\t: collectionType === \"playlists\"\n\t\t\t\t\t\t\t\t\t? BaseItemKind.Playlist\n\t\t\t\t\t\t\t\t\t: BaseItemKind.Movie;\n\n\t\tuseLibraryStateStore.getState().initLibrary(params.id, {\n\t\t\tcurrentViewType: detectedViewType,\n\t\t\tsortBy:\n\t\t\t\tgetDefaultSortByForCollectionType(collectionType as any) ||\n\t\t\t\tItemSortBy.Name,\n\t\t\tsortAscending: true,\n\t\t\tlibraryName: library.Name ?? undefined,\n\t\t});\n\t},\n});\n\nfunction Library() {\n\tconst api = useApiInContext((s) => s.api);\n\n\tconst { id } = Route.useParams();\n\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst currentLib = useSuspenseQuery(\n\t\tgetLibraryQueryOptions(api, user?.Id, id),\n\t);\n\tconst _initLibrary = useLibraryStateStore((s) => s.initLibrary);\n\tconst updateLibrary = useLibraryStateStore((s) => s.updateLibrary);\n\tconst { currentViewType: storeViewType, libraryName: storeLibraryName } =\n\t\tuseLibraryStateStore(\n\t\t\tuseShallow((s) => {\n\t\t\t\tconst sl = s.libraries[id || \"\"];\n\t\t\t\treturn {\n\t\t\t\t\tcurrentViewType: sl?.currentViewType,\n\t\t\t\t\tlibraryName: sl?.libraryName,\n\t\t\t\t} as const;\n\t\t\t}),\n\t\t);\n\n\tReact.useEffect(() => {\n\t\tif (!id || !currentLib.data) return;\n\t\t// Ensure library name is available in Zustand for header consumption\n\t\tupdateLibrary(id, { libraryName: currentLib.data.Name ?? undefined });\n\t}, [id, updateLibrary, currentLib.data]);\n\n\tconst { setPageTitle } = useHeaderStore(\n\t\tuseShallow((state) => ({\n\t\t\tsetPageTitle: state.setPageTitle,\n\t\t})),\n\t);\n\t// Derive top app title from Zustand slice (library name)\n\tuseEffect(() => {\n\t\tconst title = storeLibraryName ?? \"Library\";\n\t\tsetPageTitle(title);\n\t}, [storeLibraryName, setPageTitle]);\n\n\t/* if (!id) {\n\t\t// show loading if any route param is missing\n\t\treturn <LoadingIndicator />;\n\t} */\n\n\treturn (\n\t\t<main className=\"scrollY library padded-top\">\n\t\t\t<LibraryHeader />\n\n\t\t\t<Suspense fallback={<LibraryItemsSkeleton />}>\n\t\t\t\t<div className=\"library-items\">\n\t\t\t\t\t<LibraryItemsGrid />\n\t\t\t\t</div>\n\t\t\t\t<div style={{ position: \"fixed\", right: 14, top: 80 }}>\n\t\t\t\t\t<AlphaSelector />\n\t\t\t\t</div>\n\t\t\t</Suspense>\n\t\t</main>\n\t);\n}"
  },
  {
    "path": "src/routes/_api/library/library.scss",
    "content": ".library-list {\n\t&-image {\n\t\tposition: relative;\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tz-index: 1;\n\t\tobject-fit: cover;\n\t\t&-container {\n\t\t\tposition: relative;\n\t\t\twidth: 4.5em;\n\t\t\theight: 4.5em;\n\t\t\taspect-ratio: 1;\n\t\t\tborder-radius: $border-radius_04;\n\t\t\toverflow: hidden;\n\t\t\tbox-shadow: 0 2px 7px rgb(0 0 0 / 0.2);\n\t\t}\n\t}\n\t&-icon {\n\t\tfont-size: 3em;\n\t\tfill: url(#clr-gradient-default) !important;\n\t\t&-container {\n\t\t\tposition: absolute;\n\t\t\ttop: 0;\n\t\t\tleft: 0;\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\tdisplay: flex;\n\t\t\tjustify-content: center;\n\t\t\talign-items: center;\n\t\t\tz-index: 0;\n\t\t\tbackground: $clr-gradient-dark;\n\t\t}\n\t}\n}\n.library {\n\tpadding-left: 0 !important;\n\tpadding-right: 0 !important;\n\tposition: relative;\n\tdisplay: flex;\n\tflex-direction: column;\n\talign-items: stretch;\n\t&-grid {\n\t\t--number-of-cols: 8;\n\t\tdisplay: grid;\n\t\tgrid-template-columns: repeat(var(--number-of-cols), minmax(0, 1fr));\n\t\tjustify-items: center;\n\t\talign-items: start;\n\t\t> * {\n\t\t\tmax-width: 11em;\n\t\t}\n\t}\n\t&-items {\n\t\theight: fit-content;\n\t\toverflow-y: auto;\n\t\toverflow-x: hidden;\n\t\tpadding: 1.5em $page-margin;\n\t\t// padding-top: 4em;\n\t\t&-header {\n\t\t\tposition: fixed;\n\t\t\ttop: 1em;\n\t\t\tz-index: 10;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\theight: fit-content;\n\t\t\tleft: 50%;\n\t\t\ttransform: translate(-50%);\n\t\t\tborder-radius: 100px;\n\t\t\toverflow: hidden;\n\t\t\tz-index: 1000;\n\t\t\t&::after {\n\t\t\t\tcontent: \"\";\n\t\t\t\tposition: absolute;\n\t\t\t\tinset: 0 -0.35em;\n\t\t\t\tpointer-events: none;\n\t\t\t\tborder-radius: inherit;\n\t\t\t\t\n                @include glass-effect;\n\n\t\t\t\topacity: 0;\n\t\t\t\tz-index: -1;\n\t\t\t\ttransition: all $transition-time-fast;\n\t\t\t}\n\t\t\t&.scrolling {\n\t\t\t\t&::after {\n\t\t\t\t\topacity: 1;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t&-options {\n\t\t\tpadding: 0.5em 0.75em;\n\t\t\tgap: 0.5em;\n\t\t\toverflow: hidden;\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t}\n\t\t&-container{\n\t\t\tdisplay: grid;\n\t\t\tgrid-template-columns: repeat(auto-fit, minmax(10em, 1fr)) ;\n\t\t\tgap: 1.6em;\n\t\t\tpadding: 1em 4.4em;\n\t\t\talign-content: center;\n\t\t\tjustify-items: center;\n\t\t\t> .card {\n\t\t\t\tmax-width: 11em;\n\t\t\t}\n\t\t}\n\t}\n\t&-filter {\n\t\twidth: 100%;\n\t\tjustify-content: space-between;\n\t\tmargin: 0 0.5em !important;\n\t\t&-container {\n\t\t\tmargin-top: 0.75em;\n\t\t\twidth: 22em;\n\t\t\tmax-height: 32em;\n\t\t\tbackground: rgb(0 0 0 / 0.65);\n\t\t\tborder-radius: $border-radius-default;\n\t\t\tbackdrop-filter: blur(10px);\n\t\t\toverflow-y: overlay;\n\t\t}\n\t\t&-title {\n\t\t\tjustify-self: start;\n\t\t}\n\t\t&-accordian {\n\t\t\tbackground: transparent !important;\n\t\t\tmargin: 0 !important;\n\t\t\tbox-shadow: none !important;\n\t\t}\n\t}\n\t&-genre {\n\t\t&-header {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tjustify-content: flex-start;\n\t\t\tgap: 0.5em;\n\t\t\tposition: relative;\n\t\t\tmargin-bottom: 0.25em !important;\n\t\t\t&-decoration {\n\t\t\t\twidth: 1em;\n\t\t\t\theight: 2px;\n\t\t\t\tbackground: white;\n\t\t\t}\n\t\t}\n\t}\n\t// Fix card width with virtual rows\n\t.card {\n\t\twidth: 100%;\n\t\theight: fit-content;\n\t\tmargin: 0;\n\t}\n\t// Layout css for dynamic virtual rows\n\t&-virtual-item {\n\t\twidth: 14%;\n\t\tmin-width: 0;\n\t\t// flex: 0 0 0;\n\t\theight: fit-content;\n\t\tdisplay: inline-flex;\n\t\tflex-direction: row;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tpadding: 0.75em;\n\t\t&-row {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: row;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tflex-basis: 14%;\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/routes/_api/login/$userId.$userName.tsx",
    "content": "import LoadingButton from \"@mui/lab/LoadingButton\";\nimport { Paper, TextField, Typography } from \"@mui/material\";\nimport Checkbox from \"@mui/material/Checkbox\";\nimport FormControlLabel from \"@mui/material/FormControlLabel\";\nimport IconButton from \"@mui/material/IconButton\";\nimport InputAdornment from \"@mui/material/InputAdornment\";\n\nimport { useSnackbar } from \"notistack\";\n\nimport { AvatarImage } from \"@/components/avatar/avatar.jsx\";\nimport \"./login.scss\";\n\nimport { useMutation } from \"@tanstack/react-query\";\nimport {\n\tcreateFileRoute,\n\tuseNavigate,\n\tuseRouter,\n} from \"@tanstack/react-router\";\nimport React, {\n\ttype ChangeEvent,\n\ttype FormEvent,\n\tuseEffect,\n\tuseState,\n} from \"react\";\nimport { useShallow } from \"zustand/shallow\";\nimport { saveUser } from \"@/utils/storage/user\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useBackdropStore } from \"@/utils/store/backdrop.js\";\n\nexport const Route = createFileRoute(\"/_api/login/$userId/$userName\")({\n\tcomponent: LoginUser,\n});\n\ntype PasswordState = {\n\tshowpass: boolean;\n\tpassword: string | undefined;\n};\n\nfunction LoginUser() {\n\tconst { userName, userId } = Route.useParams();\n\n\tconst router = useRouter();\n\n\tconst api = useApiInContext((s) => s.api);\n\tconst createApi = useApiInContext((s) => s.createApi);\n\n\tconst [password, setPassword] = useState<PasswordState>({\n\t\tshowpass: false,\n\t\tpassword: undefined,\n\t});\n\tconst [rememberMe, setRememberMe] = useState(true);\n\n\tconst navigate = useNavigate({ from: \"/login/$userId/$userName\" });\n\n\tconst { enqueueSnackbar } = useSnackbar();\n\n\tconst handlePassword =\n\t\t(prop: \"password\" | \"showPassord\") =>\n\t\t(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {\n\t\t\tsetPassword({\n\t\t\t\t...password,\n\t\t\t\t[prop]: event.target.value,\n\t\t\t});\n\t\t};\n\n\tconst handleShowPassword = () => {\n\t\tsetPassword({\n\t\t\t...password,\n\t\t\tshowpass: !password.showpass,\n\t\t});\n\t};\n\n\tconst setBackdrop = useBackdropStore(\n\t\tuseShallow((state) => state.setBackdrop),\n\t);\n\n\tuseEffect(() => {\n\t\tif (!api) {\n\t\t\treturn;\n\t\t}\n\t\tsetBackdrop(\"\");\n\t}, []);\n\n\tconst handleCheckRememberMe = (\n\t\t_: ChangeEvent<HTMLInputElement>,\n\t\tchecked: boolean,\n\t) => {\n\t\tsetRememberMe(checked);\n\t};\n\n\tconst handleLoginFn = async (event: FormEvent<HTMLFormElement>) => {\n\t\tevent.preventDefault();\n\t\ttry {\n\t\t\tif (!api) {\n\t\t\t\tthrow new Error(\"API not initialized\");\n\t\t\t}\n\t\t\tconst authenticate = await api.authenticateUserByName(\n\t\t\t\tuserName,\n\t\t\t\tpassword.password,\n\t\t\t);\n\t\t\tif (!authenticate.data.AccessToken) {\n\t\t\t\tthrow new Error(\"No access token received\");\n\t\t\t}\n\t\t\tcreateApi(api.basePath, authenticate.data?.AccessToken);\n\n\t\t\tif (authenticate.data.SessionInfo?.UserId && rememberMe) {\n\t\t\t\tawait saveUser(\n\t\t\t\t\tuserName,\n\t\t\t\t\tauthenticate.data.AccessToken,\n\t\t\t\t\tauthenticate.data.SessionInfo.UserId,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\trouter.invalidate().finally(() => {\n\t\t\t\tnavigate({ to: \"/home\", replace: true });\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tconsole.error(err);\n\t\t\tenqueueSnackbar(\"Unable to authenticate! Please check your password\", {\n\t\t\t\tvariant: \"error\",\n\t\t\t});\n\t\t}\n\t\treturn;\n\t};\n\n\tconst handleLogin = useMutation({\n\t\tmutationKey: [\"login\", \"authenticate\"],\n\t\tmutationFn: handleLoginFn,\n\t});\n\n\treturn (\n\t\t<div\n\t\t\tclassName=\"login flex flex-center flex-column centered\"\n\t\t\tstyle={{ marginTop: 0 }}\n\t\t>\n\t\t\t<Paper\n\t\t\t\tsx={{\n\t\t\t\t\tp: 4,\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\tmaxWidth: \"600px\",\n\t\t\t\t\tbackgroundColor: \"rgba(20, 20, 30, 0.7)\",\n\t\t\t\t\tbackdropFilter: \"blur(24px) saturate(180%)\",\n\t\t\t\t\tbackgroundImage:\n\t\t\t\t\t\t\"linear-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0))\",\n\t\t\t\t\tborder: \"1px solid rgba(255, 255, 255, 0.08)\",\n\t\t\t\t\tboxShadow: \"0 20px 50px rgba(0,0,0,0.5)\",\n\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tmarginBottom: \"2em\",\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\tgap: \"1em\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<AvatarImage userId={userId} />\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Typography variant=\"body1\" sx={{ opacity: 0.7 }}>\n\t\t\t\t\t\t\tLogin as\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t<Typography variant=\"h5\" fontWeight={700} className=\"gradient-text\">\n\t\t\t\t\t\t\t{userName}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<form\n\t\t\t\t\tonSubmit={(e) => handleLogin.mutate(e)}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\t\tgap: \"1.5rem\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<TextField\n\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\ttype={password.showpass ? \"text\" : \"password\"}\n\t\t\t\t\t\tonChange={handlePassword(\"password\")}\n\t\t\t\t\t\tlabel=\"Password\"\n\t\t\t\t\t\tvariant=\"outlined\"\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t\tInputProps={{\n\t\t\t\t\t\t\tstartAdornment: (\n\t\t\t\t\t\t\t\t<InputAdornment position=\"start\">\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\t\t\tstyle={{ opacity: 0.5 }}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</InputAdornment>\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tendAdornment: (\n\t\t\t\t\t\t\t\t<InputAdornment position=\"end\">\n\t\t\t\t\t\t\t\t\t<IconButton onClick={handleShowPassword} edge=\"end\">\n\t\t\t\t\t\t\t\t\t\t{password.showpass ? (\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">\n\t\t\t\t\t\t\t\t\t\t\t\tvisibility\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">\n\t\t\t\t\t\t\t\t\t\t\t\tvisibility_off\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t\t\t</InputAdornment>\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<FormControlLabel\n\t\t\t\t\t\t\tcontrol={\n\t\t\t\t\t\t\t\t<Checkbox\n\t\t\t\t\t\t\t\t\tchecked={rememberMe}\n\t\t\t\t\t\t\t\t\tonChange={handleCheckRememberMe}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tlabel=\"Remember me\"\n\t\t\t\t\t\t\tsx={{ m: 0 }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<LoadingButton\n\t\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\t\tendIcon={<span className=\"material-symbols-rounded\">login</span>}\n\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\tloading={handleLogin.isPending}\n\t\t\t\t\t\tloadingPosition=\"end\"\n\t\t\t\t\t\tsize=\"large\"\n\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\tsx={{ borderRadius: 2, height: 48 }}\n\t\t\t\t\t>\n\t\t\t\t\t\tLogin\n\t\t\t\t\t</LoadingButton>\n\t\t\t\t</form>\n\t\t\t</Paper>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/routes/_api/login/list.tsx",
    "content": "import { getUserApi } from \"@jellyfin/sdk/lib/utils/api/user-api\";\nimport { Box, Chip, Typography } from \"@mui/material\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport React, { useEffect } from \"react\";\n\nimport { ErrorNotice } from \"@/components/notices/errorNotice/errorNotice.jsx\";\nimport \"./login.scss\";\n\nimport type { UserDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport { Paper } from \"@mui/material\";\nimport { createFileRoute, Link } from \"@tanstack/react-router\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useBackdropStore } from \"@/utils/store/backdrop.js\";\nimport avatar from \"../../../assets/icons/avatar.png\";\n\nexport const Route = createFileRoute(\"/_api/login/list\")({\n\tcomponent: LoginPublicUsersList,\n});\n\nconst UserCard = ({ user }: { user: UserDto }) => {\n\tconst api = useApiInContext((s) => s.api);\n\treturn (\n\t\t<Link\n\t\t\tto=\"/login/$userId/$userName\"\n\t\t\tparams={{ userId: user.Id ?? \"\", userName: user.Name ?? \"\" }}\n\t\t\tclassName=\"user-list-item user-card\"\n\t\t\tstyle={{ textDecoration: \"none\", display: \"block\" }}\n\t\t>\n\t\t\t<Paper\n\t\t\t\televation={0}\n\t\t\t\tsx={{\n\t\t\t\t\tp: 2,\n\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.03)\",\n\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.1)\",\n\t\t\t\t\t\ttransform: \"translateY(-4px)\",\n\t\t\t\t\t\tboxShadow: \"0 8px 20px rgba(0,0,0,0.3)\",\n\t\t\t\t\t},\n\t\t\t\t\ttransition: \"all 0.3s ease\",\n\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\tborder: \"1px solid rgba(255,255,255,0.05)\",\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\twidth: \"160px\",\n\t\t\t\t\theight: \"100%\",\n\t\t\t\t\tcursor: \"pointer\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"user-card-image-container\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\twidth: \"100px\",\n\t\t\t\t\t\theight: \"100px\",\n\t\t\t\t\t\tborderRadius: \"50%\",\n\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t\tboxShadow: \"0 8px 16px rgba(0,0,0,0.3)\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t{user.PrimaryImageTag ? (\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tclassName=\"user-card-image\"\n\t\t\t\t\t\t\talt={\"user\"}\n\t\t\t\t\t\t\tsrc={`${api?.basePath}/Users/${user.Id}/Images/Primary?quality=80&tag=${user.PrimaryImageTag}`}\n\t\t\t\t\t\t\tstyle={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<img\n\t\t\t\t\t\t\tclassName=\"user-card-image\"\n\t\t\t\t\t\t\talt=\"user\"\n\t\t\t\t\t\t\tsrc={avatar}\n\t\t\t\t\t\t\tstyle={{ width: \"100%\", height: \"100%\", objectFit: \"cover\" }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t\t<Typography\n\t\t\t\t\tmt={2}\n\t\t\t\t\talign=\"center\"\n\t\t\t\t\tvariant=\"subtitle1\"\n\t\t\t\t\tfontWeight=\"bold\"\n\t\t\t\t\tnoWrap\n\t\t\t\t\tsx={{ width: \"100%\", color: \"text.primary\" }}\n\t\t\t\t>\n\t\t\t\t\t{user.Name}\n\t\t\t\t</Typography>\n\t\t\t</Paper>\n\t\t</Link>\n\t);\n};\n\nfunction LoginPublicUsersList() {\n\tconst api = useApiInContext((s) => s.api);\n\n\tconst users = useQuery({\n\t\tqueryKey: [\"login\", \"public-users\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return [];\n\t\t\tconst result = await getUserApi(api).getPublicUsers();\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: Boolean(api),\n\t});\n\n\tconst setBackdrop = useBackdropStore((state) => state.setBackdrop);\n\tuseEffect(() => {\n\t\tsetBackdrop(\"\");\n\t}, []);\n\n\tif (users.isSuccess) {\n\t\treturn (\n\t\t\t<div className=\"login-container scrollY flex flex-column flex-center\">\n\t\t\t\t<Paper\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tp: 4,\n\t\t\t\t\t\tmaxWidth: \"800px\",\n\t\t\t\t\t\twidth: \"90%\",\n\t\t\t\t\t\tbackgroundColor: \"rgba(20, 20, 30, 0.7)\",\n\t\t\t\t\t\tbackdropFilter: \"blur(24px) saturate(180%)\",\n\t\t\t\t\t\tbackgroundImage:\n\t\t\t\t\t\t\t\"linear-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0))\",\n\t\t\t\t\t\tborder: \"1px solid rgba(255, 255, 255, 0.08)\",\n\t\t\t\t\t\tboxShadow: \"0 20px 50px rgba(0,0,0,0.5)\",\n\t\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\tgap: 3,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Typography variant=\"h4\" align=\"center\" fontWeight=\"bold\">\n\t\t\t\t\t\tSelect User\n\t\t\t\t\t</Typography>\n\n\t\t\t\t\t<Box\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\tflexWrap: \"wrap\",\n\t\t\t\t\t\t\tgap: 3,\n\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\tmaxHeight: \"60vh\",\n\t\t\t\t\t\t\toverflowY: \"auto\",\n\t\t\t\t\t\t\tpy: 2,\n\t\t\t\t\t\t\t\"&::-webkit-scrollbar\": { display: \"none\" },\n\t\t\t\t\t\t\tscrollbarWidth: \"none\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{users.data.map((item) => {\n\t\t\t\t\t\t\treturn <UserCard user={item} key={item.Id} />;\n\t\t\t\t\t\t})}\n\t\t\t\t\t</Box>\n\n\t\t\t\t\t<Chip\n\t\t\t\t\t\tlabel={\n\t\t\t\t\t\t\t<Typography variant=\"body2\" align=\"center\">\n\t\t\t\t\t\t\t\tDon't see your user? Try using{\" \"}\n\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\tto=\"/login/manual\"\n\t\t\t\t\t\t\t\t\tstyle={{ color: \"inherit\", fontWeight: \"bold\" }}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\tManual Login\n\t\t\t\t\t\t\t\t</Link>{\" \"}\n\t\t\t\t\t\t\t\tor{\" \"}\n\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\tto=\"/setup/server/list\"\n\t\t\t\t\t\t\t\t\tstyle={{ color: \"inherit\", fontWeight: \"bold\" }}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\tChanging Server\n\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\t\t\tp: 1,\n\t\t\t\t\t\t\theight: \"auto\",\n\t\t\t\t\t\t\t\"& .MuiChip-label\": {\n\t\t\t\t\t\t\t\tpadding: \"8px 12px\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t</Paper>\n\t\t\t</div>\n\t\t);\n\t}\n\tif (users.isError) {\n\t\treturn <ErrorNotice />;\n\t}\n}\n"
  },
  {
    "path": "src/routes/_api/login/login.scss",
    "content": "\n.user {\n\t&-list {\n\t\t&-container {\n\t\t\tdisplay: flex;\n\t\t\tflex-flow: row wrap;\n\t\t\tgap: 1em;\n\t\t\tjustify-content: center;\n\t\t\talign-items: center;\n\t\t\theight: 30em;\n\t\t\toverflow-y: auto;\n\t\t\toverflow-x: hidden;\n\t\t\tscroll-snap-type: y mandatory;\n\t\t\tmargin: 2em 0;\n\t\t}\t\t\n\t}\n\t&-card {\n\t\twidth: fit-content;\n\t\tscroll-snap-align: start;\n\t\ttext-decoration: none !important;\n\t\tcolor: white !important;\n\t\t&-image{\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\tobject-fit: cover;\n\t\t\t&-container{\n\t\t\t\taspect-ratio: 1;\n\t\t\t\twidth: 8em;\n\t\t\t\toverflow: hidden;\n\t\t\t\tborder-radius: 100%;\n\t\t\t}\n\t\t}\n\t}\n\t&Icon {\n\t\tfont-size: 5em;\n\t\tdisplay: flex;\n\t\tjustify-content: center;\n\t\talign-items: center;\n\t\tbackground: $clr-accent-dark;\n\t}\n}\n\n.buttons {\n\tdisplay: flex;\n\tgap: 1em;\n\twidth: 100%;\n\t>* {\n\t\tflex: 1 1 0 !important;\n\t}\n}\n\n.login {\n\t// width: 50em;\n\theight: 100vh;\n\tdisplay: flex;\n\tflex-direction: column;\n\tjustify-content: center;\n\talign-items: center;\n\n\t&-container{\n\t\twidth: 100vw;\n\t\theight: 100vh;\n\t\tmargin: auto;\n\t\tpadding-top: 4em;\n\t\tpadding-bottom: 4em;\t\n\t}\n\n\t&-form {\n\t\twidth: 24em;\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tjustify-content: center;\n\t\talign-items: stretch;\n\t\tgap:1em;\n\t\t&-container{\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tjustify-content: center;\n\t\t\talign-items: center;\n\t\t\tgap:2em;\n\t\t\tbackground: rgb(255 255 255 / 0.05);\n\t\t\tpadding: 2em;\n\t\t\tborder-radius: 20px;\n\t\t\t// border: 1px solid rgb(255 255 255 / 0.1);\n\t\t\t&.padded-top {\n\t\t\t\tmargin-top: 25vh;\n\t\t\t}\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/routes/_api/login/manual.tsx",
    "content": "import { getBrandingApi } from \"@jellyfin/sdk/lib/utils/api/branding-api\";\nimport LoadingButton from \"@mui/lab/LoadingButton\";\nimport { Paper, TextField, Typography } from \"@mui/material\";\nimport Button from \"@mui/material/Button\";\nimport Checkbox from \"@mui/material/Checkbox\";\nimport FormControlLabel from \"@mui/material/FormControlLabel\";\nimport IconButton from \"@mui/material/IconButton\";\nimport InputAdornment from \"@mui/material/InputAdornment\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { useSnackbar } from \"notistack\";\nimport React, {\n\ttype ChangeEvent,\n\ttype FormEvent,\n\tuseEffect,\n\tuseState,\n} from \"react\";\nimport { saveUser } from \"@/utils/storage/user.js\";\nimport { useApiInContext } from \"@/utils/store/api.js\";\nimport \"./login.scss\";\n\nimport { blue } from \"@mui/material/colors\";\nimport {\n\tcreateFileRoute,\n\tuseNavigate,\n\tuseRouter,\n} from \"@tanstack/react-router\";\nimport { useShallow } from \"zustand/shallow\";\nimport { useBackdropStore } from \"@/utils/store/backdrop.js\";\n\nexport const Route = createFileRoute(\"/_api/login/manual\")({\n\tcomponent: UserLoginManual,\n\tvalidateSearch: (search): { redirect?: string } => {\n\t\treturn { redirect: search.redirect ? String(search.redirect) : undefined };\n\t},\n});\n\nfunction UserLoginManual() {\n\tconst [rememberMe, setRememberMe] = useState(true);\n\t// console.log(Route.useRouteContext());\n\tconst api = useApiInContext((s) => s.api);\n\tconst createApi = useApiInContext((s) => s.createApi);\n\tconst router = useRouter();\n\tconst searchParams = Route.useSearch();\n\n\tconst server = useQuery({\n\t\tqueryKey: [\"login\", \"manual\", \"serverInfo\"],\n\t\tqueryFn: async () =>\n\t\t\tapi ? (await getBrandingApi(api).getBrandingOptions()).data : null,\n\t});\n\n\tconst navigate = useNavigate();\n\tconst { enqueueSnackbar } = useSnackbar();\n\n\tconst [username, setUsername] = useState(\"\");\n\tconst [password, setPassword] = useState<{\n\t\tpassword: string | undefined;\n\t\tshowpass: boolean;\n\t}>({\n\t\tshowpass: false,\n\t\tpassword: \"\",\n\t});\n\n\tconst handlePassword =\n\t\t(prop: \"password\" | \"showpass\") =>\n\t\t(event: ChangeEvent<HTMLInputElement>) => {\n\t\t\tsetPassword({\n\t\t\t\t...password,\n\t\t\t\t[prop]: event.target.value,\n\t\t\t});\n\t\t};\n\n\tconst handleShowPassword = () => {\n\t\tsetPassword({\n\t\t\t...password,\n\t\t\tshowpass: !password.showpass,\n\t\t});\n\t};\n\n\tconst handleLoginFn = async (event: FormEvent<HTMLFormElement>) => {\n\t\tevent.preventDefault();\n\t\ttry {\n\t\t\tif (!api) {\n\t\t\t\tthrow new Error(\"Api is not available\");\n\t\t\t}\n\t\t\tconst authenticate = await api.authenticateUserByName(\n\t\t\t\tusername,\n\t\t\t\tpassword.password,\n\t\t\t);\n\t\t\tif (!authenticate.data?.AccessToken) {\n\t\t\t\tthrow new Error(\"Access token not available\");\n\t\t\t}\n\t\t\tcreateApi(api.basePath, authenticate.data?.AccessToken);\n\n\t\t\tif (rememberMe)\n\t\t\t\tawait saveUser(\n\t\t\t\t\tusername,\n\t\t\t\t\tauthenticate.data.AccessToken,\n\t\t\t\t\tauthenticate.data.User?.Id ?? \"\",\n\t\t\t\t);\n\t\t\trouter.invalidate().finally(() => {\n\t\t\t\tnavigate({ to: searchParams.redirect || \"/home\", replace: true });\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tconsole.error(err);\n\t\t\tenqueueSnackbar(\n\t\t\t\t\"Unable to authenticate. Please check your username and password\",\n\t\t\t\t{ variant: \"error\" },\n\t\t\t);\n\t\t}\n\t\treturn;\n\t};\n\n\tconst handleLogin = useMutation({\n\t\tmutationKey: [\"login\", \"authenticate\"],\n\t\tmutationFn: handleLoginFn,\n\t});\n\n\tconst handleCheckRememberMe = (\n\t\t_: ChangeEvent<HTMLInputElement>,\n\t\tchecked: boolean,\n\t) => {\n\t\tsetRememberMe(checked);\n\t};\n\n\tconst setBackdrop = useBackdropStore(\n\t\tuseShallow((state) => state.setBackdrop),\n\t);\n\tuseEffect(() => {\n\t\tsetBackdrop(\"\");\n\t}, []);\n\n\treturn (\n\t\t<div className=\"login scrollY flex flex-center flex-column\">\n\t\t\t<Paper\n\t\t\t\tsx={{\n\t\t\t\t\tp: 4,\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\tmaxWidth: \"600px\",\n\t\t\t\t\tbackgroundColor: \"rgba(20, 20, 30, 0.7)\",\n\t\t\t\t\tbackdropFilter: \"blur(24px) saturate(180%)\",\n\t\t\t\t\tbackgroundImage:\n\t\t\t\t\t\t\"linear-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0))\",\n\t\t\t\t\tborder: \"1px solid rgba(255, 255, 255, 0.08)\",\n\t\t\t\t\tboxShadow: \"0 20px 50px rgba(0,0,0,0.5)\",\n\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\tgap: 3,\n\t\t\t\t\tmb: 3,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Typography variant=\"h4\" fontWeight=\"bold\" align=\"center\">\n\t\t\t\t\tLogin\n\t\t\t\t</Typography>\n\t\t\t\t<form\n\t\t\t\t\tclassName=\"flex flex-column\"\n\t\t\t\t\tstyle={{ gap: \"1.5em\", width: \"100%\" }}\n\t\t\t\t\tonSubmit={(e) => handleLogin.mutate(e)}\n\t\t\t\t>\n\t\t\t\t\t<TextField\n\t\t\t\t\t\tvariant=\"outlined\"\n\t\t\t\t\t\tlabel=\"Username\" // Label helps accessibility, theme handles style\n\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\tonChange={(e) => setUsername(e.currentTarget.value)}\n\t\t\t\t\t\tInputProps={{\n\t\t\t\t\t\t\tstartAdornment: (\n\t\t\t\t\t\t\t\t<InputAdornment position=\"start\">\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\t\t\tstyle={{ opacity: 0.5 }}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tperson\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</InputAdornment>\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t\t<TextField\n\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\ttype={password.showpass ? \"text\" : \"password\"}\n\t\t\t\t\t\tonChange={handlePassword(\"password\")}\n\t\t\t\t\t\tlabel=\"Password\"\n\t\t\t\t\t\tvariant=\"outlined\"\n\t\t\t\t\t\tInputProps={{\n\t\t\t\t\t\t\tstartAdornment: (\n\t\t\t\t\t\t\t\t<InputAdornment position=\"start\">\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\t\t\tstyle={{ opacity: 0.5 }}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tkey\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</InputAdornment>\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tendAdornment: (\n\t\t\t\t\t\t\t\t<InputAdornment position=\"end\">\n\t\t\t\t\t\t\t\t\t<IconButton\n\t\t\t\t\t\t\t\t\t\tonClick={handleShowPassword}\n\t\t\t\t\t\t\t\t\t\tedge=\"end\"\n\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\tcolor: \"rgba(255,255,255,0.7)\",\n\t\t\t\t\t\t\t\t\t\t\t\"&:hover\": { color: \"#fff\" },\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{password.showpass ? (\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">\n\t\t\t\t\t\t\t\t\t\t\t\tvisibility\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">\n\t\t\t\t\t\t\t\t\t\t\t\tvisibility_off\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t\t\t</InputAdornment>\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\n\t\t\t\t\t<FormControlLabel\n\t\t\t\t\t\tcontrol={\n\t\t\t\t\t\t\t<Checkbox\n\t\t\t\t\t\t\t\tchecked={rememberMe}\n\t\t\t\t\t\t\t\tonChange={handleCheckRememberMe}\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\tcolor: \"rgba(255,255,255,0.5)\",\n\t\t\t\t\t\t\t\t\t\"&.Mui-checked\": {\n\t\t\t\t\t\t\t\t\t\tcolor: \"primary.main\",\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlabel={\n\t\t\t\t\t\t\t<Typography variant=\"body2\" sx={{ opacity: 0.7 }}>\n\t\t\t\t\t\t\t\tRemember me\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t}\n\t\t\t\t\t/>\n\n\t\t\t\t\t<div className=\"flex flex-column\" style={{ gap: \"1em\" }}>\n\t\t\t\t\t\t<LoadingButton\n\t\t\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t\tloading={handleLogin.isPending}\n\t\t\t\t\t\t\tsx={{ width: \"100%\", height: 48, borderRadius: 2 }}\n\t\t\t\t\t\t\tsize=\"large\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tLogin\n\t\t\t\t\t\t</LoadingButton>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tcolor=\"inherit\"\n\t\t\t\t\t\t\tvariant=\"text\"\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\tcolor: \"rgba(255,255,255,0.5)\",\n\t\t\t\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\t\t\t\tcolor: \"white\",\n\t\t\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tonClick={() => navigate({ to: \"/setup/server/list\" })}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tChange Server\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</form>\n\t\t\t</Paper>\n\t\t\t{server.isSuccess && server.data?.LoginDisclaimer && (\n\t\t\t\t<Paper\n\t\t\t\t\televation={0}\n\t\t\t\t\tsx={{\n\t\t\t\t\t\tpadding: \"1.5em\",\n\t\t\t\t\t\tborderRadius: \"16px\",\n\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\tmaxWidth: \"450px\",\n\t\t\t\t\t\tbackgroundColor: \"rgba(33, 150, 243, 0.1)\",\n\t\t\t\t\t\tborder: \"1px solid rgba(33, 150, 243, 0.2)\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"flex flex-row flex-align-center\"\n\t\t\t\t\t\tstyle={{ gap: \"0.5em\", marginBottom: \"0.5em\" }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tcolor: blue[400],\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tinfo\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<Typography variant=\"subtitle1\" fontWeight=\"bold\">\n\t\t\t\t\t\t\tNotice\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t</div>\n\t\t\t\t\t<Typography variant=\"body2\" style={{ opacity: 0.8, lineHeight: 1.6 }}>\n\t\t\t\t\t\t{server.data.LoginDisclaimer}\n\t\t\t\t\t</Typography>\n\t\t\t\t</Paper>\n\t\t\t)}\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/routes/_api/login.tsx",
    "content": "import AppBarBackOnly from \"@/components/appBar/backOnly\";\nimport { getUserApi } from \"@jellyfin/sdk/lib/utils/api/user-api\";\nimport { Outlet, createFileRoute, redirect } from \"@tanstack/react-router\";\nimport React from \"react\";\n\nexport const Route = createFileRoute(\"/_api/login\")({\n\tbeforeLoad: async ({ context, location }) => {\n\t\tif (location.pathname === \"/login\") {\n\t\t\tconst api = context.api;\n\t\t\tif (!api) {\n\t\t\t\tconsole.info(\"Awaiting Api generation.\");\n\t\t\t\t// throw redirect({ to: \"/login/manual\" });\n\t\t\t} else {\n\t\t\t\tconst publicUsers = (await getUserApi(api).getPublicUsers()).data;\n\t\t\t\tif (publicUsers.length > 0) {\n\t\t\t\t\tthrow redirect({ to: \"/login/list\" });\n\t\t\t\t}\n\t\t\t\tthrow redirect({ to: \"/login/manual\" });\n\t\t\t}\n\t\t}\n\t},\n\tcomponent: () => {\n\t\treturn (\n\t\t\t<>\n\t\t\t\t<AppBarBackOnly />\n\t\t\t\t<Outlet />\n\t\t\t</>\n\t\t);\n\t},\n});\n"
  },
  {
    "path": "src/routes/_api/person/$id.tsx",
    "content": "import { BaseItemKind, LocationType } from \"@jellyfin/sdk/lib/generated-client\";\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport { getUserLibraryApi } from \"@jellyfin/sdk/lib/utils/api/user-library-api\";\nimport Box from \"@mui/material/Box\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport Divider from \"@mui/material/Divider\";\nimport Tab from \"@mui/material/Tab\";\nimport Tabs from \"@mui/material/Tabs\";\nimport Typography from \"@mui/material/Typography\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport React, {\n\ttype ReactNode,\n\tuseLayoutEffect,\n\tuseRef,\n\tuseState,\n} from \"react\";\nimport LikeButton from \"@/components/buttons/likeButton\";\nimport { Card } from \"@/components/card/card\";\nimport ItemHeader from \"@/components/itemHeader\";\nimport { ErrorNotice } from \"@/components/notices/errorNotice/errorNotice\";\nimport ShowMoreText from \"@/components/showMoreText\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport \"./person.scss\";\n\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport IconLink from \"@/components/iconLink\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useCentralStore } from \"@/utils/store/central\";\n\ntype TabPanelProps = {\n\tchildren: ReactNode;\n\tindex: number;\n\tvalue: number;\n};\n\nfunction TabPanel(props: TabPanelProps) {\n\tconst { children, value, index, ...other } = props;\n\n\treturn (\n\t\t<div\n\t\t\trole=\"tabpanel\"\n\t\t\thidden={value !== index}\n\t\t\tid={`full-width-tabpanel-${index}`}\n\t\t\taria-labelledby={`full-width-tab-${index}`}\n\t\t\t{...other}\n\t\t\tstyle={{ marginTop: \"1em\" }}\n\t\t>\n\t\t\t{value === index && <div>{children}</div>}\n\t\t</div>\n\t);\n}\n\nexport const Route = createFileRoute(\"/_api/person/$id\")({\n\tcomponent: PersonTitlePage,\n});\n\nfunction PersonTitlePage() {\n\tconst { id } = Route.useParams();\n\tconst api = useApiInContext((s) => s.api);\n\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst item = useQuery({\n\t\tqueryKey: [\"item\", id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getUserLibraryApi(api).getItem({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\titemId: id,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: !!user?.Id,\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst personMovies = useQuery({\n\t\tqueryKey: [\"item\", id, \"personMovies\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tpersonIds: [id],\n\t\t\t\tincludeItemTypes: [BaseItemKind.Movie],\n\t\t\t\trecursive: true,\n\t\t\t\tsortBy: [\"PremiereDate\", \"ProductionYear\", \"SortName\"],\n\t\t\t\tsortOrder: [\"Descending\"],\n\t\t\t\texcludeLocationTypes: [LocationType.Virtual],\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tnetworkMode: \"always\",\n\t});\n\tconst personShows = useQuery({\n\t\tqueryKey: [\"item\", id, \"personShows\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tpersonIds: [id],\n\t\t\t\tincludeItemTypes: [BaseItemKind.Series],\n\t\t\t\trecursive: true,\n\t\t\t\tsortBy: [\"PremiereDate\", \"ProductionYear\", \"SortName\"],\n\t\t\t\tsortOrder: [\"Descending\"],\n\t\t\t\texcludeLocationTypes: [LocationType.Virtual],\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tnetworkMode: \"always\",\n\t});\n\tconst personBooks = useQuery({\n\t\tqueryKey: [\"item\", id, \"personBooks\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tpersonIds: [id],\n\t\t\t\tincludeItemTypes: [BaseItemKind.Book],\n\t\t\t\trecursive: true,\n\t\t\t\tsortBy: [\"PremiereDate\", \"ProductionYear\", \"SortName\"],\n\t\t\t\tsortOrder: [\"Descending\"],\n\t\t\t\texcludeLocationTypes: [LocationType.Virtual],\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tnetworkMode: \"always\",\n\t});\n\tconst personPhotos = useQuery({\n\t\tqueryKey: [\"item\", id, \"personPhotos\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tpersonIds: [id],\n\t\t\t\tincludeItemTypes: [BaseItemKind.Photo],\n\t\t\t\trecursive: true,\n\t\t\t\tsortBy: [\"PremiereDate\", \"ProductionYear\", \"SortName\"],\n\t\t\t\texcludeLocationTypes: [LocationType.Virtual],\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tnetworkMode: \"always\",\n\t});\n\tconst personEpisodes = useQuery({\n\t\tqueryKey: [\"item\", id, \"personEpisodes\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tpersonIds: [id],\n\t\t\t\tincludeItemTypes: [BaseItemKind.Episode],\n\t\t\t\trecursive: true,\n\t\t\t\tfields: [\"SeasonUserData\", \"Overview\"],\n\t\t\t\tsortBy: [\"PremiereDate\", \"ProductionYear\", \"SortName\"],\n\t\t\t\texcludeLocationTypes: [LocationType.Virtual],\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tnetworkMode: \"always\",\n\t});\n\n\tconst [activePersonTab, setActivePersonTab] = useState(0);\n\n\tconst personTabs = [\n\t\t{\n\t\t\ttitle: \"Movies\",\n\t\t\tdata: personMovies,\n\t\t\tqueryKey: [\"item\", id, \"personMovies\"],\n\t\t},\n\t\t{\n\t\t\ttitle: \"TV Shows\",\n\t\t\tdata: personShows,\n\t\t\tqueryKey: [\"item\", id, \"personShows\"],\n\t\t},\n\t\t{\n\t\t\ttitle: \"Books\",\n\t\t\tdata: personBooks,\n\t\t\tqueryKey: [\"item\", id, \"personBooks\"],\n\t\t},\n\t\t{\n\t\t\ttitle: \"Photos\",\n\t\t\tdata: personPhotos,\n\t\t\tqueryKey: [\"item\", id, \"personPhotos\"],\n\t\t},\n\t\t{\n\t\t\ttitle: \"Episodes\",\n\t\t\tdata: personEpisodes,\n\t\t\tqueryKey: [\"item\", id, \"personEpisodes\"],\n\t\t},\n\t];\n\n\tconst setBackdrop = useBackdropStore((s) => s.setBackdrop);\n\n\tuseLayoutEffect(() => {\n\t\tif (\n\t\t\tpersonMovies.isSuccess &&\n\t\t\tpersonShows.isSuccess &&\n\t\t\tpersonBooks.isSuccess &&\n\t\t\tpersonPhotos.isSuccess &&\n\t\t\tpersonEpisodes.isSuccess\n\t\t) {\n\t\t\tif (personMovies.data?.TotalRecordCount !== 0) {\n\t\t\t\tsetActivePersonTab(0);\n\t\t\t} else if (personShows.data?.TotalRecordCount !== 0) {\n\t\t\t\tsetActivePersonTab(1);\n\t\t\t} else if (personBooks.data?.TotalRecordCount !== 0) {\n\t\t\t\tsetActivePersonTab(2);\n\t\t\t} else if (personPhotos.data?.TotalRecordCount !== 0) {\n\t\t\t\tsetActivePersonTab(3);\n\t\t\t} else if (personEpisodes.data?.TotalRecordCount !== 0) {\n\t\t\t\tsetActivePersonTab(4);\n\t\t\t}\n\t\t}\n\t\tsetBackdrop(\"\", \"\");\n\t}, [\n\t\tpersonMovies.isSuccess,\n\t\tpersonShows.isSuccess,\n\t\tpersonBooks.isSuccess,\n\t\tpersonPhotos.isSuccess,\n\t\tpersonEpisodes.isSuccess,\n\t]);\n\n\tconst [animationDirection, setAnimationDirection] = useState(\"forward\");\n\n\tconst pageRef = useRef(null);\n\n\tif (item.isPending) {\n\t\treturn (\n\t\t\t<Box\n\t\t\t\tsx={{\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\theight: \"100vh\",\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<CircularProgress />\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tif (item.isSuccess && item.data) {\n\t\treturn (\n\t\t\t<motion.div\n\t\t\t\tkey={id}\n\t\t\t\tinitial={{\n\t\t\t\t\topacity: 0,\n\t\t\t\t}}\n\t\t\t\tanimate={{\n\t\t\t\t\topacity: 1,\n\t\t\t\t}}\n\t\t\t\ttransition={{\n\t\t\t\t\tduration: 0.25,\n\t\t\t\t\tease: \"easeInOut\",\n\t\t\t\t}}\n\t\t\t\tclassName=\"scrollY padded-top item item-person flex flex-column\"\n\t\t\t\tref={pageRef}\n\t\t\t>\n\t\t\t\t<ItemHeader item={item.data} api={api}>\n\t\t\t\t\t<div className=\"item-hero-buttons-container flex flex-row\">\n\t\t\t\t\t\t<div className=\"flex flex-row fullWidth\" />\n\t\t\t\t\t\t<div className=\"flex flex-row\" style={{ gap: \"1em\" }}>\n\t\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\t\titemName={item.data.Name}\n\t\t\t\t\t\t\t\titemId={item.data.Id}\n\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\tisFavorite={item.data.UserData?.IsFavorite}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</ItemHeader>\n\t\t\t\t<div className=\"item-detail\">\n\t\t\t\t\t<div style={{ width: \"100%\" }}>\n\t\t\t\t\t\t<ShowMoreText\n\t\t\t\t\t\t\tcontent={item.data.Overview ?? \"\"}\n\t\t\t\t\t\t\tcollapsedLines={4}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\tgap: \"0.6em\",\n\t\t\t\t\t\t\t\talignSelf: \"end\",\n\t\t\t\t\t\t\t\tmarginTop: \"1em\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.data.ExternalUrls?.map(\n\t\t\t\t\t\t\t\t(url) =>\n\t\t\t\t\t\t\t\t\turl.Name &&\n\t\t\t\t\t\t\t\t\turl.Url && <IconLink url={url.Url} name={url.Name} />,\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{item.data.PremiereDate && (\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<Typography variant=\"h6\">Birth</Typography>\n\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\" sx={{ opacity: 0.8 }}>\n\t\t\t\t\t\t\t\t\t{new Date(item.data.PremiereDate).toDateString()}\n\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{item.data.EndDate && (\n\t\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t\t<Typography variant=\"h6\" mt={2}>\n\t\t\t\t\t\t\t\t\tDeath\n\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t<Typography variant=\"subtitle2\" sx={{ opacity: 0.8 }}>\n\t\t\t\t\t\t\t\t\t{new Date(item.data.EndDate).toDateString()}\n\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"item-detail-person-container\">\n\t\t\t\t\t<div className=\"item-detail-person-header\">\n\t\t\t\t\t\t<Tabs\n\t\t\t\t\t\t\tvariant=\"scrollable\"\n\t\t\t\t\t\t\tvalue={activePersonTab}\n\t\t\t\t\t\t\tonChange={(_, newVal) => {\n\t\t\t\t\t\t\t\tif (newVal > activePersonTab) {\n\t\t\t\t\t\t\t\t\tsetAnimationDirection(\"forward\");\n\t\t\t\t\t\t\t\t} else if (newVal < activePersonTab) {\n\t\t\t\t\t\t\t\t\tsetAnimationDirection(\"backwards\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tsetActivePersonTab(newVal);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{personTabs.map((tab) => {\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<Tab\n\t\t\t\t\t\t\t\t\t\tkey={tab.title}\n\t\t\t\t\t\t\t\t\t\tlabel={tab.title}\n\t\t\t\t\t\t\t\t\t\tdisabled={tab.data.data?.TotalRecordCount === 0}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t</Tabs>\n\t\t\t\t\t\t<Divider />\n\t\t\t\t\t</div>\n\t\t\t\t\t<AnimatePresence>\n\t\t\t\t\t\t{personTabs.map((tab, index) => {\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<TabPanel value={activePersonTab} index={index} key={tab.title}>\n\t\t\t\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\t\t\t\tclassName={`item-detail-person-cards ${\n\t\t\t\t\t\t\t\t\t\t\ttab.title === \"Movies\" ||\n\t\t\t\t\t\t\t\t\t\t\ttab.title === \"TV Shows\" ||\n\t\t\t\t\t\t\t\t\t\t\ttab.title === \"Books\"\n\t\t\t\t\t\t\t\t\t\t\t\t? \"col-7\"\n\t\t\t\t\t\t\t\t\t\t\t\t: \"col-4\"\n\t\t\t\t\t\t\t\t\t\t}`}\n\t\t\t\t\t\t\t\t\t\tkey={tab.queryKey.join(\"\")}\n\t\t\t\t\t\t\t\t\t\tinitial={{\n\t\t\t\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t\t\t\t\ttransform:\n\t\t\t\t\t\t\t\t\t\t\t\tanimationDirection === \"forward\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t? \"translate(30px)\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t: \"translate(-30px)\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tanimate={{\n\t\t\t\t\t\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\t\t\t\t\t\ttransform: \"translate(0px)\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\ttransition={{\n\t\t\t\t\t\t\t\t\t\t\tduration: 0.2,\n\t\t\t\t\t\t\t\t\t\t\tease: \"anticipate\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{tab.data.isSuccess &&\n\t\t\t\t\t\t\t\t\t\t\ttab.data.data?.Items?.map((tabitem) => {\n\t\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tkey={tabitem.Id}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem={tabitem}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcardTitle={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttabitem.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? tabitem.SeriesName\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: tabitem.Name\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcardCaption={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttabitem.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? `S${tabitem.ParentIndexNumber}:E${tabitem.IndexNumber} - ${tabitem.Name}`\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: tabitem.Type === BaseItemKind.Series\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? `${tabitem.ProductionYear} - ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttabitem.EndDate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? new Date(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttabitem.EndDate,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t).toLocaleString([], {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tyear: \"numeric\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"Present\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}`\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: tabitem.ProductionYear\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tcardType={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttabitem.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? \"thumb\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"portrait\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tqueryKey={tab.queryKey}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t</motion.div>\n\t\t\t\t\t\t\t\t</TabPanel>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</AnimatePresence>\n\t\t\t\t</div>\n\t\t\t</motion.div>\n\t\t);\n\t}\n\tif (item.isError) {\n\t\treturn <ErrorNotice />;\n\t}\n}\n\nexport default PersonTitlePage;\n"
  },
  {
    "path": "src/routes/_api/person/person.scss",
    "content": "$cardWidth: 18%;\n.item-detail-person {\n\t&-container {\n\t\tposition: relative;\n\t\twidth: 100%;\n\t}\n\t&-header {\n\t\twidth: 100%;\n\t}\n\t&-cards {\n\t\tdisplay: grid;\n\t\talign-items: start;\n\t}\n}\n\n.col-7 {\n\tgrid-template-columns: repeat(7, minmax(0, 1fr));\n}\n.col-4 {\n\tgrid-template-columns: repeat(4, minmax(0, 1fr));\n}\n\n.item-person.item {\n\tgap: 1em;\n\t\n\t\n\t.item-detail {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: 1fr 35%;\n\t\tjustify-items: center;\n\t\talign-items: start;\n\t\tgap: 3em;\n\t\t\n\t\t&-cast {\n\t\t\t&-container {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\tgap: 1em;\n\t\t\t}\n\t\t\t&-grid {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: repeat(5, minmax(0, 1fr));\n\t\t\t\tgap: 1em;\n\t\t\t\tmargin-bottom: 1em;\n\t\t\t}\n\t\t\t&-card {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: 6em 1fr;\n\t\t\t\tgap: 0.5em;\n\t\t\t\talign-items: center;\n\t\t\t\tcolor: white !important;\n\t\t\t\ttext-decoration: none;\n\t\t\t\tpadding: 0.5em;\n\t\t\t\tbackground: rgb(255 255 255 / 0);\n\t\t\t\ttransition: background $transition-time-default;\n\t\t\t\tborder-radius: $border-radius-default;\n\t\t\t\t&:hover {\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t}\n\t\t\t\t&-image,\n\t\t\t\t&-icon {\n\t\t\t\t\twidth: 6em;\n\t\t\t\t\theight: 6em;\n\t\t\t\t\tobject-fit: cover;\n\t\t\t\t\tborder-radius: 100%;\n\t\t\t\t\tbox-shadow: $shadow-card-image;\n\t\t\t\t}\n\t\t\t\t&-icon {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tfont-size: 1em;\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t\t.material-symbols-rounded {\n\t\t\t\t\t\tfont-size: 3em;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t&-link {\n\t\t\tcolor: white;\n\t\t\ttext-decoration-color: rgb(255 255 255 / 0.5);\n\t\t\t&:hover {\n\t\t\t\ttext-decoration-color: white;\n\t\t\t}\n\t\t}\n\t}\n}\n\n"
  },
  {
    "path": "src/routes/_api/player/audio.scss",
    "content": ".audio {\n    &-background {\n        position: fixed;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        z-index: -1;\n        overflow: hidden;\n        \n        &::after {\n            content: \"\";\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 100%;\n            background: rgba(0, 0, 0, 0.6); // Dark overlay\n            backdrop-filter: blur(60px);\n            -webkit-backdrop-filter: blur(60px);\n        }\n        \n        &-image {\n            width: 100%;\n            height: 100%;\n            object-fit: cover;\n            transform: scale(1.1); // Prevent white edges from blur\n        }\n    }\n    \n    &-container {\n        display: flex;\n        flex-direction: column;\n        height: 100vh;\n        width: 100%;\n        padding: 1rem;\n        gap: 1rem;\n        box-sizing: border-box;\n        position: relative;\n        z-index: 1;\n        color: white;\n        overflow: hidden;\n        \n        @media (min-width: 900px) {\n            flex-direction: row;\n            align-items: center;\n            justify-content: center;\n            padding: 4rem;\n            gap: 3rem;\n            max-width: 1600px;\n            margin: 0 auto;\n        }\n    }\n    \n    &-left-column {\n        flex: 1;\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        justify-content: center;\n        gap: 1.5rem;\n        width: 100%;\n        max-width: 500px;\n        margin: 0 auto;\n        min-height: 0; // Allow shrinking\n        \n        @media (min-width: 900px) {\n            gap: 2rem;\n        }\n    }\n    \n    &-right-column {\n        flex: 1;\n        display: flex;\n        flex-direction: column;\n        height: 100%;\n        width: 100%;\n        max-width: 600px;\n        overflow: hidden;\n        @include glass-effect($blur: 20px, $bg-color: rgba(255, 255, 255, 0.05));\n        border-radius: 1.5rem;\n        padding: 0;\n        min-height: 0; // Allow shrinking\n    }\n    \n    &-right-content {\n        flex: 1;\n        padding: 1.5rem;\n        overflow-y: auto;\n        overflow-x: hidden;\n        \n        &::-webkit-scrollbar {\n            width: 6px;\n        }\n        &::-webkit-scrollbar-track {\n            background: transparent;\n        }\n        &::-webkit-scrollbar-thumb {\n            background: rgba(255, 255, 255, 0.2);\n            border-radius: 3px;\n        }\n        &::-webkit-scrollbar-thumb:hover {\n            background: rgba(255, 255, 255, 0.3);\n        }\n    }\n    \n    &-info {\n        width: 100%;\n        display: flex;\n        flex-direction: column;\n        align-items: flex-start; // Left align text\n        text-align: left;\n        gap: 0.5em;\n        \n        @media (max-width: 900px) {\n            align-items: center;\n            text-align: center;\n        }\n        \n        &-image {\n            width: 100%;\n            aspect-ratio: 1;\n            height: 100%;\n            object-fit: cover;\n            border-radius: 1.5rem;\n            box-shadow: 0 20px 50px rgba(0,0,0,0.5);\n            transition: transform 0.3s ease;\n            \n            &-container {\n                width: 100%;\n                aspect-ratio: 1;\n                position: relative;\n                border-radius: 1.5rem;\n                overflow: hidden;\n                margin-bottom: 1rem;\n                \n                @media (max-width: 900px) {\n                    max-width: 70vw;\n                    max-height: 35vh;\n                    margin: 0 auto 1rem auto;\n                }\n            }\n            \n            &-icon {\n                font-size: 8em;\n                display: flex;\n                justify-content: center;\n                align-items: center;\n                height: 100%;\n                width: 100%;\n                background: rgba(255, 255, 255, 0.1);\n                border-radius: 1.5rem;\n            }\n        }\n        &-controls {\n            width: 100%;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            gap: 1.5em;\n            margin-top: 1rem;\n        }\n    }\n\n    &-queue {\n        display: flex;\n        flex-direction: column;\n        width: 100%;\n        align-items: stretch; // Ensure items take full width\n        \n        // Remove old track styles as we now use QueueListItem\n    }\n\n    &-lyrics {\n        padding: 1rem;\n        text-align: center;\n        width: 100%;\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        min-height: 100%;\n        \n        &-container {\n            width: 100%;\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            padding-bottom: 4rem; // Space for unsynced notice\n        }\n\n        &-status {\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            height: 100%;\n            width: 100%;\n            opacity: 0.7;\n            min-height: 200px;\n        }\n        \n        &-line {\n            margin-bottom: 1.5rem;\n            opacity: 0.5;\n            transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);\n            border-radius: 1rem;\n            padding: 0.75rem 1.5rem;\n            cursor: pointer;\n            font-size: 1.5rem !important;\n            line-height: 1.6 !important;\n            font-weight: 600 !important;\n            max-width: 90%;\n\n            &:hover {\n                opacity: 0.8;\n                background: rgba(255, 255, 255, 0.05);\n            }\n\n            &[data-active-lyric=\"true\"] {\n                opacity: 1;\n                font-weight: 800 !important;\n                transform: scale(1.05);\n                text-shadow: 0 0 30px rgba(255,255,255,0.3);\n                color: var(--primary-color, #90caf9);\n            }\n        }\n\n        &-unsynced-notice {\n            position: sticky;\n            bottom: 1rem;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            gap: 0.5rem;\n            padding: 0.5rem 1rem;\n            background: rgba(0, 0, 0, 0.4);\n            backdrop-filter: blur(10px);\n            border-radius: 2rem;\n            margin-top: auto;\n            opacity: 0.8;\n            \n            .material-symbols-rounded {\n                font-size: 1.2rem;\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/routes/_api/player/audio.tsx",
    "content": "import { Box, Tab, Tabs, Typography } from \"@mui/material\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport {\n\tsetIsMuted,\n\tsetVolume,\n\tuseAudioPlayback,\n} from \"@/utils/store/audioPlayback\";\nimport useQueue from \"@/utils/store/queue\";\n\nimport \"./audio.scss\";\n\nimport type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport LyricsPanel from \"@/components/playback/audioPlayer/components/LyricsPanel\";\nimport PlayerControls from \"@/components/playback/audioPlayer/components/PlayerControls\";\nimport PlayerProgress from \"@/components/playback/audioPlayer/components/PlayerProgress\";\nimport PlayerVolume from \"@/components/playback/audioPlayer/components/PlayerVolume\";\nimport QueuePanel from \"@/components/playback/audioPlayer/components/QueuePanel\";\nimport StatsPanel from \"@/components/playback/audioPlayer/components/StatsPanel\";\nimport { secToTicks, ticksToSec } from \"@/utils/date/time\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\n\nexport const Route = createFileRoute(\"/_api/player/audio\")({\n\tcomponent: AudioPlayerRoute,\n});\n\nconst SEEK_AMOUNT = 10;\n\nconst AudioInfo = React.memo(\n\t({\n\t\titem,\n\t\timageUrl,\n\t}: {\n\t\titem: BaseItemDto | undefined | null;\n\t\timageUrl: string | undefined;\n\t}) => (\n\t\t<>\n\t\t\t<div className=\"audio-info-image-container\">\n\t\t\t\t{imageUrl ? (\n\t\t\t\t\t<img\n\t\t\t\t\t\talt={item?.Name ?? \"Music\"}\n\t\t\t\t\t\tsrc={imageUrl}\n\t\t\t\t\t\tclassName=\"audio-info-image\"\n\t\t\t\t\t/>\n\t\t\t\t) : (\n\t\t\t\t\t<div className=\"audio-info-image-icon\">\n\t\t\t\t\t\t<span\n\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\tstyle={{ fontSize: \"inherit\" }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tmusic_note\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t</div>\n\n\t\t\t<div className=\"audio-info\">\n\t\t\t\t<Typography\n\t\t\t\t\tvariant=\"h4\"\n\t\t\t\t\tfontWeight={700}\n\t\t\t\t\tnoWrap\n\t\t\t\t\tstyle={{ width: \"100%\", marginBottom: \"0.2em\" }}\n\t\t\t\t>\n\t\t\t\t\t{item?.Name}\n\t\t\t\t</Typography>\n\t\t\t\t<Typography\n\t\t\t\t\tvariant=\"h6\"\n\t\t\t\t\tfontWeight={400}\n\t\t\t\t\tclassName=\"opacity-07\"\n\t\t\t\t\tnoWrap\n\t\t\t\t\tstyle={{ width: \"100%\" }}\n\t\t\t\t>\n\t\t\t\t\t{item?.Artists?.join(\", \")}\n\t\t\t\t</Typography>\n\t\t\t</div>\n\t\t</>\n\t),\n);\n\nfunction AudioPlayerRoute() {\n\tconst api = useApiInContext((s) => s.api);\n\tconst [queue, currentTrack] = useQueue((s) => [s.tracks, s.currentItemIndex]);\n\tconst [item, audioPlayerRef, volume, isMuted] = useAudioPlayback((s) => [\n\t\ts.item,\n\t\ts.player.ref,\n\t\ts.player.volume,\n\t\ts.player.isMuted,\n\t]);\n\n\tconst [progress, setProgress] = useState(0);\n\tconst [playing, setPlaying] = useState(false);\n\tconst [tabValue, setTabValue] = useState(0);\n\n\tuseEffect(() => {\n\t\tconst player = audioPlayerRef?.current;\n\t\tif (!player) return;\n\n\t\tconst updateProgress = () => setProgress(player.currentTime);\n\t\tconst updatePlaying = () => setPlaying(!player.paused);\n\n\t\tplayer.addEventListener(\"timeupdate\", updateProgress);\n\t\tplayer.addEventListener(\"play\", updatePlaying);\n\t\tplayer.addEventListener(\"pause\", updatePlaying);\n\n\t\t// Initial state\n\t\tsetProgress(player.currentTime);\n\t\tsetPlaying(!player.paused);\n\n\t\treturn () => {\n\t\t\tplayer.removeEventListener(\"timeupdate\", updateProgress);\n\t\t\tplayer.removeEventListener(\"play\", updatePlaying);\n\t\t\tplayer.removeEventListener(\"pause\", updatePlaying);\n\t\t};\n\t}, [audioPlayerRef?.current, item?.Id]);\n\n\tconst handlePlayPause = () => {\n\t\tconst player = audioPlayerRef?.current;\n\t\tif (player) {\n\t\t\tif (player.paused) {\n\t\t\t\tplayer.play();\n\t\t\t} else {\n\t\t\t\tplayer.pause();\n\t\t\t}\n\t\t}\n\t};\n\n\tconst handleSeek = (value: number) => {\n\t\tsetProgress(ticksToSec(value));\n\t};\n\n\tconst handleSeekCommit = (value: number) => {\n\t\tconst player = audioPlayerRef?.current;\n\t\tif (player) {\n\t\t\tplayer.currentTime = ticksToSec(value);\n\t\t\tsetProgress(ticksToSec(value));\n\t\t}\n\t};\n\n\tconst handleRewind = () => {\n\t\tconst player = audioPlayerRef?.current;\n\t\tif (player) {\n\t\t\tplayer.currentTime -= SEEK_AMOUNT;\n\t\t}\n\t};\n\n\tconst handleForward = () => {\n\t\tconst player = audioPlayerRef?.current;\n\t\tif (player) {\n\t\t\tplayer.currentTime += SEEK_AMOUNT;\n\t\t}\n\t};\n\tconst handleTabChange = (_: React.SyntheticEvent, newValue: number) => {\n\t\tsetTabValue(newValue);\n\t};\n\n\tconst imageUrl = useMemo(\n\t\t() =>\n\t\t\titem?.ImageTags?.Primary\n\t\t\t\t? api &&\n\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(item.Id ?? \"\", \"Primary\", {\n\t\t\t\t\t\ttag: item.ImageTags.Primary,\n\t\t\t\t\t})\n\t\t\t\t: undefined,\n\t\t[item?.Id, item?.ImageTags?.Primary, api],\n\t);\n\n\treturn (\n\t\t<>\n\t\t\t<div className=\"audio-background\">\n\t\t\t\t{imageUrl && (\n\t\t\t\t\t<img src={imageUrl} alt=\"\" className=\"audio-background-image\" />\n\t\t\t\t)}\n\t\t\t</div>\n\t\t\t<div className=\"audio-container\" key={item?.Id}>\n\t\t\t\t<div className=\"audio-left-column\">\n\t\t\t\t\t<AudioInfo item={item} imageUrl={imageUrl} />\n\n\t\t\t\t\t<PlayerProgress\n\t\t\t\t\t\tprogress={secToTicks(progress)}\n\t\t\t\t\t\tduration={item?.RunTimeTicks ?? 1}\n\t\t\t\t\t\tonSeek={handleSeek}\n\t\t\t\t\t\tonSeekCommit={handleSeekCommit}\n\t\t\t\t\t/>\n\n\t\t\t\t\t<div className=\"audio-info-controls\">\n\t\t\t\t\t\t<PlayerControls\n\t\t\t\t\t\t\tplaying={playing}\n\t\t\t\t\t\t\tonPlayPause={handlePlayPause}\n\t\t\t\t\t\t\tsize=\"large\"\n\t\t\t\t\t\t\tonRewind={handleRewind}\n\t\t\t\t\t\t\tonForward={handleForward}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"audio-info-volume\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<PlayerVolume\n\t\t\t\t\t\t\tvolume={volume}\n\t\t\t\t\t\t\tisMuted={isMuted}\n\t\t\t\t\t\t\tonVolumeChange={setVolume}\n\t\t\t\t\t\t\tonMuteToggle={() => setIsMuted(!isMuted)}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"audio-right-column\">\n\t\t\t\t\t<Box sx={{ borderBottom: 1, borderColor: \"rgba(255,255,255,0.1)\" }}>\n\t\t\t\t\t\t<Tabs\n\t\t\t\t\t\t\tvalue={tabValue}\n\t\t\t\t\t\t\tonChange={handleTabChange}\n\t\t\t\t\t\t\taria-label=\"audio player tabs\"\n\t\t\t\t\t\t\tvariant=\"fullWidth\"\n\t\t\t\t\t\t\tscrollButtons=\"auto\"\n\t\t\t\t\t\t\tallowScrollButtonsMobile\n\t\t\t\t\t\t\ttextColor=\"inherit\"\n\t\t\t\t\t\t\tindicatorColor=\"primary\"\n\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\"& .MuiTab-root\": {\n\t\t\t\t\t\t\t\t\tcolor: \"rgba(255,255,255,0.5)\",\n\t\t\t\t\t\t\t\t\t\"&.Mui-selected\": { color: \"white\" },\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<Tab label=\"Queue\" />\n\t\t\t\t\t\t\t<Tab label=\"Lyrics\" />\n\t\t\t\t\t\t\t<Tab label=\"Stats\" />\n\t\t\t\t\t\t</Tabs>\n\t\t\t\t\t</Box>\n\n\t\t\t\t\t<div className=\"audio-right-content\">\n\t\t\t\t\t\t{tabValue === 0 && queue && (\n\t\t\t\t\t\t\t<QueuePanel queue={queue} currentTrackIndex={currentTrack} />\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{tabValue === 1 && (\n\t\t\t\t\t\t\t<LyricsPanel item={item} api={api} currentTime={progress} />\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{tabValue === 2 && (\n\t\t\t\t\t\t\t<StatsPanel item={item} audioRef={audioPlayerRef} />\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</>\n\t);\n}\n\n"
  },
  {
    "path": "src/routes/_api/player/index.tsx",
    "content": "import { getPlaystateApi } from \"@jellyfin/sdk/lib/utils/api/playstate-api\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport { WebviewWindow as appWindow } from \"@tauri-apps/api/webviewWindow\";\nimport React, { useCallback, useEffect, useRef } from \"react\";\nimport ReactPlayer from \"react-player\";\nimport { secToTicks, ticksToSec } from \"@/utils/date/time\";\nimport { playItemFromQueue, usePlaybackStore } from \"@/utils/store/playback\";\n\nimport \"./videoPlayer.scss\";\n\nimport { RepeatMode } from \"@jellyfin/sdk/lib/generated-client\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { createFileRoute, useRouter } from \"@tanstack/react-router\";\nimport JASSUB from \"jassub\";\n// @ts-expect-error\nimport wasmUrl from \"jassub/dist/wasm/jassub-worker.wasm?url\"; // non-SIMD fallback\n// @ts-expect-error\nimport modernWasmUrl from \"jassub/dist/wasm/jassub-worker-modern.wasm?url\"; // SIMD\n// @ts-expect-error\nimport workerUrl from \"jassub/dist/worker/worker.js?url\"; // Web Worker\nimport { PgsRenderer } from \"libpgs\";\n//@ts-expect-error\nimport pgsWorkerUrl from \"libpgs/dist/libpgs.worker.js?url\";\n// import type { OnProgressProps } from \"react-player\";\nimport { useShallow } from \"zustand/shallow\";\nimport SkipSegmentButton from \"@/components/playback/videoPlayer/buttons/SkipSegmentButton\";\nimport VideoPlayerControls from \"@/components/playback/videoPlayer/controls\";\nimport ErrorDisplay from \"@/components/playback/videoPlayer/ErrorDisplay\";\nimport LoadingIndicator from \"@/components/playback/videoPlayer/LoadingIndicator\";\nimport StatsForNerds from \"@/components/playback/videoPlayer/StatsForNerds\";\nimport UpNextFlyout from \"@/components/playback/videoPlayer/upNextFlyout\";\nimport VolumeChangeOverlay from \"@/components/playback/videoPlayer/VolumeChangeOverlay\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport useQueue, { clearQueue } from \"@/utils/store/queue\";\nimport type subtitlePlaybackInfo from \"@/utils/types/subtitlePlaybackInfo\";\n//@ts-expect-error\nimport font from \"./Noto-Sans-Indosphere.ttf?url\";\n\n/**\n * This function is used to add a subtitle track (.vtt and .srt) to the react player instance.\n */\nfunction addSubtitleTrackToReactPlayer(\n\tvideoElem: HTMLMediaElement,\n\tsubtitleTracks: subtitlePlaybackInfo,\n\tbaseUrl: string,\n\tapiKey?: string,\n) {\n\tif (subtitleTracks.url && subtitleTracks.allTracks) {\n\t\tconst reqSubTrack = subtitleTracks.allTracks.find(\n\t\t\t(val) => val.Index === subtitleTracks.track,\n\t\t);\n\t\tif (!reqSubTrack) return;\n\n\t\t// Remove existing tracks to ensure clean state\n\t\tconst existingTracks = videoElem.querySelectorAll(\"track\");\n\t\texistingTracks.forEach((t) => {\n\t\t\tt.remove();\n\t\t});\n\n\t\tconst track = document.createElement(\"track\");\n\t\tconst separator = subtitleTracks.url.includes(\"?\") ? \"&\" : \"?\";\n\t\tconst urlParams = apiKey ? `${separator}api_key=${apiKey}` : \"\";\n\t\ttrack.src = `${baseUrl}${subtitleTracks.url}${urlParams}`;\n\t\ttrack.kind = \"subtitles\";\n\t\ttrack.srclang = reqSubTrack.Language ?? \"en\";\n\t\ttrack.label = reqSubTrack.DisplayTitle ?? \"Subtitle\";\n\t\ttrack.default = true;\n\t\ttrack.id = subtitleTracks.url;\n\n\t\ttrack.addEventListener(\"load\", () => {\n\t\t\tconst a = track.track;\n\t\t\ta.mode = \"showing\";\n\t\t});\n\t\ttrack.addEventListener(\"error\", (e) => {\n\t\t\tconsole.error(\"Error loading subtitle track\", e);\n\t\t});\n\n\t\tvideoElem.appendChild(track);\n\t\tif (track.track) {\n\t\t\ttrack.track.mode = \"showing\";\n\t\t}\n\t}\n}\n\nexport function VideoPlayer() {\n\tconst api = useApiInContext((s) => s.api);\n\tconst { history } = useRouter();\n\tconst player = useRef<HTMLVideoElement | null>(null);\n\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst {\n\t\titemId,\n\t\tuserDataLastPlayedPositionTicks,\n\t\t// mediaSegments,\n\t\tsetCurrentTime,\n\t\tvolume,\n\t\tisPlayerPlaying,\n\t\tisPlayerMuted,\n\t\tisPlayerReady,\n\t\tsetPlayerReady,\n\t\tmediaSource,\n\t\tplaysessionId,\n\t\ttoggleIsPlaying,\n\t\ttoggleIsPlayerMuted,\n\t\t// nextSegmentIndex,\n\t\t// activeSegmentId,\n\t\tclearActiveSegment,\n\t\t// setActiveSegment,\n\t\tplaybackStream,\n\t\tsetIsBuffering,\n\t\tregisterPlayerActions,\n\t\ttoggleIsPlayerFullscreen,\n\t\tsetIsUserHovering,\n\t\thandleOnSeek,\n\t\titemName,\n\t\tisEpisode,\n\t\tepisodeTitle,\n\t\tinitializeVolume,\n\t} = usePlaybackStore(\n\t\tuseShallow((state) => ({\n\t\t\titemId: state.metadata.item?.Id,\n\t\t\tuserDataLastPlayedPositionTicks:\n\t\t\t\tstate.metadata.userDataLastPlayedPositionTicks,\n\t\t\t// mediaSegments: state.metadata.mediaSegments,\n\t\t\tsetCurrentTime: state.setCurrentTime,\n\t\t\tvolume: state.playerState.volume,\n\t\t\tisPlayerPlaying: state.playerState.isPlayerPlaying,\n\t\t\tisPlayerMuted: state.playerState.isPlayerMuted,\n\t\t\tisPlayerReady: state.playerState.isPlayerReady,\n\t\t\titemName: state.metadata.item?.Name,\n\t\t\tisEpisode: state.metadata.isEpisode,\n\t\t\tepisodeTitle: state.metadata.episodeTitle,\n\n\t\t\tsetPlayerReady: state.setPlayerReady,\n\t\t\tmediaSource: state.mediaSource,\n\t\t\tplaysessionId: state.playsessionId,\n\t\t\ttoggleIsPlaying: state.toggleIsPlaying,\n\t\t\ttoggleIsPlayerMuted: state.toggleIsPlayerMuted,\n\t\t\ttoggleIsPlayerFullscreen: state.toggleIsPlayerFullscreen,\n\t\t\t// nextSegmentIndex: state.nextSegmentIndex,\n\t\t\t// activeSegmentId: state.activeSegmentId,\n\t\t\tclearActiveSegment: state.clearActiveSegment,\n\t\t\t// setActiveSegment: state.setActiveSegment,\n\t\t\tplaybackStream: state.playbackStream,\n\t\t\tsetIsBuffering: state.setIsBuffering,\n\t\t\tregisterPlayerActions: state.registerPlayerActions,\n\t\t\tsetIsUserHovering: state.setIsUserHovering,\n\t\t\thandleOnSeek: state.handleOnSeek,\n\t\t\tinitializeVolume: state.initializeVolume,\n\t\t})),\n\t);\n\n\tuseEffect(() => {\n\t\tinitializeVolume();\n\t}, [initializeVolume]);\n\n\tconst [currentQueueItemIndex, queue] = useQueue((s) => [\n\t\ts.currentItemIndex,\n\t\ts.tracks,\n\t]);\n\n\tconst [error, setError] = React.useState<any>(null);\n\n\tconst setBackdrop = useBackdropStore(useShallow((s) => s.setBackdrop));\n\tuseEffect(() => setBackdrop(\"\"), []);\n\n\tconst handlePlayNext = useMutation({\n\t\tmutationKey: [\"playNextButton\"],\n\t\tmutationFn: () => playItemFromQueue(\"next\", user?.Id, api),\n\t\tonError: (error) => console.error(error),\n\t});\n\tconst handlePlayPrev = useMutation({\n\t\tmutationKey: [\"playPreviousButton\"],\n\t\tmutationFn: () => playItemFromQueue(\"previous\", user?.Id, api),\n\t\tonError: (error) => [console.error(error)],\n\t});\n\n\tconst handleReady = async () => {\n\t\tif (api && !isPlayerReady && player.current) {\n\t\t\tplayer.current.currentTime = ticksToSec(\n\t\t\t\tuserDataLastPlayedPositionTicks ?? 0,\n\t\t\t);\n\n\t\t\tif (navigator.mediaSession && mediaSource) {\n\t\t\t\tnavigator.mediaSession.metadata = new MediaMetadata({\n\t\t\t\t\ttitle: isEpisode\n\t\t\t\t\t\t? `${itemName} - ${episodeTitle}`\n\t\t\t\t\t\t: (itemName ?? \"Blink\"),\n\t\t\t\t\talbum: isEpisode ? (itemName ?? \"Blink\") : undefined,\n\t\t\t\t\tartwork: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tsrc: getImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\titemId ?? \"\",\n\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\tsizes: \"512x512\",\n\t\t\t\t\t\t\ttype: \"image/png\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t});\n\t\t\t\tnavigator.mediaSession.setActionHandler(\"play\", () => {\n\t\t\t\t\ttoggleIsPlaying();\n\t\t\t\t});\n\t\t\t\tnavigator.mediaSession.setActionHandler(\"pause\", () => {\n\t\t\t\t\ttoggleIsPlaying();\n\t\t\t\t});\n\t\t\t\tnavigator.mediaSession.setActionHandler(\"stop\", () => {\n\t\t\t\t\thandleExitPlayer();\n\t\t\t\t});\n\t\t\t\tnavigator.mediaSession.setActionHandler(\"nexttrack\", () => {\n\t\t\t\t\tif (queue?.length !== currentQueueItemIndex + 1) {\n\t\t\t\t\t\thandlePlayNext.mutate();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconsole.warn(\"No next item in the queue\");\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\tnavigator.mediaSession.setActionHandler(\"previoustrack\", () => {\n\t\t\t\t\tif (currentQueueItemIndex > 0) {\n\t\t\t\t\t\thandlePlayPrev.mutate();\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconsole.warn(\"No previous item in the queue\");\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tconsole.log(mediaSource.subtitle);\n\n\t\t\tregisterPlayerActions({\n\t\t\t\tseekTo: (seconds: number) => {\n\t\t\t\t\tconst internalPlayer = player.current;\n\t\t\t\t\tif (internalPlayer) {\n\t\t\t\t\t\tinternalPlayer.currentTime = seconds;\n\t\t\t\t\t\tconsole.log(\"Seeking to\", seconds, \"seconds\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tconsole.warn(\"ReactPlayer is not ready yet\");\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tgetCurrentTime: () => {\n\t\t\t\t\tconst internalPlayer = player.current;\n\t\t\t\t\tif (internalPlayer) {\n\t\t\t\t\t\treturn internalPlayer.currentTime ?? 0;\n\t\t\t\t\t}\n\t\t\t\t\tconsole.warn(\"ReactPlayer is not ready yet\");\n\t\t\t\t\treturn 0;\n\t\t\t\t},\n\t\t\t});\n\t\t\tsetPlayerReady(true);\n\n\t\t\t// Report Jellyfin server: Playback has begin\n\t\t\tgetPlaystateApi(api).reportPlaybackStart({\n\t\t\t\tplaybackStartInfo: {\n\t\t\t\t\tAudioStreamIndex: mediaSource.audioTrack,\n\t\t\t\t\tCanSeek: true,\n\t\t\t\t\tIsMuted: false,\n\t\t\t\t\tIsPaused: false,\n\t\t\t\t\tItemId: itemId,\n\t\t\t\t\tMediaSourceId: mediaSource.id,\n\t\t\t\t\tPlayMethod: mediaSource.playMethod,\n\t\t\t\t\tPlaySessionId: playsessionId,\n\t\t\t\t\tPlaybackStartTimeTicks: userDataLastPlayedPositionTicks,\n\t\t\t\t\tPositionTicks: userDataLastPlayedPositionTicks,\n\t\t\t\t\tRepeatMode: RepeatMode.RepeatNone,\n\t\t\t\t\tVolumeLevel: Math.floor(volume * 100),\n\t\t\t\t},\n\t\t\t});\n\t\t}\n\t};\n\n\tconst lastReportTimeRef = useRef<number>(0);\n\n\tconst handleTimeUpdate = useCallback(async () => {\n\t\tif (!player.current) return;\n\n\t\tconst currentTime = player.current.currentTime ?? 0;\n\n\t\tconst ticks = secToTicks(currentTime);\n\t\tconst now = Date.now();\n\n\t\tsetCurrentTime(ticks);\n\n\t\tif (!api) {\n\t\t\tconsole.warn(\"API is not available, cannot report playback progress.\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Report to server every 10s\n\t\tif (now - lastReportTimeRef.current < 10000) {\n\t\t\treturn;\n\t\t}\n\t\tlastReportTimeRef.current = now;\n\n\t\t// Report Jellyfin server: Playback progress\n\t\tgetPlaystateApi(api).reportPlaybackProgress({\n\t\t\tplaybackProgressInfo: {\n\t\t\t\tAudioStreamIndex: mediaSource.audioTrack,\n\t\t\t\tCanSeek: true,\n\t\t\t\tIsMuted: isPlayerMuted,\n\t\t\t\tIsPaused: !isPlayerPlaying,\n\t\t\t\tItemId: itemId,\n\t\t\t\tMediaSourceId: mediaSource.id,\n\t\t\t\tPlayMethod: mediaSource.playMethod,\n\t\t\t\tPlaySessionId: playsessionId,\n\t\t\t\tPlaybackStartTimeTicks: userDataLastPlayedPositionTicks,\n\t\t\t\tPositionTicks: ticks,\n\t\t\t\tRepeatMode: RepeatMode.RepeatNone,\n\t\t\t\tVolumeLevel: Math.floor(volume * 100),\n\t\t\t},\n\t\t});\n\t}, [\n\t\tsetCurrentTime,\n\t\tapi,\n\t\tmediaSource,\n\t\tisPlayerMuted,\n\t\tisPlayerPlaying,\n\t\titemId,\n\t\tplaysessionId,\n\t\tuserDataLastPlayedPositionTicks,\n\t\tvolume,\n\t]);\n\n\tconst handlePause = useCallback(async () => {\n\t\tif (!api) {\n\t\t\tconsole.warn(\"API is not available, cannot report playback paused.\");\n\t\t\treturn;\n\t\t}\n\t\tlet currentTime = 0;\n\t\tif (player.current) {\n\t\t\tcurrentTime = player.current.currentTime ?? 0;\n\t\t}\n\t\t// Report Jellyfin server: Playback has been paused\n\n\t\tgetPlaystateApi(api).reportPlaybackProgress({\n\t\t\tplaybackProgressInfo: {\n\t\t\t\tAudioStreamIndex: mediaSource.audioTrack,\n\t\t\t\tCanSeek: true,\n\t\t\t\tIsMuted: isPlayerMuted,\n\t\t\t\tIsPaused: true,\n\t\t\t\tItemId: itemId,\n\t\t\t\tMediaSourceId: mediaSource.id,\n\t\t\t\tPlayMethod: mediaSource.playMethod,\n\t\t\t\tPlaySessionId: playsessionId,\n\t\t\t\tPlaybackStartTimeTicks: userDataLastPlayedPositionTicks,\n\t\t\t\tPositionTicks: secToTicks(currentTime),\n\t\t\t\tRepeatMode: RepeatMode.RepeatNone,\n\t\t\t\tVolumeLevel: Math.floor(volume * 100),\n\n\t\t\t},\n\t\t});\n\t}, [setCurrentTime,\n\t\tapi,\n\t\tmediaSource,\n\t\tisPlayerMuted,\n\t\tisPlayerPlaying,\n\t\titemId,\n\t\tplaysessionId,\n\t\tuserDataLastPlayedPositionTicks,\n\t\tvolume,]);\n\n\n\tconst handleExitPlayer = useCallback(async () => {\n\t\tappWindow.getCurrent().setFullscreen(false);\n\n\t\thistory.back();\n\t\tif (!api) {\n\t\t\tthrow Error(\"API is not available, cannot report playback stopped.\");\n\t\t}\n\t\tlet currentTime = 0;\n\t\tif (player.current) {\n\t\t\tcurrentTime = player.current.currentTime ?? 0;\n\t\t}\n\n\t\t// Report Jellyfin server: Playback has ended/stopped\n\t\tgetPlaystateApi(api).reportPlaybackStopped({\n\t\t\tplaybackStopInfo: {\n\t\t\t\tFailed: false,\n\t\t\t\tItemId: itemId,\n\t\t\t\tMediaSourceId: mediaSource.id,\n\t\t\t\tPlaySessionId: playsessionId,\n\t\t\t\tPositionTicks: secToTicks(currentTime),\n\t\t\t},\n\t\t});\n\t\tusePlaybackStore.setState(usePlaybackStore.getInitialState());\n\t\tclearQueue();\n\t}, []);\n\n\tconst handleKeyPress = useCallback((event: KeyboardEvent) => {\n\t\tif (player.current) {\n\t\t\tevent.preventDefault();\n\t\t\tconst currentTime = player.current.currentTime ?? 0;\n\n\t\t\tconst seekTo = (time: number) => {\n\t\t\t\tif (player.current) {\n\t\t\t\t\tplayer.current.currentTime = time;\n\t\t\t\t}\n\t\t\t};\n\n\t\t\tswitch (event.key) {\n\t\t\t\tcase \"ArrowRight\":\n\t\t\t\t\tseekTo(currentTime + 10);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"ArrowLeft\":\n\t\t\t\t\tseekTo(currentTime - 10);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"F8\":\n\t\t\t\tcase \" \":\n\t\t\t\t\ttoggleIsPlaying();\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"ArrowUp\":\n\t\t\t\t\t// setVolume((state) => (state <= 0.9 ? state + 0.1 : state));\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"ArrowDown\":\n\t\t\t\t\t// setVolume((state) => (state >= 0.1 ? state - 0.1 : state));\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"F\":\n\t\t\t\tcase \"f\":\n\t\t\t\t\ttoggleIsPlayerFullscreen();\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"P\":\n\t\t\t\tcase \"p\":\n\t\t\t\t\t// setIsPIP((state) => !state);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"M\":\n\t\t\t\tcase \"m\":\n\t\t\t\t\ttoggleIsPlayerMuted();\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"F7\":\n\t\t\t\t\tplayItemFromQueue(\"previous\", user?.Id, api);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"F9\":\n\t\t\t\t\tplayItemFromQueue(\"next\", user?.Id, api);\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}, []);\n\n\tuseEffect(() => {\n\t\t// attach the event listener\n\t\tdocument.addEventListener(\"keydown\", handleKeyPress);\n\n\t\t// remove the event listener\n\t\treturn () => {\n\t\t\tdocument.removeEventListener(\"keydown\", handleKeyPress);\n\t\t};\n\t}, [handleKeyPress]);\n\n\t// Manage Subtitle playback\n\tuseEffect(() => {\n\t\tif (player.current && mediaSource.subtitle.enable) {\n\t\t\tconst internalPlayer = player.current;\n\n\t\t\tif (!internalPlayer) return;\n\n\t\t\tlet jassubRenderer: JASSUB | undefined;\n\t\t\tlet pgsRenderer: PgsRenderer | undefined;\n\t\t\tconst apiKey = api?.accessToken;\n\t\t\tconst separator = mediaSource.subtitle.url?.includes(\"?\") ? \"&\" : \"?\";\n\t\t\tconst urlParams = apiKey ? `${separator}api_key=${apiKey}` : \"\";\n\n\t\t\tif (\n\t\t\t\tmediaSource.subtitle.format === \"ass\" ||\n\t\t\t\tmediaSource.subtitle.format === \"ssa\"\n\t\t\t) {\n\t\t\t\tjassubRenderer = new JASSUB({\n\t\t\t\t\tvideo: internalPlayer,\n\t\t\t\t\tsubUrl: `${api?.basePath}${mediaSource.subtitle.url}`,\n\t\t\t\t\tavailableFonts: { \"noto sans\": font },\n\t\t\t\t\tdefaultFont: \"noto sans\",\n\t\t\t\t\tworkerUrl,\n\t\t\t\t\twasmUrl,\n\t\t\t\t\tmodernWasmUrl,\n\t\t\t\t});\n\t\t\t\tjassubRenderer.ready.then(() => {\n\t\t\t\t\tconsole.log(\"JASSUB subtitle renderer is ready\");\n\t\t\t\t});\n\t\t\t} else if (\n\t\t\t\tmediaSource.subtitle.format === \"subrip\" ||\n\t\t\t\tmediaSource.subtitle.format === \"vtt\"\n\t\t\t) {\n\t\t\t\taddSubtitleTrackToReactPlayer(\n\t\t\t\t\tinternalPlayer,\n\t\t\t\t\tmediaSource.subtitle,\n\t\t\t\t\tapi?.basePath ?? \"\",\n\t\t\t\t\tapiKey,\n\t\t\t\t);\n\t\t\t} else if (mediaSource.subtitle.format === \"PGSSUB\") {\n\t\t\t\tpgsRenderer = new PgsRenderer({\n\t\t\t\t\tworkerUrl: pgsWorkerUrl,\n\t\t\t\t\tvideo: internalPlayer,\n\t\t\t\t\tsubUrl: `${api?.basePath}${mediaSource.subtitle.url}${urlParams}`,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn () => {\n\t\t\t\tif (jassubRenderer) {\n\t\t\t\t\tjassubRenderer.destroy();\n\t\t\t\t} // Remove JASSUB renderer when track changes to fix duplicate renders\n\t\t\t\tif (pgsRenderer) {\n\t\t\t\t\tpgsRenderer.dispose();\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\tif (player.current && mediaSource.subtitle.enable === false) {\n\t\t\tconst internalPlayer = player.current;\n\n\t\t\tif (internalPlayer?.textTracks) {\n\t\t\t\tfor (const i of internalPlayer.textTracks) {\n\t\t\t\t\ti.mode = \"hidden\";\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, [\n\t\tmediaSource.subtitle?.track,\n\t\tmediaSource.subtitle?.enable,\n\t\tisPlayerReady,\n\t\tapi,\n\t]);\n\n\tconst handlePlaybackEnded = useCallback(() => {\n\t\tif (queue?.length !== currentQueueItemIndex + 1) {\n\t\t\tclearActiveSegment(); // Clear active segment when playback ends\n\t\t\tplayItemFromQueue(\"next\", user?.Id, api);\n\t\t} else {\n\t\t\thandleExitPlayer(); // Exit player if playback has finished and the queue is empty\n\t\t}\n\t}, []);\n\n\tconst idleTimerRef = useRef<NodeJS.Timeout | null>(null);\n\n\t// Function to clear the existing idle timer\n\tconst clearIdleTimer = useCallback(() => {\n\t\tif (idleTimerRef.current) {\n\t\t\tclearTimeout(idleTimerRef.current);\n\t\t}\n\t}, []);\n\n\t// Function to start a new idle timer\n\tconst startIdleTimer = useCallback(() => {\n\t\t// Clear any old timer before starting a new one\n\t\tclearIdleTimer();\n\t\t// Set a timer to hide the controls after 3 seconds of inactivity\n\t\tidleTimerRef.current = setTimeout(() => {\n\t\t\tsetIsUserHovering(false);\n\t\t}, 3000);\n\t}, [clearIdleTimer]);\n\n\t// --- Event Handlers for the main player wrapper ---\n\n\tconst handleMouseMove = useCallback(() => {\n\t\t// When the mouse moves, show the controls and restart the idle timer.\n\t\tsetIsUserHovering(true);\n\t\tstartIdleTimer();\n\t}, [startIdleTimer]);\n\n\tconst handleMouseLeave = useCallback(() => {\n\t\t// When the mouse leaves the player, clear the timer and hide the controls.\n\t\tclearIdleTimer();\n\t\tsetIsUserHovering(false);\n\t}, [clearIdleTimer]);\n\n\tconst handleOnBuffer = useCallback(() => {\n\t\tsetIsBuffering(true);\n\t}, [setIsBuffering]);\n\tconst handleOnBufferEnd = useCallback(() => {\n\t\tsetIsBuffering(false);\n\t}, [setIsBuffering]);\n\n\tconst handleError = useCallback((e: any) => {\n\t\tconsole.error(\"Video playback error:\", e);\n\t\tsetError(e);\n\t}, []);\n\n\tif (!playbackStream) {\n\t\treturn (\n\t\t\t<div className=\"video-player\">\n\t\t\t\t<CircularProgress\n\t\t\t\t\tsize={72}\n\t\t\t\t\tthickness={1.4}\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\ttop: \"50%\",\n\t\t\t\t\t\tleft: \"50%\",\n\t\t\t\t\t\ttransform: \"translate(-50%, -50%)\",\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</div>\n\t\t);\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tclassName=\"video-player\"\n\t\t\tonMouseMove={handleMouseMove}\n\t\t\tonMouseLeave={handleMouseLeave}\n\t\t>\n\t\t\t<LoadingIndicator />\n\t\t\t<ErrorDisplay\n\t\t\t\terror={error}\n\t\t\t\tonExit={handleExitPlayer}\n\t\t\t\tonRetry={() => {\n\t\t\t\t\tsetError(null);\n\t\t\t\t\tif (player.current) {\n\t\t\t\t\t\t// Try to reload the video\n\t\t\t\t\t\tif (typeof player.current.load === \"function\") {\n\t\t\t\t\t\t\tplayer.current.load();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<StatsForNerds playerRef={player as any} />\n\t\t\t<VideoPlayerControls\n\t\t\t\t// isVisible={areControlsVisible}\n\t\t\t\tonHover={handleMouseMove}\n\t\t\t\tonLeave={handleMouseLeave}\n\t\t\t/>\n\t\t\t<SkipSegmentButton />\n\t\t\t<VolumeChangeOverlay />\n\t\t\t<UpNextFlyout />\n\n\t\t\t<ReactPlayer\n\t\t\t\tkey={playsessionId}\n\t\t\t\tplaying={isPlayerPlaying}\n\t\t\t\tsrc={playbackStream}\n\t\t\t\tref={player}\n\t\t\t\tonReady={handleReady}\n\t\t\t\tonTimeUpdate={handleTimeUpdate}\n\t\t\t\tonPause={handlePause}\n\t\t\t\tonPlay={handleTimeUpdate}\n\t\t\t\tonError={handleError}\n\t\t\t\tonEnded={handlePlaybackEnded}\n\t\t\t\twidth=\"100vw\"\n\t\t\t\theight=\"100vh\"\n\t\t\t\tstyle={{\n\t\t\t\t\tposition: \"fixed\",\n\t\t\t\t\tzIndex: 0,\n\t\t\t\t}}\n\t\t\t\tvolume={isPlayerMuted ? 0 : volume}\n\t\t\t\tonWaiting={handleOnBuffer}\n\t\t\t\tonPlaying={handleOnBufferEnd}\n\t\t\t\tonSeeking={(e) => handleOnSeek(e.currentTarget.currentTime)}\n\t\t\t\tplaysInline\n\t\t\t\tcrossOrigin=\"anonymous\"\n\t\t\t/>\n\t\t</div>\n\t);\n}\n\nexport const Route = createFileRoute(\"/_api/player/\")({\n\tcomponent: VideoPlayer,\n});\n"
  },
  {
    "path": "src/routes/_api/player/photos.scss",
    "content": ".photos {\n    background: black;\n    overflow: hidden;\n    height: 100vh;\n    &-currentPhoto {\n        width: 100vw;\n        height: 100vh;\n        object-fit: contain;\n    }\n    &-overlay {\n        position: absolute;\n        bottom: 0;\n        left: 0;\n        width: 100vw;\n        z-index: 1;\n        opacity: 0.5;\n        transform: translateY(80%);\n        transition: $transition-time-default;\n        will-change: opacity, transform;\n        &:hover {\n            opacity: 1;\n            transform: translateY(0);\n        }\n    }\n    &-preview {\n        // width: 10vw;\n        height: 10vh;\n        object-fit: cover;\n        border: 2px;\n        border: 2px solid transparent;\n        border-radius: 4px;\n        transition: border $transition-time-default, filter $transition-time-default;\n        cursor: pointer;\n        &:hover {\n            filter: brightness(1.2) !important;\n        }\n        &-container {\n            width: 90vw;\n            overflow-y: hidden;\n            overflow-x: auto;\n            display: flex;\n            gap: 4px;\n            margin: 0 auto;\n            bottom: 0;\n            left: 5vw;\n            &:hover .photos-preview {\n                filter: brightness(0.7);\n            }\n        }\n        &.active {\n            border-color: $clr-accent-default;\n        }\n    }\n    &-actions {\n        position: fixed;\n        width: 100vw;\n        height: 100vh;\n        top: 0;\n        left: 0;\n        display: flex;\n        justify-content: space-between;\n        align-items: center;\n        padding: 1em;\n        z-index: 0;\n    }\n}"
  },
  {
    "path": "src/routes/_api/player/photos.tsx",
    "content": "import { useApiInContext } from \"@/utils/store/api\";\nimport { usePhotosPlayback } from \"@/utils/store/photosPlayback\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport React, { type MouseEvent, useCallback, useMemo, useState } from \"react\";\n\nimport { AnimatePresence, motion } from \"motion/react\";\n//@ts-ignore\nimport brokenImage from \"/assetsStatic/broken-image.png?url\";\n\nimport { save } from \"@tauri-apps/plugin-dialog\";\n\nimport \"./photos.scss\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport {\n\tIconButton,\n\tListItemIcon,\n\tListItemText,\n\tMenu,\n\tMenuItem,\n} from \"@mui/material\";\nimport { download } from \"@tauri-apps/plugin-upload\";\nimport { useSnackbar } from \"notistack\";\n\nexport const Route = createFileRoute(\"/_api/player/photos\")({\n\tcomponent: PhotosPlayer,\n});\n\nfunction PhotosPlayer() {\n\tconst api = useApiInContext((s) => s.api);\n\tconst [photos, startIndex] = usePhotosPlayback((s) => [s.photos, s.index]);\n\tconst [currentIndex, setCurrentIndex] = useState(startIndex);\n\tconst { enqueueSnackbar } = useSnackbar();\n\tconst currentPhoto = useMemo(\n\t\t() => photos?.[currentIndex],\n\t\t[photos, currentIndex],\n\t);\n\tconst allPhotos = useMemo(\n\t\t() =>\n\t\t\tphotos?.map((item, index) => (\n\t\t\t\t<div\n\t\t\t\t\tonClick={() => setCurrentIndex(index)}\n\t\t\t\t\tkey={item?.Id ?? \"noPhoto\"}\n\t\t\t\t\tclassName={\n\t\t\t\t\t\tindex === currentIndex ? \"photos-preview active\" : \"photos-preview\"\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t<img\n\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\tapi\n\t\t\t\t\t\t\t\t? getImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\titem.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t// width: 250,\n\t\t\t\t\t\t\t\t\t\t\tfillWidth: 250,\n\t\t\t\t\t\t\t\t\t\t\tquality: 70,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t: brokenImage\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\twidth: \"auto\",\n\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\tborderRadius: \"2px\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t\talt=\"Item\"\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t)),\n\t\t[photos, currentIndex],\n\t);\n\n\tconst handlePhotosChangePrev = useCallback(() => {\n\t\tsetCurrentIndex(currentIndex - 1);\n\t}, [currentIndex]);\n\tconst handlePhotosChangeNext = useCallback(() => {\n\t\tsetCurrentIndex(currentIndex + 1);\n\t}, [currentIndex]);\n\n\tconst [contextMenu, setContextMenu] = useState<{\n\t\tmouseX: number;\n\t\tmouseY: number;\n\t} | null>(null);\n\tconst handleShowContextMenu = (e: MouseEvent) => {\n\t\te.preventDefault();\n\t\tif (contextMenu === null) {\n\t\t\tsetContextMenu({ mouseX: e.clientX + 2, mouseY: e.clientY - 6 });\n\t\t} else {\n\t\t\tsetContextMenu(null);\n\t\t}\n\t};\n\tconst handleContextMenuClose = useCallback(() => {\n\t\tsetContextMenu(null);\n\t}, []);\n\n\tconst handleImageDownload = useCallback(async () => {\n\t\tconst pathToSave = await save({\n\t\t\tfilters: [\n\t\t\t\t{\n\t\t\t\t\tname: \"Image\",\n\t\t\t\t\textensions: [\"png\", \"jpeg\"],\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\n\t\tif (pathToSave) {\n\t\t\tdownload(\n\t\t\t\t`${api?.basePath}/Items/${currentPhoto?.Id}/Download?api_key=${api?.accessToken}`,\n\t\t\t\tpathToSave,\n\t\t\t);\n\t\t\tenqueueSnackbar(\"Image download queued.\", { variant: \"info\" });\n\t\t}\n\t}, [currentIndex]);\n\n\treturn (\n\t\t<div className=\"photos\">\n\t\t\t<AnimatePresence mode=\"wait\">\n\t\t\t\t<motion.img\n\t\t\t\t\tinitial={{ opacity: 0 }}\n\t\t\t\t\tanimate={{ opacity: 1 }}\n\t\t\t\t\texit={{ opacity: 0 }}\n\t\t\t\t\tkey={currentPhoto?.Id ?? \"noPhoto\"}\n\t\t\t\t\tsrc={\n\t\t\t\t\t\tcurrentPhoto\n\t\t\t\t\t\t\t? `${api?.basePath}/Items/${currentPhoto.Id}/Download?api_key=${api?.accessToken}`\n\t\t\t\t\t\t\t: brokenImage\n\t\t\t\t\t}\n\t\t\t\t\talt=\"Item\"\n\t\t\t\t\ttransition={{ duration: 0.1 }}\n\t\t\t\t\tclassName=\"photos-currentPhoto\"\n\t\t\t\t/>\n\t\t\t</AnimatePresence>\n\t\t\t<div className=\"photos-actions\" onContextMenu={handleShowContextMenu}>\n\t\t\t\t<IconButton onClick={handlePhotosChangePrev}>\n\t\t\t\t\t<span className=\"material-symbols-rounded\">chevron_left</span>\n\t\t\t\t</IconButton>\n\t\t\t\t<IconButton onClick={handlePhotosChangeNext}>\n\t\t\t\t\t<span className=\"material-symbols-rounded\">chevron_right</span>\n\t\t\t\t</IconButton>\n\t\t\t</div>\n\t\t\t<Menu\n\t\t\t\topen={!!contextMenu?.mouseX}\n\t\t\t\tonClose={handleContextMenuClose}\n\t\t\t\tanchorReference=\"anchorPosition\"\n\t\t\t\tanchorPosition={\n\t\t\t\t\tcontextMenu !== null\n\t\t\t\t\t\t? { left: contextMenu.mouseX, top: contextMenu.mouseY }\n\t\t\t\t\t\t: undefined\n\t\t\t\t}\n\t\t\t>\n\t\t\t\t<MenuItem onClick={handleImageDownload}>\n\t\t\t\t\t<ListItemIcon>\n\t\t\t\t\t\t<span className=\"material-symbols-rounded\">download</span>\n\t\t\t\t\t</ListItemIcon>\n\t\t\t\t\t<ListItemText>Download</ListItemText>\n\t\t\t\t</MenuItem>\n\t\t\t</Menu>\n\t\t\t<div className=\"photos-overlay\">\n\t\t\t\t<div className=\"photos-preview-container\">{allPhotos}</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n}"
  },
  {
    "path": "src/routes/_api/player/videoPlayer.scss",
    "content": ".video {\n\t&-osd {\n\t\topacity: 0;\n\t\ttransition: opacity 0.1s linear;\n\t\twidth: 100vw;\n\t\tcursor: none;\n\t\t&-name {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tgap: 0.5em;\n\t\t\tmargin-left: 1.2em;\n\t\t\t&-logo {\n\t\t\t\twidth: 16em;\n\t\t\t\tmax-height: 6em;\n\t\t\t\tobject-fit: contain;\n\t\t\t\tobject-position: left top;\n\t\t\t\tfilter: drop-shadow(0 0 10px black);\n\t\t\t}\n\t\t}\n\t\t&-trickplayBubble {\n\t\t\toverflow: hidden;\n\t\t\tborder-radius: 8px;\n\t\t}\n\t}\n\t&-osd-visible {\n\t\tcursor: default;\n\t}\n\t&-player-container {\n\t\tposition: fixed;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\twidth: 100vw;\n\t\theight: 100vh;\n\t\tright: 0;\n\t\tbottom: 0;\n\t\toverflow: hidden;\n\t}\n\t&-volume-indicator {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\twidth: 12em;\n\t\taspect-ratio: 1;\n\t\tpadding: 2em;\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t\tgap: 1.5em;\n\t\tposition: fixed;\n\t\ttop: 5em;\n\t\tright: 2em;\n\t\tz-index: 10;\n\t\tborder-radius: 2em;\n\t\t.material-symbols-rounded {\n\t\t\tfont-size: 3em;\n\t\t\t--fill: 1;\n\t\t}\n\t}\n}\nvideo {\n\t&::-webkit-media-text-track-display {\n\t\toverflow: visible !important;\n\t}\n\t&::cue {\n\t\ttext-shadow: 0 2px 10px rgb(0 0 0 / 0.8);\n\t\tbackground: transparent;\n\t}\n\t&::cue(i) {\n\t\ttext-shadow: 0 2px 10px rgb(0 0 0 / 0.8);\n\t}\n}"
  },
  {
    "path": "src/routes/_api/playlist/$id.tsx",
    "content": "import React, { useEffect } from \"react\";\n\nimport Box from \"@mui/material/Box\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\n\nimport { BaseItemKind } from \"@jellyfin/sdk/lib/generated-client\";\nimport { getLibraryApi } from \"@jellyfin/sdk/lib/utils/api/library-api\";\nimport { getPlaylistsApi } from \"@jellyfin/sdk/lib/utils/api/playlists-api\";\nimport { getUserLibraryApi } from \"@jellyfin/sdk/lib/utils/api/user-library-api\";\n\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { Card } from \"@/components/card/card\";\nimport CardScroller from \"@/components/cardScroller/cardScroller\";\nimport { ErrorNotice } from \"@/components/notices/errorNotice/errorNotice\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport \"./playlist.scss\";\nimport AlbumMusicTrack from \"@/components/albumMusicTrack\";\nimport LikeButton from \"@/components/buttons/likeButton\";\nimport PlayButton from \"@/components/buttons/playButton\";\nimport ShowMoreText from \"@/components/showMoreText\";\nimport TagChip from \"@/components/tagChip\";\nimport { getTypeIcon } from \"@/components/utils/iconsCollection\";\nimport { getRuntimeCompact } from \"@/utils/date/time\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport { Typography } from \"@mui/material\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { createPortal } from \"react-dom\";\n\nexport const Route = createFileRoute(\"/_api/playlist/$id\")({\n\tcomponent: PlaylistTitlePage,\n});\n\nfunction PlaylistTitlePage() {\n\tconst { id } = Route.useParams();\n\tconst api = Route.useRouteContext().api;\n\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst item = useQuery({\n\t\tqueryKey: [\"item\", id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getUserLibraryApi(api).getItem({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\titemId: id,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: !!user?.Id,\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst similarItems = useQuery({\n\t\tqueryKey: [\"item\", id, \"similarItem\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tif (!item.data?.Id) return null;\n\t\t\tconst result = await getLibraryApi(api).getSimilarAlbums({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\titemId: item.data.Id,\n\t\t\t\tlimit: 16,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: item.isSuccess,\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst musicTracks = useQuery({\n\t\tqueryKey: [\"item\", \"musicTracks\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tif (!item.data?.Id) return null;\n\t\t\tconst result = await getPlaylistsApi(api).getPlaylistItems({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tplaylistId: item.data.Id,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: item.isSuccess,\n\t\tnetworkMode: \"always\",\n\t});\n\n\tconst [setAppBackdrop] = useBackdropStore((state) => [state.setBackdrop]);\n\n\tuseEffect(() => {\n\t\tif (item.isSuccess) {\n\t\t\tsetAppBackdrop(\"\", \"\");\n\t\t}\n\t}, []);\n\n\tif (item.isPending || similarItems.isPending) {\n\t\treturn (\n\t\t\t<Box\n\t\t\t\tsx={{\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\theight: \"100vh\",\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<CircularProgress />\n\t\t\t</Box>\n\t\t);\n\t}\n\tif (item.isSuccess && item.data && similarItems.isSuccess) {\n\t\treturn (\n\t\t\t<div key={id} className=\"scrollY padded-top item item-playlist\">\n\t\t\t\t<div className=\"item-info\">\n\t\t\t\t\t<Typography className=\"item-info-name\" variant=\"h3\">\n\t\t\t\t\t\t{item.data?.Name}\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<div className=\"flex flex-align-center item-info-playlist-info\">\n\t\t\t\t\t\t<Typography className=\"opacity-07\">\n\t\t\t\t\t\t\t{(musicTracks.data?.TotalRecordCount ?? 0) > 1\n\t\t\t\t\t\t\t\t? `${musicTracks.data?.TotalRecordCount} songs`\n\t\t\t\t\t\t\t\t: \"Single\"}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t<Typography className=\"opacity-07\">\n\t\t\t\t\t\t\t{getRuntimeCompact(item.data.CumulativeRunTimeTicks ?? 0)}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"item-info-buttons\">\n\t\t\t\t\t\t<PlayButton\n\t\t\t\t\t\t\titem={item.data}\n\t\t\t\t\t\t\taudio\n\t\t\t\t\t\t\titemType={item.data.Type ?? \"Playlist\"}\n\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\titemId={item.data.Id}\n\t\t\t\t\t\t\tisFavorite={item.data.UserData?.IsFavorite}\n\t\t\t\t\t\t\tqueryKey={[\"item\"]}\n\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\titemName={item.data.Name}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div className=\"item-info-track-container \">\n\t\t\t\t\t\t<div>\n\t\t\t\t\t\t\t<div className=\"item-info-track header\">\n\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded index\">tag</span>\n\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">Title</Typography>\n\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">Duration</Typography>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{musicTracks.data?.Items?.map((track, index) => (\n\t\t\t\t\t\t\t\t<AlbumMusicTrack\n\t\t\t\t\t\t\t\t\tkey={track.Id}\n\t\t\t\t\t\t\t\t\ttrack={track}\n\t\t\t\t\t\t\t\t\ttrackIndex={index}\n\t\t\t\t\t\t\t\t\tmusicTracks={musicTracks.data}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t{createPortal(\n\t\t\t\t\t<div className=\"item-info-sidebar\">\n\t\t\t\t\t\t<div className=\"item-info-sidebar-image-container\">\n\t\t\t\t\t\t\t{item.data.ImageTags?.Primary ? (\n\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\tclassName=\"item-info-sidebar-image\"\n\t\t\t\t\t\t\t\t\talt={item.data.Name ?? \"Playlist\"}\n\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\titem.data.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\ttag: item.data.ImageTags.Primary,\n\t\t\t\t\t\t\t\t\t\t\t\tquality: 90,\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<div className=\"item-info-sidebar-icon\">\n\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Playlist\")}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"flex flex-align-center\"\n\t\t\t\t\t\t\tstyle={{ gap: \"1em\", flexWrap: \"wrap\" }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.data.GenreItems?.map((genre) => (\n\t\t\t\t\t\t\t\t<TagChip label={genre.Name ?? \"genre\"} key={genre.Id} />\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"item-info-sidebar-overview\">\n\t\t\t\t\t\t\t<ShowMoreText\n\t\t\t\t\t\t\t\tcontent={item.data.Overview ?? \"\"}\n\t\t\t\t\t\t\t\tcollapsedLines={4}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>,\n\t\t\t\t\tdocument.body,\n\t\t\t\t)}\n\t\t\t\t{(similarItems.data?.TotalRecordCount ?? 0) > 0 && (\n\t\t\t\t\t<CardScroller\n\t\t\t\t\t\ttitle=\"You might also like\"\n\t\t\t\t\t\tdisplayCards={5}\n\t\t\t\t\t\tdisableDecoration\n\t\t\t\t\t>\n\t\t\t\t\t\t{similarItems.data?.Items?.map((similar) => {\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\tkey={similar.Id}\n\t\t\t\t\t\t\t\t\titem={similar}\n\t\t\t\t\t\t\t\t\tseriesId={similar.SeriesId}\n\t\t\t\t\t\t\t\t\tcardTitle={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? similar.SeriesName\n\t\t\t\t\t\t\t\t\t\t\t: similar.Name\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\t\t\t\t\tcardCaption={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? `S${similar.ParentIndexNumber}:E${similar.IndexNumber} - ${similar.Name}`\n\t\t\t\t\t\t\t\t\t\t\t: similar.Type === BaseItemKind.Series\n\t\t\t\t\t\t\t\t\t\t\t\t? `${similar.ProductionYear} - ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsimilar.EndDate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? new Date(similar.EndDate).toLocaleString([], {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tyear: \"numeric\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"Present\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t}`\n\t\t\t\t\t\t\t\t\t\t\t\t: similar.ProductionYear\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tcardType={\"square\"}\n\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id, \"similarItem\"]}\n\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</CardScroller>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t);\n\t}\n\tif (item.isError || similarItems.isError) {\n\t\treturn <ErrorNotice />;\n\t}\n}\n\nexport default PlaylistTitlePage;\n"
  },
  {
    "path": "src/routes/_api/playlist/playlist.scss",
    "content": "$border-radius: 20px;\n.item-playlist {\n\twidth: 70vw;\n\tpadding-right: 2.2em;\n\t.item-info {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\tgap: 1.2em;\n\t\t&-container {\n\t\t\tgrid-template-columns: 1fr 25%;\n\t\t\tdisplay: grid;\n\t\t\tgap: 3em;\n\t\t}\n\t\t&-playlist-info {\n\t\t\tgap: 1.2em;\n\t\t}\n\t\t&-buttons {\n\t\t\tdisplay: flex;\n\t\t\tgap: 1em;\n\t\t}\n\t\t&-disc-header {\n\t\t\tpadding: 1em 2em;\n\t\t\tbackground: hsla(0, 0, 100%, 10%);\n\t\t\tborder-radius: 100px;\n\t\t\tgap: 1em;\n\t\t\tmargin-top: 1em;\n\t\t}\n\t}\n}\n.item-info-sidebar {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1em;\n\tposition: fixed;\n\t// max-height: 100vh;\n\ttop: 0;\n\tright: 0;\n\twidth: 30vw;\n\tpadding: 4.4em;\n\tpadding-left: 2.2em;\n\t&-image {\n\t\twidth: 100%;\n\t\tobject-fit: cover;\n\t\tborder-radius: $border-radius-default;\n\t}\n\t&-artist {\n\t\tdisplay: flex;\n\t\tgap: 1em;\n\t\talign-items: center;\n\t\tpadding: 1em 0;\n\t\tcolor: rgb(255 255 255 / 0.7);\n\t\ttransition: $transition-time-default;\n\t\tcursor: pointer;\n\t\ttext-decoration: none;\n\t\t&:hover {\n\t\t\tcolor: rgb(255 255 255)\n\t\t}\n\t\t&-image {\n\t\t\twidth: 100%;\n\t\t\theight: 100%;\n\t\t\tobject-fit: cover;\n\t\t\tz-index: 1;\n\t\t\tposition: relative;\n\t\t\t&-container {\n\t\t\t\twidth: 3em;\n\t\t\t\taspect-ratio: 1;\n\t\t\t\tborder-radius: 100%;\n\t\t\t\toverflow: hidden;\n\t\t\t\tposition: relative;\n\t\t\t\t.material-symbols-rounded {\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0;\n\t\t\t\t\tright: 0;\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\theight: 100%;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t\tz-index: 0;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t&-container {\n\t\t\t// max-height: 20em;/\n\t\t\toverflow-y: auto;\n\t\t\toverflow-x: hidden;\n\t\t}\n\t}\n\t&-overview {\n\t\toverflow-y: auto;\n\t\tmax-height: 28vh;\n\t\tpadding-bottom: 2px;\n\t}\n}\n.item-detail- {\n\t&-track {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: 4% 1fr 80% 1fr;\n\t\talign-items: center;\n\t\tjustify-items: center;\n\t\tgap: 0.5em;\n\t\twidth: 100%;\n\t\tpadding: 0.25em 0;\n\t\tbackground: rgb(0 0 0 / 0.4);\n\t\ttransition: background $transition-time-default;\n\t\tbackdrop-filter: blur(5px);\n\t\t&:first-child {\n\t\t\tborder-radius: $border-radius $border-radius 8px 8px;\n\t\t\tcursor: default !important;\n\t\t}\n\t\t&:last-child {\n\t\t\tborder-radius: 8px 8px $border-radius $border-radius;\n\t\t}\n\t\tborder-radius: 8px;\n\t\t&s {\n\t\t\tbackground: transparent !important;\n\t\t\tbox-shadow: none !important;\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tgap: 5px;\n\t\t}\n\t\t&:hover {\n\t\t\tbackground: rgb(0 0 0 / 0.5);\n\t\t\tcursor: pointer;\n\t\t}\n\t\t&.playing {\n\t\t\tcolor: $clr-accent-default;\n\t\t}\n\t}\n}\n\n@media (max-width: 1320px) {\n\t.item-playlist .item-detail {\n\t\t&-cast {\n\t\t\t&-grid {\n\t\t\t\tgrid-template-columns: 1fr;\n\t\t\t}\n\t\t}\n\t}\n}"
  },
  {
    "path": "src/routes/_api/search/index.tsx",
    "content": "import { BaseItemKind } from \"@jellyfin/sdk/lib/generated-client\";\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport { getPersonsApi } from \"@jellyfin/sdk/lib/utils/api/persons-api\";\nimport { getSearchApi } from \"@jellyfin/sdk/lib/utils/api/search-api\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport React, { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { Card } from \"@/components/card/card\";\nimport CardScroller from \"@/components/cardScroller/cardScroller\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport \"./search.scss\";\nimport {\n\tAppBar,\n\tBox,\n\tChip,\n\tFade,\n\tFormControl,\n\tGrow,\n\tIconButton,\n\tInputBase,\n\tMenuItem,\n\tOutlinedInput,\n\tSelect,\n\ttype SelectChangeEvent,\n\tTypography,\n\tuseScrollTrigger,\n} from \"@mui/material\";\nimport { createFileRoute, Outlet, useNavigate } from \"@tanstack/react-router\";\nimport { useShallow } from \"zustand/shallow\";\nimport BackButton from \"@/components/buttons/backButton\";\nimport { UserAvatarMenu } from \"@/components/userAvatarMenu\";\nimport useDebounce from \"@/utils/hooks/useDebounce\";\nimport { useCentralStore } from \"@/utils/store/central\";\nimport useSearchStore from \"@/utils/store/search\";\n\nexport const Route = createFileRoute(\"/_api/search/\")({\n\tcomponent: SearchPage,\n\tvalidateSearch: (search: Record<string, unknown>): { query: string } => {\n\t\treturn { query: search.query as string };\n\t},\n});\n\nconst CATEGORIES = [\n\t{ label: \"Movies\", value: \"Movie\", kind: BaseItemKind.Movie },\n\t{ label: \"TV Shows\", value: \"Series\", kind: BaseItemKind.Series },\n\t{ label: \"Episodes\", value: \"Episode\", kind: \"Episode\" },\n\t{ label: \"Music\", value: \"MusicAlbum\", kind: BaseItemKind.MusicAlbum },\n\t{ label: \"Artists\", value: \"MusicArtist\", kind: BaseItemKind.MusicArtist },\n\t{ label: \"People\", value: \"Person\", kind: \"Person\" },\n\t{ label: \"Books\", value: \"Book\", kind: BaseItemKind.Book },\n];\n\nfunction SearchPage() {\n\tconst api = Route.useRouteContext().api;\n\tconst navigate = useNavigate();\n\tconst { query } = Route.useSearch();\n\tconst [setBackdrop] = useBackdropStore(useShallow((s) => [s.setBackdrop]));\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst [searchTerm, setSearchTerm] = useState(query ?? \"\");\n\tconst debouncedSearchTerm = useDebounce(searchTerm, 300);\n\tconst [selectedCategories, setSelectedCategories] = useState<string[]>([]);\n\n\tuseEffect(() => {\n\t\tif (query && query !== searchTerm) {\n\t\t\tsetSearchTerm(query);\n\t\t}\n\t}, [query]);\n\n\tuseEffect(() => {\n\t\tif (debouncedSearchTerm !== query) {\n\t\t\tnavigate({\n\t\t\t\tto: \"/search\",\n\t\t\t\tsearch: { query: debouncedSearchTerm ?? \"\" },\n\t\t\t\treplace: true,\n\t\t\t});\n\t\t}\n\t}, [debouncedSearchTerm, navigate]);\n\n\tuseEffect(() => {\n\t\tsetBackdrop(\"\");\n\t}, []);\n\n\tconst handleCategoryChange = (event: SelectChangeEvent<string[]>) => {\n\t\tconst {\n\t\t\ttarget: { value },\n\t\t} = event;\n\t\tsetSelectedCategories(typeof value === \"string\" ? value.split(\",\") : value);\n\t};\n\n\tconst activeCategories = useMemo(() => {\n\t\tif (selectedCategories.length === 0) return CATEGORIES;\n\t\treturn CATEGORIES.filter((cat) => selectedCategories.includes(cat.value));\n\t}, [selectedCategories]);\n\n\treturn (\n\t\t<div className=\"search-page scrollY\">\n\t\t\t<div\n\t\t\t\tclassName={`search-page-bg ${debouncedSearchTerm ? \"active\" : \"\"}`}\n\t\t\t/>\n\n\t\t\t<SearchAppBar\n\t\t\t\tsearchTerm={searchTerm}\n\t\t\t\tsetSearchTerm={setSearchTerm}\n\t\t\t\tselectedCategories={selectedCategories}\n\t\t\t\thandleCategoryChange={handleCategoryChange}\n\t\t\t\thandleClearCategories={() => setSelectedCategories([])}\n\t\t\t/>\n\n\t\t\t<div\n\t\t\t\tclassName=\"search-page-content\"\n\t\t\t\tstyle={{\n\t\t\t\t\tpaddingTop: \"2.4em\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{debouncedSearchTerm ? (\n\t\t\t\t\t<SearchResultsList\n\t\t\t\t\t\tcategories={activeCategories}\n\t\t\t\t\t\tquery={debouncedSearchTerm}\n\t\t\t\t\t\tapi={api}\n\t\t\t\t\t\tuser={user}\n\t\t\t\t\t/>\n\t\t\t\t) : (\n\t\t\t\t\t<Fade in timeout={800}>\n\t\t\t\t\t\t<Box className=\"search-empty-state\">\n\t\t\t\t\t\t\t<Box className=\"icon-stack\">\n\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded icon-lg\">movie</span>\n\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded icon-sm\">\n\t\t\t\t\t\t\t\t\tmusic_note\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded icon-md\">tv</span>\n\t\t\t\t\t\t\t</Box>\n\t\t\t\t\t\t\t<Typography variant=\"h4\" fontWeight={600} gutterBottom>\n\t\t\t\t\t\t\t\tExplore your library\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t<Typography variant=\"body1\" sx={{ opacity: 0.6, maxWidth: 400 }}>\n\t\t\t\t\t\t\t\tSearch for movies, tv shows, music, people and more.\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t</Box>\n\t\t\t\t\t</Fade>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t\t<Outlet />\n\t\t</div>\n\t);\n}\n\ninterface SearchAppBarProps {\n\tsearchTerm: string;\n\tsetSearchTerm: (term: string) => void;\n\tselectedCategories: string[];\n\thandleCategoryChange: (event: SelectChangeEvent<string[]>) => void;\n\thandleClearCategories: () => void;\n}\n\nfunction SearchAppBar({\n\tsearchTerm,\n\tsetSearchTerm,\n\tselectedCategories,\n\thandleCategoryChange,\n\thandleClearCategories,\n}: SearchAppBarProps) {\n\tconst scrollTrigger = useScrollTrigger({\n\t\tdisableHysteresis: true,\n\t\tthreshold: 20,\n\t});\n\n\tconst appBarStyling = useMemo(\n\t\t() => ({ backgroundColor: \"transparent\", paddingRight: \"0 !important\" }),\n\t\t[],\n\t);\n\n\tconst navigate = useNavigate();\n\tconst handleNavigateToHome = useCallback(() => navigate({ to: \"/home\" }), []);\n\tconst handleNavigateToFavorite = useCallback(() => {\n\t\tnavigate({ to: \"/favorite\" });\n\t}, []);\n\n\tconst toggleSearchDialog = useSearchStore(\n\t\tuseShallow((s) => s.toggleSearchDialog),\n\t);\n\n\treturn (\n\t\t<AppBar\n\t\t\tclassName={\n\t\t\t\tscrollTrigger\n\t\t\t\t\t? \"appBar search-header scrolling flex flex-row\"\n\t\t\t\t\t: \"appBar search-header flex flex-row\"\n\t\t\t}\n\t\t\tstyle={appBarStyling}\n\t\t\televation={0}\n\t\t\tcolor=\"transparent\"\n\t\t>\n\t\t\t<div className=\"flex flex-row\" style={{ gap: \"0.6em\" }}>\n\t\t\t\t<IconButton disabled>\n\t\t\t\t\t<div className=\"material-symbols-rounded\">menu</div>\n\t\t\t\t</IconButton>\n\t\t\t\t<BackButton />\n\t\t\t\t<IconButton onClick={handleNavigateToHome}>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\tlocation.pathname === \"/home\"\n\t\t\t\t\t\t\t\t? \"material-symbols-rounded fill\"\n\t\t\t\t\t\t\t\t: \"material-symbols-rounded\"\n\t\t\t\t\t\t}\n\t\t\t\t\t>\n\t\t\t\t\t\thome\n\t\t\t\t\t</div>\n\t\t\t\t</IconButton>\n\t\t\t</div>\n\t\t\t<div\n\t\t\t\tclassName=\"flex flex-row library-header-center\"\n\t\t\t\tstyle={{ gap: \"0.6em\" }}\n\t\t\t>\n\t\t\t\t<Box className=\"search-input-container\">\n\t\t\t\t\t<span className=\"material-symbols-rounded search-icon\">search</span>\n\t\t\t\t\t<InputBase\n\t\t\t\t\t\tclassName=\"search-input\"\n\t\t\t\t\t\tplaceholder=\"Search library...\"\n\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t\tvalue={searchTerm}\n\t\t\t\t\t\tonChange={(e) => setSearchTerm(e.target.value)}\n\t\t\t\t\t\tendAdornment={\n\t\t\t\t\t\t\tsearchTerm && (\n\t\t\t\t\t\t\t\t<IconButton\n\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\tonClick={() => setSearchTerm(\"\")}\n\t\t\t\t\t\t\t\t\tsx={{ color: \"white\" }}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\t\t\tstyle={{ fontSize: \"1.2rem\" }}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tclose\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t/>\n\n\t\t\t\t\t<FormControl size=\"small\" className=\"search-category-select\">\n\t\t\t\t\t\t<Select\n\t\t\t\t\t\t\tmultiple\n\t\t\t\t\t\t\tdisplayEmpty\n\t\t\t\t\t\t\tvalue={selectedCategories}\n\t\t\t\t\t\t\tonChange={handleCategoryChange}\n\t\t\t\t\t\t\tinput={\n\t\t\t\t\t\t\t\t<OutlinedInput\n\t\t\t\t\t\t\t\t\tendAdornment={\n\t\t\t\t\t\t\t\t\t\tselectedCategories.length > 0 && (\n\t\t\t\t\t\t\t\t\t\t\t<IconButton\n\t\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t\t\tonMouseDown={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\te.stopPropagation();\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\tonClick={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\te.stopPropagation();\n\t\t\t\t\t\t\t\t\t\t\t\t\thandleClearCategories();\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\tsx={{ mr: 2, p: 0.5 }}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{ fontSize: \"1rem\" }}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\tclose\n\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tIconComponent={(props) => (\n\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t{...props}\n\t\t\t\t\t\t\t\t\tclassName={`material-symbols-rounded ${props.className}`}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\texpand_more\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\trenderValue={(selected) => {\n\t\t\t\t\t\t\t\tif (selected.length === 0)\n\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\tvariant=\"body2\"\n\t\t\t\t\t\t\t\t\t\t\tcolor=\"textSecondary\"\n\t\t\t\t\t\t\t\t\t\t\tsx={{ opacity: 0.7 }}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tTypes\n\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<Box\n\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\t\t\tflexWrap: \"nowrap\",\n\t\t\t\t\t\t\t\t\t\t\tgap: 0.5,\n\t\t\t\t\t\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t\t\t\t\t\t\tmaxWidth: \"100%\",\n\t\t\t\t\t\t\t\t\t\t\tmaskImage:\n\t\t\t\t\t\t\t\t\t\t\t\t\"linear-gradient(to right, black 85%, transparent 100%)\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{selected.map((value) => {\n\t\t\t\t\t\t\t\t\t\t\tconst label =\n\t\t\t\t\t\t\t\t\t\t\t\tCATEGORIES.find((c) => c.value === value)?.label ||\n\t\t\t\t\t\t\t\t\t\t\t\tvalue;\n\t\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t\t<Chip\n\t\t\t\t\t\t\t\t\t\t\t\t\tkey={value}\n\t\t\t\t\t\t\t\t\t\t\t\t\tlabel={label}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tsx={{ height: 24 }}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t\t</Box>\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tMenuProps={{\n\t\t\t\t\t\t\t\tPaperProps: {\n\t\t\t\t\t\t\t\t\tclassName: \"glass-menu-paper\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tdisableScrollLock: true,\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<MenuItem disabled value=\"\">\n\t\t\t\t\t\t\t\t<Typography variant=\"overline\" color=\"textSecondary\">\n\t\t\t\t\t\t\t\t\tFilter by Type\n\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t{CATEGORIES.map((cat) => (\n\t\t\t\t\t\t\t\t<MenuItem key={cat.value} value={cat.value}>\n\t\t\t\t\t\t\t\t\t{cat.label}\n\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t</Select>\n\t\t\t\t\t</FormControl>\n\t\t\t\t</Box>\n\t\t\t</div>\n\t\t\t<div className=\"flex flex-row\" style={{ gap: \"0.6em\" }}>\n\t\t\t\t<IconButton onClick={toggleSearchDialog}>\n\t\t\t\t\t<span className=\"material-symbols-rounded\">search</span>\n\t\t\t\t</IconButton>\n\t\t\t\t<IconButton onClick={handleNavigateToFavorite}>\n\t\t\t\t\t<div className=\"material-symbols-rounded\">favorite</div>\n\t\t\t\t</IconButton>\n\t\t\t\t<UserAvatarMenu />\n\t\t\t</div>\n\t\t</AppBar>\n\t);\n}\n\nfunction SearchResultsList({ categories, query, api, user }: any) {\n\t// Lift state to know if we have ANY results\n\tconst [resultsStatus, setResultsStatus] = useState<Record<string, boolean>>(\n\t\t{},\n\t);\n\n\tconst handleResultUpdate = (catValue: string, hasResults: boolean) => {\n\t\tsetResultsStatus((prev) => {\n\t\t\tif (prev[catValue] === hasResults) return prev;\n\t\t\treturn { ...prev, [catValue]: hasResults };\n\t\t});\n\t};\n\n\t// Derived state: are *all* queried categories empty?\n\t// We only know for sure if all active categories have reported back false.\n\t// This is tricky with async. A better way implies checking isLoading.\n\t// For now, we render the sections; if they are empty, they render null.\n\t// If ALL render null, we show \"No Results\".\n\n\t// Simpler visual fix: Render \"No results found\" at the bottom if nothing appears?\n\t// No, that looks broken.\n\n\t// Let's use a timeout or assume if no results update to true after X time.\n\n\tconst _hasAnyResults = Object.values(resultsStatus).some((v) => v);\n\n\treturn (\n\t\t<Box sx={{ display: \"flex\", flexDirection: \"column\", gap: \"1em\", pb: 4 }}>\n\t\t\t{categories.map((cat: any) => (\n\t\t\t\t<CategorySection\n\t\t\t\t\tkey={cat.value}\n\t\t\t\t\tcategory={cat}\n\t\t\t\t\tquery={query}\n\t\t\t\t\tapi={api}\n\t\t\t\t\tuser={user}\n\t\t\t\t\tonResult={(hasItems: boolean) =>\n\t\t\t\t\t\thandleResultUpdate(cat.value, hasItems)\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t))}\n\n\t\t\t{/* \n               Only show \"No results\" if we are confident. \n               Since we don't have a global loading state here, \n               checking `!hasAnyResults` immediately causes flickers.\n               Better to handle \"No results\" inside the sections or leave it blank?\n               User asked for \"User needs to know search result is empty\".\n             */}\n\t\t\t<NoResultsDetector\n\t\t\t\tcategories={categories}\n\t\t\t\tresultsStatus={resultsStatus}\n\t\t\t/>\n\t\t</Box>\n\t);\n}\n\nfunction NoResultsDetector({ categories, resultsStatus }: any) {\n\t// Wait for all categories to report status\n\tconst allReported = categories.every(\n\t\t(c: any) => resultsStatus[c.value] !== undefined,\n\t);\n\tconst hasAny = Object.values(resultsStatus).some((v) => v);\n\n\tif (allReported && !hasAny) {\n\t\treturn (\n\t\t\t<Fade in>\n\t\t\t\t<Box className=\"search-no-results\">\n\t\t\t\t\t<Typography variant=\"h6\">No results found</Typography>\n\t\t\t\t\t<Typography variant=\"body2\" sx={{ opacity: 0.6 }}>\n\t\t\t\t\t\tTry adjusting your search or filters.\n\t\t\t\t\t</Typography>\n\t\t\t\t</Box>\n\t\t\t</Fade>\n\t\t);\n\t}\n\treturn null;\n}\n\nfunction CategorySection({ category, query, api, user, onResult }: any) {\n\tconst limit = 20;\n\n\tconst { data, isLoading } = useQuery({\n\t\tqueryKey: [\"search\", \"category\", category.value, query],\n\t\tqueryFn: async () => {\n\t\t\tif (!api || !user?.Id) return { items: [], total: 0 };\n\n\t\t\tif (category.value === \"Person\") {\n\t\t\t\tconst res = await getPersonsApi(api).getPersons({\n\t\t\t\t\tuserId: user.Id,\n\t\t\t\t\tsearchTerm: query,\n\t\t\t\t\tlimit,\n\t\t\t\t});\n\t\t\t\treturn { items: res.data.Items, total: res.data.TotalRecordCount };\n\t\t\t}\n\n\t\t\tif (category.value === \"Episode\") {\n\t\t\t\tconst res = await getSearchApi(api).getSearchHints({\n\t\t\t\t\tuserId: user.Id,\n\t\t\t\t\tsearchTerm: query,\n\t\t\t\t\tlimit,\n\t\t\t\t\tincludeItemTypes: [\"Episode\"],\n\t\t\t\t});\n\t\t\t\treturn {\n\t\t\t\t\titems: res.data.SearchHints,\n\t\t\t\t\ttotal: res.data.TotalRecordCount,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst res = await getItemsApi(api).getItems({\n\t\t\t\tuserId: user.Id,\n\t\t\t\tsearchTerm: query,\n\t\t\t\tincludeItemTypes: [category.kind],\n\t\t\t\trecursive: true,\n\t\t\t\tlimit,\n\t\t\t});\n\t\t\treturn { items: res.data.Items, total: res.data.TotalRecordCount };\n\t\t},\n\t\tenabled: !!query,\n\t});\n\n\tuseEffect(() => {\n\t\tif (!isLoading && data) {\n\t\t\tonResult((data.items?.length || 0) > 0);\n\t\t} else if (!isLoading && !data) {\n\t\t\tonResult(false);\n\t\t}\n\t}, [data, isLoading, onResult]);\n\n\tif (isLoading) return <CategorySkeleton />;\n\tif (!data?.items || data.items.length === 0) return null;\n\n\t// ... (Card mapping logic same as before) ...\n\treturn (\n\t\t<Grow in>\n\t\t\t<div>\n\t\t\t\t<CardScroller\n\t\t\t\t\ttitle={category.label}\n\t\t\t\t\tdisplayCards={category.value === \"Episode\" ? 4 : 7}\n\t\t\t\t\tdisableDecoration\n\t\t\t\t>\n\t\t\t\t\t{data.items.map((item: any) => {\n\t\t\t\t\t\tlet props: any = {\n\t\t\t\t\t\t\tcardTitle: item?.Name,\n\t\t\t\t\t\t\timageType: \"Primary\",\n\t\t\t\t\t\t\tcardCaption: item?.ProductionYear,\n\t\t\t\t\t\t\tcardType: \"portrait\",\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tif (category.value === \"Series\") {\n\t\t\t\t\t\t\tprops.cardCaption = `${item.ProductionYear || \"\"}`;\n\t\t\t\t\t\t} else if (\n\t\t\t\t\t\t\tcategory.value === \"MusicAlbum\" ||\n\t\t\t\t\t\t\tcategory.value === \"MusicArtist\" ||\n\t\t\t\t\t\t\tcategory.value === \"Person\"\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tprops.cardType = \"square\";\n\t\t\t\t\t\t} else if (category.value === \"Episode\") {\n\t\t\t\t\t\t\tprops = {\n\t\t\t\t\t\t\t\tcardTitle: item.Series,\n\t\t\t\t\t\t\t\timageType: \"Primary\",\n\t\t\t\t\t\t\t\tcardCaption: `S${item.ParentIndexNumber}:E${item.IndexNumber} - ${item.Name}`,\n\t\t\t\t\t\t\t\tcardType: \"thumb\",\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\tkey={item.Id}\n\t\t\t\t\t\t\t\titem={item}\n\t\t\t\t\t\t\t\t{...props}\n\t\t\t\t\t\t\t\tqueryKey={[\"search\", \"category\", category.value, query]}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t);\n\t\t\t\t\t})}\n\t\t\t\t</CardScroller>\n\t\t\t</div>\n\t\t</Grow>\n\t);\n}\n\nfunction CategorySkeleton() {\n\treturn (\n\t\t<Box sx={{ mb: 4, ml: 2, mr: 2 }}>\n\t\t\t<Box\n\t\t\t\tsx={{\n\t\t\t\t\theight: 32,\n\t\t\t\t\twidth: 200,\n\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\tborderRadius: 2,\n\t\t\t\t\tmb: 2,\n\t\t\t\t}}\n\t\t\t/>\n\t\t\t<Box sx={{ display: \"flex\", gap: 2, overflow: \"hidden\" }}>\n\t\t\t\t{[1, 2, 3, 4, 5, 6].map((i) => (\n\t\t\t\t\t<Box\n\t\t\t\t\t\tkey={i}\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\theight: 260,\n\t\t\t\t\t\t\tminWidth: 170,\n\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.03)\",\n\t\t\t\t\t\t\tborderRadius: 3,\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\t\t\t\t))}\n\t\t\t</Box>\n\t\t</Box>\n\t);\n}\n\nexport default SearchPage;\n"
  },
  {
    "path": "src/routes/_api/search/search.scss",
    "content": "\n.search-page {\n    position: relative;\n    min-height: 100vh;\n    display: flex;\n    flex-direction: column;\n    \n    // Glossy subtle background animation - fixed to viewport so it stays while scrolling\n    &-bg {\n        position: fixed;\n        top: -50%;\n        left: -50%;\n        right: -50%;\n        bottom: -50%;\n        background: radial-gradient(circle at center, rgba(var(--mui-palette-primary-mainChannel), 0.15) 0%, transparent 60%);\n        opacity: 0;\n        transition: opacity 0.8s ease;\n        pointer-events: none;\n        z-index: 0;\n        \n        &.active {\n            opacity: 1;\n        }\n    }\n    \n    // New Sticky AppBar styles mirroring LibraryHeader exactly\n    .search-header {\n        display: flex;\n        align-items: center;\n        justify-content: space-between;\n        inset: 1em 1em auto 1em !important;\n        border-radius: 9999px;\n        margin: 0 auto;\n        position: fixed;\n        width: calc(100vw - 4em) !important;\n        z-index: 100;\n        padding: 0.25em !important;\n        transition: all 0.2s ease-in-out;\n        \n        &::after {\n            content: \"\";\n            position: absolute;\n            inset: 0 -0.35em;\n            pointer-events: none;\n            border-radius: inherit;\n            @include glass-effect;\n            opacity: 0;\n            z-index: -1;\n            transition: all $transition-time-fast;\n        }\n        \n        &.scrolling {\n            &::after {\n                opacity: 1;\n            }\n        }\n    }\n    \n    &-content {\n        flex-grow: 1;\n        width: 100%;\n        align-self: center;\n        z-index: 1;\n    }\n    \n    // Input Container adapted for the pill header\n    .search-input-container {\n        display: flex;\n        align-items: center;\n        background: rgba(255, 255, 255, 0.05);\n        border: 1px solid rgba(255, 255, 255, 0.08);\n        border-radius: 9999px; // Match pill\n        padding: 2px 4px 2px 16px;\n        flex-grow: 1;\n        width: 100%;\n        max-width: 800px;\n        transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);\n        height: 48px; // Match library header elements roughly\n        \n        &:hover, &:focus-within {\n            background: rgba(255, 255, 255, 0.1);\n            border-color: rgba(255, 255, 255, 0.15);\n        }\n\n        .search-category-select {\n            flex-shrink: 0;\n            max-width: 40%;\n            \n            .MuiSelect-select {\n               padding-right: 24px !important; // Make room for clear icon inside select if needed, though we put it in EndAdornment\n            }\n        }\n    }\n    \n    .search-input {\n        flex-grow: 1;\n        \n        .MuiInputBase-input {\n            font-size: 1rem;\n            padding: 4px 8px;\n            color: white;\n            \n            &::placeholder {\n                opacity: 0.6;\n            }\n        }\n        \n        .search-icon {\n            font-size: 1.2rem;\n            opacity: 0.7;\n            margin-right: 8px;\n        }\n    }\n    \n    .search-category-select {\n        flex-shrink: 0;\n        min-width: 120px;\n        margin-left: 8px;\n        \n        .MuiOutlinedInput-root {\n            border-radius: 20px;\n            background-color: transparent ;\n            color: rgba(255, 255, 255, 0.9);\n            height: 36px;\n            font-size: 0.85rem;\n            transition: all 0.2s ease;\n            \n            fieldset { border: none; }\n            \n            &:hover {\n                background-color: rgba(255, 255, 255, 0.05);\n            }\n        }\n    }\n    \n    &-empty-state {\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        justify-content: center;\n        height: 60vh; // Take up vertical space to look nice\n        text-align: center;\n        width: 100%;\n        \n        .icon-stack {\n            position: relative;\n            height: 120px;\n            width: 200px;\n            display: flex;\n            align-items: center;\n            justify-content: center;\n            margin-bottom: 24px;\n            \n            .icon-lg { \n                font-size: 5rem; \n                z-index: 2; \n                color: var(--mui-palette-primary-main); \n                filter: drop-shadow(0 0 20px rgba(var(--mui-palette-primary-mainChannel), 0.4));\n            }\n            .icon-md { \n                font-size: 3.5rem; \n                position: absolute; \n                right: 30px; \n                top: 10px; \n                opacity: 0.4; \n                transform: rotate(15deg); \n            }\n            .icon-sm { \n                font-size: 3rem; \n                position: absolute; \n                left: 40px; \n                bottom: 10px; \n                opacity: 0.3; \n                transform: rotate(-10deg); \n            }\n        }\n    }\n    \n    &-no-results {\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        justify-content: center;\n        padding: 4em;\n        opacity: 0.7;\n    }\n}\n\n\n"
  },
  {
    "path": "src/routes/_api/series/$id.tsx",
    "content": "import { BaseItemKind, ItemFields } from \"@jellyfin/sdk/lib/generated-client\";\nimport { getLibraryApi } from \"@jellyfin/sdk/lib/utils/api/library-api\";\nimport { getTvShowsApi } from \"@jellyfin/sdk/lib/utils/api/tv-shows-api\";\nimport { getUserLibraryApi } from \"@jellyfin/sdk/lib/utils/api/user-library-api\";\nimport Box from \"@mui/material/Box\";\nimport Chip from \"@mui/material/Chip\";\nimport CircularProgress from \"@mui/material/CircularProgress\";\nimport Divider from \"@mui/material/Divider\";\nimport MenuItem from \"@mui/material/MenuItem\";\nimport TextField from \"@mui/material/TextField\";\nimport Typography from \"@mui/material/Typography\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { motion } from \"motion/react\";\nimport React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport { Card } from \"@/components/card/card\";\nimport CardScroller from \"@/components/cardScroller/cardScroller\";\nimport ItemHeader from \"@/components/itemHeader\";\nimport { ErrorNotice } from \"@/components/notices/errorNotice/errorNotice\";\nimport { getRuntimeCompact, getRuntimeMusic } from \"@/utils/date/time\";\nimport useDefaultSeason from \"@/utils/hooks/useDefaultSeason\";\nimport \"./series.scss\";\n\nimport { createFileRoute, Link, useNavigate } from \"@tanstack/react-router\";\nimport LikeButton from \"@/components/buttons/likeButton\";\nimport MarkPlayedButton from \"@/components/buttons/markPlayedButton\";\nimport PlayButton from \"@/components/buttons/playButton\";\nimport TrailerButton from \"@/components/buttons/trailerButton\";\nimport IconLink from \"@/components/iconLink\";\nimport ShowMoreText from \"@/components/showMoreText\";\nimport EpisodeSkeleton from \"@/components/skeleton/episode\";\nimport { SeasonSelectorSkeleton } from \"@/components/skeleton/seasonSelector\";\nimport { getTypeIcon } from \"@/components/utils/iconsCollection\";\nimport getImageUrlsApi from \"@/utils/methods/getImageUrlsApi\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport { useCentralStore } from \"@/utils/store/central\";\n\nconst getEpisodeDateString = (date: Date) => {\n\tconst formatter = new Intl.DateTimeFormat(navigator.language ?? \"en-US\", {\n\t\tdateStyle: \"full\",\n\t});\n\treturn formatter.format(date).toString();\n};\n\nexport const Route = createFileRoute(\"/_api/series/$id\")({\n\tcomponent: SeriesTitlePage,\n});\n\nfunction SeriesTitlePage() {\n\tconst { id } = Route.useParams();\n\n\tconst api = Route.useRouteContext().api;\n\n\tconst setBackdrop = useBackdropStore((s) => s.setBackdrop);\n\n\tconst user = useCentralStore((s) => s.currentUser);\n\n\tconst item = useQuery({\n\t\tqueryKey: [\"item\", id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getUserLibraryApi(api).getItem({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\titemId: id,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: !!user?.Id,\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst similarItems = useQuery({\n\t\tqueryKey: [\"item\", id, \"similarItem\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getLibraryApi(api).getSimilarShows({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\titemId: id,\n\t\t\t\tlimit: 16,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tconst seasons = useQuery({\n\t\tqueryKey: [\"item\", id, \"seasons\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getTvShowsApi(api).getSeasons({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tseriesId: id,\n\t\t\t\tisMissing: false,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tconst [backdropImage, setBackdropImage] = useState<SeriesBackdropImage>(\n\t\t() => {\n\t\t\tconst url = sessionStorage.getItem(`backdrop-${item.data?.Id}`);\n\t\t\tconst key = sessionStorage.getItem(`backdrop-${item.data?.Id}-key`);\n\t\t\treturn { url, key };\n\t\t},\n\t);\n\tconst [_backdropImageLoaded, setBackdropImageLoaded] = useState(false);\n\n\tconst [currentSeason, setCurrentSeason] = useDefaultSeason(\n\t\tseasons,\n\t\titem.data?.Id,\n\t);\n\n\tconst currentSeasonNumber = Number(currentSeason);\n\n\tconst currentSeasonItem = useQuery({\n\t\tqueryKey: [\"item\", id, \"season\", currentSeason],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getUserLibraryApi(api).getItem({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\titemId: seasons.data?.Items?.[Number(currentSeason)].Id ?? \"\",\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: seasons.isSuccess,\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst episodes = useQuery({\n\t\tqueryKey: [\"item\", id, \"season\", currentSeason, \"episodes\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getTvShowsApi(api).getEpisodes({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tseriesId: item.data?.Id ?? \"\",\n\t\t\t\tseasonId: seasons.data?.Items?.[Number(currentSeason)].Id,\n\t\t\t\tfields: [ItemFields.Overview],\n\t\t\t\tisMissing: false,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: seasons.isSuccess,\n\t\tnetworkMode: \"always\",\n\t\trefetchOnWindowFocus: true,\n\t});\n\n\tconst specialFeatures = useQuery({\n\t\tqueryKey: [\"item\", id, \"specialFeatures\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return null;\n\t\t\tconst result = await getUserLibraryApi(api).getSpecialFeatures({\n\t\t\t\titemId: item.data?.Id ?? \"\",\n\t\t\t\tuserId: user?.Id,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: item.isSuccess,\n\t});\n\n\tconst directors = useMemo(() => {\n\t\treturn item.data?.People?.filter((itm) => itm.Type === \"Director\") ?? [];\n\t}, [item.data?.Id]);\n\tconst writers = useMemo(() => {\n\t\treturn item.data?.People?.filter((itm) => itm.Type === \"Writer\") ?? [];\n\t}, [item.data?.Id]);\n\tconst actors = useMemo(() => {\n\t\treturn item.data?.People?.filter((itm) => itm.Type === \"Actor\") ?? [];\n\t}, [item.data?.Id]);\n\tconst producers = useMemo(() => {\n\t\treturn item.data?.People?.filter((itm) => itm.Type === \"Producer\") ?? [];\n\t}, [item.data?.Id]);\n\n\tconst lastBackdropHashRef = useRef<string | undefined>(undefined);\n\n\tuseEffect(() => {\n\t\tif (api && currentSeasonItem.isSuccess && item.isSuccess) {\n\t\t\tif (\n\t\t\t\tcurrentSeasonItem.isSuccess &&\n\t\t\t\t(currentSeasonItem.data?.BackdropImageTags?.length ?? 0) > 0\n\t\t\t) {\n\t\t\t\tsessionStorage.setItem(\n\t\t\t\t\t`backdrop-${item.data?.Id}`,\n\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\tcurrentSeasonItem.data?.Id ?? \"\",\n\t\t\t\t\t\t\"Backdrop\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttag: currentSeasonItem.data?.BackdropImageTags?.[0],\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tsessionStorage.setItem(\n\t\t\t\t\t`backdrop-${item.data?.Id}-key`,\n\t\t\t\t\tcurrentSeasonItem.data?.BackdropImageTags?.[0] ?? \"\",\n\t\t\t\t);\n\t\t\t\tsetBackdropImage({\n\t\t\t\t\turl: getImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\tcurrentSeasonItem.data?.Id ?? \"\",\n\t\t\t\t\t\t\"Backdrop\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttag: currentSeasonItem.data?.BackdropImageTags?.[0],\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t\tkey: currentSeasonItem.data?.BackdropImageTags?.[0],\n\t\t\t\t});\n\t\t\t\tconst seasonBackdropTag =\n\t\t\t\t\tcurrentSeasonItem.data?.BackdropImageTags?.[0];\n\t\t\t\tconst seasonBackdropHash =\n\t\t\t\t\tcurrentSeasonItem.data?.ImageBlurHashes?.Backdrop?.[\n\t\t\t\t\t\tseasonBackdropTag ?? \"\"\n\t\t\t\t\t] ?? \"\";\n\n\t\t\t\tif (lastBackdropHashRef.current !== seasonBackdropHash) {\n\t\t\t\t\tsetBackdrop(seasonBackdropHash);\n\t\t\t\t\tlastBackdropHashRef.current = seasonBackdropHash;\n\t\t\t\t}\n\t\t\t\tsetBackdropImageLoaded(false);\n\t\t\t} else if (\n\t\t\t\titem.isSuccess &&\n\t\t\t\t(item.data?.BackdropImageTags?.length ?? 0) > 0\n\t\t\t) {\n\t\t\t\tsessionStorage.setItem(\n\t\t\t\t\t`backdrop-${item.data?.Id}`,\n\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\titem.data?.Id ?? \"\",\n\t\t\t\t\t\t\"Backdrop\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttag: item.data?.BackdropImageTags?.[0],\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tsessionStorage.setItem(\n\t\t\t\t\t`backdrop-${item.data?.Id}-key`,\n\t\t\t\t\titem.data?.BackdropImageTags?.[0] ?? \"\",\n\t\t\t\t);\n\t\t\t\tif (backdropImage.key !== item.data?.BackdropImageTags?.[0]) {\n\t\t\t\t\tsetBackdropImageLoaded(false); // Reset Backdrop image load status if previous image is diff then new image\n\t\t\t\t}\n\t\t\t\tsetBackdropImage({\n\t\t\t\t\turl: getImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\titem.data?.Id ?? \"\",\n\t\t\t\t\t\t\"Backdrop\",\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttag: item.data?.BackdropImageTags?.[0],\n\t\t\t\t\t\t},\n\t\t\t\t\t),\n\t\t\t\t\tkey: item.data?.BackdropImageTags?.[0],\n\t\t\t\t});\n\t\t\t\tconst seriesBackdropTag = item.data?.BackdropImageTags?.[0];\n\t\t\t\tconst seriesBackdropHash =\n\t\t\t\t\titem.data?.ImageBlurHashes?.Backdrop?.[seriesBackdropTag ?? \"\"] ?? \"\";\n\n\t\t\t\tif (lastBackdropHashRef.current !== seriesBackdropHash) {\n\t\t\t\t\tsetBackdrop(seriesBackdropHash);\n\t\t\t\t\tlastBackdropHashRef.current = seriesBackdropHash;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}, [\n\t\tapi,\n\t\tcurrentSeasonItem.data,\n\t\tcurrentSeasonItem.isSuccess,\n\t\titem.data,\n\t\titem.isSuccess,\n\t\tsetBackdrop,\n\t\tbackdropImage.key,\n\t]);\n\n\tconst containerRef = useRef<HTMLDivElement | null>(null);\n\t// Parallax handled within ItemBackdrop to avoid hydration issues.\n\n\tconst navigate = useNavigate();\n\n\tif (item.isPending || similarItems.isPending) {\n\t\treturn (\n\t\t\t<Box\n\t\t\t\tsx={{\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\theight: \"100vh\",\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<CircularProgress />\n\t\t\t</Box>\n\t\t);\n\t}\n\n\tif (item.isSuccess && item.data) {\n\t\treturn (\n\t\t\t<motion.div\n\t\t\t\tkey={id}\n\t\t\t\tinitial={{\n\t\t\t\t\topacity: 0,\n\t\t\t\t}}\n\t\t\t\tanimate={{\n\t\t\t\t\topacity: 1,\n\t\t\t\t}}\n\t\t\t\ttransition={{\n\t\t\t\t\tduration: 0.25,\n\t\t\t\t\tease: \"easeInOut\",\n\t\t\t\t}}\n\t\t\t\tclassName=\"scrollY padded-top flex flex-column item item-series\"\n\t\t\t\tref={containerRef}\n\t\t\t>\n\t\t\t\t<ItemHeader\n\t\t\t\t\titem={item.data}\n\t\t\t\t\tapi={api}\n\t\t\t\t\tscrollTargetRef={containerRef}\n\t\t\t\t\tbackdropSrc={backdropImage.url}\n\t\t\t\t>\n\t\t\t\t\t<div className=\"item-hero-buttons-container\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"flex flex-row\"\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<PlayButton\n\t\t\t\t\t\t\t\titem={item.data}\n\t\t\t\t\t\t\t\titemType={BaseItemKind.Series}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\tbuttonProps={{\n\t\t\t\t\t\t\t\t\tfullWidth: true,\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div className=\"flex flex-row\" style={{ gap: \"1em\" }}>\n\t\t\t\t\t\t\t{item.data.RemoteTrailers && (\n\t\t\t\t\t\t\t\t<TrailerButton\n\t\t\t\t\t\t\t\t\ttrailerItem={item.data.RemoteTrailers}\n\t\t\t\t\t\t\t\t\tdisabled={item.data.RemoteTrailers?.length === 0}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\t\titemName={item.data.Name}\n\t\t\t\t\t\t\t\titemId={item.data.Id}\n\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\tisFavorite={item.data.UserData?.IsFavorite}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t<MarkPlayedButton\n\t\t\t\t\t\t\t\titemName={item.data.Name}\n\t\t\t\t\t\t\t\titemId={item.data.Id}\n\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\tisPlayed={item.data.UserData?.Played}\n\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</ItemHeader>\n\t\t\t\t<div className=\"item-detail\">\n\t\t\t\t\t<div style={{ width: \"100%\" }}>\n\t\t\t\t\t\t{(item.data.Taglines?.length ?? 0) > 0 && (\n\t\t\t\t\t\t\t<Typography variant=\"h5\" mb={2}>\n\t\t\t\t\t\t\t\t{item.data.Taglines?.[0]}\n\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t<ShowMoreText\n\t\t\t\t\t\t\tcontent={item.data.Overview ?? \"\"}\n\t\t\t\t\t\t\tcollapsedLines={4}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\tgap: \"0.6em\",\n\t\t\t\t\t\t\t\talignSelf: \"end\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.data.ExternalUrls?.map(\n\t\t\t\t\t\t\t\t(url) =>\n\t\t\t\t\t\t\t\t\turl.Url &&\n\t\t\t\t\t\t\t\t\turl.Name && (\n\t\t\t\t\t\t\t\t\t\t<IconLink key={url.Url} url={url.Url} name={url.Name} />\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div className=\"item-detail-cast\">\n\t\t\t\t\t\t\t{actors?.length > 0 && (\n\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-container\">\n\t\t\t\t\t\t\t\t\t<Typography variant=\"h6\" className=\"item-detail-cast-title\">\n\t\t\t\t\t\t\t\t\t\tActors\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-grid\">\n\t\t\t\t\t\t\t\t\t\t{actors.map((actor) => (\n\t\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card\"\n\t\t\t\t\t\t\t\t\t\t\t\tkey={actor.Id}\n\t\t\t\t\t\t\t\t\t\t\t\tto=\"/person/$id\"\n\t\t\t\t\t\t\t\t\t\t\t\tparams={{\n\t\t\t\t\t\t\t\t\t\t\t\t\tid: actor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{actor.PrimaryImageTag ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\t\talt={actor.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Person\")}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-text\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Role}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{writers.length > 0 && (\n\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-container\">\n\t\t\t\t\t\t\t\t\t<Typography variant=\"h6\" className=\"item-detail-cast-title\">\n\t\t\t\t\t\t\t\t\t\tWriters\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-grid\">\n\t\t\t\t\t\t\t\t\t\t{writers.map((actor) => (\n\t\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card\"\n\t\t\t\t\t\t\t\t\t\t\t\tkey={actor.Id}\n\t\t\t\t\t\t\t\t\t\t\t\tto=\"/person/$id\"\n\t\t\t\t\t\t\t\t\t\t\t\tparams={{\n\t\t\t\t\t\t\t\t\t\t\t\t\tid: actor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{actor.PrimaryImageTag ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\t\talt={actor.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Person\")}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-text\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Role}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{directors.length > 0 && (\n\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-container\">\n\t\t\t\t\t\t\t\t\t<Typography variant=\"h6\" className=\"item-detail-cast-title\">\n\t\t\t\t\t\t\t\t\t\tDirectors\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-grid\">\n\t\t\t\t\t\t\t\t\t\t{directors.map((actor) => (\n\t\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card\"\n\t\t\t\t\t\t\t\t\t\t\t\tkey={actor.Id}\n\t\t\t\t\t\t\t\t\t\t\t\tto=\"/person/$id\"\n\t\t\t\t\t\t\t\t\t\t\t\tparams={{\n\t\t\t\t\t\t\t\t\t\t\t\t\tid: actor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{actor.PrimaryImageTag ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\t\talt={actor.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Person\")}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-text\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Role}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{producers.length > 0 && (\n\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-container\">\n\t\t\t\t\t\t\t\t\t<Typography variant=\"h6\" className=\"item-detail-cast-title\">\n\t\t\t\t\t\t\t\t\t\tProducers\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-grid\">\n\t\t\t\t\t\t\t\t\t\t{producers.map((actor) => (\n\t\t\t\t\t\t\t\t\t\t\t<Link\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card\"\n\t\t\t\t\t\t\t\t\t\t\t\tkey={actor.Id}\n\t\t\t\t\t\t\t\t\t\t\t\tto=\"/person/$id\"\n\t\t\t\t\t\t\t\t\t\t\t\tparams={{\n\t\t\t\t\t\t\t\t\t\t\t\t\tid: actor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{actor.PrimaryImageTag ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\t\talt={actor.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tactor.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tquality: 80,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillWidth: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 200,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-cast-card-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{getTypeIcon(\"Person\")}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-cast-card-text\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"subtitle2\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{actor.Role}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t</Link>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t{seasons.isPending ? (\n\t\t\t\t\t<SeasonSelectorSkeleton />\n\t\t\t\t) : (\n\t\t\t\t\t<div className=\"item-series-seasons-container\">\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\t\t\t\t\tpaddingBottom: \" 0.5em\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\tgap: \"1em\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<Typography variant=\"h5\">\n\t\t\t\t\t\t\t\t\t{seasons.data?.Items?.[Number(currentSeason)]?.Name}\n\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t<Chip\n\t\t\t\t\t\t\t\t\tlabel={\n\t\t\t\t\t\t\t\t\t\tepisodes.isPending ? (\n\t\t\t\t\t\t\t\t\t\t\t<CircularProgress\n\t\t\t\t\t\t\t\t\t\t\t\tsize={20}\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t\tepisodes.data?.TotalRecordCount\n\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\tgap: \"0.75em\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{currentSeasonItem.isSuccess && (\n\t\t\t\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\t\t\t\titemId={currentSeasonItem.data?.Id}\n\t\t\t\t\t\t\t\t\t\tisFavorite={currentSeasonItem.data?.UserData?.IsFavorite}\n\t\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t\t\titemName={currentSeasonItem.data?.Name}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t{currentSeasonItem.isSuccess && (\n\t\t\t\t\t\t\t\t\t<MarkPlayedButton\n\t\t\t\t\t\t\t\t\t\titemId={currentSeasonItem.data?.Id}\n\t\t\t\t\t\t\t\t\t\tisPlayed={currentSeasonItem.data?.UserData?.Played}\n\t\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t\t\titemName={currentSeasonItem.data?.Name}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t<TextField\n\t\t\t\t\t\t\t\t\tvalue={currentSeasonNumber}\n\t\t\t\t\t\t\t\t\tonChange={(e) => {\n\t\t\t\t\t\t\t\t\t\tconst seasonIndex = Number(e.target.value);\n\t\t\t\t\t\t\t\t\t\tif (\n\t\t\t\t\t\t\t\t\t\t\t!Number.isNaN(seasonIndex) &&\n\t\t\t\t\t\t\t\t\t\t\tNumber.isFinite(seasonIndex)\n\t\t\t\t\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t\t\t\t\tsetCurrentSeason(seasonIndex);\n\t\t\t\t\t\t\t\t\t\t\tsessionStorage.setItem(\n\t\t\t\t\t\t\t\t\t\t\t\t`season-${item.data?.Id}`,\n\t\t\t\t\t\t\t\t\t\t\t\tseasonIndex.toString(),\n\t\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tselect\n\t\t\t\t\t\t\t\t\tSelectProps={{\n\t\t\t\t\t\t\t\t\t\tMenuProps: {\n\t\t\t\t\t\t\t\t\t\t\tdisableScrollLock: true,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{seasons.data?.Items?.map((season, index) => {\n\t\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t\t<MenuItem key={season.Id} value={index}>\n\t\t\t\t\t\t\t\t\t\t\t\t{season.Name}\n\t\t\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t\t\t</TextField>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<Divider />\n\t\t\t\t\t\t<div className=\"item-detail-episode-container\">\n\t\t\t\t\t\t\t{episodes.isPending ? (\n\t\t\t\t\t\t\t\t<EpisodeSkeleton />\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\tepisodes.data?.Items?.map((episode) => {\n\t\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\t\t\t\t\tkey={episode.Id}\n\t\t\t\t\t\t\t\t\t\t\tonClick={() =>\n\t\t\t\t\t\t\t\t\t\t\t\tnavigate({\n\t\t\t\t\t\t\t\t\t\t\t\t\tto: \"/episode/$id\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tparams: { id: episode.Id ?? \"\" },\n\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tinitial={{\n\t\t\t\t\t\t\t\t\t\t\t\ttransform: \"translateY(10px)\",\n\t\t\t\t\t\t\t\t\t\t\t\topacity: 0,\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\twhileInView={{\n\t\t\t\t\t\t\t\t\t\t\t\topacity: 1,\n\t\t\t\t\t\t\t\t\t\t\t\ttransform: \"translateY(0px)\",\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\tviewport={{\n\t\t\t\t\t\t\t\t\t\t\t\tonce: true,\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\ttransition={{\n\t\t\t\t\t\t\t\t\t\t\t\tduration: 0.2,\n\t\t\t\t\t\t\t\t\t\t\t\tease: \"backInOut\",\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-episode\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\" textAlign=\"center\">\n\t\t\t\t\t\t\t\t\t\t\t\t{episode.IndexNumberEnd\n\t\t\t\t\t\t\t\t\t\t\t\t\t? `${episode.IndexNumber} / ${episode.IndexNumberEnd}`\n\t\t\t\t\t\t\t\t\t\t\t\t\t: (episode.IndexNumber ?? 0)}\n\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\tclassName={\n\t\t\t\t\t\t\t\t\t\t\t\t\tepisode.UserData?.Played\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t? \"item-detail-episode-image-container watched\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"item-detail-episode-image-container\"\n\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-episode-image-overlay\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<PlayButton\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titem={episode}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\titemType={BaseItemKind.Episode}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"medium\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tbuttonProps={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t//@ts-expect-error\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tcolor: \"white\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstyle: {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tcolor: \"black \",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ticonOnly\n\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-episode-image-icon-container\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded item-detail-episode-image-icon\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttv_gen\n\t\t\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\t\t\t\talt={episode.Name ?? \"\"}\n\t\t\t\t\t\t\t\t\t\t\t\t\tsrc={\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tapi &&\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tgetImageUrlsApi(api).getItemImageUrlById(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tepisode.Id ?? \"\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\"Primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\ttag: episode.ImageTags?.Primary,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfillHeight: 300,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"item-detail-episode-image\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tonLoad={(e) => {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\te.currentTarget.style.opacity = \"1\";\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t{(episode.UserData?.PlaybackPositionTicks ?? 0) > 0 && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<div className=\"card-progress-container\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"card-progress\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\twidth: `${episode.UserData?.PlayedPercentage}%`,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-episode-info\">\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t{episode.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"flex flex-row flex-align-center\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tgap: \"0.5em\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{episode.PremiereDate && (\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<Chip\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"filled\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tlabel={getEpisodeDateString(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tnew Date(episode.PremiereDate),\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t<Typography variant=\"caption\">\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t{getRuntimeCompact(episode.RunTimeTicks ?? 0)}\n\t\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tdisplay: \"-webkit-box\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\ttextOverflow: \"ellipsis\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tWebkitLineClamp: 2,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tWebkitBoxOrient: \"vertical\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.7,\n\t\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t\t{episode.Overview}\n\t\t\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t\t<div className=\"item-detail-episode-buttons\">\n\t\t\t\t\t\t\t\t\t\t\t\t<LikeButton\n\t\t\t\t\t\t\t\t\t\t\t\t\titemId={episode.Id}\n\t\t\t\t\t\t\t\t\t\t\t\t\titemName={episode.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t\tisFavorite={episode.UserData?.IsFavorite}\n\t\t\t\t\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t\t<MarkPlayedButton\n\t\t\t\t\t\t\t\t\t\t\t\t\titemId={episode.Id}\n\t\t\t\t\t\t\t\t\t\t\t\t\titemName={episode.Name}\n\t\t\t\t\t\t\t\t\t\t\t\t\tisPlayed={episode.UserData?.Played}\n\t\t\t\t\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id]}\n\t\t\t\t\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t\t</motion.div>\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t)}\n\t\t\t\t{specialFeatures.isSuccess &&\n\t\t\t\t\t(specialFeatures.data?.length ?? 0) > 0 && (\n\t\t\t\t\t\t<CardScroller\n\t\t\t\t\t\t\ttitle=\"Special Features\"\n\t\t\t\t\t\t\tdisplayCards={7}\n\t\t\t\t\t\t\tdisableDecoration\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{specialFeatures.data?.map((special) => {\n\t\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\t\tkey={special.Id}\n\t\t\t\t\t\t\t\t\t\titem={special}\n\t\t\t\t\t\t\t\t\t\tseriesId={special.SeriesId}\n\t\t\t\t\t\t\t\t\t\tcardTitle={special.Name}\n\t\t\t\t\t\t\t\t\t\timageType=\"Primary\"\n\t\t\t\t\t\t\t\t\t\tcardCaption={getRuntimeMusic(special.RunTimeTicks ?? 0)}\n\t\t\t\t\t\t\t\t\t\tcardType=\"portrait\"\n\t\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id, \"similarItem\"]}\n\t\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t\t\tonClick={() => {}}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t})}\n\t\t\t\t\t\t</CardScroller>\n\t\t\t\t\t)}\n\t\t\t\t{(similarItems.data?.TotalRecordCount ?? 0) > 0 && (\n\t\t\t\t\t<CardScroller\n\t\t\t\t\t\ttitle=\"You might also like\"\n\t\t\t\t\t\tdisplayCards={7}\n\t\t\t\t\t\tdisableDecoration\n\t\t\t\t\t>\n\t\t\t\t\t\t{similarItems.data?.Items?.map((similar) => {\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\t\tkey={similar.Id}\n\t\t\t\t\t\t\t\t\titem={similar}\n\t\t\t\t\t\t\t\t\tseriesId={similar.SeriesId}\n\t\t\t\t\t\t\t\t\tcardTitle={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? similar.SeriesName\n\t\t\t\t\t\t\t\t\t\t\t: similar.Name\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\timageType={\"Primary\"}\n\t\t\t\t\t\t\t\t\tcardCaption={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Episode\n\t\t\t\t\t\t\t\t\t\t\t? `S${similar.ParentIndexNumber}:E${similar.IndexNumber} - ${similar.Name}`\n\t\t\t\t\t\t\t\t\t\t\t: similar.Type === BaseItemKind.Series\n\t\t\t\t\t\t\t\t\t\t\t\t? `${similar.ProductionYear} - ${\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tsimilar.EndDate\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t? new Date(similar.EndDate).toLocaleString([], {\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tyear: \"numeric\",\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t: \"Present\"\n\t\t\t\t\t\t\t\t\t\t\t\t\t}`\n\t\t\t\t\t\t\t\t\t\t\t\t: similar.ProductionYear\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tcardType={\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.MusicAlbum ||\n\t\t\t\t\t\t\t\t\t\tsimilar.Type === BaseItemKind.Audio\n\t\t\t\t\t\t\t\t\t\t\t? \"square\"\n\t\t\t\t\t\t\t\t\t\t\t: \"portrait\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tqueryKey={[\"item\", id, \"similarItem\"]}\n\t\t\t\t\t\t\t\t\tuserId={user?.Id}\n\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t})}\n\t\t\t\t\t</CardScroller>\n\t\t\t\t)}\n\t\t\t</motion.div>\n\t\t);\n\t}\n\tif (item.isError || similarItems.isError) {\n\t\treturn <ErrorNotice />;\n\t}\n}\n\nexport default SeriesTitlePage;\n"
  },
  {
    "path": "src/routes/_api/series/series.scss",
    "content": "$cardWidth: 18%;\n\n.item-series.item {\n\tgap: 1em;\n\t\n\t.item-detail {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: 1fr 35%;\n\t\tjustify-items: center;\n\t\talign-items: start;\n\t\tgap: 3em;\n\t\t\n\t\t&-nextUp {\n\t\t\tmax-width: 23em;\n\t\t}\n\t\t\n\t\t&-cast {\n\t\t\tmax-height: 50vh;\n\t\t\toverflow-y: auto;\n\t\t\toverflow-x: hidden;\n\t\t\tmask-image: linear-gradient(to top, transparent, black 1.2em);\n\t\t\t-webkit-mask-image: linear-gradient(to top, transparent, black 1.2em);\n\t\t\t&-title {\n\t\t\t\tposition: sticky;\n\t\t\t\ttop: 0;\n\t\t\t\tbackground: black;\n\t\t\t\tborder:1px solid rgb(255 255 255 / 0.2) ;\n\t\t\t\tpadding: 0.35em;\n\t\t\t\tborder-radius: 10px;\n\t\t\t\tz-index: 1;\n\t\t\t}\n\t\t\t&-container {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\tgap: 1em;\n\t\t\t}\n\t\t\t&-grid {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgap: 1em;\n\t\t\t\tmargin-bottom: 1em;\n\t\t\t\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n\t\t\t}\n\t\t\t&-card {\n\t\t\t\tdisplay: grid;\n\t\t\t\tgrid-template-columns: 6em 1fr;\n\t\t\t\tgap: 0.5em;\n\t\t\t\talign-items: center;\n\t\t\t\tcolor: white !important;\n\t\t\t\ttext-decoration: none;\n\t\t\t\tpadding: 0.5em;\n\t\t\t\tbackground: rgb(255 255 255 / 0);\n\t\t\t\ttransition: background $transition-time-default;\n\t\t\t\tborder-radius: $border-radius-default;\n\t\t\t\t&:hover {\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t}\n\t\t\t\t&-image,\n\t\t\t\t&-icon {\n\t\t\t\t\twidth: 6em;\n\t\t\t\t\theight: 6em;\n\t\t\t\t\tobject-fit: cover;\n\t\t\t\t\tborder-radius: 100%;\n\t\t\t\t\tbox-shadow: $shadow-card-image;\n\t\t\t\t}\n\t\t\t\t&-icon {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tfont-size: 1em;\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t\t.material-symbols-rounded {\n\t\t\t\t\t\tfont-size: 3em;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t&-link {\n\t\t\tcolor: white;\n\t\t\ttext-decoration-color: rgb(255 255 255 / 0.5);\n\t\t\t&:hover {\n\t\t\t\ttext-decoration-color: white;\n\t\t\t}\n\t\t}\n\t\t&-episode{\n\t\t\tdisplay:grid;\n\t\t\tgrid-template-columns: 3.2em 12em 1fr 6em;\n\t\t\tflex-direction: row;\n\t\t\twidth: 100%;\n\t\t\tgap:1.8em;\n\t\t\talign-items: center;\n\t\t\tjustify-content: center;\n\t\t\tcursor: pointer;\n\t\t\tpadding: 1em 0;\n\t\t\t\n\t\t\t&-container {\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\tflex-wrap: nowrap;\n\t\t\t\tgap: 0.5em;\n\t\t\t\tmargin-top: 0.5em;\n\t\t\t\twidth: 100%;\n\t\t\t}\n\t\t\t&-image{\n\t\t\t\twidth: 100%;\n\t\t\t\theight: 100%;\n\t\t\t\tobject-fit: cover;\n\t\t\t\topacity: 0;\n\t\t\t\ttransition: opacity $transition-time-default;\n\t\t\t\tposition: absolute;\n\t\t\t\ttop: 0;\n\t\t\t\tleft: 0;\n\t\t\t\tz-index: 0;\n\t\t\t\t&-container{\n\t\t\t\t\twidth: 100%;\n\t\t\t\t\taspect-ratio: 1.66666;\n\t\t\t\t\tposition: relative;\n\t\t\t\t\toverflow: hidden;\n\t\t\t\t\tborder-radius: $border-radius-default;\n\t\t\t\t\tflex-shrink: 0;\n\t\t\t\t\t&::after{\n\t\t\t\t\t\tcontent: \"\";\n\t\t\t\t\t\tposition: absolute;\n\t\t\t\t\t\ttop: 0;\n\t\t\t\t\t\tright: 0;\n\t\t\t\t\t\tborder: 0.65em solid #00c853;\n\t\t\t\t\t\tborder-bottom-color: transparent;\n\t\t\t\t\t\tborder-left-color: transparent;\n\t\t\t\t\t\topacity: 0;\n\t\t\t\t\t\ttransition: opacity $transition-time-default;\n\t\t\t\t\t}\n\t\t\t\t\t&.watched::after {\n\t\t\t\t\t\topacity: 1;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t&-overlay{\n\t\t\t\t\tposition: absolute;\n\t\t\t\t\ttop: 0;\n\t\t\t\t\tleft: 0;\n\t\t\t\t\tright: 0;\n\t\t\t\t\tbottom: 0;\n\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\topacity: 0;\n\t\t\t\t\ttransition: opacity $transition-time-default;\n\t\t\t\t\tz-index: 2;\n\t\t\t\t}\n\t\t\t\t&-icon {\n\t\t\t\t\tfont-size: 3em;\n\t\t\t\t\t&-container {\n\t\t\t\t\t\tdisplay: flex;\n\t\t\t\t\t\talign-items: center;\n\t\t\t\t\t\tjustify-content: center;\n\t\t\t\t\t\theight: 100%;\n\t\t\t\t\t\tbackground: rgb(255 255 255 / 0.1);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t&-info{\n\t\t\t\tdisplay: flex;\n\t\t\t\tflex-direction: column;\n\t\t\t\talign-items: flex-start;\n\t\t\t\tjustify-content: center;\n\t\t\t\tgap: 0.4em;\n\t\t\t\twidth: 100%;\n\t\t\t\tpadding-right: 2em;\n\t\t\t}\n\t\t\t&-buttons{\n\t\t\t\tdisplay: flex;\n\t\t\t\tgap: 1em;\n\t\t\t}\n\t\t\t\n\t\t\t&:hover .item-detail-episode-image-overlay, &:focus-within .item-detail-episode-image-overlay{\n\t\t\t\topacity: 1;\n\t\t\t}\n\t\t}\n\t}\n}\n\n@media (max-width: 1320px) {\n\t.item-series .item-detail {\n\t\t&-cast {\n\t\t\t&-grid {\n\t\t\t\tgrid-template-columns: 1fr;\n\t\t\t}\n\t\t}\n\t\t&-episodes-container {\n\t\t\tgrid-template-columns: repeat(3, minmax(0, 1fr));\n\t\t}\n\t}\n}\n\n@media (max-width: 830px) {\n\t.item-series .item-detail {\n\t\t&-episodes-container {\n\t\t\tgrid-template-columns: repeat(2, minmax(0, 1fr));\n\t\t}\n\t}\n}\n\n@media (max-width: 564px) {\n\t.item-series .item-detail {\n\t\t&-episodes-container {\n\t\t\tgrid-template-columns: repeat(1, minmax(0, 1fr));\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/routes/_api/settings/about.scss",
    "content": ".settings-about {\n    &-container {\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        justify-content: flex-start;\n        width: 100%;\n    }\n    &-logo{\n        width: 60%;\n        object-fit: cover;\n    }\n    &-table{\n        display: flex;\n        flex-direction: column;\n        align-items: stretch;\n        justify-content: center;\n        width: 100%;\n        max-width: 60%;\n        margin-top: 2em;\n        &-row {\n            display: flex;\n            flex-direction: row;\n            align-items: center;\n            justify-content: space-between;\n            padding: 0.5em 1em;\n            border-bottom: 1px solid rgba(255, 255, 255, 0.12);\n            &:last-child {\n                border-bottom: none;\n            }\n        }\n    }\n    &-links {\n        display: flex;\n        flex-direction: column;\n        align-items: flex-start;\n        justify-content: center;\n        width: 100%;\n        max-width: 60%;\n        margin-top: 2em;\n    }\n}"
  },
  {
    "path": "src/routes/_api/settings/about.tsx",
    "content": "import { Fab, Link, Typography } from \"@mui/material\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport React from \"react\";\nimport blinkLogo from \"@/assets/logoBlackTransparent.png\";\nimport { version } from \"../../../../package.json\";\n\nimport \"./about.scss\";\nimport { useSuspenseQuery } from \"@tanstack/react-query\";\nimport {\n\tgetSystemInfoQueryOptions,\n\tgetUpdateQueryOptions,\n} from \"@/utils/queries/about\";\nimport { useApiInContext } from \"@/utils/store/api\";\n\nexport const Route = createFileRoute(\"/_api/settings/about\")({\n\tcomponent: RouteComponent,\n\tloader: async ({ context: { queryClient, api } }) => {\n\t\tawait queryClient.ensureQueryData(getSystemInfoQueryOptions(api));\n\t\tawait queryClient.ensureQueryData(getUpdateQueryOptions);\n\t},\n});\n\nfunction RouteComponent() {\n\tconst api = useApiInContext((s) => s.api);\n\tconst systemInfo = useSuspenseQuery(getSystemInfoQueryOptions(api));\n\tconst updateInfo = useSuspenseQuery(getUpdateQueryOptions);\n\n\treturn (\n\t\t<div className=\"settings-page-scrollY settings-about-container\">\n\t\t\t{/* <Typography variant=\"h4\">About</Typography> */}\n\t\t\t<img src={blinkLogo} alt=\"Blink\" className=\"settings-about-logo\" />\n\t\t\t<Typography sx={{ opacity: 0.8, marginTop: \"-4em\" }}>\n\t\t\t\tv{version}\n\t\t\t</Typography>\n\t\t\t<div className=\"settings-about-table\">\n\t\t\t\t<div className=\"settings-about-table-row\">\n\t\t\t\t\t<Typography variant=\"subtitle1\">Server Name</Typography>\n\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t{systemInfo.data.ServerName}\n\t\t\t\t\t</Typography>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"settings-about-table-row\">\n\t\t\t\t\t<Typography variant=\"subtitle1\">Jellyfin Version</Typography>\n\t\t\t\t\t<Typography variant=\"subtitle1\">{systemInfo.data.Version}</Typography>\n\t\t\t\t</div>\n\t\t\t\t<div className=\"settings-about-table-row\">\n\t\t\t\t\t<Typography variant=\"subtitle1\">Update Available</Typography>\n\t\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\t\t{updateInfo.data\n\t\t\t\t\t\t\t? `v${updateInfo.data.version}`\n\t\t\t\t\t\t\t: \"No update available.\"}\n\t\t\t\t\t</Typography>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<div className=\"settings-about-links\">\n\t\t\t\t<Typography variant=\"h6\" style={{ marginBottom: \"0.5em\" }}>\n\t\t\t\t\tLinks\n\t\t\t\t</Typography>\n\t\t\t\t<Link href=\"https://github.com/prayag17/Blink\">\n\t\t\t\t\thttps://github.com/prayag17/Blink\n\t\t\t\t</Link>\n\t\t\t\t<Link href=\"https://jellyfin.org\">https://jellyfin.org</Link>\n\t\t\t</div>\n\t\t\t<Fab\n\t\t\t\tLinkComponent={Link}\n\t\t\t\thref=\"https://github.com/sponsors/prayag17\"\n\t\t\t\tvariant=\"extended\"\n\t\t\t\ttarget=\"_blank\"\n\t\t\t\tcolor=\"primary\"\n\t\t\t\tsx={{\n\t\t\t\t\tposition: \"fixed\",\n\t\t\t\t\tbottom: \"2em\",\n\t\t\t\t\tright: \"2em\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<span\n\t\t\t\t\tclassName=\"material-symbols-rounded fill\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tmarginRight: \"0.25em\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\tvolunteer_activism\n\t\t\t\t</span>\n\t\t\t\tSupport\n\t\t\t</Fab>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/routes/_api/settings/changeServer/index.tsx",
    "content": "import {\n\talpha,\n\tButton,\n\tCard,\n\tCardActions,\n\tCardContent,\n\tChip,\n\tGrid,\n\tIconButton,\n\tTypography,\n\tuseTheme,\n} from \"@mui/material\";\nimport { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport { useSnackbar } from \"notistack\";\nimport React, { useState } from \"react\";\nimport AddServerDialog from \"@/components/addServerDialog\";\nimport CircularPageLoadingAnimation from \"@/components/circularPageLoadingAnimation\";\nimport {\n\tdelServer,\n\tgetAllServers,\n\tgetDefaultServer,\n\tsetDefaultServer,\n} from \"@/utils/storage/servers\";\nimport { delUser } from \"@/utils/storage/user\";\nimport { useApiInContext } from \"@/utils/store/api\";\n\nexport const Route = createFileRoute(\"/_api/settings/changeServer/\")({\n\tcomponent: ChangeServerRoute,\n});\n\nfunction ChangeServerRoute() {\n\tconst _theme = useTheme();\n\tconst queryClient = useQueryClient();\n\tconst createApi = useApiInContext((s) => s.createApi);\n\tconst { enqueueSnackbar } = useSnackbar();\n\tconst [addServerDialogOpen, setAddServerDialogOpen] = useState(false);\n\n\tconst serversQuery = useQuery({\n\t\tqueryKey: [\"servers\"],\n\t\tqueryFn: async () => {\n\t\t\treturn await getAllServers();\n\t\t},\n\t});\n\n\tconst activeServerQuery = useQuery({\n\t\tqueryKey: [\"activeServer\"],\n\t\tqueryFn: async () => {\n\t\t\treturn await getDefaultServer();\n\t\t},\n\t});\n\n\tconst handleConnect = async (serverId: string) => {\n\t\ttry {\n\t\t\t// Clear current user when switching servers to force login/user fetch\n\t\t\tawait delUser();\n\t\t\tawait setDefaultServer(serverId);\n\n\t\t\tconst servers = await getAllServers();\n\t\t\tconst server = servers.find((s) => s.id === serverId);\n\n\t\t\tif (server) {\n\t\t\t\tcreateApi(server.address);\n\t\t\t}\n\n\t\t\t// Reload the page to reset API context and states\n\t\t\twindow.location.href = \"/\";\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to switch server\", error);\n\t\t\tenqueueSnackbar(\"Failed to switch server\", { variant: \"error\" });\n\t\t}\n\t};\n\n\tconst handleDelete = async (serverId: string) => {\n\t\ttry {\n\t\t\tawait delServer(serverId);\n\t\t\tqueryClient.invalidateQueries({ queryKey: [\"servers\"] });\n\t\t\tenqueueSnackbar(\"Server removed\", { variant: \"success\" });\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Failed to delete server\", error);\n\t\t\tenqueueSnackbar(\"Failed to delete server\", { variant: \"error\" });\n\t\t}\n\t};\n\n\tif (serversQuery.isLoading || activeServerQuery.isLoading) {\n\t\treturn <CircularPageLoadingAnimation />;\n\t}\n\n\treturn (\n\t\t<div className=\"settings-page-scrollY\">\n\t\t\t<div\n\t\t\t\tstyle={{\n\t\t\t\t\tmarginBottom: \"2em\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Typography variant=\"h4\">Servers</Typography>\n\t\t\t</div>\n\n\t\t\t<Grid container spacing={3}>\n\t\t\t\t{serversQuery.data?.map((server) => {\n\t\t\t\t\tconst isActive = activeServerQuery.data === server.id;\n\t\t\t\t\tconst spalshUrl =\n\t\t\t\t\t\tnew URL(server.address).pathname.length > 1\n\t\t\t\t\t\t\t? `${new URL(server.address).origin}${`${new URL(server.address).pathname}/`}Branding/Splashscreen?fillWidth=400&fillHeight=100`\n\t\t\t\t\t\t\t: `${server.address}Branding/Splashscreen?fillWidth=400&fillHeight=100`;\n\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<Grid size={{ xs: 12, sm: 12, md: 6, lg: 4 }} key={server.id}>\n\t\t\t\t\t\t\t<Card\n\t\t\t\t\t\t\t\televation={0}\n\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\t\t\tminHeight: \"280px\",\n\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\t\t\t\t\tposition: \"relative\",\n\t\t\t\t\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\t\t\t\t\tborderRadius: \"24px\",\n\t\t\t\t\t\t\t\t\ttransition: \"all 0.3s cubic-bezier(0.4, 0, 0.2, 1)\",\n\t\t\t\t\t\t\t\t\tborder: \"1px solid rgba(255, 255, 255, 0.08)\",\n\t\t\t\t\t\t\t\t\tbackground: \"rgba(30, 30, 35, 0.4)\",\n\t\t\t\t\t\t\t\t\tboxShadow: \"none\",\n\t\t\t\t\t\t\t\t\tbackdropFilter: \"blur(12px)\",\n\t\t\t\t\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\t\t\t\t\ttransform: \"translateY(-4px)\",\n\t\t\t\t\t\t\t\t\t\tboxShadow: \"0 12px 40px rgba(0,0,0,0.3)\",\n\t\t\t\t\t\t\t\t\t\tborderColor: \"rgba(255, 255, 255, 0.2)\",\n\t\t\t\t\t\t\t\t\t\t\"& .server-card-bg\": {\n\t\t\t\t\t\t\t\t\t\t\ttransform: \"scale(1.05)\",\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{/* Standard BG Section / Cover Area */}\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tclassName=\"server-card-bg\"\n\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\theight: \"100px\",\n\t\t\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\t\t\tbackground: \"linear-gradient(45deg, #2a2a35, #1a1a20)\",\n\t\t\t\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\t\t\t\ttop: 0,\n\t\t\t\t\t\t\t\t\t\tleft: 0,\n\t\t\t\t\t\t\t\t\t\tzIndex: 0,\n\t\t\t\t\t\t\t\t\t\ttransition: \"transform 0.4s ease\",\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<img\n\t\t\t\t\t\t\t\t\t\talt={server.systemInfo?.ServerName || \"Unknown Server\"}\n\t\t\t\t\t\t\t\t\t\tsrc={spalshUrl}\n\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\t\t\t\t\t\tobjectFit: \"cover\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t{isActive && (\n\t\t\t\t\t\t\t\t\t<Chip\n\t\t\t\t\t\t\t\t\t\tlabel=\"Active\"\n\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\tcolor=\"primary\"\n\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\tfontWeight: \"bold\",\n\t\t\t\t\t\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\t\t\t\t\t\ttop: 24,\n\t\t\t\t\t\t\t\t\t\t\tright: 24,\n\t\t\t\t\t\t\t\t\t\t\tzIndex: 2,\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t)}\n\n\t\t\t\t\t\t\t\t<CardContent\n\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\tflexGrow: 1,\n\t\t\t\t\t\t\t\t\t\tzIndex: 1,\n\t\t\t\t\t\t\t\t\t\tpt: \"60px !important\", // Push content down to overlap properly\n\t\t\t\t\t\t\t\t\t\tpx: 3,\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\t\t\t\t\t\t\t\talignItems: \"flex-end\",\n\t\t\t\t\t\t\t\t\t\t\tmarginBottom: \"1.5rem\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\twidth: \"64px\",\n\t\t\t\t\t\t\t\t\t\t\t\theight: \"64px\",\n\t\t\t\t\t\t\t\t\t\t\t\tborderRadius: \"20px\",\n\t\t\t\t\t\t\t\t\t\t\t\tbackground: \"rgba(30, 30, 40, 0.8)\",\n\t\t\t\t\t\t\t\t\t\t\t\tbackdropFilter: \"blur(4px)\",\n\t\t\t\t\t\t\t\t\t\t\t\tborder: \"1px solid rgba(255,255,255,0.1)\",\n\t\t\t\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\t\t\t\t\t\tboxShadow: \"0 8px 16px rgba(0,0,0,0.2)\",\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\t\t\tfontSize: \"32px\",\n\t\t\t\t\t\t\t\t\t\t\t\t\tcolor: \"#fff\",\n\t\t\t\t\t\t\t\t\t\t\t\t\topacity: isActive ? 1 : 0.8,\n\t\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\tdns\n\t\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\tvariant=\"h5\"\n\t\t\t\t\t\t\t\t\t\tcomponent=\"div\"\n\t\t\t\t\t\t\t\t\t\tfontWeight=\"bold\"\n\t\t\t\t\t\t\t\t\t\tnoWrap\n\t\t\t\t\t\t\t\t\t\ttitle={server.systemInfo?.ServerName || \"Unknown Server\"}\n\t\t\t\t\t\t\t\t\t\tsx={{ mb: 0.5, letterSpacing: \"-0.02em\" }}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{server.systemInfo?.ServerName || \"Unknown Server\"}\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\tvariant=\"body2\"\n\t\t\t\t\t\t\t\t\t\tcolor=\"text.secondary\"\n\t\t\t\t\t\t\t\t\t\tnoWrap\n\t\t\t\t\t\t\t\t\t\ttitle={server.address}\n\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\topacity: 0.6,\n\t\t\t\t\t\t\t\t\t\t\tfontFamily: \"monospace\",\n\t\t\t\t\t\t\t\t\t\t\tfontSize: \"0.85rem\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{server.address}\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t</CardContent>\n\n\t\t\t\t\t\t\t\t<CardActions\n\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\t\t\t\t\t\t\tpadding: \"24px\",\n\t\t\t\t\t\t\t\t\t\tzIndex: 1,\n\t\t\t\t\t\t\t\t\t\tmt: \"auto\",\n\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\tvariant=\"outlined\"\n\t\t\t\t\t\t\t\t\t\tcolor=\"error\"\n\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\tonClick={() => handleDelete(server.id)}\n\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\tminWidth: \"40px\",\n\t\t\t\t\t\t\t\t\t\t\twidth: \"40px\",\n\t\t\t\t\t\t\t\t\t\t\theight: \"40px\",\n\t\t\t\t\t\t\t\t\t\t\tborderRadius: \"12px\",\n\t\t\t\t\t\t\t\t\t\t\tp: 0,\n\t\t\t\t\t\t\t\t\t\t\tborder: \"1px solid rgba(211, 47, 47, 0.3)\",\n\t\t\t\t\t\t\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\t\t\t\t\t\t\tbackgroundColor: \"rgba(211, 47, 47, 0.1)\",\n\t\t\t\t\t\t\t\t\t\t\t\tborder: \"1px solid rgba(211, 47, 47, 0.8)\",\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tdisabled={isActive}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">delete</span>\n\t\t\t\t\t\t\t\t\t</Button>\n\n\t\t\t\t\t\t\t\t\t{!isActive ? (\n\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\t\t\t\t\t\t\tonClick={() => handleConnect(server.id)}\n\t\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\t\tborderRadius: \"12px\",\n\t\t\t\t\t\t\t\t\t\t\t\ttextTransform: \"none\",\n\t\t\t\t\t\t\t\t\t\t\t\tfontWeight: 600,\n\t\t\t\t\t\t\t\t\t\t\t\tpx: 3,\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tConnect\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\t\t\t\t\tdisabled\n\t\t\t\t\t\t\t\t\t\t\tstartIcon={\n\t\t\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">check</span>\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\t\tborderRadius: \"12px\",\n\t\t\t\t\t\t\t\t\t\t\t\ttextTransform: \"none\",\n\t\t\t\t\t\t\t\t\t\t\t\tfontWeight: 600,\n\t\t\t\t\t\t\t\t\t\t\t\tcolor: \"text.primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\"&.Mui-disabled\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\tcolor: \"text.primary\",\n\t\t\t\t\t\t\t\t\t\t\t\t\topacity: 0.7,\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tConnected\n\t\t\t\t\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t</CardActions>\n\t\t\t\t\t\t\t</Card>\n\t\t\t\t\t\t</Grid>\n\t\t\t\t\t);\n\t\t\t\t})}\n\n\t\t\t\t{/* Add Server Card */}\n\t\t\t\t<Grid size={{ xs: 12, sm: 12, md: 6, lg: 4 }}>\n\t\t\t\t\t<Card\n\t\t\t\t\t\tonClick={() => setAddServerDialogOpen(true)}\n\t\t\t\t\t\televation={0}\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\t\tminHeight: \"280px\",\n\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\tposition: \"relative\",\n\t\t\t\t\t\t\tborderRadius: \"24px\",\n\t\t\t\t\t\t\tcursor: \"pointer\",\n\t\t\t\t\t\t\tbackground: \"rgba(255, 255, 255, 0.02)\",\n\t\t\t\t\t\t\tborder: \"2px dashed rgba(255, 255, 255, 0.1)\",\n\t\t\t\t\t\t\ttransition: \"all 0.2s ease-in-out\",\n\t\t\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\t\t\tbackground: \"rgba(255, 255, 255, 0.05)\",\n\t\t\t\t\t\t\t\tborderColor: \"rgba(255, 255, 255, 0.3)\",\n\t\t\t\t\t\t\t\ttransform: \"translateY(-4px)\",\n\t\t\t\t\t\t\t\t\"& .add-icon\": {\n\t\t\t\t\t\t\t\t\ttransform: \"scale(1.1) rotate(90deg)\",\n\t\t\t\t\t\t\t\t\tcolor: \"var(--mui-palette-primary-main)\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"add-icon\"\n\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\twidth: \"80px\",\n\t\t\t\t\t\t\t\theight: \"80px\",\n\t\t\t\t\t\t\t\tborderRadius: \"50%\",\n\t\t\t\t\t\t\t\tbackgroundColor: \"rgba(255, 255, 255, 0.05)\",\n\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\t\t\tmarginBottom: \"1.5rem\",\n\t\t\t\t\t\t\t\ttransition: \"all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)\",\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\tstyle={{ fontSize: \"40px\", opacity: 0.7 }}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\tadd\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<Typography variant=\"h6\" fontWeight=\"bold\">\n\t\t\t\t\t\t\tAdd Server\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t<Typography variant=\"body2\" color=\"text.secondary\" sx={{ mt: 1 }}>\n\t\t\t\t\t\t\tConnect to a new Jellyfin server\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t</Card>\n\t\t\t\t</Grid>\n\t\t\t</Grid>\n\n\t\t\t<AddServerDialog\n\t\t\t\topen={addServerDialogOpen}\n\t\t\t\tsetAddServerDialog={setAddServerDialogOpen}\n\t\t\t\tsideEffect={async () => {\n\t\t\t\t\tawait queryClient.invalidateQueries({ queryKey: [\"servers\"] });\n\t\t\t\t}}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/routes/_api/settings/preferences.tsx",
    "content": "import {\n\tButton,\n\tFormControlLabel,\n\tMenuItem,\n\tSwitch,\n\tTextField,\n\tTypography,\n} from \"@mui/material\";\nimport { createFileRoute } from \"@tanstack/react-router\";\nimport React from \"react\";\n\nimport \"../settings.scss\";\nimport { getLocalizationApi } from \"@jellyfin/sdk/lib/utils/api/localization-api\";\n// import { getDisplayPreferencesApi } from \"@jellyfin/sdk/lib/utils/api/display-preferences-api\";\nimport { getUserApi } from \"@jellyfin/sdk/lib/utils/api/user-api\";\nimport { useForm } from \"@tanstack/react-form\";\nimport { useSuspenseQuery } from \"@tanstack/react-query\";\nimport { useSnackbar } from \"notistack\";\nimport CircularPageLoadingAnimation from \"@/components/circularPageLoadingAnimation\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useCentralStore } from \"@/utils/store/central\";\n\nexport const Route = createFileRoute(\"/_api/settings/preferences\")({\n\tcomponent: RouteComponent,\n\tpendingComponent: () => <CircularPageLoadingAnimation />,\n});\n\nfunction RouteComponent() {\n\tconst api = useApiInContext((s) => s.api);\n\tconst user = useCentralStore((s) => s.currentUser);\n\tconst cultures = useSuspenseQuery({\n\t\tqueryKey: [\"settings\", \"cultures\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) return;\n\t\t\tconst result = await getLocalizationApi(api).getCultures();\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n\tconst { enqueueSnackbar } = useSnackbar();\n\n\tconst form = useForm({\n\t\tdefaultValues: {\n\t\t\tpreferredAudioLanguage:\n\t\t\t\tuser?.Configuration?.AudioLanguagePreference ?? \"anyLanguage\",\n\t\t\tplayDefaultAudioTrack: user?.Configuration?.PlayDefaultAudioTrack,\n\t\t\tpreferredSubtitleLanguage: user?.Configuration?.SubtitleLanguagePreference\n\t\t\t\t? user?.Configuration?.SubtitleLanguagePreference\n\t\t\t\t: \"anyLanguage\",\n\t\t\trememberSubtitleSelections:\n\t\t\t\tuser?.Configuration?.RememberSubtitleSelections,\n\t\t},\n\t\tonSubmit: async (values) => {\n\t\t\tif (!api) return;\n\t\t\tawait getUserApi(api).updateUserConfiguration({\n\t\t\t\tuserId: user?.Id,\n\t\t\t\tuserConfiguration: {\n\t\t\t\t\tAudioLanguagePreference: values.value.preferredAudioLanguage,\n\t\t\t\t\tPlayDefaultAudioTrack: values.value.playDefaultAudioTrack,\n\t\t\t\t\tSubtitleLanguagePreference: values.value.preferredSubtitleLanguage,\n\t\t\t\t\tRememberSubtitleSelections: values.value.rememberSubtitleSelections,\n\t\t\t\t},\n\t\t\t});\n\t\t\t// await getDisplayPreferencesApi(api).updateDisplayPreferences({\n\t\t\t// \tuserId: user?.Id,\n\t\t\t// \tdisplayPreferencesId: api.deviceInfo.id,\n\t\t\t// \tclient: \"blink\",\n\t\t\t// \tdisplayPreferencesDto: {\n\t\t\t// \t\t...displayPreferences.data,\n\t\t\t// \t\tCustomPrefs: {\n\t\t\t// \t\t\tRememberVolume: JSON.stringify(values.value.rememberVolume),\n\t\t\t// \t\t},\n\t\t\t// \t},\n\t\t\t// });\n\t\t\tenqueueSnackbar(\"Settings saved\", {\n\t\t\t\tvariant: \"success\",\n\t\t\t});\n\t\t},\n\t});\n\n\treturn (\n\t\t<div className=\"settings-page-scrollY\">\n\t\t\t<Typography variant=\"h4\">Preferences</Typography>\n\t\t\t{/* <form.Pro */}\n\t\t\t<form\n\t\t\t\tonSubmit={(e) => {\n\t\t\t\t\te.preventDefault();\n\t\t\t\t\te.stopPropagation();\n\t\t\t\t\tform.handleSubmit();\n\t\t\t\t}}\n\t\t\t\tonError={() =>\n\t\t\t\t\tenqueueSnackbar(\"Error saving settings\", { variant: \"error\" })\n\t\t\t\t}\n\t\t\t\tclassName=\"settings-form\"\n\t\t\t>\n\t\t\t\t<form.Field\n\t\t\t\t\tname=\"preferredAudioLanguage\"\n\t\t\t\t\t// children={}\n\t\t\t\t>\n\t\t\t\t\t{(field) => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<FormControlLabel\n\t\t\t\t\t\t\t\tcontrol={\n\t\t\t\t\t\t\t\t\t<TextField\n\t\t\t\t\t\t\t\t\t\tdefaultValue={field.state.value}\n\t\t\t\t\t\t\t\t\t\tselect\n\t\t\t\t\t\t\t\t\t\thiddenLabel\n\t\t\t\t\t\t\t\t\t\tvariant=\"filled\"\n\t\t\t\t\t\t\t\t\t\tonChange={(e) => field.handleChange(e.target.value)}\n\t\t\t\t\t\t\t\t\t\tonBlur={field.handleBlur}\n\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\twidth: \"18em\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<MenuItem key={\"anyLanguage\"} value={\"anyLanguage\"}>\n\t\t\t\t\t\t\t\t\t\t\tAny Language\n\t\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t\t\t{cultures.data?.map((option) => (\n\t\t\t\t\t\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\t\t\t\t\t\tkey={option.ThreeLetterISOLanguageName}\n\t\t\t\t\t\t\t\t\t\t\t\tvalue={option.ThreeLetterISOLanguageName ?? \"none\"}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{option.DisplayName}\n\t\t\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</TextField>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlabel={\n\t\t\t\t\t\t\t\t\t<div className=\"settings-option-info\">\n\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\" fontWeight={400}>\n\t\t\t\t\t\t\t\t\t\t\tPreferred Audio Language\n\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"settings-option-info-caption\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tChoose your preferred audio language.\n\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlabelPlacement=\"start\"\n\t\t\t\t\t\t\t\tclassName=\"settings-option\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t);\n\t\t\t\t\t}}\n\t\t\t\t</form.Field>\n\t\t\t\t<form.Field\n\t\t\t\t\tname=\"playDefaultAudioTrack\"\n\t\t\t\t\t// children={}\n\t\t\t\t>\n\t\t\t\t\t{(field) => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<FormControlLabel\n\t\t\t\t\t\t\t\tcontrol={\n\t\t\t\t\t\t\t\t\t<Switch\n\t\t\t\t\t\t\t\t\t\tchecked={field.state.value}\n\t\t\t\t\t\t\t\t\t\tonChange={(_, checked) => field.handleChange(checked)}\n\t\t\t\t\t\t\t\t\t\tonBlur={field.handleBlur}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlabel={\n\t\t\t\t\t\t\t\t\t<div className=\"settings-option-info\">\n\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\" fontWeight={400}>\n\t\t\t\t\t\t\t\t\t\t\tPlay default audio track regardless of language\n\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"settings-option-info-caption\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tPlay the default audio track of the media item, even if it\n\t\t\t\t\t\t\t\t\t\t\tdoes not match your preferred audio language.\n\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlabelPlacement=\"start\"\n\t\t\t\t\t\t\t\tclassName=\"settings-option\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t);\n\t\t\t\t\t}}\n\t\t\t\t</form.Field>\n\t\t\t\t<form.Field\n\t\t\t\t\tname=\"preferredSubtitleLanguage\"\n\t\t\t\t\t// children={}\n\t\t\t\t>\n\t\t\t\t\t{(field) => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<FormControlLabel\n\t\t\t\t\t\t\t\tcontrol={\n\t\t\t\t\t\t\t\t\t<TextField\n\t\t\t\t\t\t\t\t\t\tdefaultValue={field.state.value}\n\t\t\t\t\t\t\t\t\t\tselect\n\t\t\t\t\t\t\t\t\t\thiddenLabel\n\t\t\t\t\t\t\t\t\t\tvariant=\"filled\"\n\t\t\t\t\t\t\t\t\t\tonChange={(e) => field.handleChange(e.target.value)}\n\t\t\t\t\t\t\t\t\t\tonBlur={field.handleBlur}\n\t\t\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\t\t\twidth: \"18em\",\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<MenuItem key={\"anyLanguage\"} value={\"anyLanguage\"}>\n\t\t\t\t\t\t\t\t\t\t\tAny Language\n\t\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t\t\t{cultures.data?.map((option) => (\n\t\t\t\t\t\t\t\t\t\t\t<MenuItem\n\t\t\t\t\t\t\t\t\t\t\t\tkey={option.ThreeLetterISOLanguageName}\n\t\t\t\t\t\t\t\t\t\t\t\tvalue={option.ThreeLetterISOLanguageName ?? \"none\"}\n\t\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\t\t{option.DisplayName}\n\t\t\t\t\t\t\t\t\t\t\t</MenuItem>\n\t\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t\t</TextField>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlabel={\n\t\t\t\t\t\t\t\t\t<div className=\"settings-option-info\">\n\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\" fontWeight={400}>\n\t\t\t\t\t\t\t\t\t\t\tPreferred Subtitle Language\n\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"settings-option-info-caption\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tChoose your preferred subtitle language.\n\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlabelPlacement=\"start\"\n\t\t\t\t\t\t\t\tclassName=\"settings-option\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t);\n\t\t\t\t\t}}\n\t\t\t\t</form.Field>\n\t\t\t\t<form.Field\n\t\t\t\t\tname=\"rememberSubtitleSelections\"\n\t\t\t\t\t// children={}\n\t\t\t\t>\n\t\t\t\t\t{(field) => {\n\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t<FormControlLabel\n\t\t\t\t\t\t\t\tcontrol={\n\t\t\t\t\t\t\t\t\t<Switch\n\t\t\t\t\t\t\t\t\t\tchecked={field.state.value}\n\t\t\t\t\t\t\t\t\t\tonChange={(_, checked) => field.handleChange(checked)}\n\t\t\t\t\t\t\t\t\t\tonBlur={field.handleBlur}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlabel={\n\t\t\t\t\t\t\t\t\t<div className=\"settings-option-info\">\n\t\t\t\t\t\t\t\t\t\t<Typography variant=\"subtitle1\" fontWeight={400}>\n\t\t\t\t\t\t\t\t\t\t\tSet subtitle track based on previous item\n\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\t\tvariant=\"caption\"\n\t\t\t\t\t\t\t\t\t\t\tclassName=\"settings-option-info-caption\"\n\t\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t\tTry to set the subtitle track to the closest match to the\n\t\t\t\t\t\t\t\t\t\t\tlast video.\n\t\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlabelPlacement=\"start\"\n\t\t\t\t\t\t\t\tclassName=\"settings-option\"\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t);\n\t\t\t\t\t}}\n\t\t\t\t</form.Field>\n\t\t\t\t<Button type=\"submit\" size=\"large\" variant=\"contained\">\n\t\t\t\t\tSave\n\t\t\t\t</Button>\n\t\t\t</form>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/routes/_api/settings.scss",
    "content": "@use \"../../styles/variables.scss\" as *;\n\n.settings {\n    &-page {\n        &-container {\n            display: grid;\n            grid-template-columns: 16em 1fr;\n            padding: 0;\n            width: 100vw;\n            height: 100vh;\n        }\n        &-scrollY {\n            padding: 2em;\n            overflow-x: hidden;\n            width: 100%;\n        }\n    }\n    &-form{\n        display: flex;\n        flex-direction: column;\n        align-items: stretch;\n        justify-content: flex-start;\n        gap: 1.5em;\n        width: 100%;\n        margin-top: 2em;\n\n        button[type=\"submit\"] {\n            align-self: flex-end;\n            min-width: 10em;\n        }\n    }\n    &-option {\n        width: 100%;\n        margin: 0 !important;\n        justify-content: space-between;\n        padding: 1em 0;\n        \n        /* Ensure the label text takes available space */\n        .MuiFormControlLabel-label {\n            flex-grow: 1;\n        }\n\n        &-info {\n            display: flex;\n            flex-direction: column;\n            gap: 0.25em;\n            padding-right: 1em;\n            \n            &-caption {\n                opacity: 0.7;\n            }\n        }\n        \n    }\n    &-sidebar {\n        display: flex;\n        flex-direction: column;\n        align-items: stretch;\n        justify-content: flex-start;\n        background: $clr-background-dark;\n        border-right: 1px solid rgba(255, 255, 255, 0.05);\n        padding: 1.5em 1em; // Reduced padding\n        gap: 1em; // Reduced gap\n\n        &-controls {\n            display: flex;\n            align-items: center;\n            justify-content: space-between;\n            padding-bottom: 0.5em; // Reduced\n            \n            // Adjust back button for this context\n            .MuiIconButton-root {\n                color: rgba(255,255,255,0.7);\n                &:hover {\n                    color: white;\n                    background: rgba(255,255,255,0.05);\n                }\n            }\n        }\n\n        &-header {\n            display: flex;\n            flex-direction: column;\n            align-items: center;\n            text-align: center;\n            gap: 0.75em;\n            padding: 1em; // Reduced padding\n            background: rgba(255, 255, 255, 0.03);\n            border-radius: 16px; // Slightly tighter radius\n            border: 1px solid rgba(255, 255, 255, 0.05);\n            transition: all 0.3s ease;\n\n            &:hover {\n                background: rgba(255, 255, 255, 0.05);\n                border-color: rgba(255, 255, 255, 0.1);\n                transform: translateY(-2px);\n            }\n\n            &-image {\n                $size: 3.5em; // Reduced size slightly (was 4.5em)\n                width: $size;\n                height: $size;\n                border-radius: 50%;\n                object-fit: cover;\n                border: 3px solid rgba(0,0,0,0.2);\n                box-shadow: 0 8px 16px rgba(0,0,0,0.3);\n            }\n        }\n\n        &-list {\n            display: flex;\n            flex-direction: column;\n            gap: 0.25em;\n\n            // Section headers\n            .MuiTypography-overline {\n                color: rgba(255,255,255,0.4);\n                font-size: 0.7rem;\n                margin-top: 1em; // Reduced\n                margin-bottom: 0.25em; // Reduced\n                padding-left: 12px;\n            }\n\n            &-item {\n                .MuiButtonBase-root {\n                    border-radius: 12px !important;\n                    padding: 8px 12px !important; // Reduced padding\n                    transition: all 0.2s ease;\n                    color: rgba(255,255,255,0.6);\n                    font-weight: 500;\n                    \n                    &:hover {\n                        background: rgba(255, 255, 255, 0.05);\n                        color: rgba(255,255,255,0.9);\n                    }\n\n                    .material-symbols-rounded {\n                         margin-right: 12px;\n                         opacity: 0.7;\n                         font-size: 20px; // Slightly smaller icons\n                    }\n                }\n\n                &.active .MuiButtonBase-root {\n                    background: rgba($clr-accent-default, 0.1) !important;\n                    color: $clr-accent-default;\n                    font-weight: 600;\n                    \n                    .material-symbols-rounded {\n                        opacity: 1;\n                        font-variation-settings: 'FILL' 1;\n                    }\n\n                     &::after {\n                        content: '';\n                        position: absolute;\n                        right: 12px;\n                        width: 6px;\n                        height: 6px;\n                        border-radius: 50%;\n                        background: currentColor;\n                     }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/routes/_api/settings.tsx",
    "content": "import { createFileRoute, Outlet, useNavigate } from \"@tanstack/react-router\";\nimport React, { useCallback, useEffect } from \"react\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\n\nimport \"./settings.scss\";\nimport { Divider, IconButton, List, Typography } from \"@mui/material\";\nimport { useShallow } from \"zustand/shallow\";\nimport BackButton from \"@/components/buttons/backButton\";\nimport ListItemLink from \"@/components/listItemLink\";\nimport { useApiInContext } from \"@/utils/store/api\";\nimport { useCentralStore } from \"@/utils/store/central\";\n\nexport const Route = createFileRoute(\"/_api/settings\")({\n\tcomponent: SettingsRoute,\n});\n\nfunction SettingsRoute() {\n\tconst setBackdrop = useBackdropStore(useShallow((s) => s.setBackdrop));\n\tconst api = useApiInContext((s) => s.api);\n\tconst user = useCentralStore((s) => s.currentUser);\n\tuseEffect(() => {\n\t\tsetBackdrop(\"\");\n\t});\n\tconst navigate = useNavigate();\n\tconst handleNavigateHome = useCallback(() => {\n\t\tnavigate({ to: \"/home\" });\n\t}, []);\n\treturn (\n\t\t<div className=\"settings-page-container\">\n\t\t\t<div className=\"settings-sidebar\">\n\t\t\t\t<div className=\"settings-sidebar-controls\">\n\t\t\t\t\t<BackButton />\n\t\t\t\t\t<IconButton\n\t\t\t\t\t\tonClick={handleNavigateHome}\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tbackground: \"rgba(255, 255, 255, 0.05)\",\n\t\t\t\t\t\t\t\"&:hover\": { background: \"rgba(255, 255, 255, 0.1)\" },\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span className=\"material-symbols-rounded\">home</span>\n\t\t\t\t\t</IconButton>\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"settings-sidebar-header\">\n\t\t\t\t\t<img\n\t\t\t\t\t\tclassName=\"settings-sidebar-header-image\"\n\t\t\t\t\t\tsrc={`${api?.basePath}/Users/${user?.Id}/Images/Primary`}\n\t\t\t\t\t\talt=\"user\"\n\t\t\t\t\t/>\n\t\t\t\t\t<div className=\"flex flex-column\">\n\t\t\t\t\t\t<Typography variant=\"h6\" fontWeight=\"bold\">\n\t\t\t\t\t\t\t{user?.Name}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t<Typography variant=\"body2\" sx={{ opacity: 0.5 }}>\n\t\t\t\t\t\t\t{user?.Policy?.IsAdministrator ? \"Administrator\" : \"User\"}\n\t\t\t\t\t\t</Typography>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\n\t\t\t\t<List\n\t\t\t\t\tcomponent=\"nav\"\n\t\t\t\t\taria-label=\"settings navigation\"\n\t\t\t\t\tclassName=\"settings-sidebar-list\"\n\t\t\t\t\tsx={{\n\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\tpadding: 0,\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tvariant=\"overline\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tpx: 2,\n\t\t\t\t\t\t\tpy: 1,\n\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\tfontWeight: \"bold\",\n\t\t\t\t\t\t\tletterSpacing: \"0.1em\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\tGeneral\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<ListItemLink\n\t\t\t\t\t\tto=\"/settings/preferences\"\n\t\t\t\t\t\ticon=\"tune\"\n\t\t\t\t\t\tprimary=\"Preferences\"\n\t\t\t\t\t\tclassName=\"settings-sidebar-list-item\"\n\t\t\t\t\t/>\n\t\t\t\t\t<ListItemLink\n\t\t\t\t\t\tto=\"/settings/changeServer\"\n\t\t\t\t\t\ticon=\"dns\"\n\t\t\t\t\t\tprimary=\"Servers\"\n\t\t\t\t\t\tclassName=\"settings-sidebar-list-item\"\n\t\t\t\t\t/>\n\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tvariant=\"overline\"\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tpx: 2,\n\t\t\t\t\t\t\tpy: 1,\n\t\t\t\t\t\t\tmt: 2,\n\t\t\t\t\t\t\topacity: 0.5,\n\t\t\t\t\t\t\tfontWeight: \"bold\",\n\t\t\t\t\t\t\tletterSpacing: \"0.1em\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\tSystem\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<ListItemLink\n\t\t\t\t\t\tto=\"/settings/about\"\n\t\t\t\t\t\ticon=\"info\"\n\t\t\t\t\t\tprimary=\"About\"\n\t\t\t\t\t\tclassName=\"settings-sidebar-list-item\"\n\t\t\t\t\t/>\n\t\t\t\t</List>\n\t\t\t</div>\n\t\t\t<Outlet />\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/routes/_api.tsx",
    "content": "import { getDefaultServer, getServer } from \"@/utils/storage/servers\";\nimport { getUser } from \"@/utils/storage/user\";\nimport { getSystemApi } from \"@jellyfin/sdk/lib/utils/api/system-api\";\nimport { getUserApi } from \"@jellyfin/sdk/lib/utils/api/user-api\";\nimport { createFileRoute, redirect } from \"@tanstack/react-router\";\n\nexport const Route = createFileRoute(\"/_api\")({\n\tbeforeLoad: async ({ context, location }) => {\n\t\tconsole.log(\"RUNNING API CHECK\");\n\t\tif (!context.api) {\n\t\t\tconst currentServerId = await getDefaultServer();\n\t\t\tif (currentServerId) {\n\t\t\t\tconst currentServer = await getServer(currentServerId);\n\t\t\t\tif (currentServer?.address) {\n\t\t\t\t\tconst apiTemp = context.jellyfinSDK.createApi(\n\t\t\t\t\t\tcurrentServer?.address,\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait getSystemApi(apiTemp).getPingSystem();\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tconsole.error(error);\n\t\t\t\t\t\tthrow redirect({\n\t\t\t\t\t\t\tto: \"/error/$code\",\n\t\t\t\t\t\t\tparams: {\n\t\t\t\t\t\t\t\tcode: \"101\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tconst userOnDisk = await getUser();\n\t\t\t\t\tif (userOnDisk) {\n\t\t\t\t\t\tif (location.pathname !== \"/login/manual\") {\n\t\t\t\t\t\t\tconst apiTemp = context.jellyfinSDK.createApi(\n\t\t\t\t\t\t\t\tcurrentServer?.address,\n\t\t\t\t\t\t\t\tuserOnDisk.AccessToken,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tawait getUserApi(apiTemp).getCurrentUser();\n\t\t\t\t\t\t\t\t// return { user: user.data };\n\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\tconsole.error(error);\n\t\t\t\t\t\t\t\tthrow redirect({\n\t\t\t\t\t\t\t\t\tto: \"/login/manual\",\n\t\t\t\t\t\t\t\t\tsearch: {\n\t\t\t\t\t\t\t\t\t\tredirect: location.href,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontext.createApi(currentServer?.address, userOnDisk.AccessToken);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tcontext.createApi(currentServer?.address, undefined); // Creates Api\n\n\t\t\t\t\t\t// return { api: apiTemp };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (context.api) {\n\t\t\t// if (context.api.accessToken && location.pathname !== \"/login/manual\") {\n\t\t\t// \ttry {\n\t\t\t// \t\tconsole.info(\"AccessToken\", context.api.accessToken);\n\t\t\t// \t\tawait getUserApi(context.api).getCurrentUser(); // Verify user is able to authenticate\n\t\t\t// \t} catch (error) {\n\t\t\t// \t\tconsole.error(error);\n\t\t\t// \t\tthrow redirect({\n\t\t\t// \t\t\tto: \"/login/manual\",\n\t\t\t// \t\t\tsearch: {\n\t\t\t// \t\t\t\tredirect: location.href,\n\t\t\t// \t\t\t},\n\t\t\t// \t\t});\n\t\t\t// \t}\n\t\t\t// }\n\t\t}\n\t},\n});"
  },
  {
    "path": "src/routes/error/$code.tsx",
    "content": "import { Button, Typography } from \"@mui/material\";\nimport { Link, createFileRoute } from \"@tanstack/react-router\";\nimport { relaunch } from \"@tauri-apps/plugin-process\";\nimport React from \"react\";\n\nexport const Route = createFileRoute(\"/error/$code\")({\n\tcomponent: ErrorRoute,\n});\n\nfunction ErrorRoute() {\n\tconst errorCode = Route.useParams({ select: (s) => s.code });\n\tswitch (errorCode) {\n\t\tcase \"101\":\n\t\t\treturn (\n\t\t\t\t<div\n\t\t\t\t\tclassName=\"flex flex-column flex-center\"\n\t\t\t\t\tstyle={{ height: \"100vh\", gap: \"0.25em\" }}\n\t\t\t\t>\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName=\"material-symbols-rounded fill gradient-text\"\n\t\t\t\t\t\tstyle={{ fontSize: \"8em\", marginBottom: \"0.0em\" }}\n\t\t\t\t\t>\n\t\t\t\t\t\treport\n\t\t\t\t\t</span>\n\t\t\t\t\t<Typography variant=\"h5\" fontWeight={500}>\n\t\t\t\t\t\tError connecting server.\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<Typography variant=\"subtitle2\" style={{ opacity: 0.6 }}>\n\t\t\t\t\t\tCheck your internet connection or contact your server administrator\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclassName=\"flex flex-center\"\n\t\t\t\t\t\tstyle={{ gap: \"1em\", marginTop: \"0.5em\" }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<Button\n\t\t\t\t\t\t\tcomponent={Link}\n\t\t\t\t\t\t\tto=\"/setup/server/list\"\n\t\t\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tChange Server\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t\t<Button variant=\"contained\" onClick={async () => await relaunch()}>\n\t\t\t\t\t\t\tRestart Blink\n\t\t\t\t\t\t</Button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t);\n\n\t\tdefault:\n\t\t\treturn <div>Error</div>;\n\t}\n} "
  },
  {
    "path": "src/routes/index.tsx",
    "content": "import { getDefaultServer } from \"@/utils/storage/servers\";\nimport { getUser } from \"@/utils/storage/user\";\nimport { createFileRoute, redirect } from \"@tanstack/react-router\";\n\nexport const Route = createFileRoute(\"/\")({\n\tbeforeLoad: async () => {\n\t\tconst currentServerId = await getDefaultServer();\n\t\tconst userOnDisk = await getUser();\n\t\tif (currentServerId) {\n\t\t\tif (userOnDisk) {\n\t\t\t\tthrow redirect({ to: \"/home\" });\n\t\t\t}\n\t\t\tthrow redirect({ to: \"/login\" });\n\t\t}\n\n\t\tthrow redirect({ to: \"/setup/server/add\" });\n\t},\n});"
  },
  {
    "path": "src/routes/setup/server.add.tsx",
    "content": "import LoadingButton from \"@mui/lab/LoadingButton\";\nimport { yellow } from \"@mui/material/colors\";\nimport Grid from \"@mui/material/Grid\";\nimport TextField from \"@mui/material/TextField\";\nimport Typography from \"@mui/material/Typography\";\nimport { useMutation } from \"@tanstack/react-query\";\n\nimport { useSnackbar } from \"notistack\";\nimport React, { type FormEvent, useState } from \"react\";\nimport { setDefaultServer, setServer } from \"@/utils/storage/servers\";\nimport { jellyfin, useApiInContext } from \"@/utils/store/api\";\n// SCSS\nimport \"./server.scss\";\nimport { Paper, Stack } from \"@mui/material\";\nimport { createFileRoute, useNavigate } from \"@tanstack/react-router\";\nimport logo from \"../../assets/logoBlackTransparent.png\";\n\nexport const Route = createFileRoute(\"/setup/server/add\")({\n\tcomponent: ServerSetup,\n});\n\nfunction ServerSetup() {\n\tconst [serverIp, setServerIp] = useState(\"\");\n\tconst createApi = useApiInContext((s) => s.createApi);\n\n\tconst { enqueueSnackbar } = useSnackbar();\n\n\tconst navigate = useNavigate();\n\n\tconst checkServer = useMutation({\n\t\tmutationFn: async (e: FormEvent<HTMLFormElement>) => {\n\t\t\te.preventDefault();\n\t\t\tconst servers =\n\t\t\t\tawait jellyfin.discovery.getRecommendedServerCandidates(serverIp);\n\t\t\tconst bestServer = jellyfin.discovery.findBestServer(servers);\n\t\t\treturn bestServer;\n\t\t},\n\t\tonSuccess: (bestServer) => {\n\t\t\tif (bestServer?.systemInfo?.Id) {\n\t\t\t\tcreateApi(bestServer.address, undefined);\n\n\t\t\t\tsetDefaultServer(bestServer.systemInfo?.Id);\n\t\t\t\tsetServer(bestServer.systemInfo?.Id, bestServer);\n\n\t\t\t\tenqueueSnackbar(\"Client added successfully\", {\n\t\t\t\t\tvariant: \"success\",\n\t\t\t\t});\n\n\t\t\t\tnavigate({ to: \"/login\", replace: true });\n\t\t\t}\n\t\t},\n\t\tonError: (err) => {\n\t\t\tconsole.error(err);\n\t\t\tenqueueSnackbar(`${JSON.stringify(err)}`, { variant: \"error\" });\n\t\t\tenqueueSnackbar(\"Something went wrong\", { variant: \"error\" });\n\t\t},\n\t\tonSettled: (bestServer) => {\n\t\t\tconsole.log(bestServer);\n\t\t\tif (!bestServer) {\n\t\t\t\tenqueueSnackbar(\"Provided server address is not a Jellyfin server.\", {\n\t\t\t\t\tvariant: \"error\",\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t});\n\n\treturn (\n\t\t<div className=\"centered serverContainer flex flex-column flex-center\">\n\t\t\t<Paper\n\t\t\t\tsx={{\n\t\t\t\t\tp: 4,\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\tmaxWidth: \"40em\",\n\t\t\t\t\tbackgroundColor: \"rgba(20, 20, 30, 0.7)\",\n\t\t\t\t\tbackdropFilter: \"blur(24px) saturate(180%)\",\n\t\t\t\t\tbackgroundImage:\n\t\t\t\t\t\t\"linear-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0))\",\n\t\t\t\t\tborder: \"1px solid rgba(255, 255, 255, 0.08)\",\n\t\t\t\t\tboxShadow: \"0 20px 50px rgba(0,0,0,0.5)\",\n\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<Stack spacing={2} width=\"30em\" alignItems=\"center\">\n\t\t\t\t\t<img\n\t\t\t\t\t\tsrc={logo}\n\t\t\t\t\t\talt=\"Blink\"\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\twidth: \"200px\",\n\t\t\t\t\t\t\theight: \"auto\",\n\t\t\t\t\t\t\tobjectFit: \"contain\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t/>\n\n\t\t\t\t\t<Typography\n\t\t\t\t\t\tvariant=\"body1\"\n\t\t\t\t\t\tcolor=\"text.secondary\"\n\t\t\t\t\t\ttextAlign=\"center\"\n\t\t\t\t\t\tmaxWidth=\"80%\"\n\t\t\t\t\t\tsx={{ lineHeight: 1.5 }}\n\t\t\t\t\t>\n\t\t\t\t\t\tEnter your Jellyfin server address to get started\n\t\t\t\t\t</Typography>\n\n\t\t\t\t\t<form\n\t\t\t\t\t\tonSubmit={(e) => checkServer.mutate(e)}\n\t\t\t\t\t\tclassName=\"flex flex-column\"\n\t\t\t\t\t\tstyle={{ gap: \"1em\", width: \"100%\" }}\n\t\t\t\t\t>\n\t\t\t\t\t\t<TextField\n\t\t\t\t\t\t\tclassName=\"textbox\"\n\t\t\t\t\t\t\tplaceholder=\"https://jellyfin.example.com\"\n\t\t\t\t\t\t\tfullWidth\n\t\t\t\t\t\t\tvariant=\"outlined\"\n\t\t\t\t\t\t\t// inputRef={serverIp}\n\t\t\t\t\t\t\tonChange={(event) => {\n\t\t\t\t\t\t\t\tsetServerIp(event.target.value);\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\tInputProps={{\n\t\t\t\t\t\t\t\tstartAdornment: (\n\t\t\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\t\t\tstyle={{ opacity: 0.5, marginRight: \"12px\" }}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\tdns\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<LoadingButton\n\t\t\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\t\t\tsx={{ width: \"100%\", borderRadius: 2, height: 48 }}\n\t\t\t\t\t\t\tsize=\"large\"\n\t\t\t\t\t\t\tloading={checkServer.isPending}\n\t\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tConnect\n\t\t\t\t\t\t</LoadingButton>\n\t\t\t\t\t</form>\n\t\t\t\t</Stack>\n\t\t\t</Paper>\n\t\t\t<Stack\n\t\t\t\tdirection=\"row\"\n\t\t\t\tspacing={1}\n\t\t\t\talignItems=\"center\"\n\t\t\t\tsx={{ mt: 3, opacity: 0.6 }}\n\t\t\t>\n\t\t\t\t<span\n\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tcolor: yellow[700],\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\tinfo\n\t\t\t\t</span>\n\t\t\t\t<Typography variant=\"subtitle1\">\n\t\t\t\t\tExample: https://demo.jellyfin.org/stable\n\t\t\t\t</Typography>\n\t\t\t</Stack>\n\t\t</div>\n\t);\n}\n"
  },
  {
    "path": "src/routes/setup/server.error.scss",
    "content": ".server-error {\n    &-container {\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        flex-direction: column;\n\n        gap: 1em;\n\n        .material-symbols-rounded {\n            font-size: 10em;\n            // --wght: 500;\n            // --opsz: 40\n        }\n    }\n}"
  },
  {
    "path": "src/routes/setup/server.error.tsx",
    "content": "import { createFileRoute, useNavigate } from \"@tanstack/react-router\";\nimport React from \"react\";\n\nimport \"./server.error.scss\";\nimport { Button, Typography } from \"@mui/material\";\nimport { relaunch } from \"@tauri-apps/plugin-process\";\n\nexport const Route = createFileRoute(\"/setup/server/error\")({\n\tcomponent: () => {\n\t\tconst navigate = useNavigate();\n\t\treturn (\n\t\t\t<div className=\"scrollY server-error-container\">\n\t\t\t\t<span className=\"material-symbols-rounded fill gradient-text\">\n\t\t\t\t\treport\n\t\t\t\t</span>\n\t\t\t\t<Typography variant=\"h4\">Unable to connect to server!</Typography>\n\t\t\t\t<div className=\"flex flex-align-center\" style={{ gap: \"1em\" }}>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\t\tonClick={() => navigate({ to: \"/setup/server/list\" })}\n\t\t\t\t\t>\n\t\t\t\t\t\tChange Server\n\t\t\t\t\t</Button>\n\t\t\t\t\t<Button\n\t\t\t\t\t\tvariant=\"contained\"\n\t\t\t\t\t\tcolor=\"secondary\"\n\t\t\t\t\t\tonClick={async () => await relaunch()}\n\t\t\t\t\t>\n\t\t\t\t\t\tRestart Blink\n\t\t\t\t\t</Button>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t);\n\t},\n});"
  },
  {
    "path": "src/routes/setup/server.list.tsx",
    "content": "import Chip from \"@mui/material/Chip\";\nimport IconButton from \"@mui/material/IconButton\";\nimport Paper from \"@mui/material/Paper\";\nimport Typography from \"@mui/material/Typography\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport {\n\tcreateFileRoute,\n\tuseNavigate,\n\tuseRouter,\n} from \"@tanstack/react-router\";\nimport { useSnackbar } from \"notistack\";\nimport React, { useLayoutEffect, useState } from \"react\";\nimport AppBarBackOnly from \"@/components/appBar/backOnly\";\nimport {\n\tdelServer,\n\tgetAllServers,\n\tgetDefaultServer,\n\ttype ServerInfo,\n\tsetDefaultServer,\n} from \"@/utils/storage/servers\";\nimport { delUser } from \"@/utils/storage/user\";\nimport { useBackdropStore } from \"@/utils/store/backdrop\";\nimport \"./serverList.scss\";\nimport { useShallow } from \"zustand/shallow\";\nimport AddServerDialog from \"@/components/addServerDialog\";\nimport { useApiInContext } from \"@/utils/store/api\";\n\nexport const Route = createFileRoute(\"/setup/server/list\")({\n\tcomponent: ServerList,\n});\n\nfunction ServerList() {\n\tconst navigate = useNavigate();\n\tconst { enqueueSnackbar } = useSnackbar();\n\tconst router = useRouter();\n\n\tconst createApi = useApiInContext((s) => s.createApi);\n\n\tconst queryClient = useQueryClient();\n\n\tconst servers = useQuery({\n\t\tqueryKey: [\"servers-list\"],\n\t\tqueryFn: async () => await getAllServers(),\n\t});\n\n\tconst defaultServer = useQuery({\n\t\tqueryKey: [\"default-server\"],\n\t\tqueryFn: async () => await getDefaultServer(),\n\t});\n\n\tconst handleServerChange = useMutation({\n\t\tmutationFn: async (server: ServerInfo) => {\n\t\t\tawait delUser();\n\t\t\tawait setDefaultServer(server.id);\n\t\t\tqueryClient.clear();\n\t\t\tawait defaultServer.refetch();\n\t\t\tcreateApi(server.address, undefined);\n\t\t\tqueryClient.removeQueries();\n\t\t},\n\t\tonSuccess: async () => {\n\t\t\tnavigate({ to: \"/login\" });\n\t\t},\n\t\tonError: (error) => {\n\t\t\tconsole.error(error);\n\t\t\tenqueueSnackbar(\"Error changing the server\", {\n\t\t\t\tvariant: \"error\",\n\t\t\t});\n\t\t},\n\t});\n\n\tconst handleDelete = async (server: ServerInfo) => {\n\t\tawait delServer(server.id);\n\n\t\tif (server.id === defaultServer.data) {\n\t\t\tawait delUser();\n\t\t\tawait servers.refetch();\n\n\t\t\tif ((servers.data?.length ?? 0) > 1) {\n\t\t\t\tif (servers.data?.[0].id) {\n\t\t\t\t\tsetDefaultServer(servers.data?.[0].id);\n\t\t\t\t\tcreateApi(servers.data?.[0].address, undefined);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tawait setDefaultServer(null);\n\t\t\t\tawait router.invalidate();\n\t\t\t}\n\t\t\tqueryClient.removeQueries();\n\t\t\tnavigate({ to: \"/\" });\n\t\t}\n\t\tenqueueSnackbar(\"Server deleted successfully!\", { variant: \"success\" });\n\n\t\tawait servers.refetch();\n\t\tawait defaultServer.refetch();\n\t};\n\n\tconst setBackdrop = useBackdropStore(\n\t\tuseShallow((state) => state.setBackdrop),\n\t);\n\n\tuseLayoutEffect(() => {\n\t\tsetBackdrop(\"\");\n\t}, []);\n\n\tconst [addServerDialog, setAddServerDialog] = useState(false);\n\n\treturn (\n\t\t<div className=\"server-list flex flex-column flex-center centered\">\n\t\t\t<AppBarBackOnly />\n\t\t\t<Paper\n\t\t\t\tsx={{\n\t\t\t\t\tp: 4,\n\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\tmaxWidth: \"600px\",\n\t\t\t\t\tbackgroundColor: \"rgba(20, 20, 30, 0.7)\",\n\t\t\t\t\tbackdropFilter: \"blur(24px) saturate(180%)\",\n\t\t\t\t\tbackgroundImage:\n\t\t\t\t\t\t\"linear-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0))\",\n\t\t\t\t\tborder: \"1px solid rgba(255, 255, 255, 0.08)\",\n\t\t\t\t\tboxShadow: \"0 20px 50px rgba(0,0,0,0.5)\",\n\t\t\t\t\tborderRadius: 4,\n\t\t\t\t\toverflow: \"hidden\",\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\tgap: 3,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\tjustifyContent: \"space-between\",\n\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\t<Typography variant=\"h4\" fontWeight=\"bold\">\n\t\t\t\t\t\tServers\n\t\t\t\t\t</Typography>\n\t\t\t\t\t<IconButton\n\t\t\t\t\t\tonClick={() => setAddServerDialog(true)}\n\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\t\t\t\"&:hover\": { bgcolor: \"rgba(255,255,255,0.1)\" },\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t<span className=\"material-symbols-rounded\">add</span>\n\t\t\t\t\t</IconButton>\n\t\t\t\t</div>\n\n\t\t\t\t<div className=\"flex flex-column\" style={{ gap: \"1em\" }}>\n\t\t\t\t\t{servers.isSuccess &&\n\t\t\t\t\t\tservers.data.map((server) => (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={server.id}\n\t\t\t\t\t\t\t\tclassName=\"server-list-item\"\n\t\t\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\t\tpadding: \"16px\",\n\t\t\t\t\t\t\t\t\tborderRadius: \"16px\",\n\t\t\t\t\t\t\t\t\tbackgroundColor: \"rgba(0,0,0,0.2)\",\n\t\t\t\t\t\t\t\t\tborder: \"1px solid rgba(255,255,255,0.05)\",\n\t\t\t\t\t\t\t\t\tgap: \"16px\",\n\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tclassName=\"material-symbols-rounded\"\n\t\t\t\t\t\t\t\t\tstyle={{ fontSize: \"24px\", opacity: 0.7 }}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\tdns\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div style={{ flex: 1 }}>\n\t\t\t\t\t\t\t\t\t<Typography\n\t\t\t\t\t\t\t\t\t\tvariant=\"h6\"\n\t\t\t\t\t\t\t\t\t\tfontWeight={600}\n\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\t\t\t\t\t\tgap: 1,\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t{server.systemInfo?.ServerName}\n\t\t\t\t\t\t\t\t\t\t{server.id === defaultServer.data && (\n\t\t\t\t\t\t\t\t\t\t\t<Chip\n\t\t\t\t\t\t\t\t\t\t\t\tlabel=\"Current\"\n\t\t\t\t\t\t\t\t\t\t\t\tcolor=\"primary\"\n\t\t\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\t\t\tsx={{ height: 20, fontSize: \"0.7rem\", fontWeight: 700 }}\n\t\t\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t\t<Typography variant=\"body2\" style={{ opacity: 0.7 }} noWrap>\n\t\t\t\t\t\t\t\t\t\t{server.address}\n\t\t\t\t\t\t\t\t\t</Typography>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t<div style={{ display: \"flex\", gap: \"8px\" }}>\n\t\t\t\t\t\t\t\t\t<IconButton\n\t\t\t\t\t\t\t\t\t\tonClick={() => {\n\t\t\t\t\t\t\t\t\t\t\thandleServerChange.mutate(server);\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t\tdisabled={handleServerChange.isPending}\n\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\t\t\t\t\t\t\t\"&:hover\": { bgcolor: \"var(--mui-palette-primary-main)\" },\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">login</span>\n\t\t\t\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t\t\t\t<IconButton\n\t\t\t\t\t\t\t\t\t\tonClick={() => handleDelete(server)}\n\t\t\t\t\t\t\t\t\t\tsize=\"small\"\n\t\t\t\t\t\t\t\t\t\tsx={{\n\t\t\t\t\t\t\t\t\t\t\tbgcolor: \"rgba(255,255,255,0.05)\",\n\t\t\t\t\t\t\t\t\t\t\t\"&:hover\": { bgcolor: \"var(--mui-palette-error-main)\" },\n\t\t\t\t\t\t\t\t\t\t}}\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<span className=\"material-symbols-rounded\">delete</span>\n\t\t\t\t\t\t\t\t\t</IconButton>\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t</Paper>\n\t\t\t<AddServerDialog\n\t\t\t\topen={addServerDialog}\n\t\t\t\tsideEffect={() => servers.refetch()}\n\t\t\t\tsetAddServerDialog={setAddServerDialog}\n\t\t\t/>\n\t\t</div>\n\t);\n}\nexport default ServerList;\n"
  },
  {
    "path": "src/routes/setup/server.scss",
    "content": ".logo {\n\twidth: min(60vw, 45em);\n\theight: auto;\n\tmargin-bottom: 2em;\n}\n\n.serverContainer {\n\tdisplay: flex;\n\tflex-direction: column;\n\tjustify-content: center;\n\talign-items: center;\n\tgap: 1.5em;\n\t.textbox,\n\t.button {\n\t\twidth: 100% !important;\n\t}\n\t.button {\n\t\tpadding: 1em;\n\t\tborder-radius: 100px;\n\t}\n}\n"
  },
  {
    "path": "src/routes/setup/serverList.scss",
    "content": "\n.server-list {\n\tposition: absolute;\n\twidth: 100vw;\n\theight: 100vh;\n\toverflow: hidden;\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n\tflex-direction: column;\n\t&-container {\n\t\tdisplay: flex;\n\t\tflex-direction: column;\n\t\twidth: 40vw;\n\t\tpadding: 1.2em;\n\t\tborder-radius: 20px !important;\n\t\tmax-height: 46vh;\n\t\toverflow-y: auto;\n\t\toverflow-x: hidden;\n\t}\n\t&-item {\n\t\tdisplay: grid;\n\t\tgrid-template-columns: 3.4em 1fr auto;\n\t\tjustify-items: start;\n\t\talign-items: center;\n\t\tgap: 0.8em;\n\t\t&-icon {\n\t\t\tfont-size: 3.4em !important;\n\t\t}\n\t\t&-info {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\t// width: 100%;\n\t\t}\n\t\t&-buttons {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: row;\n\t\t\tgap: 0.5em;\n\t\t\twidth: 100%;\n\t\t\toverflow: hidden;\n\t\t}\n\t\tborder-bottom: 1.2px solid rgb(255 255 255 / 0.1);\n\n\t\t$spacing: 0.75em;\n\n\t\tpadding-bottom: $spacing;\n\t\tmargin-bottom: $spacing;\n\n\t\t&:last-child {\n\t\t\tborder: none;\n\t\t\tmargin: 0;\n\t\t\tpadding: 0;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/styles/global.scss",
    "content": "::-webkit-scrollbar {\n\twidth: 14px;\n\tbackground: $clr-background-dark;\n}\n\ndiv::-webkit-scrollbar {\n\twidth: 14px;\n\tbackground: transparent;\n}\n\n.roundedScrollbar::-webkit-scrollbar-track {\n\tborder-radius: 1000px;\n}\n\n::-webkit-scrollbar-track {\n\tbackground-color: transparent;\n\t&:hover {\n\t\tbackground-color: rgb(255 255 255 / 0.1);\n\t}\n}\n::-webkit-scrollbar-thumb {\n\tborder: 4px solid transparent;\n\tbackground-clip: padding-box;\n\tborder-radius: 9999px;\n\tbackground-color: rgb(255 255 255 / 0.25);\n\t&:hover {\n\t\tbackground-color: rgb(255 255 255 / 0.45);\n\t}\n}\n\n::selection {\n\tbackground: $clr-secondary-default;\n}\n\nhtml {\n\toverscroll-behavior: none;\n}\n\nbody {\n\tfont-family: \"Plus Jakarta Sans Variable\",system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n\tbackground: $clr-background-default;\n\tmargin: 0;\n\tpadding: 0;\n\tposition: relative;\n\twidth: 100vw;\n\theight: 100vh;\n\toverflow-x: hidden;\n\toverflow-y: auto;\n\toverscroll-behavior: none;\n}\n\n.app-backdrop {\n\ttransition: 1.2s;\n\tobject-fit: cover;\n\twidth: calc(100% + 160px);\n\theight: calc(100% + 160px);\n\tposition: absolute;\n\ttop: -80px;\n\tleft: -80px;\n\tz-index: -2;\n\tfilter: blur(160px) saturate(140%) contrast(200%);\n\t&-container {\n\t\tposition: fixed;\n\t\twidth: 100vw;\n\t\theight: 100vh;\n\t\tleft: 0;\n\t\ttop: 0;\n\t\tz-index: -1;\n\t\tfilter: brightness(0.5);\n\t}\n}\n\n#root,\n.root-page {\n\theight: fit-content;\n\twidth: 100%;\n\tposition: relative;\n\toverflow: hidden;\n\tmin-height: 100vh;\n}\n\n.centered {\n\tposition: absolute;\n\ttop: 50%;\n\tleft: 50%;\n\ttransform: translate(-50%, -50%);\n}\n\n.loading {\n\topacity: 0.35;\n}\n\n.fadeIn {\n\tanimation: 0.5s fadeInPage forwards;\n}\n\n.fadeOut {\n\tanimation: 0.5s fadeOutPage forwards;\n}\n\n.glass-panel {\n\t@include glass-effect;\n}\n\n.glass-menu-paper, .glass-dialog-paper {\n    @include glass-effect;\n    border-radius: 16px !important;\n}\n\n.glass-menu-paper {\n    .MuiMenuItem-root {\n        font-family: inherit;\n        transition: background-color 0.2s;\n        border-radius: 8px;\n        margin: 4px;\n\n        &:hover {\n           background-color: rgba(255, 255, 255, 0.1); \n        }\n        &.Mui-selected {\n            background-color: rgba(255, 255, 255, 0.2) !important;\n            color: white;\n            &:hover {\n                background-color: rgba(255, 255, 255, 0.25) !important; \n            }\n        }\n    }\n}\n\n\n\n@keyframes fadeInPage {\n\tfrom {\n\t\topacity: 0;\n\t\ttransform: translate(-20px, 0);\n\t}\n\tto {\n\t\topacity: 1;\n\t\ttransform: translate(0px, 0px);\n\t}\n}\n\n@keyframes fadeOutPage {\n\tfrom {\n\t\topacity: 1;\n\t\ttransform: translate(0px, 0px);\n\t}\n\tto {\n\t\ttransform: translate(20px, 0);\n\t\topacity: 0;\n\t}\n}\n\nh1 {\n\tcolor: white !important;\n}\n\n.scrollY {\n\toverflow-x: hidden;\n\toverflow-y: visible;\n\theight: fit-content;\n\twidth: 100%;\n\tbox-sizing: border-box;\n\tmin-height: 100vh;\n\tpadding: ($page-margin - 1.2em) $page-margin;\n\tscrollbar-gutter: stable;\n\t&.padded-top {\n\t\tpadding-top: 4.4em;\n\t}\n}\n\n.empty {\n\tdisplay: none;\n}\n\n.MuiChip-label {\n\tdisplay: flex;\n\talign-items: center;\n\tjustify-content: center;\n}\n\n.material-symbols-rounded {\n\t--fill: 0;\n\t--wght: 300;\n\t--grad: 25;\n\t--opsz: 40;\n\t\n\tfont-variation-settings: \"FILL\" var(--fill, 0), \"wght\" var(--wght, 300), \"GRAD\" var(--grad, 25), \"opsz\" var(--opsz, 40);\n\tfont-size: 1.1em;\n\ttransition: all $transition-time-default;\n\t&.fill{\n\t\t--fill: 1;\n\t}\n}\n\n.audio-playing .scrollY {\n\tpadding-bottom: 9em !important;\n}\n\n.audio-playing .scrollY.library {\n\tpadding-bottom: 0 !important;\n}\n\n.audio-playing .library-items {\n\tpadding-bottom: 9em !important;\n}\n\n.library-items,\n.scrollY {\n\ttransition: all $transition-time-default !important;\n}\n\n// Normalize listItemIcons for material symbols\n.MuiListItemIcon-root {\n\tfont-size: 24px;\n}\n\n.animate-icon {\n\tposition: relative;\n\t&::after {\n\t\tcontent: \"\";\n\t\tposition: absolute;\n\t\ttop: 50%;\n\t\tleft: 50%;\n\t\ttransform: translate(-50%, -50%) scale(1);\n\t\twidth: 100%;\n\t\theight: 100%;\n\t\tborder-radius: 100%;\n\t\topacity: 0;\n\t\t// filter: blur(10px);\n\t\tanimation: iconAnimation ease-out 1s infinite;\n\t\tbackground: var(--clr);\n\t\tz-index: -1;\n\t\t// background: radial-gradient(transparent, var(--clr));\n\t}\n}\n\n.animate-rotate{\n\tanimation: rotation linear 1s infinite;\n}\n\n@keyframes rotation {\n\t0%{\n\t\ttransform: rotate(0deg);\n\t}\n\t100%{\n\t\ttransform: rotate(360deg);\n\t}\n}\n\n@keyframes iconAnimation {\n\t0% {\n\t\topacity: 1;\n\t\ttransform: translate(-50%, -50%) scale(0);\n\t}\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translate(-50%, -50%) scale(4);\n\t}\n}\n\n.flex {\n\tdisplay: flex;\n\t&-row {\n\t\tflex-direction: row !important;\n\t}\n\t&-column {\n\t\tflex-direction: column;\n\t}\n\t&-center {\n\t\talign-items: center;\n\t\tjustify-content: center;\n\t}\n\t&-align-center {\n\t\talign-items: center;\n\t}\n\t&-justify-spaced-between {\n\t\tjustify-content: space-between;\n\t}\n}\n\n@keyframes pulse {\n\t0% {\n\t\topacity: 0.1;\n\t}\n\t50% {\n\t\topacity: 1;\n\t}\n\t100% {\n\t\topacity: 0.1;\n\t}\n}\n\n.gradient-text{\n\tbackground: $clr-gradient-default;\n\tcolor: transparent;\n\t-webkit-background-clip: text;\n\tbackground-clip: text;\n}\n\n.divider-circle{\n\twidth: 4px;\n\theight: 4px;\n\taspect-ratio: 1;\n\tborder-radius: 10px;\n\tbackground: white;\n}\n\n.react-multi-carousel-list{\n\toverflow: visible !important;\n}\n\n.react-multi-carousel-item{\n\tfilter: brightness(0.4);\n\ttransition: filter 500ms;\n\t&.react-multi-carousel-item--active{\n\t\tfilter: brightness(1);\t\n\t}\n}\n\n@function neg($var) {\n\t@return $var * -1;\n}\n\n.fullWidth {\n\twidth: 100%;\n}\n\n.glass {\n\tbackground-color: rgba(20, 20, 30, 0.7) !important;\n\tbackdrop-filter: blur(24px) saturate(180%) !important;\n\tborder: 1px solid rgba(255, 255, 255, 0.08);\n\tbox-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);\n}\n\n.opacity-07{\n\topacity: 0.7;\n}\n\na {\n\tcolor: $clr-accent-default;\n}"
  },
  {
    "path": "src/styles/variables.scss",
    "content": "\n$clr-accent-default: hsl(337, 96%, 56%);\n$clr-accent-light: hsl(337, 96%, 90%);\n$clr-accent-dark: hsl(337, 96%, 20%);\n$clr-secondary-default: hsl(273, 100%, 36%);\n$clr-secondary-dark: hsl(273, 100%, 12%);\n$clr-background-default: hsl(256, 100%, 6%);\n$clr-background-opacity-0_5: hsl(256, 100%, 6%, 50%);\n\n$clr-background-dark: hsl(256, 100%, 4%);\n$clr-background-dark-opacity-0_8: hsl(256, 100%, 4%, 94%);\n$clr-background-light: hsl(256, 100%, 16%);\n\n$clr-gradient-default: linear-gradient(\n\t135deg,\n\t$clr-accent-default,\n\t$clr-secondary-default\n);\n$clr-gradient-dark: linear-gradient(\n\t135deg,\n\t$clr-accent-dark,\n\t$clr-secondary-dark\n);\n\n$border-radius-default: 8px;\n$border-radius_04: 4px;\n\n$transition-time-default: 250ms;\n$transition-time-fast: 150ms;\n\n$shadow-card: 0 4px 12px rgb(0 0 0 / 0.4);\n$shadow-card-image: 0 2px 8px rgb(0 0 0 / 0.25);\n\n$clr-background-card-icon: linear-gradient(45deg, rgb(255 255 255 / 0.05), rgb(255 255 255 / 0.2));\n\n$page-margin: 4em;\n\n@mixin glass-effect($blur: 24px, $saturation: 180%, $opacity: 0.7, $bg-color: rgba(20, 20, 30, $opacity)) {\n    background-color: $bg-color;\n    backdrop-filter: blur($blur) saturate($saturation);\n    background-image: linear-gradient(rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0));\n    border: 1px solid rgba(255, 255, 255, 0.08);\n    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);\n}"
  },
  {
    "path": "src/theme.ts",
    "content": "import { createTheme } from \"@mui/material/styles\";\nimport {\n\tclrAccentDefault,\n\tclrBackgroundDark,\n\tclrBackgroundDefault,\n\tclrSecondaryDefault, //@ts-ignore\n} from \"./palette.module.scss\";\n\nconst { palette } = createTheme({});\nconst { augmentColor } = palette;\nexport const theme = createTheme({\n\tpalette: {\n\t\tprimary: augmentColor({\n\t\t\tcolor: {\n\t\t\t\tmain: clrAccentDefault,\n\t\t\t},\n\t\t}),\n\t\tsecondary: augmentColor({\n\t\t\tcolor: {\n\t\t\t\tmain: clrSecondaryDefault,\n\t\t\t},\n\t\t}),\n\t\tbackground: {\n\t\t\tdefault: clrBackgroundDefault,\n\t\t\tpaper: clrBackgroundDark,\n\t\t},\n\t\t//@ts-expect-error\n\t\twhite: augmentColor({ color: { main: \"#ffffff\" } }),\n\t\tblack: augmentColor({ color: { main: \"#000\" } }),\n\t\tmode: \"dark\",\n\t},\n\ttypography: {\n\t\tfontFamily: \"Plus Jakarta Sans Variable\",\n\t},\n\tcomponents: {\n\t\tMuiButton: {\n\t\t\tstyleOverrides: {\n\t\t\t\troot: {\n\t\t\t\t\tborderRadius: \"100px\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tMuiChip: {\n\t\t\tstyleOverrides: {\n\t\t\t\troot: {\n\t\t\t\t\tbackdropFilter: \"blur(5px)\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tMuiPaper: {\n\t\t\tstyleOverrides: {\n\t\t\t\troot: {},\n\t\t\t},\n\t\t},\n\t\tMuiMenu: {\n\t\t\tstyleOverrides: {\n\t\t\t\tpaper: {\n\t\t\t\t\tborderRadius: \"10px\",\n\t\t\t\t\tborder: \"1px solid rgb(255 255 255 / 0.1)\",\n\t\t\t\t},\n\t\t\t\tlist: {\n\t\t\t\t\tpadding: \"8px\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tdefaultProps: {\n\t\t\t\tdisableScrollLock: true,\n\t\t\t\tslotProps: {\n\t\t\t\t\tpaper: {\n\t\t\t\t\t\tclassName: \"glass\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tMuiMenuItem: {\n\t\t\tstyleOverrides: {\n\t\t\t\troot: {\n\t\t\t\t\tborderRadius: \"4px\",\n\t\t\t\t\ttransition: \"250ms\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tMuiDialog: {\n\t\t\tstyleOverrides: {\n\t\t\t\tpaper: {\n\t\t\t\t\tborderRadius: \"20px\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tdefaultProps: {\n\t\t\t\tPaperProps: {\n\t\t\t\t\tclassName: \"glass\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tMuiInputLabel: {\n\t\t\tstyleOverrides: {\n\t\t\t\troot: {\n\t\t\t\t\tmarginLeft: \"6px\",\n\t\t\t\t\tcolor: \"rgba(255, 255, 255, 0.5)\",\n\t\t\t\t\t\"&.Mui-focused\": {\n\t\t\t\t\t\tcolor: clrAccentDefault,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tMuiOutlinedInput: {\n\t\t\tstyleOverrides: {\n\t\t\t\tnotchedOutline: {\n\t\t\t\t\tpaddingLeft: \"15px\",\n\t\t\t\t},\n\t\t\t\troot: {\n\t\t\t\t\tbackgroundColor: \"rgba(0,0,0,0.2)\",\n\t\t\t\t\tborderRadius: \"16px\",\n\t\t\t\t\ttransition: \"border-color 0.2s, background-color 0.2s\",\n\t\t\t\t\t\"&.Mui-focused\": {\n\t\t\t\t\t\tbackgroundColor: \"rgba(0,0,0,0.3)\",\n\t\t\t\t\t},\n\t\t\t\t\t\"&:hover\": {\n\t\t\t\t\t\tbackgroundColor: \"rgba(0,0,0,0.25)\",\n\t\t\t\t\t},\n\t\t\t\t\t\"& .MuiOutlinedInput-notchedOutline\": {\n\t\t\t\t\t\tborderColor: \"rgba(255,255,255,0.1)\",\n\t\t\t\t\t},\n\t\t\t\t\t\"&:hover .MuiOutlinedInput-notchedOutline\": {\n\t\t\t\t\t\tborderColor: \"rgba(255,255,255,0.3)\",\n\t\t\t\t\t},\n\t\t\t\t\t\"&.Mui-focused .MuiOutlinedInput-notchedOutline\": {\n\t\t\t\t\t\tborderColor: clrAccentDefault,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t},\n});\n"
  },
  {
    "path": "src/utils/browser-detection.ts",
    "content": "/**\n * Utilities to detect the browser and get information on the current environment\n * Based on https://github.com/google/shaka-player/blob/master/lib/util/platform.js\n *\n * @deprecated - Parsing User Agent is a maintenance burden and\n * should rely on external libraries only. It's also going to be replaced with Client-Hints.\n * Migration paths:\n * * Check for platform-specific features *where needed*\n * directly (i.e Chromecast/AirPlay/MSE) instead of a per-browser basis.\n * This will always be 100% fault free.\n *\n * * Use something like https://www.npmjs.com/package/unique-names-generator to\n * distinguish between instances. Instance names could be shown and be modified by the user\n * at settings. This would make user instances distinguishable in a 100% fault-tolerant way\n * and solve incongruencies like how a device is named. For instance,\n * an instance running in an Android Auto headset will be recognised as Android only, which is less\n * than ideal.\n */\nexport function supportsMediaSource(): boolean {\n  /*\n   * Browsers that lack a media source implementation will have no reference\n   * to |window.MediaSource|.\n   */\n  return !!window.MediaSource;\n}\n\n/**\n * Check if the user agent of the navigator contains a key.\n *\n * @private\n * @static\n * @param key - Key for which to perform a check.\n * @returns Determines if user agent of navigator contains a key\n */\nfunction userAgentContains(key: string): boolean {\n  const userAgent = navigator.userAgent || '';\n\n  return userAgent.includes(key);\n}\n\n/* Desktop Browsers */\n\n/**\n * Check if the current platform is Mozilla Firefox.\n *\n * @returns Determines if browser is Mozilla Firefox\n */\nexport function isFirefox(): boolean {\n  return userAgentContains('Firefox/');\n}\n\n/**\n * Check if the current platform is Microsoft Edge.\n *\n * @static\n * @returns Determines if browser is Microsoft Edge\n */\nexport function isEdge(): boolean {\n  return userAgentContains('Edg/') || userAgentContains('Edge/');\n}\n\n/**\n * Check if the current platform is Chromium based.\n *\n * @returns Determines if browser is Chromium based\n */\nexport function isChromiumBased(): boolean {\n  return userAgentContains('Chrome');\n}\n\n/**\n * Check if the current platform is Google Chrome.\n *\n * @returns Determines if browser is Google Chrome\n */\nexport function isChrome(): boolean {\n  /*\n   * The Edge user agent will also contain the \"Chrome\" keyword, so we need\n   * to make sure this is not Edge.\n   */\n  return userAgentContains('Chrome') && !isEdge() && !isWebOS();\n}\n\n/**\n * Check if the current platform is from Apple.\n *\n * Returns true on all iOS browsers and on desktop Safari.\n *\n * Returns false for non-Safari browsers on macOS, which are independent of\n * Apple.\n *\n * @returns Determines if current platform is from Apple\n */\nexport function isApple(): boolean {\n  return navigator.vendor.includes('Apple') && !isTizen();\n}\n\n/**\n * Returns a major version number for Safari, or Safari-based iOS browsers.\n *\n * @returns The major version number for Safari\n */\nexport function safariVersion(): number | undefined {\n  // All iOS browsers and desktop Safari will return true for isApple().\n  if (!isApple()) {\n    return;\n  }\n\n  let userAgent = '';\n\n  if (navigator.userAgent) {\n    userAgent = navigator.userAgent;\n  }\n\n  /*\n   * This works for iOS Safari and desktop Safari, which contain something\n   * like \"Version/13.0\" indicating the major Safari or iOS version.\n   */\n  let match = /Version\\/(\\d+)/.exec(userAgent);\n\n  if (match) {\n    return Number.parseInt(match[1], /* Base= */ 10);\n  }\n\n  /*\n   * This works for all other browsers on iOS, which contain something like\n   * \"OS 13_3\" indicating the major & minor iOS version.\n   */\n  match = /OS (\\d+)(?:_\\d+)?/.exec(userAgent);\n\n  if (match) {\n    return Number.parseInt(match[1], /* Base= */ 10);\n  }\n}\n\n/* TV Platforms */\n\n/**\n * Check if the current platform is Tizen.\n *\n * @returns Determines if current platform is Tizen\n */\nexport function isTizen(): boolean {\n  return userAgentContains('Tizen');\n}\n\n/**\n * Check if the current platform is Tizen 2\n *\n * @returns Determines if current platform is Tizen 2\n */\nexport function isTizen2(): boolean {\n  return userAgentContains('Tizen 2');\n}\n\n/**\n * Check if the current platform is Tizen 3\n *\n * @returns Determines if current platform is Tizen 3\n * @memberof BrowserDetector\n */\nexport function isTizen3(): boolean {\n  return userAgentContains('Tizen 3');\n}\n\n/**\n * Check if the current platform is Tizen 4.\n *\n * @returns Determines if current platform is Tizen 4\n * @memberof BrowserDetector\n */\nexport function isTizen4(): boolean {\n  return userAgentContains('Tizen 4');\n}\n\n/**\n * Check if the current platform is Tizen 5.\n *\n * @returns Determines if current platform is Tizen 5\n * @memberof BrowserDetector\n */\nexport function isTizen5(): boolean {\n  return userAgentContains('Tizen 5');\n}\n\n/**\n * Check if the current platform is Tizen 5.5.\n *\n * @returns Determines if current platform is Tizen 5.5\n * @memberof BrowserDetector\n */\nexport function isTizen55(): boolean {\n  return userAgentContains('Tizen 5.5');\n}\n\n/**\n * Check if the current platform is WebOS.\n *\n * @returns Determines if current platform is WebOS\n * @memberof BrowserDetector\n */\nexport function isWebOS(): boolean {\n  return userAgentContains('Web0S');\n}\n\n/**\n * Determines if current platform is WebOS1\n */\nexport function isWebOS1(): boolean {\n  return (\n    isWebOS()\n    && userAgentContains('AppleWebKit/537')\n    && !userAgentContains('Chrome/')\n  );\n}\n\n/**\n * Determines if current platform is WebOS2\n */\nexport function isWebOS2(): boolean {\n  return (\n    isWebOS()\n    && userAgentContains('AppleWebKit/538')\n    && !userAgentContains('Chrome/')\n  );\n}\n\n/**\n * Determines if current platform is WebOS3\n */\nexport function isWebOS3(): boolean {\n  return isWebOS() && userAgentContains('Chrome/38');\n}\n\n/**\n * Determines if current platform is WebOS4\n */\nexport function isWebOS4(): boolean {\n  return isWebOS() && userAgentContains('Chrome/53');\n}\n\n/**\n * Determines if current platform is WebOS5\n */\nexport function isWebOS5(): boolean {\n  return isWebOS() && userAgentContains('Chrome/68');\n}\n\n/* Platform Utilities */\n\n/**\n * Determines if current platform is Android\n */\nexport function isAndroid(): boolean {\n  return userAgentContains('Android');\n}\n\n/**\n * Guesses if the platform is a mobile one (iOS or Android).\n *\n * @returns Determines if current platform is mobile (Guess)\n */\nexport function isMobile(): boolean {\n  let userAgent = '';\n\n  if (navigator.userAgent) {\n    userAgent = navigator.userAgent;\n  }\n\n  if (/iPhone|iPad|iPod|Android/.test(userAgent)) {\n    // This is Android, iOS, or iPad < 13.\n    return true;\n  }\n\n  /*\n   * Starting with iOS 13 on iPad, the user agent string no longer has the\n   * word \"iPad\" in it.  It looks very similar to desktop Safari.  This seems\n   * to be intentional on Apple's part.\n   * See: https://forums.developer.apple.com/thread/119186\n   *\n   * So if it's an Apple device with multi-touch support, assume it's a mobile\n   * device.  If some future iOS version starts masking their user agent on\n   * both iPhone & iPad, this clause should still work.  If a future\n   * multi-touch desktop Mac is released, this will need some adjustment.\n   */\n  return isApple() && navigator.maxTouchPoints > 1;\n}\n\n/**\n * Guesses if the platform is a Smart TV (Tizen or WebOS).\n *\n * @returns Determines if platform is a Smart TV\n */\nexport function isTv(): boolean {\n  return isTizen() || isWebOS();\n}\n\n/**\n * Guesses if the platform is a PS4\n *\n * @returns Determines if the device is a PS4\n */\nexport function isPs4(): boolean {\n  return userAgentContains('playstation 4');\n}\n\n/**\n * Guesses if the platform is a Xbox\n *\n * @returns Determines if the device is a Xbox\n */\nexport function isXbox(): boolean {\n  return userAgentContains('xbox');\n}\n"
  },
  {
    "path": "src/utils/constants/library.ts",
    "content": "import {\n\tBaseItemKind,\n\tCollectionType,\n\tItemSortBy,\n} from \"@jellyfin/sdk/lib/generated-client\";\n\nexport const AVAILABLE_VIEWS: Array<{\n\ttitle: string;\n\tvalue: BaseItemKind | \"Artist\";\n\tcompatibleCollectionTypes: CollectionType[];\n}> = [\n\t{\n\t\ttitle: \"Movies\",\n\t\tvalue: BaseItemKind.Movie,\n\t\tcompatibleCollectionTypes: [CollectionType.Movies],\n\t},\n\t{\n\t\ttitle: \"Shows\",\n\t\tvalue: BaseItemKind.Series,\n\t\tcompatibleCollectionTypes: [CollectionType.Tvshows],\n\t},\n\t{\n\t\ttitle: \"Albums\",\n\t\tvalue: BaseItemKind.MusicAlbum,\n\t\tcompatibleCollectionTypes: [CollectionType.Music],\n\t},\n\t{\n\t\ttitle: \"Collections\",\n\t\tvalue: BaseItemKind.CollectionFolder,\n\t\tcompatibleCollectionTypes: [CollectionType.Movies, CollectionType.Boxsets],\n\t},\n\t{\n\t\ttitle: \"Books\",\n\t\tvalue: BaseItemKind.Book,\n\t\tcompatibleCollectionTypes: [CollectionType.Books],\n\t},\n\t{\n\t\ttitle: \"Genres\",\n\t\tcompatibleCollectionTypes: [\n\t\t\tCollectionType.Movies,\n\t\t\tCollectionType.Tvshows,\n\t\t\tCollectionType.Music,\n\t\t],\n\t\tvalue: BaseItemKind.Genre,\n\t},\n\t{\n\t\ttitle: \"Trailers\",\n\t\tvalue: BaseItemKind.Trailer,\n\t\tcompatibleCollectionTypes: [CollectionType.Movies],\n\t},\n\t{\n\t\ttitle: \"TV Networks\",\n\t\tvalue: BaseItemKind.Studio,\n\t\tcompatibleCollectionTypes: [CollectionType.Tvshows],\n\t},\n\t{\n\t\ttitle: \"Album Artist\",\n\t\tvalue: BaseItemKind.MusicArtist,\n\t\tcompatibleCollectionTypes: [CollectionType.Music],\n\t},\n\t{\n\t\ttitle: \"Artist\",\n\t\tvalue: \"Artist\",\n\t\tcompatibleCollectionTypes: [CollectionType.Music],\n\t},\n\t{\n\t\ttitle: \"Playlist\",\n\t\tvalue: BaseItemKind.Playlist,\n\t\tcompatibleCollectionTypes: [CollectionType.Music, CollectionType.Playlists],\n\t},\n\t{\n\t\ttitle: \"Songs\",\n\t\tvalue: BaseItemKind.Audio,\n\t\tcompatibleCollectionTypes: [CollectionType.Music],\n\t},\n];\n\nexport const SORT_BY_OPTIONS: Array<{\n\ttitle: string;\n\tvalue: ItemSortBy | ItemSortBy[];\n\tcompatibleCollectionTypes?: CollectionType[];\n\tcompatibleViewTypes?: BaseItemKind[];\n}> = [\n\t{\n\t\ttitle: \"Name\",\n\t\tvalue: ItemSortBy.SortName,\n\t\tcompatibleCollectionTypes: [\n\t\t\tCollectionType.Movies,\n\t\t\tCollectionType.Tvshows,\n\t\t\tCollectionType.Boxsets,\n\t\t\tCollectionType.Books,\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Track Name\",\n\t\tvalue: ItemSortBy.Name,\n\t\tcompatibleViewTypes: [\n\t\t\tBaseItemKind.Audio,\n\t\t\tBaseItemKind.MusicAlbum,\n\t\t\tBaseItemKind.Playlist,\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Album\",\n\t\tvalue: ItemSortBy.Album,\n\t\tcompatibleViewTypes: [BaseItemKind.Audio],\n\t},\n\t{\n\t\ttitle: \"Album Artist\",\n\t\tvalue: ItemSortBy.AlbumArtist,\n\t\tcompatibleViewTypes: [BaseItemKind.Audio, BaseItemKind.MusicAlbum],\n\t},\n\t{\n\t\ttitle: \"Artist\",\n\t\tvalue: ItemSortBy.Artist,\n\t\tcompatibleViewTypes: [BaseItemKind.Audio],\n\t},\n\t{\n\t\ttitle: \"Date Added\",\n\t\tvalue: ItemSortBy.DateCreated,\n\t\tcompatibleViewTypes: [\n\t\t\tBaseItemKind.MusicAlbum,\n\t\t\tBaseItemKind.Audio,\n\t\t\tBaseItemKind.Playlist,\n\t\t],\n\t\tcompatibleCollectionTypes: [\n\t\t\tCollectionType.Movies,\n\t\t\tCollectionType.Boxsets,\n\t\t\tCollectionType.Tvshows,\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Date Played\",\n\t\tvalue: ItemSortBy.DatePlayed,\n\t\tcompatibleViewTypes: [BaseItemKind.Audio, BaseItemKind.Playlist],\n\t\tcompatibleCollectionTypes: [\n\t\t\tCollectionType.Movies,\n\t\t\tCollectionType.Tvshows,\n\t\t\tCollectionType.Boxsets,\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Play Count\",\n\t\tvalue: ItemSortBy.PlayCount,\n\t\tcompatibleViewTypes: [BaseItemKind.Audio, BaseItemKind.Playlist],\n\t\tcompatibleCollectionTypes: [CollectionType.Movies, CollectionType.Boxsets],\n\t},\n\t{\n\t\ttitle: \"Release Date\",\n\t\tvalue: ItemSortBy.PremiereDate,\n\t\tcompatibleViewTypes: [\n\t\t\tBaseItemKind.MusicAlbum,\n\t\t\tBaseItemKind.Audio,\n\t\t\tBaseItemKind.Playlist,\n\t\t],\n\t\tcompatibleCollectionTypes: [\n\t\t\tCollectionType.Movies,\n\t\t\tCollectionType.Tvshows,\n\t\t\tCollectionType.Boxsets,\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Runtime\",\n\t\tvalue: ItemSortBy.Runtime,\n\t\tcompatibleViewTypes: [\n\t\t\tBaseItemKind.MusicAlbum,\n\t\t\tBaseItemKind.Audio,\n\t\t\tBaseItemKind.Playlist,\n\t\t],\n\t\tcompatibleCollectionTypes: [CollectionType.Movies, CollectionType.Boxsets],\n\t},\n\t{\n\t\ttitle: \"Random\",\n\t\tvalue: ItemSortBy.Random,\n\t\tcompatibleViewTypes: [\n\t\t\t// BaseItemKind.MusicAlbum,\n\t\t\t// BaseItemKind.Audio,\n\t\t\t// BaseItemKind.Playlist,\n\t\t],\n\t\tcompatibleCollectionTypes: [\n\t\t\t// CollectionType.Movies,\n\t\t\t// CollectionType.Tvshows,\n\t\t\t// CollectionType.Boxsets,\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Community Rating\",\n\t\tvalue: ItemSortBy.CommunityRating,\n\t\tcompatibleViewTypes: [\n\t\t\tBaseItemKind.MusicAlbum,\n\t\t\tBaseItemKind.Audio,\n\t\t\tBaseItemKind.Playlist,\n\t\t],\n\t\tcompatibleCollectionTypes: [\n\t\t\tCollectionType.Movies,\n\t\t\tCollectionType.Tvshows,\n\t\t\tCollectionType.Boxsets,\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Critics Rating\",\n\t\tvalue: ItemSortBy.CriticRating,\n\t\tcompatibleViewTypes: [\n\t\t\tBaseItemKind.MusicAlbum,\n\t\t\tBaseItemKind.Audio,\n\t\t\tBaseItemKind.Playlist,\n\t\t],\n\t\tcompatibleCollectionTypes: [CollectionType.Movies, CollectionType.Boxsets],\n\t},\n\t{\n\t\ttitle: \"Folder\",\n\t\tvalue: [ItemSortBy.IsFolder, ItemSortBy.SortName],\n\t\tcompatibleViewTypes: [BaseItemKind.Playlist],\n\t},\n\t{\n\t\ttitle: \"Parental Rating\",\n\t\tvalue: ItemSortBy.OfficialRating,\n\t\tcompatibleViewTypes: [BaseItemKind.Playlist],\n\t\tcompatibleCollectionTypes: [\n\t\t\tCollectionType.Movies,\n\t\t\tCollectionType.Tvshows,\n\t\t\tCollectionType.Boxsets,\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Release Date\",\n\t\tvalue: [\n\t\t\tItemSortBy.ProductionYear,\n\t\t\tItemSortBy.PremiereDate,\n\t\t\tItemSortBy.SortName,\n\t\t],\n\t\tcompatibleViewTypes: [\n\t\t\tBaseItemKind.MusicAlbum,\n\t\t\tBaseItemKind.Audio,\n\t\t\tBaseItemKind.Playlist,\n\t\t],\n\t},\n\t{\n\t\ttitle: \"Date Episode Added\",\n\t\tvalue: ItemSortBy.DateLastContentAdded,\n\t\tcompatibleCollectionTypes: [CollectionType.Tvshows],\n\t},\n];\n\nexport const getDefaultSortByForCollectionType = (\n\tcollectionType: CollectionType,\n): ItemSortBy => {\n\tconst option = SORT_BY_OPTIONS.find((opt) =>\n\t\topt.compatibleCollectionTypes?.includes(collectionType),\n\t);\n\treturn Array.isArray(option?.value)\n\t\t? option.value[0]\n\t\t: option?.value || ItemSortBy.SortName;\n};\n\n\nexport type FILTERS =\n\t| \"isPlayed\"\n\t| \"isUnPlayed\"\n\t| \"isResumable\"\n\t| \"isFavorite\"\n\t| \"hasSubtitles\"\n\t| \"hasTrailer\"\n\t| \"hasSpecialFeature\"\n\t| \"hasThemeSong\"\n\t| \"hasThemeVideo\"\n\t| \"isSD\"\n\t| \"isHD\"\n\t| \"is4K\"\n\t| \"is3D\";\n"
  },
  {
    "path": "src/utils/date/formateDate.ts",
    "content": "/**\n * @format\n * @param {string} date - Date from server\n * @returns {string} Formated Date\n */\n\nexport const formateDate = (date: string | number | Date) => {\n\tconst dateObj = new Date(date);\n\tconst formatedDate = new Intl.DateTimeFormat().format(dateObj);\n\treturn formatedDate;\n};\n"
  },
  {
    "path": "src/utils/date/time.ts",
    "content": "/**\n * @format\n * @param {number} ticks - C# ticks of a particular item\n * @return {number} Converted ticks to Milliseconds\n */\nexport const ticksToMs = (ticks: number) => {\n\treturn Math.round(ticks / 10000);\n};\n\n/**\n * @format\n * @param {number} ticks - C# ticks of a particular item\n * @return {number} Converted ticks to Seconds\n */\nexport const ticksToSec = (ticks: number) => {\n\treturn Math.round(ticksToMs(ticks) / 1000);\n};\n\n/**\n * @format\n * @param {number} sec - Sec of a particular item\n * @return {number} Converted Seconds to C# ticks\n */\nexport const secToTicks = (ticks: number) => {\n\treturn Math.round(ticks * 10000000);\n};\n\n/**\n * @format\n * @param {number} ticks - C# ticks of a particular item\n * @return {number} Converted ticks to Hhr Mmin formate\n */\nexport const getRuntime = (ticks: number) => {\n\tconst time = ticksToMs(ticks);\n\tlet formatedTime = \"\";\n\tlet timeSec = Math.floor(time / 1000);\n\tlet timeMin = Math.floor(timeSec / 60);\n\ttimeSec -= timeMin * 60;\n\tif (timeMin > 60) {\n\t\tconst timeHr = Math.floor(timeMin / 60);\n\t\ttimeMin -= timeHr * 60;\n\t\tformatedTime = `${timeHr}hr ${timeMin}min`;\n\t} else {\n\t\tformatedTime = `${timeMin}min`;\n\t}\n\treturn formatedTime;\n};\n\n/**\n * @format\n * @param {number} ticks - C# ticks of a particular item\n * @return {number} Converted ticks to hh:mm:ss formate\n */\nexport const getRuntimeMusic = (ticks: number) => {\n\tconst time = ticksToMs(ticks);\n\tlet formatedTime = \"\";\n\tlet timeSec = Math.floor(time / 1000);\n\tlet timeMin = Math.floor(timeSec / 60);\n\ttimeSec -= timeMin * 60;\n\ttimeSec = timeSec === 0 ? 0o0 : timeSec;\n\tif (timeMin > 60) {\n\t\tconst timeHr = Math.floor(timeMin / 60);\n\t\ttimeMin -= timeHr * 60;\n\t\tformatedTime = `${timeHr}:${timeMin.toLocaleString([], {\n\t\t\tminimumIntegerDigits: 2,\n\t\t\tuseGrouping: false,\n\t\t})}:${timeSec.toLocaleString([], {\n\t\t\tminimumIntegerDigits: 2,\n\t\t\tuseGrouping: false,\n\t\t})}`;\n\t} else {\n\t\tformatedTime = `${timeMin}:${timeSec.toLocaleString([], {\n\t\t\tminimumIntegerDigits: 2,\n\t\t\tuseGrouping: false,\n\t\t})}`;\n\t}\n\treturn formatedTime;\n};\n\n/**\n * @format\n * @param {number} ticks - C# ticks of a particular item\n * @return {number} Converted ticks to H hour M minutes formate\n */\nexport const getRuntimeFull = (ticks: number) => {\n\tconst time = ticksToMs(ticks);\n\tlet formatedTime = \"\";\n\tlet timeSec = Math.floor(time / 1000);\n\tlet timeMin = Math.floor(timeSec / 60);\n\ttimeSec -= timeMin * 60;\n\tif (timeMin > 60) {\n\t\tconst timeHr = Math.floor(timeMin / 60);\n\t\ttimeMin -= timeHr * 60;\n\t\tformatedTime = `${timeHr} hour ${timeMin} minutes`;\n\t} else {\n\t\tformatedTime = `${timeMin} minutes`;\n\t}\n\treturn formatedTime;\n};\n\n/**\n * @format\n * @param {number} ticks - C# ticks of a particular item\n * @return {number} Converted ticks to Hh Mm formate\n */\nexport const getRuntimeCompact = (ticks: number) => {\n\tconst time = ticksToMs(ticks);\n\tlet formatedTime = \"\";\n\tlet timeSec = Math.floor(time / 1000);\n\tlet timeMin = Math.floor(timeSec / 60);\n\ttimeSec -= timeMin * 60;\n\tif (timeMin > 60) {\n\t\tconst timeHr = Math.floor(timeMin / 60);\n\t\ttimeMin -= timeHr * 60;\n\t\tformatedTime = `${timeHr}h ${timeMin}m`;\n\t} else {\n\t\tformatedTime = `${timeMin}m`;\n\t}\n\treturn formatedTime;\n};\n\n/**\n * @format\n * @param {number} ticks - C# ticks of a particular item\n * @return {number} End time of an item\n */\nexport const endsAt = (ticks: number) => {\n\tconst current = new Date();\n\tconst currentTime = current.getTime();\n\tconst time = ticksToMs(ticks);\n\tconst calculatedTime = currentTime + time;\n\tconst formated = new Date(calculatedTime).toLocaleString([], {\n\t\thour: \"2-digit\",\n\t\tminute: \"2-digit\",\n\t});\n\t// let formated = `${hr - 12}:${min}`;\n\t// return `Ends at ${formated.getHours()}:${formated.getMinutes()}`;\n\treturn `Ends at ${formated}`;\n};\n"
  },
  {
    "path": "src/utils/hooks/useDebounce.ts",
    "content": "import { useEffect, useState } from \"react\";\n\n// see https://github.com/tannerlinsley/react-query/issues/293\n// see https://usehooks.com/useDebounce/\nexport default function useDebounce(value: string, delay: number) {\n\t// State and setters for debounced value\n\tconst [debouncedValue, setDebouncedValue] = useState(value);\n\n\tuseEffect(\n\t\t() => {\n\t\t\t// Update debounced value after delay\n\t\t\tconst handler = setTimeout(() => {\n\t\t\t\tsetDebouncedValue(value);\n\t\t\t}, delay);\n\n\t\t\t// Cancel the timeout if value changes (also on delay change or unmount)\n\t\t\t// This is how we prevent debounced value from updating if value is changed ...\n\t\t\t// .. within the delay period. Timeout gets cleared and restarted.\n\t\t\treturn () => {\n\t\t\t\tclearTimeout(handler);\n\t\t\t};\n\t\t},\n\t\t[value, delay], // Only re-call effect if value or delay changes\n\t);\n\n\treturn debouncedValue;\n}\n"
  },
  {
    "path": "src/utils/hooks/useDefaultSeason.ts",
    "content": "import type { BaseItemDtoQueryResult } from \"@jellyfin/sdk/lib/generated-client\";\nimport type { UseQueryResult, useQuery } from \"@tanstack/react-query\";\nimport type React from \"react\";\nimport { useEffect, useState } from \"react\";\n\n/**\n * Custom hook for selecting and managing the default season for a TV series.\n * Handles season selection logic including:\n * - Restoring previously selected season from sessionStorage\n * - Auto-selecting the first unwatched regular season (prioritizes regular seasons over specials)\n * - Only falls back to special seasons if no regular seasons exist\n * - Properly handles the case where some episodes in special seasons are watched\n */\n\nfunction useDefaultSeason(\n\tseasons: UseQueryResult<BaseItemDtoQueryResult | null, unknown>,\n\titemId: string | undefined,\n): [number, React.Dispatch<React.SetStateAction<number>>] {\n\tconst [currentSeason, setCurrentSeason] = useState<number>(() => {\n\t\tconst result = sessionStorage.getItem(`season-${itemId}`);\n\t\treturn result ? Number(result) : 0;\n\t});\n\n\t// Track if this is the initial load to avoid switching during episode updates\n\tconst [hasInitialized, setHasInitialized] = useState(false);\n\tuseEffect(() => {\n\t\tif (seasons.isSuccess && seasons.data?.Items) {\n\t\t\tconst savedSeason = sessionStorage.getItem(`season-${itemId}`);\n\n\t\t\t// Always re-evaluate the best season based on viewing progress\n\t\t\tlet defaultSeasonIndex = 0;\n\n\t\t\t// Map all seasons with their original index\n\t\t\tconst allSeasonsWithIndex = seasons.data.Items.map(\n\t\t\t\t(season, index: number) => ({\n\t\t\t\t\t...season,\n\t\t\t\t\toriginalIndex: index,\n\t\t\t\t}),\n\t\t\t);\n\n\t\t\t// Separate regular seasons (not specials) and special seasons\n\t\t\tconst regularSeasons = allSeasonsWithIndex.filter(\n\t\t\t\t(season) => season.IndexNumber !== 0 && season.IndexNumber != null,\n\t\t\t);\n\t\t\tconst specialSeasons = allSeasonsWithIndex.filter(\n\t\t\t\t(season) => season.IndexNumber === 0,\n\t\t\t);\n\n\t\t\t// Find the optimal season based on viewing progress\n\t\t\tif (regularSeasons.length > 0) {\n\t\t\t\tlet targetSeason = regularSeasons[0];\n\n\t\t\t\t// Look for the first season that isn't fully watched\n\t\t\t\tfor (const season of regularSeasons) {\n\t\t\t\t\t// A season is considered \"unwatched\" if:\n\t\t\t\t\t// 1. The season itself isn't marked as played, AND\n\t\t\t\t\t// 2. The season has a play percentage less than 100%\n\t\t\t\t\tconst isSeasonFullyWatched =\n\t\t\t\t\t\tseason.UserData?.Played ||\n\t\t\t\t\t\t(season.UserData?.PlayedPercentage &&\n\t\t\t\t\t\t\tseason.UserData.PlayedPercentage >= 100);\n\n\t\t\t\t\tif (!isSeasonFullyWatched) {\n\t\t\t\t\t\ttargetSeason = season;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// If the selected season is still fully watched, try to move to the next one\n\t\t\t\tconst isTargetFullyWatched =\n\t\t\t\t\ttargetSeason.UserData?.Played ||\n\t\t\t\t\t(targetSeason.UserData?.PlayedPercentage &&\n\t\t\t\t\t\ttargetSeason.UserData.PlayedPercentage >= 100);\n\n\t\t\t\tif (isTargetFullyWatched) {\n\t\t\t\t\tconst currentIndex = regularSeasons.findIndex(\n\t\t\t\t\t\t(s) => s.Id === targetSeason.Id,\n\t\t\t\t\t);\n\t\t\t\t\tconst nextSeasonIndex = currentIndex + 1;\n\t\t\t\t\tif (nextSeasonIndex < regularSeasons.length) {\n\t\t\t\t\t\ttargetSeason = regularSeasons[nextSeasonIndex];\n\t\t\t\t\t}\n\t\t\t\t\t// If we're at the last season and it's fully watched, stay on it\n\t\t\t\t}\n\n\t\t\t\tdefaultSeasonIndex = targetSeason.originalIndex;\n\t\t\t}\n\t\t\t// Only fall back to special seasons if no regular seasons exist\n\t\t\telse if (specialSeasons.length > 0) {\n\t\t\t\t// For special seasons, just pick the first one\n\t\t\t\tdefaultSeasonIndex = specialSeasons[0].originalIndex;\n\t\t\t}\n\n\t\t\t// Smart season selection logic\n\t\t\tif (savedSeason) {\n\t\t\t\tconst savedSeasonNumber = Number(savedSeason);\n\t\t\t\tconst currentSeasonData = allSeasonsWithIndex[savedSeasonNumber];\n\n\t\t\t\t// Check if user is currently in a special season\n\t\t\t\tconst isInSpecialSeason = currentSeasonData?.IndexNumber === 0;\n\n\t\t\t\t// Check if current season is fully complete\n\t\t\t\tconst isCurrentSeasonComplete =\n\t\t\t\t\tcurrentSeasonData?.UserData?.Played ||\n\t\t\t\t\t(currentSeasonData?.UserData?.PlayedPercentage &&\n\t\t\t\t\t\tcurrentSeasonData.UserData.PlayedPercentage >= 100);\n\n\t\t\t\tif (isInSpecialSeason) {\n\t\t\t\t\t// For special seasons: advance to regular seasons if specials are complete\n\t\t\t\t\tif (isCurrentSeasonComplete && regularSeasons.length > 0) {\n\t\t\t\t\t\t// Special season is done, move to optimal regular season\n\t\t\t\t\t\tsetCurrentSeason(defaultSeasonIndex);\n\t\t\t\t\t\tsessionStorage.setItem(\n\t\t\t\t\t\t\t`season-${itemId}`,\n\t\t\t\t\t\t\tdefaultSeasonIndex.toString(),\n\t\t\t\t\t\t);\n\t\t\t\t\t} else if (!hasInitialized && regularSeasons.length > 0) {\n\t\t\t\t\t\t// Initial load: prioritize regular seasons over specials\n\t\t\t\t\t\tsetCurrentSeason(defaultSeasonIndex);\n\t\t\t\t\t\tsessionStorage.setItem(\n\t\t\t\t\t\t\t`season-${itemId}`,\n\t\t\t\t\t\t\tdefaultSeasonIndex.toString(),\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Stay in specials (either no regular seasons or specials not complete)\n\t\t\t\t\t\tsetCurrentSeason(savedSeasonNumber);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// For regular seasons: advance to next season if current is complete\n\t\t\t\t\tif (isCurrentSeasonComplete) {\n\t\t\t\t\t\t// Find next season after current\n\t\t\t\t\t\tconst currentRegularIndex = regularSeasons.findIndex(\n\t\t\t\t\t\t\t(s: { originalIndex: number }) =>\n\t\t\t\t\t\t\t\ts.originalIndex === savedSeasonNumber,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tcurrentRegularIndex >= 0 &&\n\t\t\t\t\t\t\tcurrentRegularIndex < regularSeasons.length - 1\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\t// Move to next regular season\n\t\t\t\t\t\t\tconst nextSeason = regularSeasons[currentRegularIndex + 1];\n\t\t\t\t\t\t\tsetCurrentSeason(nextSeason.originalIndex);\n\t\t\t\t\t\t\tsessionStorage.setItem(\n\t\t\t\t\t\t\t\t`season-${itemId}`,\n\t\t\t\t\t\t\t\tnextSeason.originalIndex.toString(),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// At last season or use calculated optimal\n\t\t\t\t\t\t\tsetCurrentSeason(defaultSeasonIndex);\n\t\t\t\t\t\t\tsessionStorage.setItem(\n\t\t\t\t\t\t\t\t`season-${itemId}`,\n\t\t\t\t\t\t\t\tdefaultSeasonIndex.toString(),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if (\n\t\t\t\t\t\t!hasInitialized &&\n\t\t\t\t\t\tsavedSeasonNumber !== defaultSeasonIndex\n\t\t\t\t\t) {\n\t\t\t\t\t\t// Initial load: use calculated optimal season\n\t\t\t\t\t\tsetCurrentSeason(defaultSeasonIndex);\n\t\t\t\t\t\tsessionStorage.setItem(\n\t\t\t\t\t\t\t`season-${itemId}`,\n\t\t\t\t\t\t\tdefaultSeasonIndex.toString(),\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Stay in current season\n\t\t\t\t\t\tsetCurrentSeason(savedSeasonNumber);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// No saved season, use the calculated default\n\t\t\t\tsetCurrentSeason(defaultSeasonIndex);\n\t\t\t\tsessionStorage.setItem(\n\t\t\t\t\t`season-${itemId}`,\n\t\t\t\t\tdefaultSeasonIndex.toString(),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Mark as initialized after first run\n\t\t\tif (!hasInitialized) {\n\t\t\t\tsetHasInitialized(true);\n\t\t\t}\n\t\t}\n\t}, [seasons.isSuccess, seasons.data?.Items, itemId, hasInitialized]);\n\treturn [currentSeason, setCurrentSeason];\n}\n\nexport default useDefaultSeason;\n"
  },
  {
    "path": "src/utils/hooks/useInterval.tsx",
    "content": "import { useEffect, useRef } from \"react\";\n\nexport default function useInterval(\n\tcallback: () => void,\n\tdelay: number | null,\n) {\n\tconst savedCallback = useRef(callback);\n\n\t// Remember the latest callback if it changes.\n\tuseEffect(() => {\n\t\tsavedCallback.current = callback;\n\t}, [callback]);\n\n\t// Set up the interval.\n\tuseEffect(() => {\n\t\t// Don't schedule if no delay is specified.\n\t\t// Note: 0 is a valid value for delay.\n\t\tif (delay === null) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst id = setInterval(() => {\n\t\t\tsavedCallback.current();\n\t\t}, delay);\n\n\t\treturn () => {\n\t\t\tclearInterval(id);\n\t\t};\n\t}, [delay]);\n}"
  },
  {
    "path": "src/utils/hooks/useKeyPress.tsx",
    "content": "import { useEffect, useState } from \"react\";\n\nconst useKeyPress = (targetKey: string) => {\n\t// State for keeping track of whether key is pressed\n\tconst [keyPressed, setKeyPressed] = useState(false);\n\t// If pressed key is our target key then set to true\n\tfunction downHandler({ key }: KeyboardEvent) {\n\t\tif (key === targetKey) {\n\t\t\tsetKeyPressed(true);\n\t\t}\n\t}\n\t// If released key is our target key then set to false\n\tconst upHandler = ({ key }: KeyboardEvent) => {\n\t\tif (key === targetKey) {\n\t\t\tsetKeyPressed(false);\n\t\t}\n\t};\n\t// Add event listeners\n\tuseEffect(() => {\n\t\twindow.addEventListener(\"keydown\", downHandler);\n\t\twindow.addEventListener(\"keyup\", upHandler);\n\t\t// Remove event listeners on cleanup\n\t\treturn () => {\n\t\t\twindow.removeEventListener(\"keydown\", downHandler);\n\t\t\twindow.removeEventListener(\"keyup\", upHandler);\n\t\t};\n\t}); // Empty array ensures that effect is only run on mount and unmount\n\treturn keyPressed;\n};\n\nexport default useKeyPress;\n"
  },
  {
    "path": "src/utils/hooks/useParallax.tsx",
    "content": "import { type MotionValue, useTransform } from \"motion/react\";\n\nexport default function useParallax(\n\tvalue: MotionValue<number>,\n\tdistance: number,\n) {\n\treturn useTransform(value, [0, 1], [-distance, distance]);\n}\n"
  },
  {
    "path": "src/utils/methods/getImageUrlsApi.ts",
    "content": "import type { Api } from \"@jellyfin/sdk\";\nimport { ImageUrlsApi } from \"@jellyfin/sdk/lib/utils/api/image-urls-api\";\n\nconst getImageUrlsApi = (api: Api) => new ImageUrlsApi(api.configuration);\n\nexport default getImageUrlsApi;"
  },
  {
    "path": "src/utils/methods/getSubtitles.ts",
    "content": "import type { MediaStream } from \"@jellyfin/sdk/lib/generated-client\";\nimport type subtitlePlaybackInfo from \"../types/subtitlePlaybackInfo\";\n\nexport default function getSubtitle(\n\ttrack: number | \"nosub\" | undefined,\n\tmediaStreams: MediaStream[] | undefined | null,\n): subtitlePlaybackInfo | undefined {\n\tconst availableSubtitles = mediaStreams?.filter(\n\t\t(stream) => stream.Type === \"Subtitle\",\n\t);\n\tif (track === \"nosub\" || track === -1)\n\t\treturn {\n\t\t\ttrack: -1,\n\t\t\tenable: false,\n\t\t\tformat: \"vtt\",\n\t\t\tallTracks: availableSubtitles,\n\t\t\turl: null,\n\t\t};\n\tconst requiredSubtitle = availableSubtitles?.filter(\n\t\t(stream) => stream.Index === track,\n\t);\n\tconst url = requiredSubtitle?.[0]?.DeliveryUrl;\n\tif (track !== undefined && track !== -1) {\n\t\treturn {\n\t\t\ttrack,\n\t\t\tenable: true,\n\t\t\tformat: requiredSubtitle?.[0]?.Codec,\n\t\t\tallTracks: availableSubtitles,\n\t\t\turl,\n\t\t};\n\t}\n}"
  },
  {
    "path": "src/utils/methods/playback.ts",
    "content": "import type { Api } from \"@jellyfin/sdk\";\nimport {\n\ttype BaseItemDto,\n\tBaseItemKind,\n\tItemFields,\n\tItemFilter,\n\tLocationType,\n\tSortOrder,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport { getMediaInfoApi } from \"@jellyfin/sdk/lib/utils/api/media-info-api\";\nimport { getMediaSegmentsApi } from \"@jellyfin/sdk/lib/utils/api/media-segments-api\";\nimport { getPlaylistsApi } from \"@jellyfin/sdk/lib/utils/api/playlists-api\";\nimport { getTvShowsApi } from \"@jellyfin/sdk/lib/utils/api/tv-shows-api\";\nimport { getUserLibraryApi } from \"@jellyfin/sdk/lib/utils/api/user-library-api\";\nimport playbackProfile from \"@/utils/playback-profiles\";\n\nexport async function getNextEpisode(\n\tapi: Api,\n\tuserId: string,\n\tseriesId: string,\n\tseason: number,\n) {\n\tconst episode = (\n\t\tawait getTvShowsApi(api).getEpisodes({\n\t\t\tseriesId: seriesId,\n\t\t\tuserId: userId,\n\t\t\tenableUserData: true,\n\t\t\tseason,\n\t\t\tfields: [ItemFields.MediaStreams, ItemFields.MediaSources],\n\t\t})\n\t).data.Items?.find((ep) => ep.UserData?.Played !== true);\n\n\tif (episode) {\n\t\treturn episode;\n\t}\n\n\tconst allEpisodes = (\n\t\tawait getTvShowsApi(api).getEpisodes({\n\t\t\tseriesId: seriesId,\n\t\t\tuserId: userId,\n\t\t\tenableUserData: true,\n\t\t\tfields: [ItemFields.MediaStreams, ItemFields.MediaSources],\n\t\t})\n\t).data.Items;\n\n\tif (allEpisodes) {\n\t\treturn allEpisodes[0];\n\t}\n\n\treturn null;\n}\n\nexport interface PlaybackInfoOptions {\n\tcurrentAudioTrack?: number | \"auto\";\n\tcurrentSubTrack?: number | \"nosub\";\n\tcurrentEpisodeId?: string;\n\tplaylistItem?: boolean;\n\tplaylistItemId?: string;\n}\n\nexport async function getPlaybackInfo(\n\tapi: Api,\n\tuserId: string,\n\titem: BaseItemDto,\n\toptions: PlaybackInfoOptions = {},\n) {\n\tconst {\n\t\tcurrentAudioTrack = \"auto\",\n\t\tcurrentSubTrack,\n\t\tcurrentEpisodeId,\n\t\tplaylistItem,\n\t\tplaylistItemId,\n\t} = options;\n\n\tlet result: any;\n\tlet mediaSource: any;\n\tlet mediaSegments: any;\n\tlet index = 0;\n\n\tif (playlistItem && playlistItemId) {\n\t\tresult = await getPlaylistsApi(api).getPlaylistItems({\n\t\t\tuserId: userId,\n\t\t\tplaylistId: playlistItemId,\n\t\t});\n\t} else {\n\t\tswitch (item.Type) {\n\t\t\tcase BaseItemKind.Episode:\n\t\t\t\tif (item.SeriesId && item.SeasonId) {\n\t\t\t\t\tresult = await getTvShowsApi(api).getEpisodes({\n\t\t\t\t\t\tseriesId: item.SeriesId,\n\t\t\t\t\t\tfields: [\n\t\t\t\t\t\t\tItemFields.MediaSources,\n\t\t\t\t\t\t\tItemFields.MediaStreams,\n\t\t\t\t\t\t\tItemFields.Overview,\n\t\t\t\t\t\t\tItemFields.Chapters,\n\t\t\t\t\t\t\tItemFields.Trickplay,\n\t\t\t\t\t\t],\n\t\t\t\t\t\tenableUserData: true,\n\t\t\t\t\t\tuserId: userId,\n\t\t\t\t\t\tseasonId: item.SeasonId,\n\t\t\t\t\t\tisMissing: false,\n\t\t\t\t\t});\n\n\t\t\t\t\tif (currentEpisodeId) {\n\t\t\t\t\t\tindex =\n\t\t\t\t\t\t\tresult.data.Items?.map((i: any) => i.Id).indexOf(\n\t\t\t\t\t\t\t\tcurrentEpisodeId,\n\t\t\t\t\t\t\t) ?? 0;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tindex =\n\t\t\t\t\t\t\tresult.data.Items?.map((i: any) => i.Id).indexOf(item.Id) ?? 0;\n\t\t\t\t\t}\n\n\t\t\t\t\tif (index === -1) index = 0;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase BaseItemKind.Series:\n\t\t\t\tresult = await getTvShowsApi(api).getEpisodes({\n\t\t\t\t\tseriesId: item.Id || \"\",\n\t\t\t\t\tfields: [\n\t\t\t\t\t\tItemFields.MediaSources,\n\t\t\t\t\t\tItemFields.MediaStreams,\n\t\t\t\t\t\tItemFields.Overview,\n\t\t\t\t\t\tItemFields.Chapters,\n\t\t\t\t\t\tItemFields.Trickplay,\n\t\t\t\t\t],\n\t\t\t\t\tenableUserData: true,\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\tisMissing: false,\n\t\t\t\t});\n\n\t\t\t\tif (currentEpisodeId) {\n\t\t\t\t\tindex =\n\t\t\t\t\t\tresult.data.Items?.map((i: any) => i.Id).indexOf(\n\t\t\t\t\t\t\tcurrentEpisodeId,\n\t\t\t\t\t\t) ?? 0;\n\t\t\t\t}\n\t\t\t\tif (index === -1) index = 0;\n\t\t\t\tbreak;\n\t\t\tcase BaseItemKind.Playlist:\n\t\t\tcase BaseItemKind.MusicAlbum:\n\t\t\t\tresult = await getItemsApi(api).getItems({\n\t\t\t\t\tparentId: item.Id,\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\tfields: [ItemFields.MediaSources, ItemFields.MediaStreams],\n\t\t\t\t\tsortOrder: [SortOrder.Ascending],\n\t\t\t\t\tsortBy: [\"IndexNumber\"],\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase BaseItemKind.MusicArtist:\n\t\t\t\tresult = await getItemsApi(api).getItems({\n\t\t\t\t\tartistIds: [item.Id ?? \"\"],\n\t\t\t\t\trecursive: true,\n\t\t\t\t\tincludeItemTypes: [BaseItemKind.Audio],\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\tfields: [ItemFields.MediaSources, ItemFields.MediaStreams],\n\t\t\t\t\tsortOrder: [SortOrder.Ascending],\n\t\t\t\t\tsortBy: [\"PremiereDate\", \"ProductionYear\", \"SortName\"],\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase BaseItemKind.BoxSet:\n\t\t\t\tresult = await getItemsApi(api).getItems({\n\t\t\t\t\tparentId: item.Id,\n\t\t\t\t\tuserId,\n\t\t\t\t\tfields: [\n\t\t\t\t\t\tItemFields.MediaSources,\n\t\t\t\t\t\tItemFields.MediaStreams,\n\t\t\t\t\t\tItemFields.Chapters,\n\t\t\t\t\t\tItemFields.Trickplay,\n\t\t\t\t\t],\n\t\t\t\t\tsortOrder: [SortOrder.Ascending],\n\t\t\t\t\tsortBy: [\"IndexNumber\"],\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\tcase BaseItemKind.Photo: {\n\t\t\t\tconst photo = (\n\t\t\t\t\tawait getUserLibraryApi(api).getItem({\n\t\t\t\t\t\titemId: item.Id || \"\",\n\t\t\t\t\t})\n\t\t\t\t).data;\n\t\t\t\tresult = await getItemsApi(api).getItems({\n\t\t\t\t\tparentId: photo.ParentId ?? \"\",\n\t\t\t\t\tfilters: [ItemFilter.IsNotFolder],\n\t\t\t\t\trecursive: false,\n\t\t\t\t\tsortBy: [\"SortName\"],\n\t\t\t\t\tmediaTypes: [\"Photo\"],\n\t\t\t\t\texcludeLocationTypes: [LocationType.Virtual],\n\t\t\t\t\tcollapseBoxSetItems: false,\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\tresult = await getItemsApi(api).getItems({\n\t\t\t\t\tids: [item.Id ?? \"\"],\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\tfields: [\n\t\t\t\t\t\tItemFields.MediaSources,\n\t\t\t\t\t\tItemFields.MediaStreams,\n\t\t\t\t\t\tItemFields.Chapters,\n\t\t\t\t\t\tItemFields.Trickplay,\n\t\t\t\t\t],\n\t\t\t\t\tsortOrder: [SortOrder.Ascending],\n\t\t\t\t\tsortBy: [\"IndexNumber\"],\n\t\t\t\t});\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tif (\n\t\tresult?.data?.Items?.[index]?.Id &&\n\t\t(result.data.Items[index].MediaSources?.[0]?.Id ||\n\t\t\titem.Type === BaseItemKind.Photo)\n\t) {\n\t\tconst targetItem = result.data.Items[index];\n\t\tif (item.Type !== BaseItemKind.Photo) {\n\t\t\tlet audioStreamIndex = currentAudioTrack;\n\t\t\tif (audioStreamIndex === \"auto\") {\n\t\t\t\taudioStreamIndex =\n\t\t\t\t\ttargetItem.MediaSources?.[0].DefaultAudioStreamIndex ?? 0;\n\t\t\t}\n\n\t\t\tconst playbackInfoPromise = getMediaInfoApi(api).getPostedPlaybackInfo({\n\t\t\t\taudioStreamIndex: Number(audioStreamIndex),\n\t\t\t\tsubtitleStreamIndex: currentSubTrack === \"nosub\" ? -1 : currentSubTrack,\n\t\t\t\titemId: targetItem.Id,\n\t\t\t\tstartTimeTicks: targetItem.UserData?.PlaybackPositionTicks,\n\t\t\t\tuserId: userId,\n\t\t\t\tmediaSourceId: targetItem.MediaSources?.[0]?.Id,\n\t\t\t\tplaybackInfoDto: {\n\t\t\t\t\tDeviceProfile: playbackProfile,\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tconst segmentsPromise = await getMediaSegmentsApi(api).getItemSegments({\n\t\t\t\titemId: targetItem.Id,\n\t\t\t});\n\n\t\t\tconst [playbackInfoResponse, segmentsResponse] = await Promise.all([\n\t\t\t\tplaybackInfoPromise,\n\t\t\t\tsegmentsPromise,\n\t\t\t]);\n\n\t\t\tmediaSource = playbackInfoResponse;\n\t\t\tmediaSegments = segmentsResponse?.data;\n\t\t}\n\t}\n\n\treturn {\n\t\titem: result?.data,\n\t\tmediaSource: mediaSource?.data,\n\t\tmediaSegments,\n\t\tepisodeIndex: index,\n\t};\n}\n"
  },
  {
    "path": "src/utils/methods/ticksDisplay.ts",
    "content": "/**\n * Formats a given ticks value into a human-readable time string.\n * @param ticks - The ticks value to be formatted, typically in 100-nanosecond intervals.\n * @returns Formatted time string in the format HH:MM:SS.\n */\nconst ticksDisplay = (ticks: number) => {\n\tconst time = Math.round(ticks / 10000);\n\tlet formatedTime = \"\";\n\tlet timeSec = Math.floor(time / 1000);\n\tlet timeMin = Math.floor(timeSec / 60);\n\ttimeSec -= timeMin * 60;\n\ttimeSec = timeSec === 0 ? 0o0 : timeSec;\n\tconst timeHr = Math.floor(timeMin / 60);\n\ttimeMin -= timeHr * 60;\n\tformatedTime = `${timeHr.toLocaleString([], {\n\t\tminimumIntegerDigits: 2,\n\t\tuseGrouping: false,\n\t})}:${timeMin.toLocaleString([], {\n\t\tminimumIntegerDigits: 2,\n\t\tuseGrouping: false,\n\t})}:${timeSec.toLocaleString([], {\n\t\tminimumIntegerDigits: 2,\n\t\tuseGrouping: false,\n\t})}`;\n\treturn formatedTime;\n};\n\nexport default ticksDisplay;\n"
  },
  {
    "path": "src/utils/misc/debug.ts",
    "content": "function getVideoDebugInfo(videoElement: HTMLVideoElement) {\n\tconst quality = videoElement.getVideoPlaybackQuality();\n\n\tconst debugInfo = {\n\t\tdroppedFrames: quality.droppedVideoFrames,\n\t\ttotalFrames: quality.totalVideoFrames,\n\t\tresolution: {\n\t\t\twidth: videoElement.videoWidth,\n\t\t\theight: videoElement.videoHeight,\n\t\t},\n\t\tcontainer:\n\t\t\tvideoElement.currentSrc.split(\".\").pop()?.split(\"?\").pop() || \"unknown\",\n\t\tduration: videoElement.duration,\n\t\tcurrentTime: videoElement.currentTime,\n\t\tplaybackRate: videoElement.playbackRate,\n\t\tvolume: videoElement.volume,\n\t\tcurrentQuality: {\n\t\t\twidth: videoElement.videoWidth,\n\t\t\theight: videoElement.videoHeight,\n\t\t},\n\t};\n\n\treturn debugInfo;\n}\n"
  },
  {
    "path": "src/utils/misc/konami.ts",
    "content": "import { useState } from \"react\";\nimport useKonami from \"react-use-konami\";\n\nexport const useKonamiEasterEgg = () => {\n\tconst [easterEgg, setEasterEgg] = useState(false);\n\tconst sixtyNine = () => {\n\t\tsetEasterEgg(true);\n\t};\n\n\tuseKonami(sixtyNine, {\n\t\tcode: [\n\t\t\t\"ArrowUp\",\n\t\t\t\"ArrowUp\",\n\t\t\t\"ArrowDown\",\n\t\t\t\"ArrowDown\",\n\t\t\t\"ArrowLeft\",\n\t\t\t\"ArrowRight\",\n\t\t\t\"ArrowLeft\",\n\t\t\t\"ArrowRight\",\n\t\t\t\"b\",\n\t\t\t\"a\",\n\t\t],\n\t});\n\n\treturn [easterEgg, setEasterEgg] as const;\n};\n"
  },
  {
    "path": "src/utils/misc/relaunch.ts",
    "content": "import { relaunch } from \"@tauri-apps/plugin-process\";\n\nexport const handleRelaunch = async (_event: unknown, reason?: string) => {\n\tif (reason && reason === \"backdropClick\") {\n\t\treturn;\n\t}\n\tawait relaunch();\n};\n"
  },
  {
    "path": "src/utils/navigation.ts",
    "content": "// Deprecated: navigation helpers for search-param based library state were removed.\n// This file will be removed in a future commit. No exports remain.\n\nexport {}; // keep as empty module\n"
  },
  {
    "path": "src/utils/playback-profiles/README.md",
    "content": "source (jellyfin/jellyfin-vue)[https://github.com/jellyfin/jellyfin-vue/tree/master/frontend/src/utils/playback-profiles]"
  },
  {
    "path": "src/utils/playback-profiles/directplay-profile.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport {\n  type DirectPlayProfile,\n  DlnaProfileType\n} from '@jellyfin/sdk/lib/generated-client';\nimport { getSupportedAudioCodecs } from \"./helpers/audio-formats\";\nimport { getSupportedMP4AudioCodecs } from \"./helpers/mp4-audio-formats\";\nimport { getSupportedMP4VideoCodecs } from \"./helpers/mp4-video-formats\";\nimport { hasMkvSupport } from './helpers/transcoding-formats';\nimport { getSupportedWebMAudioCodecs } from './helpers/webm-audio-formats';\nimport { getSupportedWebMVideoCodecs } from \"./helpers/webm-video-formats\";\n\n/**\n * Returns a valid DirectPlayProfile for the current platform.\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns An array of direct play profiles for the current platform.\n */\nexport function getDirectPlayProfiles(\n  videoTestElement: HTMLVideoElement\n): DirectPlayProfile[] {\n  const DirectPlayProfiles: DirectPlayProfile[] = [];\n\n  const webmVideoCodecs = getSupportedWebMVideoCodecs(videoTestElement);\n  const webmAudioCodecs = getSupportedWebMAudioCodecs(videoTestElement);\n\n  const mp4VideoCodecs = getSupportedMP4VideoCodecs(videoTestElement);\n  const mp4AudioCodecs = getSupportedMP4AudioCodecs(videoTestElement);\n\n  if (webmVideoCodecs.length > 0) {\n    DirectPlayProfiles.push({\n      Container: 'webm',\n      Type: DlnaProfileType.Video,\n      VideoCodec: webmVideoCodecs.join(','),\n      AudioCodec: webmAudioCodecs.join(',')\n    });\n  }\n\n  if (mp4VideoCodecs.length > 0) {\n    DirectPlayProfiles.push({\n      Container: 'mp4,m4v',\n      Type: DlnaProfileType.Video,\n      VideoCodec: mp4VideoCodecs.join(','),\n      AudioCodec: mp4AudioCodecs.join(',')\n    });\n  }\n\n  if (hasMkvSupport(videoTestElement) && mp4VideoCodecs.length > 0) {\n    DirectPlayProfiles.push({\n      Container: 'mkv',\n      Type: DlnaProfileType.Video,\n      VideoCodec: mp4VideoCodecs.join(','),\n      AudioCodec: mp4AudioCodecs.join(',')\n    });\n  }\n\n  const supportedAudio = [\n\t\t\t\"opus\",\n\t\t\t\"mp3\",\n\t\t\t\"mp2\",\n\t\t\t\"aac\",\n\t\t\t\"flac\",\n\t\t\t\"alac\",\n\t\t\t\"webma\",\n\t\t\t\"wma\",\n\t\t\t\"wav\",\n\t\t\t\"ogg\",\n\t\t\t\"oga\",\n\t\t\t\"eac3\",\n\t\t];\n\n  for (const audioFormat of supportedAudio.filter(format =>\n    getSupportedAudioCodecs(format)\n  )) {\n    DirectPlayProfiles.push({\n      Container: audioFormat,\n      Type: DlnaProfileType.Audio\n    });\n\n    if (audioFormat === 'opus' || audioFormat === 'webma') {\n      DirectPlayProfiles.push({\n        Container: 'webm',\n        Type: DlnaProfileType.Audio,\n        AudioCodec: audioFormat\n      });\n    }\n\n    // Aac also appears in the m4a and m4b container\n    if (audioFormat === 'aac' || audioFormat === 'alac') {\n      DirectPlayProfiles.push(\n        {\n          Container: 'm4a',\n          AudioCodec: audioFormat,\n          Type: DlnaProfileType.Audio\n        },\n        {\n          Container: 'm4b',\n          AudioCodec: audioFormat,\n          Type: DlnaProfileType.Audio\n        }\n      );\n    }\n  }\n\n  return DirectPlayProfiles;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/helpers/audio-formats.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport { isApple, isTizen, isTv, isWebOS } from \"@/utils/browser-detection\";\n\n/**\n * Determines if audio codec is supported\n */\nexport function getSupportedAudioCodecs(format: string): boolean {\n\tlet typeString: string | undefined;\n\n\tif (format === \"flac\" && isTv()) {\n\t\treturn true;\n\t} else if (format === \"eac3\") {\n\t\t// This is specific to Blink\n\t\treturn true;\n\t} else if (format === \"wma\" && isTizen()) {\n\t\treturn true;\n\t} else if (format === \"asf\" && isTv()) {\n\t\treturn true;\n\t} else if (format === \"opus\") {\n\t\tif (!isWebOS()) {\n\t\t\ttypeString = 'audio/ogg; codecs=\"opus\"';\n\n\t\t\treturn !!document\n\t\t\t\t.createElement(\"audio\")\n\t\t\t\t.canPlayType(typeString)\n\t\t\t\t.replace(/no/, \"\");\n\t\t}\n\n\t\treturn false;\n\t} else if (format === \"alac\" && isApple()) {\n\t\treturn true;\n\t} else if (format === \"webma\") {\n\t\ttypeString = \"audio/webm\";\n\t} else if (format === \"mp2\") {\n\t\ttypeString = \"audio/mpeg\";\n\t} else {\n\t\ttypeString = \"audio/\" + format;\n\t}\n\n\treturn !!document\n\t\t.createElement(\"audio\")\n\t\t.canPlayType(typeString)\n\t\t.replace(/no/, \"\");\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/helpers/codec-profiles.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport {\n\tisApple,\n\tisChromiumBased,\n\tisEdge,\n\tisMobile,\n\tisPs4,\n\tisTizen,\n\tisTv,\n\tisWebOS,\n\tisXbox,\n\tsafariVersion,\n} from \"@/utils/browser-detection\";\nimport {\n\ttype CodecProfile,\n\tCodecType,\n\ttype ProfileCondition,\n\tProfileConditionType,\n\tProfileConditionValue,\n} from \"@jellyfin/sdk/lib/generated-client\";\n\n/**\n * Gets the max video bitrate\n *\n * @returns Returns the MaxVideoBitrate\n */\nfunction getGlobalMaxVideoBitrate(): number | undefined {\n\tlet isTizenFhd = false;\n\n\tif (\n\t\tisTizen() &&\n\t\t\"webapis\" in window &&\n\t\ttypeof window.webapis === \"object\" &&\n\t\twindow.webapis &&\n\t\t\"productinfo\" in window.webapis &&\n\t\ttypeof window.webapis.productinfo === \"object\" &&\n\t\twindow.webapis.productinfo &&\n\t\t\"isUdPanelSupported\" in window.webapis.productinfo &&\n\t\ttypeof window.webapis.productinfo.isUdPanelSupported === \"function\"\n\t) {\n\t\tisTizenFhd = !window.webapis.productinfo.isUdPanelSupported();\n\t}\n\n\t/*\n\t * TODO: These values are taken directly from Jellyfin-web.\n\t * The source of them needs to be investigated.\n\t */\n\tif (isPs4()) {\n\t\treturn 8_000_000;\n\t}\n\n\tif (isXbox()) {\n\t\treturn 12_000_000;\n\t}\n\n\tif (isTizen() && isTizenFhd) {\n\t\treturn 20_000_000;\n\t}\n}\n\n/**\n * Creates a profile condition object for use in device playback profiles.\n *\n * @param Property - Value for the property\n * @param Condition - Condition that the property must comply with\n * @param Value - Value to check in the condition\n * @param IsRequired - Whether this property is required\n * @returns - Constructed ProfileCondition object\n */\nfunction createProfileCondition(\n\tProperty: ProfileConditionValue,\n\tCondition: ProfileConditionType,\n\tValue: string,\n\tIsRequired = false,\n): ProfileCondition {\n\treturn {\n\t\tCondition,\n\t\tProperty,\n\t\tValue,\n\t\tIsRequired,\n\t};\n}\n\n/**\n * Gets the AAC audio codec profile conditions\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns - Array of ACC Profile conditions\n */\nexport function getAacCodecProfileConditions(\n\tvideoTestElement: HTMLVideoElement,\n): ProfileCondition[] {\n\tconst supportsSecondaryAudio = isTizen();\n\n\tconst conditions: ProfileCondition[] = [];\n\n\t// Handle he-aac not supported\n\tif (\n\t\t!videoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"avc1.640029, mp4a.40.5\"')\n\t\t\t.replace(/no/, \"\")\n\t) {\n\t\t// TODO: This needs to become part of the stream url in order to prevent stream copy\n\t\tconditions.push(\n\t\t\tcreateProfileCondition(\n\t\t\t\tProfileConditionValue.AudioProfile,\n\t\t\t\tProfileConditionType.NotEquals,\n\t\t\t\t\"HE-AAC\",\n\t\t\t),\n\t\t);\n\t}\n\n\tif (!supportsSecondaryAudio) {\n\t\tconditions.push(\n\t\t\tcreateProfileCondition(\n\t\t\t\tProfileConditionValue.IsSecondaryAudio,\n\t\t\t\tProfileConditionType.Equals,\n\t\t\t\t\"false\",\n\t\t\t),\n\t\t);\n\t}\n\n\treturn conditions;\n}\n\n/**\n * Gets an array with all the codec profiles that this client supports\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns - Array containing the different profiles for the client\n */\nexport function getCodecProfiles(\n\tvideoTestElement: HTMLVideoElement,\n): CodecProfile[] {\n\tconst CodecProfiles: CodecProfile[] = [];\n\n\tconst aacProfileConditions = getAacCodecProfileConditions(videoTestElement);\n\n\tconst supportsSecondaryAudio = isTizen();\n\n\tif (aacProfileConditions.length > 0) {\n\t\tCodecProfiles.push({\n\t\t\tType: CodecType.VideoAudio,\n\t\t\tCodec: \"aac\",\n\t\t\tConditions: aacProfileConditions,\n\t\t});\n\t}\n\n\tif (!supportsSecondaryAudio) {\n\t\tCodecProfiles.push({\n\t\t\tType: CodecType.VideoAudio,\n\t\t\tConditions: [\n\t\t\t\tcreateProfileCondition(\n\t\t\t\t\tProfileConditionValue.IsSecondaryAudio,\n\t\t\t\t\tProfileConditionType.Equals,\n\t\t\t\t\t\"false\",\n\t\t\t\t),\n\t\t\t],\n\t\t});\n\t}\n\n\tlet maxH264Level = 42;\n\tlet h264Profiles = \"high|main|baseline|constrained baseline\";\n\n\tif (\n\t\tisTv() ||\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"avc1.640833\"')\n\t\t\t.replace(/no/, \"\")\n\t) {\n\t\tmaxH264Level = 51;\n\t}\n\n\tif (\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"avc1.640834\"')\n\t\t\t.replace(/no/, \"\")\n\t) {\n\t\tmaxH264Level = 52;\n\t}\n\n\tif (\n\t\t(isTizen() ||\n\t\t\tvideoTestElement\n\t\t\t\t.canPlayType('video/mp4; codecs=\"avc1.6e0033\"')\n\t\t\t\t.replace(/no/, \"\")) && // TODO: These tests are passing in Safari, but playback is failing\n\t\t(!isApple() || !isWebOS() || !(isEdge() && !isChromiumBased()))\n\t) {\n\t\th264Profiles += \"|high 10\";\n\t}\n\n\tlet maxHevcLevel = 120;\n\tlet hevcProfiles = \"main\";\n\tconst hevcProfilesMain10 = \"main|main 10\";\n\n\t// HEVC Main profile, Level 4.1\n\tif (\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"hvc1.1.4.L123\"')\n\t\t\t.replace(/no/, \"\") ||\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"hev1.1.4.L123\"')\n\t\t\t.replace(/no/, \"\")\n\t) {\n\t\tmaxHevcLevel = 123;\n\t}\n\n\t// HEVC Main10 profile, Level 4.1\n\tif (\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"hvc1.2.4.L123\"')\n\t\t\t.replace(/no/, \"\") ||\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"hev1.2.4.L123\"')\n\t\t\t.replace(/no/, \"\")\n\t) {\n\t\tmaxHevcLevel = 123;\n\t\thevcProfiles = hevcProfilesMain10;\n\t}\n\n\t// HEVC Main10 profile, Level 5.1\n\tif (\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"hvc1.2.4.L153\"')\n\t\t\t.replace(/no/, \"\") ||\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"hev1.2.4.L153\"')\n\t\t\t.replace(/no/, \"\")\n\t) {\n\t\tmaxHevcLevel = 153;\n\t\thevcProfiles = hevcProfilesMain10;\n\t}\n\n\t// HEVC Main10 profile, Level 6.1\n\tif (\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"hvc1.2.4.L183\"')\n\t\t\t.replace(/no/, \"\") ||\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"hev1.2.4.L183\"')\n\t\t\t.replace(/no/, \"\")\n\t) {\n\t\tmaxHevcLevel = 183;\n\t\thevcProfiles = hevcProfilesMain10;\n\t}\n\n\tconst hevcCodecProfileConditions: ProfileCondition[] = [\n\t\tcreateProfileCondition(\n\t\t\tProfileConditionValue.IsAnamorphic,\n\t\t\tProfileConditionType.NotEquals,\n\t\t\t\"true\",\n\t\t),\n\t\tcreateProfileCondition(\n\t\t\tProfileConditionValue.VideoProfile,\n\t\t\tProfileConditionType.EqualsAny,\n\t\t\thevcProfiles,\n\t\t),\n\t\tcreateProfileCondition(\n\t\t\tProfileConditionValue.VideoLevel,\n\t\t\tProfileConditionType.LessThanEqual,\n\t\t\tmaxHevcLevel.toString(),\n\t\t),\n\t];\n\n\tconst h264CodecProfileConditions: ProfileCondition[] = [\n\t\tcreateProfileCondition(\n\t\t\tProfileConditionValue.IsAnamorphic,\n\t\t\tProfileConditionType.NotEquals,\n\t\t\t\"true\",\n\t\t),\n\t\tcreateProfileCondition(\n\t\t\tProfileConditionValue.VideoProfile,\n\t\t\tProfileConditionType.EqualsAny,\n\t\t\th264Profiles,\n\t\t),\n\t\tcreateProfileCondition(\n\t\t\tProfileConditionValue.VideoLevel,\n\t\t\tProfileConditionType.LessThanEqual,\n\t\t\tmaxH264Level.toString(),\n\t\t),\n\t];\n\n\tif (!isTv()) {\n\t\th264CodecProfileConditions.push(\n\t\t\tcreateProfileCondition(\n\t\t\t\tProfileConditionValue.IsInterlaced,\n\t\t\t\tProfileConditionType.NotEquals,\n\t\t\t\t\"true\",\n\t\t\t),\n\t\t);\n\t\thevcCodecProfileConditions.push(\n\t\t\tcreateProfileCondition(\n\t\t\t\tProfileConditionValue.IsInterlaced,\n\t\t\t\tProfileConditionType.NotEquals,\n\t\t\t\t\"true\",\n\t\t\t),\n\t\t);\n\t}\n\n\tconst globalMaxVideoBitrate = (getGlobalMaxVideoBitrate() ?? \"\").toString();\n\n\tif (globalMaxVideoBitrate) {\n\t\th264CodecProfileConditions.push(\n\t\t\tcreateProfileCondition(\n\t\t\t\tProfileConditionValue.VideoBitrate,\n\t\t\t\tProfileConditionType.LessThanEqual,\n\t\t\t\tglobalMaxVideoBitrate,\n\t\t\t\ttrue,\n\t\t\t),\n\t\t);\n\t}\n\n\tif (globalMaxVideoBitrate) {\n\t\thevcCodecProfileConditions.push(\n\t\t\tcreateProfileCondition(\n\t\t\t\tProfileConditionValue.VideoBitrate,\n\t\t\t\tProfileConditionType.LessThanEqual,\n\t\t\t\tglobalMaxVideoBitrate,\n\t\t\t\ttrue,\n\t\t\t),\n\t\t);\n\t}\n\n\t// On iOS 12.x, for TS container max h264 level is 4.2\n\tif (isApple() && isMobile() && Number(safariVersion()) < 13) {\n\t\tconst codecProfile = {\n\t\t\tType: CodecType.Video,\n\t\t\tCodec: \"h264\",\n\t\t\tContainer: \"ts\",\n\t\t\tConditions: h264CodecProfileConditions.filter((condition) => {\n\t\t\t\treturn condition.Property !== \"VideoLevel\";\n\t\t\t}),\n\t\t};\n\n\t\tcodecProfile.Conditions.push(\n\t\t\tcreateProfileCondition(\n\t\t\t\tProfileConditionValue.VideoLevel,\n\t\t\t\tProfileConditionType.LessThanEqual,\n\t\t\t\t\"42\",\n\t\t\t),\n\t\t);\n\n\t\tCodecProfiles.push(codecProfile);\n\t}\n\n\tCodecProfiles.push(\n\t\t{\n\t\t\tType: CodecType.Video,\n\t\t\tCodec: \"h264\",\n\t\t\tConditions: h264CodecProfileConditions,\n\t\t},\n\t\t{\n\t\t\tType: CodecType.Video,\n\t\t\tCodec: \"hevc\",\n\t\t\tConditions: hevcCodecProfileConditions,\n\t\t},\n\t);\n\n\treturn CodecProfiles;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/helpers/fmp4-audio-formats.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport { isEdge } from \"@/utils/browser-detection\";\nimport { getSupportedAudioCodecs } from \"./audio-formats\";\nimport {\n\thasAacSupport,\n\thasAc3InHlsSupport,\n\thasAc3Support,\n\thasEac3Support,\n\thasMp3AudioSupport,\n} from \"./mp4-audio-formats\";\n\n/**\n * Gets an array with the supported fmp4 codecs\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns List of supported FMP4 audio codecs\n */\nexport function getSupportedFmp4AudioCodecs(\n\tvideoTestElement: HTMLVideoElement,\n): string[] {\n\tconst codecs = [];\n\n\tif (hasAacSupport(videoTestElement)) {\n\t\tcodecs.push(\"aac\");\n\t}\n\n\tif (hasMp3AudioSupport(videoTestElement)) {\n\t\tcodecs.push(\"mp3\");\n\t}\n\n\tif (hasAc3Support(videoTestElement) && hasAc3InHlsSupport(videoTestElement)) {\n\t\tcodecs.push(\"ac3\");\n\n\t\tif (hasEac3Support(videoTestElement)) {\n\t\t\tcodecs.push(\"eac3\");\n\t\t}\n\t}\n\n\tif (getSupportedAudioCodecs(\"flac\") && !isEdge()) {\n\t\tcodecs.push(\"flac\");\n\t}\n\n\tif (getSupportedAudioCodecs(\"alac\")) {\n\t\tcodecs.push(\"alac\");\n\t}\n\n\treturn codecs;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/helpers/fmp4-video-formats.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport {\n\tisApple,\n\tisChrome,\n\tisEdge,\n\tisFirefox,\n\tisTizen,\n\tisWebOS,\n} from \"@/utils/browser-detection\";\nimport { hasH264Support, hasHevcSupport } from \"./mp4-video-formats\";\n\n/**\n * Gets an array of supported fmp4 video codecs\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns List of supported fmp4 video codecs\n */\nexport function getSupportedFmp4VideoCodecs(\n\tvideoTestElement: HTMLVideoElement,\n): string[] {\n\tconst codecs = [];\n\n\tif (\n\t\t(isApple() || isEdge() || isTizen() || isWebOS()) &&\n\t\thasHevcSupport(videoTestElement)\n\t) {\n\t\tcodecs.push(\"hevc\");\n\t}\n\n\tif (\n\t\thasH264Support(videoTestElement) &&\n\t\t(isChrome() ||\n\t\t\tisFirefox() ||\n\t\t\tisApple() ||\n\t\t\tisEdge() ||\n\t\t\tisTizen() ||\n\t\t\tisWebOS())\n\t) {\n\t\tcodecs.push(\"h264\");\n\t}\n\n\treturn codecs;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/helpers/hls-formats.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport { isTv } from \"@/utils/browser-detection\";\nimport { getSupportedAudioCodecs } from \"./audio-formats\";\nimport { hasAacSupport, hasEac3Support } from \"./mp4-audio-formats\";\nimport { hasH264Support, hasH265Support } from \"./mp4-video-formats\";\n\n/**\n * Check if client supports AC3 in HLS stream\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if the browser has AC3 in HLS support\n */\nfunction supportsAc3InHls(\n\tvideoTestElement: HTMLVideoElement,\n): boolean | string {\n\tif (isTv()) {\n\t\treturn true;\n\t}\n\n\tif (videoTestElement.canPlayType) {\n\t\treturn (\n\t\t\tvideoTestElement\n\t\t\t\t.canPlayType('application/x-mpegurl; codecs=\"avc1.42E01E, ac-3\"')\n\t\t\t\t.replace(/no/, \"\") ||\n\t\t\tvideoTestElement\n\t\t\t\t.canPlayType(\n\t\t\t\t\t'application/vnd.apple.mpegURL; codecs=\"avc1.42E01E, ac-3\"',\n\t\t\t\t)\n\t\t\t\t.replace(/no/, \"\")\n\t\t);\n\t}\n\n\treturn false;\n}\n\n/**\n * Gets the supported HLS video codecs\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Array of video codecs supported in HLS\n */\nexport function getHlsVideoCodecs(\n\tvideoTestElement: HTMLVideoElement,\n): string[] {\n\tconst hlsVideoCodecs = [];\n\n\tif (hasH264Support(videoTestElement)) {\n\t\thlsVideoCodecs.push(\"h264\");\n\t}\n\n\tif (hasH265Support(videoTestElement) || isTv()) {\n\t\thlsVideoCodecs.push(\"h265\", \"hevc\");\n\t}\n\n\treturn hlsVideoCodecs;\n}\n\n/**\n * Gets the supported HLS audio codecs\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Array of audio codecs supported in HLS\n */\nexport function getHlsAudioCodecs(\n\tvideoTestElement: HTMLVideoElement,\n): string[] {\n\tconst hlsVideoAudioCodecs = [];\n\n\tif (supportsAc3InHls(videoTestElement)) {\n\t\thlsVideoAudioCodecs.push(\"ac3\");\n\n\t\tif (hasEac3Support(videoTestElement)) {\n\t\t\thlsVideoAudioCodecs.push(\"eac3\");\n\t\t}\n\t}\n\n\tif (hasAacSupport(videoTestElement)) {\n\t\thlsVideoAudioCodecs.push(\"aac\");\n\t}\n\n\tif (getSupportedAudioCodecs(\"opus\")) {\n\t\thlsVideoAudioCodecs.push(\"opus\");\n\t}\n\n\treturn hlsVideoAudioCodecs;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/helpers/mp4-audio-formats.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport {\n\tisTizen,\n\tisTizen4,\n\tisTizen5,\n\tisTizen55,\n\tisTv,\n\tisWebOS,\n} from \"@/utils/browser-detection\";\nimport { getSupportedAudioCodecs } from \"./audio-formats\";\nimport { hasVp8Support } from \"./mp4-video-formats\";\n\n/**\n * Checks if the client can play the AC3 codec\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if the browser has AC3 support\n */\nexport function hasAc3Support(videoTestElement: HTMLVideoElement): boolean {\n\tif (isTv()) {\n\t\treturn true;\n\t}\n\n\treturn !!videoTestElement\n\t\t.canPlayType('audio/mp4; codecs=\"ac-3\"')\n\t\t.replace(/no/, \"\");\n}\n\n/**\n * Checks if the client can play AC3 in a HLS stream\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if the browser has AC3 support\n */\nexport function hasAc3InHlsSupport(\n\tvideoTestElement: HTMLVideoElement,\n): boolean {\n\tif (isTizen() || isWebOS()) {\n\t\treturn true;\n\t}\n\n\tif (videoTestElement.canPlayType) {\n\t\treturn !!(\n\t\t\tvideoTestElement\n\t\t\t\t.canPlayType('application/x-mpegurl; codecs=\"avc1.42E01E, ac-3\"')\n\t\t\t\t.replace(/no/, \"\") ||\n\t\t\tvideoTestElement\n\t\t\t\t.canPlayType(\n\t\t\t\t\t'application/vnd.apple.mpegURL; codecs=\"avc1.42E01E, ac-3\"',\n\t\t\t\t)\n\t\t\t\t.replace(/no/, \"\")\n\t\t);\n\t}\n\n\treturn false;\n}\n\n/**\n * Checks if the cliemt has E-AC3 codec support\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if browser has EAC3 support\n */\nexport function hasEac3Support(videoTestElement: HTMLVideoElement): boolean {\n\tif (isTv()) {\n\t\treturn true;\n\t}\n\n\treturn !!videoTestElement\n\t\t.canPlayType('audio/mp4; codecs=\"ec-3\"')\n\t\t.replace(/no/, \"\");\n}\n\n/**\n * Checks if the client has AAC codec support\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if browser has AAC support\n */\nexport function hasAacSupport(videoTestElement: HTMLVideoElement): boolean {\n\treturn !!videoTestElement\n\t\t.canPlayType('video/mp4; codecs=\"avc1.640029, mp4a.40.2\"')\n\t\t.replace(/no/, \"\");\n}\n\n/**\n * Checks if the client has MP2 codec support\n *\n * @returns Determines if browser has MP2 support\n */\nexport function hasMp2AudioSupport(): boolean {\n\treturn isTv();\n}\n\n/**\n * Checks if the client has MP3 audio codec support\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if browser has Mp3 support\n */\nexport function hasMp3AudioSupport(\n\tvideoTestElement: HTMLVideoElement,\n): boolean {\n\treturn !!(\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"avc1.640029, mp4a.69\"')\n\t\t\t.replace(/no/, \"\") ||\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"avc1.640029, mp4a.6B\"')\n\t\t\t.replace(/no/, \"\") ||\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"avc1.640029, mp3\"')\n\t\t\t.replace(/no/, \"\")\n\t);\n}\n\n/**\n * Determines DTS audio support\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if browserr has DTS audio support\n */\nexport function hasDtsSupport(\n\tvideoTestElement: HTMLVideoElement,\n): boolean | string {\n\t// DTS audio not supported in 2018 models (Tizen 4.0)\n\tif (isTizen4() || isTizen5() || isTizen55()) {\n\t\treturn false;\n\t}\n\n\treturn (\n\t\tisTv() ||\n\t\tvideoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"dts-\"')\n\t\t\t.replace(/no/, \"\") ||\n\t\tvideoTestElement.canPlayType('video/mp4; codecs=\"dts+\"').replace(/no/, \"\")\n\t);\n}\n\n/**\n * Gets an array of supported MP4 codecs\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Array of supported MP4 audio codecs\n */\nexport function getSupportedMP4AudioCodecs(\n\tvideoTestElement: HTMLVideoElement,\n): string[] {\n\tconst codecs = [];\n\n\tif (hasAacSupport(videoTestElement)) {\n\t\tcodecs.push(\"aac\");\n\t}\n\n\tif (hasMp3AudioSupport(videoTestElement)) {\n\t\tcodecs.push(\"mp3\");\n\t}\n\n\tif (hasAc3Support(videoTestElement)) {\n\t\tcodecs.push(\"ac3\");\n\n\t\tif (hasEac3Support(videoTestElement)) {\n\t\t\tcodecs.push(\"eac3\");\n\t\t}\n\t}\n\n\tif (hasMp2AudioSupport()) {\n\t\tcodecs.push(\"mp2\");\n\t}\n\n\tif (hasDtsSupport(videoTestElement)) {\n\t\tcodecs.push(\"dca\", \"dts\");\n\t}\n\n\tif (isTizen() || isWebOS()) {\n\t\tcodecs.push(\"pcm_s16le\", \"pcm_s24le\");\n\t}\n\n\tif (isTizen()) {\n\t\tcodecs.push(\"aac_latm\");\n\t}\n\n\tif (getSupportedAudioCodecs(\"opus\")) {\n\t\tcodecs.push(\"opus\");\n\t}\n\n\tif (getSupportedAudioCodecs(\"flac\")) {\n\t\tcodecs.push(\"flac\");\n\t}\n\n\tif (getSupportedAudioCodecs(\"alac\")) {\n\t\tcodecs.push(\"alac\");\n\t}\n\n\tif (hasVp8Support(videoTestElement) || isTizen()) {\n\t\tcodecs.push(\"vorbis\");\n\t}\n\n\treturn codecs;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/helpers/mp4-video-formats.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport {\n\tisApple,\n\tisTizen,\n\tisTizen55,\n\tisTv,\n\tisWebOS5,\n} from \"@/utils/browser-detection\";\n\n/**\n * Checks if the client has support for the H264 codec\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if browser has H264 support\n */\nexport function hasH264Support(videoTestElement: HTMLVideoElement): boolean {\n\treturn !!videoTestElement\n\t\t.canPlayType('video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"')\n\t\t.replace(/no/, \"\");\n}\n\n/**\n * Checks if the client has support for the H265 codec\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if browser has H265 support\n */\nexport function hasH265Support(videoTestElement: HTMLVideoElement): boolean {\n\tif (isTv()) {\n\t\treturn true;\n\t}\n\n\treturn !!(\n\t\tvideoTestElement.canPlayType &&\n\t\t(videoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"hvc1.1.L120\"')\n\t\t\t.replace(/no/, \"\") ||\n\t\t\tvideoTestElement\n\t\t\t\t.canPlayType('video/mp4; codecs=\"hev1.1.L120\"')\n\t\t\t\t.replace(/no/, \"\") ||\n\t\t\tvideoTestElement\n\t\t\t\t.canPlayType('video/mp4; codecs=\"hvc1.1.0.L120\"')\n\t\t\t\t.replace(/no/, \"\") ||\n\t\t\tvideoTestElement\n\t\t\t\t.canPlayType('video/mp4; codecs=\"hev1.1.0.L120\"')\n\t\t\t\t.replace(/no/, \"\"))\n\t);\n}\n\n/**\n * Checks if the client has support for the HEVC codec\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if browser has HEVC Support\n */\nexport function hasHevcSupport(videoTestElement: HTMLVideoElement): boolean {\n\tif (isTv()) {\n\t\treturn true;\n\t}\n\n\treturn !!(\n\t\t!!videoTestElement.canPlayType &&\n\t\t(videoTestElement\n\t\t\t.canPlayType('video/mp4; codecs=\"hvc1.1.L120\"')\n\t\t\t.replace(/no/, \"\") ||\n\t\t\tvideoTestElement\n\t\t\t\t.canPlayType('video/mp4; codecs=\"hev1.1.L120\"')\n\t\t\t\t.replace(/no/, \"\") ||\n\t\t\tvideoTestElement\n\t\t\t\t.canPlayType('video/mp4; codecs=\"hvc1.1.0.L120\"')\n\t\t\t\t.replace(/no/, \"\") ||\n\t\t\tvideoTestElement\n\t\t\t\t.canPlayType('video/mp4; codecs=\"hev1.1.0.L120\"')\n\t\t\t\t.replace(/no/, \"\"))\n\t);\n}\n\n/**\n * Checks if the client has support for the AV1 codec\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if browser has AV1 support\n */\nexport function hasAv1Support(videoTestElement: HTMLVideoElement): boolean {\n\tif (\n\t\t(isTizen() && isTizen55()) ||\n\t\t(isWebOS5() && window.outerHeight >= 2160)\n\t) {\n\t\treturn true;\n\t}\n\n\treturn !!videoTestElement\n\t\t.canPlayType('video/webm; codecs=\"av01.0.15M.10\"')\n\t\t.replace(/no/, \"\");\n}\n\n/**\n * Check if the client has support for the VC1 codec\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if browser has VC1 support\n */\nfunction hasVc1Support(videoTestElement: HTMLVideoElement): boolean {\n\treturn !!(\n\t\tisTv() ||\n\t\tvideoTestElement.canPlayType('video/mp4; codecs=\"vc-1\"').replace(/no/, \"\")\n\t);\n}\n\n/**\n * Checks if the client has support for the VP8 codec\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if browser has VP8 support\n */\nexport function hasVp8Support(videoTestElement: HTMLVideoElement): boolean {\n\treturn !!videoTestElement\n\t\t.canPlayType('video/webm; codecs=\"vp8\"')\n\t\t.replace(/no/, \"\");\n}\n\n/**\n * Checks if the client has support for the VP9 codec\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if browser has VP9 support\n */\nexport function hasVp9Support(videoTestElement: HTMLVideoElement): boolean {\n\treturn !!videoTestElement\n\t\t.canPlayType('video/webm; codecs=\"vp9\"')\n\t\t.replace(/no/, \"\");\n}\n\n/**\n * Queries the platform for the codecs suppers in an MP4 container.\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Array of codec identifiers.\n */\nexport function getSupportedMP4VideoCodecs(\n\tvideoTestElement: HTMLVideoElement,\n): string[] {\n\tconst codecs = [];\n\n\tif (hasH264Support(videoTestElement)) {\n\t\tcodecs.push(\"h264\");\n\t}\n\n\tif (\n\t\thasHevcSupport(videoTestElement) && // Safari is lying on HDR and 60fps videos, use fMP4 instead\n\t\t!isApple()\n\t) {\n\t\tcodecs.push(\"hevc\");\n\t}\n\n\tif (isTv()) {\n\t\tcodecs.push(\"mpeg2video\");\n\t}\n\n\tif (hasVc1Support(videoTestElement)) {\n\t\tcodecs.push(\"vc1\");\n\t}\n\n\tif (isTizen()) {\n\t\tcodecs.push(\"msmpeg4v2\");\n\t}\n\n\tif (hasVp8Support(videoTestElement)) {\n\t\tcodecs.push(\"vp8\");\n\t}\n\n\tif (hasVp9Support(videoTestElement)) {\n\t\tcodecs.push(\"vp9\");\n\t}\n\n\tif (hasAv1Support(videoTestElement)) {\n\t\tcodecs.push(\"av1\");\n\t}\n\n\treturn codecs;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/helpers/transcoding-formats.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport {\n\tisEdge,\n\tisTizen,\n\tisTv,\n\tsupportsMediaSource,\n} from \"@/utils/browser-detection\";\n\n/**\n * Checks if the client can play native HLS\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns Determines if the browser can play native Hls\n */\nexport function canPlayNativeHls(videoTestElement: HTMLVideoElement): boolean {\n\tif (isTizen()) {\n\t\treturn true;\n\t}\n\n\treturn !!(\n\t\tvideoTestElement.canPlayType(\"application/x-mpegURL\").replace(/no/, \"\") ||\n\t\tvideoTestElement\n\t\t\t.canPlayType(\"application/vnd.apple.mpegURL\")\n\t\t\t.replace(/no/, \"\")\n\t);\n}\n\n/**\n * Determines if the browser can play Hls with Media Source Extensions\n */\nexport function canPlayHlsWithMSE(): boolean {\n\treturn supportsMediaSource();\n}\n\n/**\n * Determines if the browser can play Mkvs\n */\nexport function hasMkvSupport(videoTestElement: HTMLVideoElement): boolean {\n\tif (isTv()) {\n\t\treturn true;\n\t}\n\n\tif (\n\t\tvideoTestElement.canPlayType(\"video/x-matroska\").replace(/no/, \"\") ||\n\t\tvideoTestElement.canPlayType(\"video/mkv\").replace(/no/, \"\")\n\t) {\n\t\treturn true;\n\t}\n\n\treturn !!isEdge();\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/helpers/ts-audio-formats.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport {\n  hasAacSupport,\n  hasAc3InHlsSupport,\n  hasAc3Support,\n  hasEac3Support,\n  hasMp3AudioSupport\n} from './mp4-audio-formats';\n\n/**\n * List of supported Ts audio codecs\n */\nexport function getSupportedTsAudioCodecs(\n  videoTestElement: HTMLVideoElement\n): string[] {\n  const codecs = [];\n\n  if (hasAacSupport(videoTestElement)) {\n    codecs.push('aac');\n  }\n\n  if (hasMp3AudioSupport(videoTestElement)) {\n    codecs.push('mp3');\n  }\n\n  if (hasAc3Support(videoTestElement) && hasAc3InHlsSupport(videoTestElement)) {\n    codecs.push('ac3');\n\n    if (hasEac3Support(videoTestElement)) {\n      codecs.push('eac3');\n    }\n  }\n\n  return codecs;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/helpers/ts-video-formats.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport { hasH264Support } from './mp4-video-formats';\n\n/**\n * List of supported ts video codecs\n */\nexport function getSupportedTsVideoCodecs(\n  videoTestElement: HTMLVideoElement\n): string[] {\n  const codecs = [];\n\n  if (hasH264Support(videoTestElement)) {\n    codecs.push('h264');\n  }\n\n  return codecs;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/helpers/webm-audio-formats.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport { isWebOS } from \"@/utils/browser-detection\";\n\n/**\n * Get an array of supported codecs\n */\nexport function getSupportedWebMAudioCodecs(\n\tvideoTestElement: HTMLVideoElement,\n): string[] {\n\tconst codecs = [];\n\n\tcodecs.push(\"vorbis\");\n\n\tif (\n\t\t!isWebOS() &&\n\t\tvideoTestElement.canPlayType('audio/ogg; codecs=\"opus\"').replace(/no/, \"\")\n\t) {\n\t\tcodecs.push(\"opus\");\n\t}\n\n\treturn codecs;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/helpers/webm-video-formats.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport {\n  hasAv1Support,\n  hasVp8Support,\n  hasVp9Support\n} from './mp4-video-formats';\n\n/**\n * Get an array of supported codecs WebM video codecs\n */\nexport function getSupportedWebMVideoCodecs(\n  videoTestElement: HTMLVideoElement\n): string[] {\n  const codecs = [];\n\n  if (hasVp8Support(videoTestElement)) {\n    codecs.push('vp8');\n  }\n\n  if (hasVp9Support(videoTestElement)) {\n    codecs.push('vp9');\n  }\n\n  if (hasAv1Support(videoTestElement)) {\n    codecs.push('av1');\n  }\n\n  return codecs;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/index.ts",
    "content": "/**\n * @deprecated\n * Since we're targeting modern environments/devices only, it makes sense to switch\n * to the native MediaCapabilities API, widely supported on modern devices, but not in older.\n *\n * Given a media file, we should test with MC the compatibility of video, audio and subtitle streams\n * independently:\n * If success: Don't request transcoding and direct play that specific stream.\n * If failure: Request transcoding of the failing streams to a previously hardcoded\n * bitrate/codec combination\n *\n * For the hardcoded bitrate/codecs combination we can use what we know that are universally\n * compatible, even without testing for explicit compatibility (we can do simple checks,\n * but the more we do, the complex/less portable our solution can get).\n * Examples: H264, AAC and VTT/SASS (thanks to JASSUB).\n *\n * Other codec combinations can be hardcoded, even if they're not direct-playable in\n * most browsers (like H265 or AV1), so the few browsers that support them benefits from less bandwidth\n * usage (although this will rarely happen: The most expected situations when transcoding\n * is when the media's codecs are more \"powerful\" than what the client is capable of, and H265 is\n * pretty modern, so it would've been catched-up by MediaCapabilities. However,\n * we must take into account the playback of really old codecs like MPEG or H263,\n * whose support are probably likely going to be removed from browsers,\n * so MediaCapabilities reports as unsupported, so we would be going from an \"inferior\" codec to a\n * \"superior\" codec in this situation)\n */\n\nimport type { DeviceProfile } from '@jellyfin/sdk/lib/generated-client';\nimport { getCodecProfiles } from './helpers/codec-profiles';\nimport { getDirectPlayProfiles } from './directplay-profile';\nimport { getTranscodingProfiles } from './transcoding-profile';\nimport { getSubtitleProfiles } from './subtitle-profile';\nimport { getResponseProfiles } from './response-profile';\n\n/**\n * Creates a device profile containing supported codecs for the active Cast device.\n *\n * @param videoTestElement - Dummy video element for compatibility tests\n */\nfunction getDeviceProfile(videoTestElement: HTMLVideoElement): DeviceProfile {\n  // MaxStaticBitrate seems to be for offline sync only\n  return {\n    MaxStreamingBitrate: 120_000_000,\n    MaxStaticBitrate: 0,\n    MusicStreamingTranscodingBitrate: Math.min(120_000_000, 192_000),\n    DirectPlayProfiles: getDirectPlayProfiles(videoTestElement),\n    TranscodingProfiles: getTranscodingProfiles(videoTestElement),\n    ContainerProfiles: [],\n    CodecProfiles: getCodecProfiles(videoTestElement),\n      SubtitleProfiles: getSubtitleProfiles(),\n    //@ts-expect-error\n    ResponseProfiles: getResponseProfiles()\n  };\n}\n\nconst videoTestElement = document.createElement('video');\nconst playbackProfile = getDeviceProfile(videoTestElement);\n\nexport default playbackProfile;\n"
  },
  {
    "path": "src/utils/playback-profiles/response-profile.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport {\n  DlnaProfileType,\n  type ResponseProfile\n} from '@jellyfin/sdk/lib/generated-client';\n\n/**\n * Returns a valid ResponseProfile for the current platform.\n *\n * @returns An array of subtitle profiles for the current platform.\n */\nexport function getResponseProfiles(): ResponseProfile[] {\n  const ResponseProfiles = [];\n\n  ResponseProfiles.push({\n    Type: DlnaProfileType.Video,\n    Container: 'm4v',\n    MimeType: 'video/mp4'\n  });\n\n  return ResponseProfiles;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/subtitle-profile.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport {\n  SubtitleDeliveryMethod,\n  type SubtitleProfile\n} from '@jellyfin/sdk/lib/generated-client';\n\n/**\n * Returns a valid SubtitleProfile for the current platform.\n *\n * @returns An array of subtitle profiles for the current platform.\n */\nexport function getSubtitleProfiles(): SubtitleProfile[] {\n  const SubtitleProfiles: SubtitleProfile[] = [];\n\n  SubtitleProfiles.push(\n\t\t\t{\n\t\t\t\tFormat: \"vtt\",\n\t\t\t\tMethod: SubtitleDeliveryMethod.External,\n\t\t\t},\n\t\t\t{\n\t\t\t\tFormat: \"ass\",\n\t\t\t\tMethod: SubtitleDeliveryMethod.External,\n\t\t\t},\n\t\t\t{\n\t\t\t\tFormat: \"ssa\",\n\t\t\t\tMethod: SubtitleDeliveryMethod.External,\n\t\t\t},\n\t\t\t{\n\t\t\t\tFormat: \"pgssub\",\n\t\t\t\tMethod: SubtitleDeliveryMethod.External,\n\t\t\t},\n\t\t);\n\n  return SubtitleProfiles;\n}\n"
  },
  {
    "path": "src/utils/playback-profiles/transcoding-profile.ts",
    "content": "/**\n * @deprecated - Check @/utils/playback-profiles/index\n */\n\nimport {\n\tisAndroid,\n\tisApple,\n\tisChromiumBased,\n\tisEdge,\n\tisTizen,\n\tisTv,\n} from \"@/utils/browser-detection\";\nimport {\n\tDlnaProfileType,\n\tEncodingContext,\n\ttype TranscodingProfile,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { getSupportedAudioCodecs } from \"./helpers/audio-formats\";\nimport { getSupportedMP4AudioCodecs } from \"./helpers/mp4-audio-formats\";\nimport {\n\tgetSupportedMP4VideoCodecs,\n\thasVp8Support,\n} from \"./helpers/mp4-video-formats\";\nimport {\n\tcanPlayHlsWithMSE,\n\tcanPlayNativeHls,\n\thasMkvSupport,\n} from \"./helpers/transcoding-formats\";\nimport { getSupportedTsAudioCodecs } from \"./helpers/ts-audio-formats\";\nimport { getSupportedTsVideoCodecs } from \"./helpers/ts-video-formats\";\n\n/**\n * Returns a valid TranscodingProfile for the current platform.\n *\n * @param videoTestElement - A HTML video element for testing codecs\n * @returns An array of transcoding profiles for the current platform.\n */\nexport function getTranscodingProfiles(\n\tvideoTestElement: HTMLVideoElement,\n): TranscodingProfile[] {\n\tconst TranscodingProfiles: TranscodingProfile[] = [];\n\tconst physicalAudioChannels = isTv() ? 6 : 2;\n\n\tconst hlsBreakOnNonKeyFrames = !!(\n\t\tisApple() ||\n\t\t(isEdge() && !isChromiumBased()) ||\n\t\t!canPlayNativeHls(videoTestElement)\n\t);\n\n\tconst mp4AudioCodecs = getSupportedMP4AudioCodecs(videoTestElement);\n\tconst mp4VideoCodecs = getSupportedMP4VideoCodecs(videoTestElement);\n\tconst canPlayHls = canPlayNativeHls(videoTestElement) || canPlayHlsWithMSE();\n\n\tif (canPlayHls) {\n\t\tTranscodingProfiles.push({\n\t\t\t// Hlsjs, edge, and android all seem to require ts container\n\t\t\tContainer:\n\t\t\t\t!canPlayNativeHls(videoTestElement) ||\n\t\t\t\t(isEdge() && !isChromiumBased()) ||\n\t\t\t\tisAndroid()\n\t\t\t\t\t? \"ts\"\n\t\t\t\t\t: \"aac\",\n\t\t\tType: DlnaProfileType.Audio,\n\t\t\tAudioCodec: \"aac\",\n\t\t\tContext: EncodingContext.Streaming,\n\t\t\tProtocol: \"hls\",\n\t\t\tMaxAudioChannels: physicalAudioChannels.toString(),\n\t\t\tMinSegments: isApple() ? 2 : 1,\n\t\t\tBreakOnNonKeyFrames: hlsBreakOnNonKeyFrames,\n\t\t});\n\t}\n\n\tfor (const audioFormat of [\"aac\", \"mp3\", \"opus\", \"wav\"].filter((format) =>\n\t\tgetSupportedAudioCodecs(format),\n\t)) {\n\t\tTranscodingProfiles.push({\n\t\t\tContainer: audioFormat,\n\t\t\tType: DlnaProfileType.Audio,\n\t\t\tAudioCodec: audioFormat,\n\t\t\tContext: EncodingContext.Streaming,\n\t\t\tProtocol: \"http\",\n\t\t\tMaxAudioChannels: physicalAudioChannels.toString(),\n\t\t});\n\t}\n\n\tconst hlsInTsVideoCodecs = getSupportedTsVideoCodecs(videoTestElement);\n\tconst hlsInTsAudioCodecs = getSupportedTsAudioCodecs(videoTestElement);\n\n\tif (\n\t\tcanPlayHls &&\n\t\thlsInTsVideoCodecs.length > 0 &&\n\t\thlsInTsAudioCodecs.length > 0\n\t) {\n\t\tTranscodingProfiles.push({\n\t\t\tContainer: \"ts\",\n\t\t\tType: DlnaProfileType.Video,\n\t\t\tAudioCodec: hlsInTsAudioCodecs.join(\",\"),\n\t\t\tVideoCodec: hlsInTsVideoCodecs.join(\",\"),\n\t\t\tContext: EncodingContext.Streaming,\n\t\t\tProtocol: \"hls\",\n\t\t\tMaxAudioChannels: physicalAudioChannels.toString(),\n\t\t\tMinSegments: isApple() ? 2 : 1,\n\t\t\tBreakOnNonKeyFrames: hlsBreakOnNonKeyFrames,\n\t\t});\n\t}\n\n\tif (hasMkvSupport(videoTestElement) && !isTizen()) {\n\t\tTranscodingProfiles.push({\n\t\t\tContainer: \"mkv\",\n\t\t\tType: DlnaProfileType.Video,\n\t\t\tAudioCodec: mp4AudioCodecs.join(\",\"),\n\t\t\tVideoCodec: mp4VideoCodecs.join(\",\"),\n\t\t\tContext: EncodingContext.Streaming,\n\t\t\tMaxAudioChannels: physicalAudioChannels.toString(),\n\t\t\tCopyTimestamps: true,\n\t\t});\n\t}\n\n\tif (hasVp8Support(videoTestElement)) {\n\t\tTranscodingProfiles.push({\n\t\t\tContainer: \"webm\",\n\t\t\tType: DlnaProfileType.Video,\n\t\t\tAudioCodec: \"vorbis\",\n\t\t\tVideoCodec: \"vpx\",\n\t\t\tContext: EncodingContext.Streaming,\n\t\t\tProtocol: \"http\",\n\t\t\t/*\n\t\t\t * If audio transcoding is needed, limit channels to number of physical audio channels\n\t\t\t * Trying to transcode to 5 channels when there are only 2 speakers generally does not sound good\n\t\t\t */\n\t\t\tMaxAudioChannels: physicalAudioChannels.toString(),\n\t\t});\n\t}\n\n\treturn TranscodingProfiles;\n}\n"
  },
  {
    "path": "src/utils/queries/about.ts",
    "content": "import type { Api } from \"@jellyfin/sdk\";\nimport { getSystemApi } from \"@jellyfin/sdk/lib/utils/api/system-api\";\nimport { queryOptions } from \"@tanstack/react-query\";\nimport { check } from \"@tauri-apps/plugin-updater\";\n\nexport const getSystemInfoQueryOptions = (api: Api | null | undefined) =>\n\tqueryOptions({\n\t\tqueryKey: [\"about\", \"serverInfo\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) {\n\t\t\t\tthrow Error(\"API is not defined\");\n\t\t\t}\n\t\t\tconst result = await getSystemApi(api).getSystemInfo();\n\t\t\treturn result.data;\n\t\t},\n\t});\n\nexport const getUpdateQueryOptions = queryOptions({\n\tqueryKey: [\"about\", \"checkForUpdates\"],\n\tqueryFn: async () => await check(),\n});\n"
  },
  {
    "path": "src/utils/queries/items.ts",
    "content": "import type { Api } from \"@jellyfin/sdk\";\nimport { BaseItemKind, ItemFields } from \"@jellyfin/sdk/lib/generated-client\";\nimport { getUserLibraryApi } from \"@jellyfin/sdk/lib/utils/api/user-library-api\";\nimport { queryOptions } from \"@tanstack/react-query\";\n\n/**\n * @param id itemId\n * @param api api instance\n * @param userId user's id\n * @returns query options for the item\n * @description This function returns the query options for the item. It uses the getUserLibraryApi to get the item data.\n */\nexport const getItemQueryOptions = (\n\tid: string,\n\tapi: Api | null | undefined,\n\tuserId: string | null | undefined,\n) =>\n\tqueryOptions({\n\t\tqueryKey: [\"item\", id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) {\n\t\t\t\tthrow Error(\"API is not defined\");\n\t\t\t}\n\t\t\tif (!userId) {\n\t\t\t\tthrow Error(\"User ID is not defined\");\n\t\t\t}\n\t\t\tconst result = await getUserLibraryApi(api).getItem({\n\t\t\t\tuserId: userId,\n\t\t\t\titemId: id,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t});\n\n/**\n * @param api api instance\n * @param userId user's id\n * @returns query options for the latest items\n * @description This function returns the query options for the latest items. It uses the getUserLibraryApi to get the latest items data.\n */\nexport const getLatestItemsQueryOptions = (\n\tapi: Api | null | undefined,\n\tuserId: string | null | undefined,\n) =>\n\tqueryOptions({\n\t\tqueryKey: [\"home\", \"latestMedia\"],\n\t\tqueryFn: async () => {\n\t\t\tif (!api) {\n\t\t\t\tthrow Error(\"API is not defined\");\n\t\t\t}\n\t\t\tif (!userId) {\n\t\t\t\tthrow Error(\"User ID is not defined\");\n\t\t\t}\n\t\t\tconst media = await getUserLibraryApi(api).getLatestMedia({\n\t\t\t\tuserId: userId,\n\t\t\t\tfields: [\n\t\t\t\t\tItemFields.Overview,\n\t\t\t\t\tItemFields.ParentId,\n\t\t\t\t\tItemFields.SeasonUserData,\n\t\t\t\t\tItemFields.IsHd,\n\t\t\t\t\tItemFields.MediaStreams,\n\t\t\t\t\tItemFields.MediaSources,\n\t\t\t\t],\n\t\t\t\tincludeItemTypes: [\n\t\t\t\t\tBaseItemKind.Movie,\n\t\t\t\t\tBaseItemKind.Series,\n\t\t\t\t\tBaseItemKind.MusicAlbum,\n\t\t\t\t],\n\t\t\t\tenableUserData: true,\n\t\t\t\tenableImages: true,\n\t\t\t});\n\t\t\treturn media.data;\n\t\t},\n\t});\n"
  },
  {
    "path": "src/utils/queries/library.ts",
    "content": "import type { Api } from \"@jellyfin/sdk\";\nimport { getUserLibraryApi } from \"@jellyfin/sdk/lib/utils/api/user-library-api\";\nimport { queryOptions } from \"@tanstack/react-query\";\n\n/**\n * @param api Api instance\n * @param userId user's id\n * @param id library id\n * @returns query options for the current library\n */\nexport const getLibraryQueryOptions = (\n\tapi: Api | null | undefined,\n\tuserId: string | null | undefined,\n\tid: string,\n) =>\n\tqueryOptions({\n\t\tqueryKey: [\"library\", \"currentLib\", id],\n\t\tqueryFn: async () => {\n\t\t\tif (!api || !userId) throw Error(\"API or User ID is not defined\");\n\t\t\tconst result = await getUserLibraryApi(api).getItem({\n\t\t\t\tuserId: userId,\n\t\t\t\titemId: id,\n\t\t\t});\n\t\t\treturn result.data;\n\t\t},\n\t\tenabled: !!api && !!userId,\n\t});\n"
  },
  {
    "path": "src/utils/queries/libraryItems.ts",
    "content": "import type { Api } from \"@jellyfin/sdk\";\nimport type {\n\tBaseItemDtoQueryResult,\n\tBaseItemKind,\n\tItemSortBy,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport {\n\tItemFilter,\n\tSortOrder,\n\tVideoType,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { getArtistsApi } from \"@jellyfin/sdk/lib/utils/api/artists-api\";\nimport { getItemsApi } from \"@jellyfin/sdk/lib/utils/api/items-api\";\nimport { getPersonsApi } from \"@jellyfin/sdk/lib/utils/api/persons-api\";\nimport { getStudiosApi } from \"@jellyfin/sdk/lib/utils/api/studios-api\";\nimport { queryOptions } from \"@tanstack/react-query\";\n\nexport interface LibraryItemsParams {\n\tcurrentViewType?:\n\t\t| BaseItemKind\n\t\t| \"Artist\"\n\t\t| \"Person\"\n\t\t| \"Studio\"\n\t\t| \"MusicArtist\"\n\t\t| \"BoxSet\";\n\tsortAscending: boolean;\n\tsortBy: string; // comma separated\n\tfilters?: Record<string, boolean | undefined>;\n\tvideoTypesState?: {\n\t\tBluRay?: boolean;\n\t\tDvd?: boolean;\n\t\tIso?: boolean;\n\t\tVideoFile?: boolean;\n\t};\n\tnameStartsWith?: string;\n\tgenreIds?: string[];\n\tcollectionType?: string; // from library metadata (e.g. boxsets)\n\tlimit?: number; // optional (e.g. 0 for count-only)\n}\n\n/** Build TanStack Query options for fetching library items with unified branching logic. */\nexport const getLibraryItemsQueryOptions = (\n\tapi: Api | null | undefined,\n\tuserId: string | null | undefined,\n\tlibraryId: string | null | undefined,\n\tparams: LibraryItemsParams,\n) => {\n\treturn queryOptions<BaseItemDtoQueryResult>({\n\t\tenabled: !!params.currentViewType,\n\t\tqueryKey: [\n\t\t\t\"library\",\n\t\t\t\"items\",\n\t\t\tlibraryId,\n\t\t\t{\n\t\t\t\tcurrentViewType: params.currentViewType,\n\t\t\t\tsortAscending: params.sortAscending,\n\t\t\t\tsortBy: params.sortBy,\n\t\t\t\tfilters: params.filters,\n\t\t\t\tvideoTypesState: params.videoTypesState,\n\t\t\t\tnameStartsWith: params.nameStartsWith,\n\t\t\t\tgenreIds: params.genreIds,\n\t\t\t\tcollectionType: params.collectionType,\n\t\t\t\tlimit: params.limit,\n\t\t\t},\n\t\t],\n\t\tqueryFn: async () => {\n\t\t\tif (!api || !userId || !libraryId || !params.currentViewType) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t\"getLibraryItemsQueryOptions: Missing required parameters\",\n\t\t\t\t\t{ api, userId, libraryId, currentViewType: params.currentViewType },\n\t\t\t\t);\n\t\t\t\treturn { Items: [], TotalRecordCount: 0 } as BaseItemDtoQueryResult;\n\t\t\t}\n\t\t\tconsole.info(\"Fetching library items with params:\", params);\n\t\t\tconst { currentViewType, videoTypesState, filters } = params;\n\t\t\tconst buildVideoTypes = (state: typeof videoTypesState) => {\n\t\t\t\tconst videoTypes: VideoType[] = [];\n\t\t\t\tif (state?.BluRay) videoTypes.push(VideoType.BluRay);\n\t\t\t\tif (state?.Dvd) videoTypes.push(VideoType.Dvd);\n\t\t\t\tif (state?.Iso) videoTypes.push(VideoType.Iso);\n\t\t\t\tif (state?.VideoFile) videoTypes.push(VideoType.VideoFile);\n\t\t\t\treturn videoTypes;\n\t\t\t};\n\t\t\tconst buildFilters = (f: typeof filters) => {\n\t\t\t\tconst filtersArray: ItemFilter[] = [];\n\t\t\t\tif (f?.isPlayed) filtersArray.push(ItemFilter.IsPlayed);\n\t\t\t\tif (f?.isUnPlayed) filtersArray.push(ItemFilter.IsUnplayed);\n\t\t\t\tif (f?.isResumable) filtersArray.push(ItemFilter.IsResumable);\n\t\t\t\tif (f?.isFavorite) filtersArray.push(ItemFilter.IsFavorite);\n\t\t\t\treturn filtersArray;\n\t\t\t};\n\t\t\tlet result: { data: BaseItemDtoQueryResult };\n\t\t\tif (currentViewType === \"MusicArtist\") {\n\t\t\t\tresult = await getArtistsApi(api).getAlbumArtists({\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\tparentId: libraryId,\n\t\t\t\t\tlimit: params.limit,\n\t\t\t\t});\n\t\t\t} else if (currentViewType === \"Artist\") {\n\t\t\t\tresult = await getArtistsApi(api).getArtists({\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\tparentId: libraryId,\n\t\t\t\t\tlimit: params.limit,\n\t\t\t\t});\n\t\t\t} else if (currentViewType === \"Person\") {\n\t\t\t\tresult = await getPersonsApi(api).getPersons({\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\tpersonTypes: [\"Actor\"],\n\t\t\t\t\tlimit: params.limit,\n\t\t\t\t});\n\t\t\t} else if (currentViewType === \"Studio\") {\n\t\t\t\tresult = await getStudiosApi(api).getStudios({\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\tparentId: libraryId,\n\t\t\t\t\tlimit: params.limit,\n\t\t\t\t});\n\t\t\t} else if (currentViewType === \"BoxSet\") {\n\t\t\t\tresult = await getItemsApi(api).getItems({\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\tparentId: libraryId,\n\t\t\t\t\trecursive: params.collectionType === \"boxsets\" ? undefined : true,\n\t\t\t\t\tsortOrder: [\n\t\t\t\t\t\tparams.sortAscending ? SortOrder.Ascending : SortOrder.Descending,\n\t\t\t\t\t],\n\t\t\t\t\tsortBy: params.sortBy.split(\",\") as ItemSortBy[],\n\t\t\t\t\tfilters: buildFilters(filters),\n\t\t\t\t\thasSubtitles: filters?.hasSubtitles ? true : undefined,\n\t\t\t\t\thasTrailer: filters?.hasTrailer ? true : undefined,\n\t\t\t\t\thasSpecialFeature: filters?.hasSpecialFeature ? true : undefined,\n\t\t\t\t\thasThemeSong: filters?.hasThemeSong ? true : undefined,\n\t\t\t\t\thasThemeVideo: filters?.hasThemeVideo ? true : undefined,\n\t\t\t\t\tvideoTypes: buildVideoTypes(videoTypesState),\n\t\t\t\t\tisHd: filters?.isSD ? true : filters?.isHD || undefined,\n\t\t\t\t\tis4K: filters?.is4K || undefined,\n\t\t\t\t\tis3D: filters?.is3D || undefined,\n\t\t\t\t\tenableUserData: true,\n\t\t\t\t\tnameStartsWith: params.nameStartsWith || undefined,\n\t\t\t\t\tgenreIds:\n\t\t\t\t\t\tparams.genreIds && params.genreIds.length > 0\n\t\t\t\t\t\t\t? params.genreIds\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\tlimit: params.limit,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tresult = await getItemsApi(api).getItems({\n\t\t\t\t\tuserId: userId,\n\t\t\t\t\tparentId: libraryId,\n\t\t\t\t\trecursive: true,\n\t\t\t\t\tincludeItemTypes: [currentViewType],\n\t\t\t\t\tsortOrder: [\n\t\t\t\t\t\tparams.sortAscending ? SortOrder.Ascending : SortOrder.Descending,\n\t\t\t\t\t],\n\t\t\t\t\tsortBy: params.sortBy.split(\",\") as ItemSortBy[],\n\t\t\t\t\tfilters: buildFilters(filters),\n\t\t\t\t\thasSubtitles: filters?.hasSubtitles ? true : undefined,\n\t\t\t\t\thasTrailer: filters?.hasTrailer ? true : undefined,\n\t\t\t\t\thasSpecialFeature: filters?.hasSpecialFeature ? true : undefined,\n\t\t\t\t\thasThemeSong: filters?.hasThemeSong ? true : undefined,\n\t\t\t\t\thasThemeVideo: filters?.hasThemeVideo ? true : undefined,\n\t\t\t\t\tvideoTypes: buildVideoTypes(videoTypesState),\n\t\t\t\t\tisHd: filters?.isSD ? true : filters?.isHD || undefined,\n\t\t\t\t\tis4K: filters?.is4K || undefined,\n\t\t\t\t\tis3D: filters?.is3D || undefined,\n\t\t\t\t\tenableUserData: true,\n\t\t\t\t\tnameStartsWith: params.nameStartsWith || undefined,\n\t\t\t\t\tgenreIds:\n\t\t\t\t\t\tparams.genreIds && params.genreIds.length > 0\n\t\t\t\t\t\t\t? params.genreIds\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\tlimit: params.limit,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn result.data;\n\t\t},\n\t\tstaleTime: 5_000,\n\t\tgcTime: 5 * 60_000,\n\t});\n};\n"
  },
  {
    "path": "src/utils/reducers/videoPlayerReducer.ts",
    "content": "import { WebviewWindow } from \"@tauri-apps/api/webviewWindow\";\nimport type ReactPlayer from \"react-player\";\n\ntype VideoPlayerState = {\n\tref: React.RefObject<ReactPlayer | null>;\n\tisPlayerReady: boolean;\n\tisPlayerPlaying: boolean;\n\tisPlayerMuted: boolean;\n\tisPlayerFullscreen: boolean;\n\t/**\n\t * Volume of the player\n\t * @type {number} 0 to 1\n\t */\n\tplayerVolume: number;\n\tisSeeking: boolean;\n\tsliderSeek: number;\n\t/**\n\t * Progress of the video\n\t * @type {number} in C# ticks\n\t */\n\tprogress: number;\n\tisHovering: boolean;\n};\n\nexport enum VideoPlayerActionKind {\n\tSET_PLAYER_REF = \"SET_PLAYER_REF\",\n\tSET_PLAYER_READY = \"SET_PLAYER_READY\",\n\tSET_PLAYER_PLAYING = \"SET_PLAYER_PLAYING\",\n\tSET_PLAYER_MUTED = \"SET_PLAYER_MUTED\",\n\tSET_PLAYER_FULLSCREEN = \"SET_PLAYER_FULLSCREEN\",\n\tSET_PLAYER_VOLUME = \"SET_PLAYER_VOLUME\",\n\tSET_PLAYER_VOLUME_UP_BY_STEP = \"SET_PLAYER_VOLUME_BY_STEP\",\n\tSET_PLAYER_VOLUME_DOWN_BY_STEP = \"SET_PLAYER_VOLUME_DOWN_STEP\",\n\tSET_SEEKING = \"SET_SEEKING\",\n\tSET_SLIDER_SEEK = \"SET_SLIDER_SEEK\",\n\tSET_PROGRESS = \"SET_PROGRESS\",\n\tSET_HOVERING = \"SET_HOVERING\",\n\tTOGGLE_PLAYER_PLAYING = \"TOGGLE_PLAYER_PLAYING\",\n\tTOGGLE_PLAYER_FULLSCREEN = \"TOGGLE_PLAYER_FULLSCREEN\",\n\tTOGGLE_PLAYER_MUTED = \"TOGGLE_PLAYER_MUTED\",\n}\n\nexport interface VideoPlayerAction {\n\ttype: VideoPlayerActionKind;\n\tpayload?: number | boolean | React.RefObject<HTMLVideoElement>;\n}\n\nconst reducer = (\n\tstate: VideoPlayerState,\n\taction: VideoPlayerAction,\n): VideoPlayerState => {\n\tswitch (action.type) {\n\t\tcase VideoPlayerActionKind.SET_PLAYER_REF:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tref: action.payload as unknown as React.RefObject<ReactPlayer | null>,\n\t\t\t};\n\t\tcase VideoPlayerActionKind.SET_PLAYER_READY:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisPlayerReady: action.payload as boolean,\n\t\t\t};\n\t\tcase VideoPlayerActionKind.SET_PLAYER_PLAYING:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisPlayerPlaying: action.payload as boolean,\n\t\t\t};\n\t\tcase VideoPlayerActionKind.SET_PLAYER_MUTED:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisPlayerMuted: action.payload as boolean,\n\t\t\t};\n\t\tcase VideoPlayerActionKind.SET_PLAYER_FULLSCREEN:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisPlayerFullscreen: action.payload as boolean,\n\t\t\t};\n\t\tcase VideoPlayerActionKind.SET_PLAYER_VOLUME:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tplayerVolume: action.payload as number,\n\t\t\t};\n\t\tcase VideoPlayerActionKind.SET_SEEKING:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisSeeking: action.payload as boolean,\n\t\t\t};\n\t\tcase VideoPlayerActionKind.SET_SLIDER_SEEK:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tsliderSeek: action.payload as number,\n\t\t\t};\n\t\tcase VideoPlayerActionKind.SET_PROGRESS:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tprogress: action.payload as number,\n\t\t\t};\n\t\tcase VideoPlayerActionKind.SET_HOVERING:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisHovering: action.payload as boolean,\n\t\t\t};\n\t\tcase VideoPlayerActionKind.TOGGLE_PLAYER_PLAYING:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisPlayerPlaying: !state.isPlayerPlaying,\n\t\t\t};\n\t\tcase VideoPlayerActionKind.TOGGLE_PLAYER_FULLSCREEN: {\n\t\t\tWebviewWindow.getCurrent().setFullscreen(!state.isPlayerFullscreen);\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisPlayerFullscreen: !state.isPlayerFullscreen,\n\t\t\t};\n\t\t}\n\t\tcase VideoPlayerActionKind.TOGGLE_PLAYER_MUTED:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tisPlayerMuted: !state.isPlayerMuted,\n\t\t\t};\n\t\tcase VideoPlayerActionKind.SET_PLAYER_VOLUME_UP_BY_STEP:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tplayerVolume: Math.min(\n\t\t\t\t\t1,\n\t\t\t\t\tMath.max(0, state.playerVolume + (action.payload as number)),\n\t\t\t\t),\n\t\t\t};\n\t\tcase VideoPlayerActionKind.SET_PLAYER_VOLUME_DOWN_BY_STEP:\n\t\t\treturn {\n\t\t\t\t...state,\n\t\t\t\tplayerVolume: Math.min(\n\t\t\t\t\t1,\n\t\t\t\t\tMath.max(0, state.playerVolume - (action.payload as number)),\n\t\t\t\t),\n\t\t\t};\n\n\t\tdefault:\n\t\t\treturn state;\n\t}\n};\n\nexport default reducer;\n"
  },
  {
    "path": "src/utils/schema/librarySearch.ts",
    "content": "// Deprecated: Router search schema removed in favor of Zustand store.\n// This file will be removed in a future commit. No exports remain.\n\n"
  },
  {
    "path": "src/utils/storage/player.ts",
    "content": "import { load } from \"@tauri-apps/plugin-store\";\n\nconst PlayerStore = await load(\".player.dat\", { autoSave: true });\n\nexport const setPlayerVolume = async (type: \"audio\" | \"video\", volume: number) => {\n\tawait PlayerStore.set(`${type}.volume`, volume);\n\tawait PlayerStore.save();\n};\n\nexport const getPlayerVolume = async (type: \"audio\" | \"video\") => {\n\tconst value = await PlayerStore.get<number>(`${type}.volume`);\n\treturn value ?? 0.8;\n};\n\nexport const setPlayerMuted = async (type: \"audio\" | \"video\", muted: boolean) => {\n\tawait PlayerStore.set(`${type}.muted`, muted);\n\tawait PlayerStore.save();\n};\n\nexport const getPlayerMuted = async (type: \"audio\" | \"video\") => {\n\tconst value = await PlayerStore.get<boolean>(`${type}.muted`);\n\treturn value ?? false;\n};\n"
  },
  {
    "path": "src/utils/storage/servers.ts",
    "content": "import type { RecommendedServerInfo } from \"@jellyfin/sdk\";\nimport { load } from \"@tauri-apps/plugin-store\";\n\nexport interface ServerInfo extends RecommendedServerInfo {\n\tid: string;\n}\n\nexport interface ServerStore {\n\tdefaultServer: string | null;\n\tservers: ServerInfo[];\n}\n\nconst store = await load(\".servers.dat\", { autoSave: true });\n\n/**\n * Set server in .servers.dat\n */\nexport const setServer = async (\n\tserverId: string,\n\tserverInfo: RecommendedServerInfo,\n) => {\n\tconst servers = await getAllServers();\n\tconst newServers = servers.filter((server) => server.id !== serverId);\n\n\tnewServers.push({\n\t\t...serverInfo,\n\t\tid: serverId,\n\t});\n\n\tawait store.set(\"servers\", newServers);\n\tawait store.save();\n};\n\n/**\n * Set default server\n */\nexport const setDefaultServer = async (\n\tserverId: ServerStore[\"defaultServer\"],\n) => {\n\tawait store.set(\"defaultServer\", serverId);\n\tawait store.save();\n};\n\n/**\n * Get a Server\n * @returns {ServerInfo}\n */\nexport const getServer = async (\n\tserverId: string | null,\n): Promise<ServerInfo | undefined> => {\n\tconst servers = await getAllServers();\n\treturn servers.find((server) => server.id === serverId);\n};\n\n/**\n * Get all Servers\n */\nexport const getAllServers = async () => {\n\tconst servers = (await store.get<ServerStore[\"servers\"]>(\"servers\")) || [];\n\n\treturn servers;\n};\n\n/**\n * Get default server\n */\nexport const getDefaultServer = () => {\n\treturn store.get<ServerStore[\"defaultServer\"]>(\"defaultServer\");\n};\n\n/**\n * Delete the given server from client storage\n */\nexport const delServer = async (serverId: string) => {\n\tconst servers = await getAllServers();\n\n\tconst newServers = servers.filter((server) => server.id !== serverId);\n\n\tawait store.set(\"servers\", newServers);\n\tawait store.save();\n};\n\n/**\n * Delete the all servers from client storage\n */\nexport const delAllServer = async () => {\n\tawait store.clear();\n\t// remove the default server\n\tawait setDefaultServer(null);\n\tawait store.save();\n};\n"
  },
  {
    "path": "src/utils/storage/settings.ts",
    "content": "import { load } from \"@tauri-apps/plugin-store\";\n\nconst SettingsStore = await load(\".settings.dat\", { autoSave: true });\n\n/**\n * settingKey should be of type:\n * 'settingDomain.settingKeyName'\n */\n\nexport const setSetting = async (settingKey: string, value: boolean) => {\n\tawait SettingsStore.set(settingKey, value);\n\tawait SettingsStore.save();\n};\n\nexport const getSetting = async (settingKey: string) => {\n\tconst value = await SettingsStore.get(settingKey);\n\tconsole.log(`${settingKey} - ${value}`);\n\tif (value) return value;\n\treturn false;\n};\n\nexport const allSettings = {\n\tgeneral: [\n\t\t{\n\t\t\tkey: \"general.enable_skip_intro_outro\",\n\t\t\tname: \"Enable Intro-Skipper plugin\",\n\t\t\tdescription:\n\t\t\t\t\"Shows a skip button for Intros and End Credit scenes in an episode. Note: this requires jumoog/intro-skipper plugin to be installed on server\",\n\t\t},\n\t],\n};"
  },
  {
    "path": "src/utils/storage/user.ts",
    "content": "import { load } from \"@tauri-apps/plugin-store\";\n\nexport interface UserStore {\n\t\tuser: {\n\t\t\tName: string;\n\t\t\tAccessToken: string;\n\t\t\tId: string;\n\t\t};\n\t}\n\nconst user = await load(\".user.dat\", { autoSave: true });\n\n/**\n * Set User details to .user.dat\n */\nconst saveUser = async (\n\tuserName: string,\n\taccessToken: string,\n\tuserId: string,\n) => {\n\tuser.set(\"user\", {\n\t\tName: userName,\n\t\tAccessToken: accessToken,\n\t\tId: userId,\n\t});\n\n\tawait user.save();\n};\n\n/**\n * Get saved user fro .user.dat\n * @return {object}\n */\nconst getUser = async () => {\n\treturn user.get<UserStore[\"user\"]>(\"user\");\n};\n\n/**\n * Delete user from storage\n */\nconst delUser = async () => {\n\tsessionStorage.removeItem(\"accessToken\");\n\tawait user.clear();\n\tawait user.save();\n};\n\nexport { saveUser, getUser, delUser };\n"
  },
  {
    "path": "src/utils/store/api.tsx",
    "content": "import { type Api, Jellyfin } from \"@jellyfin/sdk\";\nimport React, { type ReactNode } from \"react\";\nimport { createContext, useContext, useState } from \"react\";\nimport { type StoreApi, createStore } from \"zustand\";\nimport { shallow } from \"zustand/shallow\";\nimport { useStoreWithEqualityFn } from \"zustand/traditional\";\nimport { version as appVer } from \"../../../package.json\";\nimport { getDefaultServer, getServer } from \"../storage/servers\";\nimport { getUser } from \"../storage/user\";\n\n// Initial custom axios client to use tauri's http module\nimport axios from \"axios\";\n\ntype ApiStore = {\n\tapi: Api | undefined;\n\tdeviceId: string | null;\n\tjellyfin: Jellyfin;\n\tcreateApi: (serverAddress: string, accessToken?: string | undefined) => void;\n};\n\n/**\n * @deprecated\n */\nexport const axiosClient = axios.create({\n\t// adapter: axiosTauriApiAdapter,\n\theaders: {\n\t\t\"Access-Control-Allow-Origin\": \"*\",\n\t},\n\ttimeout: 60000,\n});\n\nconst deviceId = localStorage.getItem(\"deviceId\") || crypto.randomUUID();\n\nif (!localStorage.getItem(\"deviceId\")) {\n\tlocalStorage.setItem(\"deviceId\", deviceId);\n}\n\nexport const jellyfin = new Jellyfin({\n\tclientInfo: {\n\t\tname: \"Blink\",\n\t\tversion: appVer,\n\t},\n\tdeviceInfo: {\n\t\tname: \"Blink\",\n\t\tid: deviceId,\n\t},\n});\n\nexport const initializeApi = async () => {\n\tconst currentServerId = await getDefaultServer();\n\tif (currentServerId) {\n\t\tconst currentServer = await getServer(currentServerId);\n\t\tif (currentServer?.address) {\n\t\t\tconst userOnDisk = await getUser();\n\t\t\tif (userOnDisk) {\n\t\t\t\treturn jellyfin.createApi(\n\t\t\t\t\tcurrentServer.address,\n\t\t\t\t\tuserOnDisk.AccessToken,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn jellyfin.createApi(currentServer.address);\n\t\t}\n\t}\n\treturn undefined;\n};\n\nexport const ApiContext = createContext<StoreApi<ApiStore>>(undefined!);\n\nexport const ApiProvider = ({\n\tchildren,\n\tinitialApi,\n}: { children: ReactNode; initialApi?: Api }) => {\n\tconst [store] = useState(() =>\n\t\tcreateStore<ApiStore>()((set) => ({\n\t\t\tapi: initialApi,\n\t\t\tdeviceId: deviceId,\n\t\t\tjellyfin: jellyfin,\n\t\t\tcreateApi: (serverAddress, accessToken?) =>\n\t\t\t\tset((state) => {\n\t\t\t\t\tconst apiTemp = state.jellyfin.createApi(serverAddress, accessToken);\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...state,\n\t\t\t\t\t\tapi: apiTemp,\n\t\t\t\t\t};\n\t\t\t\t}),\n\t\t})),\n\t);\n\treturn <ApiContext.Provider value={store}>{children}</ApiContext.Provider>;\n};\n\nexport function useApiInContext<T>(selector?: (state: ApiStore) => T) {\n\tconst store = useContext(ApiContext);\n\tif (!store) {\n\t\tthrow new Error(\"Missing ApiProvider\");\n\t}\n\treturn useStoreWithEqualityFn(store, selector!, shallow);\n}"
  },
  {
    "path": "src/utils/store/audioPlayback.ts",
    "content": "import type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\n\nimport type React from \"react\";\nimport { shallow } from \"zustand/shallow\";\nimport { createWithEqualityFn } from \"zustand/traditional\";\nimport playbackProfile from \"../playback-profiles\";\nimport {\n\tgetPlayerMuted,\n\tgetPlayerVolume,\n\tsetPlayerMuted,\n\tsetPlayerVolume,\n} from \"../storage/player\";\n\ntype AudioPlaybackStore = {\n\tdisplay: boolean;\n\turl: string;\n\titem: BaseItemDto | undefined | null;\n\tplaylistItemId: string | undefined;\n\tplayer: {\n\t\tcurrentTick: number;\n\t\tplaying: boolean;\n\t\tref: React.RefObject<HTMLAudioElement> | null;\n\t\tvolume: number;\n\t\tisMuted: boolean;\n\t};\n};\nexport const useAudioPlayback = createWithEqualityFn<AudioPlaybackStore>(\n\t() => ({\n\t\tdisplay: false,\n\t\turl: \"\",\n\t\titem: undefined,\n\t\tplaylistItemId: undefined,\n\t\tplayer: {\n\t\t\tcurrentTick: 0,\n\t\t\tplaying: false,\n\t\t\tref: null,\n\t\t\tvolume: 0.8,\n\t\t\tisMuted: false,\n\t\t},\n\t}),\n\tshallow,\n);\n\n// Initialize volume from storage\n(async () => {\n\tconst volume = await getPlayerVolume(\"audio\");\n\tconst isMuted = await getPlayerMuted(\"audio\");\n\tuseAudioPlayback.setState((state) => ({\n\t\tplayer: { ...state.player, volume, isMuted },\n\t}));\n})();\n\nexport const playAudio = (\n\turl: string,\n\titem: BaseItemDto | undefined | null,\n\tplaylistItemId?: string | undefined,\n): void => {\n\tuseAudioPlayback.setState({\n\t\tdisplay: true,\n\t\turl,\n\t\titem,\n\t\tplaylistItemId,\n\t});\n};\n\nexport const generateAudioStreamUrl = (\n\titemId: string,\n\tuserId: string,\n\tdeviceId: string,\n\tbasePath: string,\n\tapi_key: string,\n) => {\n\tconst transcodingProfile = playbackProfile.TranscodingProfiles?.filter(\n\t\t(val) => val.Type === \"Audio\" && val.Context === \"Streaming\",\n\t)[0];\n\n\tlet directPlayContainers = \"\";\n\tif (playbackProfile.DirectPlayProfiles) {\n\t\tfor (const p of playbackProfile.DirectPlayProfiles) {\n\t\t\tif (p.Type === \"Audio\") {\n\t\t\t\tif (directPlayContainers) {\n\t\t\t\t\tdirectPlayContainers += `, ${p.Container}`;\n\t\t\t\t} else {\n\t\t\t\t\tdirectPlayContainers = p.Container ?? \"\";\n\t\t\t\t}\n\n\t\t\t\tif (p.AudioCodec) {\n\t\t\t\t\tdirectPlayContainers += `| ${p.AudioCodec}`;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tconst urlOptions = {\n\t\tuserId,\n\t\tdeviceId,\n\t\tapi_key,\n\t\tcontainer: directPlayContainers,\n\t\tmaxAudioChannels: transcodingProfile?.MaxAudioChannels,\n\t\ttranscodingContainer: transcodingProfile?.Container,\n\t\ttranscodingProtocol: transcodingProfile?.Protocol,\n\t\taudioCodec: transcodingProfile?.AudioCodec,\n\t\tplaySessionId: Date.now(),\n\t\tstartTimeTicks: 0,\n\t\tenableRemoteMedia: false,\n\t\tenableAudioVbrEncoding: true,\n\t};\n\tconst urlParams = new URLSearchParams(urlOptions as any).toString();\n\treturn `${basePath}/Audio/${itemId}/universal?${urlParams}`;\n};\n\nexport const setAudioRef = (ref: React.RefObject<HTMLAudioElement>) => {\n\tconst state = useAudioPlayback.getState();\n\tstate.player.ref = ref;\n\tuseAudioPlayback.setState(state);\n};\n\nexport const setProgress = (ticks: number) => {\n\tconst state = useAudioPlayback.getState();\n\tstate.player.currentTick = ticks;\n\tuseAudioPlayback.setState(state);\n};\n\nexport const setVolume = (volume: number) => {\n\tconst state = useAudioPlayback.getState();\n\tstate.player.volume = volume;\n\tuseAudioPlayback.setState(state);\n\tsetPlayerVolume(\"audio\", volume);\n};\n\nexport const setIsMuted = (isMuted: boolean) => {\n\tconst state = useAudioPlayback.getState();\n\tstate.player.isMuted = isMuted;\n\tuseAudioPlayback.setState(state);\n\tsetPlayerMuted(\"audio\", isMuted);\n};\n\nexport const stopPlayback = () => {\n\tconst current = useAudioPlayback.getState();\n\tuseAudioPlayback.setState({\n\t\tdisplay: false,\n\t\turl: \"\",\n\t\titem: undefined,\n\t\tplaylistItemId: undefined,\n\t\tplayer: {\n\t\t\tcurrentTick: 0,\n\t\t\tplaying: false,\n\t\t\tref: null,\n\t\t\tvolume: current.player.volume,\n\t\t\tisMuted: current.player.isMuted,\n\t\t},\n\t});\n};"
  },
  {
    "path": "src/utils/store/backdrop.tsx",
    "content": "import { create } from \"zustand\";\n\ntype BackdropStore = {\n\tbackdropHash: string;\n\t// backdropId: string;\n\tsetBackdrop: (hash: string | undefined) => void;\n};\n\nexport const useBackdropStore = create<BackdropStore>((set) => ({\n\tbackdropHash: \"\",\n\t// backdropI: \"\",\n\tsetBackdrop: (hash) => set(() => ({ backdropHash: hash })),\n}));\n"
  },
  {
    "path": "src/utils/store/carousel.ts",
    "content": "import { shallow } from \"zustand/shallow\";\nimport { createWithEqualityFn } from \"zustand/traditional\";\n\ntype CarouselStore = {\n\tdirection: \"left\" | \"right\";\n\tsetDirection: (dir: \"left\" | \"right\") => void;\n};\nexport const useCarouselStore = createWithEqualityFn<CarouselStore>(\n\t(set) => ({\n\t\tdirection: \"right\",\n\t\tsetDirection: (dir) => set(() => ({ direction: dir })),\n\t}),\n\tshallow,\n);\n"
  },
  {
    "path": "src/utils/store/central.tsx",
    "content": "import type { Api } from \"@jellyfin/sdk\";\nimport type { UserDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport { getUserApi } from \"@jellyfin/sdk/lib/utils/api/user-api\";\nimport React, {\n\tcreateContext,\n\ttype ReactNode,\n\tuseContext,\n\tuseState,\n} from \"react\";\nimport { createStore, type StoreApi } from \"zustand\";\nimport { shallow } from \"zustand/shallow\";\nimport { useStoreWithEqualityFn } from \"zustand/traditional\";\nimport { version } from \"../../../package.json\";\nimport {\n\tgetAllServers,\n\tgetDefaultServer,\n\tgetServer,\n\ttype ServerInfo,\n} from \"../storage/servers\";\nimport { getUser } from \"../storage/user\";\n\ntype CentralStore = {\n\tdefaultServerOnDisk: () => Promise<string | null>;\n\tdefaultServerInfoOnDisk: () => Promise<ServerInfo | undefined>;\n\tallServersOnDisk: () => Promise<ServerInfo[]>;\n\tuserOnDisk: () => Promise<{\n\t\tName: string;\n\t\tAccessToken: string;\n\t\tId: string;\n\t} | null>;\n\tclientVersion: string;\n\tcurrentUser: null | UserDto;\n\tfetchCurrentUser: (api: Api | undefined) => Promise<void>;\n\tresetCurrentUser: () => void;\n};\n\n/**\n * Sets app inital route\n * @deprecated\n */\nexport const setInitialRoute = () => {\n\t// useCentralStore.setState((state) => ({ ...state, initialRoute: route }));\n};\n\n/**\n * @deprecated\n */\nexport const setAppReady = () => {\n\t// useCentralStore.setState((state) => ({ ...state, appReady }));\n};\n\nconst CentralContext = createContext<StoreApi<CentralStore>>(null!);\n\nexport const CentralProvider = ({ children }: { children: ReactNode }) => {\n\tconst [store] = useState(() =>\n\t\tcreateStore<CentralStore>()((set) => ({\n\t\t\tdefaultServerOnDisk: async () => await getDefaultServer(),\n\t\t\tdefaultServerInfoOnDisk: async () => {\n\t\t\t\tconst a = await getDefaultServer();\n\t\t\t\treturn await getServer(a);\n\t\t\t},\n\t\t\tallServersOnDisk: async () => await getAllServers(),\n\t\t\tuserOnDisk: async () => await getUser(),\n\t\t\tclientVersion: version,\n\t\t\tcurrentUser: null,\n\t\t\tfetchCurrentUser: async (api) => {\n\t\t\t\tif (api?.accessToken) {\n\t\t\t\t\tconst user = (await getUserApi(api).getCurrentUser()).data;\n\t\t\t\t\tset((s) => ({ ...s, currentUser: user }));\n\t\t\t\t}\n\t\t\t},\n\t\t\tresetCurrentUser: () => {\n\t\t\t\tset((s) => ({ ...s, currentUser: null }));\n\t\t\t},\n\t\t})),\n\t);\n\treturn (\n\t\t<CentralContext.Provider value={store}>{children}</CentralContext.Provider>\n\t);\n};\n\nexport function useCentralStore<T>(selector?: (state: CentralStore) => T) {\n\tconst store = useContext(CentralContext);\n\tif (!store) {\n\t\tthrow new Error(\"Missing CentralProvider\");\n\t}\n\treturn useStoreWithEqualityFn(store, selector!, shallow);\n}"
  },
  {
    "path": "src/utils/store/drawer.ts",
    "content": "import { shallow } from \"zustand/shallow\";\nimport { createWithEqualityFn } from \"zustand/traditional\";\n\ntype DrawerStore = {\n\topen: boolean;\n\tsetOpen: (sopen: boolean) => void;\n};\n\nexport const useDrawerStore = createWithEqualityFn<DrawerStore>(\n\t(set) => ({\n\t\topen: false,\n\t\tsetOpen: (sopen) => {\n\t\t\tset(() => ({ open: sopen }));\n\t\t},\n\t}),\n\tshallow,\n);\n"
  },
  {
    "path": "src/utils/store/header.ts",
    "content": "import { create } from \"zustand\";\n\ntype HeaderStoreType = {\n\tpageTitle: string | null;\n\tsetPageTitle: (title: string | null) => void;\n};\n\nconst useHeaderStore = create<HeaderStoreType>((set) => ({\n\tpageTitle: null,\n\tsetPageTitle: (title: string | null) => set(() => ({ pageTitle: title })),\n}));\n\nexport default useHeaderStore;\n"
  },
  {
    "path": "src/utils/store/libraryDraft.ts",
    "content": "// Deprecated: Draft store replaced by unified library state store in Zustand.\n// This file will be removed in a future commit. No exports remain.\n\nexport {}; // ensure this is a module with no exports\n"
  },
  {
    "path": "src/utils/store/libraryState.ts",
    "content": "import type { BaseItemKind } from \"@jellyfin/sdk/lib/generated-client\";\nimport { create } from \"zustand\";\nimport { createJSONStorage, persist } from \"zustand/middleware\";\n\nexport type VideoTypesState = {\n\tBluRay?: boolean;\n\tDvd?: boolean;\n\tIso?: boolean;\n\tVideoFile?: boolean;\n};\n\nexport type FiltersState = Record<string, boolean | undefined>;\n\nexport interface LibraryStateSlice {\n\tcurrentViewType?: BaseItemKind | \"Artist\";\n\tsortBy: string; // comma joined for multi-key\n\tsortAscending: boolean;\n\tnameStartsWith?: string;\n\tgenreIds: string[];\n\tfilters: FiltersState;\n\tvideoTypesState: VideoTypesState;\n\tlibraryName?: string;\n\titemsTotalCount?: number;\n}\n\nexport interface LibraryStateStore {\n\tlibraries: Record<string, LibraryStateSlice>;\n\tinitLibrary: (\n\t\tlibraryId: string,\n\t\tdefaults: Partial<LibraryStateSlice>,\n\t) => void;\n\tupdateLibrary: (\n\t\tlibraryId: string,\n\t\tpartial: Partial<LibraryStateSlice>,\n\t) => void;\n\tresetLibrary: (libraryId: string) => void;\n}\n\nconst defaultSlice: LibraryStateSlice = {\n\tcurrentViewType: undefined,\n\tsortBy: \"Name\",\n\tsortAscending: true,\n\tnameStartsWith: undefined,\n\tgenreIds: [],\n\tfilters: {},\n\tvideoTypesState: {},\n\tlibraryName: undefined,\n\titemsTotalCount: undefined,\n};\n\nexport const useLibraryStateStore = create<LibraryStateStore>()(\n\tpersist(\n\t\t(set) => ({\n\t\t\tlibraries: {},\n\t\t\tinitLibrary: (libraryId, defaults) =>\n\t\t\t\tset((s) => {\n\t\t\t\t\tif (s.libraries[libraryId]) return s; // already initialized\n\t\t\t\t\treturn {\n\t\t\t\t\t\tlibraries: {\n\t\t\t\t\t\t\t...s.libraries,\n\t\t\t\t\t\t\t[libraryId]: { ...defaultSlice, ...defaults },\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}),\n\t\t\tupdateLibrary: (libraryId, partial) =>\n\t\t\t\tset((s) => ({\n\t\t\t\t\tlibraries: {\n\t\t\t\t\t\t...s.libraries,\n\t\t\t\t\t\t[libraryId]: {\n\t\t\t\t\t\t\t...(s.libraries[libraryId] || defaultSlice),\n\t\t\t\t\t\t\t...partial,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t})),\n\t\t\tresetLibrary: (libraryId) =>\n\t\t\t\tset((s) => ({\n\t\t\t\t\tlibraries: { ...s.libraries, [libraryId]: { ...defaultSlice } },\n\t\t\t\t})),\n\t\t}),\n\t\t{\n\t\t\tname: \"blink-library-state\",\n\t\t\tstorage: createJSONStorage(() => sessionStorage),\n\t\t\tpartialize: (state) => ({ libraries: state.libraries }),\n\t\t\tversion: 1,\n\t\t},\n\t),\n);\n"
  },
  {
    "path": "src/utils/store/photosPlayback.ts",
    "content": "import type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport { immer } from \"zustand/middleware/immer\";\nimport { shallow } from \"zustand/shallow\";\nimport { createWithEqualityFn } from \"zustand/traditional\";\n\ntype PhotosPlaybackStore = {\n\tphotos: BaseItemDto[] | null;\n\tindex: number;\n\tplayPhotos: (photos: BaseItemDto[], index: number) => void;\n};\n\n\nexport const usePhotosPlayback = createWithEqualityFn<PhotosPlaybackStore>()(\n\timmer((set) => ({\n\t\tphotos: null,\n\t\tplayPhotos: (photos, index) =>\n\t\t\tset((state) => {\n\t\t\t\tstate.photos = photos;\n\t\t\t\tstate.index = index;\n\t\t\t}),\n\t\tindex: 0,\n\t})),\n\tshallow,\n);"
  },
  {
    "path": "src/utils/store/playback.ts",
    "content": "import type { Api } from \"@jellyfin/sdk\";\nimport {\n\ttype BaseItemDto,\n\ttype MediaSegmentDtoQueryResult,\n\ttype MediaStream,\n\ttype PlaybackInfoResponse,\n\tPlayMethod,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport { getMediaInfoApi } from \"@jellyfin/sdk/lib/utils/api/media-info-api\";\nimport { getMediaSegmentsApi } from \"@jellyfin/sdk/lib/utils/api/media-segments-api\";\nimport { getPlaystateApi } from \"@jellyfin/sdk/lib/utils/api/playstate-api\";\nimport { getUserApi } from \"@jellyfin/sdk/lib/utils/api/user-api\";\nimport { WebviewWindow as appWindow } from \"@tauri-apps/api/webviewWindow\";\nimport { create } from \"zustand\";\nimport { immer } from \"zustand/middleware/immer\";\nimport { shallow } from \"zustand/shallow\";\nimport { createWithEqualityFn } from \"zustand/traditional\";\nimport { secToTicks, ticksToSec } from \"../date/time\";\nimport getSubtitle from \"../methods/getSubtitles\";\nimport playbackProfile from \"../playback-profiles\";\nimport {\n\tgetPlayerMuted,\n\tgetPlayerVolume,\n\tsetPlayerMuted,\n\tsetPlayerVolume,\n} from \"../storage/player\";\nimport type audioPlaybackInfo from \"../types/audioPlaybackInfo\";\nimport type subtitlePlaybackInfo from \"../types/subtitlePlaybackInfo\";\nimport { generateAudioStreamUrl, playAudio } from \"./audioPlayback\";\nimport useQueue, { setQueue } from \"./queue\";\n\ntype PlaybackStoreState = {\n\tmediaSource: {\n\t\tvideoTrack: number;\n\t\taudioTrack: number;\n\t\tcontainer: string;\n\t\tid: string | undefined;\n\t\tsubtitle: subtitlePlaybackInfo;\n\t\taudio: audioPlaybackInfo;\n\t\tbitrate?: number;\n\t\tvideoCodec?: string;\n\t\taudioCodec?: string;\n\t\tplayMethod?: PlayMethod;\n\t\ttranscodingUrl?: string;\n\t};\n\tplaybackStream: string;\n\tplaysessionId: string | undefined | null;\n\tmetadata: {\n\t\titemName: string;\n\t\tepisodeTitle?: string;\n\t\tisEpisode: boolean;\n\t\t/**\n\t\t * Duration of the item in C# ticks\n\t\t * This is used to calculate the progress bar and the total duration of the item\n\t\t */\n\t\titemDuration: number;\n\t\titem: BaseItemDto;\n\t\tmediaSegments?: MediaSegmentDtoQueryResult;\n\t\tuserDataLastPlayedPositionTicks?: number;\n\t};\n\tplayerState: {\n\t\t/**\n\t\t * Persistent volume level between player instances\n\t\t */\n\t\tvolume: number;\n\t\t/**\n\t\t * Prop for ReactPlayer to mute and unmute audio volume regardless of the volume value\n\t\t */\n\t\tisPlayerMuted: boolean;\n\t\tisPlayerPlaying: boolean;\n\t\t/**\n\t\t * Used to check if ReactPlayer component has finished initial fetch of playbackStream\n\t\t * true -> Player is ready for playback\n\t\t * false -> Player is not ready for playback/is still fetching data/possible error while fetching\n\t\t */\n\t\tisPlayerReady: boolean;\n\t\tisBuffering: boolean;\n\t\t/**\n\t\t * Current playback time in C# ticks\n\t\t * This is used to update the playback time in the UI\n\t\t * Initially should be set to UserData.PlaybackPositionTicks in seconds\n\t\t * Source: ReactPlayer's onProgress event\n\t\t * */\n\t\tcurrentTime: number;\n\t\t/**\n\t\t * Used to toggle the fullscreen state of the player\n\t\t * This is used to toggle the fullscreen state of the player\n\t\t * This should be set to true when the user clicks on the fullscreen button\n\t\t */\n\t\tisPlayerFullscreen: boolean;\n\t\t/**\n\t\t * Used to see if user is seeking/scrubbing progress bar\n\t\t * Used to temporarily switch to seek progress for the progress bar\n\t\t */\n\t\tisUserSeeking: boolean;\n\t\t/**\n\t\t * Seek value\n\t\t * This value should be in C# ticks\n\t\t */\n\t\tseekValue: number;\n\t\t/**\n\t\t * Used to display controls/see if user is hovering the media player\n\t\t */\n\t\tisUserHovering: boolean;\n\t\t/**\n\t\t * Used to show the buffering spinner in the UI\n\t\t * This should be set to true when the player is buffering\n\t\t */\n\t\tisLoading: boolean;\n\t\t/**\n\t\t * Used to show the stats for nerds dialog\n\t\t */\n\t\tshowStatsForNerds: boolean;\n\t};\n\t/**\n\t * Index of the next segment to be played\n\t */\n\tnextSegmentIndex: number;\n\t/**\n\t * Id of the active segment\n\t * Set if null if no segment is active\n\t */\n\tactiveSegmentId: string | null;\n\t/**\n\t * User id of the user who is currently playing the item\n\t */\n\tuserId: string;\n\t/**\n\t * Volume change indicator\n\t * This is used to show the volume change indicator in the UI\n\t */\n\tisVolumeInidcatorVisible: boolean;\n\tvolumeIndicatorVisibleTimeoutId: NodeJS.Timeout | null;\n};\n\ntype PlaybackStoreActions = {\n\t// Player state related actions\n\t/**\n\t * Set the volume of the player\n\t * This is used to set the volume of the player\n\t * This should be set to a value between 0 and 1\n\t */\n\tsetVolume: (volume: number) => void;\n\t/**\n\t * Increase the volume of the player by a step\n\t * This is used to increase the volume of the player by a step\n\t * This should be set to a value between 0 and 1\n\t */\n\tincreaseVolumeByStep: (step: number) => void;\n\t/**\n\t * Decrease the volume of the player by a step\n\t * This is used to decrease the volume of the player by a step\n\t * This should be set to a value between 0 and 1\n\t */\n\tdecreaseVolumeByStep: (step: number) => void;\n\t/**\n\t * Toggle the player state between playing and paused\n\t * This is used to toggle the player state\n\t */\n\ttoggleIsPlaying: () => void;\n\t/**\n\t * Set the player state to playing\n\t */\n\tsetIsPlaying: (isPlaying: boolean) => void;\n\t/**\n\t * Set the player state to buffering\n\t * This is used to show the buffering spinner in the UI\n\t * This should be set to true when the player is buffering\n\t */\n\tsetIsBuffering: (isBuffering: boolean) => void;\n\t/**\n\t * Set the current time of the player\n\t */\n\tsetCurrentTime: (currentTime: number) => void;\n\t/**\n\t * Set the player state to ready\n\t * This is used to check if the player is ready for playback\n\t * This should be set to true when the player has finished fetching the playback stream\n\t */\n\tsetPlayerReady: (isPlayerReady: boolean) => void;\n\t/**\n\t * Toggle the player state to fullscreen\n\t * This is used to toggle the fullscreen state of the player\n\t * This should be set to true when the user clicks on the fullscreen button\n\t */\n\ttoggleIsPlayerFullscreen: () => void;\n\t/**\n\t * Set the user hover state\n\t * This is used to show/hide the player controls\n\t * This should be set to true when the user hovers over the player\n\t * This should be set to false when the user stops hovering over the player\n\t */\n\tsetIsUserHovering: (isUserHovering: boolean) => void;\n\t/**\n\t * Set the user seeking state\n\t * This is used to temporarily switch to seek progress for the progress bar\n\t * This should be set to true when the user is seeking/scrubbing the progress bar\n\t */\n\tsetIsUserSeeking: (isUserSeeking: boolean) => void;\n\t/**\n\t * Set the seek value\n\t * This is used to update the seek value in the UI\n\t * This should be set to the value of the progress bar when the user is seeking/scrubbing\n\t * This value should be in C# ticks\n\t */\n\tsetSeekValue: (seekValue: number) => void;\n\t/**\n\t * Toggle the player muted state\n\t * This is used to toggle the player muted state\n\t */\n\ttoggleIsPlayerMuted: () => void;\n\t/**\n\t * Set the loading state of the player\n\t * This is used to show/hide the loading spinner in the UI\n\t * This should be set to true when the player is loading\n\t * This should be set to false when the player has finished loading\n\t */\n\tsetIsLoading: (isLoading: boolean) => void;\n\t/**\n\t * Toggle the stats for nerds dialog\n\t */\n\ttoggleShowStatsForNerds: () => void;\n\tsetPlayerState: (playerState: PlaybackStoreState[\"playerState\"]) => void;\n\n\t// Playback information related actions\n\tsetMetadata: (\n\t\titemName: string,\n\t\tepisodeTitle: string,\n\t\tisEpisode: boolean,\n\t\titemDuration: number,\n\t\titem: BaseItemDto,\n\t\tmediaSegments?: MediaSegmentDtoQueryResult,\n\t\tuserDataLastPlayedPositionTicks?: number,\n\t) => void;\n\tsetMediaSource: (\n\t\tvideoTrack: number,\n\t\taudioTrack: number,\n\t\tcontainer: string,\n\t\tid: string | undefined,\n\t\tsubtitle: subtitlePlaybackInfo,\n\t\taudio: audioPlaybackInfo,\n\t) => void;\n\tsetPlaybackStream: (playbackStream: string) => void;\n\tsetPlaysessionId: (playsessionId: string | undefined | null) => void;\n\tsetUserId: (userId: string) => void;\n\tsetActiveSegment: (segmentIndex: number) => void;\n\tclearActiveSegment: () => void;\n\n\t/**\n\t * Trigger volume change indicator\n\t * This is used to show the volume change indicator in the UI\n\t */\n\ttiggerVolumeIndicator: () => void;\n\n\tinitializeVolume: () => Promise<void>;\n\n\t// ReactPlayer related actions\n\t_playerActions: {\n\t\tseekTo: (seconds: number) => void;\n\t\tgetCurrentTime: () => number;\n\t};\n\tregisterPlayerActions: (\n\t\tactions: PlaybackStoreActions[\"_playerActions\"],\n\t) => void;\n\tseekTo: (seconds: number) => void;\n\tseekForward: (seconds: number) => void;\n\tseekBackward: (seconds: number) => void;\n\tgetCurrentTime: () => number;\n\tseekToNextChapter: () => void;\n\tseekToPrevChapter: () => void;\n\thandleStartSeek: (ticks: number) => void;\n\thandleStopSeek: (ticks: number) => void;\n\t/**\n\t * Skip current media segment\n\t */\n\tskipSegment: () => void;\n\thandleOnSeek: (seconds: number) => void;\n};\n\nexport const usePlaybackStore = create<\n\tPlaybackStoreState & PlaybackStoreActions\n>()(\n\timmer((set, get) => ({\n\t\tmediaSource: {\n\t\t\tvideoTrack: undefined!,\n\t\t\taudioTrack: undefined!,\n\t\t\tcontainer: undefined!,\n\t\t\tid: undefined!,\n\t\t\tsubtitle: undefined!,\n\t\t\taudio: undefined!,\n\t\t},\n\t\tplaybackStream: undefined!,\n\t\tplaysessionId: undefined!,\n\t\tmetadata: {\n\t\t\titemName: undefined!,\n\t\t\tepisodeTitle: undefined,\n\t\t\tisEpisode: false,\n\t\t\titemDuration: undefined!,\n\t\t\titem: undefined!,\n\t\t\tmediaSegments: undefined,\n\t\t\tuserDataLastPlayedPositionTicks: 0,\n\t\t},\n\t\tplayerState: {\n\t\t\tvolume: 1,\n\t\t\tisPlayerMuted: false,\n\t\t\tisPlayerPlaying: true,\n\t\t\tisPlayerReady: false,\n\t\t\tisPlayerFullscreen: false,\n\t\t\tisUserHovering: false,\n\t\t\tisBuffering: true,\n\t\t\tcurrentTime: undefined!,\n\t\t\tisUserSeeking: false,\n\t\t\tseekValue: 0,\n\t\t\tisLoading: true,\n\t\t\tshowStatsForNerds: false,\n\t\t},\n\t\tuserId: undefined!,\n\t\t// -- MediaSegment skip feature --\n\t\tnextSegmentIndex: 0,\n\t\tactiveSegmentId: null,\n\n\t\t// -- Volume change indicator --\n\t\tisVolumeInidcatorVisible: false,\n\t\tvolumeIndicatorVisibleTimeoutId: null,\n\n\t\t// -- Player state related actions --\n\t\tsetVolume: (volume) => {\n\t\t\tif (volume < 0 || volume > 1) {\n\t\t\t\tconsole.warn(\"Volume must be between 0 and 1\");\n\t\t\t}\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.volume = volume;\n\t\t\t\tstate.playerState.isPlayerMuted = volume === 0; // Mute if volume is 0\n\t\t\t});\n\t\t\tsetPlayerVolume(\"video\", volume);\n\t\t\tsetPlayerMuted(\"video\", volume === 0);\n\t\t\tget().tiggerVolumeIndicator(); // Trigger volume change indicator\n\t\t},\n\t\tincreaseVolumeByStep: (step) => {\n\t\t\tset((state) => {\n\t\t\t\tconst newVolume = Math.min(\n\t\t\t\t\t1,\n\t\t\t\t\tMath.max(0, state.playerState.volume + step),\n\t\t\t\t);\n\t\t\t\tstate.playerState.volume = newVolume;\n\t\t\t\tsetPlayerVolume(\"video\", newVolume);\n\t\t\t});\n\t\t\tget().tiggerVolumeIndicator(); // Trigger volume change indicator\n\t\t},\n\t\tdecreaseVolumeByStep: (step) => {\n\t\t\tset((state) => {\n\t\t\t\tconst newVolume = Math.min(\n\t\t\t\t\t1,\n\t\t\t\t\tMath.max(0, state.playerState.volume - step),\n\t\t\t\t);\n\t\t\t\tstate.playerState.volume = newVolume;\n\t\t\t\tsetPlayerVolume(\"video\", newVolume);\n\t\t\t});\n\t\t\tget().tiggerVolumeIndicator(); // Trigger volume change indicator\n\t\t},\n\t\ttoggleIsPlaying: () => {\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.isPlayerPlaying = !state.playerState.isPlayerPlaying;\n\t\t\t});\n\t\t},\n\t\tsetIsPlaying: (isPlaying) =>\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.isPlayerPlaying = isPlaying;\n\t\t\t}),\n\t\tsetIsBuffering: (isBuffering) =>\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.isBuffering = isBuffering;\n\t\t\t}),\n\t\tsetCurrentTime: (ticks) => {\n\t\t\tconst { nextSegmentIndex, activeSegmentId, metadata } = get();\n\t\t\tif (activeSegmentId) {\n\t\t\t\tconst activeSegment = metadata.mediaSegments?.Items?.find(\n\t\t\t\t\t(s) => s.Id === activeSegmentId,\n\t\t\t\t);\n\t\t\t\tif (activeSegment && ticks > (activeSegment.EndTicks ?? ticks)) {\n\t\t\t\t\t// If the current time is past the active segment's end time, clear it.\n\t\t\t\t\tset((state) => {\n\t\t\t\t\t\tstate.activeSegmentId = null;\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (nextSegmentIndex !== -1) {\n\t\t\t\tconst nextSegment = metadata.mediaSegments?.Items?.[nextSegmentIndex];\n\t\t\t\tif (nextSegment && ticks >= (nextSegment.StartTicks ?? ticks + 1)) {\n\t\t\t\t\t// If the current time is past the next segment's start time, set it as active.\n\t\t\t\t\t// clearActiveSegment();\n\t\t\t\t\tset((state) => {\n\t\t\t\t\t\tstate.activeSegmentId = null;\n\t\t\t\t\t\tstate.nextSegmentIndex = nextSegmentIndex + 1;\n\t\t\t\t\t\tstate.activeSegmentId = nextSegment?.Id ?? null;\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.currentTime = ticks;\n\t\t\t});\n\t\t},\n\t\tsetPlayerReady: (isPlayerReady) =>\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.isPlayerReady = isPlayerReady;\n\t\t\t}),\n\t\tsetMetadata: (\n\t\t\titemName: string,\n\t\t\tepisodeTitle: string,\n\t\t\tisEpisode: boolean,\n\t\t\titemDuration: number,\n\t\t\titem: BaseItemDto,\n\t\t\tmediaSegments?: MediaSegmentDtoQueryResult,\n\t\t) =>\n\t\t\tset((state) => {\n\t\t\t\tstate.metadata.itemName = itemName;\n\t\t\t\tstate.metadata.episodeTitle = episodeTitle;\n\t\t\t\tstate.metadata.isEpisode = isEpisode;\n\t\t\t\tstate.metadata.itemDuration = itemDuration;\n\t\t\t\tstate.metadata.item = item;\n\t\t\t\tstate.metadata.mediaSegments = mediaSegments;\n\t\t\t}),\n\t\tsetMediaSource: (\n\t\t\tvideoTrack: number,\n\t\t\taudioTrack: number,\n\t\t\tcontainer: string,\n\t\t\tid: string | undefined,\n\t\t\tsubtitle: subtitlePlaybackInfo,\n\t\t\taudio: audioPlaybackInfo,\n\t\t\tbitrate?: number,\n\t\t\tvideoCodec?: string,\n\t\t\taudioCodec?: string,\n\t\t\tplayMethod?: PlayMethod,\n\t\t\ttranscodingUrl?: string,\n\t\t) =>\n\t\t\tset((state) => {\n\t\t\t\tstate.mediaSource.videoTrack = videoTrack;\n\t\t\t\tstate.mediaSource.audioTrack = audioTrack;\n\t\t\t\tstate.mediaSource.container = container;\n\t\t\t\tstate.mediaSource.id = id;\n\t\t\t\tstate.mediaSource.subtitle = subtitle;\n\t\t\t\tstate.mediaSource.audio = audio;\n\t\t\t\tstate.mediaSource.bitrate = bitrate;\n\t\t\t\tstate.mediaSource.videoCodec = videoCodec;\n\t\t\t\tstate.mediaSource.audioCodec = audioCodec;\n\t\t\t\tstate.mediaSource.playMethod = playMethod;\n\t\t\t\tstate.mediaSource.transcodingUrl = transcodingUrl;\n\t\t\t}),\n\t\tsetPlaybackStream: (playbackStream: string) =>\n\t\t\tset((state) => {\n\t\t\t\tstate.playbackStream = playbackStream;\n\t\t\t}),\n\t\tsetPlaysessionId: (playsessionId) =>\n\t\t\tset((state) => {\n\t\t\t\tstate.playsessionId = playsessionId;\n\t\t\t}),\n\t\ttoggleIsPlayerFullscreen: async () => {\n\t\t\tawait appWindow\n\t\t\t\t.getCurrent()\n\t\t\t\t.setFullscreen(!get().playerState.isPlayerFullscreen);\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.isPlayerFullscreen =\n\t\t\t\t\t!state.playerState.isPlayerFullscreen;\n\t\t\t});\n\t\t},\n\t\tsetIsUserHovering: (isUserHovering) =>\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.isUserHovering = isUserHovering;\n\t\t\t}),\n\t\tsetIsUserSeeking: (isUserSeeking) =>\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.isUserSeeking = isUserSeeking;\n\t\t\t}),\n\t\tsetSeekValue: (seekValue) =>\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.seekValue = seekValue;\n\t\t\t}),\n\t\ttoggleIsPlayerMuted: () => {\n\t\t\tconst isMuted = !get().playerState.isPlayerMuted;\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.isPlayerMuted = isMuted;\n\t\t\t});\n\t\t\tsetPlayerMuted(\"video\", isMuted);\n\t\t},\n\t\tsetIsLoading: (isLoading) =>\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.isLoading = isLoading;\n\t\t\t}),\n\t\ttoggleShowStatsForNerds: () =>\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.showStatsForNerds =\n\t\t\t\t\t!state.playerState.showStatsForNerds;\n\t\t\t}),\n\t\tsetPlayerState: (playerState) =>\n\t\t\tset((state) => ({\n\t\t\t\t...state,\n\t\t\t\tplayerState: {\n\t\t\t\t\t...state.playerState,\n\t\t\t\t\t...playerState,\n\t\t\t\t},\n\t\t\t})),\n\t\tsetUserId: (userId) =>\n\t\t\tset(() => ({\n\t\t\t\tuserId: userId,\n\t\t\t})),\n\t\tsetActiveSegment: (segmentIndex) => {\n\t\t\tconst segment = get().metadata.mediaSegments?.Items?.[segmentIndex];\n\t\t\tset((state) => {\n\t\t\t\tstate.nextSegmentIndex = segmentIndex + 1;\n\t\t\t\tstate.activeSegmentId = segment?.Id ?? null;\n\t\t\t});\n\t\t},\n\t\tclearActiveSegment: () => {\n\t\t\tset((state) => {\n\t\t\t\tstate.activeSegmentId = null;\n\t\t\t});\n\t\t},\n\n\t\t// -- Volume change indicator Action --\n\t\ttiggerVolumeIndicator: () => {\n\t\t\tconst timeoutId = get().volumeIndicatorVisibleTimeoutId;\n\t\t\tif (timeoutId) {\n\t\t\t\tclearTimeout(timeoutId); // Volume indicator is already visible\n\t\t\t}\n\t\t\tset((state) => {\n\t\t\t\tstate.isVolumeInidcatorVisible = true;\n\t\t\t\tstate.volumeIndicatorVisibleTimeoutId = setTimeout(() => {\n\t\t\t\t\tset((state) => {\n\t\t\t\t\t\tstate.isVolumeInidcatorVisible = false;\n\t\t\t\t\t});\n\t\t\t\t}, 1000);\n\t\t\t});\n\t\t},\n\n\t\tinitializeVolume: async () => {\n\t\t\tconst volume = await getPlayerVolume(\"video\");\n\t\t\tconst isMuted = await getPlayerMuted(\"video\");\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.volume = volume;\n\t\t\t\tstate.playerState.isPlayerMuted = isMuted;\n\t\t\t});\n\t\t},\n\n\t\t// -- ReactPlayer related actions --\n\t\t_playerActions: {\n\t\t\tseekTo: (_seconds: number) =>\n\t\t\t\tconsole.warn(\n\t\t\t\t\t\"ReactPlayer has not yet initialized. Please wait until the player is ready to seek.\",\n\t\t\t\t),\n\t\t\tgetCurrentTime: () => {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t\"ReactPlayer has not yet initialized. Please wait until the player is ready to get current time.\",\n\t\t\t\t);\n\t\t\t\treturn 0;\n\t\t\t},\n\t\t},\n\t\tregisterPlayerActions: (actions) => {\n\t\t\tconsole.info(\"Registering player actions:\", actions);\n\t\t\tset({\n\t\t\t\t_playerActions: actions,\n\t\t\t});\n\t\t},\n\t\tseekTo: (seconds: number) => {\n\t\t\tconst playerActions = get()._playerActions;\n\t\t\tplayerActions.seekTo(seconds);\n\t\t},\n\t\tseekForward: (seconds: number) => {\n\t\t\tconst playerActions = get()._playerActions;\n\t\t\tconst currentTime = playerActions.getCurrentTime();\n\t\t\tplayerActions.seekTo(currentTime + seconds);\n\t\t},\n\t\tseekBackward: (seconds: number) => {\n\t\t\tconst playerActions = get()._playerActions;\n\t\t\tconst currentTime = playerActions.getCurrentTime();\n\t\t\tplayerActions.seekTo(Math.max(0, currentTime - seconds));\n\t\t},\n\t\tgetCurrentTime: () => {\n\t\t\tconst playerActions = get()._playerActions;\n\t\t\treturn playerActions.getCurrentTime();\n\t\t},\n\t\tseekToNextChapter: () => {\n\t\t\tconst playerActions = get()._playerActions;\n\t\t\tconst next = get().metadata.item?.Chapters?.filter((chapter) => {\n\t\t\t\tif (\n\t\t\t\t\t(chapter.StartPositionTicks ?? 0) > playerActions.getCurrentTime()\n\t\t\t\t) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t})[0];\n\t\t\tplayerActions.seekTo(ticksToSec(next?.StartPositionTicks ?? 0));\n\t\t},\n\t\tseekToPrevChapter: () => {\n\t\t\tconst playerActions = get()._playerActions;\n\t\t\tconst chapters = get().metadata.item.Chapters?.filter((chapter) => {\n\t\t\t\tif (\n\t\t\t\t\t(chapter.StartPositionTicks ?? 0) <=\n\t\t\t\t\tsecToTicks(playerActions.getCurrentTime())\n\t\t\t\t) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t});\n\t\t\tif (!chapters?.length) {\n\t\t\t\tplayerActions.seekTo(0);\n\t\t\t}\n\t\t\tif (chapters?.length === 1) {\n\t\t\t\tplayerActions.seekTo(ticksToSec(chapters[0].StartPositionTicks ?? 0));\n\t\t\t} else if ((chapters?.length ?? 0) > 1) {\n\t\t\t\tplayerActions.seekTo(\n\t\t\t\t\tticksToSec(chapters?.[chapters.length - 2].StartPositionTicks ?? 0),\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t\tskipSegment: () => {\n\t\t\tconst activeSegmentId = get().activeSegmentId;\n\t\t\tconst activeSegment = get().metadata.mediaSegments?.Items?.find(\n\t\t\t\t(s) => s.Id === activeSegmentId,\n\t\t\t);\n\t\t\tif (!activeSegment) {\n\t\t\t\tconsole.warn(\"No segment to skip\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.info(\"Skipping segment:\", activeSegment);\n\t\t\tget()._playerActions.seekTo(ticksToSec(activeSegment.EndTicks ?? 0));\n\t\t},\n\t\thandleStartSeek: (ticks) => {\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.isUserSeeking = true;\n\t\t\t\tstate.playerState.seekValue = ticks;\n\t\t\t});\n\t\t},\n\t\thandleStopSeek: (ticks) => {\n\t\t\tconst playerActions = get()._playerActions;\n\t\t\tplayerActions.seekTo(ticksToSec(ticks));\n\t\t\t// Update current time to the new seek value\n\t\t\tset((state) => {\n\t\t\t\tstate.playerState.isUserSeeking = false;\n\t\t\t\tstate.playerState.currentTime = ticks;\n\t\t\t});\n\t\t},\n\t\thandleOnSeek: (seconds) => {\n\t\t\tconst ticks = secToTicks(seconds);\n\t\t\tconst segments = get().metadata.mediaSegments?.Items;\n\t\t\tif (!segments || segments.length === 0) {\n\t\t\t\tconsole.warn(\"No segments available for seeking\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst segment = segments.find(\n\t\t\t\t(s) =>\n\t\t\t\t\t(s.StartTicks ?? ticks) <= ticks && (s.EndTicks ?? ticks) >= ticks,\n\t\t\t);\n\t\t\tconst nextIndex = segments.findIndex(\n\t\t\t\t(s) => (s.StartTicks ?? ticks) > ticks,\n\t\t\t);\n\t\t\tif (segment?.Id) {\n\t\t\t\t// If the segment is found, set it as active\n\t\t\t\tset({\n\t\t\t\t\tactiveSegmentId: segment.Id,\n\t\t\t\t\tnextSegmentIndex: nextIndex !== -1 ? nextIndex : segments.length,\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\t// If no segment is found, clear the active segment\n\t\t\t\tset({\n\t\t\t\t\tactiveSegmentId: null,\n\t\t\t\t\tnextSegmentIndex: nextIndex !== -1 ? nextIndex : segments.length,\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t})),\n);\n\n// Initialize volume from storage\n// usePlaybackStore.getState().initializeVolume();\n\nexport const playItem = (args: {\n\tmediaSource: PlaybackStoreState[\"mediaSource\"];\n\tplaybackStream: PlaybackStoreState[\"playbackStream\"];\n\tplaysessionId: PlaybackStoreState[\"playsessionId\"];\n\tmetadata: PlaybackStoreState[\"metadata\"];\n\tuserDataPlayedPositionTicks: number;\n\tuserId: string;\n\tqueueItems: BaseItemDto[];\n\tqueueItemIndex: number;\n}) => {\n\tusePlaybackStore.setState((state) => {\n\t\tstate.mediaSource = args.mediaSource;\n\t\tstate.playbackStream = args.playbackStream;\n\t\tstate.playsessionId = args.playsessionId;\n\t\tstate.metadata = args.metadata;\n\t\tstate.playerState.currentTime = args.userDataPlayedPositionTicks;\n\t\tstate.userId = args.userId;\n\t\tstate.playerState.isPlayerPlaying = true;\n\t\tstate.playerState.isPlayerReady = false; // Reset player ready state\n\t});\n\n\tsetQueue(args.queueItems, args.queueItemIndex ?? 0);\n};\n\nexport const initializePlayback = async (args: {\n\tapi: Api;\n\tuserId: string;\n\titem: BaseItemDto;\n\tmediaSource: PlaybackInfoResponse;\n\tmediaSegments?: MediaSegmentDtoQueryResult;\n\tqueueItems: BaseItemDto[];\n\tqueueIndex: number;\n\tstartPositionTicks?: number;\n\taudioStreamIndex?: number;\n\tsubtitleStreamIndex?: number;\n}) => {\n\tconst {\n\t\tapi,\n\t\tuserId,\n\t\titem,\n\t\tmediaSource,\n\t\tmediaSegments,\n\t\tqueueItems,\n\t\tqueueIndex,\n\t\tstartPositionTicks,\n\t\taudioStreamIndex,\n\t\tsubtitleStreamIndex,\n\t} = args;\n\n\tif (!mediaSource.MediaSources?.[0]?.Id) {\n\t\tthrow new Error(\"Media source is undefined in initializePlayback\");\n\t}\n\n\tconst source = mediaSource.MediaSources[0];\n\tconst currentUser = (await getUserApi(api).getCurrentUser()).data;\n\n\tlet itemName = item.Name;\n\tlet episodeTitle = \"\";\n\tif (item.SeriesId && item.SeriesName) {\n\t\titemName = item.SeriesName;\n\t\tepisodeTitle = `S${item.ParentIndexNumber ?? 0}:E${\n\t\t\titem.IndexNumber ?? 0\n\t\t} ${item.Name}`;\n\t}\n\n\t// Subtitle\n\tconst userSubtitleLanguagePreference =\n\t\tcurrentUser?.Configuration?.SubtitleLanguagePreference;\n\tconst allSubtitles = source.MediaStreams?.filter(\n\t\t(stream) => stream.Type === \"Subtitle\",\n\t);\n\tconst preferredSubtitle =\n\t\tallSubtitles?.find(\n\t\t\t(sub) => sub.Language === userSubtitleLanguagePreference,\n\t\t) || null;\n\n\tconst subtitle = getSubtitle(\n\t\tsubtitleStreamIndex ??\n\t\t\tpreferredSubtitle?.Index ??\n\t\t\tsource.DefaultSubtitleStreamIndex ??\n\t\t\t-1,\n\t\tsource.MediaStreams,\n\t);\n\n\t// Audio\n\tconst userAudioLanguagePreference =\n\t\tcurrentUser?.Configuration?.AudioLanguagePreference;\n\tconst allAudioTracks = source.MediaStreams?.filter(\n\t\t(value) => value.Type === \"Audio\",\n\t);\n\tconst preferredAudioTrack =\n\t\tallAudioTracks?.find(\n\t\t\t(track) => track.Language === userAudioLanguagePreference,\n\t\t) ||\n\t\tallAudioTracks?.find((track) => track.IsDefault) ||\n\t\tallAudioTracks?.[0];\n\n\tconst audio = {\n\t\ttrack:\n\t\t\taudioStreamIndex ??\n\t\t\tpreferredAudioTrack?.Index ??\n\t\t\tsource.DefaultAudioStreamIndex ??\n\t\t\t0,\n\t\tallTracks: allAudioTracks,\n\t};\n\n\t// URL generation\n\tconst urlOptions: URLSearchParams = {\n\t\t//@ts-expect-error\n\t\tStatic: true,\n\t\ttag: source.ETag,\n\t\tmediaSourceId: source.Id,\n\t\tdeviceId: api.deviceInfo.id,\n\t\tapi_key: api.accessToken,\n\t};\n\n\tlet playMethod: PlayMethod = PlayMethod.Transcode;\n\tif (source.SupportsDirectPlay) {\n\t\tplayMethod = PlayMethod.DirectPlay;\n\t} else if (source.SupportsDirectStream) {\n\t\tplayMethod = PlayMethod.DirectStream;\n\t}\n\n\tconst urlParams = new URLSearchParams(urlOptions).toString();\n\tlet playbackUrl = `${api.basePath}/Videos/${source.Id}/stream.${source.Container}?${urlParams}`;\n\tif (playMethod === PlayMethod.Transcode && source.TranscodingUrl) {\n\t\tplaybackUrl = `${api.basePath}${source.TranscodingUrl}`;\n\t}\n\n\tconst videoTrack = source.MediaStreams?.filter(\n\t\t(value) => value.Type === \"Video\",\n\t);\n\n\tplayItem({\n\t\tmetadata: {\n\t\t\titemName: itemName ?? \"\",\n\t\t\tepisodeTitle: episodeTitle,\n\t\t\tisEpisode: !!item.SeriesId,\n\t\t\titemDuration: item.RunTimeTicks ?? 0,\n\t\t\titem: item,\n\t\t\tmediaSegments: mediaSegments,\n\t\t\tuserDataLastPlayedPositionTicks:\n\t\t\t\tstartPositionTicks ?? item.UserData?.PlaybackPositionTicks ?? 0,\n\t\t},\n\t\tmediaSource: {\n\t\t\tvideoTrack: videoTrack?.[0]?.Index ?? 0,\n\t\t\taudioTrack: audio.track,\n\t\t\tcontainer: source.Container ?? \"\",\n\t\t\tid: source.Id ?? undefined,\n\t\t\tsubtitle: {\n\t\t\t\turl: subtitle?.url,\n\t\t\t\ttrack: subtitle?.track ?? -1,\n\t\t\t\tformat: subtitle?.format ?? \"nosub\",\n\t\t\t\tallTracks: allSubtitles,\n\t\t\t\tenable: subtitle?.enable ?? false,\n\t\t\t},\n\t\t\taudio: audio,\n\t\t\tbitrate: source.Bitrate ?? undefined,\n\t\t\tvideoCodec: videoTrack?.[0]?.Codec ?? undefined,\n\t\t\taudioCodec:\n\t\t\t\taudio.allTracks?.find((t) => t.Index === audio.track)?.Codec ??\n\t\t\t\tundefined,\n\t\t\tplayMethod: playMethod,\n\t\t\ttranscodingUrl: source.TranscodingUrl ?? undefined,\n\t\t},\n\t\tplaybackStream: playbackUrl,\n\t\tplaysessionId: mediaSource.PlaySessionId,\n\t\tuserDataPlayedPositionTicks: item.UserData?.PlaybackPositionTicks ?? 0,\n\t\tuserId,\n\t\tqueueItems: queueItems ?? [],\n\t\tqueueItemIndex: queueIndex,\n\t});\n};\n\nexport const playItemFromQueue = async (\n\tindex: \"next\" | \"previous\" | number,\n\tuserId: string | undefined,\n\tapi: Api | undefined,\n) => {\n\tif (!api) {\n\t\tconsole.error(\"Unable to play item from from queue. No API provided\");\n\t\treturn;\n\t}\n\tconst { tracks: queueItems, currentItemIndex } = useQueue.getState();\n\tconst requestedItemIndex =\n\t\tindex === \"next\"\n\t\t\t? currentItemIndex + 1\n\t\t\t: index === \"previous\"\n\t\t\t\t? currentItemIndex - 1\n\t\t\t\t: index;\n\n\tconst item = queueItems?.[requestedItemIndex];\n\n\tif (!item?.Id) {\n\t\tconsole.error(\"No item found in queue\");\n\t\treturn;\n\t}\n\n\tif (item.Type === \"Audio\" && userId) {\n\t\tconst playbackUrl = generateAudioStreamUrl(\n\t\t\titem.Id,\n\t\t\tuserId,\n\t\t\tapi.deviceInfo.id,\n\t\t\tapi.basePath,\n\t\t\tapi.accessToken,\n\t\t);\n\t\tplayAudio(playbackUrl, item, undefined);\n\t\tsetQueue(queueItems, requestedItemIndex);\n\t\treturn \"playing\";\n\t}\n\n\tif (!userId) {\n\t\tconsole.error(\"No user id provided\");\n\t\treturn;\n\t}\n\n\tconst mediaSourceId = item.MediaSources?.[0]?.Id;\n\tif (!mediaSourceId) {\n\t\tconsole.error(\"No media source id found\");\n\t\treturn;\n\t}\n\n\tif (!item.Name || !item.RunTimeTicks) {\n\t\tthrow new Error(\"Item data incomplete in playItemFromQueue\");\n\t}\n\n\tconst { playsessionId: prevPlaySessionId, mediaSource: prevMediaSource } =\n\t\tusePlaybackStore.getState();\n\tconst prevItem = queueItems?.[currentItemIndex];\n\n\tconst audioStreamIndex =\n\t\tprevItem?.Id === item.Id\n\t\t\t? prevMediaSource.audio.track\n\t\t\t: (item.MediaSources?.[0]?.DefaultAudioStreamIndex ?? 0);\n\tconst subtitleStreamIndex =\n\t\tprevItem?.Id === item.Id\n\t\t\t? prevMediaSource.subtitle.track\n\t\t\t: (item?.MediaSources?.[0]?.DefaultSubtitleStreamIndex ?? -1);\n\n\t// Prepare promises for parallel execution\n\tconst playbackInfoPromise = getMediaInfoApi(api).getPostedPlaybackInfo({\n\t\taudioStreamIndex,\n\t\tsubtitleStreamIndex,\n\t\titemId: item.Id,\n\t\tstartTimeTicks: item.UserData?.PlaybackPositionTicks,\n\t\tuserId: userId,\n\t\tmediaSourceId: mediaSourceId,\n\t\tplaybackInfoDto: {\n\t\t\tDeviceProfile: playbackProfile,\n\t\t},\n\t});\n\n\tconst segmentsPromise = getMediaSegmentsApi(api).getItemSegments({\n\t\titemId: item.Id,\n\t});\n\n\tconst stopReportPromise = getPlaystateApi(api).reportPlaybackStopped({\n\t\tplaybackStopInfo: {\n\t\t\tFailed: false,\n\t\t\tItemId: prevItem?.Id,\n\t\t\tMediaSourceId: prevMediaSource.id,\n\t\t\tPlaySessionId: prevPlaySessionId,\n\t\t},\n\t});\n\n\t// Await all promises\n\tconst [playbackInfoResponse, segmentsResponse] = await Promise.all([\n\t\tplaybackInfoPromise,\n\t\tsegmentsPromise,\n\t\tstopReportPromise,\n\t]);\n\n\tconst mediaSource = playbackInfoResponse.data;\n\tconst mediaSegments = segmentsResponse.data;\n\n\tinitializePlayback({\n\t\tapi,\n\t\tuserId,\n\t\titem,\n\t\tmediaSource,\n\t\tmediaSegments,\n\t\tqueueItems: queueItems ?? [],\n\t\tqueueIndex: requestedItemIndex,\n\t\taudioStreamIndex,\n\t\tsubtitleStreamIndex,\n\t});\n\n\treturn \"playing\"; // Return any value to end mutation pending status\n};\n\ninterface PlaybackDataLoadState {\n\tisPending: boolean;\n\tsetisPending: (loading: boolean) => void;\n}\n\nexport const usePlaybackDataLoadStore =\n\tcreateWithEqualityFn<PlaybackDataLoadState>(\n\t\t(set) => ({\n\t\t\tisPending: false,\n\t\t\tsetisPending: (loading: boolean) =>\n\t\t\t\tset((state: PlaybackDataLoadState) => ({\n\t\t\t\t\t...state,\n\t\t\t\t\tisPending: loading,\n\t\t\t\t})),\n\t\t}),\n\t\tshallow,\n\t);\n\nexport const changeSubtitleTrack = (\n\ttrackIndex: number,\n\tallTracks: MediaStream[],\n) => {\n\tconst requiredSubtitle = allTracks.filter(\n\t\t(track) => track.Index === trackIndex,\n\t);\n\tusePlaybackStore.setState((state) => {\n\t\tstate.mediaSource.subtitle = {\n\t\t\turl: requiredSubtitle?.[0]?.DeliveryUrl,\n\t\t\ttrack: trackIndex,\n\t\t\tformat: requiredSubtitle?.[0]?.Codec,\n\t\t\tallTracks,\n\t\t\tenable: trackIndex !== -1,\n\t\t};\n\t});\n};\n\nexport const toggleSubtitleTrack = () => {\n\tusePlaybackStore.setState((state) => {\n\t\tif (state.mediaSource.subtitle.track !== -1) {\n\t\t\tstate.mediaSource.subtitle.enable = !state.mediaSource.subtitle.enable;\n\t\t}\n\t});\n};\n\n/**\n *\n * @param trackIndex index of the new audio track\n * @param api api instance\n * @param startPosition position of videoPlayer during audio track change (this should be in ticks)\n */\nexport const changeAudioTrack = async (trackIndex: number, api: Api) => {\n\tconst prevState = usePlaybackStore.getState();\n\n\tif (!prevState.metadata.item?.Id) {\n\t\tthrow new Error(\"item is undefined in changeAudioTrack\");\n\t}\n\tif (!prevState.metadata.item.MediaSources?.[0].Id) {\n\t\tthrow new Error(\"Media source id is undefined in changeAudioTrack\");\n\t}\n\tconst mediaSource = (\n\t\tawait getMediaInfoApi(api).getPostedPlaybackInfo({\n\t\t\taudioStreamIndex: trackIndex,\n\t\t\tsubtitleStreamIndex: prevState.mediaSource.subtitle.track,\n\t\t\titemId: prevState.metadata.item.Id,\n\t\t\tstartTimeTicks: prevState.metadata.item.UserData?.PlaybackPositionTicks,\n\t\t\tuserId: prevState.userId,\n\t\t\tmediaSourceId: prevState.metadata.item.MediaSources?.[0].Id,\n\t\t\tplaybackInfoDto: {\n\t\t\t\tDeviceProfile: playbackProfile,\n\t\t\t},\n\t\t})\n\t).data;\n\n\t// URL generation\n\tconst urlOptions: URLSearchParams = {\n\t\t//@ts-expect-error\n\t\tStatic: true,\n\t\ttag: mediaSource.MediaSources?.[0].ETag,\n\t\tmediaSourceId: mediaSource.MediaSources?.[0].Id,\n\t\tdeviceId: api?.deviceInfo.id,\n\t\tapi_key: api?.accessToken,\n\t};\n\tconst urlParams = new URLSearchParams(urlOptions).toString();\n\tlet playbackUrl = `${api?.basePath}/Videos/${mediaSource.MediaSources?.[0].Id}/stream.${mediaSource.MediaSources?.[0].Container}?${urlParams}`;\n\tif (\n\t\tmediaSource.MediaSources?.[0].SupportsTranscoding &&\n\t\tmediaSource.MediaSources?.[0].TranscodingUrl\n\t) {\n\t\tplaybackUrl = `${api.basePath}${mediaSource.MediaSources[0].TranscodingUrl}`;\n\t}\n\n\tusePlaybackStore.setState((state) => {\n\t\tstate.mediaSource.audio.track = trackIndex;\n\t\tstate.mediaSource.id = mediaSource.MediaSources?.[0].Id ?? \"\";\n\t\tstate.mediaSource.container = mediaSource.MediaSources?.[0].Container ?? \"\";\n\t\tstate.playbackStream = playbackUrl;\n\t\tstate.playsessionId = mediaSource.PlaySessionId;\n\t});\n\t// const currentItemIndex = useQueue.getState().currentItemIndex;\n\t// playItemFromQueue(currentItemIndex, prevState.userId, api);\n};"
  },
  {
    "path": "src/utils/store/queue.ts",
    "content": "import type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\nimport { shallow } from \"zustand/shallow\";\nimport { createWithEqualityFn } from \"zustand/traditional\";\n\ninterface QueueStore {\n\ttracks: BaseItemDto[] | undefined;\n\t/**\n\t * This index number should be equal to index of the item in tracks array to allow seamless playback of items having broken IndexNumber field.\n\t */\n\tcurrentItemIndex: number;\n}\n\nconst useQueue = createWithEqualityFn<QueueStore>(\n\t() => ({\n\t\ttracks: [],\n\t\tcurrentItemIndex: 0,\n\t}),\n\tshallow,\n);\n\nexport const setQueue = (\n\ttracks: BaseItemDto[] | undefined,\n\tcurrentItemIndex: number,\n) => {\n\tuseQueue.setState(() => ({ tracks, currentItemIndex }));\n};\n\nexport const setTrackIndex = (index: number) => {\n\tconsole.info(`Setting index ${index}`);\n\tuseQueue.setState((state) => ({ ...state, currentItemIndex: index }));\n};\n\nexport const reorderQueue = (newOrder: BaseItemDto[]) => {\n\tuseQueue.setState((state) => ({ ...state, tracks: newOrder }));\n};\n\nexport const removeFromQueue = (index: number) => {\n\tuseQueue.setState((state) => {\n\t\tif (!state.tracks) return state;\n\t\tconst newTracks = [...state.tracks];\n\t\tnewTracks.splice(index, 1);\n\n\t\tlet newIndex = state.currentItemIndex;\n\t\tif (index < state.currentItemIndex) {\n\t\t\tnewIndex--;\n\t\t} else if (index === state.currentItemIndex) {\n\t\t\t// If removing current item, ensure index is valid\n\t\t\tif (newIndex >= newTracks.length) {\n\t\t\t\tnewIndex = Math.max(0, newTracks.length - 1);\n\t\t\t}\n\t\t}\n\n\t\treturn { ...state, tracks: newTracks, currentItemIndex: newIndex };\n\t});\n};\n\nexport const clearUpcoming = () => {\n\tuseQueue.setState((state) => {\n\t\tif (!state.tracks) return state;\n\t\t// Keep only items up to and including the current item\n\t\tconst newTracks = state.tracks.slice(0, state.currentItemIndex + 1);\n\t\treturn { ...state, tracks: newTracks };\n\t});\n};\n\nexport const shuffleUpcoming = () => {\n\tuseQueue.setState((state) => {\n\t\tif (!state.tracks || state.tracks.length <= state.currentItemIndex + 1)\n\t\t\treturn state;\n\n\t\tconst currentItems = state.tracks.slice(0, state.currentItemIndex + 1);\n\t\tconst upcomingItems = state.tracks.slice(state.currentItemIndex + 1);\n\n\t\t// Fisher-Yates shuffle for upcoming items\n\t\tfor (let i = upcomingItems.length - 1; i > 0; i--) {\n\t\t\tconst j = Math.floor(Math.random() * (i + 1));\n\t\t\t[upcomingItems[i], upcomingItems[j]] = [\n\t\t\t\tupcomingItems[j],\n\t\t\t\tupcomingItems[i],\n\t\t\t];\n\t\t}\n\n\t\treturn { ...state, tracks: [...currentItems, ...upcomingItems] };\n\t});\n};\n\n/**\n * Resets the Queue store\n */\nexport const clearQueue = () => {\n\tuseQueue.setState(useQueue.getInitialState());\n};\n\nexport default useQueue;\n"
  },
  {
    "path": "src/utils/store/search.tsx",
    "content": "import { create } from \"zustand\";\n\ntype SearchStore = {\n\tshowSearchDialog: boolean;\n\ttoggleSearchDialog: () => void;\n};\n\nconst useSearchStore = create<SearchStore>((set) => ({\n\tshowSearchDialog: false,\n\ttoggleSearchDialog: () =>\n\t\tset((state) => ({ showSearchDialog: !state.showSearchDialog })),\n}));\n\nexport default useSearchStore;\n"
  },
  {
    "path": "src/utils/store/settings.tsx",
    "content": "import { shallow } from \"zustand/shallow\";\nimport { createWithEqualityFn } from \"zustand/traditional\";\n\nconst useSettingsStore = createWithEqualityFn(\n\t() => ({\n\t\tdialogOpen: false,\n\t\ttabValue: 0,\n\t}),\n\tshallow,\n);\n\nconst setDialogOpen = (dialog: boolean) =>\n\tuseSettingsStore.setState((state) => ({ ...state, dialogOpen: dialog }));\n\nconst setTabValue = (value: number) => {\n\tuseSettingsStore.setState((state) => ({ ...state, tabValue: value }));\n};\n\nexport default useSettingsStore;\nexport {\n\tsetDialogOpen as setSettingsDialogOpen,\n\tsetTabValue as setSettingsTabValue,\n};\n"
  },
  {
    "path": "src/utils/types/audioPlaybackInfo.ts",
    "content": "import type { MediaStream } from \"@jellyfin/sdk/lib/generated-client\";\n\ntype audioPlaybackInfo = {\n\ttrack: number;\n\tallTracks: MediaStream[] | undefined;\n};\n\nexport default audioPlaybackInfo;"
  },
  {
    "path": "src/utils/types/introMediaInfo.ts",
    "content": "type introSkipperResult = {\n\tEpisodeId: string;\n\tValid: true;\n\tIntroStart: number;\n\tIntroEnd: number;\n\tShowSkipPromptAt: number;\n\tHideSkipPromptAt: number;\n};\n\ntype IntroMediaInfo = {\n\tIntroduction: introSkipperResult;\n\tCredits: introSkipperResult;\n};\n\nexport default IntroMediaInfo"
  },
  {
    "path": "src/utils/types/mediaQualityInfo.ts",
    "content": "type MediaQualityInfo = {\n\tisAtmos: boolean;\n\tisDolbyVision: boolean;\n\tisDts: boolean;\n\tisDtsHDMA: boolean;\n\tisDD: boolean;\n\tisDDP: boolean;\n\tisUHD: boolean;\n\tisHD: boolean;\n\tisSD: boolean;\n\tisSDR: boolean;\n\tisHDR: boolean;\n\tisHDR10: boolean;\n\tisHDR10Plus: boolean;\n\tisTrueHD: boolean;\n\tisIMAX: boolean;\n};\n\nexport default MediaQualityInfo;"
  },
  {
    "path": "src/utils/types/playResult.ts",
    "content": "import type {\n\tBaseItemDtoQueryResult,\n\tMediaSegmentDtoQueryResult,\n\tPlaybackInfoResponse,\n} from \"@jellyfin/sdk/lib/generated-client\";\nimport type IntroMediaInfo from \"./introMediaInfo\";\n\ninterface PlayResult {\n\titem: BaseItemDtoQueryResult | undefined;\n\tmediaSource: PlaybackInfoResponse | undefined;\n\tmediaSegments: MediaSegmentDtoQueryResult | undefined;\n\tepisodeIndex: number;\n}\n\nexport default PlayResult;"
  },
  {
    "path": "src/utils/types/seriesBackdrop.ts",
    "content": "type SeriesBackdropImage = {\n\turl?: string | null;\n\tkey?: string | null;\n};"
  },
  {
    "path": "src/utils/types/subtitlePlaybackInfo.ts",
    "content": "import type { MediaStream } from \"@jellyfin/sdk/lib/generated-client\";\n\nexport default interface subtitlePlaybackInfo {\n\t\tenable: boolean;\n\t\ttrack: number;\n\t\tformat: \"vtt\" | \"ass\" | \"ssa\" | \"subrip\" | undefined | null | string;\n\t\tallTracks: undefined | MediaStream[];\n\t\turl: string | undefined | null;\n\t}\n"
  },
  {
    "path": "src/utils/workers/backdrop.worker.ts",
    "content": "import type { BaseItemDto } from \"@jellyfin/sdk/lib/generated-client\";\n\nlet intervalId: number | undefined;\nlet backdropItems: BaseItemDto[] = [];\nlet backdropIndex = 0;\n\nconst updateBackdrop = () => {\n\tif (backdropItems.length === 0) {\n\t\treturn;\n\t}\n\n\tbackdropIndex = (backdropIndex + 1) % backdropItems.length;\n\tconst item = backdropItems[backdropIndex];\n\tconst keys = Object.keys(item?.ImageBlurHashes?.Backdrop ?? {});\n\tconst nextHash = item?.ImageBlurHashes?.Backdrop?.[keys[0]];\n\n\tif (nextHash) {\n\t\tself.postMessage({ type: \"UPDATE_BACKDROP\", payload: nextHash });\n\t}\n};\n\nself.onmessage = (event: MessageEvent) => {\n\tconst { type, payload } = event.data;\n\n\tswitch (type) {\n\t\tcase \"SET_BACKDROP_ITEMS\":\n\t\t\tbackdropItems = payload;\n\t\t\tbackdropIndex = 0;\n\t\t\tif (backdropItems.length > 0) {\n\t\t\t\tconst firstBackdrop = backdropItems[0];\n\t\t\t\tconst firstKey = Object.keys(\n\t\t\t\t\tfirstBackdrop?.ImageBlurHashes?.Backdrop ?? {},\n\t\t\t\t)[0];\n\t\t\t\tconst initialHash =\n\t\t\t\t\tfirstBackdrop?.ImageBlurHashes?.Backdrop?.[firstKey];\n\t\t\t\tif (initialHash) {\n\t\t\t\t\tself.postMessage({ type: \"UPDATE_BACKDROP\", payload: initialHash });\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"START\":\n\t\t\tif (intervalId) {\n\t\t\t\tclearInterval(intervalId);\n\t\t\t}\n\t\t\tintervalId = self.setInterval(updateBackdrop, 10_000);\n\t\t\tbreak;\n\t\tcase \"STOP\":\n\t\t\tif (intervalId) {\n\t\t\t\tclearInterval(intervalId);\n\t\t\t\tintervalId = undefined;\n\t\t\t}\n\t\t\tbreak;\n\t\tdefault:\n\t\t\tconsole.warn(\"Unknown message type in backdrop worker:\", type);\n\t}\n};\n"
  },
  {
    "path": "src-tauri/.gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n"
  },
  {
    "path": "src-tauri/Cargo.toml",
    "content": "[package]\nname = \"blink\"\nversion = \"1.0.0-alpha.4\"\ndescription = \"A Modern Jellyfin Client\"\nauthors = [\"prayag17\", \"Blink contributors (https://github.com/prayag17/Blink/graphs/contributors)\"]\nlicense = \"GPL-3.0-only\"\nrepository = \"https://github.com/prayag17/Blink/\"\ndefault-run = \"blink\"\nedition = \"2021\"\nrust-version = \"1.59\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[build-dependencies]\ntauri-build = { version = \"2.5.4\", features = [] }\n\n[dependencies]\nlog = \"^0.4.21\"\nserde_json = \"1.0\"\nserde = { version = \"1.0\", features = [\"derive\"] }\ntauri = { version = \"2.10.1\", features = [ \"devtools\"] }\ntauri-plugin-localhost = { git = \"https://github.com/tauri-apps/plugins-workspace\", branch = \"v2\" }\ntauri-plugin-store = { git = \"https://github.com/tauri-apps/plugins-workspace\", branch = \"v2\" }\ntauri-plugin-log = { git = \"https://github.com/tauri-apps/plugins-workspace\", branch = \"v2\", features = [\"colored\"] }\nportpicker = \"0.1\" # used in the example to pick a random free port\ntauri-plugin-process = { git = \"https://github.com/tauri-apps/plugins-workspace\", branch = \"v2\" }\ntauri-plugin-updater = { git = \"https://github.com/tauri-apps/plugins-workspace\", branch = \"v2\" }\ntauri-plugin-clipboard-manager = \"2.1.0-beta.6\"\ntauri-plugin-notification = \"2.0.0-rc.1\"\ntauri-plugin-upload = \"2.0.0\"\ntauri-plugin-dialog = \"2.0.0\"\ntauri-plugin-shell = \"2.3.5\"\n\n[features]\n# by default Tauri runs in production mode\n# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL\ndefault = [ \"custom-protocol\" ]\n# this feature is used for production builds where `devPath` points to the filesystem\n# DO NOT remove this\ncustom-protocol = [ \"tauri/custom-protocol\" ]\n\n[target.'cfg(not(any(target_os = \"android\", target_os = \"ios\")))'.dependencies]\ntauri-plugin-global-shortcut = \"2\"\ntauri-plugin-window-state = \"2\"\n"
  },
  {
    "path": "src-tauri/build.rs",
    "content": "fn main() {\n    tauri_build::build()\n}\n"
  },
  {
    "path": "src-tauri/capabilities/desktop.json",
    "content": "{\n  \"identifier\": \"desktop-capability\",\n  \"platforms\": [\n    \"macOS\",\n    \"windows\",\n    \"linux\"\n  ],\n  \"windows\": [\n    \"main\"\n  ],\n  \"permissions\": [\n    \"window-state:default\",\n    \"global-shortcut:default\",\n    \"global-shortcut:allow-is-registered\",\n    \"global-shortcut:allow-register\",\n    \"global-shortcut:allow-unregister\",\n    \"shell:default\"\n  ]\n}"
  },
  {
    "path": "src-tauri/capabilities/main.json",
    "content": "{\n\t\"$schema\": \"../gen/schemas/desktop-schema.json\",\n\t\"identifier\": \"main-capabilities\",\n\t\"description\": \"Capability for desktop window\",\n\t\"windows\": [\"main\"],\n\t\"permissions\": [\n\t\t\"store:allow-get\",\n\t\t\"store:allow-set\",\n\t\t\"store:allow-save\",\n\t\t\"store:allow-clear\",\n\t\t\"store:allow-delete\",\n\t\t\"store:allow-keys\",\n\t\t\"store:allow-load\",\n\t\t\"updater:allow-check\",\n\t\t\"updater:allow-download-and-install\",\n\t\t\"updater:allow-download\",\n\t\t\"updater:allow-install\",\n\t\t\"notification:default\",\n\t\t\"core:webview:allow-internal-toggle-devtools\",\n\t\t\"dialog:default\",\n\t\t\"upload:allow-download\",\n\t\t\"clipboard-manager:allow-write-text\",\n\t\t\"shell:default\",\n\t\t\"shell:allow-open\"\n\t]\n}"
  },
  {
    "path": "src-tauri/capabilities/migrated.json",
    "content": "{\n  \"$schema\": \"../gen/schemas/desktop-schema.json\",\n  \"identifier\": \"migrated\",\n  \"description\": \"permissions that were migrated from v1\",\n  \"local\": true,\n  \"windows\": [\n    \"main\"\n  ],\n  \"permissions\": [\n    \"core:path:default\",\n    \"core:event:default\",\n    \"core:window:default\",\n    \"core:app:default\",\n    \"core:resources:default\",\n    \"core:menu:default\",\n    \"core:tray:default\",\n    \"core:window:allow-set-fullscreen\",\n    \"shell:default\"\n  ]\n}"
  },
  {
    "path": "src-tauri/gen/schemas/acl-manifests.json",
    "content": "{\"clipboard-manager\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"No features are enabled by default, as we believe\\nthe clipboard can be inherently dangerous and it is \\napplication specific if read and/or write access is needed.\\n\\nClipboard interaction needs to be explicitly enabled.\\n\",\"permissions\":[]},\"permissions\":{\"allow-clear\":{\"identifier\":\"allow-clear\",\"description\":\"Enables the clear command without any pre-configured scope.\",\"commands\":{\"allow\":[\"clear\"],\"deny\":[]}},\"allow-read-image\":{\"identifier\":\"allow-read-image\",\"description\":\"Enables the read_image command without any pre-configured scope.\",\"commands\":{\"allow\":[\"read_image\"],\"deny\":[]}},\"allow-read-text\":{\"identifier\":\"allow-read-text\",\"description\":\"Enables the read_text command without any pre-configured scope.\",\"commands\":{\"allow\":[\"read_text\"],\"deny\":[]}},\"allow-write-html\":{\"identifier\":\"allow-write-html\",\"description\":\"Enables the write_html command without any pre-configured scope.\",\"commands\":{\"allow\":[\"write_html\"],\"deny\":[]}},\"allow-write-image\":{\"identifier\":\"allow-write-image\",\"description\":\"Enables the write_image command without any pre-configured scope.\",\"commands\":{\"allow\":[\"write_image\"],\"deny\":[]}},\"allow-write-text\":{\"identifier\":\"allow-write-text\",\"description\":\"Enables the write_text command without any pre-configured scope.\",\"commands\":{\"allow\":[\"write_text\"],\"deny\":[]}},\"deny-clear\":{\"identifier\":\"deny-clear\",\"description\":\"Denies the clear command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"clear\"]}},\"deny-read-image\":{\"identifier\":\"deny-read-image\",\"description\":\"Denies the read_image command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"read_image\"]}},\"deny-read-text\":{\"identifier\":\"deny-read-text\",\"description\":\"Denies the read_text command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"read_text\"]}},\"deny-write-html\":{\"identifier\":\"deny-write-html\",\"description\":\"Denies the write_html command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"write_html\"]}},\"deny-write-image\":{\"identifier\":\"deny-write-image\",\"description\":\"Denies the write_image command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"write_image\"]}},\"deny-write-text\":{\"identifier\":\"deny-write-text\",\"description\":\"Denies the write_text command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"write_text\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default core plugins set.\",\"permissions\":[\"core:path:default\",\"core:event:default\",\"core:window:default\",\"core:webview:default\",\"core:app:default\",\"core:image:default\",\"core:resources:default\",\"core:menu:default\",\"core:tray:default\"]},\"permissions\":{},\"permission_sets\":{},\"global_scope_schema\":null},\"core:app\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin.\",\"permissions\":[\"allow-version\",\"allow-name\",\"allow-tauri-version\",\"allow-identifier\",\"allow-bundle-type\",\"allow-register-listener\",\"allow-remove-listener\"]},\"permissions\":{\"allow-app-hide\":{\"identifier\":\"allow-app-hide\",\"description\":\"Enables the app_hide command without any pre-configured scope.\",\"commands\":{\"allow\":[\"app_hide\"],\"deny\":[]}},\"allow-app-show\":{\"identifier\":\"allow-app-show\",\"description\":\"Enables the app_show command without any pre-configured scope.\",\"commands\":{\"allow\":[\"app_show\"],\"deny\":[]}},\"allow-bundle-type\":{\"identifier\":\"allow-bundle-type\",\"description\":\"Enables the bundle_type command without any pre-configured scope.\",\"commands\":{\"allow\":[\"bundle_type\"],\"deny\":[]}},\"allow-default-window-icon\":{\"identifier\":\"allow-default-window-icon\",\"description\":\"Enables the default_window_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[\"default_window_icon\"],\"deny\":[]}},\"allow-fetch-data-store-identifiers\":{\"identifier\":\"allow-fetch-data-store-identifiers\",\"description\":\"Enables the fetch_data_store_identifiers command without any pre-configured scope.\",\"commands\":{\"allow\":[\"fetch_data_store_identifiers\"],\"deny\":[]}},\"allow-identifier\":{\"identifier\":\"allow-identifier\",\"description\":\"Enables the identifier command without any pre-configured scope.\",\"commands\":{\"allow\":[\"identifier\"],\"deny\":[]}},\"allow-name\":{\"identifier\":\"allow-name\",\"description\":\"Enables the name command without any pre-configured scope.\",\"commands\":{\"allow\":[\"name\"],\"deny\":[]}},\"allow-register-listener\":{\"identifier\":\"allow-register-listener\",\"description\":\"Enables the register_listener command without any pre-configured scope.\",\"commands\":{\"allow\":[\"register_listener\"],\"deny\":[]}},\"allow-remove-data-store\":{\"identifier\":\"allow-remove-data-store\",\"description\":\"Enables the remove_data_store command without any pre-configured scope.\",\"commands\":{\"allow\":[\"remove_data_store\"],\"deny\":[]}},\"allow-remove-listener\":{\"identifier\":\"allow-remove-listener\",\"description\":\"Enables the remove_listener command without any pre-configured scope.\",\"commands\":{\"allow\":[\"remove_listener\"],\"deny\":[]}},\"allow-set-app-theme\":{\"identifier\":\"allow-set-app-theme\",\"description\":\"Enables the set_app_theme command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_app_theme\"],\"deny\":[]}},\"allow-set-dock-visibility\":{\"identifier\":\"allow-set-dock-visibility\",\"description\":\"Enables the set_dock_visibility command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_dock_visibility\"],\"deny\":[]}},\"allow-tauri-version\":{\"identifier\":\"allow-tauri-version\",\"description\":\"Enables the tauri_version command without any pre-configured scope.\",\"commands\":{\"allow\":[\"tauri_version\"],\"deny\":[]}},\"allow-version\":{\"identifier\":\"allow-version\",\"description\":\"Enables the version command without any pre-configured scope.\",\"commands\":{\"allow\":[\"version\"],\"deny\":[]}},\"deny-app-hide\":{\"identifier\":\"deny-app-hide\",\"description\":\"Denies the app_hide command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"app_hide\"]}},\"deny-app-show\":{\"identifier\":\"deny-app-show\",\"description\":\"Denies the app_show command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"app_show\"]}},\"deny-bundle-type\":{\"identifier\":\"deny-bundle-type\",\"description\":\"Denies the bundle_type command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"bundle_type\"]}},\"deny-default-window-icon\":{\"identifier\":\"deny-default-window-icon\",\"description\":\"Denies the default_window_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"default_window_icon\"]}},\"deny-fetch-data-store-identifiers\":{\"identifier\":\"deny-fetch-data-store-identifiers\",\"description\":\"Denies the fetch_data_store_identifiers command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"fetch_data_store_identifiers\"]}},\"deny-identifier\":{\"identifier\":\"deny-identifier\",\"description\":\"Denies the identifier command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"identifier\"]}},\"deny-name\":{\"identifier\":\"deny-name\",\"description\":\"Denies the name command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"name\"]}},\"deny-register-listener\":{\"identifier\":\"deny-register-listener\",\"description\":\"Denies the register_listener command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"register_listener\"]}},\"deny-remove-data-store\":{\"identifier\":\"deny-remove-data-store\",\"description\":\"Denies the remove_data_store command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"remove_data_store\"]}},\"deny-remove-listener\":{\"identifier\":\"deny-remove-listener\",\"description\":\"Denies the remove_listener command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"remove_listener\"]}},\"deny-set-app-theme\":{\"identifier\":\"deny-set-app-theme\",\"description\":\"Denies the set_app_theme command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_app_theme\"]}},\"deny-set-dock-visibility\":{\"identifier\":\"deny-set-dock-visibility\",\"description\":\"Denies the set_dock_visibility command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_dock_visibility\"]}},\"deny-tauri-version\":{\"identifier\":\"deny-tauri-version\",\"description\":\"Denies the tauri_version command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"tauri_version\"]}},\"deny-version\":{\"identifier\":\"deny-version\",\"description\":\"Denies the version command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"version\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:event\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin, which enables all commands.\",\"permissions\":[\"allow-listen\",\"allow-unlisten\",\"allow-emit\",\"allow-emit-to\"]},\"permissions\":{\"allow-emit\":{\"identifier\":\"allow-emit\",\"description\":\"Enables the emit command without any pre-configured scope.\",\"commands\":{\"allow\":[\"emit\"],\"deny\":[]}},\"allow-emit-to\":{\"identifier\":\"allow-emit-to\",\"description\":\"Enables the emit_to command without any pre-configured scope.\",\"commands\":{\"allow\":[\"emit_to\"],\"deny\":[]}},\"allow-listen\":{\"identifier\":\"allow-listen\",\"description\":\"Enables the listen command without any pre-configured scope.\",\"commands\":{\"allow\":[\"listen\"],\"deny\":[]}},\"allow-unlisten\":{\"identifier\":\"allow-unlisten\",\"description\":\"Enables the unlisten command without any pre-configured scope.\",\"commands\":{\"allow\":[\"unlisten\"],\"deny\":[]}},\"deny-emit\":{\"identifier\":\"deny-emit\",\"description\":\"Denies the emit command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"emit\"]}},\"deny-emit-to\":{\"identifier\":\"deny-emit-to\",\"description\":\"Denies the emit_to command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"emit_to\"]}},\"deny-listen\":{\"identifier\":\"deny-listen\",\"description\":\"Denies the listen command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"listen\"]}},\"deny-unlisten\":{\"identifier\":\"deny-unlisten\",\"description\":\"Denies the unlisten command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"unlisten\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:image\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin, which enables all commands.\",\"permissions\":[\"allow-new\",\"allow-from-bytes\",\"allow-from-path\",\"allow-rgba\",\"allow-size\"]},\"permissions\":{\"allow-from-bytes\":{\"identifier\":\"allow-from-bytes\",\"description\":\"Enables the from_bytes command without any pre-configured scope.\",\"commands\":{\"allow\":[\"from_bytes\"],\"deny\":[]}},\"allow-from-path\":{\"identifier\":\"allow-from-path\",\"description\":\"Enables the from_path command without any pre-configured scope.\",\"commands\":{\"allow\":[\"from_path\"],\"deny\":[]}},\"allow-new\":{\"identifier\":\"allow-new\",\"description\":\"Enables the new command without any pre-configured scope.\",\"commands\":{\"allow\":[\"new\"],\"deny\":[]}},\"allow-rgba\":{\"identifier\":\"allow-rgba\",\"description\":\"Enables the rgba command without any pre-configured scope.\",\"commands\":{\"allow\":[\"rgba\"],\"deny\":[]}},\"allow-size\":{\"identifier\":\"allow-size\",\"description\":\"Enables the size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"size\"],\"deny\":[]}},\"deny-from-bytes\":{\"identifier\":\"deny-from-bytes\",\"description\":\"Denies the from_bytes command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"from_bytes\"]}},\"deny-from-path\":{\"identifier\":\"deny-from-path\",\"description\":\"Denies the from_path command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"from_path\"]}},\"deny-new\":{\"identifier\":\"deny-new\",\"description\":\"Denies the new command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"new\"]}},\"deny-rgba\":{\"identifier\":\"deny-rgba\",\"description\":\"Denies the rgba command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"rgba\"]}},\"deny-size\":{\"identifier\":\"deny-size\",\"description\":\"Denies the size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"size\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:menu\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin, which enables all commands.\",\"permissions\":[\"allow-new\",\"allow-append\",\"allow-prepend\",\"allow-insert\",\"allow-remove\",\"allow-remove-at\",\"allow-items\",\"allow-get\",\"allow-popup\",\"allow-create-default\",\"allow-set-as-app-menu\",\"allow-set-as-window-menu\",\"allow-text\",\"allow-set-text\",\"allow-is-enabled\",\"allow-set-enabled\",\"allow-set-accelerator\",\"allow-set-as-windows-menu-for-nsapp\",\"allow-set-as-help-menu-for-nsapp\",\"allow-is-checked\",\"allow-set-checked\",\"allow-set-icon\"]},\"permissions\":{\"allow-append\":{\"identifier\":\"allow-append\",\"description\":\"Enables the append command without any pre-configured scope.\",\"commands\":{\"allow\":[\"append\"],\"deny\":[]}},\"allow-create-default\":{\"identifier\":\"allow-create-default\",\"description\":\"Enables the create_default command without any pre-configured scope.\",\"commands\":{\"allow\":[\"create_default\"],\"deny\":[]}},\"allow-get\":{\"identifier\":\"allow-get\",\"description\":\"Enables the get command without any pre-configured scope.\",\"commands\":{\"allow\":[\"get\"],\"deny\":[]}},\"allow-insert\":{\"identifier\":\"allow-insert\",\"description\":\"Enables the insert command without any pre-configured scope.\",\"commands\":{\"allow\":[\"insert\"],\"deny\":[]}},\"allow-is-checked\":{\"identifier\":\"allow-is-checked\",\"description\":\"Enables the is_checked command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_checked\"],\"deny\":[]}},\"allow-is-enabled\":{\"identifier\":\"allow-is-enabled\",\"description\":\"Enables the is_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_enabled\"],\"deny\":[]}},\"allow-items\":{\"identifier\":\"allow-items\",\"description\":\"Enables the items command without any pre-configured scope.\",\"commands\":{\"allow\":[\"items\"],\"deny\":[]}},\"allow-new\":{\"identifier\":\"allow-new\",\"description\":\"Enables the new command without any pre-configured scope.\",\"commands\":{\"allow\":[\"new\"],\"deny\":[]}},\"allow-popup\":{\"identifier\":\"allow-popup\",\"description\":\"Enables the popup command without any pre-configured scope.\",\"commands\":{\"allow\":[\"popup\"],\"deny\":[]}},\"allow-prepend\":{\"identifier\":\"allow-prepend\",\"description\":\"Enables the prepend command without any pre-configured scope.\",\"commands\":{\"allow\":[\"prepend\"],\"deny\":[]}},\"allow-remove\":{\"identifier\":\"allow-remove\",\"description\":\"Enables the remove command without any pre-configured scope.\",\"commands\":{\"allow\":[\"remove\"],\"deny\":[]}},\"allow-remove-at\":{\"identifier\":\"allow-remove-at\",\"description\":\"Enables the remove_at command without any pre-configured scope.\",\"commands\":{\"allow\":[\"remove_at\"],\"deny\":[]}},\"allow-set-accelerator\":{\"identifier\":\"allow-set-accelerator\",\"description\":\"Enables the set_accelerator command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_accelerator\"],\"deny\":[]}},\"allow-set-as-app-menu\":{\"identifier\":\"allow-set-as-app-menu\",\"description\":\"Enables the set_as_app_menu command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_as_app_menu\"],\"deny\":[]}},\"allow-set-as-help-menu-for-nsapp\":{\"identifier\":\"allow-set-as-help-menu-for-nsapp\",\"description\":\"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_as_help_menu_for_nsapp\"],\"deny\":[]}},\"allow-set-as-window-menu\":{\"identifier\":\"allow-set-as-window-menu\",\"description\":\"Enables the set_as_window_menu command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_as_window_menu\"],\"deny\":[]}},\"allow-set-as-windows-menu-for-nsapp\":{\"identifier\":\"allow-set-as-windows-menu-for-nsapp\",\"description\":\"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_as_windows_menu_for_nsapp\"],\"deny\":[]}},\"allow-set-checked\":{\"identifier\":\"allow-set-checked\",\"description\":\"Enables the set_checked command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_checked\"],\"deny\":[]}},\"allow-set-enabled\":{\"identifier\":\"allow-set-enabled\",\"description\":\"Enables the set_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_enabled\"],\"deny\":[]}},\"allow-set-icon\":{\"identifier\":\"allow-set-icon\",\"description\":\"Enables the set_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_icon\"],\"deny\":[]}},\"allow-set-text\":{\"identifier\":\"allow-set-text\",\"description\":\"Enables the set_text command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_text\"],\"deny\":[]}},\"allow-text\":{\"identifier\":\"allow-text\",\"description\":\"Enables the text command without any pre-configured scope.\",\"commands\":{\"allow\":[\"text\"],\"deny\":[]}},\"deny-append\":{\"identifier\":\"deny-append\",\"description\":\"Denies the append command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"append\"]}},\"deny-create-default\":{\"identifier\":\"deny-create-default\",\"description\":\"Denies the create_default command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"create_default\"]}},\"deny-get\":{\"identifier\":\"deny-get\",\"description\":\"Denies the get command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"get\"]}},\"deny-insert\":{\"identifier\":\"deny-insert\",\"description\":\"Denies the insert command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"insert\"]}},\"deny-is-checked\":{\"identifier\":\"deny-is-checked\",\"description\":\"Denies the is_checked command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_checked\"]}},\"deny-is-enabled\":{\"identifier\":\"deny-is-enabled\",\"description\":\"Denies the is_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_enabled\"]}},\"deny-items\":{\"identifier\":\"deny-items\",\"description\":\"Denies the items command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"items\"]}},\"deny-new\":{\"identifier\":\"deny-new\",\"description\":\"Denies the new command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"new\"]}},\"deny-popup\":{\"identifier\":\"deny-popup\",\"description\":\"Denies the popup command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"popup\"]}},\"deny-prepend\":{\"identifier\":\"deny-prepend\",\"description\":\"Denies the prepend command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"prepend\"]}},\"deny-remove\":{\"identifier\":\"deny-remove\",\"description\":\"Denies the remove command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"remove\"]}},\"deny-remove-at\":{\"identifier\":\"deny-remove-at\",\"description\":\"Denies the remove_at command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"remove_at\"]}},\"deny-set-accelerator\":{\"identifier\":\"deny-set-accelerator\",\"description\":\"Denies the set_accelerator command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_accelerator\"]}},\"deny-set-as-app-menu\":{\"identifier\":\"deny-set-as-app-menu\",\"description\":\"Denies the set_as_app_menu command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_as_app_menu\"]}},\"deny-set-as-help-menu-for-nsapp\":{\"identifier\":\"deny-set-as-help-menu-for-nsapp\",\"description\":\"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_as_help_menu_for_nsapp\"]}},\"deny-set-as-window-menu\":{\"identifier\":\"deny-set-as-window-menu\",\"description\":\"Denies the set_as_window_menu command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_as_window_menu\"]}},\"deny-set-as-windows-menu-for-nsapp\":{\"identifier\":\"deny-set-as-windows-menu-for-nsapp\",\"description\":\"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_as_windows_menu_for_nsapp\"]}},\"deny-set-checked\":{\"identifier\":\"deny-set-checked\",\"description\":\"Denies the set_checked command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_checked\"]}},\"deny-set-enabled\":{\"identifier\":\"deny-set-enabled\",\"description\":\"Denies the set_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_enabled\"]}},\"deny-set-icon\":{\"identifier\":\"deny-set-icon\",\"description\":\"Denies the set_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_icon\"]}},\"deny-set-text\":{\"identifier\":\"deny-set-text\",\"description\":\"Denies the set_text command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_text\"]}},\"deny-text\":{\"identifier\":\"deny-text\",\"description\":\"Denies the text command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"text\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:path\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin, which enables all commands.\",\"permissions\":[\"allow-resolve-directory\",\"allow-resolve\",\"allow-normalize\",\"allow-join\",\"allow-dirname\",\"allow-extname\",\"allow-basename\",\"allow-is-absolute\"]},\"permissions\":{\"allow-basename\":{\"identifier\":\"allow-basename\",\"description\":\"Enables the basename command without any pre-configured scope.\",\"commands\":{\"allow\":[\"basename\"],\"deny\":[]}},\"allow-dirname\":{\"identifier\":\"allow-dirname\",\"description\":\"Enables the dirname command without any pre-configured scope.\",\"commands\":{\"allow\":[\"dirname\"],\"deny\":[]}},\"allow-extname\":{\"identifier\":\"allow-extname\",\"description\":\"Enables the extname command without any pre-configured scope.\",\"commands\":{\"allow\":[\"extname\"],\"deny\":[]}},\"allow-is-absolute\":{\"identifier\":\"allow-is-absolute\",\"description\":\"Enables the is_absolute command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_absolute\"],\"deny\":[]}},\"allow-join\":{\"identifier\":\"allow-join\",\"description\":\"Enables the join command without any pre-configured scope.\",\"commands\":{\"allow\":[\"join\"],\"deny\":[]}},\"allow-normalize\":{\"identifier\":\"allow-normalize\",\"description\":\"Enables the normalize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"normalize\"],\"deny\":[]}},\"allow-resolve\":{\"identifier\":\"allow-resolve\",\"description\":\"Enables the resolve command without any pre-configured scope.\",\"commands\":{\"allow\":[\"resolve\"],\"deny\":[]}},\"allow-resolve-directory\":{\"identifier\":\"allow-resolve-directory\",\"description\":\"Enables the resolve_directory command without any pre-configured scope.\",\"commands\":{\"allow\":[\"resolve_directory\"],\"deny\":[]}},\"deny-basename\":{\"identifier\":\"deny-basename\",\"description\":\"Denies the basename command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"basename\"]}},\"deny-dirname\":{\"identifier\":\"deny-dirname\",\"description\":\"Denies the dirname command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"dirname\"]}},\"deny-extname\":{\"identifier\":\"deny-extname\",\"description\":\"Denies the extname command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"extname\"]}},\"deny-is-absolute\":{\"identifier\":\"deny-is-absolute\",\"description\":\"Denies the is_absolute command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_absolute\"]}},\"deny-join\":{\"identifier\":\"deny-join\",\"description\":\"Denies the join command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"join\"]}},\"deny-normalize\":{\"identifier\":\"deny-normalize\",\"description\":\"Denies the normalize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"normalize\"]}},\"deny-resolve\":{\"identifier\":\"deny-resolve\",\"description\":\"Denies the resolve command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"resolve\"]}},\"deny-resolve-directory\":{\"identifier\":\"deny-resolve-directory\",\"description\":\"Denies the resolve_directory command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"resolve_directory\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:resources\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin, which enables all commands.\",\"permissions\":[\"allow-close\"]},\"permissions\":{\"allow-close\":{\"identifier\":\"allow-close\",\"description\":\"Enables the close command without any pre-configured scope.\",\"commands\":{\"allow\":[\"close\"],\"deny\":[]}},\"deny-close\":{\"identifier\":\"deny-close\",\"description\":\"Denies the close command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"close\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:tray\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin, which enables all commands.\",\"permissions\":[\"allow-new\",\"allow-get-by-id\",\"allow-remove-by-id\",\"allow-set-icon\",\"allow-set-menu\",\"allow-set-tooltip\",\"allow-set-title\",\"allow-set-visible\",\"allow-set-temp-dir-path\",\"allow-set-icon-as-template\",\"allow-set-show-menu-on-left-click\"]},\"permissions\":{\"allow-get-by-id\":{\"identifier\":\"allow-get-by-id\",\"description\":\"Enables the get_by_id command without any pre-configured scope.\",\"commands\":{\"allow\":[\"get_by_id\"],\"deny\":[]}},\"allow-new\":{\"identifier\":\"allow-new\",\"description\":\"Enables the new command without any pre-configured scope.\",\"commands\":{\"allow\":[\"new\"],\"deny\":[]}},\"allow-remove-by-id\":{\"identifier\":\"allow-remove-by-id\",\"description\":\"Enables the remove_by_id command without any pre-configured scope.\",\"commands\":{\"allow\":[\"remove_by_id\"],\"deny\":[]}},\"allow-set-icon\":{\"identifier\":\"allow-set-icon\",\"description\":\"Enables the set_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_icon\"],\"deny\":[]}},\"allow-set-icon-as-template\":{\"identifier\":\"allow-set-icon-as-template\",\"description\":\"Enables the set_icon_as_template command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_icon_as_template\"],\"deny\":[]}},\"allow-set-menu\":{\"identifier\":\"allow-set-menu\",\"description\":\"Enables the set_menu command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_menu\"],\"deny\":[]}},\"allow-set-show-menu-on-left-click\":{\"identifier\":\"allow-set-show-menu-on-left-click\",\"description\":\"Enables the set_show_menu_on_left_click command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_show_menu_on_left_click\"],\"deny\":[]}},\"allow-set-temp-dir-path\":{\"identifier\":\"allow-set-temp-dir-path\",\"description\":\"Enables the set_temp_dir_path command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_temp_dir_path\"],\"deny\":[]}},\"allow-set-title\":{\"identifier\":\"allow-set-title\",\"description\":\"Enables the set_title command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_title\"],\"deny\":[]}},\"allow-set-tooltip\":{\"identifier\":\"allow-set-tooltip\",\"description\":\"Enables the set_tooltip command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_tooltip\"],\"deny\":[]}},\"allow-set-visible\":{\"identifier\":\"allow-set-visible\",\"description\":\"Enables the set_visible command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_visible\"],\"deny\":[]}},\"deny-get-by-id\":{\"identifier\":\"deny-get-by-id\",\"description\":\"Denies the get_by_id command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"get_by_id\"]}},\"deny-new\":{\"identifier\":\"deny-new\",\"description\":\"Denies the new command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"new\"]}},\"deny-remove-by-id\":{\"identifier\":\"deny-remove-by-id\",\"description\":\"Denies the remove_by_id command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"remove_by_id\"]}},\"deny-set-icon\":{\"identifier\":\"deny-set-icon\",\"description\":\"Denies the set_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_icon\"]}},\"deny-set-icon-as-template\":{\"identifier\":\"deny-set-icon-as-template\",\"description\":\"Denies the set_icon_as_template command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_icon_as_template\"]}},\"deny-set-menu\":{\"identifier\":\"deny-set-menu\",\"description\":\"Denies the set_menu command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_menu\"]}},\"deny-set-show-menu-on-left-click\":{\"identifier\":\"deny-set-show-menu-on-left-click\",\"description\":\"Denies the set_show_menu_on_left_click command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_show_menu_on_left_click\"]}},\"deny-set-temp-dir-path\":{\"identifier\":\"deny-set-temp-dir-path\",\"description\":\"Denies the set_temp_dir_path command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_temp_dir_path\"]}},\"deny-set-title\":{\"identifier\":\"deny-set-title\",\"description\":\"Denies the set_title command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_title\"]}},\"deny-set-tooltip\":{\"identifier\":\"deny-set-tooltip\",\"description\":\"Denies the set_tooltip command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_tooltip\"]}},\"deny-set-visible\":{\"identifier\":\"deny-set-visible\",\"description\":\"Denies the set_visible command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_visible\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:webview\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin.\",\"permissions\":[\"allow-get-all-webviews\",\"allow-webview-position\",\"allow-webview-size\",\"allow-internal-toggle-devtools\"]},\"permissions\":{\"allow-clear-all-browsing-data\":{\"identifier\":\"allow-clear-all-browsing-data\",\"description\":\"Enables the clear_all_browsing_data command without any pre-configured scope.\",\"commands\":{\"allow\":[\"clear_all_browsing_data\"],\"deny\":[]}},\"allow-create-webview\":{\"identifier\":\"allow-create-webview\",\"description\":\"Enables the create_webview command without any pre-configured scope.\",\"commands\":{\"allow\":[\"create_webview\"],\"deny\":[]}},\"allow-create-webview-window\":{\"identifier\":\"allow-create-webview-window\",\"description\":\"Enables the create_webview_window command without any pre-configured scope.\",\"commands\":{\"allow\":[\"create_webview_window\"],\"deny\":[]}},\"allow-get-all-webviews\":{\"identifier\":\"allow-get-all-webviews\",\"description\":\"Enables the get_all_webviews command without any pre-configured scope.\",\"commands\":{\"allow\":[\"get_all_webviews\"],\"deny\":[]}},\"allow-internal-toggle-devtools\":{\"identifier\":\"allow-internal-toggle-devtools\",\"description\":\"Enables the internal_toggle_devtools command without any pre-configured scope.\",\"commands\":{\"allow\":[\"internal_toggle_devtools\"],\"deny\":[]}},\"allow-print\":{\"identifier\":\"allow-print\",\"description\":\"Enables the print command without any pre-configured scope.\",\"commands\":{\"allow\":[\"print\"],\"deny\":[]}},\"allow-reparent\":{\"identifier\":\"allow-reparent\",\"description\":\"Enables the reparent command without any pre-configured scope.\",\"commands\":{\"allow\":[\"reparent\"],\"deny\":[]}},\"allow-set-webview-auto-resize\":{\"identifier\":\"allow-set-webview-auto-resize\",\"description\":\"Enables the set_webview_auto_resize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_webview_auto_resize\"],\"deny\":[]}},\"allow-set-webview-background-color\":{\"identifier\":\"allow-set-webview-background-color\",\"description\":\"Enables the set_webview_background_color command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_webview_background_color\"],\"deny\":[]}},\"allow-set-webview-focus\":{\"identifier\":\"allow-set-webview-focus\",\"description\":\"Enables the set_webview_focus command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_webview_focus\"],\"deny\":[]}},\"allow-set-webview-position\":{\"identifier\":\"allow-set-webview-position\",\"description\":\"Enables the set_webview_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_webview_position\"],\"deny\":[]}},\"allow-set-webview-size\":{\"identifier\":\"allow-set-webview-size\",\"description\":\"Enables the set_webview_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_webview_size\"],\"deny\":[]}},\"allow-set-webview-zoom\":{\"identifier\":\"allow-set-webview-zoom\",\"description\":\"Enables the set_webview_zoom command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_webview_zoom\"],\"deny\":[]}},\"allow-webview-close\":{\"identifier\":\"allow-webview-close\",\"description\":\"Enables the webview_close command without any pre-configured scope.\",\"commands\":{\"allow\":[\"webview_close\"],\"deny\":[]}},\"allow-webview-hide\":{\"identifier\":\"allow-webview-hide\",\"description\":\"Enables the webview_hide command without any pre-configured scope.\",\"commands\":{\"allow\":[\"webview_hide\"],\"deny\":[]}},\"allow-webview-position\":{\"identifier\":\"allow-webview-position\",\"description\":\"Enables the webview_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"webview_position\"],\"deny\":[]}},\"allow-webview-show\":{\"identifier\":\"allow-webview-show\",\"description\":\"Enables the webview_show command without any pre-configured scope.\",\"commands\":{\"allow\":[\"webview_show\"],\"deny\":[]}},\"allow-webview-size\":{\"identifier\":\"allow-webview-size\",\"description\":\"Enables the webview_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"webview_size\"],\"deny\":[]}},\"deny-clear-all-browsing-data\":{\"identifier\":\"deny-clear-all-browsing-data\",\"description\":\"Denies the clear_all_browsing_data command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"clear_all_browsing_data\"]}},\"deny-create-webview\":{\"identifier\":\"deny-create-webview\",\"description\":\"Denies the create_webview command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"create_webview\"]}},\"deny-create-webview-window\":{\"identifier\":\"deny-create-webview-window\",\"description\":\"Denies the create_webview_window command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"create_webview_window\"]}},\"deny-get-all-webviews\":{\"identifier\":\"deny-get-all-webviews\",\"description\":\"Denies the get_all_webviews command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"get_all_webviews\"]}},\"deny-internal-toggle-devtools\":{\"identifier\":\"deny-internal-toggle-devtools\",\"description\":\"Denies the internal_toggle_devtools command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"internal_toggle_devtools\"]}},\"deny-print\":{\"identifier\":\"deny-print\",\"description\":\"Denies the print command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"print\"]}},\"deny-reparent\":{\"identifier\":\"deny-reparent\",\"description\":\"Denies the reparent command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"reparent\"]}},\"deny-set-webview-auto-resize\":{\"identifier\":\"deny-set-webview-auto-resize\",\"description\":\"Denies the set_webview_auto_resize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_webview_auto_resize\"]}},\"deny-set-webview-background-color\":{\"identifier\":\"deny-set-webview-background-color\",\"description\":\"Denies the set_webview_background_color command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_webview_background_color\"]}},\"deny-set-webview-focus\":{\"identifier\":\"deny-set-webview-focus\",\"description\":\"Denies the set_webview_focus command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_webview_focus\"]}},\"deny-set-webview-position\":{\"identifier\":\"deny-set-webview-position\",\"description\":\"Denies the set_webview_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_webview_position\"]}},\"deny-set-webview-size\":{\"identifier\":\"deny-set-webview-size\",\"description\":\"Denies the set_webview_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_webview_size\"]}},\"deny-set-webview-zoom\":{\"identifier\":\"deny-set-webview-zoom\",\"description\":\"Denies the set_webview_zoom command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_webview_zoom\"]}},\"deny-webview-close\":{\"identifier\":\"deny-webview-close\",\"description\":\"Denies the webview_close command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"webview_close\"]}},\"deny-webview-hide\":{\"identifier\":\"deny-webview-hide\",\"description\":\"Denies the webview_hide command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"webview_hide\"]}},\"deny-webview-position\":{\"identifier\":\"deny-webview-position\",\"description\":\"Denies the webview_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"webview_position\"]}},\"deny-webview-show\":{\"identifier\":\"deny-webview-show\",\"description\":\"Denies the webview_show command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"webview_show\"]}},\"deny-webview-size\":{\"identifier\":\"deny-webview-size\",\"description\":\"Denies the webview_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"webview_size\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"core:window\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Default permissions for the plugin.\",\"permissions\":[\"allow-get-all-windows\",\"allow-scale-factor\",\"allow-inner-position\",\"allow-outer-position\",\"allow-inner-size\",\"allow-outer-size\",\"allow-is-fullscreen\",\"allow-is-minimized\",\"allow-is-maximized\",\"allow-is-focused\",\"allow-is-decorated\",\"allow-is-resizable\",\"allow-is-maximizable\",\"allow-is-minimizable\",\"allow-is-closable\",\"allow-is-visible\",\"allow-is-enabled\",\"allow-title\",\"allow-current-monitor\",\"allow-primary-monitor\",\"allow-monitor-from-point\",\"allow-available-monitors\",\"allow-cursor-position\",\"allow-theme\",\"allow-is-always-on-top\",\"allow-internal-toggle-maximize\"]},\"permissions\":{\"allow-available-monitors\":{\"identifier\":\"allow-available-monitors\",\"description\":\"Enables the available_monitors command without any pre-configured scope.\",\"commands\":{\"allow\":[\"available_monitors\"],\"deny\":[]}},\"allow-center\":{\"identifier\":\"allow-center\",\"description\":\"Enables the center command without any pre-configured scope.\",\"commands\":{\"allow\":[\"center\"],\"deny\":[]}},\"allow-close\":{\"identifier\":\"allow-close\",\"description\":\"Enables the close command without any pre-configured scope.\",\"commands\":{\"allow\":[\"close\"],\"deny\":[]}},\"allow-create\":{\"identifier\":\"allow-create\",\"description\":\"Enables the create command without any pre-configured scope.\",\"commands\":{\"allow\":[\"create\"],\"deny\":[]}},\"allow-current-monitor\":{\"identifier\":\"allow-current-monitor\",\"description\":\"Enables the current_monitor command without any pre-configured scope.\",\"commands\":{\"allow\":[\"current_monitor\"],\"deny\":[]}},\"allow-cursor-position\":{\"identifier\":\"allow-cursor-position\",\"description\":\"Enables the cursor_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"cursor_position\"],\"deny\":[]}},\"allow-destroy\":{\"identifier\":\"allow-destroy\",\"description\":\"Enables the destroy command without any pre-configured scope.\",\"commands\":{\"allow\":[\"destroy\"],\"deny\":[]}},\"allow-get-all-windows\":{\"identifier\":\"allow-get-all-windows\",\"description\":\"Enables the get_all_windows command without any pre-configured scope.\",\"commands\":{\"allow\":[\"get_all_windows\"],\"deny\":[]}},\"allow-hide\":{\"identifier\":\"allow-hide\",\"description\":\"Enables the hide command without any pre-configured scope.\",\"commands\":{\"allow\":[\"hide\"],\"deny\":[]}},\"allow-inner-position\":{\"identifier\":\"allow-inner-position\",\"description\":\"Enables the inner_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"inner_position\"],\"deny\":[]}},\"allow-inner-size\":{\"identifier\":\"allow-inner-size\",\"description\":\"Enables the inner_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"inner_size\"],\"deny\":[]}},\"allow-internal-toggle-maximize\":{\"identifier\":\"allow-internal-toggle-maximize\",\"description\":\"Enables the internal_toggle_maximize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"internal_toggle_maximize\"],\"deny\":[]}},\"allow-is-always-on-top\":{\"identifier\":\"allow-is-always-on-top\",\"description\":\"Enables the is_always_on_top command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_always_on_top\"],\"deny\":[]}},\"allow-is-closable\":{\"identifier\":\"allow-is-closable\",\"description\":\"Enables the is_closable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_closable\"],\"deny\":[]}},\"allow-is-decorated\":{\"identifier\":\"allow-is-decorated\",\"description\":\"Enables the is_decorated command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_decorated\"],\"deny\":[]}},\"allow-is-enabled\":{\"identifier\":\"allow-is-enabled\",\"description\":\"Enables the is_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_enabled\"],\"deny\":[]}},\"allow-is-focused\":{\"identifier\":\"allow-is-focused\",\"description\":\"Enables the is_focused command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_focused\"],\"deny\":[]}},\"allow-is-fullscreen\":{\"identifier\":\"allow-is-fullscreen\",\"description\":\"Enables the is_fullscreen command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_fullscreen\"],\"deny\":[]}},\"allow-is-maximizable\":{\"identifier\":\"allow-is-maximizable\",\"description\":\"Enables the is_maximizable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_maximizable\"],\"deny\":[]}},\"allow-is-maximized\":{\"identifier\":\"allow-is-maximized\",\"description\":\"Enables the is_maximized command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_maximized\"],\"deny\":[]}},\"allow-is-minimizable\":{\"identifier\":\"allow-is-minimizable\",\"description\":\"Enables the is_minimizable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_minimizable\"],\"deny\":[]}},\"allow-is-minimized\":{\"identifier\":\"allow-is-minimized\",\"description\":\"Enables the is_minimized command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_minimized\"],\"deny\":[]}},\"allow-is-resizable\":{\"identifier\":\"allow-is-resizable\",\"description\":\"Enables the is_resizable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_resizable\"],\"deny\":[]}},\"allow-is-visible\":{\"identifier\":\"allow-is-visible\",\"description\":\"Enables the is_visible command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_visible\"],\"deny\":[]}},\"allow-maximize\":{\"identifier\":\"allow-maximize\",\"description\":\"Enables the maximize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"maximize\"],\"deny\":[]}},\"allow-minimize\":{\"identifier\":\"allow-minimize\",\"description\":\"Enables the minimize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"minimize\"],\"deny\":[]}},\"allow-monitor-from-point\":{\"identifier\":\"allow-monitor-from-point\",\"description\":\"Enables the monitor_from_point command without any pre-configured scope.\",\"commands\":{\"allow\":[\"monitor_from_point\"],\"deny\":[]}},\"allow-outer-position\":{\"identifier\":\"allow-outer-position\",\"description\":\"Enables the outer_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"outer_position\"],\"deny\":[]}},\"allow-outer-size\":{\"identifier\":\"allow-outer-size\",\"description\":\"Enables the outer_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"outer_size\"],\"deny\":[]}},\"allow-primary-monitor\":{\"identifier\":\"allow-primary-monitor\",\"description\":\"Enables the primary_monitor command without any pre-configured scope.\",\"commands\":{\"allow\":[\"primary_monitor\"],\"deny\":[]}},\"allow-request-user-attention\":{\"identifier\":\"allow-request-user-attention\",\"description\":\"Enables the request_user_attention command without any pre-configured scope.\",\"commands\":{\"allow\":[\"request_user_attention\"],\"deny\":[]}},\"allow-scale-factor\":{\"identifier\":\"allow-scale-factor\",\"description\":\"Enables the scale_factor command without any pre-configured scope.\",\"commands\":{\"allow\":[\"scale_factor\"],\"deny\":[]}},\"allow-set-always-on-bottom\":{\"identifier\":\"allow-set-always-on-bottom\",\"description\":\"Enables the set_always_on_bottom command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_always_on_bottom\"],\"deny\":[]}},\"allow-set-always-on-top\":{\"identifier\":\"allow-set-always-on-top\",\"description\":\"Enables the set_always_on_top command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_always_on_top\"],\"deny\":[]}},\"allow-set-background-color\":{\"identifier\":\"allow-set-background-color\",\"description\":\"Enables the set_background_color command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_background_color\"],\"deny\":[]}},\"allow-set-badge-count\":{\"identifier\":\"allow-set-badge-count\",\"description\":\"Enables the set_badge_count command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_badge_count\"],\"deny\":[]}},\"allow-set-badge-label\":{\"identifier\":\"allow-set-badge-label\",\"description\":\"Enables the set_badge_label command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_badge_label\"],\"deny\":[]}},\"allow-set-closable\":{\"identifier\":\"allow-set-closable\",\"description\":\"Enables the set_closable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_closable\"],\"deny\":[]}},\"allow-set-content-protected\":{\"identifier\":\"allow-set-content-protected\",\"description\":\"Enables the set_content_protected command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_content_protected\"],\"deny\":[]}},\"allow-set-cursor-grab\":{\"identifier\":\"allow-set-cursor-grab\",\"description\":\"Enables the set_cursor_grab command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_cursor_grab\"],\"deny\":[]}},\"allow-set-cursor-icon\":{\"identifier\":\"allow-set-cursor-icon\",\"description\":\"Enables the set_cursor_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_cursor_icon\"],\"deny\":[]}},\"allow-set-cursor-position\":{\"identifier\":\"allow-set-cursor-position\",\"description\":\"Enables the set_cursor_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_cursor_position\"],\"deny\":[]}},\"allow-set-cursor-visible\":{\"identifier\":\"allow-set-cursor-visible\",\"description\":\"Enables the set_cursor_visible command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_cursor_visible\"],\"deny\":[]}},\"allow-set-decorations\":{\"identifier\":\"allow-set-decorations\",\"description\":\"Enables the set_decorations command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_decorations\"],\"deny\":[]}},\"allow-set-effects\":{\"identifier\":\"allow-set-effects\",\"description\":\"Enables the set_effects command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_effects\"],\"deny\":[]}},\"allow-set-enabled\":{\"identifier\":\"allow-set-enabled\",\"description\":\"Enables the set_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_enabled\"],\"deny\":[]}},\"allow-set-focus\":{\"identifier\":\"allow-set-focus\",\"description\":\"Enables the set_focus command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_focus\"],\"deny\":[]}},\"allow-set-focusable\":{\"identifier\":\"allow-set-focusable\",\"description\":\"Enables the set_focusable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_focusable\"],\"deny\":[]}},\"allow-set-fullscreen\":{\"identifier\":\"allow-set-fullscreen\",\"description\":\"Enables the set_fullscreen command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_fullscreen\"],\"deny\":[]}},\"allow-set-icon\":{\"identifier\":\"allow-set-icon\",\"description\":\"Enables the set_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_icon\"],\"deny\":[]}},\"allow-set-ignore-cursor-events\":{\"identifier\":\"allow-set-ignore-cursor-events\",\"description\":\"Enables the set_ignore_cursor_events command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_ignore_cursor_events\"],\"deny\":[]}},\"allow-set-max-size\":{\"identifier\":\"allow-set-max-size\",\"description\":\"Enables the set_max_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_max_size\"],\"deny\":[]}},\"allow-set-maximizable\":{\"identifier\":\"allow-set-maximizable\",\"description\":\"Enables the set_maximizable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_maximizable\"],\"deny\":[]}},\"allow-set-min-size\":{\"identifier\":\"allow-set-min-size\",\"description\":\"Enables the set_min_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_min_size\"],\"deny\":[]}},\"allow-set-minimizable\":{\"identifier\":\"allow-set-minimizable\",\"description\":\"Enables the set_minimizable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_minimizable\"],\"deny\":[]}},\"allow-set-overlay-icon\":{\"identifier\":\"allow-set-overlay-icon\",\"description\":\"Enables the set_overlay_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_overlay_icon\"],\"deny\":[]}},\"allow-set-position\":{\"identifier\":\"allow-set-position\",\"description\":\"Enables the set_position command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_position\"],\"deny\":[]}},\"allow-set-progress-bar\":{\"identifier\":\"allow-set-progress-bar\",\"description\":\"Enables the set_progress_bar command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_progress_bar\"],\"deny\":[]}},\"allow-set-resizable\":{\"identifier\":\"allow-set-resizable\",\"description\":\"Enables the set_resizable command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_resizable\"],\"deny\":[]}},\"allow-set-shadow\":{\"identifier\":\"allow-set-shadow\",\"description\":\"Enables the set_shadow command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_shadow\"],\"deny\":[]}},\"allow-set-simple-fullscreen\":{\"identifier\":\"allow-set-simple-fullscreen\",\"description\":\"Enables the set_simple_fullscreen command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_simple_fullscreen\"],\"deny\":[]}},\"allow-set-size\":{\"identifier\":\"allow-set-size\",\"description\":\"Enables the set_size command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_size\"],\"deny\":[]}},\"allow-set-size-constraints\":{\"identifier\":\"allow-set-size-constraints\",\"description\":\"Enables the set_size_constraints command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_size_constraints\"],\"deny\":[]}},\"allow-set-skip-taskbar\":{\"identifier\":\"allow-set-skip-taskbar\",\"description\":\"Enables the set_skip_taskbar command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_skip_taskbar\"],\"deny\":[]}},\"allow-set-theme\":{\"identifier\":\"allow-set-theme\",\"description\":\"Enables the set_theme command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_theme\"],\"deny\":[]}},\"allow-set-title\":{\"identifier\":\"allow-set-title\",\"description\":\"Enables the set_title command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_title\"],\"deny\":[]}},\"allow-set-title-bar-style\":{\"identifier\":\"allow-set-title-bar-style\",\"description\":\"Enables the set_title_bar_style command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_title_bar_style\"],\"deny\":[]}},\"allow-set-visible-on-all-workspaces\":{\"identifier\":\"allow-set-visible-on-all-workspaces\",\"description\":\"Enables the set_visible_on_all_workspaces command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set_visible_on_all_workspaces\"],\"deny\":[]}},\"allow-show\":{\"identifier\":\"allow-show\",\"description\":\"Enables the show command without any pre-configured scope.\",\"commands\":{\"allow\":[\"show\"],\"deny\":[]}},\"allow-start-dragging\":{\"identifier\":\"allow-start-dragging\",\"description\":\"Enables the start_dragging command without any pre-configured scope.\",\"commands\":{\"allow\":[\"start_dragging\"],\"deny\":[]}},\"allow-start-resize-dragging\":{\"identifier\":\"allow-start-resize-dragging\",\"description\":\"Enables the start_resize_dragging command without any pre-configured scope.\",\"commands\":{\"allow\":[\"start_resize_dragging\"],\"deny\":[]}},\"allow-theme\":{\"identifier\":\"allow-theme\",\"description\":\"Enables the theme command without any pre-configured scope.\",\"commands\":{\"allow\":[\"theme\"],\"deny\":[]}},\"allow-title\":{\"identifier\":\"allow-title\",\"description\":\"Enables the title command without any pre-configured scope.\",\"commands\":{\"allow\":[\"title\"],\"deny\":[]}},\"allow-toggle-maximize\":{\"identifier\":\"allow-toggle-maximize\",\"description\":\"Enables the toggle_maximize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"toggle_maximize\"],\"deny\":[]}},\"allow-unmaximize\":{\"identifier\":\"allow-unmaximize\",\"description\":\"Enables the unmaximize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"unmaximize\"],\"deny\":[]}},\"allow-unminimize\":{\"identifier\":\"allow-unminimize\",\"description\":\"Enables the unminimize command without any pre-configured scope.\",\"commands\":{\"allow\":[\"unminimize\"],\"deny\":[]}},\"deny-available-monitors\":{\"identifier\":\"deny-available-monitors\",\"description\":\"Denies the available_monitors command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"available_monitors\"]}},\"deny-center\":{\"identifier\":\"deny-center\",\"description\":\"Denies the center command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"center\"]}},\"deny-close\":{\"identifier\":\"deny-close\",\"description\":\"Denies the close command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"close\"]}},\"deny-create\":{\"identifier\":\"deny-create\",\"description\":\"Denies the create command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"create\"]}},\"deny-current-monitor\":{\"identifier\":\"deny-current-monitor\",\"description\":\"Denies the current_monitor command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"current_monitor\"]}},\"deny-cursor-position\":{\"identifier\":\"deny-cursor-position\",\"description\":\"Denies the cursor_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"cursor_position\"]}},\"deny-destroy\":{\"identifier\":\"deny-destroy\",\"description\":\"Denies the destroy command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"destroy\"]}},\"deny-get-all-windows\":{\"identifier\":\"deny-get-all-windows\",\"description\":\"Denies the get_all_windows command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"get_all_windows\"]}},\"deny-hide\":{\"identifier\":\"deny-hide\",\"description\":\"Denies the hide command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"hide\"]}},\"deny-inner-position\":{\"identifier\":\"deny-inner-position\",\"description\":\"Denies the inner_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"inner_position\"]}},\"deny-inner-size\":{\"identifier\":\"deny-inner-size\",\"description\":\"Denies the inner_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"inner_size\"]}},\"deny-internal-toggle-maximize\":{\"identifier\":\"deny-internal-toggle-maximize\",\"description\":\"Denies the internal_toggle_maximize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"internal_toggle_maximize\"]}},\"deny-is-always-on-top\":{\"identifier\":\"deny-is-always-on-top\",\"description\":\"Denies the is_always_on_top command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_always_on_top\"]}},\"deny-is-closable\":{\"identifier\":\"deny-is-closable\",\"description\":\"Denies the is_closable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_closable\"]}},\"deny-is-decorated\":{\"identifier\":\"deny-is-decorated\",\"description\":\"Denies the is_decorated command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_decorated\"]}},\"deny-is-enabled\":{\"identifier\":\"deny-is-enabled\",\"description\":\"Denies the is_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_enabled\"]}},\"deny-is-focused\":{\"identifier\":\"deny-is-focused\",\"description\":\"Denies the is_focused command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_focused\"]}},\"deny-is-fullscreen\":{\"identifier\":\"deny-is-fullscreen\",\"description\":\"Denies the is_fullscreen command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_fullscreen\"]}},\"deny-is-maximizable\":{\"identifier\":\"deny-is-maximizable\",\"description\":\"Denies the is_maximizable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_maximizable\"]}},\"deny-is-maximized\":{\"identifier\":\"deny-is-maximized\",\"description\":\"Denies the is_maximized command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_maximized\"]}},\"deny-is-minimizable\":{\"identifier\":\"deny-is-minimizable\",\"description\":\"Denies the is_minimizable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_minimizable\"]}},\"deny-is-minimized\":{\"identifier\":\"deny-is-minimized\",\"description\":\"Denies the is_minimized command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_minimized\"]}},\"deny-is-resizable\":{\"identifier\":\"deny-is-resizable\",\"description\":\"Denies the is_resizable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_resizable\"]}},\"deny-is-visible\":{\"identifier\":\"deny-is-visible\",\"description\":\"Denies the is_visible command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_visible\"]}},\"deny-maximize\":{\"identifier\":\"deny-maximize\",\"description\":\"Denies the maximize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"maximize\"]}},\"deny-minimize\":{\"identifier\":\"deny-minimize\",\"description\":\"Denies the minimize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"minimize\"]}},\"deny-monitor-from-point\":{\"identifier\":\"deny-monitor-from-point\",\"description\":\"Denies the monitor_from_point command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"monitor_from_point\"]}},\"deny-outer-position\":{\"identifier\":\"deny-outer-position\",\"description\":\"Denies the outer_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"outer_position\"]}},\"deny-outer-size\":{\"identifier\":\"deny-outer-size\",\"description\":\"Denies the outer_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"outer_size\"]}},\"deny-primary-monitor\":{\"identifier\":\"deny-primary-monitor\",\"description\":\"Denies the primary_monitor command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"primary_monitor\"]}},\"deny-request-user-attention\":{\"identifier\":\"deny-request-user-attention\",\"description\":\"Denies the request_user_attention command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"request_user_attention\"]}},\"deny-scale-factor\":{\"identifier\":\"deny-scale-factor\",\"description\":\"Denies the scale_factor command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"scale_factor\"]}},\"deny-set-always-on-bottom\":{\"identifier\":\"deny-set-always-on-bottom\",\"description\":\"Denies the set_always_on_bottom command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_always_on_bottom\"]}},\"deny-set-always-on-top\":{\"identifier\":\"deny-set-always-on-top\",\"description\":\"Denies the set_always_on_top command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_always_on_top\"]}},\"deny-set-background-color\":{\"identifier\":\"deny-set-background-color\",\"description\":\"Denies the set_background_color command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_background_color\"]}},\"deny-set-badge-count\":{\"identifier\":\"deny-set-badge-count\",\"description\":\"Denies the set_badge_count command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_badge_count\"]}},\"deny-set-badge-label\":{\"identifier\":\"deny-set-badge-label\",\"description\":\"Denies the set_badge_label command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_badge_label\"]}},\"deny-set-closable\":{\"identifier\":\"deny-set-closable\",\"description\":\"Denies the set_closable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_closable\"]}},\"deny-set-content-protected\":{\"identifier\":\"deny-set-content-protected\",\"description\":\"Denies the set_content_protected command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_content_protected\"]}},\"deny-set-cursor-grab\":{\"identifier\":\"deny-set-cursor-grab\",\"description\":\"Denies the set_cursor_grab command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_cursor_grab\"]}},\"deny-set-cursor-icon\":{\"identifier\":\"deny-set-cursor-icon\",\"description\":\"Denies the set_cursor_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_cursor_icon\"]}},\"deny-set-cursor-position\":{\"identifier\":\"deny-set-cursor-position\",\"description\":\"Denies the set_cursor_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_cursor_position\"]}},\"deny-set-cursor-visible\":{\"identifier\":\"deny-set-cursor-visible\",\"description\":\"Denies the set_cursor_visible command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_cursor_visible\"]}},\"deny-set-decorations\":{\"identifier\":\"deny-set-decorations\",\"description\":\"Denies the set_decorations command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_decorations\"]}},\"deny-set-effects\":{\"identifier\":\"deny-set-effects\",\"description\":\"Denies the set_effects command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_effects\"]}},\"deny-set-enabled\":{\"identifier\":\"deny-set-enabled\",\"description\":\"Denies the set_enabled command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_enabled\"]}},\"deny-set-focus\":{\"identifier\":\"deny-set-focus\",\"description\":\"Denies the set_focus command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_focus\"]}},\"deny-set-focusable\":{\"identifier\":\"deny-set-focusable\",\"description\":\"Denies the set_focusable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_focusable\"]}},\"deny-set-fullscreen\":{\"identifier\":\"deny-set-fullscreen\",\"description\":\"Denies the set_fullscreen command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_fullscreen\"]}},\"deny-set-icon\":{\"identifier\":\"deny-set-icon\",\"description\":\"Denies the set_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_icon\"]}},\"deny-set-ignore-cursor-events\":{\"identifier\":\"deny-set-ignore-cursor-events\",\"description\":\"Denies the set_ignore_cursor_events command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_ignore_cursor_events\"]}},\"deny-set-max-size\":{\"identifier\":\"deny-set-max-size\",\"description\":\"Denies the set_max_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_max_size\"]}},\"deny-set-maximizable\":{\"identifier\":\"deny-set-maximizable\",\"description\":\"Denies the set_maximizable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_maximizable\"]}},\"deny-set-min-size\":{\"identifier\":\"deny-set-min-size\",\"description\":\"Denies the set_min_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_min_size\"]}},\"deny-set-minimizable\":{\"identifier\":\"deny-set-minimizable\",\"description\":\"Denies the set_minimizable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_minimizable\"]}},\"deny-set-overlay-icon\":{\"identifier\":\"deny-set-overlay-icon\",\"description\":\"Denies the set_overlay_icon command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_overlay_icon\"]}},\"deny-set-position\":{\"identifier\":\"deny-set-position\",\"description\":\"Denies the set_position command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_position\"]}},\"deny-set-progress-bar\":{\"identifier\":\"deny-set-progress-bar\",\"description\":\"Denies the set_progress_bar command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_progress_bar\"]}},\"deny-set-resizable\":{\"identifier\":\"deny-set-resizable\",\"description\":\"Denies the set_resizable command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_resizable\"]}},\"deny-set-shadow\":{\"identifier\":\"deny-set-shadow\",\"description\":\"Denies the set_shadow command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_shadow\"]}},\"deny-set-simple-fullscreen\":{\"identifier\":\"deny-set-simple-fullscreen\",\"description\":\"Denies the set_simple_fullscreen command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_simple_fullscreen\"]}},\"deny-set-size\":{\"identifier\":\"deny-set-size\",\"description\":\"Denies the set_size command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_size\"]}},\"deny-set-size-constraints\":{\"identifier\":\"deny-set-size-constraints\",\"description\":\"Denies the set_size_constraints command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_size_constraints\"]}},\"deny-set-skip-taskbar\":{\"identifier\":\"deny-set-skip-taskbar\",\"description\":\"Denies the set_skip_taskbar command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_skip_taskbar\"]}},\"deny-set-theme\":{\"identifier\":\"deny-set-theme\",\"description\":\"Denies the set_theme command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_theme\"]}},\"deny-set-title\":{\"identifier\":\"deny-set-title\",\"description\":\"Denies the set_title command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_title\"]}},\"deny-set-title-bar-style\":{\"identifier\":\"deny-set-title-bar-style\",\"description\":\"Denies the set_title_bar_style command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_title_bar_style\"]}},\"deny-set-visible-on-all-workspaces\":{\"identifier\":\"deny-set-visible-on-all-workspaces\",\"description\":\"Denies the set_visible_on_all_workspaces command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set_visible_on_all_workspaces\"]}},\"deny-show\":{\"identifier\":\"deny-show\",\"description\":\"Denies the show command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"show\"]}},\"deny-start-dragging\":{\"identifier\":\"deny-start-dragging\",\"description\":\"Denies the start_dragging command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"start_dragging\"]}},\"deny-start-resize-dragging\":{\"identifier\":\"deny-start-resize-dragging\",\"description\":\"Denies the start_resize_dragging command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"start_resize_dragging\"]}},\"deny-theme\":{\"identifier\":\"deny-theme\",\"description\":\"Denies the theme command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"theme\"]}},\"deny-title\":{\"identifier\":\"deny-title\",\"description\":\"Denies the title command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"title\"]}},\"deny-toggle-maximize\":{\"identifier\":\"deny-toggle-maximize\",\"description\":\"Denies the toggle_maximize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"toggle_maximize\"]}},\"deny-unmaximize\":{\"identifier\":\"deny-unmaximize\",\"description\":\"Denies the unmaximize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"unmaximize\"]}},\"deny-unminimize\":{\"identifier\":\"deny-unminimize\",\"description\":\"Denies the unminimize command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"unminimize\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"dialog\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This permission set configures the types of dialogs\\navailable from the dialog plugin.\\n\\n#### Granted Permissions\\n\\nAll dialog types are enabled.\\n\\n\\n\",\"permissions\":[\"allow-ask\",\"allow-confirm\",\"allow-message\",\"allow-save\",\"allow-open\"]},\"permissions\":{\"allow-ask\":{\"identifier\":\"allow-ask\",\"description\":\"Enables the ask command without any pre-configured scope.\",\"commands\":{\"allow\":[\"ask\"],\"deny\":[]}},\"allow-confirm\":{\"identifier\":\"allow-confirm\",\"description\":\"Enables the confirm command without any pre-configured scope.\",\"commands\":{\"allow\":[\"confirm\"],\"deny\":[]}},\"allow-message\":{\"identifier\":\"allow-message\",\"description\":\"Enables the message command without any pre-configured scope.\",\"commands\":{\"allow\":[\"message\"],\"deny\":[]}},\"allow-open\":{\"identifier\":\"allow-open\",\"description\":\"Enables the open command without any pre-configured scope.\",\"commands\":{\"allow\":[\"open\"],\"deny\":[]}},\"allow-save\":{\"identifier\":\"allow-save\",\"description\":\"Enables the save command without any pre-configured scope.\",\"commands\":{\"allow\":[\"save\"],\"deny\":[]}},\"deny-ask\":{\"identifier\":\"deny-ask\",\"description\":\"Denies the ask command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"ask\"]}},\"deny-confirm\":{\"identifier\":\"deny-confirm\",\"description\":\"Denies the confirm command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"confirm\"]}},\"deny-message\":{\"identifier\":\"deny-message\",\"description\":\"Denies the message command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"message\"]}},\"deny-open\":{\"identifier\":\"deny-open\",\"description\":\"Denies the open command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"open\"]}},\"deny-save\":{\"identifier\":\"deny-save\",\"description\":\"Denies the save command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"save\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"global-shortcut\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"No features are enabled by default, as we believe\\nthe shortcuts can be inherently dangerous and it is\\napplication specific if specific shortcuts should be\\nregistered or unregistered.\\n\",\"permissions\":[]},\"permissions\":{\"allow-is-registered\":{\"identifier\":\"allow-is-registered\",\"description\":\"Enables the is_registered command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_registered\"],\"deny\":[]}},\"allow-register\":{\"identifier\":\"allow-register\",\"description\":\"Enables the register command without any pre-configured scope.\",\"commands\":{\"allow\":[\"register\"],\"deny\":[]}},\"allow-register-all\":{\"identifier\":\"allow-register-all\",\"description\":\"Enables the register_all command without any pre-configured scope.\",\"commands\":{\"allow\":[\"register_all\"],\"deny\":[]}},\"allow-unregister\":{\"identifier\":\"allow-unregister\",\"description\":\"Enables the unregister command without any pre-configured scope.\",\"commands\":{\"allow\":[\"unregister\"],\"deny\":[]}},\"allow-unregister-all\":{\"identifier\":\"allow-unregister-all\",\"description\":\"Enables the unregister_all command without any pre-configured scope.\",\"commands\":{\"allow\":[\"unregister_all\"],\"deny\":[]}},\"deny-is-registered\":{\"identifier\":\"deny-is-registered\",\"description\":\"Denies the is_registered command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_registered\"]}},\"deny-register\":{\"identifier\":\"deny-register\",\"description\":\"Denies the register command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"register\"]}},\"deny-register-all\":{\"identifier\":\"deny-register-all\",\"description\":\"Denies the register_all command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"register_all\"]}},\"deny-unregister\":{\"identifier\":\"deny-unregister\",\"description\":\"Denies the unregister command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"unregister\"]}},\"deny-unregister-all\":{\"identifier\":\"deny-unregister-all\",\"description\":\"Denies the unregister_all command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"unregister_all\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"log\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"Allows the log command\",\"permissions\":[\"allow-log\"]},\"permissions\":{\"allow-log\":{\"identifier\":\"allow-log\",\"description\":\"Enables the log command without any pre-configured scope.\",\"commands\":{\"allow\":[\"log\"],\"deny\":[]}},\"deny-log\":{\"identifier\":\"deny-log\",\"description\":\"Denies the log command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"log\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"notification\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This permission set configures which\\nnotification features are by default exposed.\\n\\n#### Granted Permissions\\n\\nIt allows all notification related features.\\n\\n\",\"permissions\":[\"allow-is-permission-granted\",\"allow-request-permission\",\"allow-notify\",\"allow-register-action-types\",\"allow-register-listener\",\"allow-cancel\",\"allow-get-pending\",\"allow-remove-active\",\"allow-get-active\",\"allow-check-permissions\",\"allow-show\",\"allow-batch\",\"allow-list-channels\",\"allow-delete-channel\",\"allow-create-channel\",\"allow-permission-state\"]},\"permissions\":{\"allow-batch\":{\"identifier\":\"allow-batch\",\"description\":\"Enables the batch command without any pre-configured scope.\",\"commands\":{\"allow\":[\"batch\"],\"deny\":[]}},\"allow-cancel\":{\"identifier\":\"allow-cancel\",\"description\":\"Enables the cancel command without any pre-configured scope.\",\"commands\":{\"allow\":[\"cancel\"],\"deny\":[]}},\"allow-check-permissions\":{\"identifier\":\"allow-check-permissions\",\"description\":\"Enables the check_permissions command without any pre-configured scope.\",\"commands\":{\"allow\":[\"check_permissions\"],\"deny\":[]}},\"allow-create-channel\":{\"identifier\":\"allow-create-channel\",\"description\":\"Enables the create_channel command without any pre-configured scope.\",\"commands\":{\"allow\":[\"create_channel\"],\"deny\":[]}},\"allow-delete-channel\":{\"identifier\":\"allow-delete-channel\",\"description\":\"Enables the delete_channel command without any pre-configured scope.\",\"commands\":{\"allow\":[\"delete_channel\"],\"deny\":[]}},\"allow-get-active\":{\"identifier\":\"allow-get-active\",\"description\":\"Enables the get_active command without any pre-configured scope.\",\"commands\":{\"allow\":[\"get_active\"],\"deny\":[]}},\"allow-get-pending\":{\"identifier\":\"allow-get-pending\",\"description\":\"Enables the get_pending command without any pre-configured scope.\",\"commands\":{\"allow\":[\"get_pending\"],\"deny\":[]}},\"allow-is-permission-granted\":{\"identifier\":\"allow-is-permission-granted\",\"description\":\"Enables the is_permission_granted command without any pre-configured scope.\",\"commands\":{\"allow\":[\"is_permission_granted\"],\"deny\":[]}},\"allow-list-channels\":{\"identifier\":\"allow-list-channels\",\"description\":\"Enables the list_channels command without any pre-configured scope.\",\"commands\":{\"allow\":[\"list_channels\"],\"deny\":[]}},\"allow-notify\":{\"identifier\":\"allow-notify\",\"description\":\"Enables the notify command without any pre-configured scope.\",\"commands\":{\"allow\":[\"notify\"],\"deny\":[]}},\"allow-permission-state\":{\"identifier\":\"allow-permission-state\",\"description\":\"Enables the permission_state command without any pre-configured scope.\",\"commands\":{\"allow\":[\"permission_state\"],\"deny\":[]}},\"allow-register-action-types\":{\"identifier\":\"allow-register-action-types\",\"description\":\"Enables the register_action_types command without any pre-configured scope.\",\"commands\":{\"allow\":[\"register_action_types\"],\"deny\":[]}},\"allow-register-listener\":{\"identifier\":\"allow-register-listener\",\"description\":\"Enables the register_listener command without any pre-configured scope.\",\"commands\":{\"allow\":[\"register_listener\"],\"deny\":[]}},\"allow-remove-active\":{\"identifier\":\"allow-remove-active\",\"description\":\"Enables the remove_active command without any pre-configured scope.\",\"commands\":{\"allow\":[\"remove_active\"],\"deny\":[]}},\"allow-request-permission\":{\"identifier\":\"allow-request-permission\",\"description\":\"Enables the request_permission command without any pre-configured scope.\",\"commands\":{\"allow\":[\"request_permission\"],\"deny\":[]}},\"allow-show\":{\"identifier\":\"allow-show\",\"description\":\"Enables the show command without any pre-configured scope.\",\"commands\":{\"allow\":[\"show\"],\"deny\":[]}},\"deny-batch\":{\"identifier\":\"deny-batch\",\"description\":\"Denies the batch command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"batch\"]}},\"deny-cancel\":{\"identifier\":\"deny-cancel\",\"description\":\"Denies the cancel command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"cancel\"]}},\"deny-check-permissions\":{\"identifier\":\"deny-check-permissions\",\"description\":\"Denies the check_permissions command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"check_permissions\"]}},\"deny-create-channel\":{\"identifier\":\"deny-create-channel\",\"description\":\"Denies the create_channel command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"create_channel\"]}},\"deny-delete-channel\":{\"identifier\":\"deny-delete-channel\",\"description\":\"Denies the delete_channel command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"delete_channel\"]}},\"deny-get-active\":{\"identifier\":\"deny-get-active\",\"description\":\"Denies the get_active command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"get_active\"]}},\"deny-get-pending\":{\"identifier\":\"deny-get-pending\",\"description\":\"Denies the get_pending command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"get_pending\"]}},\"deny-is-permission-granted\":{\"identifier\":\"deny-is-permission-granted\",\"description\":\"Denies the is_permission_granted command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"is_permission_granted\"]}},\"deny-list-channels\":{\"identifier\":\"deny-list-channels\",\"description\":\"Denies the list_channels command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"list_channels\"]}},\"deny-notify\":{\"identifier\":\"deny-notify\",\"description\":\"Denies the notify command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"notify\"]}},\"deny-permission-state\":{\"identifier\":\"deny-permission-state\",\"description\":\"Denies the permission_state command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"permission_state\"]}},\"deny-register-action-types\":{\"identifier\":\"deny-register-action-types\",\"description\":\"Denies the register_action_types command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"register_action_types\"]}},\"deny-register-listener\":{\"identifier\":\"deny-register-listener\",\"description\":\"Denies the register_listener command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"register_listener\"]}},\"deny-remove-active\":{\"identifier\":\"deny-remove-active\",\"description\":\"Denies the remove_active command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"remove_active\"]}},\"deny-request-permission\":{\"identifier\":\"deny-request-permission\",\"description\":\"Denies the request_permission command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"request_permission\"]}},\"deny-show\":{\"identifier\":\"deny-show\",\"description\":\"Denies the show command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"show\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"process\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This permission set configures which\\nprocess features are by default exposed.\\n\\n#### Granted Permissions\\n\\nThis enables to quit via `allow-exit` and restart via `allow-restart`\\nthe application.\\n\",\"permissions\":[\"allow-exit\",\"allow-restart\"]},\"permissions\":{\"allow-exit\":{\"identifier\":\"allow-exit\",\"description\":\"Enables the exit command without any pre-configured scope.\",\"commands\":{\"allow\":[\"exit\"],\"deny\":[]}},\"allow-restart\":{\"identifier\":\"allow-restart\",\"description\":\"Enables the restart command without any pre-configured scope.\",\"commands\":{\"allow\":[\"restart\"],\"deny\":[]}},\"deny-exit\":{\"identifier\":\"deny-exit\",\"description\":\"Denies the exit command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"exit\"]}},\"deny-restart\":{\"identifier\":\"deny-restart\",\"description\":\"Denies the restart command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"restart\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"shell\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\",\"permissions\":[\"allow-open\"]},\"permissions\":{\"allow-execute\":{\"identifier\":\"allow-execute\",\"description\":\"Enables the execute command without any pre-configured scope.\",\"commands\":{\"allow\":[\"execute\"],\"deny\":[]}},\"allow-kill\":{\"identifier\":\"allow-kill\",\"description\":\"Enables the kill command without any pre-configured scope.\",\"commands\":{\"allow\":[\"kill\"],\"deny\":[]}},\"allow-open\":{\"identifier\":\"allow-open\",\"description\":\"Enables the open command without any pre-configured scope.\",\"commands\":{\"allow\":[\"open\"],\"deny\":[]}},\"allow-spawn\":{\"identifier\":\"allow-spawn\",\"description\":\"Enables the spawn command without any pre-configured scope.\",\"commands\":{\"allow\":[\"spawn\"],\"deny\":[]}},\"allow-stdin-write\":{\"identifier\":\"allow-stdin-write\",\"description\":\"Enables the stdin_write command without any pre-configured scope.\",\"commands\":{\"allow\":[\"stdin_write\"],\"deny\":[]}},\"deny-execute\":{\"identifier\":\"deny-execute\",\"description\":\"Denies the execute command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"execute\"]}},\"deny-kill\":{\"identifier\":\"deny-kill\",\"description\":\"Denies the kill command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"kill\"]}},\"deny-open\":{\"identifier\":\"deny-open\",\"description\":\"Denies the open command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"open\"]}},\"deny-spawn\":{\"identifier\":\"deny-spawn\",\"description\":\"Denies the spawn command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"spawn\"]}},\"deny-stdin-write\":{\"identifier\":\"deny-stdin-write\",\"description\":\"Denies the stdin_write command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"stdin_write\"]}}},\"permission_sets\":{},\"global_scope_schema\":{\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"anyOf\":[{\"additionalProperties\":false,\"properties\":{\"args\":{\"allOf\":[{\"$ref\":\"#/definitions/ShellScopeEntryAllowedArgs\"}],\"description\":\"The allowed arguments for the command execution.\"},\"cmd\":{\"description\":\"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\"type\":\"string\"},\"name\":{\"description\":\"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\"type\":\"string\"}},\"required\":[\"cmd\",\"name\"],\"type\":\"object\"},{\"additionalProperties\":false,\"properties\":{\"args\":{\"allOf\":[{\"$ref\":\"#/definitions/ShellScopeEntryAllowedArgs\"}],\"description\":\"The allowed arguments for the command execution.\"},\"name\":{\"description\":\"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\"type\":\"string\"},\"sidecar\":{\"description\":\"If this command is a sidecar command.\",\"type\":\"boolean\"}},\"required\":[\"name\",\"sidecar\"],\"type\":\"object\"}],\"definitions\":{\"ShellScopeEntryAllowedArg\":{\"anyOf\":[{\"description\":\"A non-configurable argument that is passed to the command in the order it was specified.\",\"type\":\"string\"},{\"additionalProperties\":false,\"description\":\"A variable that is set while calling the command from the webview API.\",\"properties\":{\"raw\":{\"default\":false,\"description\":\"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\\n\\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.\",\"type\":\"boolean\"},\"validator\":{\"description\":\"[regex] validator to require passed values to conform to an expected input.\\n\\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\\n\\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\\\w+` regex would be registered as `^https?://\\\\w+$`.\\n\\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>\",\"type\":\"string\"}},\"required\":[\"validator\"],\"type\":\"object\"}],\"description\":\"A command argument allowed to be executed by the webview API.\"},\"ShellScopeEntryAllowedArgs\":{\"anyOf\":[{\"description\":\"Use a simple boolean to allow all or disable all arguments to this command configuration.\",\"type\":\"boolean\"},{\"description\":\"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.\",\"items\":{\"$ref\":\"#/definitions/ShellScopeEntryAllowedArg\"},\"type\":\"array\"}],\"description\":\"A set of command arguments allowed to be executed by the webview API.\\n\\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.\"}},\"description\":\"Shell scope entry.\",\"title\":\"ShellScopeEntry\"}},\"store\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This permission set configures what kind of\\noperations are available from the store plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\",\"permissions\":[\"allow-load\",\"allow-get-store\",\"allow-set\",\"allow-get\",\"allow-has\",\"allow-delete\",\"allow-clear\",\"allow-reset\",\"allow-keys\",\"allow-values\",\"allow-entries\",\"allow-length\",\"allow-reload\",\"allow-save\"]},\"permissions\":{\"allow-clear\":{\"identifier\":\"allow-clear\",\"description\":\"Enables the clear command without any pre-configured scope.\",\"commands\":{\"allow\":[\"clear\"],\"deny\":[]}},\"allow-delete\":{\"identifier\":\"allow-delete\",\"description\":\"Enables the delete command without any pre-configured scope.\",\"commands\":{\"allow\":[\"delete\"],\"deny\":[]}},\"allow-entries\":{\"identifier\":\"allow-entries\",\"description\":\"Enables the entries command without any pre-configured scope.\",\"commands\":{\"allow\":[\"entries\"],\"deny\":[]}},\"allow-get\":{\"identifier\":\"allow-get\",\"description\":\"Enables the get command without any pre-configured scope.\",\"commands\":{\"allow\":[\"get\"],\"deny\":[]}},\"allow-get-store\":{\"identifier\":\"allow-get-store\",\"description\":\"Enables the get_store command without any pre-configured scope.\",\"commands\":{\"allow\":[\"get_store\"],\"deny\":[]}},\"allow-has\":{\"identifier\":\"allow-has\",\"description\":\"Enables the has command without any pre-configured scope.\",\"commands\":{\"allow\":[\"has\"],\"deny\":[]}},\"allow-keys\":{\"identifier\":\"allow-keys\",\"description\":\"Enables the keys command without any pre-configured scope.\",\"commands\":{\"allow\":[\"keys\"],\"deny\":[]}},\"allow-length\":{\"identifier\":\"allow-length\",\"description\":\"Enables the length command without any pre-configured scope.\",\"commands\":{\"allow\":[\"length\"],\"deny\":[]}},\"allow-load\":{\"identifier\":\"allow-load\",\"description\":\"Enables the load command without any pre-configured scope.\",\"commands\":{\"allow\":[\"load\"],\"deny\":[]}},\"allow-reload\":{\"identifier\":\"allow-reload\",\"description\":\"Enables the reload command without any pre-configured scope.\",\"commands\":{\"allow\":[\"reload\"],\"deny\":[]}},\"allow-reset\":{\"identifier\":\"allow-reset\",\"description\":\"Enables the reset command without any pre-configured scope.\",\"commands\":{\"allow\":[\"reset\"],\"deny\":[]}},\"allow-save\":{\"identifier\":\"allow-save\",\"description\":\"Enables the save command without any pre-configured scope.\",\"commands\":{\"allow\":[\"save\"],\"deny\":[]}},\"allow-set\":{\"identifier\":\"allow-set\",\"description\":\"Enables the set command without any pre-configured scope.\",\"commands\":{\"allow\":[\"set\"],\"deny\":[]}},\"allow-values\":{\"identifier\":\"allow-values\",\"description\":\"Enables the values command without any pre-configured scope.\",\"commands\":{\"allow\":[\"values\"],\"deny\":[]}},\"deny-clear\":{\"identifier\":\"deny-clear\",\"description\":\"Denies the clear command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"clear\"]}},\"deny-delete\":{\"identifier\":\"deny-delete\",\"description\":\"Denies the delete command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"delete\"]}},\"deny-entries\":{\"identifier\":\"deny-entries\",\"description\":\"Denies the entries command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"entries\"]}},\"deny-get\":{\"identifier\":\"deny-get\",\"description\":\"Denies the get command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"get\"]}},\"deny-get-store\":{\"identifier\":\"deny-get-store\",\"description\":\"Denies the get_store command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"get_store\"]}},\"deny-has\":{\"identifier\":\"deny-has\",\"description\":\"Denies the has command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"has\"]}},\"deny-keys\":{\"identifier\":\"deny-keys\",\"description\":\"Denies the keys command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"keys\"]}},\"deny-length\":{\"identifier\":\"deny-length\",\"description\":\"Denies the length command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"length\"]}},\"deny-load\":{\"identifier\":\"deny-load\",\"description\":\"Denies the load command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"load\"]}},\"deny-reload\":{\"identifier\":\"deny-reload\",\"description\":\"Denies the reload command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"reload\"]}},\"deny-reset\":{\"identifier\":\"deny-reset\",\"description\":\"Denies the reset command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"reset\"]}},\"deny-save\":{\"identifier\":\"deny-save\",\"description\":\"Denies the save command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"save\"]}},\"deny-set\":{\"identifier\":\"deny-set\",\"description\":\"Denies the set command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"set\"]}},\"deny-values\":{\"identifier\":\"deny-values\",\"description\":\"Denies the values command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"values\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"updater\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This permission set configures which kind of\\nupdater functions are exposed to the frontend.\\n\\n#### Granted Permissions\\n\\nThe full workflow from checking for updates to installing them\\nis enabled.\\n\\n\",\"permissions\":[\"allow-check\",\"allow-download\",\"allow-install\",\"allow-download-and-install\"]},\"permissions\":{\"allow-check\":{\"identifier\":\"allow-check\",\"description\":\"Enables the check command without any pre-configured scope.\",\"commands\":{\"allow\":[\"check\"],\"deny\":[]}},\"allow-download\":{\"identifier\":\"allow-download\",\"description\":\"Enables the download command without any pre-configured scope.\",\"commands\":{\"allow\":[\"download\"],\"deny\":[]}},\"allow-download-and-install\":{\"identifier\":\"allow-download-and-install\",\"description\":\"Enables the download_and_install command without any pre-configured scope.\",\"commands\":{\"allow\":[\"download_and_install\"],\"deny\":[]}},\"allow-install\":{\"identifier\":\"allow-install\",\"description\":\"Enables the install command without any pre-configured scope.\",\"commands\":{\"allow\":[\"install\"],\"deny\":[]}},\"deny-check\":{\"identifier\":\"deny-check\",\"description\":\"Denies the check command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"check\"]}},\"deny-download\":{\"identifier\":\"deny-download\",\"description\":\"Denies the download command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"download\"]}},\"deny-download-and-install\":{\"identifier\":\"deny-download-and-install\",\"description\":\"Denies the download_and_install command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"download_and_install\"]}},\"deny-install\":{\"identifier\":\"deny-install\",\"description\":\"Denies the install command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"install\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"upload\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This permission set configures what kind of\\noperations are available from the upload plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\",\"permissions\":[\"allow-upload\",\"allow-download\"]},\"permissions\":{\"allow-download\":{\"identifier\":\"allow-download\",\"description\":\"Enables the download command without any pre-configured scope.\",\"commands\":{\"allow\":[\"download\"],\"deny\":[]}},\"allow-upload\":{\"identifier\":\"allow-upload\",\"description\":\"Enables the upload command without any pre-configured scope.\",\"commands\":{\"allow\":[\"upload\"],\"deny\":[]}},\"deny-download\":{\"identifier\":\"deny-download\",\"description\":\"Denies the download command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"download\"]}},\"deny-upload\":{\"identifier\":\"deny-upload\",\"description\":\"Denies the upload command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"upload\"]}}},\"permission_sets\":{},\"global_scope_schema\":null},\"window-state\":{\"default_permission\":{\"identifier\":\"default\",\"description\":\"This permission set configures what kind of\\noperations are available from the window state plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\",\"permissions\":[\"allow-filename\",\"allow-restore-state\",\"allow-save-window-state\"]},\"permissions\":{\"allow-filename\":{\"identifier\":\"allow-filename\",\"description\":\"Enables the filename command without any pre-configured scope.\",\"commands\":{\"allow\":[\"filename\"],\"deny\":[]}},\"allow-restore-state\":{\"identifier\":\"allow-restore-state\",\"description\":\"Enables the restore_state command without any pre-configured scope.\",\"commands\":{\"allow\":[\"restore_state\"],\"deny\":[]}},\"allow-save-window-state\":{\"identifier\":\"allow-save-window-state\",\"description\":\"Enables the save_window_state command without any pre-configured scope.\",\"commands\":{\"allow\":[\"save_window_state\"],\"deny\":[]}},\"deny-filename\":{\"identifier\":\"deny-filename\",\"description\":\"Denies the filename command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"filename\"]}},\"deny-restore-state\":{\"identifier\":\"deny-restore-state\",\"description\":\"Denies the restore_state command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"restore_state\"]}},\"deny-save-window-state\":{\"identifier\":\"deny-save-window-state\",\"description\":\"Denies the save_window_state command without any pre-configured scope.\",\"commands\":{\"allow\":[],\"deny\":[\"save_window_state\"]}}},\"permission_sets\":{},\"global_scope_schema\":null}}"
  },
  {
    "path": "src-tauri/gen/schemas/capabilities.json",
    "content": "{\"desktop-capability\":{\"identifier\":\"desktop-capability\",\"description\":\"\",\"local\":true,\"windows\":[\"main\"],\"permissions\":[\"window-state:default\",\"global-shortcut:default\",\"global-shortcut:allow-is-registered\",\"global-shortcut:allow-register\",\"global-shortcut:allow-unregister\",\"shell:default\"],\"platforms\":[\"macOS\",\"windows\",\"linux\"]},\"main-capabilities\":{\"identifier\":\"main-capabilities\",\"description\":\"Capability for desktop window\",\"local\":true,\"windows\":[\"main\"],\"permissions\":[\"store:allow-get\",\"store:allow-set\",\"store:allow-save\",\"store:allow-clear\",\"store:allow-delete\",\"store:allow-keys\",\"store:allow-load\",\"updater:allow-check\",\"updater:allow-download-and-install\",\"updater:allow-download\",\"updater:allow-install\",\"notification:default\",\"core:webview:allow-internal-toggle-devtools\",\"dialog:default\",\"upload:allow-download\",\"clipboard-manager:allow-write-text\",\"shell:default\",\"shell:allow-open\"]},\"migrated\":{\"identifier\":\"migrated\",\"description\":\"permissions that were migrated from v1\",\"local\":true,\"windows\":[\"main\"],\"permissions\":[\"core:path:default\",\"core:event:default\",\"core:window:default\",\"core:app:default\",\"core:resources:default\",\"core:menu:default\",\"core:tray:default\",\"core:window:allow-set-fullscreen\",\"shell:default\"]}}"
  },
  {
    "path": "src-tauri/gen/schemas/desktop-schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"CapabilityFile\",\n  \"description\": \"Capability formats accepted in a capability file.\",\n  \"anyOf\": [\n    {\n      \"description\": \"A single capability.\",\n      \"allOf\": [\n        {\n          \"$ref\": \"#/definitions/Capability\"\n        }\n      ]\n    },\n    {\n      \"description\": \"A list of capabilities.\",\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/Capability\"\n      }\n    },\n    {\n      \"description\": \"A list of capabilities.\",\n      \"type\": \"object\",\n      \"required\": [\n        \"capabilities\"\n      ],\n      \"properties\": {\n        \"capabilities\": {\n          \"description\": \"The list of capabilities.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/Capability\"\n          }\n        }\n      }\n    }\n  ],\n  \"definitions\": {\n    \"Capability\": {\n      \"description\": \"A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\\n\\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\\n\\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\\n\\n## Example\\n\\n```json { \\\"identifier\\\": \\\"main-user-files-write\\\", \\\"description\\\": \\\"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\\\", \\\"windows\\\": [ \\\"main\\\" ], \\\"permissions\\\": [ \\\"core:default\\\", \\\"dialog:open\\\", { \\\"identifier\\\": \\\"fs:allow-write-text-file\\\", \\\"allow\\\": [{ \\\"path\\\": \\\"$HOME/test.txt\\\" }] }, ], \\\"platforms\\\": [\\\"macOS\\\",\\\"windows\\\"] } ```\",\n      \"type\": \"object\",\n      \"required\": [\n        \"identifier\",\n        \"permissions\"\n      ],\n      \"properties\": {\n        \"identifier\": {\n          \"description\": \"Identifier of the capability.\\n\\n## Example\\n\\n`main-user-files-write`\",\n          \"type\": \"string\"\n        },\n        \"description\": {\n          \"description\": \"Description of what the capability is intended to allow on associated windows.\\n\\nIt should contain a description of what the grouped permissions should allow.\\n\\n## Example\\n\\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\",\n          \"default\": \"\",\n          \"type\": \"string\"\n        },\n        \"remote\": {\n          \"description\": \"Configure remote URLs that can use the capability permissions.\\n\\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\\n\\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\\n\\n## Example\\n\\n```json { \\\"urls\\\": [\\\"https://*.mydomain.dev\\\"] } ```\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/CapabilityRemote\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"local\": {\n          \"description\": \"Whether this capability is enabled for local app URLs or not. Defaults to `true`.\",\n          \"default\": true,\n          \"type\": \"boolean\"\n        },\n        \"windows\": {\n          \"description\": \"List of windows that are affected by this capability. Can be a glob pattern.\\n\\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\\n\\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\\n\\n## Example\\n\\n`[\\\"main\\\"]`\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"webviews\": {\n          \"description\": \"List of webviews that are affected by this capability. Can be a glob pattern.\\n\\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\\n\\n## Example\\n\\n`[\\\"sub-webview-one\\\", \\\"sub-webview-two\\\"]`\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"permissions\": {\n          \"description\": \"List of permissions attached to this capability.\\n\\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\\n\\n## Example\\n\\n```json [ \\\"core:default\\\", \\\"shell:allow-open\\\", \\\"dialog:open\\\", { \\\"identifier\\\": \\\"fs:allow-write-text-file\\\", \\\"allow\\\": [{ \\\"path\\\": \\\"$HOME/test.txt\\\" }] } ] ```\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/PermissionEntry\"\n          },\n          \"uniqueItems\": true\n        },\n        \"platforms\": {\n          \"description\": \"Limit which target platforms this capability applies to.\\n\\nBy default all platforms are targeted.\\n\\n## Example\\n\\n`[\\\"macOS\\\",\\\"windows\\\"]`\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/definitions/Target\"\n          }\n        }\n      }\n    },\n    \"CapabilityRemote\": {\n      \"description\": \"Configuration for remote URLs that are associated with the capability.\",\n      \"type\": \"object\",\n      \"required\": [\n        \"urls\"\n      ],\n      \"properties\": {\n        \"urls\": {\n          \"description\": \"Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\\n\\n## Examples\\n\\n- \\\"https://*.mydomain.dev\\\": allows subdomains of mydomain.dev - \\\"https://mydomain.dev/api/*\\\": allows any subpath of mydomain.dev/api\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"PermissionEntry\": {\n      \"description\": \"An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Reference a permission or permission set by identifier.\",\n          \"allOf\": [\n            {\n              \"$ref\": \"#/definitions/Identifier\"\n            }\n          ]\n        },\n        {\n          \"description\": \"Reference a permission or permission set by identifier and extends its scope.\",\n          \"type\": \"object\",\n          \"allOf\": [\n            {\n              \"if\": {\n                \"properties\": {\n                  \"identifier\": {\n                    \"anyOf\": [\n                      {\n                        \"description\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:default\",\n                        \"markdownDescription\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\"\n                      },\n                      {\n                        \"description\": \"Enables the execute command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-execute\",\n                        \"markdownDescription\": \"Enables the execute command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the kill command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-kill\",\n                        \"markdownDescription\": \"Enables the kill command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-open\",\n                        \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the spawn command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-spawn\",\n                        \"markdownDescription\": \"Enables the spawn command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the stdin_write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-stdin-write\",\n                        \"markdownDescription\": \"Enables the stdin_write command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the execute command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-execute\",\n                        \"markdownDescription\": \"Denies the execute command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the kill command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-kill\",\n                        \"markdownDescription\": \"Denies the kill command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-open\",\n                        \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the spawn command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-spawn\",\n                        \"markdownDescription\": \"Denies the spawn command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the stdin_write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-stdin-write\",\n                        \"markdownDescription\": \"Denies the stdin_write command without any pre-configured scope.\"\n                      }\n                    ]\n                  }\n                }\n              },\n              \"then\": {\n                \"properties\": {\n                  \"allow\": {\n                    \"items\": {\n                      \"title\": \"ShellScopeEntry\",\n                      \"description\": \"Shell scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"cmd\",\n                            \"name\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"cmd\": {\n                              \"description\": \"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"name\",\n                            \"sidecar\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            },\n                            \"sidecar\": {\n                              \"description\": \"If this command is a sidecar command.\",\n                              \"type\": \"boolean\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        }\n                      ]\n                    }\n                  },\n                  \"deny\": {\n                    \"items\": {\n                      \"title\": \"ShellScopeEntry\",\n                      \"description\": \"Shell scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"cmd\",\n                            \"name\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"cmd\": {\n                              \"description\": \"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"name\",\n                            \"sidecar\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            },\n                            \"sidecar\": {\n                              \"description\": \"If this command is a sidecar command.\",\n                              \"type\": \"boolean\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        }\n                      ]\n                    }\n                  }\n                }\n              },\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                }\n              }\n            },\n            {\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                },\n                \"allow\": {\n                  \"description\": \"Data that defines what is allowed by the scope.\",\n                  \"type\": [\n                    \"array\",\n                    \"null\"\n                  ],\n                  \"items\": {\n                    \"$ref\": \"#/definitions/Value\"\n                  }\n                },\n                \"deny\": {\n                  \"description\": \"Data that defines what is denied by the scope. This should be prioritized by validation logic.\",\n                  \"type\": [\n                    \"array\",\n                    \"null\"\n                  ],\n                  \"items\": {\n                    \"$ref\": \"#/definitions/Value\"\n                  }\n                }\n              }\n            }\n          ],\n          \"required\": [\n            \"identifier\"\n          ]\n        }\n      ]\n    },\n    \"Identifier\": {\n      \"description\": \"Permission identifier\",\n      \"oneOf\": [\n        {\n          \"description\": \"No features are enabled by default, as we believe\\nthe clipboard can be inherently dangerous and it is \\napplication specific if read and/or write access is needed.\\n\\nClipboard interaction needs to be explicitly enabled.\\n\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:default\",\n          \"markdownDescription\": \"No features are enabled by default, as we believe\\nthe clipboard can be inherently dangerous and it is \\napplication specific if read and/or write access is needed.\\n\\nClipboard interaction needs to be explicitly enabled.\\n\"\n        },\n        {\n          \"description\": \"Enables the clear command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:allow-clear\",\n          \"markdownDescription\": \"Enables the clear command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_image command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:allow-read-image\",\n          \"markdownDescription\": \"Enables the read_image command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:allow-read-text\",\n          \"markdownDescription\": \"Enables the read_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write_html command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:allow-write-html\",\n          \"markdownDescription\": \"Enables the write_html command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write_image command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:allow-write-image\",\n          \"markdownDescription\": \"Enables the write_image command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:allow-write-text\",\n          \"markdownDescription\": \"Enables the write_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the clear command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:deny-clear\",\n          \"markdownDescription\": \"Denies the clear command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_image command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:deny-read-image\",\n          \"markdownDescription\": \"Denies the read_image command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:deny-read-text\",\n          \"markdownDescription\": \"Denies the read_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the write_html command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:deny-write-html\",\n          \"markdownDescription\": \"Denies the write_html command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the write_image command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:deny-write-image\",\n          \"markdownDescription\": \"Denies the write_image command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the write_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:deny-write-text\",\n          \"markdownDescription\": \"Denies the write_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default core plugins set.\\n#### This default permission set includes:\\n\\n- `core:path:default`\\n- `core:event:default`\\n- `core:window:default`\\n- `core:webview:default`\\n- `core:app:default`\\n- `core:image:default`\\n- `core:resources:default`\\n- `core:menu:default`\\n- `core:tray:default`\",\n          \"type\": \"string\",\n          \"const\": \"core:default\",\n          \"markdownDescription\": \"Default core plugins set.\\n#### This default permission set includes:\\n\\n- `core:path:default`\\n- `core:event:default`\\n- `core:window:default`\\n- `core:webview:default`\\n- `core:app:default`\\n- `core:image:default`\\n- `core:resources:default`\\n- `core:menu:default`\\n- `core:tray:default`\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-version`\\n- `allow-name`\\n- `allow-tauri-version`\\n- `allow-identifier`\\n- `allow-bundle-type`\\n- `allow-register-listener`\\n- `allow-remove-listener`\",\n          \"type\": \"string\",\n          \"const\": \"core:app:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-version`\\n- `allow-name`\\n- `allow-tauri-version`\\n- `allow-identifier`\\n- `allow-bundle-type`\\n- `allow-register-listener`\\n- `allow-remove-listener`\"\n        },\n        {\n          \"description\": \"Enables the app_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-app-hide\",\n          \"markdownDescription\": \"Enables the app_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the app_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-app-show\",\n          \"markdownDescription\": \"Enables the app_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the bundle_type command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-bundle-type\",\n          \"markdownDescription\": \"Enables the bundle_type command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the default_window_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-default-window-icon\",\n          \"markdownDescription\": \"Enables the default_window_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the fetch_data_store_identifiers command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-fetch-data-store-identifiers\",\n          \"markdownDescription\": \"Enables the fetch_data_store_identifiers command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the identifier command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-identifier\",\n          \"markdownDescription\": \"Enables the identifier command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the name command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-name\",\n          \"markdownDescription\": \"Enables the name command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-register-listener\",\n          \"markdownDescription\": \"Enables the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_data_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-remove-data-store\",\n          \"markdownDescription\": \"Enables the remove_data_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-remove-listener\",\n          \"markdownDescription\": \"Enables the remove_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_app_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-set-app-theme\",\n          \"markdownDescription\": \"Enables the set_app_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_dock_visibility command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-set-dock-visibility\",\n          \"markdownDescription\": \"Enables the set_dock_visibility command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the tauri_version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-tauri-version\",\n          \"markdownDescription\": \"Enables the tauri_version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-version\",\n          \"markdownDescription\": \"Enables the version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the app_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-app-hide\",\n          \"markdownDescription\": \"Denies the app_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the app_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-app-show\",\n          \"markdownDescription\": \"Denies the app_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the bundle_type command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-bundle-type\",\n          \"markdownDescription\": \"Denies the bundle_type command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the default_window_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-default-window-icon\",\n          \"markdownDescription\": \"Denies the default_window_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the fetch_data_store_identifiers command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-fetch-data-store-identifiers\",\n          \"markdownDescription\": \"Denies the fetch_data_store_identifiers command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the identifier command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-identifier\",\n          \"markdownDescription\": \"Denies the identifier command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the name command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-name\",\n          \"markdownDescription\": \"Denies the name command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-register-listener\",\n          \"markdownDescription\": \"Denies the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_data_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-remove-data-store\",\n          \"markdownDescription\": \"Denies the remove_data_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-remove-listener\",\n          \"markdownDescription\": \"Denies the remove_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_app_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-set-app-theme\",\n          \"markdownDescription\": \"Denies the set_app_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_dock_visibility command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-set-dock-visibility\",\n          \"markdownDescription\": \"Denies the set_dock_visibility command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the tauri_version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-tauri-version\",\n          \"markdownDescription\": \"Denies the tauri_version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-version\",\n          \"markdownDescription\": \"Denies the version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-listen`\\n- `allow-unlisten`\\n- `allow-emit`\\n- `allow-emit-to`\",\n          \"type\": \"string\",\n          \"const\": \"core:event:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-listen`\\n- `allow-unlisten`\\n- `allow-emit`\\n- `allow-emit-to`\"\n        },\n        {\n          \"description\": \"Enables the emit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-emit\",\n          \"markdownDescription\": \"Enables the emit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the emit_to command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-emit-to\",\n          \"markdownDescription\": \"Enables the emit_to command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the listen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-listen\",\n          \"markdownDescription\": \"Enables the listen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unlisten command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-unlisten\",\n          \"markdownDescription\": \"Enables the unlisten command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the emit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-emit\",\n          \"markdownDescription\": \"Denies the emit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the emit_to command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-emit-to\",\n          \"markdownDescription\": \"Denies the emit_to command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the listen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-listen\",\n          \"markdownDescription\": \"Denies the listen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unlisten command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-unlisten\",\n          \"markdownDescription\": \"Denies the unlisten command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-from-bytes`\\n- `allow-from-path`\\n- `allow-rgba`\\n- `allow-size`\",\n          \"type\": \"string\",\n          \"const\": \"core:image:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-from-bytes`\\n- `allow-from-path`\\n- `allow-rgba`\\n- `allow-size`\"\n        },\n        {\n          \"description\": \"Enables the from_bytes command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-from-bytes\",\n          \"markdownDescription\": \"Enables the from_bytes command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the from_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-from-path\",\n          \"markdownDescription\": \"Enables the from_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the rgba command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-rgba\",\n          \"markdownDescription\": \"Enables the rgba command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-size\",\n          \"markdownDescription\": \"Enables the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the from_bytes command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-from-bytes\",\n          \"markdownDescription\": \"Denies the from_bytes command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the from_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-from-path\",\n          \"markdownDescription\": \"Denies the from_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the rgba command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-rgba\",\n          \"markdownDescription\": \"Denies the rgba command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-size\",\n          \"markdownDescription\": \"Denies the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-append`\\n- `allow-prepend`\\n- `allow-insert`\\n- `allow-remove`\\n- `allow-remove-at`\\n- `allow-items`\\n- `allow-get`\\n- `allow-popup`\\n- `allow-create-default`\\n- `allow-set-as-app-menu`\\n- `allow-set-as-window-menu`\\n- `allow-text`\\n- `allow-set-text`\\n- `allow-is-enabled`\\n- `allow-set-enabled`\\n- `allow-set-accelerator`\\n- `allow-set-as-windows-menu-for-nsapp`\\n- `allow-set-as-help-menu-for-nsapp`\\n- `allow-is-checked`\\n- `allow-set-checked`\\n- `allow-set-icon`\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-append`\\n- `allow-prepend`\\n- `allow-insert`\\n- `allow-remove`\\n- `allow-remove-at`\\n- `allow-items`\\n- `allow-get`\\n- `allow-popup`\\n- `allow-create-default`\\n- `allow-set-as-app-menu`\\n- `allow-set-as-window-menu`\\n- `allow-text`\\n- `allow-set-text`\\n- `allow-is-enabled`\\n- `allow-set-enabled`\\n- `allow-set-accelerator`\\n- `allow-set-as-windows-menu-for-nsapp`\\n- `allow-set-as-help-menu-for-nsapp`\\n- `allow-is-checked`\\n- `allow-set-checked`\\n- `allow-set-icon`\"\n        },\n        {\n          \"description\": \"Enables the append command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-append\",\n          \"markdownDescription\": \"Enables the append command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_default command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-create-default\",\n          \"markdownDescription\": \"Enables the create_default command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-get\",\n          \"markdownDescription\": \"Enables the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the insert command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-insert\",\n          \"markdownDescription\": \"Enables the insert command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-is-checked\",\n          \"markdownDescription\": \"Enables the is_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-is-enabled\",\n          \"markdownDescription\": \"Enables the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the items command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-items\",\n          \"markdownDescription\": \"Enables the items command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the popup command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-popup\",\n          \"markdownDescription\": \"Enables the popup command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the prepend command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-prepend\",\n          \"markdownDescription\": \"Enables the prepend command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-remove\",\n          \"markdownDescription\": \"Enables the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_at command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-remove-at\",\n          \"markdownDescription\": \"Enables the remove_at command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_accelerator command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-accelerator\",\n          \"markdownDescription\": \"Enables the set_accelerator command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_app_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-app-menu\",\n          \"markdownDescription\": \"Enables the set_as_app_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-help-menu-for-nsapp\",\n          \"markdownDescription\": \"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_window_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-window-menu\",\n          \"markdownDescription\": \"Enables the set_as_window_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-windows-menu-for-nsapp\",\n          \"markdownDescription\": \"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-checked\",\n          \"markdownDescription\": \"Enables the set_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-enabled\",\n          \"markdownDescription\": \"Enables the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-text\",\n          \"markdownDescription\": \"Enables the set_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-text\",\n          \"markdownDescription\": \"Enables the text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the append command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-append\",\n          \"markdownDescription\": \"Denies the append command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_default command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-create-default\",\n          \"markdownDescription\": \"Denies the create_default command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-get\",\n          \"markdownDescription\": \"Denies the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the insert command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-insert\",\n          \"markdownDescription\": \"Denies the insert command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-is-checked\",\n          \"markdownDescription\": \"Denies the is_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-is-enabled\",\n          \"markdownDescription\": \"Denies the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the items command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-items\",\n          \"markdownDescription\": \"Denies the items command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the popup command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-popup\",\n          \"markdownDescription\": \"Denies the popup command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the prepend command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-prepend\",\n          \"markdownDescription\": \"Denies the prepend command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-remove\",\n          \"markdownDescription\": \"Denies the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_at command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-remove-at\",\n          \"markdownDescription\": \"Denies the remove_at command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_accelerator command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-accelerator\",\n          \"markdownDescription\": \"Denies the set_accelerator command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_app_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-app-menu\",\n          \"markdownDescription\": \"Denies the set_as_app_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-help-menu-for-nsapp\",\n          \"markdownDescription\": \"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_window_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-window-menu\",\n          \"markdownDescription\": \"Denies the set_as_window_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-windows-menu-for-nsapp\",\n          \"markdownDescription\": \"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-checked\",\n          \"markdownDescription\": \"Denies the set_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-enabled\",\n          \"markdownDescription\": \"Denies the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-text\",\n          \"markdownDescription\": \"Denies the set_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-text\",\n          \"markdownDescription\": \"Denies the text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-resolve-directory`\\n- `allow-resolve`\\n- `allow-normalize`\\n- `allow-join`\\n- `allow-dirname`\\n- `allow-extname`\\n- `allow-basename`\\n- `allow-is-absolute`\",\n          \"type\": \"string\",\n          \"const\": \"core:path:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-resolve-directory`\\n- `allow-resolve`\\n- `allow-normalize`\\n- `allow-join`\\n- `allow-dirname`\\n- `allow-extname`\\n- `allow-basename`\\n- `allow-is-absolute`\"\n        },\n        {\n          \"description\": \"Enables the basename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-basename\",\n          \"markdownDescription\": \"Enables the basename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the dirname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-dirname\",\n          \"markdownDescription\": \"Enables the dirname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the extname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-extname\",\n          \"markdownDescription\": \"Enables the extname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_absolute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-is-absolute\",\n          \"markdownDescription\": \"Enables the is_absolute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the join command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-join\",\n          \"markdownDescription\": \"Enables the join command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the normalize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-normalize\",\n          \"markdownDescription\": \"Enables the normalize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the resolve command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-resolve\",\n          \"markdownDescription\": \"Enables the resolve command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the resolve_directory command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-resolve-directory\",\n          \"markdownDescription\": \"Enables the resolve_directory command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the basename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-basename\",\n          \"markdownDescription\": \"Denies the basename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the dirname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-dirname\",\n          \"markdownDescription\": \"Denies the dirname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the extname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-extname\",\n          \"markdownDescription\": \"Denies the extname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_absolute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-is-absolute\",\n          \"markdownDescription\": \"Denies the is_absolute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the join command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-join\",\n          \"markdownDescription\": \"Denies the join command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the normalize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-normalize\",\n          \"markdownDescription\": \"Denies the normalize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the resolve command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-resolve\",\n          \"markdownDescription\": \"Denies the resolve command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the resolve_directory command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-resolve-directory\",\n          \"markdownDescription\": \"Denies the resolve_directory command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-close`\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-close`\"\n        },\n        {\n          \"description\": \"Enables the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:allow-close\",\n          \"markdownDescription\": \"Enables the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:deny-close\",\n          \"markdownDescription\": \"Denies the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-get-by-id`\\n- `allow-remove-by-id`\\n- `allow-set-icon`\\n- `allow-set-menu`\\n- `allow-set-tooltip`\\n- `allow-set-title`\\n- `allow-set-visible`\\n- `allow-set-temp-dir-path`\\n- `allow-set-icon-as-template`\\n- `allow-set-show-menu-on-left-click`\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-get-by-id`\\n- `allow-remove-by-id`\\n- `allow-set-icon`\\n- `allow-set-menu`\\n- `allow-set-tooltip`\\n- `allow-set-title`\\n- `allow-set-visible`\\n- `allow-set-temp-dir-path`\\n- `allow-set-icon-as-template`\\n- `allow-set-show-menu-on-left-click`\"\n        },\n        {\n          \"description\": \"Enables the get_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-get-by-id\",\n          \"markdownDescription\": \"Enables the get_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-remove-by-id\",\n          \"markdownDescription\": \"Enables the remove_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon_as_template command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-icon-as-template\",\n          \"markdownDescription\": \"Enables the set_icon_as_template command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-menu\",\n          \"markdownDescription\": \"Enables the set_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_show_menu_on_left_click command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-show-menu-on-left-click\",\n          \"markdownDescription\": \"Enables the set_show_menu_on_left_click command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_temp_dir_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-temp-dir-path\",\n          \"markdownDescription\": \"Enables the set_temp_dir_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-title\",\n          \"markdownDescription\": \"Enables the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_tooltip command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-tooltip\",\n          \"markdownDescription\": \"Enables the set_tooltip command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-visible\",\n          \"markdownDescription\": \"Enables the set_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-get-by-id\",\n          \"markdownDescription\": \"Denies the get_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-remove-by-id\",\n          \"markdownDescription\": \"Denies the remove_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon_as_template command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-icon-as-template\",\n          \"markdownDescription\": \"Denies the set_icon_as_template command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-menu\",\n          \"markdownDescription\": \"Denies the set_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_show_menu_on_left_click command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-show-menu-on-left-click\",\n          \"markdownDescription\": \"Denies the set_show_menu_on_left_click command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_temp_dir_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-temp-dir-path\",\n          \"markdownDescription\": \"Denies the set_temp_dir_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-title\",\n          \"markdownDescription\": \"Denies the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_tooltip command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-tooltip\",\n          \"markdownDescription\": \"Denies the set_tooltip command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-visible\",\n          \"markdownDescription\": \"Denies the set_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-webviews`\\n- `allow-webview-position`\\n- `allow-webview-size`\\n- `allow-internal-toggle-devtools`\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-webviews`\\n- `allow-webview-position`\\n- `allow-webview-size`\\n- `allow-internal-toggle-devtools`\"\n        },\n        {\n          \"description\": \"Enables the clear_all_browsing_data command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-clear-all-browsing-data\",\n          \"markdownDescription\": \"Enables the clear_all_browsing_data command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_webview command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-create-webview\",\n          \"markdownDescription\": \"Enables the create_webview command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_webview_window command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-create-webview-window\",\n          \"markdownDescription\": \"Enables the create_webview_window command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_all_webviews command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-get-all-webviews\",\n          \"markdownDescription\": \"Enables the get_all_webviews command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the internal_toggle_devtools command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-internal-toggle-devtools\",\n          \"markdownDescription\": \"Enables the internal_toggle_devtools command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the print command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-print\",\n          \"markdownDescription\": \"Enables the print command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the reparent command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-reparent\",\n          \"markdownDescription\": \"Enables the reparent command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_auto_resize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-auto-resize\",\n          \"markdownDescription\": \"Enables the set_webview_auto_resize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-background-color\",\n          \"markdownDescription\": \"Enables the set_webview_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-focus\",\n          \"markdownDescription\": \"Enables the set_webview_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-position\",\n          \"markdownDescription\": \"Enables the set_webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-size\",\n          \"markdownDescription\": \"Enables the set_webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_zoom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-zoom\",\n          \"markdownDescription\": \"Enables the set_webview_zoom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-close\",\n          \"markdownDescription\": \"Enables the webview_close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-hide\",\n          \"markdownDescription\": \"Enables the webview_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-position\",\n          \"markdownDescription\": \"Enables the webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-show\",\n          \"markdownDescription\": \"Enables the webview_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-size\",\n          \"markdownDescription\": \"Enables the webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the clear_all_browsing_data command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-clear-all-browsing-data\",\n          \"markdownDescription\": \"Denies the clear_all_browsing_data command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_webview command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-create-webview\",\n          \"markdownDescription\": \"Denies the create_webview command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_webview_window command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-create-webview-window\",\n          \"markdownDescription\": \"Denies the create_webview_window command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_all_webviews command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-get-all-webviews\",\n          \"markdownDescription\": \"Denies the get_all_webviews command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the internal_toggle_devtools command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-internal-toggle-devtools\",\n          \"markdownDescription\": \"Denies the internal_toggle_devtools command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the print command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-print\",\n          \"markdownDescription\": \"Denies the print command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the reparent command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-reparent\",\n          \"markdownDescription\": \"Denies the reparent command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_auto_resize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-auto-resize\",\n          \"markdownDescription\": \"Denies the set_webview_auto_resize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-background-color\",\n          \"markdownDescription\": \"Denies the set_webview_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-focus\",\n          \"markdownDescription\": \"Denies the set_webview_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-position\",\n          \"markdownDescription\": \"Denies the set_webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-size\",\n          \"markdownDescription\": \"Denies the set_webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_zoom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-zoom\",\n          \"markdownDescription\": \"Denies the set_webview_zoom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-close\",\n          \"markdownDescription\": \"Denies the webview_close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-hide\",\n          \"markdownDescription\": \"Denies the webview_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-position\",\n          \"markdownDescription\": \"Denies the webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-show\",\n          \"markdownDescription\": \"Denies the webview_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-size\",\n          \"markdownDescription\": \"Denies the webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-windows`\\n- `allow-scale-factor`\\n- `allow-inner-position`\\n- `allow-outer-position`\\n- `allow-inner-size`\\n- `allow-outer-size`\\n- `allow-is-fullscreen`\\n- `allow-is-minimized`\\n- `allow-is-maximized`\\n- `allow-is-focused`\\n- `allow-is-decorated`\\n- `allow-is-resizable`\\n- `allow-is-maximizable`\\n- `allow-is-minimizable`\\n- `allow-is-closable`\\n- `allow-is-visible`\\n- `allow-is-enabled`\\n- `allow-title`\\n- `allow-current-monitor`\\n- `allow-primary-monitor`\\n- `allow-monitor-from-point`\\n- `allow-available-monitors`\\n- `allow-cursor-position`\\n- `allow-theme`\\n- `allow-is-always-on-top`\\n- `allow-internal-toggle-maximize`\",\n          \"type\": \"string\",\n          \"const\": \"core:window:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-windows`\\n- `allow-scale-factor`\\n- `allow-inner-position`\\n- `allow-outer-position`\\n- `allow-inner-size`\\n- `allow-outer-size`\\n- `allow-is-fullscreen`\\n- `allow-is-minimized`\\n- `allow-is-maximized`\\n- `allow-is-focused`\\n- `allow-is-decorated`\\n- `allow-is-resizable`\\n- `allow-is-maximizable`\\n- `allow-is-minimizable`\\n- `allow-is-closable`\\n- `allow-is-visible`\\n- `allow-is-enabled`\\n- `allow-title`\\n- `allow-current-monitor`\\n- `allow-primary-monitor`\\n- `allow-monitor-from-point`\\n- `allow-available-monitors`\\n- `allow-cursor-position`\\n- `allow-theme`\\n- `allow-is-always-on-top`\\n- `allow-internal-toggle-maximize`\"\n        },\n        {\n          \"description\": \"Enables the available_monitors command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-available-monitors\",\n          \"markdownDescription\": \"Enables the available_monitors command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the center command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-center\",\n          \"markdownDescription\": \"Enables the center command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-close\",\n          \"markdownDescription\": \"Enables the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-create\",\n          \"markdownDescription\": \"Enables the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the current_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-current-monitor\",\n          \"markdownDescription\": \"Enables the current_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-cursor-position\",\n          \"markdownDescription\": \"Enables the cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the destroy command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-destroy\",\n          \"markdownDescription\": \"Enables the destroy command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_all_windows command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-get-all-windows\",\n          \"markdownDescription\": \"Enables the get_all_windows command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-hide\",\n          \"markdownDescription\": \"Enables the hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the inner_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-inner-position\",\n          \"markdownDescription\": \"Enables the inner_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the inner_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-inner-size\",\n          \"markdownDescription\": \"Enables the inner_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the internal_toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-internal-toggle-maximize\",\n          \"markdownDescription\": \"Enables the internal_toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-always-on-top\",\n          \"markdownDescription\": \"Enables the is_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-closable\",\n          \"markdownDescription\": \"Enables the is_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_decorated command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-decorated\",\n          \"markdownDescription\": \"Enables the is_decorated command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-enabled\",\n          \"markdownDescription\": \"Enables the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_focused command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-focused\",\n          \"markdownDescription\": \"Enables the is_focused command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-fullscreen\",\n          \"markdownDescription\": \"Enables the is_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-maximizable\",\n          \"markdownDescription\": \"Enables the is_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_maximized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-maximized\",\n          \"markdownDescription\": \"Enables the is_maximized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-minimizable\",\n          \"markdownDescription\": \"Enables the is_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_minimized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-minimized\",\n          \"markdownDescription\": \"Enables the is_minimized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-resizable\",\n          \"markdownDescription\": \"Enables the is_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-visible\",\n          \"markdownDescription\": \"Enables the is_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-maximize\",\n          \"markdownDescription\": \"Enables the maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the minimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-minimize\",\n          \"markdownDescription\": \"Enables the minimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the monitor_from_point command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-monitor-from-point\",\n          \"markdownDescription\": \"Enables the monitor_from_point command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the outer_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-outer-position\",\n          \"markdownDescription\": \"Enables the outer_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the outer_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-outer-size\",\n          \"markdownDescription\": \"Enables the outer_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the primary_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-primary-monitor\",\n          \"markdownDescription\": \"Enables the primary_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the request_user_attention command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-request-user-attention\",\n          \"markdownDescription\": \"Enables the request_user_attention command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the scale_factor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-scale-factor\",\n          \"markdownDescription\": \"Enables the scale_factor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_always_on_bottom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-always-on-bottom\",\n          \"markdownDescription\": \"Enables the set_always_on_bottom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-always-on-top\",\n          \"markdownDescription\": \"Enables the set_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-background-color\",\n          \"markdownDescription\": \"Enables the set_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_badge_count command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-badge-count\",\n          \"markdownDescription\": \"Enables the set_badge_count command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_badge_label command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-badge-label\",\n          \"markdownDescription\": \"Enables the set_badge_label command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-closable\",\n          \"markdownDescription\": \"Enables the set_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_content_protected command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-content-protected\",\n          \"markdownDescription\": \"Enables the set_content_protected command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_grab command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-grab\",\n          \"markdownDescription\": \"Enables the set_cursor_grab command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-icon\",\n          \"markdownDescription\": \"Enables the set_cursor_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-position\",\n          \"markdownDescription\": \"Enables the set_cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-visible\",\n          \"markdownDescription\": \"Enables the set_cursor_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_decorations command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-decorations\",\n          \"markdownDescription\": \"Enables the set_decorations command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_effects command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-effects\",\n          \"markdownDescription\": \"Enables the set_effects command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-enabled\",\n          \"markdownDescription\": \"Enables the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-focus\",\n          \"markdownDescription\": \"Enables the set_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_focusable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-focusable\",\n          \"markdownDescription\": \"Enables the set_focusable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-fullscreen\",\n          \"markdownDescription\": \"Enables the set_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_ignore_cursor_events command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-ignore-cursor-events\",\n          \"markdownDescription\": \"Enables the set_ignore_cursor_events command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_max_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-max-size\",\n          \"markdownDescription\": \"Enables the set_max_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-maximizable\",\n          \"markdownDescription\": \"Enables the set_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_min_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-min-size\",\n          \"markdownDescription\": \"Enables the set_min_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-minimizable\",\n          \"markdownDescription\": \"Enables the set_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_overlay_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-overlay-icon\",\n          \"markdownDescription\": \"Enables the set_overlay_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-position\",\n          \"markdownDescription\": \"Enables the set_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_progress_bar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-progress-bar\",\n          \"markdownDescription\": \"Enables the set_progress_bar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-resizable\",\n          \"markdownDescription\": \"Enables the set_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_shadow command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-shadow\",\n          \"markdownDescription\": \"Enables the set_shadow command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_simple_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-simple-fullscreen\",\n          \"markdownDescription\": \"Enables the set_simple_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-size\",\n          \"markdownDescription\": \"Enables the set_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_size_constraints command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-size-constraints\",\n          \"markdownDescription\": \"Enables the set_size_constraints command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_skip_taskbar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-skip-taskbar\",\n          \"markdownDescription\": \"Enables the set_skip_taskbar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-theme\",\n          \"markdownDescription\": \"Enables the set_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-title\",\n          \"markdownDescription\": \"Enables the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title_bar_style command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-title-bar-style\",\n          \"markdownDescription\": \"Enables the set_title_bar_style command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_visible_on_all_workspaces command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-visible-on-all-workspaces\",\n          \"markdownDescription\": \"Enables the set_visible_on_all_workspaces command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-show\",\n          \"markdownDescription\": \"Enables the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the start_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-start-dragging\",\n          \"markdownDescription\": \"Enables the start_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the start_resize_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-start-resize-dragging\",\n          \"markdownDescription\": \"Enables the start_resize_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-theme\",\n          \"markdownDescription\": \"Enables the theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-title\",\n          \"markdownDescription\": \"Enables the title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-toggle-maximize\",\n          \"markdownDescription\": \"Enables the toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unmaximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-unmaximize\",\n          \"markdownDescription\": \"Enables the unmaximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unminimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-unminimize\",\n          \"markdownDescription\": \"Enables the unminimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the available_monitors command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-available-monitors\",\n          \"markdownDescription\": \"Denies the available_monitors command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the center command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-center\",\n          \"markdownDescription\": \"Denies the center command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-close\",\n          \"markdownDescription\": \"Denies the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-create\",\n          \"markdownDescription\": \"Denies the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the current_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-current-monitor\",\n          \"markdownDescription\": \"Denies the current_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-cursor-position\",\n          \"markdownDescription\": \"Denies the cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the destroy command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-destroy\",\n          \"markdownDescription\": \"Denies the destroy command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_all_windows command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-get-all-windows\",\n          \"markdownDescription\": \"Denies the get_all_windows command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-hide\",\n          \"markdownDescription\": \"Denies the hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the inner_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-inner-position\",\n          \"markdownDescription\": \"Denies the inner_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the inner_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-inner-size\",\n          \"markdownDescription\": \"Denies the inner_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the internal_toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-internal-toggle-maximize\",\n          \"markdownDescription\": \"Denies the internal_toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-always-on-top\",\n          \"markdownDescription\": \"Denies the is_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-closable\",\n          \"markdownDescription\": \"Denies the is_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_decorated command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-decorated\",\n          \"markdownDescription\": \"Denies the is_decorated command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-enabled\",\n          \"markdownDescription\": \"Denies the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_focused command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-focused\",\n          \"markdownDescription\": \"Denies the is_focused command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-fullscreen\",\n          \"markdownDescription\": \"Denies the is_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-maximizable\",\n          \"markdownDescription\": \"Denies the is_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_maximized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-maximized\",\n          \"markdownDescription\": \"Denies the is_maximized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-minimizable\",\n          \"markdownDescription\": \"Denies the is_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_minimized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-minimized\",\n          \"markdownDescription\": \"Denies the is_minimized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-resizable\",\n          \"markdownDescription\": \"Denies the is_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-visible\",\n          \"markdownDescription\": \"Denies the is_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-maximize\",\n          \"markdownDescription\": \"Denies the maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the minimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-minimize\",\n          \"markdownDescription\": \"Denies the minimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the monitor_from_point command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-monitor-from-point\",\n          \"markdownDescription\": \"Denies the monitor_from_point command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the outer_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-outer-position\",\n          \"markdownDescription\": \"Denies the outer_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the outer_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-outer-size\",\n          \"markdownDescription\": \"Denies the outer_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the primary_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-primary-monitor\",\n          \"markdownDescription\": \"Denies the primary_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the request_user_attention command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-request-user-attention\",\n          \"markdownDescription\": \"Denies the request_user_attention command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the scale_factor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-scale-factor\",\n          \"markdownDescription\": \"Denies the scale_factor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_always_on_bottom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-always-on-bottom\",\n          \"markdownDescription\": \"Denies the set_always_on_bottom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-always-on-top\",\n          \"markdownDescription\": \"Denies the set_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-background-color\",\n          \"markdownDescription\": \"Denies the set_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_badge_count command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-badge-count\",\n          \"markdownDescription\": \"Denies the set_badge_count command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_badge_label command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-badge-label\",\n          \"markdownDescription\": \"Denies the set_badge_label command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-closable\",\n          \"markdownDescription\": \"Denies the set_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_content_protected command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-content-protected\",\n          \"markdownDescription\": \"Denies the set_content_protected command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_grab command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-grab\",\n          \"markdownDescription\": \"Denies the set_cursor_grab command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-icon\",\n          \"markdownDescription\": \"Denies the set_cursor_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-position\",\n          \"markdownDescription\": \"Denies the set_cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-visible\",\n          \"markdownDescription\": \"Denies the set_cursor_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_decorations command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-decorations\",\n          \"markdownDescription\": \"Denies the set_decorations command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_effects command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-effects\",\n          \"markdownDescription\": \"Denies the set_effects command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-enabled\",\n          \"markdownDescription\": \"Denies the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-focus\",\n          \"markdownDescription\": \"Denies the set_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_focusable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-focusable\",\n          \"markdownDescription\": \"Denies the set_focusable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-fullscreen\",\n          \"markdownDescription\": \"Denies the set_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_ignore_cursor_events command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-ignore-cursor-events\",\n          \"markdownDescription\": \"Denies the set_ignore_cursor_events command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_max_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-max-size\",\n          \"markdownDescription\": \"Denies the set_max_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-maximizable\",\n          \"markdownDescription\": \"Denies the set_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_min_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-min-size\",\n          \"markdownDescription\": \"Denies the set_min_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-minimizable\",\n          \"markdownDescription\": \"Denies the set_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_overlay_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-overlay-icon\",\n          \"markdownDescription\": \"Denies the set_overlay_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-position\",\n          \"markdownDescription\": \"Denies the set_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_progress_bar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-progress-bar\",\n          \"markdownDescription\": \"Denies the set_progress_bar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-resizable\",\n          \"markdownDescription\": \"Denies the set_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_shadow command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-shadow\",\n          \"markdownDescription\": \"Denies the set_shadow command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_simple_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-simple-fullscreen\",\n          \"markdownDescription\": \"Denies the set_simple_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-size\",\n          \"markdownDescription\": \"Denies the set_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_size_constraints command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-size-constraints\",\n          \"markdownDescription\": \"Denies the set_size_constraints command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_skip_taskbar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-skip-taskbar\",\n          \"markdownDescription\": \"Denies the set_skip_taskbar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-theme\",\n          \"markdownDescription\": \"Denies the set_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-title\",\n          \"markdownDescription\": \"Denies the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title_bar_style command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-title-bar-style\",\n          \"markdownDescription\": \"Denies the set_title_bar_style command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_visible_on_all_workspaces command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-visible-on-all-workspaces\",\n          \"markdownDescription\": \"Denies the set_visible_on_all_workspaces command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-show\",\n          \"markdownDescription\": \"Denies the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the start_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-start-dragging\",\n          \"markdownDescription\": \"Denies the start_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the start_resize_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-start-resize-dragging\",\n          \"markdownDescription\": \"Denies the start_resize_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-theme\",\n          \"markdownDescription\": \"Denies the theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-title\",\n          \"markdownDescription\": \"Denies the title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-toggle-maximize\",\n          \"markdownDescription\": \"Denies the toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unmaximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-unmaximize\",\n          \"markdownDescription\": \"Denies the unmaximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unminimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-unminimize\",\n          \"markdownDescription\": \"Denies the unminimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures the types of dialogs\\navailable from the dialog plugin.\\n\\n#### Granted Permissions\\n\\nAll dialog types are enabled.\\n\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-ask`\\n- `allow-confirm`\\n- `allow-message`\\n- `allow-save`\\n- `allow-open`\",\n          \"type\": \"string\",\n          \"const\": \"dialog:default\",\n          \"markdownDescription\": \"This permission set configures the types of dialogs\\navailable from the dialog plugin.\\n\\n#### Granted Permissions\\n\\nAll dialog types are enabled.\\n\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-ask`\\n- `allow-confirm`\\n- `allow-message`\\n- `allow-save`\\n- `allow-open`\"\n        },\n        {\n          \"description\": \"Enables the ask command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-ask\",\n          \"markdownDescription\": \"Enables the ask command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the confirm command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-confirm\",\n          \"markdownDescription\": \"Enables the confirm command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the message command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-message\",\n          \"markdownDescription\": \"Enables the message command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-save\",\n          \"markdownDescription\": \"Enables the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the ask command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-ask\",\n          \"markdownDescription\": \"Denies the ask command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the confirm command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-confirm\",\n          \"markdownDescription\": \"Denies the confirm command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the message command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-message\",\n          \"markdownDescription\": \"Denies the message command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-save\",\n          \"markdownDescription\": \"Denies the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"No features are enabled by default, as we believe\\nthe shortcuts can be inherently dangerous and it is\\napplication specific if specific shortcuts should be\\nregistered or unregistered.\\n\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:default\",\n          \"markdownDescription\": \"No features are enabled by default, as we believe\\nthe shortcuts can be inherently dangerous and it is\\napplication specific if specific shortcuts should be\\nregistered or unregistered.\\n\"\n        },\n        {\n          \"description\": \"Enables the is_registered command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:allow-is-registered\",\n          \"markdownDescription\": \"Enables the is_registered command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:allow-register\",\n          \"markdownDescription\": \"Enables the register command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register_all command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:allow-register-all\",\n          \"markdownDescription\": \"Enables the register_all command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unregister command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:allow-unregister\",\n          \"markdownDescription\": \"Enables the unregister command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unregister_all command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:allow-unregister-all\",\n          \"markdownDescription\": \"Enables the unregister_all command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_registered command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:deny-is-registered\",\n          \"markdownDescription\": \"Denies the is_registered command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:deny-register\",\n          \"markdownDescription\": \"Denies the register command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register_all command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:deny-register-all\",\n          \"markdownDescription\": \"Denies the register_all command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unregister command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:deny-unregister\",\n          \"markdownDescription\": \"Denies the unregister command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unregister_all command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:deny-unregister-all\",\n          \"markdownDescription\": \"Denies the unregister_all command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Allows the log command\\n#### This default permission set includes:\\n\\n- `allow-log`\",\n          \"type\": \"string\",\n          \"const\": \"log:default\",\n          \"markdownDescription\": \"Allows the log command\\n#### This default permission set includes:\\n\\n- `allow-log`\"\n        },\n        {\n          \"description\": \"Enables the log command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"log:allow-log\",\n          \"markdownDescription\": \"Enables the log command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the log command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"log:deny-log\",\n          \"markdownDescription\": \"Denies the log command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which\\nnotification features are by default exposed.\\n\\n#### Granted Permissions\\n\\nIt allows all notification related features.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-is-permission-granted`\\n- `allow-request-permission`\\n- `allow-notify`\\n- `allow-register-action-types`\\n- `allow-register-listener`\\n- `allow-cancel`\\n- `allow-get-pending`\\n- `allow-remove-active`\\n- `allow-get-active`\\n- `allow-check-permissions`\\n- `allow-show`\\n- `allow-batch`\\n- `allow-list-channels`\\n- `allow-delete-channel`\\n- `allow-create-channel`\\n- `allow-permission-state`\",\n          \"type\": \"string\",\n          \"const\": \"notification:default\",\n          \"markdownDescription\": \"This permission set configures which\\nnotification features are by default exposed.\\n\\n#### Granted Permissions\\n\\nIt allows all notification related features.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-is-permission-granted`\\n- `allow-request-permission`\\n- `allow-notify`\\n- `allow-register-action-types`\\n- `allow-register-listener`\\n- `allow-cancel`\\n- `allow-get-pending`\\n- `allow-remove-active`\\n- `allow-get-active`\\n- `allow-check-permissions`\\n- `allow-show`\\n- `allow-batch`\\n- `allow-list-channels`\\n- `allow-delete-channel`\\n- `allow-create-channel`\\n- `allow-permission-state`\"\n        },\n        {\n          \"description\": \"Enables the batch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-batch\",\n          \"markdownDescription\": \"Enables the batch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the cancel command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-cancel\",\n          \"markdownDescription\": \"Enables the cancel command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the check_permissions command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-check-permissions\",\n          \"markdownDescription\": \"Enables the check_permissions command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_channel command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-create-channel\",\n          \"markdownDescription\": \"Enables the create_channel command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the delete_channel command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-delete-channel\",\n          \"markdownDescription\": \"Enables the delete_channel command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_active command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-get-active\",\n          \"markdownDescription\": \"Enables the get_active command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_pending command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-get-pending\",\n          \"markdownDescription\": \"Enables the get_pending command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_permission_granted command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-is-permission-granted\",\n          \"markdownDescription\": \"Enables the is_permission_granted command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the list_channels command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-list-channels\",\n          \"markdownDescription\": \"Enables the list_channels command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the notify command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-notify\",\n          \"markdownDescription\": \"Enables the notify command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the permission_state command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-permission-state\",\n          \"markdownDescription\": \"Enables the permission_state command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register_action_types command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-register-action-types\",\n          \"markdownDescription\": \"Enables the register_action_types command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-register-listener\",\n          \"markdownDescription\": \"Enables the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_active command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-remove-active\",\n          \"markdownDescription\": \"Enables the remove_active command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the request_permission command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-request-permission\",\n          \"markdownDescription\": \"Enables the request_permission command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-show\",\n          \"markdownDescription\": \"Enables the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the batch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-batch\",\n          \"markdownDescription\": \"Denies the batch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the cancel command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-cancel\",\n          \"markdownDescription\": \"Denies the cancel command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the check_permissions command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-check-permissions\",\n          \"markdownDescription\": \"Denies the check_permissions command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_channel command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-create-channel\",\n          \"markdownDescription\": \"Denies the create_channel command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the delete_channel command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-delete-channel\",\n          \"markdownDescription\": \"Denies the delete_channel command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_active command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-get-active\",\n          \"markdownDescription\": \"Denies the get_active command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_pending command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-get-pending\",\n          \"markdownDescription\": \"Denies the get_pending command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_permission_granted command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-is-permission-granted\",\n          \"markdownDescription\": \"Denies the is_permission_granted command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the list_channels command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-list-channels\",\n          \"markdownDescription\": \"Denies the list_channels command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the notify command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-notify\",\n          \"markdownDescription\": \"Denies the notify command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the permission_state command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-permission-state\",\n          \"markdownDescription\": \"Denies the permission_state command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register_action_types command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-register-action-types\",\n          \"markdownDescription\": \"Denies the register_action_types command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-register-listener\",\n          \"markdownDescription\": \"Denies the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_active command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-remove-active\",\n          \"markdownDescription\": \"Denies the remove_active command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the request_permission command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-request-permission\",\n          \"markdownDescription\": \"Denies the request_permission command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-show\",\n          \"markdownDescription\": \"Denies the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which\\nprocess features are by default exposed.\\n\\n#### Granted Permissions\\n\\nThis enables to quit via `allow-exit` and restart via `allow-restart`\\nthe application.\\n\\n#### This default permission set includes:\\n\\n- `allow-exit`\\n- `allow-restart`\",\n          \"type\": \"string\",\n          \"const\": \"process:default\",\n          \"markdownDescription\": \"This permission set configures which\\nprocess features are by default exposed.\\n\\n#### Granted Permissions\\n\\nThis enables to quit via `allow-exit` and restart via `allow-restart`\\nthe application.\\n\\n#### This default permission set includes:\\n\\n- `allow-exit`\\n- `allow-restart`\"\n        },\n        {\n          \"description\": \"Enables the exit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:allow-exit\",\n          \"markdownDescription\": \"Enables the exit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the restart command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:allow-restart\",\n          \"markdownDescription\": \"Enables the restart command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the exit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:deny-exit\",\n          \"markdownDescription\": \"Denies the exit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the restart command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:deny-restart\",\n          \"markdownDescription\": \"Denies the restart command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\",\n          \"type\": \"string\",\n          \"const\": \"shell:default\",\n          \"markdownDescription\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\"\n        },\n        {\n          \"description\": \"Enables the execute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-execute\",\n          \"markdownDescription\": \"Enables the execute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the kill command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-kill\",\n          \"markdownDescription\": \"Enables the kill command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the spawn command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-spawn\",\n          \"markdownDescription\": \"Enables the spawn command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the stdin_write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-stdin-write\",\n          \"markdownDescription\": \"Enables the stdin_write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the execute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-execute\",\n          \"markdownDescription\": \"Denies the execute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the kill command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-kill\",\n          \"markdownDescription\": \"Denies the kill command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the spawn command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-spawn\",\n          \"markdownDescription\": \"Denies the spawn command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the stdin_write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-stdin-write\",\n          \"markdownDescription\": \"Denies the stdin_write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures what kind of\\noperations are available from the store plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-load`\\n- `allow-get-store`\\n- `allow-set`\\n- `allow-get`\\n- `allow-has`\\n- `allow-delete`\\n- `allow-clear`\\n- `allow-reset`\\n- `allow-keys`\\n- `allow-values`\\n- `allow-entries`\\n- `allow-length`\\n- `allow-reload`\\n- `allow-save`\",\n          \"type\": \"string\",\n          \"const\": \"store:default\",\n          \"markdownDescription\": \"This permission set configures what kind of\\noperations are available from the store plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-load`\\n- `allow-get-store`\\n- `allow-set`\\n- `allow-get`\\n- `allow-has`\\n- `allow-delete`\\n- `allow-clear`\\n- `allow-reset`\\n- `allow-keys`\\n- `allow-values`\\n- `allow-entries`\\n- `allow-length`\\n- `allow-reload`\\n- `allow-save`\"\n        },\n        {\n          \"description\": \"Enables the clear command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-clear\",\n          \"markdownDescription\": \"Enables the clear command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the delete command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-delete\",\n          \"markdownDescription\": \"Enables the delete command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the entries command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-entries\",\n          \"markdownDescription\": \"Enables the entries command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-get\",\n          \"markdownDescription\": \"Enables the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-get-store\",\n          \"markdownDescription\": \"Enables the get_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the has command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-has\",\n          \"markdownDescription\": \"Enables the has command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the keys command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-keys\",\n          \"markdownDescription\": \"Enables the keys command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the length command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-length\",\n          \"markdownDescription\": \"Enables the length command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the load command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-load\",\n          \"markdownDescription\": \"Enables the load command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the reload command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-reload\",\n          \"markdownDescription\": \"Enables the reload command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the reset command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-reset\",\n          \"markdownDescription\": \"Enables the reset command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-save\",\n          \"markdownDescription\": \"Enables the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-set\",\n          \"markdownDescription\": \"Enables the set command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the values command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-values\",\n          \"markdownDescription\": \"Enables the values command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the clear command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-clear\",\n          \"markdownDescription\": \"Denies the clear command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the delete command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-delete\",\n          \"markdownDescription\": \"Denies the delete command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the entries command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-entries\",\n          \"markdownDescription\": \"Denies the entries command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-get\",\n          \"markdownDescription\": \"Denies the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-get-store\",\n          \"markdownDescription\": \"Denies the get_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the has command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-has\",\n          \"markdownDescription\": \"Denies the has command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the keys command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-keys\",\n          \"markdownDescription\": \"Denies the keys command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the length command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-length\",\n          \"markdownDescription\": \"Denies the length command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the load command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-load\",\n          \"markdownDescription\": \"Denies the load command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the reload command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-reload\",\n          \"markdownDescription\": \"Denies the reload command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the reset command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-reset\",\n          \"markdownDescription\": \"Denies the reset command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-save\",\n          \"markdownDescription\": \"Denies the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-set\",\n          \"markdownDescription\": \"Denies the set command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the values command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-values\",\n          \"markdownDescription\": \"Denies the values command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which kind of\\nupdater functions are exposed to the frontend.\\n\\n#### Granted Permissions\\n\\nThe full workflow from checking for updates to installing them\\nis enabled.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-check`\\n- `allow-download`\\n- `allow-install`\\n- `allow-download-and-install`\",\n          \"type\": \"string\",\n          \"const\": \"updater:default\",\n          \"markdownDescription\": \"This permission set configures which kind of\\nupdater functions are exposed to the frontend.\\n\\n#### Granted Permissions\\n\\nThe full workflow from checking for updates to installing them\\nis enabled.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-check`\\n- `allow-download`\\n- `allow-install`\\n- `allow-download-and-install`\"\n        },\n        {\n          \"description\": \"Enables the check command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-check\",\n          \"markdownDescription\": \"Enables the check command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-download\",\n          \"markdownDescription\": \"Enables the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the download_and_install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-download-and-install\",\n          \"markdownDescription\": \"Enables the download_and_install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-install\",\n          \"markdownDescription\": \"Enables the install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the check command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-check\",\n          \"markdownDescription\": \"Denies the check command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-download\",\n          \"markdownDescription\": \"Denies the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the download_and_install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-download-and-install\",\n          \"markdownDescription\": \"Denies the download_and_install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-install\",\n          \"markdownDescription\": \"Denies the install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures what kind of\\noperations are available from the upload plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-upload`\\n- `allow-download`\",\n          \"type\": \"string\",\n          \"const\": \"upload:default\",\n          \"markdownDescription\": \"This permission set configures what kind of\\noperations are available from the upload plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-upload`\\n- `allow-download`\"\n        },\n        {\n          \"description\": \"Enables the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"upload:allow-download\",\n          \"markdownDescription\": \"Enables the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the upload command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"upload:allow-upload\",\n          \"markdownDescription\": \"Enables the upload command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"upload:deny-download\",\n          \"markdownDescription\": \"Denies the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the upload command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"upload:deny-upload\",\n          \"markdownDescription\": \"Denies the upload command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures what kind of\\noperations are available from the window state plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-filename`\\n- `allow-restore-state`\\n- `allow-save-window-state`\",\n          \"type\": \"string\",\n          \"const\": \"window-state:default\",\n          \"markdownDescription\": \"This permission set configures what kind of\\noperations are available from the window state plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-filename`\\n- `allow-restore-state`\\n- `allow-save-window-state`\"\n        },\n        {\n          \"description\": \"Enables the filename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"window-state:allow-filename\",\n          \"markdownDescription\": \"Enables the filename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the restore_state command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"window-state:allow-restore-state\",\n          \"markdownDescription\": \"Enables the restore_state command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the save_window_state command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"window-state:allow-save-window-state\",\n          \"markdownDescription\": \"Enables the save_window_state command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the filename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"window-state:deny-filename\",\n          \"markdownDescription\": \"Denies the filename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the restore_state command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"window-state:deny-restore-state\",\n          \"markdownDescription\": \"Denies the restore_state command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the save_window_state command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"window-state:deny-save-window-state\",\n          \"markdownDescription\": \"Denies the save_window_state command without any pre-configured scope.\"\n        }\n      ]\n    },\n    \"Value\": {\n      \"description\": \"All supported ACL values.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Represents a null JSON value.\",\n          \"type\": \"null\"\n        },\n        {\n          \"description\": \"Represents a [`bool`].\",\n          \"type\": \"boolean\"\n        },\n        {\n          \"description\": \"Represents a valid ACL [`Number`].\",\n          \"allOf\": [\n            {\n              \"$ref\": \"#/definitions/Number\"\n            }\n          ]\n        },\n        {\n          \"description\": \"Represents a [`String`].\",\n          \"type\": \"string\"\n        },\n        {\n          \"description\": \"Represents a list of other [`Value`]s.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/Value\"\n          }\n        },\n        {\n          \"description\": \"Represents a map of [`String`] keys to [`Value`]s.\",\n          \"type\": \"object\",\n          \"additionalProperties\": {\n            \"$ref\": \"#/definitions/Value\"\n          }\n        }\n      ]\n    },\n    \"Number\": {\n      \"description\": \"A valid ACL number.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Represents an [`i64`].\",\n          \"type\": \"integer\",\n          \"format\": \"int64\"\n        },\n        {\n          \"description\": \"Represents a [`f64`].\",\n          \"type\": \"number\",\n          \"format\": \"double\"\n        }\n      ]\n    },\n    \"Target\": {\n      \"description\": \"Platform target.\",\n      \"oneOf\": [\n        {\n          \"description\": \"MacOS.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"macOS\"\n          ]\n        },\n        {\n          \"description\": \"Windows.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"windows\"\n          ]\n        },\n        {\n          \"description\": \"Linux.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"linux\"\n          ]\n        },\n        {\n          \"description\": \"Android.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"android\"\n          ]\n        },\n        {\n          \"description\": \"iOS.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"iOS\"\n          ]\n        }\n      ]\n    },\n    \"ShellScopeEntryAllowedArg\": {\n      \"description\": \"A command argument allowed to be executed by the webview API.\",\n      \"anyOf\": [\n        {\n          \"description\": \"A non-configurable argument that is passed to the command in the order it was specified.\",\n          \"type\": \"string\"\n        },\n        {\n          \"description\": \"A variable that is set while calling the command from the webview API.\",\n          \"type\": \"object\",\n          \"required\": [\n            \"validator\"\n          ],\n          \"properties\": {\n            \"raw\": {\n              \"description\": \"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\\n\\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.\",\n              \"default\": false,\n              \"type\": \"boolean\"\n            },\n            \"validator\": {\n              \"description\": \"[regex] validator to require passed values to conform to an expected input.\\n\\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\\n\\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\\\w+` regex would be registered as `^https?://\\\\w+$`.\\n\\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>\",\n              \"type\": \"string\"\n            }\n          },\n          \"additionalProperties\": false\n        }\n      ]\n    },\n    \"ShellScopeEntryAllowedArgs\": {\n      \"description\": \"A set of command arguments allowed to be executed by the webview API.\\n\\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Use a simple boolean to allow all or disable all arguments to this command configuration.\",\n          \"type\": \"boolean\"\n        },\n        {\n          \"description\": \"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/ShellScopeEntryAllowedArg\"\n          }\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "src-tauri/gen/schemas/windows-schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"CapabilityFile\",\n  \"description\": \"Capability formats accepted in a capability file.\",\n  \"anyOf\": [\n    {\n      \"description\": \"A single capability.\",\n      \"allOf\": [\n        {\n          \"$ref\": \"#/definitions/Capability\"\n        }\n      ]\n    },\n    {\n      \"description\": \"A list of capabilities.\",\n      \"type\": \"array\",\n      \"items\": {\n        \"$ref\": \"#/definitions/Capability\"\n      }\n    },\n    {\n      \"description\": \"A list of capabilities.\",\n      \"type\": \"object\",\n      \"required\": [\n        \"capabilities\"\n      ],\n      \"properties\": {\n        \"capabilities\": {\n          \"description\": \"The list of capabilities.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/Capability\"\n          }\n        }\n      }\n    }\n  ],\n  \"definitions\": {\n    \"Capability\": {\n      \"description\": \"A grouping and boundary mechanism developers can use to isolate access to the IPC layer.\\n\\nIt controls application windows' and webviews' fine grained access to the Tauri core, application, or plugin commands. If a webview or its window is not matching any capability then it has no access to the IPC layer at all.\\n\\nThis can be done to create groups of windows, based on their required system access, which can reduce impact of frontend vulnerabilities in less privileged windows. Windows can be added to a capability by exact name (e.g. `main-window`) or glob patterns like `*` or `admin-*`. A Window can have none, one, or multiple associated capabilities.\\n\\n## Example\\n\\n```json { \\\"identifier\\\": \\\"main-user-files-write\\\", \\\"description\\\": \\\"This capability allows the `main` window on macOS and Windows access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\\\", \\\"windows\\\": [ \\\"main\\\" ], \\\"permissions\\\": [ \\\"core:default\\\", \\\"dialog:open\\\", { \\\"identifier\\\": \\\"fs:allow-write-text-file\\\", \\\"allow\\\": [{ \\\"path\\\": \\\"$HOME/test.txt\\\" }] }, ], \\\"platforms\\\": [\\\"macOS\\\",\\\"windows\\\"] } ```\",\n      \"type\": \"object\",\n      \"required\": [\n        \"identifier\",\n        \"permissions\"\n      ],\n      \"properties\": {\n        \"identifier\": {\n          \"description\": \"Identifier of the capability.\\n\\n## Example\\n\\n`main-user-files-write`\",\n          \"type\": \"string\"\n        },\n        \"description\": {\n          \"description\": \"Description of what the capability is intended to allow on associated windows.\\n\\nIt should contain a description of what the grouped permissions should allow.\\n\\n## Example\\n\\nThis capability allows the `main` window access to `filesystem` write related commands and `dialog` commands to enable programmatic access to files selected by the user.\",\n          \"default\": \"\",\n          \"type\": \"string\"\n        },\n        \"remote\": {\n          \"description\": \"Configure remote URLs that can use the capability permissions.\\n\\nThis setting is optional and defaults to not being set, as our default use case is that the content is served from our local application.\\n\\n:::caution Make sure you understand the security implications of providing remote sources with local system access. :::\\n\\n## Example\\n\\n```json { \\\"urls\\\": [\\\"https://*.mydomain.dev\\\"] } ```\",\n          \"anyOf\": [\n            {\n              \"$ref\": \"#/definitions/CapabilityRemote\"\n            },\n            {\n              \"type\": \"null\"\n            }\n          ]\n        },\n        \"local\": {\n          \"description\": \"Whether this capability is enabled for local app URLs or not. Defaults to `true`.\",\n          \"default\": true,\n          \"type\": \"boolean\"\n        },\n        \"windows\": {\n          \"description\": \"List of windows that are affected by this capability. Can be a glob pattern.\\n\\nIf a window label matches any of the patterns in this list, the capability will be enabled on all the webviews of that window, regardless of the value of [`Self::webviews`].\\n\\nOn multiwebview windows, prefer specifying [`Self::webviews`] and omitting [`Self::windows`] for a fine grained access control.\\n\\n## Example\\n\\n`[\\\"main\\\"]`\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"webviews\": {\n          \"description\": \"List of webviews that are affected by this capability. Can be a glob pattern.\\n\\nThe capability will be enabled on all the webviews whose label matches any of the patterns in this list, regardless of whether the webview's window label matches a pattern in [`Self::windows`].\\n\\n## Example\\n\\n`[\\\"sub-webview-one\\\", \\\"sub-webview-two\\\"]`\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"permissions\": {\n          \"description\": \"List of permissions attached to this capability.\\n\\nMust include the plugin name as prefix in the form of `${plugin-name}:${permission-name}`. For commands directly implemented in the application itself only `${permission-name}` is required.\\n\\n## Example\\n\\n```json [ \\\"core:default\\\", \\\"shell:allow-open\\\", \\\"dialog:open\\\", { \\\"identifier\\\": \\\"fs:allow-write-text-file\\\", \\\"allow\\\": [{ \\\"path\\\": \\\"$HOME/test.txt\\\" }] } ] ```\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/PermissionEntry\"\n          },\n          \"uniqueItems\": true\n        },\n        \"platforms\": {\n          \"description\": \"Limit which target platforms this capability applies to.\\n\\nBy default all platforms are targeted.\\n\\n## Example\\n\\n`[\\\"macOS\\\",\\\"windows\\\"]`\",\n          \"type\": [\n            \"array\",\n            \"null\"\n          ],\n          \"items\": {\n            \"$ref\": \"#/definitions/Target\"\n          }\n        }\n      }\n    },\n    \"CapabilityRemote\": {\n      \"description\": \"Configuration for remote URLs that are associated with the capability.\",\n      \"type\": \"object\",\n      \"required\": [\n        \"urls\"\n      ],\n      \"properties\": {\n        \"urls\": {\n          \"description\": \"Remote domains this capability refers to using the [URLPattern standard](https://urlpattern.spec.whatwg.org/).\\n\\n## Examples\\n\\n- \\\"https://*.mydomain.dev\\\": allows subdomains of mydomain.dev - \\\"https://mydomain.dev/api/*\\\": allows any subpath of mydomain.dev/api\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"PermissionEntry\": {\n      \"description\": \"An entry for a permission value in a [`Capability`] can be either a raw permission [`Identifier`] or an object that references a permission and extends its scope.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Reference a permission or permission set by identifier.\",\n          \"allOf\": [\n            {\n              \"$ref\": \"#/definitions/Identifier\"\n            }\n          ]\n        },\n        {\n          \"description\": \"Reference a permission or permission set by identifier and extends its scope.\",\n          \"type\": \"object\",\n          \"allOf\": [\n            {\n              \"if\": {\n                \"properties\": {\n                  \"identifier\": {\n                    \"anyOf\": [\n                      {\n                        \"description\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:default\",\n                        \"markdownDescription\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\"\n                      },\n                      {\n                        \"description\": \"Enables the execute command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-execute\",\n                        \"markdownDescription\": \"Enables the execute command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the kill command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-kill\",\n                        \"markdownDescription\": \"Enables the kill command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-open\",\n                        \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the spawn command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-spawn\",\n                        \"markdownDescription\": \"Enables the spawn command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Enables the stdin_write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:allow-stdin-write\",\n                        \"markdownDescription\": \"Enables the stdin_write command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the execute command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-execute\",\n                        \"markdownDescription\": \"Denies the execute command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the kill command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-kill\",\n                        \"markdownDescription\": \"Denies the kill command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the open command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-open\",\n                        \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the spawn command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-spawn\",\n                        \"markdownDescription\": \"Denies the spawn command without any pre-configured scope.\"\n                      },\n                      {\n                        \"description\": \"Denies the stdin_write command without any pre-configured scope.\",\n                        \"type\": \"string\",\n                        \"const\": \"shell:deny-stdin-write\",\n                        \"markdownDescription\": \"Denies the stdin_write command without any pre-configured scope.\"\n                      }\n                    ]\n                  }\n                }\n              },\n              \"then\": {\n                \"properties\": {\n                  \"allow\": {\n                    \"items\": {\n                      \"title\": \"ShellScopeEntry\",\n                      \"description\": \"Shell scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"cmd\",\n                            \"name\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"cmd\": {\n                              \"description\": \"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"name\",\n                            \"sidecar\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            },\n                            \"sidecar\": {\n                              \"description\": \"If this command is a sidecar command.\",\n                              \"type\": \"boolean\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        }\n                      ]\n                    }\n                  },\n                  \"deny\": {\n                    \"items\": {\n                      \"title\": \"ShellScopeEntry\",\n                      \"description\": \"Shell scope entry.\",\n                      \"anyOf\": [\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"cmd\",\n                            \"name\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"cmd\": {\n                              \"description\": \"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.\",\n                              \"type\": \"string\"\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        },\n                        {\n                          \"type\": \"object\",\n                          \"required\": [\n                            \"name\",\n                            \"sidecar\"\n                          ],\n                          \"properties\": {\n                            \"args\": {\n                              \"description\": \"The allowed arguments for the command execution.\",\n                              \"allOf\": [\n                                {\n                                  \"$ref\": \"#/definitions/ShellScopeEntryAllowedArgs\"\n                                }\n                              ]\n                            },\n                            \"name\": {\n                              \"description\": \"The name for this allowed shell command configuration.\\n\\nThis name will be used inside of the webview API to call this command along with any specified arguments.\",\n                              \"type\": \"string\"\n                            },\n                            \"sidecar\": {\n                              \"description\": \"If this command is a sidecar command.\",\n                              \"type\": \"boolean\"\n                            }\n                          },\n                          \"additionalProperties\": false\n                        }\n                      ]\n                    }\n                  }\n                }\n              },\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                }\n              }\n            },\n            {\n              \"properties\": {\n                \"identifier\": {\n                  \"description\": \"Identifier of the permission or permission set.\",\n                  \"allOf\": [\n                    {\n                      \"$ref\": \"#/definitions/Identifier\"\n                    }\n                  ]\n                },\n                \"allow\": {\n                  \"description\": \"Data that defines what is allowed by the scope.\",\n                  \"type\": [\n                    \"array\",\n                    \"null\"\n                  ],\n                  \"items\": {\n                    \"$ref\": \"#/definitions/Value\"\n                  }\n                },\n                \"deny\": {\n                  \"description\": \"Data that defines what is denied by the scope. This should be prioritized by validation logic.\",\n                  \"type\": [\n                    \"array\",\n                    \"null\"\n                  ],\n                  \"items\": {\n                    \"$ref\": \"#/definitions/Value\"\n                  }\n                }\n              }\n            }\n          ],\n          \"required\": [\n            \"identifier\"\n          ]\n        }\n      ]\n    },\n    \"Identifier\": {\n      \"description\": \"Permission identifier\",\n      \"oneOf\": [\n        {\n          \"description\": \"No features are enabled by default, as we believe\\nthe clipboard can be inherently dangerous and it is \\napplication specific if read and/or write access is needed.\\n\\nClipboard interaction needs to be explicitly enabled.\\n\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:default\",\n          \"markdownDescription\": \"No features are enabled by default, as we believe\\nthe clipboard can be inherently dangerous and it is \\napplication specific if read and/or write access is needed.\\n\\nClipboard interaction needs to be explicitly enabled.\\n\"\n        },\n        {\n          \"description\": \"Enables the clear command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:allow-clear\",\n          \"markdownDescription\": \"Enables the clear command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_image command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:allow-read-image\",\n          \"markdownDescription\": \"Enables the read_image command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the read_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:allow-read-text\",\n          \"markdownDescription\": \"Enables the read_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write_html command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:allow-write-html\",\n          \"markdownDescription\": \"Enables the write_html command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write_image command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:allow-write-image\",\n          \"markdownDescription\": \"Enables the write_image command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the write_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:allow-write-text\",\n          \"markdownDescription\": \"Enables the write_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the clear command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:deny-clear\",\n          \"markdownDescription\": \"Denies the clear command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_image command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:deny-read-image\",\n          \"markdownDescription\": \"Denies the read_image command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the read_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:deny-read-text\",\n          \"markdownDescription\": \"Denies the read_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the write_html command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:deny-write-html\",\n          \"markdownDescription\": \"Denies the write_html command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the write_image command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:deny-write-image\",\n          \"markdownDescription\": \"Denies the write_image command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the write_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"clipboard-manager:deny-write-text\",\n          \"markdownDescription\": \"Denies the write_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default core plugins set.\\n#### This default permission set includes:\\n\\n- `core:path:default`\\n- `core:event:default`\\n- `core:window:default`\\n- `core:webview:default`\\n- `core:app:default`\\n- `core:image:default`\\n- `core:resources:default`\\n- `core:menu:default`\\n- `core:tray:default`\",\n          \"type\": \"string\",\n          \"const\": \"core:default\",\n          \"markdownDescription\": \"Default core plugins set.\\n#### This default permission set includes:\\n\\n- `core:path:default`\\n- `core:event:default`\\n- `core:window:default`\\n- `core:webview:default`\\n- `core:app:default`\\n- `core:image:default`\\n- `core:resources:default`\\n- `core:menu:default`\\n- `core:tray:default`\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-version`\\n- `allow-name`\\n- `allow-tauri-version`\\n- `allow-identifier`\\n- `allow-bundle-type`\\n- `allow-register-listener`\\n- `allow-remove-listener`\",\n          \"type\": \"string\",\n          \"const\": \"core:app:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-version`\\n- `allow-name`\\n- `allow-tauri-version`\\n- `allow-identifier`\\n- `allow-bundle-type`\\n- `allow-register-listener`\\n- `allow-remove-listener`\"\n        },\n        {\n          \"description\": \"Enables the app_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-app-hide\",\n          \"markdownDescription\": \"Enables the app_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the app_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-app-show\",\n          \"markdownDescription\": \"Enables the app_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the bundle_type command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-bundle-type\",\n          \"markdownDescription\": \"Enables the bundle_type command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the default_window_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-default-window-icon\",\n          \"markdownDescription\": \"Enables the default_window_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the fetch_data_store_identifiers command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-fetch-data-store-identifiers\",\n          \"markdownDescription\": \"Enables the fetch_data_store_identifiers command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the identifier command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-identifier\",\n          \"markdownDescription\": \"Enables the identifier command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the name command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-name\",\n          \"markdownDescription\": \"Enables the name command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-register-listener\",\n          \"markdownDescription\": \"Enables the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_data_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-remove-data-store\",\n          \"markdownDescription\": \"Enables the remove_data_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-remove-listener\",\n          \"markdownDescription\": \"Enables the remove_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_app_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-set-app-theme\",\n          \"markdownDescription\": \"Enables the set_app_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_dock_visibility command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-set-dock-visibility\",\n          \"markdownDescription\": \"Enables the set_dock_visibility command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the tauri_version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-tauri-version\",\n          \"markdownDescription\": \"Enables the tauri_version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:allow-version\",\n          \"markdownDescription\": \"Enables the version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the app_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-app-hide\",\n          \"markdownDescription\": \"Denies the app_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the app_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-app-show\",\n          \"markdownDescription\": \"Denies the app_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the bundle_type command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-bundle-type\",\n          \"markdownDescription\": \"Denies the bundle_type command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the default_window_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-default-window-icon\",\n          \"markdownDescription\": \"Denies the default_window_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the fetch_data_store_identifiers command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-fetch-data-store-identifiers\",\n          \"markdownDescription\": \"Denies the fetch_data_store_identifiers command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the identifier command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-identifier\",\n          \"markdownDescription\": \"Denies the identifier command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the name command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-name\",\n          \"markdownDescription\": \"Denies the name command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-register-listener\",\n          \"markdownDescription\": \"Denies the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_data_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-remove-data-store\",\n          \"markdownDescription\": \"Denies the remove_data_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-remove-listener\",\n          \"markdownDescription\": \"Denies the remove_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_app_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-set-app-theme\",\n          \"markdownDescription\": \"Denies the set_app_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_dock_visibility command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-set-dock-visibility\",\n          \"markdownDescription\": \"Denies the set_dock_visibility command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the tauri_version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-tauri-version\",\n          \"markdownDescription\": \"Denies the tauri_version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the version command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:app:deny-version\",\n          \"markdownDescription\": \"Denies the version command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-listen`\\n- `allow-unlisten`\\n- `allow-emit`\\n- `allow-emit-to`\",\n          \"type\": \"string\",\n          \"const\": \"core:event:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-listen`\\n- `allow-unlisten`\\n- `allow-emit`\\n- `allow-emit-to`\"\n        },\n        {\n          \"description\": \"Enables the emit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-emit\",\n          \"markdownDescription\": \"Enables the emit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the emit_to command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-emit-to\",\n          \"markdownDescription\": \"Enables the emit_to command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the listen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-listen\",\n          \"markdownDescription\": \"Enables the listen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unlisten command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:allow-unlisten\",\n          \"markdownDescription\": \"Enables the unlisten command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the emit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-emit\",\n          \"markdownDescription\": \"Denies the emit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the emit_to command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-emit-to\",\n          \"markdownDescription\": \"Denies the emit_to command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the listen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-listen\",\n          \"markdownDescription\": \"Denies the listen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unlisten command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:event:deny-unlisten\",\n          \"markdownDescription\": \"Denies the unlisten command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-from-bytes`\\n- `allow-from-path`\\n- `allow-rgba`\\n- `allow-size`\",\n          \"type\": \"string\",\n          \"const\": \"core:image:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-from-bytes`\\n- `allow-from-path`\\n- `allow-rgba`\\n- `allow-size`\"\n        },\n        {\n          \"description\": \"Enables the from_bytes command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-from-bytes\",\n          \"markdownDescription\": \"Enables the from_bytes command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the from_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-from-path\",\n          \"markdownDescription\": \"Enables the from_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the rgba command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-rgba\",\n          \"markdownDescription\": \"Enables the rgba command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:allow-size\",\n          \"markdownDescription\": \"Enables the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the from_bytes command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-from-bytes\",\n          \"markdownDescription\": \"Denies the from_bytes command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the from_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-from-path\",\n          \"markdownDescription\": \"Denies the from_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the rgba command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-rgba\",\n          \"markdownDescription\": \"Denies the rgba command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:image:deny-size\",\n          \"markdownDescription\": \"Denies the size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-append`\\n- `allow-prepend`\\n- `allow-insert`\\n- `allow-remove`\\n- `allow-remove-at`\\n- `allow-items`\\n- `allow-get`\\n- `allow-popup`\\n- `allow-create-default`\\n- `allow-set-as-app-menu`\\n- `allow-set-as-window-menu`\\n- `allow-text`\\n- `allow-set-text`\\n- `allow-is-enabled`\\n- `allow-set-enabled`\\n- `allow-set-accelerator`\\n- `allow-set-as-windows-menu-for-nsapp`\\n- `allow-set-as-help-menu-for-nsapp`\\n- `allow-is-checked`\\n- `allow-set-checked`\\n- `allow-set-icon`\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-append`\\n- `allow-prepend`\\n- `allow-insert`\\n- `allow-remove`\\n- `allow-remove-at`\\n- `allow-items`\\n- `allow-get`\\n- `allow-popup`\\n- `allow-create-default`\\n- `allow-set-as-app-menu`\\n- `allow-set-as-window-menu`\\n- `allow-text`\\n- `allow-set-text`\\n- `allow-is-enabled`\\n- `allow-set-enabled`\\n- `allow-set-accelerator`\\n- `allow-set-as-windows-menu-for-nsapp`\\n- `allow-set-as-help-menu-for-nsapp`\\n- `allow-is-checked`\\n- `allow-set-checked`\\n- `allow-set-icon`\"\n        },\n        {\n          \"description\": \"Enables the append command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-append\",\n          \"markdownDescription\": \"Enables the append command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_default command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-create-default\",\n          \"markdownDescription\": \"Enables the create_default command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-get\",\n          \"markdownDescription\": \"Enables the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the insert command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-insert\",\n          \"markdownDescription\": \"Enables the insert command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-is-checked\",\n          \"markdownDescription\": \"Enables the is_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-is-enabled\",\n          \"markdownDescription\": \"Enables the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the items command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-items\",\n          \"markdownDescription\": \"Enables the items command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the popup command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-popup\",\n          \"markdownDescription\": \"Enables the popup command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the prepend command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-prepend\",\n          \"markdownDescription\": \"Enables the prepend command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-remove\",\n          \"markdownDescription\": \"Enables the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_at command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-remove-at\",\n          \"markdownDescription\": \"Enables the remove_at command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_accelerator command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-accelerator\",\n          \"markdownDescription\": \"Enables the set_accelerator command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_app_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-app-menu\",\n          \"markdownDescription\": \"Enables the set_as_app_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-help-menu-for-nsapp\",\n          \"markdownDescription\": \"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_window_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-window-menu\",\n          \"markdownDescription\": \"Enables the set_as_window_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-as-windows-menu-for-nsapp\",\n          \"markdownDescription\": \"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-checked\",\n          \"markdownDescription\": \"Enables the set_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-enabled\",\n          \"markdownDescription\": \"Enables the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-set-text\",\n          \"markdownDescription\": \"Enables the set_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:allow-text\",\n          \"markdownDescription\": \"Enables the text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the append command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-append\",\n          \"markdownDescription\": \"Denies the append command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_default command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-create-default\",\n          \"markdownDescription\": \"Denies the create_default command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-get\",\n          \"markdownDescription\": \"Denies the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the insert command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-insert\",\n          \"markdownDescription\": \"Denies the insert command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-is-checked\",\n          \"markdownDescription\": \"Denies the is_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-is-enabled\",\n          \"markdownDescription\": \"Denies the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the items command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-items\",\n          \"markdownDescription\": \"Denies the items command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the popup command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-popup\",\n          \"markdownDescription\": \"Denies the popup command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the prepend command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-prepend\",\n          \"markdownDescription\": \"Denies the prepend command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-remove\",\n          \"markdownDescription\": \"Denies the remove command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_at command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-remove-at\",\n          \"markdownDescription\": \"Denies the remove_at command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_accelerator command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-accelerator\",\n          \"markdownDescription\": \"Denies the set_accelerator command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_app_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-app-menu\",\n          \"markdownDescription\": \"Denies the set_as_app_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-help-menu-for-nsapp\",\n          \"markdownDescription\": \"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_window_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-window-menu\",\n          \"markdownDescription\": \"Denies the set_as_window_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-as-windows-menu-for-nsapp\",\n          \"markdownDescription\": \"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_checked command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-checked\",\n          \"markdownDescription\": \"Denies the set_checked command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-enabled\",\n          \"markdownDescription\": \"Denies the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-set-text\",\n          \"markdownDescription\": \"Denies the set_text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the text command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:menu:deny-text\",\n          \"markdownDescription\": \"Denies the text command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-resolve-directory`\\n- `allow-resolve`\\n- `allow-normalize`\\n- `allow-join`\\n- `allow-dirname`\\n- `allow-extname`\\n- `allow-basename`\\n- `allow-is-absolute`\",\n          \"type\": \"string\",\n          \"const\": \"core:path:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-resolve-directory`\\n- `allow-resolve`\\n- `allow-normalize`\\n- `allow-join`\\n- `allow-dirname`\\n- `allow-extname`\\n- `allow-basename`\\n- `allow-is-absolute`\"\n        },\n        {\n          \"description\": \"Enables the basename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-basename\",\n          \"markdownDescription\": \"Enables the basename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the dirname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-dirname\",\n          \"markdownDescription\": \"Enables the dirname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the extname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-extname\",\n          \"markdownDescription\": \"Enables the extname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_absolute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-is-absolute\",\n          \"markdownDescription\": \"Enables the is_absolute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the join command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-join\",\n          \"markdownDescription\": \"Enables the join command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the normalize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-normalize\",\n          \"markdownDescription\": \"Enables the normalize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the resolve command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-resolve\",\n          \"markdownDescription\": \"Enables the resolve command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the resolve_directory command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:allow-resolve-directory\",\n          \"markdownDescription\": \"Enables the resolve_directory command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the basename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-basename\",\n          \"markdownDescription\": \"Denies the basename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the dirname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-dirname\",\n          \"markdownDescription\": \"Denies the dirname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the extname command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-extname\",\n          \"markdownDescription\": \"Denies the extname command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_absolute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-is-absolute\",\n          \"markdownDescription\": \"Denies the is_absolute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the join command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-join\",\n          \"markdownDescription\": \"Denies the join command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the normalize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-normalize\",\n          \"markdownDescription\": \"Denies the normalize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the resolve command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-resolve\",\n          \"markdownDescription\": \"Denies the resolve command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the resolve_directory command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:path:deny-resolve-directory\",\n          \"markdownDescription\": \"Denies the resolve_directory command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-close`\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-close`\"\n        },\n        {\n          \"description\": \"Enables the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:allow-close\",\n          \"markdownDescription\": \"Enables the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:resources:deny-close\",\n          \"markdownDescription\": \"Denies the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-get-by-id`\\n- `allow-remove-by-id`\\n- `allow-set-icon`\\n- `allow-set-menu`\\n- `allow-set-tooltip`\\n- `allow-set-title`\\n- `allow-set-visible`\\n- `allow-set-temp-dir-path`\\n- `allow-set-icon-as-template`\\n- `allow-set-show-menu-on-left-click`\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:default\",\n          \"markdownDescription\": \"Default permissions for the plugin, which enables all commands.\\n#### This default permission set includes:\\n\\n- `allow-new`\\n- `allow-get-by-id`\\n- `allow-remove-by-id`\\n- `allow-set-icon`\\n- `allow-set-menu`\\n- `allow-set-tooltip`\\n- `allow-set-title`\\n- `allow-set-visible`\\n- `allow-set-temp-dir-path`\\n- `allow-set-icon-as-template`\\n- `allow-set-show-menu-on-left-click`\"\n        },\n        {\n          \"description\": \"Enables the get_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-get-by-id\",\n          \"markdownDescription\": \"Enables the get_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-new\",\n          \"markdownDescription\": \"Enables the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-remove-by-id\",\n          \"markdownDescription\": \"Enables the remove_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon_as_template command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-icon-as-template\",\n          \"markdownDescription\": \"Enables the set_icon_as_template command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-menu\",\n          \"markdownDescription\": \"Enables the set_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_show_menu_on_left_click command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-show-menu-on-left-click\",\n          \"markdownDescription\": \"Enables the set_show_menu_on_left_click command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_temp_dir_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-temp-dir-path\",\n          \"markdownDescription\": \"Enables the set_temp_dir_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-title\",\n          \"markdownDescription\": \"Enables the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_tooltip command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-tooltip\",\n          \"markdownDescription\": \"Enables the set_tooltip command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:allow-set-visible\",\n          \"markdownDescription\": \"Enables the set_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-get-by-id\",\n          \"markdownDescription\": \"Denies the get_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the new command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-new\",\n          \"markdownDescription\": \"Denies the new command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_by_id command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-remove-by-id\",\n          \"markdownDescription\": \"Denies the remove_by_id command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon_as_template command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-icon-as-template\",\n          \"markdownDescription\": \"Denies the set_icon_as_template command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_menu command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-menu\",\n          \"markdownDescription\": \"Denies the set_menu command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_show_menu_on_left_click command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-show-menu-on-left-click\",\n          \"markdownDescription\": \"Denies the set_show_menu_on_left_click command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_temp_dir_path command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-temp-dir-path\",\n          \"markdownDescription\": \"Denies the set_temp_dir_path command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-title\",\n          \"markdownDescription\": \"Denies the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_tooltip command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-tooltip\",\n          \"markdownDescription\": \"Denies the set_tooltip command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:tray:deny-set-visible\",\n          \"markdownDescription\": \"Denies the set_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-webviews`\\n- `allow-webview-position`\\n- `allow-webview-size`\\n- `allow-internal-toggle-devtools`\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-webviews`\\n- `allow-webview-position`\\n- `allow-webview-size`\\n- `allow-internal-toggle-devtools`\"\n        },\n        {\n          \"description\": \"Enables the clear_all_browsing_data command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-clear-all-browsing-data\",\n          \"markdownDescription\": \"Enables the clear_all_browsing_data command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_webview command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-create-webview\",\n          \"markdownDescription\": \"Enables the create_webview command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_webview_window command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-create-webview-window\",\n          \"markdownDescription\": \"Enables the create_webview_window command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_all_webviews command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-get-all-webviews\",\n          \"markdownDescription\": \"Enables the get_all_webviews command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the internal_toggle_devtools command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-internal-toggle-devtools\",\n          \"markdownDescription\": \"Enables the internal_toggle_devtools command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the print command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-print\",\n          \"markdownDescription\": \"Enables the print command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the reparent command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-reparent\",\n          \"markdownDescription\": \"Enables the reparent command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_auto_resize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-auto-resize\",\n          \"markdownDescription\": \"Enables the set_webview_auto_resize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-background-color\",\n          \"markdownDescription\": \"Enables the set_webview_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-focus\",\n          \"markdownDescription\": \"Enables the set_webview_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-position\",\n          \"markdownDescription\": \"Enables the set_webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-size\",\n          \"markdownDescription\": \"Enables the set_webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_webview_zoom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-set-webview-zoom\",\n          \"markdownDescription\": \"Enables the set_webview_zoom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-close\",\n          \"markdownDescription\": \"Enables the webview_close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-hide\",\n          \"markdownDescription\": \"Enables the webview_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-position\",\n          \"markdownDescription\": \"Enables the webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-show\",\n          \"markdownDescription\": \"Enables the webview_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:allow-webview-size\",\n          \"markdownDescription\": \"Enables the webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the clear_all_browsing_data command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-clear-all-browsing-data\",\n          \"markdownDescription\": \"Denies the clear_all_browsing_data command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_webview command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-create-webview\",\n          \"markdownDescription\": \"Denies the create_webview command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_webview_window command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-create-webview-window\",\n          \"markdownDescription\": \"Denies the create_webview_window command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_all_webviews command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-get-all-webviews\",\n          \"markdownDescription\": \"Denies the get_all_webviews command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the internal_toggle_devtools command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-internal-toggle-devtools\",\n          \"markdownDescription\": \"Denies the internal_toggle_devtools command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the print command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-print\",\n          \"markdownDescription\": \"Denies the print command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the reparent command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-reparent\",\n          \"markdownDescription\": \"Denies the reparent command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_auto_resize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-auto-resize\",\n          \"markdownDescription\": \"Denies the set_webview_auto_resize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-background-color\",\n          \"markdownDescription\": \"Denies the set_webview_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-focus\",\n          \"markdownDescription\": \"Denies the set_webview_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-position\",\n          \"markdownDescription\": \"Denies the set_webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-size\",\n          \"markdownDescription\": \"Denies the set_webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_webview_zoom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-set-webview-zoom\",\n          \"markdownDescription\": \"Denies the set_webview_zoom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-close\",\n          \"markdownDescription\": \"Denies the webview_close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-hide\",\n          \"markdownDescription\": \"Denies the webview_hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-position\",\n          \"markdownDescription\": \"Denies the webview_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-show\",\n          \"markdownDescription\": \"Denies the webview_show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the webview_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:webview:deny-webview-size\",\n          \"markdownDescription\": \"Denies the webview_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-windows`\\n- `allow-scale-factor`\\n- `allow-inner-position`\\n- `allow-outer-position`\\n- `allow-inner-size`\\n- `allow-outer-size`\\n- `allow-is-fullscreen`\\n- `allow-is-minimized`\\n- `allow-is-maximized`\\n- `allow-is-focused`\\n- `allow-is-decorated`\\n- `allow-is-resizable`\\n- `allow-is-maximizable`\\n- `allow-is-minimizable`\\n- `allow-is-closable`\\n- `allow-is-visible`\\n- `allow-is-enabled`\\n- `allow-title`\\n- `allow-current-monitor`\\n- `allow-primary-monitor`\\n- `allow-monitor-from-point`\\n- `allow-available-monitors`\\n- `allow-cursor-position`\\n- `allow-theme`\\n- `allow-is-always-on-top`\\n- `allow-internal-toggle-maximize`\",\n          \"type\": \"string\",\n          \"const\": \"core:window:default\",\n          \"markdownDescription\": \"Default permissions for the plugin.\\n#### This default permission set includes:\\n\\n- `allow-get-all-windows`\\n- `allow-scale-factor`\\n- `allow-inner-position`\\n- `allow-outer-position`\\n- `allow-inner-size`\\n- `allow-outer-size`\\n- `allow-is-fullscreen`\\n- `allow-is-minimized`\\n- `allow-is-maximized`\\n- `allow-is-focused`\\n- `allow-is-decorated`\\n- `allow-is-resizable`\\n- `allow-is-maximizable`\\n- `allow-is-minimizable`\\n- `allow-is-closable`\\n- `allow-is-visible`\\n- `allow-is-enabled`\\n- `allow-title`\\n- `allow-current-monitor`\\n- `allow-primary-monitor`\\n- `allow-monitor-from-point`\\n- `allow-available-monitors`\\n- `allow-cursor-position`\\n- `allow-theme`\\n- `allow-is-always-on-top`\\n- `allow-internal-toggle-maximize`\"\n        },\n        {\n          \"description\": \"Enables the available_monitors command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-available-monitors\",\n          \"markdownDescription\": \"Enables the available_monitors command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the center command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-center\",\n          \"markdownDescription\": \"Enables the center command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-close\",\n          \"markdownDescription\": \"Enables the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-create\",\n          \"markdownDescription\": \"Enables the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the current_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-current-monitor\",\n          \"markdownDescription\": \"Enables the current_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-cursor-position\",\n          \"markdownDescription\": \"Enables the cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the destroy command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-destroy\",\n          \"markdownDescription\": \"Enables the destroy command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_all_windows command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-get-all-windows\",\n          \"markdownDescription\": \"Enables the get_all_windows command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-hide\",\n          \"markdownDescription\": \"Enables the hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the inner_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-inner-position\",\n          \"markdownDescription\": \"Enables the inner_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the inner_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-inner-size\",\n          \"markdownDescription\": \"Enables the inner_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the internal_toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-internal-toggle-maximize\",\n          \"markdownDescription\": \"Enables the internal_toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-always-on-top\",\n          \"markdownDescription\": \"Enables the is_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-closable\",\n          \"markdownDescription\": \"Enables the is_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_decorated command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-decorated\",\n          \"markdownDescription\": \"Enables the is_decorated command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-enabled\",\n          \"markdownDescription\": \"Enables the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_focused command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-focused\",\n          \"markdownDescription\": \"Enables the is_focused command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-fullscreen\",\n          \"markdownDescription\": \"Enables the is_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-maximizable\",\n          \"markdownDescription\": \"Enables the is_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_maximized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-maximized\",\n          \"markdownDescription\": \"Enables the is_maximized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-minimizable\",\n          \"markdownDescription\": \"Enables the is_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_minimized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-minimized\",\n          \"markdownDescription\": \"Enables the is_minimized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-resizable\",\n          \"markdownDescription\": \"Enables the is_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-is-visible\",\n          \"markdownDescription\": \"Enables the is_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-maximize\",\n          \"markdownDescription\": \"Enables the maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the minimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-minimize\",\n          \"markdownDescription\": \"Enables the minimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the monitor_from_point command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-monitor-from-point\",\n          \"markdownDescription\": \"Enables the monitor_from_point command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the outer_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-outer-position\",\n          \"markdownDescription\": \"Enables the outer_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the outer_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-outer-size\",\n          \"markdownDescription\": \"Enables the outer_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the primary_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-primary-monitor\",\n          \"markdownDescription\": \"Enables the primary_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the request_user_attention command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-request-user-attention\",\n          \"markdownDescription\": \"Enables the request_user_attention command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the scale_factor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-scale-factor\",\n          \"markdownDescription\": \"Enables the scale_factor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_always_on_bottom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-always-on-bottom\",\n          \"markdownDescription\": \"Enables the set_always_on_bottom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-always-on-top\",\n          \"markdownDescription\": \"Enables the set_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-background-color\",\n          \"markdownDescription\": \"Enables the set_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_badge_count command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-badge-count\",\n          \"markdownDescription\": \"Enables the set_badge_count command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_badge_label command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-badge-label\",\n          \"markdownDescription\": \"Enables the set_badge_label command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-closable\",\n          \"markdownDescription\": \"Enables the set_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_content_protected command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-content-protected\",\n          \"markdownDescription\": \"Enables the set_content_protected command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_grab command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-grab\",\n          \"markdownDescription\": \"Enables the set_cursor_grab command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-icon\",\n          \"markdownDescription\": \"Enables the set_cursor_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-position\",\n          \"markdownDescription\": \"Enables the set_cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_cursor_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-cursor-visible\",\n          \"markdownDescription\": \"Enables the set_cursor_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_decorations command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-decorations\",\n          \"markdownDescription\": \"Enables the set_decorations command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_effects command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-effects\",\n          \"markdownDescription\": \"Enables the set_effects command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-enabled\",\n          \"markdownDescription\": \"Enables the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-focus\",\n          \"markdownDescription\": \"Enables the set_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_focusable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-focusable\",\n          \"markdownDescription\": \"Enables the set_focusable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-fullscreen\",\n          \"markdownDescription\": \"Enables the set_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-icon\",\n          \"markdownDescription\": \"Enables the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_ignore_cursor_events command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-ignore-cursor-events\",\n          \"markdownDescription\": \"Enables the set_ignore_cursor_events command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_max_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-max-size\",\n          \"markdownDescription\": \"Enables the set_max_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-maximizable\",\n          \"markdownDescription\": \"Enables the set_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_min_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-min-size\",\n          \"markdownDescription\": \"Enables the set_min_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-minimizable\",\n          \"markdownDescription\": \"Enables the set_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_overlay_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-overlay-icon\",\n          \"markdownDescription\": \"Enables the set_overlay_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-position\",\n          \"markdownDescription\": \"Enables the set_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_progress_bar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-progress-bar\",\n          \"markdownDescription\": \"Enables the set_progress_bar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-resizable\",\n          \"markdownDescription\": \"Enables the set_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_shadow command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-shadow\",\n          \"markdownDescription\": \"Enables the set_shadow command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_simple_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-simple-fullscreen\",\n          \"markdownDescription\": \"Enables the set_simple_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-size\",\n          \"markdownDescription\": \"Enables the set_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_size_constraints command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-size-constraints\",\n          \"markdownDescription\": \"Enables the set_size_constraints command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_skip_taskbar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-skip-taskbar\",\n          \"markdownDescription\": \"Enables the set_skip_taskbar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-theme\",\n          \"markdownDescription\": \"Enables the set_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-title\",\n          \"markdownDescription\": \"Enables the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_title_bar_style command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-title-bar-style\",\n          \"markdownDescription\": \"Enables the set_title_bar_style command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set_visible_on_all_workspaces command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-set-visible-on-all-workspaces\",\n          \"markdownDescription\": \"Enables the set_visible_on_all_workspaces command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-show\",\n          \"markdownDescription\": \"Enables the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the start_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-start-dragging\",\n          \"markdownDescription\": \"Enables the start_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the start_resize_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-start-resize-dragging\",\n          \"markdownDescription\": \"Enables the start_resize_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-theme\",\n          \"markdownDescription\": \"Enables the theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-title\",\n          \"markdownDescription\": \"Enables the title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-toggle-maximize\",\n          \"markdownDescription\": \"Enables the toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unmaximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-unmaximize\",\n          \"markdownDescription\": \"Enables the unmaximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unminimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:allow-unminimize\",\n          \"markdownDescription\": \"Enables the unminimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the available_monitors command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-available-monitors\",\n          \"markdownDescription\": \"Denies the available_monitors command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the center command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-center\",\n          \"markdownDescription\": \"Denies the center command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the close command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-close\",\n          \"markdownDescription\": \"Denies the close command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-create\",\n          \"markdownDescription\": \"Denies the create command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the current_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-current-monitor\",\n          \"markdownDescription\": \"Denies the current_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-cursor-position\",\n          \"markdownDescription\": \"Denies the cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the destroy command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-destroy\",\n          \"markdownDescription\": \"Denies the destroy command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_all_windows command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-get-all-windows\",\n          \"markdownDescription\": \"Denies the get_all_windows command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the hide command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-hide\",\n          \"markdownDescription\": \"Denies the hide command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the inner_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-inner-position\",\n          \"markdownDescription\": \"Denies the inner_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the inner_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-inner-size\",\n          \"markdownDescription\": \"Denies the inner_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the internal_toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-internal-toggle-maximize\",\n          \"markdownDescription\": \"Denies the internal_toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-always-on-top\",\n          \"markdownDescription\": \"Denies the is_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-closable\",\n          \"markdownDescription\": \"Denies the is_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_decorated command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-decorated\",\n          \"markdownDescription\": \"Denies the is_decorated command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-enabled\",\n          \"markdownDescription\": \"Denies the is_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_focused command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-focused\",\n          \"markdownDescription\": \"Denies the is_focused command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-fullscreen\",\n          \"markdownDescription\": \"Denies the is_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-maximizable\",\n          \"markdownDescription\": \"Denies the is_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_maximized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-maximized\",\n          \"markdownDescription\": \"Denies the is_maximized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-minimizable\",\n          \"markdownDescription\": \"Denies the is_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_minimized command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-minimized\",\n          \"markdownDescription\": \"Denies the is_minimized command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-resizable\",\n          \"markdownDescription\": \"Denies the is_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-is-visible\",\n          \"markdownDescription\": \"Denies the is_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-maximize\",\n          \"markdownDescription\": \"Denies the maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the minimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-minimize\",\n          \"markdownDescription\": \"Denies the minimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the monitor_from_point command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-monitor-from-point\",\n          \"markdownDescription\": \"Denies the monitor_from_point command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the outer_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-outer-position\",\n          \"markdownDescription\": \"Denies the outer_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the outer_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-outer-size\",\n          \"markdownDescription\": \"Denies the outer_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the primary_monitor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-primary-monitor\",\n          \"markdownDescription\": \"Denies the primary_monitor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the request_user_attention command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-request-user-attention\",\n          \"markdownDescription\": \"Denies the request_user_attention command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the scale_factor command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-scale-factor\",\n          \"markdownDescription\": \"Denies the scale_factor command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_always_on_bottom command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-always-on-bottom\",\n          \"markdownDescription\": \"Denies the set_always_on_bottom command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_always_on_top command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-always-on-top\",\n          \"markdownDescription\": \"Denies the set_always_on_top command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_background_color command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-background-color\",\n          \"markdownDescription\": \"Denies the set_background_color command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_badge_count command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-badge-count\",\n          \"markdownDescription\": \"Denies the set_badge_count command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_badge_label command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-badge-label\",\n          \"markdownDescription\": \"Denies the set_badge_label command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_closable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-closable\",\n          \"markdownDescription\": \"Denies the set_closable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_content_protected command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-content-protected\",\n          \"markdownDescription\": \"Denies the set_content_protected command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_grab command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-grab\",\n          \"markdownDescription\": \"Denies the set_cursor_grab command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-icon\",\n          \"markdownDescription\": \"Denies the set_cursor_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-position\",\n          \"markdownDescription\": \"Denies the set_cursor_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_cursor_visible command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-cursor-visible\",\n          \"markdownDescription\": \"Denies the set_cursor_visible command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_decorations command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-decorations\",\n          \"markdownDescription\": \"Denies the set_decorations command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_effects command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-effects\",\n          \"markdownDescription\": \"Denies the set_effects command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_enabled command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-enabled\",\n          \"markdownDescription\": \"Denies the set_enabled command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_focus command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-focus\",\n          \"markdownDescription\": \"Denies the set_focus command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_focusable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-focusable\",\n          \"markdownDescription\": \"Denies the set_focusable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-fullscreen\",\n          \"markdownDescription\": \"Denies the set_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-icon\",\n          \"markdownDescription\": \"Denies the set_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_ignore_cursor_events command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-ignore-cursor-events\",\n          \"markdownDescription\": \"Denies the set_ignore_cursor_events command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_max_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-max-size\",\n          \"markdownDescription\": \"Denies the set_max_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_maximizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-maximizable\",\n          \"markdownDescription\": \"Denies the set_maximizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_min_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-min-size\",\n          \"markdownDescription\": \"Denies the set_min_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_minimizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-minimizable\",\n          \"markdownDescription\": \"Denies the set_minimizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_overlay_icon command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-overlay-icon\",\n          \"markdownDescription\": \"Denies the set_overlay_icon command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_position command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-position\",\n          \"markdownDescription\": \"Denies the set_position command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_progress_bar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-progress-bar\",\n          \"markdownDescription\": \"Denies the set_progress_bar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_resizable command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-resizable\",\n          \"markdownDescription\": \"Denies the set_resizable command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_shadow command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-shadow\",\n          \"markdownDescription\": \"Denies the set_shadow command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_simple_fullscreen command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-simple-fullscreen\",\n          \"markdownDescription\": \"Denies the set_simple_fullscreen command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_size command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-size\",\n          \"markdownDescription\": \"Denies the set_size command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_size_constraints command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-size-constraints\",\n          \"markdownDescription\": \"Denies the set_size_constraints command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_skip_taskbar command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-skip-taskbar\",\n          \"markdownDescription\": \"Denies the set_skip_taskbar command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-theme\",\n          \"markdownDescription\": \"Denies the set_theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-title\",\n          \"markdownDescription\": \"Denies the set_title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_title_bar_style command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-title-bar-style\",\n          \"markdownDescription\": \"Denies the set_title_bar_style command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set_visible_on_all_workspaces command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-set-visible-on-all-workspaces\",\n          \"markdownDescription\": \"Denies the set_visible_on_all_workspaces command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-show\",\n          \"markdownDescription\": \"Denies the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the start_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-start-dragging\",\n          \"markdownDescription\": \"Denies the start_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the start_resize_dragging command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-start-resize-dragging\",\n          \"markdownDescription\": \"Denies the start_resize_dragging command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the theme command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-theme\",\n          \"markdownDescription\": \"Denies the theme command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the title command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-title\",\n          \"markdownDescription\": \"Denies the title command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the toggle_maximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-toggle-maximize\",\n          \"markdownDescription\": \"Denies the toggle_maximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unmaximize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-unmaximize\",\n          \"markdownDescription\": \"Denies the unmaximize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unminimize command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"core:window:deny-unminimize\",\n          \"markdownDescription\": \"Denies the unminimize command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures the types of dialogs\\navailable from the dialog plugin.\\n\\n#### Granted Permissions\\n\\nAll dialog types are enabled.\\n\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-ask`\\n- `allow-confirm`\\n- `allow-message`\\n- `allow-save`\\n- `allow-open`\",\n          \"type\": \"string\",\n          \"const\": \"dialog:default\",\n          \"markdownDescription\": \"This permission set configures the types of dialogs\\navailable from the dialog plugin.\\n\\n#### Granted Permissions\\n\\nAll dialog types are enabled.\\n\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-ask`\\n- `allow-confirm`\\n- `allow-message`\\n- `allow-save`\\n- `allow-open`\"\n        },\n        {\n          \"description\": \"Enables the ask command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-ask\",\n          \"markdownDescription\": \"Enables the ask command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the confirm command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-confirm\",\n          \"markdownDescription\": \"Enables the confirm command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the message command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-message\",\n          \"markdownDescription\": \"Enables the message command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:allow-save\",\n          \"markdownDescription\": \"Enables the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the ask command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-ask\",\n          \"markdownDescription\": \"Denies the ask command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the confirm command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-confirm\",\n          \"markdownDescription\": \"Denies the confirm command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the message command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-message\",\n          \"markdownDescription\": \"Denies the message command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"dialog:deny-save\",\n          \"markdownDescription\": \"Denies the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"No features are enabled by default, as we believe\\nthe shortcuts can be inherently dangerous and it is\\napplication specific if specific shortcuts should be\\nregistered or unregistered.\\n\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:default\",\n          \"markdownDescription\": \"No features are enabled by default, as we believe\\nthe shortcuts can be inherently dangerous and it is\\napplication specific if specific shortcuts should be\\nregistered or unregistered.\\n\"\n        },\n        {\n          \"description\": \"Enables the is_registered command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:allow-is-registered\",\n          \"markdownDescription\": \"Enables the is_registered command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:allow-register\",\n          \"markdownDescription\": \"Enables the register command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register_all command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:allow-register-all\",\n          \"markdownDescription\": \"Enables the register_all command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unregister command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:allow-unregister\",\n          \"markdownDescription\": \"Enables the unregister command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the unregister_all command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:allow-unregister-all\",\n          \"markdownDescription\": \"Enables the unregister_all command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_registered command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:deny-is-registered\",\n          \"markdownDescription\": \"Denies the is_registered command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:deny-register\",\n          \"markdownDescription\": \"Denies the register command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register_all command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:deny-register-all\",\n          \"markdownDescription\": \"Denies the register_all command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unregister command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:deny-unregister\",\n          \"markdownDescription\": \"Denies the unregister command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the unregister_all command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"global-shortcut:deny-unregister-all\",\n          \"markdownDescription\": \"Denies the unregister_all command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Allows the log command\\n#### This default permission set includes:\\n\\n- `allow-log`\",\n          \"type\": \"string\",\n          \"const\": \"log:default\",\n          \"markdownDescription\": \"Allows the log command\\n#### This default permission set includes:\\n\\n- `allow-log`\"\n        },\n        {\n          \"description\": \"Enables the log command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"log:allow-log\",\n          \"markdownDescription\": \"Enables the log command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the log command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"log:deny-log\",\n          \"markdownDescription\": \"Denies the log command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which\\nnotification features are by default exposed.\\n\\n#### Granted Permissions\\n\\nIt allows all notification related features.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-is-permission-granted`\\n- `allow-request-permission`\\n- `allow-notify`\\n- `allow-register-action-types`\\n- `allow-register-listener`\\n- `allow-cancel`\\n- `allow-get-pending`\\n- `allow-remove-active`\\n- `allow-get-active`\\n- `allow-check-permissions`\\n- `allow-show`\\n- `allow-batch`\\n- `allow-list-channels`\\n- `allow-delete-channel`\\n- `allow-create-channel`\\n- `allow-permission-state`\",\n          \"type\": \"string\",\n          \"const\": \"notification:default\",\n          \"markdownDescription\": \"This permission set configures which\\nnotification features are by default exposed.\\n\\n#### Granted Permissions\\n\\nIt allows all notification related features.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-is-permission-granted`\\n- `allow-request-permission`\\n- `allow-notify`\\n- `allow-register-action-types`\\n- `allow-register-listener`\\n- `allow-cancel`\\n- `allow-get-pending`\\n- `allow-remove-active`\\n- `allow-get-active`\\n- `allow-check-permissions`\\n- `allow-show`\\n- `allow-batch`\\n- `allow-list-channels`\\n- `allow-delete-channel`\\n- `allow-create-channel`\\n- `allow-permission-state`\"\n        },\n        {\n          \"description\": \"Enables the batch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-batch\",\n          \"markdownDescription\": \"Enables the batch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the cancel command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-cancel\",\n          \"markdownDescription\": \"Enables the cancel command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the check_permissions command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-check-permissions\",\n          \"markdownDescription\": \"Enables the check_permissions command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the create_channel command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-create-channel\",\n          \"markdownDescription\": \"Enables the create_channel command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the delete_channel command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-delete-channel\",\n          \"markdownDescription\": \"Enables the delete_channel command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_active command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-get-active\",\n          \"markdownDescription\": \"Enables the get_active command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_pending command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-get-pending\",\n          \"markdownDescription\": \"Enables the get_pending command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the is_permission_granted command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-is-permission-granted\",\n          \"markdownDescription\": \"Enables the is_permission_granted command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the list_channels command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-list-channels\",\n          \"markdownDescription\": \"Enables the list_channels command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the notify command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-notify\",\n          \"markdownDescription\": \"Enables the notify command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the permission_state command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-permission-state\",\n          \"markdownDescription\": \"Enables the permission_state command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register_action_types command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-register-action-types\",\n          \"markdownDescription\": \"Enables the register_action_types command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-register-listener\",\n          \"markdownDescription\": \"Enables the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the remove_active command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-remove-active\",\n          \"markdownDescription\": \"Enables the remove_active command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the request_permission command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-request-permission\",\n          \"markdownDescription\": \"Enables the request_permission command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:allow-show\",\n          \"markdownDescription\": \"Enables the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the batch command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-batch\",\n          \"markdownDescription\": \"Denies the batch command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the cancel command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-cancel\",\n          \"markdownDescription\": \"Denies the cancel command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the check_permissions command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-check-permissions\",\n          \"markdownDescription\": \"Denies the check_permissions command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the create_channel command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-create-channel\",\n          \"markdownDescription\": \"Denies the create_channel command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the delete_channel command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-delete-channel\",\n          \"markdownDescription\": \"Denies the delete_channel command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_active command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-get-active\",\n          \"markdownDescription\": \"Denies the get_active command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_pending command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-get-pending\",\n          \"markdownDescription\": \"Denies the get_pending command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the is_permission_granted command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-is-permission-granted\",\n          \"markdownDescription\": \"Denies the is_permission_granted command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the list_channels command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-list-channels\",\n          \"markdownDescription\": \"Denies the list_channels command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the notify command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-notify\",\n          \"markdownDescription\": \"Denies the notify command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the permission_state command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-permission-state\",\n          \"markdownDescription\": \"Denies the permission_state command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register_action_types command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-register-action-types\",\n          \"markdownDescription\": \"Denies the register_action_types command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the register_listener command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-register-listener\",\n          \"markdownDescription\": \"Denies the register_listener command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the remove_active command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-remove-active\",\n          \"markdownDescription\": \"Denies the remove_active command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the request_permission command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-request-permission\",\n          \"markdownDescription\": \"Denies the request_permission command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the show command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"notification:deny-show\",\n          \"markdownDescription\": \"Denies the show command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which\\nprocess features are by default exposed.\\n\\n#### Granted Permissions\\n\\nThis enables to quit via `allow-exit` and restart via `allow-restart`\\nthe application.\\n\\n#### This default permission set includes:\\n\\n- `allow-exit`\\n- `allow-restart`\",\n          \"type\": \"string\",\n          \"const\": \"process:default\",\n          \"markdownDescription\": \"This permission set configures which\\nprocess features are by default exposed.\\n\\n#### Granted Permissions\\n\\nThis enables to quit via `allow-exit` and restart via `allow-restart`\\nthe application.\\n\\n#### This default permission set includes:\\n\\n- `allow-exit`\\n- `allow-restart`\"\n        },\n        {\n          \"description\": \"Enables the exit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:allow-exit\",\n          \"markdownDescription\": \"Enables the exit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the restart command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:allow-restart\",\n          \"markdownDescription\": \"Enables the restart command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the exit command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:deny-exit\",\n          \"markdownDescription\": \"Denies the exit command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the restart command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"process:deny-restart\",\n          \"markdownDescription\": \"Denies the restart command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\",\n          \"type\": \"string\",\n          \"const\": \"shell:default\",\n          \"markdownDescription\": \"This permission set configures which\\nshell functionality is exposed by default.\\n\\n#### Granted Permissions\\n\\nIt allows to use the `open` functionality with a reasonable\\nscope pre-configured. It will allow opening `http(s)://`,\\n`tel:` and `mailto:` links.\\n\\n#### This default permission set includes:\\n\\n- `allow-open`\"\n        },\n        {\n          \"description\": \"Enables the execute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-execute\",\n          \"markdownDescription\": \"Enables the execute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the kill command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-kill\",\n          \"markdownDescription\": \"Enables the kill command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-open\",\n          \"markdownDescription\": \"Enables the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the spawn command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-spawn\",\n          \"markdownDescription\": \"Enables the spawn command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the stdin_write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:allow-stdin-write\",\n          \"markdownDescription\": \"Enables the stdin_write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the execute command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-execute\",\n          \"markdownDescription\": \"Denies the execute command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the kill command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-kill\",\n          \"markdownDescription\": \"Denies the kill command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the open command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-open\",\n          \"markdownDescription\": \"Denies the open command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the spawn command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-spawn\",\n          \"markdownDescription\": \"Denies the spawn command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the stdin_write command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"shell:deny-stdin-write\",\n          \"markdownDescription\": \"Denies the stdin_write command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures what kind of\\noperations are available from the store plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-load`\\n- `allow-get-store`\\n- `allow-set`\\n- `allow-get`\\n- `allow-has`\\n- `allow-delete`\\n- `allow-clear`\\n- `allow-reset`\\n- `allow-keys`\\n- `allow-values`\\n- `allow-entries`\\n- `allow-length`\\n- `allow-reload`\\n- `allow-save`\",\n          \"type\": \"string\",\n          \"const\": \"store:default\",\n          \"markdownDescription\": \"This permission set configures what kind of\\noperations are available from the store plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-load`\\n- `allow-get-store`\\n- `allow-set`\\n- `allow-get`\\n- `allow-has`\\n- `allow-delete`\\n- `allow-clear`\\n- `allow-reset`\\n- `allow-keys`\\n- `allow-values`\\n- `allow-entries`\\n- `allow-length`\\n- `allow-reload`\\n- `allow-save`\"\n        },\n        {\n          \"description\": \"Enables the clear command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-clear\",\n          \"markdownDescription\": \"Enables the clear command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the delete command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-delete\",\n          \"markdownDescription\": \"Enables the delete command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the entries command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-entries\",\n          \"markdownDescription\": \"Enables the entries command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-get\",\n          \"markdownDescription\": \"Enables the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the get_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-get-store\",\n          \"markdownDescription\": \"Enables the get_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the has command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-has\",\n          \"markdownDescription\": \"Enables the has command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the keys command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-keys\",\n          \"markdownDescription\": \"Enables the keys command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the length command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-length\",\n          \"markdownDescription\": \"Enables the length command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the load command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-load\",\n          \"markdownDescription\": \"Enables the load command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the reload command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-reload\",\n          \"markdownDescription\": \"Enables the reload command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the reset command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-reset\",\n          \"markdownDescription\": \"Enables the reset command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-save\",\n          \"markdownDescription\": \"Enables the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the set command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-set\",\n          \"markdownDescription\": \"Enables the set command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the values command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:allow-values\",\n          \"markdownDescription\": \"Enables the values command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the clear command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-clear\",\n          \"markdownDescription\": \"Denies the clear command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the delete command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-delete\",\n          \"markdownDescription\": \"Denies the delete command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the entries command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-entries\",\n          \"markdownDescription\": \"Denies the entries command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-get\",\n          \"markdownDescription\": \"Denies the get command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the get_store command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-get-store\",\n          \"markdownDescription\": \"Denies the get_store command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the has command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-has\",\n          \"markdownDescription\": \"Denies the has command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the keys command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-keys\",\n          \"markdownDescription\": \"Denies the keys command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the length command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-length\",\n          \"markdownDescription\": \"Denies the length command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the load command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-load\",\n          \"markdownDescription\": \"Denies the load command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the reload command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-reload\",\n          \"markdownDescription\": \"Denies the reload command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the reset command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-reset\",\n          \"markdownDescription\": \"Denies the reset command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the save command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-save\",\n          \"markdownDescription\": \"Denies the save command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the set command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-set\",\n          \"markdownDescription\": \"Denies the set command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the values command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"store:deny-values\",\n          \"markdownDescription\": \"Denies the values command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures which kind of\\nupdater functions are exposed to the frontend.\\n\\n#### Granted Permissions\\n\\nThe full workflow from checking for updates to installing them\\nis enabled.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-check`\\n- `allow-download`\\n- `allow-install`\\n- `allow-download-and-install`\",\n          \"type\": \"string\",\n          \"const\": \"updater:default\",\n          \"markdownDescription\": \"This permission set configures which kind of\\nupdater functions are exposed to the frontend.\\n\\n#### Granted Permissions\\n\\nThe full workflow from checking for updates to installing them\\nis enabled.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-check`\\n- `allow-download`\\n- `allow-install`\\n- `allow-download-and-install`\"\n        },\n        {\n          \"description\": \"Enables the check command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-check\",\n          \"markdownDescription\": \"Enables the check command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-download\",\n          \"markdownDescription\": \"Enables the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the download_and_install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-download-and-install\",\n          \"markdownDescription\": \"Enables the download_and_install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:allow-install\",\n          \"markdownDescription\": \"Enables the install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the check command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-check\",\n          \"markdownDescription\": \"Denies the check command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-download\",\n          \"markdownDescription\": \"Denies the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the download_and_install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-download-and-install\",\n          \"markdownDescription\": \"Denies the download_and_install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the install command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"updater:deny-install\",\n          \"markdownDescription\": \"Denies the install command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures what kind of\\noperations are available from the upload plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-upload`\\n- `allow-download`\",\n          \"type\": \"string\",\n          \"const\": \"upload:default\",\n          \"markdownDescription\": \"This permission set configures what kind of\\noperations are available from the upload plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-upload`\\n- `allow-download`\"\n        },\n        {\n          \"description\": \"Enables the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"upload:allow-download\",\n          \"markdownDescription\": \"Enables the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the upload command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"upload:allow-upload\",\n          \"markdownDescription\": \"Enables the upload command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the download command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"upload:deny-download\",\n          \"markdownDescription\": \"Denies the download command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the upload command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"upload:deny-upload\",\n          \"markdownDescription\": \"Denies the upload command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"This permission set configures what kind of\\noperations are available from the window state plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-filename`\\n- `allow-restore-state`\\n- `allow-save-window-state`\",\n          \"type\": \"string\",\n          \"const\": \"window-state:default\",\n          \"markdownDescription\": \"This permission set configures what kind of\\noperations are available from the window state plugin.\\n\\n#### Granted Permissions\\n\\nAll operations are enabled by default.\\n\\n\\n#### This default permission set includes:\\n\\n- `allow-filename`\\n- `allow-restore-state`\\n- `allow-save-window-state`\"\n        },\n        {\n          \"description\": \"Enables the filename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"window-state:allow-filename\",\n          \"markdownDescription\": \"Enables the filename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the restore_state command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"window-state:allow-restore-state\",\n          \"markdownDescription\": \"Enables the restore_state command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Enables the save_window_state command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"window-state:allow-save-window-state\",\n          \"markdownDescription\": \"Enables the save_window_state command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the filename command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"window-state:deny-filename\",\n          \"markdownDescription\": \"Denies the filename command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the restore_state command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"window-state:deny-restore-state\",\n          \"markdownDescription\": \"Denies the restore_state command without any pre-configured scope.\"\n        },\n        {\n          \"description\": \"Denies the save_window_state command without any pre-configured scope.\",\n          \"type\": \"string\",\n          \"const\": \"window-state:deny-save-window-state\",\n          \"markdownDescription\": \"Denies the save_window_state command without any pre-configured scope.\"\n        }\n      ]\n    },\n    \"Value\": {\n      \"description\": \"All supported ACL values.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Represents a null JSON value.\",\n          \"type\": \"null\"\n        },\n        {\n          \"description\": \"Represents a [`bool`].\",\n          \"type\": \"boolean\"\n        },\n        {\n          \"description\": \"Represents a valid ACL [`Number`].\",\n          \"allOf\": [\n            {\n              \"$ref\": \"#/definitions/Number\"\n            }\n          ]\n        },\n        {\n          \"description\": \"Represents a [`String`].\",\n          \"type\": \"string\"\n        },\n        {\n          \"description\": \"Represents a list of other [`Value`]s.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/Value\"\n          }\n        },\n        {\n          \"description\": \"Represents a map of [`String`] keys to [`Value`]s.\",\n          \"type\": \"object\",\n          \"additionalProperties\": {\n            \"$ref\": \"#/definitions/Value\"\n          }\n        }\n      ]\n    },\n    \"Number\": {\n      \"description\": \"A valid ACL number.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Represents an [`i64`].\",\n          \"type\": \"integer\",\n          \"format\": \"int64\"\n        },\n        {\n          \"description\": \"Represents a [`f64`].\",\n          \"type\": \"number\",\n          \"format\": \"double\"\n        }\n      ]\n    },\n    \"Target\": {\n      \"description\": \"Platform target.\",\n      \"oneOf\": [\n        {\n          \"description\": \"MacOS.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"macOS\"\n          ]\n        },\n        {\n          \"description\": \"Windows.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"windows\"\n          ]\n        },\n        {\n          \"description\": \"Linux.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"linux\"\n          ]\n        },\n        {\n          \"description\": \"Android.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"android\"\n          ]\n        },\n        {\n          \"description\": \"iOS.\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"iOS\"\n          ]\n        }\n      ]\n    },\n    \"ShellScopeEntryAllowedArg\": {\n      \"description\": \"A command argument allowed to be executed by the webview API.\",\n      \"anyOf\": [\n        {\n          \"description\": \"A non-configurable argument that is passed to the command in the order it was specified.\",\n          \"type\": \"string\"\n        },\n        {\n          \"description\": \"A variable that is set while calling the command from the webview API.\",\n          \"type\": \"object\",\n          \"required\": [\n            \"validator\"\n          ],\n          \"properties\": {\n            \"raw\": {\n              \"description\": \"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\\n\\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.\",\n              \"default\": false,\n              \"type\": \"boolean\"\n            },\n            \"validator\": {\n              \"description\": \"[regex] validator to require passed values to conform to an expected input.\\n\\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\\n\\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\\\w+` regex would be registered as `^https?://\\\\w+$`.\\n\\n[regex]: <https://docs.rs/regex/latest/regex/#syntax>\",\n              \"type\": \"string\"\n            }\n          },\n          \"additionalProperties\": false\n        }\n      ]\n    },\n    \"ShellScopeEntryAllowedArgs\": {\n      \"description\": \"A set of command arguments allowed to be executed by the webview API.\\n\\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration.\",\n      \"anyOf\": [\n        {\n          \"description\": \"Use a simple boolean to allow all or disable all arguments to this command configuration.\",\n          \"type\": \"boolean\"\n        },\n        {\n          \"description\": \"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.\",\n          \"type\": \"array\",\n          \"items\": {\n            \"$ref\": \"#/definitions/ShellScopeEntryAllowedArg\"\n          }\n        }\n      ]\n    }\n  }\n}"
  },
  {
    "path": "src-tauri/src/main.rs",
    "content": "#![cfg_attr(not(debug_assertions), windows_subsystem = \"windows\")]\n\nuse tauri_plugin_log::{fern::colors::ColoredLevelConfig, Target, TargetKind};\n\nfn main() {\n    tauri::Builder::default()\n        .plugin(tauri_plugin_shell::init())\n        .plugin(tauri_plugin_global_shortcut::Builder::new().build())\n        .plugin(tauri_plugin_window_state::Builder::new().build())\n        .plugin(tauri_plugin_dialog::init())\n        .plugin(tauri_plugin_upload::init())\n        .plugin(tauri_plugin_notification::init())\n        .plugin(tauri_plugin_clipboard_manager::init())\n        .plugin(\n            tauri_plugin_log::Builder::default()\n                .targets([Target::new(TargetKind::Webview)])\n                .with_colors(ColoredLevelConfig::default())\n                .build(),\n        )\n        .plugin(tauri_plugin_store::Builder::default().build())\n        .plugin(tauri_plugin_updater::Builder::default().build())\n        // .plugin(tauri_plugin_window_state::Builder::default().build())\n        .run(tauri::generate_context!())\n        .expect(\"error while running Blink\");\n}\n"
  },
  {
    "path": "src-tauri/tauri.conf.json",
    "content": "{\n\t\"build\": {\n\t\t\"beforeBuildCommand\": \"pnpm run build\",\n\t\t\"beforeDevCommand\": \"pnpm run dev\",\n\t\t\"frontendDist\": \"../dist\",\n\t\t\"devUrl\": \"http://localhost:5173\"\n\t},\n\t\"bundle\": {\n\t\t\"active\": true,\n\t\t\"category\": \"Video\",\n\t\t\"copyright\": \"GPL-3.0-only\",\n\t\t\"windows\": {\n\t\t\t\"certificateThumbprint\": null,\n\t\t\t\"digestAlgorithm\": \"sha256\",\n\t\t\t\"timestampUrl\": \"\",\n\t\t\t\"nsis\": {\n\t\t\t\t\"sidebarImage\": \"../public/installerSideBarImage.bmp\",\n\t\t\t\t\"headerImage\": \"../public/installerHeaderImage.bmp\",\n\t\t\t\t\"installerIcon\": \"icons/icon.ico\",\n\t\t\t\t\"displayLanguageSelector\": true\n\t\t\t}\n\t\t},\n\t\t\"externalBin\": [],\n\t\t\"icon\": [\n\t\t\t\"icons/32x32.png\",\n\t\t\t\"icons/128x128.png\",\n\t\t\t\"icons/128x128@2x.png\",\n\t\t\t\"icons/icon.icns\",\n\t\t\t\"icons/icon.ico\"\n\t\t],\n\t\t\"linux\": {\n\t\t\t\"deb\": {\n\t\t\t\t\"depends\": []\n\t\t\t},\n\t\t\t\"appimage\": {\n\t\t\t\t\"bundleMediaFramework\": true\n\t\t\t}\n\t\t},\n\t\t\"longDescription\": \"A modern desktop client for Jellyfin\",\n\t\t\"macOS\": {\n\t\t\t\"entitlements\": null,\n\t\t\t\"exceptionDomain\": \"\",\n\t\t\t\"frameworks\": [],\n\t\t\t\"providerShortName\": null,\n\t\t\t\"signingIdentity\": null\n\t\t},\n\t\t\"resources\": [],\n\t\t\"shortDescription\": \"Blink\",\n\t\t\"targets\": [\"deb\", \"appimage\", \"nsis\", \"app\", \"dmg\"],\n\t\t\"licenseFile\": \"../LICENSE\",\n\t\t\"createUpdaterArtifacts\": \"v1Compatible\"\n\t},\n\t\"productName\": \"Blink\",\n\t\"version\": \"../package.json\",\n\t\"identifier\": \"com.blink.prayag17\",\n\t\"plugins\": {\n\t\t\"updater\": {\n\t\t\t\"pubkey\": \"dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEZBOTJGNzM3REU2Q0JEMEYKUldRUHZXemVOL2VTK2k2andMWTZYbEN6SnIwV0V0L2Z3NlA4RzFabThPaXRqSFRMc004WGhCb2kK\",\n\t\t\t\"endpoints\": [\n\t\t\t\t\"https://rawcdn.githack.com/prayag17/Blink/refs/heads/main/latest.json\"\n\t\t\t],\n\t\t\t\"windows\": {\n\t\t\t\t\"installMode\": \"passive\"\n\t\t\t}\n\t\t}\n\t},\n\t\"app\": {\n\t\t\"windows\": [\n\t\t\t{\n\t\t\t\t\"fullscreen\": false,\n\t\t\t\t\"height\": 600,\n\t\t\t\t\"width\": 800,\n\t\t\t\t\"resizable\": true,\n\t\t\t\t\"title\": \"Blink\",\n\t\t\t\t\"visible\": true,\n\t\t\t\t\"center\": true,\n\t\t\t\t\"label\": \"main\",\n\t\t\t\t\"dragDropEnabled\": false,\n\t\t\t\t\"backgroundColor\": \"#08001f\",\n\t\t\t\t\"transparent\": false\n\t\t\t}\n\t\t],\n\t\t\"security\": {\n\t\t\t\"csp\": null\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"paths\": {\n      \"@/*\": [\n        \"./src/*\"\n      ]\n    },\n    \"target\": \"ESNext\", // Specify ECMAScript target version\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"baseUrl\": \".\",\n    \"checkJs\": true,\n    \"allowJs\": true, // Allow JavaScript files to be compiled\n    \"skipLibCheck\": true, // Skip type checking of all declaration files\n    \"esModuleInterop\": true, // Disables namespace imports (import * as fs from \"fs\") and enables CJS/AMD/UMD style imports (import fs from \"fs\")\n    \"allowSyntheticDefaultImports\": true, // Allow default imports from modules with no default export\n    \"strict\": true, // Enable all strict type checking options\n    \"forceConsistentCasingInFileNames\": true, // Disallow inconsistently-cased references to the same file.\n    \"module\": \"esnext\", // Specify module code generation\n    \"moduleResolution\": \"node\", // Resolve modules using Node.js style\n    \"isolatedModules\": true, // Unconditionally emit imports for unresolved files\n    \"resolveJsonModule\": true, // Include modules imported with .json extension\n    \"noEmit\": true, // Do not emit output (meaning do not compile code, only perform type checking)\n    \"jsx\": \"react\", // Support JSX in .tsx files\n    \"sourceMap\": true, // Generate corrresponding .map file\n    \"declaration\": true, // Generate corresponding .d.ts file\n    \"noUnusedLocals\": true, // Report errors on unused locals\n    \"noUnusedParameters\": true, // Report errors on unused parameters\n    \"incremental\": true, // Enable incremental compilation by reading/writing information from prior compilations to a file on disk\n    \"noFallthroughCasesInSwitch\": true // Report errors for fallthrough cases in switch statement\n  },\n  \"include\": [\n    \"src/**/*\" // *** The files TypeScript should type check ***\n,\n\"src/routes/_api/player/photos/.tsx\"\n],\n  \"exclude\": [\n    \"node_modules\",\n    \"build\"\n  ]\n}"
  },
  {
    "path": "vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\nimport svgr from \"vite-plugin-svgr\";\nimport wasm from \"vite-plugin-wasm\";\nimport { tanstackRouter } from '@tanstack/router-vite-plugin'\nimport path from \"path\";\n\nconst plugins = [react(), svgr(), tanstackRouter(), wasm()];\n\nexport default defineConfig({\n  plugins: plugins,\n  css: {\n    modules: {\n      scopeBehaviour: \"global\"\n    },\n    preprocessorOptions: {\n      scss: {\n        additionalData: '@use \"/src/styles/variables.scss\" as *;',\n      }\n    }\n  },\n  resolve: {\n    alias: [\n      { find: '@', replacement: path.resolve(__dirname, 'src') },\n    ]\n  },\n  // prevent vite from obscuring rust errors\n  clearScreen: false,\n  // Tauri expects a fixed port, fail if that port is not available\n  server: {\n    strictPort: true\n  },\n  // to make use of `TAURI_PLATFORM`, `TAURI_ARCH`, `TAURI_FAMILY`,\n  // `TAURI_PLATFORM_VERSION`, `TAURI_PLATFORM_TYPE` and `TAURI_DEBUG`\n  // env variables\n  envPrefix: [\"VITE_\", \"TAURI_\"],\n  build: {\n    // Tauri uses Chromium on Windows and WebKit on macOS and Linux\n    target: process.env.TAURI_PLATFORM == \"windows\" ? \"chrome105\" : \"esnext\",\n    // don't minify for debug builds\n    minify: !process.env.TAURI_DEBUG ? \"esbuild\" : false,\n    // produce sourcemaps for debug builds\n    sourcemap: !!process.env.TAURI_DEBUG,\n    cssCodeSplit: process.env.TAURI_DEBUG ? false : undefined\n  },\n  worker: {\n    format: \"es\"\n  }\n});"
  }
]