chore: add and update gitignore rules for project files
add a comprehensive gitignore file to exclude unnecessary files like dependencies, runtime data, uploads, local configs and debug temporary files
This commit is contained in:
@@ -0,0 +1,961 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="apple-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="八音盒">
|
||||
<meta name="theme-color" content="#0f0e17">
|
||||
<title>八音盒</title>
|
||||
|
||||
<!-- PWA Manifest (inline via JS for single-file demo) -->
|
||||
<style>
|
||||
/* ── Design Tokens ─────────────────────────────────────── */
|
||||
:root {
|
||||
--bg: #0f0e17;
|
||||
--surface: #1a1928;
|
||||
--surface2: #232235;
|
||||
--border: rgba(255,255,255,0.07);
|
||||
--gold: #e8b86d;
|
||||
--gold-dim: #a07840;
|
||||
--text: #e8e6f0;
|
||||
--text-dim: #7a7890;
|
||||
--radius: 16px;
|
||||
--bar-h: 88px;
|
||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
}
|
||||
|
||||
/* ── Reset ─────────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body {
|
||||
height: 100%;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: -apple-system, 'PingFang SC', sans-serif;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
/* ── Layout ─────────────────────────────────────────────── */
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100dvh;
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ── Header ─────────────────────────────────────────────── */
|
||||
.header {
|
||||
padding: 20px 20px 12px;
|
||||
padding-top: calc(20px + env(safe-area-inset-top, 0px));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text);
|
||||
}
|
||||
.header-title span { color: var(--gold); }
|
||||
.header-count {
|
||||
font-size: 13px;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* ── View Toggle ─────────────────────────────────────────── */
|
||||
.view-toggle {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
background: var(--surface);
|
||||
border-radius: 10px;
|
||||
padding: 3px;
|
||||
margin: 0 20px 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.view-btn {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-dim);
|
||||
font-size: 13px;
|
||||
padding: 7px 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
font-family: inherit;
|
||||
}
|
||||
.view-btn.active {
|
||||
background: var(--surface2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Now Playing Card (mini) ─────────────────────────────── */
|
||||
.now-playing-hero {
|
||||
margin: 0 20px 20px;
|
||||
padding: 20px;
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
/* Signature: subtle inner glow on playing state */
|
||||
transition: box-shadow 0.6s;
|
||||
}
|
||||
.now-playing-hero.playing {
|
||||
box-shadow: 0 0 40px rgba(232, 184, 109, 0.08);
|
||||
}
|
||||
|
||||
/* ── Vinyl Disc ──────────────────────────────────────────── */
|
||||
/* SIGNATURE: rotating vinyl with subtle groove rings */
|
||||
.disc-wrap {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.disc {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
radial-gradient(circle at 50% 50%,
|
||||
#fff 0%, #fff 7%,
|
||||
transparent 7.5%, transparent 14%,
|
||||
rgba(255,255,255,0.03) 14%, rgba(255,255,255,0.03) 14.5%,
|
||||
transparent 15%, transparent 29%,
|
||||
rgba(255,255,255,0.03) 29%, rgba(255,255,255,0.03) 29.5%,
|
||||
transparent 30%, transparent 44%,
|
||||
rgba(255,255,255,0.03) 44%, rgba(255,255,255,0.03) 44.5%,
|
||||
transparent 45%),
|
||||
conic-gradient(from 0deg,
|
||||
#1a1928 0deg, #222130 60deg, #1a1928 120deg,
|
||||
#222130 180deg, #1a1928 240deg, #222130 300deg, #1a1928 360deg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: spin 8s linear infinite;
|
||||
animation-play-state: paused;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.5);
|
||||
}
|
||||
.disc.spinning { animation-play-state: running; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.disc-art {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 2px solid rgba(255,255,255,0.1);
|
||||
background: var(--surface2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
font-size: 20px;
|
||||
}
|
||||
.disc-art-placeholder {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: var(--surface2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
border: 2px solid rgba(255,255,255,0.08);
|
||||
}
|
||||
|
||||
/* ── Track Info ──────────────────────────────────────────── */
|
||||
.track-info {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
.track-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.track-artist {
|
||||
font-size: 13px;
|
||||
color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ── Progress ────────────────────────────────────────────── */
|
||||
.progress-wrap {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.progress-bar {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: var(--surface2);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.progress-bar::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: var(--gold);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 6px rgba(232,184,109,0.4);
|
||||
}
|
||||
.progress-bar::-webkit-slider-runnable-track {
|
||||
height: 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.time-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ── Controls ────────────────────────────────────────────── */
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 28px;
|
||||
width: 100%;
|
||||
}
|
||||
.ctrl-btn {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.15s, transform 0.1s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.ctrl-btn:active { transform: scale(0.9); }
|
||||
.ctrl-btn.active-mode { color: var(--gold); }
|
||||
.ctrl-btn svg { display: block; }
|
||||
|
||||
.play-btn {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
min-width: 56px;
|
||||
min-height: 56px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background: var(--gold);
|
||||
color: var(--bg);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.1s, background 0.2s;
|
||||
box-shadow: 0 4px 16px rgba(232,184,109,0.3);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.play-btn:active { transform: scale(0.94); }
|
||||
|
||||
/* ── Volume ──────────────────────────────────────────────── */
|
||||
.volume-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
.vol-icon { color: var(--text-dim); flex-shrink: 0; }
|
||||
.volume-slider {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
background: var(--surface2);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.volume-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── Playlist ────────────────────────────────────────────── */
|
||||
.playlist-wrap {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 20px;
|
||||
padding-bottom: calc(16px + var(--safe-bottom));
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.playlist-wrap::-webkit-scrollbar { display: none; }
|
||||
|
||||
.playlist-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.playlist-item:last-child { border-bottom: none; }
|
||||
.playlist-item:active { opacity: 0.6; }
|
||||
.playlist-item.active .item-title { color: var(--gold); }
|
||||
|
||||
.item-num {
|
||||
font-size: 13px;
|
||||
color: var(--text-dim);
|
||||
width: 24px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.item-num.playing-icon { color: var(--gold); }
|
||||
|
||||
.item-info { flex: 1; min-width: 0; }
|
||||
.item-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.item-sub {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.item-duration {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
flex-shrink: 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ── Mini Bar (bottom, when in list view) ─────────────────── */
|
||||
/* We use a single-view approach so no mini bar needed */
|
||||
|
||||
/* ── Loading / Empty ─────────────────────────────────────── */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 48px 20px;
|
||||
color: var(--text-dim);
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.empty-state .icon { font-size: 36px; margin-bottom: 12px; }
|
||||
|
||||
/* ── Toast ───────────────────────────────────────────────── */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: calc(100px + var(--safe-bottom));
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(10px);
|
||||
background: rgba(35,34,53,0.95);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border);
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
z-index: 100;
|
||||
}
|
||||
.toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="app">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="header-title">
|
||||
<!-- 禅坐图标 -->
|
||||
<svg width="28" height="28" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
style="display:inline-block;vertical-align:middle;margin-right:8px;margin-bottom:2px;">
|
||||
<!-- aura ring -->
|
||||
<circle cx="50" cy="44" r="32" stroke="#e8b86d" stroke-width="1.2" opacity="0.25"/>
|
||||
<!-- ground shadow -->
|
||||
<ellipse cx="50" cy="84" rx="22" ry="4" fill="rgba(232,184,109,0.12)"/>
|
||||
<!-- left leg -->
|
||||
<path d="M33 66 C24 65 16 70 14 78 C12 84 16 88 24 87 C32 86 42 79 44 72 L44 67 Z" fill="#e8e6f0"/>
|
||||
<!-- right leg -->
|
||||
<path d="M67 66 C76 65 84 70 86 78 C88 84 84 88 76 87 C68 86 58 79 56 72 L56 67 Z" fill="#e8e6f0"/>
|
||||
<!-- torso -->
|
||||
<path d="M44 67 C44 67 40 58 40 50 C40 40 44 34 48 30 L50 29 L52 30 C56 34 60 40 60 50 C60 58 56 67 56 67 C53 74 47 74 44 67 Z" fill="#e8e6f0"/>
|
||||
<!-- hands / mudra -->
|
||||
<ellipse cx="50" cy="72" rx="10" ry="4" fill="#e8e6f0"/>
|
||||
<!-- neck -->
|
||||
<rect x="46" y="25" width="8" height="7" rx="2" fill="#e8e6f0"/>
|
||||
<!-- head -->
|
||||
<ellipse cx="50" cy="17" rx="11" ry="12" fill="#e8e6f0"/>
|
||||
<!-- hair bun -->
|
||||
<ellipse cx="50" cy="6" rx="5" ry="4" fill="#e8e6f0"/>
|
||||
<!-- closed eyes -->
|
||||
<path d="M44 16 Q47 13 50 16" stroke="#0f0e17" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<path d="M50 16 Q53 13 56 16" stroke="#0f0e17" stroke-width="1.2" stroke-linecap="round"/>
|
||||
<!-- sound waves left -->
|
||||
<path d="M34 36 Q28 33 28 29 Q28 25 34 22" stroke="#e8b86d" stroke-width="1.2" stroke-linecap="round" fill="none" opacity="0.6"/>
|
||||
<path d="M31 40 Q22 36 22 29 Q22 22 31 18" stroke="#e8b86d" stroke-width="0.9" stroke-linecap="round" fill="none" opacity="0.35"/>
|
||||
<!-- sound waves right -->
|
||||
<path d="M66 36 Q72 33 72 29 Q72 25 66 22" stroke="#e8b86d" stroke-width="1.2" stroke-linecap="round" fill="none" opacity="0.6"/>
|
||||
<path d="M69 40 Q78 36 78 29 Q78 22 69 18" stroke="#e8b86d" stroke-width="0.9" stroke-linecap="round" fill="none" opacity="0.35"/>
|
||||
<!-- crown dot -->
|
||||
<circle cx="50" cy="7" r="1.5" fill="#e8b86d" opacity="0.8"/>
|
||||
</svg>
|
||||
八<span>音</span>盒
|
||||
</div>
|
||||
<div class="header-count" id="trackCount">0 首</div>
|
||||
</div>
|
||||
|
||||
<!-- View Toggle -->
|
||||
<div class="view-toggle">
|
||||
<button class="view-btn active" id="btnPlayer" onclick="switchView('player')">正在播放</button>
|
||||
<button class="view-btn" id="btnList" onclick="switchView('list')">播放列表</button>
|
||||
</div>
|
||||
|
||||
<!-- ── Player View ── -->
|
||||
<div id="viewPlayer">
|
||||
|
||||
<div class="now-playing-hero" id="heroCard">
|
||||
<!-- Disc -->
|
||||
<div class="disc-wrap">
|
||||
<div class="disc" id="disc">
|
||||
<div class="disc-art-placeholder" id="discArt">
|
||||
<svg width="36" height="36" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M33 66 C24 65 16 70 14 78 C12 84 16 88 24 87 C32 86 42 79 44 72 L44 67 Z" fill="rgba(232,184,109,0.85)"/>
|
||||
<path d="M67 66 C76 65 84 70 86 78 C88 84 84 88 76 87 C68 86 58 79 56 72 L56 67 Z" fill="rgba(232,184,109,0.85)"/>
|
||||
<path d="M44 67 C44 67 40 58 40 50 C40 40 44 34 48 30 L50 29 L52 30 C56 34 60 40 60 50 C60 58 56 67 56 67 C53 74 47 74 44 67 Z" fill="rgba(232,184,109,0.85)"/>
|
||||
<ellipse cx="50" cy="72" rx="10" ry="4" fill="rgba(232,184,109,0.85)"/>
|
||||
<rect x="46" y="25" width="8" height="7" rx="2" fill="rgba(232,184,109,0.85)"/>
|
||||
<ellipse cx="50" cy="17" rx="11" ry="12" fill="rgba(232,184,109,0.85)"/>
|
||||
<ellipse cx="50" cy="6" rx="5" ry="4" fill="rgba(232,184,109,0.85)"/>
|
||||
<path d="M44 16 Q47 13 50 16" stroke="#1a1928" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M50 16 Q53 13 56 16" stroke="#1a1928" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M34 36 Q28 33 28 29 Q28 25 34 22" stroke="rgba(232,184,109,0.5)" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M66 36 Q72 33 72 29 Q72 25 66 22" stroke="rgba(232,184,109,0.5)" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="50" cy="5" r="2" fill="rgba(232,184,109,0.9)"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Track Info -->
|
||||
<div class="track-info">
|
||||
<div class="track-title" id="trackTitle">尚未选择曲目</div>
|
||||
<div class="track-artist" id="trackArtist">—</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress -->
|
||||
<div class="progress-wrap">
|
||||
<input type="range" class="progress-bar" id="progressBar" min="0" max="100" value="0"
|
||||
oninput="onProgressInput(this.value)" onchange="onProgressChange(this.value)">
|
||||
<div class="time-row">
|
||||
<span id="timeCurrent">0:00</span>
|
||||
<span id="timeTotal">0:00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<!-- Shuffle -->
|
||||
<button class="ctrl-btn" id="btnShuffle" onclick="toggleShuffle()" title="随机">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="16 3 21 3 21 8"/>
|
||||
<path d="M4 4 C8 4 10 8 12 12 C14 16 16 20 20 20"/>
|
||||
<polyline points="16 21 21 21 21 16"/>
|
||||
<path d="M4 20 C8 20 10 16 12 12 C14 8 16 4 20 4"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Prev -->
|
||||
<button class="ctrl-btn" onclick="prevTrack()" title="上一首">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M6 6h2v12H6V6zm3.5 6 8.5 6V6l-8.5 6z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Play/Pause -->
|
||||
<button class="play-btn" id="playBtn" onclick="togglePlay()" title="播放/暂停">
|
||||
<svg id="iconPlay" width="26" height="26" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
<svg id="iconPause" width="26" height="26" viewBox="0 0 24 24" fill="currentColor" style="display:none">
|
||||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Next -->
|
||||
<button class="ctrl-btn" onclick="nextTrack()" title="下一首">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M16 6h2v12h-2V6zm-10 0v12l8.5-6L6 6z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Repeat -->
|
||||
<button class="ctrl-btn" id="btnRepeat" onclick="cycleRepeat()" title="循环">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/>
|
||||
<polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Volume -->
|
||||
<div class="volume-row">
|
||||
<span class="vol-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input type="range" class="volume-slider" id="volSlider" min="0" max="1" step="0.01" value="1"
|
||||
oninput="setVolume(this.value)">
|
||||
<span class="vol-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0 0 14 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM18.5 12c0-2.77-1.5-5.18-3.5-6.5v13c2-.82 3.5-3.43 3.5-6.5z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── List View ── -->
|
||||
<div id="viewList" style="display:none; flex:1; overflow:hidden; display:none; flex-direction:column;">
|
||||
<div class="playlist-wrap" id="playlistWrap">
|
||||
<div class="empty-state" id="emptyState">
|
||||
<div class="icon">🎶</div>
|
||||
<div>暂无曲目<br>请配置 OSS 音乐列表</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<!-- Hidden Audio -->
|
||||
<audio id="audio" preload="metadata"></audio>
|
||||
|
||||
<script>
|
||||
// ─── Config ─────────────────────────────────────────────────
|
||||
// 修改这里来接入你的 OSS 文件列表
|
||||
// 每条: { title, artist, src, cover(可选) }
|
||||
const PLAYLIST = [
|
||||
{
|
||||
title: "花与你",
|
||||
artist: "Affloret",
|
||||
src: "https://android-o1-images.oss-cn-shanghai.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/file_cabinet/090_%E8%8A%B1%E4%B8%8E%E4%BD%A0_%E8%8A%B1%E4%B8%8E%E5%BF%83%E4%BA%8BAffloret.m4a",
|
||||
cover: ""
|
||||
},
|
||||
{
|
||||
title: "心跳花火",
|
||||
artist: "Affloret",
|
||||
src: "https://android-o1-images.oss-cn-shanghai.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/file_cabinet/091_%E5%BF%83%E8%B7%B3%E8%8A%B1%E7%81%AB_%E8%8A%B1%E4%B8%8E%E5%BF%83%E4%BA%8BAffloret.m4a",
|
||||
cover: ""
|
||||
},
|
||||
{
|
||||
title: "Dear",
|
||||
artist: "lecca",
|
||||
src: "https://android-o1-images.oss-cn-shanghai.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/file_cabinet/072_Dear_lecca.m4a",
|
||||
cover: ""
|
||||
},
|
||||
{
|
||||
title: "Happy (2021 ver.)",
|
||||
artist: "ハニースパイス Re..",
|
||||
src: "https://android-o1-images.oss-cn-shanghai.aliyuncs.com/550e8400-e29b-41d4-a716-446655442082/file_cabinet/075_Happy(2021ver.)_%E3%83%8F%E3%83%8B%E3%83%BC%E3%82%B9%E3%83%91%E3%82%A4%E3%82%B9Re..m4a",
|
||||
cover: ""
|
||||
},
|
||||
];
|
||||
|
||||
// ─── State ───────────────────────────────────────────────────
|
||||
let currentIdx = 0;
|
||||
let isPlaying = false;
|
||||
let isSeeking = false;
|
||||
let repeatMode = 0; // 0=顺序 1=单曲 2=列表循环
|
||||
let shuffleOn = false;
|
||||
let shuffleOrder = [];
|
||||
|
||||
const audio = document.getElementById('audio');
|
||||
|
||||
// ─── Init ────────────────────────────────────────────────────
|
||||
function init() {
|
||||
document.getElementById('trackCount').textContent = PLAYLIST.length + ' 首';
|
||||
buildShuffleOrder();
|
||||
if (PLAYLIST.length > 0) loadTrack(0, false);
|
||||
renderPlaylist();
|
||||
setupMediaSession();
|
||||
registerSW();
|
||||
}
|
||||
|
||||
// ─── Track Loading ───────────────────────────────────────────
|
||||
function loadTrack(idx, autoplay) {
|
||||
currentIdx = idx;
|
||||
const t = PLAYLIST[idx];
|
||||
if (!t) return;
|
||||
|
||||
audio.src = t.src;
|
||||
audio.load();
|
||||
|
||||
document.getElementById('trackTitle').textContent = t.title || '未知曲目';
|
||||
document.getElementById('trackArtist').textContent = t.artist || '未知歌手';
|
||||
document.getElementById('timeCurrent').textContent = '0:00';
|
||||
document.getElementById('timeTotal').textContent = '0:00';
|
||||
document.getElementById('progressBar').value = 0;
|
||||
|
||||
// Cover art
|
||||
const artEl = document.getElementById('discArt');
|
||||
if (t.cover) {
|
||||
artEl.innerHTML = `<img src="${t.cover}" style="width:100%;height:100%;border-radius:50%;object-fit:cover;">`;
|
||||
} else {
|
||||
artEl.innerHTML = '<svg width="36" height="36" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M33 66 C24 65 16 70 14 78 C12 84 16 88 24 87 C32 86 42 79 44 72 L44 67 Z" fill="rgba(232,184,109,0.85)"/><path d="M67 66 C76 65 84 70 86 78 C88 84 84 88 76 87 C68 86 58 79 56 72 L56 67 Z" fill="rgba(232,184,109,0.85)"/><path d="M44 67 C44 67 40 58 40 50 C40 40 44 34 48 30 L50 29 L52 30 C56 34 60 40 60 50 C60 58 56 67 56 67 C53 74 47 74 44 67 Z" fill="rgba(232,184,109,0.85)"/><ellipse cx="50" cy="72" rx="10" ry="4" fill="rgba(232,184,109,0.85)"/><rect x="46" y="25" width="8" height="7" rx="2" fill="rgba(232,184,109,0.85)"/><ellipse cx="50" cy="17" rx="11" ry="12" fill="rgba(232,184,109,0.85)"/><ellipse cx="50" cy="6" rx="5" ry="4" fill="rgba(232,184,109,0.85)"/><path d="M44 16 Q47 13 50 16" stroke="#1a1928" stroke-width="2" stroke-linecap="round"/><path d="M50 16 Q53 13 56 16" stroke="#1a1928" stroke-width="2" stroke-linecap="round"/><path d="M34 36 Q28 33 28 29 Q28 25 34 22" stroke="rgba(232,184,109,0.5)" stroke-width="2" stroke-linecap="round"/><path d="M66 36 Q72 33 72 29 Q72 25 66 22" stroke="rgba(232,184,109,0.5)" stroke-width="2" stroke-linecap="round"/><circle cx="50" cy="5" r="2" fill="rgba(232,184,109,0.9)"/></svg>';
|
||||
}
|
||||
|
||||
updatePlaylistHighlight();
|
||||
updateMediaSessionMetadata();
|
||||
|
||||
if (autoplay) playAudio();
|
||||
}
|
||||
|
||||
// ─── Play/Pause ──────────────────────────────────────────────
|
||||
function togglePlay() {
|
||||
if (!PLAYLIST.length) { showToast('播放列表为空'); return; }
|
||||
if (!audio.src || audio.src === location.href) { loadTrack(0, true); return; }
|
||||
isPlaying ? pauseAudio() : playAudio();
|
||||
}
|
||||
|
||||
function playAudio() {
|
||||
audio.play().then(() => {
|
||||
isPlaying = true;
|
||||
updatePlayUI(true);
|
||||
}).catch(e => {
|
||||
console.warn('Play failed', e);
|
||||
showToast('播放失败,请检查音频地址');
|
||||
});
|
||||
}
|
||||
|
||||
function pauseAudio() {
|
||||
audio.pause();
|
||||
isPlaying = false;
|
||||
updatePlayUI(false);
|
||||
}
|
||||
|
||||
function updatePlayUI(playing) {
|
||||
document.getElementById('iconPlay').style.display = playing ? 'none' : 'block';
|
||||
document.getElementById('iconPause').style.display = playing ? 'block' : 'none';
|
||||
const disc = document.getElementById('disc');
|
||||
disc.classList.toggle('spinning', playing);
|
||||
document.getElementById('heroCard').classList.toggle('playing', playing);
|
||||
if ('mediaSession' in navigator) {
|
||||
navigator.mediaSession.playbackState = playing ? 'playing' : 'paused';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Navigation ──────────────────────────────────────────────
|
||||
function nextTrack() {
|
||||
if (!PLAYLIST.length) return;
|
||||
if (repeatMode === 1) { audio.currentTime = 0; if (isPlaying) playAudio(); return; }
|
||||
let next;
|
||||
if (shuffleOn) {
|
||||
const pos = shuffleOrder.indexOf(currentIdx);
|
||||
next = shuffleOrder[(pos + 1) % shuffleOrder.length];
|
||||
} else {
|
||||
next = (currentIdx + 1) % PLAYLIST.length;
|
||||
}
|
||||
loadTrack(next, isPlaying);
|
||||
}
|
||||
|
||||
function prevTrack() {
|
||||
if (!PLAYLIST.length) return;
|
||||
if (audio.currentTime > 3) { audio.currentTime = 0; return; }
|
||||
let prev;
|
||||
if (shuffleOn) {
|
||||
const pos = shuffleOrder.indexOf(currentIdx);
|
||||
prev = shuffleOrder[(pos - 1 + shuffleOrder.length) % shuffleOrder.length];
|
||||
} else {
|
||||
prev = (currentIdx - 1 + PLAYLIST.length) % PLAYLIST.length;
|
||||
}
|
||||
loadTrack(prev, isPlaying);
|
||||
}
|
||||
|
||||
// ─── Shuffle / Repeat ────────────────────────────────────────
|
||||
function buildShuffleOrder() {
|
||||
shuffleOrder = [...Array(PLAYLIST.length).keys()];
|
||||
for (let i = shuffleOrder.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffleOrder[i], shuffleOrder[j]] = [shuffleOrder[j], shuffleOrder[i]];
|
||||
}
|
||||
}
|
||||
|
||||
function toggleShuffle() {
|
||||
shuffleOn = !shuffleOn;
|
||||
if (shuffleOn) buildShuffleOrder();
|
||||
document.getElementById('btnShuffle').classList.toggle('active-mode', shuffleOn);
|
||||
showToast(shuffleOn ? '随机播放 开' : '随机播放 关');
|
||||
}
|
||||
|
||||
const repeatLabels = ['顺序播放', '单曲循环', '列表循环'];
|
||||
function cycleRepeat() {
|
||||
repeatMode = (repeatMode + 1) % 3;
|
||||
const btn = document.getElementById('btnRepeat');
|
||||
btn.classList.toggle('active-mode', repeatMode > 0);
|
||||
// Show repeat-one indicator
|
||||
btn.innerHTML = repeatMode === 1
|
||||
? `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/><text x="11" y="14" font-size="6" fill="currentColor" stroke="none" text-anchor="middle">1</text></svg>`
|
||||
: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg>`;
|
||||
showToast(repeatLabels[repeatMode]);
|
||||
}
|
||||
|
||||
// ─── Progress ────────────────────────────────────────────────
|
||||
function onProgressInput(val) {
|
||||
isSeeking = true;
|
||||
updateSliderFill(document.getElementById('progressBar'), val);
|
||||
}
|
||||
function onProgressChange(val) {
|
||||
if (audio.duration) audio.currentTime = (val / 100) * audio.duration;
|
||||
isSeeking = false;
|
||||
}
|
||||
|
||||
function setVolume(val) {
|
||||
audio.volume = val;
|
||||
updateSliderFill(document.getElementById('volSlider'), val * 100);
|
||||
}
|
||||
|
||||
function updateSliderFill(el, pct) {
|
||||
const fill = 'linear-gradient(to right, var(--gold) 0%, var(--gold) ' + pct + '%, var(--surface2) ' + pct + '%, var(--surface2) 100%)';
|
||||
el.style.background = fill;
|
||||
}
|
||||
|
||||
function fmtTime(s) {
|
||||
if (isNaN(s) || s < 0) return '0:00';
|
||||
const m = Math.floor(s / 60), sec = Math.floor(s % 60);
|
||||
return m + ':' + String(sec).padStart(2, '0');
|
||||
}
|
||||
|
||||
// ─── Audio Events ────────────────────────────────────────────
|
||||
audio.addEventListener('timeupdate', () => {
|
||||
if (isSeeking) return;
|
||||
const pct = audio.duration ? (audio.currentTime / audio.duration) * 100 : 0;
|
||||
document.getElementById('progressBar').value = pct;
|
||||
updateSliderFill(document.getElementById('progressBar'), pct);
|
||||
document.getElementById('timeCurrent').textContent = fmtTime(audio.currentTime);
|
||||
// Update Media Session position
|
||||
if ('mediaSession' in navigator && audio.duration) {
|
||||
try {
|
||||
navigator.mediaSession.setPositionState({
|
||||
duration: audio.duration,
|
||||
playbackRate: audio.playbackRate,
|
||||
position: audio.currentTime
|
||||
});
|
||||
} catch(e) {}
|
||||
}
|
||||
});
|
||||
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
document.getElementById('timeTotal').textContent = fmtTime(audio.duration);
|
||||
});
|
||||
|
||||
audio.addEventListener('ended', () => {
|
||||
isPlaying = false;
|
||||
updatePlayUI(false);
|
||||
if (repeatMode === 1) {
|
||||
audio.currentTime = 0; playAudio();
|
||||
} else if (repeatMode === 2 || currentIdx < PLAYLIST.length - 1) {
|
||||
nextTrack();
|
||||
}
|
||||
});
|
||||
|
||||
audio.addEventListener('play', () => { isPlaying = true; updatePlayUI(true); });
|
||||
audio.addEventListener('pause', () => { isPlaying = false; updatePlayUI(false); });
|
||||
|
||||
// ─── Playlist UI ─────────────────────────────────────────────
|
||||
function renderPlaylist() {
|
||||
const wrap = document.getElementById('playlistWrap');
|
||||
const empty = document.getElementById('emptyState');
|
||||
if (!PLAYLIST.length) { empty.style.display = 'block'; return; }
|
||||
empty.style.display = 'none';
|
||||
|
||||
// Remove old items
|
||||
wrap.querySelectorAll('.playlist-item').forEach(el => el.remove());
|
||||
|
||||
PLAYLIST.forEach((t, i) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'playlist-item' + (i === currentIdx ? ' active' : '');
|
||||
div.dataset.idx = i;
|
||||
div.innerHTML = `
|
||||
<div class="item-num" id="itemNum${i}">${i + 1}</div>
|
||||
<div class="item-info">
|
||||
<div class="item-title">${escHtml(t.title || '未知曲目')}</div>
|
||||
<div class="item-sub">${escHtml(t.artist || '未知歌手')}</div>
|
||||
</div>
|
||||
<div class="item-duration" id="itemDur${i}">—</div>
|
||||
`;
|
||||
div.addEventListener('click', () => {
|
||||
if (i === currentIdx) { togglePlay(); }
|
||||
else { loadTrack(i, true); switchView('player'); }
|
||||
});
|
||||
wrap.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function updatePlaylistHighlight() {
|
||||
document.querySelectorAll('.playlist-item').forEach((el, i) => {
|
||||
el.classList.toggle('active', i === currentIdx);
|
||||
const num = document.getElementById('itemNum' + i);
|
||||
if (num) {
|
||||
num.textContent = (i === currentIdx && isPlaying) ? '▶' : (i + 1);
|
||||
num.className = 'item-num' + (i === currentIdx ? ' playing-icon' : '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── View Switch ─────────────────────────────────────────────
|
||||
function switchView(v) {
|
||||
const isPlayer = v === 'player';
|
||||
document.getElementById('viewPlayer').style.display = isPlayer ? 'block' : 'none';
|
||||
const listView = document.getElementById('viewList');
|
||||
listView.style.display = isPlayer ? 'none' : 'flex';
|
||||
document.getElementById('btnPlayer').classList.toggle('active', isPlayer);
|
||||
document.getElementById('btnList').classList.toggle('active', !isPlayer);
|
||||
}
|
||||
|
||||
// ─── Media Session API (锁屏控制) ────────────────────────────
|
||||
function setupMediaSession() {
|
||||
if (!('mediaSession' in navigator)) return;
|
||||
navigator.mediaSession.setActionHandler('play', () => playAudio());
|
||||
navigator.mediaSession.setActionHandler('pause', () => pauseAudio());
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => nextTrack());
|
||||
navigator.mediaSession.setActionHandler('previoustrack',() => prevTrack());
|
||||
navigator.mediaSession.setActionHandler('seekto', (e) => {
|
||||
if (e.seekTime !== undefined && audio.duration) {
|
||||
audio.currentTime = e.seekTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateMediaSessionMetadata() {
|
||||
if (!('mediaSession' in navigator)) return;
|
||||
const t = PLAYLIST[currentIdx];
|
||||
if (!t) return;
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: t.title || '未知曲目',
|
||||
artist: t.artist || '未知歌手',
|
||||
album: '八音盒',
|
||||
artwork: t.cover ? [
|
||||
{ src: t.cover, sizes: '512x512', type: 'image/jpeg' }
|
||||
] : []
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Service Worker (PWA 后台播放) ──────────────────────────
|
||||
// iOS Safari 要求: 页面首次用户交互后才能播放,后台播放依赖 Media Session
|
||||
// SW 主要用于缓存页面资源使其可离线安装为 PWA
|
||||
function registerSW() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
// 实际部署时把 SW 代码写成 /sw.js 文件
|
||||
// 这里生成一个内联 blob SW 用于演示
|
||||
const swCode = `
|
||||
const CACHE = 'music-box-v1';
|
||||
self.addEventListener('install', e => {
|
||||
self.skipWaiting();
|
||||
e.waitUntil(caches.open(CACHE).then(c => c.addAll(['/'])));
|
||||
});
|
||||
self.addEventListener('activate', e => {
|
||||
e.waitUntil(clients.claim());
|
||||
});
|
||||
self.addEventListener('fetch', e => {
|
||||
// 音频文件不缓存(走网络),页面资源走缓存优先
|
||||
if (e.request.url.includes('.mp3') || e.request.url.includes('oss')) {
|
||||
e.respondWith(fetch(e.request));
|
||||
return;
|
||||
}
|
||||
e.respondWith(
|
||||
caches.match(e.request).then(r => r || fetch(e.request))
|
||||
);
|
||||
});
|
||||
`;
|
||||
const blob = new Blob([swCode], { type: 'application/javascript' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
navigator.serviceWorker.register(url).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── PWA Manifest ────────────────────────────────────────────
|
||||
(function injectManifest() {
|
||||
const manifest = {
|
||||
name: '八音盒',
|
||||
short_name: '八音盒',
|
||||
start_url: '/',
|
||||
display: 'standalone',
|
||||
background_color: '#0f0e17',
|
||||
theme_color: '#0f0e17',
|
||||
icons: [
|
||||
{ src: 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="20" fill="#0f0e17"/><circle cx="50" cy="44" r="32" stroke="#e8b86d" stroke-width="1.2" opacity="0.25" fill="none"/><path d="M33 66 C24 65 16 70 14 78 C12 84 16 88 24 87 C32 86 42 79 44 72 L44 67 Z" fill="#e8e6f0"/><path d="M67 66 C76 65 84 70 86 78 C88 84 84 88 76 87 C68 86 58 79 56 72 L56 67 Z" fill="#e8e6f0"/><path d="M44 67 C44 67 40 58 40 50 C40 40 44 34 48 30 L50 29 L52 30 C56 34 60 40 60 50 C60 58 56 67 56 67 C53 74 47 74 44 67 Z" fill="#e8e6f0"/><ellipse cx="50" cy="72" rx="10" ry="4" fill="#e8e6f0"/><rect x="46" y="25" width="8" height="7" rx="2" fill="#e8e6f0"/><ellipse cx="50" cy="17" rx="11" ry="12" fill="#e8e6f0"/><ellipse cx="50" cy="6" rx="5" ry="4" fill="#e8e6f0"/><path d="M34 36 Q28 33 28 29 Q28 25 34 22" stroke="#e8b86d" stroke-width="1.2" stroke-linecap="round" fill="none" opacity="0.7"/><path d="M66 36 Q72 33 72 29 Q72 25 66 22" stroke="#e8b86d" stroke-width="1.2" stroke-linecap="round" fill="none" opacity="0.7"/><circle cx="50" cy="7" r="1.5" fill="#e8b86d"/></svg>'), sizes: '192x192', type: 'image/svg+xml' }
|
||||
]
|
||||
};
|
||||
const blob = new Blob([JSON.stringify(manifest)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'manifest'; link.href = url;
|
||||
document.head.appendChild(link);
|
||||
})();
|
||||
|
||||
// ─── Utils ───────────────────────────────────────────────────
|
||||
function escHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||
}
|
||||
|
||||
let toastTimer;
|
||||
function showToast(msg) {
|
||||
const el = document.getElementById('toast');
|
||||
el.textContent = msg;
|
||||
el.classList.add('show');
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => el.classList.remove('show'), 1800);
|
||||
}
|
||||
|
||||
// ─── Start ───────────────────────────────────────────────────
|
||||
init();
|
||||
// init slider fills
|
||||
setTimeout(() => {
|
||||
updateSliderFill(document.getElementById('progressBar'), 0);
|
||||
updateSliderFill(document.getElementById('volSlider'), 100);
|
||||
}, 50);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user