diff --git a/frontend/glass-controls.js b/frontend/glass-controls.js index b1c32ca..982079f 100644 --- a/frontend/glass-controls.js +++ b/frontend/glass-controls.js @@ -2,6 +2,8 @@ (function(root) { 'use strict'; const paths = { + trash:'', + music:'', add:'', close:'', search:'', save:'', @@ -20,7 +22,7 @@ down:'', right:'', back:'', more:'', devices:'' }; - 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'}; + 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','โ‹ฎ':'more','๐Ÿ—‘':'trash','๐ŸŽต':'music'}; function leadingGlyph(text) { const prefix = text.match(/^\s*/)[0], rest = text.slice(prefix.length); const glyph = Object.keys(glyphs).find(key => rest.startsWith(key)); @@ -30,7 +32,7 @@ } 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 selector = '#listPane button, .sidebar button, .smart-item .pl-name, .notes-tab, .notes-fold, .mini-btn, .sidebar .playlist-item:not(.smart-item), .pl-offline-badge.all, .home-pl-offline.all, .home-pl-play, .home-pl-thumb-fallback'; 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) { @@ -46,12 +48,18 @@ 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; + const icon=svg(match.kind); + if(parent.matches('.pl-offline-badge,.home-pl-offline')) { icon.removeAttribute('aria-hidden'); icon.setAttribute('role','img'); icon.setAttribute('aria-label',parent.title || 'Available offline'); } + const 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('.sidebar .playlist-item:not(.smart-item)')&&!parent.querySelector('svg')) { + const icon=svg('folder'); parent.prepend(icon); + decorations.add({parent,svg:icon,original:doc.createElement('span'),addedLabel:false}); + } 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}); diff --git a/frontend/glass-controls.test.js b/frontend/glass-controls.test.js index 2638fd8..68b6ea0 100644 --- a/frontend/glass-controls.test.js +++ b/frontend/glass-controls.test.js @@ -7,3 +7,9 @@ test('glass icon substitution preserves leading whitespace and remaining labels' assert.equal(leadingGlyph('Clear all'),null); assert.equal(leadingGlyph('โธ').kind,'pause'); }); + +test('playlist trash, menu and music glyphs use drawn glass icons',()=>{ + assert.equal(leadingGlyph('๐Ÿ—‘๏ธ Delete').kind,'trash'); + assert.equal(leadingGlyph('โ‹ฎ').kind,'more'); + assert.equal(leadingGlyph('๐ŸŽต').kind,'music'); +}); diff --git a/frontend/theme-glass-controls.css b/frontend/theme-glass-controls.css index a84f22b..2f21992 100644 --- a/frontend/theme-glass-controls.css +++ b/frontend/theme-glass-controls.css @@ -52,3 +52,9 @@ html[data-layout="glass-stage"] .sec-rail-btn { width:44px; height:44px; border- 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; } + +/* Playlist rows and offline state use the same drawn icons as Now Playing. */ +html[data-layout="glass-stage"] .sidebar .playlist-item::before { content:none; } +html[data-layout="glass-stage"] :is(.pl-offline-badge.all,.home-pl-offline.all) { display:inline-grid; place-items:center; flex:none; width:24px; height:24px; padding:0; border-radius:8px; background:var(--tile-hi); border:1px solid var(--glass-line); color:var(--accent-bright); box-shadow:none; } +html[data-layout="glass-stage"] :is(.pl-offline-badge.partial,.home-pl-offline.partial) { font-size:11px; } +html[data-layout="glass-stage"] .home-pl-play { display:grid; place-items:center; width:44px; height:44px; border-radius:18px; background:var(--tile); border:1px solid var(--glass-line); color:var(--text); } diff --git a/tests/glass-navigation.spec.js b/tests/glass-navigation.spec.js index 1f97f88..3f819db 100644 --- a/tests/glass-navigation.spec.js +++ b/tests/glass-navigation.spec.js @@ -44,3 +44,40 @@ for (const layout of ['glass-stage', 'classic']) for (const width of [390, 1440] await before.close(); }); } + +const beforeIcons = execFileSync('git', ['show', '79a3c28:frontend/glass-controls.js'], { encoding: 'utf8' }); +async function sidebarFixture(page, layout) { + await openClassic(page, { populated: true, settings: { autoPreload: false } }); + await page.addStyleTag({ content: '*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}' }); + await page.evaluate(layout => { + data.settings.layout = layout; applyAppearance(); + data.playlists[0].videos.forEach(v => cachedIds.add(v.id)); + view = { type: 'playlist', id: 'studio' }; render(); openSidebar(); + }, layout); + await page.evaluate(async () => { await document.fonts.ready; await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); }); +} +for (const layout of ['glass-stage', 'classic']) for (const width of [390, 1440]) { + test(`${layout} playlist sidebar icons at ${width}`, async ({ page, context }) => { + await page.setViewportSize({ width, height: 900 }); + const before = await context.newPage(); await before.setViewportSize({ width, height: 900 }); + await before.route('**/glass-controls.js', route => route.fulfill({ contentType: 'text/javascript', body: beforeIcons })); + await sidebarFixture(before, layout); + const baseline = await before.screenshot({ path: `/tmp/ytplayer-done8-B-before-${layout}-${width}.png` }); + await sidebarFixture(page, layout); + const result = await page.screenshot({ path: `/tmp/ytplayer-done8-B-after-${layout}-${width}.png` }); + const badge = page.locator('.sidebar .pl-offline-badge.all').first(); + if (layout === 'classic') { + expect(differentPixels(baseline, result)).toBeLessThanOrEqual(32); + await expect(badge).toHaveText('โฌ‡'); + } else { + await expect(badge.locator('svg')).toHaveCount(1); + expect(await badge.locator('svg').evaluate(el => getComputedStyle(el).strokeWidth)).toBe('1.9px'); + await expect(page.locator('.home-pl-play svg').first()).toHaveCount(1); + await expect(page.locator('#listActions .la-primary svg')).toHaveCount(1); + await page.evaluate(() => { data.settings.layout = 'classic'; applyAppearance(); }); + await expect(badge).toHaveText('โฌ‡'); + await expect(page.locator('.glass-control-icon')).toHaveCount(0); + } + await before.close(); + }); +}