const PLUGIN_API_VERSION = "1.0" const PLUGIN_CATALOG = 'https://raw.githubusercontent.com/easydiffusion/easydiffusion-plugins/main/plugins.json' const PLUGIN_CATALOG_GITHUB = 'https://github.com/easydiffusion/easydiffusion-plugins/blob/main/plugins.json' const PLUGINS = { /** * Register new buttons to show on each output image. * * Example: * PLUGINS['IMAGE_INFO_BUTTONS'].push({ * text: 'Make a Similar Image', * on_click: function(origRequest, image) { * let newTaskRequest = getCurrentUserRequest() * newTaskRequest.reqBody = Object.assign({}, origRequest, { * init_image: image.src, * prompt_strength: 0.7, * seed: Math.floor(Math.random() * 10000000) * }) * newTaskRequest.seed = newTaskRequest.reqBody.seed * createTask(newTaskRequest) * }, * filter: function(origRequest, image) { * // this is an optional function. return true/false to show/hide the button * // if this function isn't set, the button will always be visible * return true * } * }) */ IMAGE_INFO_BUTTONS: [], GET_PROMPTS_HOOK: [], MODIFIERS_LOAD: [], TASK_CREATE: [], OUTPUTS_FORMATS: new ServiceContainer( function png() { return (reqBody) => new SD.RenderTask(reqBody) }, function jpeg() { return (reqBody) => new SD.RenderTask(reqBody) }, function webp() { return (reqBody) => new SD.RenderTask(reqBody) } ), } PLUGINS.OUTPUTS_FORMATS.register = function(...args) { const service = ServiceContainer.prototype.register.apply(this, args) if (typeof outputFormatField !== "undefined") { const newOption = document.createElement("option") newOption.setAttribute("value", service.name) newOption.innerText = service.name outputFormatField.appendChild(newOption) } return service } function loadScript(url) { const script = document.createElement("script") const promiseSrc = new PromiseSource() script.addEventListener("error", () => promiseSrc.reject(new Error(`Script "${url}" couldn't be loaded.`))) script.addEventListener("load", () => promiseSrc.resolve(url)) script.src = url + "?t=" + Date.now() console.log("loading script", url) document.head.appendChild(script) return promiseSrc.promise } async function loadUIPlugins() { try { const res = await fetch("/get/ui_plugins") if (!res.ok) { console.error(`Error HTTP${res.status} while loading plugins list. - ${res.statusText}`) return } const plugins = await res.json() const loadingPromises = plugins.map(loadScript) return await Promise.allSettled(loadingPromises) } catch (e) { console.log("error fetching plugin paths", e) } } /* PLUGIN MANAGER */ /* plugin tab */ document.querySelector('.tab-container')?.insertAdjacentHTML('beforeend', ` Plugins `) document.querySelector('#tab-content-wrapper')?.insertAdjacentHTML('beforeend', `
Loading...
`) const tabPlugin = document.querySelector('#tab-plugin') if (tabPlugin) { linkTabContents(tabPlugin) } const plugin = document.querySelector('#plugin') plugin.innerHTML = `

Plugin Manager

Changes take effect after reloading the page
` const pluginsTable = document.querySelector("#plugin-manager-section .plugins-table") const pluginNotificationTable = document.querySelector("#plugin-notification-list .notifications-table") const pluginNoNotification = document.querySelector("#plugin-notification-list .no-notification") /* notification center */ const pluginNotificationButton = document.getElementById("plugin-notification-button"); const pluginNotificationList = document.getElementById("plugin-notification-list"); const notificationPill = document.getElementById("notification-pill"); const pluginManagerSection = document.getElementById("plugin-manager-section"); let pluginNotifications; // Add event listener to show/hide the action center pluginNotificationButton.addEventListener("click", function () { // Hide the notification pill when the action center is opened notificationPill.style.display = "none" pluginNotifications.lastUpdated = Date.now() // save the notifications setStorageData('notifications', JSON.stringify(pluginNotifications)) renderPluginNotifications() if (pluginNotificationList.style.display === "none") { pluginNotificationList.style.display = "block" pluginManagerSection.style.display = "none" } else { pluginNotificationList.style.display = "none" pluginManagerSection.style.display = "block" } }) document.addEventListener("tabClick", (e) => { if (e.detail.name == 'plugin') { pluginNotificationList.style.display = "none" pluginManagerSection.style.display = "block" } }) async function addPluginNotification(pluginNotifications, messageText, error) { const now = Date.now() pluginNotifications.entries.unshift({ date: now, text: messageText, error: error }); // add new entry to the beginning of the array if (pluginNotifications.entries.length > 50) { pluginNotifications.entries.length = 50 // limit array length to 50 entries } pluginNotifications.lastUpdated = now notificationPill.style.display = "block" // save the notifications await setStorageData('notifications', JSON.stringify(pluginNotifications)) } function timeAgo(inputDate) { const now = new Date(); const date = new Date(inputDate); const diffInSeconds = Math.floor((now - date) / 1000); const units = [ { name: 'year', seconds: 31536000 }, { name: 'month', seconds: 2592000 }, { name: 'week', seconds: 604800 }, { name: 'day', seconds: 86400 }, { name: 'hour', seconds: 3600 }, { name: 'minute', seconds: 60 }, { name: 'second', seconds: 1 } ]; for (const unit of units) { const unitValue = Math.floor(diffInSeconds / unit.seconds); if (unitValue > 0) { return `${unitValue} ${unit.name}${unitValue > 1 ? 's' : ''} ago`; } } return 'just now'; } function convertSeconds(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainingSeconds = seconds % 60; let timeParts = []; if (hours === 1) { timeParts.push(`${hours} hour`); } else if (hours > 1) { timeParts.push(`${hours} hours`); } if (minutes === 1) { timeParts.push(`${minutes} minute`); } else if (minutes > 1) { timeParts.push(`${minutes} minutes`); } if (remainingSeconds === 1) { timeParts.push(`${remainingSeconds} second`); } else if (remainingSeconds > 1) { timeParts.push(`${remainingSeconds} seconds`); } return timeParts.join(', and '); } function renderPluginNotifications() { pluginNotificationTable.innerHTML = '' if (pluginNotifications.entries?.length > 0) { pluginNoNotification.style.display = "none" pluginNotificationTable.style.display = "block" } else { pluginNoNotification.style.display = "block" pluginNotificationTable.style.display = "none" } for (let i = 0; i < pluginNotifications.entries?.length; i++) { const date = pluginNotifications.entries[i].date const text = pluginNotifications.entries[i].text const error = pluginNotifications.entries[i].error const newRow = document.createElement('div') newRow.innerHTML = ` ${text}
${timeAgo(date)}
`; pluginNotificationTable.appendChild(newRow) } } /* search box */ function filterPlugins() { let search = pluginFilter.value.toLowerCase(); let searchTerms = search.split(' '); let labels = pluginsTable.querySelectorAll("label.plugin-name"); for (let i = 0; i < labels.length; i++) { let label = labels[i].innerText.toLowerCase(); let match = true; for (let j = 0; j < searchTerms.length; j++) { let term = searchTerms[j].trim(); if (term && label.indexOf(term) === -1) { match = false; break; } } if (match) { labels[i].closest('.plugin-container').style.display = "flex"; } else { labels[i].closest('.plugin-container').style.display = "none"; } } } // Call debounce function on filterImageModifierList function with 200ms wait time. Thanks JeLuf! const debouncedFilterPlugins = debounce(filterPlugins, 200); // add the searchbox pluginsTable.insertAdjacentHTML('beforebegin', ``) const pluginFilter = document.getElementById("plugin-filter") // search box // Add the debounced function to the keyup event listener pluginFilter.addEventListener('keyup', debouncedFilterPlugins); // select the text on focus pluginFilter.addEventListener('focus', function (event) { pluginFilter.select() }); // empty the searchbox on escape pluginFilter.addEventListener('keydown', function (event) { if (event.key === 'Escape') { pluginFilter.value = ''; filterPlugins(); } }); // focus on the search box upon tab selection document.addEventListener("tabClick", (e) => { if (e.detail.name == 'plugin') { pluginFilter.focus() } }) // refresh link pluginsTable.insertAdjacentHTML('afterend', `

