mirror of
https://github.com/easydiffusion/easydiffusion.git
synced 2024-11-22 08:13:22 +01:00
commit
d56c23be9a
@ -16,6 +16,7 @@
|
||||
<link rel="stylesheet" href="/media/css/image-editor.css">
|
||||
<link rel="stylesheet" href="/media/css/searchable-models.css">
|
||||
<link rel="stylesheet" href="/media/css/image-modal.css">
|
||||
<link rel="stylesheet" href="/media/css/plugins.css">
|
||||
<link rel="manifest" href="/media/manifest.webmanifest">
|
||||
<script src="/media/js/jquery-3.6.1.min.js"></script>
|
||||
<script src="/media/js/jquery-confirm.min.js"></script>
|
||||
@ -590,13 +591,13 @@
|
||||
<script src="media/js/utils.js"></script>
|
||||
<script src="media/js/engine.js"></script>
|
||||
<script src="media/js/parameters.js"></script>
|
||||
<script src="media/js/plugins.js"></script>
|
||||
|
||||
<script src="media/js/image-modifiers.js"></script>
|
||||
<script src="media/js/auto-save.js"></script>
|
||||
|
||||
<script src="media/js/searchable-models.js"></script>
|
||||
<script src="media/js/main.js"></script>
|
||||
<script src="media/js/plugins.js"></script>
|
||||
<script src="media/js/themes.js"></script>
|
||||
<script src="media/js/dnd.js"></script>
|
||||
<script src="media/js/image-editor.js"></script>
|
||||
@ -609,6 +610,7 @@ async function init() {
|
||||
await loadUIPlugins()
|
||||
await loadModifiers()
|
||||
await getSystemInfo()
|
||||
await initPlugins()
|
||||
|
||||
SD.init({
|
||||
events: {
|
||||
|
288
ui/media/css/plugins.css
Normal file
288
ui/media/css/plugins.css
Normal file
@ -0,0 +1,288 @@
|
||||
.plugins-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.plugins-table > div {
|
||||
background: var(--background-color2);
|
||||
display: flex;
|
||||
padding: 0px 4px;
|
||||
}
|
||||
|
||||
.plugins-table > div > div {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.plugins-table small {
|
||||
color: rgb(153, 153, 153);
|
||||
}
|
||||
|
||||
.plugins-table > div > div:nth-child(1) {
|
||||
font-size: 20px;
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
.plugins-table > div > div:nth-child(2) {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.plugins-table > div > div:nth-child(3) {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.plugins-table > div:first-child {
|
||||
border-radius: 12px 12px 0px 0px;
|
||||
}
|
||||
|
||||
.plugins-table > div:last-child {
|
||||
border-radius: 0px 0px 12px 12px;
|
||||
}
|
||||
|
||||
.notifications-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.notifications-table > div {
|
||||
background: var(--background-color2);
|
||||
display: flex;
|
||||
padding: 0px 4px;
|
||||
}
|
||||
|
||||
.notifications-table > div > div {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.notifications-table small {
|
||||
color: rgb(153, 153, 153);
|
||||
}
|
||||
|
||||
.notifications-table > div > div:nth-child(1) {
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.notifications-table > div > div:nth-child(2) {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.notifications-table > div:first-child {
|
||||
border-radius: 12px 12px 0px 0px;
|
||||
}
|
||||
|
||||
.notifications-table > div:last-child {
|
||||
border-radius: 0px 0px 12px 12px;
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
DIV.no-notification {
|
||||
padding-top: 16px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.plugin-manager-intro {
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
#plugin-filter {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin: 4px 0 6px 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#refresh-plugins {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#refresh-plugins a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#refresh-plugins a:active {
|
||||
transition-duration: 0.1s;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
.plugin-installed-locally {
|
||||
font-style: italic;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.plugin-source {
|
||||
font-size: x-small;
|
||||
}
|
||||
|
||||
.plugin-warning {
|
||||
color: orange;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.plugin-warning.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.plugin-warning ul {
|
||||
list-style: square;
|
||||
margin: 0 0 8px 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.plugin-warning li {
|
||||
margin-left: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* MODAL DIALOG */
|
||||
#pluginDialog-input-dialog {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pluginDialog-dialog-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(32, 33, 36, 50%);
|
||||
}
|
||||
|
||||
.pluginDialog-dialog-box {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
background: var(--background-color2);
|
||||
border: solid 1px var(--background-color3);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0px 0px 30px black;
|
||||
}
|
||||
|
||||
.pluginDialog-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.pluginDialog-dialog-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pluginDialog-dialog-close-button {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pluginDialog-dialog-close-button:hover {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.pluginDialog-dialog-content {
|
||||
padding: 0 16px 0 16px;
|
||||
}
|
||||
|
||||
.pluginDialog-dialog-content textarea {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
border-radius: var(--input-border-radius);
|
||||
padding: 4px;
|
||||
accent-color: var(--accent-color);
|
||||
background: var(--input-background-color);
|
||||
border: var(--input-border-size) solid var(--input-border-color);
|
||||
color: var(--input-text-color);
|
||||
font-size: 9pt;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.pluginDialog-dialog-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.pluginDialog-dialog-buttons button {
|
||||
margin-left: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
/*background: var(--accent-color);*/
|
||||
/*border: var(--primary-button-border);*/
|
||||
/*color: rgb(255, 221, 255);*/
|
||||
background-color: #3071a9;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pluginDialog-dialog-buttons button:hover {
|
||||
/*background: hsl(var(--accent-hue), 100%, 50%);*/
|
||||
background-color: #428bca;
|
||||
}
|
||||
|
||||
/* NOTIFICATION CENTER */
|
||||
#plugin-notification-button {
|
||||
float: right;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
#plugin-notification-button:hover {
|
||||
background: unset;
|
||||
}
|
||||
|
||||
#plugin-notification-button:active {
|
||||
transition-duration: 0.1s;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
}
|
||||
|
||||
.plugin-notification-pill {
|
||||
background-color: red;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
height: 12px;
|
||||
line-height: 12px;
|
||||
position: relative;
|
||||
right: -8px;
|
||||
text-align: center;
|
||||
top: -20px;
|
||||
width: 12px;
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
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.
|
||||
@ -78,3 +81,950 @@ async function loadUIPlugins() {
|
||||
console.log("error fetching plugin paths", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* PLUGIN MANAGER */
|
||||
/* plugin tab */
|
||||
document.querySelector('.tab-container')?.insertAdjacentHTML('beforeend', `
|
||||
<span id="tab-plugin" class="tab">
|
||||
<span><i class="fa fa-puzzle-piece icon"></i> Plugins</span>
|
||||
</span>
|
||||
`)
|
||||
|
||||
document.querySelector('#tab-content-wrapper')?.insertAdjacentHTML('beforeend', `
|
||||
<div id="tab-content-plugin" class="tab-content">
|
||||
<div id="plugin" class="tab-content-inner">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
|
||||
const tabPlugin = document.querySelector('#tab-plugin')
|
||||
if (tabPlugin) {
|
||||
linkTabContents(tabPlugin)
|
||||
}
|
||||
|
||||
const plugin = document.querySelector('#plugin')
|
||||
plugin.innerHTML = `
|
||||
<div id="plugin-manager" class="tab-content-inner">
|
||||
<i id="plugin-notification-button" class="fa-solid fa-message">
|
||||
<span class="plugin-notification-pill" id="notification-pill" style="display: none"></span>
|
||||
</i>
|
||||
<div id="plugin-notification-list" style="display: none">
|
||||
<h1>Notifications</h1>
|
||||
<div class="plugin-manager-intro">The latest plugin updates are listed below</div>
|
||||
<div class="notifications-table"></div>
|
||||
<div class="no-notification">No new notifications</div>
|
||||
</div>
|
||||
<div id="plugin-manager-section">
|
||||
<h1>Plugin Manager</h1>
|
||||
<div class="plugin-manager-intro">Changes take effect after reloading the page</div>
|
||||
<div class="plugins-table"></div>
|
||||
</div>
|
||||
</div>`
|
||||
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 = `
|
||||
<div${error === true ? ' class="notification-error"' : ''}>${text}</div>
|
||||
<div><small>${timeAgo(date)}</small></div>
|
||||
`;
|
||||
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', `<input type="text" id="plugin-filter" placeholder="Search for..." autocomplete="off"/>`)
|
||||
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', `<p id="refresh-plugins"><small><a id="refresh-plugins-link">Refresh plugins</a></small></p>
|
||||
<p><small>(Plugin developers, add your plugins to <a href='${PLUGIN_CATALOG_GITHUB}' target='_blank'>plugins.json</a>)</small></p>`)
|
||||
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 => `<li>${name}</li>`).join('');
|
||||
return `<ul>${pluginNamesList}</ul>`;
|
||||
}
|
||||
|
||||
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) ? `<span class="plugin-warning${plugin.enabled ? '' : ' hide'}">This plugin might conflict with:${getIncompatiblePlugins(plugin.id)}</span>` : ''
|
||||
const note = plugin.description ? `<small>${plugin.description.replaceAll('\n', '<br>')}</small>` : `<small>No description</small>`;
|
||||
const icon = plugin.icon ? `<i class="fa ${plugin.icon}"></i>` : '<i class="fa fa-puzzle-piece"></i>';
|
||||
const newRow = document.createElement('div')
|
||||
const localPluginFound = checkFileNameInArray(localPlugins, plugin.url)
|
||||
|
||||
newRow.innerHTML = `
|
||||
<div>${icon}</div>
|
||||
<div><label class="plugin-name">${name}${author}${version}</label>${warning}${note}<span class='plugin-source'>Source: <a href="${plugin.url}" target="_blank">${extractFilename(plugin.url)}</a><span></div>
|
||||
<div>
|
||||
${localPluginFound ? "<span class='plugin-installed-locally'>Installed locally</span>" :
|
||||
(plugin.localInstallOnly ? '<span class="plugin-installed-locally">Download and<br />install manually</span>' :
|
||||
(isGitHub(plugin.url) ?
|
||||
'<input id="plugin-' + plugin.id + '" name="plugin-' + plugin.id + '" type="checkbox">' :
|
||||
'<button id="plugin-' + plugin.id + '-install" class="tertiaryButton"></button>'
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>`;
|
||||
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\/(?<owner>[^/]+)\/(?<repo>[^/]+)\/(?<branch>[^/]+)\/(?<filePath>.+)$/;
|
||||
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 = `
|
||||
<div class="pluginDialog-dialog-overlay"></div>
|
||||
<div class="pluginDialog-dialog-box">
|
||||
<div class="pluginDialog-dialog-header">
|
||||
<h2>Paste the plugin's code here</h2>
|
||||
<button class="pluginDialog-dialog-close-button">×</button>
|
||||
</div>
|
||||
<div class="pluginDialog-dialog-content">
|
||||
<textarea id="pluginDialog-input-textarea" spellcheck="false" autocomplete="off"></textarea>
|
||||
</div>
|
||||
<div class="pluginDialog-dialog-buttons">
|
||||
<button id="pluginDialog-input-ok">OK</button>
|
||||
<button id="pluginDialog-input-cancel">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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);
|
||||
|
@ -845,6 +845,7 @@ function createTab(request) {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/* TOAST NOTIFICATIONS */
|
||||
function showToast(message, duration = 5000, error = false) {
|
||||
const toast = document.createElement("div")
|
||||
@ -927,3 +928,130 @@ function confirm(msg, title, fn) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/* STORAGE MANAGEMENT */
|
||||
// Request persistent storage
|
||||
async function requestPersistentStorage() {
|
||||
if (navigator.storage && navigator.storage.persist) {
|
||||
const isPersisted = await navigator.storage.persist();
|
||||
console.log(`Persisted storage granted: ${isPersisted}`);
|
||||
}
|
||||
}
|
||||
requestPersistentStorage()
|
||||
|
||||
// Open a database
|
||||
async function openDB() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = indexedDB.open("EasyDiffusionSettingsDatabase", 1);
|
||||
request.addEventListener("upgradeneeded", function () {
|
||||
let db = request.result;
|
||||
db.createObjectStore("EasyDiffusionSettings", { keyPath: "id" });
|
||||
});
|
||||
request.addEventListener("success", function () {
|
||||
resolve(request.result);
|
||||
});
|
||||
request.addEventListener("error", function () {
|
||||
reject(request.error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to write data to the object store
|
||||
async function setStorageData(key, value) {
|
||||
return openDB().then(db => {
|
||||
let tx = db.transaction("EasyDiffusionSettings", "readwrite");
|
||||
let store = tx.objectStore("EasyDiffusionSettings");
|
||||
let data = { id: key, value: value };
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = store.put(data);
|
||||
request.addEventListener("success", function () {
|
||||
resolve(request.result);
|
||||
});
|
||||
request.addEventListener("error", function () {
|
||||
reject(request.error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to retrieve data from the object store
|
||||
async function getStorageData(key) {
|
||||
return openDB().then(db => {
|
||||
let tx = db.transaction("EasyDiffusionSettings", "readonly");
|
||||
let store = tx.objectStore("EasyDiffusionSettings");
|
||||
return new Promise((resolve, reject) => {
|
||||
let request = store.get(key);
|
||||
request.addEventListener("success", function () {
|
||||
if (request.result) {
|
||||
resolve(request.result.value);
|
||||
} else {
|
||||
// entry not found
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
request.addEventListener("error", function () {
|
||||
reject(request.error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// indexedDB debug functions
|
||||
async function getAllKeys() {
|
||||
return openDB().then(db => {
|
||||
let tx = db.transaction("EasyDiffusionSettings", "readonly");
|
||||
let store = tx.objectStore("EasyDiffusionSettings");
|
||||
let keys = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
store.openCursor().onsuccess = function (event) {
|
||||
let cursor = event.target.result;
|
||||
if (cursor) {
|
||||
keys.push(cursor.key);
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve(keys);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function logAllStorageKeys() {
|
||||
try {
|
||||
let keys = await getAllKeys();
|
||||
console.log("All keys:", keys);
|
||||
for (const k of keys) {
|
||||
console.log(k, await getStorageData(k))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error retrieving keys:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// USE WITH CARE - THIS MAY DELETE ALL ENTRIES
|
||||
async function deleteKeys(keyToDelete) {
|
||||
let confirmationMessage = keyToDelete
|
||||
? `This will delete the template with key "${keyToDelete}". Continue?`
|
||||
: "This will delete ALL templates. Continue?";
|
||||
if (confirm(confirmationMessage)) {
|
||||
return openDB().then(db => {
|
||||
let tx = db.transaction("EasyDiffusionSettings", "readwrite");
|
||||
let store = tx.objectStore("EasyDiffusionSettings");
|
||||
return new Promise((resolve, reject) => {
|
||||
store.openCursor().onsuccess = function (event) {
|
||||
let cursor = event.target.result;
|
||||
if (cursor) {
|
||||
if (!keyToDelete || cursor.key === keyToDelete) {
|
||||
cursor.delete();
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
// refresh the dropdown and resolve
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user