diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..59c4aed --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,46 @@ +name: Bug report +description: Something in the app is broken or behaves wrong +labels: [bug] +body: + - type: textarea + id: what + attributes: + label: What happens + description: What you did, what you expected, and what you got instead. + placeholder: | + 1. Opened a playlist + 2. Clicked the third video + 3. Player stays black, audio plays + validations: + required: true + + - type: input + id: version + attributes: + label: Zuno version + description: Settings → About, or the installer filename. + placeholder: "1.2.1" + validations: + required: true + + - type: dropdown + id: os + attributes: + label: OS + options: + - Windows + - macOS + - Linux + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Log + description: | + `current.log` from the log directory — it usually says what failed. + Windows: `%LOCALAPPDATA%\com.zuno.desktop\logs` + macOS: `~/Library/Logs/com.zuno.desktop` + Linux: `~/.local/share/com.zuno.desktop/logs` + render: text diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..e42d371 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +# Blank issues stay on: not every report fits a form, and a bad-fit form is how a real +# bug turns into no report at all. +blank_issues_enabled: true +contact_links: + - name: Security vulnerability + url: https://github.com/noFAYZ/zuno/security/advisories/new + about: Report privately, not as a public issue. See SECURITY.md for scope. + + - name: Question or help using Zuno + url: https://github.com/noFAYZ/zuno/discussions/new?category=q-a + about: Not sure whether it is a bug? Ask here — it can be moved to an issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..eb93bfa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,17 @@ +name: Feature request +description: Suggest something Zuno should do +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: The problem + description: What are you trying to do that Zuno makes hard or impossible today? + validations: + required: true + + - type: textarea + id: idea + attributes: + label: What you have in mind + description: Optional. A rough idea is fine — the problem above matters more. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..0d829a2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,33 @@ +# Dependency updates. Monthly, grouped into one PR per ecosystem — zuno has a single +# maintainer, and a weekly drip of five separate PRs is how update PRs get ignored. +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: monthly + groups: + npm: + patterns: ["*"] + + - package-ecosystem: npm + directory: /landing + schedule: + interval: monthly + groups: + landing: + patterns: ["*"] + + - package-ecosystem: cargo + directory: /src-tauri + schedule: + interval: monthly + groups: + cargo: + patterns: ["*"] + + # Ungrouped: an action bump is one line and worth reading on its own. + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..9c968f7 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +## What and why + + + +Closes # + +## How it was checked + + + +- [ ] `npm run verify` passes +- [ ] `cargo test` passes (if `src-tauri/` changed) +- [ ] Ran the app and used the affected screen +- [ ] Screenshot or clip below (UI changes) + +## Notes for the reviewer + + diff --git a/.github/rulesets/README.md b/.github/rulesets/README.md new file mode 100644 index 0000000..aeca327 --- /dev/null +++ b/.github/rulesets/README.md @@ -0,0 +1,27 @@ +# Rulesets + +Rulesets are repository *settings*, not configuration GitHub reads from the tree — a file here +does nothing until it is imported. It lives in the repo so the protection on `main` is reviewable +in a diff instead of being a thing someone once clicked. + +Apply or update: + +```sh +gh api --method PUT repos/noFAYZ/zuno/rulesets/20021321 --input .github/rulesets/main.json +``` + +`main.json` is applied — id `20021321`, created 2026-07-30. Edit the file, run the command, and the +two stay in step; skip the command and the file is fiction. `gh api repos/noFAYZ/zuno/rulesets` +lists what is actually live. The UI equivalent is Settings → Rules → Rulesets. + +## What `main.json` does + +- No deletion, no force-push. +- Changes arrive by pull request. **Zero** approvals required — this is a solo-maintained repo, and + a review count of one would lock the maintainer out of their own default branch, since nobody can + approve their own PR. +- `verify` and `rust` — both CI jobs — must pass first. Not strict: a branch does not have to be + rebased onto the newest `main` before merging, which on a repo this size is churn without a + matching risk. +- No bypass actors. An admin can still edit the ruleset itself, so the escape hatch exists without + a standing exemption that quietly makes the rules advisory. diff --git a/.github/rulesets/main.json b/.github/rulesets/main.json new file mode 100644 index 0000000..b3ab48f --- /dev/null +++ b/.github/rulesets/main.json @@ -0,0 +1,35 @@ +{ + "name": "main", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { + "include": ["~DEFAULT_BRANCH"], + "exclude": [] + } + }, + "bypass_actors": [], + "rules": [ + { "type": "deletion" }, + { "type": "non_fast_forward" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false, + "allowed_merge_methods": ["merge", "squash", "rebase"] + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "do_not_enforce_on_create": false, + "required_status_checks": [{ "context": "verify" }, { "context": "rust" }] + } + } + ] +} diff --git a/.github/workflows/aur.yml b/.github/workflows/aur.yml new file mode 100644 index 0000000..58f0248 --- /dev/null +++ b/.github/workflows/aur.yml @@ -0,0 +1,108 @@ +# Publishes zuno to the AUR whenever a release is published. +# +# Runs from CI because the AUR needs an Arch environment and an SSH key, and the maintainer +# develops on Windows — doing this by hand meant either a VM or the package going stale. +# +# Requires one repository secret: +# AUR_SSH_PRIVATE_KEY — the private half of the SSH key registered on the AUR account +name: AUR + +on: + release: + types: [published] + # Lets a botched package be re-pushed without cutting another release. + workflow_dispatch: + inputs: + version: + description: "Version to publish, without the leading v (e.g. 1.2.1)" + required: true + +# Publishing happens over SSH to the AUR, not through the GitHub token. +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + container: archlinux:base-devel + steps: + - uses: actions/checkout@v4 + + - name: Resolve version + id: version + run: | + if [ -n "${{ github.event.inputs.version }}" ]; then + version="${{ github.event.inputs.version }}" + else + version="${GITHUB_REF_NAME#v}" + fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Install packaging tools + run: | + pacman -Syu --noconfirm --needed git openssh pacman-contrib sudo + # makepkg and everything built on it — updpkgsums, --printsrcinfo — refuse to run + # as root, and the container is root. One unprivileged user serves all of them. + useradd -m builder + # `makepkg -s` installs the declared dependencies, which needs pacman. + echo 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder + + - name: Update version and checksum + working-directory: packaging/aur/zuno + run: | + version="${{ steps.version.outputs.version }}" + sed -i "s/^pkgver=.*/pkgver=${version}/" PKGBUILD + sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD + chown -R builder . + # updpkgsums downloads the released .deb and rewrites sha256sums from what actually + # shipped, rather than trusting a hash edited by hand alongside the version. + # + # .SRCINFO is what the AUR reads — not the PKGBUILD — so a stale one silently serves + # the old version even after a successful push. + su builder -c "updpkgsums && makepkg --printsrcinfo > .SRCINFO" + + - name: Build it before anyone else has to + working-directory: packaging/aur/zuno + run: su builder -c "makepkg -sf --noconfirm" + + - name: Publish + env: + AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + run: | + version="${{ steps.version.outputs.version }}" + + mkdir -p ~/.ssh + echo "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur + chmod 600 ~/.ssh/aur + # `accept-new` rather than a keyscan: a keyscan that returns nothing fails silently + # and leaves an empty known_hosts, which is what broke the first run. This trusts + # the host on first contact and pins it for the rest of the job, and unlike + # StrictHostKeyChecking=no it still rejects a key that changes mid-run. + export GIT_SSH_COMMAND="ssh -i ~/.ssh/aur -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=$HOME/.ssh/known_hosts" + + git config --global user.name "${{ github.actor }}" + git config --global user.email "${{ github.actor }}@users.noreply.github.com" + + # A package that does not exist yet clones as an empty repo rather than failing, so + # the first run creates it and every run after updates it. That matters because the + # maintainer is on Windows: without this, publishing would need a Linux box once, + # purely to make the initial commit. + git clone ssh://aur@aur.archlinux.org/zuno.git /tmp/aur || { + echo "No AUR repo yet — initialising one." + mkdir -p /tmp/aur + git -C /tmp/aur init -b master + git -C /tmp/aur remote add origin ssh://aur@aur.archlinux.org/zuno.git + } + + # Only these two files belong in an AUR repo — no sources, no built packages. + cp packaging/aur/zuno/PKGBUILD packaging/aur/zuno/.SRCINFO /tmp/aur/ + + cd /tmp/aur + # `--quiet` alone reports nothing on a fresh repo where the files are untracked. + git add PKGBUILD .SRCINFO + if git diff --cached --quiet; then + echo "AUR already at ${version}, nothing to push." + exit 0 + fi + git commit -m "zuno ${version}" + git push -u origin master diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bd11176 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,99 @@ +# Typecheck and self-checks on every push and pull request. +# +# release.yml was the only workflow, and it runs `tsc` solely as part of `npm run build` at +# tag time — so a type error could sit on main until someone cut a version. This is the gate +# that was missing, kept deliberately cheap: no Rust toolchain, no bundling, no artifacts. +name: CI + +on: + push: + branches: [main] + pull_request: + +# Nothing here writes to the repo. A compromised dependency in `npm ci` gets a token that +# can only read. +permissions: + contents: read + +# A second push to the same branch makes the first run's answer irrelevant. +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Self-checks + run: npm run check + + # Windows rather than Ubuntu: the crate pulls in Tauri, and building it on Linux needs + # webkit2gtk and friends installed first. Windows needs nothing, and it is the platform + # these tests — cookie jar merging, range headers, URL sanitising — actually ship on. + rust: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - name: Rust tests + working-directory: src-tauri + run: cargo test + + # Compile-only, and only because of `#[cfg(target_os = "linux")]`. + # + # The job above runs on Windows, so every Linux-gated line — `linux_media.rs`, the MPRIS + # session, anything else added behind that cfg — was invisible to CI. A pull request touching + # it could go green without the code ever having been fed to a compiler. + # + # `cargo check` rather than `cargo test`: the tests are platform-independent and the Windows + # job already runs them. What is missing here is the compiler, not the assertions. + rust-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + # A different target dir to the Windows job's — sharing one would thrash the cache. + key: linux + + # Mirrors the list in release.yml's Linux build. If one changes, change both. + - name: System dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + libjavascriptcoregtk-4.1-dev \ + libsoup-3.0-dev \ + librsvg2-dev \ + libssl-dev \ + build-essential + + # `--locked` because a Cargo.toml change that forgets its Cargo.lock is the other way this + # goes wrong quietly. + - name: Typecheck + working-directory: src-tauri + run: cargo check --locked diff --git a/.github/workflows/flatpak.yml b/.github/workflows/flatpak.yml new file mode 100644 index 0000000..5d5a15c --- /dev/null +++ b/.github/workflows/flatpak.yml @@ -0,0 +1,95 @@ +# Builds the Flatpak and generates the offline source manifests. +# +# Flatpak builds have no network, so every crate and npm package must be listed in +# cargo-sources.json / node-sources.json. Those are generated, not written — this job +# regenerates them, builds against them, and uploads both plus the .flatpak so a submission +# can be assembled without a Linux machine. +# +# Not wired to releases: Flathub builds from its own repo once the app is accepted. This is +# for validating the manifest before and after submission. +name: Flatpak + +on: + workflow_dispatch: + pull_request: + paths: + - "packaging/flatpak/**" + - ".github/workflows/flatpak.yml" + +# Artifacts upload without repo write access. +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + container: + image: bilelmoussaoui/flatpak-github-actions:gnome-46 + options: --privileged + + steps: + - uses: actions/checkout@v4 + + - name: Install generators + run: | + # appstream and desktop-file-utils supply the two validators below; neither ships + # in the flatpak-builder image. + dnf install -y python3-pip git nodejs npm appstream desktop-file-utils + # tomlkit and aiohttp are what the cargo generator imports — python3-toml is a + # different library and does not satisfy it. + pip3 install --break-system-packages tomlkit aiohttp + curl -sSLo /usr/local/bin/flatpak-cargo-generator.py \ + https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py + chmod +x /usr/local/bin/flatpak-cargo-generator.py + # The node generator is a Python package inside flatpak-builder-tools, not an npm + # module — `npm i -g flatpak-node-generator` installs something unrelated. + git clone --depth 1 https://github.com/flatpak/flatpak-builder-tools /tmp/fbt + pip3 install --break-system-packages /tmp/fbt/node + + - name: Generate cargo sources + run: | + python3 /usr/local/bin/flatpak-cargo-generator.py \ + src-tauri/Cargo.lock -o packaging/flatpak/cargo-sources.json + echo "cargo sources: $(wc -l < packaging/flatpak/cargo-sources.json) lines" + + - name: Generate node sources + run: | + python3 -m flatpak_node_generator npm package-lock.json \ + -o packaging/flatpak/node-sources.json + echo "node sources: $(wc -l < packaging/flatpak/node-sources.json) lines" + + - name: Add flathub and install the runtime + run: | + flatpak remote-add --user --if-not-exists flathub \ + https://dl.flathub.org/repo/flathub.flatpakrepo + # The SDK extensions track the freedesktop base, not the GNOME version — GNOME 47 + # sits on 24.08, so rust-stable//47 does not exist and the build would fail + # resolving it. + flatpak install --user -y --noninteractive flathub \ + org.gnome.Platform//47 \ + org.gnome.Sdk//47 \ + org.freedesktop.Sdk.Extension.rust-stable//24.08 \ + org.freedesktop.Sdk.Extension.node20//24.08 + + - name: Validate metadata + run: | + # Both are hard gates at Flathub review, and both are cheap to check here. + appstreamcli validate --no-net packaging/flatpak/io.github.nofayz.Zuno.metainfo.xml + desktop-file-validate packaging/flatpak/io.github.nofayz.Zuno.desktop + + - name: Build + run: | + flatpak-builder --user --disable-rofiles-fuse --force-clean \ + --install-deps-from=flathub \ + --repo=repo build-dir packaging/flatpak/io.github.nofayz.Zuno.yml + flatpak build-bundle repo zuno.flatpak io.github.nofayz.Zuno + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: flatpak + path: | + zuno.flatpak + packaging/flatpak/cargo-sources.json + packaging/flatpak/node-sources.json + if-no-files-found: warn diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a02508d..d717d6f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,242 +1,364 @@ -name: Release - -on: - workflow_dispatch: - inputs: - release_tag: - description: Existing GitHub Release tag, for example v1.2.4 - required: true - type: string - release: - types: [published] -permissions: - contents: write - -env: - RELEASE_TAG: ${{ inputs.release_tag || github.event.release.tag_name }} - -jobs: - release: - name: Build ${{ matrix.platform.name }} - strategy: - fail-fast: false - matrix: - platform: - - name: Windows - os: windows-latest - rust_target: "" - build_args: --bundles msi,nsis - apple_signing_identity: "" - - name: macOS Apple Silicon - os: macos-latest - rust_target: aarch64-apple-darwin - build_args: --target aarch64-apple-darwin --bundles dmg - apple_signing_identity: "-" - - name: macOS Intel - os: macos-15-intel - rust_target: x86_64-apple-darwin - build_args: --target x86_64-apple-darwin --bundles dmg - apple_signing_identity: "-" - - name: Linux - os: ubuntu-latest - rust_target: "" - build_args: --bundles deb,appimage - apple_signing_identity: "" - runs-on: ${{ matrix.platform.os }} - - steps: - - name: Verify release exists - shell: bash - run: gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Read release type - id: release_metadata - shell: bash - run: | - set -euo pipefail - is_prerelease="$(gh release view "$RELEASE_TAG" \ - --repo "$GITHUB_REPOSITORY" \ - --json isPrerelease \ - --jq .isPrerelease)" - - if test "$is_prerelease" = "true"; then - echo "publish_update=false" >> "$GITHUB_OUTPUT" - else - echo "publish_update=true" >> "$GITHUB_OUTPUT" - fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Check out repository - uses: actions/checkout@v4 - with: - ref: ${{ env.RELEASE_TAG }} - fetch-depth: 0 - - - name: Verify checked out tag - shell: bash - run: | - set -euo pipefail - test "$(git describe --tags --exact-match)" = "$RELEASE_TAG" - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - - - name: Add Rust target - if: matrix.platform.rust_target != '' - run: rustup target add ${{ matrix.platform.rust_target }} - - - name: Install Linux system dependencies - if: matrix.platform.name == 'Linux' - shell: bash - run: | - sudo apt-get update - sudo apt-get install -y \ - libwebkit2gtk-4.1-dev \ - build-essential \ - curl \ - wget \ - file \ - libssl-dev \ - libayatana-appindicator3-dev \ - librsvg2-dev \ - libgtk-3-dev \ - libsoup-3.0-dev \ - libjavascriptcoregtk-4.1-dev \ - patchelf - - - name: Cache Rust build - uses: swatinem/rust-cache@v2 - with: - workspaces: "./src-tauri -> target" - - - name: Install frontend dependencies - run: npm ci - - - name: Build and upload installers - uses: tauri-apps/tauri-action@v0 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - APPLE_SIGNING_IDENTITY: ${{ matrix.platform.apple_signing_identity }} - TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} - TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - with: - tagName: ${{ env.RELEASE_TAG }} - releaseDraft: false - uploadUpdaterJson: ${{ steps.release_metadata.outputs.publish_update }} - uploadUpdaterSignatures: false - updaterJsonPreferNsis: true - args: ${{ matrix.platform.build_args }} - - - name: Upload Windows portable executable - if: matrix.platform.name == 'Windows' - shell: bash - run: | - set -euo pipefail - gh release upload "$RELEASE_TAG" \ - "src-tauri/target/release/just-another-music-client.exe#Just-Another-Music-Client-Windows-portable.exe" \ - --clobber - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Verify macOS DMG and app signature - if: startsWith(matrix.platform.name, 'macOS') - shell: bash - run: | - set -euo pipefail - - bundle_dir="src-tauri/target/${{ matrix.platform.rust_target }}/release/bundle/dmg" - dmg_path="$(find "$bundle_dir" -maxdepth 1 -name '*.dmg' -print -quit)" - test -n "$dmg_path" - hdiutil verify "$dmg_path" - - mount_dir="$(mktemp -d)" - cleanup() { - hdiutil detach "$mount_dir" -quiet || true - rmdir "$mount_dir" || true - } - trap cleanup EXIT - - hdiutil attach "$dmg_path" -readonly -nobrowse -mountpoint "$mount_dir" - app_path="$(find "$mount_dir" -maxdepth 1 -name '*.app' -print -quit)" - test -n "$app_path" - codesign --verify --deep --strict --verbose=2 "$app_path" - - publish-updater-channel: - name: Publish gated updater metadata - needs: release - runs-on: ubuntu-latest - - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - ref: ${{ env.RELEASE_TAG }} - fetch-depth: 0 - - - name: Read release type - id: release_metadata - shell: bash - run: | - set -euo pipefail - is_prerelease="$(gh release view "$RELEASE_TAG" \ - --repo "$GITHUB_REPOSITORY" \ - --json isPrerelease \ - --jq .isPrerelease)" - - if test "$is_prerelease" = "true"; then - echo "publish_update=false" >> "$GITHUB_OUTPUT" - else - echo "publish_update=true" >> "$GITHUB_OUTPUT" - fi - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Download generated updater metadata - if: steps.release_metadata.outputs.publish_update == 'true' - shell: bash - run: | - set -euo pipefail - gh release download "$RELEASE_TAG" \ - --repo "$GITHUB_REPOSITORY" \ - --pattern latest.json \ - --dir "$RUNNER_TEMP/updater-channel" - test -s "$RUNNER_TEMP/updater-channel/latest.json" - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish updater channel - if: steps.release_metadata.outputs.publish_update == 'true' - shell: bash - run: | - set -euo pipefail - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - if git ls-remote --exit-code --heads origin updater-channel >/dev/null 2>&1; then - git fetch origin updater-channel - git switch --force-create updater-channel origin/updater-channel - else - git switch --orphan updater-channel - fi - - git rm -rf --ignore-unmatch . - cp "$RUNNER_TEMP/updater-channel/latest.json" latest.json - git add latest.json - - if git diff --cached --quiet; then - echo "Updater channel already points to $RELEASE_TAG." - exit 0 - fi - - git commit -m "Offer $RELEASE_TAG through the updater" - git push origin updater-channel +name: Release + +on: + # Three ways in, all landing on the same `prepare` job: + # - a commit on main that changes the version in package.json (the normal path), + # - pushing a `v*` tag, + # - a manual dispatch, to rebuild a release that already exists. + push: + branches: [main] + tags: ['v*'] + workflow_dispatch: + inputs: + release_tag: + description: Tag to build, for example v1.2.4. Defaults to the version in package.json. + required: false + type: string + release: + types: [published] +permissions: + contents: write + +jobs: + prepare: + name: Resolve release + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.resolve.outputs.tag }} + proceed: ${{ steps.resolve.outputs.proceed }} + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Resolve tag and create the release + id: resolve + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + REF_NAME: ${{ github.ref_name }} + REF_TYPE: ${{ github.ref_type }} + INPUT_TAG: ${{ inputs.release_tag }} + EVENT_TAG: ${{ github.event.release.tag_name }} + run: | + set -euo pipefail + + # The tag under discussion, whichever way this run started. + case "$EVENT_NAME" in + workflow_dispatch) tag="$INPUT_TAG" ;; + release) tag="$EVENT_TAG" ;; + push) test "$REF_TYPE" = tag && tag="$REF_NAME" || tag="" ;; + *) tag="" ;; + esac + if test -z "$tag"; then + tag="v$(node -p "require('./package.json').version")" + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "Release tag: $tag" + + # An ordinary commit on main is not a release. Only the one that lands a version + # nobody has released yet is, which is what makes "bump, commit, push" enough. + if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + if test "$EVENT_NAME" = push && test "$REF_TYPE" = branch; then + echo "$tag is already released; nothing to do." + echo "proceed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "proceed=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if ! git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -a "$tag" -m "Release ${tag#v}" + git push origin "$tag" + fi + + # Notes come from the annotated tag, else the release commit's own message — the + # place the change is already described — and only then from GitHub's generator. + notes="$RUNNER_TEMP/release-notes.md" + git tag -l --format='%(contents:body)' "$tag" > "$notes" + if ! test -s "$notes"; then + git log -1 --format='%b' "$tag^{commit}" | grep -v '^Co-Authored-By:' > "$notes" || true + fi + + if test -s "$notes"; then + gh release create "$tag" --repo "$GITHUB_REPOSITORY" \ + --title "Zuno ${tag#v}" --notes-file "$notes" + else + gh release create "$tag" --repo "$GITHUB_REPOSITORY" \ + --title "Zuno ${tag#v}" --generate-notes + fi + echo "proceed=true" >> "$GITHUB_OUTPUT" + + release: + name: Build ${{ matrix.platform.name }} + needs: prepare + if: needs.prepare.outputs.proceed == 'true' + env: + RELEASE_TAG: ${{ needs.prepare.outputs.tag }} + strategy: + fail-fast: false + matrix: + platform: + - name: Windows + os: windows-latest + rust_target: "" + build_args: --bundles msi,nsis + apple_signing_identity: "" + - name: macOS Apple Silicon + os: macos-latest + rust_target: aarch64-apple-darwin + build_args: --target aarch64-apple-darwin --bundles app,dmg + apple_signing_identity: "-" + - name: macOS Intel + os: macos-15-intel + rust_target: x86_64-apple-darwin + build_args: --target x86_64-apple-darwin --bundles app,dmg + apple_signing_identity: "-" + - name: Linux + os: ubuntu-latest + rust_target: "" + build_args: --bundles deb,rpm,appimage + apple_signing_identity: "" + runs-on: ${{ matrix.platform.os }} + + steps: + - name: Verify release exists + shell: bash + run: gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Read release type + id: release_metadata + shell: bash + run: | + set -euo pipefail + is_prerelease="$(gh release view "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --json isPrerelease \ + --jq .isPrerelease)" + + if test "$is_prerelease" = "true"; then + echo "publish_update=false" >> "$GITHUB_OUTPUT" + else + echo "publish_update=true" >> "$GITHUB_OUTPUT" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check out repository + uses: actions/checkout@v4 + with: + ref: ${{ env.RELEASE_TAG }} + fetch-depth: 0 + + - name: Verify checked out tag + shell: bash + run: | + set -euo pipefail + test "$(git describe --tags --exact-match)" = "$RELEASE_TAG" + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Add Rust target + if: matrix.platform.rust_target != '' + run: rustup target add ${{ matrix.platform.rust_target }} + + - name: Install Linux system dependencies + if: matrix.platform.name == 'Linux' + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + build-essential \ + curl \ + wget \ + file \ + libssl-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + libgtk-3-dev \ + libsoup-3.0-dev \ + libjavascriptcoregtk-4.1-dev \ + patchelf + + - name: Cache Rust build + uses: swatinem/rust-cache@v2 + with: + workspaces: "./src-tauri -> target" + + - name: Install frontend dependencies + run: npm ci + + - name: Build and upload installers + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + APPLE_SIGNING_IDENTITY: ${{ matrix.platform.apple_signing_identity }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + with: + tagName: ${{ env.RELEASE_TAG }} + releaseDraft: false + uploadUpdaterJson: ${{ steps.release_metadata.outputs.publish_update }} + uploadUpdaterSignatures: false + updaterJsonPreferNsis: true + args: ${{ matrix.platform.build_args }} + + - name: Upload Windows portable executable + if: matrix.platform.name == 'Windows' + shell: bash + run: | + set -euo pipefail + # Located rather than hardcoded: the executable is named from the Cargo package + # (`zuno`), which Tauri may rename to `productName` during bundling. Globbing the + # one top-level exe survives either, and fails loudly if that assumption breaks. + portable_exe=$(find src-tauri/target/release -maxdepth 1 -type f -name '*.exe' | head -n 1) + if [ -z "$portable_exe" ]; then + echo "No portable executable found in src-tauri/target/release" >&2 + exit 1 + fi + echo "Uploading $portable_exe" + gh release upload "$RELEASE_TAG" \ + "${portable_exe}#Zuno-Windows-portable.exe" \ + --clobber + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify macOS DMG and app signature + if: startsWith(matrix.platform.name, 'macOS') + shell: bash + run: | + set -euo pipefail + + bundle_dir="src-tauri/target/${{ matrix.platform.rust_target }}/release/bundle/dmg" + dmg_path="$(find "$bundle_dir" -maxdepth 1 -name '*.dmg' -print -quit)" + test -n "$dmg_path" + hdiutil verify "$dmg_path" + + mount_dir="$(mktemp -d)" + cleanup() { + hdiutil detach "$mount_dir" -quiet || true + rmdir "$mount_dir" || true + } + trap cleanup EXIT + + hdiutil attach "$dmg_path" -readonly -nobrowse -mountpoint "$mount_dir" + app_path="$(find "$mount_dir" -maxdepth 1 -name '*.app' -print -quit)" + test -n "$app_path" + codesign --verify --deep --strict --verbose=2 "$app_path" + + publish-updater-channel: + name: Publish gated updater metadata + needs: [prepare, release] + runs-on: ubuntu-latest + env: + RELEASE_TAG: ${{ needs.prepare.outputs.tag }} + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + ref: ${{ env.RELEASE_TAG }} + fetch-depth: 0 + + - name: Read release type + id: release_metadata + shell: bash + run: | + set -euo pipefail + is_prerelease="$(gh release view "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --json isPrerelease \ + --jq .isPrerelease)" + + if test "$is_prerelease" = "true"; then + echo "publish_update=false" >> "$GITHUB_OUTPUT" + else + echo "publish_update=true" >> "$GITHUB_OUTPUT" + fi + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Download generated updater metadata + if: steps.release_metadata.outputs.publish_update == 'true' + shell: bash + run: | + set -euo pipefail + gh release download "$RELEASE_TAG" \ + --repo "$GITHUB_REPOSITORY" \ + --pattern latest.json \ + --dir "$RUNNER_TEMP/updater-channel" + test -s "$RUNNER_TEMP/updater-channel/latest.json" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish updater channel + if: steps.release_metadata.outputs.publish_update == 'true' + shell: bash + run: | + set -euo pipefail + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + if git ls-remote --exit-code --heads origin updater-channel >/dev/null 2>&1; then + git fetch origin updater-channel + git switch --force-create updater-channel origin/updater-channel + else + git switch --orphan updater-channel + fi + + git rm -rf --ignore-unmatch . + cp "$RUNNER_TEMP/updater-channel/latest.json" latest.json + git add latest.json + + if git diff --cached --quiet; then + echo "Updater channel already points to $RELEASE_TAG." + exit 0 + fi + + git commit -m "Offer $RELEASE_TAG through the updater" + git push origin updater-channel + + publish-winget: + name: Publish to winget + needs: [prepare, release] + runs-on: ubuntu-latest + env: + RELEASE_TAG: ${{ needs.prepare.outputs.tag }} + + steps: + - name: Read release type + id: release_metadata + shell: bash + run: | + set -euo pipefail + gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \ + --json isPrerelease --jq '"prerelease=" + (.isPrerelease | tostring)' \ + >> "$GITHUB_OUTPUT" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Version defaults to the release tag with the leading `v` stripped. + - uses: vedantmgoyal9/winget-releaser@main + if: steps.release_metadata.outputs.prerelease == 'false' + with: + identifier: noFAYZ.Zuno + release-tag: ${{ env.RELEASE_TAG }} + installers-regex: '_x64-setup\.exe$' + token: ${{ secrets.WINGET_TOKEN }} diff --git a/.gitignore b/.gitignore index 77a9ba9..4e636ee 100644 --- a/.gitignore +++ b/.gitignore @@ -29,12 +29,38 @@ dist-ssr *.VC.db *.VC.VC.opendb +.docs + # Tooling caches .eslintcache # Rust application lockfiles should be committed for reproducible builds. -.vscode/* - outputs/* .codex/* + +# Crash droppings from the Git-for-Windows shell. +*.stackdump + +# Updater signing keys. `tauri signer generate -w ~/...` does not expand `~` on +# Windows and drops a literal `~/` folder in the repo root — this makes sure a +# private key landing here can never be committed. +~/ +*.key +!**/*.key.pub + +# Secrets. `*.local` above already covers `.env.local`, but not a plain `.env` — +# which is the one people actually create in a hurry. +.env +.env.* +!.env.example +*.pem +*.p12 +*.pfx + +# Reference dumps and working images kept in docs/ for local use only. The committed +# documentation (architecture.md, backend.md, frontend.md, solarIcons.md) and the README's +# screenshots stay tracked — this covers the scratch material that accumulates beside them. +docs/*.txt +docs/*.webp +docs/zuno.PNG diff --git a/CLA.md b/CLA.md index aedb36d..05c8f1b 100644 --- a/CLA.md +++ b/CLA.md @@ -1,14 +1,14 @@ -# Contributor License Agreement - -By submitting a contribution to JustAnotherMusicClient, you confirm that: - -1. You have the right to submit the contribution. -2. You retain copyright ownership of your contribution. -3. You grant the project owner a perpetual, worldwide, irrevocable, - royalty-free, transferable, and sublicensable license to use, reproduce, - modify, distribute, commercialize, and relicense your contribution as part - of this project or related products. -4. Your contribution is provided without warranties or guarantees. - -Submitting a pull request or other contribution indicates your acceptance of -this agreement. +# Contributor License Agreement + +By submitting a contribution to Zuno, you confirm that: + +1. You have the right to submit the contribution. +2. You retain copyright ownership of your contribution. +3. You grant the project owner a perpetual, worldwide, irrevocable, + royalty-free, transferable, and sublicensable license to use, reproduce, + modify, distribute, commercialize, and relicense your contribution as part + of this project or related products. +4. Your contribution is provided without warranties or guarantees. + +Submitting a pull request or other contribution indicates your acceptance of +this agreement. diff --git a/README.md b/README.md index 2499ee8..69fcca0 100644 --- a/README.md +++ b/README.md @@ -1,199 +1,292 @@ +

