@aicupa/plugin-bg-music 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/inject.js +406 -0
- package/package.json +16 -0
- package/service.js +7 -0
- package/view/index.html +14 -0
package/inject.js
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
// 防止重复注入
|
|
3
|
+
if (window.__AICUPA_BG_MUSIC_LOADED__) return;
|
|
4
|
+
window.__AICUPA_BG_MUSIC_LOADED__ = true;
|
|
5
|
+
|
|
6
|
+
// ==========================================
|
|
7
|
+
// 1. 动态引入 Tone.js
|
|
8
|
+
// ==========================================
|
|
9
|
+
const script = document.createElement("script");
|
|
10
|
+
script.src = "https://unpkg.com/tone";
|
|
11
|
+
script.async = true;
|
|
12
|
+
document.head.appendChild(script);
|
|
13
|
+
|
|
14
|
+
script.onload = () => {
|
|
15
|
+
initPlugin();
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function initPlugin() {
|
|
19
|
+
// ==========================================
|
|
20
|
+
// 2. 注入 CSS 霓虹深色样式
|
|
21
|
+
// ==========================================
|
|
22
|
+
const style = document.createElement("style");
|
|
23
|
+
style.textContent = `
|
|
24
|
+
.music-floating-container {
|
|
25
|
+
position: fixed;
|
|
26
|
+
right: 20px;
|
|
27
|
+
bottom: 120px;
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: flex-end;
|
|
31
|
+
z-index: 999999; /* 确保在 Todolist 所有人机界面最上层 */
|
|
32
|
+
touch-action: none;
|
|
33
|
+
user-select: none;
|
|
34
|
+
}
|
|
35
|
+
.music-icon-wrapper {
|
|
36
|
+
width: 45px;
|
|
37
|
+
height: 45px;
|
|
38
|
+
background: #0f172a;
|
|
39
|
+
border: 2px solid #00e5ff;
|
|
40
|
+
border-radius: 50%;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
cursor: grab;
|
|
45
|
+
box-shadow: 0 0 15px rgba(0, 229, 255, 0.3);
|
|
46
|
+
transition: border-color 0.3s, box-shadow 0.3s, transform 0.1s;
|
|
47
|
+
position: relative;
|
|
48
|
+
z-index: 2;
|
|
49
|
+
}
|
|
50
|
+
.music-icon-wrapper:active {
|
|
51
|
+
cursor: grabbing;
|
|
52
|
+
transform: scale(0.95);
|
|
53
|
+
}
|
|
54
|
+
.music-icon-wrapper svg {
|
|
55
|
+
fill: #00e5ff;
|
|
56
|
+
width: 20px;
|
|
57
|
+
height: 20px;
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
}
|
|
60
|
+
.music-icon-wrapper.playing svg {
|
|
61
|
+
animation: musicPulse 1.5s infinite alternate;
|
|
62
|
+
}
|
|
63
|
+
@keyframes musicPulse {
|
|
64
|
+
0% { transform: scale(1); filter: drop-shadow(0 0 2px #00e5ff); }
|
|
65
|
+
100% { transform: scale(1.12); filter: drop-shadow(0 0 8px #00e5ff); }
|
|
66
|
+
}
|
|
67
|
+
.music-control-panel {
|
|
68
|
+
position: absolute;
|
|
69
|
+
right: 15px;
|
|
70
|
+
background: #0f172a;
|
|
71
|
+
border: 1px solid #1e293b;
|
|
72
|
+
padding: 10px 45px 10px 15px;
|
|
73
|
+
border-radius: 20px 0 0 20px;
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
gap: 6px;
|
|
77
|
+
width: 180px;
|
|
78
|
+
opacity: 0;
|
|
79
|
+
transform: translateX(20px);
|
|
80
|
+
transition: opacity 0.3s, transform 0.3s;
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
box-shadow: -5px 5px 15px rgba(0,0,0,0.5);
|
|
83
|
+
}
|
|
84
|
+
.music-floating-container:not(.dragging):hover .music-control-panel {
|
|
85
|
+
opacity: 1;
|
|
86
|
+
transform: translateX(0);
|
|
87
|
+
pointer-events: auto;
|
|
88
|
+
}
|
|
89
|
+
.music-floating-container.align-left .music-control-panel {
|
|
90
|
+
right: auto;
|
|
91
|
+
left: 15px;
|
|
92
|
+
border-radius: 0 20px 20px 0;
|
|
93
|
+
padding: 10px 15px 10px 45px;
|
|
94
|
+
transform: translateX(-20px);
|
|
95
|
+
box-shadow: 5px 5px 15px rgba(0,0,0,0.5);
|
|
96
|
+
}
|
|
97
|
+
.music-floating-container.align-left:not(.dragging):hover .music-control-panel {
|
|
98
|
+
transform: translateX(0);
|
|
99
|
+
}
|
|
100
|
+
.music-floating-container:hover .music-icon-wrapper {
|
|
101
|
+
border-color: #ffffff;
|
|
102
|
+
box-shadow: 0 0 15px rgba(255, 255, 255, 0.2);
|
|
103
|
+
}
|
|
104
|
+
.music-title {
|
|
105
|
+
color: #00e5ff;
|
|
106
|
+
font-size: 11px;
|
|
107
|
+
white-space: nowrap;
|
|
108
|
+
overflow: hidden;
|
|
109
|
+
text-overflow: ellipsis;
|
|
110
|
+
}
|
|
111
|
+
.panel-row {
|
|
112
|
+
display: flex;
|
|
113
|
+
align-items: center;
|
|
114
|
+
justify-content: space-between;
|
|
115
|
+
gap: 8px;
|
|
116
|
+
}
|
|
117
|
+
.music-ctrl-btn {
|
|
118
|
+
background: none;
|
|
119
|
+
border: 1px solid #334155;
|
|
120
|
+
color: #e2e8f0;
|
|
121
|
+
font-size: 10px;
|
|
122
|
+
padding: 2px 6px;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
border-radius: 3px;
|
|
125
|
+
}
|
|
126
|
+
.music-ctrl-btn:hover {
|
|
127
|
+
border-color: #00e5ff;
|
|
128
|
+
color: #00e5ff;
|
|
129
|
+
}
|
|
130
|
+
.music-progress-container {
|
|
131
|
+
width: 100%;
|
|
132
|
+
height: 4px;
|
|
133
|
+
background: #334155;
|
|
134
|
+
border-radius: 2px;
|
|
135
|
+
position: relative;
|
|
136
|
+
overflow: hidden;
|
|
137
|
+
}
|
|
138
|
+
.music-progress-bar {
|
|
139
|
+
height: 100%;
|
|
140
|
+
width: 0%;
|
|
141
|
+
background: #00e5ff;
|
|
142
|
+
transition: width 0.5s linear;
|
|
143
|
+
}
|
|
144
|
+
`;
|
|
145
|
+
document.head.appendChild(style);
|
|
146
|
+
|
|
147
|
+
// ==========================================
|
|
148
|
+
// 3. 动态插入 DOM 结构
|
|
149
|
+
// ==========================================
|
|
150
|
+
const container = document.createElement("div");
|
|
151
|
+
container.className = "music-floating-container";
|
|
152
|
+
container.id = "music-container";
|
|
153
|
+
container.innerHTML = `
|
|
154
|
+
<div class="music-control-panel">
|
|
155
|
+
<div class="music-title" id="track-name">Crystal Piano</div>
|
|
156
|
+
<div class="music-progress-container">
|
|
157
|
+
<div class="music-progress-bar" id="track-progress"></div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="panel-row">
|
|
160
|
+
<button class="music-ctrl-btn" id="btn-toggle">PLAY</button>
|
|
161
|
+
<button class="music-ctrl-btn" id="btn-switch">SWITCH</button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="music-icon-wrapper" id="icon-trigger">
|
|
165
|
+
<svg viewBox="0 0 24 24">
|
|
166
|
+
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
|
|
167
|
+
</svg>
|
|
168
|
+
</div>
|
|
169
|
+
`;
|
|
170
|
+
document.body.appendChild(container);
|
|
171
|
+
|
|
172
|
+
// ==========================================
|
|
173
|
+
// 4. 音频引擎调度逻辑
|
|
174
|
+
// ==========================================
|
|
175
|
+
let isPlaying = false;
|
|
176
|
+
let currentTrackIndex = 0;
|
|
177
|
+
let progressTimer = null;
|
|
178
|
+
let currentStep = 0;
|
|
179
|
+
|
|
180
|
+
const tracks = [
|
|
181
|
+
{ name: "Crystal Piano", stepsMax: 12 },
|
|
182
|
+
{ name: "Ambient Canon", stepsMax: 16 },
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const reverb = new Tone.Reverb({
|
|
186
|
+
decay: 6,
|
|
187
|
+
preDelay: 0.15,
|
|
188
|
+
wet: 0.45,
|
|
189
|
+
}).toDestination();
|
|
190
|
+
const delay = new Tone.FeedbackDelay({
|
|
191
|
+
delayTime: "4n.",
|
|
192
|
+
feedback: 0.35,
|
|
193
|
+
wet: 0.2,
|
|
194
|
+
}).connect(reverb);
|
|
195
|
+
const filter = new Tone.Filter({
|
|
196
|
+
frequency: 1800,
|
|
197
|
+
type: "lowpass",
|
|
198
|
+
}).connect(delay);
|
|
199
|
+
|
|
200
|
+
const padSynth = new Tone.PolySynth(Tone.Synth, {
|
|
201
|
+
oscillator: { type: "triangle" },
|
|
202
|
+
envelope: { attack: 0.5, decay: 2, sustain: 0.4, release: 4 },
|
|
203
|
+
}).connect(filter);
|
|
204
|
+
padSynth.volume.value = -18;
|
|
205
|
+
|
|
206
|
+
const leadSynth = new Tone.Synth({
|
|
207
|
+
oscillator: { type: "triangle" },
|
|
208
|
+
envelope: { attack: 0.005, decay: 0.6, sustain: 0.1, release: 1.2 },
|
|
209
|
+
}).connect(filter);
|
|
210
|
+
leadSynth.volume.value = -10;
|
|
211
|
+
|
|
212
|
+
const pianoChords = [
|
|
213
|
+
["F3", "A3", "C4", "E4", "G4"],
|
|
214
|
+
["G3", "B3", "D4", "G4"],
|
|
215
|
+
["C3", "E3", "G3", "B3", "D4"],
|
|
216
|
+
];
|
|
217
|
+
const pianoScale = ["C5", "D5", "E5", "G5", "A5", "C6", "D6", "E6"];
|
|
218
|
+
|
|
219
|
+
const canonChords = [
|
|
220
|
+
["C3", "E4", "G4"],
|
|
221
|
+
["G3", "D4", "B4"],
|
|
222
|
+
["A2", "C4", "E4"],
|
|
223
|
+
["E3", "B3", "G4"],
|
|
224
|
+
["F2", "A3", "C4"],
|
|
225
|
+
["C3", "G3", "E4"],
|
|
226
|
+
["F2", "A3", "C4"],
|
|
227
|
+
["G2", "B3", "D4"],
|
|
228
|
+
];
|
|
229
|
+
const canonMelody = [
|
|
230
|
+
["C5", "G5"],
|
|
231
|
+
["B4", "G5"],
|
|
232
|
+
["A4", "E5"],
|
|
233
|
+
["G4", "E5"],
|
|
234
|
+
["F4", "C5"],
|
|
235
|
+
["E4", "C5"],
|
|
236
|
+
["F4", "C5"],
|
|
237
|
+
["D4", "G4"],
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
function setupTransport() {
|
|
241
|
+
Tone.getTransport().cancel();
|
|
242
|
+
Tone.getTransport().bpm.value = 72;
|
|
243
|
+
currentStep = 0;
|
|
244
|
+
|
|
245
|
+
Tone.getTransport().scheduleRepeat((time) => {
|
|
246
|
+
if (currentTrackIndex === 0) {
|
|
247
|
+
const chord = pianoChords[currentStep % pianoChords.length];
|
|
248
|
+
padSynth.triggerAttackRelease(chord, 5, time);
|
|
249
|
+
if (Math.random() > 0.3) {
|
|
250
|
+
const note =
|
|
251
|
+
pianoScale[Math.floor(Math.random() * pianoScale.length)];
|
|
252
|
+
leadSynth.triggerAttackRelease(
|
|
253
|
+
note,
|
|
254
|
+
"4n",
|
|
255
|
+
time + Math.random() * 0.15,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
const idx = currentStep % 8;
|
|
260
|
+
padSynth.triggerAttackRelease(canonChords[idx], "2n", time);
|
|
261
|
+
const pool = canonMelody[idx];
|
|
262
|
+
leadSynth.triggerAttackRelease(pool[0], "4n", time);
|
|
263
|
+
if (Math.random() > 0.4) {
|
|
264
|
+
leadSynth.triggerAttackRelease(
|
|
265
|
+
pool[1],
|
|
266
|
+
"8n",
|
|
267
|
+
time + Tone.Time("4n"),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
currentStep++;
|
|
272
|
+
}, "2n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ==========================================
|
|
276
|
+
// 5. 交互绑定与拖拽定位
|
|
277
|
+
// ==========================================
|
|
278
|
+
const icon = document.getElementById("icon-trigger");
|
|
279
|
+
const btnToggle = document.getElementById("btn-toggle");
|
|
280
|
+
const btnSwitch = document.getElementById("btn-switch");
|
|
281
|
+
const trackName = document.getElementById("track-name");
|
|
282
|
+
const progressBar = document.getElementById("track-progress");
|
|
283
|
+
|
|
284
|
+
let isDragging = false;
|
|
285
|
+
let hasMoved = false;
|
|
286
|
+
let startX = 0,
|
|
287
|
+
startY = 0;
|
|
288
|
+
let initialLeft = 0,
|
|
289
|
+
initialTop = 0;
|
|
290
|
+
|
|
291
|
+
function onStart(e) {
|
|
292
|
+
isDragging = true;
|
|
293
|
+
hasMoved = false;
|
|
294
|
+
container.classList.add("dragging");
|
|
295
|
+
|
|
296
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
297
|
+
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
298
|
+
startX = clientX;
|
|
299
|
+
startY = clientY;
|
|
300
|
+
|
|
301
|
+
const rect = container.getBoundingClientRect();
|
|
302
|
+
initialLeft = rect.left;
|
|
303
|
+
initialTop = rect.top;
|
|
304
|
+
|
|
305
|
+
container.style.right = "auto";
|
|
306
|
+
container.style.bottom = "auto";
|
|
307
|
+
container.style.left = `${initialLeft}px`;
|
|
308
|
+
container.style.top = `${initialTop}px`;
|
|
309
|
+
|
|
310
|
+
document.addEventListener("mousemove", onMove);
|
|
311
|
+
document.addEventListener("mouseup", onEnd);
|
|
312
|
+
document.addEventListener("touchmove", onMove, { passive: false });
|
|
313
|
+
document.addEventListener("touchend", onEnd);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function onMove(e) {
|
|
317
|
+
if (!isDragging) return;
|
|
318
|
+
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
319
|
+
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
320
|
+
|
|
321
|
+
const deltaX = clientX - startX;
|
|
322
|
+
const deltaY = clientY - startY;
|
|
323
|
+
|
|
324
|
+
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
|
|
325
|
+
hasMoved = true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let newLeft = initialLeft + deltaX;
|
|
329
|
+
let newTop = initialTop + deltaY;
|
|
330
|
+
|
|
331
|
+
const padding = 10;
|
|
332
|
+
newLeft = Math.max(padding, Math.min(window.innerWidth - 55, newLeft));
|
|
333
|
+
newTop = Math.max(padding, Math.min(window.innerHeight - 55, newTop));
|
|
334
|
+
|
|
335
|
+
container.style.left = `${newLeft}px`;
|
|
336
|
+
container.style.top = `${newTop}px`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function onEnd() {
|
|
340
|
+
if (!isDragging) return;
|
|
341
|
+
isDragging = false;
|
|
342
|
+
container.classList.remove("dragging");
|
|
343
|
+
|
|
344
|
+
document.removeEventListener("mousemove", onMove);
|
|
345
|
+
document.removeEventListener("mouseup", onEnd);
|
|
346
|
+
document.removeEventListener("touchmove", onMove);
|
|
347
|
+
document.removeEventListener("touchend", onEnd);
|
|
348
|
+
|
|
349
|
+
const rect = container.getBoundingClientRect();
|
|
350
|
+
const screenWidth = window.innerWidth;
|
|
351
|
+
const padding = 20;
|
|
352
|
+
|
|
353
|
+
container.style.transition =
|
|
354
|
+
"left 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28)";
|
|
355
|
+
if (rect.left + rect.width / 2 < screenWidth / 2) {
|
|
356
|
+
container.style.left = `${padding}px`;
|
|
357
|
+
container.classList.add("align-left");
|
|
358
|
+
} else {
|
|
359
|
+
container.style.left = `${screenWidth - rect.width - padding}px`;
|
|
360
|
+
container.classList.remove("align-left");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
setTimeout(() => {
|
|
364
|
+
container.style.transition = "";
|
|
365
|
+
}, 300);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function togglePlay() {
|
|
369
|
+
if (hasMoved) return; // 拖动时不触发播放/暂停
|
|
370
|
+
|
|
371
|
+
await Tone.start();
|
|
372
|
+
await reverb.ready;
|
|
373
|
+
|
|
374
|
+
if (!isPlaying) {
|
|
375
|
+
setupTransport();
|
|
376
|
+
Tone.getTransport().start();
|
|
377
|
+
icon.classList.add("playing");
|
|
378
|
+
btnToggle.innerText = "PAUSE";
|
|
379
|
+
isPlaying = true;
|
|
380
|
+
progressTimer = setInterval(() => {
|
|
381
|
+
const max = tracks[currentTrackIndex].stepsMax;
|
|
382
|
+
progressBar.style.width = `${((currentStep % max) / max) * 100}%`;
|
|
383
|
+
}, 500);
|
|
384
|
+
} else {
|
|
385
|
+
Tone.getTransport().stop();
|
|
386
|
+
icon.classList.remove("playing");
|
|
387
|
+
btnToggle.innerText = "PLAY";
|
|
388
|
+
isPlaying = false;
|
|
389
|
+
clearInterval(progressTimer);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
icon.addEventListener("mousedown", onStart);
|
|
394
|
+
icon.addEventListener("touchstart", onStart, { passive: true });
|
|
395
|
+
icon.addEventListener("click", togglePlay);
|
|
396
|
+
btnToggle.addEventListener("click", togglePlay);
|
|
397
|
+
|
|
398
|
+
btnSwitch.addEventListener("click", () => {
|
|
399
|
+
currentTrackIndex = (currentTrackIndex + 1) % tracks.length;
|
|
400
|
+
trackName.innerText = tracks[currentTrackIndex].name;
|
|
401
|
+
currentStep = 0;
|
|
402
|
+
progressBar.style.width = "0%";
|
|
403
|
+
if (isPlaying) setupTransport();
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aicupa/plugin-bg-music",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Aicupa Todolist Background Music Inject Plugin",
|
|
5
|
+
"main": "./service",
|
|
6
|
+
"view": "",
|
|
7
|
+
"viewjs": "./inject.js",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"registry": "https://registry.npmjs.org/",
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@aicupa/api": "^1.0.4"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/service.js
ADDED