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.
117 lines
3.8 KiB
TypeScript
117 lines
3.8 KiB
TypeScript
/**
|
|
* Picture-in-picture support, Android only.
|
|
*
|
|
* TRACES: UR-041 | IR-026 | DR-053
|
|
*
|
|
* Video on Android renders into a native ExoPlayer SurfaceView behind the
|
|
* WebView, so PiP is driven by the Activity (which shrinks into a floating
|
|
* window) rather than the HTML5 `requestPictureInPicture()` API. The bridge is
|
|
* the `AndroidPictureInPicture` @JavascriptInterface installed by MainActivity.
|
|
*
|
|
* On every other platform this module reports unsupported. Notably WebKitGTK
|
|
* (the Linux webview) does not implement the Picture-in-Picture Web API at all,
|
|
* so there is no HTML5 fallback to reach for.
|
|
*/
|
|
|
|
interface AndroidPictureInPictureBridge {
|
|
enterPip(): void;
|
|
isSupported(): boolean;
|
|
canEnterPip(): boolean;
|
|
setAutoEnterEnabled(enabled: boolean): void;
|
|
setHtml5VideoState(active: boolean, width: number, height: number, playing: boolean): void;
|
|
}
|
|
|
|
declare global {
|
|
interface Window {
|
|
AndroidPictureInPicture?: AndroidPictureInPictureBridge;
|
|
}
|
|
}
|
|
|
|
function bridge(): AndroidPictureInPictureBridge | undefined {
|
|
if (typeof window === "undefined") return undefined;
|
|
return window.AndroidPictureInPicture;
|
|
}
|
|
|
|
/**
|
|
* Whether the device can do PiP at all - used to decide if the button should
|
|
* be rendered. False on desktop, and on Android devices where the user has
|
|
* disabled the feature.
|
|
*/
|
|
export function isPipSupported(): boolean {
|
|
try {
|
|
return bridge()?.isSupported() ?? false;
|
|
} catch (err) {
|
|
console.warn("[PiP] isSupported check failed:", err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether entering PiP would succeed right now: a native video must be playing
|
|
* locally. False during audio playback and while casting to a remote session.
|
|
*/
|
|
export function canEnterPip(): boolean {
|
|
try {
|
|
return bridge()?.canEnterPip() ?? false;
|
|
} catch (err) {
|
|
console.warn("[PiP] canEnterPip check failed:", err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Enter picture-in-picture. No-op where unsupported. */
|
|
export function enterPip(): void {
|
|
try {
|
|
bridge()?.enterPip();
|
|
} catch (err) {
|
|
console.error("[PiP] Failed to enter picture-in-picture:", err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable/disable auto-entering PiP when the user backgrounds the app.
|
|
*
|
|
* This is a coarse frontend override; the authoritative gate is the native
|
|
* `canEnterPip` guard, which already refuses PiP unless a local video surface
|
|
* is actively rendering (so audio playback, menu/library browsing, and
|
|
* remote/cast sessions never enter PiP regardless of this flag). The only
|
|
* caller today is the background-audio toggle, which disarms auto-PiP so the
|
|
* two background behaviours stay mutually exclusive.
|
|
*/
|
|
export function setAutoEnterEnabled(enabled: boolean): void {
|
|
try {
|
|
bridge()?.setAutoEnterEnabled(enabled);
|
|
} catch (err) {
|
|
console.warn("[PiP] Failed to set auto-enter:", err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Tell native that a WebView `<video>` is (or is no longer) the playback surface.
|
|
*
|
|
* This is what makes PiP work at all in the shipping configuration. The native
|
|
* side only ever knew about the ExoPlayer surface, and that path is behind
|
|
* `experimentalNativeVideo`, which defaults to off — so `canEnterPip` was always
|
|
* false and pressing the button did nothing. Reporting the element's state gives
|
|
* native a surface it can legitimately shrink into, plus the intrinsic size it
|
|
* needs for the PiP window's aspect ratio and the play state for its play/pause
|
|
* action.
|
|
*
|
|
* Pass `active: false` when the element goes away, or PiP would be offered over a
|
|
* video that is no longer there.
|
|
*
|
|
* TRACES: UR-041 | DR-160
|
|
*/
|
|
export function setHtml5VideoState(
|
|
active: boolean,
|
|
width: number,
|
|
height: number,
|
|
playing: boolean
|
|
): void {
|
|
try {
|
|
bridge()?.setHtml5VideoState(active, Math.round(width), Math.round(height), playing);
|
|
} catch (err) {
|
|
console.warn("[PiP] Failed to report HTML5 video state:", err);
|
|
}
|
|
}
|