diff --git a/src/components/Body/Table.js b/src/components/Body/Table.js index 112efa6..178cd17 100644 --- a/src/components/Body/Table.js +++ b/src/components/Body/Table.js @@ -1,7 +1,7 @@ import React from "react"; import Song from "../../melodii/Song"; import MusicPlayer from "../../melodii/MusicPlayer"; -import Misc, { createID } from "../MiscMethods"; +import { createID, sortTable, TableText, formatMetadata } from "../Misc"; import Emitter from "../../melodii/Events"; import Filepath from "../../melodii/Filepath"; import Settings from 'electron-settings'; @@ -91,7 +91,7 @@ export default class Table extends React.Component { */ handleSort(table, term) { const temp = table; - Emitter.emit("newTable", Misc.sortTable(temp, term)); + Emitter.emit("newTable", sortTable(temp, term)); } /** @@ -113,22 +113,22 @@ export default class Table extends React.Component { onKeyDown={this.handleKeyDown.bind(this)} tabIndex="0"> - {Misc.truncateText(obj.artist, maxWidth, "Roboto")} + {TableText.truncateText(obj.artist, maxWidth, "Roboto")} - {Misc.truncateText(obj.title, maxWidth, "Roboto")} + {TableText.truncateText(obj.title, maxWidth, "Roboto")} - {Misc.truncateText(obj.album, maxWidth, "Roboto")} + {TableText.truncateText(obj.album, maxWidth, "Roboto")} - {Misc.truncateText(obj.year, maxWidth, "Roboto")} + {TableText.truncateText(obj.year, maxWidth, "Roboto")} - {Misc.truncateText(obj.genre, maxWidth, "Roboto")} + {TableText.truncateText(obj.genre, maxWidth, "Roboto")} - {Misc.truncateText(obj.time, maxWidth, "Roboto")} + {TableText.truncateText(obj.time, maxWidth, "Roboto")} )); @@ -221,8 +221,8 @@ export async function generate(path, template) { 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)); + + table.tbody.push(formatMetadata(song, song.metadata)); dom.innerHTML = "Creating Table Data: " + ~~((i / end) * 100) + "%"; } diff --git a/src/components/Misc.js b/src/components/Misc.js new file mode 100644 index 0000000..663f359 --- /dev/null +++ b/src/components/Misc.js @@ -0,0 +1,347 @@ +import Song from "../melodii/Song"; + +const usedTableIDs = []; + + +/** + * Finds the ode of a Set of umbers + * @param {Array} arr + * @return {Number} The Mode + */ +export function 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} arr + * @return {Number} The Median + */ +export function 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} arr + * @return {Number} The Average + */ +export function average(arr) { + let total = 0; + for (let i = 0; i < arr.length; i++) { + total += arr[i]; + } + return total / arr.length; + } + + + +export class TableText { + /** + * 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 = TableText.measureText(text, font, canvas); + + if (width > maxWidth) { + //text needs truncating... + let charWidths = []; + let ellipsisWidth = TableText.measureText("...", font, canvas); + + //get Average width of every char in string + for (let char in text) + if (typeof char === "string") + charWidths.push(TableText.measureText(char, font)); + + // let charWidth = this.median(charWidths); + let charWidth = 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; + } +} + +/** +* Creates a unique ID that is not UUID compliant. +* - used to distinguish table objects from one another. +* @param {Number} length +* @return {String} +*/ +export function createID(length) { + const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let id; + + do { + id = ""; + for (let i = 0; i < length; i++) id += chars[randInt(0, chars.length)]; + } while(usedTableIDs.includes(id)); + + usedTableIDs.push(id); + return id; + + function randInt(min, max) { + return ~~(Math.random() * (max - min) + min); + } +} + + +/** + * 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 + */ +export function 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 + * + */ +export function 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 = 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 = 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 = 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 = 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 sortTable() + * - Used so that Both Strings can properly be compared + * @param {String} string1 + * @param {String} string2 + * @return {Array} Truncated Strings + * @static + */ + function 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]; + } +} + diff --git a/src/components/MiscMethods.js b/src/components/MiscMethods.js deleted file mode 100644 index 0cca7ff..0000000 --- a/src/components/MiscMethods.js +++ /dev/null @@ -1,349 +0,0 @@ -import Song from "../melodii/Song"; - -const usedTableIDs = []; - -/** - * - 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} 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} 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} 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} 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]; - } -} - - /** - * Creates a unique ID that is not UUID compliant. - * - used to distinguish table objects from one another. - * @param {Number} length - * @return {String} -*/ -export function createID(length) { - const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - let id; - - do { - id = ""; - for (let i = 0; i < length; i++) id += chars[randInt(0, chars.length)]; - } while(usedTableIDs.includes(id)); - - usedTableIDs.push(id); - return id; - - function randInt(min, max) { - return ~~(Math.random() * (max - min) + min); - } -} \ No newline at end of file diff --git a/src/melodii/Playlist.js b/src/melodii/Playlist.js index 2a70e77..6cf9cd1 100644 --- a/src/melodii/Playlist.js +++ b/src/melodii/Playlist.js @@ -1,5 +1,5 @@ import Filepath from "./Filepath"; -import Misc, { createID } from "../components/MiscMethods"; +import { createID, formatMetadata } from "../components/Misc"; import Song from "./Song"; export default class Playlist { @@ -61,7 +61,7 @@ export default class Playlist { 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)); + content.push(formatMetadata(song, song.metadata)); } res(content); });