// ==UserScript==
// @name Watchlist for tags Rule34
// @namespace Notification New Content Rule34
// @match https://rule34.xxx/*
// @connect https://api.rule34.xxx/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_listValues
// @author Ardath
// @version 1.2.31
// @description Watchlist for tags on Rule34.xxx
// @license MIT
// ==/UserScript==
// How many tags to check per seconds
const MAX_TAGS_TO_CHECK_AT_ONCE = 5;
const TIME_BETWEEN_API_CALLS_MS = 2000;
// ==================================================================================================================
// ==================================================================================================================
// Add a 'Add To Watchlist' button under the search bar
function createAddToWatchlistButton() {
const targetDiv = document.querySelector('.tag-search');
const addToWatchlistButton = document.createElement('input');
addToWatchlistButton.id = 'addToWatchlist-button';
addToWatchlistButton.type = 'submit';
addToWatchlistButton.value = 'Add To Watchlist';
addToWatchlistButton.onclick = function() {
addToWatchlistButtonEvent();
}
if (targetDiv) {
targetDiv.appendChild(addToWatchlistButton);
}
}
// ==================================================================================================================
// Add content of search bar to the watchlist
function addToWatchlistButtonEvent() {
const searchInputField = document.querySelector('input[name="tags"]');
if (!searchInputField.value.trim()) {
searchInputField.style.border = '2px solid #cc0000';
return;
}
const currentTagsInput = searchInputField.value.trim().replace(/ /g, '+');
const tagKey = 'tag_' + currentTagsInput;
const keys = GM_listValues();
if (keys.includes(tagKey)) {
searchInputField.style.border = '2px solid #cc0000';
console.log('Current search input already watched: ', GM_getValue(tagKey));
} else {
fetchTagsAndCreatedAt(currentTagsInput).then(function(createdAt) {
if (!createdAt) {
GM_setValue(tagKey, JSON.stringify({ name: currentTagsInput, created_at: 'Fri Nov 12 19:06:29 +0100 2010', new_content: false, blacklisted: false, checkDisabled: false, galleryDisabled: false }));
} else {
GM_setValue(tagKey, JSON.stringify({ name: currentTagsInput, created_at: createdAt.created_at, new_content: false, blacklisted: false, checkDisabled: false, galleryDisabled: false }));
}
searchInputField.style.border = '2px solid #00dd00';
console.log('Current search input added to the watchlist');
});
}
}
// ==================================================================================================================
// ==================================================================================================================
// Add toggle button next to each tag
function addToggleFunctionalityToTags() {
const tagItems = document.querySelectorAll('#tag-sidebar li[class*="tag"]');
tagItems.forEach(function(tagItem) {
const toggleButton = document.createElement('a');
toggleButton.textContent = '[ ]';
toggleButton.href = '#';
tagItem.prepend(toggleButton);
const tagLink = Array.from(tagItem.querySelectorAll('a')).at(-1);
if (!tagLink) return;
const href = tagLink.getAttribute('href');
const params = new URLSearchParams(href.split('?')[1]);
const tagName = params.get('tags');
const storageKey = 'tag_' + tagName;
if (GM_getValue(storageKey, null)) {
toggleButton.textContent = '[X]';
}
toggleButton.onclick = function(event) {
event.preventDefault();
addTagToGMStorage(storageKey, toggleButton, tagName);
};
});
}
// ==================================================================================================================
// Add/Remove tags from GM storage when clicking on [ ]
function addTagToGMStorage(storageKey, toggleButton, tagName) {
if (GM_getValue(storageKey, null)) {
GM_deleteValue(storageKey);
toggleButton.textContent = '[ ]';
} else {
fetchTagsAndCreatedAt(tagName, true).then(function(createdAt) {
if (createdAt) {
GM_setValue(storageKey, JSON.stringify({ name: tagName, created_at: createdAt.created_at, new_content: false, blacklisted: false, checkDisabled: false, galleryDisabled: false }));
toggleButton.textContent = '[X]';
}
});
}
}
// ==================================================================================================================
// ==================================================================================================================
// Add a 'Watchlist' button to the navigation bar
function addWatchlistButtonToNav() {
const navbar = document.getElementById('subnavbar');
const listItem = document.createElement('li');
const watchlistButton = document.createElement('a');
watchlistButton.textContent = 'Watchlist';
watchlistButton.id = 'watchlist-button';
watchlistButton.style.padding = '2px';
watchlistButton.addEventListener('mouseup', (event) => {
if (event.button === 0 && !event.ctrlKey) { // Left click opens modal
watchlistButton.href = '#';
document.getElementById('watchlist-modal').style.display = 'block';
displayTagsInModal();
}
});
watchlistButton.addEventListener('mousedown', (event) => {
if (event.button === 1 || event.ctrlKey) { // Middle-click or Ctrl+click open Watchlist Gallery in new tab
watchlistButton.href = getWatchlistURL();
}
});
listItem.appendChild(watchlistButton);
navbar.appendChild(listItem);
}
// ==================================================================================================================
// Create the modal to display watched tags
function createWatchlistModal() {
const modalElement = document.createElement('div');
modalElement.id = 'watchlist-modal';
modalElement.className = 'modal';
const modalContentElement = document.createElement('div');
modalContentElement.className = 'modal-content';
const rows = ['row1', 'row2', 'row3', 'row4', 'row5'];
rows.forEach(id => {
const row = document.createElement('div');
row.id = id;
document.body.appendChild(row); // Or any desired parent element
});
const row3CheckboxWrapper = document.createElement('div');
const checkNewContentButton = document.createElement('button');
checkNewContentButton.id = 'check-new-content-button';
checkNewContentButton.textContent = 'Check for new content';
checkNewContentButton.onclick = checkForNewContent;
const cooldownDiv = document.createElement('div');
cooldownDiv.id = 'cooldown-div';
const cooldownLabel = document.createElement('p');
cooldownLabel.id = 'cooldown-label';
cooldownLabel.textContent = 'Cooldown : ';
const cooldownInput = document.createElement('input');
cooldownInput.id = 'cooldown-input';
cooldownInput.type = 'number';
cooldownInput.min = 0;
cooldownInput.value = GM_getValue('cooldown', '0');
cooldownInput.addEventListener('input', function() {
cooldownInput.value = cooldownInput.value.replace(/[^0-9]/g, '');
GM_setValue('cooldown', parseInt(cooldownInput.value));
});
cooldownDiv.append(cooldownLabel, cooldownInput);
const lastCheckedText = document.createElement('p');
lastCheckedText.id = 'last-checked-time';
lastCheckedText.textContent = 'Last checked: -';
const filterCheckbox = document.createElement('input');
filterCheckbox.type = 'checkbox';
filterCheckbox.id = 'show-new-tags';
filterCheckbox.checked = GM_getValue('showNewTags', false);
filterCheckbox.onchange = function() {
GM_setValue('showNewTags', filterCheckbox.checked);
displayTagsInModal();
};
const filterLabel = document.createElement('label');
filterLabel.textContent = 'Show new content';
filterLabel.style.color = '#c0c0c0';
filterLabel.setAttribute('for', filterCheckbox.id);
const clearButton = document.createElement('button');
clearButton.id = 'reset-button';
clearButton.textContent = 'Clear';
clearButton.onclick = clearWatchlist;
const hideFilterButton = document.createElement('button');
hideFilterButton.id = 'hide-filter-buttons';
hideFilterButton.textContent = '+';
hideFilterButton.onclick = function() {
if (row5.contains(filterCheckButton)) {
filterCheckButton.remove();
filterGalleryButton.remove();
hotkeySpan.remove();
cooldownDiv.remove();
GM_setValue('isFilterVisible', false);
filterEvents('none');
} else {
row5.append(filterCheckButton, filterGalleryButton, cooldownDiv, hotkeySpan);
GM_setValue('isFilterVisible', true);
filterEvents('inline-block');
}
};
const filterCheckButton = document.createElement('button');
filterCheckButton.id = 'filter-check-button';
filterCheckButton.textContent = '+/- Check';
filterCheckButton.onclick = function() { disableAllEvent('checkDisabled'); };
const filterGalleryButton = document.createElement('button');
filterGalleryButton.id = 'filter-gallery-button';
filterGalleryButton.textContent = '+/- Gallery';
filterGalleryButton.onclick = function() { disableAllEvent('galleryDisabled'); };
const hotkeySpan = document.createElement('Span');
hotkeySpan.id = 'hotkey-span';
hotkeySpan.title = "Redirect to tags with new content";
const hotkeyInput1 = document.createElement('select');
const options = ['ALT', 'CTRL', 'SHIFT'];
options.forEach(optionText => {
const option = document.createElement('option');
option.value = optionText;
option.textContent = optionText;
hotkeyInput1.appendChild(option);
});
hotkeyInput1.value = GM_getValue('hotkey1_watchlist', 'ALT');
hotkeyInput1.addEventListener('change', function() {
GM_setValue('hotkey1_watchlist', hotkeyInput1.value);
});
const hotkeyInput2 = document.createElement('input');
hotkeyInput2.id = 'hotkey-input';
hotkeyInput2.type = 'text';
hotkeyInput2.maxLength = 2;
hotkeyInput2.value = GM_getValue('hotkey2_watchlist', 'F2');
hotkeyInput2.addEventListener('input', function() {
hotkeyInput2.value = hotkeyInput2.value.trim().toUpperCase();
GM_setValue('hotkey2_watchlist', hotkeyInput2.value);
});
hotkeySpan.append(hotkeyInput1, hotkeyInput2);
let isFilterVisible = GM_getValue('isFilterVisible', false);
if (isFilterVisible) {
row5.append(filterCheckButton, filterGalleryButton, cooldownDiv, hotkeySpan);
filterEvents('inline-block');
} else {
filterEvents('none');
}
const watchlistGallery = document.createElement('button');
watchlistGallery.id = 'watchlist-gallery-button';
watchlistGallery.textContent = 'Watchlist Gallery';
watchlistGallery.addEventListener('mouseup', (event) => {
if (event.button === 0 && !event.ctrlKey) window.location.href = getWatchlistURL(); // Left-click
});
watchlistGallery.addEventListener('mousedown', (event) => {
if (event.button === 1 || event.ctrlKey) { // Middle-click or Ctrl+click
event.preventDefault();
window.open(getWatchlistURL(), '_blank');
}
});
const modalText = document.createElement('div');
modalText.id = 'modal-tags';
modalText.textContent = 'Tags';
row1.append(checkNewContentButton);
row2.append(lastCheckedText);
row3CheckboxWrapper.append(filterCheckbox, filterLabel);
row3.append(row3CheckboxWrapper, clearButton);
row4.append(hideFilterButton, watchlistGallery);
modalContentElement.append(row1, row2, row3, row4, row5, modalText);
modalElement.appendChild(modalContentElement);
document.body.appendChild(modalElement);
}
// Get watchlist gallery url
function getWatchlistURL() {
const tags = getTags(true);
const filteredTags = tags.filter(tag => tag.galleryDisabled == false);
const tagString = `( ${filteredTags.map(tag => '( ' + tag.name + ' )').join(' ~ ')} )`;
return `https://rule34.xxx/index.php?page=post&s=list&tags=${tagString}`;
}
// ==================================================================================================================
// Show / hide filter buttons
function filterEvents(state) {
['filter-buttons', 'remove-button'].forEach(className => {
document.querySelectorAll(`.${className}`).forEach(element => {
element.style.display = state;
});
});
}
// ==================================================================================================================
// Get tags from storage
function getTags(gallery=false) {
const filterNewTags = document.getElementById('show-new-tags').checked;
const tags = [];
// Get all "tag_" keys stored in GM
GM_listValues().forEach(function(key) {
if (!key.startsWith('tag_')) return;
const tagData = JSON.parse(GM_getValue(key, '{}'));
if (!gallery && filterNewTags && !tagData.new_content) return;
tags.push({
key,
...tagData
});
});
// Sort tags alphabetically by name
tags.sort((a, b) => a.name.localeCompare(b.name));
return tags;
}
// ==================================================================================================================
// Display stored tags in the modal
function displayTagsInModal() {
const modalContent = document.getElementById('modal-tags');
const row5 = document.getElementById('row5');
modalContent.innerHTML = '';
var filterState = row5.contains(document.getElementById('filter-check-button')) ? 'inline-display' : 'none';
// Create and append elements for each tag
getTags().forEach(tag => {
const tagUrl = 'https://rule34.xxx/index.php?page=post&s=list&tags=' + tag.name;
const tagElement = createTagElement(tagUrl, tag.name, tag.new_content, tag.key, filterState);
modalContent.appendChild(tagElement);
});
}
// ==================================================================================================================
// Create a tag element for the modal, with a remove button and disable toggle
function createTagElement(url, tagName, hasNewContent, storageKey, filterState='none') {
const tagElement = document.createElement('p');
// Retrieve stored data once to avoid redundancy
const tagData = JSON.parse(GM_getValue(storageKey, '{}')) || {};
const isBlacklisted = tagData.blacklisted || false;
const isCheckDisabled = tagData.checkDisabled || false;
const isGalleryDisabled = tagData.galleryDisabled || false;
// Create the link element
const linkElement = document.createElement('a');
linkElement.href = url;
linkElement.textContent = tagName.replace(/\+/g, ' ');
// Style the link based on conditions
linkElement.style.color = isBlacklisted
? hasNewContent ? '#ff8000' : 'white'
: hasNewContent ? '#00dd00' : 'white';
linkElement.style.fontWeight = hasNewContent ? 'bold' : '';
linkElement.onclick = function (event) {
markTagAsRead(storageKey);
};
// Create the remove button
const removeButton = document.createElement('button');
removeButton.className = 'remove-button';
removeButton.textContent = 'X';
removeButton.style.display = filterState;
removeButton.onclick = function () {
GM_deleteValue(storageKey);
displayTagsInModal();
};
const checkFilterButton = createFilterButton('checkDisabled', isCheckDisabled, storageKey, filterState)
const galleryFilterButton = createFilterButton('galleryDisabled', isGalleryDisabled, storageKey, filterState);
tagElement.append(checkFilterButton, galleryFilterButton, linkElement, removeButton);
return tagElement;
}
// ==================================================================================================================
// Turn on/off all the tags in the watchlist
function createFilterButton(property, state, storageKey, filter) {
const button = document.createElement('input');
button.type = 'checkbox';
button.className = 'filter-buttons';
button.checked = state ? false : true;
button.style.display = filter;
button.onchange = function() {
state = !state;
console.log('ok');
button.checked = state ? false : true;
const tagData = JSON.parse(GM_getValue(storageKey, '{}'));
tagData[property] = state;
GM_setValue(storageKey, JSON.stringify(tagData));
displayTagsInModal();
};
return button;
}
// ==================================================================================================================
// Turn on/off all the tags in the watchlist
function disableAllEvent(keyType) {
const tagKeys = GM_listValues().filter(key => key.startsWith('tag_'));
if (tagKeys.length === 0) return;
// Get the state of the first tag's `keyType` (either 'checkDisabled' or 'galleryDisabled')
const firstTagData = JSON.parse(GM_getValue(tagKeys[0], '{}'));
const newState = !(firstTagData[keyType] || false);
// Loop through all tags and update their `keyType` (either 'checkDisabled' or 'galleryDisabled')
tagKeys.forEach(key => {
const tagData = JSON.parse(GM_getValue(key, '{}'));
tagData[keyType] = newState;
GM_setValue(key, JSON.stringify(tagData));
});
displayTagsInModal();
}
// ==================================================================================================================
// Mark tag as read and redirect
function markTagAsRead(storageKey) {
const tagData = JSON.parse(GM_getValue(storageKey, '{}'));
tagData.new_content = false;
GM_setValue(storageKey, JSON.stringify(tagData));
displayTagsInModal();
}
// ==================================================================================================================
// Redirect to new content when pressing hotkey
function hotkeyEvent() {
const tagKeys = GM_listValues().filter(key => key.startsWith('tag_'));
for (const key of tagKeys) {
const tagData = JSON.parse(GM_getValue(key, '{}'));
if (tagData.new_content) {
const url = `https://rule34.xxx/index.php?page=post&s=list&tags=${tagData.name}`;
markTagAsRead(key)
window.location.href = url;
return;
}
}
}
// ==================================================================================================================
// Mark all tags as read
function clearWatchlist() {
GM_listValues().forEach(function (key) {
if (key.startsWith('tag_')) {
const tagData = JSON.parse(GM_getValue(key, '{}'));
tagData.new_content = false;
GM_setValue(key, JSON.stringify(tagData));
}
});
displayTagsInModal();
updateWatchlistButton();
}
// ==================================================================================================================
// ==================================================================================================================
// Main function. Check each tag one by one for new content
async function checkForNewContent() {
const checkButton = document.getElementById('check-new-content-button');
checkButton.disabled = true;
GM_setValue('lastCheckTime', Date.now());
updateLastCheckedTimeDisplay();
const tagKeys = GM_listValues().filter(key => key.startsWith('tag_'));
const totalTags = tagKeys.length;
let checkedTags = 0;
const promises = [];
for (const key of tagKeys) {
const tagData = JSON.parse(GM_getValue(key, '{}'));
const cooldown = GM_getValue('cooldown', 0);
checkedTags++;
checkButton.textContent = `Checking for new content... [${checkedTags} out of ${totalTags}]`;
if (tagData.checkDisabled) {
console.log("Tag disabled : " , tagData.name);
continue;
}
// Cooldown in case 'Checking for new content' is interrupted
if (tagData.cooldown && Math.floor((Date.now() - tagData.cooldown) / 1000) < cooldown * 60) {
console.log("Not enough time has elapsed");
continue;
}
// If tag is removed while 'Checking for new content'
if (!tagData.name) {
checkedTags--;
console.log("Invalid tag data");
continue;
}
// If tag alread has new content
if (tagData.new_content && !tagData.blacklisted) {
console.log('New content found for : ' + tagData.name);
continue;
}
promises.push((async () => {
try {
const contentData = await fetchTagsAndCreatedAt(tagData.name);
if (contentData && new Date(contentData.created_at) > new Date(tagData.created_at)) {
if (!containsBlacklistedTags(contentData.tags, fetchBlacklist())) {
tagData.blacklisted = false;
console.log('New content found for : ' + tagData.name);
} else {
tagData.blacklisted = true;
console.log('Blacklisted content found for : ' + tagData.name);
}
console.log('New content found for : ' + tagData.name);
tagData.created_at = contentData.created_at;
tagData.new_content = true;
tagData.cooldown = Date.now();
GM_setValue(key, JSON.stringify(tagData));
displayTagsInModal();
} else {
tagData.cooldown = Date.now();
GM_setValue(key, JSON.stringify(tagData));
console.log('No new content found for : ' + tagData.name);
}
} catch (error) {
console.error(`Error processing tag ${key}:`, error);
}
})());
// Throttle the updates at 1 API calls every 2 seconds by default
if (promises.length >= MAX_TAGS_TO_CHECK_AT_ONCE) {
await Promise.all(promises);
promises.length = 0;
await new Promise(resolve => setTimeout(resolve, TIME_BETWEEN_API_CALLS_MS));
}
}
updateWatchlistButton();
checkButton.textContent = `Check completed [${checkedTags} out of ${totalTags}]`;
checkButton.disabled = false;
}
// ==================================================================================================================
// Fetch all tags and the date of the most recent content for a specific tag
async function fetchTagsAndCreatedAt(tagName) {
const apiUrl = `https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&tags=${tagName}&limit=1`;
try {
const response = await fetch(apiUrl);
const data = await response.text();
const xmlDoc = new DOMParser().parseFromString(data, "text/xml");
const firstPost = xmlDoc.getElementsByTagName("post")[0];
if (firstPost) {
const tags = firstPost.getAttribute("tags")
.split(' ')
.map(tag => tag.trim())
.filter(tag => tag !== "");
const createdAt = firstPost.getAttribute("created_at");
return { tags, created_at: createdAt };
} else {
return null;
}
} catch (error) {
console.error('Error fetching content data:', error);
return null;
}
}
// ==================================================================================================================
function fetchBlacklist() {
let cookieValue = document.cookie
.split('; ')
.find(row => row.startsWith('tag_blacklist='));
// Parse the cookie value if it exists
let blacklist = [];
if (cookieValue) {
try {
blacklist = cookieValue
.split('=')[1] // Get the value after '='
.split('%2520') // Split by the encoded delimiter
.flatMap(tags => tags.split(/[\s,]+/)) // Split further by whitespace or commas
.map(tag => tag.trim()) // Trim each tag
.filter(tag => tag); // Remove empty or falsy values
if (JSON.stringify(blacklist) !== JSON.stringify(GM_getValue('blacklisted'))) {
GM_setValue('blacklisted', blacklist);
console.log('Blacklist updated in GM storage');
}
} catch (error) {
console.error('Error decoding cookie value:', error);
}
}
// Fallback to GM storage if the cookie's blacklist is empty
if (blacklist.length === 0) {
console.log("Couldn't find tag_blacklist cookie");
if (GM_getValue('blacklisted') && GM_getValue('blacklisted').length > 0) {
blacklist = GM_getValue('blacklisted');
console.log('Blacklist retrieved from GM storage');
}
}
console.log(`Blacklist[${blacklist.length}]: ` + blacklist);
return blacklist;
}
// ==================================================================================================================
// Check if any blacklisted tag is present in the content tags
function containsBlacklistedTags(contentTags, blacklistedTags) {
return contentTags.some(tag => blacklistedTags.includes(tag));
}
// ==================================================================================================================
// Update watchlist button appearance based on new content
function updateWatchlistButton() {
const watchlistButton = document.getElementById('watchlist-button');
const hasNewContent = GM_listValues().some(function(key) {
return key.startsWith('tag_') && JSON.parse(GM_getValue(key, '{}')).new_content === true;
});
watchlistButton.style.backgroundColor = hasNewContent ? '#aa0000' : '';
watchlistButton.style.color = hasNewContent ? '#ffffff' : '';
}
// ==================================================================================================================
// Update the last checked time display
function updateLastCheckedTimeDisplay() {
const lastCheckedText = document.getElementById('last-checked-time');
const lastCheckedTime = GM_getValue('lastCheckTime', 0);
if (lastCheckedTime) {
let minutesAgo = Math.floor((Date.now() - lastCheckedTime) / (1000 * 60));
minutesAgo = minutesAgo >= 60 ? Math.floor(minutesAgo / 60) + ' hour(s) ago' : minutesAgo + ' minute(s) ago';
lastCheckedText.textContent = 'Last checked: ' + minutesAgo;
}
}
// ==================================================================================================================
// ==================================================================================================================
addWatchlistButtonToNav();
createAddToWatchlistButton();
createWatchlistModal();
addToggleFunctionalityToTags();
updateWatchlistButton();
updateLastCheckedTimeDisplay();
window.onclick = function(event) {
// Close watchlist
const watchlistModal = document.getElementById('watchlist-modal')
if (event.target === watchlistModal) watchlistModal.style.display = 'none';
// Reset search bar border
const inputField = document.querySelector('input[name="tags"]');
if (inputField && event.target != document.getElementById('addToWatchlist-button')) {
if (inputField.style.border === '2px solid rgb(204, 0, 0)' || inputField.style.border === '2px solid rgb(0, 221, 0)') {
inputField.style.border = '1px solid #505a50';
}
}
};
// Use keyboard hotkey to go through new content
document.addEventListener("keydown", function(event) {
let hotkey1 = GM_getValue('hotkey1_watchlist', 'ALT').toLowerCase();
let hotkey2 = GM_getValue('hotkey2_watchlist', 'F2');
if (event[`${hotkey1}Key`] && event.key.toUpperCase() === hotkey2) {
event.preventDefault();
hotkeyEvent();
}
});
// ==================================================================================================================
// ==================================================================================================================
//
// ===========
// == Style ==
// ===========
const styleElement = document.createElement('style');
styleElement.type = 'text/css';
styleElement.innerHTML = `
.modal {
background-color: rgba(0, 0, 0, 0.5);
display: none;
height: 100%;
left: 0;
position: fixed;
top: 0;
width: 100%;
z-index: 1;
}
.modal-content {
background-color: #293129;
border: 1px solid #888;
min-height: 50%;
max-height: 75%;
margin: 7% auto;
overflow: auto;
padding: 20px;
min-width: 20%;
max-width: 90%;
white-space: nowrap;
width: fit-content;
}
@media only screen and (max-width: 810px) {
.modal-content {
min-height: 90%;
margin: 10% auto;
padding: 15px;
min-width: 90%;
width: 90%;
}
input[class="filter-buttons"] {
width: 10px;
height: 10px;
}
#addToWatchlist-button {
display: block;
margin: 0 auto;
margin-top: 15px;
width: 100% !important;
}
}
.modal-content p {
font-size: 15px;
}
.modal-content a:hover {
text-shadow: 0px 1px 0px;
}
.modal button {
background-color: #93b393;
color: #303a30;
font-weight: bold;
border: none;
}
#header #subnavbar li a:hover {
color: white !important;
}
#watchlist-button {
cursor: pointer;
}
#row1 {
align-items: center;
display: flex;
justify-content: center;
margin-bottom: 15px;
}
#row2 {
align-items: center;
display: flex;
justify-content: flex-end;
margin: 5px 0;
}
#row3 {
align-items: center;
display: flex;
justify-content: space-between;
margin-bottom: 0;
}
#row4 {
align-items: stretch;
display: flex;
justify-content: space-between;
margin-bottom: 25px;
}
#row5 {
margin-bottom: 35px;
}
button[id="check-new-content-button"] {
cursor: pointer;
display: block;
margin: 0 auto;
}
#cooldown-label {
color: #c0c0c0;
display: inline-block;
margin: 10px 0;
margin-right: 10px;
}
#cooldown-input, #cooldown-input:focus {
background-color: #303030;
border: 1px solid #505a50;
color: white;
display: inline-block;
outline: none;
width: 15%;
}
#last-checked-time {
color: #c0c0c0;
}
#show-new-tags {
accent-color: #293129;
box-shadow: 0px 0px 1px white;
margin-right: 10px;
}
label[for="show-new-tags"] {
color: white;
cursor: pointer;
}
button[id="reset-button"] {
cursor: pointer;
}
button[id="filter-check-button"] {
cursor: pointer;
display: block;
margin-top: 20px;
}
button[id="filter-gallery-button"] {
cursor: pointer;
display: block;
margin-top: 10px;
}
#hotkey-span select,
#hotkey-span input {
background-color: #303030;
border: 1px solid #505a50;
color: white;
font-weight: bold;
outline: none;
padding: 3px;
text-align: center;
}
input[id="hotkey-input"] {
width: 30px;
}
button[id="hide-filter-buttons"] {
cursor: pointer;
display: block;
margin-top: 20px;
}
button[id="watchlist-gallery-button"] {
cursor: pointer;
display: block;
float: right;
margin-top: 20px;
}
input[class="filter-buttons"] {
accent-color: #293129;
cursor: pointer;
margin-right: 15px;
box-shadow: 0px 0px 1px white;
}
button[class="remove-button"] {
background: none;
border: none;
color: #cc0000;
cursor: pointer;
font-weight: bold;
margin-left: 10px;
}
#addToWatchlist-button {
display = inline-block;
margin: 0 auto;
}
`;
document.head.appendChild(styleElement);