仰望星辰工作室

clearlove2.1

clearlove2.1/ plugins/bgm/player.html 6.4 KB · 147 行 原始文件
1<!-- ClearLove 背景音乐播放器(由插件 bgm 注入到 footer_html) -->
2<div id="clv-bgm" hidden
3 data-src="{config:music}"
4 data-vol="{config:volume}"
5 data-auto="{config:autoplay}"
6 data-loop="{config:loop}"
7 data-pos="{config:pos}">
8 <audio id="clv-bgm-audio" preload="auto" playsinline></audio>
9 <button id="clv-bgm-btn" type="button" aria-label="播放背景音乐" aria-pressed="false">
10 <svg class="bgm-on" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M9 17.5V5.8l10-2v11.7"/><circle cx="6.6" cy="17.8" r="2.6"/><circle cx="16.6" cy="15.5" r="2.6"/></svg>
11 <svg class="bgm-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" aria-hidden="true"><path d="M9 17.5V5.8l10-2v11.7"/><circle cx="6.6" cy="17.8" r="2.6"/><circle cx="16.6" cy="15.5" r="2.6"/><path d="M3.5 3.5l17 17"/></svg>
12 </button>
13 <span id="clv-bgm-tip" hidden>点击开启背景音乐</span>
14</div>
15<style>
16#clv-bgm{position:fixed;z-index:900;bottom:22px;display:flex;align-items:center;gap:8px;animation:clv-bgm-in .45s ease both}
17#clv-bgm[hidden]{display:none!important}
18#clv-bgm.pos-right{right:18px;flex-direction:row-reverse}
19#clv-bgm.pos-left{left:18px}
20#clv-bgm-btn{width:46px;height:46px;flex:0 0 auto;display:flex;align-items:center;justify-content:center;padding:0;border:0;border-radius:50%;cursor:pointer;color:#fff;background:linear-gradient(135deg,var(--clv-accent,#ff7a9c),var(--clv-accent-2,#8a6cff));box-shadow:0 8px 22px rgba(0,0,0,.22)}
21#clv-bgm-btn:focus-visible{outline:2px solid var(--clv-accent);outline-offset:2px}
22#clv-bgm-btn svg{width:22px;height:22px}
23#clv-bgm-btn .bgm-off{display:none}
24#clv-bgm.paused #clv-bgm-btn .bgm-on{display:none}
25#clv-bgm.paused #clv-bgm-btn .bgm-off{display:block}
26#clv-bgm.playing #clv-bgm-btn{animation:clv-bgm-pulse 2.4s ease-in-out infinite}
27#clv-bgm-tip{font-size:12px;padding:5px 10px;border-radius:999px;white-space:nowrap;background:var(--clv-card,#fff);color:var(--clv-text,#23262f);border:1px solid var(--clv-border,rgba(0,0,0,.1));box-shadow:0 6px 16px rgba(0,0,0,.12)}
28@keyframes clv-bgm-in{from{opacity:0}to{opacity:1}}
29@keyframes clv-bgm-pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.06)}}
30@media (prefers-reduced-motion:reduce){#clv-bgm.playing #clv-bgm-btn{animation:none}}
31@media (max-width:480px){#clv-bgm-btn{width:42px;height:42px}}
32</style>
33<script>
34(function () {
35 var box = document.getElementById('clv-bgm');
36 if (!box) return;
37 var audio = document.getElementById('clv-bgm-audio');
38 var btn = document.getElementById('clv-bgm-btn');
39 var tip = document.getElementById('clv-bgm-tip');
40 var src = (box.getAttribute('data-src') || '').trim();
41 if (!src) return; // 未上传音乐:播放器不出现
42
43 audio.src = src;
44 audio.loop = box.getAttribute('data-loop') === '1';
45 var v = parseInt(box.getAttribute('data-vol'), 10);
46 audio.volume = Math.min(1, Math.max(0, (isNaN(v) ? 35 : v) / 100));
47 box.className = box.getAttribute('data-pos') === '左下' ? 'pos-left' : 'pos-right';
48
49 var ON = 'clv_bgm_on', POS = 'clv_bgm_pos';
50 function get(k, sess) { try { return (sess ? sessionStorage : localStorage).getItem(k); } catch (e) { return null; } }
51 function set(k, val, sess) { try { (sess ? sessionStorage : localStorage).setItem(k, val); } catch (e) { } }
52
53 // 访客的开关优先,其次才是后台的「自动播放」设置
54 var saved = get(ON);
55 var on = saved === null ? box.getAttribute('data-auto') === '1' : saved === '1';
56 var armed = false, tipTimer = null;
57
58 function render() {
59 var playing = !audio.paused && !audio.ended;
60 box.classList.toggle('playing', playing);
61 box.classList.toggle('paused', !playing);
62 btn.setAttribute('aria-pressed', playing ? 'true' : 'false');
63 btn.setAttribute('aria-label', playing ? '暂停背景音乐' : '播放背景音乐');
64 }
65
66 // 浏览器要求音频必须由用户手势触发:监听一次全局交互再播
67 function armGesture() {
68 if (armed) return;
69 armed = true;
70 var fire = function () {
71 document.removeEventListener('pointerdown', fire, true);
72 document.removeEventListener('keydown', fire, true);
73 armed = false;
74 if (on && audio.paused) play(true);
75 };
76 document.addEventListener('pointerdown', fire, true);
77 document.addEventListener('keydown', fire, true);
78 }
79
80 function showTip() {
81 tip.hidden = false;
82 clearTimeout(tipTimer);
83 tipTimer = setTimeout(function () { tip.hidden = true; }, 10000);
84 }
85
86 function play(byGesture) {
87 var p = audio.play();
88 if (p && p.catch) {
89 p.catch(function () {
90 // 自动播放被拦截:提示访客,并在其首次交互时自动开始
91 if (!byGesture) { showTip(); armGesture(); }
92 render();
93 });
94 }
95 }
96
97 btn.addEventListener('click', function () {
98 if (audio.paused) {
99 on = true;
100 set(ON, '1');
101 tip.hidden = true;
102 play(true);
103 } else {
104 on = false;
105 set(ON, '0');
106 audio.pause();
107 }
108 render();
109 });
110
111 audio.addEventListener('play', render);
112 audio.addEventListener('pause', render);
113 // 文件缺失或格式不支持时直接隐藏,避免留下一个点不动的按钮
114 audio.addEventListener('error', function () { box.hidden = true; });
115
116 // 记录播放进度,站内翻页后接着播
117 var lastSave = 0;
118 audio.addEventListener('timeupdate', function () {
119 if (Math.abs(audio.currentTime - lastSave) >= 1) {
120 lastSave = audio.currentTime;
121 set(POS, String(audio.currentTime), true);
122 }
123 });
124 window.addEventListener('pagehide', function () { set(POS, String(audio.currentTime), true); });
125 var resume = parseFloat(get(POS, true));
126 if (isFinite(resume) && resume > 0) {
127 audio.addEventListener('loadedmetadata', function () {
128 try { if (!audio.duration || resume < audio.duration) audio.currentTime = resume; } catch (e) { }
129 });
130 }
131
132 // 自动避开右下角的发帖悬浮按钮,避免两个按钮叠在一起
133 function place() {
134 var fab = document.querySelector('.fab');
135 var right = box.classList.contains('pos-right');
136 var r = fab ? fab.getBoundingClientRect() : null;
137 var sameSide = r && r.width > 0 && (right ? window.innerWidth - r.right < 90 : r.left < 90);
138 box.style.bottom = sameSide ? Math.round(window.innerHeight - r.top + 12) + 'px' : '';
139 }
140
141 box.hidden = false;
142 place();
143 window.addEventListener('resize', place);
144 render();
145 if (on) play(false);
146})();
147</script>