/* Framework7 Swipe layout: the classic design with Framework7 swipe gestures. * Cards become F7 swipeouts, swiping up on the mini bar opens an F7 "Up next" * sheet, and the mini bar / screen edge take horizontal swipes. Framework7 is * loaded only when this layout is first selected. */ (function(root){ 'use strict'; const LAYOUT='f7-swipe',F7_SRC='vendor/framework7-swipe.min.js'; // Which swipe actions a card gets in a given view. Pure, so it is testable. function cardActions(viewType,canDelete){ const left=viewType==='queue'?[]:[{act:'queue',label:'Queue'}]; const right=[{act:'playlist',label:'Playlist'}]; if(canDelete)right.push({act:'remove',label:'Remove',danger:true}); return {left,right}; } // Classify a finished drag. Returns 'left'|'right'|'up'|'down'|null. function swipeDirection(dx,dy,min){ min=min||48;const ax=Math.abs(dx),ay=Math.abs(dy); if(Math.max(ax,ay)ay*1.4)return dx<0?'left':'right'; if(ay>ax*1.4)return dy<0?'up':'down'; return null; } function mount(doc){ const html=doc.documentElement;let app=null,loading=null,active=false,sheet=null,cardsObserver=null; const on=()=>html.dataset.layout===LAYOUT; // app.js top-level `let` bindings are shared across classic scripts but are not on window. /* global view, queue, queueIndex, queueSource */ const g=(name)=>{try{switch(name){case 'view':return view;case 'queue':return queue;case 'queueIndex':return queueIndex;case 'queueSource':return queueSource;}}catch(_){}return undefined;}; function loadF7(){ if(root.Framework7)return Promise.resolve(root.Framework7); if(!loading)loading=new Promise((res,rej)=>{const s=doc.createElement('script');s.src=F7_SRC;s.onload=()=>res(root.Framework7);s.onerror=()=>{loading=null;rej(new Error('Framework7 failed to load'));};doc.head.append(s);}); return loading; } function ensureApp(){ return loadF7().then((F7)=>{ if(!app){ app=new F7({el:'body',theme:'ios',touch:{activeState:false,touchRipple:false,touchHighlight:false,tapHold:false},swipeout:{noFollow:false,removeElements:false},sheet:{backdrop:true,closeByBackdropClick:true}}); // F7's touch module swallows any click whose target isn't the touched element and // cancels the click after a moved touch. app.js relies on programmatic .click() // calls, and F7 can't be torn down, so drop those two app-wide handlers. app.off('click');app.off('touchend'); } return app; }); } // ---- Swipeout cards ---- // Framework7 only animates children of the actions box, so these are links acting as buttons. function actionBtn(a){const b=doc.createElement('a');b.setAttribute('role','button');b.tabIndex=0;b.className='f7s-act'+(a.danger?' f7s-danger swipeout-overswipe':'');b.dataset.act=a.act;b.textContent=a.label;return b;} function wrapCard(card){ if(!card.classList||!card.classList.contains('card')||card.closest('li.swipeout')||card.classList.contains('skeleton-card')||!card._video)return; const li=doc.createElement('li');li.className='swipeout f7s-row'; card.replaceWith(li);card.classList.add('swipeout-content');li.append(card); const acts=cardActions(g('view')?.type,!!card.querySelector('.card-del')); for(const side of ['left','right']){ if(!acts[side].length)continue; const box=doc.createElement('div');box.className='swipeout-actions-'+side;acts[side].forEach(a=>box.append(actionBtn(a)));li.append(box); } } function unwrapAll(){ doc.querySelectorAll('li.f7s-row').forEach(li=>{const c=li.querySelector('.swipeout-content');if(c){c.classList.remove('swipeout-content');c.style.transform='';li.replaceWith(c);}else li.remove();}); } function wrapAll(){doc.querySelectorAll('#cards .card').forEach(wrapCard);} function onAction(e){ const btn=e.target.closest('.f7s-act');if(!btn)return; const li=btn.closest('li.swipeout'),card=li&&li.querySelector('.card'),v=card&&card._video; e.stopPropagation();if(app)app.swipeout.close(li);if(!v)return; if(btn.dataset.act==='queue')root.addToQueue&&root.addToQueue(v); else if(btn.dataset.act==='playlist')root.openCardMenu&&root.openCardMenu(v); else if(btn.dataset.act==='remove'){const del=card.querySelector('.card-del');if(del)del.click();} } // ---- Up next sheet ---- function upcoming(){const q=g('queue')||[],i=g('queueIndex');return q.slice((typeof i==='number'?i:-1)+1);} function renderSheet(){ const list=sheet.el.querySelector('.f7s-sheet-list'),items=upcoming(); list.innerHTML=''; if(!items.length){const p=doc.createElement('p');p.className='f7s-empty';p.textContent='Nothing queued after this song.';list.append(p);return;} items.forEach((v,k)=>{ const li=doc.createElement('li');li.className='f7s-up'; li.innerHTML='
'; li.querySelector('img').src=v.thumbnail||'';li.querySelector('.f7s-up-title').textContent=v.title||'';li.querySelector('.f7s-up-ch').textContent=v.channel||''; li.addEventListener('click',()=>{const q=g('queue')||[],i=g('queueIndex');sheet.close();root.playFromList&&root.playFromList(q,(typeof i==='number'?i:-1)+1+k,g('queueSource'));}); list.append(li); }); } function openSheet(){ ensureApp().then((a)=>{ if(!sheet){ const el=doc.createElement('div');el.className='sheet-modal f7s-sheet'; el.innerHTML='
Up next
    '; doc.body.append(el); sheet=a.sheet.create({el,swipeToClose:true,breakpoints:[0.5],backdropBreakpoint:0.5,backdrop:true,closeByBackdropClick:true,swipeHandler:'.f7s-grab, .f7s-sheet-head'}); } renderSheet();sheet.open(); }).catch(()=>{}); } // ---- Mini bar + edge swipes (F7 has no component for these) ---- let drag=null,swallowClick=false; function down(e){ if(!on()||e.pointerType==='mouse'&&e.button!==0)return; const mini=e.target.closest&&e.target.closest('#miniBar'); const sidebarOpen=!!doc.querySelector('.app.sidebar-open'); const edge=!mini&&e.clientX<22&&!e.target.closest('.player-stage'); const inSidebar=sidebarOpen&&e.target.closest&&e.target.closest('#sidebar, #sidebarBackdrop'); if(!mini&&!edge&&!inSidebar)return; drag={x:e.clientX,y:e.clientY,kind:mini?'mini':edge?'edge':'sidebar'}; } function up(e){ if(!drag)return;const d=drag;drag=null; const dir=swipeDirection(e.clientX-d.x,e.clientY-d.y,d.kind==='mini'?36:56);if(!dir)return; if(d.kind==='mini'){ swallowClick=true;setTimeout(()=>{swallowClick=false;},400); if(dir==='left')root.playNext&&root.playNext(); else if(dir==='right')root.playPrev&&root.playPrev(); else if(dir==='up')openSheet(); }else if(d.kind==='edge'&&dir==='right')root.openSidebar&&root.openSidebar(); else if(d.kind==='sidebar'&&dir==='left')root.closeSidebar&&root.closeSidebar(); } function click(e){if(swallowClick&&e.target.closest&&e.target.closest('#miniBar')){e.stopPropagation();e.preventDefault();swallowClick=false;}} function activate(){ active=true; ensureApp().then(()=>{if(!active)return;wrapAll();}).catch(()=>{}); const cards=doc.getElementById('cards'); if(cards){cardsObserver=new root.MutationObserver(()=>{if(app)wrapAll();});cardsObserver.observe(cards,{childList:true});} doc.addEventListener('click',onAction,true); } function deactivate(){ active=false;if(cardsObserver){cardsObserver.disconnect();cardsObserver=null;} doc.removeEventListener('click',onAction,true); if(sheet&&sheet.opened)sheet.close(false); unwrapAll(); } function sync(){if(on()&&!active)activate();else if(!on()&&active)deactivate();} doc.addEventListener('pointerdown',down,{passive:true}); doc.addEventListener('pointerup',up,{passive:true}); doc.addEventListener('pointercancel',()=>{drag=null;},{passive:true}); doc.addEventListener('click',click,true); new root.MutationObserver(sync).observe(html,{attributes:true,attributeFilter:['data-layout']}); sync(); } const api={cardActions,swipeDirection}; if(typeof module!=='undefined')module.exports=api; else {root.F7Swipe=api; if(root.document.readyState==='loading')root.document.addEventListener('DOMContentLoaded',()=>mount(root.document),{once:true});else mount(root.document);} })(typeof window==='undefined'?globalThis:window);