├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── README.md
├── browserslist
├── demo
├── index.html
├── package.json
├── server.js
├── ui.css
└── ui.js
├── gulpfile.js
├── package.json
├── snapshots
├── normal.png
└── simple.png
└── src
├── events.js
├── icons
├── backward.svg
├── forward.svg
├── list.svg
├── pause.svg
├── play.svg
├── repeat-off.svg
├── repeat-one.svg
└── repeat.svg
├── index.js
├── lyric.js
├── progress.js
├── sprite.js
├── style.less
└── util.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["@babel/preset-env", {
4 | "modules": false,
5 | "loose": true,
6 | }],
7 | "@babel/preset-stage-2",
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/*.js
2 | !src/**
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'airbnb-base',
3 | parser: 'babel-eslint',
4 | parserOptions: {
5 | sourceType: 'module'
6 | },
7 | env: {
8 | browser: true,
9 | },
10 | plugins: [
11 | 'import'
12 | ],
13 | rules: {
14 | 'no-use-before-define': ['error', 'nofunc'],
15 | 'no-mixed-operators': 0,
16 | 'arrow-parens': 0,
17 | 'no-plusplus': 0,
18 | 'no-param-reassign': 0,
19 | 'consistent-return': 0,
20 | 'no-console': ['warn', {
21 | allow: ['error', 'warn', 'info'],
22 | }],
23 | 'no-bitwise': ['error', { int32Hint: true }],
24 | indent: 'off',
25 | },
26 | };
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | *.lock
4 | /.idea
5 | /dist
6 | /src/temp/**
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | demo
2 | snapshots
3 | src
4 | gulpfile.js
5 | yarn.lock
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## H5Player
2 |
3 | 
4 | 
5 |
6 | ### Installation
7 |
8 | ``` sh
9 | $ yarn add h5player
10 | # or
11 | $ npm install h5player -S
12 | ```
13 |
14 | ### Usage
15 |
16 | 1. Load `h5player`
17 |
18 | * Via global
19 |
20 | ``` html
21 |
22 |
23 |
26 | ```
27 |
28 | * Via CMD
29 |
30 | ``` javascript
31 | const H5Player = require('h5player');
32 | ```
33 |
34 | * Via ESModule
35 |
36 | ```js
37 | import H5Player from 'h5player';
38 | ```
39 |
40 | 2. Create a player and append it to `document.body` (or any mounted element).
41 |
42 | ``` javascript
43 | const player = new H5Player({
44 | image: 'http://example.com/path/to/default/image',
45 | getLyric: (song, callback) => {
46 | const lyric = getLyricFromSomewhereElse(song);
47 | callback(lyric);
48 | },
49 | });
50 | document.body.appendChild(player.el);
51 |
52 | player.setSongs([
53 | {
54 | name: 'Song1',
55 | url: 'http://example.com/path/to/song1.mp3',
56 | additionalInfo: 'whatever',
57 | }, {
58 | name: 'Song2',
59 | url: 'http://example.com/path/to/song2.mp3',
60 | }
61 | ]);
62 | player.play(0);
63 | ```
64 |
65 | ### Document
66 |
67 | Each player is built with `player = new H5Player(options)`. *options* is an object with properties below:
68 |
69 | * `theme`: *optional* string
70 |
71 | Possible values are `normal` (by default) and `simple`.
72 | Can be changed by `player.setTheme(theme)`.
73 |
74 | * `mode`: *optional* string
75 |
76 | The repeat mode for the playlist, possible values are `repeatAll` (by default), `repeatOne` and `repeatOff`.
77 | Can be changed by `player.setMode(mode)`.
78 |
79 | * `showPlaylist`: *optional* Boolean
80 |
81 | Whether to show playlist. Can be changed by `player.setPlaylist(show)`.
82 |
83 | * `image`: *optional* string *or* object
84 |
85 | Image shown when no image is assigned for the current song.
86 | It can be a string of the path to the image or an object with theme names as the keys and
87 | image paths as the values.
88 | The recommended image size for **normal** theme is 130 * 130, and 34 * 34 for **simple** theme.
89 |
90 | * `getLyric`: *optional* function
91 |
92 | An async function to get the lyric. There are two parameters for the callback. The first parameter is the song object and the second is a callback to send the lyric to the player.
93 |
94 | The `player` object has following methods:
95 |
96 | * `setSongs`(*Array* songs)
97 |
98 | Set playlist for the player, *songs* is a list of `object`s with properties below:
99 |
100 | * `name`: *required* string
101 |
102 | The name of the song.
103 |
104 | * `url`: *required* string
105 |
106 | A downloadable URL.
107 |
108 | * `artist`: *optional* string
109 |
110 | The name of the artist.
111 |
112 | * `duration`: *optional* integer
113 |
114 | Length of the song in seconds.
115 |
116 | * `image`: *optional* string *or* object
117 |
118 | The image for the current song. Similar to the default image in common settings.
119 |
120 | * `lyric`: *optional* string
121 |
122 | Lyric of the song, e.g. `[00:00]foo\n[00:05]bar\n...`.
123 |
124 | * `play`(*int* index)
125 |
126 | Start playing the *index*-th song.
127 |
128 | * `setTheme`(*string* theme)
129 |
130 | Change theme.
131 |
132 | * `setMode`(*string* mode)
133 |
134 | Change repeat mode.
135 |
136 | * `setPlaylist`(*boolean* show)
137 |
138 | Toggle playlist on / off.
139 |
140 | When the play status is changed, a `PlayerEvent` will be fired with its `detail` set to an object with following attributes:
141 |
142 | * `player`
143 |
144 | The `Player` object that is related to this event
145 |
146 | * `type`
147 |
148 | `'play'` or `'pause'`
149 |
150 | The player is mounted to `player.el`, you need to append it to the container.
151 |
152 | ### Snapshots
153 |
154 | Normal theme:
155 |
156 | 
157 |
158 | Simple theme: (multiple players)
159 |
160 | 
161 |
--------------------------------------------------------------------------------
/browserslist:
--------------------------------------------------------------------------------
1 | > 0.5%
2 | last 2 versions
3 | Firefox ESR
4 | not dead
5 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | H5Player demo
7 |
8 |
9 |
10 |
11 |
12 |
13 | H5Player
14 | Mode:
15 |
16 | ©2015-2016 Gerald
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "1.0.0",
4 | "main": "server.js",
5 | "license": "MIT",
6 | "private": true,
7 | "dependencies": {
8 | "axios": "^0.18.0",
9 | "koa": "^2.5.0",
10 | "koa-send": "^4.1.3"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/demo/server.js:
--------------------------------------------------------------------------------
1 | const Koa = require('koa');
2 | const send = require('koa-send');
3 | const axios = require('axios');
4 |
5 | const PORT = 4009;
6 | const app = new Koa();
7 |
8 | app.use(async (ctx, next) => {
9 | const { path } = ctx;
10 | if (path.startsWith('/dist/')) {
11 | return send(ctx, path, { root: `${__dirname}/..` });
12 | } else {
13 | return send(ctx, path, { root: __dirname });
14 | }
15 | });
16 |
17 | app.listen(PORT, 'localhost', () => {
18 | console.log(`Listen at http://localhost:${PORT}`);
19 | });
20 |
--------------------------------------------------------------------------------
/demo/ui.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | body {
8 | max-width: 500px;
9 | margin: 1em auto;
10 | font-family: Microsoft YaHei;
11 | }
12 |
13 | h1 {
14 | text-align: center;
15 | }
16 |
17 | #players > * {
18 | margin: 1em auto;
19 | }
20 |
--------------------------------------------------------------------------------
/demo/ui.js:
--------------------------------------------------------------------------------
1 | const data = [{
2 | name: 'My Prayer',
3 | artist: 'Devotion',
4 | url: 'http://localhost:4000/Devotion%20-%20My%20Prayer.mp3',
5 | }];
6 | const container = document.querySelector('#players');
7 | let single;
8 | setMode(true);
9 |
10 | document.querySelector('#mode>button')
11 | .addEventListener('click', e => {
12 | setMode(!single);
13 | }, false);
14 |
15 | // custom events
16 | document.addEventListener('PlayerEvent', e => {
17 | const { detail: { type, player } } = e;
18 | const title = [
19 | type === 'play' && player.songs[player.current].name,
20 | 'H5Player',
21 | ].filter(Boolean).join(' - ');
22 | document.title = title;
23 | }, false);
24 |
25 | function setMode(value) {
26 | single = value;
27 | document.querySelector('#mode>span').textContent = value ? 'Single' : 'Multiple';
28 | if (value) initSingle();
29 | else initMultiple();
30 | }
31 |
32 | function reset() {
33 | container.innerHTML = '';
34 | }
35 |
36 | function initSingle() {
37 | reset();
38 | const player = addPlayer('normal');
39 | player.setSongs(data);
40 | }
41 |
42 | function initMultiple() {
43 | reset();
44 | first5 = data.slice(0, 5);
45 | first5.forEach(song => {
46 | const player = addPlayer('simple');
47 | player.setSongs([song]);
48 | });
49 | }
50 |
51 | function addPlayer(theme) {
52 | const div = document.createElement('div');
53 | container.appendChild(div);
54 | const player = new H5Player({
55 | theme,
56 | image: 'http://cn.gravatar.com/avatar/a0ad718d86d21262ccd6ff271ece08a3?s=130',
57 | });
58 | div.append(player.el);
59 | return player;
60 | }
61 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const gulp = require('gulp');
3 | const log = require('fancy-log');
4 | const eslint = require('gulp-eslint');
5 | const less = require('gulp-less');
6 | const svgSymbols = require('gulp-svg-symbols');
7 | const autoprefixer = require('gulp-autoprefixer');
8 | const rollup = require('rollup');
9 | const pkg = require('./package.json');
10 |
11 | const DIST = 'dist';
12 | const IS_PROD = process.env.NODE_ENV === 'production';
13 | const values = {
14 | 'process.env.VERSION': pkg.version,
15 | 'process.env.NODE_ENV': process.env.NODE_ENV || 'development',
16 | };
17 |
18 |
19 | const rollupOptions = {
20 | plugins: [
21 | {
22 | transform(code, id) {
23 | if (path.extname(id) !== '.svg') return;
24 | return `export default ${JSON.stringify(code)};`;
25 | },
26 | },
27 | require('rollup-plugin-babel')({
28 | runtimeHelpers: true,
29 | exclude: 'node_modules/**',
30 | }),
31 | require('rollup-plugin-replace')({ values }),
32 | ],
33 | };
34 |
35 | function buildJs() {
36 | return rollup.rollup(Object.assign({
37 | input: 'src/index.js',
38 | }, rollupOptions))
39 | .then(bundle => bundle.write({
40 | name: 'H5Player',
41 | file: `${DIST}/index.js`,
42 | format: 'umd',
43 | }))
44 | .catch(err => {
45 | log(err.toString());
46 | });
47 | }
48 |
49 | function buildCss() {
50 | return gulp.src('src/style.less')
51 | .pipe(less())
52 | .pipe(autoprefixer())
53 | .pipe(gulp.dest(DIST));
54 | }
55 |
56 | function buildSvg() {
57 | return gulp.src('src/icons/*.svg')
58 | .pipe(svgSymbols({
59 | templates: ['default-svg'],
60 | slug: name => `h5p-${name}`,
61 | }))
62 | .pipe(gulp.dest('src/temp'));
63 | }
64 |
65 | function lint() {
66 | return gulp.src('src/**/*.js')
67 | .pipe(eslint())
68 | .pipe(eslint.format())
69 | .pipe(eslint.failAfterError());
70 | }
71 |
72 | function watch() {
73 | gulp.watch('src/**/*.js', buildJs);
74 | gulp.watch('src/**/*.less', buildCss);
75 | }
76 |
77 | const build = gulp.parallel(gulp.series(buildSvg, buildJs), buildCss);
78 |
79 | exports.lint = lint;
80 | exports.build = build;
81 | exports.dev = gulp.series(build, watch);
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "h5player",
3 | "version": "2.0.0",
4 | "description": "A simple but powerful HTML5 music player",
5 | "author": "Gerald ",
6 | "license": "MIT",
7 | "scripts": {
8 | "dev": "gulp dev",
9 | "build": "gulp build",
10 | "lint": "gulp lint"
11 | },
12 | "title": "H5Player",
13 | "main": "dist/index.js",
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/gera2ld/h5player"
17 | },
18 | "devDependencies": {
19 | "@babel/core": "^7.0.0-beta.40",
20 | "@babel/preset-env": "^7.0.0-beta.40",
21 | "@babel/preset-stage-2": "^7.0.0-beta.40",
22 | "babel-eslint": "^8.2.2",
23 | "eslint": "^4.18.2",
24 | "eslint-config-airbnb-base": "^12.1.0",
25 | "eslint-plugin-import": "^2.9.0",
26 | "fancy-log": "^1.3.2",
27 | "gulp": "^4.0.0",
28 | "gulp-autoprefixer": "^5.0.0",
29 | "gulp-eslint": "^4.0.2",
30 | "gulp-less": "^4.0.0",
31 | "gulp-svg-symbols": "^3.1.0",
32 | "rollup": "^0.56.3",
33 | "rollup-plugin-babel": "^4.0.0-beta.2",
34 | "rollup-plugin-replace": "^2.0.0",
35 | "svgo": "^1.0.5"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/snapshots/normal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gera2ld/h5player/7c87a4fada2a5079de97a31d755cce19f8f515e1/snapshots/normal.png
--------------------------------------------------------------------------------
/snapshots/simple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gera2ld/h5player/7c87a4fada2a5079de97a31d755cce19f8f515e1/snapshots/simple.png
--------------------------------------------------------------------------------
/src/events.js:
--------------------------------------------------------------------------------
1 | export default class EventEmitter {
2 | map = {};
3 |
4 | on(type, handle) {
5 | let handlers = this.map[type];
6 | if (!handlers) {
7 | handlers = [];
8 | this.map[type] = handlers;
9 | }
10 | handlers.push(handle);
11 | return () => this.off(type, handle);
12 | }
13 |
14 | off(type, handle) {
15 | const handlers = this.map[type];
16 | if (handlers) {
17 | const i = handlers.indexOf(handle);
18 | if (i >= 0) handlers.splice(i, 1);
19 | }
20 | }
21 |
22 | once(type, handle) {
23 | const revoke = this.on(type, handleOnce);
24 | return revoke;
25 | function handleOnce(...args) {
26 | handle(...args);
27 | revoke();
28 | }
29 | }
30 |
31 | emit(type, ...args) {
32 | const handlers = this.map[type];
33 | if (handlers) {
34 | handlers.forEach(handle => {
35 | handle(...args);
36 | });
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/icons/backward.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/forward.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/list.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/pause.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/repeat-off.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/repeat-one.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/icons/repeat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import LyricParser from './lyric';
2 | import Progress from './progress';
3 | import { prevent, createElement, bindEvents, empty, createSVGIcon } from './util';
4 | import './sprite';
5 |
6 | const H5P_ACTIVE = 'h5p-active';
7 | const MODES = [
8 | 'repeatAll',
9 | 'repeatOne',
10 | 'repeatOff',
11 | ];
12 | const MODE_ICONS = {
13 | repeatAll: 'h5p-repeat',
14 | repeatOne: 'h5p-repeat-one',
15 | repeatOff: 'h5p-repeat-off',
16 | };
17 |
18 | // manage all the players to ensure only one is playing at once
19 | const players = [];
20 | let currentPlayer = null;
21 |
22 | function setCurrentPlayer(player) {
23 | currentPlayer = player;
24 | players.forEach(other => {
25 | if (player !== other) other.audio.pause();
26 | });
27 | }
28 |
29 | function fireEvent(detail) {
30 | const event = new CustomEvent('PlayerEvent', {
31 | detail,
32 | bubbles: true,
33 | cancelable: true,
34 | });
35 | document.dispatchEvent(event);
36 | }
37 |
38 | export default class Player {
39 | static themes = [
40 | 'normal',
41 | 'simple',
42 | ];
43 |
44 | constructor(options) {
45 | players.push(this);
46 | this.build(options);
47 | this.setSongs([]);
48 | this.setTheme(options.theme);
49 | this.setMode(options.mode);
50 | this.setPlaylist(options.showPlaylist);
51 | }
52 |
53 | build(options) {
54 | this.defaultImage = options.image || '';
55 | this.callbackGetLyric = options.getLyric;
56 | this.progress = new Progress();
57 | const buttons = {};
58 | const image = createElement('div', {
59 | className: 'h5p-image',
60 | });
61 | const toolbar = createElement('div', {
62 | className: 'h5p-toolbar',
63 | }, [
64 | buttons.repeat = createElement('i', {
65 | className: 'h5p-button',
66 | on: {
67 | click: this.handleSwitchMode,
68 | },
69 | }, [createSVGIcon('h5p-repeat')]),
70 | buttons.list = createElement('i', {
71 | className: 'h5p-button',
72 | on: {
73 | click: this.handleToggleList,
74 | },
75 | }, [createSVGIcon('h5p-list')]),
76 | ]);
77 | const title = createElement('div', {
78 | className: 'h5p-title',
79 | });
80 | const artist = createElement('div', {
81 | className: 'h5p-artist',
82 | });
83 | const info = createElement('div', {
84 | className: 'h5p-info',
85 | }, [title, artist]);
86 | const control = createElement('div', {
87 | className: 'h5p-control',
88 | }, [
89 | createElement('i', {
90 | className: 'h5p-button',
91 | on: {
92 | click: this.handlePlayPrev,
93 | },
94 | }, [createSVGIcon('h5p-backward')]),
95 | buttons.play = createElement('i', {
96 | className: 'h5p-button',
97 | on: {
98 | click: this.handleTogglePlay,
99 | },
100 | }, [createSVGIcon('h5p-play')]),
101 | createElement('i', {
102 | className: 'h5p-button',
103 | on: {
104 | click: this.handlePlayNext,
105 | },
106 | }, [createSVGIcon('h5p-forward')]),
107 | ]);
108 | const progress = createElement('div', {
109 | className: 'h5p-progress-wrap',
110 | }, [this.progress.el]);
111 | const lyric = createElement('div', {
112 | className: 'h5p-lyric',
113 | });
114 | const playlist = createElement('div', {
115 | className: 'h5p-playlist',
116 | on: {
117 | click: this.handlePlayItem,
118 | },
119 | });
120 | const audio = bindEvents(new Audio(), {
121 | ended: this.handlePlayAnother,
122 | timeupdate: this.handleUpdateTime,
123 | play: this.handleStatusChange,
124 | pause: this.handleStatusChange,
125 | });
126 | this.progress.on('cursor', this.handleCursorChange);
127 | this.audio = audio;
128 | this.el = createElement('div', {
129 | className: 'h5p',
130 | }, [
131 | image, toolbar, info, control,
132 | progress, lyric, playlist, audio,
133 | ]);
134 | this.els = {
135 | image, buttons, lyric, playlist, title, artist,
136 | };
137 | this.lyricParser = new LyricParser();
138 | }
139 |
140 | destroy() {
141 | const { el } = this;
142 | const parent = el.parentNode;
143 | if (parent) parent.removeChild(el);
144 | }
145 |
146 | play(index) {
147 | if (index == null) index = this.current;
148 | let song = this.songs[index];
149 | if (!song) song = this.songs[index = 0];
150 | if (song) {
151 | if (this.current !== index) {
152 | const { childNodes } = this.els.playlist;
153 | const last = childNodes[this.current];
154 | if (last) last.classList.remove(H5P_ACTIVE);
155 | this.current = index;
156 | childNodes[index].classList.add(H5P_ACTIVE);
157 | this.audio.src = song.url;
158 | this.duration = song.duration ? song.duration / 1000 : null;
159 | this.showInfo(song);
160 | this.progress.setCursor(0, this.duration);
161 | }
162 | this.audio.play();
163 | }
164 | }
165 |
166 | prev() {
167 | return (this.current + this.songs.length - 1) % this.songs.length;
168 | }
169 |
170 | next() {
171 | return (this.current + 1) % this.songs.length;
172 | }
173 |
174 | setSongs(songs) {
175 | this.songs = songs;
176 | const { playlist } = this.els;
177 | empty(playlist);
178 | songs.forEach(({ name }) => {
179 | playlist.appendChild(createElement('div', {
180 | title: name,
181 | textContent: name,
182 | }));
183 | });
184 | this.current = -1;
185 | this.audio.src = '';
186 | this.duration = 0;
187 | this.showInfo(this.songs[0]);
188 | }
189 |
190 | showInfo(song) {
191 | this.updateInfo(song);
192 | const { name, artist } = song || {};
193 | const { els } = this;
194 | els.title.textContent = name || '';
195 | els.artist.textContent = artist || '';
196 | }
197 |
198 | updateInfo(item) {
199 | const song = item || this.songs[this.current];
200 | let { image } = song || {};
201 | if (typeof image === 'object') image = image[this.theme];
202 | image = image || this.defaultImage;
203 | const { els } = this;
204 | const imageEl = empty(els.image);
205 | if (image) {
206 | imageEl.appendChild(createElement('img', {
207 | src: image,
208 | }));
209 | }
210 | els.lyric.textContent = '';
211 | if (song) this.loadLyric(song);
212 | }
213 |
214 | loadLyric(song) {
215 | const { lyricParser } = this;
216 | if (song.lyric == null) {
217 | lyricParser.setLyric();
218 | const { callbackGetLyric } = this;
219 | if (callbackGetLyric) {
220 | callbackGetLyric({ ...song }, lyric => {
221 | if (song === this.songs[this.current]) {
222 | lyricParser.setLyric(song.lyric = lyric || '');
223 | }
224 | });
225 | }
226 | } else {
227 | lyricParser.setLyric(song.lyric);
228 | }
229 | }
230 |
231 | setTheme(name) {
232 | const { themes } = Player;
233 | let index = themes.indexOf(name);
234 | if (index < 0) index = 0;
235 | const oldTheme = this.theme;
236 | this.theme = themes[index];
237 | if (oldTheme !== this.theme) {
238 | const { classList } = this.el;
239 | classList.remove(`h5p-${oldTheme}`);
240 | classList.add(`h5p-${this.theme}`);
241 | this.updateInfo();
242 | }
243 | }
244 |
245 | setMode(mode) {
246 | this.mode = MODES.indexOf(mode) < 0 ? MODES[0] : mode;
247 | const icon = MODE_ICONS[this.mode];
248 | this.els.buttons.repeat.firstChild.replaceWith(createSVGIcon(icon));
249 | }
250 |
251 | setPlaylist(show) {
252 | const { playlist, buttons } = this.els;
253 | buttons.list.classList.toggle(H5P_ACTIVE, !!show);
254 | playlist.style.display = show ? 'block' : '';
255 | }
256 |
257 | handleSwitchMode = e => {
258 | prevent(e);
259 | const index = MODES.indexOf(this.mode);
260 | this.setMode(MODES[(index + 1) % MODES.length]);
261 | }
262 |
263 | handleToggleList = e => {
264 | prevent(e);
265 | this.setPlaylist(!this.els.buttons.list.classList.contains(H5P_ACTIVE));
266 | }
267 |
268 | handleTogglePlay = e => {
269 | prevent(e);
270 | if (this.current < 0) this.play(0);
271 | else if (this.audio.paused) this.audio.play();
272 | else this.audio.pause();
273 | }
274 |
275 | handlePlayPrev = e => {
276 | prevent(e);
277 | this.play(this.prev());
278 | }
279 |
280 | handlePlayNext = e => {
281 | prevent(e);
282 | this.play(this.next());
283 | }
284 |
285 | handlePlayAnother = () => {
286 | const { mode } = this;
287 | if (mode === 'repeatAll') {
288 | this.handlePlayNext();
289 | } else if (mode === 'repeatOne') {
290 | this.play();
291 | } else {
292 | const next = this.next();
293 | if (next) this.play(next);
294 | }
295 | }
296 |
297 | handleUpdateTime = e => {
298 | const { target } = e;
299 | const { currentTime } = target;
300 | this.duration = target.duration || this.duration;
301 | this.progress.setCursor(currentTime, this.duration);
302 | this.els.lyric.textContent = this.lyricParser.getLyricByTime(currentTime);
303 | }
304 |
305 | handleStatusChange = e => {
306 | const { type } = e;
307 | const isPlaying = type === 'play';
308 | if (isPlaying) {
309 | setCurrentPlayer(this);
310 | fireEvent({ type, player: this });
311 | } else if (currentPlayer === this) {
312 | currentPlayer = null;
313 | fireEvent({ type, player: this });
314 | }
315 | const { play } = this.els.buttons;
316 | play.firstChild.replaceWith(createSVGIcon(isPlaying ? 'h5p-pause' : 'h5p-play'));
317 | this.els.image.classList.toggle('h5p-roll', isPlaying);
318 | }
319 |
320 | handlePlayItem = e => {
321 | prevent(e);
322 | const { childNodes } = this.els.playlist;
323 | for (let i = 0; i < childNodes.length; i += 1) {
324 | const child = childNodes[i];
325 | if (child === e.target) {
326 | this.play(i);
327 | break;
328 | }
329 | }
330 | }
331 |
332 | handleCursorChange = pos => {
333 | const currentTime = this.duration * pos | 0;
334 | this.audio.currentTime = currentTime;
335 | this.play();
336 | }
337 | }
338 |
--------------------------------------------------------------------------------
/src/lyric.js:
--------------------------------------------------------------------------------
1 | const RE_LYRIC = /^\[([\d:.]+)\]\s*(.*)$/;
2 |
3 | function getTime(str) {
4 | let time = 0;
5 | str.split(':').forEach(part => {
6 | time = time * 60 + (+part);
7 | });
8 | return time;
9 | }
10 |
11 | export default class LyricParser {
12 | constructor() {
13 | this.reset();
14 | }
15 |
16 | reset() {
17 | this.data = [];
18 | this.index = 0;
19 | }
20 |
21 | setLyric(lyric) {
22 | this.reset();
23 | const { data } = this;
24 | (lyric || '').split('\n')
25 | .forEach(line => {
26 | const matches = line.match(RE_LYRIC);
27 | if (matches) data.push([getTime(matches[1]), matches[2]]);
28 | });
29 | }
30 |
31 | getLyricByTime(time) {
32 | const { data } = this;
33 | let { index } = this;
34 | const last = data[index] || data[index = 0];
35 | if (last) {
36 | const step = last[0] > time ? -1 : 1;
37 | while (true) { // eslint-disable-line no-constant-condition
38 | const item = data[index];
39 | const next = data[index + 1];
40 | if ((!item || item[0] <= time) && (!next || next[0] > time)) break;
41 | index += step;
42 | }
43 | }
44 | const current = data[this.index = index];
45 | return current ? current[1] : '';
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/progress.js:
--------------------------------------------------------------------------------
1 | import EventEmitter from './events';
2 | import { prevent, createElement } from './util';
3 |
4 | export default class Progress extends EventEmitter {
5 | constructor() {
6 | super();
7 | this.build();
8 | }
9 |
10 | build() {
11 | const played = createElement('div', {
12 | className: 'h5p-played',
13 | });
14 | const bar = createElement('div', {
15 | className: 'h5p-bar',
16 | }, [played]);
17 | const cursor = createElement('div', {
18 | className: 'h5p-cursor',
19 | on: {
20 | mousedown: this.handleCursor,
21 | click: prevent,
22 | },
23 | });
24 | const time = createElement('div', {
25 | className: 'h5p-time',
26 | });
27 | const el = createElement('div', {
28 | className: 'h5p-progress',
29 | on: {
30 | click: this.handleCursorChange,
31 | },
32 | }, [bar, cursor, time]);
33 | this.el = el;
34 | this.els = {
35 | bar, played, cursor, time,
36 | };
37 | }
38 |
39 | setCursor(currentTime, duration) {
40 | if (!this.cursorData) this.setCursorPos(duration ? currentTime / duration : null);
41 | this.els.time.textContent = `${formatTime(currentTime)} / ${formatTime(duration)}`;
42 | }
43 |
44 | setCursorPos(pos) {
45 | const { played, cursor } = this.els;
46 | const past = `${(pos || 0) * 100}%`;
47 | played.style.width = past;
48 | cursor.style.left = past;
49 | }
50 |
51 | getPos(e) {
52 | const pos = (e.clientX - this.cursorData.delta) / this.els.bar.offsetWidth;
53 | return Math.max(0, Math.min(1, pos));
54 | }
55 |
56 | handleCursor = e => {
57 | prevent(e);
58 | this.cursorData = {
59 | delta: e.clientX - this.els.played.offsetWidth,
60 | };
61 | document.addEventListener('mousemove', this.handleCursorMove, false);
62 | document.addEventListener('mouseup', this.handleCursorEnd, false);
63 | }
64 |
65 | handleCursorMove = e => {
66 | prevent(e);
67 | this.cursorData.moved = true;
68 | this.setCursorPos(this.getPos(e));
69 | }
70 |
71 | handleCursorChange = e => {
72 | let x;
73 | if ('offsetX' in e) {
74 | x = e.offsetX;
75 | } else {
76 | const rect = e.target.getBoundingClientRect();
77 | const docEl = document.documentElement;
78 | const win = window;
79 | x = e.pageX - (rect.left + win.pageXOffset - docEl.clientLeft);
80 | }
81 | const pos = x / this.els.bar.offsetWidth;
82 | this.setCursorPos(pos);
83 | this.emit('cursor', pos);
84 | }
85 |
86 | handleCursorEnd = e => {
87 | document.removeEventListener('mousemove', this.handleCursorMove, false);
88 | document.removeEventListener('mouseup', this.handleCursorEnd, false);
89 | const pos = this.getPos(e);
90 | this.cursorData = null;
91 | this.setCursorPos(pos);
92 | this.emit('cursor', pos);
93 | };
94 | }
95 |
96 | function formatTime(time) {
97 | const minutes = time / 60 | 0;
98 | const seconds = time % 60 | 0;
99 | return `${leftpadNumber(minutes, 2)}:${leftpadNumber(seconds, 2)}`;
100 | }
101 |
102 | function leftpadNumber(num, len) {
103 | const pad = Number.isNaN(num) ? '?' : '0';
104 | let str;
105 | for (str = `${num}`; str.length < len; str = `${pad}${str}`);
106 | return str;
107 | }
108 |
--------------------------------------------------------------------------------
/src/sprite.js:
--------------------------------------------------------------------------------
1 | import svgSprite from './temp/svg-symbols.svg';
2 | import { createElement } from './util';
3 |
4 | function initialize() {
5 | const { body } = document;
6 | if (!body) {
7 | document.addEventListener('DOMContentLoaded', initialize);
8 | return;
9 | }
10 | const sprite = createElement('div', {
11 | innerHTML: svgSprite,
12 | });
13 | sprite.style.display = 'none';
14 | body.insertBefore(sprite, body.firstChild);
15 | }
16 |
17 | initialize();
18 |
--------------------------------------------------------------------------------
/src/style.less:
--------------------------------------------------------------------------------
1 | @keyframes h5p-roll {
2 | from {
3 | transform: rotate(0deg);
4 | }
5 | to {
6 | transform: rotate(360deg);
7 | }
8 | }
9 |
10 | // common styles
11 | @bar-height: 2px;
12 | @bar-color: silver;
13 | @played-color: brown;
14 | .h5p {
15 | background: #fff;
16 | box-shadow: 0 0 10px gray;
17 | position: relative;
18 | &-button {
19 | cursor: pointer;
20 | &:hover,
21 | &.h5p-active {
22 | color: dodgerblue;
23 | }
24 | svg {
25 | width: 20px;
26 | height: 20px;
27 | fill: currentColor;
28 | }
29 | }
30 | &-image {
31 | position: absolute;
32 | img {
33 | width: 100%;
34 | height: 100%;
35 | }
36 | }
37 | &-title {
38 | font-weight: bold;
39 | }
40 | &-bar {
41 | height: @bar-height;
42 | background: @bar-color;
43 | position: relative;
44 | }
45 | &-played {
46 | position: absolute;
47 | top: 0;
48 | height: @bar-height;
49 | background: @played-color;
50 | }
51 | &-playlist {
52 | display: none;
53 | max-height: 200px;
54 | border-top: 1px dashed gray;
55 | padding: 8px 16px;
56 | overflow-y: auto;
57 | & > div {
58 | white-space: nowrap;
59 | text-overflow: ellipsis;
60 | color: #1a1a1a;
61 | cursor: pointer;
62 | &.h5p-active {
63 | color: dodgerblue;
64 | font-weight: bold;
65 | }
66 | &:hover {
67 | color: orange;
68 | }
69 | }
70 | }
71 | }
72 |
73 | // Theme: normal
74 | .h5p-normal {
75 | border-radius: 3px;
76 | max-width: 400px;
77 | font-size: 16px;
78 | .h5p-image {
79 | width: 130px;
80 | height: 130px;
81 | left: 10px;
82 | top: 10px;
83 | animation: h5p-roll 8s linear infinite;
84 | animation-play-state: paused;
85 | img {
86 | border-radius: 50%;
87 | }
88 | }
89 | .h5p-roll {
90 | animation-play-state: running;
91 | }
92 | .h5p-toolbar {
93 | position: absolute;
94 | top: 10px;
95 | right: 10px;
96 | font-size: 20px;
97 | .h5p-button {
98 | margin-left: 5px;
99 | }
100 | }
101 | .h5p-info {
102 | height: 70px;
103 | padding: 30px 10px 0 150px;
104 | text-align: center;
105 | }
106 | .h5p-control {
107 | padding: 0 10px 20px 150px;
108 | text-align: center;
109 | .h5p-button {
110 | margin: 10px;
111 | font-size: 30px;
112 | }
113 | }
114 | .h5p-artist {
115 | height: 20px;
116 | font-size: 10px;
117 | }
118 | .h5p-lyric {
119 | height: 24px;
120 | text-align: center;
121 | color: brown;
122 | font-size: 12px;
123 | }
124 | @cur-color: white;
125 | @cur-height: 10px;
126 | @cur-width: 10px;
127 | .h5p-progress-wrap {
128 | padding: 5px 10px;
129 | }
130 | .h5p-progress {
131 | position: relative;
132 | padding: (@cur-height - @bar-height) / 2 0;
133 | }
134 | .h5p-cursor {
135 | position: absolute;
136 | width: @cur-width;
137 | height: @cur-height;
138 | top: 0;
139 | margin-left: -@cur-width / 2;
140 | border: 1px solid @bar-color;
141 | border-radius: 50%;
142 | background: @cur-color;
143 | cursor: pointer;
144 | }
145 | .h5p-time {
146 | position: absolute;
147 | top: -20px;
148 | right: 0;
149 | font-size: 10px;
150 | }
151 | }
152 |
153 | // Theme: simple
154 | .h5p-simple {
155 | max-width: 300px;
156 | height: 36px;
157 | border-radius: 2px;
158 | .h5p-image {
159 | width: 34px;
160 | height: 34px;
161 | }
162 | .h5p-toolbar,
163 | .h5p-lyric,
164 | .h5p-time {
165 | display: none;
166 | }
167 | .h5p-info {
168 | position: absolute;
169 | top: 0;
170 | left: 40px;
171 | right: 72px;
172 | line-height: 34px;
173 | font-size: 10px;
174 | white-space: nowrap;
175 | text-overflow: ellipsis;
176 | overflow: hidden;
177 | & > * {
178 | margin: 0 5px;
179 | }
180 | }
181 | .h5p-control {
182 | position: absolute;
183 | top: 0;
184 | right: 5px;
185 | line-height: 34px;
186 | .h5p-button {
187 | display: inline-block;
188 | margin: 0 5px;
189 | font-size: 18px;
190 | }
191 | }
192 | .h5p-title,
193 | .h5p-artist {
194 | display: inline;
195 | }
196 | .h5p-progress-wrap {
197 | position: absolute;
198 | bottom: 0;
199 | width: 100%;
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | export function prevent(e) {
2 | if (e && e.preventDefault) {
3 | e.preventDefault();
4 | e.stopPropagation();
5 | }
6 | }
7 |
8 | export function createElement(tagName, props, children) {
9 | const el = document.createElement(tagName);
10 | if (props) {
11 | Object.keys(props).forEach(key => {
12 | const value = props[key];
13 | if (key === 'on') {
14 | bindEvents(el, value);
15 | } else {
16 | el[key] = value;
17 | }
18 | });
19 | }
20 | if (children) {
21 | children.forEach(child => {
22 | el.appendChild(child);
23 | });
24 | }
25 | return el;
26 | }
27 |
28 | export function bindEvents(el, events) {
29 | if (events) {
30 | Object.keys(events).forEach(type => {
31 | const handle = events[type];
32 | if (handle) el.addEventListener(type, handle);
33 | });
34 | }
35 | return el;
36 | }
37 |
38 | export function empty(el) {
39 | el.innerHTML = '';
40 | return el;
41 | }
42 |
43 | const NS_SVG = 'http://www.w3.org/2000/svg';
44 | const NS_XLINK = 'http://www.w3.org/1999/xlink';
45 |
46 | export function createSVGElement(tagName, children) {
47 | const el = document.createElementNS(NS_SVG, tagName);
48 | if (children) {
49 | children.forEach(child => {
50 | el.appendChild(child);
51 | });
52 | }
53 | return el;
54 | }
55 |
56 | export function createSVGIcon(name) {
57 | const use = createSVGElement('use');
58 | use.setAttributeNS(NS_XLINK, 'href', `#${name}`);
59 | return createSVGElement('svg', [use]);
60 | }
61 |
--------------------------------------------------------------------------------