@hekeh/easy-audio-player-vue
Version:
A simple audio player
559 lines (558 loc) • 23.5 kB
JavaScript
(function() {
"use strict";
try {
if (typeof document != "undefined") {
var elementStyle = document.createElement("style");
elementStyle.id = "easy-audio-player-vue-style";
elementStyle.appendChild(document.createTextNode("html {\n --easy-audio-player-text-color: #566574;\n --easy-audio-player-bg-color: #fff;\n --easy-audio-player-icon-color: #566574;\n --easy-audio-player-primary-color: #4a90e2;\n --easy-audio-player-height: 56px;\n}\n\n.easy-audio-player {\n width: 100%;\n min-width: 300px;\n height: var(--easy-audio-player-height);\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 0 15px;\n background-color: var(--easy-audio-player-background-color);\n border-radius: 4px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.07);\n user-select: none;\n cursor: default;\n}\n.easy-audio-player audio {\n display: none !important;\n}\n.easy-audio-player .easy-audio-player-controls {\n display: flex;\n align-items: center;\n width: 100%;\n gap: 8px;\n}\n.easy-audio-player .easy-audio-player-pause-btn {\n display: flex;\n justify-content: center;\n align-items: center;\n height: 28px;\n padding: 0 2px;\n background: transparent;\n border: none;\n cursor: pointer;\n}\n.easy-audio-player .easy-audio-player-pause-btn .easy-audio-player-pause-btn__icon {\n width: 12px;\n height: 16px;\n fill: var(--easy-audio-player-icon-color);\n}\n.easy-audio-player .easy-audio-player-pause-btn:hover {\n opacity: 0.8;\n}\n.easy-audio-player .easy-audio-player-pause-btn.easy-audio-player-pause-btn--loading {\n opacity: 0.5;\n cursor: wait;\n}\n.easy-audio-player .easy-audio-player-right-controls {\n position: relative;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.easy-audio-player .easy-audio-player-right-controls .easy-audio-player-control-btn {\n display: flex;\n justify-content: center;\n align-items: center;\n padding: 0;\n background: none;\n border: none;\n cursor: pointer;\n}\n.easy-audio-player .easy-audio-player-right-controls .easy-audio-player-control-btn .easy-audio-player-control-btn__icon {\n width: 18px;\n height: 18px;\n fill: var(--easy-audio-player-icon-color);\n color: var(--easy-audio-player-icon-color);\n}\n.easy-audio-player .easy-audio-player-right-controls .easy-audio-player-control-btn:hover {\n opacity: 0.8;\n}\n.easy-audio-player svg {\n vertical-align: middle;\n}\n.easy-audio-player svg:focus {\n outline: none;\n}\n\n.easy-audio-player .easy-audio-player-progress {\n flex: 1;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.easy-audio-player .easy-audio-player-progress .easy-audio-player-progress-bar__wrapper {\n flex: 1;\n cursor: pointer;\n padding: 8px 0;\n}\n.easy-audio-player .easy-audio-player-progress .easy-audio-player-progress-bar {\n height: 4px;\n border-radius: 2px;\n background-color: #e4e4e4;\n}\n.easy-audio-player .easy-audio-player-progress .easy-audio-player-progress-bar .easy-audio-player-progress-bar__fill {\n height: 100%;\n background-color: var(--easy-audio-player-primary-color);\n border-radius: 2px;\n position: relative;\n transition: width 0.1s ease-in-out;\n width: 0;\n}\n.easy-audio-player .easy-audio-player-progress .easy-audio-player-progress-bar .easy-audio-player-progress-bar__pin {\n width: 12px;\n height: 12px;\n background-color: var(--easy-audio-player-primary-color);\n border-radius: 50%;\n position: absolute;\n right: -6px;\n top: 50%;\n transform: translateY(-50%);\n cursor: grab;\n}\n.easy-audio-player .easy-audio-player-progress .easy-audio-player-progress-bar__pin:active {\n cursor: grabbing;\n}\n.easy-audio-player .easy-audio-player-progress__current-time,\n.easy-audio-player .easy-audio-player-progress__total-time {\n font-size: 12px;\n color: var(--easy-audio-player-text-color);\n width: fit-content;\n padding: 0 2px;\n}\n\n.easy-audio-player-volume__popover {\n min-width: unset !important;\n padding: 10px 0 8px 0 !important;\n background: #fff;\n border-radius: 4px;\n border: 1px solid #e4e7ed;\n}\n.easy-audio-player-volume__popover .easy-audio-player-volume-bar__wrapper {\n height: 100px;\n width: 20px;\n display: flex;\n justify-content: center;\n}\n.easy-audio-player-volume__popover .easy-audio-player-volume-bar {\n width: 4px;\n height: 100%;\n background: #e4e4e4;\n border-radius: 2px;\n position: relative;\n cursor: pointer;\n}\n.easy-audio-player-volume__popover .easy-audio-player-volume-bar .easy-audio-player-volume-bar__fill {\n position: absolute;\n bottom: 0;\n width: 100%;\n background: var(--easy-audio-player-primary-color);\n border-radius: 2px;\n transition: height 0.1s ease-in-out;\n}\n.easy-audio-player-volume__popover .easy-audio-player-volume-bar .easy-audio-player-volume-bar__pin {\n width: 10px;\n height: 10px;\n background: var(--easy-audio-player-primary-color);\n border-radius: 50%;\n position: absolute;\n right: -3px;\n top: -5px;\n cursor: grab;\n}\n.easy-audio-player-volume__popover .easy-audio-player-volume-bar .easy-audio-player-volume-bar__pin:active {\n cursor: grabbing;\n}"));
document.head.appendChild(elementStyle);
}
} catch (e) {
console.error("vite-plugin-css-injected-by-js", e);
}
})();
import { defineComponent, toRef, ref, watch, onBeforeUnmount, openBlock, createElementBlock, normalizeClass, createElementVNode, toDisplayString, normalizeStyle, withModifiers, computed, resolveComponent, createBlock, withCtx, Fragment, onMounted, useId, createVNode, createCommentVNode } from "vue";
import { ElPopover } from "@hekeh/el-popover";
const namespace = "easy-audio-player";
function formatTime(time) {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
function bem(block, element, modifier) {
return `${namespace}-${block}${element ? `__${element}` : ""}${modifier ? `--${modifier}` : ""}`;
}
function useProcess(player) {
const progressBar = ref(null);
const progressBarPin = ref(null);
const currentTime = ref("00:00");
const totalTime = ref("00:00");
const isDragging = ref(false);
const percent = ref(0);
const updateProgress = () => {
if (!player.value || !progressBar.value || isDragging.value) {
return;
}
const current = player.value.currentTime;
const duration = player.value.duration;
percent.value = current / duration;
currentTime.value = formatTime(current);
};
const handleProgressBarClick = (event) => {
var _a;
if (!player.value || !((_a = progressBar.value) == null ? void 0 : _a.parentElement)) {
return;
}
const bounds = progressBar.value.parentElement.getBoundingClientRect();
const x = event.clientX - bounds.left;
const width = bounds.width;
percent.value = x / width;
player.value.currentTime = percent.value * player.value.duration;
};
const handleDraggingProcess = (event) => {
var _a;
if (!player.value || !((_a = progressBar.value) == null ? void 0 : _a.parentElement)) {
return;
}
const bounds = progressBar.value.parentElement.getBoundingClientRect();
const x = Math.max(0, Math.min(event.clientX - bounds.left, bounds.width));
percent.value = x / bounds.width;
player.value.currentTime = percent.value * player.value.duration;
};
const stopDraggingProcess = () => {
isDragging.value = false;
removeDraggingProcessListeners();
};
const removeDraggingProcessListeners = () => {
document.removeEventListener("mousemove", handleDraggingProcess);
document.removeEventListener("mouseup", stopDraggingProcess);
};
const startDraggingProcess = (event) => {
event.stopPropagation();
event.preventDefault();
isDragging.value = true;
document.addEventListener("mousemove", handleDraggingProcess);
document.addEventListener("mouseup", stopDraggingProcess);
};
const playerEvents = {
timeupdate: updateProgress,
loadedmetadata: () => {
var _a;
totalTime.value = formatTime(((_a = player.value) == null ? void 0 : _a.duration) || 0);
}
};
watch(
() => player.value,
(newPlayer, oldPlayer) => {
if (oldPlayer) {
Object.entries(playerEvents).forEach(([event, handler]) => {
oldPlayer.removeEventListener(event, handler);
});
}
if (newPlayer) {
Object.entries(playerEvents).forEach(([event, handler]) => {
newPlayer.addEventListener(event, handler);
});
}
}
);
onBeforeUnmount(() => {
const playerDom = player.value;
if (playerDom) {
Object.entries(playerEvents).forEach(([event, handler]) => {
playerDom.removeEventListener(event, handler);
});
}
removeDraggingProcessListeners();
});
return {
progressBar,
progressBarPin,
currentTime,
totalTime,
percent,
updateProgress,
handleProgressBarClick,
startDraggingProcess
};
}
const PlayerProgress$1 = defineComponent({
name: "PlayerProgress",
props: {
player: {
type: [Object, null],
required: true
}
},
setup(props) {
const {
progressBar,
progressBarPin,
currentTime,
totalTime,
percent,
updateProgress,
handleProgressBarClick,
startDraggingProcess
} = useProcess(toRef(props, "player"));
return {
progressBar,
progressBarPin,
currentTime,
totalTime,
percent,
updateProgress,
handleProgressBarClick,
startDraggingProcess,
bem
};
}
});
const _export_sfc = (sfc, props) => {
const target = sfc.__vccOpts || sfc;
for (const [key, val] of props) {
target[key] = val;
}
return target;
};
function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
return openBlock(), createElementBlock("div", {
class: normalizeClass(_ctx.bem("progress"))
}, [
createElementVNode("span", {
class: normalizeClass(_ctx.bem("progress", "current-time"))
}, toDisplayString(_ctx.currentTime), 3),
createElementVNode("div", {
class: normalizeClass(_ctx.bem("progress-bar", "wrapper")),
onClick: _cache[2] || (_cache[2] = (...args) => _ctx.handleProgressBarClick && _ctx.handleProgressBarClick(...args))
}, [
createElementVNode("div", {
class: normalizeClass(_ctx.bem("progress-bar"))
}, [
createElementVNode("div", {
ref: "progressBar",
class: normalizeClass(_ctx.bem("progress-bar", "fill")),
style: normalizeStyle({ width: `${_ctx.percent * 100}%` })
}, [
createElementVNode("div", {
ref: "progressBarPin",
class: normalizeClass(_ctx.bem("progress-bar", "pin")),
onMousedown: _cache[0] || (_cache[0] = (...args) => _ctx.startDraggingProcess && _ctx.startDraggingProcess(...args)),
onDragstart: _cache[1] || (_cache[1] = withModifiers(() => {
}, ["prevent"]))
}, null, 34)
], 6)
], 2)
], 2),
createElementVNode("span", {
class: normalizeClass(_ctx.bem("progress", "total-time"))
}, toDisplayString(_ctx.totalTime), 3)
], 2);
}
const PlayerProgress = /* @__PURE__ */ _export_sfc(PlayerProgress$1, [["render", _sfc_render$2]]);
function useVolume(setVolume) {
const volumeBar = ref(null);
const volumeBarPin = ref(null);
const volumeBarWrapper = ref(null);
const volumePercentage = ref(1);
const isVolumeOpen = ref(true);
const isDragging = ref(false);
const handleVolumeBarClick = (event) => {
var _a;
if (!((_a = volumeBar.value) == null ? void 0 : _a.parentElement) || isDragging.value) {
return;
}
isVolumeOpen.value = true;
const bounds = volumeBar.value.parentElement.getBoundingClientRect();
const y = bounds.bottom - event.clientY;
const height = bounds.height;
const percentage = Math.max(0, Math.min(y / height, 1));
volumePercentage.value = percentage;
};
const handleDraggingVolume = (event) => {
var _a;
if (!((_a = volumeBar.value) == null ? void 0 : _a.parentElement)) {
return;
}
const bounds = volumeBar.value.parentElement.getBoundingClientRect();
if (bounds.height === 0) {
return;
}
const y = bounds.bottom - event.clientY;
const percentage = Math.round(Math.max(0, Math.min(y / bounds.height * 100, 100))) / 100;
volumePercentage.value = percentage;
};
const removeDraggingVolumeListeners = () => {
document.removeEventListener("mousemove", handleDraggingVolume);
document.removeEventListener("mouseup", stopDraggingVolume);
};
const stopDraggingVolume = () => {
isDragging.value = false;
removeDraggingVolumeListeners();
};
const startDraggingVolume = (event) => {
event.stopPropagation();
event.preventDefault();
isDragging.value = true;
document.addEventListener("mousemove", handleDraggingVolume);
document.addEventListener("mouseup", stopDraggingVolume);
};
onBeforeUnmount(() => {
removeDraggingVolumeListeners();
});
const toggleVolumeOpen = () => {
isVolumeOpen.value = !isVolumeOpen.value;
};
const volume = computed(() => {
return isVolumeOpen.value ? volumePercentage.value : 0;
});
watch(volume, (value) => {
setVolume(value);
});
return {
volumeBar,
volumeBarPin,
volume,
handleVolumeBarClick,
startDraggingVolume,
volumeBarWrapper,
toggleVolumeOpen
};
}
const emits = {
"update:volume": (_) => true
};
const PlayerVolume$1 = defineComponent({
name: "PlayerVolume",
components: {
ElPopover
},
emits,
setup(_, { emit }) {
const setVolume = (volume2) => {
emit("update:volume", volume2);
};
const {
volumeBar,
volumeBarPin,
volumeBarWrapper,
volume,
handleVolumeBarClick,
startDraggingVolume,
toggleVolumeOpen
} = useVolume(setVolume);
return {
volumeBar,
volumeBarPin,
volumeBarWrapper,
volume,
handleVolumeBarClick,
startDraggingVolume,
toggleVolumeOpen,
bem
};
}
});
function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
const _component_ElPopover = resolveComponent("ElPopover");
return openBlock(), createBlock(_component_ElPopover, {
placement: "top",
trigger: "hover",
width: "auto",
"show-arrow": false,
"popper-class": _ctx.bem("volume", "popover"),
offset: 2
}, {
reference: withCtx(() => [
createElementVNode("button", {
class: normalizeClass(_ctx.bem("control-btn")),
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.toggleVolumeOpen && _ctx.toggleVolumeOpen(...args))
}, [
(openBlock(), createElementBlock("svg", {
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 22 22",
class: normalizeClass(_ctx.bem("control-btn", "icon"))
}, [
_ctx.volume === 0 ? (openBlock(), createElementBlock(Fragment, { key: 0 }, [
_cache[4] || (_cache[4] = createElementVNode("path", { d: "M15 11a3.998 3.998 0 0 0-2-3.465v2.636l1.865 1.865A4.02 4.02 0 0 0 15 11z" }, null, -1)),
_cache[5] || (_cache[5] = createElementVNode("path", { d: "M13.583 5.583A5.998 5.998 0 0 1 17 11a6 6 0 0 1-.585 2.587l1.477 1.477a8.001 8.001 0 0 0-3.446-11.286 1 1 0 0 0-.863 1.805zm5.195 13.195-2.121-2.121-1.414-1.414-1.415-1.415L13 13l-2-2-3.889-3.889-3.889-3.889a.999.999 0 1 0-1.414 1.414L5.172 8H5a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h1l4.188 3.35a.5.5 0 0 0 .812-.39v-3.131l2.587 2.587-.01.005a1 1 0 0 0 .86 1.806c.215-.102.424-.214.627-.333l2.3 2.3a1.001 1.001 0 0 0 1.414-1.416zM11 5.04a.5.5 0 0 0-.813-.39L8.682 5.854 11 8.172V5.04z" }, null, -1))
], 64)) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [
_cache[6] || (_cache[6] = createElementVNode("path", {
"fill-rule": "evenodd",
d: "M10.188 4.65 6 8H5a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h1l4.188 3.35a.5.5 0 0 0 .812-.39V5.04a.498.498 0 0 0-.812-.39zm4.258-.872a1 1 0 0 0-.862 1.804 6.002 6.002 0 0 1-.007 10.838 1 1 0 0 0 .86 1.806A8.001 8.001 0 0 0 19 11a8.001 8.001 0 0 0-4.554-7.222z"
}, null, -1)),
_cache[7] || (_cache[7] = createElementVNode("path", { d: "M15 11a3.998 3.998 0 0 0-2-3.465v6.93A3.998 3.998 0 0 0 15 11z" }, null, -1))
], 64))
], 2))
], 2)
]),
default: withCtx(() => [
createElementVNode("div", {
ref: "volumeBarWrapper",
class: normalizeClass(_ctx.bem("volume-bar", "wrapper"))
}, [
createElementVNode("div", {
class: normalizeClass(_ctx.bem("volume-bar")),
onClick: _cache[3] || (_cache[3] = (...args) => _ctx.handleVolumeBarClick && _ctx.handleVolumeBarClick(...args))
}, [
createElementVNode("div", {
ref: "volumeBar",
class: normalizeClass(_ctx.bem("volume-bar", "fill")),
style: normalizeStyle({ height: `${_ctx.volume * 100}%` })
}, [
createElementVNode("div", {
ref: "volumeBarPin",
class: normalizeClass(_ctx.bem("volume-bar", "pin")),
onMousedown: _cache[1] || (_cache[1] = (...args) => _ctx.startDraggingVolume && _ctx.startDraggingVolume(...args)),
onDragstart: _cache[2] || (_cache[2] = withModifiers(() => {
}, ["prevent"]))
}, null, 34)
], 6)
], 2)
], 2)
]),
_: 1
}, 8, ["popper-class"]);
}
const PlayerVolume = /* @__PURE__ */ _export_sfc(PlayerVolume$1, [["render", _sfc_render$1]]);
const EasyAudioPlayer$1 = defineComponent({
name: "EasyAudioPlayer",
components: {
PlayerVolume,
PlayerProgress
},
props: {
url: {
type: String,
required: true
},
options: {
type: Object,
default: () => ({})
}
},
setup(props) {
const container = ref(null);
const player = ref(null);
const isPlaying = ref(false);
const isLoading = ref(false);
const { stopOthersOnPlay = true } = props.options;
const onUpdateVolume = (volume) => {
if (!player.value) {
return;
}
player.value.volume = volume;
};
const closePlayer = () => {
var _a;
(_a = player.value) == null ? void 0 : _a.pause();
isPlaying.value = false;
};
const { stopOtherPlayers } = useStopOtherPlayers(closePlayer);
const togglePlay = async () => {
if (!player.value) {
return;
}
try {
if (player.value.paused) {
if (stopOthersOnPlay) {
stopOtherPlayers();
}
isLoading.value = true;
await player.value.play();
isPlaying.value = true;
} else {
closePlayer();
}
} catch (error) {
console.error("Play fail:", error);
} finally {
isLoading.value = false;
}
};
const playerEvents = {
waiting: () => {
isLoading.value = true;
},
canplay: () => {
isLoading.value = false;
}
};
onMounted(() => {
if (!player.value) {
return;
}
Object.entries(playerEvents).forEach(([event, handler]) => {
var _a;
(_a = player.value) == null ? void 0 : _a.addEventListener(event, handler);
});
});
onBeforeUnmount(() => {
if (!player.value) {
return;
}
Object.entries(playerEvents).forEach(([event, handler]) => {
var _a;
(_a = player.value) == null ? void 0 : _a.removeEventListener(event, handler);
});
});
const downloadAudio = () => {
const link = document.createElement("a");
link.href = props.url;
link.download = props.url.split("/").pop() || "audio";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return {
container,
player,
isPlaying,
isLoading,
showDownloadButton: computed(
() => props.options.showDownloadButton ?? true
),
namespace,
togglePlay,
downloadAudio,
onUpdateVolume,
bem
};
}
});
function useStopOtherPlayers(closePlayer) {
const EventName = "stop-easy-audio-player";
const id = useId();
const stopOtherPlayers = () => {
document.dispatchEvent(
new CustomEvent(EventName, {
detail: { excludedId: id }
})
);
};
const stopOtherPlayersHandler = (event) => {
if (event.detail.excludedId === id) {
return;
}
closePlayer();
};
onMounted(() => {
document.addEventListener(
EventName,
stopOtherPlayersHandler
);
});
onBeforeUnmount(() => {
document.removeEventListener(
EventName,
stopOtherPlayersHandler
);
});
return { stopOtherPlayers };
}
const _hoisted_1 = ["disabled"];
const _hoisted_2 = ["d"];
const _hoisted_3 = ["src"];
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_PlayerProgress = resolveComponent("PlayerProgress");
const _component_PlayerVolume = resolveComponent("PlayerVolume");
return openBlock(), createElementBlock("div", {
ref: "container",
class: normalizeClass(_ctx.namespace)
}, [
createElementVNode("div", {
class: normalizeClass(_ctx.bem("controls"))
}, [
createElementVNode("button", {
class: normalizeClass([
_ctx.bem("pause-btn"),
{ [_ctx.bem("pause-btn", "", "loading")]: _ctx.isLoading }
]),
disabled: _ctx.isLoading,
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.togglePlay && _ctx.togglePlay(...args))
}, [
(openBlock(), createElementBlock("svg", {
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 18 24",
class: normalizeClass(_ctx.bem("pause-btn", "icon"))
}, [
createElementVNode("path", {
"fill-rule": "evenodd",
d: _ctx.isPlaying ? "M0 0h6v24H0zM12 0h6v24h-6z" : "M18 12L0 24V0"
}, null, 8, _hoisted_2)
], 2))
], 10, _hoisted_1),
createVNode(_component_PlayerProgress, { player: _ctx.player }, null, 8, ["player"]),
createElementVNode("div", {
class: normalizeClass(_ctx.bem("right-controls"))
}, [
createVNode(_component_PlayerVolume, { "onUpdate:volume": _ctx.onUpdateVolume }, null, 8, ["onUpdate:volume"]),
_ctx.showDownloadButton ? (openBlock(), createElementBlock("button", {
key: 0,
class: normalizeClass(_ctx.bem("control-btn")),
onClick: _cache[1] || (_cache[1] = (...args) => _ctx.downloadAudio && _ctx.downloadAudio(...args))
}, [
(openBlock(), createElementBlock("svg", {
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 24 24",
class: normalizeClass(_ctx.bem("control-btn", "icon"))
}, _cache[2] || (_cache[2] = [
createElementVNode("path", {
"fill-rule": "evenodd",
d: "M19 9h-4V3H9v6H5l7 8 7-8zM5 18v3h14v-3H5z"
}, null, -1)
]), 2))
], 2)) : createCommentVNode("", true)
], 2),
createElementVNode("audio", {
ref: "player",
src: _ctx.url,
preload: "metadata"
}, null, 8, _hoisted_3)
], 2)
], 2);
}
const EasyAudioPlayer = /* @__PURE__ */ _export_sfc(EasyAudioPlayer$1, [["render", _sfc_render]]);
export {
EasyAudioPlayer as default
};