diff --git a/src/assets/circulation.svg b/src/assets/circulation.svg new file mode 100644 index 00000000..936e5dd7 --- /dev/null +++ b/src/assets/circulation.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/menu.svg b/src/assets/menu.svg new file mode 100644 index 00000000..f3fe8c30 --- /dev/null +++ b/src/assets/menu.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/order.svg b/src/assets/order.svg new file mode 100644 index 00000000..3e30de1a --- /dev/null +++ b/src/assets/order.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/pause.svg b/src/assets/pause.svg new file mode 100644 index 00000000..60984d76 --- /dev/null +++ b/src/assets/pause.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/play.svg b/src/assets/play.svg new file mode 100644 index 00000000..0a279787 --- /dev/null +++ b/src/assets/play.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/random.svg b/src/assets/random.svg new file mode 100644 index 00000000..fb2873e2 --- /dev/null +++ b/src/assets/random.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/single.svg b/src/assets/single.svg new file mode 100644 index 00000000..208bab82 --- /dev/null +++ b/src/assets/single.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/volume-down.svg b/src/assets/volume-down.svg new file mode 100644 index 00000000..d09fb854 --- /dev/null +++ b/src/assets/volume-down.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/volume-off.svg b/src/assets/volume-off.svg new file mode 100644 index 00000000..62579baa --- /dev/null +++ b/src/assets/volume-off.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/volume-up.svg b/src/assets/volume-up.svg new file mode 100644 index 00000000..dd53ef49 --- /dev/null +++ b/src/assets/volume-up.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/css/index.scss b/src/css/index.scss index 46800a4c..909197c2 100644 --- a/src/css/index.scss +++ b/src/css/index.scss @@ -63,6 +63,28 @@ $aplayer-height-lrc: $aplayer-height + $lrc-height - 6; box-sizing: content-box; } + svg { + width: 100%; + height: 100%; + + path, + circle { + fill: #fff; + } + } + + &.aplayer-mobile { + .aplayer-icon-volume-down { + display: none; + } + } + + &.aplayer-arrow { + .aplayer-icon-mode { + display: none; + } + } + .aplayer-icon { width: 15px; height: 15px; @@ -77,7 +99,7 @@ $aplayer-height-lrc: $aplayer-height + $lrc-height - 6; margin: 0; display: inline; - .aplayer-fill { + path { transition: all .2s ease-in-out; } } @@ -108,7 +130,7 @@ $aplayer-height-lrc: $aplayer-height + $lrc-height - 6; opacity: 1; } - .aplayer-fill { + path { fill: #fff; } } @@ -240,7 +262,7 @@ $aplayer-height-lrc: $aplayer-height + $lrc-height - 6; cursor: pointer; transition: all 0.2s ease; - .aplayer-fill { + path { fill: #666; } @@ -249,7 +271,7 @@ $aplayer-height-lrc: $aplayer-height + $lrc-height - 6; } &:hover { - .aplayer-fill { + path { fill: #000; } } diff --git a/src/js/bar.js b/src/js/bar.js new file mode 100644 index 00000000..1bc22089 --- /dev/null +++ b/src/js/bar.js @@ -0,0 +1,27 @@ +class Bar { + constructor (template) { + this.elements = {}; + this.elements.volume = template.volume; + this.elements.played = template.played; + this.elements.loaded = template.loaded; + } + + /** + * Update progress + * + * @param {String} type - Point out which bar it is + * @param {Number} percentage + * @param {String} direction - Point out the direction of this bar, Should be height or width + */ + set (type, percentage, direction) { + percentage = Math.max(percentage, 0); + percentage = Math.min(percentage, 1); + this.elements[type].style[direction] = percentage * 100 + '%'; + } + + get (type, direction) { + return parseFloat(this.elements[type].style[direction]) / 100; + } +} + +export default Bar; \ No newline at end of file diff --git a/src/js/controller.js b/src/js/controller.js new file mode 100644 index 00000000..e69de29b diff --git a/src/js/events.js b/src/js/events.js new file mode 100644 index 00000000..e69de29b diff --git a/src/js/icons.js b/src/js/icons.js new file mode 100644 index 00000000..49e92ce6 --- /dev/null +++ b/src/js/icons.js @@ -0,0 +1,25 @@ +import play from '../assets/play.svg'; +import pause from '../assets/pause.svg'; +import volumeUp from '../assets/volume-up.svg'; +import volumeDown from '../assets/volume-down.svg'; +import volumeOff from '../assets/volume-off.svg'; +import circulation from '../assets/circulation.svg'; +import random from '../assets/random.svg'; +import order from '../assets/order.svg'; +import single from '../assets/single.svg'; +import menu from '../assets/menu.svg'; + +const Icons = { + play: play, + pause: pause, + volumeUp: volumeUp, + volumeDown: volumeDown, + volumeOff: volumeOff, + circulation: circulation, + random: random, + order: order, + single: single, + menu: menu, +}; + +export default Icons; diff --git a/src/js/index.js b/src/js/index.js index 618113ec..98f68a4b 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -1,946 +1,7 @@ -console.log("\n %c APlayer 1.6.1 %c http://aplayer.js.org \n\n", "color: #fadfa3; background: #030307; padding:5px 0;", "background: #fadfa3; padding:5px 0;"); - import '../css/index.scss'; +import APlayer from './player'; -const instances = []; - -class APlayer { - - /** - * APlayer constructor function - * - * @param {Object} option - See README - * @constructor - */ - constructor (option) { - const svg = { - 'play': ['0 0 16 31', 'M15.552 15.168q0.448 0.32 0.448 0.832 0 0.448-0.448 0.768l-13.696 8.512q-0.768 0.512-1.312 0.192t-0.544-1.28v-16.448q0-0.96 0.544-1.28t1.312 0.192z'], - 'pause': ['0 0 17 32', 'M14.080 4.8q2.88 0 2.88 2.048v18.24q0 2.112-2.88 2.112t-2.88-2.112v-18.24q0-2.048 2.88-2.048zM2.88 4.8q2.88 0 2.88 2.048v18.24q0 2.112-2.88 2.112t-2.88-2.112v-18.24q0-2.048 2.88-2.048z'], - 'volume-up': ['0 0 28 32', 'M13.728 6.272v19.456q0 0.448-0.352 0.8t-0.8 0.32-0.8-0.32l-5.952-5.952h-4.672q-0.48 0-0.8-0.352t-0.352-0.8v-6.848q0-0.48 0.352-0.8t0.8-0.352h4.672l5.952-5.952q0.32-0.32 0.8-0.32t0.8 0.32 0.352 0.8zM20.576 16q0 1.344-0.768 2.528t-2.016 1.664q-0.16 0.096-0.448 0.096-0.448 0-0.8-0.32t-0.32-0.832q0-0.384 0.192-0.64t0.544-0.448 0.608-0.384 0.512-0.64 0.192-1.024-0.192-1.024-0.512-0.64-0.608-0.384-0.544-0.448-0.192-0.64q0-0.48 0.32-0.832t0.8-0.32q0.288 0 0.448 0.096 1.248 0.48 2.016 1.664t0.768 2.528zM25.152 16q0 2.72-1.536 5.056t-4 3.36q-0.256 0.096-0.448 0.096-0.48 0-0.832-0.352t-0.32-0.8q0-0.704 0.672-1.056 1.024-0.512 1.376-0.8 1.312-0.96 2.048-2.4t0.736-3.104-0.736-3.104-2.048-2.4q-0.352-0.288-1.376-0.8-0.672-0.352-0.672-1.056 0-0.448 0.32-0.8t0.8-0.352q0.224 0 0.48 0.096 2.496 1.056 4 3.36t1.536 5.056zM29.728 16q0 4.096-2.272 7.552t-6.048 5.056q-0.224 0.096-0.448 0.096-0.48 0-0.832-0.352t-0.32-0.8q0-0.64 0.704-1.056 0.128-0.064 0.384-0.192t0.416-0.192q0.8-0.448 1.44-0.896 2.208-1.632 3.456-4.064t1.216-5.152-1.216-5.152-3.456-4.064q-0.64-0.448-1.44-0.896-0.128-0.096-0.416-0.192t-0.384-0.192q-0.704-0.416-0.704-1.056 0-0.448 0.32-0.8t0.832-0.352q0.224 0 0.448 0.096 3.776 1.632 6.048 5.056t2.272 7.552z'], - 'volume-down': ['0 0 28 32', 'M13.728 6.272v19.456q0 0.448-0.352 0.8t-0.8 0.32-0.8-0.32l-5.952-5.952h-4.672q-0.48 0-0.8-0.352t-0.352-0.8v-6.848q0-0.48 0.352-0.8t0.8-0.352h4.672l5.952-5.952q0.32-0.32 0.8-0.32t0.8 0.32 0.352 0.8zM20.576 16q0 1.344-0.768 2.528t-2.016 1.664q-0.16 0.096-0.448 0.096-0.448 0-0.8-0.32t-0.32-0.832q0-0.384 0.192-0.64t0.544-0.448 0.608-0.384 0.512-0.64 0.192-1.024-0.192-1.024-0.512-0.64-0.608-0.384-0.544-0.448-0.192-0.64q0-0.48 0.32-0.832t0.8-0.32q0.288 0 0.448 0.096 1.248 0.48 2.016 1.664t0.768 2.528z'], - 'volume-off': ['0 0 28 32', 'M13.728 6.272v19.456q0 0.448-0.352 0.8t-0.8 0.32-0.8-0.32l-5.952-5.952h-4.672q-0.48 0-0.8-0.352t-0.352-0.8v-6.848q0-0.48 0.352-0.8t0.8-0.352h4.672l5.952-5.952q0.32-0.32 0.8-0.32t0.8 0.32 0.352 0.8z'], - 'circulation': ['0 0 29 32', 'M25.6 9.92q1.344 0 2.272 0.928t0.928 2.272v9.28q0 1.28-0.928 2.24t-2.272 0.96h-22.4q-1.28 0-2.24-0.96t-0.96-2.24v-9.28q0-1.344 0.96-2.272t2.24-0.928h8v-3.52l6.4 5.76-6.4 5.76v-3.52h-6.72v6.72h19.84v-6.72h-4.8v-4.48h6.080z'], - 'random': ['0 0 33 31', 'M29.867 9.356l-5.003 5.003c-0.094 0.094-0.235 0.141-0.36 0.141-0.266 0-0.5-0.219-0.5-0.5v-3.002h-4.002c-2.079 0-3.064 1.423-3.94 3.111-0.453 0.875-0.844 1.782-1.219 2.673-1.735 4.033-3.768 8.223-8.849 8.223h-3.502c-0.281 0-0.5-0.219-0.5-0.5v-3.002c0-0.281 0.219-0.5 0.5-0.5h3.502c2.079 0 3.064-1.423 3.94-3.111 0.453-0.875 0.844-1.782 1.219-2.673 1.735-4.033 3.768-8.223 8.849-8.223h4.002v-3.002c0-0.281 0.219-0.5 0.5-0.5 0.141 0 0.266 0.063 0.375 0.156l4.987 4.987c0.094 0.094 0.141 0.235 0.141 0.36s-0.047 0.266-0.141 0.36zM10.262 14.781c-0.907-1.892-1.907-3.783-4.268-3.783h-3.502c-0.281 0-0.5-0.219-0.5-0.5v-3.002c0-0.281 0.219-0.5 0.5-0.5h3.502c2.783 0 4.831 1.298 6.41 3.518-0.876 1.344-1.517 2.798-2.142 4.268zM29.867 23.363l-5.003 5.003c-0.094 0.094-0.235 0.141-0.36 0.141-0.266 0-0.5-0.235-0.5-0.5v-3.002c-4.643 0-7.504 0.547-10.396-3.518 0.86-1.344 1.501-2.798 2.126-4.268 0.907 1.892 1.907 3.783 4.268 3.783h4.002v-3.002c0-0.281 0.219-0.5 0.5-0.5 0.141 0 0.266 0.063 0.375 0.156l4.987 4.987c0.094 0.094 0.141 0.235 0.141 0.36s-0.047 0.266-0.141 0.36z'], - 'order': ['0 0 32 32', 'M0.622 18.334h19.54v7.55l11.052-9.412-11.052-9.413v7.549h-19.54v3.725z'], - 'single': ['0 0 38 32', 'M2.072 21.577c0.71-0.197 1.125-0.932 0.928-1.641-0.221-0.796-0.333-1.622-0.333-2.457 0-5.049 4.108-9.158 9.158-9.158h5.428c0.056-0.922 0.221-1.816 0.482-2.667h-5.911c-3.158 0-6.128 1.23-8.361 3.463s-3.463 5.203-3.463 8.361c0 1.076 0.145 2.143 0.431 3.171 0.164 0.59 0.7 0.976 1.284 0.976 0.117 0 0.238-0.016 0.357-0.049zM21.394 25.613h-12.409v-2.362c0-0.758-0.528-1.052-1.172-0.652l-5.685 3.522c-0.644 0.4-0.651 1.063-0.014 1.474l5.712 3.69c0.637 0.411 1.158 0.127 1.158-0.63v-2.374h12.409c3.158 0 6.128-1.23 8.361-3.463 1.424-1.424 2.44-3.148 2.99-5.029-0.985 0.368-2.033 0.606-3.125 0.691-1.492 3.038-4.619 5.135-8.226 5.135zM28.718 0c-4.985 0-9.026 4.041-9.026 9.026s4.041 9.026 9.026 9.026 9.026-4.041 9.026-9.026-4.041-9.026-9.026-9.026zM30.392 13.827h-1.728v-6.822c-0.635 0.576-1.433 1.004-2.407 1.285v-1.713c0.473-0.118 0.975-0.325 1.506-0.62 0.532-0.325 0.975-0.665 1.329-1.034h1.3v8.904z'], - 'menu': ['0 0 22 32', 'M20.8 14.4q0.704 0 1.152 0.48t0.448 1.12-0.48 1.12-1.12 0.48h-19.2q-0.64 0-1.12-0.48t-0.48-1.12 0.448-1.12 1.152-0.48h19.2zM1.6 11.2q-0.64 0-1.12-0.48t-0.48-1.12 0.448-1.12 1.152-0.48h19.2q0.704 0 1.152 0.48t0.448 1.12-0.48 1.12-1.12 0.48h-19.2zM20.8 20.8q0.704 0 1.152 0.48t0.448 1.12-0.48 1.12-1.12 0.48h-19.2q-0.64 0-1.12-0.48t-0.48-1.12 0.448-1.12 1.152-0.48h19.2z'] - }; - this.getSVG = (type) => ` - - - - - `; - - this.isMobile = /mobile/i.test(window.navigator.userAgent); - // compatibility: some mobile browsers don't suppose autoplay - if (this.isMobile) { - option.autoplay = false; - } - - // default options - const defaultOption = { - element: document.getElementsByClassName('aplayer')[0], - narrow: false, - autoplay: false, - mutex: true, - showlrc: 0, - theme: '#b7daff', - mode: 'circulation' - }; - for (const defaultKey in defaultOption) { - if (defaultOption.hasOwnProperty(defaultKey) && !option.hasOwnProperty(defaultKey)) { - option[defaultKey] = defaultOption[defaultKey]; - } - } - - this.option = option; - this.audios = []; - this.mode = option.mode; - - /** - * Parse second to 00:00 format. 00:00:00 if audio is over an hour long. - * - * @param {Number} second - * @return {String} 00:00 format. 00:00:00 if over an hour long. - */ - this.secondToTime = (second) => { - if (isNaN(second)) { - return '00:00'; - } - const add0 = (num) => num < 10 ? '0' + num : '' + num; - const min = parseInt(second / 60); - const sec = parseInt(second - min * 60); - const hours = parseInt(min / 60); - const minAdjust = parseInt(second / 60 - 60 * parseInt(second / 60 / 60)); - return second >= 3600 ? add0(hours) + ':' + add0(minAdjust) + ':' + add0(sec) : add0(min) + ':' + add0(sec); - }; - - // save lrc - this.element = this.option.element; - if (this.option.showlrc === 2 || this.option.showlrc === true) { - this.savelrc = []; - for (let i = 0; i < this.element.getElementsByClassName('aplayer-lrc-content').length; i++) { - this.savelrc.push(this.element.getElementsByClassName('aplayer-lrc-content')[i].innerHTML); - } - } - this.lrcs = []; - - /** - * Update progress bar, including loading progress bar and play progress bar - * - * @param {String} type - Point out which bar it is, should be played loaded or volume - * @param {Number} percentage - * @param {String} direction - Point out the direction of this bar, Should be height or width - */ - this.updateBar = (type, percentage, direction) => { - percentage = percentage > 0 ? percentage : 0; - percentage = percentage < 1 ? percentage : 1; - bar[type + 'Bar'].style[direction] = percentage * 100 + '%'; - }; - - /** - * Update lrc - * - * @param {Number} currentTime - */ - this.updateLrc = (currentTime = this.audio.currentTime) => { - if (this.lrcIndex > this.lrc.length - 1 || currentTime < this.lrc[this.lrcIndex][0] || (!this.lrc[this.lrcIndex + 1] || currentTime >= this.lrc[this.lrcIndex + 1][0])) { - for (let i = 0; i < this.lrc.length; i++) { - if (currentTime >= this.lrc[i][0] && (!this.lrc[i + 1] || currentTime < this.lrc[i + 1][0])) { - this.lrcIndex = i; - this.lrcContents.style.transform = `translateY(${-this.lrcIndex * 16}px)`; - this.lrcContents.style.webkitTransform = `translateY(${-this.lrcIndex * 16}px)`; - this.lrcContents.getElementsByClassName('aplayer-lrc-current')[0].classList.remove('aplayer-lrc-current'); - this.lrcContents.getElementsByTagName('p')[i].classList.add('aplayer-lrc-current'); - } - } - } - }; - - // define APlayer events - const eventTypes = ['play', 'pause', 'canplay', 'playing', 'ended', 'error']; - this.event = {}; - for (let i = 0; i < eventTypes.length; i++) { - this.event[eventTypes[i]] = []; - } - this.trigger = (type) => { - for (let i = 0; i < this.event[type].length; i++) { - this.event[type][i](); - } - }; - - // multiple music - this.playIndex = 0; - if (Object.prototype.toString.call(option.music) !== '[object Array]') { - this.option.music = [this.option.music]; - } - this.music = this.option.music[this.playIndex]; - - // add class aplayer-withlrc - if (this.option.showlrc) { - this.element.classList.add('aplayer-withlrc'); - } - if (this.option.music.length > 1) { - this.element.classList.add('aplayer-withlist'); - } - - // Assume "circulation" mode if single music is loaded and mode isn't already "circulation" or "order". - if (!this.isMultiple() && this.mode !== 'circulation' && this.mode !== 'order') { - this.mode = 'circulation'; - } - this.getRandomOrder(); - - // fill in HTML - let eleHTML = ` -
-
- -
-
-
-
- - -
-
-
-
-
-
-
-
-
- -
-
-
-
- - - 00:00 / 00:00 - -
- -
-
-
-
-
-
- - -
-
-
-
-
    `; - for (let i = 0; i < this.option.music.length; i++) { - eleHTML += ` -
  1. - - ${(i + 1)} - ${this.option.music[i].title} - ${this.option.music[i].author} -
  2. `; - } - eleHTML += ` -
-
`; - this.element.innerHTML = eleHTML; - - // hide mode button in arrow container - if (this.element.offsetWidth < 300) { - this.element.getElementsByClassName('aplayer-icon-mode')[0].style.display = 'none'; - } - - this.ptime = this.element.getElementsByClassName('aplayer-ptime')[0]; - - if (this.element.getElementsByClassName('aplayer-info')[0].offsetWidth < 200) { - this.element.getElementsByClassName('aplayer-time')[0].classList.add('aplayer-time-narrow'); - } - // fix the width of aplayer bar - const bar = {}; - bar.barWrap = this.element.getElementsByClassName('aplayer-bar-wrap')[0]; - - // switch to narrow style - if (this.option.narrow) { - this.element.classList.add('aplayer-narrow'); - } - - // play and pause button - this.button = this.element.getElementsByClassName('aplayer-button')[0]; - this.button.addEventListener('click', () => { - this.toggle(); - }); - - // click music list: change music - const list = this.element.getElementsByClassName('aplayer-list')[0]; - list.addEventListener('click', (e) => { - let target; - if (e.target.tagName.toUpperCase() === 'LI') { - target = e.target; - } - else { - target = e.target.parentElement; - } - const musicIndex = parseInt(target.getElementsByClassName('aplayer-list-index')[0].innerHTML) - 1; - if (musicIndex !== this.playIndex) { - this.setMusic(musicIndex); - this.play(); - } - else { - this.toggle(); - } - }); - - // control play progress - bar.playedBar = this.element.getElementsByClassName('aplayer-played')[0]; - bar.loadedBar = this.element.getElementsByClassName('aplayer-loaded')[0]; - const thumb = this.element.getElementsByClassName('aplayer-thumb')[0]; - let barWidth; - bar.barWrap.addEventListener('click', (event) => { - const e = event || window.event; - barWidth = bar.barWrap.clientWidth; - const percentage = (e.clientX - getElementViewLeft(bar.barWrap)) / barWidth; - if (isNaN(this.audio.duration)) { - this.updateBar('played', 0, 'width'); - } - else { - this.updateBar('played', percentage, 'width'); - this.element.getElementsByClassName('aplayer-ptime')[0].innerHTML = this.secondToTime(percentage * this.audio.duration); - this.audio.currentTime = parseFloat(bar.playedBar.style.width) / 100 * this.audio.duration; - } - }); - - thumb.addEventListener('mouseover', () => { - thumb.style.background = this.option.theme; - }); - thumb.addEventListener('mouseout', () => { - thumb.style.background = '#fff'; - }); - - const thumbMove = (event) => { - const e = event || window.event; - let percentage = (e.clientX - getElementViewLeft(bar.barWrap)) / barWidth; - percentage = percentage > 0 ? percentage : 0; - percentage = percentage < 1 ? percentage : 1; - this.updateBar('played', percentage, 'width'); - if (this.option.showlrc) { - this.updateLrc(parseFloat(bar.playedBar.style.width) / 100 * this.audio.duration); - } - this.element.getElementsByClassName('aplayer-ptime')[0].innerHTML = this.secondToTime(percentage * this.audio.duration); - }; - - const thumbUp = () => { - document.removeEventListener('mouseup', thumbUp); - document.removeEventListener('mousemove', thumbMove); - if (isNaN(this.audio.duration)) { - this.updateBar('played', 0, 'width'); - } - else { - this.audio.currentTime = parseFloat(bar.playedBar.style.width) / 100 * this.audio.duration; - this.playedTime = setInterval(() => { - this.updateBar('played', this.audio.currentTime / this.audio.duration, 'width'); - if (this.option.showlrc) { - this.updateLrc(); - } - this.element.getElementsByClassName('aplayer-ptime')[0].innerHTML = this.secondToTime(this.audio.currentTime); - this.trigger('playing'); - }, 100); - } - }; - - thumb.addEventListener('mousedown', () => { - barWidth = bar.barWrap.clientWidth; - clearInterval(this.playedTime); - document.addEventListener('mousemove', thumbMove); - document.addEventListener('mouseup', thumbUp); - }); - - // control volume - bar.volumeBar = this.element.getElementsByClassName('aplayer-volume')[0]; - const volumeBarWrap = this.element.getElementsByClassName('aplayer-volume-bar')[0]; - this.volumeicon = this.element.getElementsByClassName('aplayer-time')[0].getElementsByTagName('button')[0]; - const barHeight = 35; - this.element.getElementsByClassName('aplayer-volume-bar-wrap')[0].addEventListener('click', (event) => { - const e = event || window.event; - let percentage = (barHeight - e.clientY + getElementViewTop(volumeBarWrap)) / barHeight; - percentage = percentage > 0 ? percentage : 0; - percentage = percentage < 1 ? percentage : 1; - this.volume(percentage); - }); - this.volumeicon.addEventListener('click', () => { - if (this.audio.muted) { - this.audio.muted = false; - this.volumeicon.className = this.audio.volume === 1 ? 'aplayer-icon aplayer-icon-volume-up' : 'aplayer-icon aplayer-icon-volume-down'; - if (this.audio.volume === 1) { - this.volumeicon.className = 'aplayer-icon aplayer-icon-volume-up'; - this.volumeicon.innerHTML = this.getSVG('volume-up'); - } - else { - this.volumeicon.className = 'aplayer-icon aplayer-icon-volume-down'; - this.volumeicon.innerHTML = this.getSVG('volume-down'); - } - this.updateBar('volume', this.audio.volume, 'height'); - } - else { - this.audio.muted = true; - this.volumeicon.className = 'aplayer-icon aplayer-icon-volume-off'; - this.volumeicon.innerHTML = this.getSVG('volume-off'); - this.updateBar('volume', 0, 'height'); - } - }); - - // get element's view position - function getElementViewLeft (element) { - let actualLeft = element.offsetLeft; - let current = element.offsetParent; - let elementScrollLeft = 0; - while (current !== null) { - actualLeft += current.offsetLeft; - current = current.offsetParent; - } - elementScrollLeft = document.body.scrollLeft + document.documentElement.scrollLeft; - return actualLeft - elementScrollLeft; - } - - function getElementViewTop (element) { - let actualTop = element.offsetTop; - let current = element.offsetParent; - let elementScrollTop = 0; - while (current !== null) { - actualTop += current.offsetTop; - current = current.offsetParent; - } - elementScrollTop = document.body.scrollTop + document.documentElement.scrollTop; - return actualTop - elementScrollTop; - } - - // mode control - const modeEle = this.element.getElementsByClassName('aplayer-icon-mode')[0]; - modeEle.addEventListener('click', () => { - if (this.isMultiple()) { - if (this.mode === 'random') { - this.mode = 'single'; - } - else if (this.mode === 'single') { - this.mode = 'order'; - } - else if (this.mode === 'order') { - this.mode = 'circulation'; - } - else if (this.mode === 'circulation') { - this.mode = 'random'; - } - } - else { - if (this.mode === 'circulation') { - this.mode = 'order'; - } - else { - this.mode = 'circulation'; - } - } - modeEle.innerHTML = this.getSVG(this.mode); - this.audio.loop = !(this.isMultiple() || this.mode === 'order'); - }); - - // toggle menu control - list.style.height = list.offsetHeight + 'px'; - this.element.getElementsByClassName('aplayer-icon-menu')[0].addEventListener('click', () => { - if (!list.classList.contains('aplayer-list-hide')) { - list.classList.add('aplayer-list-hide'); - } - else { - list.classList.remove('aplayer-list-hide'); - } - }); - - if (this.mode === 'random') { - this.setMusic(this.randomOrder[0]); - } - else { - this.setMusic(0); - } - - // autoplay - if (this.option.autoplay) { - this.play(); - } - - instances.push(this); - } - - /** - * Set music - */ - setMusic (index) { - // get this.music - if (typeof index !== 'undefined') { - this.playIndex = index; - } - const indexMusic = this.playIndex; - this.music = this.option.music[indexMusic]; - - // set html - if (this.music.pic) { - this.element.getElementsByClassName('aplayer-pic')[0].style.backgroundImage = `url('${this.music.pic}')`; - } - else { - this.element.getElementsByClassName('aplayer-pic')[0].style.backgroundImage = ''; - } - this.element.getElementsByClassName('aplayer-title')[0].innerHTML = this.music.title; - this.element.getElementsByClassName('aplayer-author')[0].innerHTML = this.music.author ? ' - ' + this.music.author : ''; - if (this.element.getElementsByClassName('aplayer-list-light')[0]) { - this.element.getElementsByClassName('aplayer-list-light')[0].classList.remove('aplayer-list-light'); - } - this.element.getElementsByClassName('aplayer-list')[0].getElementsByTagName('li')[indexMusic].classList.add('aplayer-list-light'); - - // set the previous audio object - if (!this.isMobile && this.audio) { - this.pause(); - this.audio.currentTime = 0; - } - - this.element.getElementsByClassName('aplayer-list')[0].scrollTop = indexMusic * 33; - - // get this audio object - if (this.isMobile && this.audio) { - this.audio.src = this.music.url; - } - else if (!this.isMobile && this.audios[indexMusic]) { - this.audio = this.audios[indexMusic]; - this.audio.volume = parseInt(this.element.getElementsByClassName('aplayer-volume')[0].style.height) / 100; - this.audio.currentTime = 0; - this.audio.src = this.music.url; - } - else { - this.audio = document.createElement("audio"); - this.audio.src = this.music.url; - this.audio.preload = this.option.preload ? this.option.preload : 'auto'; - - this.audio.addEventListener('play', () => { - if (this.button.classList.contains('aplayer-play')) { - this.button.classList.remove('aplayer-play'); - this.button.classList.add('aplayer-pause'); - this.button.innerHTML = ''; - setTimeout(() => { - this.button.innerHTML = ` - `; - }, 100); - - // pause other players (Thanks @Aprikyblue) - if (this.option.mutex) { - for (let i = 0; i < instances.length; i++) { - if (this !== instances[i]) { - instances[i].pause(); - } - } - } - if (this.playedTime) { - clearInterval(this.playedTime); - } - this.playedTime = setInterval(() => { - this.updateBar('played', this.audio.currentTime / this.audio.duration, 'width'); - if (this.option.showlrc) { - this.updateLrc(); - } - this.ptime.innerHTML = this.secondToTime(this.audio.currentTime); - this.trigger('playing'); - }, 100); - this.trigger('play'); - } - }); - - const pauseHandler = () => { - if (this.button && (this.button.classList.contains('aplayer-pause') || this.ended)) { - this.ended = false; - this.button.classList.remove('aplayer-pause'); - this.button.classList.add('aplayer-play'); - this.button.innerHTML = ''; - setTimeout(() => { - this.button.innerHTML = ` - `; - }, 100); - clearInterval(this.playedTime); - this.trigger('pause'); - } - }; - - this.audio.addEventListener('pause', pauseHandler); - - this.audio.addEventListener('abort', pauseHandler); - - // show audio time: the metadata has loaded or changed - this.audio.addEventListener('durationchange', () => { - if (this.audio.duration !== 1) { // compatibility: Android browsers will output 1 at first - this.element.getElementsByClassName('aplayer-dtime')[0].innerHTML = this.secondToTime(this.audio.duration); - } - }); - - // show audio loaded bar: to inform interested parties of progress downloading the media - this.audio.addEventListener('progress', () => { - const percentage = this.audio.buffered.length ? this.audio.buffered.end(this.audio.buffered.length - 1) / this.audio.duration : 0; - this.updateBar('loaded', percentage, 'width'); - }); - - // audio download error: an error occurs - this.audio.addEventListener('error', () => { - this.element.getElementsByClassName('aplayer-author')[0].innerHTML = ` - Error happens ╥﹏╥`; - this.trigger('pause'); - }); - - // audio can play: enough data is available that the media can be played - this.audio.addEventListener('canplay', () => { - this.trigger('canplay'); - }); - - // multiple music play - this.ended = false; - this.audio.addEventListener('ended', () => { - if (this.isMultiple()) { - if (this.audio.currentTime !== 0) { - if (this.mode === 'random') { - this.setMusic(this.nextRandomNum()); - this.play(); - } - else if (this.mode === 'single') { - this.setMusic(this.playIndex); - this.play(); - } - else if (this.mode === 'order') { - if (this.playIndex < this.option.music.length - 1) { - this.setMusic(++this.playIndex); - this.play(); - } - else { - this.ended = true; - this.pause(); - this.trigger('ended'); - } - } - else if (this.mode === 'circulation') { - this.playIndex = (this.playIndex + 1) % this.option.music.length; - this.setMusic(this.playIndex); - this.play(); - } - } - } - else { - if (this.mode === 'order') { - this.ended = true; - this.pause(); - this.trigger('ended'); - } - } - }); - - // control volume - this.audio.volume = parseInt(this.element.getElementsByClassName('aplayer-volume')[0].style.height) / 100; - - // loop - this.audio.loop = !(this.isMultiple() || this.mode === 'order'); - - this.audios[indexMusic] = this.audio; - } - - /** - * Parse lrc, suppose multiple time tag - * - * @param {String} lrc_s - Format: - * [mm:ss.xx]lyric - * [mm:ss.xxx]lyric - * [mm:ss.xx][mm:ss.xx][mm:ss.xx]lyric - * - * @return {String} [[time, text], [time, text], [time, text], ...] - */ - const parseLrc = (lrc_s) => { - const lyric = lrc_s.split('\n'); - const lrc = []; - const lyricLen = lyric.length; - for (let i = 0; i < lyricLen; i++) { - // match lrc time - const lrcTimes = lyric[i].match(/\[(\d{2}):(\d{2})\.(\d{2,3})]/g); - // match lrc text - const lrcText = lyric[i].replace(/\[(\d{2}):(\d{2})\.(\d{2,3})]/g, '').replace(/^\s+|\s+$/g, ''); - - if (lrcTimes) { - // handle multiple time tag - const timeLen = lrcTimes.length; - for (let j = 0; j < timeLen; j++) { - const oneTime = /\[(\d{2}):(\d{2})\.(\d{2,3})]/.exec(lrcTimes[j]); - const lrcTime = oneTime[1] * 60 + parseInt(oneTime[2]) + parseInt(oneTime[3]) / ((oneTime[3] + '').length === 2 ? 100 : 1000); - lrc.push([lrcTime, lrcText]); - } - } - } - // sort by time - lrc.sort((a, b) => a[0] - b[0]); - return lrc; - }; - - // fill in lrc - if (this.option.showlrc) { - const index = indexMusic; - - if (!this.lrcs[index]) { - let lrcs = ''; - if (this.option.showlrc === 1) { - lrcs = this.option.music[index].lrc; - } - else if (this.option.showlrc === 2 || this.option.showlrc === true) { - lrcs = this.savelrc[index]; - } - else if (this.option.showlrc === 3) { - const xhr = new XMLHttpRequest(); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) { - lrcs = xhr.responseText; - this.lrcs[index] = parseLrc(lrcs); - } - else { - console.log('Request was unsuccessful: ' + xhr.status); - this.lrcs[index] = [['00:00', 'Not available']]; - } - this.lrc = this.lrcs[index]; - let lrcHTML = ''; - this.lrcContents = this.element.getElementsByClassName('aplayer-lrc-contents')[0]; - for (let i = 0; i < this.lrc.length; i++) { - lrcHTML += `

${this.lrc[i][1]}

`; - } - this.lrcContents.innerHTML = lrcHTML; - if (!this.lrcIndex) { - this.lrcIndex = 0; - } - this.lrcContents.getElementsByTagName('p')[0].classList.add('aplayer-lrc-current'); - this.lrcContents.style.transform = 'translateY(0px)'; - this.lrcContents.style.webkitTransform = 'translateY(0px)'; - } - }; - const apiurl = this.option.music[index].lrc; - xhr.open('get', apiurl, true); - xhr.send(null); - } - if (lrcs) { - this.lrcs[index] = parseLrc(lrcs); - } - else { - if (this.option.showlrc === 3) { - this.lrcs[index] = [['00:00', 'Loading']]; - } - else { - this.lrcs[index] = [['00:00', 'Not available']]; - } - } - } - - this.lrc = this.lrcs[index]; - let lrcHTML = ''; - this.lrcContents = this.element.getElementsByClassName('aplayer-lrc-contents')[0]; - for (let i = 0; i < this.lrc.length; i++) { - lrcHTML += `

${this.lrc[i][1]}

`; - } - this.lrcContents.innerHTML = lrcHTML; - if (!this.lrcIndex) { - this.lrcIndex = 0; - } - this.lrcContents.getElementsByTagName('p')[0].classList.add('aplayer-lrc-current'); - this.lrcContents.style.transform = 'translateY(0px)'; - this.lrcContents.style.webkitTransform = 'translateY(0px)'; - } - - // set duration time - if (this.audio.duration !== 1) { // compatibility: Android browsers will output 1 at first - this.element.getElementsByClassName('aplayer-dtime')[0].innerHTML = this.audio.duration ? this.secondToTime(this.audio.duration) : '00:00'; - } - } - - /** - * Play music - */ - play (time) { - if (Object.prototype.toString.call(time) === '[object Number]') { - this.audio.currentTime = time; - } - if (this.audio.paused) { - this.audio.play(); - } - } - - /** - * Pause music - */ - pause () { - if (!this.audio.paused) { - this.audio.pause(); - } - } - - /** - * Set volume - */ - volume (percentage) { - this.updateBar('volume', percentage, 'height'); - this.audio.volume = percentage; - if (this.audio.muted) { - this.audio.muted = false; - } - if (percentage === 1) { - this.volumeicon.className = 'aplayer-icon aplayer-icon-volume-up'; - this.volumeicon.innerHTML = this.getSVG('volume-up'); - } - else { - this.volumeicon.className = 'aplayer-icon aplayer-icon-volume-down'; - this.volumeicon.innerHTML = this.getSVG('volume-down'); - } - } - - /** - * attach event - */ - on (name, func) { - if (typeof func === 'function') { - this.event[name].push(func); - } - } - - /** - * toggle between play and pause - */ - toggle () { - if (this.button.classList.contains('aplayer-play')) { - this.play(); - } - else if (this.button.classList.contains('aplayer-pause')) { - this.pause(); - } - } - - /** - * get whether multiple music definitions are loaded - */ - isMultiple () { - return this.option.music.length > 1; - } - - /** - * get random order, using Fisher–Yates shuffle - */ - getRandomOrder () { - function random (min, max) { - if (max) { - max = min; - min = 0; - } - return min + Math.floor(Math.random() * (max - min + 1)); - } - function shuffle (arr) { - const length = arr.length, - shuffled = new Array(length); - for (let index = 0, rand; index < length; index++) { - rand = random(0, index); - if (rand !== index) {shuffled[index] = shuffled[rand];} - shuffled[rand] = arr[index]; - } - return shuffled; - } - if (this.isMultiple()) { - this.randomOrder = shuffle([...Array(this.option.music.length)].map(function (item, i) { - return i; - })); - } - } - - /** - * get next random number - */ - nextRandomNum () { - if (this.isMultiple()) { - const index = this.randomOrder.indexOf(this.playIndex); - if (index === this.randomOrder.length - 1) { - return this.randomOrder[0]; - } - else { - return this.randomOrder[index + 1]; - } - } - else { - return 0; - } - } - - /** - * Remove song from playlist - */ - removeSong (indexOfSong) { - if (this.option.music[indexOfSong]) { // Check if song exists - const list = this.element.getElementsByClassName('aplayer-list')[0]; - const oList = list.getElementsByTagName('ol')[0]; - const liList = oList.getElementsByTagName('li'); - if (this.option.music[indexOfSong + 1] || this.option.music[indexOfSong - 1]) { - if (indexOfSong === this.playIndex) { - if (this.option.music[indexOfSong + 1]) { // Play next song if it exists. - this.setMusic(indexOfSong + 1); - this.playIndex = this.playIndex - 1; // Adjust play index for removed song - } - else if (!this.option.music[indexOfSong + 1]) { // Play previous song if it exists. - this.setMusic(indexOfSong - 1); - } - } - else { - if (indexOfSong < this.playIndex) { - this.playIndex = this.playIndex - 1; - } - } - if (liList[indexOfSong + 1]) { - const targetSong = liList[indexOfSong - 1]; - targetSong.getElementsByClassName('aplayer-list-index')[0].textContent = indexOfSong; - } - else { - for (let i = 1; i < liList.length; i++) { - if (liList[indexOfSong + i]) { - const targetSong = liList[indexOfSong + i]; - targetSong.getElementsByClassName('aplayer-list-index')[0].textContent = indexOfSong + i; - } - } - } - this.option.music.splice(indexOfSong, 1); // Delete song from music array - this.audios.splice(indexOfSong, 1); // Delete song from audios array - liList[indexOfSong].remove(); - if (this.option.music[0] && this.option.music[1]) { - this.multiple = false; - this.element.classList.remove('aplayer-withlist'); - } - } - const listHeight = parseInt(list.style.height, 10); - list.style.height = listHeight - 33 + "px"; - } - else { - console.error("ERROR: Song does not exist"); - } - } - - /** - * destroy this player - */ - destroy () { - instances.splice(instances.indexOf(this), 1); - this.pause(); - this.element.innerHTML = ''; - clearInterval(this.playedTime); - for (const key in this) { - if (this.hasOwnProperty(key)) { - delete this[key]; - } - } - } - - /** - * add music dynamically - * - * @param {Array} newMusic - */ - addMusic (newMusic) { - const wasSingle = !this.isMultiple(); - - this.option.music = this.option.music.concat(newMusic); - - const list = this.element.getElementsByClassName('aplayer-list')[0]; - const listEle = list.getElementsByTagName('ol')[0]; - let newItemHTML = ``; - for (let i = 0; i < newMusic.length; i++) { - newItemHTML += ` -
  • - - ${this.option.music.length - newMusic.length + i + 1} - ${newMusic[i].title} - ${newMusic[i].author} -
  • `; - } - listEle.innerHTML += newItemHTML; - - if (wasSingle && this.isMultiple()) { - this.element.classList.add('aplayer-withlist'); - this.audio.loop = false; - } - const songListLength = listEle.getElementsByTagName('li').length; - list.style.height = songListLength * 33 + 'px'; - - this.getRandomOrder(); - } -} +/* global APLAYER_VERSION GIT_HASH */ +console.log(`${'\n'} %c APlayer v${APLAYER_VERSION} ${GIT_HASH} %c http://aplayer.js.org ${'\n'}${'\n'}`, 'color: #fadfa3; background: #030307; padding:5px 0;', 'background: #fadfa3; padding:5px 0;'); -export default APlayer; +export default APlayer; \ No newline at end of file diff --git a/src/js/lrc.js b/src/js/lrc.js new file mode 100644 index 00000000..e69de29b diff --git a/src/js/options.js b/src/js/options.js new file mode 100644 index 00000000..2bc36de8 --- /dev/null +++ b/src/js/options.js @@ -0,0 +1,24 @@ +export default (options) => { + + // default options + const defaultOption = { + container: options.element || document.getElementsByClassName('aplayer')[0], + narrow: false, + autoplay: false, + mutex: true, + showlrc: 0, + theme: '#b7daff', + mode: 'circulation' + }; + for (const defaultKey in defaultOption) { + if (defaultOption.hasOwnProperty(defaultKey) && !options.hasOwnProperty(defaultKey)) { + options[defaultKey] = defaultOption[defaultKey]; + } + } + + if (Object.prototype.toString.call(options.music) !== '[object Array]') { + options.music = [options.music]; + } + + return options; +}; diff --git a/src/js/player.js b/src/js/player.js new file mode 100644 index 00000000..0ccc33b5 --- /dev/null +++ b/src/js/player.js @@ -0,0 +1,788 @@ +import utils from './utils'; +import Icons from './icons'; +import handleOption from './options'; +import Template from './template'; +import Bar from './bar'; +import User from './user'; + +const instances = []; + +class APlayer { + + /** + * APlayer constructor function + * + * @param {Object} options - See README + * @constructor + */ + constructor (options) { + this.options = handleOption(options); + + this.audios = []; + this.mode = this.options.mode; + + // save lrc + this.container = this.options.container; + if (this.options.showlrc === 2 || this.options.showlrc === true) { + this.savelrc = []; + const lrcEle = this.container.getElementsByClassName('aplayer-lrc-content'); + for (let i = 0; i < lrcEle.length; i++) { + this.savelrc.push(lrcEle[i].innerHTML); + } + } + this.lrcs = []; + + /** + * Update lrc + * + * @param {Number} currentTime + */ + this.updateLrc = (currentTime = this.audio.currentTime) => { + if (this.lrcIndex > this.lrc.length - 1 || currentTime < this.lrc[this.lrcIndex][0] || (!this.lrc[this.lrcIndex + 1] || currentTime >= this.lrc[this.lrcIndex + 1][0])) { + for (let i = 0; i < this.lrc.length; i++) { + if (currentTime >= this.lrc[i][0] && (!this.lrc[i + 1] || currentTime < this.lrc[i + 1][0])) { + this.lrcIndex = i; + this.template.lrc.style.transform = `translateY(${-this.lrcIndex * 16}px)`; + this.template.lrc.style.webkitTransform = `translateY(${-this.lrcIndex * 16}px)`; + this.template.lrc.getElementsByClassName('aplayer-lrc-current')[0].classList.remove('aplayer-lrc-current'); + this.template.lrc.getElementsByTagName('p')[i].classList.add('aplayer-lrc-current'); + } + } + } + }; + + // define APlayer events + const eventTypes = ['play', 'pause', 'canplay', 'playing', 'ended', 'error']; + this.event = {}; + for (let i = 0; i < eventTypes.length; i++) { + this.event[eventTypes[i]] = []; + } + this.trigger = (type) => { + for (let i = 0; i < this.event[type].length; i++) { + this.event[type][i](); + } + }; + + // multiple music + this.playIndex = 0; + this.music = this.options.music[this.playIndex]; + + // add class aplayer-withlrc + if (this.options.showlrc) { + this.container.classList.add('aplayer-withlrc'); + } + if (this.options.music.length > 1) { + this.container.classList.add('aplayer-withlist'); + } + + // Assume "circulation" mode if single music is loaded and mode isn't already "circulation" or "order". + if (!this.isMultiple() && this.mode !== 'circulation' && this.mode !== 'order') { + this.mode = 'circulation'; + } + this.getRandomOrder(); + + this.user = new User(this); + + if (utils.isMobile) { + this.container.classList.add('aplayer-mobile'); + } + + this.arrow = this.container.offsetWidth <= 300; + if (this.arrow) { + this.container.classList.add('aplayer-arrow'); + } + + if (this.options.narrow) { + this.container.classList.add('aplayer-narrow'); + } + + this.template = new Template({ + container: this.container, + options: this.options, + mode: this.mode + }); + + if (this.template.info.offsetWidth < 200) { + this.template.time.classList.add('aplayer-time-narrow'); + } + + this.bar = new Bar(this.template); + + // play and pause button + this.template.button.addEventListener('click', () => { + this.toggle(); + }); + + // click music list: change music + this.template.list.addEventListener('click', (e) => { + let target; + if (e.target.tagName.toUpperCase() === 'LI') { + target = e.target; + } + else { + target = e.target.parentElement; + } + const musicIndex = parseInt(target.getElementsByClassName('aplayer-list-index')[0].innerHTML) - 1; + if (musicIndex !== this.playIndex) { + this.setMusic(musicIndex); + this.play(); + } + else { + this.toggle(); + } + }); + + let barWidth; + this.template.barWrap.addEventListener('click', (event) => { + const e = event || window.event; + barWidth = this.template.barWrap.clientWidth; + const percentage = (e.clientX - utils.getElementViewLeft(this.template.barWrap)) / barWidth; + if (isNaN(this.audio.duration)) { + this.bar.set('played', 0, 'width'); + } + else { + this.bar.set('played', percentage, 'width'); + this.template.ptime.innerHTML = utils.secondToTime(percentage * this.audio.duration); + this.audio.currentTime = this.bar.get('played', 'width') * this.audio.duration; + } + }); + + this.template.thumb.addEventListener('mouseover', () => { + this.template.thumb.style.background = this.options.theme; + }); + this.template.thumb.addEventListener('mouseout', () => { + this.template.thumb.style.background = '#fff'; + }); + + const thumbMove = (event) => { + const e = event || window.event; + let percentage = (e.clientX - utils.getElementViewLeft(this.template.barWrap)) / barWidth; + percentage = percentage > 0 ? percentage : 0; + percentage = percentage < 1 ? percentage : 1; + this.bar.set('played', percentage, 'width'); + if (this.options.showlrc) { + this.updateLrc(this.bar.get('played', 'width') * this.audio.duration); + } + this.template.ptime.innerHTML = utils.secondToTime(percentage * this.audio.duration); + }; + + const thumbUp = () => { + document.removeEventListener('mouseup', thumbUp); + document.removeEventListener('mousemove', thumbMove); + if (isNaN(this.audio.duration)) { + this.bar.set('played', 0, 'width'); + } + else { + this.audio.currentTime = this.bar.get('played', 'width') * this.audio.duration; + this.playedTime = setInterval(() => { + this.bar.set('played', this.audio.currentTime / this.audio.duration, 'width'); + if (this.options.showlrc) { + this.updateLrc(); + } + this.template.ptime.innerHTML = utils.secondToTime(this.audio.currentTime); + this.trigger('playing'); + }, 100); + } + }; + + this.template.thumb.addEventListener('mousedown', () => { + barWidth = this.template.barWrap.clientWidth; + clearInterval(this.playedTime); + document.addEventListener('mousemove', thumbMove); + document.addEventListener('mouseup', thumbUp); + }); + + // control volume + const barHeight = 35; + this.template.volumeBarWrap.addEventListener('click', (event) => { + const e = event || window.event; + let percentage = (barHeight - e.clientY + utils.getElementViewTop(this.template.volumeBar)) / barHeight; + percentage = percentage > 0 ? percentage : 0; + percentage = percentage < 1 ? percentage : 1; + this.volume(percentage); + }); + this.template.volumeButton.addEventListener('click', () => { + if (this.audio.muted) { + this.audio.muted = false; + this.switchVolumeIcon(); + this.bar.set('volume', this.volume(), 'height'); + } + else { + this.audio.muted = true; + this.switchVolumeIcon(); + this.bar.set('volume', 0, 'height'); + } + }); + + // mode control + this.template.mode.addEventListener('click', () => { + if (this.isMultiple()) { + if (this.mode === 'random') { + this.mode = 'single'; + } + else if (this.mode === 'single') { + this.mode = 'order'; + } + else if (this.mode === 'order') { + this.mode = 'circulation'; + } + else if (this.mode === 'circulation') { + this.mode = 'random'; + } + } + else { + if (this.mode === 'circulation') { + this.mode = 'order'; + } + else { + this.mode = 'circulation'; + } + } + this.template.mode.innerHTML = Icons[this.mode]; + this.audio.loop = !(this.isMultiple() || this.mode === 'order'); + }); + + // toggle menu control + this.template.list.style.height = this.template.list.offsetHeight + 'px'; + this.template.menu.addEventListener('click', () => { + if (!this.template.list.classList.contains('aplayer-list-hide')) { + this.template.list.classList.add('aplayer-list-hide'); + } + else { + this.template.list.classList.remove('aplayer-list-hide'); + } + }); + + if (this.mode === 'random') { + this.setMusic(this.randomOrder[0]); + } + else { + this.setMusic(0); + } + + this.volume(this.user.get('volume'), true, true); + + // autoplay + if (this.options.autoplay) { + this.play(); + } + + instances.push(this); + } + + /** + * Set music + */ + setMusic (index) { + // get this.music + if (typeof index !== 'undefined') { + this.playIndex = index; + } + const indexMusic = this.playIndex; + this.music = this.options.music[indexMusic]; + + // set html + if (this.music.pic) { + this.template.pic.style.backgroundImage = `url('${this.music.pic}')`; + } + else { + this.template.pic.style.backgroundImage = ''; + } + this.template.title.innerHTML = this.music.title; + this.template.author.innerHTML = this.music.author ? ' - ' + this.music.author : ''; + const light = this.container.getElementsByClassName('aplayer-list-light')[0]; + if (light) { + light.classList.remove('aplayer-list-light'); + } + this.template.listItems[indexMusic].classList.add('aplayer-list-light'); + + // set the previous audio object + if (!utils.isMobile && this.audio) { + this.pause(); + this.audio.currentTime = 0; + } + + this.template.list.scrollTop = indexMusic * 33; + + // get this audio object + if (utils.isMobile && this.audio) { + this.audio.src = this.music.url; + } + else if (!utils.isMobile && this.audios[indexMusic]) { + this.audio = this.audios[indexMusic]; + this.audio.volume = parseInt(this.template.volume.style.height) / 100; + this.audio.currentTime = 0; + this.audio.src = this.music.url; + } + else { + this.audio = document.createElement("audio"); + this.audio.src = this.music.url; + this.audio.preload = this.options.preload ? this.options.preload : 'auto'; + + this.audio.addEventListener('play', () => { + if (this.template.button.classList.contains('aplayer-play')) { + this.template.button.classList.remove('aplayer-play'); + this.template.button.classList.add('aplayer-pause'); + this.template.button.innerHTML = ''; + setTimeout(() => { + this.template.button.innerHTML = ` + `; + }, 100); + + // pause other players (Thanks @Aprikyblue) + if (this.options.mutex) { + for (let i = 0; i < instances.length; i++) { + if (this !== instances[i]) { + instances[i].pause(); + } + } + } + if (this.playedTime) { + clearInterval(this.playedTime); + } + this.playedTime = setInterval(() => { + this.bar.set('played', this.audio.currentTime / this.audio.duration, 'width'); + if (this.options.showlrc) { + this.updateLrc(); + } + this.template.ptime.innerHTML = utils.secondToTime(this.audio.currentTime); + this.trigger('playing'); + }, 100); + this.trigger('play'); + } + }); + + const pauseHandler = () => { + if (this.template.button && (this.template.button.classList.contains('aplayer-pause') || this.ended)) { + this.ended = false; + this.template.button.classList.remove('aplayer-pause'); + this.template.button.classList.add('aplayer-play'); + this.template.button.innerHTML = ''; + setTimeout(() => { + this.template.button.innerHTML = ` + `; + }, 100); + clearInterval(this.playedTime); + this.trigger('pause'); + } + }; + + this.audio.addEventListener('pause', pauseHandler); + + this.audio.addEventListener('abort', pauseHandler); + + // show audio time: the metadata has loaded or changed + this.audio.addEventListener('durationchange', () => { + if (this.audio.duration !== 1) { // compatibility: Android browsers will output 1 at first + this.template.dtime.innerHTML = utils.secondToTime(this.audio.duration); + } + }); + + // show audio loaded bar: to inform interested parties of progress downloading the media + this.audio.addEventListener('progress', () => { + const percentage = this.audio.buffered.length ? this.audio.buffered.end(this.audio.buffered.length - 1) / this.audio.duration : 0; + this.bar.set('loaded', percentage, 'width'); + }); + + // audio download error: an error occurs + this.audio.addEventListener('error', () => { + this.template.author.innerHTML = ` - Error happens ╥﹏╥`; + this.trigger('pause'); + }); + + // audio can play: enough data is available that the media can be played + this.audio.addEventListener('canplay', () => { + this.trigger('canplay'); + }); + + // multiple music play + this.ended = false; + this.audio.addEventListener('ended', () => { + if (this.isMultiple()) { + if (this.audio.currentTime !== 0) { + if (this.mode === 'random') { + this.setMusic(this.nextRandomNum()); + this.play(); + } + else if (this.mode === 'single') { + this.setMusic(this.playIndex); + this.play(); + } + else if (this.mode === 'order') { + if (this.playIndex < this.options.music.length - 1) { + this.setMusic(++this.playIndex); + this.play(); + } + else { + this.ended = true; + this.pause(); + this.trigger('ended'); + } + } + else if (this.mode === 'circulation') { + this.playIndex = (this.playIndex + 1) % this.options.music.length; + this.setMusic(this.playIndex); + this.play(); + } + } + } + else { + if (this.mode === 'order') { + this.ended = true; + this.pause(); + this.trigger('ended'); + } + } + }); + + // control volume + this.audio.volume = parseInt(this.template.volume.style.height) / 100; + + // loop + this.audio.loop = !(this.isMultiple() || this.mode === 'order'); + + this.audios[indexMusic] = this.audio; + } + + /** + * Parse lrc, suppose multiple time tag + * + * @param {String} lrc_s - Format: + * [mm:ss.xx]lyric + * [mm:ss.xxx]lyric + * [mm:ss.xx][mm:ss.xx][mm:ss.xx]lyric + * + * @return {String} [[time, text], [time, text], [time, text], ...] + */ + const parseLrc = (lrc_s) => { + const lyric = lrc_s.split('\n'); + const lrc = []; + const lyricLen = lyric.length; + for (let i = 0; i < lyricLen; i++) { + // match lrc time + const lrcTimes = lyric[i].match(/\[(\d{2}):(\d{2})\.(\d{2,3})]/g); + // match lrc text + const lrcText = lyric[i].replace(/\[(\d{2}):(\d{2})\.(\d{2,3})]/g, '').replace(/^\s+|\s+$/g, ''); + + if (lrcTimes) { + // handle multiple time tag + const timeLen = lrcTimes.length; + for (let j = 0; j < timeLen; j++) { + const oneTime = /\[(\d{2}):(\d{2})\.(\d{2,3})]/.exec(lrcTimes[j]); + const lrcTime = oneTime[1] * 60 + parseInt(oneTime[2]) + parseInt(oneTime[3]) / ((oneTime[3] + '').length === 2 ? 100 : 1000); + lrc.push([lrcTime, lrcText]); + } + } + } + // sort by time + lrc.sort((a, b) => a[0] - b[0]); + return lrc; + }; + + // fill in lrc + if (this.options.showlrc) { + const index = indexMusic; + + if (!this.lrcs[index]) { + let lrcs = ''; + if (this.options.showlrc === 1) { + lrcs = this.options.music[index].lrc; + } + else if (this.options.showlrc === 2 || this.options.showlrc === true) { + lrcs = this.savelrc[index]; + } + else if (this.options.showlrc === 3) { + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) { + lrcs = xhr.responseText; + this.lrcs[index] = parseLrc(lrcs); + } + else { + console.log('Request was unsuccessful: ' + xhr.status); + this.lrcs[index] = [['00:00', 'Not available']]; + } + this.lrc = this.lrcs[index]; + let lrcHTML = ''; + for (let i = 0; i < this.lrc.length; i++) { + lrcHTML += `

    ${this.lrc[i][1]}

    `; + } + this.template.lrc.innerHTML = lrcHTML; + if (!this.lrcIndex) { + this.lrcIndex = 0; + } + this.template.lrc.getElementsByTagName('p')[0].classList.add('aplayer-lrc-current'); + this.template.lrc.style.transform = 'translateY(0px)'; + this.template.lrc.style.webkitTransform = 'translateY(0px)'; + } + }; + const apiurl = this.options.music[index].lrc; + xhr.open('get', apiurl, true); + xhr.send(null); + } + if (lrcs) { + this.lrcs[index] = parseLrc(lrcs); + } + else { + if (this.options.showlrc === 3) { + this.lrcs[index] = [['00:00', 'Loading']]; + } + else { + this.lrcs[index] = [['00:00', 'Not available']]; + } + } + } + + this.lrc = this.lrcs[index]; + let lrcHTML = ''; + for (let i = 0; i < this.lrc.length; i++) { + lrcHTML += `

    ${this.lrc[i][1]}

    `; + } + this.template.lrc.innerHTML = lrcHTML; + if (!this.lrcIndex) { + this.lrcIndex = 0; + } + this.template.lrc.getElementsByTagName('p')[0].classList.add('aplayer-lrc-current'); + this.template.lrc.style.transform = 'translateY(0px)'; + this.template.lrc.style.webkitTransform = 'translateY(0px)'; + } + + // set duration time + if (this.audio.duration !== 1) { // compatibility: Android browsers will output 1 at first + this.template.dtime.innerHTML = this.audio.duration ? utils.secondToTime(this.audio.duration) : '00:00'; + } + } + + /** + * Play music + */ + play (time) { + if (Object.prototype.toString.call(time) === '[object Number]') { + this.audio.currentTime = time; + } + if (this.audio.paused) { + this.audio.play(); + } + } + + /** + * Pause music + */ + pause () { + if (!this.audio.paused) { + this.audio.pause(); + } + } + + switchVolumeIcon () { + if (this.volume() >= 0.95) { + this.template.volumeButton.innerHTML = Icons.volumeUp; + } + else if (this.volume() > 0) { + this.template.volumeButton.innerHTML = Icons.volumeDown; + } + else { + this.template.volumeButton.innerHTML = Icons.volumeOff; + } + } + + /** + * Set volume + */ + volume (percentage, nostorage) { + percentage = parseFloat(percentage); + if (!isNaN(percentage)) { + percentage = Math.max(percentage, 0); + percentage = Math.min(percentage, 1); + this.bar.set('volume', percentage, 'height'); + if (!nostorage) { + this.user.set('volume', percentage); + } + + this.audio.volume = percentage; + if (this.audio.muted) { + this.audio.muted = false; + } + + this.switchVolumeIcon(); + } + + return this.audio.volume; + } + + /** + * attach event + */ + on (name, func) { + if (typeof func === 'function') { + this.event[name].push(func); + } + } + + /** + * toggle between play and pause + */ + toggle () { + if (this.template.button.classList.contains('aplayer-play')) { + this.play(); + } + else if (this.template.button.classList.contains('aplayer-pause')) { + this.pause(); + } + } + + /** + * get whether multiple music definitions are loaded + */ + isMultiple () { + return this.options.music.length > 1; + } + + /** + * get random order, using Fisher–Yates shuffle + */ + getRandomOrder () { + function random (min, max) { + if (max) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + } + function shuffle (arr) { + const length = arr.length, + shuffled = new Array(length); + for (let index = 0, rand; index < length; index++) { + rand = random(0, index); + if (rand !== index) { shuffled[index] = shuffled[rand]; } + shuffled[rand] = arr[index]; + } + return shuffled; + } + if (this.isMultiple()) { + this.randomOrder = shuffle([...Array(this.options.music.length)].map(function (item, i) { + return i; + })); + } + } + + /** + * get next random number + */ + nextRandomNum () { + if (this.isMultiple()) { + const index = this.randomOrder.indexOf(this.playIndex); + if (index === this.randomOrder.length - 1) { + return this.randomOrder[0]; + } + else { + return this.randomOrder[index + 1]; + } + } + else { + return 0; + } + } + + /** + * Remove song from playlist + */ + removeSong (indexOfSong) { + if (this.options.music[indexOfSong]) { // Check if song exists + if (this.options.music[indexOfSong + 1] || this.options.music[indexOfSong - 1]) { + if (indexOfSong === this.playIndex) { + if (this.options.music[indexOfSong + 1]) { // Play next song if it exists. + this.setMusic(indexOfSong + 1); + this.playIndex = this.playIndex - 1; // Adjust play index for removed song + } + else if (!this.options.music[indexOfSong + 1]) { // Play previous song if it exists. + this.setMusic(indexOfSong - 1); + } + } + else { + if (indexOfSong < this.playIndex) { + this.playIndex = this.playIndex - 1; + } + } + if (this.template.list[indexOfSong + 1]) { + const targetSong = this.template.list[indexOfSong - 1]; + targetSong.getElementsByClassName('aplayer-list-index')[0].textContent = indexOfSong; + } + else { + for (let i = 1; i < this.template.list.length; i++) { + if (this.template.list[indexOfSong + i]) { + const targetSong = this.template.list[indexOfSong + i]; + targetSong.getElementsByClassName('aplayer-list-index')[0].textContent = indexOfSong + i; + } + } + } + this.options.music.splice(indexOfSong, 1); // Delete song from music array + this.audios.splice(indexOfSong, 1); // Delete song from audios array + this.template.list[indexOfSong].remove(); + if (this.options.music[0] && this.options.music[1]) { + this.multiple = false; + this.container.classList.remove('aplayer-withlist'); + } + } + const listHeight = parseInt(this.template.list.style.height, 10); + this.template.list.style.height = listHeight - 33 + "px"; + } + else { + console.error("ERROR: Song does not exist"); + } + } + + /** + * destroy this player + */ + destroy () { + instances.splice(instances.indexOf(this), 1); + this.pause(); + this.container.innerHTML = ''; + clearInterval(this.playedTime); + for (const key in this) { + if (this.hasOwnProperty(key)) { + delete this[key]; + } + } + } + + /** + * add music dynamically + * + * @param {Array} newMusic + */ + addMusic (newMusic) { + const wasSingle = !this.isMultiple(); + + this.options.music = this.options.music.concat(newMusic); + + let newItemHTML = ``; + for (let i = 0; i < newMusic.length; i++) { + newItemHTML += ` +
  • + + ${this.options.music.length - newMusic.length + i + 1} + ${newMusic[i].title} + ${newMusic[i].author} +
  • `; + } + this.template.listOl.innerHTML += newItemHTML; + + if (wasSingle && this.isMultiple()) { + this.container.classList.add('aplayer-withlist'); + this.audio.loop = false; + } + const songListLength = this.template.listItems.length; + this.template.list.style.height = songListLength * 33 + 'px'; + + this.getRandomOrder(); + } +} + +export default APlayer; diff --git a/src/js/template.js b/src/js/template.js new file mode 100644 index 00000000..b0a62522 --- /dev/null +++ b/src/js/template.js @@ -0,0 +1,44 @@ +import Icons from './icons'; +import tplPlayer from '../template/player.art'; + +class Template { + constructor (options) { + this.container = options.container; + this.options = options.options; + this.mode = options.mode; + this.init(); + } + + init () { + this.container.innerHTML = tplPlayer({ + options: this.options, + icons: Icons, + mode: this.mode + }); + + this.lrc = this.container.querySelector('.aplayer-lrc-contents'); + this.ptime = this.container.querySelector('.aplayer-ptime'); + this.info = this.container.querySelector('.aplayer-info'); + this.time = this.container.querySelector('.aplayer-time'); + this.barWrap = this.container.querySelector('.aplayer-bar-wrap'); + this.button = this.container.querySelector('.aplayer-button'); + this.list = this.container.querySelector('.aplayer-list'); + this.listOl = this.container.querySelector('.aplayer-list ol'); + this.listItems = this.container.querySelectorAll('.aplayer-list li'); + this.played = this.container.querySelector('.aplayer-played'); + this.loaded = this.container.querySelector('.aplayer-loaded'); + this.thumb = this.container.querySelector('.aplayer-thumb'); + this.volume = this.container.querySelector('.aplayer-volume'); + this.volumeBar = this.container.querySelector('.aplayer-volume-bar'); + this.volumeButton = this.container.querySelector('.aplayer-time button'); + this.volumeBarWrap = this.container.querySelector('.aplayer-volume-bar-wrap'); + this.mode = this.container.querySelector('.aplayer-icon-mode'); + this.menu = this.container.querySelector('.aplayer-icon-menu'); + this.pic = this.container.querySelector('.aplayer-pic'); + this.title = this.container.querySelector('.aplayer-title'); + this.author = this.container.querySelector('.aplayer-author'); + this.dtime = this.container.querySelector('.aplayer-dtime'); + } +} + +export default Template; diff --git a/src/js/time.js b/src/js/time.js new file mode 100644 index 00000000..e69de29b diff --git a/src/js/user.js b/src/js/user.js new file mode 100644 index 00000000..9e7757da --- /dev/null +++ b/src/js/user.js @@ -0,0 +1,33 @@ +import utils from './utils'; + +class User { + constructor (player) { + this.storageName = { + volume: 'aplayer-volume', + }; + this.default = { + volume: player.options.volume || 0.7, + }; + this.data = {}; + + this.init(); + } + + init () { + for (const item in this.storageName) { + const name = this.storageName[item]; + this.data[item] = parseFloat(utils.storage.get(name) || this.default[item]); + } + } + + get (key) { + return this.data[key]; + } + + set (key, value) { + this.data[key] = value; + utils.storage.set(this.storageName[key], value); + } +} + +export default User; \ No newline at end of file diff --git a/src/js/utils.js b/src/js/utils.js new file mode 100644 index 00000000..cf4a4405 --- /dev/null +++ b/src/js/utils.js @@ -0,0 +1,71 @@ +const isMobile = /mobile/i.test(window.navigator.userAgent); + +const utils = { + + /** + * Parse second to time string + * + * @param {Number} second + * @return {String} 00:00 or 00:00:00 + */ + secondToTime: (second) => { + const add0 = (num) => num < 10 ? '0' + num : '' + num; + const hour = Math.floor(second / 3600); + const min = Math.floor((second - hour * 3600) / 60); + const sec = Math.floor(second - hour * 3600 - min * 60); + return (hour > 0 ? [hour, min, sec] : [min, sec]).map(add0).join(':'); + }, + + /** + * control play progress + */ + // get element's view position + getElementViewLeft: (element) => { + let actualLeft = element.offsetLeft; + let current = element.offsetParent; + const elementScrollLeft = document.body.scrollLeft + document.documentElement.scrollLeft; + if (!document.fullscreenElement && !document.mozFullScreenElement && !document.webkitFullscreenElement) { + while (current !== null) { + actualLeft += current.offsetLeft; + current = current.offsetParent; + } + } + else { + while (current !== null && current !== element) { + actualLeft += current.offsetLeft; + current = current.offsetParent; + } + } + return actualLeft - elementScrollLeft; + }, + + getElementViewTop: (element) => { + let actualTop = element.offsetTop; + let current = element.offsetParent; + let elementScrollTop = 0; + while (current !== null) { + actualTop += current.offsetTop; + current = current.offsetParent; + } + elementScrollTop = document.body.scrollTop + document.documentElement.scrollTop; + return actualTop - elementScrollTop; + }, + + isMobile: isMobile, + + storage: { + set: (key, value) => { + localStorage.setItem(key, value); + }, + + get: (key) => localStorage.getItem(key) + }, + + nameMap: { + dragStart: isMobile ? 'touchstart' : 'mousedown', + dragMove: isMobile ? 'touchmove' : 'mousemove', + dragEnd: isMobile ? 'touchend' : 'mouseup' + } +}; + +export default utils; \ No newline at end of file diff --git a/src/template/player.art b/src/template/player.art new file mode 100644 index 00000000..a6a5d45e --- /dev/null +++ b/src/template/player.art @@ -0,0 +1,59 @@ +
    +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + + - 00:00 / 00:00 + +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
      + {{each options.music}} +
    1. + + {{ $index + 1 }} + {{ $value.title }} + {{ $value.author }} +
    2. + {{/each}} +
    +
    \ No newline at end of file diff --git a/webpack/dev.config.js b/webpack/dev.config.js index 01534a28..31b78594 100644 --- a/webpack/dev.config.js +++ b/webpack/dev.config.js @@ -75,6 +75,14 @@ module.exports = { options: { 'limit': 40000 } + }, + { + test: /\.svg$/, + loader: 'svg-inline-loader' + }, + { + test: /\.art$/, + loader: 'art-template-loader' } ] }, diff --git a/webpack/prod.config.js b/webpack/prod.config.js index 237cdd89..5412e8eb 100644 --- a/webpack/prod.config.js +++ b/webpack/prod.config.js @@ -83,6 +83,14 @@ module.exports = { options: { 'limit': 40000 } + }, + { + test: /\.svg$/, + loader: 'svg-inline-loader' + }, + { + test: /\.art$/, + loader: 'art-template-loader' } ] },