Add forward skip to mini player, home-screen app support, cover fallback
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1df566d85d
commit
7f59c6d27b
7 files changed
+41
-10
No files matched your search
+17
-8
@@ -8,11 +8,17 @@
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Audiobooks">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="apple-touch-icon" href="/icon-180.png">
|
||||
<title>Audiobook Player</title>
|
||||
<style>
|
||||
:root{--bg:#f6f6f4;--surf:#fff;--surf2:#ececea;--fg:#16161a;--mut:#74747c;--acc:#e8590c;--acc2:#ff8a3d;--bd:rgba(0,0,0,.08);--sh:0 8px 30px rgba(0,0,0,.12)}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#0f0f12;--surf:#1a1a1f;--surf2:#26262d;--fg:#f2f2f4;--mut:#8f8f99;--acc:#ff7a2f;--acc2:#ffb067;--bd:rgba(255,255,255,.08);--sh:0 8px 30px rgba(0,0,0,.5)}}
|
||||
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none}
|
||||
html{-webkit-text-size-adjust:100%;overscroll-behavior-y:none}
|
||||
button,a,.card,input[type=range]{touch-action:manipulation}
|
||||
button,.card,.ib,.pill{-webkit-user-select:none;user-select:none}
|
||||
html,body{margin:0}
|
||||
body{background:var(--bg);color:var(--fg);font:15px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif;padding-bottom:calc(100px + env(safe-area-inset-bottom))}
|
||||
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;padding:0}
|
||||
@@ -63,7 +69,7 @@ h2{font-size:18px;margin:26px 0 12px;letter-spacing:-.01em}
|
||||
#mini b,#mini span{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
#mini span{color:var(--mut);font-size:12.5px}
|
||||
#mini .mbar{position:absolute;left:0;bottom:0;height:3px;background:var(--acc);width:0}
|
||||
.ib{width:44px;height:44px;border-radius:50%;display:grid;place-items:center;font-size:22px;flex:none}
|
||||
.ib{position:relative;width:44px;height:44px;border-radius:50%;display:grid;place-items:center;font-size:22px;flex:none}
|
||||
.ib:hover{background:var(--surf2)}
|
||||
.pp{background:var(--acc)!important;color:#fff;box-shadow:0 4px 14px color-mix(in srgb,var(--acc) 50%,transparent)}
|
||||
|
||||
@@ -85,7 +91,7 @@ input[type=range]::-moz-range-thumb{width:16px;height:16px;border-radius:50%;bac
|
||||
.times{display:flex;justify-content:space-between;color:var(--mut);font-size:12px;font-variant-numeric:tabular-nums}
|
||||
.ctl{display:flex;justify-content:center;align-items:center;gap:18px;margin:14px 0}
|
||||
.ctl .ib{width:56px;height:56px;font-size:28px;position:relative}
|
||||
.ctl .ib small{position:absolute;font-size:10px;font-weight:800;top:50%;left:50%;transform:translate(-50%,-38%)}
|
||||
.ib small{position:absolute;font-size:10px;font-weight:800;top:50%;left:50%;transform:translate(-50%,-38%)}
|
||||
.ctl .pp{width:76px;height:76px;font-size:34px}
|
||||
.opts{display:flex;justify-content:center;gap:10px;margin:6px 0 14px}
|
||||
.pill{background:var(--surf);border:1px solid var(--bd);border-radius:99px;padding:8px 16px;font-weight:600;font-size:13.5px}
|
||||
@@ -133,7 +139,8 @@ a.pill{text-decoration:none;color:var(--fg);display:inline-block}
|
||||
|
||||
<div id="mini" class="hide" role="button" aria-label="Open player">
|
||||
<img id="mimg" alt=""><div class="meta"><b id="mtitle"></b><span id="msub"></span></div>
|
||||
<button class="ib" id="mback" aria-label="Back 30 seconds"><svg><use href="#i-back"/></svg></button>
|
||||
<button class="ib" id="mback" aria-label="Back 30 seconds"><svg><use href="#i-back"/></svg><small>30</small></button>
|
||||
<button class="ib" id="mfwd" aria-label="Forward 30 seconds"><svg><use href="#i-fwd"/></svg><small>30</small></button>
|
||||
<button class="ib pp" id="mplay" aria-label="Play or pause"><svg><use href="#i-play"/></svg></button>
|
||||
<div class="mbar" id="mbar"></div>
|
||||
</div>
|
||||
@@ -272,7 +279,8 @@ async function play(b, open) {
|
||||
saveProgress();
|
||||
current = b;
|
||||
$('mini').classList.remove('hide');
|
||||
for (const [i, t] of [['mimg', 'src'], ['bigcov', 'src']]) $(i).src = cover(b);
|
||||
const fallback = 'data:image/svg+xml,' + encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="hsl(${hue(title(b))} 55% 40%)"/><stop offset="1" stop-color="hsl(${hue(title(b)) + 40} 55% 28%)"/></linearGradient></defs><rect width="100" height="100" fill="url(#g)"/><text x="50" y="62" font-size="40" font-family="sans-serif" font-weight="700" text-anchor="middle" fill="#fff">${(title(b)[0] || '?').replace(/[<&]/g, '')}</text></svg>`);
|
||||
for (const i of ['mimg', 'bigcov']) { $(i).onerror = () => { $(i).onerror = null; $(i).src = fallback; }; $(i).src = cover(b); }
|
||||
$('fullbg').style.backgroundImage = `url("${cover(b)}")`;
|
||||
$('mtitle').textContent = $('ftitle').textContent = title(b);
|
||||
$('msub').textContent = $('fsub').textContent = author(b);
|
||||
@@ -287,7 +295,7 @@ async function play(b, open) {
|
||||
const ms = navigator.mediaSession;
|
||||
ms.metadata = new MediaMetadata({ title: title(b), artist: author(b), artwork: [{ src: cover(b) }] });
|
||||
ms.setActionHandler('seekbackward', () => skip(-30)); ms.setActionHandler('seekforward', () => skip(30));
|
||||
ms.setActionHandler('previoustrack', () => jump(-1)); ms.setActionHandler('nexttrack', () => jump(1));
|
||||
ms.setActionHandler('seekto', e => { audio.currentTime = e.seekTime; });
|
||||
ms.setActionHandler('play', () => audio.play()); ms.setActionHandler('pause', () => audio.pause());
|
||||
}
|
||||
}
|
||||
@@ -319,7 +327,7 @@ function jump(d) {
|
||||
const toggle = () => audio.paused ? audio.play() : audio.pause();
|
||||
for (const id of ['play', 'mplay']) $(id).onclick = e => { e.stopPropagation(); toggle(); };
|
||||
for (const id of ['back', 'mback']) $(id).onclick = e => { e.stopPropagation(); skip(-30); };
|
||||
$('fwd').onclick = () => skip(30); $('pch').onclick = () => jump(-1); $('nch').onclick = () => jump(1);
|
||||
for (const id of ['fwd', 'mfwd']) $(id).onclick = e => { e.stopPropagation(); skip(30); }; $('pch').onclick = () => jump(-1); $('nch').onclick = () => jump(1);
|
||||
audio.onplay = () => { ico('play', 'pause'); ico('mplay', 'pause'); };
|
||||
audio.onpause = () => { ico('play', 'play'); ico('mplay', 'play'); saveProgress(); };
|
||||
audio.onended = () => { if (current) setProg(bookId(current), { pos: 0, dur: audio.duration, t: Date.now() }); render(); };
|
||||
@@ -352,11 +360,12 @@ async function pull() {
|
||||
} catch {}
|
||||
}
|
||||
function saveProgress() { if (current && audio.duration) setProg(bookId(current), { pos: audio.currentTime, dur: audio.duration, t: Date.now() }); }
|
||||
let lastSave = 0;
|
||||
let lastSave = 0, lastPos = 0;
|
||||
audio.ontimeupdate = () => {
|
||||
const d = audio.duration || 0, t = audio.currentTime;
|
||||
$('seek').max = d || 1; $('seek').value = t; $('seek').style.setProperty('--p', d ? t / d * 100 + '%' : '0%');
|
||||
$('mbar').style.width = d ? t / d * 100 + '%' : '0';
|
||||
if (d && 'mediaSession' in navigator && navigator.mediaSession.setPositionState && Date.now() - lastPos > 1000) { lastPos = Date.now(); try { navigator.mediaSession.setPositionState({ duration: d, position: Math.min(t, d), playbackRate: audio.playbackRate }); } catch {} }
|
||||
$('cur').textContent = fmt(t); $('left').textContent = '-' + fmt((d - t) / audio.playbackRate);
|
||||
if (Date.now() - lastSave > 5000) { lastSave = Date.now(); saveProgress(); }
|
||||
let i = -1; chapters.forEach((c, k) => { if (t >= c.start) i = k; });
|
||||
|
||||
Reference in new issue
Block a user