import { $TD } from "../api/bridge.js"; if (!("$TDP" in window)) { throw "Missing $TDP in global scope"; } /** * @type {Object} * @property {function(token: number, path: string): Promise<boolean>} checkFileExists * @property {function(token: number, path: string, cache: boolean): Promise<string>} readFile * @property {function(token: number, path: string): Promise<string>} readFileRoot * @property {function(token: number)} setConfigurable * @property {function(token: number, path: string, contents: string): Promise<string>} writeFile */ export const $TDP = window.$TDP; /** * @typedef TweetDuckPlugin * @type {Object} * * @property {string} $id * @property {number} $token * @property {Storage} [$storage] * @property {function} [configure] */ /** * Validates that a plugin object contains a token. * @param {TweetDuckPlugin} pluginObject */ export function validatePluginObject(pluginObject) { if (!("$token" in pluginObject)) { throw "Invalid plugin object."; } } /** * Loads a simple JavaScript object as configuration. * @param {TweetDuckPlugin} pluginObject * @param {string} fileNameUser * @param {string|null} fileNameDefault * @param {function(Object)} onSuccess * @param {function(Error)} onFailure */ export function loadConfigurationFile(pluginObject, fileNameUser, fileNameDefault, onSuccess, onFailure) { validatePluginObject(pluginObject); const identifier = pluginObject.$id; const token = pluginObject.$token; $TDP.checkFileExists(token, fileNameUser).then(exists => { /** @type {string|null} */ const fileName = exists ? fileNameUser : fileNameDefault; if (fileName === null) { onSuccess && onSuccess({}); return; } (exists ? $TDP.readFile(token, fileName, true) : $TDP.readFileRoot(token, fileName)).then(contents => { let obj; try { // noinspection DynamicallyGeneratedCodeJS obj = eval("(" + contents + ")"); } catch (err) { if (!(onFailure && onFailure(err))) { $TD.alert("warning", "Problem loading '" + fileName + "' file for '" + identifier + "' plugin, the JavaScript syntax is invalid: " + err.message); } return; } onSuccess && onSuccess(obj); }).catch(err => { if (!(onFailure && onFailure(err))) { $TD.alert("warning", "Problem loading '" + fileName + "' file for '" + identifier + "' plugin: " + err.message); } }); }).catch(err => { if (!(onFailure && onFailure(err))) { $TD.alert("warning", "Problem checking '" + fileNameUser + "' file for '" + identifier + "' plugin: " + err.message); } }); } /** * Creates and returns an object for managing a custom stylesheet. * @param {TweetDuckPlugin} pluginObject * @returns {{insert: (function(string): number), remove: (function(): void), element: HTMLStyleElement}} */ export function createCustomStyle(pluginObject) { validatePluginObject(pluginObject); const element = document.createElement("style"); element.id = "plugin-" + pluginObject.$id + "-" + Math.random().toString(36).substring(2, 7); document.head.appendChild(element); return { insert: (rule) => element.sheet.insertRule(rule, 0), remove: () => element.remove(), element }; } /** * Returns a function that mimics a Storage object that will be saved in the plugin. * @param {TweetDuckPlugin} pluginObject * @param {function(Storage)} onReady */ export function createStorage(pluginObject, onReady) { validatePluginObject(pluginObject); if ("$storage" in pluginObject) { if (pluginObject.$storage !== null) { // set to null while the file is still loading onReady(pluginObject.$storage); } return; } class Storage { get length() { return Object.keys(this).length; } key(index) { return Object.keys(this)[index]; } // noinspection JSUnusedGlobalSymbols getItem(key) { return this[key] || null; } setItem(key, value) { this[key] = value; updateFile(); } removeItem(key) { delete this[key]; updateFile(); } clear() { for (const key of Object.keys(this)) { delete this[key]; } updateFile(); } replace(obj, silent) { for (const key of Object.keys(this)) { delete this[key]; } for (const key in obj) { this[key] = obj[key]; } if (!silent) { updateFile(); } } } // noinspection JSUnusedLocalSymbols,JSUnusedGlobalSymbols const handler = { get(obj, prop, receiver) { const value = obj[prop]; return typeof value === "function" ? value.bind(obj) : value; }, set(obj, prop, value) { obj.setItem(prop, value); return true; }, deleteProperty(obj, prop) { obj.removeItem(prop); return true; }, enumerate(obj) { return Object.keys(obj); } }; const storage = new Proxy(new Storage(), handler); let delay = -1; const updateFile = function() { window.clearTimeout(delay); delay = window.setTimeout(function() { // noinspection JSIgnoredPromiseFromCall $TDP.writeFile(pluginObject.$token, ".storage", JSON.stringify(storage)); }, 0); }; pluginObject.$storage = null; loadConfigurationFile(pluginObject, ".storage", null, function(obj) { storage.replace(obj, true); onReady(pluginObject.$storage = storage); }, function() { onReady(pluginObject.$storage = storage); }); }