(async () => { while (!window.stash) { await new Promise((resolve) => setTimeout(resolve, 100)); } const svgChevDN = ''; const svgChevUP = ''; const svgBTN = 'paint-icon-svg'; let menuCreated = false; function menuOrderListReset(element) { const defaultOrder = [ "Scenes", "Images", "Groups", "Markers", "Galleries", "Performers", "Studios", "Tags", ]; const ul = element.querySelector("ul"); const lis = Array.from(ul.querySelectorAll("li")); // Sort the li elements based on their text content lis.sort((a, b) => { const aIndex = defaultOrder.indexOf(a.textContent); const bIndex = defaultOrder.indexOf(b.textContent); return aIndex - bIndex; }); // Append the sorted li elements to the ul in their new order lis.forEach((li) => ul.appendChild(li)); } function menuOrderResetButton(elementToAppend) { if (!document.getElementById("themeSwitchPlugin-menuOrderReset")) { const resetButton = document.createElement("button"); resetButton.id = "themeSwitchPlugin-menuOrderReset"; resetButton.className = "btn btn-primary"; resetButton.innerHTML = "Reset Menu Order"; resetButton.addEventListener("click", function () { localStorage.removeItem( "themeSwitchPlugin-menu-changeOrderOfNavButtons" ); document .getElementById("themeSwitchPlugin-menu-changeOrderOfNavButtons") .remove(); menuOrderListReset(elementToAppend); resetButton.remove(); }); elementToAppend.appendChild(resetButton); } } function buldDragCSS(themename, key) { const container = document.querySelector(".draggable-ul-container"); const list = container.querySelectorAll("li"); let css = ""; for (let i = 0; i < list.length; i++) { const item = list[i]; const key = item.getAttribute("data-rb-event-key"); const order = i - list.length; css += `div.nav-link[data-rb-event-key="${key}"] { order: ${order}; }`; } const style = document.createElement("style"); style.type = "text/css"; style.id = key; style.innerHTML = `nav .navbar-nav:first-child { display: flex; flex-direction: row; } ${css}`; const existing = document.getElementById(key); if (existing) { existing.remove(); } document.getElementsByTagName("head")[0].appendChild(style); const data = { name: themename, css: css, key: key, id: key, applied: "true", }; localStorage.setItem(key, JSON.stringify(data)); menuOrderResetButton(container.parentElement); } function enableDragSort(listClass) { const sortableLists = document.getElementsByClassName(listClass); Array.prototype.map.call(sortableLists, (list) => { enableDragList(list); }); } function enableDragList(list) { Array.prototype.map.call(list.children, (item) => { enableDragItem(item); }); } function enableDragItem(item) { item.setAttribute("draggable", true); item.addEventListener("touchstart", handleTouchStart, { passive: true }); item.addEventListener("touchmove", handleTouchMove, { passive: true }); item.addEventListener("touchend", handleTouchEnd, { passive: true }); item.ondrag = handleDrag; item.ondragend = handleDrop; } let touchEndY = null; function handleTouchStart(event) { event.preventDefault(); touchStartY = event.touches[0].clientY; } function handleTouchMove(event) { event.preventDefault(); touchEndY = event.changedTouches[0].clientY; const selectedItem = event.target, list = selectedItem.parentNode; selectedItem.classList.add("drag-sort-active"); let swapItem = document.elementFromPoint( selectedItem.getBoundingClientRect().x, touchEndY ) === null ? selectedItem : document.elementFromPoint( selectedItem.getBoundingClientRect().x, touchEndY ); if (list === swapItem.parentNode) { swapItem = swapItem !== selectedItem.nextSibling ? swapItem : swapItem.nextSibling; list.insertBefore(selectedItem, swapItem); } } function handleTouchEnd(event) { event.preventDefault(); touchEndY = event.changedTouches[0].clientY; handleDragTouch(event.target); } function handleDragTouch(item) { const selectedItem = item, list = selectedItem.parentNode; selectedItem.classList.remove("drag-sort-active"); let swapItem = document.elementFromPoint( selectedItem.getBoundingClientRect().x, touchEndY ) === null ? selectedItem : document.elementFromPoint( selectedItem.getBoundingClientRect().x, touchEndY ); if (list === swapItem.parentNode) { swapItem = swapItem !== selectedItem.nextSibling ? swapItem : swapItem.nextSibling; list.insertBefore(selectedItem, swapItem); } setTimeout(() => { buldDragCSS( "Change the order of navigation bar buttons", "themeSwitchPlugin-menu-changeOrderOfNavButtons" ); }, 100); } function handleDrag(item) { const selectedItem = item.target, list = selectedItem.parentNode, x = event.clientX, y = event.clientY; selectedItem.classList.add("drag-sort-active"); let swapItem = document.elementFromPoint(x, y) === null ? selectedItem : document.elementFromPoint(x, y); if (list === swapItem.parentNode) { swapItem = swapItem !== selectedItem.nextSibling ? swapItem : swapItem.nextSibling; list.insertBefore(selectedItem, swapItem); } } function handleDrop(item) { item.target.classList.remove("drag-sort-active"); setTimeout(() => { buldDragCSS( "Change the order of navigation bar buttons", "themeSwitchPlugin-menu-changeOrderOfNavButtons" ); }, 100); } (() => { enableDragSort("drag-sort-enable"); })(); waitForElementClass("draggable-ul-container", function () { enableDragSort("draggable-ul-container"); }); function setObject(key, category, active) { return new Promise((resolve, reject) => { try { localStorage.setItem( key, JSON.stringify({ category: category, active: active }) ); resolve(); } catch (error) { reject(error); } }); } function checkActive(key) { let object = JSON.parse(localStorage.getItem(key)); if (object && object.active === true) { return true; } else { return false; } } function addStyleSheet(key, path) { console.log(key, path); const styleSheet = document.createElement("link"); const serverURL = window.location.origin + document.querySelector("base")?.getAttribute("href") ?? "/"; styleSheet.setAttribute( "href", `${serverURL}plugin/themeSwitch/assets${path}` ); styleSheet.setAttribute("rel", "stylesheet"); styleSheet.setAttribute("type", "text/css"); styleSheet.id = key; document.getElementsByTagName("head")[0].appendChild(styleSheet); } async function applyCSS(category, key, path, pluginId, pluginSrc) { if (category === "Themes") { // Turn Off old Theme let regex = /(themeSwitchPlugin-theme-.*)/; for (let i = 0; i < localStorage.length; i++) { let storageKey = localStorage.key(i); let match = storageKey.match(regex); if ( match && storageKey !== key && JSON.parse(localStorage.getItem(storageKey)).active === true ) { setObject(storageKey, category, false); let element = document.getElementById(storageKey); if (element && !pluginId) { element.remove(); } else { const oldThemePluginId = getDataFromKey(storageKey, "pluginId"); if (oldThemePluginId) { await enablePlugin(oldThemePluginId, false); setTimeout(() => { location.reload(); }, 1000); } } } } const theme = JSON.parse(localStorage.getItem(key)); if (!theme && key != "themeSwitchPlugin-theme-default") { setObject(key, category, true).then(() => { addStyleSheet(key, path); }); } else if (!theme && key === "themeSwitchPlugin-theme-default") { setObject(key, category, true); } else if (theme && key === "themeSwitchPlugin-theme-default") { setObject(key, category, true); } else if (theme && key !== "themeSwitchPlugin-theme-default") { setObject(key, category, true).then(async () => { if (pluginId) { if (!(await isPluginInstalled(pluginId))) { await installPlugin(pluginId, pluginSrc); } await enablePlugin(pluginId, true); setTimeout(() => { location.reload(); }, 1000); } else if (path) { addStyleSheet(key, path); } }); } } else { // CSS Other than themes const storageObject = JSON.parse(localStorage.getItem(key)); if (!storageObject || storageObject.active === false) { setObject(key, category, true).then(() => { addStyleSheet(key, path); }); } else if (storageObject.active && !document.getElementById(key)) { addStyleSheet(key, path); } else { setObject(key, category, false).then(() => { document.getElementById(key).remove(); }); } } } function expandCollapse(category, name, span, collapse) { let categorySpanClick = document.getElementById(span); let categoryCollapseClick = document.getElementById(collapse); let expanded = categoryCollapseClick.getAttribute("aria-expanded") === "true"; if (expanded) { categoryCollapseClick.setAttribute("aria-expanded", "false"); categoryCollapseClick.classList.add("expanding"); setTimeout(function () { categoryCollapseClick.classList.remove("show"); categoryCollapseClick.classList.remove("expanding"); categorySpanClick.innerHTML = svgChevUP + category; }, 300); } else { categoryCollapseClick.setAttribute("aria-expanded", "true"); categoryCollapseClick.classList.add("expanding"); setTimeout(function () { categoryCollapseClick.classList.add("show"); categoryCollapseClick.classList.remove("expanding"); categorySpanClick.innerHTML = svgChevDN + category; }, 300); } } const getInstalledPlugins = async () => csLib .callGQL({ query: `query Plugins{plugins{id}}` }) .then((data) => data.plugins.map((plugin) => plugin.id)) .catch((err) => console.error(err)); const isPluginInstalled = async (plugin) => getInstalledPlugins().then((plugins) => plugins.includes(plugin)); async function enablePlugin(plugin, state) { const query = "mutation SetPluginsEnabled($enabledMap: BoolMap!) { setPluginsEnabled(enabledMap: $enabledMap) }"; const variables = { enabledMap: {} }; variables.enabledMap[plugin] = state; await csLib .callGQL({ query, variables }) .catch((err) => console.error(err)); } async function installPlugin(plugin, src) { const query = "mutation InstallPluginPackages($packages: [PackageSpecInput!]!) {installPackages(type: Plugin, packages: $packages)}"; const variables = { packages: [ { id: plugin, sourceURL: src, }, ], }; await csLib .callGQL({ query, variables }) .catch((err) => console.error(err)); } function createBTNMenu() { return new Promise(function (resolve, reject) { waitForElementClass("top-nav", function () { if (!document.getElementById("themeSwitchPlugin")) { const pluginDiv = document.createElement("div"); pluginDiv.className = "mr-2 dropdown"; pluginDiv.innerHTML = '"; const themesDiv = document.createElement("div"); themesDiv.className = "dropdown-menu theme-plugin-menu"; document.addEventListener("click", function (event) { const isClickInside = pluginDiv.contains(event.target) || themesDiv.contains(event.target); const isClickInsideThemesDiv = themesDiv.contains(event.target); const themeSwitchPlugin = document.getElementById("themeSwitchPlugin"); const expanded = themeSwitchPlugin.getAttribute("aria-expanded") === "true"; if (expanded && !isClickInsideThemesDiv) { themeSwitchPlugin.setAttribute("aria-expanded", "false"); themesDiv.classList.remove("show"); themesDiv.style = "position: absolute; to0px; left: 0px; margin: 0px; opacity: 0; pointer-events: none;"; } else if (!expanded && isClickInside) { themeSwitchPlugin.setAttribute("aria-expanded", "true"); themesDiv.classList.add("show"); themesDiv.style = "position: absolute; top: 133%; left: 0px; margin: 0px; opacity: 1; pointer-events: auto; width: max-content; min-width: 20rem; left: -575%;"; } else if (!isClickInside && !isClickInsideThemesDiv) { themeSwitchPlugin.setAttribute("aria-expanded", "false"); themesDiv.classList.remove("show"); themesDiv.style = "position: absolute; top: 0px; left: 0px; margin: 0px; opacity: 0; pointer-events: none;"; } }); const accordion = document.createElement("div"); accordion.className = "criterion-list accordion"; accordion.style = "max-width: 20rem !important;"; const header = document.createElement("div"); header.innerHTML = "Theme/CSS Switcher"; header.className = "modal-header"; accordion.append(header); Object.entries(window.themeSwitchCSS).forEach( ([category, themesInCategory], i) => { const categoryDiv = document.createElement("div"); categoryDiv.className = "card"; categoryDiv.setAttribute("data-type", category); categoryDiv.setAttribute( "id", "themeSwitchPlugin-card-" + category ); categoryDiv.style = "padding: 2px !important;"; const categoryHeader = document.createElement("div"); categoryHeader.className = "card-header"; categoryHeader.style = "padding: 0.375rem 0.625rem !important;"; const categorySpan = document.createElement("span"); categorySpan.innerHTML = svgChevUP + category; categorySpan.setAttribute( "id", "themeSwitchPlugin-span-" + category ); const collapseDiv = document.createElement("div"); collapseDiv.className = "collapse"; collapseDiv.setAttribute("aria-expanded", "false"); collapseDiv.setAttribute( "id", "themeSwitchPlugin-collapse-" + category ); categoryDiv.addEventListener( "click", (function () { return function (event) { if (!event.target.closest(".collapse")) { expandCollapse( category, "themeSwitchPlugin-card-" + category, "themeSwitchPlugin-span-" + category, "themeSwitchPlugin-collapse-" + category ); } }; })(i), { capture: true } ); const cardBody = document.createElement("div"); cardBody.className = "card-body"; const editorDiv = document.createElement("div"); editorDiv.className = "criterion-editor"; const blankDiv = document.createElement("div"); const optionListFilter = document.createElement("div"); optionListFilter.className = "option-list-filter"; blankDiv.append(optionListFilter); editorDiv.append(blankDiv); cardBody.append(editorDiv); collapseDiv.append(cardBody); categoryHeader.append(categorySpan, collapseDiv); const fieldset = document.createElement("fieldset"); fieldset.className = "checkbox-switch"; // Loop over themes in each category themesInCategory.forEach((theme) => { if (category === "Navigation") { } else { const forRow = document.createElement("div"); forRow.className = "checkbox-switch-form-row"; const legend = document.createElement("legend"); legend.innerHTML = theme.displayName; legend.className = "legend-right"; const input = document.createElement("input"); if (category === "Themes") { input.type = "radio"; input.name = "themeGroup"; const active = checkActive(theme.key); input.checked = active; } else { input.type = "checkbox"; const active = checkActive(theme.key); input.checked = active; } const themeData = { category: category, key: theme.key, ...(theme.path ? { path: theme.path } : theme.pluginId ? { pluginId: theme.pluginId, pluginSrc: theme.pluginSrc, } : {}), }; input.setAttribute("id", category + "-" + theme.key); input.addEventListener( "click", (function (themeData) { return async function () { applyCSS( themeData.category, themeData.key, themeData.path, themeData.pluginId, themeData.pluginSrc ); }; })(themeData), false ); const label = document.createElement("label"); label.setAttribute("for", category + "-" + theme.key); label.title = "Turn " + theme.displayName + " on/off"; label.className = "checkbox-right"; // Append the legend, input, and label elements to the fieldset element forRow.appendChild(legend); forRow.appendChild(input); forRow.appendChild(label); fieldset.appendChild(forRow); optionListFilter.appendChild(fieldset); categoryDiv.append(categoryHeader); accordion.append(categoryDiv); } }); Object.entries(themesInCategory).forEach(([themeId, theme]) => { if (category === "Navigation") { const forRow = document.createElement("div"); forRow.className = "switch-form-row"; const legend = document.createElement("legend"); legend.innerHTML = theme.displayName; legend.className = "legend-left"; const draggableStylesheetContent = localStorage.getItem( "themeSwitchPlugin-menu-changeOrderOfNavButtons" ); if (draggableStylesheetContent) { // Read the stylesheet content and create an array called "elements" var reset = true; var navMenuItems = []; const css = JSON.parse(draggableStylesheetContent).css; const regex = /data-rb-event-key="\/([^"]+)"/g; let match; while ((match = regex.exec(css))) { const key = match[1]; if (match[1] === "scenes/markers") { const name = "Markers"; navMenuItems.push({ name, key }); } else { const name = match[1].charAt(0).toUpperCase() + match[1].slice(1); navMenuItems.push({ name, key }); } } navMenuItems.sort((a, b) => a.order - b.order); } else { var navMenuItems = [ { name: "Scenes", key: "scenes" }, { name: "Images", key: "images" }, { name: "Groups", key: "groups" }, { name: "Markers", key: "scenes/markers" }, { name: "Galleries", key: "galleries" }, { name: "Performers", key: "performers" }, { name: "Studios", key: "studios" }, { name: "Tags", key: "tags" }, ]; } // const label = document.createElement("label"); legend.setAttribute("for", category + "-" + theme.key); optionListFilter.appendChild(legend); const formCheck = document.createElement("ul"); formCheck.className = "draggable-ul-container"; for (let i = 0; i < navMenuItems.length; i++) { const li = document.createElement("li"); li.className = "draggable-li"; li.setAttribute( "data-rb-event-key", "/" + navMenuItems[i].key ); li.setAttribute("draggable", true); const newSpan = document.createElement("span"); newSpan.className = "grippy"; li.appendChild(newSpan); const textNode = document.createTextNode( navMenuItems[i].name ); li.appendChild(textNode); formCheck.appendChild(li); } optionListFilter.appendChild(formCheck); if (reset) { menuOrderResetButton(optionListFilter); } fieldset.appendChild(forRow); optionListFilter.appendChild(fieldset); categoryDiv.append(categoryHeader); accordion.append(categoryDiv); } }); } ); themesDiv.append(accordion); pluginDiv.append(themesDiv); waitForElementClass("navbar-buttons", function () { const mainDiv = document.getElementsByClassName("navbar-buttons")[0]; const secondLastChild = mainDiv.childNodes[mainDiv.childNodes.length - 4]; mainDiv.insertBefore(pluginDiv, secondLastChild); // Resolving the promise once everything is set up menuCreated = true; resolve(); }); } else { // Rejecting the promise if the element already exists reject(new Error("Element already exists")); } }); }); } function getDataFromKey(key, field) { for (const [, categoryThemes] of Object.entries(window.themeSwitchCSS)) { for (const theme of categoryThemes) { if (key === theme.key) { return theme[field]; } } } } function init() { // Apply active Themes and stylesheets let selectedTheme; const regex = /(themeSwitchPlugin(?!-theme-default).*)/; const defaultRegex = /(themeSwitchPlugin-theme-default)/; let defaultFound = false; let appliedThemeOtherThanDefault = []; for (let i = 0; i < localStorage.length; i++) { let key = localStorage.key(i); let defaultMatch = key.match(defaultRegex); if (defaultMatch) { defaultFound = true; } let match = key.match(regex); if (match) { selectedTheme = JSON.parse(localStorage.getItem(key)); if (selectedTheme.active === true) { appliedThemeOtherThanDefault.push("True"); applyCSS(selectedTheme.category, key, getDataFromKey(key, "path")); } } } if (!defaultFound) { let setDefaultActive; if (appliedThemeOtherThanDefault.length > 0) { setDefaultActive = false; } else { setDefaultActive = true; } setObject("themeSwitchPlugin-theme-default", "Themes", setDefaultActive); } } const StashPages = [ "stash:page:scenes", "stash:page:scene", "stash:page:images", "stash:page:image", "stash:page:groups", "stash:page:group", "stash:page:markers", "stash:page:galleries", "stash:page:performers", "stash:page:performer", "stash:page:studios", "stash:page:studio", "stash:page:tags", "stash:page:tag", "stash:page:settings", "stash:page:stats", "stash:page:home", ]; function createMenuAndInit() { if (!menuCreated) { createBTNMenu() .then(function () { console.log("Theme Plugin: Menu created successfully"); }) .catch(function (error) { console.error("Theme Plugin: Error creating menu:", error.message); }); menuCreated = true; } init(); } PluginApi.Event.addEventListener("stash:location", (e) => createMenuAndInit() ); createMenuAndInit(); // Reset menuCreated flag on hard refresh window.addEventListener("beforeunload", function () { menuCreated = false; }); })();