8f5c9023d0fd0e14b6edff73b916c016560ff295
40
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d32ca13d00 |
chore: give the project its own identity instead of the scaffold's
Cargo.toml still carried `description = "A Tauri App"` and `authors = ["you"]`, package.json's description was empty with no author or repository, and there was no LICENSE file at all despite package.json declaring MIT. The user-visible half matters more. productName was the scaffold's lowercase "jellytau", which is what the Android *release* build shows under its icon and what the deb/rpm/NSIS bundles carry as their display name. It went unnoticed because build.gradle.kts overrides the label to "JellyTau Debug" for the debug build type — the install a developer sees every day was the only correctly-cased one. mainBinaryName pins the executable filename to "jellytau" so build-windows-cross.sh and the Arch PKGBUILD, which both resolve it by name, need no change. strings.xml moves into the canonical android tree rather than being edited in gen/, since sync-android-sources.sh already copies res/values/*.xml — so the fix survives the next regeneration. Bundle metadata (publisher, copyright, category, descriptions, licence) was absent entirely, so the packages shipped with no maintainer or description. The hand-written PKGBUILD and .desktop had all of it; only the generated packaging was wrong. Adds .env.example: three scripts require signing vars from a gitignored .env and .gitignore already whitelists the example, but none existed. TRACES: | DR-214 |
||
|
|
c18d79c656 |
fix(android): stop background audio rewinding to where it started
A video handed off to background audio (UR-040) streams a live mp3 transcode
over plain HTTP. That response is chunked, so there is no Content-Length, and a
live encode carries no Xing header, so the extractor establishes no duration —
on device every position tick reads "<position> / 0.0".
ProgressiveMediaPeriod.configureRetry resumes a failed load in place only when
the content length is known or the seek map has a duration. With neither it
assumes the source is live, sets pendingDeferredRetry, and when the sample
queues next run dry resets them and re-requests the URL from offset 0. Our URL
carries StartTimeTicks = the handoff point, so "offset 0" is where audio-only
mode began: a transient load error armed a retry that fired minutes later, when
the buffer finally drained, and playback resumed at the handoff point and ran
on from there. A successful retry raises no error and ends nothing, so neither
arm of DR-129 was consulted and no discontinuity handler existed — the only
trace was a position that went backwards, which is why it read as random, and
why the two earlier fixes for the same symptom (DR-129's phantom end, DR-159's
relative-timeline leak) left it standing.
A retry that can only restart the stream is worth less than no retry at all.
player_retry_restarts_stream marks a Remote audio-only video item,
loadWithMetadata carries the answer to Kotlin, and the pure StreamRetryDecision
holds it for a DefaultLoadErrorHandlingPolicy that returns C.TIME_UNSET —
making onLoadError answer DONT_RETRY_FATAL before it reaches configureRetry.
The rewind becomes a recoverable error, which recoverable_error_resume already
answers by re-opening at the position playback reached, StartTimeTicks
rewritten so the selected audio track survives. Every other source keeps the
player's retry: a static file and an HLS playlist declare their timeline and
are resumed where the load stopped. onPositionDiscontinuity is added for its
log line alone, loud for DISCONTINUITY_REASON_INTERNAL, which is the rewind's
own signature.
Verified on device (FP5), same procedure both runs — handoff, 60s to fill the
buffer, a 45s radio outage:
before 13:54:52 BUFFERING, then "Media ready! Duration: -9.22e15"
(C.TIME_UNSET) and position 1165.4s -> 840.349s, exactly the handoff
base, 3.5 minutes after the outage with nothing logged between
after 14:05:08 "declining the player's retry", playback undisturbed off the
buffer for 69s (a fatal load error is only raised when the renderer
next needs data), then ERROR_CODE_IO_NETWORK_CONNECTION_FAILED ->
re-opening at 785.6s -> READY, and no rewind in the following 7 min
Kotlin tests run with ./gradlew :app:testUniversalDebugUnitTest.
TRACES: UR-040, UR-004 | DR-203 | UT-200
|
||
|
|
caebf2d139 |
fix(android): keep the display awake while video plays
Android counts its display timeout from the last user input, and watching
something is exactly the case where there is none — so the screen dimmed and
slept mid-playback unless the user kept tapping it.
Nothing held it. FLAG_KEEP_SCREEN_ON appeared nowhere in the app, and neither
renderer supplies a hold for free: ExoPlayer's setWakeMode is a CPU/wifi wake
lock that says nothing about the display, and it draws into the TextureView we
own (DR-192) rather than media3's PlayerView, which is the widget that would
otherwise set keepScreenOn itself; the webview <video> path is no better,
because the display wake lock Chrome takes for video lives in the browser layer
and not in an embedded WebView.
ScreenWakeManager toggles FLAG_KEEP_SCREEN_ON on the Activity window — window
scoped, so it stops applying the moment the app is not visible and cannot
outlive a crash the way an acquired PowerManager.WakeLock can, and it needs no
permission. The two rendering paths are independent holders OR-ed in the pure
ScreenWakeState: the native path follows onIsPlayingChanged plus surface
teardown, so the hold tracks what ExoPlayer reports rather than what the UI
intends, and the webview path reuses the setHtml5VideoState report the frontend
already sends for PiP. Audio is deliberately not a holder — screen-off music is
the point of that path.
Also the repo's first Kotlin JVM unit tests: ScreenWakeState is framework-free,
so the decision is testable off-device with
./gradlew :app:testUniversalDebugUnitTest
(note the variant — plain testDebugUnitTest is ambiguous here). sync-android
-sources.sh mirrors src/test into the gen tree alongside the main sources.
TRACES: UR-003, UR-004 | DR-202 | UT-199
|
||
|
|
6dfc6b259a |
fix(player): lockscreen skip scrubs instead of advancing in background audio
onSkipToNext/onSkipToPrevious forwarded a bare next/previous to Rust, which always advanced the queue. Correct for music, wrong for a video whose audio is running through a background-audio handoff (UR-040): pressing skip to re-hear a line jumped to the next episode instead of scrubbing. resolve_skip_action in player/seek.rs maps the command to Advance or SeekTo, and is_background_audio_active() is the whole test — the handoff exists only for video, and an episode played through it reports MediaType::Audio, so media type cannot distinguish the case. Forward 30s, back 10s, both clamped to [0, duration] so a skip near either end cannot seek negative or read as EOF and advance. Routed through the same spawn-then-seek_absolute path as the scrubber, because a handoff seek re-opens the stream and must not run under the blocking lock (DR-159). Kotlin keeps sending the opaque command; it only gains FAST_FORWARD/ REWIND in the PlaybackStateCompat so the system stops drawing skip arrows for a control that scrubs. The remote-volume action block is deliberately untouched: the handoff never applies to cast sessions, where skip really does mean advance. Tests written first and watched fail (left: Advance, right: SeekTo). 706 Rust tests pass, clippy 0, coverage 90%. |
||
|
|
88e15e3e12 |
merge: Android runtime security (B1, B3)
Correct the POST_NOTIFICATIONS mechanism: the lockscreen notification is exempt because of the MediaSession token, not because it belongs to a foreground service — FGS notifications are explicitly NOT exempt. So no permission prompt and no checkSelfPermission gate; instead both notification builders bind the token once and log loudly if it is ever null, turning a silent failure into a logcat line. Stop the webview undoing the network security config: mixedContentMode COMPATIBILITY, allowFileAccess/allowContentAccess false. Conflict resolution: this branch's DR-198 collided with the Tauri branch's, so it was renumbered DR-200 (3 TRACES in JellyTauPlaybackService.kt and the UR-006 matrix row updated). DR-199 was uncontested. Pinned counts summed to DR 191 / total 334; UR-071 takes both DR-198 and DR-199. |
||
|
|
a93cee9241 |
merge: stop backing up credentials no key can ever open (B2, B4, B5)
allowBackup=false plus data_extraction_rules covering device-transfer, not just cloud-backup; treat an undecryptable credential blob as a logout rather than a hard error; drop the half-declared leanback/TV entries; jvmTarget 1.8 -> 17. |
||
|
|
2d21f092d5 |
fix(android): stop the webview undoing the network security config
MainActivity set mixedContentMode = MIXED_CONTENT_ALWAYS_ALLOW together with allowFileAccess/allowContentAccess = true, which is a blanket cleartext opt-in reached by hand — the exact thing network_security_config.xml exists to prevent and its own comment warns against. Nothing needed any of the three: - file:// is never loaded. Cached thumbnails go through convertFileSrc, which on Android resolves to http://asset.localhost/... and is answered by wry's request interceptor rather than the filesystem; downloaded media goes over the loopback HTTP server (DR-137), which exists precisely because the asset/file route cannot stream a large file. - content:// is never loaded. The manifest's FileProvider is for outbound share intents, not webview navigation. - Mixed content never arises. Tauri serves the UI from http://tauri.localhost (use_https_scheme defaults false and is not set), and both 127.0.0.1 and asset.localhost are loopback/.localhost origins Chromium treats as potentially trustworthy. A plain-HTTP remote server would be mixed content, but the network security config already rejects it first — so ALWAYS_ALLOW bought nothing. COMPATIBILITY_MODE rather than NEVER_ALLOW is a deliberate hedge: the platform default at targetSdk 21+ is NEVER_ALLOW, so this is still one step looser, and it keeps passive content working if the analysis missed a path. The two files now cross-reference each other so the pair cannot drift apart again. Also records why POST_NOTIFICATIONS is declared but never requested. An audit read the missing runtime request as a threat to the lockscreen controls; it is not. A foreground-service notification is explicitly NOT exempt, but a media-session one is, and the platform predicate (Notification.isMediaNotification) requires MediaStyle AND a non-null session token. Confirmed on device: appops POST_NOTIFICATION: ignore with the transport notification live. So no permission prompt is added and startForeground stays ungated — a guard there would trade a cosmetic problem for the "did not then call Service.startForeground()" kill. What is added is the guard matching the real precondition: both builders bind the token once and log an error if it is ever null, since SystemUI's media carousel is gated on the same predicate and a token-less notification loses the lockscreen controls entirely, silently. TRACES: UR-006, UR-071 | DR-198, DR-199 |
||
|
|
ebf9a99b80 |
docs(traces): tag the twelve "Done but untraced" requirements, and stop the matrix over-reporting
Twelve requirements were marked Done in docs/requirements.md with zero TRACES anywhere in the tree. The features work — the tags were simply never written — so the matrix over-reported on exactly the requirements a reviewer would most want to verify. Each is now tagged at the code that actually implements it: - JA-006 / JA-009 / JA-013 / JA-014 / JA-015 / JA-018 and IR-022 / IR-024 at their Jellyfin call sites in repository/online.rs (search, get_item's MediaStreams/People fields, Items/Resume, Shows/NextUp, FavoriteItems DELETE, get_person/get_items_by_person), plus the commands that expose them. - UR-006 / IR-006 across the lockscreen spine: JellyTauPlaybackService (the MediaSessionCompat owner), the nativeOnMediaCommand JNI intake, and LockscreenMetadata / update_lockscreen_metadata. - IR-008 at both audio-focus mechanisms — ExoPlayer-managed for audio, the manual AudioFocusRequest listener for video — and at the media-type string that chooses between them. - UR-037 (with DR-042, also untraced) on the video-library poster grid: LibraryGrid, MediaCard, and the tv/movies routes. Resolve contradictory statuses across layers, evidence first: - IR-018/IR-019 were Planned under Done URs because they were scoped to libmpv. MpvBackend is the audio-only backend and overrides neither set_subtitle_track nor set_audio_track — the trait's not_implemented() default still stands — so UR-020/UR-021 are met by ExoPlayer and by the HTML5 <video> path instead. Both IRs are re-scoped to those backends and marked Done; IT-008/IT-009 and the stale @req-planned markers in backend.rs follow. - IR-005 (MPRIS) stays Planned: there is no MPRIS/D-Bus code or dependency in the project and update_lockscreen_metadata is a no-op off Android. UR-006 is corrected to Done (Android) rather than the IR being marked Done. - A note under the IR table records where a UR is met by a different mechanism than its IR anticipated. Define the two dangling IDs the source already referenced: DR-189 (the control bar never auto-hid on a touchscreen, because its timer was armed only from onmousemove) and UT-188 (its rule test). The live-denominator assertion in extract-traces.test.ts moves 187/330 to 188/331 accordingly. Traced requirements 444 to 459; IR coverage 19/32 to 25/32. |
||
|
|
4c9361d020 |
fix(android): stop backing up credentials no key can ever open
The app's data dir was eligible for Google cloud backup: the manifest set neither allowBackup nor any extraction rules, so the SQLite catalogue (library metadata, watch history) and the jellytau_secure_prefs credential blob were shipped to the user's Google account. Restoring that is worse than not having it — SecureStorage encrypts under an Android Keystore key, and Keystore keys are never backed up, so a restored install gets ciphertext with nothing to open it and fails auth silently while looking signed in. Backup and device-to-device transfer are both turned off. allowBackup ="false" covers API 24-30 outright and kills cloud backup on 31+; it does NOT stop D2D there, so @xml/data_extraction_rules excludes every domain from both channels. Nothing is lost: the catalogue is a rebuildable mirror of the Jellyfin server, and watch state lives on the server. The credential-load path degrades instead of erroring, because a device can still arrive at undecryptable ciphertext (an older install's backup, a Keystore key invalidated by a lockscreen change). Both backends now distinguish "nothing stored" from "stored but unreadable" and answer the second as the first: CredentialStore::load_credentials_file logs and returns an empty map rather than CredentialError::Encryption — which storage_get_access_token was turning into a hard Err and storage_get_active_session into a warning — and SecureStorage.getCredential discards the dead blob so it cannot fail every subsequent read. The result is a login screen rather than a broken session, and the next successful sign-in rewrites the store. Also removes the half-declared Android TV support: the manifest offered LEANBACK_LAUNCHER and the leanback uses-feature with no D-pad focus model, no TV layouts, and neither of the two declarations Play's TV validation also requires (touchscreen required="false", android:banner). That fails review while advertising the app to TV launchers. All four go back together when a focus pass is actually done. And raises jvmTarget from 1.8 to 17 under compileSdk 36, with matching compileOptions — AGP 8.11 already requires a JDK 17 toolchain, so 1.8 was only capping emitted bytecode. Nothing else in the build assumed 1.8. TRACES: UR-012 | IR-014 |
||
|
|
1285908733 |
fix(android): paint the letterbox bars, so stale pixels stop surviving in them
Native video left debris in the padding around the video: the "previous frame" flash on rotation, a ghost copy of the control bar stranded in the top bar, each new clock digit drawn over the one before it (35:42 with the 1 still showing through the 2), and the sleep/quality menus leaving their imprint after closing. One cause under all of it — nothing painted those bars. The window surface is opaque; the theme is not translucent and dumpsys window shows no translucency flag. For an opaque surface HWUI deliberately does NOT clear the damaged region before replaying a frame: it assumes the view hierarchy covers every pixel it owns. Here that hierarchy is window background → video TextureView → transparent WebView, and fitSurfaceToScreen sizes the TextureView to the letterboxed video rect. So the bars were the window background's alone to paint, and setTransparent(true) cleared it to TRANSPARENT — leaving them painted by nobody, with whatever was last in the framebuffer surviving there. The window background now stays opaque black while compositing. It cannot hide the video: the TextureView is drawn on top of it, and the WebView's own background is what lets the picture through. Three previous attempts missed because they aimed at the window's rotation animation and at TextureView frame-retention — two postOnAnimation hops, an onSurfaceTextureUpdated reveal, then ROTATION_ANIMATION_JUMPCUT with FLAG_FULLSCREEN to make it stick. The pixels were never the animation's, which is also why the artefact reproduces standing still, with no rotation involved. Those are removed. The alpha-hiding among them actively made things worse: it blanked the one view that reliably paints its own rect. FLAG_FULLSCREEN goes too — it fought edge-to-edge insets for no gain. Verified on device (HONOR ROD2-W09, Android 16): reproduced with native video on — ghost control bar in the top bar, doubled clock digit — then absent after the fix across playback, the control bar and a rotation round-trip. DR-194 is rewritten to record the real mechanism and marked Done. |
||
|
|
dccb5f53dd |
fix(android): stop the rotation cross-fade replaying the old video frame
Rotating with native video on shows the previous frame flashing in what become
the letterbox bars. It reads as a TextureView artefact — the view retains its
last frame, so between the rotation and fitSurfaceToScreen() landing that frame
sits at the old size — and two fixes were built on that reading:
1. reveal after two postOnAnimation hops. An animation frame is not a video
frame; at 24fps the next decoded frame can be several vsyncs away.
2. reveal on onSurfaceTextureUpdated, i.e. when a real frame lands. This meant
owning the SurfaceTextureListener and handing ExoPlayer the Surface directly
instead of via setVideoTextureView, which installs its own and leaves us
blind to frame arrival.
Neither stopped the flash. The mechanism is the WINDOW's rotation animation:
Android cross-fades a screenshot of the old orientation, that screenshot holds
the old video frame at the old size, and nothing at the TextureView level can
reach it. The app cannot pre-empt the screenshot either — onConfigurationChanged
fires after it is taken.
So the animation itself has to go: ROTATION_ANIMATION_JUMPCUT. That was accepted
and silently ignored, and the platform said why out loud —
"VRI[MainActivity]: setLayoutParams: not fullscreen" — because the attribute is
honoured only for a fullscreen window. FLAG_FULLSCREEN is therefore set with it,
scoped to while native compositing is active so the rest of the app keeps its
normal animation. After the change that complaint is gone from logcat.
The frame-arrival reveal is kept: it replaces a fixed-timeout guess with a real
signal, and its timeout is required rather than defensive — a resize while paused
means no new frame is ever coming, and revealing a stale frame beats a
permanently black player.
NOT CONFIRMED FIXED on device. The forced-rotation harness
(settings put system user_rotation) proved unreliable here, and screenrecord
fixes its canvas at start, so a rotation inside a recording never changes frame
dimensions — which defeated two separate attempts to measure this. DR-194 is
recorded as "Needs device verification" rather than Done.
|
||
|
|
c142568230 |
fix(player): make transport reach the player that is actually rendering
Play/pause did nothing on the Android native video path — from the on-screen tap, from the control bar, and from a direct player_toggle invocation — while seek and skip kept working. That asymmetry was the whole clue: seek decides in player_seek_video, transport decides in toggle_playback. DR-195 is the cause. `html5_playing` is Rust's record of "a webview <video> is active and in this state", and toggle_playback/play/pause all route transport to that element whenever it is set. The player route mirrored element state into it UNCONDITIONALLY — from handleReportStart and, fatally, from handleReportProgress, which VideoPlayer calls on a 10-second interval. So on the native path the frontend re-declared every ten seconds that an element was playing when none existed, and every transport intent was emitted into the void. It also explains the flashing: the control bar and the JRay overlay both key off isPlaying, which was being contradicted on every tick. The mirror now lives in mirrorElementStateToRust() in VideoPlayer, gated on useHtml5Element — the only place that knows whether an element renders at all. The route cannot tell the paths apart, which is exactly how it came to lie. DR-193 hands transport authority back to the native backend when an item loads into it. Necessary but insufficient alone: the progress interval put the flag straight back, which is why the first device test after it still failed. DR-192 presents native video through a TextureView instead of a SurfaceView. A SurfaceView renders on its own layer outside the app window and punches a transparent region through it, and everything drawn above that hole — here, the entire Svelte UI — depends on that composition path. The overlay dropped its incremental damage: the DOM advanced (slider 476 -> 479 across three seconds) behind a screen showing neither, so the progress bar froze, controls would not fade and rotation lost the transport UI, while structural DOM changes got through, which is why the play overlay always appeared to work. It supersedes DR-191, which forced redraws in a loop and treated the symptom. DR-194 hides the video view across a resize and reveals it two frames later. A TextureView retains its last frame, so between a rotation and the re-fit landing that frame is stretched across the old rect and the previous frame flashes in what should be the letterbox bars. Verified on device (Honor ROD2-W09, Android 16) by driving ADB and reading the live DOM over the devtools socket: surface tap pauses (position frozen across 12 seconds, overlay raised, transport flipped) and resumes; the control bar does both. UT-189 drives the real 10-second interval under fake timers — an earlier version asserted on a freshly mounted player, passed with the guard deleted, and guarded nothing. Still open, and deliberately not claimed: DR-192's effect on the overlay repaint is unverified on device, DR-194's letterbox reset is untested, and the native default (DR-188) stays off pending DR-190, the background-audio return. |
||
|
|
95129d04a3 |
fix(player): make Android native video actually visible, and usable
DR-172 reverted native video to opt-in after it shipped as audio with no picture, naming the compositing as the suspect. The compositing was fine. Five separate defects sat between ExoPlayer and the screen, each able to produce that exact symptom on its own, and each invisible to the others. DR-185 — the app shell painted over the surface. app.css clears the page's opaque layers through three selectors, one of which targets `[data-app-shell]`, an attribute NO component has ever set, in any commit. The shell paints --color-background across the whole viewport and VideoPlayer stacks above it, so the WebView composited opaque no matter what else was cleared. Invisible three ways over: the CSS is valid, the selector is plausible, and a rule matching nothing looks exactly like a rule matching something already transparent. DR-182 — nothing could lift the poster card. Every markMediaReady() call site is an HTML5 <video> event, and the native branch renders no element, so the black title card covered the surface for the entire session. The first fix hooked `player://position-update` / `player://state-changed`; those channels are never emitted by the backend, so it passed a test that fired them by hand and did nothing on a device. Driven from the player store now, as the seek bar already was. DR-183 — the JS bridges raced the page load. Installed 500ms after onCreate by walking the view tree, while WebView binds injected objects at page-load time, and the identity guard then declined to re-inject forever. setTransparent(true) could never arrive. Installed from WryActivity.onWebViewCreate instead, which wry calls immediately before the first loadUrl. DR-184 — the SurfaceView was never detached. detachVideoSurface had no callers anywhere, mirroring the DR-151 defect: every native video left its surface parented to the content view and the next one stacked another beneath it. DR-191 — the overlay stopped repainting. Incremental damage (the clock's text, the control bar's opacity) never reached the screen while structural changes did, so the progress bar froze, the controls would not fade, and the play overlay appeared to work because it is added and removed from the DOM. Driven from the Activity via postInvalidateOnAnimation while compositing is on. Two UI defects only this path could reveal came with them: isPlaying froze at its initial value, leaving the play overlay dimming and covering the video (DR-186), and the control bar's auto-hide was armed solely by mousemove, which a touchscreen never fires (DR-189). Immersive mode now applies on entering the player rather than only via the fullscreen button (DR-187). Verified on a device (Honor ROD2-W09, Android 16): logcat carries `WebView transparent = true` and `Marking media ready` with video on screen — the pair DR-172 went looking for and could not find — and skip, seek, rotation and subtitle rendering were exercised by hand. The default stays OFF (DR-188). Turning it on surfaced a further unverified sub-path: returning from background audio is HTML5-only, so playback stays dead (DR-190, proposed). Shipping it would have repeated DR-161 exactly — a verified sub-path made default over an unverified one. |
||
|
|
521acc75fd |
build(android): add a side-by-side release build for validating R8
R8 has broken release APKs here before by stripping the JNI-loaded player and security classes, and the only way to reproduce that was to build with the real signing key and clobber the install you actually use. `./scripts/build-and-deploy.sh release --device --debug` now builds a fully minified release APK — exactly what ships — into the .debug applicationId slot, signed with the local debug keystore: release com.dtourolle.jellytau 0.5.5 release --debug com.dtourolle.jellytau.debug 0.5.5-debug-release debug com.dtourolle.jellytau.debug 0.5.5-debug It shares the applicationId *and* the signature with the plain debug build, so the two replace each other cleanly rather than colliding, and the versionName suffix says which is currently installed. No real key is needed, so the side-by-side path deliberately skips write-keystore-properties.sh. The flag reaches Gradle as JT_SIDE_BY_SIDE=1. CI never sets it, and the release manifest merges byte-identical without it — verified both ways through processUniversalReleaseMainManifest. deploy-android.sh and build-and-deploy.sh learned the flag too, since the APK path is unchanged but the package to launch is not. |
||
|
|
2cc39cd7fd |
build(android): install the debug build alongside release as its own app
Testing a debug build meant uninstalling the real one first: same
applicationId signed with a different key is INSTALL_FAILED_UPDATE_
INCOMPATIBLE, so every experiment cost the app's settings, credentials
and offline cache.
The debug build type now carries applicationIdSuffix ".debug" and
versionNameSuffix "-debug", so it installs as com.dtourolle.jellytau.debug
("JellyTau Debug", 0.5.5-debug) with its own data directory — two
independent apps on one device.
Only the *application* id is suffixed. Kotlin classes stay in the
`namespace` package com.dtourolle.jellytau, so the JNI loadClass lookups
in player/android/mod.rs, the manifest <service> entry and the R8 keep
rules are untouched, and the FileProvider authority was already
${applicationId}-relative. Launcher names come from the appLabel /
activityLabel manifestPlaceholders rather than resValue, which would
collide with Tauri's generated strings.xml; release resolves them back to
@string/app_name and merges byte-identical.
deploy-android.sh reports the target package and explains an
UPDATE_INCOMPATIBLE failure instead of leaving it raw; logcat.sh takes a
debug|release argument (it was filtering on com.jellytau.app, a package
that has never existed) and attaches by pid when the app is running.
Verified: aapt2 badging on the built APK reports
com.dtourolle.jellytau.debug / 0.5.5-debug / "JellyTau Debug", and the
release manifest merge is unchanged.
|
||
|
|
9f5f57cba4 |
fix(ui,player): scroll restore, immersive fullscreen, watched toggle, handoff timeline, PiP
Batch of reported bugs and enhancements. UI - Pages no longer inherit the previous page's scroll position (DR-156, UR-072). The shell keeps its scrollers alive across navigation by design, so the element never remounts and its scrollTop survived the route change; SvelteKit restores window scroll, which this app never uses. ScrollMemory records the offset per route and per container: forward moves reset to the top, Back restores where the route was left. - Season header stacks on narrow screens, and the title span gets min-w-0 so it actually truncates instead of overflowing under the action buttons. - Favourites gets a labelled tile at the head of the library grid rather than only an unlabelled heart icon in the header. Playback - Full-screen video on Android hides the system bars (DR-157, UR-066). requestFullscreen() cannot touch the Activity window from inside a WebView, so the control did nothing visible while the bars stayed painted over the video. ImmersiveModeBridge hides them, restored on exit, Escape and teardown. - Background-audio handoff stops leaking its relative timeline (DR-159). background_audio_base was a display-only correction applied in two places while progress reports to Jellyfin, the frontend and media3's own seeks all worked in the relative timeline treating it as absolute — each crossing losing exactly `base` seconds. The conversion now happens once, in the position tick, and inbound seeks resolve through seek_absolute, which re-opens the stream at the requested position because the handoff transcode cannot seek. - Picture-in-picture works on the path that actually plays video (DR-160). canEnterPip demanded a native ExoPlayer surface, but that path is behind a flag defaulting to off, so PiP could never engage. It now accepts the WebView <video> too, keeping the WebView visible and routing play/pause to the element. - Native video is now the default so PiP has a real surface (DR-161). The scrub-regression tests pinned the flag-off path implicitly; they now mock it off explicitly. The native scrub/seek path is not covered by the suite and needs device verification. Watched state - Watched toggle on the episode row, season header, series and movie hero, and the Episode Focus View (DR-158, UR-073). Both backend halves already existed with no caller. storage_set_watched covers a container's episodes so the toggle is honest offline, and QueuedOp::MarkUnplayed gives the sync queue the missing direction. Release - Fix the Android versionCode floor (set-version.sh). v0.5.2 shipped code 5002 under an earlier minor*1000 scheme, but the current minor*100 formula yields 1502 for that version and 1503 for 0.5.3 — so every 0.5.x release built from it was an un-installable downgrade for anyone already on v0.5.2. Widened to 10000 + major*1000000 + minor*1000 + patch (0.5.3 -> 15003). - Bump to 0.5.3. |
||
|
|
e144e62b31 |
feat(player): render Android video natively behind a transparent webview (DR-150, DR-151, DR-152)
Rust already reported `use_html5_element: false` on Android, but two frontend overrides threw that answer away, so ExoPlayer's video path had never actually run. Both are lifted behind an `experimentalNativeVideo` opt-in (default off). The flag is a suppressor, never a promoter: off forces HTML5 even where Rust says native, so an in-progress spike cannot ship as the default, but it can never select native where Rust reported HTML5 — Linux cannot composite behind WebKitGTK, and promoting there would be a black screen. Two blockers the spec did not anticipate, both in code assumed to be merely unreachable rather than broken: - `JellyTauPlayer.setActivity()` had zero callers, so `currentActivity` was always null and `autoAttachSurface()` bailed. The SurfaceView was created and wired to ExoPlayer but never added to the view hierarchy — video would have decoded to a surface that was never on screen, whatever the webview did. This also revives PiP on the video path, which gated on the same flag. - `createAdapter()` was not the real gate; it is never called in production. The actual override was in VideoPlayer.svelte, which forced HTML5 and stopped the native backend `player_play_item` had just started. Both sites now route through `createAdapter()`. Compositing needs two independent opaque layers cleared, not one. Clearing only the page leaves the WebView widget opaque — audio over a black picture, exactly the symptom the old INTERIM comment described. `videoSurface.ts` toggles both: the widget background and window drawable from Kotlin, the page backgrounds via a `data-native-video` attribute keyed by app.css. Transparency lives in `tauri.android.conf.json` so Linux keeps an opaque window, and is scoped to the playback session so the launcher never shows through the rest of the app. Phase 3's rect plumbing turned out to be unnecessary: video is fullscreen on the player route, and `fitSurfaceToScreen()` already letterboxes and re-fits on rotation. The mini-player transition remains unverified on device. Also removes the `navigator.userAgent` sniffing in webviewAudio.ts, which was a second copy of the Rust cfg gate free to drift from it. `player_get_capabilities` now reports `usesWebviewAudio` and `supportsNativeVideo` from those same gates. Tests: adapter selection covers the full matrix, including the regression guard that the flag off beats Rust. Written first and confirmed failing (2 of 7) before the fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1b70926c36 |
feat(offline): play downloaded video, and drain the offline sync queue (0.4.6)
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 20m34s
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 6m6s
Traceability Validation / Check Requirement Traces (push) Successful in 18s
Build & Release / Run Tests (push) Successful in 20m26s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 10m3s
Build & Release / Build Linux (push) Successful in 37m59s
Build & Release / Build Windows (push) Successful in 23m0s
Build & Release / Build Android (push) Successful in 40m26s
Build & Release / Create Release (push) Successful in 1m20s
Bundles this session's work plus the concurrent search/offline/player changes.
Every gate passes on the combined tree: 885 frontend tests, 610 Rust tests,
clippy clean, boundary clean, trace coverage 86%.
Offline video playback — four separate defects, each of which alone stopped it:
DR-133 A completed download's file_path is already absolute (the worker
rewrites it on completion), but the player rooted it a second time and
handed the webview /data/user/0/app//data/user/0/app/videos/x.mp4.
DR-134 The asset protocol was never enabled: no protocol-asset feature and no
assetProtocol config, so convertFileSrc produced URLs nothing answered.
Also silently defeated the cached-thumbnail path, which fails soft to
the server copy and hid it whenever the server was reachable.
DR-137 Tauri's asset protocol answers a range-less request by reading the
whole file into memory, and only advertises Accept-Ranges from inside
its range branch, so the first request never learns ranges exist.
Chromium gave up with PIPELINE_ERROR_READ after ~31s. Local media is
now served by a loopback HTTP server: bounded 4 MiB chunks streamed
from the file handle, every response length-delimited, and a range-less
request answered with one chunk rather than the file. Confined by a
per-session token and to the app data directory, because loopback is
shared between apps on Android.
DR-138 Release builds set usesCleartextTraffic=false, so Android rejected the
request to that server before any I/O. A network-security-config
exempts 127.0.0.1 only; a remote server must still be HTTPS.
Downloads:
DR-135 download_item never records media_type and the reconnect resolver read
that NULL as 'audio', so a movie queued from a media card had its URL
resolved by get_audio_stream_url and completed as an audio-only
transcode. The item's own type now decides.
DR-136 Rows already downloaded that way are requeued on reconnect, since
prevention alone leaves them reading "downloaded" and still unplayable.
Known limitation: a download taken at `original` quality is a byte copy of the
source, so it can be any container. One such file is an AVI holding XVID, which
the webview cannot play in any case — the media server serves it correctly and
Chromium refuses it. That needs either a transcoded download preset or the
native ExoPlayer surface work, and is not addressed here.
Also fixes two ID collisions between concurrent work: DR-143 defined twice
(search vs offline gate) and UT-131 defined twice (Episode Focus hero vs channel
cap). The search requirement is now DR-147 and the channel-cap test UT-141, with
their code references and matrix rows updated.
|
||
|
|
7b531a40be |
fix(player): pick a decodable track in the no-audio fallback (DR-146)
When ExoPlayer selected no audio track, the recovery forced group 0 / track 0 unconditionally. But the most likely reason nothing was selected is that this very track cannot be decoded on this device, so the override reinstated the silence it was meant to fix. Scan the groups for the first isTrackSupported track and override to that. Also clear setTrackTypeDisabled(TRACK_TYPE_AUDIO), since audio may equally have been off at the type level, which an override alone does not undo. When no group holds a supported track, log it as an error — the server was expected to transcode — instead of leaving a silent video with no explanation in the log. Verified by compiling :app:compileArm64DebugKotlin. Not unit-tested: this tree has no Kotlin test source set, as noted in the previous commit. |
||
|
|
19bc265a8d |
fix(player): do not start a video before audio focus is granted (DR-145)
Video manages audio focus by hand (handleAudioFocus=false, since ExoPlayer's automatic handling is reserved for the audio path), and all three outcomes of the request were treated as success. AUDIOFOCUS_ REQUEST_DELAYED — which setAcceptsDelayedFocusGain(true) explicitly invites, and which means the system is withholding our audio until it calls back — and an outright REQUEST_FAILED were logged and then followed by playWhenReady = true. The picture rolled with no sound, which to the user is indistinguishable from a broken stream. Hold playback when focus is not granted and start it from the AUDIOFOCUS_GAIN callback. An explicit play() re-requests focus instead of resuming into a stream the system is still muting, guarded by a held-focus flag so repeated plays do not leak focus requests. LOSS clears the pending flag so an unrelated later GAIN cannot start playback the user never asked for. Verified by compiling :app:compileArm64DebugKotlin. Not unit-tested: this tree has no Kotlin test source set (the Gradle project lives in the generated, gitignored gen/ tree), so the logic cannot be exercised off device without restructuring the Android build. |
||
|
|
a53042fe80 |
fix(playback): bound the device profile by the audio route's channels (DR-141)
MediaCodecList answers "can this device decode 5.1", which is not the question that decides whether the user hears anything: a phone decodes an AC-3 5.1 track happily and still has two channels to play it out of. The DeviceProfile carried no MaxAudioChannels, so Jellyfin was free to direct-play the multichannel track to a two-channel sink — silence or dialogue folded into surround channels that go nowhere, depending on the device. Report media3 AudioCapabilities.maxChannelCount for the current route over JNI alongside the codec lists, and bound the direct-play and transcoding profiles (and the HLS URL's TranscodingMaxAudioChannels, previously hardcoded to 2) by it. No codec is ever removed, so a device with genuine surround output keeps direct-playing it. A missing or zero reading means "route not yet established", not "no audio", and falls back to stereo. |
||
|
|
c55ff45692 |
fix(android): clear the system bars and display cutout (UR-066)
The bottom nav rendered under the Android navigation bar, and full-screen
playback controls spilled into unusable screen edges. It looked device-specific
(Motorola bad, Fairphone fine) but every device was equally unpadded — only the
intrusion differed: a tall opaque 3-button bar swallows the nav, a thin
translucent gesture pill overlaps harmlessly.
None of the app's safe-area handling was ever active, for two independent
reasons:
1. app.html had no `viewport-fit=cover`, so every `env(safe-area-inset-*)`
resolved to 0px — the padding in app.css and BottomUi was a no-op.
2. Android WebView maps only the *display cutout* into `env()`; the status bar
and navigation bar are never reported. With enableEdgeToEdge() and
targetSdk 36 (enforced from 35, opt-out ignored from 36) the WebView always
spans them, so CSS could not learn about them by any route.
WindowInsetsBridge now reads `systemBars() | displayCutout()` and publishes
`--jt-inset-*` CSS custom properties, both pushed on every inset change
(rotation, nav-mode switch, PiP) and pullable via `AndroidInsets.get()` — the
pull is required because the first inset pass lands before the document exists
and a page load wipes the pushed inline style. app.css folds them with `env()`
via `max()` into `--safe-*`, the only thing components may pad from.
Exactly one element owns each edge: the shell takes top/left/right, BottomUi
takes bottom (inside its surface box, so the colour extends behind the gesture
bar), and shellReservesBottomInset hands bottom back to the shell on routes with
no bottom UI. The full-screen players inset their control layers only, leaving
video and artwork edge-to-edge.
The theme's `fitsSystemWindows=true` claimed the opposite of what actually
happened — overridden at runtime, ignored at this target SDK — and is removed.
Also converts six nested `h-screen`/`min-h-screen` boxes to `h-full`: the shell
is `h-screen` *and* inset-padded, so its content box is `100vh - safe-top` and
any nested 100vh box overflows by exactly the inset (the library column would
have clipped its own BottomUi). A test guards against reintroduction.
|
||
|
|
cb79a376b3 |
feat(android): implement audio settings (EQ, normalization, gapless)
🏗️ Build and Test JellyTau / Android Compile Check (push) Has been cancelled
Publish Documentation / Build & publish docs to gitea-pages (push) Has been cancelled
Traceability Validation / Check Requirement Traces (push) Has been cancelled
🏗️ Build and Test JellyTau / Run Tests (push) Has been cancelled
ExoPlayerBackend was the only backend not overriding the PlayerBackend trait's set_audio_settings/audio_settings defaults, so the Settings > Audio controls rendered on Android and silently did nothing — the default returns Ok(()) while applying nothing, so the failure was invisible. Rust owns what the values are (canonical 10-band ISO layout, preset curves, normalization presets); Kotlin owns when the AudioEffect objects exist, since that needs the live audio session id. - settings.rs: audio_settings_jni_payload() sanitises (crossfade clamped, band vector normalised) before serialising, so a malformed vector cannot reach the Kotlin parser. JSON rather than a wide JNI signature, matching how load() already passes subtitles — adding a field will not change the signature. - ExoPlayerBackend: set_audio_settings/audio_settings over JNI; ExoPlayerState gains the first command-side field (settings are pushed out, never reported). - JellyTauPlayer.kt: Equalizer, LoudnessEnhancer, and gapless via pauseAtEndOfMediaItems. Three details that are easy to get wrong: - Effects re-attach on onAudioSessionIdChanged. ExoPlayer rebuilds its audio sink on a format change, which invalidates effects bound to the old session; without this the EQ silently stops applying mid-queue. - All effect work is posted to mainHandler rather than run inline. AudioEffect construction from a player callback can re-enter the player and deadlock — the same shape as the AutoplayDecision lock-scrutinee bug. - Device equalizers expose a device-dependent band count (commonly 5) at fixed centres, so the canonical 10 bands are resampled by nearest centre frequency. resampleBands() is a pure @JvmStatic function so that mapping is testable without a device. Normalization is approximate, not parity: LoudnessEnhancer is a gain stage, not a true EBU R128 normalizer like MPV's dynaudnorm. Recorded as such rather than claimed as equivalent. Crossfade is deliberately excluded — unimplemented on every platform and blocked on mpv, so building it on Android alone would invert the parity gap. Tests written first and observed failing (cannot find function audio_settings_jni_payload) before the implementation: the payload contract is pinned by tests because a serde rename would otherwise silently break the Kotlin parser. Not yet verified on a physical device — AudioEffect availability and band layouts are device-specific. Requirements matrix marks these rows accordingly, and flipping the trait default to Err(not_implemented()) is deferred until that verification lands. |
||
|
|
e5d3cc06f2 |
fix(android): register WebView JS bridges once; stop audio-focus fight
Locking the screen killed audio on video playback even with the background-audio toggle armed. configureWebViewForMedia() ran from onCreate's delayed post AND from every onResume, re-calling addJavascriptInterface on each pass — five times in a 45s session. WebView binds injected objects at page-load time, so re-injecting over a live page leaves JS holding a stale proxy: the object stays truthy (passing the `bridge()?.` optional chain) while its methods vanish. Logcat showed 66 "WebView: Unknown object" errors and, in JS, "TypeError: setEnabled is not a function". So the toggle turned blue but never reached native. backgroundAudioEnabled stayed false, onStop never dispatched 'jellytau-background', the handoff never ran, and audio stopped the instant the screen locked. PiP and audio focus broke identically. - Register the bridges exactly once per WebView (identity-compared), and split the idempotent settings/chrome-client work into configureWebViewSettings() so it still runs on every resume. - Forward WebView console output to logcat as "JellyTauWeb". The frontend was previously invisible to adb, which is what made this bug so hard to place; keep it for the next boundary-spanning diagnosis. - setBackgroundAudioEnabled now reports whether native was actually reached instead of silently no-oping, so a dead bridge can never again masquerade as an armed toggle. Removing the re-injection revived a latent conflict it had been masking: the focus calls started working, and three AUDIOFOCUS_GAIN requesters inside one uid began fighting — MainActivity, ExoPlayer, and Chromium's own AudioFocusDelegate. The grant was followed ~45ms later by AUDIOFOCUS_LOSS, whose handler paused playback, so arming background audio (or just pressing play) paused the video in a loop. WebView already manages focus for <video>. Drop the redundant AndroidAudioFocus bridge, its listeners and its helpers entirely, and leave focus to whichever engine is actually rendering — consistent with the player-is-authoritative principle. Also drops the dead AndroidBackgroundAudio.isSupported() probe, unused since the button gate moved to platform(). TRACES: UR-040 | IR-025, DR-051 | UT-062 |
||
|
|
e083b53ee8 |
feat(downloads): WiFi-only network-type-aware download gating
Add a metered/cellular network detector so downloads honour a "WiFi only" preference. Android reports network type via NetworkTypeMonitor; Rust exposes it through download/network.rs and holds the queue pump when on a metered connection, emitting a queue-wide waitingForNetwork event. The frontend surfaces this via the networkType service and a waitingForNetwork store flag. TRACES: UR-053 | DR-074 |
||
|
|
acf1bb200d | fix resuming video playback after background audio only mode. | ||
|
|
3fbf6afdbc |
Background-audio handoff for video + repository/player refactor
Hand video playback off to a native audio-only stream when the app is backgrounded or locked, with no on-device video decode (UR-040). Adds player_enter/exit_background_audio commands, an audio-only stream URL for video items across the repository layer, and the frontend handoff state machine wired into VideoPlayer. Includes accompanying repository/offline/player refactoring and regenerates the traceability matrix. |
||
|
|
1fa5aa46f9 |
Android picture-in-picture, and fix three dead Android config files
Add PiP for native (ExoPlayer) video on Android. Video renders into a SurfaceView behind the WebView, so PiP is driven by the Activity shrinking into a floating window rather than the HTML5 PiP API (which WebKitGTK does not implement, hence Android-only). - PictureInPictureManager.kt: enter PiP with the video's aspect ratio (clamped to the 1:2.39-2.39:1 range Android accepts, outside which it throws), plus a play/pause RemoteAction. Hides the WebView while in PiP - it is opaque and sits above the surface, so it would otherwise occlude the video entirely - and re-fits the surface on exit. - MainActivity.kt: onUserLeaveHint auto-PiP, onPictureInPictureModeChanged, and an AndroidPictureInPicture JS interface following the existing AndroidAudioFocus pattern. - pictureInPicture.ts + VideoPlayer.svelte: PiP button, rendered only when the native bridge reports support. - proguard: keep rules for @JavascriptInterface methods, which are only referenced from JS and would be stripped in minified release builds. Casting needs no special handling: canEnterPip() checks natively that a local video surface is attached and playing, which a remote session lacks. While wiring the manifest, found that three tracked files under src-tauri/android/ were never reaching any build. Gradle reads only gen/android/app/src/main/, and sync-android-sources.sh did not copy them: - src/main/AndroidManifest.xml was a partial <application> fragment written as if Tauri merged it. It does not - there is no manifest-merger hook here, so its hardwareAccelerated flag never reached an APK. Promoted to the complete authoritative manifest (folding in that flag) and synced. - src/main/res/values/themes.xml (transparent status bar, fitsSystemWindows) was never copied; the sync only globbed mipmap-*. Now synced. - build.gradle.kts was a leftover com.android.library module config with stale media3 1.5.1 deps. The live deps are in app/build.gradle.kts at 1.5.0. Deleted. Verified: merged manifest now carries hardwareAccelerated, supportsPictureInPicture, resizeableActivity and the density configChange; themes.xml compiles into merged resources; Kotlin builds warning-free; svelte-check clean; 537 frontend tests pass. Not verified: PiP behaviour on a device, and the release keep rules against a minified build. assembleUniversalDebug cannot complete in this environment - the Rust step wants a dev-server addr file that only exists under `tauri android dev`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
acb7e5f221 |
fix offline mode and layout bugs
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m32s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
Build & Release / Run Tests (push) Successful in 5m21s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m25s
Build & Release / Build Linux (push) Successful in 17m31s
Build & Release / Build Android (push) Successful in 22m5s
Build & Release / Create Release (push) Successful in 16s
|
||
|
|
8938e3fdba |
Android launcher: drop monochrome (themed) icon, keep color only
The monochrome adaptive-icon layer produced a poor themed-icon rendering. Remove the <monochrome> reference from mipmap-anydpi-v26/ic_launcher.xml and delete the ic_launcher_monochrome.png files so Android always uses the color adaptive icon (background + foreground). sync-android-sources.sh also drops any monochrome layer Tauri regenerates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
e2c12615c5 |
Fix CI apk build
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 6m14s
Traceability Validation / Check Requirement Traces (push) Successful in 27s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 29m11s
Build & Release / Run Tests (push) Successful in 4m35s
Build & Release / Build Linux (push) Successful in 17m28s
Build & Release / Build Android (push) Successful in 21m35s
Build & Release / Create Release (push) Successful in 11s
|
||
|
|
8eae4ae253 | layout improvements | ||
|
|
385d2270c9 |
fix(android): keep lockscreen/media controls in sync with playback
The lockscreen controls drifted out of sync, especially while casting, and couldn't control remote playback. Two media sessions were competing (a Media3 MediaSession driving transport vs a MediaSessionCompat driving the notification), position was only pushed on play/pause so the scrubber froze mid-track, and remote mode showed stale local metadata with dead buttons. - Make MediaSessionCompat the single source of truth; route all transport commands (both the Compat callback and the Media3 wrappedPlayer) through Rust via nativeOnMediaCommand instead of touching ExoPlayer directly. - Push position on every 250ms tick via a lightweight updatePlaybackPosition, and report 0.0 playback speed when paused so Android stops extrapolating. - Mirror the remote session's now-playing onto the lockscreen from the native session poller (works while the screen is locked, unlike WebView timers) via a new player::update_lockscreen_metadata JNI bridge. - Make MediaSessionHandler mode-aware: in remote mode forward play/pause/next/ prev/seek to the remote Jellyfin session; Stop while casting emits RemoteDisconnectRequested, which the frontend handles by transferring to local. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
1836615dc0 |
feat(library): genre sliders, artist links, and navigation utils
- music landing: diverse per-genre album sliders (online counts / offline wide-probe fallback) and home-screen library shortcuts - add ArtistLinks component and shared navigation/genreDiversity utils - player/playback-mode refinements across Rust and frontend |
||
|
|
17a35573a0 |
feat(library): focused music/TV/movie landing screens + self-draining download queue
Library screens:
- Add dedicated music, TV, and movie landing pages (hero banner +
horizontal carousels) backed by new music/tv/movies stores.
- Route tvshows libraries to /library/tv; surface rediscover ("haven't
listened to in a while") albums via a new repository method across
online/offline/hybrid repos plus the repository_get_rediscover_albums
command.
- Add an A-Z jump bar for long alphabetically-sorted lists, with grid
index anchors in LibraryGrid/LibraryListView/TrackList.
- Filter the "Podcasts" folder out of music library queries.
Downloads:
- Add a backend queue pump: enqueue_download / enqueue_video_downloads
persist the resolved stream URL + target dir on each row (migration
017), and the pump starts up to max_concurrent and drains the rest
automatically as slots free, instead of the frontend silently dropping
items past the concurrency limit. Album/series/season buttons now
enqueue rather than calling start_download directly.
Other fixes:
- Hybrid search now returns instant cache results and pushes the merged
cache+server union via a request-id-tagged search-event, so superseded
queries can't clobber fresher results.
- URL-encode SearchTerm / genres / item types in online repo requests.
- Android: pause on audio-becoming-noisy (headphone/BT disconnect).
|
||
|
|
3faa595b76 |
fix(android): track VideoOverlayManager.kt in canonical source tree
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 2m28s
Traceability Validation / Check Requirement Traces (push) Successful in 20s
Build & Release / Run Tests (push) Successful in 3m11s
🏗️ Build and Test JellyTau / Build Android APK (push) Successful in 17m51s
Build & Release / Build Linux (push) Successful in 15m29s
Build & Release / Build Android (push) Failing after 18m9s
Build & Release / Create Release (push) Has been skipped
JellyTauPlayer.kt references com.dtourolle.jellytau.VideoOverlayManager, but the file existed only in the gitignored gen/android dir, so it survived locally but vanished in CI (which regenerates gen/android via 'tauri android init'). sync-android-sources.sh copies top-level *.kt from src-tauri/android, so adding it there gets it synced into the build. Fixes: 'Unresolved reference: VideoOverlayManager' in :app:compileUniversalReleaseKotlin. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
26286ac6e7 | sign build | ||
|
|
e8e37649fa | Many improvemtns and fixes related to decoupling of svelte and rust on android. | ||
|
|
9594e963bc | album art on lock scree and test fix | ||
|
|
cfddc1edea | First working POC |