// ==UserScript==
// @name MAM Named Searches
// @namespace yyyzzz999
// @author yyyzzz999
// @version 1.3
// @description Tracks and saves search URLs with custom names. Replaces the H4 "Search" heading with the current name and adds a list button to manage saved searches in a popup.
// @license MIT
// @match https://www.myanonamouse.net/tor/browse.php*
// @icon https://cdn.myanonamouse.net/imagebucket/164109/GlassMouse.jpg
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_log
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// --- Configuration ---
const STORAGE_KEY = 'savedSearchUrls';
const TARGET_H4_TEXT = 'Search';
const PAGINATION_PARAM = 'tor[startNumber]';
var DEBUG =0; // Debugging mode on (1) or off (0)
if (DEBUG > 0) console.log('Starting MAM Named Searches');
// Theme Configuration
const MAIN_ELEMENT_ID = 'mainBody'; // ID of the container element to check background color
const THEME_MODE = 'auto'; // Options: 'light', 'dark', or 'auto'
// Global references
let searches = {};
let currentThemeColors = {}; // Holds the active color scheme
// --- Color Schemas ---
const COLOR_SCHEMES = {
light: {
// Buttons
nameBg: '#e6ffe6', nameText: '#107c10', nameBorder: '#107c10',
listBg: '#e6e6ff', listText: '#10107c', listBorder: '#10107c',
// Modal
modalBg: 'white', modalText: '#333', modalShadow: 'rgba(0, 0, 0, 0.25)',
overlayBg: 'rgba(0, 0, 0, 0.5)',
// Modal Items
navBg: '#d1e7dd', navText: '#0f5132', navBorder: '#0f5132',
delBg: '#f8d7da', delText: '#842029', delBorder: '#842029',
hoverBg1: '#ccffcc', hoverBg2: '#ccccff'
},
dark: {
// Buttons
nameBg: '#1c3e1c', nameText: '#b3ffb3', nameBorder: '#3cb371',
listBg: '#1c1c3e', listText: '#b3b3ff', listBorder: '#4169e1',
// Modal
modalBg: '#2d2d2d', modalText: 'white', modalShadow: 'rgba(255, 255, 255, 0.1)',
overlayBg: 'rgba(0, 0, 0, 0.75)',
// Modal Items
navBg: '#1f362f', navText: '#b3ffb3', navBorder: '#60c08b',
delBg: '#4a1f22', delText: '#ffb3b3', delBorder: '#ff5c6a',
hoverBg1: '#2e5a2e', hoverBg2: '#2e2e5a'
}
};
// --- Utility Functions for Theming ---
/**
* Converts a hex color string to an RGB array [r, g, b].
* @param {string} hex - The hex color string (e.g., "#RRGGBB").
* @returns {number[]} RGB components.
*/
function hexToRgb(hex) {
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16)
] : [0, 0, 0];
}
/**
* Converts an RGB color string (e.g., "rgb(r, g, b)") to an RGB array.
* @param {string} rgb - The rgb color string.
* @returns {number[]} RGB components.
*/
function rgbToRgb(rgb) {
const match = rgb.match(/\d+/g);
return match ? match.map(Number) : [0, 0, 0];
}
/**
* Calculates the relative luminance of an RGB color using the WCAG formula.
* @param {number[]} rgb - Array of [r, g, b] values (0-255).
* @returns {number} Relative luminance (0-1).
*/
function getRelativeLuminance(rgb) {
const R = rgb[0] / 255;
const G = rgb[1] / 255;
const B = rgb[2] / 255;
const components = [R, G, B].map(v => {
return (v <= 0.03928) ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
// Luminance formula (sRGB)
return 0.2126 * components[0] + 0.7152 * components[1] + 0.0722 * components[2];
}
/**
* Determines and sets the theme colors based on config and background luminance.
*/
function setDynamicTheme() {
if (THEME_MODE === 'light') {
currentThemeColors = COLOR_SCHEMES.light;
return;
}
if (THEME_MODE === 'dark') {
currentThemeColors = COLOR_SCHEMES.dark;
return;
}
// --- Auto Mode ---
const mainElement = document.getElementById(MAIN_ELEMENT_ID);
if (!mainElement) {
GM_log(`Element with ID "${MAIN_ELEMENT_ID}" not found. Defaulting to 'light' theme.`);
currentThemeColors = COLOR_SCHEMES.light;
return;
}
if (DEBUG > 0) console.log('Detecting background color');
const style = window.getComputedStyle(mainElement);
let bgColor = style.backgroundColor;
let rgb;
if (bgColor.startsWith('#')) {
rgb = hexToRgb(bgColor);
} else if (bgColor.startsWith('rgb')) {
rgb = rgbToRgb(bgColor);
} else {
// Default to white for unparseable or transparent backgrounds
GM_log('Could not parse background color. Defaulting to light theme.');
rgb = [255, 255, 255];
}
const luminance = getRelativeLuminance(rgb);
// Threshold: 0.179 for common WCAG contrast recommendations
if (DEBUG > 0) console.log('luminance: ', luminance);
const isDarkBackground = luminance < 0.179;
currentThemeColors = isDarkBackground ? COLOR_SCHEMES.dark : COLOR_SCHEMES.light;
GM_log(`Background Luminance: ${luminance.toFixed(3)}. Using ${isDarkBackground ? 'dark' : 'light'} theme.`);
}
// --- Core Script Functions ---
/**
* Retrieves the current base search URL by removing the pagination parameter.
* @returns {string} The unique base URL for the current search query.
*/
function getCurrentBaseUrl() {
try {
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
if (params.has(PAGINATION_PARAM)) {
params.delete(PAGINATION_PARAM);
}
let baseUrl = url.origin + url.pathname;
const queryString = params.toString();
if (queryString) {
baseUrl += '?' + queryString;
}
if (baseUrl.endsWith('?')) {
baseUrl = baseUrl.slice(0, -1);
}
return baseUrl;
} catch (e) {
GM_log('Error processing URL:', e);
return window.location.href;
}
}
/**
* Loads the saved searches map from Tampermonkey storage.
* @returns {Object<string, string>} A map of {name: url}.
*/
function loadSavedSearches() {
const savedData = GM_getValue(STORAGE_KEY, '{}');
try {
return JSON.parse(savedData);
} catch (e) {
GM_log('Error parsing saved search data:', e);
GM_setValue(STORAGE_KEY, '{}');
return {};
}
}
/**
* Finds the custom name associated with the current base URL, if any.
*/
function findCurrentSearchName(currentUrl, searchMap) {
for (const name in searchMap) {
if (searchMap[name] === currentUrl) {
return name;
}
}
return null;
}
/**
* Saves the current URL with a custom name to storage.
*/
function nameCurrentSearch() {
const currentUrl = getCurrentBaseUrl();
const customName = prompt('Enter a custom name for this search:');
if (customName) {
if (searches[customName]) {
if (!confirm(`A search named "${customName}" already exists. Do you want to overwrite it?`)) {
return;
}
}
searches[customName] = currentUrl;
GM_setValue(STORAGE_KEY, JSON.stringify(searches));
window.location.reload();
}
}
/**
* Deletes a search entry by name and reloads the modal content.
*/
function deleteSearch(name) {
if (confirm(`Are you sure you want to delete the search named "${name}"?`)) {
delete searches[name];
GM_setValue(STORAGE_KEY, JSON.stringify(searches));
showModal();
// Reload if the deleted search was the current one
if (findCurrentSearchName(getCurrentBaseUrl(), searches) === name) {
window.location.reload();
}
}
}
/**
* Closes the modal dialog.
*/
function closeModal() {
const modalOverlay = document.getElementById('gm-search-modal-overlay');
if (modalOverlay) {
modalOverlay.style.display = 'none';
}
}
/**
* Creates and displays the modal dialog with the list of saved searches.
*/
function showModal() {
searches = loadSavedSearches();
let modalOverlay = document.getElementById('gm-search-modal-overlay');
if (!modalOverlay) {
// Inject dynamic styles
const style = document.createElement('style');
style.textContent = `
.gm-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: ${currentThemeColors.overlayBg};
display: flex;
justify-content: center;
align-items: center;
z-index: 99999;
}
.gm-modal-content {
background: ${currentThemeColors.modalBg};
color: ${currentThemeColors.modalText};
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px ${currentThemeColors.modalShadow};
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
}
.gm-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.gm-modal-header h4 {
margin: 0;
color: ${currentThemeColors.modalText};
}
.gm-close-button {
background: none;
border: none;
font-size: 1.5em;
cursor: pointer;
line-height: 1;
padding: 0;
color: ${currentThemeColors.modalText};
}
.gm-search-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid ${currentThemeColors.modalText}20;
}
.gm-search-list-item:last-child {
border-bottom: none;
}
/* NEW: Styling for the clickable name */
.gm-search-name-link {
color: ${currentThemeColors.modalText};
font-weight: 500;
cursor: pointer;
flex-grow: 1;
padding-right: 15px;
}
.gm-search-name-link:hover {
text-decoration: underline;
opacity: 0.9;
}
.gm-search-list-item button {
padding: 2px 6px;
margin-left: 10px;
border-radius: 4px;
cursor: pointer;
font-size: 0.85em;
}
.gm-list-navigate {
background-color: ${currentThemeColors.navBg};
color: ${currentThemeColors.navText};
border: 1px solid ${currentThemeColors.navBorder};
}
.gm-list-delete {
background-color: ${currentThemeColors.delBg};
color: ${currentThemeColors.delText};
border: 1px solid ${currentThemeColors.delBorder};
}
.gm-search-list-item button:hover {
opacity: 0.85;
}
`;
document.head.appendChild(style);
// Create the overlay and content structure
modalOverlay = document.createElement('div');
modalOverlay.id = 'gm-search-modal-overlay';
modalOverlay.className = 'gm-modal-overlay';
modalOverlay.onclick = (e) => {
if (e.target === modalOverlay) {
closeModal();
}
};
const modalContent = document.createElement('div');
modalContent.className = 'gm-modal-content';
modalOverlay.appendChild(modalContent);
document.body.appendChild(modalOverlay);
}
const modalContent = modalOverlay.querySelector('.gm-modal-content');
modalContent.innerHTML = ''; // Clear existing content
// --- Build Header ---
const header = document.createElement('div');
header.className = 'gm-modal-header';
header.innerHTML = '<h4>Saved Searches</h4>';
const closeBtn = document.createElement('button');
closeBtn.className = 'gm-close-button';
closeBtn.innerHTML = '×';
closeBtn.onclick = closeModal;
header.appendChild(closeBtn);
modalContent.appendChild(header);
// --- Build List ---
const listDiv = document.createElement('div');
const searchNames = Object.keys(searches).sort();
if (searchNames.length === 0) {
listDiv.innerHTML = `<p style="color: ${currentThemeColors.modalText};">No saved searches yet. Click "Name this" to save the current search.</p>`;
} else {
searchNames.forEach(name => {
const item = document.createElement('div');
item.className = 'gm-search-list-item';
// **REVISED: Name Span is now clickable**
const nameSpan = document.createElement('span');
nameSpan.textContent = name;
nameSpan.className = 'gm-search-name-link';
// Click handler for the name and the Go button
const navigateAction = () => {
closeModal();
window.location.href = searches[name];
};
nameSpan.onclick = navigateAction;
item.appendChild(nameSpan);
const buttonGroup = document.createElement('div');
// Navigate Button (still exists, though redundant now)
const navigateBtn = document.createElement('button');
navigateBtn.textContent = 'Go';
navigateBtn.className = 'gm-list-navigate';
navigateBtn.onclick = navigateAction; // Uses the same action
buttonGroup.appendChild(navigateBtn);
// Delete Button
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.className = 'gm-list-delete';
deleteBtn.onclick = () => deleteSearch(name);
buttonGroup.appendChild(deleteBtn);
item.appendChild(buttonGroup);
listDiv.appendChild(item);
});
}
modalContent.appendChild(listDiv);
modalOverlay.style.display = 'flex';
}
/**
* Main function to initialize the UI modifications.
*/
function init() {
// 1. Determine the theme before any UI elements are created
setDynamicTheme();
// 2. Load data and identify current state
const currentUrl = getCurrentBaseUrl();
searches = loadSavedSearches();
const currentName = findCurrentSearchName(currentUrl, searches);
// 3. Find the target H4 element
const h4Elements = Array.from(document.querySelectorAll('h4'));
const targetH4 = h4Elements.find(h4 => h4.textContent.trim() === TARGET_H4_TEXT);
if (!targetH4) {
GM_log(`Could not find an <h4> element with content "${TARGET_H4_TEXT}". Script halted.`);
return;
}
const parent = targetH4.parentNode;
if (!parent) return;
// 4. Create the "Name this" button (Themed)
const nameButton = document.createElement('button');
nameButton.textContent = 'Name this';
nameButton.style.cssText = `
margin-right: 10px;
padding: 2px 8px;
height: fit-content;
border: 1px solid ${currentThemeColors.nameBorder};
background-color: ${currentThemeColors.nameBg};
color: ${currentThemeColors.nameText};
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 0.9em;
transition: background-color 0.2s;
`;
nameButton.onmouseover = () => nameButton.style.backgroundColor = currentThemeColors.hoverBg1;
nameButton.onmouseout = () => nameButton.style.backgroundColor = currentThemeColors.nameBg;
nameButton.onclick = nameCurrentSearch;
// 5. Create the "Saved List" button (Themed)
const savedListButton = document.createElement('button');
savedListButton.textContent = 'Saved List';
savedListButton.style.cssText = `
margin-left: 10px;
padding: 2px 8px;
height: fit-content;
border: 1px solid ${currentThemeColors.listBorder};
background-color: ${currentThemeColors.listBg};
color: ${currentThemeColors.listText};
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 0.9em;
transition: background-color 0.2s;
`;
savedListButton.onmouseover = () => savedListButton.style.backgroundColor = currentThemeColors.hoverBg2;
savedListButton.onmouseout = () => savedListButton.style.backgroundColor = currentThemeColors.listBg;
savedListButton.onclick = showModal;
// 6. Create a container for the new elements
const uiContainer = document.createElement('span');
uiContainer.style.display = 'flex';
uiContainer.style.alignItems = 'center';
// 7. Set the dynamic H4 text content
const searchNameText = document.createElement('span');
searchNameText.textContent = currentName || TARGET_H4_TEXT;
searchNameText.style.fontWeight = 'bold';
searchNameText.style.fontSize = '1.2em';
searchNameText.style.color = currentThemeColors.modalText; // Use the main text color
// 8. Inject all elements into the container
uiContainer.appendChild(nameButton);
uiContainer.appendChild(searchNameText);
if (Object.keys(searches).length > 0) {
uiContainer.appendChild(savedListButton);
}
// 9. Replace the original H4 element
targetH4.replaceWith(uiContainer);
}
// Run the initialization logic
init();
if (DEBUG > 0) console.log('MAM Named Searches is running');
})();