Two defects in the video control bar, reported together because they present together: the menus cover each other, and in portrait they cover the edge of the screen instead of the video. DR-256 (a) — audio track, quality and subtitles each owned a `show…` boolean and no toggle cleared the others. Opening a second menu stacked it over the first in the same corner: the newer panel hid rows of the older, both stayed live, and both kept taking clicks. A single `openMenu` value replaces the three booleans, which makes "at most one menu is open" a property of the state rather than something every handler has to remember to enforce. The desktop volume popup was a fourth uncoordinated menu in the same row, so `VolumeControl` grew optional controlled-open props and joined the group; without them it still manages itself, which is how MiniPlayer and AudioPlayer keep it. DR-256 (b) — every panel was `absolute right-0` against *its own icon button*. Those icons sit mid-row, so a 200-220 px panel extended left from a point well inside the bar and hung off the left edge of a phone in portrait: half the tracks could not be read, let alone tapped. One shared panel now anchors to the control ROW's right edge, clamped to `min(20rem, 100vw - 2rem)` wide and `min(300px, 45vh)` tall. A full-screen dismiss layer inside the controls subtree closes it on a tap elsewhere — inside, so the tap never reaches the container's gesture layer and cannot toggle playback (DR-098). The volume popup had the same placement bug from the other side: `left-full` opened it rightward from an icon near the right end of every bar it appears in. It opens upward, right-aligned, now. And the icon row wraps rather than overflowing — in portrait the transport controls plus nine icons are wider than the screen, which pushed fullscreen and close past the edge. The test renders the real component and drives the toggles, because neither fault is visible from a helper: both are properties of the composition. It was written first and failed on both counts — `["Audio Track", "Subtitles"]` open at once, and no shared panel to anchor.

JellyTau
A cross-platform Jellyfin client built with Tauri, SvelteKit, and TypeScript.
Business logic lives in a Rust backend; a UI-rich Svelte frontend handles presentation and talks to it over Tauri's IPC. Targets Linux (libmpv) and Android (ExoPlayer).
Getting Started
This project uses bun as its package manager.
# Activate the Rust environment (fish shell)
source "$HOME/.cargo/env.fish"
# Install dependencies
bun install
# Run in development
bun run tauri dev
# Type-check the frontend
bun run check
# Build for Linux
bun run tauri build
# Build for Android
bun run tauri android build
For the full set of build, test, and Android helper scripts, see scripts/README.md.
Documentation
| Topic | Location |
|---|---|
| Architecture overview & subsystem docs | docs/architecture/ |
| Requirements, traceability & technical debt | docs/requirements.md |
| Build & release process | docs/build/build-release.md |
| Docker builds | docs/build/docker.md |
| Traceability tooling & CI | docs/traceability.md, docs/traceability-ci.md |
| Release checklist | docs/release-checklist.md |
| UX flows | docs/ux-flows.md |
| CI operations (builder image, secrets, runner) | docs/build/ci-operations.md |
Contributing
CONTRIBUTING.md covers the setup, the gates a change has to pass, and the two rules that catch people out (bug fixes start with a failing test; Jellyfin's taxonomy stays in Rust). Please also read the Code of Conduct.
Found a security problem? Do not open an issue — see SECURITY.md.
Verifying a download
Every release publishes SHA256SUMS covering all of its artifacts, plus an SBOM
of what went into the build:
sha256sum -c SHA256SUMS
Desktop builds update themselves from Settings → Updates, verifying each payload against JellyTau's signing key before installing. Android installs are handled by the system installer, so the app links to the releases page instead.
Recommended IDE Setup
VS Code + Svelte + Tauri + rust-analyzer.
License
MIT