UNPKG

@anson0370/shikwasa

Version:

A web audio player born for podcasts. With chapters, playback speed and forward/backward controls, it is best paired with your podcasting website.

1,316 lines (1,306 loc) 46.2 kB
var base = ""; var chapter = ""; const PlayerTemplate = ` <div class="shk-player"> <div class="shk-bar_wrap"> <div class="shk-bar" aria-label="progress bar"> <div class="shk-bar_loaded" role="progressbar" aria-label="loaded progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="1"></div> <div class="shk-bar_played" role="progressbar" aria-label="played progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="1"> <button class="shk-bar-handle" role="slider" aria-label="seek progress" aria-valuenow="0" aria-valuemin="0" aria-orientation="horizontal" aria-valuemax="1"></button> </div> </div> </div> <div class="shk-body"> <div class="shk-cover"> <div class="shk-img"></div> </div> <div class="shk-main"> <div class="shk-text"> <div class="shk-artist_wrap"> <span class="shk-artist"></span> </div> <div class="shk-title_wrap"> <div class="shk-title_inner"> <span class="shk-title"></span> </div> </div> </div> <div class="shk-controls"> <div class="shk-controls_basic"> <button class="shk-btn shk-btn_speed" aria-label="toggle playback rate" title="change playback rate" aria-live="polite">1.0x</button> <button class="shk-btn shk-btn_backward" aria-label="rewind 10 seconds" title="rewind 10 seconds"> <svg aria-hidden="true"> <use xlink:href="#shk-icon_backward" /> </svg> </button> <button class="shk-btn shk-btn_toggle" aria-label="toggle play and pause"> <svg class="shk-btn_play" aria-hidden="true"> <use xlink:href="#shk-icon_play" /> </svg> <svg class="shk-btn_pause" aria-hidden="true"> <use xlink:href="#shk-icon_pause" /> </svg> </button> <button class="shk-btn shk-btn_forward" aria-label="forward 10 seconds" title="forward 10 seconds"> <svg aria-hidden="true"> <use xlink:href="#shk-icon_forward" /> </svg> </button> <button class="shk-btn shk-btn_more" aria-label="more controls" title="more controls"> <svg aria-hidden="true"> <use xlink:href="#shk-icon_more" /> </svg> </button> </div> <div class="shk-controls_extra"> <button class="shk-btn shk-btn_volume" aria-label="toggle volume" title="volume"> <svg class="shk-btn_unmute" aria-hidden="true"> <use xlink:href="#shk-icon_unmute" /> </svg> <svg class="shk-btn_mute" aria-hidden="true"> <use xlink:href="#shk-icon_mute" /> </svg> </button> </div> </div> <div class="shk-display"> <span class="shk-loader" aria-live="polite"> <span class="shk-visuallyhidden" tabindex="-1">loading</span> <svg aria-hidden="true" aria-label="loading" aria-live="polite" viewbox="0 0 66 66"> <circle cx="33" cy="33" r="30" fill="transparent" stroke="url(#shk-gradient)" stroke-dasharray="170" stroke-dashoffset="20" stroke-width="6" /> <lineargradient id="shk-gradient"> <stop offset="50%" stop-color="currentColor" /> <stop offset="65%" stop-color="currentColor" stop-opacity=".5" /> <stop offset="100%" stop-color="currentColor" stop-opacity="0" /> </lineargradient> </svg> </span> <span class="shk-time"> <span class="shk-time_now">00:00</span><span class="shk-time_duration">00:00</span> </span> <div class="shk-live">live</div> </div> </div> </div> </div> `; const IconTemplate = ` <svg class="shk-icons" xmlns="http://www.w3.org/2000/svg"> <symbol id="shk-icon_play" viewbox="0 0 64 64"> <path d="M32 0a32 32 0 1 1 0 64 32 32 0 0 1 0-64zm-9 17.8c-1 0-1.7.6-1.7 1.4v25.6c0 .8.8 1.4 1.7 1.4 0 0 25-12 26.2-13.1 1-1 .3-1.9.1-2.1z" /> </symbol> <symbol id="shk-icon_pause" viewbox="0 0 64 64"> <path fill-rule="nonzero" d="M32 0a32 32 0 1 0 0 64 32 32 0 0 0 0-64zm-4 40a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2V24c0-1.1.9-2 2-2h4a2 2 0 0 1 2 2v16zm16 0a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2V24c0-1.1.9-2 2-2h4a2 2 0 0 1 2 2v16z" /> </symbol> <symbol id="shk-icon_download" viewbox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" /> </symbol> <symbol id="shk-icon_forward" viewbox="0 0 128 139"> <path d="M64 11v14h-.8A50 50 0 1 0 114 75h14a64 64 0 1 1-64-64zm16.9 35c6.6 0 11.8 2.7 15.6 8.3a38 38 0 0 1 5.3 21.4c0 9-1.8 16-5.3 21.3-3.8 5.6-9 8.4-15.6 8.4-6.7 0-12-2.8-15.6-8.4A38 38 0 0 1 60 75.7c0-9 1.8-16.1 5.3-21.4C69 48.7 74.2 46 80.9 46zm-32.5 1v57.1H39V58.3a32.3 32.3 0 0 1-13 7V56a34 34 0 0 0 15.4-9h7zm32.5 7c-4.6 0-7.8 2.4-9.6 7.5-1.3 3.5-2 8.2-2 14.2 0 5.9.7 10.6 2 14.1 1.8 5 5 7.6 9.6 7.6 4.5 0 7.7-2.5 9.6-7.6 1.3-3.5 1.9-8.2 1.9-14.1 0-6-.6-10.7-2-14.2-1.8-5.1-5-7.6-9.5-7.6zM64 0l48 19-48 19V0z" /> </symbol> <symbol id="shk-icon_backward" viewbox="0 0 128 139"> <path d="M64 0v11A64 64 0 1 1 0 75h14a50 50 0 1 0 50-50v13L16 19 64 0zm17 45.8c6.6 0 11.8 2.7 15.6 8.3a38 38 0 0 1 5.3 21.3c0 9-1.8 16.1-5.3 21.4a18 18 0 0 1-15.6 8.3c-6.7 0-12-2.8-15.6-8.3a38 38 0 0 1-5.3-21.4c0-9 1.8-16 5.3-21.3 3.7-5.6 8.9-8.3 15.6-8.3zm-32.4 1V104h-9.4V58.2a32.3 32.3 0 0 1-13 7v-9.4a34 34 0 0 0 15.4-9h7zm32.4 7c-4.6 0-7.8 2.5-9.6 7.6-1.3 3.4-2 8.1-2 14.1s.7 10.7 2 14.2c1.8 5 5 7.6 9.6 7.6 4.5 0 7.7-2.6 9.6-7.6 1.3-3.5 2-8.2 2-14.2s-.7-10.7-2-14.1c-2-5.1-5.1-7.6-9.6-7.6z" /> </symbol> <symbol id="shk-icon_more" viewbox="0 0 64 64"> <path d="M8 24a8 8 0 0 0-8 8 8 8 0 0 0 8 8 8 8 0 0 0 8-8 8 8 0 0 0-8-8zm48 0a8 8 0 0 0-8 8 8 8 0 0 0 8 8 8 8 0 0 0 8-8 8 8 0 0 0-8-8zm-24 0a8 8 0 0 0-8 8 8 8 0 0 0 8 8 8 8 0 0 0 8-8 8 8 0 0 0-8-8zm0 0" /> </symbol> <symbol id="shk-icon_chapter" viewbox="0 0 64 64"> <path d="M60.8 55.3H18.1a3.2 3.2 0 1 1 0-6.4h42.8a3.2 3.2 0 0 1 3.1 4.9c-.7 1-1.9 1.6-3.1 1.5zm0-20.2H18.1a3.2 3.2 0 1 1 0-6.4h42.8a3.2 3.2 0 1 1 0 6.4zm0-20H18.1a3.2 3.2 0 1 1 0-6.3h42.8a3.2 3.2 0 0 1 0 6.3zM8.5 12.3a4.2 4.2 0 1 1-8.5 0 4.2 4.2 0 0 1 8.5 0zm0 19.8A4.2 4.2 0 1 1 0 32a4.2 4.2 0 0 1 8.5 0zm0 19.8a4.3 4.3 0 1 1-8.5 0 4.3 4.3 0 0 1 8.5 0zm0 0"/> </symbol> <symbol id="shk-icon_unmute" viewBox="0 0 64 66"> <path d="M34.3 0c1.5.1 2.6 1.4 2.9 2.8v60.3c-.3 1.4-1.4 2.7-3 2.8-.7.1-1.6-.1-2.6-.7L13.7 50H4.2a4 4 0 0 1-4.1-4V20.6c0-2.2 1.8-4 4-4.1h9.6c.7-.8 17.2-15.1 17.9-15.8 1-.6 1.9-.8 2.6-.7zm6.4 9.3a2 2 0 0 1 2.4-1.6 25.8 25.8 0 0 1 0 50.5l-.4.1a2 2 0 1 1-.4-4.1 21.7 21.7 0 0 0 0-42.5 2 2 0 0 1-1.6-2.4zm4.7 12.8a12.1 12.1 0 0 1 0 21.8 2 2 0 0 1-2.8-1c-.5-1-.1-2.2 1-2.8a8 8 0 0 0 0-14.4 2 2 0 0 1-1-2.7 2 2 0 0 1 2.8-.9zm0 0"/> </symbol> <symbol id="shk-icon_mute" viewBox="0 0 64 64"> <path d="M33.2 0c-.8 0-1.6.1-2.6.7l-17.4 15H4a4 4 0 0 0-4 4v24.1a4 4 0 0 0 4 4h9.2l17.4 14.5c1 .6 1.8.8 2.6.7 1.4-.1 2.5-1.4 2.8-2.6V2.7C35.7 1.4 34.6 0 33.2 0zm23.2 31.5l6.9-6.8c.7-.6 1-1.5.7-2.4-.2-.8-.9-1.5-1.8-1.7-.8-.2-1.8 0-2.4.7L53 28.1 46 21.3c-.6-.6-1.5-1-2.4-.7-.9.2-1.5.9-1.8 1.7-.2.9.1 1.8.8 2.4l6.9 6.8-7 6.8c-.8 1-.8 2.4.2 3.3.9 1 2.4 1 3.3.1l7-6.8 6.8 6.8c1 .9 2.5.8 3.4 0 1-1 1-2.4 0-3.4l-6.8-6.8zm0 0"/> </symbol> <symbol id="shk-icon_triangle" viewbox="0 0 64 64"><path d="M59 29.2L7.8.4A3.2 3.2 0 003 3.2v57.6a3.2 3.2 0 004.8 2.8L59 34.8a3.2 3.2 0 000-5.6z" /></symbol> <symbol id="shk-icon_chart" viewbox="0 0 64 64"><g transform="matrix(1 0 0 -1 0 64)"><rect x="10" width="8" height="54.1" rx="3"><animate attributeName="height" begin="0s" dur="2s" values="64;55;33;5;60;23;58;33;12;14;52;64" calcMode="linear" repeatCount="indefinite"/></rect><rect x="26" width="8" height="32.8" rx="3"><animate attributeName="height" begin="0s" dur="2s" values="50;34;64;23;56;23;34;4;64;54;21;50" calcMode="linear" repeatCount="indefinite"/></rect><rect x="42" width="8" height="42.6" rx="3"><animate attributeName="height" begin="0s" dur="2s" values="30;45;13;64;56;24;45;64;34;23;64;30" calcMode="linear" repeatCount="indefinite"/></rect></g></symbol> <symbol id="shk-icon_close" viewbox="0 0 16 16"><path d="M3.207 14.207a1 1 0 1 1-1.414-1.414l11-11a1 1 0 0 1 1.414 1.414zm11-1.414a1 1 0 0 1-1.414 1.414l-11-11a1 1 0 0 1 1.414-1.414z"></path></symbol> </svg> `; const DEFAULT = { container: () => document.querySelector("body"), parser: null, fixed: { type: "auto", position: "bottom" }, download: false, themeColor: "#0d6efd", theme: "auto", autoPlay: false, muted: false, preload: "metadata", speedOptions: [0.5, 0.75, 1, 1.25, 1.5], audio: null }; const CONFIG = { fixedOptions: ["auto", "static", "fixed"], audioOptions: { title: "Unknown Title", artist: "Unknown Artist", duration: NaN, cover: null, chapters: [], src: null, album: "", live: false } }; function secondToTime(time) { time = Math.round(time); let hour = Math.floor(time / 3600); let min = Math.floor((time - hour * 3600) / 60); let sec = Math.floor(time - hour * 3600 - min * 60); min = min < 10 ? "0" + min : min; sec = sec < 10 ? "0" + sec : sec; if (hour === 0) { return `${min}:${sec}`; } hour = hour < 10 ? "0" + hour : hour; return `${hour}:${min}:${sec}`; } function numToString(num) { const float = parseFloat(num).toFixed(2); return float.slice(-1) === "0" ? float.slice(0, -1) : float; } function marquee(textWrap, textEl, speed = 60) { const overflow = textEl.offsetWidth - textWrap.offsetWidth; if (overflow > 0) { textWrap.setAttribute("data-overflow", ""); const duration = textEl.offsetWidth / speed; textWrap.style.animationDuration = `${duration}s`; } else { textWrap.removeAttribute("data-overflow"); } } function handleOptions(options) { const _options = Object.assign({}, options); _options.audio = Object.assign({}, options.audio); Object.keys(DEFAULT).forEach((k) => { _options[k] = _options[k] || typeof _options[k] === "boolean" ? _options[k] : DEFAULT[k]; }); if (typeof _options.container === "function") { _options.container = _options.container(); } const fixedType = CONFIG.fixedOptions.find((item) => item === _options.fixed.type); if (!fixedType) { _options.fixed.type = DEFAULT.fixed.type; } if (!Array.isArray(_options.speedOptions)) { _options.speedOptions = [_options.speedOptions]; } if (_options.speedOptions.indexOf(1) === -1) { _options.speedOptions.push(1); } _options.speedOptions = _options.speedOptions.map((sp) => parseFloat(sp)).filter((sp) => !isNaN(sp)); if (_options.speedOptions.length > 1) { _options.speedOptions.sort((a, b) => a - b); } if (!_options.initSpeed || _options.speedOptions.indexOf(_options.initSpeed) === -1) { _options.initSpeed = 1; } return _options; } function handleAudio(audio = {}, parsedData = {}) { let audioData = Object.assign({}, audio); Object.keys(CONFIG.audioOptions).forEach((k) => { audioData[k] = audioData[k] || parsedData[k] || CONFIG.audioOptions[k]; }); return audioData; } async function parseAudio(audio = {}, parser = {}) { const { tags } = await parserWrap(audio.src, parser) || {}; const tagData = handleParsedTags(tags); return handleAudio(audio, tagData); } function parserWrap(src, parser) { return new Promise((resolve, reject) => { parser.read(src, { onSuccess: resolve, onError: reject }); }); } function handleParsedTags(tags = {}) { let cover, chapters, duration; const { title, artist } = tags; if (tags.picture && tags.picture.data && tags.picture.format) { const byteArray = new Uint8Array(tags.picture.data); const blob = new Blob([byteArray], { type: tags.picture.format }); cover = URL.createObjectURL(blob); } if (tags.TLEN && tags.TLEN.data) { duration = +tags.TLEN.data / 1e3; } if (tags.CHAP && tags.CHAP.length) { chapters = tags.CHAP.filter((ch) => ch.id === "CHAP").map((ch) => { if (ch.data && ch.data.subFrames && ch.data.subFrames.TIT2) { return { id: ch.data.id, startTime: ch.data.startTime / 1e3, endTime: ch.data.endTime / 1e3, title: ch.data.subFrames.TIT2.data }; } return false; }).sort((a, b) => a.id - b.id); } return { title, artist, cover, duration, chapters }; } function createElement(options) { options.tag = options.tag || "div"; const el = document.createElement(options.tag); if (options.className) { if (typeof options.className === "string") { el.classList.add(options.className); } else { options.className.forEach((className) => { el.classList.add(className); }); } } if (options.attrs) { Object.keys(options.attrs).forEach((key) => { el.setAttribute(key, options.attrs[key]); }); } if (options.innerHTML) { el.innerHTML = options.innerHTML; } return el; } function toggleAttribute(el, name, val) { if (typeof val === "boolean") { val ? el.setAttribute(name, "") : el.removeAttribute(name); return; } if (typeof el.toggleAttribute === "function") { el.toggleAttribute(name); } else { el.hasAttribute(name) ? el.removeAttribute(name) : el.setAttribute(name, ""); } } function animateScroll(timestamp, startTime, duration, startPos, distance, scrollEl) { const elapsed = (timestamp - startTime) / 1e3; const t = elapsed / duration; scrollEl.scrollTop = startPos + distance * t; if (t < 1) { window.requestAnimationFrame((ts) => { animateScroll(ts, startTime, duration, startPos, distance, scrollEl); }); } } function applyFocusVisiblePolyfill(parent, supportsPassive2) { var hadKeyboardEvent = true; var hadFocusVisibleRecently = false; var hadFocusVisibleRecentlyTimeout = null; function isValidFocusTarget(el) { if (el && el !== document && el.nodeName !== "HTML" && el.nodeName !== "BODY" && "classList" in el && "contains" in el.classList) { return true; } return false; } function addFocusVisibleClass(el) { if (el.classList.contains("focus-visible")) { return; } el.classList.add("focus-visible"); el.setAttribute("data-focus-visible-added", ""); } function removeFocusVisibleClass(el) { if (!el.hasAttribute("data-focus-visible-added")) { return; } el.classList.remove("focus-visible"); el.removeAttribute("data-focus-visible-added"); } function onKeyDown(e) { if (e.metaKey || e.altKey || e.ctrlKey) { return; } if (isValidFocusTarget(document.activeElement) && parent.contains(document.activeElement)) { addFocusVisibleClass(document.activeElement); } hadKeyboardEvent = true; } function onPointerDown() { hadKeyboardEvent = false; } function onFocus(e) { if (!isValidFocusTarget(e.target)) { return; } if (hadKeyboardEvent) { addFocusVisibleClass(e.target); } } function onBlur(e) { if (!isValidFocusTarget(e.target)) { return; } if (e.target.classList.contains("focus-visible") || e.target.hasAttribute("data-focus-visible-added")) { hadFocusVisibleRecently = true; window.clearTimeout(hadFocusVisibleRecentlyTimeout); hadFocusVisibleRecentlyTimeout = window.setTimeout(function() { hadFocusVisibleRecently = false; }, 100); removeFocusVisibleClass(e.target); } } function onVisibilityChange() { if (document.visibilityState === "hidden") { if (hadFocusVisibleRecently) { hadKeyboardEvent = true; } addInitialPointerMoveListeners(); } } function addInitialPointerMoveListeners() { parent.addEventListener("mousemove", onInitialPointerMove); parent.addEventListener("mousedown", onInitialPointerMove); parent.addEventListener("mouseup", onInitialPointerMove); parent.addEventListener("pointermove", onInitialPointerMove); parent.addEventListener("pointerdown", onInitialPointerMove); parent.addEventListener("pointerup", onInitialPointerMove); parent.addEventListener("touchmove", onInitialPointerMove, supportsPassive2 ? { passive: true } : false); parent.addEventListener("touchstart", onInitialPointerMove, supportsPassive2 ? { passive: true } : false); parent.addEventListener("touchend", onInitialPointerMove, supportsPassive2 ? { passive: true } : false); } function removeInitialPointerMoveListeners(el) { parent.removeEventListener("mousemove", onInitialPointerMove); parent.removeEventListener("mousedown", onInitialPointerMove); parent.removeEventListener("mouseup", onInitialPointerMove); parent.removeEventListener("pointermove", el); parent.removeEventListener("pointerdown", onInitialPointerMove); parent.removeEventListener("pointerup", onInitialPointerMove); parent.removeEventListener("touchmove", onInitialPointerMove, supportsPassive2 ? { passive: true } : false); parent.removeEventListener("touchstart", onInitialPointerMove, supportsPassive2 ? { passive: true } : false); parent.removeEventListener("touchend", onInitialPointerMove, supportsPassive2 ? { passive: true } : false); } function onInitialPointerMove() { hadKeyboardEvent = false; removeInitialPointerMoveListeners(); } parent.addEventListener("keydown", onKeyDown, true); parent.addEventListener("mousedown", onPointerDown, true); parent.addEventListener("pointerdown", onPointerDown, true); parent.addEventListener("touchstart", onPointerDown, supportsPassive2 ? { passive: true, capture: true } : true); parent.addEventListener("visibilitychange", onVisibilityChange, true); addInitialPointerMoveListeners(); parent.addEventListener("focus", onFocus, true); parent.addEventListener("blur", onBlur, true); parent.classList.add("js-focus-visible"); } let resize$1, coverUrl = null; let cooldown = true; class UI { constructor(options) { this.mounted = false; this.icons = createElement({ className: "shk-icons", innerHTML: IconTemplate }); this.initEl(); this.initOptions(options); } async initEl() { this.el = createElement({ className: ["shk", "shikwasa"], attrs: { "data-name": "shikwasa" }, innerHTML: PlayerTemplate }); this.playBtn = this.el.querySelector(".shk-btn_toggle"); this.fwdBtn = this.el.querySelector(".shk-btn_forward"); this.bwdBtn = this.el.querySelector(".shk-btn_backward"); this.speedBtn = this.el.querySelector(".shk-btn_speed"); this.moreBtn = this.el.querySelector(".shk-btn_more"); this.muteBtn = this.el.querySelector(".shk-btn_volume"); this.extraControls = this.el.querySelector(".shk-controls_extra"); this.texts = this.el.querySelector(".shk-text"); this.artist = this.el.querySelector(".shk-artist"); this.artistWrap = this.el.querySelector(".shk-artist_wrap"); this.titleWrap = this.el.querySelector(".shk-title_wrap"); this.titleInner = this.el.querySelector(".shk-title_inner"); this.title = this.el.querySelector(".shk-title"); this.currentTime = this.el.querySelector(".shk-time_now"); this.duration = this.el.querySelector(".shk-time_duration"); this.bar = this.el.querySelector(".shk-bar"); this.barWrap = this.el.querySelector(".shk-bar_wrap"); this.audioPlayed = this.el.querySelector(".shk-bar_played"); this.audioLoaded = this.el.querySelector(".shk-bar_loaded"); this.handle = this.el.querySelector(".shk-bar-handle"); this.cover = this.el.querySelector(".shk-cover"); this.seekControls = [this.fwdBtn, this.bwdBtn, this.handle]; } initOptions(options) { this.el.style = `--color-primary: ${options.themeColor}`; this.el.setAttribute("data-theme", options.theme); if (options.download && options.audio && options.audio.src) { this.downloadBtn = createElement({ tag: "a", className: ["shk-btn", "shk-btn_download"], attrs: { title: "download", "aria-label": "download", href: typeof options.download === "string" ? options.download : options.audio.src, download: "", target: "_blank", rel: "noopener noreferrer" }, innerHTML: ` <svg aria-hidden="true"> <use xlink:href="#shk-icon_download" /> </svg> ` }); this.extraControls.append(this.downloadBtn); } this.el.setAttribute("data-fixed-type", options.fixed.type); if (options.fixed.type !== "static" && options.fixed.position === "top") { this.el.setAttribute("data-fixed-pos", options.fixed.position); } options.autoPlay ? this.setPlaying() : this.setPaused(); if (options.muted) { this.setMute(options.muted); } } initEvents(supportsPassive2) { this.moreBtn.addEventListener("click", () => { toggleAttribute(this.el, "data-extra"); }); Array.from(this.extraControls.children).forEach((el) => { this.hideExtraControl(el); }); applyFocusVisiblePolyfill(this.el, supportsPassive2); resize$1 = () => { if (!cooldown) return; cooldown = false; setTimeout(() => cooldown = true, 100); marquee.call(this, this.titleWrap, this.title); }; window.addEventListener("resize", resize$1); } setAudioInfo(audio = {}) { if (coverUrl) { URL.revokeObjectURL(coverUrl); coverUrl = null; } if (/blob/.test(audio.cover)) { coverUrl = audio.cover; } if (audio.cover) { this.cover.style.backgroundImage = `url(${audio.cover})`; } else { this.cover.style.backgroundImage = "none"; } this.title.innerHTML = audio.title; this.titleInner.setAttribute("data-title", audio.title); this.artist.innerHTML = audio.artist; if (audio.duration) { this.duration.innerHTML = secondToTime(audio.duration); } if (this.downloadBtn) { this.downloadBtn.href = audio.src; } this.setBar("loaded", 0); this.setLive(audio.live); marquee(this.titleWrap, this.title); } setPlaying() { this.el.setAttribute("data-play", "playing"); } setPaused() { this.el.setAttribute("data-play", "paused"); this.setLoading(false); } setTime(type, time) { this[type].innerHTML = secondToTime(time); } setBar(type, percentage) { const typeName = "audio" + type.charAt(0).toUpperCase() + type.substr(1); percentage = Math.min(percentage, 1); percentage = Math.max(percentage, 0); this[typeName].style.width = percentage * 100 + "%"; const ariaNow = percentage.toFixed(2); this[typeName].setAttribute("aria-valuenow", ariaNow); this.handle.setAttribute("aria-valuenow", ariaNow); } setProgress(time = 0, percentage = 0, duration = 0) { if (time && !percentage) { percentage = duration ? time / duration : 0; } else { time = percentage * (duration || 0); } this.setTime("currentTime", time); this.setBar("played", percentage); } setSpeed(speed) { this.speedBtn.innerHTML = numToString(speed) + "x"; } setMute(mute) { toggleAttribute(this.el, "data-mute", mute); } setLive(live = false) { toggleAttribute(this.el, "data-live", live); } setLoading(loading) { toggleAttribute(this.el, "data-loading", loading); } setSeeking(seeking) { toggleAttribute(this.el, "data-seeking", seeking); } setControls(allowControl) { this.seekControls.forEach((el) => { toggleAttribute(el, "disabled", !allowControl); }); } getPercentByPos(e) { const handlePos = e.clientX || e.changedTouches && e.changedTouches[0].clientX || 0; const initPos = this.barWrap.getBoundingClientRect().left; const barLength = this.barWrap.clientWidth; let percentage = (handlePos - initPos) / barLength; percentage = Math.min(percentage, 1); percentage = Math.max(0, percentage); return percentage; } hideExtraControl(el) { el.addEventListener("click", () => { setTimeout(() => { this.el.removeAttribute("data-extra"); }, 800); }); } mount(container, supportsPassive2) { container.innerHTML = ""; container.append(this.el); if (this.icons) { container.append(this.icons); } this.mounted = true; this.initEvents(supportsPassive2); marquee(this.titleWrap, this.title); } destroy() { window.removeEventListener("resize", resize$1); if (coverUrl) { URL.revokeObjectURL(coverUrl); } } } class Events { constructor() { this.audioEvents = ["abort", "canplay", "canplaythrough", "complete", "durationchange", "emptied", "ended", "error", "loadeddata", "loadedmetadata", "loadstart", "pause", "play", "playing", "progress", "ratechange", "seeked", "seeking", "stalled", "suspend", "timeupdate", "volumechange", "waiting"]; this.playerEvents = ["audioupdate", "audioparse"]; this.events = {}; } on(name, callback) { if (this.type(name) && typeof callback == "function") { if (!this.events[name]) { this.events[name] = []; } this.events[name].push(callback); } } trigger(name, data = {}) { if (this.events[name] && this.events[name].length) { this.events[name].forEach((fn) => fn(data)); } } type(name) { if (this.playerEvents.indexOf(name) !== -1) { return "player"; } else if (this.audioEvents.indexOf(name) !== -1) { return "audio"; } console.error(`Shikwasa: unknown event name: ${name}`); return null; } destroy() { this.events = {}; } } const playerArr = []; const REGISTERED_COMPS = []; const isMobile = typeof window !== "undefined" ? /mobile/i.test(window.navigator.userAgent) : false; const dragStart = isMobile ? "touchstart" : "mousedown"; const dragMove = isMobile ? "touchmove" : "mousemove"; const dragEnd = isMobile ? "touchend" : "mouseup"; let supportsPassive = false; if (typeof window !== "undefined") { try { const opts = Object.defineProperty({}, "passive", { get: function() { supportsPassive = true; return false; } }); window.addEventListener("testPassvie", null, opts); window.removeEventListener("testPassvie", null, opts); } catch (e) { supportsPassive = false; } } const addPassive = supportsPassive && isMobile; class Player { constructor(options) { this.id = playerArr.length; playerArr.push(this); this.comps = {}; this._audio = {}; this._hasMediaSession = false; this._initSeek = 0; this.live = false; this._canplay = false; this._dragging = false; this.events = new Events(); this.options = handleOptions(options); this.renderComponents(); this.initUI(this.options); this.initAudio(); this.ui.mount(this.options.container, supportsPassive); } get duration() { if (!this.audio || !this.audio.duration) { return this._audio.duration; } return this.audio.duration; } get seekable() { return !this.live && Boolean(this.duration); } set seekable(v) { this.ui.setControls(v); } get currentTime() { return this.audio ? this.audio.currentTime : void 0; } get playbackRate() { return this.audio ? this.audio.playbackRate : void 0; } set playbackRate(v) { if (this.audio) { this.audio.playbackRate = v; this.audio.defaultPlaybackRate = v; this.ui.setSpeed(v); } } get muted() { return this.audio ? this.audio.muted : void 0; } set muted(v) { if (this.audio) { this.audio.muted = v; this.audio.defaultMuted = v; this.ui.setMute(v); } } initUI() { this.ui = new UI(this.options); this.el = this.ui.el; this.initControlEvents(); this.initBarEvents(); } initControlEvents() { this.ui.playBtn.addEventListener("click", () => { this.toggle(); }); this.ui.muteBtn.addEventListener("click", () => { this.muted = !this.muted; }); this.ui.fwdBtn.addEventListener("click", () => { this.seekBySpan(); }); this.ui.bwdBtn.addEventListener("click", () => { this.seekBySpan({ forward: false }); }); this.ui.speedBtn.addEventListener("click", () => { const index = this.options.speedOptions.indexOf(this.playbackRate); const speedRange = this.options.speedOptions; this.playbackRate = index + 1 >= speedRange.length ? speedRange[0] : speedRange[index + 1]; }); } initBarEvents() { let targetTime = 0; const dragStartHandler = (e) => { if (!this.seekable) return; e.preventDefault(); this.ui.setSeeking(true); this._dragging = true; document.addEventListener(dragMove, dragMoveHandler, addPassive ? { passive: true } : false); document.addEventListener(dragEnd, dragEndHandler); }; const dragMoveHandler = (e) => { this.ui.setProgress(null, this.ui.getPercentByPos(e), this.duration); }; const dragEndHandler = (e) => { e.preventDefault(); document.removeEventListener(dragMove, dragMoveHandler); document.removeEventListener(dragEnd, dragEndHandler); targetTime = this.ui.getPercentByPos(e) * this.duration; this.seek(targetTime); this._dragging = false; setTimeout(() => this.ui.setSeeking(false), 50); }; const keydownHandler = (e) => { if (!this.seekable) return; const key = e.key.replace("Arrow", ""); const backwardKeys = ["Left", "Down"]; const forwardKeys = ["Right", "Up"]; const largeStepKeys = ["PageDown", "PageUp"]; const edgeKeys = ["Home", "End"]; const isBack = backwardKeys.indexOf(key) !== -1; const isFwd = forwardKeys.indexOf(key) !== -1; const isWayBack = key === largeStepKeys[0]; const isWayFwd = key === largeStepKeys[1]; const isStart = key === edgeKeys[0]; const isEnd = key === edgeKeys[1]; if (!isBack && !isFwd && largeStepKeys.indexOf(key) === -1 && edgeKeys.indexOf(key) === -1) { return; } if (isStart) { this.seek(0); return; } if (isEnd) { this.seek(this.duration); return; } const step = (isWayFwd || isWayBack ? 0.1 : 0.01) * (isFwd || isWayFwd ? 1 : -1); const currentTime = this._canplay ? this.currentTime : this._initSeek; const time = step * this.duration + currentTime; this.seek(time); }; this.ui.barWrap.addEventListener(dragStart, dragStartHandler); this.ui.handle.addEventListener("keydown", keydownHandler); } initAudio() { if (this.options.audio.src) { this.audio = new Audio(); this.initAudioEvents(); this.events.audioEvents.forEach((name) => { this.audio.addEventListener(name, (e) => { this.events.trigger(name, e); }); }); this.audio.preload = this.options.preload; this.audio.playbackRate = this.options.initSpeed; this.muted = this.options.muted; this.update(this.options.audio); } } initAudioEvents() { this.on("play", () => { this.ui.setPlaying(); playerArr.forEach((player) => { if (player.id !== this.id && player.audio && !player.audio.paused) { player.pause(); } }); }); this.on("pause", () => { this.ui.setPaused(); }); this.on("ended", () => { this.ui.setPaused(); this.seek(0); }); this.on("waiting", () => { this.ui.setLoading(true); }); this.on("durationchange", () => { if (this.duration !== Infinity && this.live) { this.live = false; } if (this.duration && this.duration !== 1 && this.duration !== Infinity) { this.seekable = true; this.ui.setTime("duration", this.duration); } }); this.on("canplay", () => { if (!this._canplay) { this._canplay = true; if (this._initSeek) { this.seek(this._initSeek); this._initSeek = 0; } } if (this.duration === Infinity && !this.live) { this.live = true; this.ui.setLive(this.live); } }); this.on("canplaythrough", () => { this.ui.setLoading(false); }); this.on("progress", () => { if (this.audio.buffered.length) { const percentage = this.audio.buffered.length ? this.audio.buffered.end(this.audio.buffered.length - 1) / this.duration : 0; this.ui.setBar("loaded", percentage); } }); this.on("timeupdate", () => { if (!this._dragging) { this.ui.setProgress(this.audio.currentTime, null, this.duration); } }); this.on("abort", () => { this.ui.setPaused(); }); this.on("audioupdate", (audio) => { this.seekable = audio.duration && audio.duration !== Infinity; this.updateMetadata(audio); }); this.on("audioparse", (audio) => { this.seekable = audio.duration && audio.duration !== Infinity; this.updateMetadata(audio); }); } initMediaSession() { const self = this; if ("mediaSession" in navigator) { this._hasMediaSession = true; this.setMediaMetadata(this._audio); const controls = { play: this.play.bind(self), pause: this.pause.bind(self), seekforward: this.seekBySpan.bind(self), seekbackward: () => this.seekBySpan({ forward: false }), seekto: this.seek.bind(self) }; Object.keys(controls).forEach((key) => { navigator.mediaSession.setActionHandler(key, controls[key]); }); } } setMediaMetadata(audio) { const artwork = audio.cover ? [{ src: audio.cover, sizes: "150x150" }] : void 0; if ("MediaMetadata" in window) { navigator.mediaSession.metadata = new window.MediaMetadata({ title: audio.title, artist: audio.artist, album: audio.album, artwork }); } } on(name, callback) { this.events.on(name, callback); } play() { if (!this.audio.paused) return; const promise = this.audio.play(); if (promise instanceof Promise) { promise.then(() => { this.initMediaSession(); }); promise.catch((e) => { if (e.name === "NotAllowedError" || e.name === "NotSupportedError") { this.pause(); } }); } else { this.initMediaSession(); } return promise; } pause() { if (this.audio.paused) return; this.audio.pause(); } toggle() { return this.audio.paused ? this.play() : this.pause(); } seek(time) { if (!this.seekable) return; time = parseFloat(time); if (isNaN(time)) { console.error("Shikwasa: seeking time is NaN"); } time = Math.min(time, this.duration); time = Math.max(time, 0); this.ui.setProgress(time, null, this.duration); if (!this._canplay) { this._initSeek = time; } else { this.audio.currentTime = time; } } seekBySpan({ time = 10, forward = true } = {}) { const currentTime = this._canplay ? this.audio.currentTime : this._initSeek; const targetTime = currentTime + time * (forward ? 1 : -1); this.seek(targetTime); } update(audio) { if (audio && audio.src) { this._audio = handleAudio(audio); this.live = this._audio.live; this._canplay = false; this.audio.src = this._audio.src; this.events.trigger("audioupdate", this._audio); const metaIncomplete = !audio.title || !audio.artist || !audio.cover || !audio.chapters; if (!this.live && this.options.parser && metaIncomplete) { parseAudio(Object.assign({}, audio), this.options.parser).then((audioData) => { this._audio = audioData || this._audio; this.events.trigger("audioparse", this._audio); }); } } else { throw new Error("Shikwasa: audio source is not specified"); } } updateMetadata(audio) { this.audio.title = audio.title; this.ui.setAudioInfo(audio); if (this._hasMediaSession) { this.setMediaMetadata(audio); } } destroyAudio() { this.audio.pause(); this.audio.src = ""; this.audio.load(); this.audio = null; } destroy() { this.events.destroy(); this.destroyAudio(); this.ui.destroy(); Object.keys(this.comps).forEach((k) => { if (this.comps[k].destroy && typeof this.comps[k].destroy === "function") { this.comps[k].destroy(); } }); this.comps = null; this.options.container.innerHTML = ""; } renderComponents() { if (!REGISTERED_COMPS.length) return; REGISTERED_COMPS.forEach((comp) => { this.comps[comp._name] = new comp(this); }); } } Player.use = function(comp) { REGISTERED_COMPS.push(comp); }; const chapterTemplate = ` <div class="shk-chapter_main"> <ol class="shk-chapter_list"></ol> </div> <button class="shk-btn shk-btn_close" aria-label="close chapter panel" title="close chapter panel"> <svg class="shk-icon_close" aria-hidden="true"> <use xlink:href="#shk-icon_close" /> </svg> </button> `; let resize; class Chapter { constructor(ctx) { this.ctx = ctx; this.list = []; this.initEvents(); this.current = null; this._currentSrc = null; this._chapterPatched = false; } init() { this.patchPlayer(); this.ui = new ChapterUI(this.ctx); this.ctx.on("chapterchange", (data) => { const id = data && data.newVal ? data.newVal.id : null; this.ui.setChapterActive(id); }); } initEvents() { this.ctx.on("audioupdate", (audio) => { if (!this._chapterPatched) { this.init(); this._chapterPatched = true; } this.updateList(audio); }); this.ctx.on("audioparse", (audio) => { this.updateList(audio); }); this.ctx.on("timeupdate", this.onTimeupdate.bind(this)); } clearList() { this.ui.chapterList.innerHTML = ""; this.list = []; this.current = null; } updateList(audio) { if (this.list.length) { this.clearList(); } if (audio.chapters.length) { this.list = this.handleChapters(audio); this.ui.renderChapterList(this.ctx.chapters); this.clickChapterHandler(); } this.ui.handleChapterPanel(this.ctx, audio); } handleChapters(audio) { if (audio.chapters && audio.chapters.length) { return audio.chapters.map((chap, i) => { if (!/^ch\d+$/.test(chap.id)) { chap.id = `ch${i}`; } return chap; }); } } patchPlayer() { const self = this; Object.defineProperties(this.ctx, { chapters: { get() { return self.list; } }, currentChapter: { get() { return self.current; } } }); this.ctx.events.playerEvents.push("chapterchange"); this.ctx.updateChapter = updateChapter.bind(self); function updateChapter(index) { this.setCurrent(this.list[index]); this.ctx.seek(this.current.startTime); this.ctx.play(); } } setCurrent(chapter2) { const _oldCurrentChapter = this.current ? { ...this.current } : null; this.current = chapter2; this.ctx.events.trigger("chapterchange", { newVal: this.current, oldVal: _oldCurrentChapter }); } onTimeupdate(e) { if (this._currentSrc !== e.currentTarget.src) { this._currentSrc = e.currentTarget.src; return; } const direction = this.searchDirection(this.ctx.currentTime, this.current); if (direction) { let searchPool; const index = this.list.indexOf(this.current); if (index === -1) { searchPool = this.list; } else { searchPool = direction === 1 ? this.list.slice(index) : this.list.slice(0, index + 1); } const currentChapter = searchPool.find((ch) => { return !this.searchDirection(this.ctx.currentTime, ch); }); this.setCurrent(currentChapter); } } searchDirection(time, chapter2) { time = Math.round(time); if (!chapter2 || typeof chapter2 !== "object" || chapter2.endTime <= time) { return 1; } if (chapter2.startTime > time) { return -1; } return 0; } clickChapterHandler() { Array.from(this.ui.chapterList.children).forEach((chEl) => { chEl.addEventListener("click", () => { if (!this.ctx.seekable) return; let id = chEl.getAttribute("data-id").match(/\d+$/); if (id) { this.ctx.updateChapter(+id[0]); } }); }); } destroy() { this.ui.destroy(); } } class ChapterUI { constructor(player, audio) { this.initEl(player); this.initEvents(player, audio); this.renderChapterList(player.chapters); player.ui.el.append(this.el); this.activeChapterEl = null; } initEl(player) { this.el = createElement({ className: "shk-chapter", innerHTML: chapterTemplate }); const attrs = { title: "view chapters", "aria-label": "view chapters" }; if (!player.seekable) { attrs.disabled = ""; } this.chapterBtn = createElement({ tag: "button", className: ["shk-btn", "shk-btn_chapter"], attrs, innerHTML: ` <svg aria-hidden="true"> <use xlink:href="#shk-icon_chapter" /> </svg> ` }); player.ui.seekControls.push(this.chapterBtn); player.ui.extraControls.append(this.chapterBtn); this.closeBtn = this.el.querySelector(".shk-btn_close"); this.chapterList = this.el.querySelector(".shk-chapter_list"); this.overflowLayer = this.el.querySelector(".shk-chapter_main"); } initEvents(player) { this.chapterBtn.addEventListener("click", () => { toggleAttribute(player.el, "data-show-chapter"); }); player.ui.hideExtraControl(this.chapterBtn); this.closeBtn.addEventListener("click", () => { player.el.removeAttribute("data-show-chapter"); }); resize = () => { if (!this.activeChapterEl) return; const textWrap = this.activeChapterEl.querySelector(".shk-chapter_title_wrap"); const text = this.activeChapterEl.querySelector(".shk-chapter_title"); marquee.call(this, textWrap, text); }; window.addEventListener("resize", resize); } handleChapterPanel(player, audio) { if (audio.chapters.length) { player.el.setAttribute("data-has-chapter", ""); } else { player.el.removeAttribute("data-has-chapter"); } if (!audio.chapters.length || !player.seekable) { player.el.removeAttribute("data-show-chapter"); } } renderChapterList(chapters) { if (!chapters.length) return; chapters.forEach((ch) => { const chapterItemEl = this.renderChapterItem(ch); this.chapterList.append(chapterItemEl); }); } renderChapterItem(chapter2) { const startTime = secondToTime(chapter2.startTime); const innerHTML = ` <button class="shk-btn shk-chapter_btn" title="seek chapter: ${chapter2.title}" aria-label="seek chapter: ${chapter2.title}"> <div class="shk-icon_chapter" aria-hidden="true"> <span class="shk-icon_playing"></span> <span class="shk-icon_triangle"> <svg> <use xlink:href="#shk-icon_triangle" /> </svg> </span> </div> <div class="shk-chapter_duration">${startTime}</div> <div class="shk-chapter_title_wrap"> <div class="shk-chapter_title_inner" data-chapter="${chapter2.title}"> <div class="shk-chapter_title">${chapter2.title}</div> </div> </div> </button> `; return createElement({ tag: "li", className: "shk-chapter_item", innerHTML, attrs: { "data-id": chapter2.id } }); } setChapterActive(id) { this.chapterList.querySelectorAll(".shk-chapter_item").forEach((chEl) => { if (chEl.getAttribute("data-id") === id) { chEl.setAttribute("data-active", ""); this.scrollIntoView(chEl); this.activeChapterEl = chEl; const titleEl = chEl.querySelector(".shk-chapter_title"); const titleWrap = chEl.querySelector(".shk-chapter_title_wrap"); marquee(titleWrap, titleEl); } else { chEl.removeAttribute("data-active"); } }); } scrollIntoView(el) { if (this.el.style.visibility === "hidden") return; const layerMargin = window.getComputedStyle(this.overflowLayer).marginTop; const listMargin = window.getComputedStyle(this.chapterList).marginTop; const offsetTop = parseInt(layerMargin) + parseInt(listMargin); const outOfView = this.overflowLayer.scrollTop + offsetTop - el.offsetTop > 0 || el.offsetTop - this.overflowLayer.scrollTop - this.overflowLayer.offsetHeight > 0; const startPos = this.overflowLayer.scrollTop; const distance = el.offsetTop - startPos - offsetTop; const startTime = performance.now(); const duration = 0.2; if (outOfView) { animateScroll( startTime, startTime, duration, startPos, distance, this.overflowLayer ); } } destroy() { window.removeEventListener("resize", resize); } } Chapter._name = "chapter"; console.log(`%c\u{1F34A}%c Shikwasa Podcast Player v2.2.3 %c https://shikwasa.js.org`, "background-color:#00869B40;padding:4px;", "background:#00869B80;color:#fff;padding:4px 0", "padding: 2px 0;"); export { Chapter, Player };