Unify Glass Stage navigation and page controls with Now Playing

This commit is contained in:
Jonathan Sykes
2026-10-03 18:19:56 +08:00
parent 8ea92c1aac
commit f2c9459a54
9 changed files with 269 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
/* Draw legacy control glyphs only in Glass; restore them on theme changes. */
(function(root) {
'use strict';
const paths = {
add:'<path d="M12 5v14M5 12h14"/>', close:'<path d="m6 6 12 12M18 6 6 18"/>',
search:'<circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 5 5"/>',
save:'<path d="M12 3v12m-5-5 5 5 5-5M4 17v4h16v-4"/>',
play:'<path d="m8 5 11 7-11 7z"/>', pause:'<path d="M8 5v14M16 5v14"/>',
next:'<path d="m5 5 10 7-10 7zM19 5v14"/>', previous:'<path d="m19 5-10 7 10 7zM5 5v14"/>',
repeat:'<path d="m17 2 4 4-4 4M3 11V8a2 2 0 0 1 2-2h16M7 22l-4-4 4-4m14-1v3a2 2 0 0 1-2 2H3"/>',
edit:'<path d="m14 5 5 5M4 20l5-1L21 7l-4-4L5 15z"/>',
lyrics:'<rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 10v2a7 7 0 0 0 14 0v-2M12 19v3M8 22h8"/>',
share:'<path d="M12 16V3m-4 4 4-4 4 4M5 12v9h14v-9"/>',
copy:'<rect x="8" y="8" width="12" height="13" rx="2"/><path d="M16 8V3H3v13h5"/>',
check:'<path d="m5 12 4 4L19 6"/>', folder:'<path d="M3 7V5h7l2 3h9v12H3z"/>',
settings:'<path d="M4 6h16M4 12h16M4 18h16"/><circle cx="8" cy="6" r="2"/><circle cx="16" cy="12" r="2"/><circle cx="10" cy="18" r="2"/>',
chapters:'<rect x="4" y="3" width="16" height="18" rx="2"/><path d="M8 7h8M8 12h8M8 17h5"/>',
clock:'<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
shuffle:'<path d="m18 3 3 3-3 3M3 6h3c5 0 7 12 12 12h3m-3-3 3 3-3 3M3 18h3c2 0 4-3 6-6s4-6 6-6h3"/>',
down:'<path d="m6 9 6 6 6-6"/>', right:'<path d="m9 6 6 6-6 6"/>', back:'<path d="M20 12H4m6-6-6 6 6 6"/>', more:'<circle cx="5" cy="12" r="1"/><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/>',
devices:'<rect x="3" y="4" width="14" height="12" rx="2"/><path d="M7 20h6M10 16v4"/><rect x="17" y="10" width="5" height="11" rx="1"/>'
};
const glyphs = {'+':'add','✕':'close','×':'close','✖':'close','🔍':'search','🔎':'search','⬇':'save','⤓':'save','💾':'save','▶':'play','⏸':'pause','⏭':'next','⏮':'previous','↻':'repeat','↺':'repeat','✎':'edit','✏':'edit','📝':'edit','🎤':'lyrics','↗':'share','⧉':'copy','✓':'check','📁':'folder','⚙':'settings','📋':'chapters','🕘':'clock','🔀':'shuffle','📡':'devices','▾':'down','▸':'right','←':'back','⋯':'more'};
function leadingGlyph(text) {
const prefix = text.match(/^\s*/)[0], rest = text.slice(prefix.length);
const glyph = Object.keys(glyphs).find(key => rest.startsWith(key));
if (!glyph) return null;
const length = glyph.length + (rest[glyph.length] === '\uFE0F' ? 1 : 0);
return { kind:glyphs[glyph], original:prefix+rest.slice(0,length), rest:rest.slice(length) };
}
function mount(doc) {
const decorations = new Set(); let scheduled=false;
const selector = '#listPane button, .sidebar button, .smart-item .pl-name, .notes-tab, .notes-fold, .mini-btn';
const svg = kind => { const icon=doc.createElementNS('http://www.w3.org/2000/svg','svg'); icon.setAttribute('viewBox','0 0 24 24');icon.setAttribute('aria-hidden','true');icon.classList.add('glass-control-icon');icon.innerHTML=paths[kind];return icon; };
const observer=new root.MutationObserver(schedule), themeObserver=new root.MutationObserver(schedule);
function restore(record) {
record.svg.remove(); if(record.original.isConnected)record.original.replaceWith(doc.createTextNode(record.original.textContent));
if(record.addedLabel && record.parent.isConnected)record.parent.removeAttribute('aria-label');
}
function paint() {
scheduled=false;observer.disconnect();
const active=doc.documentElement.dataset.layout==='glass-stage';
for(const record of decorations)if(!active || !record.svg.isConnected){restore(record);decorations.delete(record);}
if(active) for(const parent of doc.querySelectorAll(selector)) {
// Existing SVGs already express the action; only legacy glyphs change.
if(parent.querySelector('svg:not(.glass-control-icon),.legacy-nav-icon'))continue;
for(const node of [...parent.childNodes]) {
if(node.nodeType!==3)continue;const match=leadingGlyph(node.textContent);if(!match)continue;
const icon=svg(match.kind), original=doc.createElement('span'); original.className='glass-original-glyph';original.textContent=match.original;
node.textContent=match.rest;parent.insertBefore(icon,node);parent.insertBefore(original,node);
const addedLabel=parent.tagName==='BUTTON'&&!parent.hasAttribute('aria-label')&&!parent.textContent.replace(match.original,'').trim();
if(addedLabel)parent.setAttribute('aria-label',parent.title || ({play:'Play',pause:'Pause',close:'Close',add:'Add',save:'Save'}[match.kind] || match.kind));
decorations.add({parent,svg:icon,original,addedLabel});break;
}
if(parent.matches('.notes-tab')&&!parent.querySelector('svg')&&paths[parent.dataset.tab==='markers'?'chapters':parent.dataset.tab]) {
const kind=parent.dataset.tab==='markers'?'chapters':parent.dataset.tab,icon=svg(kind);parent.prepend(icon);
const original=doc.createElement('span');decorations.add({parent,svg:icon,original,addedLabel:false});
}
}
if(active)observer.observe(doc.body,{childList:true,subtree:true,characterData:true});
}
function schedule(){if(!scheduled){scheduled=true;root.queueMicrotask(paint);}}
themeObserver.observe(doc.documentElement,{attributes:true,attributeFilter:['data-layout']});paint();
return ()=>{observer.disconnect();themeObserver.disconnect();for(const record of decorations)restore(record);};
}
if(typeof module!=='undefined')module.exports={leadingGlyph,paths};
else if(root.document.readyState==='loading')root.document.addEventListener('DOMContentLoaded',()=>mount(root.document),{once:true});
else mount(root.document);
})(typeof window!=='undefined'?window:globalThis);