+ Zuno +

+ +

Zuno

+ +

+ A fast, native-feeling desktop client for YouTube Music.
+ Built with Tauri, React and TypeScript for Windows, macOS and Linux. +

+ +

+ Downloads + Version + License + Stars + AUR +

- - + + + Zuno home view -

- Downloads - Version - License - Stars - Reddit -

-

-
- -A desktop YouTube Music client built with Tauri, React, and TypeScript for **Windows, MacOs & Linux**. -
+> [!IMPORTANT] +> Zuno is an independent, unofficial project. It is not affiliated with, authorized by, +> sponsored by, or endorsed by YouTube or Google. + +
+ +## About + +Zuno brings YouTube Music to the desktop as a focused application rather than a browser tab. +There is no official YouTube Music desktop client, so the goal here is a polished one that +feels native on each platform and stays fast with large libraries. + +If you find it useful, **starring the repo** genuinely helps. + +
+ +## Screenshots + + + + + + + + + + + + + +
Home — darkHome — light
Zuno home view in dark themeZuno home view in light theme
The "Made for you" carousel, quick destinations and recently played. The carousel can be hidden from Settings.
+ + + + + + + + + + + + + +
Lyrics — darkLyrics — light
Synced lyrics in dark theme, following the current lineSynced lyrics in light theme, in the two-column layout
Lines fade with distance from the one playing. Click any line to seek, nudge the timing if a match is a little off, and see which source the words came from.
+ + + + + + + + + + + + + +
Artist — darkArtist — light
An artist page in dark theme, showing popular songs and releasesAn artist page in light theme, with the queue panel open
Popular tracks and every release, filtered by albums, singles or EPs — with subscribe and shuffle in reach.
+ + + + + + + + + + + + + +
Queue — darkLibrary — light
The queue panel open beside the home view in dark themeThe library songs list, with explicit badges and the now-playing meter
The queue separates what you added by hand from what came next on its own. The library marks what is liked, downloaded and explicit at a glance.
-> **IMPORTANT** This is an independent, unofficial project and is not affiliated with, authorized by, sponsored by, or endorsed by YouTube or Google. The only reason I am making this, is becuase there is no official YouTube music desktop client. +