Refresh plugins

(Plugin developers, add your plugins to plugins.json)

`) const refreshPlugins = document.getElementById("refresh-plugins") refreshPlugins.addEventListener("click", async function (event) { event.preventDefault() await initPlugins(true) }) function showPluginToast(message, duration = 5000, error = false, addNotification = true) { if (addNotification === true) { addPluginNotification(pluginNotifications, message, error) } try { showToast(message, duration, error) } catch (error) { console.error('Error while trying to show toast:', error); } } function matchPluginFileNames(fileName1, fileName2) { const regex = /^(.+?)(?:-\d+(\.\d+)*)?\.plugin\.js$/; const match1 = fileName1.match(regex); const match2 = fileName2.match(regex); if (match1 && match2 && match1[1] === match2[1]) { return true; // the two file names match } else { return false; // the two file names do not match } } function extractFilename(filepath) { // Normalize the path separators to forward slashes and make the file names lowercase const normalizedFilePath = filepath.replace(/\\/g, "/").toLowerCase(); // Strip off the path from the file name const fileName = normalizedFilePath.substring(normalizedFilePath.lastIndexOf("/") + 1); return fileName } function checkFileNameInArray(paths, filePath) { // Strip off the path from the file name const fileName = extractFilename(filePath); // Check if the file name exists in the array of paths return paths.some(path => { // Strip off the path from the file name const baseName = extractFilename(path); // Check if the file names match and return the result as a boolean return matchPluginFileNames(fileName, baseName); }); } function isGitHub(url) { return url.startsWith("https://raw.githubusercontent.com/") === true } /* fill in the plugins table */ function getIncompatiblePlugins(pluginId) { const enabledPlugins = plugins.filter(plugin => plugin.enabled && plugin.id !== pluginId); const incompatiblePlugins = enabledPlugins.filter(plugin => plugin.compatIssueIds?.includes(pluginId)); const pluginNames = incompatiblePlugins.map(plugin => plugin.name); if (pluginNames.length === 0) { return null; } const pluginNamesList = pluginNames.map(name => `
  • ${name}
  • `).join(''); return ``; } async function initPluginTable(plugins) { pluginsTable.innerHTML = '' plugins.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })) plugins.forEach(plugin => { const name = plugin.name const author = plugin.author ? ', by ' + plugin.author : '' const version = plugin.version ? ' (version: ' + plugin.version + ')' : '' const warning = getIncompatiblePlugins(plugin.id) ? `This plugin might conflict with:${getIncompatiblePlugins(plugin.id)}` : '' const note = plugin.description ? `${plugin.description.replaceAll('\n', '
    ')}
    ` : `No description`; const icon = plugin.icon ? `` : ''; const newRow = document.createElement('div') const localPluginFound = checkFileNameInArray(localPlugins, plugin.url) newRow.innerHTML = `
    ${icon}
    ${warning}${note}Source: ${extractFilename(plugin.url)}
    ${localPluginFound ? "Installed locally" : (plugin.localInstallOnly ? 'Download and
    install manually
    ' : (isGitHub(plugin.url) ? '' : '' ) ) }
    `; newRow.classList.add('plugin-container') //console.log(plugin.id, plugin.localInstallOnly) pluginsTable.appendChild(newRow) const pluginManualInstall = pluginsTable.querySelector('#plugin-' + plugin.id + '-install') updateManualInstallButtonCaption() // checkbox event handler const pluginToggle = pluginsTable.querySelector('#plugin-' + plugin.id) if (pluginToggle !== null) { pluginToggle.checked = plugin.enabled // set initial state of checkbox pluginToggle.addEventListener('change', async () => { const container = pluginToggle.closest(".plugin-container"); const warningElement = container.querySelector(".plugin-warning"); // if the plugin got enabled, download the plugin's code plugin.enabled = pluginToggle.checked if (plugin.enabled) { const pluginSource = await getDocument(plugin.url); if (pluginSource !== null) { // Store the current scroll position before navigating away const currentPosition = window.pageYOffset; initPluginTable(plugins) // When returning to the page, set the scroll position to the stored value window.scrollTo(0, currentPosition); warningElement?.classList.remove("hide"); plugin.code = pluginSource loadPlugins([plugin]) console.log(`Plugin ${plugin.name} installed`); showPluginToast("Plugin " + plugin.name + " installed"); } else { plugin.enabled = false pluginToggle.checked = false console.error(`Couldn't download plugin ${plugin.name}`); showPluginToast("Failed to install " + plugin.name + " (Couldn't fetch " + extractFilename(plugin.url) + ")", 5000, true); } } else { warningElement?.classList.add("hide"); // Store the current scroll position before navigating away const currentPosition = window.pageYOffset; initPluginTable(plugins) // When returning to the page, set the scroll position to the stored value window.scrollTo(0, currentPosition); console.log(`Plugin ${plugin.name} uninstalled`); showPluginToast("Plugin " + plugin.name + " uninstalled"); } await setStorageData('plugins', JSON.stringify(plugins)) }) } // manual install event handler if (pluginManualInstall !== null) { pluginManualInstall.addEventListener('click', async () => { pluginDialogOpenDialog(inputOK, inputCancel) pluginDialogTextarea.value = plugin.code ? plugin.code : '' pluginDialogTextarea.select() pluginDialogTextarea.focus() }) } // Dialog OK async function inputOK() { let pluginSource = pluginDialogTextarea.value // remove empty lines and trim existing lines plugin.code = pluginSource if (pluginSource.trim() !== '') { plugin.enabled = true console.log(`Plugin ${plugin.name} installed`); showPluginToast("Plugin " + plugin.name + " installed"); } else { plugin.enabled = false console.log(`No code provided for plugin ${plugin.name}, disabling the plugin`); showPluginToast("No code provided for plugin " + plugin.name + ", disabling the plugin"); } updateManualInstallButtonCaption() await setStorageData('plugins', JSON.stringify(plugins)) } // Dialog Cancel async function inputCancel() { plugin.enabled = false console.log(`Installation of plugin ${plugin.name} cancelled`); showPluginToast("Cancelled installation of " + plugin.name); } // update button caption function updateManualInstallButtonCaption() { if (pluginManualInstall !== null) { pluginManualInstall.innerHTML = plugin.code === undefined || plugin.code.trim() === '' ? 'Install' : 'Edit' } } }) prettifyInputs(pluginsTable) filterPlugins() } /* version management. Thanks Madrang! */ const parseVersion = function (versionString, options = {}) { if (typeof versionString === "undefined") { throw new Error("versionString is undefined."); } if (typeof versionString !== "string") { throw new Error("versionString is not a string."); } const lexicographical = options && options.lexicographical; const zeroExtend = options && options.zeroExtend; let versionParts = versionString.split('.'); function isValidPart(x) { const re = (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/); return re.test(x); } if (!versionParts.every(isValidPart)) { throw new Error("Version string is invalid."); } if (zeroExtend) { while (versionParts.length < 4) { versionParts.push("0"); } } if (!lexicographical) { versionParts = versionParts.map(Number); } return versionParts; }; const versionCompare = function (v1, v2, options = {}) { if (typeof v1 == "undefined") { throw new Error("vi is undefined."); } if (typeof v2 === "undefined") { throw new Error("v2 is undefined."); } let v1parts; if (typeof v1 === "string") { v1parts = parseVersion(v1, options); } else if (Array.isArray(v1)) { v1parts = [...v1]; if (!v1parts.every(p => typeof p === "number" && p !== NaN)) { throw new Error("v1 part array does not only contains numbers."); } } else { throw new Error("v1 is of an unexpected type: " + typeof v1); } let v2parts; if (typeof v2 === "string") { v2parts = parseVersion(v2, options); } else if (Array.isArray(v2)) { v2parts = [...v2]; if (!v2parts.every(p => typeof p === "number" && p !== NaN)) { throw new Error("v2 part array does not only contains numbers."); } } else { throw new Error("v2 is of an unexpected type: " + typeof v2); } while (v1parts.length < v2parts.length) { v1parts.push("0"); } while (v2parts.length < v1parts.length) { v2parts.push("0"); } for (let i = 0; i < v1parts.length; ++i) { if (v2parts.length == i) { return 1; } if (v1parts[i] == v2parts[i]) { continue; } else if (v1parts[i] > v2parts[i]) { return 1; } else { return -1; } } return 0; }; function filterPluginsByMinEDVersion(plugins, EDVersion) { const filteredPlugins = plugins.filter(plugin => { if (plugin.minEDVersion) { return versionCompare(plugin.minEDVersion, EDVersion) <= 0; } return true; }); return filteredPlugins; } function extractVersionNumber(elem) { const versionStr = elem.innerHTML; const regex = /v(\d+\.\d+\.\d+)/; const matches = regex.exec(versionStr); if (matches && matches.length > 1) { return matches[1]; } else { return null; } } const EasyDiffusionVersion = extractVersionNumber(document.querySelector('#top-nav > #logo')) /* PLUGIN MANAGEMENT */ let plugins let localPlugins let initPluginsInProgress = false async function initPlugins(refreshPlugins = false) { let pluginsLoaded if (initPluginsInProgress === true) { return } initPluginsInProgress = true const res = await fetch('/get/ui_plugins') if (!res.ok) { console.error(`Error HTTP${res.status} while loading plugins list. - ${res.statusText}`) } else { localPlugins = await res.json() } if (refreshPlugins === false) { // load the notifications pluginNotifications = await getStorageData('notifications') if (typeof pluginNotifications === "string") { try { pluginNotifications = JSON.parse(pluginNotifications) } catch (e) { console.error("Failed to parse pluginNotifications", e); pluginNotifications = {}; pluginNotifications.entries = []; } } if (pluginNotifications !== undefined) { if (pluginNotifications.entries && pluginNotifications.entries.length > 0 && pluginNotifications.entries[0].date && pluginNotifications.lastUpdated <= pluginNotifications.entries[0].date) { notificationPill.style.display = "block"; } } else { pluginNotifications = {}; pluginNotifications.entries = []; } // try and load plugins from local cache plugins = await getStorageData('plugins') if (plugins !== undefined) { plugins = JSON.parse(plugins) // remove duplicate entries if any (should not happen) plugins = deduplicatePluginsById(plugins) // remove plugins that don't meet the min ED version requirement plugins = filterPluginsByMinEDVersion(plugins, EasyDiffusionVersion) // remove from plugins the entries that don't have mandatory fields (id, name, url) plugins = plugins.filter((plugin) => { return plugin.id !== '' && plugin.name !== '' && plugin.url !== ''; }); // populate the table initPluginTable(plugins) await loadPlugins(plugins) pluginsLoaded = true } else { plugins = [] pluginsLoaded = false } } // update plugins asynchronously (updated versions will be available next time the UI is loaded) if (refreshAllowed()) { let pluginCatalog = await getDocument(PLUGIN_CATALOG) if (pluginCatalog !== null) { let parseError = false; try { pluginCatalog = JSON.parse(pluginCatalog); console.log('Plugin catalog successfully downloaded'); } catch (error) { console.error('Error parsing plugin catalog:', error); parseError = true; } if (!parseError) { await downloadPlugins(pluginCatalog, plugins, refreshPlugins) // update compatIssueIds updateCompatIssueIds() // remove plugins that don't meet the min ED version requirement plugins = filterPluginsByMinEDVersion(plugins, EasyDiffusionVersion) // remove from plugins the entries that don't have mandatory fields (id, name, url) plugins = plugins.filter((plugin) => { return plugin.id !== '' && plugin.name !== '' && plugin.url !== ''; }); // remove from plugins the entries that no longer exist in the catalog plugins = plugins.filter((plugin) => { return pluginCatalog.some((p) => p.id === plugin.id) }); if (pluginCatalog.length > plugins.length) { const newPlugins = pluginCatalog.filter((plugin) => { return !plugins.some((p) => p.id === plugin.id); }); newPlugins.forEach((plugin, index) => { setTimeout(() => { showPluginToast(`New plugin "${plugin.name}" is available.`); }, (index + 1) * 1000); }); } let pluginsJson; try { pluginsJson = JSON.stringify(plugins); // attempt to parse plugins to JSON } catch (error) { console.error('Error converting plugins to JSON:', error); } if (pluginsJson) { // only store the data if pluginsJson is not null or undefined await setStorageData('plugins', pluginsJson) } // refresh the display of the plugins table initPluginTable(plugins) if (pluginsLoaded && pluginsLoaded === false) { loadPlugins(plugins) } } } } else { if (refreshPlugins) { showPluginToast('Plugins have been refreshed recently, refresh will be available in ' + convertSeconds(getTimeUntilNextRefresh()), 5000, true, false) } } initPluginsInProgress = false } function updateMetaTagPlugins(plugin) { // Update the meta tag with the list of loaded plugins let metaTag = document.querySelector('meta[name="plugins"]'); if (metaTag === null) { metaTag = document.createElement('meta'); metaTag.name = 'plugins'; document.head.appendChild(metaTag); } const pluginArray = [...(metaTag.content ? metaTag.content.split(',') : []), plugin.id]; metaTag.content = pluginArray.join(','); } function updateCompatIssueIds() { // Loop through each plugin plugins.forEach(plugin => { // Check if the plugin has `compatIssueIds` property if (plugin.compatIssueIds !== undefined) { // Loop through each of the `compatIssueIds` plugin.compatIssueIds.forEach(issueId => { // Find the plugin with the corresponding `issueId` const issuePlugin = plugins.find(p => p.id === issueId); // If the corresponding plugin is found, initialize its `compatIssueIds` property with an empty array if it's undefined if (issuePlugin) { if (issuePlugin.compatIssueIds === undefined) { issuePlugin.compatIssueIds = []; } // If the current plugin's ID is not already in the `compatIssueIds` array, add it if (!issuePlugin.compatIssueIds.includes(plugin.id)) { issuePlugin.compatIssueIds.push(plugin.id); } } }); } else { // If the plugin doesn't have `compatIssueIds` property, initialize it with an empty array plugin.compatIssueIds = []; } }); } function deduplicatePluginsById(plugins) { const seenIds = new Set(); const deduplicatedPlugins = []; for (const plugin of plugins) { if (!seenIds.has(plugin.id)) { seenIds.add(plugin.id); deduplicatedPlugins.push(plugin); } else { // favor dupes that have enabled == true const index = deduplicatedPlugins.findIndex(p => p.id === plugin.id); if (index >= 0) { if (plugin.enabled) { deduplicatedPlugins[index] = plugin; } } } } return deduplicatedPlugins; } async function loadPlugins(plugins) { for (let i = 0; i < plugins.length; i++) { const plugin = plugins[i]; if (plugin.enabled === true && plugin.localInstallOnly !== true) { const localPluginFound = checkFileNameInArray(localPlugins, plugin.url); if (!localPluginFound) { try { // Indirect eval to work around sloppy plugin implementations const indirectEval = { eval }; console.log("Loading plugin " + plugin.name); indirectEval.eval(plugin.code); console.log("Plugin " + plugin.name + " loaded"); await updateMetaTagPlugins(plugin); // add plugin to the meta tag } catch (err) { showPluginToast("Error loading plugin " + plugin.name + " (" + err.message + ")", null, true); console.error("Error loading plugin " + plugin.name + ": " + err.message); } } else { console.log("Skipping plugin " + plugin.name + " (installed locally)"); } } } } async function getFileHash(url) { const regex = /^https:\/\/raw\.githubusercontent\.com\/(?[^/]+)\/(?[^/]+)\/(?[^/]+)\/(?.+)$/; const match = url.match(regex); if (!match) { console.error('Invalid GitHub repository URL.'); return Promise.resolve(null); } const owner = match.groups.owner; const repo = match.groups.repo; const branch = match.groups.branch; const filePath = match.groups.filePath; const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}`; try { const response = await fetch(apiUrl); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}, url: ${apiUrl}`); } const data = await response.json(); return data.sha; } catch (error) { console.error('Error fetching data from url:', apiUrl, 'Error:', error); return null; } } // only allow two refresh per hour function getTimeUntilNextRefresh() { const lastRuns = JSON.parse(localStorage.getItem('lastRuns') || '[]'); const currentTime = new Date().getTime(); const numRunsLast60Min = lastRuns.filter(run => currentTime - run <= 60 * 60 * 1000).length; if (numRunsLast60Min >= 2) { return 3600 - Math.round((currentTime - lastRuns[lastRuns.length - 1]) / 1000); } return 0; } function refreshAllowed() { const timeUntilNextRefresh = getTimeUntilNextRefresh(); if (timeUntilNextRefresh > 0) { console.log(`Next refresh available in ${timeUntilNextRefresh} seconds`); return false; } const lastRuns = JSON.parse(localStorage.getItem('lastRuns') || '[]'); const currentTime = new Date().getTime(); lastRuns.push(currentTime); localStorage.setItem('lastRuns', JSON.stringify(lastRuns)); return true; } async function downloadPlugins(pluginCatalog, plugins, refreshPlugins) { // download the plugins as needed for (const plugin of pluginCatalog) { //console.log(plugin.id, plugin.url) const existingPlugin = plugins.find(p => p.id === plugin.id); // get the file hash in the GitHub repo let sha if (isGitHub(plugin.url) && existingPlugin?.enabled === true) { sha = await getFileHash(plugin.url) } if (plugin.localInstallOnly !== true && isGitHub(plugin.url) && existingPlugin?.enabled === true && (refreshPlugins || (existingPlugin.sha !== undefined && existingPlugin.sha !== null && existingPlugin.sha !== sha) || existingPlugin?.code === undefined)) { const pluginSource = await getDocument(plugin.url); if (pluginSource !== null && pluginSource !== existingPlugin.code) { console.log(`Plugin ${plugin.name} updated`); showPluginToast("Plugin " + plugin.name + " updated", 5000); // Update the corresponding plugin const updatedPlugin = { ...existingPlugin, icon: plugin.icon ? plugin.icon : "fa-puzzle-piece", id: plugin.id, name: plugin.name, description: plugin.description, url: plugin.url, localInstallOnly: Boolean(plugin.localInstallOnly), version: plugin.version, code: pluginSource, author: plugin.author, sha: sha, compatIssueIds: plugin.compatIssueIds }; // Replace the old plugin in the plugins array const pluginIndex = plugins.indexOf(existingPlugin); if (pluginIndex >= 0) { plugins.splice(pluginIndex, 1, updatedPlugin); } else { plugins.push(updatedPlugin); } } } else if (existingPlugin !== undefined) { // Update the corresponding plugin's metadata const updatedPlugin = { ...existingPlugin, icon: plugin.icon ? plugin.icon : "fa-puzzle-piece", id: plugin.id, name: plugin.name, description: plugin.description, url: plugin.url, localInstallOnly: Boolean(plugin.localInstallOnly), version: plugin.version, author: plugin.author, compatIssueIds: plugin.compatIssueIds }; // Replace the old plugin in the plugins array const pluginIndex = plugins.indexOf(existingPlugin); plugins.splice(pluginIndex, 1, updatedPlugin); } else { plugins.push(plugin); } } } async function getDocument(url) { try { let response = await fetch(url === PLUGIN_CATALOG ? PLUGIN_CATALOG : url, { cache: "no-cache" }); if (!response.ok) { throw new Error(`Response error: ${response.status} ${response.statusText}`); } let document = await response.text(); return document; } catch (error) { showPluginToast("Couldn't fetch " + extractFilename(url) + " (" + error + ")", null, true); console.error(error); return null; } } /* MODAL DIALOG */ const pluginDialogDialog = document.createElement("div"); pluginDialogDialog.id = "pluginDialog-input-dialog"; pluginDialogDialog.style.display = "none"; pluginDialogDialog.innerHTML = `

    Paste the plugin's code here

    `; document.body.appendChild(pluginDialogDialog); const pluginDialogOverlay = document.querySelector(".pluginDialog-dialog-overlay"); const pluginDialogOkButton = document.getElementById("pluginDialog-input-ok"); const pluginDialogCancelButton = document.getElementById("pluginDialog-input-cancel"); const pluginDialogCloseButton = document.querySelector(".pluginDialog-dialog-close-button"); const pluginDialogTextarea = document.getElementById("pluginDialog-input-textarea"); let callbackOK let callbackCancel function pluginDialogOpenDialog(inputOK, inputCancel) { pluginDialogDialog.style.display = "block"; callbackOK = inputOK callbackCancel = inputCancel } function pluginDialogCloseDialog() { pluginDialogDialog.style.display = "none"; } function pluginDialogHandleOkClick() { const userInput = pluginDialogTextarea.value; // Do something with the user input callbackOK() pluginDialogCloseDialog(); } function pluginDialogHandleCancelClick() { callbackCancel() pluginDialogCloseDialog(); } function pluginDialogHandleOverlayClick(event) { if (event.target === pluginDialogOverlay) { pluginDialogCloseDialog(); } } function pluginDialogHandleKeyDown(event) { if ((event.key === "Enter" && event.ctrlKey) || event.key === "Escape") { event.preventDefault(); if (event.key === "Enter" && event.ctrlKey) { pluginDialogHandleOkClick(); } else { pluginDialogCloseDialog(); } } } pluginDialogTextarea.addEventListener("keydown", pluginDialogHandleKeyDown); pluginDialogOkButton.addEventListener("click", pluginDialogHandleOkClick); pluginDialogCancelButton.addEventListener("click", pluginDialogHandleCancelClick); pluginDialogCloseButton.addEventListener("click", pluginDialogCloseDialog); pluginDialogOverlay.addEventListener("click", pluginDialogHandleOverlayClick);