View File

@@ -0,0 +1,9 @@
const test=require('node:test'),assert=require('node:assert/strict');
const {leadingGlyph}=require('./glass-controls.js');
test('glass icon substitution preserves leading whitespace and remaining labels',()=>{
assert.deepEqual(leadingGlyph(' ⬇ Save'),{kind:'save',original:' ⬇',rest:' Save'});
assert.deepEqual(leadingGlyph('⚙️ Settings'),{kind:'settings',original:'⚙️',rest:' Settings'});
assert.equal(leadingGlyph('A song with 🔍 in its title'),null);
assert.equal(leadingGlyph('Clear all'),null);
assert.equal(leadingGlyph('⏸').kind,'pause');
});

View File

@@ -32,6 +32,7 @@
<link rel="stylesheet" href="car-mode.css" />
<link rel="stylesheet" href="offline-pages.css" />
<link rel="stylesheet" href="saved-page.css" />
<link rel="stylesheet" href="theme-glass-controls.css" />
</head>
<body>
<div class="app">
@@ -663,6 +664,7 @@
<script src="downloads-page.js"></script>
<script src="saved-page.js"></script>
<script src="server-backfill.js"></script>
<script src="glass-controls.js"></script>
<script src="party-dj.js"></script>
<script src="lyrics-window.js"></script>
<script src="midi.js"></script>

View File

