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(); | |
})(); |