installed plugin AudioIgniter version 1.7.3

This commit is contained in:
2022-04-17 00:39:10 +00:00
committed by Gitium
parent dbc3ae04ae
commit 55ea6cf8dd
56 changed files with 13860 additions and 0 deletions

View File

@ -0,0 +1,7 @@
{
"presets": [
"es2015",
"react",
"stage-2"
]
}

View File

@ -0,0 +1,18 @@
root = true
[*]
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.php]
indent_style = tab
[*.js]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
indent_size = 4

View File

@ -0,0 +1,2 @@
build/
node_modules/

View File

@ -0,0 +1,38 @@
{
"extends": [
"airbnb",
"plugin:prettier/recommended",
"prettier/react"
],
"plugins": [
"import"
],
"globals": {
"aiStrings": true
},
"env": {
"browser": true
},
"rules": {
"arrow-body-style": 0,
"no-confusing-arrow": 0,
"global-require": 0,
"import/no-extraneous-dependencies": ["error", {"devDependencies": true}],
"import/prefer-default-export": 0,
"react/jsx-filename-extension": 0,
"react/require-default-props": 0,
"react/forbid-prop-types": 0,
"react/default-props-match-prop-types": 0,
"react/prefer-stateless-function": 0,
"react/jsx-curly-spacing": [2, {
"when": "never",
"children": true
}],
"react/no-array-index-key": 0,
"jsx-a11y/anchor-is-valid": 0,
"jsx-a11y/no-static-element-interactions": 0,
"react/destructuring-assignment": 0,
"react/button-has-type": 0,
"jsx-a11y/label-has-for": 0
}
}

View File

@ -0,0 +1 @@
v8.12.0

View File

@ -0,0 +1,13 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"printWidth": 80,
"proseWrap": "never",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>AudioIgniter</title>
<style>
body {
padding-bottom: 120px;
}
</style>
<link href="style.css" rel="stylesheet"></head>
<body>
<div
class="audioigniter-root"
data-player-type="full"
data-tracks-url="/dev-tracks.json"
data-display-active-cover="true"
data-display-tracklist-covers="true"
data-display-credits="true"
data-display-tracklist="true"
data-allow-tracklist-toggle="true"
data-allow-tracklist-loop="true"
data-allow-track-loop="true"
data-allow-playback-rate="true"
data-display-track-no="true"
data-display-artist-names="true"
data-display-buy-buttons="true"
data-buy-buttons-target="true"
data-volume="50"
data-cycle-tracks="true"
data-limit-tracklist-height="true"
data-tracklist-height="185"
data-reverse-track-order="false"
data-skip-amount="15"
data-max-width="600px"
data-initial-track="1"
data-stop-on-finish="false"
data-tracks-delay="5"
data-timer-countdown="false"
data-shuffle="true"
data-shuffle-default="false"
data-soundcloud-client-id=""
></div>
<div
class="audioigniter-root"
data-player-type="simple"
data-tracks-url="/dev-tracks.json"
data-display-credits="true"
data-display-track-no="true"
data-allow-playback-rate="true"
data-display-artist-names="true"
data-display-buy-buttons="true"
data-buy-buttons-target="true"
data-allow-track-loop="true"
data-volume="50"
data-reverse-track-order="false"
data-max-width="600px"
data-initial-track="3"
data-stop-on-finish="false"
data-tracks-delay="0"
data-timer-countdown="false"
data-shuffle="true"
data-soundcloud-client-id=""
></div>
<div
class="audioigniter-root"
data-player-type="global-footer"
data-tracks-url="/dev-tracks.json"
data-display-active-cover="true"
data-display-tracklist-covers="true"
data-display-credits="true"
data-display-tracklist="false"
data-allow-tracklist-toggle="true"
data-allow-tracklist-loop="true"
data-allow-track-loop="true"
data-display-track-no="true"
data-allow-playback-rate="true"
data-display-artist-names="true"
data-display-buy-buttons="true"
data-buy-buttons-target="true"
data-volume="50"
data-skip-amount="15"
data-cycle-tracks="false"
data-limit-tracklist-height="true"
data-tracklist-height="185"
data-reverse-track-order="false"
data-max-width="600px"
data-initial-track="1"
data-stop-on-finish="false"
data-tracks-delay="0"
data-timer-countdown="true"
data-shuffle="true"
data-soundcloud-client-id=""
></div>
<script type="text/javascript" src="app.js"></script><script type="text/javascript" src="style.js"></script></body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
!function(n){function r(e){if(t[e])return t[e].exports;var o=t[e]={i:e,l:!1,exports:{}};return n[e].call(o.exports,o,o.exports,r),o.l=!0,o.exports}var t={};r.m=n,r.c=t,r.d=function(n,t,e){r.o(n,t)||Object.defineProperty(n,t,{configurable:!1,enumerable:!0,get:e})},r.n=function(n){var t=n&&n.__esModule?function(){return n.default}:function(){return n};return r.d(t,"a",t),t},r.o=function(n,r){return Object.prototype.hasOwnProperty.call(n,r)},r.p="",r(r.s=28)}({28:function(n,r){}});

View File

@ -0,0 +1,64 @@
[
{
"title": "Sunrise",
"subtitle": "Thoribass",
"audio": "https://www.cssigniter.com/assets/audioigniter/sunrise.mp3",
"buyUrl": "https://www.cssigniter.com",
"downloadUrl": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/sunrise.mp3",
"cover": "https:\/\/www.cssigniter.com\/preview\/audioigniter\/files\/2016\/08\/Thoribass-Sunrise.jpg",
"lyrics": "Here in my mind\nYou know you might find\nSomething that you\n\nYou thought you once knew\nBut now it's all gone\nAnd you know it's no fun\n\nYeah I know it's no fun\nOh I know it's no fun\nI'm free to be whatever I\nWhatever I choose\nAnd I'll sing the blues if I want\nI'm free to be whatever I\nWhatever I choose\nAnd I'll sing the blues if I want\nWhatever you do\nWhatever you say\nYeah I know it's alright"
},
{
"title": "Seriously long title lorem ipsum dolor sit amet, consectetur adipiscing elit. Non est enim.",
"subtitle": "The Fisherman",
"audio": "https://www.cssigniter.com/assets/audioigniter/sunrise.mp3",
"buyUrl": "https://www.cssigniter.com",
"downloadUrl": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/sunrise.mp3",
"cover": "https:\/\/www.cssigniter.com\/preview\/audioigniter\/files\/2016\/08\/The-Fisherman-Another-Day.jpg"
},
{
"title": "Remix Safety Guide",
"subtitle": "Rocavaco",
"audio": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/remix.mp3",
"buyUrl": "https://www.cssigniter.com",
"cover": "https:\/\/www.cssigniter.com\/preview\/audioigniter\/files\/2016\/08\/Rocavaco-Remix-Safety-Guide.jpg",
"lyrics": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. At eius hic illo natus vitae. Assumenda commodi eaque eos est eum excepturi fugiat provident, quidem saepe? Aut doloremque, unde? Delectus, dolorum."
},
{
"title": "Tomorrow",
"subtitle": "MegaEnx",
"audio": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/tomorrow.mp3",
"buyUrl": "",
"downloadUrl": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/sunrise.mp3",
"cover": ""
},
{
"title": "Is It Because I'm Black (David August Reconstruction)",
"subtitle": "Syl Johnson (SoundCloud)",
"audio": "https://soundcloud.com/enterofficial/maceo-plex-b2b-richie-hawtin-enterweek-11-sake-bar-space-ibiza-september-10th-2015",
"buyUrl": "",
"cover": "https://www.cssigniter.com/preview/audioigniter/files/2016/08/artworks-000103551140-ez6k4x-t500x500.jpg"
},
{
"title": "Deep House Radio",
"subtitle": "",
"audio": "https://deephouseradio.radioca.st/stream/1/",
"buyUrl": "https://www.cssigniter.com",
"cover": "https://www.cssigniter.com/preview/audioigniter/files/2016/08/Rocavaco-Remix-Safety-Guide.jpg"
},
{
"title": "Flash of Light",
"subtitle": "Kxmode",
"audio": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/flashlight.mp3",
"buyUrl": "https://www.cssigniter.com",
"cover": "https:\/\/www.cssigniter.com\/preview\/audioigniter\/files\/2016\/08\/Kxmode-Flash-of-Light.jpg",
"lyrics": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. At eius hic illo natus vitae. Assumenda commodi eaque eos est eum excepturi fugiat provident, quidem saepe? Aut doloremque, unde? Delectus, dolorum."
},
{
"title": "We Get Mental",
"subtitle": "BitBurner",
"audio": "https:\/\/www.cssigniter.com\/assets\/audioigniter\/mental.mp3",
"buyUrl": "",
"cover": "https:\/\/www.cssigniter.com\/preview\/audioigniter\/files\/2016\/08\/BitBurner-We-Get-Mental.jpg"
}
]

View File

@ -0,0 +1,62 @@
{
"name": "audioigniter",
"version": "1.6.1",
"description": "React audio player",
"main": "index.js",
"scripts": {
"start": "webpack-dev-server",
"build": "rm -rf ./build && webpack",
"lint": "eslint ./src --ext .js --ext .jsx --cache || true",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"audio",
"audio player",
"react"
],
"author": "vmasto",
"license": "ISC",
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.25.0",
"babel-loader": "^7.1.1",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-react-hmre": "1.1.1",
"babel-preset-stage-1": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"css-loader": "^0.28.4",
"eslint": "3.19.0",
"eslint-config-airbnb": "15.0.2",
"eslint-config-airbnb-base": "^11.2.0",
"eslint-config-prettier": "^4.1.0",
"eslint-plugin-import": "2.7.0",
"eslint-plugin-jsx-a11y": "5.1.1",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "7.1.0",
"extract-text-webpack-plugin": "^3.0.0",
"html-webpack-plugin": "^2.29.0",
"node-sass": "^4.5.3",
"postcss-loader": "^2.0.6",
"precss": "^2.0.0",
"prettier": "^1.16.4",
"sass-loader": "^6.0.6",
"style-loader": "^0.18.2",
"webpack": "^3.2.0",
"webpack-dev-server": "^2.5.1",
"webpack-merge": "^4.1.0"
},
"dependencies": {
"classnames": "2.3.0",
"es6-promise": "^4.1.1",
"prop-types": "^15.7.2",
"react": "^16.8.3",
"react-custom-scrollbars": "^4.1.2",
"react-dom": "^16.8.3",
"react-modal": "^3.8.1",
"react-sound": "^1.2.0",
"soundmanager2": "^2.97.20170602",
"sprintf-js": "1.1.1",
"whatwg-fetch": "0.11.1"
}
}

View File

@ -0,0 +1,64 @@
import React, { Fragment, useState, createContext } from 'react';
import PropTypes from 'prop-types';
import Player from './player/Player';
import SimplePlayer from './player/SimplePlayer';
import GlobalFooterPlayer from './player/GlobalFooterPlayer';
import TrackLyricsModal from './player/components/TrackLyricsModal';
export const AppContext = createContext();
const App = ({ type, ...props }) => {
const [modal, setModalState] = useState({
open: false,
track: null,
});
const toggleLyricsModal = (open, track) =>
setModalState(prevState => ({
...prevState,
track,
open,
}));
const { track, open } = modal;
const PlayerActual = (() => {
if (type === 'simple') {
return SimplePlayer;
}
if (type === 'global-footer') {
return GlobalFooterPlayer;
}
return Player;
})();
return (
<Fragment>
<AppContext.Provider
value={{
toggleLyricsModal,
}}
>
<PlayerActual {...props} />
</AppContext.Provider>
{track && track.lyrics && (
<TrackLyricsModal
isOpen={open}
closeModal={() => toggleLyricsModal(false)}
>
{track && track.lyrics}
</TrackLyricsModal>
)}
</Fragment>
);
};
App.propTypes = {
type: PropTypes.string,
};
export default App;

