├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.py ├── config.py ├── dist └── public │ ├── StashDB Userscripts Bundle.user.js │ └── scene.css ├── images └── allow-cors-tamper-monkey.png └── src ├── StashDBUserscriptLibrary.js ├── body ├── StashDB Copy Scene Name.js ├── StashDB Copy StashID.user.js └── StashDB Scene Filter.user.js ├── header ├── StashDB Copy Scene Name.js ├── StashDB Copy StashID.user.js └── StashDB Scene Filter.user.js └── scene.css /.gitignore: -------------------------------------------------------------------------------- 1 | dist/local 2 | __pycache__ 3 | stashdb-userscripts.code-workspace -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.5.1 4 | * Fixed to work with stashdb.org updates 5 | 6 | ## 0.5.0 7 | * Updated to work with Stash v0.24.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StashDB Userscripts 2 | 3 | ## [INSTALL USERSCRIPT](dist/public/StashDB%20Userscripts%20Bundle.user.js?raw=1) 4 | 5 | Installation requires a browser extension such as [Violentmonkey](https://violentmonkey.github.io/) / [Tampermonkey](https://www.tampermonkey.net/) / [Greasemonkey](https://www.greasespot.net/). 6 | 7 | > You may remove any unwanted userscripts from the bundle by removing the line that starts with `// @require` that corresponds to the userscript you wish to remove. 8 | 9 | ![Allow cors - Tamper Monkey](images/allow-cors-tamper-monkey.png?raw=true "Allow cors - Tamper Monkey") 10 | 11 | *Known issues: If username/password access is enabled in stash, Firefox + Tampermonkey does not work, but Firefox + Violentmonkey works. Both work in Chrome* 12 | 13 | ## Developing 14 | 15 | Each userscript source is split into two files: 16 | * `src/header` - Folder with userscript metadata blocks 17 | * `src/body` - Folder with main script code 18 | 19 | Execute `py build.py` to combine source files and generate: 20 | * a userscript bundle to `dist\local` for local development 21 | * individual userscripts and a bundle to `dist\public` for release 22 | 23 | Build output directories: 24 | * `dist\local` - A userscript bundle with `@require` headers that load the script code from local files (`src/body`) 25 | * `dist\public` - Userscripts with `@require` headers that load the script code from this github repo -------------------------------------------------------------------------------- /build.py: -------------------------------------------------------------------------------- 1 | import os 2 | import config 3 | import shutil 4 | from pathlib import Path 5 | 6 | def get_active_branch_name(): 7 | head_dir = Path(".") / ".git" / "HEAD" 8 | with head_dir.open("r") as f: content = f.read().splitlines() 9 | 10 | for line in content: 11 | if line[0:4] == "ref:": 12 | return line.partition("refs/heads/")[2] 13 | 14 | def build(): 15 | ROOTDIR = Path(__file__).parent.resolve() 16 | LIBFILE = "StashDBUserscriptLibrary.js" 17 | GIT_BRANCH = get_active_branch_name() 18 | GITHUB_ROOT_URL = config.GITHUB_ROOT_URL.replace('%%BRANCH%%', GIT_BRANCH) 19 | print('git branch', GIT_BRANCH) 20 | 21 | localbodyfiles = [] 22 | distbodyfiles = [] 23 | distlibfile = os.path.join(GITHUB_ROOT_URL, 'src', LIBFILE) 24 | for file in os.listdir('src/header'): 25 | # headerpath = os.path.join('src/header', file) 26 | # bodypath = os.path.join('src/body', file) 27 | # distpublicpath = os.path.join('dist/public', file) 28 | # header = open(headerpath, 'r').read() 29 | # body = open(bodypath, 'r').read() 30 | 31 | localbodyfiles.append("file://" + os.path.join(ROOTDIR, 'src/body', file)) 32 | distbodyfiles.append(os.path.join(GITHUB_ROOT_URL, 'src/body', file)) 33 | 34 | # header = header.replace("%NAMESPACE%", config.NAMESPACE) \ 35 | # .replace("%LIBRARYPATH%", distlibfile) \ 36 | # .replace("%MATCHURL%", f"{config.SERVER_URL}/*") \ 37 | # .replace("// @require %FILEPATH%\n", "") 38 | # distscript = header + "\n\n" + body 39 | # with open(distpublicpath, 'w') as f: 40 | # f.write(distscript) 41 | # print(distpublicpath) 42 | 43 | localpath = 'dist/local/StashDB Userscripts Development Bundle.user.js' 44 | locallibfile = "file://" + os.path.join(ROOTDIR, 'src', LIBFILE) 45 | with open(localpath, 'w') as f: 46 | f.write(f"""// ==UserScript== 47 | // @name StashDB Userscripts Development Bundle 48 | // @namespace {config.NAMESPACE} 49 | // @description StashDB Userscripts Development Bundle 50 | // @version {config.BUNDLE_VERSION} 51 | // @author 7dJx1qP 52 | // @match {config.SERVER_URL}/* 53 | // @resource IMPORTED_CSS file://{os.path.join(ROOTDIR, 'src')}\scene.css 54 | // @grant unsafeWindow 55 | // @grant GM_setClipboard 56 | // @grant GM_getResourceText 57 | // @grant GM_addStyle 58 | // @grant GM.getValue 59 | // @grant GM.setValue 60 | // @grant GM.listValues 61 | // @grant GM.xmlHttpRequest 62 | // @require {locallibfile} 63 | // 64 | // ************************************************************************************************** 65 | // * YOU MAY REMOVE ANY OF THE @require LINES BELOW FOR SCRIPTS YOU DO NOT WANT * 66 | // ************************************************************************************************** 67 | //\n""") 68 | for localbodyfile in localbodyfiles: 69 | f.write(f"// @require {localbodyfile}\n") 70 | f.write("\n// ==/UserScript==\n") 71 | print(localpath) 72 | 73 | distpath = 'dist/public/StashDB Userscripts Bundle.user.js' 74 | with open(distpath, 'w') as f: 75 | f.write(f"""// ==UserScript== 76 | // @name StashDB Userscripts Bundle 77 | // @namespace {config.NAMESPACE} 78 | // @description StashDB Userscripts Bundle 79 | // @version {config.BUNDLE_VERSION} 80 | // @author 7dJx1qP 81 | // @match {config.SERVER_URL}/* 82 | // @resource IMPORTED_CSS https://raw.githubusercontent.com/7dJx1qP/stashdb-userscripts/{GIT_BRANCH}/dist/public/scene.css 83 | // @grant unsafeWindow 84 | // @grant GM_setClipboard 85 | // @grant GM_getResourceText 86 | // @grant GM_addStyle 87 | // @grant GM.getValue 88 | // @grant GM.setValue 89 | // @grant GM.listValues 90 | // @grant GM.xmlHttpRequest 91 | // @require {distlibfile} 92 | // 93 | // ************************************************************************************************** 94 | // * YOU MAY REMOVE ANY OF THE @require LINES BELOW FOR SCRIPTS YOU DO NOT WANT * 95 | // ************************************************************************************************** 96 | //\n""") 97 | for distbodyfile in distbodyfiles: 98 | f.write(f"// @require {distbodyfile}\n") 99 | f.write("\n// ==/UserScript==\n") 100 | print(distpath) 101 | 102 | shutil.copyfile('src/scene.css', 'dist/public/scene.css') 103 | 104 | build() -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | GITHUB_ROOT_URL = r"https://raw.githubusercontent.com/7dJx1qP/stashdb-userscripts/%%BRANCH%%/" 2 | BUNDLE_VERSION = "0.5.1" 3 | SERVER_URL = "https://stashdb.org" 4 | NAMESPACE = "https://github.com/7dJx1qP/stashdb-userscripts" -------------------------------------------------------------------------------- /dist/public/StashDB Userscripts Bundle.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name StashDB Userscripts Bundle 3 | // @namespace https://github.com/7dJx1qP/stashdb-userscripts 4 | // @description StashDB Userscripts Bundle 5 | // @version 0.5.1 6 | // @author 7dJx1qP 7 | // @match https://stashdb.org/* 8 | // @resource IMPORTED_CSS https://raw.githubusercontent.com/7dJx1qP/stashdb-userscripts/master/dist/public/scene.css 9 | // @grant unsafeWindow 10 | // @grant GM_setClipboard 11 | // @grant GM_getResourceText 12 | // @grant GM_addStyle 13 | // @grant GM.getValue 14 | // @grant GM.setValue 15 | // @grant GM.listValues 16 | // @grant GM.xmlHttpRequest 17 | // @require https://raw.githubusercontent.com/7dJx1qP/stashdb-userscripts/master/src\StashDBUserscriptLibrary.js 18 | // 19 | // ************************************************************************************************** 20 | // * YOU MAY REMOVE ANY OF THE @require LINES BELOW FOR SCRIPTS YOU DO NOT WANT * 21 | // ************************************************************************************************** 22 | // 23 | // @require https://raw.githubusercontent.com/7dJx1qP/stashdb-userscripts/master/src/body\StashDB Copy Scene Name.js 24 | // @require https://raw.githubusercontent.com/7dJx1qP/stashdb-userscripts/master/src/body\StashDB Copy StashID.user.js 25 | // @require https://raw.githubusercontent.com/7dJx1qP/stashdb-userscripts/master/src/body\StashDB Scene Filter.user.js 26 | 27 | // ==/UserScript== 28 | -------------------------------------------------------------------------------- /dist/public/scene.css: -------------------------------------------------------------------------------- 1 | .svg-inline--fa { 2 | display: var(--fa-display, inline-block); 3 | height: 1em; 4 | overflow: visible; 5 | vertical-align: -0.125em; 6 | } 7 | 8 | .nav-link .fa-gear { 9 | width: 24px; 10 | height: 24px; 11 | } 12 | 13 | .stash_id_match a:hover { 14 | background-color: rgba(0, 0, 0, 0.541); 15 | color: #fff !important; 16 | } 17 | 18 | .stash_id_match.search_match { 19 | position: absolute; 20 | top: 10px; 21 | right: 10px; 22 | align-self: center; 23 | } 24 | 25 | .stash_id_match.scene_match { 26 | position: relative; 27 | margin-left: 10px; 28 | cursor: pointer; 29 | align-self: center; 30 | display: inline; 31 | } 32 | 33 | .match-yes { 34 | color: green; 35 | } 36 | 37 | .match-no { 38 | color: red; 39 | } 40 | 41 | .stash_id_ignored .match-no { 42 | color: yellow; 43 | } 44 | 45 | .stash_id_wanted .match-no { 46 | color: gold; 47 | } 48 | 49 | .stash_id_match svg { 50 | height: 24px; 51 | width: 24px; 52 | } 53 | 54 | .stash-performer-link img, 55 | .stash-scene-link img, 56 | .stash-studio-link img { 57 | width: 2rem; 58 | padding-left: 0.5rem; 59 | } 60 | 61 | .scene-performers .stash-performer-link { 62 | padding-right: 0.25rem; 63 | } 64 | 65 | .SearchPage .stash-performer-link img { 66 | width: 2rem; 67 | padding-left: 0rem; 68 | margin-right: 10px; 69 | } 70 | 71 | .stash_id_ignored, 72 | .stash_id_ignored > .card { 73 | background-color: rgba(48, 64, 77, 0.25) !important; 74 | } 75 | 76 | .stash_id_ignored img { 77 | opacity: 0.25; 78 | } 79 | 80 | .settings-box { 81 | padding: 1rem; 82 | margin-bottom: 0; 83 | position: absolute; 84 | right: 0; 85 | z-index: 999; 86 | background-color: inherit; 87 | } -------------------------------------------------------------------------------- /images/allow-cors-tamper-monkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/7dJx1qP/stashdb-userscripts/314ac382ddfb7e9a7726424baccfd1ffb0dfeea4/images/allow-cors-tamper-monkey.png -------------------------------------------------------------------------------- /src/StashDBUserscriptLibrary.js: -------------------------------------------------------------------------------- 1 | // StashDB Userscript Library 2 | // Exports utility functions and a StashDB class that emits events whenenever a page navigation change is detected 3 | // version 0.2.0 4 | 5 | (function () { 6 | 'use strict'; 7 | 8 | const css = GM_getResourceText("IMPORTED_CSS"); 9 | GM_addStyle(css); 10 | 11 | const STASH_IMAGE = 'data:image/png;base64,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'; 12 | 13 | const stashdb = function () { 14 | 15 | const { fetch: originalFetch } = window; 16 | const stashdbListener = new EventTarget(); 17 | 18 | unsafeWindow.fetch = async (...args) => { 19 | let [resource, config ] = args; 20 | // request interceptor here 21 | const response = await originalFetch(resource, config); 22 | // response interceptor here 23 | const contentType = response.headers.get("content-type"); 24 | if (contentType && contentType.indexOf("application/json") !== -1) { 25 | const data = await response.clone().json(); 26 | stashdbListener.dispatchEvent(new CustomEvent('response', { 'detail': data })); 27 | } 28 | return response; 29 | }; 30 | 31 | class Logger { 32 | constructor(enabled) { 33 | this.enabled = enabled; 34 | } 35 | debug() { 36 | if (!this.enabled) return; 37 | console.debug(...arguments); 38 | } 39 | } 40 | 41 | function waitForElementId(elementId, callBack, time) { 42 | time = (typeof time !== 'undefined') ? time : 100; 43 | window.setTimeout(() => { 44 | const element = document.getElementById(elementId); 45 | if (element) { 46 | callBack(elementId, element); 47 | } else { 48 | waitForElementId(elementId, callBack); 49 | } 50 | }, time); 51 | } 52 | 53 | function waitForElementClass(elementId, callBack, time) { 54 | time = (typeof time !== 'undefined') ? time : 100; 55 | window.setTimeout(() => { 56 | const element = document.getElementsByClassName(elementId); 57 | if (element.length > 0) { 58 | callBack(elementId, element); 59 | } else { 60 | waitForElementClass(elementId, callBack); 61 | } 62 | }, time); 63 | } 64 | 65 | function waitForElementByXpath(xpath, callBack, time) { 66 | time = (typeof time !== 'undefined') ? time : 100; 67 | window.setTimeout(() => { 68 | const element = getElementByXpath(xpath); 69 | if (element) { 70 | callBack(xpath, element); 71 | } else { 72 | waitForElementByXpath(xpath, callBack); 73 | } 74 | }, time); 75 | } 76 | 77 | function getElementByXpath(xpath, contextNode) { 78 | return document.evaluate(xpath, contextNode || document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 79 | } 80 | 81 | function getElementsByXpath(xpath, contextNode) { 82 | return document.evaluate(xpath, contextNode || document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); 83 | } 84 | 85 | function getClosestAncestor(el, selector, stopSelector) { 86 | let retval = null; 87 | while (el) { 88 | if (el.matches(selector)) { 89 | retval = el; 90 | break 91 | } else if (stopSelector && el.matches(stopSelector)) { 92 | break 93 | } 94 | el = el.parentElement; 95 | } 96 | return retval; 97 | } 98 | 99 | function insertAfter(newNode, existingNode) { 100 | existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling); 101 | } 102 | 103 | function createElementFromHTML(htmlString) { 104 | const div = document.createElement('div'); 105 | div.innerHTML = htmlString.trim(); 106 | 107 | // Change this to div.childNodes to support multiple top-level nodes. 108 | return div.firstChild; 109 | } 110 | 111 | 112 | function setNativeValue(element, value) { 113 | const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set; 114 | const prototype = Object.getPrototypeOf(element); 115 | const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set; 116 | 117 | if (valueSetter && valueSetter !== prototypeValueSetter) { 118 | prototypeValueSetter.call(element, value); 119 | } else { 120 | valueSetter.call(element, value); 121 | } 122 | } 123 | 124 | function updateTextInput(element, value) { 125 | setNativeValue(element, value); 126 | element.dispatchEvent(new Event('input', { bubbles: true })); 127 | } 128 | 129 | function concatRegexp(reg, exp) { 130 | let flags = reg.flags + exp.flags; 131 | flags = Array.from(new Set(flags.split(''))).join(); 132 | return new RegExp(reg.source + exp.source, flags); 133 | } 134 | 135 | function sortElementChildren(node) { 136 | const items = node.childNodes; 137 | const itemsArr = []; 138 | for (const i in items) { 139 | if (items[i].nodeType == Node.ELEMENT_NODE) { // get rid of the whitespace text nodes 140 | itemsArr.push(items[i]); 141 | } 142 | } 143 | 144 | itemsArr.sort((a, b) => { 145 | return a.innerHTML == b.innerHTML 146 | ? 0 147 | : (a.innerHTML > b.innerHTML ? 1 : -1); 148 | }); 149 | 150 | for (let i = 0; i < itemsArr.length; i++) { 151 | node.appendChild(itemsArr[i]); 152 | } 153 | } 154 | 155 | function xPathResultToArray(result) { 156 | let node = null; 157 | const nodes = []; 158 | while (node = result.iterateNext()) { 159 | nodes.push(node); 160 | } 161 | return nodes; 162 | } 163 | 164 | const reloadImg = url => 165 | fetch(url, { cache: 'reload', mode: 'no-cors' }) 166 | .then(() => document.body.querySelectorAll(`img[src='${url}']`) 167 | .forEach(img => img.src = url)); 168 | 169 | function isInViewport(element) { 170 | const rect = element.getBoundingClientRect(); 171 | return ( 172 | rect.top >= 0 && 173 | rect.left >= 0 && 174 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && 175 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) 176 | ); 177 | } 178 | 179 | function toHHMMSS(i) { 180 | const sec_num = parseInt(i, 10); // don't forget the second param 181 | const hours = Math.floor(sec_num / 3600); 182 | const minutes = Math.floor((sec_num - (hours * 3600)) / 60); 183 | const seconds = sec_num - (hours * 3600) - (minutes * 60); 184 | 185 | const parts = []; 186 | 187 | if (hours > 0) { 188 | parts.push(hours); 189 | } 190 | parts.push(String(minutes).padStart(2, '0')); 191 | parts.push(String(seconds).padStart(2, '0')); 192 | 193 | return parts.join(':'); 194 | } 195 | 196 | const checkLabel = ''; 197 | const timesLabel = ''; 198 | const clearLabel = ''; 199 | const starLabel = ''; 200 | 201 | class StashDB extends EventTarget { 202 | constructor({ pageUrlCheckInterval = 50, logging = false } = {}) { 203 | super(); 204 | this.stashUrl = 'http://localhost:9999'; 205 | this.loggedIn = false; 206 | this.userName = null; 207 | this.log = new Logger(logging); 208 | this._pageUrlCheckInterval = pageUrlCheckInterval; 209 | this.fireOnHashChangesToo = true; 210 | this.pageURLCheckTimer = setInterval(() => { 211 | // Loop every 500ms 212 | if (this.lastPathStr !== location.pathname || this.lastQueryStr !== location.search || (this.fireOnHashChangesToo && this.lastHashStr !== location.hash)) { 213 | this.lastPathStr = location.pathname; 214 | this.lastQueryStr = location.search; 215 | this.lastHashStr = location.hash; 216 | this.gmMain(); 217 | } 218 | }, this._pageUrlCheckInterval); 219 | stashdbListener.addEventListener('response', (evt) => { 220 | //this.processScenes(evt.detail); 221 | //this.processPerformers(evt.detail); 222 | this.dispatchEvent(new CustomEvent('stashdb:response', { 'detail': evt.detail })); 223 | }); 224 | } 225 | async callGQL(reqData) { 226 | const options = { 227 | method: 'POST', 228 | body: JSON.stringify(reqData), 229 | headers: { 230 | 'Content-Type': 'application/json' 231 | } 232 | } 233 | if (this.stashApiKey) { 234 | options.headers.ApiKey = this.stashApiKey; 235 | } 236 | 237 | return new Promise((resolve, reject) => { 238 | GM.xmlHttpRequest({ 239 | method: "POST", 240 | url: this.stashUrl + '/graphql', 241 | data: JSON.stringify(reqData), 242 | headers: { 243 | "Content-Type": "application/json" 244 | }, 245 | onload: response => { 246 | resolve(JSON.parse(response.response)); 247 | }, 248 | onerror: reject 249 | }); 250 | }); 251 | } 252 | async callStashDbGQL(reqData) { 253 | const options = { 254 | method: 'POST', 255 | body: JSON.stringify(reqData), 256 | headers: { 257 | 'Content-Type': 'application/json' 258 | } 259 | } 260 | 261 | try { 262 | const res = await unsafeWindow.fetch('/graphql', options); 263 | this.log.debug(res); 264 | return res.json(); 265 | } 266 | catch (err) { 267 | console.error(err); 268 | } 269 | } 270 | async findSceneByStashId(id) { 271 | const reqData = { 272 | "variables": { 273 | "scene_filter": { 274 | "stash_id_endpoint": { 275 | "endpoint": "", 276 | "stash_id": id, 277 | "modifier": "EQUALS" 278 | } 279 | } 280 | }, 281 | "query": `query FindSceneByStashId($scene_filter: SceneFilterType) { 282 | findScenes(scene_filter: $scene_filter) { 283 | scenes { 284 | title 285 | stash_ids { 286 | endpoint 287 | stash_id 288 | } 289 | id 290 | } 291 | } 292 | }` 293 | } 294 | return this.callGQL(reqData); 295 | } 296 | async findStudioByStashId(id) { 297 | const reqData = { 298 | "variables": { 299 | "studio_filter": { 300 | "stash_id_endpoint": { 301 | "endpoint": "", 302 | "stash_id": id, 303 | "modifier": "EQUALS" 304 | } 305 | } 306 | }, 307 | "query": `query FindStudioByStashId($studio_filter: StudioFilterType) { 308 | findStudios(studio_filter: $studio_filter) { 309 | studios { 310 | stash_ids { 311 | endpoint 312 | stash_id 313 | } 314 | id 315 | } 316 | } 317 | }` 318 | } 319 | return this.callGQL(reqData); 320 | } 321 | async findPerformerByStashId(id) { 322 | const reqData = { 323 | "variables": { 324 | "performer_filter": { 325 | "stash_id_endpoint": { 326 | "endpoint": "", 327 | "stash_id": id, 328 | "modifier": "EQUALS" 329 | } 330 | } 331 | }, 332 | "query": `query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) { 333 | findPerformers(filter: $filter, performer_filter: $performer_filter) { 334 | count 335 | performers { 336 | id 337 | } 338 | } 339 | }` 340 | } 341 | return this.callGQL(reqData); 342 | } 343 | /*async processScenes(data) { 344 | if (data?.data?.queryScenes?.scenes) { 345 | return Promise.all(data?.data?.queryScenes?.scenes.map(scene => this.processListScene(scene.id))); 346 | } 347 | }*/ 348 | async processListScene(stashId, sceneEl) { 349 | const data = await this.findSceneByStashId(stashId); 350 | const localId = data?.data?.findScenes?.scenes[0]?.id; 351 | waitForElementByXpath(`//div[@class='card-footer']//a[contains(@href,'${stashId}')]`, async (xpath, el) => { 352 | await this.addSceneMarker(stashId, localId, el.parentElement, sceneEl); 353 | }); 354 | } 355 | async processPageScene(stashId) { 356 | const data = await this.findSceneByStashId(stashId); 357 | const localId = data?.data?.findScenes?.scenes[0]?.id; 358 | waitForElementByXpath(`//div[contains(@class,'scene-info')]/div[@class='card-header']/div[@class='float-end']`, async (xpath, el) => { 359 | await this.addSceneMarker(stashId, localId, el, getClosestAncestor(el, '.scene-info')); 360 | }); 361 | } 362 | async processSearchScene(stashId, sceneEl) { 363 | const data = await this.findSceneByStashId(stashId); 364 | const localId = data?.data?.findScenes?.scenes[0]?.id; 365 | waitForElementByXpath(`//a[contains(@href,'${stashId}')]//h5`, async (xpath, el) => { 366 | el.classList.add('d-flex'); 367 | const markerEl = await this.addSceneMarker(stashId, localId, el, sceneEl); 368 | markerEl.classList.add('ms-auto'); 369 | }); 370 | } 371 | async addSceneMarker(stashId, localId, parentElement, sceneEl) { 372 | let markerEl = parentElement.querySelector('.stash_id_match'); 373 | if (!markerEl) { 374 | let label = localId ? checkLabel : timesLabel; 375 | 376 | const sceneState = JSON.parse(await GM.getValue(stashId, '{"ignored":false,"wanted":false}')); 377 | if (sceneState.ignored) { 378 | sceneEl.classList.add('stash_id_ignored'); 379 | label = clearLabel; 380 | } 381 | else if (sceneState.wanted) { 382 | sceneEl.classList.add('stash_id_wanted'); 383 | label = starLabel; 384 | } 385 | 386 | markerEl = createElementFromHTML(`
${label}
`); 387 | markerEl.classList.add(localId ? 'match-yes' : 'match-no'); 388 | 389 | let dropdownEl; 390 | markerEl.addEventListener('mouseenter', evt => { 391 | dropdownEl = createElementFromHTML(``); 392 | markerEl.appendChild(dropdownEl); 393 | 394 | const rect = document.body.getBoundingClientRect(); 395 | const rect2 = evt.currentTarget.getBoundingClientRect(); 396 | const x = rect2.left;// - rect.left; 397 | const y = rect2.top;// - rect.top; 398 | dropdownEl.style.left = `${x}px`; 399 | dropdownEl.style.top = `${y}px`; 400 | 401 | dropdownEl.addEventListener("click", evt => { 402 | evt.preventDefault(); 403 | evt.stopImmediatePropagation(); 404 | }); 405 | 406 | const menuEl = createElementFromHTML(``); 407 | dropdownEl.appendChild(menuEl); 408 | 409 | if (localId) { 410 | const localLink = this.stashUrl + '/scenes/' + localId; 411 | const gotoSceneEl = createElementFromHTML(`Go to Scene`); 412 | gotoSceneEl.addEventListener("click", evt => { 413 | evt.preventDefault(); 414 | evt.stopImmediatePropagation(); 415 | window.open( 416 | localLink, 417 | '_blank' 418 | ); 419 | }); 420 | menuEl.appendChild(gotoSceneEl); 421 | } 422 | 423 | const ignoreEl = createElementFromHTML(``); 424 | const wishlistEl = createElementFromHTML(`>`); 425 | 426 | if (sceneState.ignored) { 427 | ignoreEl.innerText = 'Clear Ignore'; 428 | 429 | ignoreEl.addEventListener("click", async evt => { 430 | evt.preventDefault(); 431 | evt.stopImmediatePropagation(); 432 | sceneState.ignored = false; 433 | sceneEl.classList.remove('stash_id_ignored'); 434 | markerEl.querySelector('a').innerHTML = localId ? checkLabel : timesLabel; 435 | menuEl.remove(); 436 | await GM.setValue(stashId, JSON.stringify(sceneState)); 437 | }); 438 | 439 | menuEl.append(ignoreEl); 440 | } 441 | else if (sceneState.wanted) { 442 | wishlistEl.innerText = 'Remove From Wishlist'; 443 | 444 | wishlistEl.addEventListener("click", async evt => { 445 | evt.preventDefault(); 446 | evt.stopImmediatePropagation(); 447 | sceneState.wanted = false; 448 | sceneEl.classList.remove('stash_id_wanted'); 449 | markerEl.querySelector('a').innerHTML = localId ? checkLabel : timesLabel; 450 | menuEl.remove(); 451 | await GM.setValue(stashId, JSON.stringify(sceneState)); 452 | }); 453 | 454 | menuEl.append(wishlistEl); 455 | } 456 | else if (!localId) { 457 | wishlistEl.innerText = 'Add to Wishlist'; 458 | ignoreEl.innerText = 'Ignore Scene'; 459 | 460 | wishlistEl.addEventListener("click", async evt => { 461 | evt.preventDefault(); 462 | evt.stopImmediatePropagation(); 463 | sceneState.wanted = true; 464 | sceneEl.classList.add('stash_id_wanted'); 465 | markerEl.querySelector('a').innerHTML = starLabel; 466 | menuEl.remove(); 467 | if (!sceneState.data) { 468 | const data = (await this.findStashboxSceneByStashId(stashId))?.data?.findScene; 469 | const { title = '', release_date = '', duration } = data; 470 | const studioName = data?.studio?.name || ''; 471 | const studioId = data?.studio?.id || ''; 472 | const cover = data?.images[0]?.url; 473 | sceneState.data = { 474 | title, 475 | release_date, 476 | duration, 477 | studioName, 478 | studioId, 479 | cover 480 | } 481 | } 482 | await GM.setValue(stashId, JSON.stringify(sceneState)); 483 | }); 484 | 485 | ignoreEl.addEventListener("click", async evt => { 486 | evt.preventDefault(); 487 | evt.stopImmediatePropagation(); 488 | sceneState.ignored = true; 489 | sceneEl.classList.add('stash_id_ignored'); 490 | markerEl.querySelector('a').innerHTML = clearLabel; 491 | menuEl.remove(); 492 | await GM.setValue(stashId, JSON.stringify(sceneState)); 493 | }); 494 | 495 | menuEl.append(wishlistEl); 496 | menuEl.append(ignoreEl); 497 | } 498 | 499 | if(!isInViewport(menuEl)) { 500 | dropdownEl.style.left = `${x-150}px`; 501 | dropdownEl.style.top = `${y-80}px`; 502 | } 503 | 504 | }); 505 | markerEl.addEventListener('mouseleave', () => { 506 | dropdownEl.remove(); 507 | }); 508 | parentElement.appendChild(markerEl); 509 | } 510 | this.dispatchEvent(new CustomEvent('scenecard', { 'detail': { sceneEl } })); 511 | return markerEl; 512 | } 513 | async createStashPerformerLink(stashId, callback) { 514 | const reqData = { 515 | "variables": { 516 | "performer_filter": { 517 | "stash_id_endpoint": { 518 | "endpoint": "", 519 | "stash_id": stashId, 520 | "modifier": "EQUALS" 521 | } 522 | } 523 | }, 524 | "query": `query FindPerformers($filter: FindFilterType, $performer_filter: PerformerFilterType) { 525 | findPerformers(filter: $filter, performer_filter: $performer_filter) { 526 | count 527 | performers { 528 | id 529 | } 530 | } 531 | }` 532 | } 533 | const results = await this.callGQL(reqData); 534 | if (results.data.findPerformers.count === 0) return; 535 | const performerId = results.data.findPerformers.performers[0].id; 536 | const performerUrl = `${this.stashUrl}/performers/${performerId}`; 537 | const performerLink = document.createElement('a'); 538 | performerLink.classList.add('stash-performer-link'); 539 | performerLink.href = performerUrl; 540 | const stashIcon = document.createElement('img'); 541 | stashIcon.src = STASH_IMAGE; 542 | performerLink.appendChild(stashIcon); 543 | performerLink.setAttribute('target', '_blank'); 544 | callback(performerLink); 545 | } 546 | addStashPerformerLinks() { 547 | if (!document.querySelector('.stash-performer-link')) { 548 | for (const searchPerformer of document.querySelectorAll('div.PerformerCard a')) { 549 | const url = new URL(searchPerformer.href); 550 | const stashId = url.pathname.replace('/performers/', ''); 551 | const searchPerformerHeader = searchPerformer.querySelector('div.card-footer > h5'); 552 | this.createStashPerformerLink(stashId, function (performerLink) { 553 | searchPerformerHeader.appendChild(performerLink); 554 | performerLink.addEventListener('click', function (event) { 555 | event.preventDefault(); 556 | window.open(performerLink.href, '_blank'); 557 | }); 558 | }); 559 | } 560 | } 561 | } 562 | addStashPerformerLink() { 563 | if (!document.querySelector('.stash-performer-link')) { 564 | const header = document.querySelector('.card-header h3'); 565 | const stashId = window.location.pathname.replace('/performers/', ''); 566 | this.createStashPerformerLink(stashId, function (performerLink) { 567 | header.appendChild(performerLink); 568 | }); 569 | } 570 | } 571 | addStashScenePerformerLink() { 572 | if (!document.querySelector('.stash-performer-link')) { 573 | const header = document.querySelector('.scene-performers'); 574 | for (const scenePerformer of document.querySelectorAll('a.scene-performer')) { 575 | const url = new URL(scenePerformer.href); 576 | const stashId = url.pathname.replace('/performers/', ''); 577 | this.createStashPerformerLink(stashId, function (performerLink) { 578 | header.insertBefore(performerLink, scenePerformer); 579 | }); 580 | } 581 | } 582 | } 583 | addStashSearchPerformerLink() { 584 | if (!document.querySelector('.stash-performer-link')) { 585 | for (const searchPerformer of document.querySelectorAll('a.SearchPage-performer')) { 586 | const url = new URL(searchPerformer.href); 587 | const stashId = url.pathname.replace('/performers/', ''); 588 | const searchPerformerHeader = searchPerformer.querySelector('div.card > div.ms-3 > h4 > span'); 589 | this.createStashPerformerLink(stashId, function (performerLink) { 590 | searchPerformerHeader.parentElement.insertBefore(performerLink, searchPerformerHeader); 591 | performerLink.addEventListener('click', function (event) { 592 | event.preventDefault(); 593 | window.open(performerLink.href, '_blank'); 594 | }); 595 | }); 596 | } 597 | } 598 | } 599 | processEdits() { 600 | for (const sceneEdit of document.querySelectorAll('.EditCard')) { 601 | this.processEdit(sceneEdit); 602 | } 603 | } 604 | addStashLink(node, localId, stashType) { 605 | if (!localId) return; 606 | const localLink = `${this.stashUrl}/${stashType}/${localId}`; 607 | const link = document.createElement('a'); 608 | link.classList.add(`stash-${stashType.slice(0, -1)}-link`); 609 | link.href = localLink; 610 | const stashIcon = document.createElement('img'); 611 | stashIcon.src = STASH_IMAGE; 612 | link.appendChild(stashIcon); 613 | link.setAttribute('target', '_blank'); 614 | insertAfter(link, node); 615 | } 616 | processEdit(sceneEdit) { 617 | for (const anchor of sceneEdit.querySelectorAll('a')) { 618 | if (anchor.classList.contains('checked-stash')) continue; 619 | anchor.classList.add('checked-stash'); 620 | const url = new URL(anchor.href); 621 | if (url.pathname.startsWith('/scenes/')) { 622 | const stashId = url.pathname.replace('/scenes/', ''); 623 | this.findSceneByStashId(stashId).then(data => { 624 | const localId = data?.data?.findScenes?.scenes[0]?.id; 625 | this.addStashLink(anchor, localId, 'scenes'); 626 | }); 627 | } 628 | else if (url.pathname.startsWith('/performers/')) { 629 | const stashId = url.pathname.replace('/performers/', ''); 630 | this.findPerformerByStashId(stashId).then(data => { 631 | const localId = data?.data?.findPerformers?.performers[0]?.id; 632 | this.addStashLink(anchor, localId, 'performers'); 633 | }); 634 | } 635 | else if (url.pathname.startsWith('/studios/')) { 636 | const stashId = url.pathname.replace('/studios/', ''); 637 | this.findStudioByStashId(stashId).then(data => { 638 | const localId = data?.data?.findStudios?.studios[0]?.id; 639 | this.addStashLink(anchor, localId, 'studios'); 640 | }); 641 | } 642 | } 643 | } 644 | matchUrl(location, fragment) { 645 | const regexp = concatRegexp(new RegExp(location.origin), fragment); 646 | this.log.debug(regexp, location.href.match(regexp)); 647 | return location.href.match(regexp) != null; 648 | } 649 | gmMain() { 650 | const location = window.location; 651 | this.log.debug(URL, window.location); 652 | 653 | waitForElementByXpath('//div[contains(@class, "align-items-center") and contains(@class, "navbar-nav")]//a', async (xpath, el) => { 654 | this.loggedIn = el.tagName === 'A'; 655 | this.userName = this.loggedIn ? el.innerText : null; 656 | 657 | if (this.loggedIn && !document.querySelector('.settings-box')) { 658 | const gearIcon = ``; 659 | const settingsEl = createElementFromHTML(`${gearIcon}`); 660 | el.parentElement.appendChild(settingsEl); 661 | const settingsMenuEl = createElementFromHTML(``); 676 | settingsEl.appendChild(settingsMenuEl); 677 | 678 | settingsEl.addEventListener('click', evt => { 679 | if (settingsMenuEl.style.display === 'none') { 680 | settingsMenuEl.style.display = 'block'; 681 | } 682 | else { 683 | settingsMenuEl.style.display = 'none'; 684 | } 685 | }); 686 | 687 | settingsMenuEl.addEventListener('click', evt => { 688 | evt.stopPropagation(); 689 | }); 690 | 691 | this.stashUrl = await GM.getValue('stashAddress', 'http://localhost:9999'); 692 | const stashAddress = document.getElementById('address'); 693 | stashAddress.value = this.stashUrl; 694 | stashAddress.addEventListener('change', async () => { 695 | await GM.setValue('stashAddress', stashAddress.value || 'http://localhost:9999'); 696 | }); 697 | 698 | this.stashApiKey = await GM.getValue('stashApiKey', ''); 699 | const stashApiKey = document.getElementById('apiKey'); 700 | stashApiKey.value = this.stashApiKey; 701 | stashApiKey.addEventListener('change', async () => { 702 | await GM.setValue('stashApiKey', stashApiKey.value || ''); 703 | }); 704 | } 705 | 706 | const [_, stashType, stashId, action] = location.pathname.split('/'); 707 | if (location.pathname === '/' || 708 | (stashType === 'scenes' && !stashId) || 709 | (stashType === 'performers' && stashId && !action) || 710 | (stashType === 'studios' && stashId && !action) || 711 | (stashType === 'tags' && stashId && !action) || 712 | (stashType === 'wishlist' && !stashId)) { 713 | waitForElementByXpath('(//div[contains(@class, "HomePage-scenes")]/div[@class="col"]|//div[@class="scenes-list"]/div[@class="row"]/div[@class="col-3"])/div[contains(@class, "SceneCard")]', (xpath, el) => { 714 | const sceneCards = document.querySelectorAll('.row .SceneCard'); 715 | for (const sceneCard of sceneCards) { 716 | const stashId = getElementByXpath("./div[@class='card-footer']//a/@href", sceneCard).value.replace('/scenes/', ''); 717 | this.processListScene(stashId, sceneCard); 718 | } 719 | }); 720 | } 721 | else if (stashType === 'scenes' && stashId && !action) { 722 | this.processPageScene(stashId); 723 | } 724 | else if (stashType === 'search' && stashId && !action) { 725 | waitForElementByXpath('//div[@class="SearchPage"]/div[@class="row"]/div[@class="col-6"]/h3[text()="Scenes"]', (xpath, el) => { 726 | const sceneCards = document.querySelectorAll('.SearchPage-scene'); 727 | for (const sceneCard of sceneCards) { 728 | const stashId = sceneCard.href.split('/').pop(); 729 | this.processSearchScene(stashId, sceneCard); 730 | } 731 | }); 732 | } 733 | 734 | if (stashType === 'performers' && !stashId) { 735 | waitForElementClass('PerformerCard', (className, el) => { 736 | this.addStashPerformerLinks(); 737 | }); 738 | } 739 | if (stashType === 'performers' && stashId && !action) { 740 | waitForElementClass('PerformerInfo', (className, el) => { 741 | this.addStashPerformerLink(); 742 | }); 743 | } 744 | else if (stashType === 'scenes' && stashId && !action) { 745 | waitForElementClass('scene-performers', (className, el) => { 746 | this.addStashScenePerformerLink(); 747 | }); 748 | } 749 | else if (stashType === 'search' && stashId && !action) { 750 | waitForElementClass('SearchPage-performer', (className, el) => { 751 | this.addStashSearchPerformerLink(); 752 | }); 753 | } 754 | else if (stashType === 'users' && stashId && action === 'edits') { 755 | waitForElementByXpath("//div[contains(@class, 'EditCard')]|//h4[text()='No results']", (xpath, el) => { 756 | this.processEdits(); 757 | }); 758 | } 759 | else if (stashType === 'edits') { 760 | if (stashId) { 761 | waitForElementByXpath("//div[contains(@class, 'EditCard')]|//h4[text()='No results']", (xpath, el) => { 762 | this.processEdits(); 763 | }); 764 | } 765 | else { 766 | waitForElementByXpath("//div[contains(@class, 'EditCard')]|//h4[text()='No results']", (xpath, el) => { 767 | this.processEdits(); 768 | }); 769 | } 770 | } 771 | 772 | if (location.pathname === '/') { 773 | this.log.debug('[Navigation] Home Page'); 774 | } 775 | 776 | if (stashType === 'wishlist') { 777 | this.viewWishlist(); 778 | } 779 | else { 780 | if (document.getElementById('wishlist')) { 781 | document.getElementById('wishlist').remove(); 782 | document.getElementById('wishlist-header').remove(); 783 | document.getElementById('wishlist-pagination').remove(); 784 | } 785 | } 786 | 787 | waitForElementByXpath("//div[contains(@class, 'navbar-nav')]", (xpath, el) => { 788 | if (!document.getElementById('nav-wishlist')) { 789 | const navWishlist = createElementFromHTML(`Wishlist`); 790 | el.appendChild(navWishlist); 791 | } 792 | if (stashType === 'wishlist') { 793 | document.getElementById('nav-wishlist').classList.add('active'); 794 | document.getElementById('nav-wishlist').setAttribute('aria-current', 'page'); 795 | } 796 | else { 797 | document.getElementById('nav-wishlist').classList.remove('active'); 798 | document.getElementById('nav-wishlist').removeAttribute('aria-current'); 799 | } 800 | }); 801 | 802 | this.dispatchEvent(new CustomEvent('page', { 'detail': { stashType, stashId, action } })); 803 | }); 804 | } 805 | createPaginationButtons(maxPages, page, paginationEl) { 806 | let start = Math.max(page - 2, 1); 807 | let end = Math.min(start + 4, maxPages); 808 | if (maxPages <= 5) { 809 | start = 1; 810 | end = maxPages; 811 | } 812 | else if (end - start < 4) { 813 | start = Math.max(end - 4, 1); 814 | } 815 | 816 | const paginationButtonHandler = evt => { 817 | const page = getClosestAncestor(evt.target, '.page-item').querySelector('.page-link').dataset.page; 818 | evt.preventDefault(); 819 | evt.stopPropagation(); 820 | let url = window.location.href.split('?')[0]; 821 | if (page > 1) { 822 | url += `?page=${page}`; 823 | } 824 | window.location = url; 825 | } 826 | 827 | let pageEl; 828 | if (start > 1) { 829 | pageEl = createElementFromHTML(`
  • 830 | First 831 |
  • `); 832 | pageEl.addEventListener('click', paginationButtonHandler); 833 | paginationEl.appendChild(pageEl); 834 | } 835 | pageEl = createElementFromHTML(`
  • 836 | 837 | 838 | Previous 839 | 840 |
  • `); 841 | if (page !== 1) { 842 | pageEl.addEventListener('click', paginationButtonHandler); 843 | } 844 | paginationEl.appendChild(pageEl); 845 | const spanCurrent = ` (current)`; 846 | for (let i = start; i <= end; i++) { 847 | pageEl = createElementFromHTML(`
  • 848 | ${i}${page === i ? spanCurrent : ''} 849 |
  • `); 850 | pageEl.addEventListener('click', paginationButtonHandler); 851 | paginationEl.appendChild(pageEl); 852 | } 853 | pageEl = createElementFromHTML(`
  • 854 | 855 | 856 | Next 857 | 858 |
  • `); 859 | if (page !== maxPages) { 860 | pageEl.addEventListener('click', paginationButtonHandler); 861 | } 862 | paginationEl.appendChild(pageEl); 863 | if (end < maxPages) { 864 | pageEl = createElementFromHTML(`
  • 865 | Last 866 |
  • `); 867 | pageEl.addEventListener('click', paginationButtonHandler); 868 | paginationEl.appendChild(pageEl); 869 | } 870 | } 871 | async viewWishlist() { 872 | waitForElementClass('NarrowPage', async (className, el) => { 873 | 874 | const keys = await GM.listValues(); 875 | let stashIds = []; 876 | const sceneStates = {}; 877 | for (const key of keys) { 878 | if (key.startsWith('stash')) continue; 879 | const data = JSON.parse(await GM.getValue(key)); 880 | if (data.wanted) { 881 | stashIds.push(key); 882 | sceneStates[key] = data; 883 | } 884 | } 885 | 886 | el[0].appendChild(createElementFromHTML(`

    Wishlist

    `)); 887 | 888 | const pagination = createElementFromHTML(`
    889 |
    890 | ${stashIds.length.toLocaleString()} results 891 | 893 |
    894 |
    `); 895 | el[0].appendChild(pagination); 896 | 897 | const searchParams = new URLSearchParams(window.location.search); 898 | let page = parseInt(searchParams.get('page') || 1); 899 | const pageSize = 20; 900 | const maxPages = Math.ceil(stashIds.length / pageSize); 901 | if (isNaN(page) || page < 1) page = 1; 902 | if (page > maxPages) page = maxPages; 903 | this.createPaginationButtons(maxPages, page, pagination.querySelector('ul.pagination')); 904 | const startIndex = (page - 1) * pageSize; 905 | const endIndex = page * pageSize; 906 | stashIds = stashIds.slice(startIndex, endIndex); 907 | 908 | const scenesList = createElementFromHTML(`
    `); 909 | el[0].appendChild(scenesList); 910 | const scenesRow = scenesList.firstChild; 911 | 912 | for (const stashId of stashIds) { 913 | const sceneState = sceneStates[stashId]; 914 | let title, release_date, duration, studioName, studioId, cover; 915 | if (sceneState.data) { 916 | const data = sceneState.data; 917 | title = data.title; 918 | release_date = data.release_date; 919 | duration = data.duration; 920 | studioName = data.studioName; 921 | studioId = data.studioId; 922 | cover = data.cover; 923 | } 924 | else { 925 | const data = (await this.findStashboxSceneByStashId(stashId))?.data?.findScene; 926 | title = data.title; 927 | release_date = data.release_date; 928 | duration = data.duration; 929 | studioName = data?.studio?.name || ''; 930 | studioId = data?.studio?.id || ''; 931 | cover = data?.images[0]?.url; 932 | sceneState.data = { 933 | title, 934 | release_date, 935 | duration, 936 | studioName, 937 | studioId, 938 | cover 939 | } 940 | await GM.setValue(stashId, JSON.stringify(sceneState)); 941 | } 942 | const scene = createElementFromHTML(`
    943 |
    944 |
    945 | 946 | 947 | 948 |
    949 | 966 |
    967 |
    `); 968 | scenesRow.appendChild(scene); 969 | } 970 | }); 971 | } 972 | async findStashboxSceneByStashId(stashId) { 973 | const reqData = { 974 | "operationName": "Scene", 975 | "variables": { 976 | "id": stashId 977 | }, 978 | "query": `query Scene($id: ID!) { 979 | findScene(id: $id) { 980 | id 981 | release_date 982 | title 983 | deleted 984 | duration 985 | images { 986 | id 987 | url 988 | width 989 | height 990 | } 991 | studio { 992 | id 993 | name 994 | } 995 | } 996 | }` 997 | }; 998 | return this.callStashDbGQL(reqData); 999 | } 1000 | } 1001 | 1002 | return { 1003 | stashdb: new StashDB({ logging: false }), 1004 | StashDB, 1005 | waitForElementId, 1006 | waitForElementClass, 1007 | waitForElementByXpath, 1008 | getElementByXpath, 1009 | getElementsByXpath, 1010 | getClosestAncestor, 1011 | insertAfter, 1012 | createElementFromHTML, 1013 | setNativeValue, 1014 | updateTextInput, 1015 | sortElementChildren, 1016 | xPathResultToArray, 1017 | reloadImg, 1018 | Logger, 1019 | }; 1020 | }; 1021 | 1022 | if (!unsafeWindow.stashdb) { 1023 | unsafeWindow.stashdb = stashdb(); 1024 | } 1025 | })(); -------------------------------------------------------------------------------- /src/body/StashDB Copy Scene Name.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | const { 5 | stashdb, 6 | StashDB, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | sortElementChildren, 12 | createElementFromHTML, 13 | } = unsafeWindow.stashdb; 14 | 15 | function createTooltipElement() { 16 | const copyTooltip = document.createElement('span'); 17 | copyTooltip.setAttribute('id', 'copy-tooltip'); 18 | copyTooltip.innerText = 'Copied!'; 19 | copyTooltip.classList.add('fade', 'hide'); 20 | copyTooltip.style.position = "absolute"; 21 | copyTooltip.style.left = '0px'; 22 | copyTooltip.style.top = '0px'; 23 | copyTooltip.style.marginLeft = '40px'; 24 | copyTooltip.style.padding = '5px 12px'; 25 | copyTooltip.style.backgroundColor = '#000000df'; 26 | copyTooltip.style.borderRadius = '4px'; 27 | copyTooltip.style.color = '#fff'; 28 | document.body.appendChild(copyTooltip); 29 | return copyTooltip; 30 | } 31 | 32 | function createCopyButton() { 33 | const copyBtn = document.createElement('button'); 34 | copyBtn.setAttribute('id', 'copy-scene-name'); 35 | copyBtn.title = 'Copy to clipboard'; 36 | copyBtn.innerHTML = `Copy Scene Name`; 37 | copyBtn.classList.add('btn', 'btn-secondary', 'btn-sm', 'minimal', 'ml-1'); 38 | copyBtn.addEventListener('click', evt => { 39 | const title = document.querySelector('.card-header > h3').innerText; 40 | const studio = document.querySelector('.card-header > h6 > a').innerText.replaceAll(' ', ''); 41 | const datestring = document.querySelector('.card-header > h6').childNodes[2].nodeValue.replaceAll('-', '.'); 42 | const performers = [...document.querySelectorAll('.scene-performer > svg[data-icon=venus] + span')].map(node => node.innerText); 43 | GM_setClipboard(`[${studio}] ${performers.join(', ')} - ${title} (${datestring})`); 44 | const copyTooltip = createTooltipElement(); 45 | const rect = document.body.getBoundingClientRect(); 46 | const rect2 = evt.currentTarget.getBoundingClientRect(); 47 | const x = rect2.left - rect.left; 48 | const y = rect2.top - rect.top; 49 | copyTooltip.classList.add('show'); 50 | copyTooltip.style.left = `${x}px`; 51 | copyTooltip.style.top = `${y}px`; 52 | setTimeout(() => { 53 | copyTooltip.remove(); 54 | }, 500); 55 | }); 56 | return copyBtn; 57 | } 58 | 59 | stashdb.addEventListener('page', evt => { 60 | const { stashType, stashId, action } = evt.detail; 61 | 62 | waitForElementByXpath("//div[contains(@class, 'card-header')]", (xpath, el) => { 63 | if ((stashType === 'scenes' && stashId && !action)) { 64 | if (!document.getElementById('copy-scene-name')) { 65 | el.appendChild(createCopyButton()); 66 | } 67 | else { 68 | document.getElementById('copy-scene-name').style.display = 'inline-block'; 69 | } 70 | } 71 | else if (document.getElementById('copy-scene-name')) { 72 | document.getElementById('copy-scene-name').style.display = 'none'; 73 | } 74 | }); 75 | 76 | 77 | }); 78 | 79 | 80 | 81 | 82 | })(); -------------------------------------------------------------------------------- /src/body/StashDB Copy StashID.user.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | const { 5 | stashdb, 6 | StashDB, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | sortElementChildren, 12 | createElementFromHTML, 13 | } = unsafeWindow.stashdb; 14 | 15 | function createTooltipElement() { 16 | const copyTooltip = document.createElement('span'); 17 | copyTooltip.setAttribute('id', 'copy-tooltip'); 18 | copyTooltip.innerText = 'Copied!'; 19 | copyTooltip.classList.add('fade', 'hide'); 20 | copyTooltip.style.position = "absolute"; 21 | copyTooltip.style.left = '0px'; 22 | copyTooltip.style.top = '0px'; 23 | copyTooltip.style.marginLeft = '40px'; 24 | copyTooltip.style.padding = '5px 12px'; 25 | copyTooltip.style.backgroundColor = '#000000df'; 26 | copyTooltip.style.borderRadius = '4px'; 27 | copyTooltip.style.color = '#fff'; 28 | document.body.appendChild(copyTooltip); 29 | return copyTooltip; 30 | } 31 | 32 | function createCopyButton() { 33 | const copyBtn = document.createElement('button'); 34 | copyBtn.setAttribute('id', 'copy-stashid'); 35 | copyBtn.title = 'Copy to clipboard'; 36 | copyBtn.innerHTML = `Copy StashID`; 37 | copyBtn.classList.add('btn', 'btn-secondary', 'btn-sm', 'minimal', 'ml-1'); 38 | copyBtn.addEventListener('click', evt => { 39 | GM_setClipboard(window.location.pathname.split('/').pop()); 40 | const copyTooltip = createTooltipElement(); 41 | const rect = document.body.getBoundingClientRect(); 42 | const rect2 = evt.currentTarget.getBoundingClientRect(); 43 | const x = rect2.left - rect.left; 44 | const y = rect2.top - rect.top; 45 | copyTooltip.classList.add('show'); 46 | copyTooltip.style.left = `${x}px`; 47 | copyTooltip.style.top = `${y}px`; 48 | setTimeout(() => { 49 | copyTooltip.remove(); 50 | }, 500); 51 | }); 52 | return copyBtn; 53 | } 54 | 55 | stashdb.addEventListener('page', evt => { 56 | const { stashType, stashId, action } = evt.detail; 57 | 58 | waitForElementByXpath("//div[contains(@class, 'navbar-nav')]", (xpath, el) => { 59 | if ((stashType === 'scenes' && stashId && !action) || 60 | (stashType === 'performers' && stashId && !action) || 61 | (stashType === 'studios' && stashId && !action)) { 62 | if (!document.getElementById('copy-stashid')) { 63 | el.appendChild(createCopyButton()); 64 | } 65 | else { 66 | document.getElementById('copy-stashid').style.display = 'inline-block'; 67 | } 68 | } 69 | else if (document.getElementById('copy-stashid')) { 70 | document.getElementById('copy-stashid').style.display = 'none'; 71 | } 72 | }); 73 | 74 | 75 | }); 76 | 77 | 78 | 79 | 80 | })(); -------------------------------------------------------------------------------- /src/body/StashDB Scene Filter.user.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | const { 5 | stashdb, 6 | StashDB, 7 | waitForElementId, 8 | waitForElementClass, 9 | waitForElementByXpath, 10 | getElementByXpath, 11 | sortElementChildren, 12 | createElementFromHTML, 13 | insertAfter, 14 | getClosestAncestor, 15 | } = unsafeWindow.stashdb; 16 | 17 | function updateVisibility(dropdown) { 18 | for (const sceneCard of document.querySelectorAll('.SceneCard')) { 19 | sceneCard.parentElement.classList.remove('d-none'); 20 | } 21 | if (dropdown.value === 'OWNED') { 22 | for (const node of document.querySelectorAll('.match-no')) { 23 | const sceneCard = getClosestAncestor(node, '.SceneCard'); 24 | sceneCard.parentElement.classList.add('d-none'); 25 | } 26 | } 27 | else if (dropdown.value === 'MISSING') { 28 | for (const node of document.querySelectorAll('.match-yes')) { 29 | const sceneCard = getClosestAncestor(node, '.SceneCard'); 30 | sceneCard.parentElement.classList.add('d-none'); 31 | } 32 | } 33 | } 34 | 35 | stashdb.addEventListener('page', evt => { 36 | const { stashType, stashId, action } = evt.detail; 37 | 38 | waitForElementByXpath("//div[contains(@class, 'navbar-nav')]", (xpath, el) => { 39 | if ((stashType === 'scenes' && !stashId && !action) || 40 | (stashType === 'performers' && stashId && !action) || 41 | (stashType === 'studios' && stashId && !action)) { 42 | waitForElementClass('scene-sort', (className, el) => { 43 | if (!document.querySelector('.visible-filter')) { 44 | const dropdownContainer = createElementFromHTML(`
    45 | 50 |
    `); 51 | insertAfter(dropdownContainer, el[0].parentElement); 52 | 53 | const dropdown = document.querySelector('.visible-filter select'); 54 | dropdown.addEventListener('change', evt => { 55 | updateVisibility(dropdown); 56 | }) 57 | } 58 | }); 59 | } 60 | }); 61 | 62 | 63 | }); 64 | 65 | stashdb.addEventListener('scenecard', evt => { 66 | const { sceneEl } = evt.detail; 67 | const dropdown = document.querySelector('.visible-filter select'); 68 | if (!dropdown) return; 69 | sceneEl.parentElement.classList.remove('d-none'); 70 | if (dropdown.value === 'OWNED' && sceneEl.querySelector('.match-no')) { 71 | sceneEl.parentElement.classList.add('d-none'); 72 | } 73 | else if (dropdown.value === 'MISSING' && sceneEl.querySelector('.match-yes')) { 74 | sceneEl.parentElement.classList.add('d-none'); 75 | } 76 | }); 77 | 78 | 79 | 80 | 81 | })(); -------------------------------------------------------------------------------- /src/header/StashDB Copy Scene Name.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name StashDB Copy Scene Name 3 | // @namespace https://github.com/7dJx1qP/stashdb-userscripts 4 | // @description StashDB Copy Scene Name 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match https://stashdb.org/* 8 | // @resource IMPORTED_CSS https://raw.githubusercontent.com/7dJx1qP/stashdb-userscripts/dist/public/scene.css 9 | // @grant unsafeWindow 10 | // @grant GM_setClipboard 11 | // @grant GM_getResourceText 12 | // @grant GM_addStyle 13 | // @grant GM.getValue 14 | // @grant GM.setValue 15 | // @grant GM.listValues 16 | // @grant GM.xmlHttpRequest 17 | // @require %LIBRARYPATH% 18 | // @require %FILEPATH% 19 | // @run-at document-start 20 | 21 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/StashDB Copy StashID.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name StashDB Copy StashID 3 | // @namespace https://github.com/7dJx1qP/stashdb-userscripts 4 | // @description StashDB Copy StashID 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match https://stashdb.org/* 8 | // @resource IMPORTED_CSS https://raw.githubusercontent.com/7dJx1qP/stashdb-userscripts/dist/public/scene.css 9 | // @grant unsafeWindow 10 | // @grant GM_setClipboard 11 | // @grant GM_getResourceText 12 | // @grant GM_addStyle 13 | // @grant GM.getValue 14 | // @grant GM.setValue 15 | // @grant GM.listValues 16 | // @grant GM.xmlHttpRequest 17 | // @require %LIBRARYPATH% 18 | // @require %FILEPATH% 19 | // @run-at document-start 20 | 21 | // ==/UserScript== -------------------------------------------------------------------------------- /src/header/StashDB Scene Filter.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name StashDB Scene Filter 3 | // @namespace https://github.com/7dJx1qP/stashdb-userscripts 4 | // @description StashDB Scene Filter 5 | // @version 0.2.0 6 | // @author 7dJx1qP 7 | // @match https://stashdb.org/* 8 | // @resource IMPORTED_CSS https://raw.githubusercontent.com/7dJx1qP/stashdb-userscripts/dist/public/scene.css 9 | // @grant unsafeWindow 10 | // @grant GM_setClipboard 11 | // @grant GM_getResourceText 12 | // @grant GM_addStyle 13 | // @grant GM.getValue 14 | // @grant GM.setValue 15 | // @grant GM.listValues 16 | // @grant GM.xmlHttpRequest 17 | // @require %LIBRARYPATH% 18 | // @require %FILEPATH% 19 | // @run-at document-start 20 | 21 | // ==/UserScript== -------------------------------------------------------------------------------- /src/scene.css: -------------------------------------------------------------------------------- 1 | .svg-inline--fa { 2 | display: var(--fa-display, inline-block); 3 | height: 1em; 4 | overflow: visible; 5 | vertical-align: -0.125em; 6 | } 7 | 8 | .nav-link .fa-gear { 9 | width: 24px; 10 | height: 24px; 11 | } 12 | 13 | .stash_id_match a:hover { 14 | background-color: rgba(0, 0, 0, 0.541); 15 | color: #fff !important; 16 | } 17 | 18 | .stash_id_match.search_match { 19 | position: absolute; 20 | top: 10px; 21 | right: 10px; 22 | align-self: center; 23 | } 24 | 25 | .stash_id_match.scene_match { 26 | position: relative; 27 | margin-left: 10px; 28 | cursor: pointer; 29 | align-self: center; 30 | display: inline; 31 | } 32 | 33 | .match-yes { 34 | color: green; 35 | } 36 | 37 | .match-no { 38 | color: red; 39 | } 40 | 41 | .stash_id_ignored .match-no { 42 | color: yellow; 43 | } 44 | 45 | .stash_id_wanted .match-no { 46 | color: gold; 47 | } 48 | 49 | .stash_id_match svg { 50 | height: 24px; 51 | width: 24px; 52 | } 53 | 54 | .stash-performer-link img, 55 | .stash-scene-link img, 56 | .stash-studio-link img { 57 | width: 2rem; 58 | padding-left: 0.5rem; 59 | } 60 | 61 | .scene-performers .stash-performer-link { 62 | padding-right: 0.25rem; 63 | } 64 | 65 | .SearchPage .stash-performer-link img { 66 | width: 2rem; 67 | padding-left: 0rem; 68 | margin-right: 10px; 69 | } 70 | 71 | .stash_id_ignored, 72 | .stash_id_ignored > .card { 73 | background-color: rgba(48, 64, 77, 0.25) !important; 74 | } 75 | 76 | .stash_id_ignored img { 77 | opacity: 0.25; 78 | } 79 | 80 | .settings-box { 81 | padding: 1rem; 82 | margin-bottom: 0; 83 | position: absolute; 84 | right: 0; 85 | z-index: 999; 86 | background-color: inherit; 87 | } --------------------------------------------------------------------------------