Tracking Development with Git now
This commit is contained in:
commit
3aedb93bbe
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"]
|
||||
}
|
|
@ -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
|
|
@ -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.
|
|
@ -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)
|
|
@ -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>
|
|
@ -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();
|
||||
});
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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;
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
|
@ -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});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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' />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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)} />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -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" }
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import EventEmitter from 'events';
|
||||
|
||||
class Emitter extends EventEmitter {};
|
||||
|
||||
let emitter = new Emitter();
|
||||
emitter.setMaxListeners(Infinity);
|
||||
|
||||
export default emitter;
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
})();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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'));
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
body {
|
||||
margin: 0;
|
||||
padding:0;
|
||||
/* height: 100vh;
|
||||
width: 100vw; */
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
--recursive
|
||||
--require @babel/register
|
||||
--require @babel/polyfill
|
|
@ -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