View File

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title><%= htmlWebpackPlugin.options.title %></title>
<style>
body {
padding-bottom: 120px;
}
</style>
</head>
<body>
<div
class="audioigniter-root"
data-player-type="full"
data-tracks-url="/dev-tracks.json"
data-display-active-cover="true"
data-display-tracklist-covers="true"
data-display-credits="true"
data-display-tracklist="true"
data-allow-tracklist-toggle="true"
data-allow-tracklist-loop="true"
data-allow-track-loop="true"
data-allow-playback-rate="true"
data-display-track-no="true"
data-display-artist-names="true"
data-display-buy-buttons="true"
data-buy-buttons-target="true"
data-volume="50"
data-cycle-tracks="true"
data-limit-tracklist-height="true"
data-tracklist-height="185"
data-reverse-track-order="false"
data-skip-amount="15"
data-max-width="600px"
data-initial-track="1"
data-stop-on-finish="false"
data-tracks-delay="5"
data-timer-countdown="false"
data-shuffle="true"
data-shuffle-default="false"
data-soundcloud-client-id=""
></div>
<div
class="audioigniter-root"
data-player-type="simple"
data-tracks-url="/dev-tracks.json"
data-display-credits="true"
data-display-track-no="true"
data-allow-playback-rate="true"
data-display-artist-names="true"
data-display-buy-buttons="true"
data-buy-buttons-target="true"
data-allow-track-loop="true"
data-volume="50"
data-reverse-track-order="false"
data-max-width="600px"
data-initial-track="3"
data-stop-on-finish="false"
data-tracks-delay="0"
data-timer-countdown="false"
data-shuffle="true"
data-soundcloud-client-id=""
></div>
<div
class="audioigniter-root"
data-player-type="global-footer"
data-tracks-url="/dev-tracks.json"
data-display-active-cover="true"
data-display-tracklist-covers="true"
data-display-credits="true"
data-display-tracklist="false"
data-allow-tracklist-toggle="true"
data-allow-tracklist-loop="true"
data-allow-track-loop="true"
data-display-track-no="true"
data-allow-playback-rate="true"
data-display-artist-names="true"
data-display-buy-buttons="true"
data-buy-buttons-target="true"
data-volume="50"
data-skip-amount="15"
data-cycle-tracks="false"
data-limit-tracklist-height="true"
data-tracklist-height="185"
data-reverse-track-order="false"
data-max-width="600px"
data-initial-track="1"
data-stop-on-finish="false"
data-tracks-delay="0"
data-timer-countdown="true"
data-shuffle="true"
data-soundcloud-client-id=""
></div>
</body>
</html>

View File

@ -0,0 +1,97 @@
import React from 'react';
import { render } from 'react-dom';
import 'es6-promise/auto';
import 'whatwg-fetch';
import App from './App';
// Set up translatable strings here
// for development purposes only. The production build
// gets them from WordPress's injection
if (process.env.NODE_ENV !== 'production') {
window.aiStrings = {
play_title: 'Play %s',
pause_title: 'Pause %s',
previous: 'Previous track',
next: 'Next track',
toggle_list_repeat: 'Toggle track listing repeat',
toggle_list_visible: 'Toggle track listing visibility',
toggle_track_repeat: 'Toggle track repeat',
buy_track: 'Buy this track',
download_track: 'Download this track',
volume_up: 'Volume Up',
volume_down: 'Volume Down',
open_track_lyrics: 'Open track lyrics',
set_playback_rate: 'Set playback rate',
skip_forward: 'Skip forward',
skip_backward: 'Skip backward',
shuffle: 'Shuffle',
};
}
const nodes = document.getElementsByClassName('audioigniter-root');
function renderApp(node) {
const type = node.getAttribute('data-player-type');
const props = {
tracksUrl: node.getAttribute('data-tracks-url'),
displayTracklistCovers: JSON.parse(
node.getAttribute('data-display-tracklist-covers'),
),
displayActiveCover: JSON.parse(
node.getAttribute('data-display-active-cover'),
),
displayCredits: JSON.parse(node.getAttribute('data-display-credits')),
displayTracklist: JSON.parse(node.getAttribute('data-display-tracklist')),
allowTracklistToggle: JSON.parse(
node.getAttribute('data-allow-tracklist-toggle'),
),
allowPlaybackRate: JSON.parse(
node.getAttribute('data-allow-playback-rate'),
),
allowTracklistLoop: JSON.parse(
node.getAttribute('data-allow-tracklist-loop'),
),
allowTrackLoop: JSON.parse(node.getAttribute('data-allow-track-loop')),
displayTrackNo: JSON.parse(node.getAttribute('data-display-track-no')),
displayBuyButtons: JSON.parse(
node.getAttribute('data-display-buy-buttons'),
),
buyButtonsTarget: JSON.parse(node.getAttribute('data-buy-buttons-target')),
volume: parseInt(node.getAttribute('data-volume'), 10),
displayArtistNames: JSON.parse(
node.getAttribute('data-display-artist-names'),
),
cycleTracks: JSON.parse(node.getAttribute('data-cycle-tracks')),
limitTracklistHeight: JSON.parse(
node.getAttribute('data-limit-tracklist-height'),
),
tracklistHeight: parseInt(node.getAttribute('data-tracklist-height'), 10),
reverseTrackOrder: JSON.parse(
node.getAttribute('data-reverse-track-order'),
),
maxWidth: node.getAttribute('data-max-width'),
soundcloudClientId: node.getAttribute('data-soundcloud-client-id'),
skipAmount: parseInt(node.getAttribute('data-skip-amount'), 10),
initialTrack: parseInt(node.getAttribute('data-initial-track'), 10),
delayBetweenTracks: parseInt(node.getAttribute('data-tracks-delay'), 10),
stopOnTrackFinish: JSON.parse(node.getAttribute('data-stop-on-finish')),
defaultShuffle: JSON.parse(node.getAttribute('data-shuffle')),
shuffleEnabled: JSON.parse(node.getAttribute('data-shuffle')),
countdownTimerByDefault: JSON.parse(
node.getAttribute('data-timer-countdown'),
),
};
render(<App type={type} {...props} />, node);
}
Array.prototype.slice.call(nodes).forEach(node => {
renderApp(node);
});
// eslint-disable-next-line no-underscore-dangle
window.__CI_AUDIOIGNITER_MANUAL_INIT__ = node => {
renderApp(node);
};

View File

@ -0,0 +1,357 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import Sound from 'react-sound';
import { sprintf } from 'sprintf-js';
import classNames from 'classnames';
import soundProvider from './soundProvider';
import Cover from './components/Cover';
import Button from './components/Button';
import ProgressBar from './components/ProgressBar';
import Time from './components/Time';
import VolumeControl from './components/VolumeControl';
import TracklistWrap from './components/TracklistWrap';
import {
PlayIcon,
PauseIcon,
NextIcon,
PreviousIcon,
PlaylistIcon,
RefreshIcon,
LyricsIcon,
} from './components/Icons';
import { AppContext } from '../App';
import typographyDisabled from '../utils/typography-disabled';
class GlobalFooterPlayer extends React.Component {
constructor(props) {
super(props);
this.state = {
isTrackListOpen: this.props.displayTracklist,
};
this.toggleTracklist = this.toggleTracklist.bind(this);
}
toggleTracklist() {
this.setState(state => ({
isTrackListOpen: !state.isTrackListOpen,
}));
}
render() {
const { isTrackListOpen } = this.state;
const {
tracks,
playStatus,
activeIndex,
volume,
position,
duration,
playbackRate,
currentTrack,
playTrack,
togglePlay,
nextTrack,
prevTrack,
setPosition,
setVolume,
toggleTracklistCycling,
cycleTracks,
setTrackCycling,
setPlaybackRate,
allowPlaybackRate,
allowTracklistToggle,
allowTracklistLoop,
allowTrackLoop,
reverseTrackOrder,
displayTrackNo,
displayTracklistCovers,
displayActiveCover,
limitTracklistHeight,
tracklistHeight,
displayBuyButtons,
buyButtonsTarget,
displayArtistNames,
repeatingTrackIndex,
skipAmount,
skipPosition,
countdownTimerByDefault,
buffering,
} = this.props;
const classes = classNames({
'ai-wrap': true,
'ai-type-global-footer': true,
'ai-is-loading': !tracks.length,
'ai-with-typography': !typographyDisabled(),
});
const audioControlClasses = classNames({
'ai-audio-control': true,
'ai-audio-playing': playStatus === Sound.status.PLAYING,
'ai-audio-loading': buffering,
});
return (
<div
ref={ref => (this.root = ref)} // eslint-disable-line no-return-assign
className={classes}
>
<div className="ai-control-wrap">
{displayActiveCover && (
<Cover
className="ai-thumb ai-control-wrap-thumb"
src={currentTrack.cover}
alt={currentTrack.title}
/>
)}
<div className="ai-control-wrap-controls">
<ProgressBar
setPosition={setPosition}
duration={duration}
position={position}
/>
<div className="ai-audio-controls-main">
<Button
onClick={togglePlay}
className={audioControlClasses}
ariaLabel={
playStatus === Sound.status.PLAYING
? sprintf(aiStrings.pause_title, currentTrack.title)
: sprintf(aiStrings.play_title, currentTrack.title)
}
ariaPressed={playStatus === Sound.status.PLAYING}
>
{playStatus === Sound.status.PLAYING ? (
<PauseIcon />
) : (
<PlayIcon />
)}
<span className="ai-control-spinner" />
</Button>
<div className="ai-audio-controls-meta">
{tracks.length > 1 && (
<Button
className="ai-btn ai-tracklist-prev"
onClick={prevTrack}
ariaLabel={aiStrings.previous}
>
<PreviousIcon />
</Button>
)}
{tracks.length > 1 && (
<Button
className="ai-btn ai-tracklist-next"
onClick={nextTrack}
ariaLabel={aiStrings.next}
>
<NextIcon />
</Button>
)}
<VolumeControl
volume={volume}
// eslint-disable-next-line no-shadow
setVolume={setVolume}
/>
{allowTracklistLoop && (
<Button
className={`ai-btn ai-btn-repeat ${cycleTracks &&
'ai-btn-active'}`}
onClick={toggleTracklistCycling}
ariaLabel={aiStrings.toggle_list_repeat}
>
<RefreshIcon />
</Button>
)}
{allowPlaybackRate && (
<Button
className="ai-btn ai-btn-playback-rate"
onClick={setPlaybackRate}
ariaLabel={aiStrings.set_playback_rate}
>
<Fragment>&times;{playbackRate}</Fragment>
</Button>
)}
{skipAmount > 0 && (
<Fragment>
<Button
className="ai-btn ai-btn-skip-position"
onClick={() => skipPosition(-1)}
ariaLabel={aiStrings.skip_backward}
>
-{skipAmount}s
</Button>
<Button
className="ai-btn ai-btn-skip-position"
onClick={() => skipPosition(1)}
ariaLabel={aiStrings.skip_forward}
>
+{skipAmount}s
</Button>
</Fragment>
)}
{currentTrack && currentTrack.lyrics && !isTrackListOpen && (
<AppContext.Consumer>
{({ toggleLyricsModal }) => (
<Button
className="ai-btn ai-lyrics"
onClick={() => toggleLyricsModal(true, currentTrack)}
ariaLabel={aiStrings.open_track_lyrics}
title={aiStrings.open_track_lyrics}
>
<LyricsIcon />
</Button>
)}
</AppContext.Consumer>
)}
</div>
<div className="ai-track-info">
<p className="ai-track-title">
<span>{currentTrack.title}</span>
</p>
{(tracks.length === 0 || currentTrack.subtitle) &&
displayArtistNames && (
<p className="ai-track-subtitle">
<span>{currentTrack.subtitle}</span>
</p>
)}
</div>
<div className="ai-audio-controls-meta-right">
<Time
duration={duration}
position={position}
countdown={countdownTimerByDefault}
/>
{allowTracklistToggle && (
<Button
className="ai-btn ai-tracklist-toggle"
onClick={this.toggleTracklist}
ariaLabel={aiStrings.toggle_list_visible}
>
<PlaylistIcon />
</Button>
)}
</div>
</div>
</div>
</div>
<div
className={`ai-tracklist-wrap ${
isTrackListOpen ? 'ai-tracklist-open' : ''
}`}
style={{ display: isTrackListOpen ? 'block' : 'none' }}
>
<TracklistWrap
className="ai-tracklist"
trackClassName="ai-track"
tracks={tracks}
activeTrackIndex={activeIndex}
isOpen={isTrackListOpen}
displayTrackNo={displayTrackNo}
displayCovers={displayTracklistCovers}
displayBuyButtons={displayBuyButtons}
buyButtonsTarget={buyButtonsTarget}
displayArtistNames={displayArtistNames}
reverseTrackOrder={reverseTrackOrder}
limitTracklistHeight={limitTracklistHeight}
tracklistHeight={tracklistHeight}
onTrackClick={playTrack}
onTrackLoop={allowTrackLoop ? setTrackCycling : undefined}
repeatingTrackIndex={repeatingTrackIndex}
/>
</div>
</div>
);
}
}
GlobalFooterPlayer.propTypes = {
tracks: PropTypes.arrayOf(PropTypes.object),
playStatus: PropTypes.oneOf([
Sound.status.PLAYING,
Sound.status.PAUSED,
Sound.status.STOPPED,
]),
activeIndex: PropTypes.number,
volume: PropTypes.number,
position: PropTypes.number,
duration: PropTypes.number,
currentTrack: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
playTrack: PropTypes.func.isRequired,
togglePlay: PropTypes.func.isRequired,
nextTrack: PropTypes.func.isRequired,
prevTrack: PropTypes.func.isRequired,
setPosition: PropTypes.func.isRequired,
setVolume: PropTypes.func.isRequired,
toggleTracklistCycling: PropTypes.func.isRequired,
cycleTracks: PropTypes.bool.isRequired,
displayTracklist: PropTypes.bool,
allowTracklistToggle: PropTypes.bool,
allowTracklistLoop: PropTypes.bool,
reverseTrackOrder: PropTypes.bool,
displayTrackNo: PropTypes.bool,
displayActiveCover: PropTypes.bool,
displayTracklistCovers: PropTypes.bool,
limitTracklistHeight: PropTypes.bool,
tracklistHeight: PropTypes.number,
displayBuyButtons: PropTypes.bool,
buyButtonsTarget: PropTypes.bool,
displayArtistNames: PropTypes.bool,
setTrackCycling: PropTypes.func.isRequired,
repeatingTrackIndex: PropTypes.number,
allowTrackLoop: PropTypes.bool,
playbackRate: PropTypes.number,
setPlaybackRate: PropTypes.func,
skipAmount: PropTypes.number,
skipPosition: PropTypes.func.isRequired,
countdownTimerByDefault: PropTypes.bool,
allowPlaybackRate: PropTypes.bool,
buffering: PropTypes.bool,
};
export default soundProvider(GlobalFooterPlayer, {
onFinishedPlaying(props) {
const {
repeatingTrackIndex,
cycleTracks,
nextTrack,
activeIndex,
playTrack,
trackQueue,
} = props;
if (repeatingTrackIndex != null) {
playTrack(repeatingTrackIndex);
return;
}
if (cycleTracks) {
nextTrack();
return;
}
// Check if not the last track
if (activeIndex !== trackQueue[trackQueue.length - 1]) {
nextTrack();
}
},
});

