webamp/js/utils.ts
Jordan Eldredge a053818c5e
Revert "Serialize to indexdb" (#664)
* Revert "Don't re-center windows when loading from serialized state"

This reverts commit d778c03f61.

* Revert "Clean up comment"

This reverts commit 8253093a2b.

* Revert "Fix spelling of IndexedDB"

This reverts commit b34d90e998.

* Revert "Add a few more generic assertions about serialized state"

This reverts commit a8267581d0.

* Revert "Refactor debounce and throttle"

This reverts commit 6ecc5142b5.

* Revert "Double check that the serialized state we generated equals the one we have saved"

This reverts commit 82a81bf584.

* Revert "Add many tests for serialization"

This reverts commit 55603346c2.

* Revert "Split serialized state types into their own file"

This reverts commit 4c9c5b99d3.

* Revert "Refactor how we manage window positions"

This reverts commit d6a1baece3.

* Revert "Fix export"

This reverts commit 462cf4b42d.

* Revert "Clean up selectors file"

This reverts commit cdfa99383a.

* Revert "Mark serialization methods priviate for now"

This reverts commit fc79c77451.

* Revert "Add types for WindowManager"

This reverts commit 0a7f258b64.

* Revert "Export Box"

This reverts commit e94cdef100.

* Revert "Use browserSize from state. Make it non-optional"

This reverts commit 53481ba892.

* Revert "Add back resetWindowLayout"

This reverts commit 35f4004caa.

* Revert "Begin tracking window size"

This reverts commit 96ed2b353c.

* Revert "Use namespaced Utils for webamplazy"

This reverts commit 949a2bc771.

* Revert "Use namespaced Utils"

This reverts commit 3049350701.

* Revert "Add initial approach of recovering from bad window positions"

This reverts commit 1791babf1a.

* Revert "Move centerWindowsIfNeeded to an action creatorThis forces us to type it, as a nice side benefit"

This reverts commit 40e31f0577.

* Revert "Make state serialization opt-in (for now)"

This reverts commit bef421ebed.

* Revert "Persist focus, and handle bad focus"

This reverts commit 3f1861d4f8.

* Revert "Handle the case where `positions` might be an empty object to begin with"

This reverts commit f8544ed126.

* Revert "Don't center windows when restoring from serialized state"

This reverts commit ca1cfe3dc6.

* Revert "Center windows correctly, even if the windows don't start at 0,0"

This reverts commit 777d482e73.

* Revert "Make hotkeys a function not a singleton class that has side effects when you construct it"

This reverts commit 87ca43ba45.

* Revert "Move global file input out of NPM module"

This reverts commit 9f726899c7.

* Revert "Don't exclude generic windows"

This reverts commit 245dd166a2.

* Revert "Serialize window position as well"

This reverts commit b71e09284e.

* Revert "Persist window states (exept position)"

This reverts commit 690f650e4c.

* Revert "Serialize media state, and apply equalizer state to on deserialize"

This reverts commit 94e105b104.

* Revert "Add a flag to clear IndexDB state"

This reverts commit 364ddb7411.

* Revert "Serialize state to indexdb"

This reverts commit 60429b280a.
2018-10-01 12:50:43 -07:00

332 lines
8.3 KiB
TypeScript

import { DEFAULT_SKIN } from "./constants";
interface Time {
minutesFirstDigit: string;
minutesSecondDigit: string;
secondsFirstDigit: string;
secondsSecondDigit: string;
}
interface IniData {
[section: string]: {
[key: string]: string;
};
}
export const getTimeObj = (time: number | null): Time => {
if (time == null) {
// If we clean up `<MiniTime />` we don't need to do this any more.
return {
minutesFirstDigit: " ",
minutesSecondDigit: " ",
secondsFirstDigit: " ",
secondsSecondDigit: " "
};
}
const minutes = Math.floor(time / 60);
const seconds = time % 60;
const digits =
time == null
? [" ", " ", " ", " "]
: [
String(Math.floor(minutes / 10)),
String(Math.floor(minutes % 10)),
String(Math.floor(seconds / 10)),
String(Math.floor(seconds % 10))
];
const [
minutesFirstDigit,
minutesSecondDigit,
secondsFirstDigit,
secondsSecondDigit
] = digits;
return {
minutesFirstDigit,
minutesSecondDigit,
secondsFirstDigit,
secondsSecondDigit
};
};
export const getTimeStr = (
time: number | null,
truncate: boolean = true
): string => {
if (time == null) {
return "";
}
const {
minutesFirstDigit,
minutesSecondDigit,
secondsFirstDigit,
secondsSecondDigit
} = getTimeObj(time);
return [
truncate && minutesFirstDigit === "0" ? "" : minutesFirstDigit,
minutesSecondDigit,
":",
secondsFirstDigit,
secondsSecondDigit
].join("");
};
export const getFileExtension = (fileName: string): string | null => {
const matches = /\.([a-z]{3,4})$/i.exec(fileName);
return matches ? matches[1].toLowerCase() : null;
};
export const parseViscolors = (text: string): string[] => {
const entries = text.split("\n");
const regex = /^(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/;
const colors = [];
// changed to a hard number to deal with empty lines at the end...
// plus it's only meant to be an exact quantity of numbers anyway.
// - @PAEz
for (let i = 0; i < 24; i++) {
const matches = regex.exec(entries[i]);
colors[i] = matches
? `rgb(${matches.slice(1, 4).join(",")})`
: DEFAULT_SKIN.colors[i];
}
return colors;
};
const SECTION_REGEX = /^\s*\[(.+?)\]\s*$/;
const PROPERTY_REGEX = /^\s*([^;].*)\s*=\s*(.*)\s*$/;
export const parseIni = (text: string): IniData => {
let section: string, match;
return text.split(/[\r\n]+/g).reduce((data: IniData, line) => {
if ((match = line.match(PROPERTY_REGEX)) && section != null) {
const value = match[2].replace(/(^")|("$)|(^')|('$)/gi, "");
data[section][match[1].trim().toLowerCase()] = value;
} else if ((match = line.match(SECTION_REGEX))) {
section = match[1].trim().toLowerCase();
data[section] = {};
}
return data;
}, {});
};
export const clamp = (value: number, min: number, max: number): number =>
Math.min(Math.max(value, min), max);
export const base64FromArrayBuffer = (arrayBuffer: ArrayBuffer): string => {
const dataView = new Uint8Array(arrayBuffer);
return window.btoa(String.fromCharCode(...dataView));
};
// https://stackoverflow.com/a/15832662/1263117
export function downloadURI(uri: string, name: string): void {
const link = document.createElement("a");
link.download = name;
link.href = uri;
window.document.body.appendChild(link);
link.click();
window.document.body.removeChild(link);
}
export const toPercent = (min: number, max: number, value: number): number =>
(value - min) / (max - min);
export const percentToRange = (percent: number, min: number, max: number) =>
min + Math.round(percent * (max - min));
export const percentToIndex = (percent: number, length: number): number =>
percentToRange(percent, 0, length - 1);
const rebound = (
oldMin: number,
oldMax: number,
newMin: number,
newMax: number
) => (oldValue: number): number =>
percentToRange(toPercent(oldMin, oldMax, oldValue), newMin, newMax);
// Convert a .eqf value to a 1-100
export const normalize = rebound(1, 64, 1, 100);
// Convert a 0-100 to an .eqf value
export const denormalize = rebound(1, 100, 1, 64);
// Merge a `source` object to a `target` recursively
// TODO: The typing here is a bit of a disaster.
export function merge<T extends object, S extends object>(
target: T,
source: S
): T & S {
const s = source as any;
const t = target as any;
// Iterate through `source` properties and if an `Object` set property to merge of `target` and `source` properties
for (const key of Object.keys(s as any)) {
if (s[key] instanceof Object) Object.assign(s[key], merge(t[key], s[key]));
}
// Join `target` and modified `source`
Object.assign(target || {}, source);
return target as any;
}
// Maps a value in a range (defined my min/max) to a value in an array (options).
export function segment<V>(
min: number,
max: number,
value: number,
newValues: V[]
): V {
const ratio = toPercent(min, max, value);
/*
| 0 | 1 | 2 |
0 1 2 3
*/
return newValues[percentToIndex(ratio, newValues.length)];
}
export const arraysAreEqual = (a: any[], b: any[]): boolean =>
a.length === b.length && a.every((value, i) => value === b[i]);
// https://bost.ocks.org/mike/shuffle/
// Shuffle an array in O(n)
export function shuffle<T>(array: T[]): T[] {
const sorted = [...array];
let m = sorted.length;
// While there remain elements to shuffle…
while (m) {
// Pick a remaining element…
const i = Math.floor(Math.random() * m--);
// And swap it with the current element.
const val = sorted[m];
sorted[m] = sorted[i];
sorted[i] = val;
}
return sorted;
}
export function sort<T>(
array: T[],
iteratee: (value: T) => number | string
): T[] {
return [...array].sort((a, b) => {
const aKey = iteratee(a);
const bKey = iteratee(b);
if (aKey < bKey) {
return -1;
} else if (aKey > bKey) {
return 1;
}
return 0;
});
}
export function moveSelected<V>(
arr: V[],
isSelected: (index: number) => boolean,
offset: number
): V[] {
const newArr = new Array(arr.length);
let next = 0;
for (let i = 0; i < newArr.length; i++) {
const from = i - offset;
// Is a value supposed to move here?
if (from >= 0 && from < arr.length && isSelected(from)) {
newArr[i] = arr[from];
} else {
while (next < arr.length && isSelected(next)) {
next++;
}
newArr[i] = arr[next];
next++;
}
}
return newArr;
}
export function spliceIn<T>(original: T[], start: number, newValues: T[]): T[] {
const newArr = [...original];
newArr.splice(start, 0, ...newValues);
return newArr;
}
type Procedure = (...args: any[]) => void;
export function debounce<F extends Procedure>(func: F, delay: number): F {
let token: NodeJS.Timer;
return function(this: any, ...args: any[]): void {
if (token != null) {
clearTimeout(token);
}
token = setTimeout(() => {
func.apply(this, args);
}, delay);
} as any;
}
let counter = 0;
export function uniqueId() {
return counter++;
}
export function objectForEach<V>(
obj: { [key: string]: V },
cb: (value: V, key: string) => void
): void {
Object.keys(obj).forEach(key => cb(obj[key], key));
}
export function objectMap<V, N>(
obj: { [key: string]: V },
cb: (value: V, key: string) => N
): { [key: string]: N } {
const modified: { [key: string]: N } = {};
Object.keys(obj).forEach(key => (modified[key] = cb(obj[key], key)));
return modified;
}
export function objectFilter<V>(
obj: { [key: string]: V },
predicate: (value: V, key: string) => boolean
) {
// TODO: Could return the original reference if no values change
return Object.keys(obj).reduce((newObj: { [key: string]: V }, key) => {
if (predicate(obj[key], key)) {
newObj[key] = obj[key];
}
return newObj;
}, {});
}
interface Window {
left: number;
top: number;
x: number;
y: number;
width: number;
height: number;
}
export const calculateBoundingBox = (windows: Window[]) =>
windows.reduce(
(b, w) => ({
left: Math.min(b.left, w.x),
top: Math.min(b.top, w.y),
bottom: Math.max(b.bottom, w.y + w.height),
right: Math.max(b.right, w.x + w.width)
}),
{ top: 0, bottom: 0, left: 0, right: 0 }
);
export function findLastIndex<T>(arr: T[], cb: (val: T) => boolean) {
for (let i = arr.length - 1; i >= 0; i--) {
if (cb(arr[i])) {
return i;
}
}
return -1;
}