Files
ytplayer/frontend/glass-controls.js

97 lines
8.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* Draw legacy control glyphs in Glass and Bento; restore them on theme changes. */
(function(root) {
'use strict';
const paths = {
trash:'<path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7M14 10v7"/>',
music:'<path d="M9 18V5l11-2v13M9 8l11-2"/><ellipse cx="6" cy="18" rx="3" ry="2"/><ellipse cx="17" cy="16" rx="3" ry="2"/>',
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"/>',
television:'<rect x="3" y="4" width="18" height="13" rx="2"/><path d="M8 21h8m-4-4v4"/>', pin:'<path d="M9 3h6l-1 6 4 4H6l4-4zM12 13v8"/>',
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','⋮':'more','🗑':'trash','🎵':'music'};
function leadingGlyph(text,bento=false) {
const mapping=bento?{...glyphs,'📺':'television','⏱':'clock','⇅':'settings','📌':'pin','🎧':'music','📚':'folder'}:glyphs;
const prefix = text.match(/^\s*/)[0], rest = text.slice(prefix.length);
const glyph = Object.keys(mapping).find(key => rest.startsWith(key));
if (!glyph) return null;
const length = glyph.length + (rest[glyph.length] === '\uFE0F' ? 1 : 0);
return { kind:mapping[glyph], original:prefix+rest.slice(0,length), rest:rest.slice(length) };
}
function mount(doc) {
const decorations = new Set(), tabLabels = new Map(); let scheduled=false;
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) {
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 restoreTab(parent, record) {
for(const label of record.labels)if(label.isConnected)label.replaceWith(doc.createTextNode(label.textContent));
for(const [name,value] of Object.entries(record.attributes)) { if(value===null)parent.removeAttribute(name);else parent.setAttribute(name,value); }
}
function paint() {
scheduled=false;observer.disconnect();
const bento=doc.documentElement.dataset.layout==='bento-hub';
const active=bento || doc.documentElement.dataset.layout==='glass-stage';
for(const [parent,record] of tabLabels)if(!active || !parent.isConnected){restoreTab(parent,record);tabLabels.delete(parent);}
for(const record of decorations)if(!active || !record.svg.isConnected){restore(record);decorations.delete(record);}
if(active) for(const parent of doc.querySelectorAll(selector+(bento?', .service-btn, .np-btn, .ctrl, .empty-icon, .dl-saved-cta, .brand-mark, .upnext-head button, .ph-logo, .saved-badge':''))) {
// 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,bento);if(!match)continue;
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});
}
}
if(active)for(const parent of doc.querySelectorAll('.notes-tab')) {
if(tabLabels.has(parent))continue;
const record={labels:[],attributes:{'aria-label':parent.getAttribute('aria-label'),title:parent.getAttribute('title')}};
const name=parent.dataset.tab==='markers'?'Chapters & bookmarks':parent.dataset.tab==='lyrics'?'Lyrics':'Search';
if(!parent.hasAttribute('aria-label'))parent.setAttribute('aria-label',name);
if(!parent.hasAttribute('title'))parent.setAttribute('title',name);
for(const node of [...parent.childNodes])if(node.nodeType===3 && node.textContent.trim()) {
const label=doc.createElement('span');label.className='glass-tab-label';node.replaceWith(label);label.append(node);record.labels.push(label);
}
tabLabels.set(parent,record);
}
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);for(const [parent,record] of tabLabels)restoreTab(parent,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);