View File

@ -0,0 +1,403 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import Sound from 'react-sound';
import { sprintf } from 'sprintf-js';
import classNames from 'classnames';
import TracklistWrap from './components/TracklistWrap';
import ProgressBar from './components/ProgressBar';
import Time from './components/Time';
import VolumeControl from './components/VolumeControl';
import Button from './components/Button';
import Cover from './components/Cover';
import {
PlayIcon,
PauseIcon,
NextIcon,
PreviousIcon,
PlaylistIcon,
RefreshIcon,
LyricsIcon,
ShuffleIcon,
} from './components/Icons';
import soundProvider from './soundProvider';
import { AppContext } from '../App';
import typographyDisabled from '../utils/typography-disabled';
class Player extends React.Component {
constructor(props) {
super(props);
this.state = {
isTrackListOpen: this.props.displayTracklist,
};
this.toggleTracklist = this.toggleTracklist.bind(this);
this.isNarrowContext = this.isNarrowContext.bind(this);
}
isNarrowContext() {
return this.root && this.root.offsetWidth < 480 && window.innerWidth > 480;
}
toggleTracklist() {
this.setState(state => ({
isTrackListOpen: !state.isTrackListOpen,
}));
}
render() {
const { isTrackListOpen } = this.state;
const {
tracks,
playStatus,
activeIndex,
volume,
position,
duration,
playbackRate,
shuffle,
shuffleEnabled,
currentTrack,
playTrack,
togglePlay,
nextTrack,
prevTrack,
setPosition,
setVolume,
setPlaybackRate,
toggleTracklistCycling,
cycleTracks,
toggleShuffle,
allowTracklistToggle,
allowTracklistLoop,
allowPlaybackRate,
allowTrackLoop,
setTrackCycling,
reverseTrackOrder,
displayTrackNo,
displayTracklistCovers,
displayActiveCover,
displayCredits,
limitTracklistHeight,
tracklistHeight,
displayBuyButtons,
buyButtonsTarget,
displayArtistNames,
maxWidth,
repeatingTrackIndex,
skipAmount,
skipPosition,
countdownTimerByDefault,
buffering,
} = this.props;
const classes = classNames({
'ai-wrap': true,
'ai-type-full': true,
'ai-is-loading': !tracks.length,
'ai-narrow': this.isNarrowContext(),
'ai-with-typography': !typographyDisabled(),
});
const audioControlClasses = classNames({
'ai-audio-control': true,
'ai-audio-playing': playStatus === Sound.status.PLAYING,
'ai-audio-loading': buffering,
});
return (
<div
ref={ref => (this.root = ref)} // eslint-disable-line no-return-assign
className={classes}
style={{ maxWidth }}
>
<div className="ai-control-wrap">
{displayActiveCover && (
<Cover
className="ai-thumb ai-control-wrap-thumb"
src={currentTrack.cover}
alt={currentTrack.title}
/>
)}
<div className="ai-control-wrap-controls">
<div className="ai-audio-controls-main">
<Button
onClick={togglePlay}
className={audioControlClasses}
ariaLabel={
playStatus === Sound.status.PLAYING
? sprintf(aiStrings.pause_title, currentTrack.title)
: sprintf(aiStrings.play_title, currentTrack.title)
}
ariaPressed={playStatus === Sound.status.PLAYING}
>
{playStatus === Sound.status.PLAYING ? (
<PauseIcon />
) : (
<PlayIcon />
)}
<span className="ai-control-spinner" />
</Button>
<div className="ai-track-info">
<p className="ai-track-title">
<span>{currentTrack.title}</span>
</p>
{(tracks.length === 0 || currentTrack.subtitle) &&
displayArtistNames && (
<p className="ai-track-subtitle">
<span>{currentTrack.subtitle}</span>
</p>
)}
</div>
</div>
<div className="ai-audio-controls-progress">
<ProgressBar
setPosition={setPosition}
duration={duration}
position={position}
/>
<Time
duration={duration}
position={position}
countdown={countdownTimerByDefault}
/>
</div>
<div className="ai-audio-controls-meta">
{tracks.length > 1 && (
<Button
className="ai-btn ai-tracklist-prev"
onClick={prevTrack}
ariaLabel={aiStrings.previous}
title={aiStrings.previous}
>
<PreviousIcon />
</Button>
)}
{tracks.length > 1 && (
<Button
className="ai-btn ai-tracklist-next"
onClick={nextTrack}
ariaLabel={aiStrings.next}
title={aiStrings.next}
>
<NextIcon />
</Button>
)}
<VolumeControl
volume={volume}
// eslint-disable-next-line no-shadow
setVolume={setVolume}
/>
{allowTracklistLoop && (
<Button
className={`ai-btn ai-btn-repeat ${cycleTracks &&
'ai-btn-active'}`}
onClick={toggleTracklistCycling}
ariaLabel={aiStrings.toggle_list_repeat}
>
<RefreshIcon />
</Button>
)}
{shuffleEnabled && (
<Button
className={`ai-btn ai-btn-shuffle ${shuffle &&
'ai-btn-active'}`}
onClick={toggleShuffle}
ariaLabel={aiStrings.shuffle}
>
<ShuffleIcon />
</Button>
)}
{allowPlaybackRate && (
<Button
className="ai-btn ai-btn-playback-rate"
onClick={setPlaybackRate}
ariaLabel={aiStrings.set_playback_rate}
>
<Fragment>&times;{playbackRate}</Fragment>
</Button>
)}
{skipAmount > 0 && (
<Fragment>
<Button
className="ai-btn ai-btn-skip-position"
onClick={() => skipPosition(-1)}
ariaLabel={aiStrings.skip_backward}
>
-{skipAmount}s
</Button>
<Button
className="ai-btn ai-btn-skip-position"
onClick={() => skipPosition(1)}
ariaLabel={aiStrings.skip_forward}
>
+{skipAmount}s
</Button>
</Fragment>
)}
{currentTrack && currentTrack.lyrics && !isTrackListOpen && (
<AppContext.Consumer>
{({ toggleLyricsModal }) => (
<Button
className="ai-btn ai-lyrics"
onClick={() => toggleLyricsModal(true, currentTrack)}
ariaLabel={aiStrings.open_track_lyrics}
title={aiStrings.open_track_lyrics}
>
<LyricsIcon />
</Button>
)}
</AppContext.Consumer>
)}
{allowTracklistToggle && (
<Button
className="ai-btn ai-tracklist-toggle"
onClick={this.toggleTracklist}
ariaLabel={aiStrings.toggle_list_visible}
ariaExpanded={isTrackListOpen}
>
<PlaylistIcon />
</Button>
)}
</div>
</div>
</div>
<div
className={`ai-tracklist-wrap ${
isTrackListOpen ? 'ai-tracklist-open' : ''
}`}
>
<TracklistWrap
className="ai-tracklist"
trackClassName="ai-track"
tracks={tracks}
activeTrackIndex={activeIndex}
isOpen={isTrackListOpen}
displayTrackNo={displayTrackNo}
displayCovers={displayTracklistCovers}
displayBuyButtons={displayBuyButtons}
buyButtonsTarget={buyButtonsTarget}
displayArtistNames={displayArtistNames}
reverseTrackOrder={reverseTrackOrder}
limitTracklistHeight={limitTracklistHeight}
tracklistHeight={tracklistHeight}
onTrackClick={playTrack}
onTrackLoop={allowTrackLoop ? setTrackCycling : undefined}
repeatingTrackIndex={repeatingTrackIndex}
/>
</div>
{displayCredits && (
<div className="ai-footer">
<p>
Powered by{' '}
<a
href="https://www.cssigniter.com/plugins/audioigniter?utm_source=player&utm_medium=link&utm_content=audioigniter&utm_campaign=footer-link"
target="_blank"
rel="noopener noreferrer"
>
AudioIgniter
</a>
</p>
</div>
)}
</div>
);
}
}
Player.propTypes = {
tracks: PropTypes.arrayOf(PropTypes.object),
playStatus: PropTypes.oneOf([
Sound.status.PLAYING,
Sound.status.PAUSED,
Sound.status.STOPPED,
]),
activeIndex: PropTypes.number,
volume: PropTypes.number,
position: PropTypes.number,
duration: PropTypes.number,
currentTrack: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
playTrack: PropTypes.func.isRequired,
togglePlay: PropTypes.func.isRequired,
nextTrack: PropTypes.func.isRequired,
prevTrack: PropTypes.func.isRequired,
setPosition: PropTypes.func.isRequired,
setVolume: PropTypes.func.isRequired,
toggleTracklistCycling: PropTypes.func.isRequired,
setTrackCycling: PropTypes.func.isRequired,
cycleTracks: PropTypes.bool.isRequired,
displayTracklist: PropTypes.bool,
allowTracklistToggle: PropTypes.bool,
allowTracklistLoop: PropTypes.bool,
allowTrackLoop: PropTypes.bool,
reverseTrackOrder: PropTypes.bool,
displayTrackNo: PropTypes.bool,
displayCredits: PropTypes.bool,
displayActiveCover: PropTypes.bool,
displayTracklistCovers: PropTypes.bool,
limitTracklistHeight: PropTypes.bool,
tracklistHeight: PropTypes.number,
displayBuyButtons: PropTypes.bool,
buyButtonsTarget: PropTypes.bool,
displayArtistNames: PropTypes.bool,
maxWidth: PropTypes.string,
repeatingTrackIndex: PropTypes.number,
playbackRate: PropTypes.number,
setPlaybackRate: PropTypes.func,
skipAmount: PropTypes.number,
skipPosition: PropTypes.func.isRequired,
countdownTimerByDefault: PropTypes.bool,
allowPlaybackRate: PropTypes.bool,
buffering: PropTypes.bool,
shuffleEnabled: PropTypes.bool,
shuffle: PropTypes.bool,
toggleShuffle: PropTypes.func.isRequired,
};
export default soundProvider(Player, {
onFinishedPlaying(props) {
const {
repeatingTrackIndex,
cycleTracks,
nextTrack,
activeIndex,
playTrack,
trackQueue,
} = props;
if (repeatingTrackIndex != null) {
playTrack(repeatingTrackIndex);
return;
}
if (cycleTracks) {
nextTrack();
return;
}
// Check if not the last track
if (activeIndex !== trackQueue[trackQueue.length - 1]) {
nextTrack();
}
},
});

