diff --git a/plugins/CommunityScriptsUILibrary/CommunityScriptsUILibrary.yml b/plugins/CommunityScriptsUILibrary/CommunityScriptsUILibrary.yml new file mode 100644 index 0000000..55074e8 --- /dev/null +++ b/plugins/CommunityScriptsUILibrary/CommunityScriptsUILibrary.yml @@ -0,0 +1,6 @@ +name: CommunityScriptsUILibrary +description: CommunityScripts UI helper library +version: 1.0.0 +ui: + javascript: + - cs-ui-lib.js diff --git a/plugins/CommunityScriptsUILibrary/cs-ui-lib.js b/plugins/CommunityScriptsUILibrary/cs-ui-lib.js new file mode 100644 index 0000000..a403b6c --- /dev/null +++ b/plugins/CommunityScriptsUILibrary/cs-ui-lib.js @@ -0,0 +1,49 @@ +// CommunityScripts UI Library +// cs-ui-lib.js +(function () { + // get base URL for graphQL queries + const baseURL = document.querySelector("base")?.getAttribute("href") ?? "/"; + + // call GQL query, returns data without `data` wrapper + const callGQL = (reqData) => + fetch(`${baseURL}graphql`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(reqData), + }) + .then((res) => res.json()) + .then((res) => res.data); + + // get configuration via GQL + const getConfiguration = async (pluginId, fallback) => { + const query = `query Configuration { configuration { plugins }}`; + const response = await callGQL({ query }); + return response.configuration.plugins?.[pluginId] ?? fallback; + }; + + // wait for key elements + function waitForElement(selector, callback) { + var el = document.querySelector(selector); + if (el) return callback(el); + setTimeout(waitForElement, 100, selector, callback); + } + + // wait for a path match, then for key elements + const PathElementListener = (path, element, callback) => { + // startup location + if (window.location.pathname.startsWith(path)) + waitForElement(element, callback); + PluginApi.Event.addEventListener("stash:location", (e) => { + if (e.detail.data.location.pathname.startsWith(path)) + waitForElement(element, callback); + }); + }; + + // export to window + window.csLib = { + callGQL, + getConfiguration, + waitForElement, + PathElementListener, + }; +})(); diff --git a/plugins/StashBatchResultToggle/stashBatchResultToggle.js b/plugins/StashBatchResultToggle/stashBatchResultToggle.js deleted file mode 100644 index 9067a2c..0000000 --- a/plugins/StashBatchResultToggle/stashBatchResultToggle.js +++ /dev/null @@ -1,377 +0,0 @@ -(function () { - let running = false; - const buttons = []; - let maxCount = 0; - - function resolveToggle(el) { - let button = null; - if (el?.classList.contains("optional-field-content")) { - button = el.previousElementSibling; - } else if (el?.tagName === "SPAN" && el?.classList.contains("ml-auto")) { - button = el.querySelector(".optional-field button"); - } else if ( - el?.parentElement?.classList.contains("optional-field-content") - ) { - button = el.parentElement.previousElementSibling; - } - const state = button?.classList.contains("text-success"); - return { - button, - state, - }; - } - - function toggleSearchItem(searchItem, toggleMode) { - const searchResultItem = searchItem.querySelector( - "li.search-result.selected-result.active" - ); - if (!searchResultItem) return; - - const { - urlNode, - url, - id, - data, - nameNode, - name, - queryInput, - performerNodes, - } = stash.parseSearchItem(searchItem); - - const { - remoteUrlNode, - remoteId, - remoteUrl, - remoteData, - urlNode: matchUrlNode, - detailsNode, - imageNode, - titleNode, - codeNode, - dateNode, - studioNode, - performerNodes: matchPerformerNodes, - matches, - } = stash.parseSearchResultItem(searchResultItem); - - const studioMatchNode = matches.find( - (o) => o.matchType === "studio" - )?.matchNode; - const performerMatchNodes = matches - .filter((o) => o.matchType === "performer") - .map((o) => o.matchNode); - - const includeTitle = document.getElementById("result-toggle-title").checked; - const includeCode = document.getElementById("result-toggle-code").checked; - const includeDate = document.getElementById("result-toggle-date").checked; - const includeCover = document.getElementById("result-toggle-cover").checked; - const includeStashID = document.getElementById( - "result-toggle-stashid" - ).checked; - const includeURL = document.getElementById("result-toggle-url").checked; - const includeDetails = document.getElementById( - "result-toggle-details" - ).checked; - const includeStudio = document.getElementById( - "result-toggle-studio" - ).checked; - const includePerformers = document.getElementById( - "result-toggle-performers" - ).checked; - - let options = []; - - options.push(["title", includeTitle, titleNode, resolveToggle(titleNode)]); - options.push(["code", includeCode, codeNode, resolveToggle(codeNode)]); - options.push(["date", includeDate, dateNode, resolveToggle(dateNode)]); - options.push(["cover", includeCover, imageNode, resolveToggle(imageNode)]); - options.push([ - "stashid", - includeStashID, - remoteUrlNode, - resolveToggle(remoteUrlNode), - ]); - options.push([ - "url", - includeURL, - matchUrlNode, - resolveToggle(matchUrlNode), - ]); - options.push([ - "details", - includeDetails, - detailsNode, - resolveToggle(detailsNode), - ]); - options.push([ - "studio", - includeStudio, - studioMatchNode, - resolveToggle(studioMatchNode), - ]); - options = options.concat( - performerMatchNodes.map((o) => [ - "performer", - includePerformers, - o, - resolveToggle(o), - ]) - ); - - for (const [ - optionType, - optionValue, - optionNode, - { button, state }, - ] of options) { - let wantedState = optionValue; - if (toggleMode === 1) { - wantedState = true; - } else if (toggleMode === -1) { - wantedState = false; - } - if (optionNode && wantedState !== state) { - button.click(); - } - } - } - - function run() { - if (!running) return; - const button = buttons.pop(); - stash.setProgress(((maxCount - buttons.length) / maxCount) * 100); - if (button) { - const searchItem = getClosestAncestor(button, ".search-item"); - let toggleMode = 0; - if (btn === btnOn) { - toggleMode = 1; - } else if (btn === btnOff) { - toggleMode = -1; - } else if (btn === btnMixed) { - toggleMode = 0; - } - toggleSearchItem(searchItem, toggleMode); - setTimeout(run, 0); - } else { - stop(); - } - } - - const btnGroup = document.createElement("div"); - const btnGroupId = "batch-result-toggle"; - btnGroup.setAttribute("id", btnGroupId); - btnGroup.classList.add("btn-group", "ml-3"); - - const checkLabel = - ''; - const timesLabel = - ''; - const startLabel = - ''; - let btn; - - const btnOffId = "batch-result-toggle-off"; - const btnOff = document.createElement("button"); - btnOff.setAttribute("id", btnOffId); - btnOff.title = "Result Toggle All Off"; - btnOff.classList.add("btn", "btn-primary"); - btnOff.innerHTML = timesLabel; - btnOff.onclick = () => { - if (running) { - stop(); - } else { - btn = btnOff; - start(); - } - }; - btnGroup.appendChild(btnOff); - - const btnMixedId = "batch-result-toggle-mixed"; - const btnMixed = document.createElement("button"); - btnMixed.setAttribute("id", btnMixedId); - btnMixed.title = "Result Toggle All"; - btnMixed.classList.add("btn", "btn-primary"); - btnMixed.innerHTML = startLabel; - btnMixed.onclick = () => { - if (running) { - stop(); - } else { - btn = btnMixed; - start(); - } - }; - btnGroup.appendChild(btnMixed); - - const btnOnId = "batch-result-toggle-on"; - const btnOn = document.createElement("button"); - btnOn.setAttribute("id", btnOnId); - btnOn.title = "Result Toggle All On"; - btnOn.classList.add("btn", "btn-primary"); - btnOn.innerHTML = checkLabel; - btnOn.onclick = () => { - if (running) { - stop(); - } else { - btn = btnOn; - start(); - } - }; - btnGroup.appendChild(btnOn); - - function start() { - // btn.innerHTML = stopLabel; - btn.classList.remove("btn-primary"); - btn.classList.add("btn-danger"); - btnMixed.disabled = true; - btnOn.disabled = true; - btnOff.disabled = true; - btn.disabled = false; - running = true; - stash.setProgress(0); - buttons.length = 0; - for (const button of document.querySelectorAll(".btn.btn-primary")) { - if (button.innerText === "Search") { - buttons.push(button); - } - } - maxCount = buttons.length; - run(); - } - - function stop() { - // btn.innerHTML = startLabel; - btn.classList.remove("btn-danger"); - btn.classList.add("btn-primary"); - running = false; - stash.setProgress(0); - btnMixed.disabled = false; - btnOn.disabled = false; - btnOff.disabled = false; - } - - stash.addEventListener("tagger:mutations:header", (evt) => { - const el = getElementByXpath("//button[text()='Scrape All']"); - if (el && !document.getElementById(btnGroupId)) { - const container = el.parentElement; - container.appendChild(btnGroup); - sortElementChildren(container); - el.classList.add("ml-3"); - } - }); - - const resultToggleConfigId = "result-toggle-config"; - - stash.addEventListener("tagger:configuration", (evt) => { - const el = evt.detail; - if (!document.getElementById(resultToggleConfigId)) { - const configContainer = el.parentElement; - const resultToggleConfig = createElementFromHTML(` -
-
Result Toggle ${startLabel} Configuration
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- `); - configContainer.appendChild(resultToggleConfig); - loadSettings(); - } - }); - - async function loadSettings() { - for (const input of document.querySelectorAll( - `#${resultToggleConfigId} input` - )) { - input.checked = await sessionStorage.getItem( - input.id, - input.dataset.default === "true" - ); - input.addEventListener("change", async () => { - await sessionStorage.setItem(input.id, input.checked); - }); - } - } - - stash.addEventListener("tagger:mutation:add:remoteperformer", (evt) => - toggleSearchItem(getClosestAncestor(evt.detail.node, ".search-item"), 0) - ); - stash.addEventListener("tagger:mutation:add:remotestudio", (evt) => - toggleSearchItem(getClosestAncestor(evt.detail.node, ".search-item"), 0) - ); - stash.addEventListener("tagger:mutation:add:local", (evt) => - toggleSearchItem(getClosestAncestor(evt.detail.node, ".search-item"), 0) - ); - stash.addEventListener("tagger:mutation:add:container", (evt) => - toggleSearchItem(getClosestAncestor(evt.detail.node, ".search-item"), 0) - ); - stash.addEventListener("tagger:mutation:add:subcontainer", (evt) => - toggleSearchItem(getClosestAncestor(evt.detail.node, ".search-item"), 0) - ); - - function checkSaveButtonDisplay() { - const taggerContainer = document.querySelector(".tagger-container"); - const saveButton = getElementByXpath( - "//button[text()='Save']", - taggerContainer - ); - btnGroup.style.display = saveButton ? "inline-block" : "none"; - } - - stash.addEventListener( - "tagger:mutations:searchitems", - checkSaveButtonDisplay - ); -})(); diff --git a/plugins/StashBatchResultToggle/stashBatchResultToggle.yml b/plugins/StashBatchResultToggle/stashBatchResultToggle.yml deleted file mode 100644 index 1277243..0000000 --- a/plugins/StashBatchResultToggle/stashBatchResultToggle.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: Stash Batch Result Toggle. -# requires: StashUserscriptLibrary -description: In Scene Tagger, adds button to toggle all stashdb scene match result fields. Saves clicks when you only want to save a few metadata fields. Instead of turning off every field, you batch toggle them off, then toggle on the ones you want -version: 1.0 -ui: - requires: - - StashUserscriptLibrary - javascript: - - stashBatchResultToggle.js diff --git a/plugins/VideoScrollWheel/VideoScrollWheel.yml b/plugins/VideoScrollWheel/VideoScrollWheel.yml index 42777bb..bd54484 100644 --- a/plugins/VideoScrollWheel/VideoScrollWheel.yml +++ b/plugins/VideoScrollWheel/VideoScrollWheel.yml @@ -1,5 +1,6 @@ name: VideoScrollWheel description: Adds functionality to change volume/time in scene video player by hovering over left/right side of player and scrolling with mouse scrollwheel. Scroll while hovering on left side to adjust volume, scroll on right side to skip forward/back. +#requires: CommunityScriptsUILibrary version: 0.2 settings: allowVolumeChange: @@ -36,6 +37,6 @@ settings: type: NUMBER ui: requires: - - StashUserscriptLibrary + - CommunityScriptsUILibrary javascript: - - videoScrollWheel.js + - VideoScrollWheel.js diff --git a/plugins/VideoScrollWheel/videoScrollWheel.js b/plugins/VideoScrollWheel/videoScrollWheel.js index b34a205..c1f8f10 100644 --- a/plugins/VideoScrollWheel/videoScrollWheel.js +++ b/plugins/VideoScrollWheel/videoScrollWheel.js @@ -1,8 +1,4 @@ (async () => { - while (!window.stash) { - await new Promise((resolve) => setTimeout(resolve, 100)); - } - const volumeScrollScale = -0.00065; const timeScrollScale = 0.01; const timeScrollFriction = 0.00015; @@ -11,7 +7,8 @@ let vjsPlayer = null; let scrollVelocity = 1; let previousTime = Date.now(); - let pluginSettings = { + let pluginSettings = {}; + const defaultPluginSettings = { allowVolumeChange: false, volumeScrollSpeed: 100.0, timeScrollSpeed: 100.0, @@ -24,14 +21,11 @@ async function setupVideoScrollWheel() { // Get settings - var settings = await getPluginConfig("videoScrollWheel"); - if (settings) { - for (var key in settings) { - if (pluginSettings.hasOwnProperty(key)) { - pluginSettings[key] = settings[key]; - } - } - } + const settings = await csLib.getConfiguration("VideoScrollWheel", {}); // getConfiguration is from cs-ui-lib.js + pluginSettings = { + ...defaultPluginSettings, + ...settings, + }; // Get video player and register wheel event listener. vjsPlayer = document.getElementById("VideoJsPlayer").player; @@ -95,26 +89,10 @@ } } - // Util functions for getting plugin settings. - async function getPluginConfigs() { - const reqData = { - operationName: "Configuration", - variables: {}, - query: `query Configuration { - configuration { - plugins - } - }`, - }; - return stash.callGQL(reqData); - } - async function getPluginConfig(pluginId) { - const data = await getPluginConfigs(); - return data.data.configuration.plugins[pluginId]; - } - // Wait for video player to load on scene page. - stash.addEventListener("stash:page:scene", function () { - waitForElementId("VideoJsPlayer", setupVideoScrollWheel); - }); + csLib.PathElementListener( + "/scenes/", + "#VideoJsPlayer", + setupVideoScrollWheel + ); // PathElementListener is from cs-ui-lib.js })(); diff --git a/plugins/discordPresence/discordPresence.js b/plugins/discordPresence/discordPresence.js index ff2a54f..f9aa956 100644 --- a/plugins/discordPresence/discordPresence.js +++ b/plugins/discordPresence/discordPresence.js @@ -34,7 +34,6 @@ query FindScene($id: ID!) { findScene(id: $id) { ...SceneData - __typename } } @@ -57,39 +56,16 @@ last_played_at play_duration play_count - files { - duration - __typename - } - studio { - ...SlimStudioData - __typename - } - __typename - } - - fragment SlimStudioData on Studio { - id - name - __typename + files { duration } + studio { name } } `; - while (!window.stash) { - await new Promise((resolve) => setTimeout(resolve, 100)); - } - const PLUGIN_ID = "discordPresence"; - let userConfig = await getPluginConfig(); + const userConfig = await csLib.getConfiguration(PLUGIN_ID, {}); console.debug("Discord Presence Plugin: user config", userConfig); - for (let [key, val] of Object.entries(userConfig)) { - if (val === "" || val === undefined || val === null) { - delete userConfig[key]; - } - } - /** @type {Required} */ const CONFIG = { // DEFAULTS @@ -101,7 +77,6 @@ discordLargeImageText: "Stashapp", discordShowUrlButton: false, discordUrlButtonText: "Watch", - ...userConfig, }; @@ -109,6 +84,7 @@ let SCENE_ID = null; let INTERVAL_ID = null; + let WS_ALIVE = false; const doUpdatingPresence = (e) => { clearInterval(INTERVAL_ID); @@ -128,6 +104,7 @@ // https://github.com/lolamtisch/Discord-RPC-Extension/releases const ws = new WebSocket("ws://localhost:6969"); + ws.addEventListener("message", () => (WS_ALIVE = true)); ws.addEventListener("open", () => PluginApi.Event.addEventListener("stash:location", doUpdatingPresence) ); @@ -141,17 +118,17 @@ window.addEventListener("beforeunload", () => { clearDiscordActivity(); }); - - /** @returns {Promise} */ - async function getPluginConfig() { - const reqData = { - operationName: "Configuration", - variables: {}, - query: `query Configuration { configuration { plugins } }`, - }; - const data = await stash.callGQL(reqData); - return data.data.configuration.plugins[PLUGIN_ID]; - } + // set timeout for checking liveliness + const checkLiveliness = () => { + if (!WS_ALIVE) { + unbindVideoListener(document.querySelector("#VideoJsPlayer video")); + clearInterval(INTERVAL_ID); + throw new Error(`Discord Presence Plugin: Discord RPC Extension not running + Please consult the README on how to set up the Discord RPC Extension + (https://github.com/stashapp/CommunityScripts/tree/main/plugins/discordPresence)`); + } + }; + setTimeout(checkLiveliness, 2000); /** @return {Promise} */ async function getSceneData(sceneId) { @@ -159,18 +136,16 @@ return { sceneData: null, duration: 0 }; } const reqData = { - operationName: "FindScene", variables: { id: sceneId }, query: SCENE_GQL_QUERY, }; /** @type {GQLSceneDataResponse} */ - const data = await stash.callGQL(reqData); - const sceneData = data.data.findScene; + const sceneData = await csLib + .callGQL(reqData) + .then((data) => data.findScene); - if (sceneData === null) { - return null; - } + if (!sceneData) return null; const newProps = { studio_name: sceneData.studio?.name ?? "Unknown Studio", @@ -196,38 +171,31 @@ async function setDiscordActivity() { const sceneData = await getSceneData(SCENE_ID); - - if (!sceneData) { - return; - } + if (!sceneData) return; const currentTime = getCurrentVideoTime() ?? 0; const endTimestamp = Date.now() + (sceneData.file_duration - currentTime) * 1000; - let body = {}; - - if (sceneData !== null) { - body = { - details: replaceVars(CONFIG.discordDetailsText, sceneData), - state: replaceVars(CONFIG.discordStateText, sceneData), - largeImageKey: CONFIG.discordShowImage - ? CONFIG.discordLargeImageKey + let body = { + details: replaceVars(CONFIG.discordDetailsText, sceneData), + state: replaceVars(CONFIG.discordStateText, sceneData), + largeImageKey: CONFIG.discordShowImage + ? CONFIG.discordLargeImageKey + : undefined, + largeImageText: replaceVars(CONFIG.discordLargeImageText, sceneData), + endTimestamp: sceneData.file_duration > 0 ? endTimestamp : undefined, + buttons: + CONFIG.discordShowUrlButton && URL.canParse(sceneData.url) + ? [ + { + label: replaceVars(CONFIG.discordUrlButtonText, sceneData), + url: sceneData.url, + }, + ] : undefined, - largeImageText: replaceVars(CONFIG.discordLargeImageText, sceneData), - endTimestamp: sceneData.file_duration > 0 ? endTimestamp : undefined, - buttons: - CONFIG.discordShowUrlButton && isValidUrl(sceneData.url) - ? [ - { - label: replaceVars(CONFIG.discordUrlButtonText, sceneData), - url: sceneData.url, - }, - ] - : undefined, - instance: true, - }; - } + instance: true, + }; if (!ws.OPEN) { return; @@ -242,15 +210,8 @@ ); } - function getCurrentVideoTime() { - const videoElem = document.querySelector("#VideoJsPlayer video"); - - if (!videoElem) { - return null; - } - - return videoElem.currentTime; - } + const getCurrentVideoTime = () => + document.querySelector("#VideoJsPlayer video")?.currentTime; /** * Performs string replacement on templated config vars with scene data @@ -262,13 +223,20 @@ return templateStr.replace(pattern, (_, token) => sceneData[token] ?? ""); } - function isValidUrl(str) { - try { - new URL(str); - } catch { - return false; - } - - return true; - } + // add listener for video events + const videoListener = (video) => { + SCENE_ID = parseInt(location.pathname.split("/")[2]); + video.addEventListener("playing", setDiscordActivity); + video.addEventListener("play", setDiscordActivity); + video.addEventListener("seeked", setDiscordActivity); + // end on video end + video.addEventListener("ended", clearDiscordActivity); + }; + const unbindVideoListener = (video) => { + video.removeEventListener("playing", setDiscordActivity); + video.removeEventListener("play", setDiscordActivity); + video.removeEventListener("seeked", setDiscordActivity); + video.removeEventListener("ended", clearDiscordActivity); + }; + csLib.PathElementListener("/scenes/", "video", videoListener); })(); diff --git a/plugins/discordPresence/discordPresence.yml b/plugins/discordPresence/discordPresence.yml index 4385427..67de7f6 100644 --- a/plugins/discordPresence/discordPresence.yml +++ b/plugins/discordPresence/discordPresence.yml @@ -1,6 +1,7 @@ name: Discord Presence description: Sets currently playing scene data as your Discord status. See README for prerequisites and config options (blue hyperlink next to enable/disable button) url: https://github.com/stashapp/CommunityScripts/tree/main/plugins/discordPresence +#requires: CommunityScriptsUILibrary version: 1.0 settings: discordClientId: @@ -37,6 +38,9 @@ settings: type: STRING ui: requires: - - StashUserscriptLibrary + - CommunityScriptsUILibrary javascript: - discordPresence.js + csp: + connect-src: + - ws://localhost:6969 diff --git a/plugins/sceneCoverCropper/sceneCoverCropper.js b/plugins/sceneCoverCropper/sceneCoverCropper.js index eaddea7..78fdc52 100644 --- a/plugins/sceneCoverCropper/sceneCoverCropper.js +++ b/plugins/sceneCoverCropper/sceneCoverCropper.js @@ -4,16 +4,10 @@ (function () { let cropping = false; let cropper = null; - - try { - stash.getVersion(); - } catch (e) { - console.error( - "Stash not loaded - please install 1. stashUserscriptLibrary from CommunityScripts" - ); - } + const csLib = window.csLib; function setupCropper() { + console.log("setupCropper"); const cropBtnContainerId = "crop-btn-container"; if (document.getElementById(cropBtnContainerId)) return; const sceneId = window.location.pathname @@ -92,7 +86,7 @@ cropAccept.setAttribute("id", "crop-accept"); cropAccept.classList.add("btn", "btn-success", "mr-2"); cropAccept.innerText = "OK"; - cropAccept.addEventListener("click", async (evt) => { + cropAccept.addEventListener("click", async (e) => { cropping = false; cropStart.style.display = "inline-block"; cropAccept.style.display = "none"; @@ -107,13 +101,9 @@ id: sceneId, }, }, - query: `mutation SceneUpdate($input: SceneUpdateInput!) { - sceneUpdate(input: $input) { - id - } - }`, + query: `mutation SceneUpdate($input: SceneUpdateInput!) { sceneUpdate(input: $input) { id }}`, }; - await stash.callGQL(reqData); + await csLib.callGQL(reqData); reloadImg(image.src); cropper.destroy(); cropperModal.close("cropAccept"); @@ -141,7 +131,5 @@ cropBtnContainer.appendChild(cropInfo); } - stash.addEventListener("stash:page:scene", function () { - waitForElementId("scene-edit-details", setupCropper); - }); + csLib.PathElementListener("/scenes/", "#scene-edit-details", setupCropper); })(); diff --git a/plugins/sceneCoverCropper/sceneCoverCropper.yml b/plugins/sceneCoverCropper/sceneCoverCropper.yml index 5706c5f..995d41e 100644 --- a/plugins/sceneCoverCropper/sceneCoverCropper.yml +++ b/plugins/sceneCoverCropper/sceneCoverCropper.yml @@ -1,10 +1,10 @@ name: Scene Cover Cropper -# requires: StashUserscriptLibrary description: Crop Scene Cover Images +# requires: CommunityScriptsUILibrary version: 1.0 ui: requires: - - StashUserscriptLibrary + - CommunityScriptsUILibrary css: - https://cdn.jsdelivr.net/npm/cropperjs@1.6.1/dist/cropper.min.css javascript: diff --git a/plugins/stashAI/stashai.js b/plugins/stashAI/stashai.js index e1b4665..759af2d 100644 --- a/plugins/stashAI/stashai.js +++ b/plugins/stashAI/stashai.js @@ -34,26 +34,6 @@ "Cumshot", ]; - function waitForElm(selector) { - return new Promise((resolve) => { - if (document.querySelector(selector)) { - return resolve(document.querySelector(selector)); - } - - const observer = new MutationObserver((mutations) => { - if (document.querySelector(selector)) { - resolve(document.querySelector(selector)); - observer.disconnect(); - } - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - }); - }); - } - /** * Retrieves the tags associated with a given scene ID. * @@ -61,17 +41,10 @@ * @returns {Promise} - A promise that resolves with an array of tag IDs. */ async function getTagsForScene(scene_id) { - const reqData = { - query: `{ - findScene(id: "${scene_id}") { - tags { - id - } - } - }`, - }; - var result = await stash.callGQL(reqData); - return result.data.findScene.tags.map((p) => p.id); + const query = `query { findScene(id: "${scene_id}") { tags { id }}}`; + return csLib + .callGQL({ query }) + .then((data) => data.findScene.tags.map((p) => p.id)); } /** @@ -81,15 +54,10 @@ * @returns {Promise} - A promise that resolves with the updated scene object. */ async function updateScene(scene_id, tag_ids) { - const reqData = { - variables: { input: { id: scene_id, tag_ids: tag_ids } }, - query: `mutation sceneUpdate($input: SceneUpdateInput!){ - sceneUpdate(input: $input) { - id - } - }`, - }; - return stash.callGQL(reqData); + const variables = { input: { id: scene_id, tag_ids: tag_ids } }; + const query = `mutation sceneUpdate($input: SceneUpdateInput!){ + sceneUpdate(input: $input) { id }}`; + return csLib.callGQL({ query, variables }); } /** @@ -109,16 +77,11 @@ * @returns {Promise} - A Promise that resolves with the ID of the newly created tag. */ async function createTag(tag_name) { - const reqData = { - variables: { input: { name: tag_name } }, - query: `mutation tagCreate($input: TagCreateInput!) { - tagCreate(input: $input){ - id - } - }`, - }; - let result = await stash.callGQL(reqData); - return result.data.tagCreate.id; + const variables = { input: { name: tag_name } }; + const query = `mutation ($input: TagCreateInput!) { tagCreate(input: $input){ id }}`; + return await csLib + .callGQL({ query, variables }) + .then((data) => data.tagCreate.id); } /** @@ -129,20 +92,18 @@ * @returns {Promise} - The ID of the created marker. */ async function createMarker(scene_id, primary_tag_id, seconds) { - const reqData = { - variables: { - scene_id: scene_id, - primary_tag_id: primary_tag_id, - seconds: seconds, - }, - query: `mutation SceneMarkerCreate($seconds: Float!, $scene_id: ID!, $primary_tag_id: ID!) { + const variables = { + scene_id: scene_id, + primary_tag_id: primary_tag_id, + seconds: seconds, + }; + const query = `mutation SceneMarkerCreate($seconds: Float!, $scene_id: ID!, $primary_tag_id: ID!) { sceneMarkerCreate(input: {title:"", seconds: $seconds, scene_id: $scene_id, primary_tag_id: $primary_tag_id}) { id - } - }`, - }; - let result = await stash.callGQL(reqData); - return result.data.sceneMarkerCreate.id; + }}`; + return csLib + .callGQL({ query, variables }) + .then((data) => data.sceneMarkerCreate.id); } /** @@ -150,17 +111,15 @@ * @returns {Promise} An object with tag names as keys and tag IDs as values. */ async function getAllTags() { - const reqData = { - query: `{ + const query = `{ allTags{ id name aliases } - }`, - }; - var result = await stash.callGQL(reqData); - return result.data.allTags.reduce((map, obj) => { + }`; + var result = await csLib.callGQL({ query }); + return result.allTags.reduce((map, obj) => { map[obj.name.toLowerCase()] = obj.id; obj.aliases.forEach((alias) => { map[alias.toLowerCase()] = obj.id; @@ -175,23 +134,12 @@ * @returns {Promise} - A Promise that resolves with the sprite URL if it exists, or null if it does not. */ async function getUrlSprite(scene_id) { - const reqData = { - query: `{ - findScene(id: ${scene_id}){ - paths{ - sprite - } - } - }`, - }; - var result = await stash.callGQL(reqData); - const url = result.data.findScene.paths["sprite"]; + const query = `query { findScene(id: ${scene_id}){ paths{ sprite }} }`; + const url = await csLib + .callGQL({ query }) + .then((data) => data.findScene.paths.sprite); const response = await fetch(url); - if (response.status === 404) { - return null; - } else { - return result.data.findScene.paths["sprite"]; - } + return response.ok ? url : null; } function noop() {} @@ -4172,20 +4120,15 @@ } } - stash.addEventListener("stash:page:scene", function () { - let elms = ".ml-auto .btn-group"; - waitForElm(elms).then(() => { - if (!document.querySelector("#stashmarker")) { - new MarkerButton({ target: document.querySelector(elms) }); - } - }); - }); - stash.addEventListener("stash:page:scene", function () { - let elms = ".ml-auto .btn-group"; - waitForElm(elms).then(() => { - if (!document.querySelector("#stashtag")) { - new TagButton({ target: document.querySelector(elms) }); - } - }); - }); + function addButtons(elms) { + if (!document.querySelector("#stashtag")) new TagButton({ target: elms }); + if (!document.querySelector("#stashmarker")) + new MarkerButton({ target: elms }); + } + // v25 and v24 compatibility + csLib.PathElementListener( + "/scenes/", + ".scene-toolbar-group .btn-group, .ml-auto .btn-group", + addButtons + ); })(); diff --git a/plugins/stashAI/stashai.yml b/plugins/stashAI/stashai.yml index 9b67e61..e413932 100644 --- a/plugins/stashAI/stashai.yml +++ b/plugins/stashAI/stashai.yml @@ -1,10 +1,10 @@ name: Stash AI -# requires: StashUserscriptLibrary +# requires: CommunityScriptsUILibrary description: Add Tags or Markers to a video using AI version: 1.0.2 ui: requires: - - StashUserscriptLibrary + - CommunityScriptsUILibrary javascript: - stashai.js css: diff --git a/plugins/stashRealbooru/stash-realbooru.js b/plugins/stashRealbooru/stash-realbooru.js index c237541..a8073d6 100644 --- a/plugins/stashRealbooru/stash-realbooru.js +++ b/plugins/stashRealbooru/stash-realbooru.js @@ -4,26 +4,6 @@ let REALBOORU_API_URL = "https://cc1234-deepdanbooru.hf.space/api/predict"; let THRESHOLD = 0.6; // remove matches with a distance higher than this - function waitForElm(selector) { - return new Promise((resolve) => { - if (document.querySelector(selector)) { - return resolve(document.querySelector(selector)); - } - - const observer = new MutationObserver((mutations) => { - if (document.querySelector(selector)) { - resolve(document.querySelector(selector)); - observer.disconnect(); - } - }); - - observer.observe(document.body, { - childList: true, - subtree: true, - }); - }); - } - /** * Retrieves the tags associated with a given image ID. * @@ -31,17 +11,12 @@ * @returns {Promise} - A promise that resolves with an array of tag IDs. */ async function getTagsForImage(image_id) { - const reqData = { - query: `{ - findImage(id: "${image_id}") { - tags { - id - } - } - }`, - }; - var result = await stash.callGQL(reqData); - return result.data.findImage.tags.map((p) => p.id); + const query = `query ($id: ID) { findImage(id: $id) { tags { id }}}`; + const variables = { id: image_id }; + I; + return csLib + .callGQL({ query, variables }) + .then((data) => data.findImage.tags.map((p) => p.id)); } /** @@ -51,15 +26,10 @@ * @returns {Promise} - A promise that resolves with the updated image object. */ async function updateImage(image_id, tag_ids) { - const reqData = { - variables: { input: { id: image_id, tag_ids: tag_ids } }, - query: `mutation imageUpdate($input: ImageUpdateInput!){ - imageUpdate(input: $input) { - id - } - }`, - }; - return stash.callGQL(reqData); + const variables = { input: { id: image_id, tag_ids: tag_ids } }; + const query = `mutation ($input: ImageUpdateInput!){ + imageUpdate(input: $input) { id }}`; + return csLib.callGQL({ query, variables }); } /** @@ -79,16 +49,11 @@ * @returns {Promise} - A Promise that resolves with the ID of the newly created tag. */ async function createTag(tag_name) { - const reqData = { - variables: { input: { name: tag_name } }, - query: `mutation tagCreate($input: TagCreateInput!) { - tagCreate(input: $input){ - id - } - }`, - }; - let result = await stash.callGQL(reqData); - return result.data.tagCreate.id; + const variables = { input: { name: tag_name } }; + const query = `mutation ($input: TagCreateInput!) { tagCreate(input: $input){ id }}`; + return await csLib + .callGQL({ query, variables }) + .then((data) => data.tagCreate.id); } /** @@ -96,17 +61,15 @@ * @returns {Promise} An object with tag names as keys and tag IDs as values. */ async function getAllTags() { - const reqData = { - query: `{ + const query = `{ allTags{ id name aliases } - }`, - }; - var result = await stash.callGQL(reqData); - return result.data.allTags.reduce((map, obj) => { + }`; + var result = await csLib.callGQL({ query }); + return result.allTags.reduce((map, obj) => { map[obj.name.toLowerCase()] = obj.id; obj.aliases.forEach((alias) => { map[alias.toLowerCase()] = obj.id; @@ -13287,12 +13250,14 @@ } } - stash.addEventListener("stash:page:image", function () { - let elms = ".ml-auto .btn-group"; - waitForElm(elms).then(() => { - if (!document.querySelector("#stashrealbooru")) { - new TagButton({ target: document.querySelector(elms) }); - } - }); - }); + function createTagButton(target) { + if (document.querySelector("#stashrealbooru")) return; + new TagButton({ target }); + } + // v25 and v24 compatibility + csLib.PathElementListener( + "/images/", + ".image-toolbar-group .btn-group, .ml-auto .btn-group", + createTagButton + ); })(); diff --git a/plugins/stashRealbooru/stash-realbooru.yml b/plugins/stashRealbooru/stash-realbooru.yml index b0a255c..ce84579 100644 --- a/plugins/stashRealbooru/stash-realbooru.yml +++ b/plugins/stashRealbooru/stash-realbooru.yml @@ -1,10 +1,10 @@ name: Stash Realbooru -# requires: StashUserscriptLibrary +# requires: CommunityScriptsUILibrary description: Add tags based on the realbooru model, This works on individual images. version: 1.0.1 ui: requires: - - StashUserscriptLibrary + - CommunityScriptsUILibrary javascript: - stash-realbooru.js css: diff --git a/plugins/stashUserscriptLibrary/StashUserscriptLibrary.yml b/plugins/stashUserscriptLibrary/StashUserscriptLibrary.yml deleted file mode 100644 index 33d4b0b..0000000 --- a/plugins/stashUserscriptLibrary/StashUserscriptLibrary.yml +++ /dev/null @@ -1,6 +0,0 @@ -name: Stash Userscript Library -description: Exports utility functions and a Stash class that emits events whenever a GQL response is received and whenenever a page navigation change is detected -version: 1.0 -ui: - javascript: - - stashUserscriptLibrary.js diff --git a/plugins/stashUserscriptLibrary/stashUserscriptLibrary.js b/plugins/stashUserscriptLibrary/stashUserscriptLibrary.js deleted file mode 100644 index 87fbdcf..0000000 --- a/plugins/stashUserscriptLibrary/stashUserscriptLibrary.js +++ /dev/null @@ -1,1606 +0,0 @@ -const stashListener = new EventTarget(); - -const { fetch: originalFetch } = window; - -const baseURL = document.querySelector("base")?.getAttribute("href") ?? "/"; - -window.fetch = async (...args) => { - let [resource, config] = args; - // request interceptor here - const response = await originalFetch(resource, config); - // response interceptor here - const contentType = response.headers.get("content-type"); - if ( - contentType && - contentType.indexOf("application/json") !== -1 && - resource.endsWith("/graphql") - ) { - try { - const data = await response.clone().json(); - stashListener.dispatchEvent( - new CustomEvent("response", { - detail: data, - }) - ); - } catch (e) {} - } - return response; -}; - -class Logger { - constructor(enabled) { - this.enabled = enabled; - } - debug() { - if (!this.enabled) return; - console.debug(...arguments); - } -} - -class Stash extends EventTarget { - constructor({ - pageUrlCheckInterval = 100, - detectReRenders = true, - logging = false, - } = {}) { - super(); - this.log = new Logger(logging); - this._pageUrlCheckInterval = pageUrlCheckInterval; - this._detectReRenders = detectReRenders; - this._lastPathStr = ""; - this._lastQueryStr = ""; - this._lastHref = ""; - this._lastStashPageEvent = ""; - this.waitForElement(this._detectReRenders ? ".main > div" : "html").then( - () => { - this._pageURLCheckTimerId = setInterval(() => { - // Loop every 100 ms - if ( - this._lastPathStr !== location.pathname || - this._lastHref !== location.href || - this._lastQueryStr !== location.search || - (!document.querySelector(".main > div[stashUserscriptLibrary]") && - this._detectReRenders) - ) { - this._dispatchPageEvent("stash:page", false); - - this._handlePageChange({ - lastPathStr: this._lastPathStr, - lastQueryStr: this._lastQueryStr, - lastHref: this._lastHref, - lastStashPageEvent: this._lastStashPageEvent, - }); - - this._lastPathStr = location.pathname; - this._lastQueryStr = location.search; - this._lastHref = location.href; - - if (this._detectReRenders) { - this.waitForElement(".main > div", 10000).then((element) => { - if (element) element.setAttribute("stashUserscriptLibrary", ""); - }); - } - } - }, this._pageUrlCheckInterval); - } - ); - stashListener.addEventListener("response", (evt) => { - this.processRemoteScenes(evt.detail); - this.processScene(evt.detail); - this.processScenes(evt.detail); - this.processStudios(evt.detail); - this.processPerformers(evt.detail); - this.dispatchEvent( - new CustomEvent("stash:response", { - detail: evt.detail, - }) - ); - }); - this.version = [0, 0, 0]; - this.getVersion(); - this.remoteScenes = {}; - this.scenes = {}; - this.studios = {}; - this.performers = {}; - this.userscripts = []; - this._pageListeners = {}; - this._initDefaultPageListeners(); - } - async getVersion() { - const reqData = { - operationName: "", - variables: {}, - query: `query version { - version { - version - } - }`, - }; - const data = await this.callGQL(reqData); - const versionString = data.data.version.version; - this.version = versionString - .substring(1) - .split(".") - .map((o) => parseInt(o)); - } - compareVersion(minVersion) { - let [currMajor, currMinor, currPatch = 0] = this.version; - let [minMajor, minMinor, minPatch = 0] = minVersion - .split(".") - .map((i) => parseInt(i)); - if (currMajor > minMajor) return 1; - if (currMajor < minMajor) return -1; - if (currMinor > minMinor) return 1; - if (currMinor < minMinor) return -1; - return 0; - } - async runPluginTask(pluginId, taskName, args = []) { - const reqData = { - operationName: "RunPluginTask", - variables: { - plugin_id: pluginId, - task_name: taskName, - args: args, - }, - query: - "mutation RunPluginTask($plugin_id: ID!, $task_name: String!, $args: [PluginArgInput!]) {\n runPluginTask(plugin_id: $plugin_id, task_name: $task_name, args: $args)\n}\n", - }; - return this.callGQL(reqData); - } - async callGQL(reqData) { - const options = { - method: "POST", - body: JSON.stringify(reqData), - headers: { - "Content-Type": "application/json", - }, - }; - - try { - const res = await window.fetch(baseURL + "graphql", options); - this.log.debug(res); - return res.json(); - } catch (err) { - console.error(err); - } - } - async getFreeOnesStats(link) { - try { - const doc = await fetch(link) - .then(function (response) { - // When the page is loaded convert it to text - return response.text(); - }) - .then(function (html) { - // Initialize the DOM parser - var parser = new DOMParser(); - - // Parse the text - var doc = parser.parseFromString(html, "text/html"); - - // You can now even select part of that html as you would in the regular DOM - // Example: - // var docArticle = doc.querySelector('article').innerHTML; - - console.log(doc); - return doc; - }) - .catch(function (err) { - console.log("Failed to fetch page: ", err); - }); - - var data = new Object(); - data.rank = doc.querySelector("rank-chart-button"); - console.log(data.rank); - data.views = doc.querySelector( - ".d-none.d-m-flex.flex-column.align-items-center.global-header > div.font-weight-bold" - ).textContent; - data.votes = "0"; - return JSON.stringify(data); - } catch (err) { - console.error(err); - } - } - async getPlugins() { - const reqData = { - operationName: "Plugins", - variables: {}, - query: `query Plugins { - plugins { - id - name - description - url - version - tasks { - name - description - __typename - } - hooks { - name - description - hooks - } - } - } - `, - }; - return this.callGQL(reqData); - } - async getStashBoxes() { - const reqData = { - operationName: "Configuration", - variables: {}, - query: `query Configuration { - configuration { - general { - stashBoxes { - endpoint - api_key - name - } - } - } - }`, - }; - return this.callGQL(reqData); - } - async getApiKey() { - const reqData = { - operationName: "Configuration", - variables: {}, - query: `query Configuration { - configuration { - general { - apiKey - } - } - }`, - }; - return this.callGQL(reqData); - } - matchUrl(href, fragment) { - const regexp = concatRegexp( - new RegExp(window.location.origin + baseURL), - fragment - ); - return href.match(regexp) != null; - } - get serverUrl() { - return window.location.origin + baseURL; - } - async waitForElement( - selector, - timeout = null, - location = document.body, - disconnectOnPageChange = false - ) { - return new Promise((resolve) => { - if (document.querySelector(selector)) { - return resolve(document.querySelector(selector)); - } - - const observer = new MutationObserver(async () => { - if (document.querySelector(selector)) { - resolve(document.querySelector(selector)); - observer.disconnect(); - } else { - if (timeout) { - async function timeOver() { - return new Promise((resolve) => { - setTimeout(() => { - observer.disconnect(); - resolve(false); - }, timeout); - }); - } - resolve(await timeOver()); - } - } - }); - - observer.observe(location, { - childList: true, - subtree: true, - }); - - if (disconnectOnPageChange) { - const stash = this; - function disconnect() { - resolve(false); - observer.disconnect(); - stash.removeEventListener("stash:page", disconnect); - } - stash.addEventListener("stash:page", disconnect); - } - }); - } - async waitForElementDeath( - selector, - location = document.body, - disconnectOnPageChange = false - ) { - return new Promise((resolve) => { - const observer = new MutationObserver(async () => { - if (!document.querySelector(selector)) { - resolve(true); - observer.disconnect(); - } - }); - - observer.observe(location, { - childList: true, - subtree: true, - }); - - if (disconnectOnPageChange) { - const stash = this; - function disconnect() { - resolve(false); - observer.disconnect(); - stash.removeEventListener("stash:page", disconnect); - } - stash.addEventListener("stash:page", disconnect); - } - }); - } - async _listenForNonPageChanges({ - selector = "", - location = document.body, - listenType = "", - event = "", - recursive = false, - reRunHandlePageChange = false, - listenDefaultTab = true, - callback = () => {}, - } = {}) { - if (recursive) return; - - if (listenType === "tabs") { - const tabsContainer = await this.waitForElement( - selector, - 10000, - location, - true - ); - const stash = this; - let previousEvent = ""; - - function listenForTabClicks(domEvent) { - const clickedChild = domEvent.target ? domEvent.target : domEvent; - - if (!clickedChild.classList?.contains("nav-link")) return; - - const tagName = clickedChild.getAttribute("data-rb-event-key"); - const parentEvent = tagName.split("-")[0]; - const childEvent = tagName.split("-").slice(1, -1).join("-"); - - event = `stash:page:${parentEvent}:${childEvent}`; - - if (previousEvent === event) return; - previousEvent = event; - - stash._dispatchPageEvent(`stash:page:any:${childEvent}`, false); - stash._dispatchPageEvent(event); - } - - if (listenDefaultTab) - listenForTabClicks(tabsContainer.querySelector(".nav-link.active")); - - tabsContainer.addEventListener("click", listenForTabClicks); - - function removeEventListenerOnPageChange() { - tabsContainer.removeEventListener("click", listenForTabClicks); - stash.removeEventListener( - "stash:page", - removeEventListenerOnPageChange - ); - } - stash.addEventListener("stash:page", removeEventListenerOnPageChange); - } else if (await this.waitForElement(selector, null, location, true)) { - this._dispatchPageEvent(event); - - if (await this.waitForElementDeath(selector, location, true)) { - if ( - this._lastPathStr === window.location.pathname && - reRunHandlePageChange - ) { - // triggered after home, performer, studio, tag's edit page close - this._handlePageChange({ - recursive: true, - lastPathStr: this._lastPathStr, - lastQueryStr: this._lastQueryStr, - lastHref: this._lastHref, - lastStashPageEvent: this._lastStashPageEvent, - }); - } - } - } - - callback(); - } - _dispatchPageEvent(event, addToHistory = true) { - this.dispatchEvent( - new CustomEvent(event, { - detail: { - event: event, - lastEventState: { - lastPathStr: this._lastPathStr, - lastQueryStr: this._lastQueryStr, - lastHref: this._lastHref, - lastStashPageEvent: this._lastStashPageEvent, - }, - }, - }) - ); - - if (addToHistory) { - this.log.debug(`[Navigation] ${event}`); - if (event.startsWith("stash:")) { - this._lastStashPageEvent = event; - } - } - - // if (event!=="stash:page" && !addToHistory) this.log.debug(`[Navigation] ${event}`); // log ":any:" events - } - addPageListener(eventData) { - const { - event, - regex, - callback = () => {}, - manuallyHandleDispatchEvent = false, - } = eventData; - if ( - event && - !event?.startsWith("stash:") && - regex && - this._pageListeners[event] === undefined - ) { - this._pageListeners[event] = { - regex: regex, - callback: callback, - manuallyHandleDispatchEvent: manuallyHandleDispatchEvent, - }; - - return event; - } else { - if (this._pageListeners[event] !== undefined) { - console.error(`Can't add page listener: Event ${event} already exists`); - } else if (event?.startsWith("stash:")) { - console.error( - `Can't add page listener: Event name can't start with "stash:"` - ); - } else { - console.error( - `Can't add page listener: Missing required argument(s) "event", "regex"` - ); - } - - return false; - } - } - removePageListener(event) { - if (event && !event?.startsWith("stash:") && this._pageListeners[event]) { - delete this._pageListeners[event]; - return event; - } else { - if (this._pageListeners[event] === undefined && event) { - console.error( - `Can't remove page listener: Event ${event} doesn't exists` - ); - } else if (event?.startsWith("stash:")) { - console.error( - `Can't remove page listener: Event ${event} is a built in event` - ); - } else { - console.error(`Can't remove page listener: Missing "event" argument`); - } - - return false; - } - } - stopPageListener() { - clearInterval(this._pageURLCheckTimerId); - } - _initDefaultPageListeners() { - this._pageListeners = { - // scenes tab - "stash:page:scenes": { - regex: /scenes\?/, - manuallyHandleDispatchEvent: true, - handleDisplayView: "ignoreDisplayViewCondition", - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - this.processTagger(); - } - }, - }, - "stash:page:scene:new": { - regex: /scenes\/new/, - }, - "stash:page:scene": { - regex: /scenes\/\d+\?/, - callback: ({ recursive = false }) => - this._listenForNonPageChanges({ - selector: ".scene-tabs .nav-tabs", - listenType: "tabs", - recursive: recursive, - }), - }, - - // images tab - "stash:page:images": { - regex: /images\?/, - handleDisplayView: true, - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:image": { - regex: /images\/\d+/, - callback: ({ recursive = false }) => - this._listenForNonPageChanges({ - selector: ".image-tabs .nav-tabs", - listenType: "tabs", - recursive: recursive, - }), - }, - - // movies tab - "stash:page:movies": { - regex: /movies\?/, - }, - "stash:page:movie": { - regex: /movies\/\d+/, - }, - "stash:page:movie:scenes": { - regex: /movies\/\d+\?/, - callback: () => this.processTagger(), - }, - - // markers tab - "stash:page:markers": { - regex: /scenes\/markers/, - }, - - // galleries tab - "stash:page:galleries": { - regex: /galleries\?/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:gallery:new": { - regex: /galleries\/new/, - }, - "stash:page:gallery:images": { - regex: /galleries\/\d+\?/, - manuallyHandleDispatchEvent: true, - handleDisplayView: "ignoreDisplayViewCondition", - callback: ({ lastHref, lastPathStr, recursive = false, event }) => { - if ( - !this.matchUrl(lastHref, /\/galleries\/\d+\//) && - lastPathStr !== window.location.pathname - ) { - this._dispatchPageEvent("stash:page:gallery"); - this._listenForNonPageChanges({ - selector: ".gallery-tabs .nav-tabs .nav-link.active", - event: "stash:page:gallery:details", - recursive: recursive, - }); - } - - this._dispatchPageEvent(event); - - this._listenForNonPageChanges({ - selector: ".gallery-tabs .nav-tabs", - listenType: "tabs", - recursive: recursive, - listenDefaultTab: false, - }); - }, - }, - "stash:page:gallery:add": { - regex: /galleries\/\d+\/add/, - manuallyHandleDispatchEvent: true, - handleDisplayView: "ignoreDisplayViewCondition", - callback: ({ lastHref, lastPathStr, recursive = false, event }) => { - if ( - !this.matchUrl(lastHref, /\/galleries\/\d+/) && - lastPathStr !== window.location.pathname - ) { - this._dispatchPageEvent("stash:page:gallery"); - this._listenForNonPageChanges({ - selector: ".gallery-tabs .nav-tabs .nav-link.active", - event: "stash:page:gallery:details", - recursive: recursive, - }); - } - - this._dispatchPageEvent(event); - - this._listenForNonPageChanges({ - selector: ".gallery-tabs .nav-tabs", - listenType: "tabs", - recursive: recursive, - listenDefaultTab: false, - }); - }, - }, - - // performers tab - "stash:page:performers": { - regex: /performers\?/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex) || this._detectReRenders) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:performer:new": { - regex: /performers\/new/, - }, - "stash:page:performer": { - regex: /performers\/\d+/, - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - this.processTagger(); - } - - this._listenForNonPageChanges({ - selector: "#performer-edit", - event: "stash:page:performer:edit", - reRunHandlePageChange: true, - callback: () => - this._detectReRenders ? this._dispatchPageEvent(event) : null, - }); - }, - }, - "stash:page:performer:scenes": { - regex: /performers\/\d+\?/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:performer:galleries": { - regex: /performers\/\d+\/galleries/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:performer:images": { - regex: /performers\/\d+\/images/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:performer:movies": { - regex: /performers\/\d+\/movies/, - }, - "stash:page:performer:appearswith": { - regex: /performers\/\d+\/appearswith/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - this.processTagger(); - } - }, - }, - - // studios tab - "stash:page:studios": { - regex: /studios\?/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:studio:new": { - regex: /studios\/new/, - }, - "stash:page:studio": { - regex: /studios\/\d+/, - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - this.processTagger(); - } - - this._listenForNonPageChanges({ - selector: "#studio-edit", - event: "stash:page:studio:edit", - reRunHandlePageChange: true, - callback: () => - this._detectReRenders ? this._dispatchPageEvent(event) : null, - }); - }, - }, - "stash:page:studio:scenes": { - regex: /studios\/\d+\?/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:studio:galleries": { - regex: /studios\/\d+\/galleries/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:studio:images": { - regex: /studios\/\d+\/images/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:studio:performers": { - regex: /studios\/\d+\/performers/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:studio:movies": { - regex: /studios\/\d+\/movies/, - }, - "stash:page:studio:childstudios": { - regex: /studios\/\d+\/childstudios/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - - // tags tab - "stash:page:tags": { - regex: /tags\?/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:tag:new": { - regex: /tags\/new/, - }, - "stash:page:tag": { - regex: /tags\/\d+/, - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - this.processTagger(); - } - - this._listenForNonPageChanges({ - selector: "#tag-edit", - event: "stash:page:tag:edit", - reRunHandlePageChange: true, - callback: () => - this._detectReRenders ? this._dispatchPageEvent(event) : null, - }); - }, - }, - "stash:page:tag:scenes": { - regex: /tags\/\d+\?/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:tag:galleries": { - regex: /tags\/\d+\/galleries/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:tag:images": { - regex: /tags\/\d+\/images/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:tag:markers": { - regex: /tags\/\d+\/markers/, - }, - "stash:page:tag:performers": { - regex: /tags\/\d+\/performers/, - handleDisplayView: "ignoreDisplayViewCondition", - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - - // settings page - "stash:page:settings": { - regex: /settings/, - manuallyHandleDispatchEvent: true, - callback: ({ lastHref, event, regex }) => { - if (!this.matchUrl(lastHref, regex)) { - this._dispatchPageEvent(event); - } - }, - }, - "stash:page:settings:tasks": { - regex: /settings\?tab=tasks/, - }, - "stash:page:settings:library": { - regex: /settings\?tab=library/, - }, - "stash:page:settings:interface": { - regex: /settings\?tab=interface/, - }, - "stash:page:settings:security": { - regex: /settings\?tab=security/, - }, - "stash:page:settings:metadata-providers": { - regex: /settings\?tab=metadata-providers/, - }, - "stash:page:settings:services": { - regex: /settings\?tab=services/, - }, - "stash:page:settings:system": { - regex: /settings\?tab=system/, - }, - "stash:page:settings:plugins": { - regex: /settings\?tab=plugins/, - }, - "stash:page:settings:logs": { - regex: /settings\?tab=logs/, - }, - "stash:page:settings:tools": { - regex: /settings\?tab=tools/, - }, - "stash:page:settings:changelog": { - regex: /settings\?tab=changelog/, - }, - "stash:page:settings:about": { - regex: /settings\?tab=about/, - }, - - // stats page - "stash:page:stats": { - regex: /stats/, - }, - - // home page - "stash:page:home": { - regex: /$/, - callback: () => - this._listenForNonPageChanges({ - selector: ".recommendations-container-edit", - event: "stash:page:home:edit", - reRunHandlePageChange: true, - }), - }, - }; - } - _handlePageChange(args) { - const events = Object.keys(this._pageListeners); - - for (const event of events) { - const { - regex, - callback = () => {}, - manuallyHandleDispatchEvent = false, - handleDisplayView = false, - } = this._pageListeners[event]; - - let isDisplayViewPage = false; - let isGridPage, isListPage, isWallPage, isTaggerPage; - - const splitEvent = event.split(":"); - const tabPage = { page: "", tab: "" }; - let childAnyEventCondition = false; - - if (splitEvent.length === 4) { - childAnyEventCondition = true; - tabPage.page = splitEvent[2]; - tabPage.tab = splitEvent[3]; - } - - splitEvent.pop(); - - if (handleDisplayView) { - isGridPage = this.matchUrl( - window.location.href, - concatRegexp(regex, /(?!.*disp=)/) - ); - isListPage = this.matchUrl( - window.location.href, - concatRegexp(regex, /.*disp=1/) - ); - isWallPage = this.matchUrl( - window.location.href, - concatRegexp(regex, /.*disp=2/) - ); - isTaggerPage = this.matchUrl( - window.location.href, - concatRegexp(regex, /.*disp=3/) - ); - - if (isListPage || isWallPage || isTaggerPage) isDisplayViewPage = true; - } - - function dispatchViewEvent(view, stash) { - stash._dispatchPageEvent(event + `:${view}`); - if (childAnyEventCondition) { - stash._dispatchPageEvent( - "stash:page:" + tabPage.page + `:any:${view}`, - false - ); - stash._dispatchPageEvent( - "stash:page:any:" + tabPage.tab + `:${view}`, - false - ); - } else { - stash._dispatchPageEvent(`stash:page:any:${view}`, false); - } - } - - const handleDisplayViewCondition = - handleDisplayView !== true || - (handleDisplayView && (!isDisplayViewPage || args.lastHref === "")); - - if ( - this.matchUrl(window.location.href, regex) && - handleDisplayViewCondition - ) { - if (!manuallyHandleDispatchEvent) this._dispatchPageEvent(event); - callback({ - ...args, - location: window.location, - event: event, - regex: regex, - }); - - if (isGridPage) dispatchViewEvent("grid", this); - } - - if (handleDisplayView) { - let view = ""; - switch (true) { - case isListPage: - view = "list"; - break; - case isWallPage: - view = "wall"; - break; - case isTaggerPage: - view = "tagger"; - break; - } - if (view) dispatchViewEvent(view, this); - } - } - } - addEventListeners(events, callback, ...options) { - events.forEach((event) => { - this.addEventListener(event, callback, ...options); - }); - } - async pollLogsForMessage(prefix) { - const reqTime = Date.now(); - const reqData = { - variables: {}, - query: `query Logs { - logs { - time - level - message - } - }`, - }; - await new Promise((r) => setTimeout(r, 500)); - let retries = 0; - while (true) { - const delay = 2 ** retries * 100; - await new Promise((r) => setTimeout(r, delay)); - retries++; - - const logs = await this.callGQL(reqData); - for (const log of logs.data.logs) { - const logTime = Date.parse(log.time); - if (logTime > reqTime && log.message.startsWith(prefix)) { - return log.message.replace(prefix, "").trim(); - } - } - - if (retries >= 5) { - throw `Poll logs failed for message: ${prefix}`; - } - } - } - processTagger() { - waitForElementByXpath("//button[text()='Scrape All']", (xpath, el) => { - this.dispatchEvent( - new CustomEvent("tagger", { - detail: el, - }) - ); - - const searchItemContainer = - document.querySelector(".tagger-container").lastChild; - - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - mutation.addedNodes.forEach((node) => { - if ( - node?.classList?.contains("entity-name") && - node.innerText.startsWith("Performer:") - ) { - this.dispatchEvent( - new CustomEvent("tagger:mutation:add:remoteperformer", { - detail: { - node, - mutation, - }, - }) - ); - } else if ( - node?.classList?.contains("entity-name") && - node.innerText.startsWith("Studio:") - ) { - this.dispatchEvent( - new CustomEvent("tagger:mutation:add:remotestudio", { - detail: { - node, - mutation, - }, - }) - ); - } else if ( - node.tagName === "SPAN" && - node.innerText.startsWith("Matched:") - ) { - this.dispatchEvent( - new CustomEvent("tagger:mutation:add:local", { - detail: { - node, - mutation, - }, - }) - ); - } else if (node.tagName === "UL") { - this.dispatchEvent( - new CustomEvent("tagger:mutation:add:container", { - detail: { - node, - mutation, - }, - }) - ); - } else if (node?.classList?.contains("col-lg-6")) { - this.dispatchEvent( - new CustomEvent("tagger:mutation:add:subcontainer", { - detail: { - node, - mutation, - }, - }) - ); - } else if (node.tagName === "H5") { - // scene date - this.dispatchEvent( - new CustomEvent("tagger:mutation:add:date", { - detail: { - node, - mutation, - }, - }) - ); - } else if ( - node.tagName === "DIV" && - node?.classList?.contains("d-flex") && - node?.classList?.contains("flex-column") - ) { - // scene stashid, url, details - this.dispatchEvent( - new CustomEvent("tagger:mutation:add:detailscontainer", { - detail: { - node, - mutation, - }, - }) - ); - } else { - this.dispatchEvent( - new CustomEvent("tagger:mutation:add:other", { - detail: { - node, - mutation, - }, - }) - ); - } - }); - }); - this.dispatchEvent( - new CustomEvent("tagger:mutations:searchitems", { - detail: mutations, - }) - ); - }); - observer.observe(searchItemContainer, { - childList: true, - subtree: true, - }); - - const taggerContainerHeader = document.querySelector( - ".tagger-container-header" - ); - const taggerContainerHeaderObserver = new MutationObserver( - (mutations) => { - this.dispatchEvent( - new CustomEvent("tagger:mutations:header", { - detail: mutations, - }) - ); - } - ); - taggerContainerHeaderObserver.observe(taggerContainerHeader, { - childList: true, - subtree: true, - }); - - for (const searchItem of document.querySelectorAll(".search-item")) { - this.dispatchEvent( - new CustomEvent("tagger:searchitem", { - detail: searchItem, - }) - ); - } - - if (!document.getElementById("progress-bar")) { - const progressBar = createElementFromHTML( - `
` - ); - progressBar.classList.add("progress"); - progressBar.style.display = "none"; - taggerContainerHeader.appendChild(progressBar); - } - }); - waitForElementByXpath( - "//div[@class='tagger-container-header']/div/div[@class='row']/h4[text()='Configuration']", - (xpath, el) => { - this.dispatchEvent( - new CustomEvent("tagger:configuration", { - detail: el, - }) - ); - } - ); - } - setProgress(value) { - const progressBar = document.getElementById("progress-bar"); - if (progressBar) { - progressBar.firstChild.style.width = value + "%"; - progressBar.style.display = value <= 0 || value > 100 ? "none" : "flex"; - } - } - processRemoteScenes(data) { - if (data.data?.scrapeMultiScenes) { - for (const matchResults of data.data.scrapeMultiScenes) { - for (const scene of matchResults) { - this.remoteScenes[scene.remote_site_id] = scene; - } - } - } else if (data.data?.scrapeSingleScene) { - for (const scene of data.data.scrapeSingleScene) { - this.remoteScenes[scene.remote_site_id] = scene; - } - } - } - processScene(data) { - if (data.data.findScene) { - this.scenes[data.data.findScene.id] = data.data.findScene; - } - } - processScenes(data) { - if (data.data.findScenes?.scenes) { - for (const scene of data.data.findScenes.scenes) { - this.scenes[scene.id] = scene; - } - } - } - processStudios(data) { - if (data.data.findStudios?.studios) { - for (const studio of data.data.findStudios.studios) { - this.studios[studio.id] = studio; - } - } - } - processPerformers(data) { - if (data.data.findPerformers?.performers) { - for (const performer of data.data.findPerformers.performers) { - this.performers[performer.id] = performer; - } - } - } - parseSearchItem(searchItem) { - const urlNode = searchItem.querySelector("a.scene-link"); - const url = new URL(urlNode.href); - const id = url.pathname.replace(baseURL + "scenes/", ""); - const data = this.scenes[id]; - const nameNode = searchItem.querySelector( - "a.scene-link > div.TruncatedText" - ); - const name = nameNode.innerText; - const queryInput = searchItem.querySelector("input.text-input"); - const performerNodes = searchItem.querySelectorAll( - ".performer-tag-container" - ); - - return { - urlNode, - url, - id, - data, - nameNode, - name, - queryInput, - performerNodes, - }; - } - parseSearchResultItem(searchResultItem) { - const remoteUrlNode = searchResultItem.querySelector( - ".scene-details .optional-field .optional-field-content a" - ); - const remoteId = remoteUrlNode?.href.split("/").pop(); - const remoteUrl = remoteUrlNode?.href ? new URL(remoteUrlNode.href) : null; - const remoteData = this.remoteScenes[remoteId]; - - const sceneDetailNodes = searchResultItem.querySelectorAll( - ".scene-details .optional-field .optional-field-content" - ); - let urlNode = null; - let detailsNode = null; - for (const sceneDetailNode of sceneDetailNodes) { - if ( - sceneDetailNode.innerText.startsWith("http") && - remoteUrlNode?.href !== sceneDetailNode.innerText - ) { - urlNode = sceneDetailNode; - } else if (!sceneDetailNode.innerText.startsWith("http")) { - detailsNode = sceneDetailNode; - } - } - - const imageNode = searchResultItem.querySelector( - ".scene-image-container .optional-field .optional-field-content" - ); - - const metadataNode = searchResultItem.querySelector(".scene-metadata"); - const titleNode = metadataNode.querySelector( - "h4 .optional-field .optional-field-content" - ); - const codeAndDateNodes = metadataNode.querySelectorAll( - "h5 .optional-field .optional-field-content" - ); - let codeNode = null; - let dateNode = null; - for (const node of codeAndDateNodes) { - if (node.textContent.includes("-")) { - dateNode = node; - } else { - codeNode = node; - } - } - - const entityNodes = searchResultItem.querySelectorAll(".entity-name"); - let studioNode = null; - const performerNodes = []; - for (const entityNode of entityNodes) { - if (entityNode.innerText.startsWith("Studio:")) { - studioNode = entityNode; - } else if (entityNode.innerText.startsWith("Performer:")) { - performerNodes.push(entityNode); - } - } - - const matchNodes = searchResultItem.querySelectorAll( - "div.col-lg-6 div.mt-2 div.row.no-gutters.my-2 span.ml-auto" - ); - const matches = []; - for (const matchNode of matchNodes) { - let matchType = null; - const entityNode = matchNode.parentElement.querySelector(".entity-name"); - - const matchName = matchNode.querySelector( - ".optional-field-content b" - ).innerText; - const remoteName = entityNode.querySelector("b").innerText; - - let data; - if (entityNode.innerText.startsWith("Performer:")) { - matchType = "performer"; - if (remoteData) { - data = remoteData.performers.find( - (performer) => performer.name === remoteName - ); - } - } else if (entityNode.innerText.startsWith("Studio:")) { - matchType = "studio"; - if (remoteData) { - data = remoteData.studio; - } - } - - matches.push({ - matchType, - matchNode, - entityNode, - matchName, - remoteName, - data, - }); - } - - return { - remoteUrlNode, - remoteId, - remoteUrl, - remoteData, - urlNode, - detailsNode, - imageNode, - titleNode, - codeNode, - dateNode, - studioNode, - performerNodes, - matches, - }; - } -} - -window.stash = new Stash(); - -function waitForElementQuerySelector(query, callback, time) { - time = typeof time !== "undefined" ? time : 100; - window.setTimeout(() => { - const element = document.querySelector(query); - if (element) { - callback(query, element); - } else { - waitForElementQuerySelector(query, callback, time); - } - }, time); -} - -function waitForElementClass(elementId, callback, time) { - time = typeof time !== "undefined" ? time : 100; - window.setTimeout(() => { - const element = document.getElementsByClassName(elementId); - if (element.length > 0) { - callback(elementId, element); - } else { - waitForElementClass(elementId, callback, time); - } - }, time); -} - -function waitForElementId(elementId, callback, time) { - time = typeof time !== "undefined" ? time : 100; - window.setTimeout(() => { - const element = document.getElementById(elementId); - if (element != null) { - callback(elementId, element); - } else { - waitForElementId(elementId, callback, time); - } - }, time); -} - -function waitForElementByXpath(xpath, callback, time) { - time = typeof time !== "undefined" ? time : 100; - window.setTimeout(() => { - const element = getElementByXpath(xpath); - if (element) { - callback(xpath, element); - } else { - waitForElementByXpath(xpath, callback, time); - } - }, time); -} - -function getElementByXpath(xpath, contextNode) { - return document.evaluate( - xpath, - contextNode || document, - null, - XPathResult.FIRST_ORDERED_NODE_TYPE, - null - ).singleNodeValue; -} - -function createElementFromHTML(htmlString) { - const div = document.createElement("div"); - div.innerHTML = htmlString.trim(); - - // Change this to div.childNodes to support multiple top-level nodes. - return div.firstChild; -} - -function getElementByXpath(xpath, contextNode) { - return document.evaluate( - xpath, - contextNode || document, - null, - XPathResult.FIRST_ORDERED_NODE_TYPE, - null - ).singleNodeValue; -} - -function getElementsByXpath(xpath, contextNode) { - return document.evaluate( - xpath, - contextNode || document, - null, - XPathResult.ORDERED_NODE_ITERATOR_TYPE, - null - ); -} - -function getClosestAncestor(el, selector, stopSelector) { - let retval = null; - while (el) { - if (el.matches(selector)) { - retval = el; - break; - } else if (stopSelector && el.matches(stopSelector)) { - break; - } - el = el.parentElement; - } - return retval; -} - -function setNativeValue(element, value) { - const valueSetter = Object.getOwnPropertyDescriptor(element, "value").set; - const prototype = Object.getPrototypeOf(element); - const prototypeValueSetter = Object.getOwnPropertyDescriptor( - prototype, - "value" - ).set; - - if (valueSetter && valueSetter !== prototypeValueSetter) { - prototypeValueSetter.call(element, value); - } else { - valueSetter.call(element, value); - } -} - -function updateTextInput(element, value) { - setNativeValue(element, value); - element.dispatchEvent( - new Event("input", { - bubbles: true, - }) - ); -} - -function concatRegexp(reg, exp) { - let flags = reg.flags + exp.flags; - flags = Array.from(new Set(flags.split(""))).join(); - return new RegExp(reg.source + exp.source, flags); -} - -function sortElementChildren(node) { - const items = node.childNodes; - const itemsArr = []; - for (const i in items) { - if (items[i].nodeType == Node.ELEMENT_NODE) { - // get rid of the whitespace text nodes - itemsArr.push(items[i]); - } - } - - itemsArr.sort((a, b) => { - return a.innerHTML == b.innerHTML ? 0 : a.innerHTML > b.innerHTML ? 1 : -1; - }); - - for (let i = 0; i < itemsArr.length; i++) { - node.appendChild(itemsArr[i]); - } -} - -function xPathResultToArray(result) { - let node = null; - const nodes = []; - while ((node = result.iterateNext())) { - nodes.push(node); - } - return nodes; -} - -function createStatElement(container, title, heading) { - const statEl = document.createElement("div"); - statEl.classList.add("stats-element"); - container.appendChild(statEl); - - const statTitle = document.createElement("p"); - statTitle.classList.add("title"); - statTitle.innerText = title; - statEl.appendChild(statTitle); - - const statHeading = document.createElement("p"); - statHeading.classList.add("heading"); - statHeading.innerText = heading; - statEl.appendChild(statHeading); -} - -const reloadImg = (url) => - fetch(url, { - cache: "reload", - mode: "no-cors", - }).then(() => - document.body - .querySelectorAll(`img[src='${url}']`) - .forEach((img) => (img.src = url)) - ); diff --git a/plugins/stats/stats.js b/plugins/stats/stats.js index 7180d2f..b54951e 100644 --- a/plugins/stats/stats.js +++ b/plugins/stats/stats.js @@ -15,145 +15,96 @@ statEl.appendChild(statHeading); } - async function createSceneStashIDPct(row) { - const reqData = { - variables: { - scene_filter: { - stash_id_endpoint: { - modifier: "NOT_NULL", - }, - }, - }, - query: - "query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene_ids: [Int!]) {\n findScenes(filter: $filter, scene_filter: $scene_filter, scene_ids: $scene_ids) {\n count\n }\n}", - }; - const stashIdCount = (await stash.callGQL(reqData)).data.findScenes.count; + const noStashIDFilter = { stash_id_endpoint: { modifier: "NOT_NULL" } }; - const reqData2 = { - variables: { - scene_filter: {}, - }, - query: - "query FindScenes($filter: FindFilterType, $scene_filter: SceneFilterType, $scene_ids: [Int!]) {\n findScenes(filter: $filter, scene_filter: $scene_filter, scene_ids: $scene_ids) {\n count\n }\n}", - }; - const totalCount = (await stash.callGQL(reqData2)).data.findScenes.count; + async function findPerformers(filter) { + const query = `query ($filter: PerformerFilterType) { findPerformers(performer_filter: $filter) { count } }`; + return await csLib + .callGQL({ query, variables: { filter } }) + .then((data) => data.findPerformers.count); + } + + async function findScenes(filter) { + const query = `query ($filter: SceneFilterType) { findScenes(scene_filter: $filter) { count }}`; + return await csLib + .callGQL({ query, variables: { filter } }) + .then((data) => data.findScenes.count); + } + + async function findStudios(filter) { + const query = `query ($filter: StudioFilterType) { findStudios(studio_filter: $filter) { count }}`; + return await csLib + .callGQL({ query, variables: { filter } }) + .then((data) => data.findStudios.count); + } + + const percentage = (portion, total) => + ((portion / total) * 100).toFixed(2) + "%"; + + async function createSceneStashIDPct(row) { + const stashIdCount = await findScenes(noStashIDFilter); + const totalCount = await findScenes(); createStatElement( row, - ((stashIdCount / totalCount) * 100).toFixed(2) + "%", + percentage(stashIdCount, totalCount), "Scene StashIDs" ); } async function createPerformerStashIDPct(row) { - const reqData = { - variables: { - performer_filter: { - stash_id_endpoint: { - modifier: "NOT_NULL", - }, - }, - }, - query: - "query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) {\n findPerformers(filter: $filter, performer_filter: $performer_filter) {\n count\n }\n}", - }; - const stashIdCount = (await stash.callGQL(reqData)).data.findPerformers - .count; - - const reqData2 = { - variables: { - performer_filter: {}, - }, - query: - "query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) {\n findPerformers(filter: $filter, performer_filter: $performer_filter) {\n count\n }\n}", - }; - const totalCount = (await stash.callGQL(reqData2)).data.findPerformers - .count; + const stashIdCount = await findPerformers(noStashIDFilter); + const totalCount = await findPerformers(); createStatElement( row, - ((stashIdCount / totalCount) * 100).toFixed(2) + "%", + percentage(stashIdCount, totalCount), "Performer StashIDs" ); } async function createStudioStashIDPct(row) { - const reqData = { - variables: { - studio_filter: { - stash_id_endpoint: { - modifier: "NOT_NULL", - }, - }, - }, - query: - "query FindStudios($filter: FindFilterType, $studio_filter: StudioFilterType) {\n findStudios(filter: $filter, studio_filter: $studio_filter) {\n count\n }\n}", - }; - const stashIdCount = (await stash.callGQL(reqData)).data.findStudios.count; - - const reqData2 = { - variables: { - scene_filter: {}, - }, - query: - "query FindStudios($filter: FindFilterType, $studio_filter: StudioFilterType) {\n findStudios(filter: $filter, studio_filter: $studio_filter) {\n count\n }\n}", - }; - const totalCount = (await stash.callGQL(reqData2)).data.findStudios.count; + const stashIdCount = await findStudios(noStashIDFilter); + const totalCount = await findStudios(); createStatElement( row, - ((stashIdCount / totalCount) * 100).toFixed(2) + "%", + percentage(stashIdCount, totalCount), "Studio StashIDs" ); } async function createPerformerFavorites(row) { - const reqData = { - variables: { - performer_filter: { - filter_favorites: true, - }, - }, - query: - "query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) {\n findPerformers(filter: $filter, performer_filter: $performer_filter) {\n count\n }\n}", - }; - const perfCount = (await stash.callGQL(reqData)).data.findPerformers.count; + const filter = { filter_favorites: true }; + const perfCount = await findPerformers(filter); createStatElement(row, perfCount, "Favorite Performers"); } async function createMarkersStat(row) { - const reqData = { - variables: { - scene_marker_filter: {}, - }, - query: - "query FindSceneMarkers($filter: FindFilterType, $scene_marker_filter: SceneMarkerFilterType) {\n findSceneMarkers(filter: $filter, scene_marker_filter: $scene_marker_filter) {\n count\n }\n}", - }; - const totalCount = (await stash.callGQL(reqData)).data.findSceneMarkers - .count; + const query = `query { findSceneMarkers { count }}`; + const totalCount = (await csLib.callGQL({ query })).findSceneMarkers.count; createStatElement(row, totalCount, "Markers"); } - stash.addEventListener("stash:page:stats", function () { - waitForElementByXpath( - "//div[contains(@class, 'container-fluid')]/div[@class='mt-5']", - function (xpath, el) { - if (!document.getElementById("custom-stats-row")) { - const changelog = el.querySelector("div.changelog"); - const row = document.createElement("div"); - row.setAttribute("id", "custom-stats-row"); - row.classList.add("col", "col-sm-8", "m-sm-auto", "row", "stats"); - el.insertBefore(row, changelog); + csLib.PathElementListener( + "/stats", + "div.container-fluid div.mt-5", + setupStats + ); + function setupStats(el) { + if (document.getElementById("custom-stats-row")) return; + const changelog = el.querySelector("div.changelog"); + const row = document.createElement("div"); + row.id = "custom-stats-row"; + row.classList = "col col-sm-8 m-sm-auto row stats"; + el.insertBefore(row, changelog); - createSceneStashIDPct(row); - createStudioStashIDPct(row); - createPerformerStashIDPct(row); - createPerformerFavorites(row); - createMarkersStat(row); - } - } - ); - }); + createSceneStashIDPct(row); + createStudioStashIDPct(row); + createPerformerStashIDPct(row); + createPerformerFavorites(row); + createMarkersStat(row); + } })(); diff --git a/plugins/stats/stats.yml b/plugins/stats/stats.yml index 9197b05..2cd9fd0 100644 --- a/plugins/stats/stats.yml +++ b/plugins/stats/stats.yml @@ -1,9 +1,9 @@ name: Extended Stats -# requires: StashUserscriptLibrary +#requires: CommunityScriptsUILibrary description: Adds new stats to the stats page version: 1.0 ui: requires: - - StashUserscriptLibrary + - CommunityScriptsUILibrary javascript: - stats.js diff --git a/plugins/themeSwitch/themeSwitch.yml b/plugins/themeSwitch/themeSwitch.yml index 98d2562..6abfaa9 100644 --- a/plugins/themeSwitch/themeSwitch.yml +++ b/plugins/themeSwitch/themeSwitch.yml @@ -1,10 +1,11 @@ name: Theme Switch description: Theme and CSS script manager located in main menu bar top right. -url: +# requires: CommunityScriptsUILibrary +url: https://github.com/stashapp/CommunityScripts/tree/main/plugins/themeSwitch version: 2.1 ui: requires: - - stashUserscriptLibrary + - CommunityScriptsUILibrary javascript: - themeSwitchMain.js - themeSwitchCSS.js diff --git a/plugins/themeSwitch/themeSwitchMain.js b/plugins/themeSwitch/themeSwitchMain.js index 5702def..7bf4761 100644 --- a/plugins/themeSwitch/themeSwitchMain.js +++ b/plugins/themeSwitch/themeSwitchMain.js @@ -247,9 +247,12 @@ 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", - `${stash.serverUrl}plugin/themeSwitch/assets${path}` + `${serverURL}plugin/themeSwitch/assets${path}` ); styleSheet.setAttribute("rel", "stylesheet"); styleSheet.setAttribute("type", "text/css"); @@ -353,61 +356,39 @@ } } - async function getInstalledPlugins() { - try { - const res = await stash.callGQL({ - operationName: "Plugins", - variables: {}, - query: "query Plugins{plugins{id}}", - }); - return res.data.plugins.map((plugin) => plugin.id); - } catch (err) { - console.error(err); - } - } + const getInstalledPlugins = async () => + csLib + .callGQL({ query: `query Plugins{plugins{id}}` }) + .then((data) => data.plugins.map((plugin) => plugin.id)) + .catch((err) => console.error(err)); - async function isPluginInstalled(plugin) { - const installedPlugins = await getInstalledPlugins(); - return installedPlugins.includes(plugin); - } + const isPluginInstalled = async (plugin) => + getInstalledPlugins().then((plugins) => plugins.includes(plugin)); async function enablePlugin(plugin, state) { - try { - const query = { - operationName: "SetPluginsEnabled", - variables: { - enabledMap: {}, - }, - query: - "mutation SetPluginsEnabled($enabledMap: BoolMap!) {\n setPluginsEnabled(enabledMap: $enabledMap)\n}", - }; - - query.variables.enabledMap[plugin] = state; - - await stash.callGQL(query); - } catch (err) { - console.error(err); - } + 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) { - try { - await stash.callGQL({ - operationName: "InstallPluginPackages", - variables: { - packages: [ - { - id: plugin, - sourceURL: src, - }, - ], + const query = + "mutation InstallPluginPackages($packages: [PackageSpecInput!]!) {installPackages(type: Plugin, packages: $packages)}"; + const variables = { + packages: [ + { + id: plugin, + sourceURL: src, }, - query: - "mutation InstallPluginPackages($packages: [PackageSpecInput!]!) {installPackages(type: Plugin, packages: $packages)}", - }); - } catch (err) { - console.error(err); - } + ], + }; + await csLib + .callGQL({ query, variables }) + .catch((err) => console.error(err)); } function createBTNMenu() { @@ -766,7 +747,10 @@ init(); } - stash.addEventListeners(StashPages, createMenuAndInit); + PluginApi.Event.addEventListener("stash:location", (e) => + createMenuAndInit() + ); + createMenuAndInit(); // Reset menuCreated flag on hard refresh window.addEventListener("beforeunload", function () { diff --git a/plugins/visage/visage.js b/plugins/visage/visage.js index 1a5810a..02276e0 100644 --- a/plugins/visage/visage.js +++ b/plugins/visage/visage.js @@ -33,75 +33,67 @@ } async function getPerformers(performer_id) { - const reqData = { - query: `{ + const query = `{ findPerformers( performer_filter: {stash_id_endpoint: {endpoint: "", stash_id: "${performer_id}", modifier: EQUALS}}){ performers { name id } } - }`, - }; - var results = await stash.callGQL(reqData); - return results.data.findPerformers.performers; + }`; + return await csLib + .callGQL({ query }) + .then((data) => data.findPerformers.performers); } async function getPerformersForScene(scene_id) { - const reqData = { - query: `{ + const query = `{ findScene(id: "${scene_id}") { performers { id } } - }`, - }; - var result = await stash.callGQL(reqData); - return result.data.findScene.performers.map((p) => p.id); + }`; + return await csLib + .callGQL({ query }) + .then((data) => data.findScene.performers.map((p) => p.id)); } async function getPerformersForImage(image_id) { - const reqData = { - query: `{ + const query = `{ findImage(id: "${image_id}") { performers { id } } - }`, - }; - var result = await stash.callGQL(reqData); - return result.data.findImage.performers.map((p) => p.id); + }`; + return await csLib + .callGQL({ query }) + .then((data) => data.findImage.performers.map((p) => p.id)); } async function updateScene(scene_id, performer_ids) { - const reqData = { - variables: { input: { id: scene_id, performer_ids: performer_ids } }, - query: `mutation sceneUpdate($input: SceneUpdateInput!){ + const variables = { input: { id: scene_id, performer_ids: performer_ids } }; + const query = `mutation sceneUpdate($input: SceneUpdateInput!){ sceneUpdate(input: $input) { id } - }`, - }; - return stash.callGQL(reqData); + }`; + return csLib.callGQL({ query, variables }); } async function updateImage(image_id, performer_ids) { - const reqData = { - variables: { input: { id: image_id, performer_ids: performer_ids } }, - query: `mutation imageUpdate($input: ImageUpdateInput!){ + const variables = { input: { id: image_id, performer_ids: performer_ids } }; + const query = `mutation imageUpdate($input: ImageUpdateInput!){ imageUpdate(input: $input) { id } - }`, - }; - return stash.callGQL(reqData); + }`; + return csLib.callGQL({ query, variables }); } async function getStashboxEndpoint() { - const reqData = { - query: `{ + const query = `{ configuration { general { stashBoxes { @@ -109,66 +101,62 @@ } } } - }`, - }; - var result = await stash.callGQL(reqData); - return result.data.configuration.general.stashBoxes[0].endpoint; + }`; + return await csLib + .callGQL({ query }) + .then((data) => data.configuration.general.stashBoxes[0].endpoint); } async function getPerformerDataFromStashID(stash_id) { - const reqData = { - variables: { - source: { - stash_box_index: 0, - }, - input: { - query: stash_id, - }, + const variables = { + source: { + stash_box_index: 0, + }, + input: { + query: stash_id, }, - query: `query ScrapeSinglePerformer($source: ScraperSourceInput!, $input: ScrapeSinglePerformerInput!) { - scrapeSinglePerformer(source: $source, input: $input) { - name - disambiguation - gender - url - twitter - instagram - birthdate - ethnicity - country - eye_color - height - measurements - fake_tits - career_length - tattoos - piercings - aliases - images - details - death_date - hair_color - weight - remote_site_id - } - }`, }; - var result = await stash.callGQL(reqData); - return result.data.scrapeSinglePerformer.filter( + const query = `query ScrapeSinglePerformer($source: ScraperSourceInput!, $input: ScrapeSinglePerformerInput!) { + scrapeSinglePerformer(source: $source, input: $input) { + name + disambiguation + gender + url + twitter + instagram + birthdate + ethnicity + country + eye_color + height + measurements + fake_tits + career_length + tattoos + piercings + aliases + images + details + death_date + hair_color + weight + remote_site_id + } + }`; + var result = await csLib.callGQL({ query, variables }); + return result.scrapeSinglePerformer.filter( (p) => p.remote_site_id === stash_id )[0]; } async function createPerformer(performer) { - const reqData = { - variables: { input: performer }, - query: `mutation performerCreate($input: PerformerCreateInput!) { - performerCreate(input: $input){ - id - } - }`, - }; - return stash.callGQL(reqData); + const variables = { input: performer }; + const query = `mutation performerCreate($input: PerformerCreateInput!) { + performerCreate(input: $input){ + id + } + }`; + return csLib.callGQL({ query, variables }); } function smoothload(node) { @@ -216,23 +204,12 @@ * @returns {Promise} - A Promise that resolves with the sprite URL if it exists, or null if it does not. */ async function getUrlSprite(scene_id) { - const reqData = { - query: `{ - findScene(id: ${scene_id}){ - paths{ - sprite - } - } - }`, - }; - var result = await stash.callGQL(reqData); - const url = result.data.findScene.paths["sprite"]; + const query = `query { findScene(id: ${scene_id}){ paths{ sprite }} }`; + const url = await csLib + .callGQL({ query }) + .then((data) => data.findScene.paths.sprite); const response = await fetch(url); - if (response.status === 404) { - return null; - } else { - return result.data.findScene.paths["sprite"]; - } + return response.ok ? url : null; } function noop() {} @@ -15315,26 +15292,22 @@ } } - stash.addEventListener("stash:page:scene", function () { - let elms = ".ml-auto .btn-group"; - waitForElm(elms).then(() => { - const e = document.querySelector(elms); - if (!document.querySelector("#visage")) { - new SearchFaces({ target: e }); - } - if (!document.querySelector("#faces")) { - const e = document.querySelector(elms); - new FindFaces({ target: e }); - } - }); - }); - stash.addEventListener("stash:page:image", function () { - let elms = ".ml-auto .btn-group"; - waitForElm(elms).then(() => { - if (!document.querySelector("#visage")) { - const e = document.querySelector(elms); - new SearchFaces({ target: e }); - } - }); - }); + function addSceneButtons(elem) { + if (!document.querySelector("#visage")) new SearchFaces({ target: elem }); + if (!document.querySelector("#faces")) new FindFaces({ target: elem }); + } + function addImageButtons(elem) { + if (!document.querySelector("#visage")) new SearchFaces({ target: elem }); + } + // v25 and v24 compatibility + csLib.PathElementListener( + "/scenes/", + ".scene-toolbar-group .btn-group, .ml-auto .btn-group", + addSceneButtons + ); + csLib.PathElementListener( + "/images/", + ".image-toolbar-group .btn-group, .ml-auto .btn-group", + addImageButtons + ); })(); diff --git a/plugins/visage/visage.yml b/plugins/visage/visage.yml index baac0f9..2387f59 100644 --- a/plugins/visage/visage.yml +++ b/plugins/visage/visage.yml @@ -1,10 +1,10 @@ name: Visage -# requires: StashUserscriptLibrary +# requires: CommunityScriptsUILibrary description: Use facial Recognition To Lookup Performers. version: 1.0.2 ui: requires: - - StashUserscriptLibrary + - CommunityScriptsUILibrary javascript: - visage.js css: