// ==UserScript==
// @name Gelbooru Visited and Type Highlighter
// @namespace http://tampermonkey.net/
// @version 11.1.2
// @description Marks previously visited images on Gelbooru, marks gifs as well similar to the built-in webm highlighting, and makes webm highlighting work in more places.
// @author Xerodusk
// @homepage https://greasyfork.org/en/users/460331-xerodusk
// @include https://gelbooru.com/index.php*page=post*s=list*
// @include https://gelbooru.com/index.php*page=post*s=view*
// @include https://gelbooru.com/index.php*page=pool*s=show*
// @include https://gelbooru.com/index.php*page=favorites*s=view*
// @include https://gelbooru.com/index.php*page=tags*s=saved_search*
// @include https://gelbooru.com/index.php*page=wiki*s=view*
// @include https://gelbooru.com/index.php*page=account*s=profile*
// @grant none
// @icon https://gelbooru.com/favicon.png
// ==/UserScript==
/* jshint esversion: 6 */
/* configuration */
// Highlight colors
// Values can be hexadecimal, rgb, rgba, hsl, hsla, color name, or whatever CSS color definitions your browser supports
const imgUnvisitedColor = '#E1F5FE'; // Color for unvisted images
const imgVisitedColor = '#2E7D32'; // Color for visited images
const webmUnvisitedColor = '#1565C0'; // Color for unvisted WebMs
const webmVisitedColor = '#C62828'; // Color for visited WebMs
const gifUnvisitedColor = '#FFD600'; // Color for unvisited animated gifs/pngs
const gifVisitedColor = '#6A1B9A'; // Color for visited animated gifs/pngs
// Whether to display visited/unvisited highlighting for your own favorites
// If false: Will only show visited/unvisited on other users' favorites pages
// Animated GIF/WebM type highlighting will always be shown on all favorites
// If true: Will also show visited/unvisited on your own favorites page
const displayCurrentUserFavoritesVisited = false;
/*-------------------*/
// Tests whether value is in items
function inSortedList(items, value) {
'use strict';
function binarySearch(array, value, first, last) {
if (first > last) {
return false;
}
const middle = (last + first) >> 1;
if (array[middle] === value) {
return true;
}
if (array[middle] > value) {
return binarySearch(array, value, first, middle - 1);
} else {
return binarySearch(array, value, middle + 1, last);
}
}
return binarySearch(items, value, 0, items.length - 1);
}
// Inserts value in items if not already present, returns whether insertion took place
function insertIntoSortedList(items, value) {
'use strict';
let first = 0,
last = items.length - 1,
middle;
while (first <= last) {
middle = (last + first) >> 1;
if (items[middle] > value) {
last = middle - 1;
continue;
}
first = middle + 1;
if (items[middle] === value) {
return false;
}
}
items.splice(first, 0, value);
return true;
}
// Check if link is in visited list
function markIfVisited(galleryLink, visitedIDs) {
'use strict';
const linkURL = new URL(galleryLink.getAttribute('href'), window.location.href);
const linkSearchParams = new URLSearchParams(linkURL.search);
const id = parseInt(linkSearchParams.get('id'));
if (inSortedList(visitedIDs, id)) {
galleryLink.classList.add('visited');
}
}
// Checks all provided links and marks visited if in list
function markVisitedLinks(galleryLinks) {
'use strict';
let links = Array.from(galleryLinks);
function applyVisitedToAllLinksInList() {
const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
links.forEach(link => markIfVisited(link, visitedIDs));
links = links.filter(link => !link.classList.contains('visited'));
if (!links.length) {
window.removeEventListener('storage', applyVisitedToAllLinksInList);
}
}
// Also mark visited images opened in new tab/windows from this page, or by any other means while this page is open
window.addEventListener('storage', applyVisitedToAllLinksInList);
applyVisitedToAllLinksInList();
}
// Get cookie by name
// From https://www.w3schools.com/js/js_cookies.asp
function getCookie(cname) {
'use strict';
const name = cname + "=";
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
// Get current user's user ID, if exists
function getUserID() {
'use strict';
// Get user ID from cookie
const userID = getCookie('user_id');
return userID ? parseInt(userID) : -1;
}
// Create interface for history backups
function createBackupInterface() {
'use strict';
// Get header
const header = document.getElementById('navbar') || document.querySelector('.header .center');
if (!header) {
return;
}
// Create element for header
const headerWrapper = document.createElement('ul');
headerWrapper.classList.add('flat-list');
headerWrapper.classList.add('navbar-nav');
headerWrapper.classList.add('nav');
headerWrapper.style = 'float: right';
// Create button
const openDialogButtonContainer = document.createElement('li');
const openDialogButton = document.createElement('a');
openDialogButton.textContent = 'Visited History Backups';
openDialogButton.setAttribute('role', 'button');
openDialogButton.href = 'javascript:void(0)';
openDialogButton.onclick = () => {
let visitedIDs = localStorage.getItem('visitedIDs') || '[]';
visitedIDs = visitedIDs.slice(0, visitedIDs.length - 1).slice(1);
const textArea = document.getElementById('dialog-data-field');
textArea.value = visitedIDs;
textArea.select();
document.getElementById('backup-dialog').classList.add('open');
};
openDialogButtonContainer.appendChild(openDialogButton);
headerWrapper.appendChild(openDialogButtonContainer);
// Create dialog
const dialog = document.createElement('div');
dialog.id = 'backup-dialog';
const dialogHeader = document.createElement('h2');
dialogHeader.id = 'dialog-header';
dialogHeader.textContent = 'Back Up Visited Image History';
const dialogText = document.createElement('label');
dialogText.id = 'dialog-text';
dialogText.setAttribute('for', 'dialog-data-field');
dialogText.textContent = 'Copy the content of the text field and save it somewhere. To import a backup, paste in the text field and click "Import" to overwrite the current history or "Merge" to combine them.';
const dialogDataField = document.createElement('textarea');
dialogDataField.id = 'dialog-data-field';
dialogDataField.setAttribute('autocomplete', 'off');
dialogDataField.setAttribute('name', 'dialog-data-field');
dialogDataField.setAttribute('rows', '3');
// Create the buttons
const dialogButtons = document.createElement('div');
dialogButtons.id = 'dialog-buttons';
const copyButton = document.createElement('button');
const importButton = document.createElement('button');
const mergeButton = document.createElement('button');
const closeButton = document.createElement('button');
copyButton.id = 'dialog-copy-button';
copyButton.textContent = 'Copy to Clipboard';
copyButton.onclick = async () => {
const backupText = document.getElementById('dialog-data-field').value;
try {
await navigator.clipboard.writeText(backupText);
} catch (e) {
console.error('Failed to copy', e);
}
};
importButton.id = 'dialog-import-button';
importButton.textContent = 'Import';
importButton.onclick = () => {
const textareaContents = document.getElementById('dialog-data-field').value;
if (!(/(^$)|(^[0-9]+(,[0-9]+)*$)/.test(textareaContents))) {
document.getElementById('dialog-data-field').value = 'Invalid input';
return false;
}
const importedIDs = JSON.parse('[' + textareaContents + ']');
importedIDs.sort((a, b) => a - b);
localStorage.setItem('visitedIDs', JSON.stringify(importedIDs));
document.getElementById('backup-dialog').classList.remove('open');
};
mergeButton.id = 'dialog-merge-button';
mergeButton.textContent = 'Merge';
mergeButton.onclick = () => {
const textareaContents = document.getElementById('dialog-data-field').value;
if (!(/(^$)|(^[0-9]+(,[0-9]+)*$)/.test(textareaContents))) {
document.getElementById('dialog-data-field').value = 'Invalid input';
return false;
}
const importedIDs = JSON.parse('[' + textareaContents + ']');
const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
const combinedIDs = [...importedIDs, ...visitedIDs];
const mergedIDs = [...new Set(combinedIDs)];
mergedIDs.sort((a, b) => a - b);
localStorage.setItem('visitedIDs', JSON.stringify(mergedIDs));
document.getElementById('backup-dialog').classList.remove('open');
};
closeButton.id = 'dialog-close-button';
closeButton.textContent = 'Close';
closeButton.onclick = () => {
document.getElementById('backup-dialog').classList.remove('open');
};
if (!!navigator.clipboard) {
dialogButtons.appendChild(copyButton);
}
dialogButtons.appendChild(importButton);
dialogButtons.appendChild(mergeButton);
dialogButtons.appendChild(closeButton);
dialog.appendChild(dialogHeader);
dialog.appendChild(dialogText);
dialog.appendChild(dialogDataField);
dialog.appendChild(dialogButtons);
// Style everything
const css = document.createElement('style');
css.innerHTML = `
#backup-dialog {
position: fixed;
top: 0;
right: -400px;
background-color: white;
box-shadow: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2);
width: 400px;
max-width: 90vw;
max-height: 90vh;
padding: 12px;
font-size: 12px;
line-height: 1.42857143;
box-sizing: border-box;
transition: right 0.2s cubic-bezier(0,0,0.3,1);
}
#backup-dialog.open {
right: 0;
transition: right 0.25s cubic-bezier(0,0,0.3,1);
}
#backup-dialog * {
box-sizing: border-box;
font-family: verdana, sans-serif;
line-height: inherit;
}
#dialog-header {
all: revert;
}
#dialog-text {
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
white-space: unset;
}
#dialog-data-field {
width: 100%;
resize: vertical;
font-size: inherit;
padding: revert;
display: revert;
}
#dialog-data-field:focus {
background-color: unset;
}
#dialog-buttons button {
margin-right: 6px;
cursor: pointer;
font-size: inherit;
}
`;
document.head.appendChild(css);
// Attach button to header
header.appendChild(headerWrapper);
// Attach dialog to page
document.body.appendChild(dialog);
}
(function() {
'use strict';
// Find out what kind of page we're on
const searchParams = new URLSearchParams(window.location.search);
if (!searchParams.has('page') || !searchParams.has('s')) {
return false;
}
const page = searchParams.get('page');
const s = searchParams.get('s');
if (page === 'post') {
if (s === 'view') { // Image page
// Get id of current image
const url = new URL(window.location);
const currentURLSearchParams = new URLSearchParams(url.search);
const id = parseInt(currentURLSearchParams.get('id'));
// Add to list of visited images
function updateVisitedIDs(event) {
const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
if (insertIntoSortedList(visitedIDs, id)) {
localStorage.setItem('visitedIDs', JSON.stringify(visitedIDs));
} else {
window.removeEventListener('storage', updateVisitedIDs);
}
}
window.addEventListener('storage', updateVisitedIDs); // Update changes if another image being loaded in a different window changes the list before this one
updateVisitedIDs();
// "More Like This" results, currently in beta, could break, but this code should hypothetically never break the page from this end
// Unfortunately, type highlighting is impossible with their current implementation, but visited highlighting is still possible
const mltContainer = document.getElementsByClassName('contain-push')[0];
// Get all image thumbnail links
const mltLinks = mltContainer.querySelectorAll('#right-col > div > div > a[href*="page=post&s=view"]');
if (!!mltLinks) {
markVisitedLinks(mltLinks);
}
const css = document.createElement('style');
css.innerHTML = `
a img.mltThumbs {
padding: 5px;
margin: 5px !important;
outline: 3px solid ` + imgUnvisitedColor + `;
background-color: #FFFFFF;
}
a:visited img.mltThumbs,
a.visited img.mltThumbs {
outline-color: ` + imgVisitedColor + `;
}
a:not(.visited):visited img.mltThumbs {
background-color: #9E9E9E;
}
`;
document.head.appendChild(css);
} else if (s === 'list') { // Search page
// Get search results area
const galleryContainer = document.querySelector('.thumbnail-container');
if (!!galleryContainer) {
// Get all image thumbnail links
const galleryLinks = galleryContainer.querySelectorAll('div.thumbnail-preview a[href*="page=post&s=view"]');
if (!!galleryLinks) {
markVisitedLinks(galleryLinks);
}
}
// Apply borders
const css = document.createElement('style');
css.innerHTML = `
div.thumbnail-preview {
background-color: transparent;
}
div.thumbnail-preview a img.thumbnail-preview {
background-color: #FFFFFF;
}
div.thumbnail-preview a:not(.visited):visited img.thumbnail-preview {
background-color: #9E9E9E;
}
div.thumbnail-preview a img.thumbnail-preview:not(.webm) {
outline: 3px solid ` + imgUnvisitedColor + `;
}
div.thumbnail-preview a:visited img.thumbnail-preview,
div.thumbnail-preview a.visited img.thumbnail-preview {
outline-color: ` + imgVisitedColor + `;
}
div.thumbnail-preview a img.thumbnail-preview.webm {
border-color: ` + webmUnvisitedColor + ` !important;
}
div.thumbnail-preview a:visited img.thumbnail-preview.webm,
div.thumbnail-preview a.visited img.thumbnail-preview.webm {
border-color: ` + webmVisitedColor + ` !important;
}
div.thumbnail-preview a img.thumbnail-preview[title*="animated_gif"],
div.thumbnail-preview a img.thumbnail-preview[title*="animated_png"],
div.thumbnail-preview a img.thumbnail-preview[title*="animated "]:not(.webm) {
outline-color: ` + gifUnvisitedColor + `;
}
div.thumbnail-preview a:visited img.thumbnail-preview[title*="animated_gif"],
div.thumbnail-preview a:visited img.thumbnail-preview[title*="animated_png"],
div.thumbnail-preview a:visited img.thumbnail-preview[title*="animated "]:not(.webm),
div.thumbnail-preview a.visited img.thumbnail-preview[title*="animated_gif"],
div.thumbnail-preview a.visited img.thumbnail-preview[title*="animated_png"],
div.thumbnail-preview a.visited img.thumbnail-preview[title*="animated "]:not(.webm) {
outline-color: ` + gifVisitedColor + `;
}
div.thumbnail-preview a:focus img.thumbnail-preview:not(.webm) {
outline-color: #FFA726 !important;
}
div.thumbnail-preview a:focus img.thumbnail-preview.webm {
border-color: #FFA726 !important;
}
`;
document.head.appendChild(css);
}
} else if (page === 'pool' && s === 'show') { // Pool page
// Get image thumbnails area
const galleryContainer = document.querySelector('.thumbnail-container');
if (!!galleryContainer) {
// Get all image thumbnail links
const galleryLinks = galleryContainer.querySelectorAll('span a[href*="page=post&s=view"]');
if (!!galleryLinks) {
markVisitedLinks(galleryLinks);
}
}
// Apply borders
const css = document.createElement('style');
css.innerHTML = `
div.thumbnail-container a img {
outline: 3px solid ` + imgUnvisitedColor + `;
background-color: #FFFFFF;
}
div.thumbnail-container a:visited img,
div.thumbnail-container a.visited img {
outline-color: ` + imgVisitedColor + `;
}
div.thumbnail-container a:not(.visited):visited img {
background-color: #9E9E9E;
}
div.thumbnail-container a img[title*=" webm "] {
outline: 5px solid ` + webmUnvisitedColor + ` !important;
}
div.thumbnail-container a:visited img[title*=" webm "],
div.thumbnail-container a.visited img[title*=" webm "] {
outline-color: ` + webmVisitedColor + ` !important;
}
div.thumbnail-container a img[title*="animated_gif"],
div.thumbnail-container a img[title*="animated_png"],
div.thumbnail-container a img[title*="animated "]:not([title*=" webm "]) {
outline-color: ` + gifUnvisitedColor + `;
}
div.thumbnail-container a:visited img[title*="animated_gif"],
div.thumbnail-container a:visited img[title*="animated_png"],
div.thumbnail-container a:visited img[title*="animated "]:not([title*=" webm "]),
div.thumbnail-container a.visited img[title*="animated_gif"],
div.thumbnail-container a.visited img[title*="animated_png"],
div.thumbnail-container a.visited img[title*="animated "]:not([title*=" webm "]) {
outline-color: ` + gifVisitedColor + `;
}
div.thumbnail-container a:focus img {
outline-color: #FFA726 !important;
}
`;
document.head.appendChild(css);
} else if (page === 'favorites' && s === 'view') { // Favorites page
// Apply borders
const css = document.createElement('style');
css.innerHTML = `
.thumb {
margin: 5px;
}
.thumb a img {
padding: 5px;
margin: 5px;
}
.thumb a img[title*="animated_gif"],
.thumb a img[title*="animated_png"],
.thumb a img[title*="animated "]:not([title*=" webm"]) {
outline: 3px solid ` + gifUnvisitedColor + `;
}
.thumb a img[title*=" webm"] {
outline: 5px solid ` + webmUnvisitedColor + `;
}
`;
const userID = displayCurrentUserFavoritesVisited ? -1 : getUserID();
if (searchParams.has('id') && parseInt(searchParams.get('id')) != userID) {
// Get list of visited images
const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
// Mark visited links
if (visitedIDs.length > 0) {
const galleryLinks = document.querySelectorAll('.thumb a[href*="page=post&s=view"]');
markVisitedLinks(galleryLinks);
}
css.innerHTML += `
.thumb a img {
outline: 3px solid ` + imgUnvisitedColor + `;
background-color: #FFFFFF;
}
.thumb a:visited img,
.thumb a.visited img {
outline-color: ` + imgVisitedColor + `;
}
.thumb a:not(.visited):visited img {
background-color: #9E9E9E;
}
.thumb a:visited img[title*=" webm"],
.thumb a.visited img[title*=" webm"] {
outline-color: ` + webmVisitedColor + `;
}
.thumb a:visited img[title*="animated_gif"],
.thumb a:visited img[title*="animated_png"],
.thumb a:visited img[title*="animated "]:not([title*=" webm"]),
.thumb a.visited img[title*="animated_gif"],
.thumb a.visited img[title*="animated_png"],
.thumb a.visited img[title*="animated "]:not([title*=" webm"]) {
outline-color: ` + gifVisitedColor + `;
}
`;
}
document.head.appendChild(css);
} else if (page === 'tags' && s === 'saved_search') { // Saved Searches page
/// Mark visited links
const galleryLinks = document.querySelectorAll('.container-fluid > .thumb a[href*="page=post&s=view"]');
markVisitedLinks(galleryLinks);
// Apply borders
const css = document.createElement('style');
css.innerHTML = `
.container-fluid > .thumb a .thumbnail-preview {
outline: 3px solid ` + imgUnvisitedColor + `;
background-color: #FFFFFF;
}
.container-fluid > .thumb a:visited .thumbnail-preview,
.container-fluid > .thumb a.visited .thumbnail-preview {
outline-color: ` + imgVisitedColor + `;
}
.container-fluid > .thumb a:not(.visited):visited .thumbnail-preview {
background-color: #9E9E9E;
}
.container-fluid > .thumb a .thumbnail-preview[alt*="animated_gif"],
.container-fluid > .thumb a .thumbnail-preview[alt*="animated_png"],
.container-fluid > .thumb a .thumbnail-preview[alt*="animated "]:not([alt*=" webm"]) {
outline: 3px solid ` + gifUnvisitedColor + `;
}
.container-fluid > .thumb a .thumbnail-preview[alt*=" webm"] {
outline: 5px solid ` + webmUnvisitedColor + `;
margin: 5px 7px;
}
.container-fluid > .thumb a:visited .thumbnail-preview[alt*=" webm"],
.container-fluid > .thumb a.visited .thumbnail-preview[alt*=" webm"] {
outline-color: ` + webmVisitedColor + `;
}
.container-fluid > .thumb a:visited .thumbnail-preview[alt*="animated_gif"],
.container-fluid > .thumb a:visited .thumbnail-preview[alt*="animated_png"],
.container-fluid > .thumb a:visited .thumbnail-preview[alt*="animated "]:not([alt*=" webm"]),
.container-fluid > .thumb a.visited .thumbnail-preview[alt*="animated_gif"],
.container-fluid > .thumb a.visited .thumbnail-preview[alt*="animated_png"],
.container-fluid > .thumb a.visited .thumbnail-preview[alt*="animated "]:not([alt*=" webm"]) {
outline-color: ` + gifVisitedColor + `;
}
`;
document.head.appendChild(css);
} else if (page === 'wiki' && s === 'view') { // Wiki entry page
// Mark visited links
const galleryLinks = document.querySelectorAll('tr > td:nth-child(2) a[href*="page=post&s=view"]');
markVisitedLinks(galleryLinks);
// Apply borders
const css = document.createElement('style');
css.innerHTML = `
a .thumbnail-preview img {
padding: 5px;
outline: 3px solid ` + imgUnvisitedColor + `;
background-color: #FFFFFF;
}
a:visited .thumbnail-preview img,
a.visited .thumbnail-preview img {
outline-color: ` + imgVisitedColor + `;
}
a:not(.visited):visited .thumbnail-preview img {
background-color: #9E9E9E;
}
a .thumbnail-preview img[alt*="animated_gif"],
a .thumbnail-preview img[alt*="animated_png"],
a .thumbnail-preview img[alt*="animated "]:not([alt*=" webm"]) {
outline: 3px solid ` + gifUnvisitedColor + `;
}
a .thumbnail-preview img[alt*=" webm"] {
outline: 5px solid ` + webmUnvisitedColor + `;
margin: 5px 7px;
}
a:visited .thumbnail-preview img[alt*=" webm"],
a.visited .thumbnail-preview img[alt*=" webm"] {
outline-color: ` + webmVisitedColor + `;
}
a:visited .thumbnail-preview img[alt*="animated_gif"],
a:visited .thumbnail-preview img[alt*="animated_png"],
a:visited .thumbnail-preview img[alt*="animated "]:not([alt*=" webm"]),
a.visited .thumbnail-preview img[alt*="animated_gif"],
a.visited .thumbnail-preview img[alt*="animated_png"],
a.visited .thumbnail-preview img[alt*="animated "]:not([alt*=" webm"]) {
outline-color: ` + gifVisitedColor + `;
}
`;
document.head.appendChild(css);
} else if (page === 'account' && s === 'profile') { // Profile page
// Mark visited links
const galleryLinks = document.querySelectorAll('a[href*="page=post&s=view"]');
markVisitedLinks(galleryLinks);
// Apply borders
const css = document.createElement('style');
css.innerHTML = `
.profileThumbnailPadding {
max-width: none !important;
}
#statistics > span:last-child {
display: none;
}
a[href*="s=view"] img {
padding: 5px;
outline: 3px solid ` + imgUnvisitedColor + `;
max-height: 190px;
object-fit: scale-down;
background-color: #FFFFFF;
}
a[href*="s=view"]:visited img,
a[href*="s=view"].visited img {
outline-color: ` + imgVisitedColor + `;
}
a[href*="s=view"]:not(.visited):visited img {
background-color: #9E9E9E;
}
a[href*="s=view"] img[alt*="animated_gif"],
a[href*="s=view"] img[alt*="animated_png"],
a[href*="s=view"] img[alt*="animated "]:not([alt*=" webm"]) {
outline: 3px solid ` + gifUnvisitedColor + `;
}
a[href*="s=view"] img[alt*=" webm"] {
outline: 5px solid ` + webmUnvisitedColor + `;
margin: 5px 7px;
}
a[href*="s=view"]:visited img[alt*=" webm"],
a[href*="s=view"].visited img[alt*=" webm"] {
outline-color: ` + webmVisitedColor + `;
}
a[href*="s=view"]:visited img[alt*="animated_gif"],
a[href*="s=view"]:visited img[alt*="animated_png"],
a[href*="s=view"]:visited img[alt*="animated "]:not([alt*=" webm"]),
a[href*="s=view"].visited img[alt*="animated_gif"],
a[href*="s=view"].visited img[alt*="animated_png"],
a[href*="s=view"].visited img[alt*="animated "]:not([alt*=" webm"]) {
outline-color: ` + gifVisitedColor + `;
}
`;
document.head.appendChild(css);
}
createBackupInterface();
})();