function getRandomIndex(arr) { return Math.floor(Math.random() * arr.length); } function getRandomValue(arr) { return arr[getRandomIndex(arr)]; } function getLast(arr) { return arr[arr.length - 1]; } async function readFileAsText(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(e) { resolve(e.target.result); }; reader.onerror = function(e) { reject(e); }; reader.readAsText(file); }); } /** * Track a collection of async loaded presets * * Presets can be changed via `next`, `previous` or `selectIndex`. In each case, * a promise is returned. If the promise resolves to `null` it means the * selection was canceled by a subsequent request before it could be fulfilled. * If the request is successful, the promise resolves to the selected preset. * * We assume a model where some portion of the preset are supplied at initialization * and the remainder can be loaded async via the function `getRest`. */ export default class Presets { constructor({ keys, initialPresets, getRest, presetConverterEndpoint, loadConvertPreset, randomize = true }) { this._keys = keys; // Alphabetical list of preset names this._presets = initialPresets; // Presets indexed by name this._getRest = getRest; // An async function to get the rest of the presets this._presetConverterEndpoint = presetConverterEndpoint; this._loadConvertPreset = loadConvertPreset; this._history = []; // Indexes into _keys this._randomize = randomize; // Initialize with a key that we already have. const avaliableKeys = Object.keys(initialPresets); const currentKey = randomize ? getRandomValue(avaliableKeys) : avaliableKeys[0]; this._currentIndex = this._keys.indexOf(currentKey); this._history.push(this._currentIndex); } toggleRandomize() { this._randomize = !this._randomize; } setRandomize(val) { this._randomize = val; } addPresets(presets) { const startIdx = this._keys.length; this._keys = this._keys.concat(Object.keys(presets)); const endIndx = this._keys.length; this._presets = Object.assign(this._presets, presets); return [startIdx, endIndx]; } loadPresets(presets) { this._keys = Object.keys(presets); this._presets = presets; this._history = []; return this._keys.length; } async next() { let idx; if (this._randomize || this._history.length === 0) { idx = getRandomIndex(this._keys); } else { idx = (getLast(this._history) + 1) % this._keys.length; } this._history.push(idx); return this._selectIndex(idx); } async previous() { if (this._history.length > 1) { this._history.pop(); return this._selectIndex(getLast(this._history)); } // We are at the very beginning. There is no "previous" preset. return Promise.resolve(); } async selectIndex(idx) { // The public version of this method must add to the history this._history.push(idx); return this._selectIndex(idx); } async _convertPreset(file) { const convertPreset = await this._loadConvertPreset(); return convertPreset(file, this._presetConverterEndpoint); } async _selectIndex(idx) { const preset = this._presets[this._keys[idx]]; if (!preset) { const rest = await this._getRest(); this._presets = Object.assign(this._presets, rest); if (getLast(this._history) !== idx) { // This selection must be obsolete. Return null so that // the caller knows this request got canceled. return null; } } if (preset && preset.file) { try { const fileContents = await readFileAsText(preset.file); const convertedPreset = await this._convertPreset(fileContents); this._presets[this._keys[idx]] = convertedPreset; } catch (e) { console.error(e); alert(`Unable to convert MilkDrop preset ${this._keys[idx]}`); } } this._currentIndex = idx; return this.getCurrent(); } getKeys() { return this._keys; } getCurrentIndex() { return this._currentIndex; } getCurrent() { // #matryoshka return this._presets[this._keys[this._currentIndex]]; } }