@@ -89,6 +89,8 @@ const SHELL = [
'/offline-pages.css',
'/saved-page.js',
'/server-backfill.js',
'/glass-controls.js',
'/theme-glass-controls.css',
'/saved-page.css',
'/party-dj.js',
'/lyrics-window.js',

View File

@@ -0,0 +1,53 @@
/* Now Playing's stroke and frosted-tile vocabulary, scoped to Glass Stage. */
html[data-layout="glass-stage"] .legacy-nav-icon,
html[data-layout="glass-stage"] .glass-original-glyph { display:none; }
html[data-layout="glass-stage"] :is(.classic-nav-icon,.glass-control-icon) { display:inline-block; flex:none; width:20px; height:20px; fill:none; stroke:currentColor; stroke-width:1.9; stroke-linecap:round; stroke-linejoin:round; vertical-align:middle; pointer-events:none; }
html[data-layout="glass-stage"] :is(#listPane button,.settings-section-row,.notes-tab,.mini-btn,.list-icon-btn) svg { width:20px; height:20px; fill:none; stroke:currentColor; stroke-width:1.9; stroke-linecap:round; stroke-linejoin:round; flex:none; }
html[data-layout="glass-stage"] :is(.bottom-nav-btn,.nav-item,.playlist-item) { min-height:44px; color:var(--text-2); }
html[data-layout="glass-stage"] .bottom-nav { gap:2px; padding-inline:4px; }
html[data-layout="glass-stage"] .bottom-nav-btn { border:1px solid transparent; border-radius:18px; gap:6px; min-width:44px; background:transparent; }
html[data-layout="glass-stage"] .bottom-nav-icon { display:grid; place-items:center; width:24px; height:24px; background:none; box-shadow:none; transform:none; }
html[data-layout="glass-stage"] .bottom-nav-label { font-size:11px; }
html[data-layout="glass-stage"] .bottom-nav-btn.active { color:var(--accent-bright); background:var(--tile-hi); border-color:var(--glass-line); }
html[data-layout="glass-stage"] .bottom-nav-btn.active .bottom-nav-icon { background:none; box-shadow:none; transform:none; }
html[data-layout="glass-stage"] :is(.bottom-nav-badge,.nav-badge,.pl-count) { background:var(--tile-hi); border:1px solid var(--glass-line); color:var(--text-2); border-radius:99px; font-size:11px; min-width:18px; font-variant-numeric:tabular-nums; }
html[data-layout="glass-stage"] :is(.bottom-nav-btn.active .bottom-nav-badge,.nav-item.active .nav-badge) { color:var(--accent-bright); }
html[data-layout="glass-stage"] :is(.nav-item,.playlist-item) { border:1px solid transparent; border-radius:18px; gap:10px; padding:10px 12px; }
html[data-layout="glass-stage"] :is(.nav-item.active,.playlist-item.active) { color:var(--accent-bright); border-color:var(--glass-line); background:var(--tile-hi); }
html[data-layout="glass-stage"] :is(.nav-item,.playlist-item):hover { background:var(--tile); }
html[data-layout="glass-stage"] .smart-item .pl-name { display:flex; align-items:center; gap:10px; }
html[data-layout="glass-stage"] :is(#listPane button,.sidebar .icon-btn,.sb-profile-btn,.notes-tab,.mini-btn) { min-height:44px; min-width:44px; border-radius:18px; border:1px solid var(--glass-line); background:var(--tile); color:var(--text-2); box-shadow:none; }
html[data-layout="glass-stage"] :is(#listPane button,.sidebar .icon-btn,.sb-profile-btn,.notes-tab,.mini-btn):hover { background:var(--tile-hi); color:var(--text); border-color:var(--glass-line); }
html[data-layout="glass-stage"] :is(#listPane button,.sidebar button,.notes-tab,.mini-btn,.playlist-item):focus-visible { outline:2px solid var(--accent-bright); outline-offset:3px; }
html[data-layout="glass-stage"] :is(#listPane button,.notes-tab,.mini-btn):disabled { opacity:.5; cursor:not-allowed; }
html[data-layout="glass-stage"] :is(.card-menu,.card-del,.card-act,.saved-file-action,.mini-btn) { width:44px; height:44px; padding:11px; opacity:1; display:inline-flex; align-items:center; justify-content:center; flex:none; }
html[data-layout="glass-stage"] :is(.card-del,.saved-file-action.danger,.saved-clear,.dl-act.danger) { color:var(--accent-bright); }
html[data-layout="glass-stage"] :is(.settings-section-row[aria-current="true"],.notes-tab.active) { color:var(--accent-bright); background:var(--tile-hi); border-color:var(--glass-line); }
html[data-layout="glass-stage"] .settings-section-row { margin-bottom:6px; }
html[data-layout="glass-stage"] .settings-section-row svg { color:inherit; }
html[data-layout="glass-stage"] #listActions button { height:auto; min-height:44px; font-size:12px; }
html[data-layout="glass-stage"] #listActions button::after { font-size:11px; }
html[data-layout="glass-stage"] .notes-tabs { display:flex; flex-wrap:wrap; gap:6px; padding:6px; border-radius:22px; background:var(--tile); border:1px solid var(--glass-line); }
html[data-layout="glass-stage"] .notes-tab { display:inline-flex; align-items:center; justify-content:center; gap:6px; padding:8px 10px; font-size:12px; }
html[data-layout="glass-stage"] .mini-bar-inner { gap:8px; }
html[data-layout="glass-stage"] .mini-close { color:var(--text-2); }
html[data-layout="glass-stage"] .mini-service { color:var(--accent-bright); }
html[data-layout="glass-stage"] .list-filter-bar input { min-height:44px; }
/* Reserve room for both row actions rather than squeezing the media title. */
html[data-layout="glass-stage"] .card:not(.saved-card):not(.dl-row) { display:grid; grid-template-columns:72px minmax(0,1fr) 44px; align-items:center; gap:8px; }
html[data-layout="glass-stage"] .card:not(.saved-card):not(.dl-row):has(.card-del) { grid-template-columns:72px minmax(0,1fr) 44px 44px; }
html[data-layout="glass-stage"] .card:not(.saved-card):not(.dl-row) .thumb { grid-column:1; grid-row:1; width:72px; aspect-ratio:16/9; }
html[data-layout="glass-stage"] .card:not(.saved-card):not(.dl-row) .card-info { grid-column:2; grid-row:1; min-width:0; }
html[data-layout="glass-stage"] .card:not(.saved-card):not(.dl-row) .card-menu { grid-column:3; grid-row:1; }
html[data-layout="glass-stage"] .card:not(.saved-card):not(.dl-row) .card-del { grid-column:4; grid-row:1; }
html[data-layout="glass-stage"] .card .card-title { overflow:hidden; overflow-wrap:anywhere; }
html[data-layout="glass-stage"] .notes-fold { min-width:44px; min-height:44px; border-radius:18px; background:var(--tile); border:1px solid var(--glass-line); color:var(--text-2); }
html[data-layout="glass-stage"] .notes-count { font-size:11px; }
html[data-layout="glass-stage"] .bottom-nav-btn { padding:8px 2px; }
html[data-layout="glass-stage"] .bottom-nav-label { overflow:visible; text-overflow:clip; max-width:none; letter-spacing:0; }
/* Keep the section shortcuts away from right-hand file actions. */
html[data-layout="glass-stage"] .sec-rail { top:auto; left:12px; right:auto; bottom:var(--rail-bottom,24px); transform:none; width:max-content; max-width:calc(100% - 100px); flex-direction:row; gap:4px; padding:6px; border-radius:22px; background:var(--glass); border-color:var(--glass-line); }
html[data-layout="glass-stage"] .sec-rail-btn { width:44px; height:44px; border-radius:18px; background:var(--tile); border:1px solid var(--glass-line); color:var(--text-2); }
html[data-layout="glass-stage"] .sec-rail-btn.active { background:var(--tile-hi); color:var(--accent-bright); border-color:var(--glass-line); }
html[data-layout="glass-stage"] .sec-rail-label { right:auto; left:0; bottom:50px; }
html[data-layout="glass-stage"] #listPane { padding-bottom:calc(var(--bottom-chrome,0px) + 88px) !important; }