// ==UserScript==
// @name Gelbooru Visited and Type Highlighter
// @namespace http://tampermonkey.net/
// @version 10.1.0
// @description Marks previously visited images on Gelbooru search pages, and extends animation highlighting from webm to also include gifs.
// @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 imgUnvistedColor = '#E1F5FE'; // Color for unvisted images
const imgVisitedColor = '#2E7D32'; // Color for visited images
const webmUnvistedColor = '#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 = true;
/*-------------------*/
function binarySearch(items, value, first, last) {
if (items.length === 0) {
return -1;
}
const middle = (last + first) >> 1;
if (last - first <= 1) {
return (value < items[middle]) ? middle - 1 : middle;
}
if (value === items[middle]) {
return middle;
}
if (value < items[middle]) {
return binarySearch(items, value, first, middle);
}
return binarySearch(items, value, middle, last);
}
// Tests whether value is in items
function truthyBinarySearch(items, value) {
if (items.length === 0 || value > items[items.length - 1] || value < items[0]) {
return false;
}
if (value === items[0] || value === items[items.length - 1]) {
return true;
}
const index = binarySearch(items, value, 0, items.length - 1);
return (items[index] === value);
}
// Inserts value in items if not already present, returns whether insertion took place
function binaryInsertion(items, value) {
if (items.length === 0 || value > items[items.length - 1]) {
items.push(value);
return true;
}
if (value < items[0]) {
items.unshift(value);
return true;
}
const index = binarySearch(items, value, 0, items.length - 1);
if (items[index] !== value) {
items.splice(index + 1, 0, value);
return true;
}
return false;
}
// Check if link is in visited list
function markIfVisited(galleryLink, visitedIDs) {
const linkURL = new URL(galleryLink.getAttribute('href'), window.location.href);
const linkSearchParams = new URLSearchParams(linkURL.search);
const id = parseInt(linkSearchParams.get('id'));
if (truthyBinarySearch(visitedIDs, id)) {
galleryLink.classList.add('visited');
}
}
// 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;
}
(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'));
// Get list of visited images
let visitedIDs;
function updateVisitedIDs(event) {
visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
if (binaryInsertion(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');
if (!!mltLinks) {
mltLinks.forEach(mltLink => markIfVisited(mltLink, visitedIDs));
}
const css = document.createElement('style');
css.innerHTML = css.innerHTML + `
a img.mltThumbs {
padding: 5px;
margin: 5px !important;
outline: 3px solid ` + imgUnvistedColor + `;
}
a:visited img.mltThumbs,
a.visited img.mltThumbs {
outline-color: ` + imgVisitedColor + `;
}
`;
document.head.appendChild(css);
} else if (s === 'list') { // Search page
// Get list of visited images
const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
if (visitedIDs.length > 0) {
// 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');
if (!!galleryLinks) {
galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
}
}
}
// Apply borders
const css = document.createElement('style');
css.innerHTML = `
div.thumbnail-preview {
background-color: transparent;
}
div.thumbnail-preview a img.thumbnail-preview:not(.webm) {
outline: 3px solid ` + imgUnvistedColor + `;
}
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: ` + webmUnvistedColor + ` !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 list of visited images
const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
if (visitedIDs.length > 0) {
// Get image thumbnails area
const galleryContainer = document.querySelector('.thumbnail-container');
if (!!galleryContainer) {
// Get all image thumbnail links
const galleryLinks = galleryContainer.querySelectorAll('span a');
if (!!galleryLinks) {
galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
}
}
}
// Apply borders
const css = document.createElement('style');
css.innerHTML = `
div.thumbnail-container a img {
outline: 3px solid ` + imgUnvistedColor + `;
}
div.thumbnail-container a:visited img,
div.thumbnail-container a.visited img {
outline-color: ` + imgVisitedColor + `;
}
div.thumbnail-container a img[title*=" webm "] {
outline: 5px solid ` + webmUnvistedColor + ` !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 ` + webmUnvistedColor + `;
}
`;
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"]');
galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
}
css.innerHTML += `
.thumb a img {
outline: 3px solid ` + imgUnvistedColor + `;
}
.thumb a:visited img,
.thumb a.visited img {
outline-color: ` + imgVisitedColor + `;
}
.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
// Get list of visited images
const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
// Mark visited links
if (visitedIDs.length > 0) {
const galleryLinks = document.querySelectorAll('.container-fluid > .thumb a');
galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
}
// Apply borders
const css = document.createElement('style');
css.innerHTML = `
.container-fluid > .thumb a .thumbnail-preview {
outline: 3px solid ` + imgUnvistedColor + `;
}
.container-fluid > .thumb a:visited .thumbnail-preview,
.container-fluid > .thumb a.visited .thumbnail-preview {
outline-color: ` + imgVisitedColor + `;
}
.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 ` + webmUnvistedColor + `;
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
// Get list of visited images
const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
// Mark visited links
if (visitedIDs.length > 0) {
const galleryLinks = document.querySelectorAll('tr > td:nth-child(2) a[href*="s=view"]');
galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
}
// Apply borders
const css = document.createElement('style');
css.innerHTML = `
a .thumbnail-preview img {
padding: 5px;
outline: 3px solid ` + imgUnvistedColor + `;
}
a:visited .thumbnail-preview img,
a.visited .thumbnail-preview img {
outline-color: ` + imgVisitedColor + `;
}
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 ` + webmUnvistedColor + `;
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
// Get list of visited images
const visitedIDs = JSON.parse(localStorage.getItem('visitedIDs')) || [];
// Mark visited links
if (visitedIDs.length > 0) {
const galleryLinks = document.querySelectorAll('a[href*="s=view"]');
galleryLinks.forEach(galleryLink => markIfVisited(galleryLink, visitedIDs));
}
// 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 ` + imgUnvistedColor + `;
max-height: 190px;
object-fit: scale-down;
}
a[href*="s=view"]:visited img,
a[href*="s=view"].visited img {
outline-color: ` + imgVisitedColor + `;
}
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 ` + webmUnvistedColor + `;
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);
}
})();