diff --git a/src/mobile/Mobile.svelte b/src/mobile/Mobile.svelte index dac041d..efd6d61 100644 --- a/src/mobile/Mobile.svelte +++ b/src/mobile/Mobile.svelte @@ -16,6 +16,7 @@ import Sheet from "./Sheet.svelte"; import HalfSheet from "./HalfSheet.svelte"; import Viewer from "./Viewer.svelte"; + import { heroArt } from "./heroart.svelte.js"; import SyncRoom from "./SyncRoom.svelte"; import Home from "./screens/Home.svelte"; import Search from "./screens/Search.svelte"; @@ -121,6 +122,10 @@ reading. A pushed page has one because it carries the back chevron, and it is static — there and titled from the first frame. */ const showBar = $derived(!root); + /* A hero page paints its picture a second time behind the scroller + (heroart.svelte.js): the mask cuts the page where it meets the clock, + and that copy fills the cut back in — so the photograph passes under + the status bar and the name does not. The mask itself never moves. */ const playing = $derived(player.status === "playing" || player.status === "buffering"); const played = $derived(player.duration ? Math.min(1, player.position / player.duration) : 0); @@ -334,10 +339,18 @@
+ {#if meta.hero && heroArt.src} + + + {/if} {#key nav.seq}
{ scrolled = e.currentTarget.scrollTop; }} @@ -383,14 +396,11 @@ {/key}
- + {#if showBar} -
- - {meta.title}{#if meta.kicker && meta.kicker !== meta.title}{meta.kicker}{/if} - -
+ + {/if}
diff --git a/src/mobile/heroart.svelte.js b/src/mobile/heroart.svelte.js new file mode 100644 index 0000000..989b993 --- /dev/null +++ b/src/mobile/heroart.svelte.js @@ -0,0 +1,12 @@ +/* The artist hero's picture, published for the copy that lives BEHIND the + scroller. + + The page meets the Android status bar and is cut off there — but the rule is + that backgrounds may pass under the clock and words may not, and a mask on + the scroller cannot tell a photograph from a sentence: it takes both. So the + hero is painted twice. The page's own copy is inside the masked scroller and + is cut away in that band along with everything else; this one sits behind + the scroller, unmasked, and shows through exactly where the mask took the + first copy away. Same image, same box, same scale, offset by the same + scroll — so the two are one picture, and only the name dissolves. */ +export const heroArt = $state({ src: "" }); diff --git a/src/mobile/mobile.css b/src/mobile/mobile.css index 0d4fbe5..f75d0c0 100644 --- a/src/mobile/mobile.css +++ b/src/mobile/mobile.css @@ -62,6 +62,16 @@ --inset-left: env(safe-area-inset-left, 0px); --inset-right: env(safe-area-inset-right, 0px); --top-h: calc(var(--bar-h) + var(--inset-top)); + /* The floating chrome a pushed page carries: a 38px disc, 6px below the + status bar. Both the reserve at the top of a page and the fade are + measured from these, so none of the three can drift apart. */ + --chrome-d: 38px; + --chrome-gap: 6px; + /* How far the page takes to disappear where it meets the status bar. + It has to reach the FOOT of the chrome: any shallower and the chevron + and the ⋯ stand on crisp text, which is how the page's ⋯ came to sit + on a track row's own ⋯. Two extra pixels so it finishes just clear. */ + --fade-top: calc(var(--chrome-gap) + var(--chrome-d) + 2px); /* ── motion: real springs, sampled ──────────────────────────────────── Material 3 Expressive specifies motion as springs; CSS takes a sampled diff --git a/src/mobile/screens/Artist.svelte b/src/mobile/screens/Artist.svelte index da140f2..a671bda 100644 --- a/src/mobile/screens/Artist.svelte +++ b/src/mobile/screens/Artist.svelte @@ -17,6 +17,7 @@ import { openUrl } from "@tauri-apps/plugin-opener"; import { albumsBy, tracksOf, isFollowed, toggleFollow } from "../lib.svelte.js"; import { art, wantArt } from "../art.svelte.js"; + import { heroArt } from "../heroart.svelte.js"; import { player, playEntries, playAlbum } from "../player.svelte.js"; import { push, openHalf } from "../nav.svelte.js"; import { catalog, lookupArtist, artistKey, fans, yearOf, looseTitle, loadExtras, loadPopular, loadHeroSeed, loadOverview, picture } from "../catalog.svelte.js"; @@ -78,6 +79,8 @@ $effect(() => { const id = artistId; if (id != null) untrack(() => { loadPopular(id); loadHeroSeed(id); loadOverview(id); }); }); const seedSrc = $derived(seedPath ? art[seedPath] : ""); const heroSrc = $derived(heroUrl ? art[heroUrl] : ""); + /* the copy behind the scroller, which is the one allowed under the clock */ + $effect(() => { heroArt.src = heroSrc || seedSrc || ""; return () => { heroArt.src = ""; }; }); const extras = $derived(artistId != null ? catalog.extras[artistId] ?? null : null); const socials = $derived(page?.socials?.length ? page.socials : (extras?.socials ?? [])); diff --git a/src/mobile/shell.css b/src/mobile/shell.css index 9f53b96..57cdeb7 100644 --- a/src/mobile/shell.css +++ b/src/mobile/shell.css @@ -83,11 +83,17 @@ scrollbar-width: none; -webkit-overflow-scrolling: touch; padding-top: var(--inset-top); + /* The page is cut off at the status bar: it fades out into the ground + over --fade-top rather than sliding under the clock. Pinned to the + scroller's box, not to its content, so it never moves and costs no + layout — and at rest it hides nothing, because padding-top has + already put the first pixel below it. */ + -webkit-mask-image: linear-gradient(to bottom, transparent 0, transparent var(--inset-top), #000 calc(var(--inset-top) + var(--fade-top)), #000 100%); + mask-image: linear-gradient(to bottom, transparent 0, transparent var(--inset-top), #000 calc(var(--inset-top) + var(--fade-top)), #000 100%); } .screen::-webkit-scrollbar { width: 0; display: none; } /* a pushed screen whose content starts at the very top must clear the bar; heroes bleed behind it and the record head has its own clearance */ -.screen.under-bar { padding-top: var(--top-h); } /* the pad that clears the mini player + nav bar */ .endpad { height: calc(var(--nav-h) + var(--inset-bottom) + 24px); } .has-mini .endpad { height: calc(var(--mini-h) + var(--nav-h) + var(--inset-bottom) + 16px); } @@ -98,47 +104,28 @@ and no room for a hierarchy trail, so the trail collapses to a KICKER under the title, and the arrows to one chevron that only appears when there is somewhere to go back to. It runs up under the status bar. */ -.gbar { +/* The chrome a pushed page carries once the bar is gone: a back chevron and + the page's ⋯, floating over the artwork with nothing behind them but their + own disc. No backdrop blur — the bar's own note measured what three blurred + bands did to a scroll on the Pixel 6a, and these would be a fourth. */ +.fchrome { position: absolute; - top: 0; left: 0; right: 0; - z-index: 40; - height: var(--top-h); - padding: var(--inset-top) 8px 0 6px; - display: flex; - align-items: center; - gap: 10px; - /* SOLID by default on the phone. The desktop's glass — the 28% tint over - a 12px backdrop blur — was measured on the Pixel 6a (tests/mobile/ - perf.mjs): every backdrop blur re-blurs its band on every frame that - scrolls or animates under it, and the three of them (this bar, the mini - bar, the nav) turned a 60 Hz scroll into 30 Hz and dropped every other - frame of the player's opening. The blur is a switch (You → Appearance → - Glass) so the two can be compared; the design was built to stand on - the solid floor first. */ - background: rgba(18, 18, 22, 0.92); - box-shadow: 0 1px 0 rgba(255, 255, 255, 0.05); -} -html[data-glass="blur"] .gbar { background: rgba(20, 20, 24, 0.28); backdrop-filter: blur(12px) saturate(1.7); -webkit-backdrop-filter: blur(12px) saturate(1.7); } -html[data-material="solid"] .gbar { background: rgba(24, 24, 24, 0.94); backdrop-filter: none; -webkit-backdrop-filter: none; } -.gbar .gb-t { - font-size: 15px; - font-weight: 800; - letter-spacing: -0.2px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - min-width: 0; - flex: 1; - text-shadow: 0 1px 4px rgba(0, 0, 0, 0.45); -} -.gbar .gb-t small { - display: block; - font-size: 10.5px; - font-weight: 700; - color: rgba(255, 255, 255, 0.55); - letter-spacing: 0.2px; - margin-top: -1px; + top: calc(var(--inset-top) + var(--chrome-gap)); + z-index: 45; + width: var(--chrome-d); + height: var(--chrome-d); + padding: 0; + border: 0; + border-radius: var(--r-pill); + display: grid; + place-items: center; + color: #fff; + background: rgba(0, 0, 0, 0.46); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08), 0 2px 10px rgba(0, 0, 0, 0.35); } +.fchrome:active { background: rgba(0, 0, 0, 0.62); } +.fchrome.back { left: calc(10px + var(--inset-left)); } +.fchrome.more { right: calc(10px + var(--inset-right)); color: rgba(255, 255, 255, 0.82); } /* the round chrome button — the desktop bar's .arrow at touch size */ .iconbtn { @@ -568,8 +555,8 @@ html[data-material="solid"] .card, html[data-material="solid"] .group { backgrou /* ── the artist hero ────────────────────────────────────────────────────── The desktop hero is a wide banner beside the identity block. A phone has no beside, so the portrait goes full-bleed and the name sits ON it. */ -.hero { position: relative; height: 300px; display: flex; align-items: flex-end; overflow: hidden; margin-bottom: -6px; } -.hero-img { position: absolute; inset: 0; background-size: cover; background-position: center 22%; transform: scale(1.04); } +.hero { position: relative; height: 258px; display: flex; align-items: flex-end; overflow: hidden; margin-bottom: -6px; } +.hero-img { position: absolute; inset: 0; background-size: cover; background-position: center 24%; transform: scale(1.04); } /* the blurred SEED: the picture the library already has, soft and oversized, until a sharp one exists */ .hero-img.seed { filter: blur(16px) saturate(1.15); transform: scale(1.12); } @@ -579,6 +566,43 @@ html[data-material="solid"] .card, html[data-material="solid"] .group { backgrou from { opacity: 0; filter: blur(16px) saturate(1.15); transform: scale(1.12); } to { opacity: 1; filter: blur(0) saturate(1); transform: scale(1.04); } } +/* The hero's picture again, behind the scroller and outside its mask, so the + photograph is allowed under the clock while the words are not. Its geometry + has to match .hero/.hero-img exactly — 258px tall, the same 24% focal point, + the same 1.04 scale — or the seam shows. */ +.hero-behind { + position: absolute; + left: 0; + right: 0; + top: 0; + height: 258px; + z-index: 0; + pointer-events: none; + /* the clip .hero gives its own picture; without it the 1.04 below renders + 268px in a 258px box and five pixels of photograph appear under the + hero's foot, below the black the scrim fades to */ + overflow: hidden; + transform: translateY(var(--hero-shift, 0px)); +} +.hero-behind i { + position: absolute; + inset: 0; + display: block; + background-size: cover; + background-position: center 24%; + transform: scale(1.04); +} +/* the clock's own backing, pinned where the clock is rather than to the photo */ +.hero-wash { + position: absolute; + left: 0; + right: 0; + top: 0; + height: calc(var(--inset-top) + 26px); + z-index: 2; + pointer-events: none; + background: linear-gradient(to bottom, rgba(0, 0, 0, 0.47), transparent); +} .hero-scrim { position: absolute; inset: 0; @@ -1207,6 +1231,12 @@ html[data-material="solid"] .peer { background: var(--bg-elevated); box-shadow: .countline { display: flex; align-items: center; justify-content: space-between; padding: 0 var(--pad) 10px; } /* a hero bleeds up under the status bar; the scrim keeps the chrome legible */ .screen.has-hero { padding-top: 0; } +/* A pushed page starts BELOW the floating chevron and ⋯, or its first line is + under a button — which is what the bar's own padding used to prevent. Sized + to the chrome that is there now: the inset, its 6px of top margin, the 38px + disc, and 8px of air under it. A hero page is the exception and keeps none, + because its photograph is meant to be behind them. */ +.screen.has-chrome { padding-top: calc(var(--inset-top) + var(--chrome-gap) + var(--chrome-d) + 8px); } /* not a stray: `.lane` is width: 100%, and the mini bar's lane is ALSO positioned with both left: 16px and right: 16px — an over-constrained absolute box drops `right`, so without this it comes out 32px wider than @@ -1434,7 +1464,6 @@ html[data-motion="off"] *, html[data-motion="off"] *::before, html[data-motion=" left/right and .toast's left/right to win at equal specificity. */ /* the side insets: the cutout and the gesture pill, from the edges */ -.gbar { padding-left: calc(6px + var(--inset-left)); padding-right: calc(8px + var(--inset-right)); } .nav { padding-left: var(--inset-left); padding-right: var(--inset-right); } .mini { left: calc(8px + var(--inset-left)); right: calc(8px + var(--inset-right)); } .screen { padding-left: var(--inset-left); padding-right: var(--inset-right); } diff --git a/tests/mobile/smoke.mjs b/tests/mobile/smoke.mjs index 08b3768..bcd2da1 100644 --- a/tests/mobile/smoke.mjs +++ b/tests/mobile/smoke.mjs @@ -169,7 +169,7 @@ const PROBE = `(function () { function inside(b) { return !!b && b.height > 0 && b.top >= -1 && b.bottom <= innerHeight + 1 && b.left >= -1 && b.right <= innerWidth + 1; } return { screen: e ? e.className : '', - title: (document.querySelector('.gb-t') || {}).textContent || '', + title: (function () { var q = document.querySelector('.screen'); return q ? (q.dataset.title || '') + (q.dataset.kicker || '') : ''; })(), mini: !!document.querySelector('.mini'), sheet: (document.querySelector('.sheet') || {}).className || '', half: (document.querySelector('.halfsheet') || {}).className || '', @@ -465,7 +465,7 @@ const PROBE = `(function () { actsIn: inside(box('.screen .hero-acts') || box('.screen .rec-acts')), recCols: (function () { var c = box('.screen .rec-cover'), m = box('.screen .rec-meta'); return c && m ? (c.bottom <= m.top + 1 ? 'stacked' : 'beside') : ''; })(), /* the glass bar above plus the floating bars below, as a share of the screen */ - chromePct: (function () { var g = box('.gbar'), low = box('.mini') || box('.nav'); return Math.round((((g ? g.height : 0) + (low ? innerHeight - low.top : 0)) / innerHeight) * 100); })(), + chromePct: (function () { var g = box('.fchrome.back'), low = box('.mini') || box('.nav'); return Math.round((((g ? g.height : 0) + (low ? innerHeight - low.top : 0)) / innerHeight) * 100); })(), miniLeft: Math.round((box('.mini') || { left: -1 }).left), /* ── the long session: what the app is still HOLDING ───────────── Minutes is all a walk has ever been. These are the numbers that @@ -707,9 +707,9 @@ const MOCK_STEPS = [ { do: `history.back()`, wait: 900 }, /* the artist's photo: the page's ⋯ → a picture from this phone becomes the hero, the page is asked again, then the catalog's own come back */ - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "artist-more", wait: 700 }, + { do: `document.querySelector('.fchrome.more').click()`, name: "artist-more", wait: 700 }, { do: `__click('.halfsheet .row', 'Choose hero photo')`, name: "artist-hero-set", wait: 1400 }, - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "artist-more-custom", wait: 800 }, + { do: `document.querySelector('.fchrome.more').click()`, name: "artist-more-custom", wait: 800 }, { do: `__click('.halfsheet .row', 'Remove custom hero')`, name: "artist-hero-cleared", wait: 1400 }, /* an artist whose exhaustive page the mock keeps for four seconds: what is on screen a second and a half in can only be the two fast lanes */ @@ -769,7 +769,7 @@ const MOCK_STEPS = [ /* the two guest rows the record's own anchor had no count for */ { name: "cat-album-listeners", wait: 900 }, /* the record page's own sheet from the bar */ - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "cat-album-sheet", wait: 700 }, + { do: `document.querySelector('.fchrome.more').click()`, name: "cat-album-sheet", wait: 700 }, { do: `document.querySelector('.scrim').click()`, wait: 500 }, { do: `document.querySelectorAll('.tracks .trk')[1].click()`, name: "cat-album-row", wait: 5500 }, { do: `__ss.next()`, name: "cat-album-next", wait: 7000 }, @@ -833,10 +833,10 @@ const MOCK_STEPS = [ { do: `(function () { __ssMock.fail('playlist_set_order', ''); __ss.player.error = ''; return true; })()`, wait: 400 }, /* the bulk actions from the page's sheet: Add to library files the streamed row (its cache is complete); Download all finds it cached too */ - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 600 }, + { do: `document.querySelector('.fchrome.more').click()`, wait: 600 }, { do: `__click('.halfsheet .row', 'Add to library')`, name: "playlist-bulk-cached", wait: 1500 }, { do: `__click('.bulkline .bulkx', null)`, wait: 400 }, - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 600 }, + { do: `document.querySelector('.fchrome.more').click()`, wait: 600 }, { do: `__click('.halfsheet .row', 'Download all tracks')`, name: "playlist-bulk-download", wait: 1500 }, { do: `__click('.bulkline .bulkx', null)`, wait: 400 }, /* the repair: the unknown row finds a library track and takes its identity */ @@ -844,14 +844,14 @@ const MOCK_STEPS = [ { do: `__click('.halfsheet .row', 'Find a local match')`, name: "playlist-match", wait: 500 }, { do: `(function () { var i = document.querySelector('.plmatch input'); i.value = 'caribou track 4'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, name: "playlist-match-results", wait: 700 }, { do: `__click('.halfsheet .sheet-rows .row', 'Track 4')`, name: "playlist-matched", wait: 1000 }, - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "playlist-more", wait: 700 }, + { do: `document.querySelector('.fchrome.more').click()`, name: "playlist-more", wait: 700 }, { do: `__click('.halfsheet .row', 'Unpin')`, name: "playlist-unpinned", wait: 900 }, /* the cover: the custom one removed (the mosaic), then a photo chosen */ - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 600 }, + { do: `document.querySelector('.fchrome.more').click()`, wait: 600 }, { do: `__click('.halfsheet .row', 'Remove custom cover')`, name: "cover-removed", wait: 1000 }, - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 600 }, + { do: `document.querySelector('.fchrome.more').click()`, wait: 600 }, { do: `__click('.halfsheet .row', 'Choose cover photo')`, name: "cover-set", wait: 1000 }, - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 600 }, + { do: `document.querySelector('.fchrome.more').click()`, wait: 600 }, { do: `__click('.halfsheet .row', 'Rename')`, name: "playlist-rename", wait: 700 }, { do: `(function () { var i = document.querySelector('.halfsheet .prompt .field'); i.value = 'Late night drive II'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, wait: 300 }, { do: `__click('.halfsheet .prompt-acts .btn-fill', 'Rename')`, name: "playlist-renamed", wait: 1000 }, @@ -865,7 +865,7 @@ const MOCK_STEPS = [ { do: `__click('.halfsheet .row', 'New Playlist')`, name: "playlist-added", wait: 1200 }, { do: `__click('.segtabs button', 'Playlists')`, wait: 700 }, { do: `__click('.plgrid .release', 'New Playlist')`, name: "playlist-filled", wait: 1200 }, - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 600 }, + { do: `document.querySelector('.fchrome.more').click()`, wait: 600 }, { do: `__click('.halfsheet .row', 'Delete playlist')`, name: "playlist-delete-confirm", wait: 700 }, { do: `__click('.halfsheet .prompt-acts .btn-fill', 'Delete')`, name: "playlist-deleted", wait: 1200 }, /* the import doors: a pasted tracklist reviewed and created with the @@ -1209,7 +1209,7 @@ const MOCK_STEPS = [ { do: `document.querySelectorAll('.nav button')[2].click()`, wait: 800 }, { do: `__click('.segtabs button', 'Playlists')`, wait: 800 }, { do: `__click('.plgrid .release', 'Late night drive')`, name: "export-open", wait: 1200 }, - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 700 }, + { do: `document.querySelector('.fchrome.more').click()`, wait: 700 }, { do: `__click('.halfsheet .row', 'Export')`, name: "export-sheet", wait: 800 }, { do: `__click('.halfsheet .row', 'CSV')`, name: "export-written", wait: 1000 }, /* a sheet dismissed: the command answers null and the phone says @@ -1218,13 +1218,13 @@ const MOCK_STEPS = [ than any wait here, and a wait that had to outlast it would be the fragile kind. */ { do: `__ssMock.answer('playlist_export', null); __ss.notice.text = ''; true`, wait: 300 }, - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 700 }, + { do: `document.querySelector('.fchrome.more').click()`, wait: 700 }, { do: `__click('.halfsheet .row', 'Export')`, wait: 700 }, { do: `__click('.halfsheet .row', 'M3U8')`, name: "export-cancelled", wait: 1000 }, /* and a refusal — what Android answers when the document is somewhere the app cannot write — read back as a sentence rather than as Rust */ { do: `__ssMock.forget('playlist_export'); __ssMock.fail('playlist_export', 'permission denied (os error 13)'); true`, wait: 300 }, - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 700 }, + { do: `document.querySelector('.fchrome.more').click()`, wait: 700 }, { do: `__click('.halfsheet .row', 'Export')`, wait: 700 }, { do: `__click('.halfsheet .row', 'Backup (JSON)')`, name: "export-refused", wait: 1200 }, { do: `__ssMock.clear(); __ss.player.error = ''; __ss.lib.error = ''; history.back(); true`, wait: 900 }, @@ -1821,7 +1821,7 @@ const PHONE_STEPS = [ { do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 400 }, { do: `__click('.grid2 .release', ${albumText})`, name: "album", wait: 1400 }, { do: `document.querySelector('.rec-acts .disc').click()`, name: "album-playing", wait: 3200 }, - { do: `document.querySelector('.gbar .iconbtn').click()`, name: "library-ambience", wait: 1400 }, + { do: `document.querySelector('.fchrome.back').click()`, name: "library-ambience", wait: 1400 }, { do: `document.querySelectorAll('.nav button')[0].click()`, name: "home-ambience", wait: 1200 }, { do: `document.querySelector('.mini').click()`, name: "sheet", wait: 1400 }, { do: `document.querySelector('.np-art').click()`, name: "viewer", wait: 1200 }, @@ -1910,11 +1910,11 @@ const PHONE_STEPS = [ cancel path is what gets walked, and NOTHING is written to this phone's storage. The playlist is the "Walk test" one the walk itself makes, and deletes three steps below. */ - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 800 }, + { do: `document.querySelector('.fchrome.more').click()`, wait: 800 }, { do: `__click('.halfsheet .row', 'Export')`, name: "export-door", wait: 800 }, { do: `__click('.halfsheet .row', 'M3U8')`, name: "export-picker", wait: 2600 }, { adb: ["shell", "input", "keyevent", "4"], name: "export-dismissed", wait: 2000 }, - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "playlist-more", wait: 800 }, + { do: `document.querySelector('.fchrome.more').click()`, name: "playlist-more", wait: 800 }, { do: `__click('.halfsheet .row', 'Delete playlist')`, wait: 700 }, { do: `__click('.halfsheet .prompt-acts .btn-fill', 'Delete')`, name: "playlist-deleted", wait: 1500 }, /* the paste door on the real core: two lines that resolve to a record @@ -1937,7 +1937,7 @@ const PHONE_STEPS = [ { do: `__click('.halfsheet .row', 'Find a local match')`, wait: 600 }, { do: `(function () { var i = document.querySelector('.plmatch input'); i.value = 'aerodynamic'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, name: "import-match-results", wait: 1200 }, { do: `__click('.halfsheet .sheet-rows .row', 'Aerodynamic')`, name: "import-matched", wait: 1500 }, - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, wait: 800 }, + { do: `document.querySelector('.fchrome.more').click()`, wait: 800 }, { do: `__click('.halfsheet .row', 'Delete playlist')`, wait: 700 }, { do: `__click('.halfsheet .prompt-acts .btn-fill', 'Delete')`, name: "import-deleted", wait: 1500 }, /* the pasted-links door on the real core: one track link, and the wait @@ -2009,7 +2009,7 @@ const PHONE_STEPS = [ { name: "artist", wait: 4500 }, /* the page's ⋯ on the real core: the hero door is there and knows the artist's id — it opens the system picker, so the walk only looks */ - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "artist-more", wait: 900 }, + { do: `document.querySelector('.fchrome.more').click()`, name: "artist-more", wait: 900 }, { do: `document.querySelector('.scrim').click()`, wait: 600 }, /* a discography card the library does NOT hold opens the catalog record */ { do: `(function () { var c = [].slice.call(document.querySelectorAll('.grid2 .release')); if (!c.length) throw new Error('no discography grid'); var pick = c.find(function (x) { return x.textContent.indexOf('in your library') < 0; }) || c[c.length - 1]; pick.click(); })()`, name: "catalog-album", wait: 3500 }, @@ -2040,7 +2040,7 @@ const PHONE_STEPS = [ { do: `__click('.screen button.row', null)`, wait: 500 }, { wait: 2500 }, { do: `__click('.sec .all', 'All')`, name: "dg-page", wait: 1600 }, - { do: `document.querySelectorAll('.gbar .iconbtn')[1].click()`, name: "dg-sheet", wait: 1000 }, + { do: `document.querySelector('.fchrome.more').click()`, name: "dg-sheet", wait: 1000 }, { do: `__click('.halfsheet .chip', 'A–Z')`, name: "dg-az", wait: 1000 }, { do: `document.querySelector('.scrim').click()`, wait: 700 }, { do: `history.back()`, wait: 800 }, @@ -3043,7 +3043,7 @@ async function main() { /* the chrome that is actually on screen: a mini bar under a raised sheet and a nav under one are translated away on purpose */ var out = []; - ['.gbar', '.screen', '.mini:not(.hidden)', '.nav:not(.gone)'].forEach(function (sel) { + ['.fchrome.back', '.screen', '.mini:not(.hidden)', '.nav:not(.gone)'].forEach(function (sel) { var b = box(sel); if (b && b.height > 0 && !inside(b)) out.push(sel); }); @@ -3076,7 +3076,7 @@ async function main() { and a check of the left alone would pass by the inset being 0 */ miniLeft: Math.round((box('.mini') || { left: -1 }).left), miniRight: Math.round(innerWidth - (box('.mini') || { right: innerWidth + 1 }).right), - chromePct: (function () { var g = box('.gbar'), low = box('.mini') || box('.nav'); return Math.round((((g ? g.height : 0) + (low ? innerHeight - low.top : 0)) / innerHeight) * 100); })(), + chromePct: (function () { var g = box('.fchrome.back'), low = box('.mini') || box('.nav'); return Math.round((((g ? g.height : 0) + (low ? innerHeight - low.top : 0)) / innerHeight) * 100); })(), outside: out.join('|'), sheet: !!document.querySelector('.sheet.up'), 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, @@ -3322,7 +3322,7 @@ async function main() { if (resOk) { /* the hero's page: an artist's (tabs, Popular) or a record's */ - const HERO = `(function () { var e = document.querySelector('.screen'); return { screen: e ? e.className : '', title: (document.querySelector('.gb-t') || {}).textContent || '', tabs: document.querySelectorAll('.disco-tabs button').length, pop: document.querySelectorAll('.row.pop').length, socs: document.querySelectorAll('.stats .soc').length, top: (document.querySelector('.top .top-name') || {}).textContent || '' }; })()`; + const HERO = `(function () { var e = document.querySelector('.screen'); return { screen: e ? e.className : '', title: (function () { var q = document.querySelector('.screen'); return q ? (q.dataset.title || '') + (q.dataset.kicker || '') : ''; })(), tabs: document.querySelectorAll('.disco-tabs button').length, pop: document.querySelectorAll('.row.pop').length, socs: document.querySelectorAll('.stats .soc').length, top: (document.querySelector('.top .top-name') || {}).textContent || '' }; })()`; const topName = (await cdp.evaluate(HERO)).top; await cdp.evaluate(HELPERS); await cdp.evaluate(`__click('.top .btn-ghost', null)`); @@ -3384,7 +3384,7 @@ async function main() { await sleep(700); await cdp.evaluate(HELPERS); await cdp.evaluate(`__click('.halfsheet .row', 'Browse their files')`); - const BRW = `(function () { return { title: (document.querySelector('.gb-t') || {}).textContent || '', dirs: document.querySelectorAll('.brw-dir').length, busy: !!document.querySelector('.imp-wait'), err: [].some.call(document.querySelectorAll('.screen .stub'), function (x) { return /shares/.test(x.textContent); }), head: (document.querySelector('.brw-head .m') || {}).textContent || '' }; })()`; + const BRW = `(function () { return { title: (function () { var q = document.querySelector('.screen'); return q ? (q.dataset.title || '') + (q.dataset.kicker || '') : ''; })(), dirs: document.querySelectorAll('.brw-dir').length, busy: !!document.querySelector('.imp-wait'), err: [].some.call(document.querySelectorAll('.screen .stub'), function (x) { return /shares/.test(x.textContent); }), head: (document.querySelector('.brw-head .m') || {}).textContent || '' }; })()`; let brw = null; for (let i = 0; i < 40; i++) { await sleep(500); brw = await cdp.evaluate(BRW); if (brw.dirs > 0 || brw.err) break; } await still(path.join(OUT, `${prefix}net-browse.png`));