View File

@ -0,0 +1,124 @@
import React from 'react';
import PropTypes from 'prop-types';
import Sound from 'react-sound';
import classNames from 'classnames';
import soundProvider from './soundProvider';
import Tracklist from './components/Tracklist';
import typographyDisabled from '../utils/typography-disabled';
const SimplePlayer = props => {
const { playStatus } = props;
const activeIndex =
playStatus === Sound.status.PLAYING || playStatus === Sound.status.PAUSED
? props.activeIndex
: undefined;
const classes = classNames({
'ai-wrap': true,
'ai-type-simple': true,
'ai-with-typography': !typographyDisabled(),
});
return (
<div className={classes} style={{ maxWidth: props.maxWidth }}>
<div className="ai-tracklist ai-tracklist-open">
<Tracklist
tracks={props.tracks}
playStatus={props.playStatus}
activeTrackIndex={activeIndex}
onTrackClick={props.togglePlay}
setPosition={props.setPosition}
duration={props.duration}
position={props.position}
playbackRate={props.playbackRate}
className="ai-tracklist"
trackClassName="ai-track"
reverseTrackOrder={props.reverseTrackOrder}
displayTrackNo={props.displayTrackNo}
displayBuyButtons={props.displayBuyButtons}
buyButtonsTarget={props.buyButtonsTarget}
displayArtistNames={props.displayArtistNames}
standaloneTracks
onTrackLoop={props.allowTrackLoop ? props.setTrackCycling : undefined}
repeatingTrackIndex={props.repeatingTrackIndex}
setPlaybackRate={props.setPlaybackRate}
allowPlaybackRate={props.allowPlaybackRate}
buffering={props.buffering}
/>
</div>
{props.displayCredits && (
<div className="ai-footer">
<p>
Powered by{' '}
<a
href="https://www.cssigniter.com/plugins/audioigniter?utm_source=player&utm_medium=link&utm_content=audioigniter&utm_campaign=footer-link"
target="_blank"
rel="noopener noreferrer"
>
AudioIgniter
</a>
</p>
</div>
)}
</div>
);
};
SimplePlayer.propTypes = {
tracks: PropTypes.arrayOf(PropTypes.object),
playStatus: PropTypes.oneOf([
Sound.status.PLAYING,
Sound.status.PAUSED,
Sound.status.STOPPED,
]),
activeIndex: PropTypes.number,
position: PropTypes.number,
duration: PropTypes.number,
setPosition: PropTypes.func.isRequired,
togglePlay: PropTypes.func.isRequired,
setTrackCycling: PropTypes.func.isRequired,
allowTrackLoop: PropTypes.bool,
maxWidth: PropTypes.string,
reverseTrackOrder: PropTypes.bool,
displayTrackNo: PropTypes.bool,
buyButtonsTarget: PropTypes.bool,
displayArtistNames: PropTypes.bool,
displayBuyButtons: PropTypes.bool,
displayCredits: PropTypes.bool,
repeatingTrackIndex: PropTypes.number,
playbackRate: PropTypes.number,
setPlaybackRate: PropTypes.func,
allowPlaybackRate: PropTypes.bool,
buffering: PropTypes.bool,
};
export default soundProvider(SimplePlayer, {
onFinishedPlaying(props) {
const {
repeatingTrackIndex,
cycleTracks,
nextTrack,
activeIndex,
playTrack,
trackQueue,
} = props;
if (repeatingTrackIndex != null) {
playTrack(repeatingTrackIndex);
return;
}
if (cycleTracks) {
nextTrack();
return;
}
// Check if not the last track
if (activeIndex !== trackQueue[trackQueue.length - 1]) {
nextTrack();
}
},
});

View File

@ -0,0 +1,35 @@
import React from 'react';
import PropTypes from 'prop-types';
const Button = ({
className,
onClick,
children,
ariaLabel,
ariaPressed,
ariaExpanded,
ariaControls,
}) => (
<button
className={className}
onClick={onClick}
aria-label={ariaLabel}
aria-pressed={ariaPressed}
aria-expanded={ariaExpanded}
aria-controls={ariaControls}
>
{children}
</button>
);
Button.propTypes = {
className: PropTypes.string,
onClick: PropTypes.func,
children: PropTypes.node,
ariaLabel: PropTypes.string,
ariaPressed: PropTypes.bool,
ariaExpanded: PropTypes.bool,
ariaControls: PropTypes.string,
};
export default Button;

View File

@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { MusicNoteIcon } from './Icons';
const Cover = ({ className, title, src, onClick }) => (
<div
className={className + (src ? '' : ' ai-track-no-thumb')}
onClick={onClick}
>
{src ? <img src={src} alt={title || ''} /> : <MusicNoteIcon />}
</div>
);
Cover.propTypes = {
className: PropTypes.string,
title: PropTypes.string,
src: PropTypes.string,
onClick: PropTypes.func,
};
export default Cover;

View File

