Bird Species Image Preview

Show Flickr images when hovering over bird species names (IOC nomenclature) on a webpage.

// ==UserScript==
// @name         Bird Species Image Preview
// @namespace    http://tampermonkey.net/
// @version      3.2.8
// @description  Show Flickr images when hovering over bird species names (IOC nomenclature) on a webpage.
// @author       Isidro Vila Verde
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @connect      api.flickr.com
// @connect      api.github.com
// @connect      flickr.com
// @connect      live.staticflickr.com
// @run-at       document-end
// ==/UserScript==

(async function () {
    'use strict';

    // Generate a random postfix for IDs to avoid conflicts
    const randomPostfix = `_${Math.random().toString(36).substring(2, 9)}`; // e.g., "_a1b2c3d"

    // Constants and Configurations
    const FLICKR_API_KEY = 'c161f42fac23abc42328d8abd9f14fc5';
    const GISTID = ['70598ae6bef6da21ade780c12d907452','d31217a5c802d1018893907df29d1d45','01ce512accba1ebd38be9f6b301e437c'];
    const API_CALL_DELAY = 300; // 300 milliseconds delay between API calls
    const DEBOUNCE_DELAY = 300; // Debounce delay for mouseover events
    const highlightColor = GM_getValue('highlightColor', 'yellow')
    let speciesList = GM_getValue('speciesList', []);
    // Default sort order
    let currentSortOrder = GM_getValue('flickrSortOrder', 'interestingness-desc');
    // Default search mode
    let currentSearchMode = GM_getValue('flickrSearchMode', 'tags');


    // State Variables
    let currentIndex = 0;
    let currentImages = [];
    let popupVisible = false;
    let observer;
    let observerActive = false;
    let lastApiCallTime = 0;
    let cursorPosition = { x: 0, y: 0 };
    let isKeydownListenerAdded = false;

    console.log('Script loaded.');

    // Initialize the script
    async function initializeScript() {
        console.log('Initializing script.');

        // Try to load speciesList from GitHub Gists
        if (speciesList.length === 0) {
            let mergedSpeciesList = [];

            for (const gistId of GISTID) { // Iterate over multiple Gist IDs
                console.log(`Get names from gist ${gistId}`);
                const gistUrl = `https://api.github.com/gists/${gistId}`;
                try {
                    const gistData = await gmFetch(gistUrl); // Reuse gmFetch function
                    const speciesSubset = loadAndMergeSpeciesLists(gistData); // Load, merge, sort, and deduplicate
                    mergedSpeciesList.push(...speciesSubset);
                } catch (error) {
                    console.warn(`Failed to load or process species list from Gist ${gistId}:`, error);
                }
            }

            // Remove duplicates and sort
            speciesList = [...new Set(mergedSpeciesList)].sort((a, b) => a.localeCompare(b));
            console.log(`Species list loaded and processed: ${speciesList.length} unique names`);

            // Cache the speciesList
            GM_setValue('speciesList', speciesList);
        }
    }

    // Function to load, merge, sort, and deduplicate species lists from Gist files
    function loadAndMergeSpeciesLists(gistData) {
        const mergedList = [];

        // Iterate over all files in the Gist
        for (const file of Object.values(gistData.files)) {
            // Check if the file name matches 'birdnames'
            if (file.filename.includes('birdnames')) {
                try {
                    // Parse the file content as JSON and add to the merged list
                    const content = JSON.parse(file.content);
                    if (Array.isArray(content)) {
                        mergedList.push(...content);
                    } else {
                        console.warn(`File ${file.filename} does not contain a valid array.`);
                    }
                } catch (error) {
                    console.error(`Error parsing file ${file.filename}:`, error);
                }
            }
        }

        return mergedList;
    }


    // Add style for add-on
    function add_style() {
        console.log('Adding styles.');
        GM_addStyle(`
            .bird-popup {
                position: fixed;
                z-index: 10000;
                background: #121212; /* Dark background */
                border: 1px solid #333; /* Dark border */
                border-radius: 10px;
                padding: 10px;
                box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.5);
                max-width: 80vw;
                display: none;
                text-align: center;
                transition: opacity 0.3s ease;
                resize: both;
                overflow: auto;
                color: #ffffff; /* Light text color */
            }

            /* Ensure all child elements inherit the dark mode styles */
            .bird-popup * {
                color: inherit; /* Inherit light text color */
                background-color: transparent; /* Transparent background for child elements */
                border-color: #555; /* Dark border for child elements */
            }

            /* Style buttons for dark mode */
            .bird-popup button {
                background-color: #333; /* Dark background for buttons */
                color: #ffffff; /* Light text color for buttons */
                border: 1px solid #555; /* Dark border for buttons */
                padding: 5px 10px;
                border-radius: 5px;
                cursor: pointer;
            }

            .bird-popup button:hover {
                background-color: #444; /* Slightly lighter background for hovered buttons */
            }

            .bird-popup img {
                max-width: 100%;
                height: auto;
                display: block;
                margin: auto;
            }

            .bird-highlight {
                background-color: ${highlightColor};
                cursor: pointer;
            }

            .bird-popup .nav-button {
                position: absolute;
                top: 50%;
                transform: translateY(-50%);
                font-size: 24px;
                background: rgba(0, 0, 0, 0.5);
                color: white;
                border: none;
                padding: 10px;
                cursor: pointer;
                user-select: none;
            }

            .bird-popup .nav-button:hover {
                background: rgba(0, 0, 0, 0.8);
            }

            #prev-button${randomPostfix} { left: 10px; }
            #next-button${randomPostfix} { right: 10px; }

            .dismiss-button {
                display: block;
                margin: 10px auto;
                padding: 5px 10px;
                background-color: red;
                color: white;
                cursor: pointer;
                border: none;
                border-radius: 5px;
            }

            .loading-indicator {
                font-size: 18px;
                color: #333;
                padding: 20px;
            }

            .photo-info {
                margin: 10px 0;
                font-size: 14px;
                color: #555;
            }
        `);
    }

    // Initialize popup element
    function initializePopup() {
        console.log('Initializing popup.');
        const popup = document.createElement('div');
        popup.id = `bird-popup${randomPostfix}`; // Add random postfix to ID
        popup.className = 'bird-popup';
        popup.setAttribute('role', 'dialog');
        popup.setAttribute('aria-labelledby', 'popup-title');
        document.body.appendChild(popup);
        return popup;
    }

    // Show popup with images
    function showPopup(e, imageData) {
        if (imageData.length === 0) {
            console.log('No images to display.');
            return;
        }

        currentImages = imageData;
        currentIndex = 0;

        let popup = document.getElementById(`bird-popup${randomPostfix}`); // Add random postfix to ID
        if (!popup) {
            popup = initializePopup();
        }

        popup.innerHTML = `<div class="loading-indicator">Loading...</div>`;
        popup.style.display = 'block';

        pauseObserver();
        popup.innerHTML = `
            <button class="nav-button" id="prev-button${randomPostfix}">&lsaquo;</button>
            <img src="${currentImages[currentIndex].url}" alt="Bird Image" />
            <button class="nav-button" id="next-button${randomPostfix}">&rsaquo;</button>
            <div class="photo-info">
                <strong>${currentImages[currentIndex].title}</strong> by ${currentImages[currentIndex].author || "Loading author..."}
            </div>
            <a href="${currentImages[currentIndex].flickrPage}" target="_blank">View on Flickr</a>
            <button class="dismiss-button" id="dismiss-button${randomPostfix}">Close</button>
        `;
        resumeObserver();

        cursorPosition = { x: e.clientX, y: e.clientY };

        const img = popup.querySelector('img');
        if (img.complete) {
            positionPopup(popup);
        } else {
            img.addEventListener('load', () => positionPopup(popup));
        }

        setupNavigation(popup);
        updateNavigationButtons();
        popupVisible = true;

        window.addEventListener('scroll', () => positionPopup(popup), { passive: true });
        window.addEventListener('resize', () => positionPopup(popup), { passive: true });
    }

    // Position popup relative to viewport with scroll handling
    function positionPopup(popup) {
        console.log('Positioning popup.');
        // const popupWidth = popup.offsetWidth;
        // const popupHeight = popup.offsetHeight;
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;
        const scrollY = window.scrollY || window.pageYOffset || document.documentElement.scrollTop;

        const cursorX = cursorPosition.x;
        const cursorY = cursorPosition.y + scrollY;

        const spaceBelow = viewportHeight + scrollY - cursorY;
        const spaceAbove = cursorY - scrollY;

        const spaceLeft = cursorX;
        const spaceRigth = viewportWidth - cursorX;

        pauseObserver();
        if (spaceBelow <= spaceAbove) {
            const top = 10;
            popup.style.top = `${top}px`;
            popup.style.removeProperty('bottom');
        } else {
            const bottom = 10;
            popup.style.bottom = `${bottom}px`;
            popup.style.removeProperty('top');
        }

        if (spaceRigth <= spaceLeft) {
            const left = 10;
            popup.style.left = `${left}px`;
            if (popup.style.right) popup.style.removeProperty('right');
        } else {
            const right = 10;
            popup.style.right = `${right}px`;
            popup.style.removeProperty('left');
        }
        resumeObserver();
    }

    // Setup navigation buttons
    function setupNavigation(popup) {
        console.log('Setting up navigation buttons.');

        // Button event listeners
        document.getElementById(`prev-button${randomPostfix}`).addEventListener('click', () => navigate(-1));
        document.getElementById(`next-button${randomPostfix}`).addEventListener('click', () => navigate(1));
        document.getElementById(`dismiss-button${randomPostfix}`).addEventListener('click', hidePopup);

        // Keyboard event listeners
        if (!isKeydownListenerAdded) {
            document.addEventListener('keydown', function (e) {
                if (popupVisible) {
                    if (e.key === 'ArrowLeft') {
                        navigate(-1);
                    } else if (e.key === 'ArrowRight') {
                        navigate(1);
                    } else if (e.key === 'Escape') {
                        hidePopup();
                    }
                }
            });
            isKeydownListenerAdded = true;
        }

        // Touch event listeners for swipe gestures
        popup.addEventListener('touchstart', handleTouchStart, false);
        popup.addEventListener('touchmove', handleTouchMove, false);
        popup.addEventListener('touchend', handleTouchEnd, false);
    }

    // Variables to track touch positions
    let touchStartX = null;
    let touchStartY = null;
    // Handle touch start event
    function handleTouchStart(event) {
        const firstTouch = event.touches[0];
        touchStartX = firstTouch.clientX;
        touchStartY = firstTouch.clientY;
    }

    // Handle touch move event
    function handleTouchMove(event) {
        if (!touchStartX || !touchStartY) return;

        const touchEndX = event.touches[0].clientX;
        const touchEndY = event.touches[0].clientY;

        const deltaX = touchEndX - touchStartX;
        const deltaY = touchEndY - touchStartY;

        // Determine if the movement is primarily horizontal
        if (Math.abs(deltaX) > Math.abs(deltaY)) {
            // Prevent vertical scrolling during horizontal swipe
            event.preventDefault();
        }
    }

    // Handle touch end event
    function handleTouchEnd(event) {
        if (!touchStartX || !touchStartY) return;

        const touchEndX = event.changedTouches[0].clientX;
        const touchEndY = event.changedTouches[0].clientY;

        const deltaX = touchEndX - touchStartX;
        const deltaY = touchEndY - touchStartY;

        // Define a threshold for swipe detection (e.g., 50 pixels)
        const swipeThreshold = 50;

        // Check if the swipe is horizontal and exceeds the threshold
        if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > swipeThreshold) {
            if (deltaX > 0) {
                // Swipe right -> navigate to the previous photo
                navigate(-1);
            } else {
                // Swipe left -> navigate to the next photo
                navigate(1);
            }
        }

        // Reset touch coordinates
        touchStartX = null;
        touchStartY = null;
    }

    // Navigate between images
    function navigate(direction) {
        currentIndex = Math.max(0, Math.min(currentImages.length - 1, currentIndex + direction));
        console.log(`Navigating to image index: ${currentIndex}`);
        updateImage();
        updateNavigationButtons();
    }

    // Update displayed image and info
    function updateImage() {
        console.log('Updating displayed image.');
        const popup = document.getElementById(`bird-popup${randomPostfix}`);
        if (popup) {
            pauseObserver();
            popup.querySelector('img').src = currentImages[currentIndex].url;
            popup.querySelector('.photo-info').innerHTML = `
                <strong>${currentImages[currentIndex].title}</strong> by ${currentImages[currentIndex].author || "Loading author..."}
            `;
            popup.querySelector('a').href = currentImages[currentIndex].flickrPage;
            resumeObserver();
        }
    }

    // Update navigation buttons visibility
    function updateNavigationButtons() {
        console.log('Updating navigation buttons visibility.');
        const prevButton = document.getElementById(`prev-button${randomPostfix}`);
        const nextButton = document.getElementById(`next-button${randomPostfix}`);
        if (prevButton) prevButton.style.display = currentIndex > 0 ? 'block' : 'none';
        if (nextButton) nextButton.style.display = currentIndex < currentImages.length - 1 ? 'block' : 'none';
    }

    // Hide popup
    function hidePopup() {
        console.log('Hiding popup.');
        pauseObserver();
        const popup = document.getElementById(`bird-popup${randomPostfix}`);
        if (popup) {
            popup.style.display = 'none';
            popupVisible = false;
        }
        resumeObserver();
    }

    // --- Core Functionality ---

    // Process species list and highlight species in text
    function processSpecies() {
        console.log('Processing species list.');
        // Find and highlight species in text
        function findSpeciesInText(root) {
            const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
            const nodesToReplace = [];

            const speciesSet = new Set(speciesList); // Fast lookup
            const speciesRegex = new RegExp(`\\b(${[...speciesSet].join("|")})\\b`, "gi"); // Single regex

            let node;
            while ((node = treeWalker.nextNode())) {
                let text = node.nodeValue;
                let replacedText = text.replace(speciesRegex, (match) => {
                    console.log(`Found species: ${match}`);
                    return `<span class="bird-highlight">${match}</span>`; // Inline replacement
                });

                if (text !== replacedText) {
                    nodesToReplace.push({ node, replacedText });
                }
            }
            console.log(`Found ${nodesToReplace.length} occurencies`)
            // DOM updates
            pauseObserver();
            nodesToReplace.forEach(({ node, replacedText }) => {
                const wrapper = document.createElement("span");
                wrapper.innerHTML = replacedText;
                node.parentNode.replaceChild(wrapper, node);
            });
            resumeObserver();
        }

        findSpeciesInText(document.body);

        observer = new MutationObserver((mutations) => {
            if (!observerActive) return;

            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === 1) {
                        findSpeciesInText(node);
                    }
                });
            });
        });

        observerActive = true;
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // Pause the observer
    function pauseObserver() {
        if (observer && observerActive) {
            console.log('Pausing observer.');
            observer.disconnect();
            observerActive = false;
        }
    }

    // Resume the observer
    function resumeObserver() {
        if (observer && !observerActive) {
            console.log('Resuming observer.');
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
            observerActive = true;
        }
    }

    // Fetch images from Flickr API with rate limiting
    async function fetchFlickrImages(species, callback) {
        const now = Date.now();
        if (now - lastApiCallTime < API_CALL_DELAY) {
            console.log('Rate limiting API calls. Waiting...');
            await new Promise(resolve => setTimeout(resolve, API_CALL_DELAY - (now - lastApiCallTime)));
        }
        lastApiCallTime = Date.now();

        try {
            const searchUrl = `https://www.flickr.com/services/rest/?method=flickr.photos.search&api_key=${FLICKR_API_KEY}&${currentSearchMode}=${encodeURIComponent(species)}&sort=${currentSortOrder}&format=json&nojsoncallback=1&per_page=10`;

            console.log(`Fetching images for species: ${species}`);
            const searchResponse = await gmFetch(searchUrl);
            if (!searchResponse.photos || searchResponse.photos.photo.length === 0) {
                console.warn(`No images found for species: ${species}`);
                callback([]);
                return;
            }

            const photos = searchResponse.photos.photo.slice(0, 10);
            const images = photos.map(photo => ({
                url: `https://farm${photo.farm}.staticflickr.com/${photo.server}/${photo.id}_${photo.secret}_c.jpg`,
                title: photo.title,
                author: null,
                flickrPage: `https://www.flickr.com/photos/${photo.owner}/${photo.id}`
            }));

            callback(images);

            photos.forEach(async (photo, index) => {
                const ownerName = await fetchOwnerName(photo.owner);
                images[index].author = ownerName;

                if (currentIndex === index) {
                    updateImage();
                }
            });
        } catch (error) {
            console.error(`Error fetching images for ${species}:`, error);
            callback([]);
        }
    }

    // Helper function to fetch data using GM_xmlhttpRequest
    function gmFetch(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                headers: { 'Origin': 'null' },
                anonymous: true,
                onload: function (response) {
                    if (response.status >= 200 && response.status < 300) {
                        try {
                            resolve(JSON.parse(response.responseText));
                        } catch (error) {
                            console.error('Error parsing response:', error);
                            reject(`Error parsing response: ${error}`);
                        }
                    } else {
                        console.error(`API error: ${response.statusText}`);
                        reject(`API error: ${response.statusText}`);
                    }
                },
                onerror: function (error) {
                    console.error('Request failed:', error);
                    reject(`Request failed: ${error}`);
                }
            });
        });
    }

    // Fetch the owner's real name (or username if real name is missing)
    async function fetchOwnerName(ownerId) {
        try {
            const ownerUrl = `https://www.flickr.com/services/rest/?method=flickr.people.getInfo&api_key=${FLICKR_API_KEY}&user_id=${ownerId}&format=json&nojsoncallback=1`;
            console.log(`Fetching owner info for ID: ${ownerId}`);
            const ownerData = await gmFetch(ownerUrl);
            const person = ownerData.person;
            if (person) {
                return (person.realname ? person.realname._content : null)
                    || (person.username ? person.username._content : null)
                    || person.path_alias
                    || "Unknown";
            }
            return "Unknown";
        } catch (error) {
            console.error(`Error fetching owner info:`, error);
            return "Unknown";
        }
    }

    // Function to handle species highlight (for both mouse and touch events)
    function handleSpeciesHighlight(event) {
        let target;
        if (event.type === 'touchstart' || event.type === 'touchend') {
            // For touch events, use the first touch point
            target = event.touches ? event.touches[0].target : event.target;
        } else {
            // For mouse events, use the event target directly
            target = event.target;
        }

        // Check if the target has the 'bird-highlight' class
        if (target.classList.contains('bird-highlight')) {
            const species = target.textContent;
            console.log(`Highlight detected on species: ${species}`);
            fetchFlickrImages(species, (imageData) => showPopup(event, imageData));

            // Prevent default behavior for touchend events on bird-highlight elements
            if (event.type === 'touchend') {
                event.preventDefault();
            }
        }
    }

    // Debounce function to limit the rate of execution
    function debounce(func, delay) {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), delay);
        };
    }

    // Attach event listeners for both mouse and touch events
    const debouncedHighlight = debounce(handleSpeciesHighlight, DEBOUNCE_DELAY);

    // Mouse events for desktop
    document.addEventListener('mouseover', debouncedHighlight);

    // Touch events for smartphones and tablets
    let touchStartX2, touchStartY2;

    document.addEventListener('touchstart', (e) => {
        const touch = e.touches[0];
        touchStartX2 = touch.clientX;
        touchStartY2 = touch.clientY;
    });

    document.addEventListener('touchend', (e) => {
        const touch = e.changedTouches[0];
        const deltaX = Math.abs(touch.clientX - touchStartX2);
        const deltaY = Math.abs(touch.clientY - touchStartY2);

        // Only trigger if the touch movement is small (e.g., less than 10 pixels)
        if (deltaX < 10 && deltaY < 10) {
            debouncedHighlight(e);
        }
    });

    await initializeScript();
    processSpecies();
    add_style();

    GM_registerMenuCommand('Clear Species List', function () {
        GM_setValue('speciesList', []);
        alert('Species list cleared. You should reload the page now');
    });

    /* ------------------------- This an an extra code to allow a user to configure some parameters ------------------------- */
    /* ------------------------- We can live without the code bellow ------------------------- *

    /* A lot of code just to allow user to pickup a color for hightligths */
    GM_addStyle(`
        #colorPickerContainer${randomPostfix} {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #121212; /* Dark theme */
            border: 1px solid #333;
            border-radius: 10px;
            padding: 15px;
            z-index: 10001;
            display: flex;
            flex-direction: column; /* Column layout for input, button, and text */
            justify-content: center;
            align-items: center;
            box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.5);
            min-width: 35vw;
            min-height: 20vh;
            text-align: center;
        }

        #colorPickerContainer${randomPostfix} * {
            color: #ffffff; /* Light text */
            background-color: transparent;
            border-color: #555;
        }

        #instructionText${randomPostfix} {
            margin-bottom: 10px;
            font-size: 16px;
            color: #dddddd;
            font-weight: normal;
        }

        .picker-row${randomPostfix} {
            display: flex;
            flex-direction: row;
            width: 100%;
            margin: 10px;
        }

        #highlightColorInput${randomPostfix} {
            height: 75px;
            flex-grow: 3;
            border-radius: 5px;
            padding: 5px;
            cursor: pointer;
            box-sizing: border-box;
        }

        .picker-buttons${randomPostfix} {
            display: flex;
            flex-direction: column;
            flex-grow: 1;
            margin: 10px;
        }

        #applyHighlightColor${randomPostfix},
        #dismissButton${randomPostfix} {
            padding: 10px 0;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            transition: background 0.2s ease;
        }

        #applyHighlightColor${randomPostfix} {
            background: #333;
            color: #ffffff;
            border: 1px solid #555;
        }

        #applyHighlightColor${randomPostfix}:hover {
            background: #444;
        }

        #dismissButton${randomPostfix} {
            background: #e74c3c;
            color: #ffffff;
            border: 1px solid #555;
            margin-top: 10px;
        }

        #dismissButton${randomPostfix}:hover {
            background: #c0392b;
        }
    `);

    function openColorPicker() {
        if (document.getElementById(`colorPickerContainer${randomPostfix}`)) return;

        const picker = document.createElement('div');
        picker.id = `colorPickerContainer${randomPostfix}`;
        picker.innerHTML = `
            <div id="instructionText${randomPostfix}">
                Select a color to highlight the bird species on the page, then click "Apply".
            </div>
            <div class="picker-row${randomPostfix}">
                <input type='color' id="highlightColorInput${randomPostfix}">
                <div class="picker-buttons${randomPostfix}">
                    <button id="applyHighlightColor${randomPostfix}">Apply</button>
                    <button id="dismissButton${randomPostfix}">Dismiss</button>
                </div>
            </div>
        `;
        document.body.appendChild(picker);

        // Set default color
        const colorInput = document.getElementById(`highlightColorInput${randomPostfix}`);
        colorInput.value = GM_getValue('highlightColor', '#FFFF00');

        // Apply event listener
        document.getElementById(`applyHighlightColor${randomPostfix}`).addEventListener('click', () => {
            GM_setValue('highlightColor', colorInput.value);
            updateHighlightElements(colorInput.value);
            removePicker();
        });

        // Dismiss event listener
        document.getElementById(`dismissButton${randomPostfix}`).addEventListener('click', removePicker);

        // Attach event listeners
        setTimeout(() => {
            document.addEventListener('click', closeOnClickOutside);
            document.addEventListener('keydown', closeOnEscape);
        }, 0);

        function closeOnClickOutside(e) {
            if (!picker.contains(e.target)) {
                removePicker();
            }
        }

        function closeOnEscape(e) {
            if (e.key === 'Escape') {
                removePicker();
            }
        }

        function removePicker() {
            document.removeEventListener('click', closeOnClickOutside);
            document.removeEventListener('keydown', closeOnEscape);
            picker.remove();
        }
    }

    // Function to update all .bird-highlight elements dynamically
    function updateHighlightElements(color) {
        document.querySelectorAll('.bird-highlight').forEach(el => {
            el.style.backgroundColor = color;
        });
    }

    // Add context menu option to open the color picker
    GM_registerMenuCommand('Change Highlight Color', openColorPicker);

    /* Allow user to change of flickr should sort the results */
    // Constants for sorting options
    const SORT_OPTIONS = [
        { value: 'date-posted-asc', label: 'Date Posted (Oldest First)' },
        { value: 'date-posted-desc', label: 'Date Posted (Newest First)' },
        { value: 'date-taken-asc', label: 'Date Taken (Oldest First)' },
        { value: 'date-taken-desc', label: 'Date Taken (Newest First)' },
        { value: 'interestingness-desc', label: 'Interestingness (Most First)' },
        { value: 'interestingness-asc', label: 'Interestingness (Least First)' },
        { value: 'relevance', label: 'Relevance' }
    ];

    // Constants for search modes
    const SEARCH_MODES = [
        { value: 'tags', label: 'Search by Tags' },
        { value: 'text', label: 'Search by Text' }
    ];

    // Function to update sort order menu dynamically
    function updateSortOrderMenu() {
        let currentSortOrder = GM_getValue('flickrSortOrder', 'relevance');

        SORT_OPTIONS.forEach(option => {
            const isSelected = option.value === currentSortOrder;
            const label = `${isSelected ? '✔ ' : ''}${option.label}`;

            // Update the existing menu item or create a new one
            GM_registerMenuCommand(label, () => {
                GM_setValue('flickrSortOrder', option.value);
                location.reload();
            });
        });
    }

    // Function to update search mode menu dynamically
    function updateSearchModeMenu() {
        let currentSearchMode = GM_getValue('flickrSearchMode', 'tags');

        SEARCH_MODES.forEach(mode => {
            const isSelected = mode.value === currentSearchMode;
            const label = `${isSelected ? '✔ ' : ''}${mode.label}`;

            // Update the existing menu item or create a new one
            GM_registerMenuCommand(label, () => {
                GM_setValue('flickrSearchMode', mode.value);
                location.reload();
            });
        });
    }

    // Call the functions to register the menus
    updateSortOrderMenu();
    updateSearchModeMenu();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址