Merge branch 'master' into worktree-mosaic-library
# Conflicts: # scripts/extract-traces.test.ts
This commit is contained in:
@@ -2082,7 +2082,7 @@ export type Library = { id: string; name: string; collectionType: string; imageT
|
||||
* collection-type → category table any more than an item-type one. See
|
||||
* `SearchScope::for_collection_type`.
|
||||
*
|
||||
* TRACES: UR-075 | DR-173
|
||||
* TRACES: UR-075 | DR-175
|
||||
*/
|
||||
favoritesScope?: SearchScope | null }
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* Called once the bitmap is decoded, with its intrinsic pixel size. Lets a
|
||||
* layout that sizes boxes from artwork (the mosaic) use the shape the image
|
||||
* actually has rather than the one its item type suggests.
|
||||
* TRACES: UR-075 | DR-172
|
||||
* TRACES: UR-075 | DR-174
|
||||
*/
|
||||
onNaturalSize?: (width: number, height: number) => void;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
arrives over a few hundred milliseconds, and re-packing on each arrival would
|
||||
shuffle the grid under the viewer's cursor several times over.
|
||||
|
||||
TRACES: UR-075 | DR-172
|
||||
TRACES: UR-075 | DR-174
|
||||
-->
|
||||
<script lang="ts" generics="T extends { key: string; ratio: number }">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
lines would no longer line up with its neighbours. Keeping everything inside
|
||||
the box is what lets `layoutMosaic` own the geometry completely.
|
||||
|
||||
TRACES: UR-075 | DR-172
|
||||
TRACES: UR-075 | DR-174
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// `favoritesScope` (Rust: `SearchScope::for_collection_type`). This file only
|
||||
// decides what to *call* it and where to put it.
|
||||
//
|
||||
// TRACES: UR-075, UR-067 | DR-172, DR-173 | UT-167
|
||||
// TRACES: UR-075, UR-067 | DR-174, DR-175 | UT-167
|
||||
|
||||
import type { Library } from "$lib/api/types";
|
||||
import {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//
|
||||
// Presentation only — nothing here knows what a library or a media item is.
|
||||
//
|
||||
// TRACES: UR-075 | DR-172 | UT-158, UT-159, UT-160
|
||||
// TRACES: UR-075 | DR-174 | UT-158, UT-159, UT-160
|
||||
|
||||
/** A tile to place: an opaque key and the aspect ratio (width / height) to honour. */
|
||||
export interface MosaicInput {
|
||||
|
||||
@@ -23,12 +23,12 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
// ---- Mocks (must precede component import) --------------------------------
|
||||
|
||||
const channelHandlers: Record<string, (event: any) => void> = {};
|
||||
// These tests pin the **flag-off** interim behaviour: when `experimentalNativeVideo`
|
||||
// is off, VideoPlayer overrides Android's native backend response to HTML5
|
||||
// rendering and stops the native backend. That flag now defaults to *on*
|
||||
// (DR-160, so picture-in-picture has a real surface to shrink into), so the
|
||||
// default no longer selects this path and the tests have to say which path they
|
||||
// are guarding rather than inherit it. (DR-161)
|
||||
// These tests pin the **flag-off** behaviour: when `experimentalNativeVideo` is
|
||||
// off, VideoPlayer overrides Android's native backend response to HTML5
|
||||
// rendering and stops the native backend. That is the default again (DR-172,
|
||||
// after native video shipped as audio with no picture), so this mock now agrees
|
||||
// with the default rather than opposing it — kept explicit so the tests state
|
||||
// which path they guard instead of inheriting whatever the default happens to be.
|
||||
vi.mock("$lib/stores/nativeVideo", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("$lib/stores/nativeVideo")>();
|
||||
return {
|
||||
|
||||
@@ -26,12 +26,12 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
// ---- Mocks (must precede component import) --------------------------------
|
||||
|
||||
const channelHandlers: Record<string, (event: any) => void> = {};
|
||||
// These tests pin the **flag-off** interim behaviour: when `experimentalNativeVideo`
|
||||
// is off, VideoPlayer overrides Android's native backend response to HTML5
|
||||
// rendering and stops the native backend. That flag now defaults to *on*
|
||||
// (DR-160, so picture-in-picture has a real surface to shrink into), so the
|
||||
// default no longer selects this path and the tests have to say which path they
|
||||
// are guarding rather than inherit it. (DR-161)
|
||||
// These tests pin the **flag-off** behaviour: when `experimentalNativeVideo` is
|
||||
// off, VideoPlayer overrides Android's native backend response to HTML5
|
||||
// rendering and stops the native backend. That is the default again (DR-172,
|
||||
// after native video shipped as audio with no picture), so this mock now agrees
|
||||
// with the default rather than opposing it — kept explicit so the tests state
|
||||
// which path they guard instead of inheriting whatever the default happens to be.
|
||||
vi.mock("$lib/stores/nativeVideo", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("$lib/stores/nativeVideo")>();
|
||||
return {
|
||||
|
||||
@@ -27,26 +27,32 @@ const STORAGE_KEY = "jellytau-experimental-native-video";
|
||||
const NATIVE_VIDEO_ATTR = "data-native-video";
|
||||
|
||||
/**
|
||||
* Whether the native path is on, defaulting to **on** when the user has never
|
||||
* chosen.
|
||||
* Whether the native path is on. **Off** unless the user turned it on.
|
||||
*
|
||||
* It shipped defaulting to off while the native path was a spike. It is now the
|
||||
* default because picture-in-picture is built on it: PiP shrinks the *Activity*,
|
||||
* so it needs a real video surface behind the WebView to show, and on the HTML5
|
||||
* path there is nothing for it to shrink into but the UI itself (DR-160).
|
||||
* DR-161 briefly made this default to on, so picture-in-picture could shrink a
|
||||
* real video surface. On a device that shipped as **audio with no picture**:
|
||||
* ExoPlayer decoded correctly and fed its SurfaceView, but the SurfaceView sits
|
||||
* *behind* the WebView and the compositing that clears the opaque layers above it
|
||||
* never took effect — logcat showed `WebView transparent = false` and never
|
||||
* `= true`. So the video was rendering the whole time, behind the page.
|
||||
*
|
||||
* That is the defect the flag existed to contain, and it is why the default is
|
||||
* back off: video working matters more than PiP showing the native surface, and
|
||||
* PiP still works without it via the HTML5 path (DR-160). Native video remains
|
||||
* available in Settings for anyone testing it.
|
||||
*
|
||||
* An explicit stored choice still wins in both directions, so anyone who turned
|
||||
* it off keeps it off.
|
||||
* it on keeps it on.
|
||||
*
|
||||
* TRACES: UR-003, UR-004 | DR-172
|
||||
*/
|
||||
function load(): boolean {
|
||||
if (typeof localStorage === "undefined") return true;
|
||||
if (typeof localStorage === "undefined") return false;
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
return stored === null ? true : stored === "true";
|
||||
return localStorage.getItem(STORAGE_KEY) === "true";
|
||||
} catch {
|
||||
// Private-mode / disabled storage — no stored choice is readable, so this is
|
||||
// the same case as "never chosen".
|
||||
return true;
|
||||
// Private-mode / disabled storage — default to the safe (HTML5) path.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,8 +80,8 @@ function createExperimentalNativeVideoStore() {
|
||||
}
|
||||
|
||||
/**
|
||||
* User opt-out for the native Android video path. **Defaults to on** since
|
||||
* DR-161 — see `load()`. The name still says "experimental" because the flag
|
||||
* User opt-in for the native Android video path. **Defaults to off** again since
|
||||
* DR-172 — see `load()`. The name says "experimental" because the flag
|
||||
* remains a suppressor of Rust's backend choice, not a promoter of it.
|
||||
*/
|
||||
export const experimentalNativeVideo = createExperimentalNativeVideoStore();
|
||||
|
||||
@@ -41,7 +41,7 @@ export function resolveFavoritesScope(raw: string | null | undefined): Favorites
|
||||
* than folded into "all": a caller asking "which category is this?" wants no
|
||||
* answer, not the cross-category one.
|
||||
*
|
||||
* TRACES: UR-075 | DR-173
|
||||
* TRACES: UR-075 | DR-175
|
||||
*/
|
||||
export function asFavoritesScope(
|
||||
scope: SearchScope | null | undefined,
|
||||
|
||||
@@ -97,9 +97,9 @@ export function setAutoEnterEnabled(enabled: boolean): void {
|
||||
* needs for the PiP window's aspect ratio and the play state for its play/pause
|
||||
* action.
|
||||
*
|
||||
* The flag defaults to **on** now (DR-161), so Android normally shrinks the real
|
||||
* ExoPlayer surface instead; this remains the path for Linux and for anyone who
|
||||
* turned the flag off.
|
||||
* The flag is back to defaulting **off** (DR-172, after native video shipped as
|
||||
* audio with no picture), so this is once again the path Android normally takes —
|
||||
* which is why PiP does not depend on that flag being on.
|
||||
*
|
||||
* Pass `active: false` when the element goes away, or PiP would be offered over a
|
||||
* video that is no longer there.
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
// The shortcut strip is a mosaic row: one height, each tile as wide as its own
|
||||
// artwork. It used to force 16:9 on everything so square music covers lined up
|
||||
// with wide backdrops — which lined them up by cropping the covers.
|
||||
// TRACES: UR-075 | DR-172
|
||||
// TRACES: UR-075 | DR-174
|
||||
const LIBRARY_STRIP_HEIGHT = 132;
|
||||
const libraryTiles = $derived(
|
||||
shortcutLibraries.map((lib) => ({ key: lib.id, ratio: assumedLibraryRatio(lib), library: lib }))
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
// The overview is a mosaic: rows of one height, tiles of their own widths, so
|
||||
// a square music cover sits beside a wide backdrop without either being
|
||||
// cropped to the other's shape. Each category also gets a favourites tile of
|
||||
// its own, beside the library it belongs to. TRACES: UR-075 | DR-172, DR-173
|
||||
// its own, beside the library it belongs to. TRACES: UR-075 | DR-174, DR-175
|
||||
const mosaicEntries = $derived(buildLibraryMosaic(visibleLibraries));
|
||||
|
||||
// Track if we've done an initial load and previous server state
|
||||
@@ -255,7 +255,7 @@
|
||||
category's own favourites sits beside its library — a labelled tile
|
||||
at the same weight as a library is the difference between a feature
|
||||
people find and one they don't. ux-flows §5C.2.
|
||||
TRACES: UR-067, UR-075 | DR-117, DR-172 -->
|
||||
TRACES: UR-067, UR-075 | DR-117, DR-174 -->
|
||||
<MosaicGrid items={mosaicEntries} gap={8}>
|
||||
{#snippet tile(entry)}
|
||||
{#if entry.kind === "favorites"}
|
||||
|
||||
@@ -744,10 +744,10 @@
|
||||
</h3>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
Decode video with the device's hardware decoder instead of the
|
||||
built-in web player. Better performance and battery life, and
|
||||
required for picture-in-picture to show the video rather than
|
||||
the app. Still less tested — turn this off if video fails to
|
||||
appear or seeking misbehaves.
|
||||
built-in web player. Better performance and battery life in
|
||||
principle, but incomplete: on some devices the picture does not
|
||||
appear at all and only the sound plays. Leave this off unless
|
||||
you are helping test it.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user