├── .env ├── src ├── stores │ ├── index.js │ └── appStore.js ├── assets │ ├── fonts │ │ ├── music.eot │ │ ├── music.ttf │ │ ├── music.woff │ │ ├── Dosis-Light.ttf │ │ ├── Dosis-Regular.ttf │ │ └── music.svg │ ├── icons │ │ ├── win │ │ │ └── icon.ico │ │ ├── mac │ │ │ └── icon.icns │ │ └── png │ │ │ ├── 16x16.png │ │ │ ├── 24x24.png │ │ │ ├── 32x32.png │ │ │ ├── 48x48.png │ │ │ ├── 64x64.png │ │ │ ├── 96x96.png │ │ │ ├── 128x128.png │ │ │ ├── 256x256.png │ │ │ └── 512x512.png │ └── installer background │ │ └── background.png ├── styles │ ├── index.css │ ├── variables.css │ ├── fonts.css │ ├── base.css │ └── icons.css ├── components │ ├── Nav.css │ ├── Nav.js │ └── player │ │ ├── components │ │ ├── songs list │ │ │ ├── index.js │ │ │ ├── SongsList.css │ │ │ └── Song.js │ │ └── player controls │ │ │ ├── PlayerControls.css │ │ │ └── index.js │ │ └── index.js ├── index.js ├── App.js ├── electron │ └── index.js └── registerServiceWorker.js ├── public ├── favicon.ico ├── manifest.json └── index.html ├── README.md ├── .gitignore ├── LICENSE └── package.json /.env: -------------------------------------------------------------------------------- 1 | BROWSER=none -------------------------------------------------------------------------------- /src/stores/index.js: -------------------------------------------------------------------------------- 1 | export * from './appStore'; 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/fonts/music.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/fonts/music.eot -------------------------------------------------------------------------------- /src/assets/fonts/music.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/fonts/music.ttf -------------------------------------------------------------------------------- /src/assets/fonts/music.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/fonts/music.woff -------------------------------------------------------------------------------- /src/assets/icons/win/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/icons/win/icon.ico -------------------------------------------------------------------------------- /src/assets/icons/mac/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/icons/mac/icon.icns -------------------------------------------------------------------------------- /src/assets/icons/png/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/icons/png/16x16.png -------------------------------------------------------------------------------- /src/assets/icons/png/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/icons/png/24x24.png -------------------------------------------------------------------------------- /src/assets/icons/png/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/icons/png/32x32.png -------------------------------------------------------------------------------- /src/assets/icons/png/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/icons/png/48x48.png -------------------------------------------------------------------------------- /src/assets/icons/png/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/icons/png/64x64.png -------------------------------------------------------------------------------- /src/assets/icons/png/96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/icons/png/96x96.png -------------------------------------------------------------------------------- /src/assets/fonts/Dosis-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/fonts/Dosis-Light.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Dosis-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/fonts/Dosis-Regular.ttf -------------------------------------------------------------------------------- /src/assets/icons/png/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/icons/png/128x128.png -------------------------------------------------------------------------------- /src/assets/icons/png/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/icons/png/256x256.png -------------------------------------------------------------------------------- /src/assets/icons/png/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/icons/png/512x512.png -------------------------------------------------------------------------------- /src/styles/index.css: -------------------------------------------------------------------------------- 1 | @import 'variables.css'; 2 | @import 'icons.css'; 3 | @import 'fonts.css'; 4 | @import 'base.css'; -------------------------------------------------------------------------------- /src/assets/installer background/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kiarash-Z/redp/HEAD/src/assets/installer background/background.png -------------------------------------------------------------------------------- /src/components/Nav.css: -------------------------------------------------------------------------------- 1 | .nav { 2 | display: flex; 3 | justify-content: flex-end; 4 | padding: 2rem 2.5rem; 5 | font-size: 2rem; 6 | } 7 | 8 | .nav__open-button { 9 | font-size: 2.5rem; 10 | padding: 0.2rem; 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RedP 2 | 3 | RedP is a simple cross-platform music player built with Electron and React. 4 | 5 | ![RedP screenshot](https://user-images.githubusercontent.com/20098648/40877410-a772cc6a-6695-11e8-921a-1f8aff6ae8b9.jpg) 6 | -------------------------------------------------------------------------------- /src/styles/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-primary: #E63354; 3 | --color-secondary: #c9c8c8; 4 | --color-black: rgb(27, 27, 27); 5 | --color-black-light: rgba(0,0,0, 0.3); 6 | --color-black-very-light: rgba(0,0,0, 0.14); 7 | --color-grey: #B2AFB5; 8 | --color-white: #fff; 9 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './styles/index.css'; 4 | import App from './App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /src/styles/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Dosis'; 3 | src: url('../assets//fonts/Dosis-Light.ttf') format('truetype'); 4 | font-weight: 300; 5 | } 6 | 7 | @font-face { 8 | font-family: 'Dosis'; 9 | src: url('../assets//fonts/Dosis-Regular.ttf') format('truetype'); 10 | font-weight: 400; 11 | } -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Music Player", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.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 | 9 | # production 10 | /build 11 | 12 | #app 13 | /dist 14 | 15 | # misc 16 | .DS_Store 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /src/components/Nav.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import './Nav.css'; 3 | 4 | const { ipcRenderer } = window.require('electron'); 5 | 6 | class Nav extends PureComponent { 7 | sendOpenDialog = () => { 8 | ipcRenderer.send('dialog:open'); 9 | } 10 | render() { 11 | return ( 12 | 17 | ); 18 | } 19 | } 20 | 21 | export default Nav; -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Provider } from 'mobx-react'; 3 | 4 | import * as stores from './stores'; 5 | import Nav from './components/Nav'; 6 | import Player from './components/player'; 7 | 8 | class App extends Component { 9 | render() { 10 | return ( 11 | 12 |
13 |
16 |
17 | ); 18 | } 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/styles/base.css: -------------------------------------------------------------------------------- 1 | * { box-sizing: border-box; margin: 0; padding: 0; } 2 | 3 | html { 4 | font-family: 'Dosis'; 5 | font-size: 62.5%; 6 | font-weight: 300; 7 | user-select: none; 8 | } 9 | 10 | body { 11 | background: var(--color-white); 12 | -webkit-app-region: drag; 13 | overflow-x: hidden; 14 | } 15 | 16 | button { 17 | border: none; 18 | background: transparent; 19 | cursor: pointer; 20 | } 21 | 22 | [role="button"], button { 23 | -webkit-app-region: no-drag; 24 | } 25 | 26 | button:active, button:focus { 27 | outline: none; 28 | } 29 | 30 | h1, h2, h3, h4, h5, h6, p, span, strong, i { 31 | -webkit-app-region: drag; 32 | } 33 | 34 | @media (min-width: 1200px) { 35 | html { 36 | font-size: 75%; 37 | } 38 | } 39 | @media (max-width: 576px) { 40 | html { 41 | font-size: 56.25%; 42 | } 43 | } -------------------------------------------------------------------------------- /src/components/player/components/songs list/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | 4 | import Song from './Song'; 5 | 6 | const audioContext = new AudioContext(); 7 | const SongsList = inject('appStore')(observer(class SongsListClass extends Component { 8 | renderSongs = () => { 9 | if (!this.props.appStore.songs.length) return ; 10 | return this.props.appStore.songs.map(song => ); 11 | } 12 | render() { 13 | return ( 14 |
18 | {this.renderSongs()} 19 |
20 | ) 21 | } 22 | })); 23 | 24 | export default SongsList; 25 | -------------------------------------------------------------------------------- /src/components/player/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | 4 | import SongsList from './components/songs list'; 5 | import PlayerControls from './components/player controls'; 6 | 7 | const { ipcRenderer } = window.require('electron'); 8 | 9 | const Player = inject('appStore')(observer(class PlayerClass extends Component { 10 | componentDidMount() { 11 | ipcRenderer.on('files:open', (e, filePaths) => { 12 | this.props.appStore.openFile(filePaths); 13 | }) 14 | } 15 | render() { 16 | return ( 17 |
26 | 27 | 28 |
29 | ); 30 | } 31 | })); 32 | 33 | export default Player; 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kiarash Zarinmehr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/styles/icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'music'; 3 | src: url('../assets/fonts/music.eot?hc5393'); 4 | src: url('../assets/fonts/music.eot?hc5393#iefix') format('embedded-opentype'), 5 | url('../assets/fonts/music.ttf?hc5393') format('truetype'), 6 | url('../assets/fonts/music.woff?hc5393') format('woff'), 7 | url('../assets/fonts/music.svg?hc5393#music') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | i { 13 | /* use !important to prevent issues with browser extensions that change fonts */ 14 | font-family: 'music' !important; 15 | speak: none; 16 | font-style: normal; 17 | font-weight: normal; 18 | font-variant: normal; 19 | text-transform: none; 20 | line-height: 1; 21 | 22 | /* Better Font Rendering =========== */ 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | .a-folder:before { 28 | content: "\e900"; 29 | } 30 | .a-music:before { 31 | content: "\e908"; 32 | } 33 | .a-next:before { 34 | content: "\e903"; 35 | } 36 | .a-previous:before { 37 | content: "\e904"; 38 | } 39 | .a-pause:before { 40 | content: "\e906"; 41 | } 42 | .a-play:before { 43 | content: "\e907"; 44 | } 45 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | Music Player 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/player/components/player controls/PlayerControls.css: -------------------------------------------------------------------------------- 1 | .player-controls { 2 | width: 50%; 3 | margin-top: 8.5rem; 4 | min-width: 30rem; 5 | } 6 | 7 | .player-progress-container { 8 | padding: 0.5rem 0; 9 | cursor: pointer; 10 | width: 100%; 11 | } 12 | 13 | .player-progress { 14 | width: 100%; 15 | height: 0.3rem; 16 | border-radius: 3px; 17 | background: #DEDEDE; 18 | pointer-events: none; 19 | } 20 | 21 | .player-progress__bar { 22 | width: 0; 23 | height: 100%; 24 | background: var(--color-primary); 25 | position: relative; 26 | transition: 0.05s; 27 | pointer-events: none; 28 | } 29 | 30 | .player-progress__dragger { 31 | position: absolute; 32 | top: 50%; 33 | transform: translateY(-50%); 34 | right: -0.6rem; 35 | width: 1.2rem; 36 | height: 1.2rem; 37 | border-radius: 50%; 38 | transition: 0.2s; 39 | background: var(--color-primary); 40 | opacity: 0; 41 | pointer-events: none; 42 | } 43 | 44 | .player-progress-container:hover .player-progress__dragger{ 45 | opacity: 1; 46 | } 47 | 48 | .player-time { 49 | margin-top: 1rem; 50 | font-size: 1.5rem; 51 | color: var(--color-primary); 52 | display: flex; 53 | justify-content: space-between; 54 | font-weight: 400; 55 | } 56 | 57 | .player-controls-container { 58 | display: flex; 59 | align-items: center; 60 | justify-content: center; 61 | margin-top: 0.8rem; 62 | margin-bottom: 3.5rem; 63 | } 64 | 65 | .player-controls__button { 66 | color: var(--color-grey); 67 | font-size: 3rem; 68 | } 69 | 70 | .player-controls__button.-play { 71 | padding: 2.5rem; 72 | border-radius: 50%; 73 | box-shadow: 0 0 3rem var(--color-black-very-light); 74 | display: flex; 75 | justify-content: center; 76 | align-items: center; 77 | text-align: center; 78 | margin: 0 6rem; 79 | color: var(--color-primary); 80 | } 81 | 82 | .player-controls__play { 83 | transform: translateX(3px); 84 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": "./", 3 | "name": "redp", 4 | "productName": "RedP", 5 | "description": "A simple music player for your local files", 6 | "version": "0.1.0", 7 | "private": true, 8 | "postinstall": "electron-builder install-app-deps", 9 | "main": "src/electron", 10 | "dependencies": { 11 | "bluebird": "^3.5.1", 12 | "classnames": "^2.2.5", 13 | "concurrently": "^3.5.1", 14 | "electron-is-dev": "^0.3.0", 15 | "gsap": "^2.0.1", 16 | "jsmediatags": "^3.8.1", 17 | "mobx": "^4.2.1", 18 | "mobx-react": "^5.1.2", 19 | "react": "^16.3.2", 20 | "react-dom": "^16.3.2", 21 | "react-scripts": "1.1.4", 22 | "readdir-absolute": "^1.0.1" 23 | }, 24 | "scripts": { 25 | "rs-start": "react-scripts start", 26 | "start": "concurrently \"yarn rs-start\" \"wait-on http://localhost:3000 && electron .\"", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test --env=jsdom", 29 | "eject": "react-scripts eject", 30 | "package-app": "electron-builder -m -w --linux deb", 31 | "package-app-32": "electron-builder --win --linux deb --ia32" 32 | }, 33 | "devDependencies": { 34 | "electron": "^2.0.0", 35 | "electron-builder": "^20.15.1", 36 | "wait-on": "^2.1.0" 37 | }, 38 | "author": { 39 | "name": "Kiarash Zarinmehr", 40 | "email": "kiarash.zar@gmail.com" 41 | }, 42 | "build": { 43 | "appId": "com.electron.redp", 44 | "mac": { 45 | "category": "Music", 46 | "icon": "./src/assets/icons/mac/icon.icns" 47 | }, 48 | "dmg": { 49 | "background": "./src/assets/installer background/background.png" 50 | }, 51 | "win": { 52 | "icon": "./src/assets/icons/win/icon.ico" 53 | }, 54 | "linux": { 55 | "icon": "./src/assets/icons/png" 56 | }, 57 | "files": [ 58 | "build/**/*", 59 | "src/electron/*", 60 | "node_modules/**/*" 61 | ], 62 | "directories": { 63 | "buildResources": "assets" 64 | }, 65 | "extends": null 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/player/components/songs list/SongsList.css: -------------------------------------------------------------------------------- 1 | .songs-list { 2 | display: flex; 3 | align-items: center; 4 | align-self: flex-start; 5 | padding: 0 calc((100vw - 61vw) / 2); 6 | } 7 | 8 | .song { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | position: relative; 13 | width: 61vw; 14 | } 15 | 16 | .song.-scaled { 17 | transform: scale(0.85); 18 | } 19 | 20 | .song.-scaled .song__image { 21 | z-index: 1; 22 | cursor: pointer; 23 | } 24 | 25 | .song__image.-next:hover { 26 | transform: translateX(-2rem); 27 | } 28 | 29 | .song__image.-previous:hover { 30 | transform: translateX(2rem); 31 | } 32 | 33 | .song__image.-next:active, .song__image.-previous:active { 34 | transform: translateX(0); 35 | } 36 | 37 | .song__titles-container { 38 | text-align: center; 39 | font-size: 1.5rem; 40 | } 41 | 42 | .song__titles-container.-hidden { 43 | transform: translateY(20px) !important; 44 | opacity: 0 !important; 45 | } 46 | 47 | .song__title { 48 | color: var(--color-primary); 49 | margin-bottom: 0.7rem; 50 | white-space: nowrap; 51 | } 52 | 53 | .song__title.-secondary { 54 | color: var(--color-black); 55 | font-size: 1.4rem; 56 | white-space: nowrap; 57 | } 58 | 59 | .song__image-container { 60 | width: 43vh; 61 | height: 43vh; 62 | margin-top: 4rem; 63 | border-radius: 5px; 64 | box-shadow: 0.5rem 0.5rem 3rem var(--color-black-light); 65 | display: flex; 66 | justify-content: center; 67 | align-items: center; 68 | } 69 | 70 | .song__image { 71 | width: 100%; 72 | height: 100%; 73 | border-radius: 5px; 74 | transform: translateX(0); 75 | transition: 0.2s transform; 76 | position: static; 77 | } 78 | 79 | .song__image-icon { 80 | font-size: 15rem; 81 | transform: translateX(-15px); 82 | background: -webkit-linear-gradient(var(--color-secondary), var(--color-primary)); 83 | background-clip: text; 84 | -webkit-background-clip: text; 85 | -webkit-text-fill-color: transparent; 86 | } 87 | 88 | .song__bars { 89 | position: absolute; 90 | bottom: 0; 91 | width: 43vh; 92 | } -------------------------------------------------------------------------------- /src/assets/fonts/music.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/electron/index.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, Menu, dialog, ipcMain } = require('electron'); 2 | const isDev = require('electron-is-dev'); 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const { promisify } = require('bluebird'); 6 | const readdirAboslute = require('readdir-absolute'); 7 | 8 | const readdir = promisify(readdirAboslute); 9 | 10 | app.setName('RedP'); 11 | 12 | const isMac = process.platform === 'darwin'; 13 | 14 | const openDialog = (type = 'file') => { 15 | const properties = ['multiSelections']; 16 | if (type === 'both') properties.unshift('openDirectory', 'openFile'); 17 | else if (type === 'folder') properties.unshift('openDirectory'); 18 | else if (type === 'file') properties.unshift('openFile'); 19 | 20 | dialog.showOpenDialog(mainWindow, { 21 | properties, 22 | filters: [ 23 | {name: 'Audio Files', extensions: ['mp3']}, 24 | ] 25 | }, filePaths => { 26 | if (!filePaths) return; 27 | const flatten = arr => { 28 | let flatted = []; 29 | for(let i = 0; i < arr.length; i++) { 30 | if (Array.isArray(arr[i])) { 31 | flatted = flatted.concat(flatten(arr[i])); 32 | } else flatted.push(arr[i]); 33 | } 34 | return flatted; 35 | } 36 | const paths = filePaths.map(path => { 37 | if(fs.lstatSync(path).isDirectory()) return readdir(path); 38 | return Promise.resolve(path); 39 | }); 40 | Promise.all(paths).then(values => { 41 | if (values.length === 1) values[0] = [values[0]]; 42 | const filteredPaths = flatten(values) 43 | .filter(path => path.endsWith('.mp3')); 44 | mainWindow.webContents.send('files:open', filteredPaths); 45 | }); 46 | }); 47 | } 48 | 49 | // Menu 50 | 51 | const fileSub = [ 52 | { 53 | label: isMac ? 'Open' : 'Open File', 54 | click() { 55 | openDialog(isMac ? 'both' : 'file'); 56 | }, 57 | accelerator: isMac ? 'Command+O' : 'Ctrl+O', 58 | } 59 | ]; 60 | 61 | const mainMenuTemplate = [ 62 | { 63 | label: 'File', 64 | submenu: fileSub, 65 | } 66 | ]; 67 | 68 | if (isMac) { 69 | mainMenuTemplate.unshift({ 70 | label: app.getName(), 71 | submenu: [ 72 | { role: 'about'}, 73 | { type: 'separator'}, 74 | { type: 'separator' }, 75 | { role: 'hide' }, 76 | { role: 'hideothers' }, 77 | { role: 'unhide' }, 78 | { type: 'separator' }, 79 | { 80 | label: 'Quit', 81 | accelerator: isMac ? 'Command+Q' : 'Ctrl+Q', 82 | click() { 83 | app.quit(); 84 | } 85 | }, 86 | ] 87 | }) 88 | } else { 89 | fileSub.push({ 90 | label: 'Open Folder', 91 | click() { 92 | openDialog('folder'); 93 | }, 94 | accelerator: 'Ctrl+Shift+O', 95 | }) 96 | } 97 | 98 | let mainWindow 99 | 100 | function createWindow () { 101 | mainWindow = new BrowserWindow({ 102 | width: 1000, 103 | height: 750, 104 | titleBarStyle: 'hidden', 105 | webPreferences: { webSecurity: false }, 106 | icon: path.join(__dirname, '../assets/icons/png/64x64.png'), 107 | }); 108 | mainWindow.loadURL(isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../../build/index.html')}`); 109 | mainWindow.on('closed', () => { mainWindow = null }); 110 | mainWindow.setMinimumSize(420, 530); 111 | mainWindow.setMaximumSize(1200, 900); 112 | const mainMenu = Menu.buildFromTemplate(mainMenuTemplate); 113 | Menu.setApplicationMenu(mainMenu); 114 | } 115 | 116 | if (isDev) { 117 | mainMenuTemplate.push({ 118 | label: 'Developer Tools', 119 | submenu: [ 120 | { 121 | label: 'Toggle DevTools', 122 | accelerator: isMac ? 'Command+I' : 'Ctrl+I', 123 | click(item, focusedWindow) { 124 | focusedWindow.toggleDevTools(); 125 | } 126 | }, 127 | { role: 'reload' } 128 | ] 129 | }) 130 | } 131 | 132 | // open dialog from the app 133 | ipcMain.on('dialog:open', openDialog); 134 | 135 | app.on('ready', createWindow) 136 | 137 | app.on('window-all-closed', () => { 138 | if (process.platform !== 'darwin') { 139 | app.quit() 140 | } 141 | }); 142 | 143 | app.on('activate',() => { 144 | if (mainWindow === null) { 145 | createWindow() 146 | } 147 | }); 148 | -------------------------------------------------------------------------------- /src/components/player/components/player controls/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | 4 | import './PlayerControls.css'; 5 | 6 | let isMouseDown = false; 7 | 8 | const PlayerControls = inject('appStore')(observer(class PlayerControlsClass extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.barRef = React.createRef(); 12 | this.progressRef = React.createRef(); 13 | } 14 | 15 | componentDidMount() { 16 | const progressEl = this.progressRef.current 17 | progressEl.addEventListener('mousedown', () => { isMouseDown = true }); 18 | window.addEventListener('mousemove', this.handleMouseMove); 19 | window.addEventListener('mouseup', e => { 20 | if (!isMouseDown) return; 21 | isMouseDown = false; 22 | this.changeProgress(e); 23 | }); 24 | window.addEventListener('keypress', this.handleKeyPress); 25 | } 26 | 27 | componentDidUpdate() { 28 | if (!this.props.appStore.playingSong.audio) return; 29 | this.props.appStore.playingSong.audio.removeEventListener('timeupdate', this.updateBar); 30 | this.props.appStore.playingSong.audio.addEventListener('timeupdate', this.updateBar); 31 | 32 | this.props.appStore.playingSong.audio.removeEventListener('loadedmetadata', this.props.appStore.setDuration); 33 | this.props.appStore.playingSong.audio.addEventListener('loadedmetadata', this.props.appStore.setDuration); 34 | } 35 | 36 | handleKeyPress = ({ code }) => { 37 | if (code === 'Space') this.props.appStore.togglePlay(); 38 | } 39 | 40 | handleMouseMove = e => { 41 | if (!isMouseDown) return; 42 | const progressEl = this.progressRef.current 43 | const { offsetLeft: left } = progressEl; 44 | const right = progressEl.offsetWidth + left; 45 | let progressed = 0; 46 | if (e.clientX <= right && e.clientX >= left) progressed = (e.clientX - left) / progressEl.offsetWidth; 47 | else if (e.clientX > right) progressed = 1; 48 | else if (e.clientX < left) progressed = 0; 49 | this.props.appStore.updateCurrent(progressed * this.props.appStore.duration) 50 | } 51 | 52 | changeProgress = e => { 53 | e.stopPropagation(); 54 | const progressEl = this.progressRef.current; 55 | this.props.appStore.seek((e.clientX - progressEl.offsetLeft) / progressEl.offsetWidth); 56 | } 57 | 58 | updateBar = ({ target: audio }) => { 59 | if (isMouseDown) return; 60 | this.props.appStore.updateCurrent(); 61 | } 62 | render() { 63 | const { appStore } = this.props; 64 | return ( 65 |
66 | 73 | 74 |
75 | {appStore.formattedCurrent} 76 | {appStore.formattedDuration} 77 |
78 | 79 |
80 | 87 | 88 | 95 | 96 | 103 |
104 |
105 | ); 106 | } 107 | })); 108 | 109 | export default PlayerControls; 110 | -------------------------------------------------------------------------------- /src/components/player/components/songs list/Song.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import classNames from 'classnames'; 4 | 5 | import './SongsList.css'; 6 | 7 | let isMouseDown = false; 8 | const Song = inject('appStore')(observer(class Song extends Component { 9 | canvasRef = React.createRef(); 10 | imageRef = React.createRef(); 11 | 12 | handleMouseUp = () => { 13 | const imageEl = this.imageRef.current; 14 | if (!isMouseDown) return; 15 | imageEl.style.transform = 'translateX(0)'; 16 | } 17 | 18 | componentDidUpdate() { 19 | const { playingSong } = this.props.appStore; 20 | if (playingSong.id !== this.props.id) this.updateImageTransition(); 21 | if (!playingSong.isPlaying || playingSong.id !== this.props.id) return; 22 | this.animateCanvas(); 23 | } 24 | 25 | updateImageTransition = () => { 26 | // remove inline style so css class transform will be applied 27 | const imageEl = this.imageRef.current; 28 | if (imageEl) imageEl.style.removeProperty('transform'); 29 | } 30 | 31 | animateCanvas = () => { 32 | const audio = this.props.audio; 33 | this.audioContext = this.audioContext || this.props.audioContext; 34 | this.source = this.source || this.audioContext.createMediaElementSource(audio); 35 | const analyser = this.audioContext.createAnalyser(); 36 | const canvas = this.canvasRef.current; 37 | const ctx = canvas.getContext('2d'); 38 | this.source.connect(analyser); 39 | analyser.connect(this.audioContext.destination); 40 | frameLooper(); 41 | 42 | function frameLooper() { 43 | window.requestAnimationFrame(frameLooper); 44 | const fbc_array = new Uint8Array(analyser.frequencyBinCount); 45 | analyser.getByteFrequencyData(fbc_array); 46 | ctx.clearRect(0, 0, canvas.width, canvas.height); 47 | const grd = ctx.createLinearGradient(0,0,0,canvas.height - 20); 48 | grd.addColorStop(0,'rgba(230, 51, 84, 0.7)'); 49 | grd.addColorStop(1,'white'); 50 | ctx.fillStyle = grd; 51 | const bars = 150; 52 | for (var i = 0; i < bars; i++) { 53 | const bar_x = i * 3; 54 | const bar_width = 2; 55 | const bar_height = -(fbc_array[i] / 3); 56 | // fillRect( x, y, width, height ) // Explanation of the parameters below 57 | ctx.fillRect(bar_x, canvas.height, bar_width, bar_height); 58 | } 59 | } 60 | } 61 | 62 | render() { 63 | const { appStore, title, artist, picture, id, index } = this.props; 64 | const { index: playingIndex, id: playingId } = appStore.playingSong; 65 | const isNext = index > playingIndex; 66 | const isPrevious = index < playingIndex 67 | const isNotActive = isPrevious || isNext; 68 | return ( 69 |
73 |
74 |

{title} 

75 |

{artist} 

76 |
77 | {picture ? ( 78 |
79 | { isMouseDown = true; }} 87 | onMouseOut={() => { isMouseDown = false; }} 88 | onClick={() => { 89 | if (!isNotActive) return; 90 | if (isNext) appStore.startSongChange('next'); 91 | else if (isPrevious) appStore.startSongChange('previous'); 92 | }} 93 | /> 94 |
95 | ) : ( 96 |
97 | 98 |
99 | )} 100 | 101 |
102 | ); 103 | } 104 | })) 105 | 106 | export default Song; 107 | -------------------------------------------------------------------------------- /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 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/stores/appStore.js: -------------------------------------------------------------------------------- 1 | import { decorate, observable, computed, action, intercept } from 'mobx'; 2 | import { TweenLite } from 'gsap/all'; 3 | const jsmediatags = window.require('jsmediatags'); 4 | 5 | let isAnimating = false; 6 | 7 | class AppStore { 8 | songs = []; 9 | duration = 0; 10 | current = 0; 11 | 12 | constructor() { 13 | intercept(this, 'songs', change => { 14 | if (change.newValue.length === 0) change.newValue = [{ 'dsf': 'sdfsd' }]; 15 | return change; 16 | }) 17 | } 18 | 19 | openFile(filePaths) { 20 | const update = () => { 21 | this.songs = filePaths.map((path, index) => { 22 | const filePath = `file://${path}` 23 | const audio = new Audio(); 24 | audio.src = filePath; 25 | return { 26 | index, 27 | filePath, 28 | audio, 29 | fsPath: path, 30 | id: path, 31 | } 32 | }); 33 | this.playFirstSong(); 34 | this.readSongsMetadata(); 35 | } 36 | if (this.songs.length) this.resetPlayer().then(update); 37 | else update(); 38 | } 39 | 40 | resetPlayer() { 41 | if (this.playingSong.audio) this.playingSong.audio.pause(); 42 | const songsListEl = document.getElementById('songsList'); 43 | TweenLite.set(songsListEl, { x: '0%' }); 44 | this.songs = []; 45 | // allow [] to be sent to component 46 | return Promise.resolve(); 47 | } 48 | 49 | readSongsMetadata() { 50 | const that = this; 51 | this.songs.forEach(item => { 52 | new jsmediatags.Reader(item.fsPath) 53 | .setTagsToRead(['title', 'artist', 'picture']) 54 | .read({ 55 | onSuccess({ tags }) { 56 | that.songs = that.songs.map(song => { 57 | if (song.id === item.id && !song.artist) { 58 | let picture = ''; 59 | if (tags.picture) { 60 | const imageData = tags.picture.data; 61 | let base64String = ''; 62 | for (var i = 0; i < imageData.length; i++) { 63 | base64String += String.fromCharCode(imageData[i]); 64 | } 65 | picture = `data:${tags.picture.format};base64, ${window.btoa(base64String)}`; 66 | } 67 | song = { 68 | ...song, 69 | picture, 70 | audio: song.audio, 71 | title: tags.title, 72 | artist: tags.artist, 73 | }; 74 | } 75 | return song; 76 | }); 77 | }, 78 | onError(err) { 79 | console.log(err) 80 | } 81 | }) 82 | }); 83 | } 84 | 85 | playFirstSong() { 86 | this.songs = this.songs.map((song, index) => { 87 | if (!index) { 88 | song.audio.play(); 89 | song.isPlaying = true; 90 | song.isActive = true; 91 | } 92 | return song; 93 | }) 94 | } 95 | 96 | togglePlay() { 97 | const audio = this.playingSong.audio; 98 | if (audio.paused) audio.play(); 99 | else audio.pause(); 100 | this.songs = this.songs.map(song => { 101 | if (song.id === this.playingSong.id) song.isPlaying = !audio.paused; 102 | return song; 103 | }); 104 | } 105 | 106 | seek(value) { 107 | this.songs = this.songs.map(song => { 108 | if (song.id === this.playingSong.id) { 109 | song.audio.currentTime = value * song.audio.duration; 110 | this.updateCurrent(); 111 | } 112 | return song; 113 | }); 114 | } 115 | 116 | setDuration() { 117 | this.duration = this.playingSong.audio.duration; 118 | } 119 | 120 | updateCurrent(value) { 121 | // 0 is false 122 | this.current = value === undefined ? this.playingSong.audio.currentTime : value; 123 | if (this.current === this.playingSong.audio.duration && 124 | this.current === this.playingSong.audio.currentTime 125 | ) { 126 | this.startSongChange('next'); 127 | } 128 | } 129 | 130 | startSongChange(direction) { 131 | const nextSong = this.songs 132 | .find(song => song.index === this.playingSong.index + (direction === 'next' ? 1 : -1)); 133 | if (isAnimating) return; 134 | if (!nextSong) { 135 | this.songs = this.songs.map(song => { 136 | if (song.id === this.playingSong.id) { 137 | song.isPlaying = false; 138 | song.audio.pause(); 139 | this.seek(0); 140 | } 141 | return song; 142 | }); 143 | return; 144 | } 145 | const songsListEl = document.getElementById('songsList'); 146 | const prevSongEl = document.getElementById('activeSong'); 147 | const prevSongTitleEl = prevSongEl.querySelector('.song__titles-container'); 148 | const nextSongEl = prevSongEl[direction === 'next' ? 'nextElementSibling' : 'previousElementSibling']; 149 | const nextSongTitleEl = nextSongEl.querySelector('.song__titles-container'); 150 | const movement = `${direction === 'next' ? '-' : '+' }=${(prevSongEl.offsetWidth / songsListEl.offsetWidth) * 100}%`; 151 | nextSongTitleEl.classList.remove('-hidden'); 152 | const animationDuration = 0.4; 153 | isAnimating = true; 154 | TweenLite.to(prevSongEl, animationDuration, { 155 | scale: 0.85, 156 | x: 0, 157 | }); 158 | TweenLite.to(nextSongEl, animationDuration, { 159 | scale: 1, 160 | x: 0, 161 | }); 162 | TweenLite.to(nextSongTitleEl, animationDuration / 1.5, { 163 | opacity: 1, 164 | y: 0, 165 | }) 166 | TweenLite.to(prevSongTitleEl, animationDuration / 1.5, { 167 | opacity: 0, 168 | y: 20, 169 | }) 170 | TweenLite.to(songsListEl, animationDuration, { 171 | x: movement, 172 | onComplete: () => { 173 | isAnimating = false; 174 | this.changeSong(direction) 175 | }, 176 | }); 177 | } 178 | 179 | changeSong(direction) { 180 | const nextSong = this.songs 181 | .find(song => song.index === this.playingSong.index + (direction === 'next' ? 1 : -1)); 182 | if (!nextSong) return; 183 | this.resetCurrentSong(); 184 | const { id: nextSongId } = nextSong; 185 | this.songs = this.songs.map(song => { 186 | const condition = song.id === nextSongId; 187 | return {...song, isPlaying: condition, isActive: condition }; 188 | }); 189 | this.setDuration(); 190 | this.updateCurrent(); 191 | this.playingSong.audio.play(); 192 | } 193 | 194 | resetCurrentSong() { 195 | this.playingSong.audio.pause(); 196 | this.playingSong.audio.currentTime = 0; 197 | } 198 | 199 | get playingSong() { 200 | return this.songs.find(song => song.isActive) || {}; 201 | } 202 | 203 | get formattedCurrent() { 204 | if (!this.playingSong.audio) return '00:00'; 205 | const min = Math.floor(this.current / 60); 206 | let sec = String(Math.floor(this.current - min * 60)); 207 | if (sec.length < 2) sec = `0${sec}`; 208 | return `${min}:${sec}`; 209 | } 210 | get formattedDuration() { 211 | if (!this.playingSong.audio) return '00:00'; 212 | const min = Math.floor(this.duration / 60); 213 | let sec = String(Math.floor(this.duration - min * 60)); 214 | if (sec.length < 2) sec = `0${sec}`; 215 | return `${min}:${sec}`; 216 | } 217 | get barWidth() { 218 | return `${(this.current / this.duration) * 100}%` 219 | } 220 | get isControlDisabled() { 221 | return !this.playingSong.audio; 222 | } 223 | } 224 | 225 | decorate(AppStore, { 226 | songs : observable, 227 | duration: observable, 228 | current: observable, 229 | 230 | openFile: action.bound, 231 | togglePlay: action.bound, 232 | seek: action.bound, 233 | setDuration: action.bound, 234 | updateCurrent: action.bound, 235 | readSongsMetadata: action.bound, 236 | playFirstSong: action.bound, 237 | startSongChange: action.bound, 238 | changeSong: action.bound, 239 | resetCurrentSong: action.bound, 240 | resetPlayer: action.bound, 241 | 242 | playingSong: computed, 243 | formattedCurrent: computed, 244 | formattedDuration: computed, 245 | barWidth: computed, 246 | isControlDisabled: computed, 247 | }); 248 | 249 | const appStore = new AppStore(); 250 | 251 | export { appStore }; 252 | --------------------------------------------------------------------------------