@ -0,0 +1,105 @@
import React from 'react';
export const PlayIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 24">
<path d="M18 12c0 .712-.37 1.355-.99 1.72L3.159 23.625C2.757 23.889 2.382 24 2 24c-1.103 0-2-.897-2-2V2C0 .897.897 0 2 0c.385 0 .76.111 1.085.323l13.962 9.981c.583.34.953.983.953 1.695z" />
</svg>
);
};
export const PauseIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M9 2v20c0 1.103-.897 2-2 2H2c-1.103 0-2-.897-2-2V2C0 .897.897 0 2 0h5c1.103 0 2 .897 2 2zm13-2h-5c-1.103 0-2 .897-2 2v20c0 1.103.897 2 2 2h5c1.103 0 2-.897 2-2V2c0-1.103-.897-2-2-2z" />
</svg>
);
};
export const NextIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M24 1.999v19.989c0 1.102-.897 1.999-2 1.999h-5c-1.103 0-2-.897-2-1.999v-6.837L3.16 23.612C1.597 24.635 0 23.472 0 21.988V1.999C0 .897.897 0 2 0c.384 0 .76.111 1.085.322L15 8.837V1.999C15 .897 15.897 0 17 0h5c1.103 0 2 .897 2 1.999z" />
</svg>
);
};
export const PreviousIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M24 2.014v19.987C24 23.103 23.103 24 22 24c-.385 0-.76-.111-1.085-.323L9 15.164v6.838c0 1.102-.897 1.999-2 1.999H2c-1.103 0-2-.897-2-1.999V2.015C0 .913.897.016 2 .016h5c1.103 0 2 .897 2 1.999v6.837L20.841.391C22.41-.636 24 .533 24 2.016z" />
</svg>
);
};
export const PlaylistIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M.871 5h10.758c.488 0 .871-.439.871-1s-.383-1-.871-1H.871C.383 3 0 3.439 0 4s.383 1 .871 1zM.871 10.25h10.758c.488 0 .871-.439.871-1s-.383-1-.871-1H.871c-.488 0-.871.439-.871 1s.383 1 .871 1zM23.595 3.129l-.002-.001c-.254-.156-.574-.17-.833-.036l-7.449 3.756c-.291.148-.472.442-.472.77v8.259c-.5-.234-1.055-.356-1.626-.356-1.841 0-3.339 1.229-3.339 2.74s1.498 2.74 3.339 2.74 3.338-1.229 3.338-2.74V8.15l5.736-2.893v8.116c-.5-.233-1.056-.355-1.627-.355-1.841 0-3.338 1.229-3.338 2.739s1.497 2.74 3.338 2.74 3.339-1.229 3.339-2.74V3.862c0-.3-.151-.574-.405-.733zM8.129 13.5H.871c-.488 0-.871.439-.871 1s.383 1 .871 1h7.258c.488 0 .871-.439.871-1s-.383-1-.871-1z" />
</svg>
);
};
export const VolumeUpIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M24 11v2c0 1.103-.897 2-2 2h-7v7c0 1.103-.897 2-2 2h-2c-1.103 0-2-.897-2-2v-7H2c-1.103 0-2-.897-2-2v-2c0-1.103.897-2 2-2h7V2c0-1.103.897-2 2-2h2c1.103 0 2 .897 2 2v7h7c1.103 0 2 .897 2 2z" />
</svg>
);
};
export const VolumeDownIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 24">
<path d="M24 11v2c0 1.103-.897 2-2 2H2c-1.103 0-2-.897-2-2v-2c0-1.103.897-2 2-2h20c1.103 0 2 .897 2 2z" />
</svg>
);
};
export const MusicNoteIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 24">
<path d="M18 2v16c0 1.654-1.794 3-4 3s-4-1.346-4-3 1.794-3 4-3V4.5L8 6.374V21c0 1.654-1.794 3-4 3s-4-1.346-4-3 1.794-3 4-3V5c0-.966.691-1.793 1.645-1.966L15.238.157c.204-.097.481-.157.763-.157 1.103 0 2 .897 2 2z" />
</svg>
);
};
export const CartIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M8.707 15h9.898c1.042 0 1.985-.657 2.346-1.636l2.94-7.979c.072-.196.109-.402.109-.616 0-.976-.794-1.77-1.77-1.77H5.734l-.339-1.188C5.09.744 4.101-.001 2.991-.001H.5c-.276 0-.5.224-.5.5s.224.5.5.5h2.491c.666 0 1.259.447 1.442 1.088l3.505 12.267-2.379 2.379c-.361.36-.56.841-.56 1.356 0 1.054.857 1.91 1.91 1.91h15.59c.276 0 .5-.224.5-.5s-.224-.5-.5-.5H6.909c-.502 0-.91-.408-.91-.916 0-.243.095-.472.267-.644l2.44-2.44zM18 12h-7.5c-.276 0-.5-.224-.5-.5s.224-.5.5-.5H18c.276 0 .5.224.5.5s-.224.5-.5.5zm.5-2.5H10c-.276 0-.5-.224-.5-.5s.224-.5.5-.5h8.5c.276 0 .5.224.5.5s-.224.5-.5.5zM9.5 6H20c.276 0 .5.224.5.5s-.224.5-.5.5H9.5c-.276 0-.5-.224-.5-.5s.224-.5.5-.5zM21 20c1.103 0 2 .897 2 2s-.897 2-2 2-2-.897-2-2 .897-2 2-2zM8 20c1.103 0 2 .897 2 2s-.897 2-2 2-2-.897-2-2 .897-2 2-2z" />
</svg>
);
};
export const RefreshIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M24 12c0 2.756-2.243 4.999-5 4.999-.004 0-.02.001-.047.001-.295 0-1.919-.082-3.953-1.398v.397c0 .553-.447 1-1 1s-1-.447-1-1v-2.5c0-.553.447-1 1-1h2.5c.553 0 1 .447 1 1 0 .403-.241.745-.584.903 1.193.589 2.011.604 2.055.597 1.683 0 3.028-1.345 3.028-3s-1.346-3-3-3c-2.151 0-4.213 1.832-6.396 3.772-2.338 2.078-4.756 4.227-7.604 4.227-2.757 0-5-2.243-5-4.999S2.242 7 4.999 7c.046-.002 1.777-.044 4 1.394V8c0-.553.447-1 1-1s1 .447 1 1v2.5c0 .553-.447 1-1 1h-2.5c-.553 0-1-.447-1-1 0-.403.241-.746.585-.904-1.186-.587-1.997-.6-2.056-.596C3.345 9 2 10.346 2 12s1.346 3 3 3c2.089 0 4.122-1.807 6.275-3.722C13.641 9.176 16.087 7.001 19 7.001c2.757 0 5 2.243 5 4.999z" />
</svg>
);
};
export const DownloadIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M24 15c0 2.757-2.243 5-5 5h-.183c-.177 0-.333-.092-.422-.23-.05-.078-.078-.17-.078-.269 0-.078.018-.153.05-.219.419-.882.632-1.819.632-2.782 0-3.584-2.916-6.5-6.5-6.5s-6.5 2.916-6.5 6.5c0 .923.196 1.823.583 2.676.074.087.119.2.119.324 0 .276-.224.5-.5.5-.005.001-.013 0-.02 0h-.183c-3.309 0-6-2.691-6-6 0-2.158 1.143-4.121 3.003-5.193C3.104 5.036 6.203 2 9.998 2c2.759 0 5.205 1.58 6.35 4.062.227-.042.439-.063.65-.063 2.206 0 4 1.794 4 4 0 .142-.008.283-.024.428 1.825.785 3.024 2.572 3.024 4.572zm-6 1.5c0 3.032-2.468 5.5-5.5 5.5S7 19.532 7 16.5 9.468 11 12.5 11s5.5 2.468 5.5 5.5zm-3.146.646c-.195-.195-.512-.195-.707 0l-1.146 1.146v-4.793c0-.276-.224-.5-.5-.5s-.5.224-.5.5v4.793l-1.146-1.146c-.195-.195-.512-.195-.707 0s-.195.512 0 .707l2 2c.046.046.1.083.161.108.059.025.124.038.192.038.065 0 .129-.013.19-.038h.002c.002-.001.003-.003.005-.004.057-.024.111-.058.157-.105l2-2c.195-.195.195-.512 0-.707z" />
</svg>
);
};
export const LyricsIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 4.5C0 3.673.673 3 1.5 3h21c.827 0 1.5.673 1.5 1.5S23.327 6 22.5 6h-21C.673 6 0 5.327 0 4.5zM1.5 11h15c.827 0 1.5-.673 1.5-1.5S17.327 8 16.5 8h-15C.673 8 0 8.673 0 9.5S.673 11 1.5 11zm15 7h-15c-.827 0-1.5.673-1.5 1.5S.673 21 1.5 21h15c.827 0 1.5-.673 1.5-1.5s-.673-1.5-1.5-1.5zm6-5h-21c-.827 0-1.5.673-1.5 1.5S.673 16 1.5 16h21c.827 0 1.5-.673 1.5-1.5s-.673-1.5-1.5-1.5z" />
</svg>
);
};
export const ShuffleIcon = () => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M23.927 16.827c.098.23.098.504-.004.743-.044.111-.119.223-.212.314l-2.876 2.833c-.184.182-.428.282-.688.282s-.506-.101-.69-.283c-.187-.183-.289-.428-.289-.689s.103-.506.29-.69l1.188-1.171h-.86c-1.881 0-3.649-.722-4.979-2.034l-2.14-2.107c-.187-.185-.289-.43-.289-.69 0-.176.062-.336.149-.484l-2.372 2.337c-1.329 1.312-3.098 2.034-4.979 2.034H.98c-.54 0-.979-.436-.979-.972s.438-.972.979-.972h4.196c1.36 0 2.639-.522 3.599-1.469l2.354-2.319c-.148.086-.308.146-.484.146-.26 0-.505-.1-.689-.282l-1.179-1.163c-.962-.947-2.24-1.469-3.601-1.469H.98c-.54 0-.979-.436-.979-.972s.438-.972.979-.972h4.196c1.88 0 3.648.722 4.979 2.033l1.179 1.163c.188.184.29.429.29.69 0 .177-.063.339-.152.487l3.333-3.284c1.33-1.312 3.099-2.034 4.979-2.034h.86l-1.188-1.171c-.188-.184-.29-.429-.29-.69s.103-.506.29-.69c.379-.375.998-.375 1.379.001l2.874 2.833c.096.094.168.202.217.323.098.231.098.505-.004.743-.044.111-.116.219-.21.312l-2.878 2.835c-.363.363-1.013.365-1.38-.001-.186-.182-.288-.428-.288-.689s.104-.506.29-.69l1.188-1.17h-.86c-1.36 0-2.639.521-3.601 1.469l-3.313 3.265c.374-.215.855-.181 1.174.134l2.139 2.108c.963.947 2.241 1.469 3.602 1.469h.86l-1.188-1.171c-.188-.184-.29-.429-.29-.69s.104-.506.29-.69c.379-.374.998-.375 1.379.001l2.877 2.834c.094.094.166.202.214.321z" />
</svg>
);
};

View File

@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class ProgressBar extends React.Component {
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
const { duration, setPosition } = this.props;
if (setPosition == null) {
return;
}
const offsetX =
event.pageX - event.currentTarget.getBoundingClientRect().left;
const posX = offsetX / event.currentTarget.offsetWidth;
setPosition(posX * duration);
}
render() {
const { position, duration } = this.props;
return (
<span onClick={this.handleClick} className="ai-track-progress-bar">
<span
className="ai-track-progress"
style={{ width: `${(position * 100) / duration}%` }}
/>
</span>
);
}
}
ProgressBar.propTypes = {
setPosition: PropTypes.func,
position: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
};

View File

@ -0,0 +1,68 @@
import React from 'react';
import PropTypes from 'prop-types';
export default class Time extends React.Component {
constructor(props) {
super(props);
const { countdown } = this.props;
this.state = {
showRemaining: countdown || false,
};
this.handleClick = this.handleClick.bind(this);
}
/**
* Pretty prints time remaining/elapsed
*
* @param {number} position - Track position in milliseconds
* @param {number} duration - Track duration in milliseconds
* @returns {string} - Time pretty formatted
*/
formatTime(position, duration) {
const { showRemaining } = this.state;
const positionInSeconds = showRemaining
? (duration - position) / 1000
: position / 1000;
const hours = Math.floor(positionInSeconds / 3600);
let min = Math.floor((positionInSeconds % 3600) / 60);
let sec = Math.floor(positionInSeconds % 60);
let time = '00:00';
min = min >= 10 ? min : `0${min}`;
sec = sec >= 10 ? sec : `0${sec}`;
if (!isNaN(sec)) {
if (hours) {
time = `${hours}:${min}:${sec}`;
} else {
time = `${min}:${sec}`;
}
}
return showRemaining ? `-${time}` : time;
}
handleClick() {
const { showRemaining } = this.state;
this.setState({ showRemaining: !showRemaining });
}
render() {
const { position, duration } = this.props;
return (
<span className="ai-track-time" onClick={this.handleClick}>
{this.formatTime(position, duration)}
</span>
);
}
}
Time.propTypes = {
position: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
countdown: PropTypes.bool.isRequired,
};

View File

@ -0,0 +1,153 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import Sound from 'react-sound';
import { sprintf } from 'sprintf-js';
import classNames from 'classnames';
import TrackTitle from './TrackTitle';
import Cover from './Cover';
import TrackButtons from './TrackButtons';
import ProgressBar from './ProgressBar';
import { PlayIcon, PauseIcon } from './Icons';
import { AppContext } from '../../App';
const Track = ({
track,
index,
trackNo,
isActive,
playStatus,
duration,
position,
setPosition,
isStandalone,
buyButtonsTarget,
displayArtistNames,
displayCovers,
displayBuyButtons,
onTrackClick,
onTrackLoop,
className,
isLooping,
playbackRate,
setPlaybackRate,
allowPlaybackRate,
buffering,
}) => {
const { toggleLyricsModal } = useContext(AppContext);
const isPlaying = isActive && playStatus === Sound.status.PLAYING;
const hasProgressBar =
typeof position !== 'undefined' &&
typeof duration !== 'undefined' &&
isActive &&
isStandalone;
const classes = classNames({
[className]: !!className,
'ai-track-active': isActive,
'ai-track-loading': isActive && buffering,
});
return (
<li className={classes}>
{displayCovers && (
<Cover
className="ai-track-thumb"
src={track.cover}
alt={track.title}
onClick={() => onTrackClick(index)}
/>
)}
{isStandalone && (
<button
className={classNames({
'ai-track-btn ai-track-inline-play-btn': true,
'ai-is-loading': isActive && buffering,
})}
onClick={() => onTrackClick(index)}
aria-label={
isPlaying
? sprintf(aiStrings.pause_title, track.title)
: sprintf(aiStrings.play_title, track.title)
}
aria-pressed={isPlaying}
>
{isPlaying ? <PauseIcon /> : <PlayIcon />}
<span className="ai-track-spinner" />
</button>
)}
<div className="ai-track-control" onClick={() => onTrackClick(index)}>
<TrackTitle
className="ai-track-name"
track={track}
trackNo={trackNo}
displayArtistNames={displayArtistNames}
/>
</div>
<TrackButtons
buyButtonsTarget={buyButtonsTarget}
buyUrl={track.buyUrl}
downloadUrl={track.downloadUrl}
downloadFilename={track.downloadFilename}
onTrackLoop={onTrackLoop && (() => onTrackLoop(index))}
isLooping={isLooping}
displayBuyButtons={displayBuyButtons}
onOpenTrackLyrics={
track.lyrics && (() => toggleLyricsModal(true, track))
}
playbackRate={playbackRate}
setPlaybackRate={setPlaybackRate}
allowPlaybackRate={allowPlaybackRate}
isPlaying={isPlaying}
/>
{hasProgressBar && (
<ProgressBar
setPosition={setPosition}
duration={duration}
position={position}
/>
)}
</li>
);
};
Track.propTypes = {
track: PropTypes.shape({
audio: PropTypes.string,
buyUrl: PropTypes.string,
cover: PropTypes.string,
title: PropTypes.string,
subtitle: PropTypes.string,
lyrics: PropTypes.string,
downloadUrl: PropTypes.string,
}),
index: PropTypes.number.isRequired,
trackNo: PropTypes.number,
isActive: PropTypes.bool,
position: PropTypes.number,
duration: PropTypes.number,
setPosition: PropTypes.func,
playStatus: PropTypes.oneOf([
Sound.status.PLAYING,
Sound.status.PAUSED,
Sound.status.STOPPED,
]),
onTrackClick: PropTypes.func.isRequired,
onTrackLoop: PropTypes.func,
className: PropTypes.string.isRequired,
isStandalone: PropTypes.bool,
buyButtonsTarget: PropTypes.bool,
displayArtistNames: PropTypes.bool,
displayCovers: PropTypes.bool,
displayBuyButtons: PropTypes.bool,
isLooping: PropTypes.bool,
playbackRate: PropTypes.number,
setPlaybackRate: PropTypes.func,
allowPlaybackRate: PropTypes.bool,
buffering: PropTypes.bool,
};
export default Track;

