Greasy Fork 还支持 简体中文。

Bird Species Image Preview

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

目前為 2025-03-23 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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();
})();