Add Framework7 Swipe layout with swipeout rows, up-next sheet and mini-bar gestures
This commit is contained in:
@@ -54,6 +54,12 @@ JSON shapes mirror the Tauri Rust bridge exactly — don't change one side alone
|
|||||||
refreshed every 1.5 s by the `glassStage` IIFE), four stacked icon tiles and secondary actions as a pill row,
|
refreshed every 1.5 s by the `glassStage` IIFE), four stacked icon tiles and secondary actions as a pill row,
|
||||||
floating mini player. Elements that only this theme shows (`.stg-overlay`, `.skip10`, `.deck-chips`, `.vol-pod`
|
floating mini player. Elements that only this theme shows (`.stg-overlay`, `.skip10`, `.deck-chips`, `.vol-pod`
|
||||||
extras, `.mode-name`) are in the shared markup and hidden by default.
|
extras, `.mode-name`) are in the shared markup and hidden by default.
|
||||||
|
- **f7-swipe** (`frontend/f7-layout.js` + `theme-f7.css`): classic look + Framework7 gestures. Loads
|
||||||
|
`vendor/framework7-swipe.min.js` (F7 9.2.0 core+Store+Swipeout+Modal+Sheet, rebuilt from
|
||||||
|
`vendor/framework7-swipe.entry.js`) only when picked. Cards are wrapped in `li.swipeout` (Queue / Playlist /
|
||||||
|
Remove, via `card._video`), swipe up on the mini bar = F7 "Up next" sheet, sideways = next/prev, left-edge
|
||||||
|
swipe = sidebar. F7's touch module swallows programmatic `.click()`s, so `app.off('click')` +
|
||||||
|
`app.off('touchend')` run after init — keep that, or app.js buttons driven by `.click()` silently stop working.
|
||||||
- Now-playing tiles are `[svg.ti][span.tl]`; change state text with `setTile(btn, label)`, never `textContent`.
|
- Now-playing tiles are `[svg.ti][span.tl]`; change state text with `setTile(btn, label)`, never `textContent`.
|
||||||
|
|
||||||
## Saved searches + caching notes
|
## Saved searches + caching notes
|
||||||
|
|||||||
@@ -658,7 +658,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
density: 'comfortable', // 'comfortable' | 'compact'
|
density: 'comfortable', // 'comfortable' | 'compact'
|
||||||
perfMode: false, // disable heavy visual effects for speed
|
perfMode: false, // disable heavy visual effects for speed
|
||||||
reduceMotion: false, // disable animations
|
reduceMotion: false, // disable animations
|
||||||
layout: 'classic', // 'classic'|'glass-stage'|'sanctuary-stage'|'liturgical'|'living-hymnal'|'broadcaster'|'contemplative'|'bento-hub'
|
layout: 'classic', // 'classic'|'glass-stage'|'sanctuary-stage'|'liturgical'|'living-hymnal'|'broadcaster'|'contemplative'|'bento-hub'|'f7-swipe'
|
||||||
autoBackupEnabled: false,
|
autoBackupEnabled: false,
|
||||||
autoBackupIntervalDays: 7,
|
autoBackupIntervalDays: 7,
|
||||||
serviceVideo: false, // show the video panel inside service mode (lyric videos)
|
serviceVideo: false, // show the video panel inside service mode (lyric videos)
|
||||||
@@ -9535,7 +9535,7 @@ async function renderSettings() {
|
|||||||
</label>
|
</label>
|
||||||
<label class="set-row">
|
<label class="set-row">
|
||||||
<span>Layout theme</span>
|
<span>Layout theme</span>
|
||||||
${sel('setLayout', data.settings.layout || 'classic', [['classic', 'Classic (default)'], ['glass-stage', 'Glass Stage'], ['sanctuary-stage', 'Sanctuary Stage'], ['liturgical', 'Liturgical Chronicle'], ['living-hymnal', 'Living Hymnal'], ['broadcaster', 'Sunday Broadcaster'], ['contemplative', 'Contemplative'], ['bento-hub', 'Bento Hub']])}
|
${sel('setLayout', data.settings.layout || 'classic', [['classic', 'Classic (default)'], ['glass-stage', 'Glass Stage'], ['sanctuary-stage', 'Sanctuary Stage'], ['liturgical', 'Liturgical Chronicle'], ['living-hymnal', 'Living Hymnal'], ['broadcaster', 'Sunday Broadcaster'], ['contemplative', 'Contemplative'], ['bento-hub', 'Bento Hub'], ['f7-swipe', 'Framework7 Swipe']])}
|
||||||
</label>
|
</label>
|
||||||
<label class="set-row">
|
<label class="set-row">
|
||||||
<span>Font size</span>
|
<span>Font size</span>
|
||||||
@@ -10128,6 +10128,7 @@ function renderCard(v, index, list) {
|
|||||||
const isDownloading = downloading.has(v.id);
|
const isDownloading = downloading.has(v.id);
|
||||||
card.className = 'card' + (isCached ? ' cached' : '') + (isDownloading ? ' downloading' : '') + (selectedIds.has(v.id) ? ' selected' : '');
|
card.className = 'card' + (isCached ? ' cached' : '') + (isDownloading ? ' downloading' : '') + (selectedIds.has(v.id) ? ' selected' : '');
|
||||||
card.dataset.id = v.id;
|
card.dataset.id = v.id;
|
||||||
|
card._video = v; // read by f7-layout.js swipe actions
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="batch-check"></div>
|
<div class="batch-check"></div>
|
||||||
<div class="thumb">
|
<div class="thumb">
|
||||||
|
|||||||
151
frontend/f7-layout.js
Normal file
151
frontend/f7-layout.js
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
/* 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)<min)return null;
|
||||||
|
if(ax>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 <a> 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='<img alt="" loading="lazy" decoding="async"><div class="f7s-up-info"><div class="f7s-up-title"></div><div class="f7s-up-ch"></div></div>';
|
||||||
|
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='<div class="sheet-modal-inner"><div class="f7s-grab" aria-hidden="true"></div><div class="f7s-sheet-head"><span>Up next</span></div><ul class="f7s-sheet-list"></ul></div>';
|
||||||
|
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);
|
||||||
19
frontend/f7-layout.test.js
Normal file
19
frontend/f7-layout.test.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert');
|
||||||
|
const { cardActions, swipeDirection } = require('./f7-layout');
|
||||||
|
|
||||||
|
test('cardActions: queue view has no Queue action, removable rows get Remove', () => {
|
||||||
|
assert.deepStrictEqual(cardActions('queue', true).left, []);
|
||||||
|
assert.deepStrictEqual(cardActions('queue', true).right.map((a) => a.act), ['playlist', 'remove']);
|
||||||
|
assert.deepStrictEqual(cardActions('search', false).left.map((a) => a.act), ['queue']);
|
||||||
|
assert.deepStrictEqual(cardActions('search', false).right.map((a) => a.act), ['playlist']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('swipeDirection classifies clear swipes and ignores short or diagonal drags', () => {
|
||||||
|
assert.strictEqual(swipeDirection(-80, 5), 'left');
|
||||||
|
assert.strictEqual(swipeDirection(80, -10), 'right');
|
||||||
|
assert.strictEqual(swipeDirection(4, -70), 'up');
|
||||||
|
assert.strictEqual(swipeDirection(10, 20), null);
|
||||||
|
assert.strictEqual(swipeDirection(60, 60), null);
|
||||||
|
assert.strictEqual(swipeDirection(40, 0, 36), 'right');
|
||||||
|
});
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
<link rel="stylesheet" href="related-videos.css" />
|
<link rel="stylesheet" href="related-videos.css" />
|
||||||
<link rel="stylesheet" href="download-actions.css" />
|
<link rel="stylesheet" href="download-actions.css" />
|
||||||
<link rel="stylesheet" href="theme-bento-hub.css" />
|
<link rel="stylesheet" href="theme-bento-hub.css" />
|
||||||
|
<link rel="stylesheet" href="theme-f7.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
@@ -680,6 +681,7 @@
|
|||||||
<script src="server-backfill.js"></script>
|
<script src="server-backfill.js"></script>
|
||||||
<script src="glass-controls.js"></script>
|
<script src="glass-controls.js"></script>
|
||||||
<script src="bento-hub.js"></script>
|
<script src="bento-hub.js"></script>
|
||||||
|
<script src="f7-layout.js"></script>
|
||||||
<script src="glass-panel-layout.js"></script>
|
<script src="glass-panel-layout.js"></script>
|
||||||
<script src="party-dj.js"></script>
|
<script src="party-dj.js"></script>
|
||||||
<script src="lyrics-window.js"></script>
|
<script src="lyrics-window.js"></script>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ const SHELL = [
|
|||||||
'/refresh.css',
|
'/refresh.css',
|
||||||
'/theme-glass.css',
|
'/theme-glass.css',
|
||||||
'/theme-bento-hub.css',
|
'/theme-bento-hub.css',
|
||||||
|
'/theme-f7.css',
|
||||||
'/fonts/PlusJakartaSans-latin.woff2',
|
'/fonts/PlusJakartaSans-latin.woff2',
|
||||||
'/fonts/PlusJakartaSans-latin-ext.woff2',
|
'/fonts/PlusJakartaSans-latin-ext.woff2',
|
||||||
'/theme-glass-panel.css',
|
'/theme-glass-panel.css',
|
||||||
@@ -109,6 +110,8 @@ const SHELL = [
|
|||||||
'/server-backfill.js',
|
'/server-backfill.js',
|
||||||
'/glass-controls.js',
|
'/glass-controls.js',
|
||||||
'/bento-hub.js',
|
'/bento-hub.js',
|
||||||
|
'/f7-layout.js',
|
||||||
|
'/vendor/framework7-swipe.min.js',
|
||||||
'/theme-glass-controls.css',
|
'/theme-glass-controls.css',
|
||||||
'/saved-page.css',
|
'/saved-page.css',
|
||||||
'/party-dj.js',
|
'/party-dj.js',
|
||||||
|
|||||||
139
frontend/theme-f7.css
Normal file
139
frontend/theme-f7.css
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
/* Framework7 Swipe layout (html[data-layout="f7-swipe"]).
|
||||||
|
Keeps the classic look; only the Framework7 swipeout / sheet mechanics are
|
||||||
|
styled here, with the app's own tokens instead of Framework7's stylesheet. */
|
||||||
|
|
||||||
|
/* ---- Swipeout rows ---- */
|
||||||
|
[data-layout="f7-swipe"] .cards li.f7s-row {
|
||||||
|
list-style: none;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 12px;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
touch-action: pan-y;
|
||||||
|
}
|
||||||
|
[data-layout="f7-swipe"] .f7s-row > .swipeout-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
background: var(--bg);
|
||||||
|
transition: background 0.16s, border-color 0.16s;
|
||||||
|
}
|
||||||
|
[data-layout="f7-swipe"] .f7s-row.swipeout-active > .swipeout-content,
|
||||||
|
[data-layout="f7-swipe"] .f7s-row.swipeout-opened > .swipeout-content {
|
||||||
|
background: var(--bg-2);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
[data-layout="f7-swipe"] .f7s-row.swipeout-transitioning > .swipeout-content,
|
||||||
|
[data-layout="f7-swipe"] .f7s-row.swipeout-transitioning .f7s-act {
|
||||||
|
transition-duration: 0.3s;
|
||||||
|
transition-property: transform, left, width, background-color;
|
||||||
|
transition-timing-function: var(--ease, ease);
|
||||||
|
}
|
||||||
|
[data-layout="f7-swipe"] .f7s-row .swipeout-actions-left,
|
||||||
|
[data-layout="f7-swipe"] .f7s-row .swipeout-actions-right {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
[data-layout="f7-swipe"] .f7s-row .swipeout-actions-left { left: 0; }
|
||||||
|
[data-layout="f7-swipe"] .f7s-row .swipeout-actions-right { right: 0; }
|
||||||
|
[data-layout="f7-swipe"] .f7s-act {
|
||||||
|
position: relative;
|
||||||
|
left: 0;
|
||||||
|
transform: scale(0);
|
||||||
|
border: 1px solid var(--line, transparent);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg-3);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
[data-layout="f7-swipe"] .swipeout-actions-left .f7s-act { background: var(--accent); border-color: var(--accent); color: #fff; }
|
||||||
|
[data-layout="f7-swipe"] .f7s-act.f7s-danger { background: var(--accent-deep); border-color: var(--accent-deep); color: #fff; }
|
||||||
|
[data-layout="f7-swipe"] .f7s-row.swipeout-overswipe-active .f7s-danger { box-shadow: 0 0 0 3px var(--accent-glow); }
|
||||||
|
/* Swipe replaces the hover-only row buttons on touch screens. */
|
||||||
|
@media (hover: none) {
|
||||||
|
[data-layout="f7-swipe"] .f7s-row .card-del { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Up next sheet ---- */
|
||||||
|
.sheet-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 11000;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s, visibility 0.3s;
|
||||||
|
}
|
||||||
|
.sheet-backdrop.backdrop-in { opacity: 1; visibility: visible; pointer-events: auto; }
|
||||||
|
.f7s-sheet {
|
||||||
|
--f7-sheet-transition-duration: 300ms;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 12500;
|
||||||
|
display: none;
|
||||||
|
height: min(85vh, 720px);
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 18px 18px 0 0;
|
||||||
|
border: 1px solid var(--line, transparent);
|
||||||
|
border-bottom: 0;
|
||||||
|
background: var(--bg-1);
|
||||||
|
color: var(--text);
|
||||||
|
box-shadow: 0 -12px 40px rgba(0, 0, 0, 0.45);
|
||||||
|
transform: translate3d(0, 100%, 0);
|
||||||
|
transition-property: transform;
|
||||||
|
transition-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||||
|
will-change: transform;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
}
|
||||||
|
.f7s-sheet.modal-in, .f7s-sheet.modal-out { transition-duration: var(--f7-sheet-transition-duration); }
|
||||||
|
.f7s-sheet.not-animated { transition-duration: 0s; }
|
||||||
|
.f7s-sheet.modal-in { display: block; transform: translate3d(0, 0, 0); }
|
||||||
|
.f7s-sheet.modal-in-breakpoint, .f7s-sheet.modal-in-swipe-step {
|
||||||
|
display: block;
|
||||||
|
transform: translate3d(0, var(--f7-sheet-breakpoint, var(--f7-sheet-swipe-step, 0)), 0);
|
||||||
|
}
|
||||||
|
.f7s-sheet.modal-out { transform: translate3d(0, 100%, 0); }
|
||||||
|
.f7s-sheet .sheet-modal-inner { height: 100%; display: flex; flex-direction: column; }
|
||||||
|
.f7s-grab { flex: none; height: 22px; display: grid; place-items: center; cursor: grab; touch-action: none; }
|
||||||
|
.f7s-grab::before { content: ''; width: 40px; height: 5px; border-radius: 3px; background: var(--text-dim); opacity: 0.6; }
|
||||||
|
.f7s-sheet-head { flex: none; padding: 0 18px 10px; font-weight: 700; font-size: 15px; touch-action: none; }
|
||||||
|
.f7s-sheet-list { flex: 1; overflow-y: auto; margin: 0; padding: 0 10px 16px; list-style: none; }
|
||||||
|
.f7s-up { display: flex; gap: 12px; align-items: center; padding: 8px; border-radius: 12px; cursor: pointer; }
|
||||||
|
.f7s-up:active, .f7s-up:hover { background: var(--bg-2); }
|
||||||
|
.f7s-up img { width: 88px; aspect-ratio: 16 / 9; object-fit: cover; border-radius: 8px; flex: none; background: var(--bg-3); }
|
||||||
|
.f7s-up-info { min-width: 0; }
|
||||||
|
.f7s-up-title { font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
|
||||||
|
.f7s-up-ch { font-size: 12px; color: var(--text-dim); margin-top: 4px; }
|
||||||
|
.f7s-empty { color: var(--text-dim); padding: 12px 8px; margin: 0; }
|
||||||
|
|
||||||
|
/* A grab hint on the mini bar: swipe up for Up next, sideways to skip. */
|
||||||
|
[data-layout="f7-swipe"] #miniBar { touch-action: none; }
|
||||||
|
[data-layout="f7-swipe"] #miniBar .mini-bar-inner { position: relative; }
|
||||||
|
[data-layout="f7-swipe"] #miniBar .mini-bar-inner::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 50%;
|
||||||
|
width: 32px;
|
||||||
|
height: 4px;
|
||||||
|
margin-left: -16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--text-dim);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
13
frontend/vendor/framework7-swipe.entry.js
vendored
Normal file
13
frontend/vendor/framework7-swipe.entry.js
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Build: bun build framework7-swipe.entry.js --minify --format=iife (framework7@9.2.0 installed alongside)
|
||||||
|
import Framework7 from './node_modules/framework7/components/app/app-class.js';
|
||||||
|
import DeviceModule from './node_modules/framework7/modules/device/device.js';
|
||||||
|
import SupportModule from './node_modules/framework7/modules/support/support.js';
|
||||||
|
import UtilsModule from './node_modules/framework7/modules/utils/utils.js';
|
||||||
|
import ResizeModule from './node_modules/framework7/modules/resize/resize.js';
|
||||||
|
import TouchModule from './node_modules/framework7/modules/touch/touch.js';
|
||||||
|
import StoreModule from './node_modules/framework7/modules/store/store.js';
|
||||||
|
import Swipeout from './node_modules/framework7/components/swipeout/swipeout.js';
|
||||||
|
import Modal from './node_modules/framework7/components/modal/modal.js';
|
||||||
|
import Sheet from './node_modules/framework7/components/sheet/sheet.js';
|
||||||
|
Framework7.use([DeviceModule, SupportModule, UtilsModule, ResizeModule, TouchModule, StoreModule, Swipeout, Modal, Sheet]);
|
||||||
|
window.Framework7 = Framework7;
|
||||||
234
frontend/vendor/framework7-swipe.min.js
vendored
Normal file
234
frontend/vendor/framework7-swipe.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user