├── .babelrc ├── .editorconfig ├── .gitignore ├── README.md ├── build └── bundle.js ├── index.html ├── package-lock.json ├── package.json ├── screenshot.png ├── src ├── components │ ├── Slider.js │ └── widget.js └── index.js ├── test ├── browser │ └── index.js └── karma.conf.js ├── tools ├── build.cli.js ├── lint.cli.js ├── start.cli.js └── test.cli.js └── webpack.config.babel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "presets": [ 4 | ["es2015", { "loose":true }], 5 | "stage-0" 6 | ], 7 | "plugins": [ 8 | ["transform-decorators-legacy"], 9 | ["transform-react-jsx", { "pragma": "h" }] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Preact Audio Player 2 | 3 | > An audio player built with Preact Habitat 4 | 5 | ![Screenshot](screenshot.png) 6 | 7 | ## License 8 | 9 | MIT 10 | 11 | ### Icons 12 | 13 | - [Volume](https://thenounproject.com/term/volume/1050668) by Chinnaking from the Noun Project 14 | -------------------------------------------------------------------------------- /build/bundle.js: -------------------------------------------------------------------------------- 1 | !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var n=t();for(var o in n)("object"==typeof exports?exports:e)[o]=n[o]}}(this,function(){return function(e){function t(o){if(n[o])return n[o].exports;var r=n[o]={exports:{},id:o,loaded:!1};return e[o].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="/",t(0)}([function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{default:e}}function r(){var e=(0,a.default)(u.default);e.render()}var i=n(3),a=o(i),l=n(5),u=o(l);r()},function(e,t,n){var o,r,i,a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};!function(n,l){"object"==a(t)&&"undefined"!=typeof e?l(t):(r=[t],o=l,i="function"==typeof o?o.apply(t,r):o,!(void 0!==i&&(e.exports=i)))}(void 0,function(e){function t(e,t,n){this.nodeName=e,this.attributes=t,this.children=n,this.key=t&&t.key}function n(e,n){var o,r,i,a,l;for(l=arguments.length;l-- >2;)O.push(arguments[l]);for(n&&n.children&&(O.length||O.push(n.children),delete n.children);O.length;)if((i=O.pop())instanceof Array)for(l=i.length;l--;)O.push(i[l]);else null!=i&&i!==!0&&i!==!1&&("number"==typeof i&&(i=String(i)),a="string"==typeof i,a&&r?o[o.length-1]+=i:((o||(o=[])).push(i),r=a));var u=new t(e,n||void 0,o||z);return B.vnode&&B.vnode(u),u}function o(e,t){if(t)for(var n in t)e[n]=t[n];return e}function r(e){return o({},e)}function i(e,t){for(var n=t.split("."),o=0;o2?[].slice.call(arguments,2):e.children)}function d(e,t,n){var o=t.split(".");return function(t){for(var r=t&&t.target||this,a={},l=a,s=u(n)?i(t,n):r.nodeName?r.type.match(/^che|rad/)?r.checked:r.value:t,c=0;c=h?e.appendChild(u):u!==s[g]&&(u===s[g+1]&&_(s[g]),e.insertBefore(u,s[g]||null)))}if(f)for(var g in d)d[g]&&N(d[g]);for(;p<=m;)u=c[m--],u&&N(u)}function N(e,t){var n=e._component;if(n)F(n,!t);else{e[X]&&e[X].ref&&e[X].ref(null),t||w(e);for(var o;o=e.lastChild;)N(o,t)}}function T(e,t,n){var o;for(o in n)t&&o in t||null==n[o]||b(e,o,n[o],n[o]=void 0,ee);if(t)for(o in t)"children"===o||"innerHTML"===o||o in n&&t[o]===("value"===o||"checked"===o?e[o]:n[o])||b(e,o,n[o],n[o]=t[o],ee)}function L(e){var t=e.constructor.name,n=ne[t];n?n.push(e):ne[t]=[e]}function E(e,t,n){var o=new e(t,n),r=ne[e.name];if(R.call(o,t,n),r)for(var i=r.length;i--;)if(r[i].constructor===e){o.nextBase=r[i].nextBase,r.splice(i,1);break}return o}function D(e,t,n,o,r){e._disable||(e._disable=!0,(e.__ref=t.ref)&&delete t.ref,(e.__key=t.key)&&delete t.key,!e.base||r?e.componentWillMount&&e.componentWillMount():e.componentWillReceiveProps&&e.componentWillReceiveProps(t,o),o&&o!==e.context&&(e.prevContext||(e.prevContext=e.context),e.context=o),e.prevProps||(e.prevProps=e.props),e.props=t,e._disable=!1,0!==n&&(1!==n&&B.syncComponentUpdates===!1&&e.base?f(e):j(e,1,r)),e.__ref&&e.__ref(e))}function j(e,t,n,i){if(!e._disable){var a,u,s,c,d=e.props,f=e.state,p=e.context,v=e.prevProps||d,y=e.prevState||f,_=e.prevContext||p,b=e.base,P=e.nextBase,x=b||P,w=e._component;if(b&&(e.props=v,e.state=y,e.context=_,2!==t&&e.shouldComponentUpdate&&e.shouldComponentUpdate(d,f,p)===!1?a=!0:e.componentWillUpdate&&e.componentWillUpdate(d,f,p),e.props=d,e.state=f,e.context=p),e.prevProps=e.prevState=e.prevContext=e.nextBase=null,e._dirty=!1,!a){for(e.render&&(u=e.render(d,f,p)),e.getChildContext&&(p=o(r(p),e.getChildContext()));h(u);)u=m(u,p);var C,k,A=u&&u.nodeName;if(l(A)){var T=g(u);s=w,s&&s.constructor===A&&T.key==s.__key?D(s,T,1,p):(C=s,s=E(A,T,p),s.nextBase=s.nextBase||P,s._parentComponent=e,e._component=s,D(s,T,0,p),j(s,1,n,!0)),k=s.base}else c=x,C=w,C&&(c=e._component=null),(x||1===t)&&(c&&(c._component=null),k=M(c,u,p,n||!b,x&&x.parentNode,!0));if(x&&k!==x&&s!==w){var L=x.parentNode;L&&k!==L&&(L.replaceChild(k,x),C||(x._component=null,N(x)))}if(C&&F(C,k!==x),e.base=k,k&&!i){for(var U=e,R=e;R=R._parentComponent;)(U=R).base=k;k._component=U,k._componentConstructor=U.constructor}}!b||n?Z.unshift(e):a||(e.componentDidUpdate&&e.componentDidUpdate(v,y,_),B.afterUpdate&&B.afterUpdate(e));var W,O=e._renderCallbacks;if(O)for(;W=O.pop();)W.call(e);$||i||S()}}function U(e,t,n,o){for(var r=e&&e._component,i=r,a=e,l=r&&e._componentConstructor===t.nodeName,u=l,s=g(t);r&&!u&&(r=r._parentComponent);)u=r.constructor===t.nodeName;return r&&u&&(!o||r._component)?(D(r,s,3,n,o),e=r.base):(i&&!l&&(F(i,!0),e=a=null),r=E(t.nodeName,s,n),e&&!r.nextBase&&(r.nextBase=e,a=null),D(r,s,1,n,o),e=r.base,a&&e!==a&&(a._component=null,N(a))),e}function F(e,t){B.beforeUnmount&&B.beforeUnmount(e);var n=e.base;e._disable=!0,e.componentWillUnmount&&e.componentWillUnmount(),e.base=null;var o=e._component;if(o)F(o,t);else if(n){n[X]&&n[X].ref&&n[X].ref(null),e.nextBase=n,t&&(_(n),L(e));for(var r;r=n.lastChild;)N(r,!t)}e.__ref&&e.__ref(null),e.componentDidUnmount&&e.componentDidUnmount()}function R(e,t){this._dirty=!0,this.context=t,this.props=e,this.state||(this.state={})}function W(e,t,n){return M(n,e,{},!1,t)}var B={},O=[],z=[],V={},H=function(e){return V[e]||(V[e]=e.toLowerCase())},I="undefined"!=typeof Promise&&Promise.resolve(),G=I?function(e){I.then(e)}:setTimeout,q={},X="undefined"!=typeof Symbol?Symbol.for("preactattr"):"__preactattr_",J={boxFlex:1,boxFlexGroup:1,columnCount:1,fillOpacity:1,flex:1,flexGrow:1,flexPositive:1,flexShrink:1,flexNegative:1,fontWeight:1,lineClamp:1,lineHeight:1,opacity:1,order:1,orphans:1,strokeOpacity:1,widows:1,zIndex:1,zoom:1},K={blur:1,error:1,focus:1,load:1,resize:1,scroll:1},Q=[],Y={},Z=[],$=0,ee=!1,te=!1,ne={};o(R.prototype,{linkState:function(e,t){var n=this._linkedStates||(this._linkedStates={});return n[e+t]||(n[e+t]=d(this,e,t))},setState:function(e,t){var n=this.state;this.prevState||(this.prevState=r(n)),o(n,l(e)?e(n,this.props):e),t&&(this._renderCallbacks=this._renderCallbacks||[]).push(t),f(this)},forceUpdate:function(){j(this,2)},render:function(){}}),e.h=n,e.cloneElement=c,e.Component=R,e.render=W,e.rerender=p,e.options=B})},function(e,t){"use strict";function n(e,t,n){if(e+="",t-=e.length,t<=0)return e;if(n||0===n||(n=" "),n+=""," "===n&&t<10)return o[t]+e;for(var r="";;){if(1&t&&(r+=n),t>>=1,!t)break;n+=n}return r+e}e.exports=n;var o=[""," "," "," "," "," "," "," "," "," "]},function(e,t,n){var o,r,i,a="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};!function(l,u){"object"===a(t)&&"undefined"!=typeof e?e.exports=u(n(1)):(r=[n(1)],o=u,i="function"==typeof o?o.apply(t,r):o,!(void 0!==i&&(e.exports=i)))}(void 0,function(e){"use strict";e="default"in e?e.default:e;var t=function(e){return e.replace(/-([a-z])/gi,function(e,t){return t.toUpperCase()})},n=function(){return document.currentScript||function(){var e=document.getElementsByTagName("script");return e[e.length-1]}()},o=function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"data-prop",o=e.attributes,r={};return Object.keys(o).forEach(function(e){if(o.hasOwnProperty(e)){var i=o[e].name,a=i.split(n+"-").pop();if(a=t(a),i!==a){var l=o[e].nodeValue;r[a]=l}}}),r},r=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.name,o=void 0===t?"data-widget":t,r=e.value,i=void 0===r?null:r,a=e.inline,l=void 0===a||a,u=[],s=n();if(!i){var c=s.attributes;Object.keys(c).forEach(function(e){if(c.hasOwnProperty(e)){var t=c[e].name;"data-mount"===t&&(i=c[e].nodeValue)}})}return!i&&l?[].concat(s.parentNode):([].forEach.call(document.querySelectorAll("["+o+"]"),function(e){i===e.getAttribute(o)&&u.push(e)}),u)},i=function(){return!document.attachEvent&&"interactive"===document.readyState||"complete"===document.readyState},a=function(t,n,r){n.forEach(function(n){var i=n,a=o(n)||{};return e.render(e.h(t,a),i,r)})},l=function(e){var t=e,n=!1,o=null,l=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},l=e.name,u=void 0===l?"data-widget":l,s=e.value,c=void 0===s?null:s,d=e.inline,f=void 0===d||d,p=i();if(p&&!n){var h=r({name:u,value:c,inline:f});return n=!0,a(t,h,o)}document.onreadystatechange=function(){var e=r({name:u,value:c,inline:f});if(p&&!n)return n=!0,a(t,e,o)}};return{render:l}};return l})},function(e,t,n){"use strict";function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function r(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function i(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}t.__esModule=!0;var a=n(1),l="#3FB3D2",u="#00A0AD",s="#FFFFFF",c=function(e){function t(){var n,i,a;o(this,t);for(var l=arguments.length,u=Array(l),s=0;s1?1:t;i.props.onChange(n)}},i.handleMouseUp=function(){i.setState({isDragging:!1})},i.handleMouseDown=function(e){e.preventDefault(),i.setState({isDragging:!0,leftEdge:e.x-e.offsetX,sliderWidth:e.target.clientWidth}),i.handleMouseMove(e)},a=n,r(i,a)}return i(t,e),t.prototype.componentDidMount=function(){document.addEventListener("mousemove",this.handleMouseMove.bind(this)),document.addEventListener("mouseup",this.handleMouseUp.bind(this))},t.prototype.render=function(){var e=this.props,t=e.value,n=e.width,o=(e.onChange,{PreactAudioPlayer__Slider:{position:"relative",height:20,width:n,backgroundColor:s,borderRadius:15,cursor:"pointer"},PreactAudioPlayer__SliderHandle:{position:"relative",height:20,width:20,top:-20,left:t*n-20*t,backgroundColor:l,borderRadius:"50%",pointerEvents:"none"},PreactAudioPlayer__SliderFill:{height:20,width:t*n-20*t+20,backgroundColor:u,borderRadius:15,pointerEvents:"none"}});return(0,a.h)("div",{style:o.PreactAudioPlayer__Slider,onMouseDown:this.handleMouseDown},(0,a.h)("div",{style:o.PreactAudioPlayer__SliderFill}),(0,a.h)("div",{style:o.PreactAudioPlayer__SliderHandle}))},t}(a.Component);t.default=c},function(e,t,n){"use strict";function o(e){return e&&e.__esModule?e:{default:e}}function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function a(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}t.__esModule=!0;var l,u,s=n(1),c=n(2),d=o(c),f=n(4),p=o(f),h="#3FB3D2",m="#1A83A1",v="#FFFFFF",y=function(e){var t=[1,1.25,1.5,2],n=t.indexOf(e);return t[n===t.length-1?0:n+1]},g=function(e){return(0,d.default)(Math.floor(e/60),2,"0")+":"+(0,d.default)(e%60,2,"0")},_=function(){return(0,s.h)("svg",{width:"20",height:"20",viewBox:"0 0 100 100"},(0,s.h)("path",{d:"M10.148,33.29v33.42h23.314l21.111,16.446V16.844L33.463,33.29H10.148z M74.477,50c0-8.232-5.002-15.315-12.125-18.379 v36.758C69.475,65.315,74.477,58.232,74.477,50z M62.352,15.52v7.226c11.826,3.423,20.5,14.341,20.5,27.255 s-8.674,23.832-20.5,27.255v7.226c15.727-3.591,27.5-17.682,27.5-34.48S78.078,19.11,62.352,15.52z",fill:v}))},b=function(){return(0,s.h)("svg",{width:"20",height:"20",viewBox:"0 0 100 100"},(0,s.h)("path",{d:"M10.148,33.29v33.42h23.314l21.111,16.446V16.844L33.463,33.29H10.148z",fill:v}))},P=function(){return(0,s.h)("svg",{width:"40",height:"40",viewBox:"-5 0 25 20"},(0,s.h)("polygon",{points:"0,0 0,20 20,10",fill:"white"}))},x=function(){return(0,s.h)("svg",{width:"40",height:"40",viewBox:"-5 0 25 20"},(0,s.h)("path",{d:"M0,0 L0,20 L5,20 L5,0 L0,0 M10,0 L10,20 L15,20 L15,0, L10,0",fill:"white"}))},w=20,C=100,S=5,M=40,k=20,A=20,N=(u=l=function(e){function t(){var n,o,a;r(this,t);for(var l=arguments.length,u=Array(l),s=0;s99,showMute:n>34})},o.componentDidMount=function(){o.audio=document.getElementById("PreactAudioPlayer"),window.addEventListener("resize",o.updateDimensions),o.updateDimensions()},o.componentWillUnmount=function(){window.removeEventListener("resize",o.updateDimensions)},o.handlePlayClick=function(){o.setState({isPlaying:!o.state.isPlaying}),o.state.isPlaying?(o.audio.play(),o.audio.addEventListener("timeupdate",function(e){o.setState({currentTime:Math.floor(o.audio.currentTime),duration:Math.floor(o.audio.duration)})},!1)):o.audio.pause()},o.handleVolumeChange=function(e){o.setState({volume:e,isMuted:0===e}),o.audio.volume=e},o.handleTimeChange=function(e){o.setState({currentTime:Math.floor(o.state.duration*e)}),o.audio.currentTime=o.state.currentTime},o.handleMuteClick=function(){o.setState({isMuted:!o.state.isMuted,volume:0===o.state.volume?.5:o.state.volume}),o.audio.volume=o.state.isMuted?0:o.state.volume},o.handlePlaybackRate=function(){o.setState({rate:y(o.state.rate)}),o.audio.playbackRate=o.state.rate},a=n,i(o,a)}return a(t,e),t.prototype.render=function(){var e=this,t=this.props.url,n=this.state,o=n.isMuted,r=n.isPlaying,i=n.volume,a=n.currentTime,l=n.duration,u=n.rate,c={PreactAudioPlayer:{display:"flex",justifyContent:"space-between",alignItems:"center",boxSizing:"border-box",height:50,margin:"40px 0",backgroundColor:m,fontFamily:"sans-serif"},PreactAudioPlayer__Play:{display:"flex",flex:"none",justifyContent:"center",alignItems:"center",boxSizing:"border-box",width:C,height:C,marginLeft:w,backgroundColor:h,borderRadius:"50%",border:S+"px solid "+m,cursor:"pointer"},PreactAudioPlayer__Time:{display:"flex",color:v},PreactAudioPlayer__TimeLeft:{marginLeft:w,width:M},PreactAudioPlayer__TimeSlider:{marginLeft:w},PreactAudioPlayer__TimeRight:{width:M,marginLeft:w},PreactAudioPlayer__Rate:{width:k,marginLeft:w,color:v,cursor:"pointer"},PreactAudioPlayer__Volume:{display:"flex",marginRight:w},PreactAudioPlayer__Mute:{width:A,marginLeft:w,border:"none",color:v,background:"none",cursor:"pointer"},PreactAudioPlayer__VolumeSlider:{marginLeft:w}};return(0,s.h)("div",{style:c.PreactAudioPlayer,ref:function(t){return e.component=t}},(0,s.h)("audio",{id:"PreactAudioPlayer"},(0,s.h)("source",{src:t,type:"audio/mp3"})),(0,s.h)("button",{type:"button",title:"Play/Pause","aria-label":"Play/Pause",style:c.PreactAudioPlayer__Play,onClick:this.handlePlayClick},r?(0,s.h)(x,null):(0,s.h)(P,null)),(0,s.h)("div",{style:c.PreactAudioPlayer__Time},(0,s.h)("div",{style:c.PreactAudioPlayer__TimeLeft},g(a)),(0,s.h)("div",{style:c.PreactAudioPlayer__TimeSlider},(0,s.h)(p.default,{value:a/l||0,width:this.state.timeSliderWidth,onChange:this.handleTimeChange})),this.state.showRightTime?(0,s.h)("div",{style:c.PreactAudioPlayer__TimeRight},g(l)):null),(0,s.h)("div",{style:c.PreactAudioPlayer__Rate,onClick:this.handlePlaybackRate},u,"x"),(0,s.h)("div",{style:c.PreactAudioPlayer__Volume},this.state.showMute?(0,s.h)("button",{type:"button",title:"Mute Toggle","aria-label":"Mute Toggle",style:c.PreactAudioPlayer__Mute,onClick:this.handleMuteClick},o?(0,s.h)(b,null):(0,s.h)(_,null)):null,(0,s.h)("div",{style:c.PreactAudioPlayer__VolumeSlider},(0,s.h)(p.default,{value:o?0:i,width:this.state.volumeSliderWidth,onChange:this.handleVolumeChange}))))},t}(s.Component),l.defaultProps={url:null},u);t.default=N}])}); -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Preact Audio Player 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

Preact Audio Player

18 |
19 | Star 20 |
21 |

22 | preact-audio-player plays audio. 23 |

24 |

Widget 1

25 | 26 |
27 | 28 |

Built with 💛 and Preact by Adam Garrett-Harris

29 |
30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "preact-audio-player", 3 | "version": "0.0.0", 4 | "description": "Audio player made with Preact and Preact Habitat", 5 | "scripts": { 6 | "dev": "cross-env NODE_ENV=development webpack-dev-server --inline --hot --progress", 7 | "start": "serve build -s -c 1 && node ./tools/start.cli.js", 8 | "prestart": "npm run build", 9 | "build": "cross-env webpack -p --progress && node ./tools/build.cli.js", 10 | "prebuild": "mkdirp build", 11 | "test": "npm run lint && npm run -s test:karma && node ./tools/test.cli.js", 12 | "test:karma": "karma start test/karma.conf.js --single-run", 13 | "lint": "eslint ./src/ || true && node ./tools/lint.cli.js", 14 | "deploy": "git subtree push --prefix build origin gh-pages" 15 | }, 16 | "keywords": [ 17 | "preact", 18 | "audio-player" 19 | ], 20 | "license": "MIT", 21 | "author": "Adam Harris", 22 | "devDependencies": { 23 | "autoprefixer": "^6.4.0", 24 | "babel": "^6.5.2", 25 | "babel-core": "^6.14.0", 26 | "babel-eslint": "^7.0.0", 27 | "babel-loader": "^6.2.5", 28 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 29 | "babel-plugin-transform-react-jsx": "^6.8.0", 30 | "babel-preset-es2015": "^6.14.0", 31 | "babel-preset-stage-0": "^6.5.0", 32 | "babel-register": "^6.14.0", 33 | "babel-runtime": "^6.11.6", 34 | "chai": "^3.5.0", 35 | "colors": "^1.1.2", 36 | "copy-webpack-plugin": "^4.0.1", 37 | "core-js": "^2.4.1", 38 | "cross-env": "^3.1.3", 39 | "css-loader": "^0.26.1", 40 | "eslint": "^3.0.1", 41 | "extract-text-webpack-plugin": "^1.0.1", 42 | "file-loader": "^0.9.0", 43 | "html-webpack-plugin": "^2.22.0", 44 | "isparta-loader": "^2.0.0", 45 | "json-loader": "^0.5.4", 46 | "karma": "^1.0.0", 47 | "karma-chai": "^0.1.0", 48 | "karma-chai-sinon": "^0.1.5", 49 | "karma-coverage": "^1.1.1", 50 | "karma-mocha": "^1.0.1", 51 | "karma-mocha-reporter": "^2.1.0", 52 | "karma-phantomjs-launcher": "^1.0.2", 53 | "karma-sourcemap-loader": "^0.3.7", 54 | "karma-webpack": "^1.8.0", 55 | "less": "^2.7.1", 56 | "less-loader": "^2.2.3", 57 | "mkdirp": "^0.5.1", 58 | "mocha": "^3.2.0", 59 | "ncp": "^2.0.0", 60 | "node-sass": "^4.2.0", 61 | "phantomjs-prebuilt": "^2.1.12", 62 | "postcss-loader": "^1.2.1", 63 | "raw-loader": "^0.5.1", 64 | "replace-bundle-webpack-plugin": "^1.0.0", 65 | "sass-loader": "^4.1.1", 66 | "sinon": "^1.17.7", 67 | "sinon-chai": "^2.8.0", 68 | "source-map-loader": "^0.1.6", 69 | "style-loader": "^0.13.0", 70 | "url-loader": "^0.5.7", 71 | "webpack": "^1.13.2", 72 | "webpack-dev-server": "^1.15.0" 73 | }, 74 | "dependencies": { 75 | "left-pad": "^1.1.3", 76 | "preact": "^7.2.1", 77 | "preact-compat": "^3.0.0", 78 | "preact-habitat": "^2.0.0", 79 | "promise-polyfill": "^6.0.2", 80 | "proptypes": "^0.14.3", 81 | "serve": "^2.0.0", 82 | "yt-player": "^2.5.2" 83 | }, 84 | "main": "webpack.config.babel.js", 85 | "directories": { 86 | "test": "test" 87 | }, 88 | "repository": { 89 | "type": "git", 90 | "url": "git+https://github.com/agarrharr/preact-audio-player.git" 91 | }, 92 | "bugs": { 93 | "url": "https://github.com/agarrharr/preact-audio-player/issues" 94 | }, 95 | "homepage": "https://github.com/agarrharr/preact-audio-player#readme" 96 | } 97 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agarrharr/preact-audio-player/bc87dc14a3db2a7d23b46b4b777235cfb2c95ff4/screenshot.png -------------------------------------------------------------------------------- /src/components/Slider.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | 3 | const BLUE = '#3FB3D2'; 4 | const BLUE_2 = '#00A0AD'; 5 | const WHITE = '#FFFFFF'; 6 | 7 | const getMinutesAndSeconds = time => { 8 | return `${leftPad(Math.floor(time / 60), 2, '0')}:${leftPad(time % 60, 2, '0')}` 9 | }; 10 | 11 | class Slider extends Component { 12 | state = { 13 | leftEdge: 0, 14 | sliderWidth: 0, 15 | isDragging: false, 16 | }; 17 | 18 | handleMouseMove = (e) => { 19 | if (this.state.isDragging) { 20 | const relativePosition = (e.x - this.state.leftEdge) / this.state.sliderWidth; 21 | const value = relativePosition < 0 22 | ? 0 23 | : relativePosition > 1 24 | ? 1 25 | : relativePosition; 26 | this.props.onChange(value); 27 | } 28 | }; 29 | 30 | handleMouseUp = () => { 31 | this.setState({isDragging: false}); 32 | }; 33 | 34 | handleMouseDown = (e) => { 35 | e.preventDefault(); 36 | this.setState({ 37 | isDragging: true, 38 | leftEdge: e.x - e.offsetX, 39 | sliderWidth: e.target.clientWidth, 40 | }); 41 | this.handleMouseMove(e); 42 | }; 43 | 44 | componentDidMount() { 45 | document.addEventListener('mousemove', this.handleMouseMove.bind(this)); 46 | document.addEventListener('mouseup', this.handleMouseUp.bind(this)); 47 | } 48 | 49 | render() { 50 | const { value, width, onChange } = this.props; 51 | 52 | const styles = { 53 | PreactAudioPlayer__Slider: { 54 | position: 'relative', 55 | height: 20, 56 | width, 57 | backgroundColor: WHITE, 58 | borderRadius: 15, 59 | cursor: 'pointer', 60 | }, 61 | PreactAudioPlayer__SliderHandle: { 62 | position: 'relative', 63 | height: 20, 64 | width: 20, 65 | top: -20, 66 | left: (value * width) - (value * 20), 67 | backgroundColor: BLUE, 68 | borderRadius: '50%', 69 | pointerEvents: 'none', 70 | }, 71 | PreactAudioPlayer__SliderFill: { 72 | height: 20, 73 | width: (value * width) - (value * 20) + 20, 74 | backgroundColor: BLUE_2, 75 | borderRadius: 15, 76 | pointerEvents: 'none', 77 | }, 78 | }; 79 | 80 | return ( 81 |
82 |
83 |
84 |
85 | ); 86 | } 87 | } 88 | 89 | export default Slider; 90 | -------------------------------------------------------------------------------- /src/components/widget.js: -------------------------------------------------------------------------------- 1 | import { h, Component } from 'preact'; 2 | import leftPad from 'left-pad'; 3 | import Slider from './Slider'; 4 | 5 | const VOLUME_WIDTH = 100; 6 | const SLIDER_WIDTH = 300; 7 | const BLUE = '#3FB3D2'; 8 | const BLUE_DARK = '#1A83A1'; 9 | const BLUE_2 = '#00A0AD'; 10 | const WHITE = '#FFFFFF'; 11 | 12 | const getNextRate = currentRate => { 13 | const rates = [1, 1.25, 1.5, 2]; 14 | const i = rates.indexOf(currentRate); 15 | return rates[i === rates.length - 1 ? 0 : i + 1]; 16 | } 17 | 18 | const getMinutesAndSeconds = time => { 19 | return `${leftPad(Math.floor(time / 60), 2, '0')}:${leftPad(time % 60, 2, '0')}` 20 | }; 21 | 22 | const MuteIcon = () => 23 | 24 | 25 | 26 | 27 | const UnmuteIcon = () => 28 | 29 | 30 | 31 | 32 | 33 | const PlayIcon = () => 34 | 35 | 36 | 37 | 38 | const PauseIcon = () => 39 | 40 | 41 | 42 | 43 | const MARGIN_WIDTH = 20; 44 | const PLAY_BUTTON_WIDTH = 100; 45 | const PLAY_BUTTON_BORDER_WIDTH = 5; 46 | const TIME_WIDTH = 40; 47 | const RATE_WIDTH = 20; 48 | const MUTE_WIDTH = 20; 49 | 50 | class Widget extends Component { 51 | state = { 52 | isMuted: false, 53 | isPlaying: false, 54 | volume: 1, 55 | currentTime: null, 56 | duration: null, 57 | rate: 1, 58 | window: 0, 59 | }; 60 | 61 | static defaultProps = { 62 | url: null 63 | }; 64 | 65 | updateDimensions = () => { 66 | const componentWidth = this.component.offsetWidth; 67 | const fixedWidthItems = (MARGIN_WIDTH * 8) + PLAY_BUTTON_WIDTH + PLAY_BUTTON_BORDER_WIDTH * 2 + TIME_WIDTH * 2 + RATE_WIDTH + MUTE_WIDTH; 68 | const remainingSpace = componentWidth - fixedWidthItems; 69 | const extraSpace = remainingSpace < 100 70 | ? remainingSpace < 35 71 | ? TIME_WIDTH + MUTE_WIDTH + (MARGIN_WIDTH * 2) 72 | : TIME_WIDTH + MARGIN_WIDTH 73 | : 0; 74 | 75 | this.setState({ 76 | timeSliderWidth: (remainingSpace + extraSpace) * 0.7, 77 | volumeSliderWidth: (remainingSpace + extraSpace) * 0.3, 78 | showRightTime: remainingSpace > 99, 79 | showMute: remainingSpace > 34, 80 | }); 81 | }; 82 | 83 | componentDidMount = () => { 84 | this.audio = document.getElementById('PreactAudioPlayer'); 85 | window.addEventListener("resize", this.updateDimensions); 86 | this.updateDimensions(); 87 | }; 88 | 89 | componentWillUnmount = () => { 90 | window.removeEventListener("resize", this.updateDimensions); 91 | }; 92 | 93 | handlePlayClick = () => { 94 | this.setState({isPlaying: !this.state.isPlaying}); 95 | 96 | if (this.state.isPlaying) { 97 | this.audio.play(); 98 | this.audio.addEventListener('timeupdate', (event) => { 99 | this.setState({ 100 | currentTime: Math.floor(this.audio.currentTime), 101 | duration: Math.floor(this.audio.duration), 102 | }); 103 | }, false); 104 | } else { 105 | this.audio.pause(); 106 | } 107 | }; 108 | 109 | handleVolumeChange = volume => { 110 | this.setState({ 111 | volume, 112 | isMuted: volume === 0, 113 | }); 114 | this.audio.volume = volume; 115 | }; 116 | 117 | handleTimeChange = percentTime => { 118 | this.setState({ 119 | currentTime: Math.floor(this.state.duration * percentTime), 120 | }); 121 | this.audio.currentTime = this.state.currentTime; 122 | }; 123 | 124 | handleMuteClick = () => { 125 | this.setState({ 126 | isMuted: !this.state.isMuted, 127 | volume: this.state.volume === 0 ? 0.5 : this.state.volume, 128 | }); 129 | this.audio.volume = this.state.isMuted ? 0 : this.state.volume; 130 | }; 131 | 132 | handlePlaybackRate = () => { 133 | this.setState({ 134 | rate: getNextRate(this.state.rate), 135 | }); 136 | this.audio.playbackRate = this.state.rate; 137 | } 138 | 139 | render() { 140 | const {url} = this.props; 141 | const {isMuted, isPlaying, volume, currentTime, duration, rate} = this.state; 142 | const styles = { 143 | PreactAudioPlayer: { 144 | display: 'flex', 145 | justifyContent: 'space-between', 146 | alignItems: 'center', 147 | boxSizing: 'border-box', 148 | height: 50, 149 | margin: '40px 0', 150 | backgroundColor: BLUE_DARK, 151 | fontFamily: 'sans-serif', 152 | }, 153 | PreactAudioPlayer__Play: { 154 | display: 'flex', 155 | flex: 'none', 156 | justifyContent: 'center', 157 | alignItems: 'center', 158 | boxSizing: 'border-box', 159 | width: PLAY_BUTTON_WIDTH, 160 | height: PLAY_BUTTON_WIDTH, 161 | marginLeft: MARGIN_WIDTH, 162 | backgroundColor: BLUE, 163 | borderRadius: '50%', 164 | border: `${PLAY_BUTTON_BORDER_WIDTH}px solid ${BLUE_DARK}`, 165 | cursor: 'pointer', 166 | }, 167 | PreactAudioPlayer__Time: { 168 | display: 'flex', 169 | color: WHITE, 170 | }, 171 | PreactAudioPlayer__TimeLeft: { 172 | marginLeft: MARGIN_WIDTH, 173 | width: TIME_WIDTH, 174 | }, 175 | PreactAudioPlayer__TimeSlider: { 176 | marginLeft: MARGIN_WIDTH, 177 | }, 178 | PreactAudioPlayer__TimeRight: { 179 | width: TIME_WIDTH, 180 | marginLeft: MARGIN_WIDTH, 181 | }, 182 | PreactAudioPlayer__Rate: { 183 | width: RATE_WIDTH, 184 | marginLeft: MARGIN_WIDTH, 185 | color: WHITE, 186 | cursor: 'pointer', 187 | }, 188 | PreactAudioPlayer__Volume: { 189 | display: 'flex', 190 | marginRight: MARGIN_WIDTH, 191 | }, 192 | PreactAudioPlayer__Mute: { 193 | width: MUTE_WIDTH, 194 | marginLeft: MARGIN_WIDTH, 195 | border: 'none', 196 | color: WHITE, 197 | background: 'none', 198 | cursor: 'pointer', 199 | }, 200 | PreactAudioPlayer__VolumeSlider: { 201 | marginLeft: MARGIN_WIDTH, 202 | }, 203 | }; 204 | 205 | return ( 206 |
this.component = e}> 207 | 210 | 213 |
214 |
{getMinutesAndSeconds(currentTime)}
215 |
216 | 217 |
218 | {this.state.showRightTime 219 | ?
{getMinutesAndSeconds(duration)}
220 | : null} 221 |
222 |
223 | {rate}x 224 |
225 |
226 | {this.state.showMute 227 | ? 230 | : null} 231 |
232 | 233 |
234 |
235 |
236 | ); 237 | } 238 | } 239 | 240 | export default Widget; 241 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import habitat from 'preact-habitat'; 2 | import Widget from './components/widget'; 3 | 4 | function init() { 5 | let newWidget = habitat(Widget); 6 | newWidget.render() 7 | } 8 | 9 | // in development, set up HMR: 10 | if (module.hot) { 11 | require('preact/devtools'); // enables React DevTools, be careful on IE 12 | module.hot.accept('./components/widget', () => requestAnimationFrame(init)); 13 | } 14 | 15 | init(); 16 | -------------------------------------------------------------------------------- /test/browser/index.js: -------------------------------------------------------------------------------- 1 | import { h, render, rerender } from 'preact'; 2 | import SigninWidget from 'components/signin'; 3 | 4 | /*global sinon,expect*/ 5 | 6 | describe('App', () => { 7 | let scratch; 8 | 9 | before( () => { 10 | scratch = document.createElement('div'); 11 | (document.body || document.documentElement).appendChild(scratch); 12 | }); 13 | 14 | beforeEach( () => { 15 | scratch.innerHTML = ''; 16 | }); 17 | 18 | after( () => { 19 | scratch.parentNode.removeChild(scratch); 20 | scratch = null; 21 | }); 22 | 23 | describe('Siginin widget', () => { 24 | it('Should render Signin Widget', () => { 25 | render(, scratch); 26 | rerender(); 27 | 28 | expect(scratch.innerHTML).to.contain(`

