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