View File

@ -0,0 +1,131 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { CartIcon, DownloadIcon, LyricsIcon, RefreshIcon } from './Icons';
const TrackButtons = ({
buyButtonsTarget,
buyUrl,
downloadUrl,
downloadFilename,
onTrackLoop,
isLooping,
displayBuyButtons,
onOpenTrackLyrics,
setPlaybackRate,
playbackRate,
allowPlaybackRate,
isPlaying,
}) => {
if (
buyUrl == null &&
downloadUrl == null &&
!onTrackLoop &&
!onOpenTrackLyrics
) {
return null;
}
return (
<div className="ai-track-control-buttons">
{buyUrl && displayBuyButtons && (
<a
href={buyUrl}
className="ai-track-btn"
rel={buyButtonsTarget ? 'noopener noreferrer' : undefined}
target={buyButtonsTarget ? '_blank' : '_self'}
role="button"
aria-label={aiStrings.buy_track}
title={aiStrings.buy_track}
>
<CartIcon />
</a>
)}
{downloadUrl && downloadFilename && displayBuyButtons && (
<a
href={downloadUrl}
download={downloadFilename}
className="ai-track-btn"
role="button"
aria-label={aiStrings.download_track}
title={aiStrings.download_track}
>
<DownloadIcon />
</a>
)}
{onOpenTrackLyrics && (
// eslint-disable-next-line
<a
href="#"
className="ai-track-btn"
role="button"
aria-label={aiStrings.open_track_lyrics}
title={aiStrings.open_track_lyrics}
onClick={event => {
event.preventDefault();
onOpenTrackLyrics();
}}
>
<LyricsIcon />
</a>
)}
{allowPlaybackRate && isPlaying && (
<a
href="#"
className="ai-track-btn ai-btn-playback-rate"
role="button"
aria-label={aiStrings.set_playback_rate}
title={aiStrings.set_playback_rate}
onClick={event => {
event.preventDefault();
setPlaybackRate();
}}
>
<Fragment>&times;{playbackRate}</Fragment>
</a>
)}
{onTrackLoop && (
// eslint-disable-next-line
<a
href="#"
className="ai-track-btn ai-track-btn-repeat"
role="button"
aria-label={aiStrings.toggle_track_repeat}
title={aiStrings.toggle_track_repeat}
onClick={event => {
event.preventDefault();
onTrackLoop();
}}
>
<span
style={{
opacity: isLooping ? 1 : 0.3,
}}
>
<RefreshIcon />
</span>
</a>
)}
</div>
);
};
TrackButtons.propTypes = {
buyButtonsTarget: PropTypes.bool,
buyUrl: PropTypes.string,
downloadUrl: PropTypes.string,
downloadFilename: PropTypes.string,
onTrackLoop: PropTypes.func,
isLooping: PropTypes.bool,
displayBuyButtons: PropTypes.bool,
onOpenTrackLyrics: PropTypes.func,
playbackRate: PropTypes.number,
setPlaybackRate: PropTypes.func,
allowPlaybackRate: PropTypes.bool,
isPlaying: PropTypes.bool,
};
export default TrackButtons;

View File

@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import Modal from 'react-modal';
if (document.querySelector('.audioigniter-root')) {
Modal.setAppElement('.audioigniter-root');
}
const TrackLyricsModal = ({ isOpen, closeModal, children }) => {
return (
<Modal
isOpen={isOpen}
closeModal={closeModal}
onRequestClose={closeModal}
overlayClassName="ai-modal-overlay"
className="ai-modal"
>
<div className="ai-modal-wrap">
<div className="ai-modal-header">
<button
className="ai-modal-dismiss"
type="button"
onClick={closeModal}
>
&times;
</button>
</div>
<div className="ai-modal-content">{children}</div>
</div>
</Modal>
);
};
const propTypes = {
isOpen: PropTypes.bool,
closeModal: PropTypes.func.isRequired,
children: PropTypes.any,
};
TrackLyricsModal.propTypes = propTypes;
export default TrackLyricsModal;

View File

@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
const TrackTitle = ({
className,
style,
track,
trackNo,
displayArtistNames,
}) => {
let trackTitle = track.title;
if (displayArtistNames && track.subtitle) {
trackTitle = `${track.title} - ${track.subtitle}`;
}
if (trackNo != null) {
trackTitle = `${trackNo}. ${trackTitle}`;
}
return (
<span className={className} style={style}>
{trackTitle}
</span>
);
};
TrackTitle.propTypes = {
track: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
trackNo: PropTypes.number,
style: PropTypes.object, // eslint-disable-line react/forbid-prop-types
className: PropTypes.string,
displayArtistNames: PropTypes.bool,
};
export default TrackTitle;

View File

@ -0,0 +1,77 @@
import React from 'react';
import PropTypes from 'prop-types';
import Sound from 'react-sound';
import Track from './Track';
const Tracklist = ({ ...props }) => {
const { tracks } = props;
return (
<ul className={props.className} aria-expanded="true">
{tracks &&
tracks.map((track, index) => {
const trackNo = props.reverseTrackOrder
? tracks.length - index
: index + 1;
const isLooping = index === props.repeatingTrackIndex;
return (
<Track
key={index}
track={track}
index={index}
trackNo={props.displayTrackNo ? trackNo : undefined}
playStatus={props.playStatus}
isActive={props.activeTrackIndex === index}
buyButtonsTarget={props.buyButtonsTarget}
displayArtistNames={props.displayArtistNames}
displayBuyButtons={props.displayBuyButtons}
displayCovers={props.displayCovers}
onTrackClick={props.onTrackClick}
onTrackLoop={props.onTrackLoop}
setPosition={props.setPosition}
duration={props.duration}
position={props.position}
className={props.trackClassName}
isStandalone={props.standaloneTracks}
isLooping={isLooping}
playbackRate={props.playbackRate}
setPlaybackRate={props.setPlaybackRate}
allowPlaybackRate={props.allowPlaybackRate}
buffering={props.buffering}
/>
);
})}
</ul>
);
};
Tracklist.propTypes = {
tracks: PropTypes.arrayOf(PropTypes.object).isRequired,
playStatus: PropTypes.oneOf([
Sound.status.PLAYING,
Sound.status.PAUSED,
Sound.status.STOPPED,
]),
activeTrackIndex: PropTypes.number,
position: PropTypes.number,
duration: PropTypes.number,
setPosition: PropTypes.func,
standaloneTracks: PropTypes.bool,
onTrackClick: PropTypes.func.isRequired,
onTrackLoop: PropTypes.func,
className: PropTypes.string,
trackClassName: PropTypes.string,
reverseTrackOrder: PropTypes.bool,
displayTrackNo: PropTypes.bool,
displayBuyButtons: PropTypes.bool,
buyButtonsTarget: PropTypes.bool,
displayCovers: PropTypes.bool,
displayArtistNames: PropTypes.bool,
playbackRate: PropTypes.number,
setPlaybackRate: PropTypes.func,
allowPlaybackRate: PropTypes.bool,
buffering: PropTypes.bool,
};
export default Tracklist;

View File

@ -0,0 +1,95 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Scrollbars } from 'react-custom-scrollbars';
import Tracklist from './Tracklist';
export default class TracklistWrap extends React.Component {
componentWillReceiveProps(nextProps) {
const { activeTrackIndex, limitTracklistHeight } = this.props;
if (
activeTrackIndex !== nextProps.activeTrackIndex &&
limitTracklistHeight
) {
this.scrollToTrack(nextProps.activeTrackIndex);
}
}
scrollToTrack(trackIndex) {
const { tracks } = this.props;
const trackHeight = this.scrollbarsRef.getScrollHeight() / tracks.length;
if (!this.isTrackVisible(trackIndex)) {
this.scrollbarsRef.scrollTop(trackHeight * trackIndex);
}
}
isTrackVisible(trackIndex) {
const { tracks } = this.props;
const trackHeight = this.scrollbarsRef.getScrollHeight() / tracks.length;
const trackPosition = trackHeight * trackIndex;
const scrollTop = this.scrollbarsRef.getScrollTop();
const scrollBottom = scrollTop + this.scrollbarsRef.getClientHeight();
return !(trackPosition < scrollTop || trackPosition > scrollBottom);
}
renderTracklist() {
return (
<Tracklist
tracks={this.props.tracks}
activeTrackIndex={this.props.activeTrackIndex}
onTrackClick={this.props.onTrackClick}
className={this.props.className}
trackClassName={this.props.trackClassName}
reverseTrackOrder={this.props.reverseTrackOrder}
displayTrackNo={this.props.displayTrackNo}
displayBuyButtons={this.props.displayBuyButtons}
buyButtonsTarget={this.props.buyButtonsTarget}
displayCovers={this.props.displayCovers}
displayArtistNames={this.props.displayArtistNames}
onTrackLoop={this.props.onTrackLoop}
repeatingTrackIndex={this.props.repeatingTrackIndex}
/>
);
}
render() {
const { isOpen, limitTracklistHeight, tracklistHeight } = this.props;
return (
<div id="tracklisting" style={{ display: isOpen ? 'block' : 'none' }}>
{limitTracklistHeight ? (
<Scrollbars
className="ai-scroll-wrap"
ref={ref => (this.scrollbarsRef = ref)} // eslint-disable-line no-return-assign
style={{ height: tracklistHeight }}
>
{this.renderTracklist()}
</Scrollbars>
) : (
this.renderTracklist()
)}
</div>
);
}
}
TracklistWrap.propTypes = {
tracks: PropTypes.arrayOf(PropTypes.object).isRequired,
activeTrackIndex: PropTypes.number.isRequired,
onTrackClick: PropTypes.func.isRequired,
isOpen: PropTypes.bool,
className: PropTypes.string,
trackClassName: PropTypes.string,
reverseTrackOrder: PropTypes.bool,
displayTrackNo: PropTypes.bool,
limitTracklistHeight: PropTypes.bool,
tracklistHeight: PropTypes.number,
displayBuyButtons: PropTypes.bool,
buyButtonsTarget: PropTypes.bool,
displayCovers: PropTypes.bool,
displayArtistNames: PropTypes.bool,
onTrackLoop: PropTypes.func,
repeatingTrackIndex: PropTypes.number,
};