External Signin Widget

`); 29 | }); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | var webpack = require('../webpack.config.babel.js'); 3 | var path = require('path'); 4 | 5 | webpack.module.loaders.push({ 6 | test: /\.jsx?$/, 7 | loader: 'isparta', 8 | include: path.resolve(__dirname, '../src') 9 | }); 10 | 11 | module.exports = function(config) { 12 | config.set({ 13 | basePath: '../', 14 | frameworks: ['mocha', 'chai-sinon'], 15 | reporters: ['mocha', 'coverage'], 16 | coverageReporter: { 17 | reporters: [ 18 | { 19 | type: 'text-summary' 20 | }, 21 | { 22 | type: 'html', 23 | dir: 'coverage', 24 | subdir: '.' 25 | } 26 | ] 27 | }, 28 | 29 | browsers: ['PhantomJS'], 30 | 31 | files: [ 32 | 'test/browser/**/*.js' 33 | ], 34 | 35 | preprocessors: { 36 | 'test/**/*.js': ['webpack'], 37 | 'src/**/*.js': ['webpack'], 38 | '**/*.js': ['sourcemap'] 39 | }, 40 | 41 | webpack: webpack, 42 | webpackMiddleware: { noInfo: true } 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /tools/build.cli.js: -------------------------------------------------------------------------------- 1 | require('colors'); 2 | 3 | console.log('\n 📦 building task is finished... \n'.magenta); 4 | -------------------------------------------------------------------------------- /tools/lint.cli.js: -------------------------------------------------------------------------------- 1 | require('colors'); 2 | 3 | console.log('\n ✨ task lint is finished... \n'.yellow); 4 | -------------------------------------------------------------------------------- /tools/start.cli.js: -------------------------------------------------------------------------------- 1 | require('colors'); 2 | 3 | console.log('\n 🚀 server started and is running your production package! \n'.cyan); 4 | -------------------------------------------------------------------------------- /tools/test.cli.js: -------------------------------------------------------------------------------- 1 | require('colors'); 2 | 3 | console.log('\n 🙏 testing is done \n'.bgGreen); 4 | -------------------------------------------------------------------------------- /webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | import autoprefixer from 'autoprefixer'; 4 | import CopyWebpackPlugin from 'copy-webpack-plugin'; 5 | import ReplacePlugin from 'replace-bundle-webpack-plugin'; 6 | import path from 'path'; 7 | 8 | const ENV = process.env.NODE_ENV || 'development'; 9 | 10 | const CSS_MAPS = ENV !== 'production'; 11 | 12 | module.exports = { 13 | context: path.resolve(__dirname, 'src'), 14 | entry: './index.js', 15 | 16 | output: { 17 | path: path.resolve(__dirname, 'build'), 18 | publicPath: '/', 19 | filename: 'bundle.js', 20 | libraryTarget: 'umd' 21 | }, 22 | 23 | resolve: { 24 | extensions: ['', '.jsx', '.js', '.json', '.scss'], 25 | modulesDirectories: [ 26 | path.resolve(__dirname, 'src/lib'), 27 | path.resolve(__dirname, 'node_modules'), 28 | 'node_modules' 29 | ], 30 | alias: { 31 | components: path.resolve(__dirname, 'src/components'), // used for tests 32 | style: path.resolve(__dirname, 'src/style'), 33 | react: 'preact-compat', 34 | 'react-dom': 'preact-compat' 35 | } 36 | }, 37 | 38 | module: { 39 | loaders: [ 40 | { 41 | test: /\.jsx?$/, 42 | exclude: [], 43 | loader: 'babel' 44 | }, 45 | { 46 | // Transform our own .(scss|css) files with PostCSS and CSS-modules 47 | test: /\.(scss|css)$/, 48 | include: [path.resolve(__dirname, 'src/components')], 49 | loader: [ 50 | `style-loader?singleton`, 51 | `css-loader?modules&importLoaders=1&sourceMap=${CSS_MAPS}`, 52 | 'postcss-loader', 53 | `sass-loader?sourceMap=${CSS_MAPS}` 54 | ].join('!') 55 | }, 56 | { 57 | test: /\.(scss|css)$/, 58 | exclude: [path.resolve(__dirname, 'src/components')], 59 | loader: [ 60 | `style-loader?singleton`, 61 | `css?sourceMap=${CSS_MAPS}`, 62 | `postcss`, 63 | `sass?sourceMap=${CSS_MAPS}` 64 | ].join('!') 65 | }, 66 | { 67 | test: /\.json$/, 68 | loader: 'json' 69 | }, 70 | { 71 | test: /\.(xml|html|txt|md)$/, 72 | loader: 'raw' 73 | }, 74 | { 75 | test: /\.(svg|woff2?|ttf|eot|jpe?g|png|gif)(\?.*)?$/i, 76 | loader: ENV === 'production' 77 | ? 'file?name=[path][name]_[hash:base64:5].[ext]' 78 | : 'url' 79 | } 80 | ] 81 | }, 82 | 83 | postcss: () => [autoprefixer({ browsers: 'last 2 versions' })], 84 | 85 | plugins: [ 86 | new webpack.NoErrorsPlugin(), 87 | new webpack.DefinePlugin({ 88 | 'process.env.NODE_ENV': JSON.stringify(ENV) 89 | }) 90 | ].concat( 91 | ENV === 'production' 92 | ? [ 93 | // strip out babel-helper invariant checks 94 | new ReplacePlugin([ 95 | { 96 | // this is actually the property name https://github.com/kimhou/replace-bundle-webpack-plugin/issues/1 97 | partten: /throw\s+(new\s+)?[a-zA-Z]+Error\s*\(/g, 98 | replacement: () => 'return;(' 99 | } 100 | ]) 101 | ] 102 | : [] 103 | ), 104 | 105 | stats: { colors: true }, 106 | 107 | node: { 108 | global: true, 109 | process: false, 110 | Buffer: false, 111 | __filename: false, 112 | __dirname: false, 113 | setImmediate: false 114 | }, 115 | 116 | devtool: ENV === 'production' ? 'source-map' : '', 117 | 118 | devServer: { 119 | port: process.env.PORT || 8080, 120 | host: 'localhost', 121 | colors: true, 122 | publicPath: '/build', 123 | contentBase: './', 124 | historyApiFallback: true, 125 | open: true 126 | } 127 | }; 128 | --------------------------------------------------------------------------------