├── .env ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── PULL_REQUEST-TEMPLATE.md ├── README.md ├── build ├── favicon.png ├── icon.svg ├── icons │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ └── playstore │ │ └── icon.png └── manifest.json ├── image-1.jpg ├── image-2.jpg ├── image-3.jpg ├── image-4.jpg ├── image.png ├── package.json ├── public ├── favicon.png ├── icon.svg ├── icons │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ └── playstore │ │ └── icon.png ├── index.html └── manifest.json ├── src ├── App.jsx ├── actions │ └── index.js ├── components │ ├── AddSongs.jsx │ ├── Header.jsx │ ├── NowPlaying.jsx │ ├── PlayingCtrl.jsx │ ├── Song.jsx │ └── SongList.jsx ├── index.css ├── index.js ├── middleware.js ├── reducers │ ├── common.js │ ├── index.js │ ├── page.js │ ├── playState.js │ └── songs.js ├── registerServiceWorker.js ├── store │ └── localStore.js ├── utils │ ├── keyboardEvents.js │ └── media-session.js └── views │ ├── MainView.jsx │ └── PlayingView.jsx └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | registerServiceWorker.js 2 | middleware.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb"], 3 | "parser": "babel-eslint", 4 | "env": { 5 | "browser": true 6 | }, 7 | "rules": { 8 | "import/prefer-default-export": "off" 9 | } 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | # misc 9 | .DS_Store 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | build/ 20 | package-lock* 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 11 5 | install: 6 | - npm install 7 | - npm install -D terser@3.14.1 8 | script: 9 | - npm run build 10 | - npm run lint 11 | deploy: 12 | provider: pages 13 | local-dir: build 14 | committer-from-gh: true 15 | skip-cleanup: true 16 | github-token: $GITHUB_TOKEN 17 | keep-history: true 18 | on: 19 | branch: master -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to react-music-player 2 | 3 | We would love for you to contribute and help make it even better 4 | than it is today! As a contributor, here are the guidelines we would like you 5 | to follow: 6 | 7 | - [ESLint](#eslint) 8 | - [Issues and Bugs](#issue) 9 | - [Feature Requests](#feature) 10 | - [Submission Guidelines](#submit) 11 | 12 | ## ESLint 13 | This project uses ESLint, please make sure all files pass linting before making pull request. Do not edit the ESLint configuration file 14 | 15 | ## Found an Issue? 16 | If you find a bug in the source code or a mistake in the documentation, you can help us by [submitting an issue](#submit-issue) to our [GitHub Repository](https://github.com/ashinzekene/react-music-player). Even better, you can [submit a Pull Request](#submit-pr) with a fix. 17 | 18 | ## Want a Feature? 19 | You can * request * a new feature by [submitting an issue](#submit - issue) to our [GitHub 20 | Repository][github].If you would like to * implement * a new feature, please submit an issue with 21 | a proposal for your work first, to be sure that we can use it. 22 | 23 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit - pr). 24 | 25 | ## Submission Guidelines 26 | 27 | ### Submitting an Issue 28 | Before you submit an issue, search the archive, maybe your question was already answered. 29 | 30 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 31 | Help us to maximize the effort we can spend fixing issues and adding new 32 | features, by not reporting duplicate issues.Providing the following information will increase the 33 | chances of your issue being dealt with quickly: 34 | 35 | * **Overview of the Issue** - if an error is being thrown a non- minified stack trace helps 36 | * **Version ** - what version is affected (e.g. 0.1.2) 37 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 38 | * **Browsers and Operating System** - is this a problem with all browsers? 39 | * **Reproduce the Error** - provide a live example [Runnable][runnable]) or a unambiguous set of steps 40 | * **Related Issues** - has a similar issue been reported before? 41 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 42 | causing the problem (line of code or commit) 43 | 44 | You can file new issues by providing the above information [here](https://github.com/ashinzekene/react-music-player/issues/new). 45 | 46 | ### Submitting a Pull Request (PR) 47 | Before you submit your Pull Request (PR) consider the following guidelines: 48 | 49 | * Search [GitHub](https://github.com/ashinzekene/react-music-player/pulls) for an open or closed PR 50 | that relates to your submission.You don't want to duplicate effort. 51 | 52 | * Make your changes in a new git fork: 53 | 54 | * Commit your changes using a descriptive commit message 55 | * Push your fork to GitHub: 56 | * In GitHub, send a pull request 57 | * If we suggest changes then: 58 | * Make the required updates. 59 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 60 | 61 | ```shell 62 | git rebase master -i 63 | git push -f 64 | ``` 65 | 66 | That's it! Thank you for your contribution! 67 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | > Please provide us with the following information: 2 | > --------------------------------------------------------------- 3 | 4 | ### OS and Version? 5 | > Windows 7, 8 or 10. Linux (which distribution).macOS(Yosemite ? El Capitan? Sierra ?) 6 | > Browser 7 | 8 | ### Versions 9 | > Node 10 | > React 11 | 12 | ### Repro steps 13 | > 14 | 15 | ### The log given by the failure. 16 | > 17 | 18 | ### Mention any other details that might be useful. 19 | 20 | > --------------------------------------------------------------- 21 | > Thanks! We'll be in touch soon. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Ashinze Ekene 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /PULL_REQUEST-TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | * ... 3 | 4 | ## What 5 | * ... 6 | 7 | ## How to Test 8 | * ... 9 | 10 | ## What to Check 11 | Verify that the following are valid 12 | * ... -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MUSIC PLAYER 2 | 3 | [![Build Status](https://travis-ci.com/ashinzekene/react-music-player.svg?branch=master)](https://travis-ci.com/ashinzekene/react-music-player) 4 | 5 | A react music player PWA that plays local files using the Files API 6 | 7 | [CHECK OUT THE WEB APP](https://ashinzekene.github.io/react-music-player) 8 | 9 | React music player 1 10 | React music player 2 11 | React music player 3 12 | React music player 4 13 | 14 | ## CONTRIBUTING 15 | 16 | Feel free to contribute to the repo. Make sure you configure eslint, or run lint before submitting pull requests 17 | ### TECH STACK 18 | - React 19 | - Redux 20 | 21 | ## Features 22 | 1. Play/Pause 23 | 1. Repeat Options 24 | 1. Progress Bar 25 | 1. Drag and Drop - Thanks to [@CliffReimers](https://github.com/CliffReimers) 26 | 1. Keyborad Controls - Thanks to [@Spring3](https://github.com/Spring3) 27 | 28 | ## TODO LIST 29 | 30 | 1. Play Next Automatically ✅ 31 | 1. Controls - Next, Previous, Progress Bar ✅ 32 | 1. Saving Songs(localStroage) ✅ 33 | 1. UI ✅ 34 | 1. A Page for currently playing song ✅ 35 | 1. Host on GitHub ✅ 36 | 1. Repeat ✅ 37 | 1. Seek progressbar on nowPlayingPage ✅ 38 | 1. Let playing song show as playing 39 | 1. Add Icons to sidebar 40 | 1. Show Time 41 | 1. Use the MediaAPI 42 | 1. Fix linting 43 | 1. Shuffle 44 | 1. Search 45 | 1. Playlists 46 | 47 | ## BUGS 48 | 49 | 1. Pauses on state change ✅ 50 | > Was due to the fact the audio element was in a child component which unmounts 51 | > was resolved by moving the audio element to a component that does not unmount -------------------------------------------------------------------------------- /build/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/build/favicon.png -------------------------------------------------------------------------------- /build/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /build/icons/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/build/icons/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /build/icons/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/build/icons/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /build/icons/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/build/icons/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /build/icons/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/build/icons/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /build/icons/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/build/icons/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /build/icons/playstore/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/build/icons/playstore/icon.png -------------------------------------------------------------------------------- /build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Music Player", 3 | "author": "Ashinze Ekene", 4 | "name": "React Music Player", 5 | "icons": [{ 6 | "src": "icons/mipmap-mdpi/ic_launcher.png", 7 | "sizes": "48x48", 8 | "type": "image/png" 9 | }, { 10 | "src": "icons/mipmap-hdpi/ic_launcher.png", 11 | "sizes": "72x72", 12 | "type": "image/png" 13 | }, { 14 | "src": "icons/mipmap-xhdpi/ic_launcher.png", 15 | "sizes": "96x96", 16 | "type": "image/png" 17 | }, { 18 | "src": "icons/mipmap-xxhdpi/ic_launcher.png", 19 | "sizes": "144x144", 20 | "type": "image/png" 21 | }, { 22 | "src": "icons/mipmap-xxxhdpi/ic_launcher.png", 23 | "sizes": "192x192", 24 | "type": "image/png" 25 | }, { 26 | "src": "icons/playstore/icon.png", 27 | "sizes": "512x512", 28 | "type": "image/png" 29 | }], 30 | "start_url": ".", 31 | "display": "standalone", 32 | "theme_color": "#673ab7;", 33 | "background_color": "#ffffff" 34 | } 35 | -------------------------------------------------------------------------------- /image-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/image-1.jpg -------------------------------------------------------------------------------- /image-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/image-2.jpg -------------------------------------------------------------------------------- /image-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/image-3.jpg -------------------------------------------------------------------------------- /image-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/image-4.jpg -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/image.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "music-player", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^3.9.1", 7 | "@material-ui/icons": "^3.0.2", 8 | "@material-ui/lab": "^3.0.0-alpha.29", 9 | "localforage": "^1.5.0", 10 | "prop-types": "^15.6.2", 11 | "react": "16.8.0-alpha.1", 12 | "react-dom": "16.8.0-alpha.1", 13 | "react-redux": "^5.0.6", 14 | "redux": "^4.0.1" 15 | }, 16 | "devDependencies": { 17 | "eslint": "5.6.0", 18 | "eslint-config-airbnb": "^17.1.0", 19 | "eslint-plugin-import": "^2.16.0", 20 | "eslint-plugin-jsx-a11y": "^6.2.0", 21 | "eslint-plugin-react": "^7.12.4", 22 | "react-scripts": "^2.1.3", 23 | "terser": "^3.14.1" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "predeploy": "npm run build", 29 | "lint": "./node_modules/.bin/eslint src/**/* --fix", 30 | "deploy": "gh-pages -d build", 31 | "test": "react-scripts test --env=jsdom", 32 | "eject": "react-scripts eject" 33 | }, 34 | "homepage": "https://ashinzekene.github.io/react-music-player", 35 | "browserslist": [ 36 | ">0.2%", 37 | "not dead", 38 | "not ie <= 11", 39 | "not op_mini all" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/public/favicon.png -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/public/icons/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /public/icons/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/public/icons/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /public/icons/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/public/icons/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /public/icons/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/public/icons/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /public/icons/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/public/icons/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /public/icons/playstore/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashinzekene/react-music-player/46f1fdd79157639208f19174d95a91559b7f5065/public/icons/playstore/icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Music Player 12 | 13 | 14 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Music Player", 3 | "author": "Ashinze Ekene", 4 | "name": "React Music Player", 5 | "icons": [{ 6 | "src": "icons/mipmap-mdpi/ic_launcher.png", 7 | "sizes": "48x48", 8 | "type": "image/png" 9 | }, { 10 | "src": "icons/mipmap-hdpi/ic_launcher.png", 11 | "sizes": "72x72", 12 | "type": "image/png" 13 | }, { 14 | "src": "icons/mipmap-xhdpi/ic_launcher.png", 15 | "sizes": "96x96", 16 | "type": "image/png" 17 | }, { 18 | "src": "icons/mipmap-xxhdpi/ic_launcher.png", 19 | "sizes": "144x144", 20 | "type": "image/png" 21 | }, { 22 | "src": "icons/mipmap-xxxhdpi/ic_launcher.png", 23 | "sizes": "192x192", 24 | "type": "image/png" 25 | }, { 26 | "src": "icons/playstore/icon.png", 27 | "sizes": "512x512", 28 | "type": "image/png" 29 | }], 30 | "start_url": ".", 31 | "display": "standalone", 32 | "theme_color": "#7050FA", 33 | "background_color": "#ffffff" 34 | } 35 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import Snackbar from '@material-ui/core/Snackbar'; 5 | import Button from '@material-ui/core/Button'; 6 | import { NOW_PLAYING_PAGE, togglePlaying, playSong } from './actions'; 7 | 8 | import MainView from './views/MainView'; 9 | import Header from './components/Header'; 10 | import PlayingView from './views/PlayingView'; 11 | import keyboardEvents from './utils/keyboardEvents'; 12 | 13 | 14 | const mapStateToProps = state => ({ 15 | page: state.page, 16 | songs: state.songs, 17 | playState: state.playState, 18 | repeatType: state.common.repeat, 19 | }); 20 | 21 | const mapDispatchToProps = dispatch => ({ 22 | toggle: () => dispatch(togglePlaying()), 23 | playSong: id => dispatch(playSong(id)), 24 | }); 25 | 26 | class App extends Component { 27 | constructor(props) { 28 | super(props); 29 | this.state = { 30 | currentTime: 0, 31 | snackBarOpen: false, 32 | hasRejectedInstall: false, 33 | snackMsg: '', 34 | hideSnackAction: false, 35 | installEvent: null, 36 | addToHomeScreenUIVisible: false, 37 | }; 38 | } 39 | 40 | componentDidMount() { 41 | const { songs, toggle } = this.props; 42 | if (songs[0]) { 43 | this.audioPlayer.src = URL.createObjectURL(songs[0]); 44 | } 45 | this.releaseKeyboardEvents = keyboardEvents({ 46 | playNext: this.playNext, 47 | playPrevious: this.playPrevious, 48 | togglePlaying: toggle, 49 | }); 50 | window.addEventListener('beforeinstallprompt', (e) => { 51 | // Prevent Chrome 67 and earlier from automatically showing the prompt 52 | e.preventDefault(); 53 | // Stash the event so it can be triggered later. 54 | this.setState({ installEvent: e, addToHomeScreenUIVisible: true }); 55 | }); 56 | } 57 | 58 | componentWillReceiveProps(nextProps) { 59 | const { playState } = this.props; 60 | const { installEvent, hasRejectedInstall } = this.state; 61 | if (nextProps.playState !== playState) { 62 | if (!nextProps.playState.playing) { 63 | // PAUSE 64 | this.audioPlayer.pause(); 65 | } else if (nextProps.playState.songId === -1) { 66 | this.playSong(0); 67 | } else if (nextProps.playState.songId === playState.songId) { 68 | // RESUME 69 | console.log('Should only resume'); 70 | this.audioPlayer.play(); 71 | // Start playing 72 | } else { 73 | this.playSong(nextProps.playState.songId); 74 | } 75 | if (installEvent && !hasRejectedInstall) { 76 | installEvent.prompt(); 77 | installEvent.userChoice.then((choiceResult) => { 78 | if (choiceResult.outcome === 'accepted') { 79 | this.setState({ 80 | snackBarOpen: true, 81 | hideSnackAction: true, 82 | hasRejectedInstall: false, 83 | snackMsg: '🤗 Yay! You\'ve installed the app', 84 | }); 85 | } else { 86 | this.setState({ 87 | snackBarOpen: true, 88 | hideSnackAction: true, 89 | hasRejectedInstall: true, 90 | snackMsg: '😥 Reload the page whenever you change your mind', 91 | }); 92 | } 93 | this.snackBarOpen({ installEvent: null }); 94 | }); 95 | } 96 | } 97 | } 98 | 99 | componentWillUnmount() { 100 | this.releaseKeyboardEvents(); 101 | } 102 | 103 | playNext = () => { 104 | const { songs, playState, playSong: play } = this.props; 105 | URL.revokeObjectURL(songs[playState.songId]); 106 | const nextSongId = (playState.songId + 1) % songs.length; 107 | play(nextSongId); 108 | } 109 | 110 | songEnded = () => { 111 | const { 112 | songs, playState, repeatType, playSong: play, 113 | } = this.props; 114 | // No repeat 115 | if (repeatType === 0) { 116 | URL.revokeObjectURL(songs[playState.songId]); 117 | if (playState.songId < songs.length - 1) play(playState.songId + 1); 118 | } else if (repeatType === 1) { 119 | // repeat one 120 | play(playState.songId); 121 | // repeat all 122 | } else this.playNext(); 123 | } 124 | 125 | playPrevious = () => { 126 | const { songs, playState, playSong: play } = this.props; 127 | URL.revokeObjectURL(songs[playState.songId]); 128 | const prevSongId = playState.songId === 0 ? songs.length - 1 : playState.songId - 1; 129 | play(prevSongId); 130 | } 131 | 132 | updateTime = () => { 133 | const currentTime = 100 * this.audioPlayer.currentTime / this.audioPlayer.duration || 0; 134 | this.setState({ currentTime }); 135 | } 136 | 137 | playSong = (id) => { 138 | const { songs } = this.props; 139 | if (songs[id]) { 140 | const fileSrc = URL.createObjectURL(songs[id]); 141 | this.audioPlayer.src = fileSrc; 142 | this.audioPlayer.play(); 143 | window.document.title = songs[id].name.replace('.mp3', ''); 144 | } 145 | } 146 | 147 | timeDrag = (time) => { 148 | this.audioPlayer.currentTime = this.audioPlayer.duration * (time / 100); 149 | } 150 | 151 | handleActionClick = () => { 152 | window.open('https://github.com/ashinzekene/react-music-player', '_blank'); 153 | } 154 | 155 | handleRequestClose = () => { 156 | this.setState({ snackBarOpen: false, snackMsg: '', hideSnackAction: false }); 157 | } 158 | 159 | render() { 160 | const { 161 | currentTime, snackBarOpen, snackMsg, installEvent, addToHomeScreenUIVisible, hideSnackAction, 162 | } = this.state; 163 | const { 164 | songs, playState, toggle, repeatType, page, 165 | } = this.props; 166 | return ( 167 | <> 168 |
this.setState({ snackBarOpen: true })} 173 | /> 174 | 183 | { 184 | page === NOW_PLAYING_PAGE ? ( 185 | this.setState({ snackBarOpen: true, snackMsg: msg })} 194 | /> 195 | ) : ( 196 | this.setState({ snackBarOpen: true, snackMsg: msg })} 202 | /> 203 | )} 204 | {snackMsg || 'Not Implemented yet 😊'} 211 | )} 212 | action={ 213 | !hideSnackAction && ( 214 | 217 | )} 218 | /> 219 | 220 | ); 221 | } 222 | } 223 | 224 | App.propTypes = { 225 | page: PropTypes.string.isRequired, 226 | songs: PropTypes.arrayOf(PropTypes.any).isRequired, 227 | playState: PropTypes.shape({ 228 | playing: PropTypes.bool.isRequired, 229 | songId: PropTypes.number.isRequired, 230 | }).isRequired, 231 | repeatType: PropTypes.oneOf([0, 1, 2]).isRequired, 232 | toggle: PropTypes.func.isRequired, 233 | playSong: PropTypes.func.isRequired, 234 | }; 235 | 236 | export default connect(mapStateToProps, mapDispatchToProps)(App); 237 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-cycle */ 2 | import mediaSession from '../utils/media-session'; 3 | 4 | export const ADD_SONGS = 'ADD_SONGS'; 5 | export const REMOVE_SONGS = 'REMOVE_SONGS'; 6 | export const TOGGLE_PLAYING = 'TOGGLE_PLAYING'; 7 | export const FILTER_SONGS = 'FILTER_SONGS'; 8 | export const PLAY_SONG = 'PLAY_SONG'; 9 | export const TOGGLE_SIDEBAR = 'TOGGLE_SIDEBAR'; 10 | export const PLAYLIST_PAGE = 'PLAYLIST_PAGE'; 11 | export const SHUFFLE = 'SHUFFLE'; 12 | export const REPEAT = 'REPEAT'; 13 | export const HOME_PAGE = 'HOME_PAGE'; 14 | export const SETTINGS_PAGE = 'SETTINGS_PAGE'; 15 | export const NOW_PLAYING_PAGE = 'NOW_PLAYING_PAGE'; 16 | 17 | export const addSongs = songs => ({ 18 | type: ADD_SONGS, 19 | songs, 20 | }); 21 | 22 | export const removeSong = id => ({ 23 | type: REMOVE_SONGS, 24 | id, 25 | }); 26 | 27 | export const playSong = (id) => { 28 | mediaSession.playSong(id); 29 | return { 30 | type: PLAY_SONG, 31 | id, 32 | }; 33 | }; 34 | 35 | export const repeatType = id => ({ 36 | type: REPEAT, 37 | id, 38 | }); 39 | 40 | export const togglePlaying = () => ({ 41 | type: TOGGLE_PLAYING, 42 | }); 43 | 44 | export const toggleSidebar = () => ({ 45 | type: TOGGLE_SIDEBAR, 46 | }); 47 | 48 | export const filterSong = filter => ({ 49 | type: FILTER_SONGS, 50 | filter, 51 | }); 52 | 53 | export const homePage = () => ({ 54 | type: HOME_PAGE, 55 | }); 56 | 57 | export const nowPlayingPage = () => ({ 58 | type: NOW_PLAYING_PAGE, 59 | }); 60 | 61 | export const settingsPage = () => ({ 62 | type: SETTINGS_PAGE, 63 | }); 64 | -------------------------------------------------------------------------------- /src/components/AddSongs.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Fab from '@material-ui/core/Fab'; 3 | import AddIcon from '@material-ui/icons/Add'; 4 | import PropTypes from 'prop-types'; 5 | import { connect } from 'react-redux'; 6 | import { addSongs } from '../actions'; 7 | 8 | const mapDispatchToProps = dispatch => ({ 9 | addSongs: songs => dispatch(addSongs(songs)), 10 | }); 11 | 12 | class AddSongs extends Component { 13 | addSong = (e) => { 14 | const { addSongs: add } = this.props; 15 | add(e.currentTarget.files); 16 | } 17 | 18 | render() { 19 | return ( 20 | 29 | 37 | 38 | 39 | ); 40 | } 41 | } 42 | 43 | AddSongs.propTypes = { 44 | addSongs: PropTypes.func.isRequired, 45 | }; 46 | 47 | export default connect(null, mapDispatchToProps)(AddSongs); 48 | -------------------------------------------------------------------------------- /src/components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import propTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | 5 | import AppBar from '@material-ui/core/AppBar'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import ListItem from '@material-ui/core/ListItem'; 8 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 9 | import ListItemText from '@material-ui/core/ListItemText'; 10 | import SwipeableDrawer from '@material-ui/core/SwipeableDrawer'; 11 | import Toolbar from '@material-ui/core/Toolbar'; 12 | import Typography from '@material-ui/core/Typography'; 13 | 14 | import MenuIcon from '@material-ui/icons/Menu'; 15 | import NowPlayingIcon from '@material-ui/icons/PlaylistPlay'; 16 | import PlayListIcon from '@material-ui/icons/List'; 17 | import HomeIcon from '@material-ui/icons/Home'; 18 | import SettingsIcon from '@material-ui/icons/Settings'; 19 | 20 | import { 21 | HOME_PAGE, SETTINGS_PAGE, NOW_PLAYING_PAGE, PLAYLIST_PAGE, 22 | } from '../actions'; 23 | 24 | const mapDispatchToProps = dispatch => ({ 25 | openPage: type => dispatch({ type }), 26 | }); 27 | 28 | const menuOptions = [ 29 | { 30 | option: 'Home', 31 | page: HOME_PAGE, 32 | icon: , 33 | }, 34 | { 35 | option: 'NowPlaying', 36 | page: NOW_PLAYING_PAGE, 37 | icon: , 38 | }, 39 | { 40 | option: 'Playlists', 41 | page: PLAYLIST_PAGE, 42 | icon: , 43 | }, 44 | { 45 | option: 'Settings', 46 | page: SETTINGS_PAGE, 47 | icon: , 48 | }, 49 | ]; 50 | 51 | class Header extends Component { 52 | state = { 53 | open: false, 54 | }; 55 | 56 | openPage = page => () => { 57 | const { openPage, playState, openSnackbar } = this.props; 58 | this.setState(prevState => ({ open: !prevState.open })); 59 | if (page === PLAYLIST_PAGE || page === SETTINGS_PAGE) { 60 | openSnackbar(); 61 | return; 62 | } 63 | // Don't Open now playing page when there is no song 64 | if (!playState && page === NOW_PLAYING_PAGE) return; 65 | if (page) openPage(page); 66 | } 67 | 68 | render() { 69 | const { open } = this.state; 70 | return ( 71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 | Music Player 79 | 80 | 81 | 82 |
83 | 84 |
85 | { 86 | menuOptions.map(option => ( 87 | 88 | {option.icon} 89 | {option.option} 90 | 91 | )) 92 | } 93 | 94 |
95 | ); 96 | } 97 | } 98 | 99 | Header.propTypes = { 100 | openPage: propTypes.func.isRequired, 101 | playState: propTypes.objectOf(propTypes.any).isRequired, 102 | openSnackbar: propTypes.func.isRequired, 103 | }; 104 | 105 | export default connect(null, mapDispatchToProps)(Header); 106 | -------------------------------------------------------------------------------- /src/components/NowPlaying.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import propTypes from 'prop-types'; 3 | import LinearProgress from '@material-ui/core/LinearProgress'; 4 | 5 | import Paper from '@material-ui/core/Paper'; 6 | import PauseIcon from '@material-ui/icons/Pause'; 7 | import MusicNote from '@material-ui/icons/MusicNote'; 8 | import PlayIcon from '@material-ui/icons/PlayCircleFilled'; 9 | import Avatar from '@material-ui/core/Avatar'; 10 | 11 | const NowPlaying = ({ 12 | playState, playingSong, currentTime, togglePlaying, openNowPlaying, 13 | }) => { 14 | const handleClick = (e) => { 15 | if (!e.target.closest('[type="button"]') && playingSong) { 16 | openNowPlaying(); 17 | } 18 | }; 19 | 20 | return ( 21 | 22 | 23 |
24 | 25 | 26 | 27 |
28 | {playingSong ? playingSong.name : '[No song]'} 29 |
30 | 33 |
34 |
35 | ); 36 | }; 37 | 38 | NowPlaying.defaultProps = { 39 | playingSong: null, 40 | }; 41 | 42 | NowPlaying.propTypes = { 43 | playState: propTypes.objectOf(propTypes.any).isRequired, 44 | playingSong: propTypes.objectOf(propTypes.any), 45 | currentTime: propTypes.number.isRequired, 46 | togglePlaying: propTypes.func.isRequired, 47 | openNowPlaying: propTypes.func.isRequired, 48 | }; 49 | 50 | export default NowPlaying; 51 | -------------------------------------------------------------------------------- /src/components/PlayingCtrl.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import propTypes from 'prop-types'; 4 | import Slider from '@material-ui/lab/Slider'; 5 | import Paper from '@material-ui/core/Paper'; 6 | import SkipPrevious from '@material-ui/icons/SkipPrevious'; 7 | import IconButton from '@material-ui/core/IconButton'; 8 | import PlayIcon from '@material-ui/icons/PlayCircleFilled'; 9 | import PauseIcon from '@material-ui/icons/PauseCircleFilled'; 10 | import SkipNext from '@material-ui/icons/SkipNext'; 11 | import ShuffleIcon from '@material-ui/icons/Shuffle'; 12 | import Repeat from '@material-ui/icons/Repeat'; 13 | import RepeatOne from '@material-ui/icons/RepeatOne'; 14 | import { repeatType, togglePlaying } from '../actions'; 15 | 16 | const mapStateToProps = state => ({ 17 | playState: state.playState, 18 | repeatType: state.common.repeat, 19 | }); 20 | 21 | const mapDispatchToProps = dispatch => ({ 22 | changeRepeat: id => dispatch(repeatType(id)), 23 | togglePlaying: () => dispatch(togglePlaying()), 24 | }); 25 | 26 | class PlayingCtrl extends Component { 27 | componentDidMount() { 28 | const { installEvent } = this.props; 29 | setTimeout(() => typeof installEvent === 'function' && installEvent(), 3000); 30 | } 31 | 32 | changeRepeat = () => { 33 | const { repeatType: repeat, changeRepeat } = this.props; 34 | const nextRepeat = repeat === 2 ? 0 : repeat + 1; 35 | changeRepeat(nextRepeat); 36 | } 37 | 38 | render() { 39 | const { 40 | playState, song, playNext, playPrevious, currentTime, repeatType: repeat, 41 | togglePlaying: toggle, timeDrag, openSnackbar, 42 | } = this.props; 43 | 44 | return ( 45 | 46 |

{ song.name }

47 | timeDrag(newVal)} max={100} min={0} defaultValue={2} /> 48 |
49 |
50 | 51 | { repeat === 1 52 | ? : 53 | } 54 | 55 | 56 | 57 | 58 |
59 |
60 | 61 | { playState.playing ? : } 62 | 63 |
64 |
65 | 66 | 67 | 68 | openSnackbar('Shuffle doesn\'t work yet, You can make a PR 😊')} 72 | /> 73 |
74 |
75 |
76 | ); 77 | } 78 | } 79 | 80 | PlayingCtrl.defaultProps = { 81 | installEvent: null, 82 | }; 83 | 84 | PlayingCtrl.propTypes = { 85 | timeDrag: propTypes.func.isRequired, 86 | playNext: propTypes.func.isRequired, 87 | playPrevious: propTypes.func.isRequired, 88 | openSnackbar: propTypes.func.isRequired, 89 | repeatType: propTypes.number.isRequired, 90 | changeRepeat: propTypes.func.isRequired, 91 | currentTime: propTypes.number.isRequired, 92 | togglePlaying: propTypes.func.isRequired, 93 | song: propTypes.objectOf(propTypes.any).isRequired, 94 | playState: propTypes.objectOf(propTypes.any).isRequired, 95 | installEvent: propTypes.func, 96 | }; 97 | 98 | export default connect(mapStateToProps, mapDispatchToProps)(PlayingCtrl); 99 | -------------------------------------------------------------------------------- /src/components/Song.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import IconButton from '@material-ui/core/IconButton'; 4 | import ListItem from '@material-ui/core/ListItem'; 5 | import ListItemAvatar from '@material-ui/core/ListItemAvatar'; 6 | import ListItemText from '@material-ui/core/ListItemText'; 7 | import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; 8 | import Avatar from '@material-ui/core/Avatar'; 9 | import MoreVert from '@material-ui/icons/MoreVert'; 10 | import MusicNote from '@material-ui/icons/MusicNote'; 11 | 12 | import EqualizerIcon from '@material-ui/icons/Equalizer'; 13 | 14 | 15 | const Song = ({ 16 | song, handleClick, handleIconClick, isPlaying, 17 | }) => ( 18 | 19 | 20 | 21 | { !isPlaying ? : } 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | 36 | Song.propTypes = { 37 | song: PropTypes.objectOf(PropTypes.any).isRequired, 38 | handleClick: PropTypes.func.isRequired, 39 | handleIconClick: PropTypes.func.isRequired, 40 | isPlaying: PropTypes.bool.isRequired, 41 | }; 42 | export default Song; 43 | -------------------------------------------------------------------------------- /src/components/SongList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import connect from 'react-redux/es/connect/connect'; 4 | import Divider from '@material-ui/core/Divider'; 5 | import List from '@material-ui/core/List'; 6 | import Menu from '@material-ui/core/Menu'; 7 | import MenuItem from '@material-ui/core/MenuItem'; 8 | 9 | import { removeSong, playSong } from '../actions'; 10 | import Song from './Song'; 11 | 12 | const mapStateToProps = store => ({ 13 | playState: store.playState, 14 | }); 15 | 16 | const SongList = ({ 17 | songs, remove, play, playState, 18 | }) => { 19 | const [anchorEl, setAnchorEl] = useState(null); 20 | 21 | const [activeSong, setActiveSong] = useState(-1); 22 | 23 | const setActiveSongItem = ind => ({ target }) => { 24 | setAnchorEl(target); 25 | setActiveSong(ind); 26 | }; 27 | 28 | const handleSongClick = ind => () => play(ind); 29 | 30 | if (!songs.length) { 31 | return ( 32 |

No Songs Present. Please Add Songs

33 | ); 34 | } 35 | return ( 36 |
37 | setAnchorEl(null)}> 38 | remove(activeSong)}>Remove Song 39 | 40 | 41 | { 42 | songs.map((song, ind) => ( 43 | [ 44 | , 51 | , 52 | ] 53 | )) 54 | } 55 | 56 |
57 | ); 58 | }; 59 | 60 | SongList.propTypes = { 61 | remove: PropTypes.func.isRequired, 62 | play: PropTypes.func.isRequired, 63 | songs: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.any)).isRequired, 64 | playState: PropTypes.objectOf(PropTypes.any).isRequired, 65 | }; 66 | 67 | export default connect(mapStateToProps, { remove: removeSong, play: playSong })(SongList); 68 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | .sr-only { 8 | display: none !important; 9 | } 10 | 11 | .small-now-playing { 12 | background-color: white; 13 | z-index: 500; 14 | position: fixed; 15 | bottom: 0px; 16 | left: 0px; 17 | width: 100%; 18 | height: 90px; 19 | } 20 | 21 | .small-now-playing .song-title { 22 | width: Calc( 85% - 60px ); 23 | padding: 10px; 24 | } 25 | .small-now-playing .play-pause-button { 26 | display: flex; 27 | float: left; 28 | width: 60px; 29 | } 30 | 31 | .play-control { 32 | position: fixed; 33 | bottom: 0px; 34 | left: 0px; 35 | width: 100%; 36 | padding-bottom: 10px; 37 | height: 130px; 38 | } 39 | .song-title { 40 | padding: 0px 12px; 41 | white-space: nowrap; 42 | text-overflow: ellipsis; 43 | overflow: hidden; 44 | } 45 | 46 | .play-control .song-title { 47 | padding: 0px 12px; 48 | font-weight: 400; 49 | text-align: center; 50 | white-space: nowrap; 51 | text-overflow: ellipsis; 52 | overflow: hidden; 53 | } 54 | .material-icons { 55 | width: 30px; 56 | } 57 | .side-icons { 58 | display: flex; 59 | justify-content: space-around; 60 | align-items: center; 61 | } 62 | .now-playing-container { 63 | display: flex; 64 | padding: 15px; 65 | } 66 | .now-playing-container .song-name { 67 | padding: 5px; 68 | padding-left: 10px; 69 | flex: 1; 70 | color: #333333; 71 | line-height: 1.7rem; 72 | } 73 | .now-playing-container button { 74 | border: none; 75 | background-color: transparent; 76 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { createStore, applyMiddleware } from 'redux'; 5 | import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; 6 | import deepPurple from '@material-ui/core/colors/deepPurple'; 7 | 8 | import './index.css'; 9 | import App from './App'; 10 | import registerServiceWorker from './registerServiceWorker'; 11 | import reducers from './reducers'; 12 | import loggerMiddleware from './middleware'; 13 | import { saveState, getState } from './store/localStore'; 14 | import mediaNotification from './utils/media-session'; 15 | 16 | const muiTheme = createMuiTheme({ 17 | palette: { 18 | primary: deepPurple, 19 | }, 20 | }); 21 | 22 | 23 | getState().then((localState) => { 24 | let store; 25 | if (process.env.NODE_ENV === 'development') { 26 | store = createStore(reducers, localState, applyMiddleware(loggerMiddleware)); 27 | } else { 28 | store = createStore(reducers, localState); 29 | } 30 | mediaNotification.setStore(store); 31 | store.subscribe(() => { 32 | saveState({ 33 | songs: store.getState().songs, 34 | }); 35 | }); 36 | ReactDOM.render( 37 | // eslint-disable-next-line 38 | 39 | 40 | 41 | 42 | , document.getElementById('root'), 43 | ); 44 | }); 45 | registerServiceWorker(); 46 | -------------------------------------------------------------------------------- /src/middleware.js: -------------------------------------------------------------------------------- 1 | export default store => next => (action) => { 2 | console.group(action.type,); 3 | console.info('PREVIOUS STATE', store.getState()); 4 | const result = next(action); 5 | console.log('ACTION', action); 6 | console.log('NEXT STATE', store.getState()); 7 | console.groupEnd(); 8 | return result; 9 | }; 10 | -------------------------------------------------------------------------------- /src/reducers/common.js: -------------------------------------------------------------------------------- 1 | import { TOGGLE_SIDEBAR, REPEAT } from '../actions/index'; 2 | 3 | const initialState = { 4 | sidebarOpen: false, 5 | repeat: 0, 6 | }; 7 | 8 | export default (state = initialState, action) => { 9 | switch (action.type) { 10 | case TOGGLE_SIDEBAR: { 11 | return { ...state, sidebarOpen: !state.sidebarOpen }; 12 | } 13 | case REPEAT: { 14 | return { ...state, repeat: action.id }; 15 | } 16 | default: { 17 | return state; 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import songs from './songs'; 3 | import playState from './playState'; 4 | import common from './common'; 5 | import page from './page'; 6 | 7 | const reducers = combineReducers({ 8 | songs, 9 | common, 10 | playState, 11 | page, 12 | }); 13 | 14 | export default reducers; 15 | -------------------------------------------------------------------------------- /src/reducers/page.js: -------------------------------------------------------------------------------- 1 | import { 2 | HOME_PAGE, NOW_PLAYING_PAGE, SETTINGS_PAGE, PLAYLIST_PAGE, 3 | } from '../actions/index'; 4 | 5 | export default (state = HOME_PAGE, action) => { 6 | switch (action.type) { 7 | case HOME_PAGE: { 8 | return HOME_PAGE; 9 | } 10 | case PLAYLIST_PAGE: { 11 | return PLAYLIST_PAGE; 12 | } 13 | case NOW_PLAYING_PAGE: { 14 | return NOW_PLAYING_PAGE; 15 | } 16 | case SETTINGS_PAGE: { 17 | return SETTINGS_PAGE; 18 | } 19 | default: { 20 | return state; 21 | } 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/reducers/playState.js: -------------------------------------------------------------------------------- 1 | import { PLAY_SONG, TOGGLE_PLAYING } from '../actions/index'; 2 | 3 | const initalState = { 4 | playing: false, 5 | songId: -1, 6 | }; 7 | 8 | export default (state = initalState, action) => { 9 | switch (action.type) { 10 | case PLAY_SONG: { 11 | return { playing: true, songId: action.id }; 12 | } 13 | case TOGGLE_PLAYING: { 14 | return Object.assign({}, state, { playing: !state.playing }); 15 | } 16 | default: { 17 | return state; 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/reducers/songs.js: -------------------------------------------------------------------------------- 1 | import { ADD_SONGS, REMOVE_SONGS } from '../actions'; 2 | 3 | export default (state = [], action) => { 4 | switch (action.type) { 5 | case ADD_SONGS: { 6 | return [...state, ...action.songs]; 7 | } 8 | case REMOVE_SONGS: { 9 | return state.filter((song, index) => index !== action.id); 10 | } 11 | default: { 12 | return state; 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' 13 | // [::1] is the IPv6 localhost address. 14 | || window.location.hostname === '[::1]' 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | || window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, 18 | ), 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (!isLocalhost) { 36 | // Is not local host. Just register service worker 37 | registerValidSW(swUrl); 38 | } else { 39 | // This is running on localhost. Lets check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then((registration) => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch((error) => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then((response) => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 82 | || response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then((registration) => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.', 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then((registration) => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/store/localStore.js: -------------------------------------------------------------------------------- 1 | import * as LocalForage from 'localforage'; 2 | 3 | export const saveState = (state) => { 4 | LocalForage.setItem('state', state); 5 | }; 6 | 7 | export const getState = () => LocalForage.getItem('state').then((state) => { 8 | if (state === null) { 9 | return undefined; 10 | } 11 | return state; 12 | }); 13 | -------------------------------------------------------------------------------- /src/utils/keyboardEvents.js: -------------------------------------------------------------------------------- 1 | const ACTION_KEYS = [ 2 | 'ARROWLEFT', 3 | 'ARROWRIGHT', 4 | 'SPACE', 5 | ]; 6 | 7 | export default function keyboardEvents(handlers) { 8 | const handler = (e) => { 9 | const upperCaseKey = e.code.toUpperCase(); 10 | 11 | if (ACTION_KEYS.indexOf(upperCaseKey) === -1) { 12 | return; 13 | } 14 | 15 | const { 16 | playNext, 17 | playPrevious, 18 | togglePlaying, 19 | } = handlers; 20 | 21 | switch (upperCaseKey) { 22 | case 'ARROWLEFT': 23 | playPrevious(); 24 | break; 25 | case 'ARROWRIGHT': 26 | playNext(); 27 | break; 28 | case 'SPACE': 29 | togglePlaying(); 30 | break; 31 | default: 32 | break; 33 | } 34 | }; 35 | window.addEventListener('keydown', handler); 36 | return () => window.removeEventListener('keydown', handler); 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/media-session.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-cycle */ 2 | 3 | import { togglePlaying, playSong } from '../actions'; 4 | 5 | let store; 6 | const mediaSessionEnabled = ('mediaSession' in navigator); 7 | const addNewSong = (id) => { 8 | const state = store.getState(); 9 | navigator.mediaSession.metadata = new window.MediaMetadata({ 10 | title: state.songs[id].name, 11 | artist: 'Unknown', 12 | album: 'Unknown Albumn', 13 | artwork: [{ 14 | src: 'icons/mipmap-xhdpi/ic_launcher.png', 15 | sizes: '96x96', 16 | type: 'image/png', 17 | }, { 18 | src: 'icons/mipmap-xxhdpi/ic_launcher.png', 19 | sizes: '144x144', 20 | type: 'image/png', 21 | }, { 22 | src: 'icons/mipmap-xxxhdpi/ic_launcher.png', 23 | sizes: '192x192', 24 | type: 'image/png', 25 | }, { 26 | src: 'icons/playstore/icon.png', 27 | sizes: '512x512', 28 | type: 'image/png', 29 | }], 30 | }); 31 | }; 32 | 33 | const addActionListeners = () => { 34 | navigator.mediaSession.setActionHandler('previoustrack', () => { 35 | if (store) { 36 | const state = store.getState(); 37 | const prevId = state.playState.songId === 0 38 | ? state.songs.length - 1 : state.playState.songId - 1; 39 | store.dispatch(playSong(prevId)); 40 | } 41 | }); 42 | 43 | navigator.mediaSession.setActionHandler('nexttrack', () => { 44 | if (store) { 45 | const state = store.getState(); 46 | const nextId = (state.playState.songId + 1) % state.songs.length; 47 | store.dispatch(playSong(nextId)); 48 | } 49 | }); 50 | 51 | navigator.mediaSession.setActionHandler('play', () => { 52 | if (store) store.dispatch(togglePlaying()); 53 | }); 54 | 55 | navigator.mediaSession.setActionHandler('pause', () => { 56 | if (store) store.dispatch(togglePlaying()); 57 | }); 58 | }; 59 | if (mediaSessionEnabled) addActionListeners(); 60 | 61 | export default { 62 | setStore(s) { 63 | store = s; 64 | }, 65 | playSong(song) { 66 | if (mediaSessionEnabled) { 67 | addNewSong(song); 68 | } 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /src/views/MainView.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import propTypes from 'prop-types'; 4 | 5 | import AddSongs from '../components/AddSongs'; 6 | import SongList from '../components/SongList'; 7 | import NowPlaying from '../components/NowPlaying'; 8 | import { togglePlaying, nowPlayingPage, addSongs } from '../actions'; 9 | 10 | const mapDispatchToProps = dispatch => ({ 11 | toggle: () => dispatch(togglePlaying()), 12 | openNowPlaying: () => dispatch(nowPlayingPage()), 13 | addSongs: songs => dispatch(addSongs(songs)), 14 | }); 15 | 16 | class MainView extends Component { 17 | handleDragOver = (event) => { 18 | event.preventDefault(); 19 | event.stopPropagation(); 20 | // eslint-disable-next-line no-param-reassign 21 | event.dataTransfer.dropEffect = 'copy'; 22 | }; 23 | 24 | render() { 25 | const { 26 | songs, playState, openNowPlaying, openSnackbar, currentTime, addSongs: add, toggle, 27 | } = this.props; 28 | return ( 29 |
{ 32 | this.handleDragOver(event); 33 | if (window.File && window.FileReader && window.FileList && window.Blob) { 34 | const files = [...event.dataTransfer.files].filter(({ name }) => name && name.endsWith('.mp3')); 35 | if (files.length > 0) add(files); 36 | } else { 37 | openSnackbar('The File APIs are not fully supported in this browser.'); 38 | } 39 | return false; 40 | }} 41 | > 42 | 43 | 44 | 51 |
52 | ); 53 | } 54 | } 55 | 56 | MainView.propTypes = { 57 | openNowPlaying: propTypes.func.isRequired, 58 | toggle: propTypes.func.isRequired, 59 | addSongs: propTypes.func.isRequired, 60 | songs: propTypes.arrayOf(propTypes.any).isRequired, 61 | playState: propTypes.objectOf(propTypes.any).isRequired, 62 | currentTime: propTypes.number.isRequired, 63 | openSnackbar: propTypes.func.isRequired, 64 | }; 65 | 66 | export default connect(null, mapDispatchToProps)(MainView); 67 | -------------------------------------------------------------------------------- /src/views/PlayingView.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import propTypes from 'prop-types'; 3 | import Album from '@material-ui/icons/Album'; 4 | 5 | import PlayingCtrl from '../components/PlayingCtrl'; 6 | 7 | const PlayingView = ({ 8 | playNext, 9 | timeDrag, 10 | repeatType, 11 | currentTime, 12 | playingSong, 13 | openSnackbar, 14 | playPrevious, 15 | installEvent, 16 | }) => ( 17 |
18 |
26 | 29 |
30 | 40 |
41 | ); 42 | 43 | PlayingView.defaultProps = { 44 | installEvent: () => {}, 45 | }; 46 | 47 | PlayingView.propTypes = { 48 | timeDrag: propTypes.func.isRequired, 49 | playNext: propTypes.func.isRequired, 50 | repeatType: propTypes.number.isRequired, 51 | openSnackbar: propTypes.func.isRequired, 52 | playPrevious: propTypes.func.isRequired, 53 | installEvent: propTypes.func, 54 | currentTime: propTypes.number.isRequired, 55 | playingSong: propTypes.objectOf(propTypes.any).isRequired, 56 | }; 57 | 58 | export default PlayingView; 59 | --------------------------------------------------------------------------------