Tracking Development with Git now
This commit is contained in:
		
							
								
								
									
										3
									
								
								.babelrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.babelrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | { | ||||||
|  |   "presets": ["@babel/preset-env", "@babel/preset-react"] | ||||||
|  | } | ||||||
							
								
								
									
										76
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | # Logs | ||||||
|  | logs | ||||||
|  | *.log | ||||||
|  | npm-debug.log* | ||||||
|  | yarn-debug.log* | ||||||
|  | yarn-error.log* | ||||||
|  |  | ||||||
|  | # Runtime data | ||||||
|  | pids | ||||||
|  | *.pid | ||||||
|  | *.seed | ||||||
|  | *.pid.lock | ||||||
|  |  | ||||||
|  | # Directory for instrumented libs generated by jscoverage/JSCover | ||||||
|  | lib-cov | ||||||
|  |  | ||||||
|  | # Coverage directory used by tools like istanbul | ||||||
|  | coverage | ||||||
|  |  | ||||||
|  | # nyc test coverage | ||||||
|  | .nyc_output | ||||||
|  |  | ||||||
|  | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | ||||||
|  | .grunt | ||||||
|  |  | ||||||
|  | # Bower dependency directory (https://bower.io/) | ||||||
|  | bower_components | ||||||
|  |  | ||||||
|  | # node-waf configuration | ||||||
|  | .lock-wscript | ||||||
|  |  | ||||||
|  | # Compiled binary addons (https://nodejs.org/api/addons.html) | ||||||
|  | build/Release | ||||||
|  |  | ||||||
|  | # Dependency directories | ||||||
|  | node_modules/ | ||||||
|  | jspm_packages/ | ||||||
|  |  | ||||||
|  | # TypeScript v1 declaration files | ||||||
|  | typings/ | ||||||
|  |  | ||||||
|  | # Optional npm cache directory | ||||||
|  | .npm | ||||||
|  |  | ||||||
|  | # Optional eslint cache | ||||||
|  | .eslintcache | ||||||
|  |  | ||||||
|  | # Optional REPL history | ||||||
|  | .node_repl_history | ||||||
|  |  | ||||||
|  | # Output of 'npm pack' | ||||||
|  | *.tgz | ||||||
|  |  | ||||||
|  | # Yarn Integrity file | ||||||
|  | .yarn-integrity | ||||||
|  |  | ||||||
|  | # dotenv environment variables file | ||||||
|  | .env | ||||||
|  |  | ||||||
|  | # parcel-bundler cache (https://parceljs.org/) | ||||||
|  | .cache | ||||||
|  |  | ||||||
|  | # next.js build output | ||||||
|  | .next | ||||||
|  |  | ||||||
|  | # nuxt.js build output | ||||||
|  | .nuxt | ||||||
|  |  | ||||||
|  | # vuepress build output | ||||||
|  | .vuepress/dist | ||||||
|  |  | ||||||
|  | # Serverless directories | ||||||
|  | .serverless | ||||||
|  |  | ||||||
|  | #Custom | ||||||
|  | build/bundle.js | ||||||
							
								
								
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | MIT License | ||||||
|  |  | ||||||
|  | Copyright (c) 2018 Rekai Nyangadzayi Musuka | ||||||
|  |  | ||||||
|  | Permission is hereby granted, free of charge, to any person obtaining a copy | ||||||
|  | of this software and associated documentation files (the "Software"), to deal | ||||||
|  | in the Software without restriction, including without limitation the rights | ||||||
|  | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||||
|  | copies of the Software, and to permit persons to whom the Software is | ||||||
|  | furnished to do so, subject to the following conditions: | ||||||
|  |  | ||||||
|  | The above copyright notice and this permission notice shall be included in all | ||||||
|  | copies or substantial portions of the Software. | ||||||
|  |  | ||||||
|  | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||||
|  | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||||
|  | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||||
|  | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||||
|  | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||||
|  | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||||
|  | SOFTWARE. | ||||||
							
								
								
									
										52
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | # Melodii Music Player | ||||||
|  |  | ||||||
|  | ### An Electron Based Music Player | ||||||
|  |  | ||||||
|  | ## Why Was Melodii Music Player Made? | ||||||
|  |  | ||||||
|  | MMP was created as a long term project to introduce me to electron and more complicated programming in general. | ||||||
|  | Before this project I had worked on small projects that never took more than an hour. After seing a friend create a videogame | ||||||
|  | in Java, I felt the need to challenge myself as well... | ||||||
|  |  | ||||||
|  | ## Goals | ||||||
|  |  | ||||||
|  | I wish to have a Operational Music Player that can achieve a couple of things: | ||||||
|  |  | ||||||
|  | -   Good Looking Flat UI | ||||||
|  | -   Ability to Play at the bare minimum: FLAC and MP3. | ||||||
|  | -   Ability to read a directory chosen by user and scan said directory for Music | ||||||
|  | -   Play, Pause, Go Back 30s, Skip Forward and Backwards Controls | ||||||
|  | -   Progress Bar thing that allows you to go back and forward in a song. | ||||||
|  | -   Total song Time & current song time e.g. 3:20/6:23 | ||||||
|  | -   Display Albums with Album Covers | ||||||
|  | -   Have Album Cover in general | ||||||
|  |  | ||||||
|  | Damn This is very ambitous for a beginner like me... | ||||||
|  |  | ||||||
|  | ## TODO | ||||||
|  |  | ||||||
|  | -   UI | ||||||
|  |     -   Design | ||||||
|  |         -   HTML, CSS Written | ||||||
|  |         -   Choose Music From Directory | ||||||
|  |             -   Working Table in which you can select songs from | ||||||
|  |         -   ~~Read Files from directory~~ | ||||||
|  |         -   Pull Metadata from music files. | ||||||
|  |         -   Media Controls | ||||||
|  |         -   ~~Audio Seeking~~ | ||||||
|  | -   Audio Playback | ||||||
|  |     -   Support for: | ||||||
|  |         -   ~~FLAC~~ | ||||||
|  |         -   ~~MP4~~ | ||||||
|  |         -   ~~MP3~~ | ||||||
|  |         -   ~~M4a~~ | ||||||
|  |         -   ~~AAC~~ | ||||||
|  |         -   ~~WAV~~ | ||||||
|  |         -   ~~Ogg~~ | ||||||
|  | -   Sorting of albums | ||||||
|  |     -   By Artist | ||||||
|  |     -   By Title | ||||||
|  |     -   By Album Artist | ||||||
|  | -   Searching of albums | ||||||
|  | -   [last.fm](http://last.fm) scrobbling. | ||||||
|  | -   [Visualizer](https://github.com/paoda/js-visualizer) | ||||||
							
								
								
									
										15
									
								
								build/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								build/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html lang="en"> | ||||||
|  | <head> | ||||||
|  |   <meta charset="UTF-8"> | ||||||
|  |   <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||
|  |   <meta http-equiv="X-UA-Compatible" content="ie=edge"> | ||||||
|  |   <title>Melodii</title> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |   <div class="app"></div> | ||||||
|  |  | ||||||
|  |   <!-- Webpack --> | ||||||
|  |   <script src="./bundle.js"></script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										28
									
								
								main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								main.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | require('dotenv').config(); | ||||||
|  |  | ||||||
|  | const { app, BrowserWindow } = require('electron'); | ||||||
|  |  | ||||||
|  | let win; | ||||||
|  |  | ||||||
|  | function createWindow() { | ||||||
|  |   win = new BrowserWindow({ width: 800, height: 600, frame: false, webPreferences: { webSecurity: false } }); //TODO: Please find a better solution for the love of god. | ||||||
|  |  | ||||||
|  |   if (process.env.NODE_ENV) win.loadURL(`http://localhost:${process.env.PORT}`); | ||||||
|  |   else win.loadFile('./build/index.html'); | ||||||
|  |  | ||||||
|  |   win.webContents.openDevTools(); | ||||||
|  |  | ||||||
|  |   win.on('closed', () => { | ||||||
|  |     win = null; | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | app.on('ready', createWindow); | ||||||
|  |  | ||||||
|  | app.on('window-all-closed', () => { | ||||||
|  |   if (process.platform !== 'darwin') app.quit(); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | app.on('activate', () => { | ||||||
|  |   if (win === null) createWindow(); | ||||||
|  | }); | ||||||
							
								
								
									
										50
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | { | ||||||
|  |   "name": "melodii", | ||||||
|  |   "version": "2018.1226.0", | ||||||
|  |   "description": "Melodii Music Player", | ||||||
|  |   "main": "main.js", | ||||||
|  |   "author": "Rekai Nyangadzayi Musuka", | ||||||
|  |   "license": "MIT", | ||||||
|  |   "scripts": { | ||||||
|  |     "app": "electron .", | ||||||
|  |     "serve": "webpack-dev-server", | ||||||
|  |     "build:prod": "webpack --mode production", | ||||||
|  |     "build:dev": "webpack --mode development", | ||||||
|  |     "start": "run-p serve app", | ||||||
|  |     "start:serverless": "run-p build:dev app", | ||||||
|  |     "test": "node ./node_modules/mocha/bin/mocha" | ||||||
|  |   }, | ||||||
|  |   "private": false, | ||||||
|  |   "devDependencies": { | ||||||
|  |     "@babel/core": "^7.2.2", | ||||||
|  |     "@babel/polyfill": "^7.2.5", | ||||||
|  |     "@babel/preset-env": "^7.2.3", | ||||||
|  |     "@babel/preset-react": "^7.0.0", | ||||||
|  |     "@babel/register": "^7.0.0", | ||||||
|  |     "babel-loader": "^8.0.4", | ||||||
|  |     "chai": "^4.2.0", | ||||||
|  |     "css-loader": "^2.1.0", | ||||||
|  |     "electron": "^4.0.0", | ||||||
|  |     "file-loader": "^3.0.1", | ||||||
|  |     "mocha": "^5.2.0", | ||||||
|  |     "node-sass": "^4.11.0", | ||||||
|  |     "npm-run-all": "^4.1.5", | ||||||
|  |     "sass-loader": "^7.1.0", | ||||||
|  |     "style-loader": "^0.23.1", | ||||||
|  |     "webpack": "^4.28.2", | ||||||
|  |     "webpack-cli": "^3.1.2", | ||||||
|  |     "webpack-dev-server": "^3.1.14" | ||||||
|  |   }, | ||||||
|  |   "dependencies": { | ||||||
|  |     "@fortawesome/fontawesome-svg-core": "^1.2.12", | ||||||
|  |     "@fortawesome/free-solid-svg-icons": "^5.6.3", | ||||||
|  |     "@fortawesome/react-fontawesome": "^0.1.3", | ||||||
|  |     "dotenv": "^6.2.0", | ||||||
|  |     "electron-settings": "^3.2.0", | ||||||
|  |     "lastfm": "^0.9.2", | ||||||
|  |     "lastfmapi": "^0.1.1", | ||||||
|  |     "music-metadata": "^3.3.2", | ||||||
|  |     "react": "^16.7.0", | ||||||
|  |     "react-dom": "^16.7.0" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										134
									
								
								src/App.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								src/App.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | |||||||
|  | import React, { Component } from "react"; | ||||||
|  | import "./scss/App.scss"; | ||||||
|  |  | ||||||
|  | import Header from "./components/Header.js"; | ||||||
|  | import Body from "./components/Body.js"; | ||||||
|  | import SeekBar from "./components/SeekBar.js"; | ||||||
|  | import AlbumArt from "./components/AlbumArt.js"; | ||||||
|  | import Footer from "./components/Footer.js"; | ||||||
|  | import SettingsManager from "./components/Modal/SettingsManager"; | ||||||
|  |  | ||||||
|  | import Song from "./melodii/Song"; | ||||||
|  | import MusicPlayer from "./melodii/MusicPlayer"; | ||||||
|  | import Filepath from "./melodii/Filepath"; | ||||||
|  | import Archive from "./melodii/SongArchive"; | ||||||
|  |  | ||||||
|  | var archive = new Archive(); | ||||||
|  | var mp = new MusicPlayer(); | ||||||
|  |  | ||||||
|  | // const playlist = new Playlist("Test", "\\\\PAODA-SERVER\\Weeb_Home\\Music") | ||||||
|  | // archive.add(null, playlist); | ||||||
|  |  | ||||||
|  | const filepath = new Filepath("C:\\Users\\Paoda\\Downloads"); | ||||||
|  |  | ||||||
|  | (async () => { | ||||||
|  |     let list = await filepath.getValidFiles(); | ||||||
|  |  | ||||||
|  |     let song = new Song(list[~~(Math.random() * list.length)]); | ||||||
|  |     song = await Song.getMetadata(song); | ||||||
|  |     Song.setAlbumArt(song.metadata); | ||||||
|  |  | ||||||
|  |     mp.load(song); | ||||||
|  |     // mp.play(); | ||||||
|  |  | ||||||
|  |     mp.element.onended = async () => { | ||||||
|  |         let song = new Song(list[~~(Math.random() * list.length)]); | ||||||
|  |         song = await Song.getMetadata(song); | ||||||
|  |         Song.setAlbumArt(song.metadata); | ||||||
|  |         mp.load(song); | ||||||
|  |         mp.play(); | ||||||
|  |     }; | ||||||
|  | })(); | ||||||
|  |  | ||||||
|  | class App extends Component { | ||||||
|  |     constructor(props) { | ||||||
|  |         super(props); | ||||||
|  |         this.seekBarFps = 60; //fps | ||||||
|  |         this.textFps = 10; | ||||||
|  |         this.state = { | ||||||
|  |             currentTime: 0, | ||||||
|  |             duration: 100, | ||||||
|  |             title: "", | ||||||
|  |             artist: "", | ||||||
|  |             album: "" | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |     componentDidMount() { | ||||||
|  |         this.seekBarTimer = window.setInterval( | ||||||
|  |             this.checkSeekBar.bind(this), | ||||||
|  |             1000 / this.seekBarFps | ||||||
|  |         ); | ||||||
|  |         this.textTimer = window.setInterval( | ||||||
|  |             this.checkText.bind(this), | ||||||
|  |             1000 / this.textFps | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     componentWillUnmount() { | ||||||
|  |         window.clearInterval(this.seekBarTimer); | ||||||
|  |         window.clearInterval(this.textTimer); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Updates the Seekbar with the current time. */ | ||||||
|  |     checkSeekBar() { | ||||||
|  |         if (archive.currentSongExists()) { | ||||||
|  |             let dur = mp.duration(); | ||||||
|  |             let pos = mp.currentTime(); | ||||||
|  |             if (dur !== this.state.duration) { | ||||||
|  |                 //both position and duration have changed, update both. | ||||||
|  |                 this.setState({ | ||||||
|  |                     currentTime: mp.currentTime() || 0, | ||||||
|  |                     duration: mp.duration() || 0 | ||||||
|  |                 }); | ||||||
|  |             } else if (pos !== this.state.currentTime) { | ||||||
|  |                 this.setState({ | ||||||
|  |                     //only currentTime() has changed | ||||||
|  |                     currentTime: mp.currentTime() || 0 | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Updates the Song Text area with the current Song's Title - Artist | Album Name | ||||||
|  |      * @async | ||||||
|  |      * @return {void} | ||||||
|  |      */ | ||||||
|  |     async checkText() { | ||||||
|  |         if (archive.currentSongExists()) { | ||||||
|  |             let song = archive.getCurrentSong(); | ||||||
|  |             if (!song.metadata) { | ||||||
|  |                 song = await Song.getMetadata(song); | ||||||
|  |                 console.log("Async checkText() was run"); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (song.metadata.common.title !== this.state.title) { | ||||||
|  |                 //if title's changed might as well re render entire thing. | ||||||
|  |                 this.setState({ | ||||||
|  |                     title: song.metadata.common.title || "", | ||||||
|  |                     artist: song.metadata.common.artist || "", | ||||||
|  |                     album: song.metadata.common.album || "" | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     render() { | ||||||
|  |         return ( | ||||||
|  |             <div className="melodiiContainer"> | ||||||
|  |                 <Header /> | ||||||
|  |                 <Body /> | ||||||
|  |                 <SeekBar | ||||||
|  |                     currentTime={this.state.currentTime} | ||||||
|  |                     duration={this.state.duration} | ||||||
|  |                 /> | ||||||
|  |                 <AlbumArt /> | ||||||
|  |                 <Footer | ||||||
|  |                     title={this.state.title} | ||||||
|  |                     artist={this.state.artist} | ||||||
|  |                     album={this.state.album} | ||||||
|  |                 /> | ||||||
|  |                 <SettingsManager /> | ||||||
|  |             </div> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default App; | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/fonts/Roboto-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/fonts/Roboto-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/noalbumart.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/img/noalbumart.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 36 KiB | 
							
								
								
									
										32
									
								
								src/components/AlbumArt.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/components/AlbumArt.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import noalbumart from '../assets/img/noalbumart.png'; | ||||||
|  | import Emitter from '../melodii/Events'; | ||||||
|  |  | ||||||
|  | export default class AlbumArt extends React.Component { | ||||||
|  |     constructor() { | ||||||
|  |         super(); | ||||||
|  |         this.state = {albumArt: noalbumart}; | ||||||
|  |  | ||||||
|  |         this.handleEvents(); | ||||||
|  |     } | ||||||
|  |     shouldComponentUpdate(nextprops, nextState) { | ||||||
|  |         return this.state.albumArt !== nextState.albumArt; | ||||||
|  |     } | ||||||
|  |     render() { | ||||||
|  |         console.log("Album Art Updated"); | ||||||
|  |         return ( | ||||||
|  |             <div id='albumContainer'> | ||||||
|  |                 <img alt='Album-Art' src={this.state.albumArt} id='albumImg'></img> | ||||||
|  |             </div> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     /** | ||||||
|  |      * @listens Song#updateAlbumArt Updates Album Art  | ||||||
|  |      */ | ||||||
|  |     handleEvents() { | ||||||
|  |         Emitter.on('updateAlbumArt', (blob, err) => { | ||||||
|  |             if (err) throw err; | ||||||
|  |             this.setState({albumArt: blob}); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										236
									
								
								src/components/Body.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								src/components/Body.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,236 @@ | |||||||
|  | import React from "react"; | ||||||
|  | import Table from "./Body/Table"; | ||||||
|  | import Song from "../melodii/Song"; | ||||||
|  | import Filepath from "../melodii/Filepath"; | ||||||
|  | import Misc from "./MiscMethods"; | ||||||
|  | import Emitter from "../melodii/Events"; | ||||||
|  | import Modal from "./Modal"; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const Settings = window.require("electron-settings"); | ||||||
|  |  | ||||||
|  | export default class Body extends React.Component { | ||||||
|  |     constructor() { | ||||||
|  |         super(); | ||||||
|  |         this.state = { | ||||||
|  |             table: null, | ||||||
|  |             msg: "" | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @listens Table#newTable loads new Table*/ | ||||||
|  |     handleEvents() { | ||||||
|  |         Emitter.on("newTable", (obj, err) => { | ||||||
|  |             if (err) throw err; | ||||||
|  |             this.setState({ table: obj }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     componentWillMount() { | ||||||
|  |         this.initialize(); | ||||||
|  |     } | ||||||
|  |     shouldComponentUpdate(nextProps, nextState) { | ||||||
|  |         return this.state.table !== nextState.table; | ||||||
|  |     } | ||||||
|  |     render() { | ||||||
|  |         return ( | ||||||
|  |             <div className="wrapper"> | ||||||
|  |                 <div id="searchBar"> | ||||||
|  |                     <input | ||||||
|  |                         type="text" | ||||||
|  |                         placeholder="Search..." | ||||||
|  |                         onKeyUp={this.checkKey.bind(this)} | ||||||
|  |                         tabIndex="0" | ||||||
|  |                     /> | ||||||
|  |                     <input | ||||||
|  |                         type="button" | ||||||
|  |                         onClick={this.openSettings.bind(this)} | ||||||
|  |                         value="Settings" | ||||||
|  |                     /> | ||||||
|  |                     <span id="bad-search-syntax" /> | ||||||
|  |                 </div> | ||||||
|  |                 <Table table={this.state.table} /> | ||||||
|  |             </div> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Initializes Body Element (Therefore Table too) @async @return {void}*/ | ||||||
|  |     async initialize() { | ||||||
|  |         this.handleEvents.bind(this); | ||||||
|  |         this.handleEvents(); | ||||||
|  |  | ||||||
|  |         let template = { | ||||||
|  |             thead: { | ||||||
|  |                 tr: ["Artist", "Title", "Album", "Year", "Genre", "Time"] //contains <th> strings | ||||||
|  |             }, | ||||||
|  |             tbody: [] | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         if (!Settings.has("tableJSON")) { | ||||||
|  |             let tableJSON = await this.generate(template); | ||||||
|  |  | ||||||
|  |             this.setState({ | ||||||
|  |                 table: tableJSON | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             let timestamp = Date.now(); | ||||||
|  |             Settings.set("tableJSON", { | ||||||
|  |                 data: tableJSON, | ||||||
|  |                 timestamp: timestamp | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             let date = new Date(timestamp); | ||||||
|  |             console.log( | ||||||
|  |                 "Table data created at: " + | ||||||
|  |                     date.toDateString() + | ||||||
|  |                     " at " + | ||||||
|  |                     date.toTimeString() | ||||||
|  |             ); | ||||||
|  |         } else { | ||||||
|  |             console.log("Data Loaded from Persistent Storage Space"); | ||||||
|  |  | ||||||
|  |             this.setState({ | ||||||
|  |                 table: Settings.get("tableJSON").data | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             let date = new Date(Settings.get("tableJSON").timestamp); | ||||||
|  |             console.log( | ||||||
|  |                 "Table data created on: " + | ||||||
|  |                     date.toDateString() + | ||||||
|  |                     " at " + | ||||||
|  |                     date.toTimeString() | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Generates Table From Scratch | ||||||
|  |      * @param {Array<String>} template | ||||||
|  |      * @return {Promise<}  | ||||||
|  |      * @async | ||||||
|  |      */ | ||||||
|  |     async generate(template) { | ||||||
|  |         let filepath = new Filepath("C:\\Users\\Paoda\\Downloads"); | ||||||
|  |         let list = await filepath.getValidFiles(); | ||||||
|  |         console.log("Found " + list.length + "valid files."); | ||||||
|  |         return new Promise(async (res, rej) => { | ||||||
|  |             let temp = await this.generateBody( | ||||||
|  |                 template, | ||||||
|  |                 list, | ||||||
|  |                 0, | ||||||
|  |                 list.length - 1 | ||||||
|  |             ); | ||||||
|  |             res(temp); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Generates Body of Table from Scratch  | ||||||
|  |      * @param {Object} tableArg Table Element  | ||||||
|  |      * @param {Array<String>} arr Array of Valid Song Files  | ||||||
|  |      * @param {Number} start of Array | ||||||
|  |      * @param {Number} end of Array | ||||||
|  |      * @return {Object} Table Object with Body Completely parsed. | ||||||
|  |      * @async | ||||||
|  |      */ | ||||||
|  |     async generateBody(tableArg, arr, start, end) { | ||||||
|  |         let table = tableArg; | ||||||
|  |         let t1 = performance.now(); | ||||||
|  |         let dom = document.getElementById("bad-search-syntax"); | ||||||
|  |         for (let i = 0; i <= end; i++) { | ||||||
|  |             let song = new Song(arr[i]); | ||||||
|  |             song = await Song.getMetadata(song); | ||||||
|  |             table.tbody.push(Misc.formatMetadata(song, song.metadata)); | ||||||
|  |             dom.innerHTML = "Creating Table Data: " + ~~((i / end) * 100) + "%"; | ||||||
|  |         } | ||||||
|  |         let t2 = performance.now(); | ||||||
|  |         console.log( | ||||||
|  |             "Time Taken (Table Data Creation): " + | ||||||
|  |                 Math.floor(t2 - t1) / 1000 + | ||||||
|  |                 "s" | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         return new Promise((res, rej) => { | ||||||
|  |             res(table); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     | ||||||
|  |     /** | ||||||
|  |      * Handles Key Presses | ||||||
|  |      * @param {KeyboardEvent} e  | ||||||
|  |      */ | ||||||
|  |     checkKey(e) { | ||||||
|  |         if (e.keyCode === 13) this.search(e.target.value); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Searches for Elements using a the provided String Parameter as an argument. | ||||||
|  |      * - Directly Manipulates the State of Body once it's done so it returns void | ||||||
|  |      * @param {String} string  | ||||||
|  |      */ | ||||||
|  |     search(string) { | ||||||
|  |         const table = Settings.get("tableJSON").data; | ||||||
|  |         console.log("Search Text: " + string); | ||||||
|  |  | ||||||
|  |         if (string === "") { | ||||||
|  |             this.setState({ table: table }); | ||||||
|  |         } else if (string.includes(":")) { | ||||||
|  |             let type = string.match(/^(.*):/)[0]; | ||||||
|  |             type = type.substr(0, type.length - 1); | ||||||
|  |  | ||||||
|  |             let term = string.match(/:( ?.*)$/)[0]; | ||||||
|  |             if (term[1] === " ") term = term.substr(2, term.length); | ||||||
|  |             else term = term.substr(1, term.length); | ||||||
|  |  | ||||||
|  |             type = type.toLowerCase(); | ||||||
|  |  | ||||||
|  |             let temp = { | ||||||
|  |                 tbody: null, | ||||||
|  |                 thead: table.thead | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             if (type === "title") { | ||||||
|  |                 term = term.toLowerCase(); | ||||||
|  |                 temp.tbody = table.tbody.filter(obj => | ||||||
|  |                     obj.title.toLowerCase().includes(term) | ||||||
|  |                 ); | ||||||
|  |             } else if (type === "artist") { | ||||||
|  |                 term = term.toLowerCase(); | ||||||
|  |                 temp.tbody = table.tbody.filter(obj => | ||||||
|  |                     obj.artist.toLowerCase().includes(term) | ||||||
|  |                 ); | ||||||
|  |             } else if (type === "album") { | ||||||
|  |                 term = term.toLowerCase(); | ||||||
|  |                 temp.tbody = table.tbody.filter(obj => | ||||||
|  |                     obj.album.toLowerCase().includes(term) | ||||||
|  |                 ); | ||||||
|  |             } else if (type === "genre") { | ||||||
|  |                 term = term.toLowerCase(); | ||||||
|  |                 temp.tbody = table.tbody.filter(obj => | ||||||
|  |                     obj.genre.toLowerCase().includes(term) | ||||||
|  |                 ); | ||||||
|  |             } else if (type === "year") { | ||||||
|  |                 term = parseInt(term, 10); | ||||||
|  |                 temp.tbody = table.tbody.filter(obj => obj.year === term); | ||||||
|  |             } else { | ||||||
|  |                 // type == time | ||||||
|  |                 term = term.toLowerCase(); | ||||||
|  |                 temp.tbody = table.tbody.filter(obj => | ||||||
|  |                     obj.time.toLowerCase().includes(term) | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |             this.setState({ table: temp }); | ||||||
|  |             let error = document.getElementById("bad-search-syntax"); | ||||||
|  |             if (error.innerHTML !== "") error.innerHTML = ""; | ||||||
|  |             console.log("Search found: " + temp.tbody.length + " Songs"); | ||||||
|  |         } else { | ||||||
|  |             document.getElementById("bad-search-syntax").innerHTML = | ||||||
|  |                 "Invalid Syntax!"; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     openSettings() { | ||||||
|  |         const settings = document.querySelector('.settings-window'); | ||||||
|  |         Emitter.emit('loadModal'); | ||||||
|  |  | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										179
									
								
								src/components/Body/Table.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/components/Body/Table.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | |||||||
|  | import React from "react"; | ||||||
|  | import Song from "../../melodii/Song"; | ||||||
|  | import MusicPlayer from "../../melodii/MusicPlayer"; | ||||||
|  | import Misc from "../MiscMethods"; | ||||||
|  | import Emitter from "../../melodii/Events"; | ||||||
|  |  | ||||||
|  | /** @type {HTMLElement} */ | ||||||
|  | var active = document.createElement("tr"); | ||||||
|  | active.classList.toggle("active"); | ||||||
|  |  | ||||||
|  | const mp = new MusicPlayer(); | ||||||
|  | var JSXcache; | ||||||
|  |  | ||||||
|  | /** The React Component Responsible for Rendering the Song Table */ | ||||||
|  | export default class Table extends React.Component { | ||||||
|  |  | ||||||
|  |     /** Throttles the interval of which the class re-renders when resizing.  | ||||||
|  |      * @param {Object} props */ | ||||||
|  |     constructor(props) { | ||||||
|  |         super(props); | ||||||
|  |         let self = this; | ||||||
|  |         let throttle; | ||||||
|  |         window.onresize = e => { | ||||||
|  |             window.clearTimeout(throttle); | ||||||
|  |             throttle = setTimeout(() => { | ||||||
|  |                 self.forceUpdate(); | ||||||
|  |             }, 250); | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Method responsible for Initializing Table.headJSX and Table.bodyJSX, generating the Table JSX. | ||||||
|  |      * @param {Object} table Obejct of Elements, Was once JSON (usually) | ||||||
|  |      */ | ||||||
|  |     initialize(table) { | ||||||
|  |         this.headJSX = this.parseHead(table); | ||||||
|  |         this.bodyJSX = this.parseBody(table); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     render() { | ||||||
|  |         if (this.props.table) { | ||||||
|  |             if (this.props.table !== JSXcache) { | ||||||
|  |                 this.initialize(this.props.table); | ||||||
|  |                 console.log("Table Rendered from Scratch"); | ||||||
|  |                 JSXcache = this.props.table; | ||||||
|  |                 return ( | ||||||
|  |                     <table id="songTable"> | ||||||
|  |                         <thead> | ||||||
|  |                             <tr>{this.headJSX}</tr> | ||||||
|  |                         </thead> | ||||||
|  |                         <tbody>{this.bodyJSX}</tbody> | ||||||
|  |                     </table> | ||||||
|  |                 ); | ||||||
|  |             } else { | ||||||
|  |                 return ( | ||||||
|  |                     <table id="songTable"> | ||||||
|  |                         <thead> | ||||||
|  |                             <tr>{this.headJSX}</tr> | ||||||
|  |                         </thead> | ||||||
|  |                         <tbody>{this.bodyJSX}</tbody> | ||||||
|  |                     </table> | ||||||
|  |                 ); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             return <table id="songTable" />; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Parses the Head Portion of the Table | ||||||
|  |      * @param {Object} table  | ||||||
|  |      * @return {Array<HTMLTableHeaderCellElement>} | ||||||
|  |      */ | ||||||
|  |     parseHead(table) { | ||||||
|  |         let arr = table.thead.tr; | ||||||
|  |         return arr.map(string => ( | ||||||
|  |             <th | ||||||
|  |                 key={string} | ||||||
|  |                 onClick={this.handleSort.bind(this, table, string)}> | ||||||
|  |                 {string} | ||||||
|  |             </th> | ||||||
|  |         )); | ||||||
|  |     } | ||||||
|  |     /** | ||||||
|  |      * Handles Sorting the table by a Album, Title, Year etc.. | ||||||
|  |      * @fires Table#newTable Event to Generate a new Table | ||||||
|  |      * @param {Object} table  | ||||||
|  |      * @param {String} term  | ||||||
|  |      */ | ||||||
|  |     handleSort(table, term) { | ||||||
|  |         const temp = table; | ||||||
|  |         Emitter.emit("newTable", Misc.sortTable(temp, term)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Parses the Body Portion of the Table Object | ||||||
|  |      * @param {Object} table | ||||||
|  |      * @return {Array<HTMLTableRowElement>} | ||||||
|  |      */ | ||||||
|  |     parseBody(table) { | ||||||
|  |         let arr = table.tbody; | ||||||
|  |         let clientWidth = document.documentElement.clientWidth; | ||||||
|  |         let innerWidth = window.innerWidth || 0; | ||||||
|  |         let maxWidth = Math.max(clientWidth, innerWidth) / 6; | ||||||
|  |  | ||||||
|  |         let temp = arr.map(obj => ( | ||||||
|  |             <tr | ||||||
|  |                 key={obj.location} | ||||||
|  |                 data-filepath={obj.location} | ||||||
|  |                 onClick={this.handleClick.bind(this)} | ||||||
|  |                 onKeyDown={this.handleKeyDown.bind(this)} | ||||||
|  |                 tabIndex="0"> | ||||||
|  |                 <td id="text"> | ||||||
|  |                     {Misc.truncateText(obj.artist, maxWidth, "Roboto")} | ||||||
|  |                 </td> | ||||||
|  |                 <td id="text"> | ||||||
|  |                     {Misc.truncateText(obj.title, maxWidth, "Roboto")} | ||||||
|  |                 </td> | ||||||
|  |                 <td id="text"> | ||||||
|  |                     {Misc.truncateText(obj.album, maxWidth, "Roboto")} | ||||||
|  |                 </td> | ||||||
|  |                 <td id="number"> | ||||||
|  |                     {Misc.truncateText(obj.year, maxWidth, "Roboto")} | ||||||
|  |                 </td> | ||||||
|  |                 <td id="text"> | ||||||
|  |                     {Misc.truncateText(obj.genre, maxWidth, "Roboto")} | ||||||
|  |                 </td> | ||||||
|  |                 <td id="number"> | ||||||
|  |                     {Misc.truncateText(obj.time, maxWidth, "Roboto")} | ||||||
|  |                 </td> | ||||||
|  |             </tr> | ||||||
|  |         )); | ||||||
|  |         return temp; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Handles a Click on the Table | ||||||
|  |      * @param {MouseEvent} e | ||||||
|  |      * @return {void} | ||||||
|  |      * @async | ||||||
|  |      */ | ||||||
|  |     async handleClick(e) { | ||||||
|  |         if (active !== e.currentTarget) { | ||||||
|  |             active.classList.toggle("active"); | ||||||
|  |             e.currentTarget.classList.toggle("active"); | ||||||
|  |             active = e.currentTarget; | ||||||
|  |         } else { | ||||||
|  |             let filepath = e.currentTarget.dataset.filepath; | ||||||
|  |  | ||||||
|  |             let song = new Song(filepath); | ||||||
|  |             mp.load(song); | ||||||
|  |             mp.play(); | ||||||
|  |  | ||||||
|  |             song = await Song.getMetadata(song); | ||||||
|  |             Song.setAlbumArt(song.metadata); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Handles a KeyDown Event | ||||||
|  |      * @param {KeyboardEvent} e | ||||||
|  |      * @async | ||||||
|  |      */ | ||||||
|  |     async handleKeyDown(e) { | ||||||
|  |         // console.log("Focus:" + e.currentTarget.dataset.filepath + " Key: " + e.key); | ||||||
|  |  | ||||||
|  |         if (e.keyCode === 13 && e.currentTarget === active) { | ||||||
|  |             //Active and Presses Enter | ||||||
|  |             let filepath = e.currentTarget.dataset.filepath; | ||||||
|  |  | ||||||
|  |             let song = new Song(filepath); | ||||||
|  |             mp.load(song); | ||||||
|  |             mp.play(); | ||||||
|  |  | ||||||
|  |             song = await Song.getMetadata(song); | ||||||
|  |             Song.setAlbumArt(song.metadata); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										22
									
								
								src/components/Footer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/components/Footer.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import SongInfo from './Footer/SongInfo'; | ||||||
|  |  | ||||||
|  | import PlayPause from './Footer/PlayPause'; | ||||||
|  | import Volume from './Footer/Volume'; | ||||||
|  | export default class Footer extends React.Component { | ||||||
|  |     render() { | ||||||
|  |         return ( | ||||||
|  |             <footer> | ||||||
|  |                 <div className='mediaControls'> | ||||||
|  |                     {/* <SkipBkwd /> */} | ||||||
|  |                     <PlayPause /> | ||||||
|  |                     {/* <SkipFwd /> */} | ||||||
|  |                     <Volume /> | ||||||
|  |                 </div> | ||||||
|  |                 <div className='songInfo'> | ||||||
|  |                     <SongInfo title={this.props.title} artist={this.props.artist} album={this.props.album} /> | ||||||
|  |                 </div> | ||||||
|  |             </footer> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								src/components/Footer/Mute.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/components/Footer/Mute.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | // import '@fortawesome/fontawesome-free/css/all.css'; | ||||||
|  | import { library } from '@fortawesome/fontawesome-svg-core'; | ||||||
|  | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' | ||||||
|  | import { faVolumeUp } from '@fortawesome/free-solid-svg-icons'; | ||||||
|  |  | ||||||
|  | library.add(faVolumeUp) | ||||||
|  |  | ||||||
|  | export default class Mute extends React.Component { | ||||||
|  |     render() { | ||||||
|  |         return ( | ||||||
|  |             // <i className= 'fa fa-volume-up' id='muteIcon'></i> | ||||||
|  |             <FontAwesomeIcon icon="faVolumeUp" id='muteIcon' /> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										52
									
								
								src/components/Footer/PlayPause.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/components/Footer/PlayPause.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | import React from "react"; | ||||||
|  | import Emitter from "../../melodii/Events"; | ||||||
|  | import MusicPlayer from "../../melodii/MusicPlayer"; | ||||||
|  | // import "@fortawesome/fontawesome-free/css/all.css"; | ||||||
|  |  | ||||||
|  | import { library } from '@fortawesome/fontawesome-svg-core'; | ||||||
|  | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | ||||||
|  | import { faPause, faPlay } from '@fortawesome/free-solid-svg-icons' | ||||||
|  |  | ||||||
|  |  | ||||||
|  | library.add(faPause); | ||||||
|  | library.add(faPlay); | ||||||
|  |  | ||||||
|  | const mp = new MusicPlayer(); | ||||||
|  | export default class PlayPause extends React.Component { | ||||||
|  |  | ||||||
|  |     /** @listens */ | ||||||
|  |     constructor(props) { | ||||||
|  |         super(props); | ||||||
|  |  | ||||||
|  |         Emitter.on("toggle", bool => this.handleEvent(bool)); | ||||||
|  |  | ||||||
|  |         this.state = { icon: "pause" }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     handleClick() { | ||||||
|  |         //Updates Icon and controls the Audio came from clicking on button | ||||||
|  |         if (this.state.icon === "pause") { | ||||||
|  |             //set to play | ||||||
|  |             mp.pause(); | ||||||
|  |             this.setState({ icon: "play" }); | ||||||
|  |         } else { | ||||||
|  |             //set to pause | ||||||
|  |             mp.play(); | ||||||
|  |             this.setState({ icon: "pause" }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Updates Icon From Event | ||||||
|  |      * @param {Boolean} bool  | ||||||
|  |      */ | ||||||
|  |     handleEvent(bool) { | ||||||
|  |         if (!bool) this.setState({ icon: "play" }); | ||||||
|  |         else this.setState({ icon: "pause" }); | ||||||
|  |     } | ||||||
|  |     render() { | ||||||
|  |         return (  | ||||||
|  |             <FontAwesomeIcon icon={this.state.icon} id="playPause" onClick={this.handleClick.bind(this)} /> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								src/components/Footer/SongInfo.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/components/Footer/SongInfo.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | import React from 'react'; | ||||||
|  |  | ||||||
|  | export default class SongInfo extends React.Component { | ||||||
|  |     render() { | ||||||
|  |         return ( | ||||||
|  |         <span>{this.props.title} - {this.props.artist} | {this.props.album}</span> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										48
									
								
								src/components/Footer/Volume.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/components/Footer/Volume.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | import React from "react"; | ||||||
|  | import MusicPlayer from "../../melodii/MusicPlayer"; | ||||||
|  |  | ||||||
|  | const Settings = window.require("electron-settings"); | ||||||
|  | const mp = new MusicPlayer(); | ||||||
|  |  | ||||||
|  | export default class Volume extends React.Component { | ||||||
|  |     constructor() { | ||||||
|  |         super(); | ||||||
|  |         this.state = { | ||||||
|  |             input: Settings.get("Volume") || 50, | ||||||
|  |             max: 50 | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |     render() { | ||||||
|  |         return ( | ||||||
|  |             <input | ||||||
|  |                 style={{ | ||||||
|  |                     backgroundSize: | ||||||
|  |                         (this.state.input * 100) / this.state.max + "% 100%" | ||||||
|  |                 }} | ||||||
|  |                 value={this.state.input} | ||||||
|  |                 id="volBar" | ||||||
|  |                 type="range" | ||||||
|  |                 max={this.state.max} | ||||||
|  |                 onChange={this.setVolume.bind(this)} | ||||||
|  |                 onMouseUp={this.setLastVolume.bind(this)} | ||||||
|  |             /> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     /** | ||||||
|  |      * Sets Volume | ||||||
|  |      * @param {Event} e  | ||||||
|  |      */ | ||||||
|  |     setVolume(e) { | ||||||
|  |         this.setState({ | ||||||
|  |             input: e.target.value | ||||||
|  |         }); | ||||||
|  |         mp.setVolume(e.target.value / 50); | ||||||
|  |     } | ||||||
|  |     /** | ||||||
|  |      * Saves the Previous Volume | ||||||
|  |      * @param {Event} e  | ||||||
|  |      */ | ||||||
|  |     setLastVolume(e) { | ||||||
|  |         Settings.set("Volume", e.target.value); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								src/components/Header.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/components/Header.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import Minimize from './Header/Minimize'; | ||||||
|  | import Quit from './Header/Quit'; | ||||||
|  |  | ||||||
|  | export default class Header extends React.Component { | ||||||
|  |     shouldComponentUpdate() { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  |     render() { | ||||||
|  |         return ( | ||||||
|  |             <header> | ||||||
|  |                 <span>Melodii Music Player</span> | ||||||
|  |                 <div id='headerIcons'> | ||||||
|  |                     <Minimize /> | ||||||
|  |                     <Quit /> | ||||||
|  |                 </div> | ||||||
|  |             </header> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								src/components/Header/Minimize.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/components/Header/Minimize.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import Buttons from '../../melodii/Buttons'; | ||||||
|  | // import '@fortawesome/fontawesome-free/css/all.css'; | ||||||
|  |  | ||||||
|  | import { library } from '@fortawesome/fontawesome-svg-core'; | ||||||
|  | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | ||||||
|  | import { faWindowMinimize } from '@fortawesome/free-solid-svg-icons' | ||||||
|  |  | ||||||
|  | library.add(faWindowMinimize) | ||||||
|  |  | ||||||
|  | export default class Minimize extends React.Component { | ||||||
|  |     render() { | ||||||
|  |         return ( | ||||||
|  |             <FontAwesomeIcon icon="window-minimize" onClick={this.minimize} />  | ||||||
|  |             // <i onClick={this.minimize} className= 'fa fa-window-minimize'></i> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     minimize() { | ||||||
|  |         Buttons.minimize(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								src/components/Header/Quit.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/components/Header/Quit.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import Buttons from '../../melodii/Buttons'; | ||||||
|  | // import '@fortawesome/fontawesome-free/css/all.css'; | ||||||
|  |  | ||||||
|  | import { library } from '@fortawesome/fontawesome-svg-core'; | ||||||
|  | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | ||||||
|  | import { faTimes } from '@fortawesome/free-solid-svg-icons' | ||||||
|  |  | ||||||
|  | library.add(faTimes); | ||||||
|  |  | ||||||
|  | export default class Quit extends React.Component { | ||||||
|  |     render() { | ||||||
|  |         return ( | ||||||
|  |             // <i onClick={this.quit} className= 'fa fa-times'></i> | ||||||
|  |             <FontAwesomeIcon icon="times" onClick={this.quit} /> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |     quit() { | ||||||
|  |         Buttons.quit(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										324
									
								
								src/components/MiscMethods.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								src/components/MiscMethods.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,324 @@ | |||||||
|  | import Song from "../melodii/Song"; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * - Every Method in this file must be Static and _probably_ Synchronous | ||||||
|  |  * - The Methods contained in this class must only be methods that don't really fit anywhere else | ||||||
|  |  * - Any Functions that require the use of Fs are not allowd in this Class. | ||||||
|  |  */ | ||||||
|  | export default class MiscMethods { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Finds the Mode of a Set of Numbers | ||||||
|  |      * @param {Array<Number>} arr | ||||||
|  |      * @return {Number} The Mode | ||||||
|  |      * @static  | ||||||
|  |      */ | ||||||
|  |     static mode(arr) { | ||||||
|  |         //https://codereview.stackexchange.com/a/68431 | ||||||
|  |         return arr.reduce( | ||||||
|  |             function(current, item) { | ||||||
|  |                 var val = (current.numMapping[item] = | ||||||
|  |                     (current.numMapping[item] || 0) + 1); | ||||||
|  |                 if (val > current.greatestFreq) { | ||||||
|  |                     current.greatestFreq = val; | ||||||
|  |                     current.mode = item; | ||||||
|  |                 } | ||||||
|  |                 return current; | ||||||
|  |             }, | ||||||
|  |             { mode: null, greatestFreq: -Infinity, numMapping: {} }, | ||||||
|  |             arr | ||||||
|  |         ).mode; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Finds the Median of a Set of Numbers | ||||||
|  |      * @param {Array<Number>} arr  | ||||||
|  |      * @return {Number} The Median | ||||||
|  |      * @static | ||||||
|  |      */ | ||||||
|  |     static median(arr) { | ||||||
|  |         arr.sort((a, b) => { | ||||||
|  |             return a - b; | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         let half = ~~(arr.length / 2); | ||||||
|  |  | ||||||
|  |         if (arr.length % 2) return arr[half]; | ||||||
|  |         else return (arr[half - 1] + arr[half]) / 2.0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Finds the Average of a Set of Numbers | ||||||
|  |      * @param {Array<Number>} arr | ||||||
|  |      * @return {Number} The Average | ||||||
|  |      * @static | ||||||
|  |      */ | ||||||
|  |     static average(arr) { | ||||||
|  |         let total = 0; | ||||||
|  |         for (let i = 0; i < arr.length; i++) { | ||||||
|  |             total += arr[i]; | ||||||
|  |         } | ||||||
|  |         return total / arr.length; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Stack Overflow: https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 | ||||||
|  |      *  | ||||||
|  |      * This Method finds how much hortizontal space text takes up (UTF8 Compliant) using the HTML5 Canvas | ||||||
|  |      *  | ||||||
|  |      * @param {String} text Text to Measure | ||||||
|  |      * @param {String} font Font of Text | ||||||
|  |      * @param {*} cnvs  Cached Canvas (if it exists) | ||||||
|  |      * @return {Number} Width of String of Text | ||||||
|  |      * @static | ||||||
|  |      */ | ||||||
|  |     static measureText(text, font, cnvs) { | ||||||
|  |         // let canvas = | ||||||
|  |         //   self.canvas || (self.canvas = document.createElement("canvas")); | ||||||
|  |         // let ctx = canvas.getContext("2d"); | ||||||
|  |  | ||||||
|  |         let ctx; | ||||||
|  |         let canvas = cnvs; | ||||||
|  |         if (canvas) ctx = canvas.getContext("2d"); | ||||||
|  |         else { | ||||||
|  |             canvas = document.createElement("canvas"); | ||||||
|  |             ctx = canvas.getContext("2d"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ctx.font = font; | ||||||
|  |         let metrics = ctx.measureText(text); | ||||||
|  |         return metrics.width; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * This Method Truncates Text given the amount of available horizontal space and the font of the text desired so that | ||||||
|  |      * All the text fits onto one line. If truncated the String ends up looking like thi... | ||||||
|  |      *  | ||||||
|  |      *  | ||||||
|  |      * @param {String} text Text to be Truncated | ||||||
|  |      * @param {Number} maxWidth How much Horizontal Space is available to be used up by text. | ||||||
|  |      * @param {String} font Name of Font | ||||||
|  |      * @return {String} Truncated Text | ||||||
|  |      * @static | ||||||
|  |      */ | ||||||
|  |     static truncateText(text, maxWidth, font) { | ||||||
|  |         let canvas = document.createElement("canvas"); | ||||||
|  |  | ||||||
|  |         let width = MiscMethods.measureText(text, font, canvas); | ||||||
|  |  | ||||||
|  |         if (width > maxWidth) { | ||||||
|  |             //text needs truncating... | ||||||
|  |             let charWidths = []; | ||||||
|  |             let ellipsisWidth = MiscMethods.measureText("...", font, canvas); | ||||||
|  |  | ||||||
|  |             //get Average width of every char in string | ||||||
|  |             for (let char in text) | ||||||
|  |                 if (typeof char === "string") | ||||||
|  |                     charWidths.push(MiscMethods.measureText(char, font)); | ||||||
|  |  | ||||||
|  |             // let charWidth = this.median(charWidths); | ||||||
|  |             let charWidth = MiscMethods.average(charWidths); | ||||||
|  |             // let charWidth = this.mode(charWidths); | ||||||
|  |  | ||||||
|  |             //Find out how many of these characters fit in max Width; | ||||||
|  |             let maxChars = (maxWidth - ellipsisWidth) / charWidth; | ||||||
|  |  | ||||||
|  |             let truncated = ""; | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 truncated = text.substr(0, maxChars); | ||||||
|  |             } catch (e) { | ||||||
|  |                 // console.warn('\n' + e + ' ASSUMPTION: Melodii width shrunk to extremely small proportions'); | ||||||
|  |                 // console.warn('Text: "' + text + '"\nMaximum Width: ' + maxWidth + 'px.\nMaximum Space for Characters: ' + maxChars + 'px.'); | ||||||
|  |             } | ||||||
|  |             return truncated + "..."; | ||||||
|  |         } else return text; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * This function takes a Song and the Metadata of said Song and formats it so that it can be easlily processed by Table Generation. | ||||||
|  |      * @param {Song} song  | ||||||
|  |      * @param {Object} metadata | ||||||
|  |      * @return {Object} The Formateed Metadata | ||||||
|  |      * @static  | ||||||
|  |      */ | ||||||
|  |     static formatMetadata(song, metadata) { | ||||||
|  |         let format = metadata.format; | ||||||
|  |         let common = metadata.common; | ||||||
|  |         let min = ~~((format.duration % 3600) / 60); | ||||||
|  |         let sec = ~~(format.duration % 60); | ||||||
|  |         if (sec < 10) sec = "0" + sec; | ||||||
|  |         let time = min + ":" + sec; | ||||||
|  |  | ||||||
|  |         return { | ||||||
|  |             location: song.location, | ||||||
|  |             time: time, | ||||||
|  |             artist: common.artist || "", | ||||||
|  |             title: common.title || "", | ||||||
|  |             album: common.album || "", | ||||||
|  |             year: common.year || "", | ||||||
|  |             genre: common.genre ? common.genre.toString() : "", | ||||||
|  |             inSeconds: format.duration | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Sorts a Table based on a Term Given to the Method | ||||||
|  |      *  | ||||||
|  |      * @param {Object} table Table Object | ||||||
|  |      * @param {*} term Sort Term | ||||||
|  |      * @return {Object} Processed Table Object | ||||||
|  |      * @static | ||||||
|  |      *  | ||||||
|  |      */ | ||||||
|  |     static sortTable(table, term) { | ||||||
|  |         term = term.toLowerCase(); | ||||||
|  |         let tbody = table.tbody.slice(); | ||||||
|  |  | ||||||
|  |         let res = { | ||||||
|  |             thead: { | ||||||
|  |                 tr: table.thead.tr.slice() | ||||||
|  |             }, | ||||||
|  |             tbody: null | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         if (term === "title") { | ||||||
|  |             tbody.sort((a, b) => { | ||||||
|  |                 // turns ["   Uptown Funk"] into ["Uptown Funk"] | ||||||
|  |                 let fixedStr = MiscMethods.removeLeadingWhitespaces( | ||||||
|  |                     a.title, | ||||||
|  |                     b.title | ||||||
|  |                 ); | ||||||
|  |                 a.title = fixedStr[0]; | ||||||
|  |                 b.title = fixedStr[1]; | ||||||
|  |  | ||||||
|  |                 if (a.title < b.title) return -1; | ||||||
|  |                 else if (a.title > b.title) return 1; | ||||||
|  |                 else return 0; | ||||||
|  |             }); | ||||||
|  |         } else if (term === "artist") { | ||||||
|  |             tbody.sort((a, b) => { | ||||||
|  |                 let fixedStr = MiscMethods.removeLeadingWhitespaces( | ||||||
|  |                     a.artist, | ||||||
|  |                     b.artist | ||||||
|  |                 ); | ||||||
|  |                 a.artist = fixedStr[0]; | ||||||
|  |                 b.artist = fixedStr[1]; | ||||||
|  |  | ||||||
|  |                 if (a.artist < b.artist) return -1; | ||||||
|  |                 else if (a.artist > b.artist) return 1; | ||||||
|  |                 else return 0; | ||||||
|  |             }); | ||||||
|  |         } else if (term === "album") { | ||||||
|  |             tbody.sort((a, b) => { | ||||||
|  |                 let fixedStr = MiscMethods.removeLeadingWhitespaces( | ||||||
|  |                     a.album, | ||||||
|  |                     b.album | ||||||
|  |                 ); | ||||||
|  |                 a.album = fixedStr[0]; | ||||||
|  |                 b.album = fixedStr[1]; | ||||||
|  |  | ||||||
|  |                 if (a.album < b.album) return -1; | ||||||
|  |                 else if (a.album > b.album) return 1; | ||||||
|  |                 else return 0; | ||||||
|  |             }); | ||||||
|  |         } else if (term === "genre") { | ||||||
|  |             tbody.sort((a, b) => { | ||||||
|  |                 let fixedStr = MiscMethods.removeLeadingWhitespaces( | ||||||
|  |                     a.genre, | ||||||
|  |                     b.genre | ||||||
|  |                 ); | ||||||
|  |                 a.genre = fixedStr[0]; | ||||||
|  |                 b.genre = fixedStr[1]; | ||||||
|  |  | ||||||
|  |                 if (a.genre < b.genre) return -1; | ||||||
|  |                 else if (a.genre > b.genre) return 1; | ||||||
|  |                 else return 0; | ||||||
|  |             }); | ||||||
|  |         } else if (term === "year") { | ||||||
|  |             tbody.sort((a, b) => { | ||||||
|  |                 return a.year - b.year; | ||||||
|  |             }); | ||||||
|  |         } else if (term === "time") { | ||||||
|  |             //term == time convert the time into seconds | ||||||
|  |             //The messy else if + else is becomes a.inSeconds || b.inSeconds can be undefined. | ||||||
|  |             tbody.sort((a, b) => { | ||||||
|  |                 if (a.inSeconds && b.inSeconds) { | ||||||
|  |                     return a.inSeconds - b.inSeconds; | ||||||
|  |                 } else if (a.inSeconds || b.inSeconds) { | ||||||
|  |                     if (a.inSeconds) { | ||||||
|  |                         let parsedB = b.time.split(":"); | ||||||
|  |                         if (parsedB[0][0] === "0") parsedB[0] = parsedB[0][1]; | ||||||
|  |                         if (parsedB[1][0] === "0") parsedB[1] = parsedB[1][1]; | ||||||
|  |                         let totalB = | ||||||
|  |                             parseInt(parsedB[0], 10) * 60 + | ||||||
|  |                             parseInt(parsedB[1], 10); | ||||||
|  |                         b.inSeconds = totalB; | ||||||
|  |  | ||||||
|  |                         return a.inSeconds - totalB; | ||||||
|  |                     } else { | ||||||
|  |                         let parsedA = a.time.split(":"); | ||||||
|  |                         if (parsedA[0][0] === "0") parsedA[0] = parsedA[0][1]; | ||||||
|  |                         if (parsedA[1][0] === "0") parsedA[1] = parsedA[1][1]; | ||||||
|  |                         let totalA = | ||||||
|  |                             parseInt(parsedA[0], 10) * 60 + | ||||||
|  |                             parseInt(parsedA[1], 10); | ||||||
|  |                         a.inSeconds = totalA; | ||||||
|  |  | ||||||
|  |                         return totalA - b.inSeconds; | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     let parsedA = a.time.split(":"); | ||||||
|  |                     if (parsedA[0][0] === "0") parsedA[0] = parsedA[0][1]; | ||||||
|  |                     if (parsedA[1][0] === "0") parsedA[1] = parsedA[1][1]; | ||||||
|  |  | ||||||
|  |                     let parsedB = b.time.split(":"); | ||||||
|  |                     if (parsedB[0][0] === "0") parsedB[0] = parsedB[0][1]; | ||||||
|  |                     if (parsedB[1][0] === "0") parsedB[1] = parsedB[1][1]; | ||||||
|  |  | ||||||
|  |                     let totalA = | ||||||
|  |                         parseInt(parsedA[0], 10) * 60 + | ||||||
|  |                         parseInt(parsedA[1], 10); | ||||||
|  |                     let totalB = | ||||||
|  |                         parseInt(parsedB[0], 10) * 60 + | ||||||
|  |                         parseInt(parsedB[1], 10); | ||||||
|  |                     a.inSeconds = totalA; | ||||||
|  |                     b.inSeconds = totalB; | ||||||
|  |  | ||||||
|  |                     return totalA - totalB; | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |         if (table.tbody === tbody) console.warn("Music has not been sorted"); | ||||||
|  |         else console.log("Music has been sorted"); | ||||||
|  |  | ||||||
|  |         res.tbody = tbody; | ||||||
|  |         return res; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Removes Leading Whitespaces from 2 Strings | ||||||
|  |      * - Used Exclusively in MiscMethods.sortTable | ||||||
|  |      *  - Used so that Both Strings can properly be compared | ||||||
|  |      * @param {String} string1  | ||||||
|  |      * @param {String} string2  | ||||||
|  |      * @return {Array<String>} Truncated Strings | ||||||
|  |      * @static | ||||||
|  |      */ | ||||||
|  |     static removeLeadingWhitespaces(string1, string2) { | ||||||
|  |         const regex = /^\s+/i; | ||||||
|  |         let stringA = string1; | ||||||
|  |         let stringB = string2; | ||||||
|  |  | ||||||
|  |         if (regex.test(stringA)) { | ||||||
|  |             let spaces = regex.exec(stringA)[0]; | ||||||
|  |             stringA = stringA.substr(spaces.length, stringA.length); | ||||||
|  |         } | ||||||
|  |         if (regex.test(stringB)) { | ||||||
|  |             let spaces = regex.exec(stringB)[0]; | ||||||
|  |             stringB = stringB.substr(spaces.length, stringB.length); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return [stringA, stringB]; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										44
									
								
								src/components/Modal.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/components/Modal.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import Emitter from '../melodii/Events'; | ||||||
|  |  | ||||||
|  | export default class Modal extends React.Component { | ||||||
|  |   constructor(props) { | ||||||
|  |     super(props); | ||||||
|  |     this.state = { | ||||||
|  |       style: { display: "none" } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Emitter.on('loadModal', () => { | ||||||
|  |       this.setState({ | ||||||
|  |         style: { display: "flex" } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |   } | ||||||
|  |   render() { | ||||||
|  |     console.log("Modal Created"); | ||||||
|  |     return( | ||||||
|  |       <div  | ||||||
|  |       className="modal" | ||||||
|  |       onClick={this.handleClick.bind(this)} | ||||||
|  |       style={this.state.style} | ||||||
|  |       > | ||||||
|  |        {this.props.content} | ||||||
|  |       </div>   | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   shouldComponentUpdate(nProps, nState) { | ||||||
|  |      | ||||||
|  |     return ( | ||||||
|  |       this.props.content !== nProps.content || | ||||||
|  |       this.state.display !== nState.style.display) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   handleClick() { | ||||||
|  |     console.log("Dismissing Modal."); | ||||||
|  |     this.setState({ | ||||||
|  |       style: { display: "none" } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										14
									
								
								src/components/Modal/SettingsManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/components/Modal/SettingsManager.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import Modal from '../Modal'; | ||||||
|  |  | ||||||
|  | const Settings = window.require("electron-settings"); | ||||||
|  |  | ||||||
|  | export default class SettingsManager extends Modal { | ||||||
|  |  | ||||||
|  |     render() { | ||||||
|  |         const JSX = <div className='settings-window'>Hello</div> | ||||||
|  |         return( | ||||||
|  |             <Modal content={JSX} /> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										54
									
								
								src/components/SeekBar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/components/SeekBar.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | |||||||
|  | import React from "react"; | ||||||
|  | import MusicPlayer from "../melodii/MusicPlayer"; | ||||||
|  |  | ||||||
|  | var mp = new MusicPlayer(); | ||||||
|  |  | ||||||
|  | export default class SeekBar extends React.Component { | ||||||
|  |     constructor(props) { | ||||||
|  |         super(props); | ||||||
|  |         this.isPlayingOnMouseDown = false; | ||||||
|  |         this.onChangeUsed = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @param {Event} e */ | ||||||
|  |     handleChange(e) { | ||||||
|  |         mp.seek(+e.target.value); | ||||||
|  |         this.onChangeUsed = true; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @param {KeyboardEvent} e */ | ||||||
|  |     handleMouseDown(e) { | ||||||
|  |         this.isPlayingOnMouseDown = !mp.isPaused; | ||||||
|  |         mp.pause(); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** @param {MouseEvent} e */ | ||||||
|  |     handleMouseUp(e) { | ||||||
|  |         if (!this.onChangeUsed) { | ||||||
|  |             mp.seek(+e.target.value); | ||||||
|  |         } | ||||||
|  |         if (this.isPlayingOnMouseDown) mp.play(); | ||||||
|  |     } | ||||||
|  |     render() { | ||||||
|  |         return ( | ||||||
|  |             <div className="seekBar"> | ||||||
|  |                 <input | ||||||
|  |                     type="range" | ||||||
|  |                     style={{ | ||||||
|  |                         backgroundSize: | ||||||
|  |                             ((this.props.currentTime || 0) * 100) / | ||||||
|  |                                 (this.props.duration || 0) + | ||||||
|  |                             "% 100%" | ||||||
|  |                     }} | ||||||
|  |                     value={this.props.currentTime || 0} | ||||||
|  |                     max={this.props.duration || 0} | ||||||
|  |                     id="seekRange" | ||||||
|  |                     className="melodiiSlider" | ||||||
|  |                     onChange={this.handleChange.bind(this)} | ||||||
|  |                     onMouseDown={this.handleMouseDown.bind(this)} | ||||||
|  |                     onMouseUp={this.handleMouseUp.bind(this)} | ||||||
|  |                 /> | ||||||
|  |             </div> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								src/melodii/Buttons.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/melodii/Buttons.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | const electron = window.require('electron'); | ||||||
|  |  | ||||||
|  | export default class Buttons { | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * Quits Melodii | ||||||
|  |      * @static | ||||||
|  |      */ | ||||||
|  |     static quit() { | ||||||
|  |         electron.remote.app.quit(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Minimized Melodii | ||||||
|  |      * @static | ||||||
|  |      */ | ||||||
|  |     static minimize() { | ||||||
|  |         electron.remote.BrowserWindow.getFocusedWindow().minimize(); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										8
									
								
								src/melodii/Events.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/melodii/Events.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | import EventEmitter from 'events'; | ||||||
|  |  | ||||||
|  | class Emitter extends EventEmitter {}; | ||||||
|  |  | ||||||
|  | let emitter = new Emitter(); | ||||||
|  | emitter.setMaxListeners(Infinity); | ||||||
|  |  | ||||||
|  | export default emitter; | ||||||
							
								
								
									
										74
									
								
								src/melodii/Filepath.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/melodii/Filepath.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | import os from "os"; | ||||||
|  | const fs = window.require("fs"); | ||||||
|  |  | ||||||
|  | export default class Filepath { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @param {String} location Filepath | ||||||
|  |      * @param {Object} cache I don't know what this is. | ||||||
|  |      */ | ||||||
|  |     constructor(location, cache) { | ||||||
|  |         this.cache = cache; | ||||||
|  |         this.location = location; | ||||||
|  |  | ||||||
|  |         if (os.platform() !== "win32") this.slash = "/"; | ||||||
|  |         else this.slash = "\\"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * @return {Promise<Array<String>>} list of valid files. | ||||||
|  |      */ | ||||||
|  |     getValidFiles() { | ||||||
|  |         return new Promise((res, rej) => { | ||||||
|  |             this.scan(this.location, (err, list) => { | ||||||
|  |                 if (err) rej(err); | ||||||
|  |                 else { | ||||||
|  |                     let filteredList = list.filter(arg => { | ||||||
|  |                         if ( | ||||||
|  |                             arg.match( | ||||||
|  |                                 /^.*\.(flac|mp4|mp3|m4a|aac|wav|ogg)$/gi | ||||||
|  |                             ) !== null | ||||||
|  |                         ) | ||||||
|  |                             return true; | ||||||
|  |                         else return false; | ||||||
|  |                     }); | ||||||
|  |                     res(filteredList); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * Stack Overflow: http://stackoverflow.com/questions/5827612/node-js-fs-readdir-recursive-directory-search | ||||||
|  |      *  | ||||||
|  |      * Recursively  | ||||||
|  |      *  | ||||||
|  |      * @param {String} dir  | ||||||
|  |      * @param {Object} done Callback | ||||||
|  |      */ | ||||||
|  |     scan(dir, done) { | ||||||
|  |         let self = this; | ||||||
|  |         let results = []; | ||||||
|  |         fs.readdir(dir, (err, list) => { | ||||||
|  |             if (err) return done(err); | ||||||
|  |             let i = 0; | ||||||
|  |             (function next() { | ||||||
|  |                 let file = list[i++]; | ||||||
|  |                 if (!file) return done(null, results); | ||||||
|  |                 file = dir + self.slash + file; | ||||||
|  |                 fs.stat(file, (err, stat) => { | ||||||
|  |                     if (stat && stat.isDirectory()) { | ||||||
|  |                         self.scan(file, (err, res) => { | ||||||
|  |                             results = results.concat(res); | ||||||
|  |                             next(); | ||||||
|  |                         }); | ||||||
|  |                     } else { | ||||||
|  |                         results.push(file); | ||||||
|  |                         next(); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             })(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										114
									
								
								src/melodii/MusicPlayer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/melodii/MusicPlayer.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | import Emitter from "./Events"; | ||||||
|  | import SongArchive from "./SongArchive"; | ||||||
|  | import Song from './Song'; | ||||||
|  |  | ||||||
|  | var mp = new Audio(); | ||||||
|  | var archive = new SongArchive(); | ||||||
|  |  | ||||||
|  | export default class MusicPlayer { | ||||||
|  |     constructor() { | ||||||
|  |         this.element = mp; | ||||||
|  |         this.isPaused = false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Pauses Music and sets currentTime to 0. */ | ||||||
|  |     stop() { | ||||||
|  |         this.pause(); | ||||||
|  |         this.isPaused = false; | ||||||
|  |         this.element.currentTime = 0.0; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Pauses Music*/ | ||||||
|  |     pause() { | ||||||
|  |         this.element.pause(); | ||||||
|  |         this.isPaused = true; | ||||||
|  |         Emitter.emit("toggle", false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Plays Music | ||||||
|  |      * @async | ||||||
|  |     */ | ||||||
|  |     async play() { | ||||||
|  |         if (this.isPaused) { | ||||||
|  |             this.element.play(); | ||||||
|  |             this.isPaused = false; | ||||||
|  |             Emitter.emit("toggle", true); | ||||||
|  |  | ||||||
|  |             let song = await Song.getMetadata(archive.getCurrentSong()); | ||||||
|  |             document.title = song.metadata.common.title; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Loads Song | ||||||
|  |      * @param {Song} song | ||||||
|  |      */ | ||||||
|  |     load(song) { | ||||||
|  |         if (archive.getCurrentSong() !== undefined) | ||||||
|  |             archive.add(archive.getCurrentSong()); | ||||||
|  |         let path = song.location; | ||||||
|  |         archive.setCurrentSong(song); | ||||||
|  |         if (!this.isPaused) this.pause(); | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             this.element.src = this.getURICompatible(path); | ||||||
|  |             this.element.load(); | ||||||
|  |             console.log(path + " succesfully loaded"); | ||||||
|  |         } catch (e) { | ||||||
|  |             console.error(path + " failed to load: " + e.name); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Turns a Filepath into one Chrome can handle | ||||||
|  |      * @param {string} path | ||||||
|  |      */ | ||||||
|  |     getURICompatible(path) { | ||||||
|  |         //eslint-disable-next-line | ||||||
|  |         return path.replace(/[!'()*#?@$&+,;=\[\]]/g, c => { | ||||||
|  |             //Excluded '/' '\' ':' | ||||||
|  |             return "%" + c.charCodeAt(0).toString(16); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Seeks to a certain position in a song  | ||||||
|  |      * @param {Number} pos | ||||||
|  |     */ | ||||||
|  |     seek(pos) { | ||||||
|  |         this.element.currentTime = pos; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** gets the Current Time in song. | ||||||
|  |      * @return {Number} currentTIme | ||||||
|  |      */ | ||||||
|  |     currentTime() { | ||||||
|  |         return this.element.currentTime; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** gets the Duration of the Song so far. | ||||||
|  |      * @return {Number} duration | ||||||
|  |      */ | ||||||
|  |     duration() { | ||||||
|  |         return this.element.duration; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Sets the Volume of the Audio Element | ||||||
|  |      * @param {Number} vol | ||||||
|  |      */ | ||||||
|  |     setVolume(vol) { | ||||||
|  |         if (vol <= 1) this.element.volume = vol; | ||||||
|  |         else if (vol < 0) console.error(vol + "is too small (Volume)"); | ||||||
|  |         else if (vol > 1) console.error(vol + " is too large (Volume)"); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** onSongEnd Handler. | ||||||
|  |      * @param {Object} lastfm   will scrobble track | ||||||
|  |      * @param {Boolean} random  will choose a random song and play it. | ||||||
|  |      * @param {Object} playlist will load next song in given playlist | ||||||
|  |      */ | ||||||
|  |     setOnSongEnd(lastfm, random, playlist) { | ||||||
|  |         this.element.onended = () => { | ||||||
|  |             if (random) console.log("random"); | ||||||
|  |             if (lastfm) console.log("lastfm"); | ||||||
|  |             if (playlist) console.log("playlist"); | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								src/melodii/Playlist.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/melodii/Playlist.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | import Filepath from "./Filepath"; | ||||||
|  | import Misc from "../components/MiscMethods"; | ||||||
|  | import Song from "./Song"; | ||||||
|  |  | ||||||
|  | export default class Playlist { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Creates Playlist with Friendly Title, and Filepath | ||||||
|  |      * @param {String} title  | ||||||
|  |      * @param {(String|Filepath)} path | ||||||
|  |      */ | ||||||
|  |     constructor(title, path) { | ||||||
|  |         this.initialize(title, path); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Initializes Playlist | ||||||
|  |      * @param {String} title  | ||||||
|  |      * @param {(String|Filepath)} path | ||||||
|  |      * @return {void} | ||||||
|  |      * @async | ||||||
|  |      */ | ||||||
|  |     async initialize(title, path) { | ||||||
|  |         if (typeof path === "string") { | ||||||
|  |             // is filepath | ||||||
|  |             this.path = path; | ||||||
|  |             let filepaths = await new Filepath(path).getValidFiles(); | ||||||
|  |             this.content = await this.getTableData(filepaths); | ||||||
|  |         } else if (typeof path === "object") { | ||||||
|  |             //array of songs | ||||||
|  |             this.path = null; | ||||||
|  |             this.content = path; | ||||||
|  |         } | ||||||
|  |         this.title = title; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Gets Formated Metadata for Table.js | ||||||
|  |      * @param {Array<String>} filepaths | ||||||
|  |      * @return {Object} Object of Metadata Information meant for Table Generation | ||||||
|  |      */ | ||||||
|  |     getTableData(filepaths) { | ||||||
|  |         return new Promise(async (res, rej) => { | ||||||
|  |             let content = []; | ||||||
|  |             for (let i = 0; i < filepaths.length - 1; i++) { | ||||||
|  |                 let song = new Song(filepaths[i]); | ||||||
|  |                 song = await Song.getMetadata(song); | ||||||
|  |                 content.push(Misc.formatMetadata(song, song.metadata)); | ||||||
|  |             } | ||||||
|  |             res(content); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										70
									
								
								src/melodii/Song.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/melodii/Song.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | import noalbumart from '../assets/img/noalbumart.png'; | ||||||
|  | import Emitter from './Events'; | ||||||
|  | const mm = window.require('music-metadata'); | ||||||
|  |  | ||||||
|  | export default class Song { | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @param {String} path  | ||||||
|  |      * @param {Boolean} ShouldAlbumArtExist  | ||||||
|  |      */ | ||||||
|  |     constructor(path, ShouldAlbumArtExist) { //whether album art should exist | ||||||
|  |         this.location = path; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Gets Metadata from Song | ||||||
|  |      * @param {Song} song | ||||||
|  |      * @return {Promise<Song>} | ||||||
|  |      * @static | ||||||
|  |      */ | ||||||
|  |     static getMetadata(song) { | ||||||
|  |         const location = song.location; | ||||||
|  |         return new Promise((res, rej) => { | ||||||
|  |             mm.parseFile(location, {native: true, duration: true}).then((metadata) => { | ||||||
|  |                 song.metadata = metadata; | ||||||
|  |                res(song); | ||||||
|  |             }).catch((err) => { | ||||||
|  |                 rej(err); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * gets and Displays Album Art | ||||||
|  |      * @param {Object} metadata Song Metadata | ||||||
|  |      * @static | ||||||
|  |      */ | ||||||
|  |     static setAlbumArt(metadata) { | ||||||
|  |         if (metadata.common.picture) { | ||||||
|  |             if (metadata.common.picture.length > 0) { | ||||||
|  |                 let picture = metadata.common.picture[0]; | ||||||
|  |                 let url = URL.createObjectURL(new Blob([picture.data], { | ||||||
|  |                     'type': 'image/' + picture.format | ||||||
|  |                 })); | ||||||
|  |                 Emitter.emit('updateAlbumArt', url); | ||||||
|  |             } else { | ||||||
|  |                 console.error(metadata.common.title + ' has Album Art, but no data was present'); | ||||||
|  |                 Emitter.emit('updateAlbumArt', noalbumart); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             console.warn(metadata.common.title + ' des not have Album Art'); | ||||||
|  |             Emitter.emit('updateAlbumArt', noalbumart); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Combines the functions of Song.getMetadata and Song.setAlbumArt into one. | ||||||
|  |      * @param {Song} song | ||||||
|  |      * @return {Promise<Object>} | ||||||
|  |      * @static | ||||||
|  |      */ | ||||||
|  |     static doAll(song) { | ||||||
|  |         return new Promise(async (res, rej) => { | ||||||
|  |             let metadata = await this.getMetadata(song).catch((err) => rej(err)); | ||||||
|  |             this.setAlbumArt(metadata); | ||||||
|  |             res(metadata); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										110
									
								
								src/melodii/SongArchive.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/melodii/SongArchive.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | |||||||
|  | import Song from './Song'; | ||||||
|  | import Playlist from './Playlist'; | ||||||
|  |  | ||||||
|  | /** @type {Array<Song>} */ | ||||||
|  | var archive = []; | ||||||
|  |  | ||||||
|  | /** @type {Array<Playlist>} */ | ||||||
|  | let playlists = []; | ||||||
|  |  | ||||||
|  | /** @type {Song} */ | ||||||
|  | let currentSong; | ||||||
|  |  | ||||||
|  | /** @type {Playlist} */ | ||||||
|  | let currentPlaylist; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Class for Keeping Information about Current Session of Melodii's Songs | ||||||
|  |  */ | ||||||
|  | export default class SongArchive { | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Adds Song to Archive | ||||||
|  |      * @param {Song} song  | ||||||
|  |      * @param {Playlist} playlist  | ||||||
|  |      */ | ||||||
|  |     add(song, playlist) { | ||||||
|  |         if (song !== null) { | ||||||
|  |             if (archive[archive.length - 1] !== song) { | ||||||
|  |                 console.log('Archive Updated.'); | ||||||
|  |                 archive.push(song); | ||||||
|  |             } else console.log('Archive not Updated (Same Song Loaded).'); | ||||||
|  |         } else if (playlist !== null) { | ||||||
|  |             playlists.push(playlist); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Set the Currently Playing Song | ||||||
|  |      * @param {Song} song | ||||||
|  |      */ | ||||||
|  |     setCurrentSong(song) { | ||||||
|  |         currentSong = song; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @return {SongArchive} The Instanced Archive */ | ||||||
|  |     get() { | ||||||
|  |         return archive; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @return {Number} Archive length */ | ||||||
|  |     length() { | ||||||
|  |         return archive.length; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Get the Currently Playing Song | ||||||
|  |      * @return {Song} | ||||||
|  |      */ | ||||||
|  |     getCurrentSong() { | ||||||
|  |         return currentSong; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @return {Boolean} Whether a currently playing song exists or not. */ | ||||||
|  |     currentSongExists() { | ||||||
|  |         return (currentSong) ? true : false; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** Gets the Previously Played Song | ||||||
|  |      * @return {Song} | ||||||
|  |      */ | ||||||
|  |     getPreviousSong() { | ||||||
|  |         let index; | ||||||
|  |         let pos = archive.indexOf(currentSong); | ||||||
|  |  | ||||||
|  |         if (pos === -1) index = this.length() - 1; //song doesn't exist | ||||||
|  |         else if (pos === 0) index = 0; //is the first song | ||||||
|  |         else index = pos -1; | ||||||
|  |  | ||||||
|  |         return archive[index]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Gets a pre-existing Playlist by it's title. | ||||||
|  |      * @param {String} title  | ||||||
|  |      */ | ||||||
|  |     getPlaylist(title) { | ||||||
|  |         let pos = playlists.map(obj => obj.title).indexOf(title); | ||||||
|  |         return playlists[pos]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Sets the Current Playlist | ||||||
|  |      * @param {Playlist} playlist  | ||||||
|  |      */ | ||||||
|  |     setCurrentPlaylist(playlist) { | ||||||
|  |         currentPlaylist = playlist; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Gets the Curernt Playlist | ||||||
|  |      * @return {Playlist} | ||||||
|  |      */ | ||||||
|  |     getCurrentPlaylist() { | ||||||
|  |         return currentPlaylist; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** @return {Boolean} whether a currently playing playlist exists or not */ | ||||||
|  |     currentPlaylistExists() { | ||||||
|  |         return (currentPlaylist) ? true: false; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										180
									
								
								src/melodii/extras/LastFM.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								src/melodii/extras/LastFM.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | |||||||
|  | import API from "lastfmapi"; | ||||||
|  |  | ||||||
|  | const remote = window.require("electron").remote; | ||||||
|  | const process = remote.getGlobal("process"); | ||||||
|  | const settings = window.require("electron-settings"); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Handles all Supported https://last.fm API Functions | ||||||
|  |  */ | ||||||
|  | export default class LastFM { | ||||||
|  |     /** | ||||||
|  |      * Creats LastFM with credientials in order to start communicating w/ Last.FM Servers | ||||||
|  |      * @param {String} apiKey | ||||||
|  |      * @param {String} secret | ||||||
|  |      */ | ||||||
|  |     constructor(apiKey, secret) { | ||||||
|  |  | ||||||
|  |         if (apiKey && secret) { | ||||||
|  |             if (!settings.has("lastfm.apiKey")) { | ||||||
|  |                 //Save API Key | ||||||
|  |                 settings.set("lastfm", { | ||||||
|  |                     apiKey: apiKey, | ||||||
|  |                     secret: secret | ||||||
|  |                 }); | ||||||
|  |             } else if (settings.get("lastfm.apiKey") !== apiKey) { | ||||||
|  |                 //APi Keys are Different | ||||||
|  |                 settings.set("lastfm", { | ||||||
|  |                     apiKey: apiKey, | ||||||
|  |                     secret: secret | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |      | ||||||
|  |             this.api = new API({ | ||||||
|  |                 api_key: apiKey, //eslint-disable-line camelcase | ||||||
|  |                 secret: secret, | ||||||
|  |                 useragent: `melodii/v${process.env.npm_package_version} Melodii` | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * All In one Function which handles entire Authentication Process | ||||||
|  |      * @async | ||||||
|  |      */ | ||||||
|  |     enable() { | ||||||
|  |         return new Promise(async (res, rej) => { | ||||||
|  |             let token = await this.getToken(); | ||||||
|  |             let sessionKey = await this.getSessionKey(token); | ||||||
|  |             this.startSession(this.sessionName, sessionKey); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Displays lastFM authentication Page to get Session Key | ||||||
|  |      * @return {Promise<String>} | ||||||
|  |      * @async | ||||||
|  |      */ | ||||||
|  |     getToken() { | ||||||
|  |         return new Promise((res, rej) => { | ||||||
|  |             let key = ""; | ||||||
|  |  | ||||||
|  |             //create 800x600 chrome window. | ||||||
|  |             let win = new remote.BrowserWindow({ | ||||||
|  |                 width: 800, | ||||||
|  |                 height: 600 | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             //Load last.fm signin page to get Session Key. (Therefore Giving acess to user's account) | ||||||
|  |             win.loadURL( | ||||||
|  |                 `http://www.last.fm/api/auth/?api_key=${this.api.api_key}` | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             //Don't Show the Window until lastfm is done loading. | ||||||
|  |             win.webContents.on("did-finish-load", () => { | ||||||
|  |                 win.show(); | ||||||
|  |                 win.focus(); | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             //Grab the Session Key the second LastFM puts it in the URL. | ||||||
|  |             win.webContents.on("will-navigate", (e, url) => { | ||||||
|  |                 let self = this; | ||||||
|  |                 if (e) rej(e); | ||||||
|  |                 try { | ||||||
|  |                     let match = url.match(/token=(.*)/g); | ||||||
|  |                     key = match[0].substring(6, 38); //Guaranteed to be the session key | ||||||
|  |                     win.close(); | ||||||
|  |  | ||||||
|  |                     if (!settings.has("lastfm.token")) | ||||||
|  |                         settings.set("lastfm.token", key); | ||||||
|  |                     else if (settings.get("lastfm.token") !== key) | ||||||
|  |                         settings.set("lastfm.token", key); | ||||||
|  |  | ||||||
|  |                     res(key); | ||||||
|  |                 } catch (e) { | ||||||
|  |                     rej(e); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     startSession(sessionName, sessionKey) { | ||||||
|  |         this.api.setSessionCredentials(sessionName, sessionKey); | ||||||
|  |     } | ||||||
|  |     /** | ||||||
|  |      * Returns Session Key | ||||||
|  |      * @param {String} token | ||||||
|  |      * @return {Promise<String>} @async | ||||||
|  |      */ | ||||||
|  |     getSessionKey(token) { | ||||||
|  |         return Promise((res, rej) => { | ||||||
|  |             this.api.authenticate(token, (err, session) => { | ||||||
|  |                 if (err) rej(err); | ||||||
|  |  | ||||||
|  |                 if (!settings.has("lastfm.session.name")) { | ||||||
|  |                     settings.set("lastfm.session", { | ||||||
|  |                         name: session.username, | ||||||
|  |                         key: session.key | ||||||
|  |                     }); | ||||||
|  |                 } else if ( | ||||||
|  |                     settings.get("lastfm.session.name") !== session.username | ||||||
|  |                 ) { | ||||||
|  |                     settings.set("lastfm.session", { | ||||||
|  |                         name: session.username, | ||||||
|  |                         key: session.key | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 this.sessionName = session.username; | ||||||
|  |                 res(session.key); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Updates nowPlaying on LastFM | ||||||
|  |      * @param {String} artist | ||||||
|  |      * @param {String} track | ||||||
|  |      * @param {String} album | ||||||
|  |      * @param {String} albumArtist | ||||||
|  |      */ | ||||||
|  |     nowPlaying(artist, track, album, albumArtist) { | ||||||
|  |         this.api.track.updateNowPlaying( | ||||||
|  |             { | ||||||
|  |                 artist: artist, | ||||||
|  |                 track: track, | ||||||
|  |                 album: album, | ||||||
|  |                 albumArtist, | ||||||
|  |                 albumArtist | ||||||
|  |             }, | ||||||
|  |             (err, nowPlaying) => { | ||||||
|  |                 if (err) throw err; | ||||||
|  |                 console.log("Now Playing Updated"); | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Scrobbles Song to LastFM | ||||||
|  |      * @param {String} artist | ||||||
|  |      * @param {String} track | ||||||
|  |      * @param {String} album | ||||||
|  |      * @param {String} albumArtist | ||||||
|  |      */ | ||||||
|  |     scrobble(artist, track, album, albumArtist) { | ||||||
|  |         this.api.track.scrobble( | ||||||
|  |             { | ||||||
|  |                 artist: artist, | ||||||
|  |                 track: track, | ||||||
|  |                 timestamp: 1, | ||||||
|  |                 album: album, | ||||||
|  |                 albumArtist, | ||||||
|  |                 albumArtist | ||||||
|  |             }, | ||||||
|  |             (err, scrobble) => { | ||||||
|  |                 if (err) throw err; | ||||||
|  |                 console.log("Scrobbling Successful."); | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								src/melodii/extras/RichPresence.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/melodii/extras/RichPresence.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | const settings = window.require('electron-settings'); | ||||||
|  |  | ||||||
|  | class RichPresence { | ||||||
|  |     constructor(clientID, secret) { | ||||||
|  |         this.clientID = ""; | ||||||
|  |         this.secret = ""; | ||||||
|  |  | ||||||
|  |         if (settings.has("richPresence.clientID")) { | ||||||
|  |             this.clientID = settings.get("richPresence.clientID"); | ||||||
|  |             this.secret = settings.get("richPresence.secret"); | ||||||
|  |         } else if (clientID && secret) { | ||||||
|  |             settings.set("richPresence", { | ||||||
|  |                 clientID: clientID, | ||||||
|  |                 secret: secret | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             this.clientID = clientID; | ||||||
|  |             this.secret = secret; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										6
									
								
								src/renderer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/renderer.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import ReactDOM from 'react-dom'; | ||||||
|  | import App from './App'; | ||||||
|  | import './scss/main.scss'; | ||||||
|  |  | ||||||
|  | ReactDOM.render(<App />, document.querySelector('.app')); | ||||||
							
								
								
									
										291
									
								
								src/scss/App.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										291
									
								
								src/scss/App.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,291 @@ | |||||||
|  | // @font-face { | ||||||
|  | //   font-family: Roboto; | ||||||
|  | //   src: url("./assets/fonts/Roboto-Regular.ttf"); | ||||||
|  | // } | ||||||
|  |  | ||||||
|  | :root { | ||||||
|  |   --ui-bg-colour: #616161; | ||||||
|  |   --bg-colour: gray; | ||||||
|  |   --accent-colour: #512da8; | ||||||
|  |   --table-bg-even: #616161; | ||||||
|  |   --table-bg-odd: #9e9e9e; | ||||||
|  |   --table-bg-active: #64b5f6; | ||||||
|  |   --table-txt-active: white; | ||||||
|  |   --icon-color: white; | ||||||
|  |   --song-info-text: white; | ||||||
|  |   --album-art-bg: #bdbdbd; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .melodiiContainer { | ||||||
|  |   height: 100vh; | ||||||
|  |   width: 100vw; | ||||||
|  |   margin: 0; | ||||||
|  |   padding: 0; | ||||||
|  |   display: flex; | ||||||
|  |   flex-flow: column; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* CSS for Song Information */ | ||||||
|  |  | ||||||
|  | .songInfo { | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |   align-items: center; | ||||||
|  |   margin-right: 0.75em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .songInfo span { | ||||||
|  |   color: white; | ||||||
|  |   text-align: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* CSS for the Header */ | ||||||
|  |  | ||||||
|  | header { | ||||||
|  |   -webkit-app-region: drag; | ||||||
|  |   top: 0; | ||||||
|  |   right: 0; | ||||||
|  |   height: 25px; | ||||||
|  |   background: var(--ui-bg-colour); | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-between; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | header svg { | ||||||
|  |   -webkit-app-region: no-drag; | ||||||
|  |   color: var(--icon-color); | ||||||
|  |   cursor: pointer; | ||||||
|  |   text-shadow: 1px 1px 2px #292929; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | header span { | ||||||
|  |   color: white; | ||||||
|  |   align-self: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .fa-window-minimize { | ||||||
|  |   font-size: 15px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .fa-times { | ||||||
|  |   font-size: 20px; | ||||||
|  |   margin-right: 0.1em; | ||||||
|  |   margin-left: 0.3em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Song Table */ | ||||||
|  |  | ||||||
|  | .wrapper table { | ||||||
|  |   cursor: default; | ||||||
|  |   border-spacing: 0; | ||||||
|  |   border-collapse: collapse; | ||||||
|  |   user-select: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .wrapper table tbody:hover { | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .active { | ||||||
|  |   background: var(--accent-colour) !important; | ||||||
|  |   color: white !important; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .wrapper table thead { | ||||||
|  |   border-bottom: 2px solid #666666; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .wrapper table td { | ||||||
|  |   width: calc(100vw / 6); | ||||||
|  |   vertical-align: middle; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .wrapper table tr:nth-child(even) { | ||||||
|  |   background: var(--table-bg-even); | ||||||
|  |   color: white; | ||||||
|  | } | ||||||
|  | .wrapper table tbody tr td#number { | ||||||
|  |   text-align: right; | ||||||
|  |   padding-right: calc((100vw / 6) / 10); | ||||||
|  | } | ||||||
|  | .wrapper table tbody tr td#text { | ||||||
|  |   text-align: left; | ||||||
|  |   padding-left: calc((100vw / 6) / 10); | ||||||
|  | } | ||||||
|  | .wrapper table tr:nth-child(odd) { | ||||||
|  |   background: var(--table-bg-odd); | ||||||
|  |   color: white; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .wrapper table tr:focus { | ||||||
|  |   outline: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /*CSS for the Body */ | ||||||
|  |  | ||||||
|  | .wrapper { | ||||||
|  |   flex: 2; | ||||||
|  |   overflow: auto; | ||||||
|  |   background: var(--bg-colour); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* CSS for the Footer */ | ||||||
|  |  | ||||||
|  | footer { | ||||||
|  |   min-height: 2.75em; | ||||||
|  |   text-align: center; | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   background: var(--ui-bg-colour); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /*CSS for Media Controls */ | ||||||
|  |  | ||||||
|  | .mediaControls { | ||||||
|  |   flex-shrink: 1; | ||||||
|  |   display: flex; | ||||||
|  |   justify-content: center; | ||||||
|  |   padding-left: 8em; | ||||||
|  |   /* margin-right: auto; */ | ||||||
|  |   align-items: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .mediaControls svg { | ||||||
|  |   font-size: 2em; | ||||||
|  |   color: var(--icon-color); | ||||||
|  |   margin-left: 0.3em; | ||||||
|  |   display: block; | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .mediaControls #SkipFwd { | ||||||
|  |   margin-right: 0.5em; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .mediaControls #VolBar { | ||||||
|  |   margin-top: auto; | ||||||
|  |   margin-bottom: auto; | ||||||
|  |   margin-left: 0.4em; | ||||||
|  |   margin-right: 0.1em; | ||||||
|  |   background-size: 100% 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /*CSS for Album Art */ | ||||||
|  |  | ||||||
|  | #albumContainer { | ||||||
|  |   position: absolute; | ||||||
|  |   align-items: center; | ||||||
|  |   justify-content: center; | ||||||
|  |   display: flex; | ||||||
|  |   bottom: 0; | ||||||
|  |   left: 0; | ||||||
|  |   width: 7em; | ||||||
|  |   height: 7em; | ||||||
|  |   z-index: 2; | ||||||
|  |   background: var(--album-art-bg); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #albumImg { | ||||||
|  |   width: auto; | ||||||
|  |   height: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /*CSS for SeekRange */ | ||||||
|  |  | ||||||
|  | .seekBar { | ||||||
|  |   background: var(--bg-colour); | ||||||
|  |   z-index: 1; | ||||||
|  |   display: flex; | ||||||
|  |   width: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #seekRange { | ||||||
|  |   margin-left: 8.5em; | ||||||
|  |   width: 100%; | ||||||
|  |   display: flex; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /*CSS for Range Input */ | ||||||
|  |  | ||||||
|  | input[type="range"] { | ||||||
|  |   margin: auto; | ||||||
|  |   outline: none; | ||||||
|  |   padding: 0; | ||||||
|  |   height: 6px; | ||||||
|  |   background-color: var(--album-art-bg); | ||||||
|  |   background-image: linear-gradient( | ||||||
|  |       var(--accent-colour), | ||||||
|  |       var(--accent-colour) | ||||||
|  |   ); | ||||||
|  |   border-radius: 10px; | ||||||
|  |   background-size: 50% 100%; | ||||||
|  |   background-repeat: no-repeat; | ||||||
|  |   cursor: pointer; | ||||||
|  |   -webkit-appearance: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | input[type="range"]::-webkit-slider-runnable-track { | ||||||
|  |   box-shadow: none; | ||||||
|  |   border: none; | ||||||
|  |   background: transparent; | ||||||
|  |   -webkit-appearance: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | input[type="range"]::-webkit-slider-thumb { | ||||||
|  |   width: 14px; | ||||||
|  |   height: 14px; | ||||||
|  |   border: 0; | ||||||
|  |   background: var(--icon-color); | ||||||
|  |   border-radius: 100%; | ||||||
|  |   box-shadow: 0 0 1px 0 rgba(0, 0, 0, 0.1); | ||||||
|  |   -webkit-appearance: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | input[type="range"]#seekRange { | ||||||
|  |   border-radius: 0; | ||||||
|  |   background-size: 0% 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /* Scrollbar CSS */ | ||||||
|  | :-webkit-scrollbar-button { | ||||||
|  |   display: none; | ||||||
|  |   height: 13px; | ||||||
|  |   border-radius: 0px; | ||||||
|  |   background-color: transparent; | ||||||
|  | } | ||||||
|  | ::-webkit-scrollbar-button:hover { | ||||||
|  |   background-color: transparent; | ||||||
|  | } | ||||||
|  | ::-webkit-scrollbar-thumb { | ||||||
|  |   background-color: #512da8; | ||||||
|  |   box-shadow: rgba(0, 0, 0, 0.5); | ||||||
|  | } | ||||||
|  | ::-webkit-scrollbar-thumb:hover { | ||||||
|  |   background-color: #5831b3; | ||||||
|  | } | ||||||
|  | ::-webkit-scrollbar-track { | ||||||
|  |   background-color: rgba(158, 158, 158, 0.25); | ||||||
|  | } | ||||||
|  | ::-webkit-scrollbar { | ||||||
|  |   width: 13px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // Modal | ||||||
|  | .modal { | ||||||
|  |   background: rgba(0,0,0, 0.3); | ||||||
|  |   display: none; | ||||||
|  |   align-items: center; | ||||||
|  |   justify-content: center; | ||||||
|  |  | ||||||
|  |   position: fixed; | ||||||
|  |   height: 100%; // or vh? | ||||||
|  |   width: 100%; // or vw? | ||||||
|  |   z-index: 3; | ||||||
|  |   .settings-window { | ||||||
|  |     background: var(--table-bg-odd); | ||||||
|  |     height: 50vh; | ||||||
|  |     width: 70vw; | ||||||
|  |     z-index: 4; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										7
									
								
								src/scss/main.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/scss/main.scss
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | body { | ||||||
|  |   margin: 0; | ||||||
|  |   padding:0; | ||||||
|  |   /* height: 100vh; | ||||||
|  |     width: 100vw; */ | ||||||
|  |   font-family: Arial, Helvetica, sans-serif; | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								test/mocha.opts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								test/mocha.opts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | --recursive | ||||||
|  | --require @babel/register | ||||||
|  | --require @babel/polyfill | ||||||
							
								
								
									
										45
									
								
								webpack.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								webpack.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | require('dotenv').config(); | ||||||
|  | const path = require('path'); | ||||||
|  | const webpack = require('webpack'); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |   mode: process.env.NODE_ENV, | ||||||
|  |  | ||||||
|  |   entry: ['@babel/polyfill', './src/renderer.js'], | ||||||
|  |   output: { | ||||||
|  |     path: path.resolve(__dirname, 'build'), | ||||||
|  |     filename: 'bundle.js' | ||||||
|  |   }, | ||||||
|  |   module: { | ||||||
|  |     rules: [ | ||||||
|  |       { | ||||||
|  |         test: /\.jsx?$/, | ||||||
|  |         exclude: /node_modules/, | ||||||
|  |         use: { | ||||||
|  |           loader: 'babel-loader', | ||||||
|  |           options: { | ||||||
|  |             presets: ['@babel/preset-env', '@babel/preset-react'] | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }, { | ||||||
|  |         test: /\.s?css$/, | ||||||
|  |         use: [ | ||||||
|  |           { loader: 'style-loader' }, | ||||||
|  |           { loader: 'css-loader' }, | ||||||
|  |           { loader: 'sass-loader' } | ||||||
|  |         ] | ||||||
|  |       }, { | ||||||
|  |         test: /\.(png|jpg|svg)$/, | ||||||
|  |         use: [{ loader: 'file-loader' }] | ||||||
|  |       } | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|  |   devtool: 'inline-source-map', | ||||||
|  |   target: 'electron-renderer', | ||||||
|  |   devServer: { | ||||||
|  |     contentBase: path.join(__dirname, 'build'), | ||||||
|  |     compress: true, | ||||||
|  |     port: process.env.PORT | ||||||
|  |   } | ||||||
|  | }; | ||||||
		Reference in New Issue
	
	Block a user