├── .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 | 
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------