Tracking Development with Git now
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user