Skip to content

Instantly share code, notes, and snippets.

@peolic
Last active July 27, 2023 16:49
Show Gist options
  • Save peolic/c38aa4792a668b635e7d99476e3433bb to your computer and use it in GitHub Desktop.
Save peolic/c38aa4792a668b635e7d99476e3433bb to your computer and use it in GitHub Desktop.
Add page titles to StashDB

StashDB Titles Userscript

This userscript adds titles to StashDB pages.

screenshot

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();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment