diff --git a/js/player.js b/js/player.js new file mode 100644 index 0000000..4c3e643 --- /dev/null +++ b/js/player.js @@ -0,0 +1,220 @@ +"use strict"; + +const Player = { + audio: new Audio(), + ctx: null, + analyser: null, + freq: null, + queue: [], + index: -1, + dur: 0, + cur: 0, + playing: false, + endedFlag: false, + + onState: null, + onTime: null, + onTrack: null, + + init() { + this.volume = Store.volume; + this.shuffle = Store.shuffle; + this.repeat = Store.repeat; + this.audio.volume = this.volume; + this.audio.preload = "auto"; + + this.audio.addEventListener("loadedmetadata", () => { + this.dur = isFinite(this.audio.duration) ? this.audio.duration : 0; + this.emitTime(); + }); + this.audio.addEventListener("durationchange", () => { + this.dur = isFinite(this.audio.duration) ? this.audio.duration : this.dur; + this.emitTime(); + }); + this.audio.addEventListener("timeupdate", () => { + this.cur = this.audio.currentTime; + this.emitTime(); + }); + this.audio.addEventListener("play", () => { + this.playing = true; + if (this.onState) this.onState(true); + }); + this.audio.addEventListener("pause", () => { + this.playing = false; + if (this.onState) this.onState(false); + }); + this.audio.addEventListener("ended", () => this.onEnded()); + this.audio.addEventListener("error", () => { + if (this.endedFlag) return; + }); + }, + + emitTime() { + if (this.onTime) this.onTime(this.cur, this.dur); + }, + + current() { + return this.queue[this.index] || null; + }, + + playContext(ids, start) { + if (!ids || !ids.length) return; + this.queue = ids.map((x) => (typeof x === "object" && x != null ? x : D.song(x))).filter(Boolean); + this.index = start || 0; + this.loadCurrent(); + }, + + loadCurrent() { + const s = this.current(); + if (!s) return; + this.endedFlag = false; + this.audio.src = s.audio; + this.audio.play().catch(() => {}); + Store.pushHistory(s.id); + if (this.onTrack) this.onTrack(s); + }, + + togglePlay() { + if (!this.current()) { + this.playContext(D.topSongs(40).map((s) => s.id), 0); + return; + } + if (this.audio.paused) this.audio.play().catch(() => {}); + else this.audio.pause(); + }, + + next() { + if (!this.queue.length) return; + if (this.shuffle) { + if (this.queue.length < 2) return; + let i; + do { + i = Math.floor(Math.random() * this.queue.length); + } while (i === this.index); + this.index = i; + this.loadCurrent(); + return; + } + if (this.index + 1 < this.queue.length) { + this.index++; + this.loadCurrent(); + } else if (this.repeat === 1) { + this.index = 0; + this.loadCurrent(); + } else { + this.audio.pause(); + this.cur = 0; + this.dur = this.current() ? this.current().dur : 0; + this.emitTime(); + } + }, + + prev() { + if (!this.queue.length) return; + if (this.audio.currentTime > 3) { + this.seek(0); + return; + } + if (this.index - 1 >= 0) { + this.index--; + this.loadCurrent(); + } else if (this.repeat === 1 && this.queue.length > 1) { + this.index = this.queue.length - 1; + this.loadCurrent(); + } else { + this.seek(0); + } + }, + + onEnded() { + if (this.repeat === 2) { + this.audio.currentTime = 0; + this.audio.play().catch(() => {}); + return; + } + this.next(); + }, + + seek(t) { + if (!this.current()) return; + const clamp = Math.max(0, Math.min(t, this.dur || t)); + this.audio.currentTime = clamp; + this.cur = clamp; + this.emitTime(); + }, + + setVolume(v) { + this.volume = Math.max(0, Math.min(1, v)); + this.audio.volume = this.volume; + Store.volume = this.volume; + Store.save(); + }, + + toggleMute() { + if (this.audio.volume > 0) { + this._lastVol = this.audio.volume; + this.setVolume(0); + } else { + this.setVolume(this._lastVol || 0.85); + } + }, + + toggleShuffle() { + this.shuffle = !this.shuffle; + Store.shuffle = this.shuffle; + Store.save(); + }, + + cycleRepeat() { + this.repeat = (this.repeat + 1) % 3; + Store.repeat = this.repeat; + Store.save(); + }, + + removeAt(i) { + if (i < 0 || i >= this.queue.length) return; + const removedCurrent = i === this.index; + this.queue.splice(i, 1); + if (!this.queue.length) { + this.audio.pause(); + this.audio.removeAttribute("src"); + this.index = -1; + this.cur = 0; + this.dur = 0; + this.emitTime(); + if (this.onTrack) this.onTrack(null); + return; + } + if (removedCurrent) { + if (i >= this.queue.length) this.index = this.queue.length - 1; + else this.index = i; + this.loadCurrent(); + } else if (i < this.index) { + this.index--; + } + }, + + ensureGraph() { + if (this.ctx) return; + const Ctx = window.AudioContext || window.webkitAudioContext; + if (!Ctx) return; + try { + this.ctx = new Ctx(); + const src = this.ctx.createMediaElementSource(this.audio); + this.analyser = this.ctx.createAnalyser(); + this.analyser.fftSize = 256; + this.analyser.smoothingTimeConstant = 0.82; + src.connect(this.analyser); + this.analyser.connect(this.ctx.destination); + this.freq = new Uint8Array(this.analyser.frequencyBinCount); + } catch (e) {} + }, + + resumeGraph() { + if (this.ctx && this.ctx.state === "suspended") { + this.ctx.resume().catch(() => {}); + } + } +}; + +Player.init();