View File

@ -0,0 +1,52 @@
import React from 'react';
import PropTypes from 'prop-types';
import Button from './Button';
import { VolumeUpIcon, VolumeDownIcon } from './Icons';
export default class VolumeControl extends React.Component {
renderVolumeBars() {
const { volume, setVolume } = this.props;
return Array(...Array(11)).map((bar, i) => (
<span
key={i} // eslint-disable-line react/no-array-index-key
className={`ai-volume-bar ${
i <= volume / 10 ? 'ai-volume-bar-active' : ''
}`}
onClick={() => setVolume(i * 10)}
/>
));
}
render() {
const { volume, setVolume } = this.props;
return (
<div className="ai-audio-volume-control">
<div className="ai-audio-volume-bars">{this.renderVolumeBars()}</div>
<div className="ai-audio-volume-control-btns">
<Button
className="ai-btn"
onClick={() => setVolume(volume >= 100 ? volume : volume + 10)}
aria-label={aiStrings.volume_up}
>
<VolumeUpIcon />
</Button>
<Button
className="ai-btn"
onClick={() => setVolume(volume <= 0 ? volume : volume - 10)}
aria-label={aiStrings.volume_down}
>
<VolumeDownIcon />
</Button>
</div>
</div>
);
}
}
VolumeControl.propTypes = {
volume: PropTypes.number.isRequired,
setVolume: PropTypes.func.isRequired,
};

View File

@ -0,0 +1,82 @@
import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import WaveSurfer from 'wavesurfer.js';
const propTypes = {
position: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
audio: PropTypes.string,
setPosition: PropTypes.func.isRequired,
};
const WaveformProgressBar = ({ audio, position, duration, setPosition }) => {
const waveFormDomRef = useRef(null);
const wavesurfer = useRef(null);
useEffect(() => {
if (waveFormDomRef.current && audio) {
wavesurfer.current = WaveSurfer.create({
container: waveFormDomRef.current,
mediaControls: false,
height: 40,
barWidth: 2,
barGap: 2,
barRadius: 3,
responsive: true,
cursorWidth: 0,
backgroundColor: 'transparent',
progressColor: '#f70f5d',
waveColor: '#fff',
xhr: {
mode: 'no-cors',
},
});
wavesurfer.current.load(audio);
wavesurfer.current.on('ready', () => {
console.log('wavesurfer loaded');
});
}
return () => {
if (wavesurfer.current) {
wavesurfer.current.destroy();
}
};
}, [audio, waveFormDomRef.current]);
useEffect(() => {
// Sync wavesurfer with current playing position
const progress = position / duration;
if (wavesurfer.current && !Number.isNaN(progress)) {
wavesurfer.current.seekTo(progress || 0);
}
}, [position]);
const handleClick = event => {
if (setPosition == null) {
return;
}
const offsetX =
event.pageX - event.currentTarget.getBoundingClientRect().left;
const posX = offsetX / event.currentTarget.offsetWidth;
setPosition(posX * duration);
};
if (!audio) {
return null;
}
return (
<div className="ai-waveform-bar" onClick={handleClick}>
<div className="ai-waveform" ref={waveFormDomRef} />
<div className="ai-waveform-progress" />
</div>
);
};
WaveformProgressBar.propTypes = propTypes;
export default WaveformProgressBar;

View File

@ -0,0 +1,418 @@
import React from 'react';
import PropTypes from 'prop-types';
import Sound from 'react-sound';
import SoundCloud from '../utils/soundcloud';
import multiSoundDisabled from '../utils/multi-sound-disabled';
import { getInitialTrackQueueAndIndex } from '../utils/getInitialTrackIndex';
const PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2, 3];
const soundProvider = (Player, events) => {
class EnhancedPlayer extends React.Component {
constructor(props) {
super(props);
const {
volume,
cycleTracks,
defaultShuffle,
shuffleEnabled,
} = this.props;
this.state = {
tracks: [],
activeIndex: 0, // Determine active track by index
// trackQueue: List of track indexes that represents the order of the playlist
// i.e. [0, 1, 2, 3, 4] will play the 1st, 2nd, 3rd, etc track.
// [5, 4, 3, 2, 1] will play the tracks reversed.
// [4, 2, 0, ...] will play the 5th track first, 3rd second, then the 1st, etc.
trackQueue: [],
playStatus: Sound.status.STOPPED,
position: 0,
duration: 0,
playbackRate: 1,
volume: volume == null ? 100 : volume,
cycleTracks,
repeatingTrackIndex: null,
isMultiSoundDisabled: multiSoundDisabled(),
buffering: false,
shuffle: shuffleEnabled && defaultShuffle,
};
this.playTrack = this.playTrack.bind(this);
this.pauseTrack = this.pauseTrack.bind(this);
this.togglePlay = this.togglePlay.bind(this);
this.nextTrack = this.nextTrack.bind(this);
this.prevTrack = this.prevTrack.bind(this);
this.setPosition = this.setPosition.bind(this);
this.setVolume = this.setVolume.bind(this);
this.skipPosition = this.skipPosition.bind(this);
this.setPlaybackRate = this.setPlaybackRate.bind(this);
this.toggleTracklistCycling = this.toggleTracklistCycling.bind(this);
this.toggleShuffle = this.toggleShuffle.bind(this);
this.setTrackCycling = this.setTrackCycling.bind(this);
this.reverseTracks = this.reverseTracks.bind(this);
this.getFinalProps = this.getFinalProps.bind(this);
this.onPlaying = this.onPlaying.bind(this);
this.onFinishedPlaying = this.onFinishedPlaying.bind(this);
}
componentDidMount() {
const {
tracksUrl,
soundcloudClientId,
reverseTrackOrder,
initialTrack,
} = this.props;
const { shuffle } = this.state;
const tracksPromised = fetch(tracksUrl).then(res => res.json());
if (!soundcloudClientId) {
tracksPromised.then(tracks => {
const { trackQueue, activeIndex } = getInitialTrackQueueAndIndex({
tracks,
initialTrack,
reverseTrackOrder,
shuffle,
});
this.setState(
{
tracks,
activeIndex,
trackQueue,
},
() => {
if (reverseTrackOrder) {
this.reverseTracks();
}
},
);
});
return;
}
const sc = new SoundCloud(soundcloudClientId);
const scTracks = tracksPromised
.then(tracks => sc.fetchSoundCloudStreams(tracks))
.catch(err => console.error(err)); // eslint-disable-line no-console
// Make sure if SoundCloud fetching fails
// we delegate and load our tracks anyway
const promiseArray = [tracksPromised, scTracks].map(p =>
p.catch(error => ({
status: 'error',
error,
})),
);
Promise.all(promiseArray).then(res => {
if (res[1].status === 'error') {
return this.setState({ tracks: res[0] });
}
const tracks = sc.mapStreamsToTracks(...res);
const { trackQueue, activeIndex } = getInitialTrackQueueAndIndex({
tracks,
initialTrack,
reverseTrackOrder,
shuffle,
});
return this.setState(
() => ({
tracks,
activeIndex,
trackQueue,
}),
() => {
if (reverseTrackOrder) {
this.reverseTracks();
}
},
);
});
}
// Events
onPlaying({ duration, position }) {
this.setState(
() => ({ duration, position }),
() => {
if (events && events.onPlaying) {
events.onPlaying(this.getFinalProps());
}
},
);
}
onFinishedPlaying() {
const { stopOnTrackFinish, delayBetweenTracks = 0 } = this.props;
const delayBetweenTracksMs = delayBetweenTracks * 1000;
this.setState(() => ({ playStatus: Sound.status.STOPPED }));
if (stopOnTrackFinish) {
return;
}
if (events && events.onFinishedPlaying) {
setTimeout(() => {
events.onFinishedPlaying(this.getFinalProps());
}, delayBetweenTracksMs);
}
}
getFinalProps() {
const { tracks, activeIndex } = this.state;
const currentTrack = tracks[activeIndex] || {};
return {
playTrack: this.playTrack,
pauseTrack: this.pauseTrack,
togglePlay: this.togglePlay,
nextTrack: this.nextTrack,
prevTrack: this.prevTrack,
setPosition: this.setPosition,
skipPosition: this.skipPosition,
setPlaybackRate: this.setPlaybackRate,
setVolume: this.setVolume,
toggleTracklistCycling: this.toggleTracklistCycling,
setTrackCycling: this.setTrackCycling,
toggleShuffle: this.toggleShuffle,
currentTrack,
...this.props,
...this.state,
};
}
setVolume(volume) {
this.setState(() => ({ volume }));
}
setPosition(position) {
this.setState(() => ({ position }));
}
setTrackCycling(index, event) {
if (event) {
event.preventDefault();
}
const { activeIndex, cycleTracks } = this.state;
if (cycleTracks && index != null) {
this.toggleTracklistCycling();
}
this.setState(
({ repeatingTrackIndex }) => ({
repeatingTrackIndex: repeatingTrackIndex === index ? null : index,
}),
() => {
if (index != null && activeIndex !== index) {
this.playTrack(index);
}
},
);
}
setPlaybackRate() {
this.setState(({ playbackRate }) => {
const currentIndex = PLAYBACK_RATES.findIndex(
rate => rate === playbackRate,
);
const nextIndex =
(PLAYBACK_RATES.length + (currentIndex + 1)) % PLAYBACK_RATES.length;
return {
playbackRate: PLAYBACK_RATES[nextIndex],
};
});
}
toggleShuffle() {
const { initialTrack, reverseTrackOrder } = this.props;
const { tracks } = this.state;
this.setState(
prev => ({
shuffle: !prev.shuffle,
}),
() => {
this.setState(() => {
const { trackQueue } = getInitialTrackQueueAndIndex({
tracks,
initialTrack,
reverseTrackOrder,
shuffle: this.state.shuffle,
});
return {
trackQueue,
};
});
if (this.state.shuffle) {
// Shuffle track queue
} else {
// Unshuffle track queue
}
},
);
}
skipPosition(direction = 1) {
const { position } = this.state;
const { skipAmount } = this.props;
const amount = parseInt(skipAmount, 10) * 1000;
this.setPosition(position + amount * direction);
}
playTrack(index, event) {
if (event) {
event.preventDefault();
}
const { repeatingTrackIndex, isMultiSoundDisabled } = this.state;
if (isMultiSoundDisabled) {
window.soundManager.pauseAll();
}
this.setState(() => ({
activeIndex: index,
position: 0,
playStatus: Sound.status.PLAYING,
}));
// Reset repating track index if the track is not the active one.
if (index !== repeatingTrackIndex && repeatingTrackIndex != null) {
this.setTrackCycling(null);
}
}
pauseTrack(event) {
if (event) {
event.preventDefault();
}
const { playStatus } = this.state;
if (playStatus === Sound.status.PLAYING) {
this.setState(() => ({ playStatus: Sound.status.PAUSED }));
}
}
togglePlay(index, event) {
if (event) {
event.preventDefault();
}
const { activeIndex } = this.state;
if (typeof index === 'number' && index !== activeIndex) {
this.playTrack(index);
return;
}
this.setState(({ playStatus, isMultiSoundDisabled }) => {
if (playStatus !== Sound.status.PLAYING && isMultiSoundDisabled) {
window.soundManager.pauseAll();
}
return {
playStatus:
playStatus === Sound.status.PLAYING
? Sound.status.PAUSED
: Sound.status.PLAYING,
};
});
}
nextTrack() {
const { trackQueue, activeIndex } = this.state;
const currentQueueIndex = trackQueue.indexOf(activeIndex);
const nextQueueIndex = (currentQueueIndex + 1) % trackQueue.length;
const nextTrackIndex = trackQueue[nextQueueIndex];
this.playTrack(nextTrackIndex);
}
prevTrack() {
const { trackQueue, activeIndex } = this.state;
const currentQueueIndex = trackQueue.indexOf(activeIndex);
const prevQueueIndex =
(currentQueueIndex + trackQueue.length - 1) % trackQueue.length;
const prevTrackIndex = trackQueue[prevQueueIndex];
this.playTrack(prevTrackIndex);
}
toggleTracklistCycling() {
const { repeatingTrackIndex } = this.state;
if (repeatingTrackIndex !== null) {
this.setTrackCycling(null);
}
this.setState(state => ({
cycleTracks: !state.cycleTracks,
}));
}
reverseTracks() {
this.setState(state => ({
tracks: state.tracks.slice().reverse(),
}));
}
render() {
const { tracks, playStatus, position, volume, playbackRate } = this.state;
const finalProps = this.getFinalProps();
return (
<div className="ai-audioigniter">
<Player {...finalProps} />
{tracks.length > 0 && (
<Sound
url={finalProps.currentTrack.audio}
playStatus={playStatus}
position={position}
volume={volume}
onPlaying={this.onPlaying}
onFinishedPlaying={this.onFinishedPlaying}
onPause={() => this.pauseTrack()}
playbackRate={playbackRate}
onBufferChange={buffering => {
this.setState({ buffering });
}}
/>
)}
</div>
);
}
}
EnhancedPlayer.propTypes = {
volume: PropTypes.number,
cycleTracks: PropTypes.bool,
tracksUrl: PropTypes.string,
soundcloudClientId: PropTypes.string,
reverseTrackOrder: PropTypes.bool,
skipAmount: PropTypes.number,
stopOnTrackFinish: PropTypes.bool,
delayBetweenTracks: PropTypes.number,
initialTrack: PropTypes.number,
shuffleEnabled: PropTypes.bool,
defaultShuffle: PropTypes.bool,
};
return EnhancedPlayer;
};
export default soundProvider;

