android: what a screen reader says — names, roles, and the sheets out of the tree when they are down
Every control on the phone has a name now, in the app's own words, and the desktop's word where it already has one; the nav is a tab set whose badge count is part of the tab's name — "You, 3 transfers" rather than a stray 3 read out before it. The two sheets are dialogs with somewhere sensible for the focus to land, and the half sheet's grab handle is finally a real Close — the first dismissal a screen reader could reach, because the scrim never was one. A sheet that is down is inert rather than sitting off-screen in the accessibility tree with the hidden mini bar and the nav under it, and one live region is mounted for the life of the app so that an undo offer is actually announced; switches say on or off, chips say pressed, and the waveform is a slider where it can seek. Nothing visible changed. The walk counts the controls with no name, with a junk name, in a hidden layer, or with unsaid state — all four held at zero on ten screens, and the nameless count on a room and on both prompts besides, which is where the six fields named here actually live.
This commit is contained in:
1 parent
9df162360e
commit
4fc6e9c630
22 files changed
+355
-83
No files matched your search
+68
@@ -516,6 +516,74 @@ with a `ClassNotFoundException`. `src-tauri/android/build.sh` exports it;
|
||||
after adding a plugin, touch `src-tauri/build.rs` too, so the app's build
|
||||
script re-runs and rewrites the wiring.
|
||||
|
||||
## What a screen reader says
|
||||
|
||||
Every control on the phone is named by what it does, in the app's own
|
||||
words, and where the desktop already has a word for something it is the
|
||||
word used here too — Seek, View album artwork, Close artwork viewer. An
|
||||
icon-only control carries an `aria-label`; one with visible text is named
|
||||
by the text it shows.
|
||||
|
||||
The nav is a tab set: `role="tablist"` on the bar, `role="tab"` and
|
||||
`aria-selected` on the five destinations, and the count badge round
|
||||
seventeen added is part of the tab's own name — "You, 3 transfers",
|
||||
"Community, 2 unread" — rather than a digit read out before the word it
|
||||
belongs to. The badge itself is `aria-hidden`, because the number is in
|
||||
the name now.
|
||||
|
||||
The two sheets are dialogs: `role="dialog" aria-modal="true"`, the player
|
||||
sheet named for what is playing, the half sheet reading its own first
|
||||
title back out of the DOM so that twenty-seven kinds of sheet need no list
|
||||
of names to keep in step. Opening one puts the focus somewhere sensible —
|
||||
the player sheet on its Close, the half sheet on its grab handle, which is
|
||||
a real button now ("Close this sheet") and the first dismissal a screen
|
||||
reader could ever reach; the scrim behind it never was one.
|
||||
|
||||
A sheet that is down is `inert` and `aria-hidden`. Neither sheet is
|
||||
unmounted — they are translated off the bottom of the screen, and the half
|
||||
sheet deliberately keeps its last body rendered through the slide down —
|
||||
so before this the closed sheets' rows, the hidden mini bar and the nav
|
||||
under an open sheet were all still in the accessibility tree, and TalkBack
|
||||
could swipe into every one of them.
|
||||
|
||||
There is one live region, `.sr-live`, mounted for the life of the app and
|
||||
clipped to a single pixel, because a region that appears with its words
|
||||
already inside it is announced reliably by nobody; an announcement here is
|
||||
a change of text in a node that was always there. That is what carries an
|
||||
error and a notice, so an Undo offer is actually read out — and the Undo
|
||||
button itself stays reachable, only the sentence beside it is hidden from
|
||||
the tree so it is not said twice.
|
||||
|
||||
Artwork that opens the viewer is a control, not decoration. Artwork that
|
||||
is decoration carries `alt=""` and every icon is `aria-hidden`, so the
|
||||
tree never reads a filename out. The waveform is `role="slider"` with a
|
||||
value and arrow keys where it can seek, and `aria-hidden` on the mini bar
|
||||
where it is only a picture of the position. Switches say on or off, choice
|
||||
chips say pressed, and the two long descriptions and the gap line say
|
||||
expanded.
|
||||
|
||||
The check, so this does not quietly rot: the walk's probe sweeps every
|
||||
interactive element in the live DOM and counts four things — the ones with
|
||||
no accessible name (`a11yAnon`, with `a11yWho` naming the first three
|
||||
offenders so one run tells you what to add), the ones whose name is junk
|
||||
(`a11yBad`: bare digits, a filename, a lone glyph, an essay), the ones
|
||||
sitting in a layer that is off-screen but still in the tree (`a11yGhost`)
|
||||
and the ones that look chosen without saying so (`a11yState`). All four
|
||||
are asserted at zero on Home, the library, a record, a catalog record, the
|
||||
artist page, You, the taste page, a half sheet, the player sheet and the
|
||||
viewer — and the anonymous count on a room and on both shapes of prompt
|
||||
besides, because a room's message box and the prompt's own field are where
|
||||
the fields this round finally named actually live: assert it only where
|
||||
they are not and the round guards everything except its own work.
|
||||
|
||||
One known exception, left alone on purpose: the per-row ⋯ is a
|
||||
`role="button"` inside the row's own `<button>`, and the mini bar's three
|
||||
transport buttons sit inside its `role="button"` the same way. That is
|
||||
invalid ARIA, and both ends are named so nothing is anonymous; unpicking
|
||||
it means splitting `.trk` (one flex row with a whole-row `:active`) and
|
||||
rewriting the walk's `.trk .r` selectors, for no gain a screen reader
|
||||
would notice.
|
||||
|
||||
## Performance
|
||||
|
||||
`tests/mobile/perf.mjs` logs the frames the WebView produces through
|
||||
|
||||
+38
-10
@@ -3,6 +3,7 @@
|
||||
and toolbar popovers. Only rows that do something are here; what a
|
||||
row cannot do yet is not a dead row, it is absent. Facts about a file
|
||||
are key/value rows, not buttons. */
|
||||
import { tick } from "svelte";
|
||||
import Icon from "../components/Icon.svelte";
|
||||
import Cover from "./Cover.svelte";
|
||||
import Identicon from "../components/Identicon.svelte";
|
||||
@@ -29,6 +30,32 @@
|
||||
let shown = $state(null);
|
||||
$effect(() => { if (nav.half) shown = nav.half; });
|
||||
|
||||
/* What this sheet is called, for a screen reader: whatever its own
|
||||
first title says. Every kind writes one — a .sheet-title, or the
|
||||
head's .t — so read it back out of the DOM rather than keeping a
|
||||
list of twenty-seven names in step with the markup. */
|
||||
let hsName = $state("");
|
||||
let grabEl = $state(null);
|
||||
$effect(() => {
|
||||
if (!nav.half) return;
|
||||
void shown;
|
||||
tick().then(() => {
|
||||
const el = document.querySelector(".halfsheet .sheet-title, .halfsheet .sheet-head .t");
|
||||
hsName = el ? (el.textContent || "").replace(/\s+/g, " ").trim() : "Options";
|
||||
});
|
||||
});
|
||||
/* the grab handle is where the focus lands — unless the sheet has a
|
||||
field, which takes it for itself a moment later (focusLater) */
|
||||
$effect(() => {
|
||||
if (!nav.half) return;
|
||||
const t = setTimeout(() => {
|
||||
if (!grabEl || document.querySelector(".halfsheet .field")) return;
|
||||
if (grabEl.closest(".halfsheet")?.contains(document.activeElement)) return;
|
||||
grabEl.focus({ preventScroll: true });
|
||||
}, 160);
|
||||
return () => clearTimeout(t);
|
||||
});
|
||||
|
||||
const kind = $derived.by(() => {
|
||||
const h = shown;
|
||||
if (!h) return null;
|
||||
@@ -317,15 +344,16 @@
|
||||
{/snippet}
|
||||
|
||||
{#snippet swrow(icon, t, m, on, fn)}
|
||||
<button class="row" onclick={fn}>
|
||||
<button class="row" onclick={fn} role="switch" aria-checked={on}>
|
||||
<span class="lead"><Icon name={icon} size={20} /></span>
|
||||
<span class="b"><span class="t">{t}</span>{#if m}<span class="m">{m}</span>{/if}</span>
|
||||
<span class="sw" class:on><span></span></span>
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
<div class="halfsheet" class:up={!!nav.half}>
|
||||
<div class="grab"></div>
|
||||
<div class="halfsheet" class:up={!!nav.half} role="dialog" aria-modal="true" aria-label={hsName || "Options"}
|
||||
aria-hidden={nav.half ? undefined : "true"} inert={!nav.half}>
|
||||
<button class="grab" bind:this={grabEl} onclick={closeHalf} aria-label="Close this sheet"></button>
|
||||
<div class="hs-body">
|
||||
{#if kind === "track" && trackArg}
|
||||
{@const album = trackArg.album}
|
||||
@@ -504,9 +532,9 @@
|
||||
<div class="sheet-title">{a.title}</div>
|
||||
<div class="prompt">
|
||||
{#if a.multiline}
|
||||
<textarea class="field area" bind:value={promptVal} placeholder={a.placeholder || ""} rows="3" use:focusLater></textarea>
|
||||
<textarea class="field area" bind:value={promptVal} placeholder={a.placeholder || ""} rows="3" aria-label={a.title} use:focusLater></textarea>
|
||||
{:else}
|
||||
<input class="field" bind:value={promptVal} placeholder={a.placeholder || ""} autocomplete="off" spellcheck="false" inputmode={a.numeric ? "numeric" : undefined} enterkeyhint="done" onkeydown={(e) => { if (e.key === "Enter") submitPrompt(); }} use:focusLater />
|
||||
<input class="field" bind:value={promptVal} placeholder={a.placeholder || ""} autocomplete="off" spellcheck="false" inputmode={a.numeric ? "numeric" : undefined} aria-label={a.title} enterkeyhint="done" onkeydown={(e) => { if (e.key === "Enter") submitPrompt(); }} use:focusLater />
|
||||
{/if}
|
||||
<div class="prompt-acts">
|
||||
<button class="btn btn-ghost" onclick={closeHalf}>Cancel</button>
|
||||
@@ -590,12 +618,12 @@
|
||||
{@render row("tag", "Group", m.group || "a word to sort them by", () => metaPrompt(u, "group", "Group", "Traders", m.group || ""))}
|
||||
{@render row("file-text", "Note", m.note || "a private note, for you only", () => metaPrompt(u, "note", "Note", "What you want to remember about them", m.note || "", true))}
|
||||
<div class="sheet-hair"></div>
|
||||
<button class="row" onclick={() => setMeta(u, { favorite: !m.favorite })}>
|
||||
<button class="row" onclick={() => setMeta(u, { favorite: !m.favorite })} role="switch" aria-checked={!!m.favorite}>
|
||||
<span class="lead"><Icon name="star" size={20} /></span>
|
||||
<span class="b"><span class="t">Favorite</span><span class="m">kept at the top of the buddy list</span></span>
|
||||
<span class="sw" class:on={m.favorite}><span></span></span>
|
||||
</button>
|
||||
<button class="row" onclick={() => setMeta(u, { trusted: !m.trusted })}>
|
||||
<button class="row" onclick={() => setMeta(u, { trusted: !m.trusted })} role="switch" aria-checked={!!m.trusted}>
|
||||
<span class="lead"><Icon name="shield" size={20} /></span>
|
||||
<span class="b"><span class="t">Trusted</span><span class="m">someone you take at their word</span></span>
|
||||
<span class="sw" class:on={m.trusted}><span></span></span>
|
||||
@@ -684,13 +712,13 @@
|
||||
<div class="sheet-sec">Order</div>
|
||||
<div class="chips" style="padding-bottom:12px">
|
||||
{#each SORTS as [k, l] (k)}
|
||||
<button class="chip" class:on={(top().c.sort || "added") === k} class:quiet={(top().c.sort || "added") !== k} onclick={() => patch({ sort: k, tab: "albums" })}>{l}</button>
|
||||
<button class="chip" class:on={(top().c.sort || "added") === k} class:quiet={(top().c.sort || "added") !== k} aria-pressed={(top().c.sort || "added") === k} onclick={() => patch({ sort: k, tab: "albums" })}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="sheet-sec">View</div>
|
||||
<div class="chips" style="padding-bottom:12px">
|
||||
{#each [["grid", "Grid"], ["list", "List"]] as [k, l] (k)}
|
||||
<button class="chip" class:on={(top().c.view || "grid") === k} class:quiet={(top().c.view || "grid") !== k} onclick={() => patch({ view: k, tab: "albums" })}>{l}</button>
|
||||
<button class="chip" class:on={(top().c.view || "grid") === k} class:quiet={(top().c.view || "grid") !== k} aria-pressed={(top().c.view || "grid") === k} onclick={() => patch({ view: k, tab: "albums" })}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div style="padding: 4px var(--pad) 16px"><button class="btn btn-fill wide" onclick={closeHalf}>Done</button></div>
|
||||
@@ -755,7 +783,7 @@
|
||||
<div class="sheet-sec">Pause after</div>
|
||||
<div class="chips" style="padding-bottom:16px">
|
||||
{#each [[0, "Off"], [15, "15 min"], [30, "30 min"], [45, "45 min"], [60, "1 hour"], [90, "1½ hours"]] as [m, l] (m)}
|
||||
<button class="chip" class:on={m === 0 ? !player.sleepAt : Math.abs((player.sleepAt - Date.now()) / 60000 - m) < 1} class:quiet={!(m === 0 ? !player.sleepAt : Math.abs((player.sleepAt - Date.now()) / 60000 - m) < 1)} onclick={act(() => setSleep(m))}>{l}</button>
|
||||
<button class="chip" class:on={m === 0 ? !player.sleepAt : Math.abs((player.sleepAt - Date.now()) / 60000 - m) < 1} class:quiet={!(m === 0 ? !player.sleepAt : Math.abs((player.sleepAt - Date.now()) / 60000 - m) < 1)} aria-pressed={m === 0 ? !player.sleepAt : Math.abs((player.sleepAt - Date.now()) / 60000 - m) < 1} onclick={act(() => setSleep(m))}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if player.sleepAt}<div class="sec-note" style="padding-bottom:14px">Playback pauses in {sleepLabel().replace(" left", "")}.</div>{/if}
|
||||
|
||||
@@ -87,8 +87,20 @@
|
||||
}
|
||||
function pm(e) { if (down) at(e); }
|
||||
function pu() { down = false; }
|
||||
/* a seek from the keyboard, or from TalkBack: two per cent a press —
|
||||
the fraction the pointer path already speaks */
|
||||
function key(e) {
|
||||
if (!onseek) return;
|
||||
const step = e.key === "ArrowRight" ? 0.02 : e.key === "ArrowLeft" ? -0.02 : 0;
|
||||
if (!step) return;
|
||||
e.preventDefault();
|
||||
onseek(Math.max(0, Math.min(1, played + step)));
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -- the role and the
|
||||
tabindex arrive together: a lane that can seek is a slider, and one
|
||||
that cannot is out of the tree entirely -->
|
||||
<span
|
||||
class="lane {cls}"
|
||||
class:pending
|
||||
@@ -102,6 +114,14 @@
|
||||
onpointermove={pm}
|
||||
onpointerup={pu}
|
||||
onpointercancel={pu}
|
||||
onkeydown={key}
|
||||
role={onseek ? "slider" : undefined}
|
||||
tabindex={onseek ? 0 : undefined}
|
||||
aria-hidden={onseek ? undefined : "true"}
|
||||
aria-label={onseek ? "Seek" : undefined}
|
||||
aria-valuemin={onseek ? 0 : undefined}
|
||||
aria-valuemax={onseek ? 100 : undefined}
|
||||
aria-valuenow={onseek ? Math.round(played * 100) : undefined}
|
||||
>
|
||||
{#each Array(bars) as _, i (i)}
|
||||
<i style="--i:{i}; height:{heightOf(i)}%; --fill:{fillOf(i).toFixed(3)}; opacity:{dimOf(i) ? PENDING_ALPHA : 1}"></i>
|
||||
|
||||
@@ -107,6 +107,30 @@
|
||||
const playing = $derived(player.status === "playing" || player.status === "buffering");
|
||||
const played = $derived(player.duration ? Math.min(1, player.position / player.duration) : 0);
|
||||
|
||||
/* ── what the machine says ───────────────────────────────────────── */
|
||||
/* A tab's own name: the section, and when the dot is up, what the
|
||||
number counts. "You, 3 transfers" — not a stray 3 read out before
|
||||
the word it belongs to. */
|
||||
function tabCount(id) {
|
||||
if (id === "you") return activeTransfers().length;
|
||||
if (id === "community") return unreadTotal();
|
||||
return 0;
|
||||
}
|
||||
function tabName(id, label) {
|
||||
const n = tabCount(id);
|
||||
if (!n) return label;
|
||||
return label + ", " + n + (id === "you" ? " transfer" + (n === 1 ? "" : "s") : " unread");
|
||||
}
|
||||
|
||||
/* The one live region. It is mounted for the life of the app on
|
||||
purpose: a region that appears with its words already inside it is
|
||||
announced reliably by neither TalkBack nor VoiceOver, so an
|
||||
announcement here is a TEXT CHANGE in a node that was always there.
|
||||
The desktop does the same (FollowToast's always-mounted .layer,
|
||||
PreviewOverlay's .sr-announce). Never drawn — shell.css clips it to
|
||||
a pixel. */
|
||||
const announce = $derived([player.error || lib.error, notice.text, notice.action?.label].filter(Boolean).join(". "));
|
||||
|
||||
/* ── the ground: two layers, the incoming one fades up over the old ── */
|
||||
let ambA = $state(null);
|
||||
let ambB = $state(null);
|
||||
@@ -272,7 +296,7 @@
|
||||
</div>
|
||||
|
||||
{#if player.track}
|
||||
<div class="mini" class:hidden={nav.sheet} use:drag={miniDrag} onclick={onMiniTap} oncontextmenu={(e) => { e.preventDefault(); openHalf("track", { album: player.album, track: player.track }); }} role="button" tabindex="0" onkeydown={() => {}} aria-label="Now playing">
|
||||
<div class="mini" class:hidden={nav.sheet} use:drag={miniDrag} onclick={onMiniTap} oncontextmenu={(e) => { e.preventDefault(); openHalf("track", { album: player.album, track: player.track }); }} role="button" tabindex="0" onkeydown={() => {}} aria-hidden={nav.sheet ? "true" : undefined} aria-label={"Now playing: " + (player.track.title || "") + (player.track.artist || player.album?.artist ? " · " + (player.track.artist || player.album?.artist) : "")}>
|
||||
<span class="art" bind:this={miniArt}><Cover path={player.album?.art_path} eager size={20} /></span>
|
||||
<span class="b">
|
||||
<span class="t">{player.track.title}</span>
|
||||
@@ -293,32 +317,40 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<nav class="nav" class:gone={nav.sheet}>
|
||||
<!-- svelte-ignore a11y_no_noninteractive_element_to_interactive_role -- the
|
||||
five destinations ARE a tab set, and the desktop's own segmented
|
||||
control says so too; wrapping them in an inner div to keep <nav>'s
|
||||
implicit role would change the bar's flex layout -->
|
||||
<nav class="nav" class:gone={nav.sheet} role="tablist" aria-label="Sections" aria-hidden={nav.sheet ? "true" : undefined}>
|
||||
{#each TABS as [id, icon, label] (id)}
|
||||
<button class:on={nav.tab === id} onclick={() => go(id)}>
|
||||
<button class:on={nav.tab === id} onclick={() => go(id)} role="tab" aria-selected={nav.tab === id} aria-label={tabName(id, label)}>
|
||||
<span class="pill"></span>
|
||||
<span class="gl">
|
||||
<Icon name={icon} size={22} />
|
||||
{#if id === "you" && activeTransfers().length}<span class="nb">{activeTransfers().length}</span>{/if}
|
||||
{#if id === "community" && unreadTotal()}<span class="nb">{unreadTotal()}</span>{/if}
|
||||
{#if id === "you" && activeTransfers().length}<span class="nb" aria-hidden="true">{activeTransfers().length}</span>{/if}
|
||||
{#if id === "community" && unreadTotal()}<span class="nb" aria-hidden="true">{unreadTotal()}</span>{/if}
|
||||
</span>
|
||||
<span class="lb">{label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<div class="scrim" class:on={!!nav.half} onclick={closeHalf} role="presentation"></div>
|
||||
<div class="scrim" class:on={!!nav.half} onclick={closeHalf} aria-hidden="true"></div>
|
||||
<HalfSheet />
|
||||
<Sheet />
|
||||
<Viewer />
|
||||
<SyncRoom />
|
||||
|
||||
<!-- the announcement: one node, always here, so a change of its words
|
||||
is what gets read out. The toasts below stay where they were and
|
||||
say nothing on their own — two announcers say everything twice -->
|
||||
<div class="sr-live" data-live aria-live="polite" aria-atomic="true">{announce}</div>
|
||||
{#if player.error || lib.error}
|
||||
<div class="toast" onclick={() => { player.error = ""; lib.error = ""; }} role="status">{player.error || lib.error}</div>
|
||||
<div class="toast" onclick={() => { player.error = ""; lib.error = ""; }} role="button" tabindex="0" onkeydown={() => {}} aria-label="Dismiss this message">{player.error || lib.error}</div>
|
||||
{/if}
|
||||
{#if notice.text}
|
||||
<div class="toast notice" role="status">
|
||||
<span class="nt">{notice.text}</span>
|
||||
<div class="toast notice">
|
||||
<span class="nt" aria-hidden="true">{notice.text}</span>
|
||||
{#if notice.action}<button class="na" onclick={() => { const f = notice.action.fn; clearNote(); f?.(); }}>{notice.action.label}</button>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="release" class:w2 {onclick} oncontextmenu={hold} use:visible={path}>
|
||||
<button class="release" class:w2 {onclick} oncontextmenu={hold} use:visible={path} aria-label={[title, sub, badge].filter(Boolean).join(", ")}>
|
||||
<span class="cover-wrap">
|
||||
{#if badge}<span class="ed-badge">{badge}</span>{/if}
|
||||
{#if badge}<span class="ed-badge" aria-hidden="true">{badge}</span>{/if}
|
||||
{#if mark}<span class="mark"><Icon name={mark} size={11} strokeWidth={3} /></span>{/if}
|
||||
{#if src}<img src={src} alt="" loading="lazy" />{:else}<span class="ph"><Icon name={icon} size={26} /></span>{/if}
|
||||
</span>
|
||||
|
||||
+21
-4
@@ -64,6 +64,20 @@
|
||||
}
|
||||
});
|
||||
|
||||
/* Somewhere sensible to land when the sheet opens: its own Close. The
|
||||
gate is nav.sheet and not "shown" — "shown" is false for a frame at
|
||||
the start of a morph arrival and the dialog has to be live by then —
|
||||
and preventScroll because that arrival is measuring rects in the
|
||||
same frames. */
|
||||
$effect(() => {
|
||||
if (!nav.sheet) return;
|
||||
const t = setTimeout(() => {
|
||||
const el = document.querySelector(".sheet.up .np-head .iconbtn");
|
||||
if (el && !el.closest(".sheet")?.contains(document.activeElement)) el.focus({ preventScroll: true });
|
||||
}, 160);
|
||||
return () => clearTimeout(t);
|
||||
});
|
||||
|
||||
function flip(a) {
|
||||
const to = frameEl;
|
||||
const css = getComputedStyle(document.documentElement);
|
||||
@@ -134,7 +148,9 @@
|
||||
function lineSeek(l) { seekSecs(Math.max(0, (l.time_ms + lyr.offset) / 1000)); }
|
||||
</script>
|
||||
|
||||
<div class="sheet" class:up={shown} class:morphing use:drag={sheetDrag}>
|
||||
<div class="sheet" class:up={shown} class:morphing use:drag={sheetDrag}
|
||||
role="dialog" aria-modal="true" aria-label={"Now playing" + (player.track ? ": " + player.track.title : "")}
|
||||
aria-hidden={nav.sheet ? undefined : "true"} inert={!nav.sheet}>
|
||||
{#if player.album}
|
||||
<div class="sheet-bg" aria-hidden="true">
|
||||
<div class="amb on" style:background-image={cover ? `url("${cover}")` : null}></div>
|
||||
@@ -151,7 +167,8 @@
|
||||
<button class="iconbtn" onclick={more} aria-label="More"><Icon name="more" size={22} /></button>
|
||||
</div>
|
||||
|
||||
<div class="np-art" class:small onclick={() => openViewer(player.album?.art_path)} role="presentation">
|
||||
<div class="np-art" class:small onclick={() => openViewer(player.album?.art_path)}
|
||||
role="button" tabindex="0" onkeydown={() => {}} aria-label={"View album artwork" + (player.album?.title ? " — " + player.album.title : "")}>
|
||||
<div class="np-frame" bind:this={frameEl}><Cover path={player.album.art_path} eager size={64} /></div>
|
||||
</div>
|
||||
|
||||
@@ -247,9 +264,9 @@
|
||||
</div>
|
||||
|
||||
<div class="np-foot">
|
||||
<div class="np-faces">
|
||||
<div class="np-faces" role="tablist" aria-label="Player faces">
|
||||
{#each [["art", "disc", "Art"], ["lyrics", "lyrics", "Lyrics"], ["queue", "playlist", "Queue"]] as [f, ic, lb] (f)}
|
||||
<button class="fbtn" class:on={face === f} onclick={() => (nav.face = f)}><Icon name={ic} size={17} /><span>{lb}</span></button>
|
||||
<button class="fbtn" class:on={face === f} onclick={() => (nav.face = f)} role="tab" aria-selected={face === f}><Icon name={ic} size={17} /><span>{lb}</span></button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
</script>
|
||||
|
||||
{#if nav.timing}
|
||||
<div class="syncroom">
|
||||
<div class="syncroom" role="dialog" aria-modal="true" aria-label="Tap in time">
|
||||
<div class="sy-head">
|
||||
<button class="iconbtn sy-close" onclick={close} aria-label={confirmDiscard ? "Discard the timing" : "Close"}>
|
||||
{#if confirmDiscard}<span class="sy-discard">Discard?</span>{:else}<Icon name="x" size={22} />{/if}
|
||||
|
||||
@@ -70,7 +70,8 @@
|
||||
</script>
|
||||
|
||||
{#if nav.viewer}
|
||||
<div class="viewer" onclick={tap} onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up} role="presentation">
|
||||
<div class="viewer" onclick={tap} onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}
|
||||
role="button" tabindex="0" onkeydown={(e) => { if (e.key === "Enter" || e.key === " ") closeViewer(); }} aria-label="Close artwork viewer">
|
||||
{#if src}<img src={src} alt="" draggable="false" style:transform={scale === 1 ? "" : `translate(${tx}px, ${ty}px) scale(${scale})`} />{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
{#if album}
|
||||
<div class="rec-head">
|
||||
<div class="rec-cover" onclick={() => openViewer(album.art_path)} role="presentation"><Cover path={album.art_path} eager size={48} /></div>
|
||||
<div class="rec-cover" onclick={() => openViewer(album.art_path)} role="button" tabindex="0" onkeydown={() => {}} aria-label={"View album artwork — " + album.title}><Cover path={album.art_path} eager size={48} /></div>
|
||||
<div class="rec-meta">
|
||||
<div class="rec-title">{album.title}</div>
|
||||
<button class="rec-artist" onclick={() => push("artist", { name: album.artist }, screenEl)}>{album.artist}</button>
|
||||
@@ -64,14 +64,14 @@
|
||||
{#if discs.length > 1}<div class="disc-head">Disc {d}</div>{/if}
|
||||
<div class="tracks">
|
||||
{#each list as t, i (t.id)}
|
||||
<button class="trk" class:playing={isPlaying(t)} onclick={() => playAt(t)} oncontextmenu={(e) => { e.preventDefault(); openHalf("track", { album, track: t }); }}>
|
||||
<button class="trk" class:playing={isPlaying(t)} onclick={() => playAt(t)} oncontextmenu={(e) => { e.preventDefault(); openHalf("track", { album, track: t }); }} aria-label={(t.track_no ?? i + 1) + ". " + t.title + (various && t.artist ? " · " + t.artist : "")}>
|
||||
<span class="n">{t.track_no ?? i + 1}</span>
|
||||
<span class="b">
|
||||
<span class="t">{t.title}</span>
|
||||
{#if various && t.artist}<span class="m">{t.artist}</span>{/if}
|
||||
</span>
|
||||
<span class="d">{mmss(t.duration_secs)}</span>
|
||||
<span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}}
|
||||
<span class="r" role="button" tabindex="0" aria-label={"More for " + t.title} onkeydown={() => {}}
|
||||
onclick={(e) => { e.stopPropagation(); openHalf("track", { album, track: t }); }}>
|
||||
<Icon name="more" size={17} />
|
||||
</span>
|
||||
|
||||
@@ -276,9 +276,9 @@
|
||||
{#if tabs.length}
|
||||
<div class="sec"><h2>Discography <em>· {releasedCount} on the catalog</em></h2></div>
|
||||
{#if tabs.length > 1}
|
||||
<div class="segtabs disco-tabs">
|
||||
<div class="segtabs disco-tabs" role="tablist" aria-label="Discography">
|
||||
{#each tabs as t (t.key)}
|
||||
<button class:on={tab === t.key} onclick={() => (tab = t.key)}>{t.label}<span class="count">{t.items.length}</span></button>
|
||||
<button class:on={tab === t.key} onclick={() => (tab = t.key)} role="tab" aria-selected={tab === t.key} aria-label={t.label + ", " + t.items.length}>{t.label}<span class="count" aria-hidden="true">{t.items.length}</span></button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -343,7 +343,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if extras.bio}
|
||||
<p class="bio" class:open={bioOpen} onclick={() => (bioOpen = !bioOpen)} role="button" tabindex="0" onkeydown={() => {}}>{extras.bio}</p>
|
||||
<p class="bio" class:open={bioOpen} onclick={() => (bioOpen = !bioOpen)} role="button" tabindex="0" aria-expanded={bioOpen} onkeydown={() => {}}>{extras.bio}</p>
|
||||
{/if}
|
||||
{#if extras.profile?.tags?.length}
|
||||
<div class="tags">{#each extras.profile.tags.slice(0, 6) as t (t)}<span class="chip quiet">{t}</span>{/each}</div>
|
||||
|
||||
@@ -206,7 +206,7 @@
|
||||
<div class="about">
|
||||
{#if desc.lede}<p class="lede">{desc.lede}</p>{/if}
|
||||
{#if desc.rest}
|
||||
<p class="bio" class:open={descOpen} onclick={() => (descOpen = !descOpen)} role="button" tabindex="0" onkeydown={() => {}}>{desc.rest}</p>
|
||||
<p class="bio" class:open={descOpen} onclick={() => (descOpen = !descOpen)} role="button" tabindex="0" aria-expanded={descOpen} onkeydown={() => {}}>{desc.rest}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -107,9 +107,9 @@
|
||||
<div class="signin">
|
||||
<div class="signin-t">Sign in to Soulseek</div>
|
||||
<div class="signin-m">The network's rooms, its people and, next, its music. Your account is kept on this phone only.</div>
|
||||
<input class="field" bind:value={user} placeholder="Username" autocomplete="username" autocapitalize="off" spellcheck="false" enterkeyhint="next" />
|
||||
<input class="field" bind:value={user} placeholder="Username" aria-label="Soulseek username" autocomplete="username" autocapitalize="off" spellcheck="false" enterkeyhint="next" />
|
||||
<div class="field-row">
|
||||
<input class="field" type={showPass ? "text" : "password"} bind:value={pass} placeholder="Password" autocomplete="current-password" enterkeyhint="go" onkeydown={(e) => { if (e.key === "Enter") signIn(); }} />
|
||||
<input class="field" type={showPass ? "text" : "password"} bind:value={pass} placeholder="Password" aria-label="Soulseek password" autocomplete="current-password" enterkeyhint="go" onkeydown={(e) => { if (e.key === "Enter") signIn(); }} />
|
||||
<button class="iconbtn sm muted" onclick={() => (showPass = !showPass)} aria-label="Show password"><Icon name="eye" size={18} /></button>
|
||||
</div>
|
||||
{#if slsk.loginError}<div class="signin-err">{slsk.loginError}</div>{/if}
|
||||
@@ -126,10 +126,10 @@
|
||||
They live on the private layer; the network side is here first.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="segtabs" style="padding-top:12px">
|
||||
<div class="segtabs" style="padding-top:12px" role="tablist" aria-label="Community sections">
|
||||
{#each [["rooms", "Rooms"], ["chat", "Chat"], ["buddies", "Buddies"], ["taste", "Taste"]] as [k, l] (k)}
|
||||
<button class:on={tab === k} onclick={() => patch({ tab: k })}>
|
||||
{l}{#if k === "chat" && unreadTotal()} <span class="count">{unreadTotal()}</span>{/if}
|
||||
<button class:on={tab === k} onclick={() => patch({ tab: k })} role="tab" aria-selected={tab === k} aria-label={l + (k === "chat" && unreadTotal() ? ", " + unreadTotal() + " unread" : "")}>
|
||||
{l}{#if k === "chat" && unreadTotal()} <span class="count" aria-hidden="true">{unreadTotal()}</span>{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
<span class="r"><Icon name="chevron-right" size={17} /></span>
|
||||
</button>
|
||||
{#if w.unresolved_count > 0}
|
||||
<button class="row" onclick={() => (importer.includeUnresolved = !importer.includeUnresolved)}>
|
||||
<button class="row" onclick={() => (importer.includeUnresolved = !importer.includeUnresolved)} role="switch" aria-checked={!!importer.includeUnresolved}>
|
||||
<span class="lead"><Icon name="alert" size={20} /></span>
|
||||
<span class="b"><span class="t">Keep unresolved entries</span><span class="m">as placeholders, to repair on the desktop</span></span>
|
||||
<span class="sw" class:on={importer.includeUnresolved}><span></span></span>
|
||||
|
||||
@@ -102,16 +102,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="segtabs">
|
||||
<div class="segtabs" role="tablist" aria-label="Library sections">
|
||||
{#each TABS as [k, l] (k)}
|
||||
<button class:on={tab === k} onclick={() => patch({ tab: k })}>{l}</button>
|
||||
<button class:on={tab === k} onclick={() => patch({ tab: k })} role="tab" aria-selected={tab === k}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if tab === "albums"}
|
||||
<div class="chips" style="padding-bottom:12px">
|
||||
{#each SORTS as [k, l] (k)}
|
||||
<button class="chip" class:on={sort === k} class:quiet={sort !== k} onclick={() => patch({ sort: k })}>{l}</button>
|
||||
<button class="chip" class:on={sort === k} class:quiet={sort !== k} aria-pressed={sort === k} onclick={() => patch({ sort: k })}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="countline">
|
||||
@@ -170,7 +170,7 @@
|
||||
<span class="r"><Icon name="chevron-right" size={17} /></span>
|
||||
</button>
|
||||
{#if count !== 0}
|
||||
<button class="gapline" onclick={() => toggleGaps(ar.name)}>
|
||||
<button class="gapline" onclick={() => toggleGaps(ar.name)} aria-expanded={!!gapOpen[key]}>
|
||||
{#if gapLoading[key]}Checking the catalogue…{:else if count != null}<b>{count}</b> more released{:else}See what's missing{/if}
|
||||
<span class="cta">{gapOpen[key] ? "hide" : "show"}<Icon name={gapOpen[key] ? "chevron-down" : "chevron-right"} size={12} /></span>
|
||||
</button>
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
{#if item.artist || item.album}<span class="m">{item.artist}{item.artist && item.album ? " — " : ""}{item.album ?? ""}</span>{/if}
|
||||
</span>
|
||||
<span class="d">{item.duration_secs != null ? mmss(item.duration_secs) : "–:––"}</span>
|
||||
<span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); sheet(item); }}><Icon name="more" size={17} /></span>
|
||||
<span class="r" role="button" tabindex="0" aria-label={"More for " + item.title} onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); sheet(item); }}><Icon name="more" size={17} /></span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
{#if room}
|
||||
<div class="room-head">
|
||||
<span class="b"><span class="t-sub">{room.users.length} here now</span></span>
|
||||
<button class="btn btn-ghost" style="height:32px;padding:0 12px" onclick={() => openHalf("roomusers", { name })}>{room.users.length} <Icon name="users" size={14} /></button>
|
||||
<button class="btn btn-ghost" style="height:32px;padding:0 12px" onclick={() => openHalf("roomusers", { name })} aria-label={room.users.length + " people in this room"}>{room.users.length} <Icon name="users" size={14} /></button>
|
||||
</div>
|
||||
<div class="chat">
|
||||
{#if !lines.length}
|
||||
@@ -60,7 +60,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
<div class="composer">
|
||||
<input class="field" bind:this={inputEl} bind:value={text} placeholder="Message #{name}…" autocomplete="off" enterkeyhint="send" onkeydown={(e) => { if (e.key === "Enter") { e.preventDefault(); send(); } }} />
|
||||
<input class="field" bind:this={inputEl} bind:value={text} placeholder="Message #{name}…" aria-label={"Message #" + name} autocomplete="off" enterkeyhint="send" onkeydown={(e) => { if (e.key === "Enter") { e.preventDefault(); send(); } }} />
|
||||
<button class="disc sm" onclick={send} aria-label="Send" disabled={!text.trim()}><Icon name="send" size={16} /></button>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
@@ -414,7 +414,7 @@
|
||||
<span class="n"><Icon name="play" size={12} /></span>
|
||||
<span class="b"><span class="t">{row.title}</span><span class="m">{row.artist} · {row.album}</span></span>
|
||||
<span class="d">{mmss(row.duration_secs)}</span>
|
||||
<span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); moreHit(row); }}><Icon name="more" size={17} /></span>
|
||||
<span class="r" role="button" tabindex="0" aria-label={"More for " + row.title} onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); moreHit(row); }}><Icon name="more" size={17} /></span>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
@@ -432,9 +432,9 @@
|
||||
{#if net.error}<div class="sec-note">{net.error}</div>{/if}
|
||||
{#if net.groups.length}
|
||||
<div class="chips netfilters" style="padding-bottom: 10px">
|
||||
<button class="chip" class:on={netFree} class:quiet={!netFree} onclick={() => (netFree = !netFree)}>Free slot</button>
|
||||
<button class="chip" class:on={netFree} class:quiet={!netFree} aria-pressed={netFree} onclick={() => (netFree = !netFree)}>Free slot</button>
|
||||
{#each [["any", "Any format"], ["flac", "FLAC"], ["mp3320", "MP3 320+"]] as [v, l] (v)}
|
||||
<button class="chip" class:on={netFmt === v} class:quiet={netFmt !== v} onclick={() => (netFmt = v)}>{l}</button>
|
||||
<button class="chip" class:on={netFmt === v} class:quiet={netFmt !== v} aria-pressed={netFmt === v} onclick={() => (netFmt = v)}>{l}</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if shownGroups.length !== net.groups.length}<div class="sec-note">{shownGroups.length} of {net.groups.length} folders</div>{/if}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
<div class="composer">
|
||||
<input class="field" bind:this={inputEl} bind:value={text} placeholder="Message {display(username)}…" autocomplete="off" enterkeyhint="send" onkeydown={(e) => { if (e.key === "Enter") { e.preventDefault(); send(); } }} />
|
||||
<input class="field" bind:this={inputEl} bind:value={text} placeholder="Message {display(username)}…" aria-label={"Message " + display(username)} autocomplete="off" enterkeyhint="send" onkeydown={(e) => { if (e.key === "Enter") { e.preventDefault(); send(); } }} />
|
||||
<button class="disc sm" onclick={send} aria-label="Send" disabled={!text.trim()}><Icon name="send" size={16} /></button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<span class="n">{i + 1}</span>
|
||||
<span class="b"><span class="t">{r.title}</span><span class="m">{r.artist} · {r.album}</span></span>
|
||||
<span class="d">{mmss(r.duration_secs)}</span>
|
||||
<span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); more(r); }}><Icon name="more" size={17} /></span>
|
||||
<span class="r" role="button" tabindex="0" aria-label={"More for " + r.title} onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); more(r); }}><Icon name="more" size={17} /></span>
|
||||
</button>
|
||||
{/each}
|
||||
{#if index && !list.length}
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
<div class="sheet-sec" style="padding-top: 4px">Appearance</div>
|
||||
<div class="group">
|
||||
{#each ROWS as [k, icon, t, m] (k)}
|
||||
<button class="row" onclick={() => setSetting(k, !settings[k])}>
|
||||
<button class="row" onclick={() => setSetting(k, !settings[k])} role="switch" aria-checked={!!settings[k]}>
|
||||
<span class="lead"><Icon name={icon} size={20} /></span>
|
||||
<span class="b"><span class="t">{t}</span><span class="m">{m}</span></span>
|
||||
<span class="sw" class:on={settings[k]}><span></span></span>
|
||||
@@ -188,7 +188,7 @@
|
||||
|
||||
<div class="sheet-sec">Playback</div>
|
||||
<div class="group">
|
||||
<button class="row" onclick={toggleNormalization}>
|
||||
<button class="row" onclick={toggleNormalization} role="switch" aria-checked={normalization}>
|
||||
<span class="lead"><Icon name="volume" size={20} /></span>
|
||||
<span class="b"><span class="t">Volume normalization</span><span class="m">every track at about the same loudness (−14 LUFS); a track is measured on its first play and matched from then on</span></span>
|
||||
<span class="sw" class:on={normalization}><span></span></span>
|
||||
@@ -232,13 +232,13 @@
|
||||
<div class="pref">
|
||||
<div class="pref-l"><span class="t">Keep streamed tracks</span><span class="m">how long the cache holds a track after its last play</span></div>
|
||||
<div class="chips">
|
||||
{#each RETENTION as [v, l] (v)}<button class="chip" class:on={retention === v} class:quiet={retention !== v} onclick={() => setRetention(v)}>{l}</button>{/each}
|
||||
{#each RETENTION as [v, l] (v)}<button class="chip" class:on={retention === v} class:quiet={retention !== v} aria-pressed={retention === v} onclick={() => setRetention(v)}>{l}</button>{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pref">
|
||||
<div class="pref-l"><span class="t">Cache size limit</span><span class="m">the oldest streamed tracks go first past this size</span></div>
|
||||
<div class="chips">
|
||||
{#each CACHE as [v, l] (v)}<button class="chip" class:on={cacheMax === v} class:quiet={cacheMax !== v} onclick={() => setCacheMax(v)}>{l}</button>{/each}
|
||||
{#each CACHE as [v, l] (v)}<button class="chip" class:on={cacheMax === v} class:quiet={cacheMax !== v} aria-pressed={cacheMax === v} onclick={() => setCacheMax(v)}>{l}</button>{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,19 +251,19 @@
|
||||
<div class="pref">
|
||||
<div class="pref-l"><span class="t">Maximum</span><span class="m">lossy only avoids huge lossless transfers</span></div>
|
||||
<div class="chips">
|
||||
{#each MAX_Q as [v, l] (v)}<button class="chip" class:on={prefsOf(key).max === v} class:quiet={prefsOf(key).max !== v} onclick={() => setPref(key, "max", v)}>{l}</button>{/each}
|
||||
{#each MAX_Q as [v, l] (v)}<button class="chip" class:on={prefsOf(key).max === v} class:quiet={prefsOf(key).max !== v} aria-pressed={prefsOf(key).max === v} onclick={() => setPref(key, "max", v)}>{l}</button>{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pref">
|
||||
<div class="pref-l"><span class="t">Minimum</span><span class="m">files below the floor are skipped; a lossy file must report its bitrate</span></div>
|
||||
<div class="chips">
|
||||
{#each MIN_Q as [v, l] (v)}<button class="chip" class:on={prefsOf(key).min === v} class:quiet={prefsOf(key).min !== v} onclick={() => setPref(key, "min", v)}>{l}</button>{/each}
|
||||
{#each MIN_Q as [v, l] (v)}<button class="chip" class:on={prefsOf(key).min === v} class:quiet={prefsOf(key).min !== v} aria-pressed={prefsOf(key).min === v} onclick={() => setPref(key, "min", v)}>{l}</button>{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pref">
|
||||
<div class="pref-l"><span class="t">Priority</span><span class="m">among the files inside that window</span></div>
|
||||
<div class="chips">
|
||||
{#each PRI as [v, l] (v)}<button class="chip" class:on={prefsOf(key).priority === v} class:quiet={prefsOf(key).priority !== v} onclick={() => setPref(key, "priority", v)}>{l}</button>{/each}
|
||||
{#each PRI as [v, l] (v)}<button class="chip" class:on={prefsOf(key).priority === v} class:quiet={prefsOf(key).priority !== v} aria-pressed={prefsOf(key).priority === v} onclick={() => setPref(key, "priority", v)}>{l}</button>{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -271,7 +271,7 @@
|
||||
|
||||
<div class="sheet-sec">Downloads</div>
|
||||
<div class="group">
|
||||
<button class="row" onclick={() => setSetting("wifiOnly", !settings.wifiOnly)}>
|
||||
<button class="row" onclick={() => setSetting("wifiOnly", !settings.wifiOnly)} role="switch" aria-checked={!!settings.wifiOnly}>
|
||||
<span class="lead"><Icon name="globe" size={20} /></span>
|
||||
<span class="b"><span class="t">Downloads on Wi-Fi only</span><span class="m">on mobile data, Save waits; streaming is never held back</span></span>
|
||||
<span class="sw" class:on={settings.wifiOnly}><span></span></span>
|
||||
@@ -314,7 +314,7 @@
|
||||
|
||||
<div class="sheet-sec">Lyrics</div>
|
||||
<div class="group">
|
||||
<button class="row" onclick={() => setFlag("wordTiming", !cfg?.word_timing)}>
|
||||
<button class="row" onclick={() => setFlag("wordTiming", !cfg?.word_timing)} role="switch" aria-checked={!!cfg?.word_timing}>
|
||||
<span class="lead"><Icon name="lyrics" size={20} /></span>
|
||||
<span class="b"><span class="t">Word-by-word fill</span><span class="m">each word fills as it is sung; off keeps the line-by-line sync everywhere</span></span>
|
||||
<span class="sw" class:on={!!cfg?.word_timing}><span></span></span>
|
||||
@@ -326,16 +326,16 @@
|
||||
<div class="pref">
|
||||
<div class="pref-l"><span class="t">Art inside downloaded files</span><span class="m">the canonical cover, or whatever the uploader embedded</span></div>
|
||||
<div class="chips">
|
||||
{#each ART_MODE as [v, l] (v)}<button class="chip" class:on={artMode === v} class:quiet={artMode !== v} onclick={() => setFlag("replaceEmbeddedArt", v === "replace")}>{l}</button>{/each}
|
||||
{#each ART_MODE as [v, l] (v)}<button class="chip" class:on={artMode === v} class:quiet={artMode !== v} aria-pressed={artMode === v} onclick={() => setFlag("replaceEmbeddedArt", v === "replace")}>{l}</button>{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pref">
|
||||
<div class="pref-l"><span class="t">Embedded cover size</span><span class="m">the longest edge of the copy written into each file; the original is kept in the folder</span></div>
|
||||
<div class="chips">
|
||||
{#each ART_EDGE as [v, l] (v)}<button class="chip" class:on={artEdge === v} class:quiet={artEdge !== v} onclick={() => setNumber("embedArtMaxEdge", Number(v))}>{l}</button>{/each}
|
||||
{#each ART_EDGE as [v, l] (v)}<button class="chip" class:on={artEdge === v} class:quiet={artEdge !== v} aria-pressed={artEdge === v} onclick={() => setNumber("embedArtMaxEdge", Number(v))}>{l}</button>{/each}
|
||||
</div>
|
||||
</div>
|
||||
<button class="row" onclick={() => setFlag("writeFolderCover", !(cfg?.write_folder_cover ?? true))}>
|
||||
<button class="row" onclick={() => setFlag("writeFolderCover", !(cfg?.write_folder_cover ?? true))} role="switch" aria-checked={!!(cfg?.write_folder_cover ?? true)}>
|
||||
<span class="lead"><Icon name="image" size={20} /></span>
|
||||
<span class="b"><span class="t">Album folder cover</span><span class="m">cover.jpg and folder.jpg beside the files, so other players show the same art</span></span>
|
||||
<span class="sw" class:on={cfg?.write_folder_cover ?? true}><span></span></span>
|
||||
|
||||
@@ -1507,3 +1507,9 @@ html[data-motion="off"] *, html[data-motion="off"] *::before, html[data-motion="
|
||||
/* 76% of 411 is 312; a half sheet may take all but a strip */
|
||||
.halfsheet { max-height: calc(100% - 36px); }
|
||||
}
|
||||
|
||||
/* The one live region: never drawn, never laid out, always there — so an
|
||||
announcement is a change of text inside it rather than a node arriving
|
||||
with its words already in it (PreviewOverlay.svelte's .sr-announce, at
|
||||
phone scale) */
|
||||
.sr-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
||||
+119
-19
@@ -68,6 +68,60 @@ window.__settle = function () {
|
||||
/* what the page shows, read back after every still */
|
||||
const PROBE = `(function () {
|
||||
var e = document.querySelector('.screen');
|
||||
/* ── what a screen reader would be handed ────────────────────────────
|
||||
'exposed' is the tree TalkBack actually walks: nothing under
|
||||
aria-hidden or inert, nothing display:none or zero-boxed. 'aname' is
|
||||
the name it would announce — aria-label, aria-labelledby, then the
|
||||
visible text with the aria-hidden subtrees left out (an Icon is
|
||||
aria-hidden, so an icon-only control computes to nothing at all). A
|
||||
PLACEHOLDER is deliberately not a name here: it goes away the moment
|
||||
the field has text in it. Every backslash in this block is doubled —
|
||||
it lives inside a template literal. */
|
||||
var A11Y_SEL = 'button, [role=button], a, input, select, textarea, [role=tab], [role=switch], [role=slider], [role=checkbox]';
|
||||
function exposed(el) {
|
||||
if (el.closest('[aria-hidden="true"], [inert]')) return false;
|
||||
if (!el.getClientRects().length) return false;
|
||||
var st = getComputedStyle(el);
|
||||
return st.visibility !== 'hidden' && st.display !== 'none';
|
||||
}
|
||||
function vistext(el) {
|
||||
var out = '';
|
||||
for (var i = 0; i < el.childNodes.length; i++) {
|
||||
var n = el.childNodes[i];
|
||||
if (n.nodeType === 3) { out += n.nodeValue; continue; }
|
||||
if (n.nodeType !== 1) continue;
|
||||
if (n.getAttribute('aria-hidden') === 'true') continue;
|
||||
if (n.tagName === 'IMG') { out += ' ' + (n.getAttribute('alt') || ''); continue; }
|
||||
out += ' ' + vistext(n);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function aname(el) {
|
||||
var l = el.getAttribute('aria-label');
|
||||
if (l && l.trim()) return l.trim();
|
||||
var by = el.getAttribute('aria-labelledby');
|
||||
if (by) {
|
||||
var s = '';
|
||||
by.split(' ').forEach(function (id) { var n = document.getElementById(id); if (n) s += ' ' + vistext(n); });
|
||||
if (s.trim()) return s.replace(/\\s+/g, ' ').trim();
|
||||
}
|
||||
var t = el.tagName;
|
||||
if (t === 'INPUT' || t === 'TEXTAREA' || t === 'SELECT') {
|
||||
if (el.id) { var lab = document.querySelector('label[for="' + el.id + '"]'); if (lab && lab.textContent.trim()) return lab.textContent.replace(/\\s+/g, ' ').trim(); }
|
||||
return (el.getAttribute('title') || '').trim();
|
||||
}
|
||||
return vistext(el).replace(/\\s+/g, ' ').trim() || (el.getAttribute('title') || '').trim();
|
||||
}
|
||||
function whatis(el) { return (el.tagName.toLowerCase() + '.' + String(el.className || '').split(' ').slice(0, 2).join('.')).slice(0, 36); }
|
||||
function junk(el, nm) {
|
||||
if (/^[0-9 .,:;+·—–-]+$/.test(nm)) return 'digits';
|
||||
if (/\\.(png|jpe?g|webp|gif|svg|mp3|flac|m4a|ogg|wav)$/i.test(nm)) return 'file';
|
||||
if (/^[⋯…×✕✓→←]$/.test(nm)) return 'glyph';
|
||||
if (nm.length > 200 && !el.hasAttribute('aria-expanded')) return 'essay';
|
||||
return '';
|
||||
}
|
||||
var a11yAll = [].filter.call(document.querySelectorAll(A11Y_SEL), exposed);
|
||||
var a11yNone = a11yAll.filter(function (el) { return !aname(el); });
|
||||
/* geometry, for the orientation steps: box() is the rect or null, and
|
||||
inside() asks whether every pixel of it is within the viewport —
|
||||
which, sideways, is the whole question */
|
||||
@@ -188,6 +242,52 @@ const PROBE = `(function () {
|
||||
halfSub: ((document.querySelector('.halfsheet .sheet-head .m') || {}).textContent || '').trim(),
|
||||
halfSw: [].filter.call(document.querySelectorAll('.halfsheet .row'), function (r) { return !!r.querySelector('.sw.on'); }).map(function (r) { return ((r.querySelector('.t') || {}).textContent || '').trim(); }).join('|'),
|
||||
contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),
|
||||
/* the number this round exists to hold at zero, and who broke it */
|
||||
a11yAnon: a11yNone.length,
|
||||
a11yWho: a11yNone.slice(0, 3).map(whatis).join(' | '),
|
||||
a11yBad: a11yAll.map(function (el) { var nm = aname(el); if (!nm) return ''; var j = junk(el, nm); return j ? j + ':' + whatis(el) + ':' + nm.slice(0, 22) : ''; }).filter(Boolean).slice(0, 3).join(' | '),
|
||||
/* a layer that is off-screen but still in the tree: the closed
|
||||
sheets, the hidden mini bar, the nav under a sheet */
|
||||
a11yGhost: (function () {
|
||||
var n = 0;
|
||||
[].forEach.call(document.querySelectorAll('.sheet:not(.up), .halfsheet:not(.up), .mini.hidden, .nav.gone'), function (h) {
|
||||
if (h.matches(A11Y_SEL) && exposed(h)) n++;
|
||||
n += [].filter.call(h.querySelectorAll(A11Y_SEL), exposed).length;
|
||||
});
|
||||
return n;
|
||||
})(),
|
||||
/* a control that LOOKS chosen and does not say so */
|
||||
a11yState: (function () {
|
||||
var bad = [];
|
||||
[].forEach.call(document.querySelectorAll('.row .sw'), function (sw) {
|
||||
var b = sw.closest('button, [role=button]');
|
||||
if (!b || !exposed(b)) return;
|
||||
var want = sw.classList.contains('on') ? 'true' : 'false';
|
||||
if (b.getAttribute('role') !== 'switch' || b.getAttribute('aria-checked') !== want) bad.push('sw:' + aname(b).slice(0, 18));
|
||||
});
|
||||
[].forEach.call(document.querySelectorAll('.segtabs button, .np-faces .fbtn, .nav button'), function (b) {
|
||||
if (!exposed(b)) return;
|
||||
var want = b.classList.contains('on') ? 'true' : 'false';
|
||||
if (b.getAttribute('role') !== 'tab' || b.getAttribute('aria-selected') !== want) bad.push('tab:' + aname(b).slice(0, 18));
|
||||
});
|
||||
[].forEach.call(document.querySelectorAll('button.chip'), function (b) {
|
||||
if (!exposed(b) || b.classList.contains('tchip')) return;
|
||||
if (!b.hasAttribute('aria-pressed')) { if (b.classList.contains('on')) bad.push('chip:' + aname(b).slice(0, 18)); return; }
|
||||
if (b.getAttribute('aria-pressed') !== (b.classList.contains('on') ? 'true' : 'false')) bad.push('chip:' + aname(b).slice(0, 18));
|
||||
});
|
||||
return bad.slice(0, 3).join(' | ');
|
||||
})(),
|
||||
/* the layer on top: its role, whether it is modal, its name, and
|
||||
whether the focus went with it */
|
||||
dlg: (function () {
|
||||
var d = document.querySelector('.viewer') || document.querySelector('.syncroom')
|
||||
|| document.querySelector('.halfsheet.up') || document.querySelector('.sheet.up');
|
||||
if (!d) return '';
|
||||
var nm = (d.getAttribute('aria-label') || '').trim();
|
||||
return (d.getAttribute('role') || '-') + '/' + (d.getAttribute('aria-modal') || '-') + '/' + nm.slice(0, 34) + '/' + (d.contains(document.activeElement) ? 'focus' : 'nofocus');
|
||||
})(),
|
||||
navTabs: [].map.call(document.querySelectorAll('.nav button'), function (b) { return aname(b) + (b.getAttribute('aria-selected') === 'true' ? '*' : ''); }).join('|'),
|
||||
announce: ((document.querySelector('[data-live]') || {}).textContent || '').trim(),
|
||||
vp: innerWidth + 'x' + innerHeight,
|
||||
/* the sheet's controls: any one of them off the viewport is the landscape bug */
|
||||
npOut: [].filter.call(document.querySelectorAll('.sheet.up .np-frame, .sheet.up .np-meta, .sheet.up .np-scrub, .sheet.up .np-transport, .sheet.up .np-foot'), function (x) { var b = x.getBoundingClientRect(); return b.height > 0 && !inside(b); }).length,
|
||||
@@ -617,14 +717,14 @@ const MOCK_EXPECT = {
|
||||
"buddy-petname": (i) => i.budNames === "Zola from Vinyl Rips|cy" && /ada/.test(i.budSubs) && !i.toast,
|
||||
"room-users": (i) => i.half.includes("up") && /Zola from Vinyl Rips/.test(i.halfText) && /ada/.test(i.halfText) && /bo/.test(i.halfText),
|
||||
"contact-cleared": (i) => !i.half.includes("up") && i.budNames === "ada|cy" && !i.toast,
|
||||
taste: (i) => i.likes === "Daft Punk|french house|Warp Records" && i.hates === "loudness war" && i.similar === 4,
|
||||
taste: (i) => i.likes === "Daft Punk|french house|Warp Records" && i.hates === "loudness war" && i.similar === 4 && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,
|
||||
"taste-declared": (i) => /Boards of Canada/.test(i.likes) && i.similar === 5,
|
||||
"taste-hated": (i) => i.hates === "loudness war|loudness war 2",
|
||||
"taste-dropped": (i) => !/Boards of Canada/.test(i.likes) && i.similar === 4 && !i.toast,
|
||||
"taste-peer": (i) => i.half.includes("up") && /hifi_hank/.test(i.halfText),
|
||||
"home-more": (i) => i.half.includes("up"),
|
||||
you: (i) => i.title.startsWith("You") && i.norm === "on" && i.folderRow && i.srcMax === "No limit",
|
||||
"you-norm-off": (i) => i.norm === "off",
|
||||
you: (i) => i.title.startsWith("You") && i.norm === "on" && i.folderRow && i.srcMax === "No limit" && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,
|
||||
"you-norm-off": (i) => i.norm === "off" && i.a11yState === "",
|
||||
"you-norm-on": (i) => i.norm === "on",
|
||||
"you-lossy": (i) => i.srcMax === "Lossy only",
|
||||
"you-keep": (i) => i.keep === "1 week",
|
||||
@@ -665,15 +765,15 @@ const MOCK_EXPECT = {
|
||||
"landscape-artist": (i) => i.heroH > 0 && i.heroH <= 200 && i.actsIn,
|
||||
"landscape-insets": (i) => i.miniLeft >= 34 && i.screenPadL >= 26,
|
||||
"portrait-again": (i) => i.vp === "411x914" && i.sheet.includes("up") && i.npCols === "stacked" && i.npOut === 0 && i.chromePct <= 24,
|
||||
home: (i) => /^(Good |Still)/.test(i.title),
|
||||
home: (i) => /^(Good |Still)/.test(i.title) && /^Home\*\|Search\|Library\|Community(, \d+ unread)?\|You(, \d+ transfers?)?$/.test(i.navTabs) && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,
|
||||
"hold-sheet": (i) => i.half.includes("up") && /Re-fetch the cover/.test(i.halfText),
|
||||
"cover-refetched": (i) => !i.half.includes("up") && /Cover re-fetched/.test(i.notice),
|
||||
"search-empty": (i) => i.title.startsWith("Search") && !i.half.includes("up"),
|
||||
"search-dym": (i) => i.dym === "shpongle",
|
||||
"search-dym-pick": (i) => i.sq === "shpongle" && !i.dym,
|
||||
search: (i) => i.title.startsWith("Search"),
|
||||
library: (i) => i.title.startsWith("Your library") && i.fmtBadges >= 10 && /hours/.test(i.countline),
|
||||
album: (i) => i.screen.includes("tr-push") && !i.mini,
|
||||
library: (i) => i.title.startsWith("Your library") && i.fmtBadges >= 10 && /hours/.test(i.countline) && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,
|
||||
album: (i) => i.screen.includes("tr-push") && !i.mini && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,
|
||||
lyrics: (i) => i.sheet.includes("up") && i.lyrLines >= 3,
|
||||
"lyrics-sheet": (i) => i.half.includes("up") && /Lyrics later/.test(i.halfText) && /Search again/.test(i.halfText),
|
||||
"lyrics-nudged": (i) => /\+0\.5 s/.test(i.sheetTitle),
|
||||
@@ -693,13 +793,13 @@ const MOCK_EXPECT = {
|
||||
"sync-saved-sheet": (i) => i.half.includes("up") && i.lyrAuto === "off",
|
||||
"lyrics-reduced": (i) => /instrumental\.wav$/.test(i.trackPath) && !i.toast,
|
||||
"lyrics-original": (i) => !!i.trackPath && !/instrumental\.wav$/.test(i.trackPath) && !i.toast,
|
||||
viewer: (i) => i.viewer,
|
||||
viewer: (i) => i.viewer && /^button\/-\/Close artwork viewer/.test(i.dlg) && !i.a11yAnon && !i.a11yBad && !i.a11yGhost,
|
||||
"viewer-closed": (i) => !i.viewer && i.sheet.includes("up"),
|
||||
"album-playing": (i) => i.mini,
|
||||
"track-sheet": (i) => i.half.includes("up"),
|
||||
sheet: (i) => i.sheet.includes("up"),
|
||||
"track-sheet": (i) => i.half.includes("up") && /^dialog\/true\/.+\/focus$/.test(i.dlg) && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,
|
||||
sheet: (i) => i.sheet.includes("up") && /^dialog\/true\/Now playing/.test(i.dlg) && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,
|
||||
"after-back": (i) => !i.sheet.includes("up"),
|
||||
artist: (i) => i.screen.includes("has-hero") && i.amb.includes("on:img") && /played/.test(i.played),
|
||||
artist: (i) => i.screen.includes("has-hero") && i.amb.includes("on:img") && /played/.test(i.played) && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,
|
||||
queue: (i) => { globalThis.__qrows = i.qrows; return i.sheet.includes("up") && i.qrows >= 2; },
|
||||
"queue-row-sheet": (i) => i.half.includes("up") && /Remove from queue/.test(i.halfText) && /Play now/.test(i.halfText),
|
||||
"queue-removed": (i) => !i.half.includes("up") && i.qrows === globalThis.__qrows - 1,
|
||||
@@ -709,7 +809,7 @@ const MOCK_EXPECT = {
|
||||
record leads), the news, the back catalogue, recommendations,
|
||||
complete the set, following — the desktop's page */
|
||||
"home-again": (i) => /^(Good |Still)/.test(i.title) && /^Coming/.test(i.feat) && i.news >= 1 && /New from your artists/.test(i.secs) && /From the back catalogue/.test(i.secs) && /Recommended for you/.test(i.secs) && /Following/.test(i.secs),
|
||||
room: (i) => i.screen.includes("tr-push") && i.msgs >= 2,
|
||||
room: (i) => i.screen.includes("tr-push") && i.msgs >= 2 && !i.a11yAnon && !i.a11yBad,
|
||||
"rooms-filtered": (i) => i.rooms === 1,
|
||||
"rooms-more": (i) => i.rooms >= 95,
|
||||
/* the artist with a picture shows it on the hero and its card; the
|
||||
@@ -746,12 +846,12 @@ const MOCK_EXPECT = {
|
||||
browse: (i) => i.screen.includes("tr-push") && i.brwDirs >= 2 && /folders/.test(i.brwHead),
|
||||
"browse-dir": (i) => i.brwFiles >= 8,
|
||||
"browse-play": (i) => i.playing.includes("Aerodynamic") && /streaming from/.test(i.live),
|
||||
"cat-album": (i) => i.screen.includes("tr-push") && i.title.startsWith("Discovery") && i.eds >= 2 && i.lede && i.bars >= 10,
|
||||
"cat-album": (i) => i.screen.includes("tr-push") && i.title.startsWith("Discovery") && i.eds >= 2 && i.lede && i.bars >= 10 && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,
|
||||
"cat-album-listeners": (i) => i.bars === 14 && i.barCounts === 14,
|
||||
"cat-album-sheet": (i) => i.half.includes("up") && /Save to library/.test(i.halfText) && /Add to playlist/.test(i.halfText) && /Go to artist/.test(i.halfText),
|
||||
"cat-track-sheet": (i) => i.half.includes("up") && /Save to library/.test(i.halfText),
|
||||
"cat-track-save": (i) => /Saving from /.test(i.halfText) || !i.half.includes("up"),
|
||||
"cat-album-save": (i) => /^Saving \d+ from /.test(i.save) && /^\d+$/.test(i.navBadges),
|
||||
"cat-album-save": (i) => /^Saving \d+ from /.test(i.save) && /^\d+$/.test(i.navBadges) && /\|You, \d+ transfers?$/.test(i.navTabs),
|
||||
transfers: (i) => i.title.startsWith("Transfers") && i.xfers >= 11,
|
||||
gaps: (i) => i.title.startsWith("Your library") && i.gaplines >= 1,
|
||||
"gaps-open": (i) => i.gapCards >= 3,
|
||||
@@ -763,7 +863,7 @@ const MOCK_EXPECT = {
|
||||
"playlist-stream": (i) => i.playing.includes("One More Time") && /streaming from/.test(i.live),
|
||||
"playlist-filter": (i) => i.plrows === 1,
|
||||
"playlist-row-sheet": (i) => i.half.includes("up") && /Remove from playlist/.test(i.halfText),
|
||||
"playlist-removed": (i) => i.plrows === 5 && /Removed/.test(i.notice),
|
||||
"playlist-removed": (i) => i.plrows === 5 && /Removed/.test(i.notice) && /Removed/.test(i.announce) && /Undo/.test(i.announce),
|
||||
"playlist-undone": (i) => i.plrows === 6 && !i.notice,
|
||||
"playlist-moved-up": (i) => i.plfirst.startsWith("Track 2"),
|
||||
"playlist-moved-down": (i) => i.plfirst.startsWith("Track 1"),
|
||||
@@ -780,7 +880,7 @@ const MOCK_EXPECT = {
|
||||
"playlist-rename": (i) => i.half.includes("up") && i.promptVal === "Late night drive",
|
||||
"playlist-renamed": (i) => i.title.startsWith("Late night drive II"),
|
||||
"playlists-again": (i) => i.plcards === 3 && !i.plpinned && /Late night drive II/.test(i.plnames),
|
||||
"playlist-new-prompt": (i) => i.half.includes("up") && /^New Playlist \d+$/.test(i.promptVal),
|
||||
"playlist-new-prompt": (i) => i.half.includes("up") && /^New Playlist \d+$/.test(i.promptVal) && !i.a11yAnon,
|
||||
"playlist-new": (i) => i.screen.includes("tr-push") && /^New Playlist/.test(i.title) && i.plrows === 0,
|
||||
"playlist-pick": (i) => i.half.includes("up") && /New playlist/.test(i.halfText) && /Late night drive II/.test(i.halfText),
|
||||
"playlist-added": (i) => /Added \d+ tracks to New Playlist/.test(i.notice),
|
||||
@@ -788,7 +888,7 @@ const MOCK_EXPECT = {
|
||||
"playlist-delete-confirm": (i) => i.half.includes("up") && /Delete/.test(i.halfText),
|
||||
"playlist-deleted": (i) => i.title.startsWith("Your library") && i.plcards === 3 && !/New Playlist/.test(i.plnames),
|
||||
"import-sheet": (i) => i.half.includes("up") && /Playlist from a link/.test(i.halfText) && /Paste a tracklist/.test(i.halfText) && /Folder of audio files/.test(i.halfText),
|
||||
"import-paste-prompt": (i) => i.half.includes("up") && /Paste a tracklist/.test(i.halfText) && i.promptVal === "",
|
||||
"import-paste-prompt": (i) => i.half.includes("up") && /Paste a tracklist/.test(i.halfText) && i.promptVal === "" && !i.a11yAnon,
|
||||
"import-review": (i) => i.title.startsWith("Import") && i.imp === 3 && /1 in library/.test(i.impCounts) && /tags/.test(i.impTags) && /unresolved/.test(i.impTags) && /catalog/.test(i.impTags) && i.keepOn && /Create playlist \(3 tracks\)/.test(i.impBtn),
|
||||
"import-keep-off": (i) => !i.keepOn && /Create playlist \(2 tracks\)/.test(i.impBtn),
|
||||
"import-created": (i) => i.title.startsWith("Imported playlist") && i.plrows === 2,
|
||||
@@ -951,7 +1051,7 @@ const PHONE_STEPS = [
|
||||
{ do: `document.querySelectorAll('.nav button')[0].click()`, name: "end", wait: 800 },
|
||||
];
|
||||
const PHONE_EXPECT = {
|
||||
album: (i) => i.screen.includes("tr-push") && i.amb.includes("on:img"),
|
||||
album: (i) => i.screen.includes("tr-push") && i.amb.includes("on:img") && !i.a11yAnon && !i.a11yGhost,
|
||||
"album-playing": (i) => i.mini && !i.toast,
|
||||
"lyrics-sheet": (i) => i.half.includes("up") && /Search again/.test(i.halfText),
|
||||
"lyrics-nudged": (i) => i.half.includes("up") && !i.toast,
|
||||
@@ -966,7 +1066,7 @@ const PHONE_EXPECT = {
|
||||
viewer: (i) => i.viewer,
|
||||
"viewer-closed": (i) => !i.viewer && i.sheet.includes("up"),
|
||||
"home-ambience": (i) => /^(Good |Still)/.test(i.title) && i.amb.includes("on:img"),
|
||||
sheet: (i) => i.sheet.includes("up"),
|
||||
sheet: (i) => i.sheet.includes("up") && /^dialog\/true\/Now playing/.test(i.dlg) && !i.a11yGhost,
|
||||
"after-back": (i) => !i.sheet.includes("up"),
|
||||
"after-reload": (i) => i.mini && !!i.playing && i.amb.includes("on:img") && !i.toast,
|
||||
search: (i) => i.title.startsWith("Search"),
|
||||
@@ -974,7 +1074,7 @@ const PHONE_EXPECT = {
|
||||
tracks: (i) => i.title.startsWith("Your library"),
|
||||
"contacts-read": (i) => i.contactsLoaded === "yes" && !i.toast,
|
||||
"artist-cold": (i) => i.screen.includes("has-hero"),
|
||||
you: (i) => i.title.startsWith("You") && !!i.norm && !!i.srcMax,
|
||||
you: (i) => i.title.startsWith("You") && !!i.norm && !!i.srcMax && !i.a11yAnon && !i.a11yGhost && !i.a11yState,
|
||||
"you-norm-off": (i) => i.norm === "off" && !i.toast,
|
||||
"you-norm-on": (i) => i.norm === "on" && !i.toast,
|
||||
"you-lossy": (i) => i.srcMax === "Lossy only" && !i.toast,
|
||||
|
||||
Reference in new issue
Block a user