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