View File

@ -0,0 +1,14 @@
/**
* Shifts an array to right / left by n positions.
*
* @param {Array} arr The array.
* @param {number} direction The direction - 0 for left 1 for right.
* @param {number} n Number of positions to shift by.
* @returns {any[]}
*/
const arrayShift = (arr, direction, n) => {
const times = n > arr.length ? n % arr.length : n;
return arr.concat(arr.splice(0, direction > 0 ? arr.length - times : times));
};
export default arrayShift;

View File

@ -0,0 +1,24 @@
/**
* Shuffles an array.
* Copied from https://github.com/sindresorhus/array-shuffle
*
* @param {Array} array The array to be shuffled.
* @returns {*[]|*}
*/
const arrayShuffle = array => {
if (!Array.isArray(array)) {
return array;
}
const clone = [...array];
// eslint-disable-next-line no-plusplus
for (let index = clone.length - 1; index > 0; index--) {
const newIndex = Math.floor(Math.random() * (index + 1));
[clone[index], clone[newIndex]] = [clone[newIndex], clone[index]];
}
return clone;
};
export default arrayShuffle;

View File

@ -0,0 +1,76 @@
import arrayShuffle from './array-shuffle';
import arrayShift from './array-shift';
/**
* Fetches the initial track index.
*
* @param {Object} options The options.
* @param {Array} options.tracks The tracks.
* @param {number} [options.initialTrack] The initial track index.
* @param {boolean} options.reverseTrackOrder Whether the track order is reversed.
* @returns {number}
*/
export const getInitialTrackIndex = ({
tracks = [],
initialTrack = 1,
reverseTrackOrder = false,
}) => {
// The user provides a 1-index value.
const initialTrackIndex = initialTrack - 1;
if (!tracks.length || !initialTrack || initialTrack > tracks.length) {
return 0;
}
if (reverseTrackOrder) {
return Math.max(tracks.length - initialTrack, 0);
}
return initialTrackIndex;
};
/**
* Fetches the initial track index and the initial track queue.
*
* @param {Object} options The options.
* @param {Array} options.tracks The tracks.
* @param {Number} options.initialTrack The initial track number (1-indexed).
* @param {Boolean} reverseTrackOrder Whether the track order is reversed.
* @param {Boolean} shuffle Whether the track queue is shuffled.
* @returns {{activeIndex: number, trackQueue: (*[]|*)}|{activeIndex: number, trackQueue: *}}
*/
export const getInitialTrackQueueAndIndex = ({
tracks = [],
initialTrack = 1,
reverseTrackOrder = false,
shuffle = false,
}) => {
const activeIndex = getInitialTrackIndex({
tracks,
initialTrack,
reverseTrackOrder,
});
const orderedTrackIndexes = tracks.map((_, index) => index);
if (!shuffle) {
const shiftAmount = orderedTrackIndexes.indexOf(activeIndex);
return {
activeIndex,
trackQueue: arrayShift(orderedTrackIndexes, 0, shiftAmount),
};
}
const shuffledQueue = arrayShuffle(orderedTrackIndexes);
// Always bring the initial track (activeIndex) to the front of the queue.
shuffledQueue.splice(shuffledQueue.indexOf(activeIndex), 1);
shuffledQueue.unshift(activeIndex);
return {
activeIndex,
trackQueue: shuffledQueue,
};
};
export default getInitialTrackIndex;

View File

@ -0,0 +1,8 @@
const multiSoundDisabled = () => {
return (
window.ai_pro_front_scripts &&
!!window.ai_pro_front_scripts.multi_sound_disabled
);
};
export default multiSoundDisabled;

View File

@ -0,0 +1,88 @@
export default class SoundCloud {
constructor(clientId) {
if (!clientId) {
throw new Error('SoundCloud client ID is required');
}
this.clientId = clientId;
this.baseUrl = 'https://api.soundcloud.com';
}
/**
* Checks if a URL is from SoundCloud
*
* @param {string} url - URL to be checked
*
* @returns {boolean}
*/
static isSoundCloudUrl(url) {
return url.indexOf('soundcloud.com') > -1;
}
/**
* Resolves a SoundCloud URL into a track object
*
* @param {string} url - URL to be resolved
*
* @returns {Promise.<*>}
*/
resolve(url) {
/*
* Tell the SoundCloud API not to serve a redirect. This is to get around
* CORS issues on Safari 7+, which likes to send pre-flight requests
* before following redirects, which has problems.
*
* https://github.com/soundcloud/soundcloud-javascript/issues/27
*/
const statusCodeMap = encodeURIComponent('_status_code_map[302]=200');
return fetch(
`${this.baseUrl}/resolve?url=${url}&client_id=${
this.clientId
}&${statusCodeMap}`,
)
.then(res => res.json())
.then(res => fetch(res.location))
.then(res => res.json());
}
/**
* Resolves and fetches SoundCloud track objects
*
* @param {Object[]} tracks - Tracks object
*
* @returns {Promise.<*>}
*/
fetchSoundCloudStreams(tracks) {
const scTracks = tracks
.filter(track => SoundCloud.isSoundCloudUrl(track.audio))
.map(track => this.resolve(track.audio));
return Promise.all(scTracks);
}
/**
* Maps a SoundCloud tracks object into an AudioIgniter one
* by replacing `track.audio` with `sctrack.stream_url`.
*
* Works *in order* of appearance in the `tracks` object.
*
* @param {Object[]} tracks - AudioIgniter tracks object
* @param {Object[]} scTracks - SoundCloud tracks object
*
* @returns {Object[]}
*/
mapStreamsToTracks(tracks, scTracks) {
let i = 0;
return tracks.map(track => {
if (SoundCloud.isSoundCloudUrl(track.audio)) {
// eslint-disable-next-line no-param-reassign
track.audio = `${scTracks[i].stream_url}?client_id=${this.clientId}`;
i++; // eslint-disable-line no-plusplus
}
return track;
});
}
}

View File

@ -0,0 +1,8 @@
const typographyDisabled = () => {
return (
window.ai_pro_front_scripts &&
!!window.ai_pro_front_scripts.typography_disabled
);
};
export default typographyDisabled;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,80 @@
const path = require('path');
const merge = require('webpack-merge');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const parts = require('./webpack.parts');
const TARGET = process.env.npm_lifecycle_event;
process.env.BABEL_ENV = TARGET;
const PATHS = {
app: path.join(__dirname, 'src'),
build: path.join(__dirname, 'build'),
style: path.join(__dirname, 'styles', 'style.scss'),
};
const common = {
entry: {
style: PATHS.style,
app: PATHS.app,
},
output: {
path: PATHS.build,
filename: '[name].js',
},
plugins: [
new HtmlWebpackPlugin({
title: 'AudioIgniter',
template: `${PATHS.app}/index.ejs`,
}),
],
devServer: {
contentBase: path.resolve('assets'),
},
resolve: {
extensions: ['.js', '.jsx'],
},
module: {
rules: [
{
test: /\.jsx?$/,
use: ['babel-loader?cacheDirectory'],
},
],
},
};
let config;
// Detect how npm is run and branch based on that
switch (TARGET) {
case 'build': {
config = merge(
common,
{
resolve: {
modules: [path.resolve(__dirname), 'node_modules'],
extensions: ['.js', '.jsx'],
},
},
parts.minify(),
parts.extractCSS(PATHS.style),
parts.setFreeVariable('process.env.NODE_ENV', 'production'),
);
break;
}
default: {
config = merge(
common,
{
devtool: 'eval-source-map',
},
parts.setupSass(PATHS.style),
parts.devServer({
host: process.env.HOST,
port: process.env.PORT,
}),
);
}
}
module.exports = config;

View File

@ -0,0 +1,107 @@
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const autoprefixer = require('autoprefixer');
exports.setupSass = paths => ({
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
include: paths,
},
],
},
});
exports.extractCSS = paths => ({
module: {
rules: [
{
test: /\.scss$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: {
minimize: true,
},
},
{
loader: 'postcss-loader',
options: {
plugins: () => [
autoprefixer({
browsers: [
'Chrome >= 46',
'Firefox ESR',
'Edge >= 12',
'Explorer >= 9',
'iOS >= 8',
'Safari >= 8',
'Android >= 4',
],
cascade: false,
}),
],
},
},
{
loader: 'sass-loader',
options: {
outputStyle: 'expanded',
},
},
],
}),
include: paths,
},
],
},
plugins: [
new ExtractTextPlugin({
filename: '[name].css',
}),
],
});
exports.devServer = options => ({
devServer: {
contentBase: __dirname,
historyApiFallback: true,
hot: false,
inline: true,
stats: 'errors-only',
host: options.host,
port: options.port,
overlay: {
warnings: true,
errors: true,
},
},
});
exports.minify = () => ({
plugins: [
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
drop_console: true,
screw_ie8: true,
},
output: {
comments: false,
},
}),
],
});
exports.setFreeVariable = (key, value) => {
const env = {};
env[key] = JSON.stringify(value);
return {
plugins: [new webpack.DefinePlugin(env)],
};
};

File diff suppressed because it is too large Load Diff