This userscript adds titles to StashDB pages.
Installation requires a browser extension such as Tampermonkey or Greasemonkey.
This userscript adds titles to StashDB pages.
Installation requires a browser extension such as Tampermonkey or Greasemonkey.
| // ==UserScript== | |
| // @name StashDB Titles | |
| // @author peolic | |
| // @version 1.10 | |
| // @description StashDB page titles. | |
| // @namespace https://github.com/peolic | |
| // @include https://stashdb.org/* | |
| // @run-at document-idle | |
| // @homepageURL https://gist.github.com/peolic/c38aa4792a668b635e7d99476e3433bb | |
| // @downloadURL https://gist.github.com/peolic/c38aa4792a668b635e7d99476e3433bb/raw/stashdb-titles.user.js | |
| // @updateURL https://gist.github.com/peolic/c38aa4792a668b635e7d99476e3433bb/raw/stashdb-titles.user.js | |
| // ==/UserScript== | |
| (() => { | |
| const eventPrefix = 'stashdb_titles'; | |
| function main() { | |
| if (!document.body.dataset.titlesInjected) { | |
| document.body.dataset.titlesInjected = 'true'; | |
| setTitle(); | |
| window.addEventListener(`${eventPrefix}_locationchange`, setTitle); | |
| } | |
| } | |
| async function setTitle() { | |
| await elementReadyIn('.StashDBContent > .LoadingIndicator', 100); | |
| if (document.querySelector('main')) { | |
| console.log('[titles] disabled'); | |
| setTimeout(window.removeEventListener, 0, `${eventPrefix}_locationchange`, setTitle); | |
| return; | |
| } | |
| const pathname = window.location.pathname.replace(/^\//, ''); | |
| const pathParts = pathname ? pathname.split(/\//g) : []; | |
| console.debug(`[titles]`, pathParts); | |
| if (pathParts.length === 0) { | |
| return document.title = 'Home | StashDB'; | |
| } | |
| if (pathParts.length === 1) { | |
| const object = pathParts[0].charAt(0).toUpperCase() + pathParts[0].slice(1); | |
| let filtersText = ''; | |
| if (pathParts[0] === 'edits') { | |
| const filters = Array.from(document.querySelectorAll('h3 + div > form select')) | |
| .slice(1) | |
| .map((el) => (el.value === '' ? '' : el.selectedOptions[0].innerText)) | |
| .filter(Boolean); | |
| if (filters.length !== 0) { | |
| filtersText = ': ' + filters.join(', '); | |
| } | |
| } else if (pathParts[0] === 'scenes') { | |
| const searchParams = new URLSearchParams(window.location.search); | |
| if (searchParams.get('fingerprint')) { | |
| filtersText = ' by fingerprint'; | |
| } | |
| } | |
| return document.title = `${object}${filtersText} | StashDB`; | |
| } | |
| if (pathParts.length === 2) { | |
| if (isUUID(pathParts[1]) || pathParts[0] === 'users') { | |
| if (await titleViewPage(...pathParts)) return; | |
| } | |
| if (pathParts[1] === 'add') { | |
| return await titleCreatePage(...pathParts); | |
| } | |
| if (pathParts[0] === 'search') { | |
| const search = await elementReadyIn('.SearchPage-input > input', 1000); | |
| if (search.value) { | |
| return document.title = `Search: "${search.value}" | StashDB`; | |
| } else { | |
| return document.title = `Search | StashDB`; | |
| } | |
| } | |
| } | |
| if (pathParts.length === 3) { | |
| if (isUUID(pathParts[1])) { | |
| if (['edit', 'merge', 'delete'].includes(pathParts[2])) return await titleActionPage(...pathParts); | |
| } | |
| if (pathParts[0] === 'users' && pathParts[2] === 'edits') { | |
| return document.title = `Edits by ${pathParts[1]} | StashDB`; | |
| } | |
| } | |
| return document.title = 'StashDB'; | |
| } | |
| async function titleViewPage(object, ident) { | |
| if (object === 'performers') { | |
| const performerInfo = await elementReady('.performer-info'); | |
| const name = | |
| Array.from(performerInfo.querySelector('h3').childNodes) | |
| .slice(1) | |
| .map(n => n.textContent) | |
| .join(' '); | |
| return document.title = `${name} | StashDB`; | |
| } | |
| if (object === 'scenes') { | |
| const sceneInfo = await elementReady('.scene-info'); | |
| const titleEl = sceneInfo.querySelector('h3'); | |
| const title = !titleEl.hasChildNodes() || titleEl.textContent.includes('<MISSING>') | |
| ? '<untitled>' | |
| : titleEl.firstChild.textContent; | |
| const studio = sceneInfo.querySelector('h6 > a[href^="/studios/"]').textContent; | |
| return document.title = `${title} - ${studio} | StashDB`; | |
| } | |
| if (object === 'studios') { | |
| const studio = await elementReady('h3'); | |
| const studioName = studio.childElementCount >= 2 ? studio.children[1].textContent : studio.textContent; | |
| return document.title = `Studio: ${studioName} | StashDB`; | |
| } | |
| if (object === 'tags') { | |
| const tag = await elementReady('h3'); | |
| const tagName = Array.from(tag.childNodes) | |
| .map(n => n.textContent) | |
| .join(' '); | |
| return document.title = `${tagName} | StashDB`; | |
| } | |
| if (object === 'categories') { | |
| const cat = await elementReady('h3'); | |
| const catName = cat.textContent; | |
| return document.title = `Category: ${catName} | StashDB`; | |
| } | |
| if (object === 'edits') { | |
| const card = await elementReadyIn('div.card', 1000); | |
| const action = card.querySelector('.card-header a[href^="/edits/"]').innerText; | |
| let target; | |
| const targetLink = card.querySelector('.card-body h6 > a') || card.querySelector('.card-body a > span.EditDiff.bg-danger') || Array.from(card.querySelectorAll('.card-body > .mb-4 a')); | |
| if (targetLink instanceof Element) { | |
| target = targetLink.innerText; | |
| } else if (Array.isArray(targetLink) && targetLink.length > 0) { | |
| target = targetLink.slice(-1)[0].innerText; | |
| } | |
| // Creation | |
| else { | |
| const fields = Array.from(card.querySelectorAll('.card-body > .row > b')); | |
| const nameDiff = fields.find((f) => f.innerText === 'Name'); | |
| if (nameDiff) { | |
| target = nameDiff.nextElementSibling.innerText; | |
| const disambiguationDiff = fields.find((f) => f.innerText === 'Disambiguation'); | |
| if (disambiguationDiff) { | |
| const disambiguation = disambiguationDiff.nextElementSibling.innerText; | |
| target += ` (${disambiguation})`; | |
| } | |
| } | |
| } | |
| if (action.startsWith('Merge ')) { | |
| return document.title = `${action}s into ${target} | StashDB`; | |
| } else { | |
| return document.title = `${action}: ${target} | StashDB`; | |
| } | |
| } | |
| if (object === 'users') { | |
| return document.title = `User: ${ident} | StashDB`; | |
| } | |
| return null; | |
| } | |
| async function titleCreatePage(object) { | |
| const type = object.slice(0, -1); | |
| const template = (name) => `Creating ${type}${name ? `: ${name}` : ''} | StashDB`; | |
| document.title = template(''); | |
| const nameInput = await elementReadyIn('input[name="name"], input[name="title"]', 1000); | |
| if (nameInput) { | |
| if (type === 'performer') { | |
| const disambiguation = document.querySelector('input[name="disambiguation"]'); | |
| if (disambiguation) { | |
| disambiguation.addEventListener('input', () => { | |
| const name = nameInput.value + (disambiguation.value ? ` (${disambiguation.value})` : ''); | |
| document.title = template(name); | |
| }); | |
| } | |
| } | |
| nameInput.addEventListener('input', () => { | |
| let name = nameInput.value; | |
| if (type === 'performer') { | |
| const disambiguation = document.querySelector('input[name="disambiguation"]'); | |
| name += disambiguation && disambiguation.value ? ` (${disambiguation.value})` : ''; | |
| } | |
| document.title = template(name); | |
| }); | |
| } | |
| return; | |
| } | |
| async function titleActionPage(object, uuid, actionType) { | |
| const type = object.slice(0, -1); | |
| const action = | |
| actionType === 'edit' | |
| ? 'Editing' | |
| : (actionType.charAt(0).toUpperCase() + actionType.slice(1, -1) + 'ing'); | |
| if (actionType === 'merge') { | |
| const nameEl = await elementReadyIn('.StashDBContent h3 > em', 1000); | |
| const name = nameEl ? `: ${nameEl.innerText}` : ''; | |
| return document.title = `${action} into ${type}${name} | StashDB`; | |
| } | |
| if (actionType === 'delete') { | |
| const nameEl = await elementReadyIn('.StashDBContent h4 > em', 1000); | |
| const name = nameEl ? `: ${nameEl.innerText}` : ''; | |
| return document.title = `${action} ${type}${name} | StashDB`; | |
| } | |
| const nameInput = await elementReadyIn('input[name="name"], input[name="title"]', 1000); | |
| if (nameInput) { | |
| let name = nameInput.value; | |
| if (type === 'performer') { | |
| const disambiguation = document.querySelector('input[name="disambiguation"]'); | |
| name += disambiguation && disambiguation.value ? ` (${disambiguation.value})` : ''; | |
| } | |
| return document.title = `${action} ${type}: ${name} | StashDB`; | |
| } | |
| return document.title = `${action} ${type} | StashDB`; | |
| } | |
| /** | |
| * @param {string} text | |
| */ | |
| const isUUID = (text) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(text); | |
| /** | |
| * @param {number} ms | |
| */ | |
| const wait = (/** @type {number} */ ms) => new Promise((resolve) => setTimeout(resolve, ms)); | |
| /** | |
| * @param {string} selector | |
| * @param {number} [timeout] fail after, in milliseconds | |
| */ | |
| const elementReadyIn = (selector, timeout) => { | |
| const promises = [elementReady(selector)]; | |
| if (timeout) promises.push(wait(timeout).then(() => null)); | |
| return Promise.race(promises); | |
| }; | |
| // Based on: https://dirask.com/posts/JavaScript-on-location-changed-event-on-url-changed-event-DKeyZj | |
| (function() { | |
| const { pushState, replaceState } = history; | |
| const eventPushState = new Event(`${eventPrefix}_pushstate`); | |
| const eventReplaceState = new Event(`${eventPrefix}_replacestate`); | |
| const eventLocationChange = new Event(`${eventPrefix}_locationchange`); | |
| history.pushState = function() { | |
| pushState.apply(history, arguments); | |
| window.dispatchEvent(eventPushState); | |
| window.dispatchEvent(eventLocationChange); | |
| } | |
| history.replaceState = function() { | |
| replaceState.apply(history, arguments); | |
| window.dispatchEvent(eventReplaceState); | |
| window.dispatchEvent(eventLocationChange); | |
| } | |
| window.addEventListener('popstate', function() { | |
| window.dispatchEvent(eventLocationChange); | |
| }); | |
| })(); | |
| // MIT Licensed | |
| // Author: jwilson8767 | |
| // https://gist.github.com/jwilson8767/db379026efcbd932f64382db4b02853e | |
| /** | |
| * Waits for an element satisfying selector to exist, then resolves promise with the element. | |
| * Useful for resolving race conditions. | |
| * | |
| * @param {string} selector | |
| * @returns {Promise<Element>} | |
| */ | |
| function elementReady(selector) { | |
| return new Promise((resolve, reject) => { | |
| let el = document.querySelector(selector); | |
| if (el) {resolve(el);} | |
| new MutationObserver((mutationRecords, observer) => { | |
| // Query for elements matching the specified selector | |
| Array.from(document.querySelectorAll(selector)).forEach((element) => { | |
| resolve(element); | |
| //Once we have resolved we don't need the observer anymore. | |
| observer.disconnect(); | |
| }); | |
| }) | |
| .observe(document.documentElement, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| }); | |
| } | |
| main(); | |
| })(); |