+ Search open over an artist page +
+ Search is one keystroke away — Ctrl+Space from anywhere, across artists, songs, playlists and albums. +

-

- - Donate on Ko-fi - -
+ Zuno settings, showing the library and lyrics options +
+ Settings — quality, lyrics source, translation and text size, downloads and the disk cap. +

- +
-## About ℹ️ +## Features -JustAnotherMusicClient brings YouTube Music to the desktop in a focused, native-feeling application. YouTube does not provide an official desktop client, so this project aims to provide a polished alternative that integrates with YouTube Music while keeping the experience fast and familiar. +| Feature | Description | +|---|---| +| **Multiple tabs** | Each tab keeps its own queue, volume and player state — start an album in one, browse in another, and the first keeps playing | +| **Offline downloads** | Save a song, a selection, or a whole album or playlist, with its own quality setting and a size cap you control | +| **Mini player** | A morphing capsule that appears when you tab away; drag it anywhere, hover to expand | +| **Synced lyrics** | Line-by-line lyrics that follow the song — not available on the official web client. Click a line to jump to it, nudge the timing when a match is slightly off, and pick which source is tried first | +| **Lyrics translation** | Show a translation under each line, in any of twenty languages, with an adjustable text size | +| **Like & dislike** | Rate from the row, the player or the right-click menu; ratings sync to your YouTube Music account | +| **Batch actions** | Shift/ctrl-select rows, then queue, download, add to a playlist or remove them together | +| **Queue control** | Collapses to an artwork rail — end the queue here, build a queue from here, shuffle, clear | +| **Recommendations** | Personalised suggestions plus a "surprise me" shuffle — hide the carousel if you would rather open on your library | +| **Browse** | Explore, charts, moods and genres, and podcasts, with mood chips you can drill into | +| **Local files** | Folders from your own machine sit alongside your library, with a tag editor for fixing metadata | +| **Discord & Last.fm** | Rich Presence and scrobbling, each toggleable straight from the toolbar | +| **Account support** | Sign in with Google for your library and playlists, and switch between channels on the same account | +| **Playlist import/export** | Save a playlist to a file and bring it back, on this machine or another | +| **Desktop integration** | Media keys, minimise to tray, launch at login, remembered window position, rebindable shortcuts | +| **Light & dark themes** | Follows the OS by default, or pin either one — plus a reduced-motion mode | +| **Caching** | Playlists, lyrics and artwork are cached, so revisits are instant | +| **Auto-updates** | Signed updates install themselves; no manual re-download | + +
+ +## Download + +Grab the newest installer from the **[latest release](https://github.com/noFAYZ/zuno/releases/latest)** +for Windows, macOS or Linux. + +On Arch and derivatives, install from the AUR instead: -If you like this project, **starring it on Github** would help A LOT! +```bash +yay -S zuno # or: paru -S zuno +``` -**Contact for legal purposes:** team@2late.org +
-[**Buy me a cofee**](https://ko-fi.com/totally2late): This supports development and helps me update this project and pay for hosting! If you want a feature to be finished a bit faster, mabey add it to a message there (; - - - -

- - - - - - -

-

-

+## Platform support -## Features ✨ -| Feature | Description | -|---|---| -| Multiple Tabs | Create multiple music tabs, each with its own playback queue, volume, and player state | -| Caching | Playlists, lyrics, and more are cached for significantly faster performance | -| Recommendations | Home Tab with personalized song sugesstions and a "random" shuffle wheel, just like on the mobile client | -| Discord RPC | The songs you are listening to are displayed on Discord | -| Last FM | Last FM integration to track listening | -| Mini player | Small Mini Music player when you tab out of the app that can be moved with either right mouse or dragging | -| Synced Lyrics | Real-time synced lyrics, not even available on the official client | -| YouTube Music Integration | Browse, search, like songs, and play music via an integrated search bar | -| Account Support | Sign in to access your library, playlists, recommendations, and other account features | -| Song Management | Add songs to playlists or queue, view your queue, quickly via Ctrl+S or right-click | -## Download ⏬ - -Download the **newest available installer** from the [latest release](https://github.com/2latemc/JustAnotherMusicClient/releases/latest) for either Windows, MacOs or Linux. - - - - Screenshot - - - -## Roadmap 📌 -- Website with easy download buttons! -- **Better lyrics consistency** (some songs still don't have them) -- Raycast support with app specific URLs -- Fix Windows media player controls sometimes not working -- **Music Video** support -- ~~Last.fm integration~~ -- ~~App Auto-Update so that you don't manually have to check for changes~~ -- ~~Maximize Button + An option for Windows-style window controls~~ -- ~~Start App on boot option~~ -- ~~Liked songs playlist support~~ -- ~~Discord RCP is coming~~ -- ~~Full Linux and MacOS support~~ -- ~~Space to pause sometimes selects elements instead of pausing / resuming~~ -- ~~Cap Too long Song names and make them smoothly move vertically~~ -- ~~Artist Pages with monthly listeners, playlists and popular songs alongside streams amount feature when clicking on the artist name, also integrate into the search results so that one can find artists~~ - -## Platform Support 💻 - -- **Windows** is the primary supported platform. -- **macOS** supported. Sometimes some experimental releases don't work at first, so try some older ones if that is the case. -- **Linux** supported. Sometimes some experimental releases don't work at first, so try some older ones if that is the case - -### Linux Notes -On some Wayland desktops, the AppImage can open a blank grey window with an EGL error. If that happens, try launching it with: +- **Windows** — primary supported platform. +- **macOS** — supported. +- **Linux** — supported. + +### Linux notes + +Install the `.deb` or `.rpm`, or `zuno` from the AUR on Arch. These use your system's +WebKitGTK and GStreamer, which is what playback runs through. + +**No sound, or "YouTube player error 5".** Playback decodes through GStreamer, and most +distros do not install the codecs YouTube needs by default: ```bash -LD_PRELOAD=/usr/lib/libwayland-client.so ./Just.Another.Music.Client_1.2.71_amd64.AppImage +# Debian, Ubuntu, Mint +sudo apt install gstreamer1.0-libav gstreamer1.0-plugins-base gstreamer1.0-plugins-good + +# Fedora (gstreamer1-libav needs RPM Fusion enabled) +sudo dnf install gstreamer1-libav gstreamer1-plugins-base gstreamer1-plugins-good + +# Arch — installed automatically with the AUR package +sudo pacman -S gst-libav gst-plugins-base gst-plugins-good ``` -If playback or window controls still fail, open the app log from Settings and include it with the issue report. The exact desktop environment, display server, and distro help a lot for Linux bugs. +Confirm they registered: -### MacOs Issues -**MacOs may prompt you with a Keychain popup asking for permission.** The app stores one encryption key in its own Keychain entry. Your YouTube Music session is encrypted with that key before it is saved in the app data directory. +```bash +gst-inspect-1.0 | grep -E 'avdec_aac|avdec_h264' +``` -If you do not need signing into YouTube Music you dont need to grant Keychain permissions. If you do it is recommended to click "always allow" in the popup to prevent MacOs from being annoying +**A blank grey window.** A WebKitGTK rendering problem under Wayland, most often on Nvidia. +Launch with one of: -## Legal -**This project DOES NOT offer any downloading functionality. This is a client providing Theme and Decorative additions for audio listening only.** +```bash +WEBKIT_DISABLE_DMABUF_RENDERER=1 zuno +WEBKIT_DISABLE_COMPOSITING_MODE=1 zuno +GDK_BACKEND=x11 zuno +``` -**If anyone from Google reads this:** There was no official client, I just wanted a good desktop client. Thats why I made this, please don't sue me! -**Contact me here:** team@2late.org +Open the app log from **Settings → Library → Application log** for anything else, and attach +it to an issue. The desktop environment, display server and distro all help a lot for Linux +bugs. The log also lives at `~/.local/share/com.zuno.desktop/logs/current.log`. -The application interacts with YouTube and YouTube Music. Access to those services remains governed by their respective terms, policies, availability, and regional restrictions. +### macOS notes -JustAnotherMusicClient does not host or claim ownership of music, videos, artwork, metadata, or other content supplied by third parties. Rights in that content remain with their respective owners. +**"Apple is not able to verify that it is free from malware."** The macOS builds are not +signed with an Apple Developer ID, so Gatekeeper blocks them on first launch. This is not a +malware finding — it means the binary is unsigned. Drag Zuno to Applications, then either: -The project is not intended to circumvent access controls, geographic restrictions, advertising, paid service requirements, or content licensing. It is also not intended to enable unauthorized downloading, copying, redistribution, or public performance of third-party content. +- Open **System Settings → Privacy & Security**, scroll to the message about Zuno, and click + **Open Anyway**, or +- clear the quarantine flag yourself: -YouTube and YouTube Music are trademarks of Google LLC. All other trademarks are the property of their respective owners. References to third-party products are used only to describe compatibility and integration. +```bash +xattr -dr com.apple.quarantine /Applications/Zuno.app +``` -- [YouTube Terms of Service](https://www.youtube.com/static?template=terms) -- [YouTube API Services Terms of Service](https://developers.google.com/youtube/terms/api-services-terms-of-service) -- [YouTube API Services Developer Policies](https://developers.google.com/youtube/terms/developer-policies) +Build from source if you would rather not trust a prebuilt binary. +macOS may also show a Keychain prompt. Zuno stores one encryption key in its own Keychain entry, +and your YouTube Music session is encrypted with that key before being written to the app +data directory. Choosing **Always Allow** avoids repeated prompts. +If you do not intend to sign in to YouTube Music, you can decline it. -## For Developers 🛠️ +
-### Prerequisites +## For developers -Install these before running the app: +### Prerequisites - Node.js LTS and npm - [Rust and Cargo](https://rustup.rs/) -- Windows C++ build tools -- Microsoft Edge WebView2 Runtime +- C++ build tools (MSVC on Windows) +- Microsoft Edge WebView2 Runtime (Windows) -The Tauri CLI is included in the project's development dependencies. A global Tauri installation is not required. +The Tauri CLI ships in the project's dev dependencies — no global install needed. +### Install, run, build -### Install - -```powershell +```bash npm install -``` - -### Run - -```powershell npm run tauri dev +npm run tauri build ``` -### Build +### Architecture -```powershell -npm run tauri build -``` +The `docs/` folder documents the codebase: + +- [`docs/architecture.md`](docs/architecture.md) — system overview and module map +- [`docs/frontend.md`](docs/frontend.md) — React structure, styling tokens, icon conventions +- [`docs/backend.md`](docs/backend.md) — Rust commands and the IPC surface ### Contributing -Contributions are welcome. Fork the repository, create a branch for your change, test it locally, and open a pull request with a clear description of what you changed and why. +Contributions are welcome. Fork the repo, branch, test locally, and open a pull request +describing what changed and why. For larger changes, open an issue first so the approach can +be discussed. + +By contributing you agree to the [Contributor License Agreement](CLA.md). + +
+ +## Credits -By submitting a contribution, you agree to the [Contributor License Agreement](CLA.md). You retain copyright in your contribution while granting the project owner the rights needed to use, modify, distribute, commercialize, and relicense it. +Zuno is a fork of **[JustAnotherMusicClient](https://github.com/2latemc/JustAnotherMusicClient)** +by [2latemc](https://github.com/2latemc), used under the Apache 2.0 licence. The original +project did the hard groundwork of getting YouTube Music working on the desktop. -For larger changes, consider opening an issue first so the approach can be discussed before implementation. +If you want to support the original author, they accept donations +[on Ko-fi](https://ko-fi.com/totally2late). -### Common Issues +
-#### Rust is not installed +## Legal + +**Zuno provides no downloading functionality.** It is a client for audio listening, with +theming and interface additions. + +Zuno interacts with YouTube and YouTube Music. Access to those services remains governed by +their own terms, policies, availability and regional restrictions. + +Zuno does not host or claim ownership of music, videos, artwork, metadata, or any other +content supplied by third parties. Rights in that content remain with their respective +owners. -Install Rust and Cargo from [rustup.rs](https://rustup.rs/), restart your terminal, and run the command again. +The project is not intended to circumvent access controls, geographic restrictions, +advertising, paid service requirements, or content licensing, nor to enable unauthorised +downloading, copying, redistribution or public performance of third-party content. -#### WebView2 is missing +YouTube and YouTube Music are trademarks of Google LLC. All other trademarks are the property +of their respective owners. References to third-party products describe compatibility and +integration only. -Install the [Microsoft Edge WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/), then run the app again. +- [YouTube Terms of Service](https://www.youtube.com/static?template=terms) +- [YouTube API Services Terms of Service](https://developers.google.com/youtube/terms/api-services-terms-of-service) +- [YouTube API Services Developer Policies](https://developers.google.com/youtube/terms/developer-policies) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..679bc02 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,31 @@ +# Security Policy + +## Supported versions + +Only the latest release gets fixes. Zuno auto-updates; older versions are not patched. + +## Reporting a vulnerability + +Use GitHub's [private vulnerability reporting](https://github.com/noFAYZ/zuno/security/advisories/new) +— it is private until a fix ships. If that is unavailable, email m.faizanasad97@gmail.com +with `zuno security` in the subject. + +Please do not open a public issue for a vulnerability. + +Include what you have: affected version, OS, steps to reproduce, and what an attacker gains. +Expect a first reply within a week. This is a one-maintainer hobby project — there is no +bounty, and a fix lands in the next release rather than on a fixed SLA. + +## Scope + +Zuno is a desktop app that plays YouTube content locally. Things worth reporting: + +- Remote content (video metadata, captions, thumbnails, URLs) escaping into command + execution, file writes outside the app's own data directory, or the webview's privileged + context. +- Anything that leaks the updater signing key, cookies, or the local cookie jar. +- A malicious or spoofed update passing signature verification. + +Out of scope: YouTube's own terms of service, rate limits or bot checks, missing hardening +that is not exploitable, and vulnerabilities in a dependency that Zuno does not actually +reach. diff --git a/app-icon.png b/app-icon.png new file mode 100644 index 0000000..c5b19e1 Binary files /dev/null and b/app-icon.png differ diff --git a/assets/img/2.png b/assets/img/2.png new file mode 100644 index 0000000..5cc00b7 Binary files /dev/null and b/assets/img/2.png differ diff --git a/assets/img/2.svg b/assets/img/2.svg new file mode 100644 index 0000000..fffef34 --- /dev/null +++ b/assets/img/2.svg @@ -0,0 +1 @@ +AABCHWp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAEH3anVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTo3YTlmYjNhNi03ODgwLTQ5MTMtOTQ0YS0yZWE5NGVhNDhkNGYAAAAB5Gp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAO5qdW1iAAAAQWp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuYWN0aW9ucy52MgAAAAAYYzJzaPF4V/K+2O1MklL5ycvc5EMAAAClY2JvcqFnYWN0aW9uc4GjZmFjdGlvbmxjMnBhLmNyZWF0ZWRtc29mdHdhcmVBZ2VudGhDYW52YSBBSXFkaWdpdGFsU291cmNlVHlwZXhTaHR0cDovL2N2LmlwdGMub3JnL25ld3Njb2Rlcy9kaWdpdGFsc291cmNldHlwZS9jb21wb3NpdGVXaXRoVHJhaW5lZEFsZ29yaXRobWljTWVkaWEAAADFanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaE87g9R8u4Z3jVG/HfBY0SoAAAB9Y2JvcqVqZXhjbHVzaW9uc4GiZXN0YXJ0GQETZmxlbmd0aBlYKGRuYW1lbmp1bWJmIG1hbmlmZXN0Y2FsZ2ZzaGEyNTZkaGFzaFgg+TufoEAIKslRhgU9wwiO50WETMswRG07z90RRCUYAp5jcGFkSQAAAAAAAAAAAAAAAgtqdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAAB3GNib3Knamluc3RhbmNlSUR4LHhtcDppaWQ6MWY0YWZmNDktM2U4OS00ZGEwLWJkMzUtNjFhNTFjNjc4YWM0dGNsYWltX2dlbmVyYXRvcl9pbmZvo2RuYW1lZ2MycGEtcnNndmVyc2lvbmUwLjAuMHdvcmcuY29udGVudGF1dGguYzJwYV9yc2UwLjAuMGlzaWduYXR1cmV4TXNlbGYjanVtYmY9L2MycGEvdXJuOmMycGE6N2E5ZmIzYTYtNzg4MC00OTEzLTk0NGEtMmVhOTRlYTQ4ZDRmL2MycGEuc2lnbmF0dXJlcmNyZWF0ZWRfYXNzZXJ0aW9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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2WQIAaarnRTgafFmqupaSlGCAozEPO3Weqwe1oTPkls+XrC3k42KEgi+pOSjCOtGBrqTsEA+d3LKb3UtorA07fQO97sKPyUVxkoKFI5ER4rDvLX9rd7+wKQ5fGGFBKctNfDekMs3uUBaL2nAXtnZNYf9wlc8cUMiJOA4wkwwbGBwDKJmVF7erbcfPqfuHFMLKfybn9Yv6wdLZn+9luUIGW1llrISo86ogZJ1t+461Mewwmqmhi77nDDe/4V/5mh2ts9aqUBaGop/jAlDS38V9SkW41CAN+v5A745BuLkSu02s/7sKtzT6rpvQTpu8CtQGZPUWdnV24HzyKO9golwfwoP0YtBGIotXTHINioPUMwr7JNUF/HgmjZ7/4xv8YiREPsn6aiVWOTnDA/EnpZmeIBS2QyP9H1PecApT2SNs4vaGj+neHrqVqAGDLgfjR8f0LqfwtA8euswwYGh3sbcPPTWf+asDOhcsLTws38mDcp5/bkKb7T/j5TIWLnwS0AozhE/biNCBYHhPDMgM7QGkfs1OpvG8ivYz2VB0vNeXjRsPUzAiNxTDsF/4CuyDSK/2+YSIRB7Oi2ZloKOrbGxjHycycp8gSvq6ptbiLefwyMamjhsvyZ/H9RB4WOSFZiNumpQxD1HS+xPOg42sXhm/8tWSk6KobFZ6Is+P8KBlXqQ+PEM=Yes \ No newline at end of file diff --git a/assets/img/6.svg b/assets/img/6.svg new file mode 100644 index 0000000..d98c331 --- /dev/null +++ b/assets/img/6.svg @@ -0,0 +1 @@ +AABCHWp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAAEH3anVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTpjM2MyOGU2OC0yNzBlLTQ3ZDUtODk1ZC03ODZiNmM0YWNhODEAAAAB5Gp1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAAO5qdW1iAAAAQWp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuYWN0aW9ucy52MgAAAAAYYzJzaLVYdjsHeLcgqvvt0Ekn6uUAAAClY2JvcqFnYWN0aW9uc4GjZmFjdGlvbmxjMnBhLmNyZWF0ZWRtc29mdHdhcmVBZ2VudGhDYW52YSBBSXFkaWdpdGFsU291cmNlVHlwZXhTaHR0cDovL2N2LmlwdGMub3JnL25ld3Njb2Rlcy9kaWdpdGFsc291cmNldHlwZS9jb21wb3NpdGVXaXRoVHJhaW5lZEFsZ29yaXRobWljTWVkaWEAAADFanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaPDZWnL3f8zWzt795YTxsQcAAAB9Y2JvcqVqZXhjbHVzaW9uc4GiZXN0YXJ0GQETZmxlbmd0aBlYKGRuYW1lbmp1bWJmIG1hbmlmZXN0Y2FsZ2ZzaGEyNTZkaGFzaFggtJSRSi9BF230a6XIkGW0pi/3lMFrYE85kHGmJH5VRZ9jcGFkSQAAAAAAAAAAAAAAAgtqdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAAB3GNib3Knamluc3RhbmNlSUR4LHhtcDppaWQ6N2ZjYjQ0ZjYtZGQ1MC00ZTAxLWJlZGYtNGYyNDg2NmQ5YjhidGNsYWltX2dlbmVyYXRvcl9pbmZvo2RuYW1lZ2MycGEtcnNndmVyc2lvbmUwLjAuMHdvcmcuY29udGVudGF1dGguYzJwYV9yc2UwLjAuMGlzaWduYXR1cmV4TXNlbGYjanVtYmY9L2MycGEvdXJuOmMycGE6YzNjMjhlNjgtMjcwZS00N2Q1LTg5NWQtNzg2YjZjNGFjYTgxL2MycGEuc2lnbmF0dXJlcmNyZWF0ZWRfYXNzZXJ0aW9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AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2WQIAq5GGDDJIhB2OfYnlhlIyFUUgRAE7LRilCQDgQ1EN60Ebur2ImtzPoTgzK5VDYciv7ArlgN1xJvDaoANI/5lwf9ihSpxIYCa4olnZeRrewwpKtVo/ys2w/fLlKPMs3JQzkjFQItErZvMIctW/nyBI28Ixd/HjhOSPotpFaHmBgUqifvqs6bUkQlq5GKInPIQ83chyw8WSA9TVtI2++aA9igh/wwm83A+cPbLNEn+tmZz0EWkl2LleimxhGPVwxn5iRTDCOKm2wOyCnUgtRKgYhDBVmqI0hk/2eExqVg34kViDZEtUv9W6Q9DesKFuBk9hZeUl/kxixAgmvE8AKV14RBtTuC8c+tA+WjJmVVYDE5/irQ4OZGqEFtoNodsyYCqhugvyP75NGnWYXZtsmXPilteFKlRcub9fcsEq7RXzx0BlXu/9NcLDlRVFrO8br/Wym97lEWnLvDUXscpIHu7HNObgyrYz3hB/DejpMfEvljMCWRcttHktiIkMezkeiuqqRfAcY0Y9SfubfMntyMFcTUIqZS9PggZgytG7hloZpvRXYUyMhMtOkZsB2dD9nNxhghHdNU8UnuJ5OKdMN+ptbsWv+GqGubV5hxA2iLXC46vtboyo5hrowFu4nFn7Ccc3mBKzDAVt+YYaLnDG9pAM5/PHUY4/K2qAmZuk4iP/EM8=Yes \ No newline at end of file diff --git a/assets/img/Logo.png b/assets/img/Logo.png index 5335d45..c5b19e1 100644 Binary files a/assets/img/Logo.png and b/assets/img/Logo.png differ diff --git a/assets/img/liked.jpg b/assets/img/liked.jpg new file mode 100644 index 0000000..79e3803 Binary files /dev/null and b/assets/img/liked.jpg differ diff --git a/assets/img/zuno-d-1.2.PNG b/assets/img/zuno-d-1.2.PNG new file mode 100644 index 0000000..ea0c792 Binary files /dev/null and b/assets/img/zuno-d-1.2.PNG differ diff --git a/assets/img/zuno-d1-1.2.PNG b/assets/img/zuno-d1-1.2.PNG new file mode 100644 index 0000000..495c192 Binary files /dev/null and b/assets/img/zuno-d1-1.2.PNG differ diff --git a/assets/img/zuno-d3-1.2.PNG b/assets/img/zuno-d3-1.2.PNG new file mode 100644 index 0000000..3a3df74 Binary files /dev/null and b/assets/img/zuno-d3-1.2.PNG differ diff --git a/assets/img/zuno-d4-1.2.PNG b/assets/img/zuno-d4-1.2.PNG new file mode 100644 index 0000000..d8eb52f Binary files /dev/null and b/assets/img/zuno-d4-1.2.PNG differ diff --git a/assets/img/zuno-l-1.2.PNG b/assets/img/zuno-l-1.2.PNG new file mode 100644 index 0000000..921cf70 Binary files /dev/null and b/assets/img/zuno-l-1.2.PNG differ diff --git a/assets/img/zuno-l2-1.2.PNG b/assets/img/zuno-l2-1.2.PNG new file mode 100644 index 0000000..39ff7bf Binary files /dev/null and b/assets/img/zuno-l2-1.2.PNG differ diff --git a/assets/img/zuno-l3-1.2.PNG b/assets/img/zuno-l3-1.2.PNG new file mode 100644 index 0000000..e4be312 Binary files /dev/null and b/assets/img/zuno-l3-1.2.PNG differ diff --git a/assets/img/zuno-l4-1.2.PNG b/assets/img/zuno-l4-1.2.PNG new file mode 100644 index 0000000..8d63808 Binary files /dev/null and b/assets/img/zuno-l4-1.2.PNG differ diff --git a/assets/img/zuno-l5-1.2.PNG b/assets/img/zuno-l5-1.2.PNG new file mode 100644 index 0000000..6bd611b Binary files /dev/null and b/assets/img/zuno-l5-1.2.PNG differ diff --git a/assets/img/zuno-set-1.2.PNG b/assets/img/zuno-set-1.2.PNG new file mode 100644 index 0000000..81054a1 Binary files /dev/null and b/assets/img/zuno-set-1.2.PNG differ diff --git a/assets/img/zuno.mp4 b/assets/img/zuno.mp4 new file mode 100644 index 0000000..db06114 Binary files /dev/null and b/assets/img/zuno.mp4 differ diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..d646b5c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,487 @@ +# Architecture — Zuno + +An unofficial desktop YouTube Music client. Tauri 2 (Rust) shell + React 19 / TypeScript +front end, bundled by Vite 7. No router and no Redux — plain classes with `useSyncExternalStore` +and direct Tauri IPC. The UI is built on Tailwind v4 plus animated components vendored from the +[beUI](https://beui.dev) registry, with [Solar](https://solar-icons.vercel.app) icons. + +- Version: `1.2.1` (`package.json`, `src-tauri/Cargo.toml`, `src-tauri/tauri.conf.json` are kept in lockstep) +- Bundle id: `com.zuno.desktop` · Rust crate `zuno` / lib `zuno_lib` +- Platforms: Windows, macOS, Linux +- Companion docs: [frontend.md](./frontend.md) (UI), [backend.md](./backend.md) (Rust/IPC) + +--- + +## 1. Big picture + +```mermaid +flowchart TB + subgraph Main["Main window (index.html → src/main.tsx)"] + UI["React UI
src/ui/**"] + STATE["Controllers + stores
src/player/**"] + DS["DataSource layer
src/datasource/**"] + IFRAME["Two hidden IFrame decks
or an <audio> element"] + end + + subgraph Mini["mini-player window (mini.html → src/mini.tsx)"] + MP["MiniPlayer component"] + end + + subgraph Rust["Tauri backend (src-tauri/src/**)"] + CMD["Tauri commands"] + CACHE["File cache (LRU)"] + OFF["Offline audio store"] + SET["settings-v1.json"] + KEY["OS keyring / encrypted session"] + HTTP["reqwest proxy + cookie jar"] + MEDIA["SMTC / MPNowPlayingInfoCenter"] + TRAY["Tray icon"] + RPC["Discord IPC"] + LFM["Last.fm API"] + SRV["127.0.0.1 media server"] + WATCH["notify folder watcher"] + end + + UI --> STATE --> DS + STATE --> IFRAME + DS -->|invoke| CMD + UI -->|invoke| CMD + MP <-->|Tauri events| UI + CMD --> CACHE & OFF & SET & KEY & HTTP & MEDIA & TRAY & RPC & LFM & SRV & WATCH + HTTP -->|HTTPS| YT["music.youtube.com / googlevideo.com"] + IFRAME -->|audio| YT +``` + +Four rules explain most of the design: + +1. **All network traffic to Google goes through Rust** (`proxy_http_request`) so the WebView's + CORS/cookie rules never apply, cookie auth can be signed properly, and rotated `Set-Cookie` + values can be merged back into the stored session. +2. **There are two playback engines and the user picks one** (`ui/settings/audioEngine.ts`). + `iframe` (the default) hands the video id to a hidden YouTube IFrame player, dodging the 403s + that signed googlevideo URLs return when replayed from a different context — at the cost of a + `youtube.com` subframe process, roughly 90 MB. `native` resolves and downloads the track through + Rust and plays it from an `