Search the Ships (Enhanced UI v7)

Adds a beautifully designed button to book-related websites to search the current book title on various archives, with a centralized status indicator.

// ==UserScript==
// @name         Search the Ships (Enhanced UI v7)
// @namespace    Violentmonkey Scripts
// @version      1.4
// @description  Adds a beautifully designed button to book-related websites to search the current book title on various archives, with a centralized status indicator.
// @author       Delaxy (UI by Gemini)
// @match        https://thegreatestbooks.org/*
// @match        https://www.goodreads.com/*
// @match        https://www.amazon.com/*
// @match        https://www.amazon.fr/*
// @match        https://www.amazon.de/*
// @match        https://www.amazon.co.uk/*
// @match        https://www.amazon.it/*
// @match        https://www.amazon.*/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const searchSites = {
        'Z-Library': {
            queryKey: '',
            separator: '?',
            urls: [
                {
                    name: 'All Files',
                    base: 'https://z-lib.gd/s/',
                    extra: ''
                },
                {
                    name: 'EPUBs',
                    base: 'https://z-lib.gd/s/',
                    extra: 'extensions[]=EPUB'
                },
                {
                    name: 'PDFs',
                    base: 'https://z-lib.gd/s/',
                    extra: 'extensions[]=PDF'
                }
            ]
        },
        "Anna's Archive": {
            queryKey: 'q',
            separator: '&',
            urls: [
                {
                    name: 'All Files',
                    base: 'https://annas-archive.org/search',
                    extra: 'page=1&sort='
                },
                {
                    name: 'EPUBs',
                    base: 'https://annas-archive.org/search',
                    extra: 'page=1&sort=&ext=epub'
                },
                 {
                    name: 'PDFs',
                    base: 'https://annas-archive.org/search',
                    extra: 'page=1&sort=&ext=pdf'
                }
            ]
        },
        'Library Genesis': {
            queryKey: 'req',
            separator: '&',
            urls: [
                {
                    name: 'Default Search',
                    base: 'https://libgen.li/index.php',
                    extra: 'lg_topic=libgen&open=0&view=simple&res=25&phrase=1&column=def'
                }
            ]
        },
        'Mobilism': {
            queryKey: 'keywords',
            separator: '&',
            urls: [
                {
                    name: 'Books Forum',
                    base: 'https://forum.mobilism.org/search.php',
                    extra: 'fid[]=120&sr=topics&sf=titleonly'
                }
            ]
        }
    };


    function getBookTitle() {
        let title = '';
        const hostname = window.location.hostname;
        
        if (hostname.includes('goodreads.com')) {
            const isBookPage = window.location.pathname.includes('/book/show/');
            if (isBookPage) {
                const el = document.querySelector('[data-testid="bookTitle"]');
                if (el) title = el.innerText.trim();
            }
        }
        else if (hostname.includes('amazon.')) {
            const isProductPage = window.location.pathname.includes('/dp/') || window.location.pathname.includes('/gp/product/');
            const breadcrumb = document.querySelector('#wayfinding-breadcrumbs_feature_div');
            const isBookCategory = breadcrumb && (breadcrumb.innerText.includes('Books') || breadcrumb.innerText.includes('Kindle Store'));

            if (isProductPage && isBookCategory) {
                const el = document.getElementById('productTitle') || document.getElementById('bookTitle');
                if (el) title = el.innerText.trim();
            }
            
        } else if (hostname.includes('thegreatestbooks.org')) {
            const isBookPage = window.location.pathname.includes('/books/');
            if (isBookPage) {
                const el = document.querySelector('h1 a.no-underline-link');
                if (el) title = el.textContent.trim();
            }
        }

        if (!title) {
            return '';
        } else {   
            return cleanTitle(title);
        }
    }

    function cleanTitle(title) {
        return title
            .replace(/\(Paperback\)/, '')
            .replace(/\(Hardcover\)/, '')
            .replace(/\(Kindle Edition\)/, '')
            .replace(/\(Audible Audio Edition\)/, '')
            .trim();
    }

    function constructSearchUrl(siteConfig, urlObject, encodedTitle) {
        let finalUrl;
        if (!siteConfig.queryKey) {
            finalUrl = `${urlObject.base}${encodedTitle}`;
            if (urlObject.extra) {
                finalUrl += `${siteConfig.separator}${urlObject.extra}`;
            }
        }
        else {
            finalUrl = `${urlObject.base}?${siteConfig.queryKey}=${encodedTitle}`;
            if (urlObject.extra) {
                finalUrl += `${siteConfig.separator}${urlObject.extra}`;
            }
        }
        return finalUrl;
    }


    function checkStatuses() {
        const statusElements = document.querySelectorAll('.sts-ship-status');
        statusElements.forEach(span => {
            const url = span.dataset.url;
            if (!url || !span.classList.contains('sts-pending')) return;

            const baseUrl = new URL(url).origin;
            fetch(baseUrl, { method: 'HEAD', mode: 'no-cors' })
                .then(() => {
                    span.classList.remove('sts-pending');
                    span.classList.add('sts-online');
                    span.title = 'Online';
                })
                .catch(() => {
                    span.classList.remove('sts-pending');
                    span.classList.add('sts-offline');
                    span.title = 'Offline';
                });
        });
    }

    function createSearchButton() {
        const bookTitle = getBookTitle();
        if (!bookTitle) return;

        const encodedTitle = encodeURIComponent(bookTitle);

        const styles = `
            @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');

            .sts-container {
                position: fixed;
                bottom: 20px;
                right: 20px;
                z-index: 10000;
                font-family: 'Inter', sans-serif;
            }
            @media (max-width: 480px) {
                .sts-container {
                    right: 10px;
                    bottom: 10px;
                }
            }
            .sts-button {
                background: linear-gradient(135deg, #5A67D8 0%, #9F7AEA 100%);
                color: white;
                padding: 12px 20px;
                border: none;
                border-radius: 50px;
                cursor: pointer;
                font-size: 16px;
                font-weight: 600;
                box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
                transition: all 0.3s ease;
                outline: none;
                display: flex;
                align-items: center;
                gap: 8px;
            }
            .sts-button:hover {
                transform: translateY(-3px);
                box-shadow: 0 7px 25px rgba(0, 0, 0, 0.25);
            }
            .sts-button svg {
                width: 20px;
                height: 20px;
                fill: currentColor;
            }
            .sts-dropdown {
                position: absolute;
                bottom: calc(100% + 10px);
                left: 50%;
                background-color: rgba(255, 255, 255, 0.85);
                backdrop-filter: blur(10px);
                -webkit-backdrop-filter: blur(10px);
                border: 1px solid rgba(0, 0, 0, 0.1);
                border-radius: 12px;
                box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
                display: none;
                z-index: 10001;
                min-width: 220px;
                padding: 6px;
                box-sizing: border-box;
                opacity: 0;
                transform: translate(-50%, 10px);
                transition: opacity 0.2s ease, transform 0.2s ease;
                pointer-events: none;
            }
            .sts-dropdown.sts-visible {
                display: block;
                opacity: 1;
                transform: translate(-50%, 0);
                pointer-events: auto;
            }
            .sts-site-div {
                padding: 10px 15px;
                cursor: pointer;
                font-size: 15px;
                font-weight: 500;
                color: #333;
                transition: background-color 0.2s ease;
                border-radius: 8px;
                position: relative;
                /* --- MODIFIED --- */
                display: flex;
                justify-content: space-between;
                align-items: center;
            }
            .sts-site-div:hover {
                background-color: rgba(0, 0, 0, 0.05);
            }
            .sts-submenu {
                display: none;
                position: absolute;
                top: -6px;
                right: calc(100% + 8px);
                background-color: rgba(255, 255, 255, 0.85);
                backdrop-filter: blur(10px);
                -webkit-backdrop-filter: blur(10px);
                border: 1px solid rgba(0, 0, 0, 0.1);
                border-radius: 12px;
                box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
                min-width: 220px;
                z-index: 10002;
                padding: 6px;
                opacity: 0;
                transform: translateX(10px);
                transition: opacity 0.2s ease, transform 0.2s ease;
                pointer-events: none;
            }
            .sts-site-div:last-child > .sts-submenu {
                top: auto;
                bottom: -6px;
            }
            .sts-submenu.sts-submenu-visible {
                display: block;
                opacity: 1;
                transform: translateX(0);
                pointer-events: auto;
            }
            .sts-link {
                /* --- MODIFIED --- */
                display: block;
                padding: 8px 12px;
                color: #4A5568;
                text-decoration: none;
                font-size: 14px;
                border-radius: 6px;
                transition: background-color 0.2s ease, color 0.2s ease;
            }
            .sts-link:hover {
                background-color: rgba(90, 103, 216, 0.1);
                color: #5A67D8;
            }
            .sts-ship-status {
                width: 8px;
                height: 8px;
                border-radius: 50%;
                display: inline-block;
                margin-left: 10px;
                flex-shrink: 0;
            }
            .sts-pending { background-color: #9CA3AF; }
            .sts-online { background-color: #34D399; }
            .sts-offline { background-color: #F87171; }
        `;
        const styleSheet = document.createElement("style");
        styleSheet.innerText = styles;
        document.head.appendChild(styleSheet);

        const buttonContainer = document.createElement('div');
        buttonContainer.className = 'sts-container';

        const button = document.createElement('button');
        button.className = 'sts-button';
        button.innerHTML = `
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
            <span>Search the Ships</span>
        `;

        const dropdown = document.createElement('div');
        dropdown.className = 'sts-dropdown';

        for (const siteName in searchSites) {
            const siteConfig = searchSites[siteName];
            const siteDiv = document.createElement('div');
            siteDiv.className = 'sts-site-div';
            
            // --- NEW LOGIC ---
            // Create a separate span for the name to allow flexbox positioning.
            const nameSpan = document.createElement('span');
            nameSpan.innerText = siteName;
            siteDiv.appendChild(nameSpan);

            // Add the status indicator directly to the siteDiv.
            if (siteConfig.urls && siteConfig.urls.length > 0) {
                const statusSpan = document.createElement('span');
                statusSpan.className = 'sts-ship-status sts-pending';
                // Use the base URL of the first link for the status check.
                statusSpan.dataset.url = siteConfig.urls[0].base;
                statusSpan.title = 'Checking...';
                siteDiv.appendChild(statusSpan);
            }
            
            const subMenu = document.createElement('div');
            subMenu.className = 'sts-submenu';

            siteConfig.urls.forEach(urlObject => {
                const finalUrl = constructSearchUrl(siteConfig, urlObject, encodedTitle);
                const link = document.createElement('a');
                link.href = finalUrl;
                link.target = '_blank';
                link.rel = 'noopener noreferrer';
                link.className = 'sts-link';
                link.innerText = urlObject.name;
                
                // --- REMOVED ---
                // The status span is no longer added to each link.
                
                subMenu.appendChild(link);
            });

            siteDiv.appendChild(subMenu);
            dropdown.appendChild(siteDiv);
        }

        button.appendChild(dropdown);
        buttonContainer.appendChild(button);
        document.body.appendChild(buttonContainer);

        let hideDropdownTimeout;
        buttonContainer.addEventListener('mouseenter', () => {
            clearTimeout(hideDropdownTimeout);
            if (!dropdown.classList.contains('sts-visible')) {
                dropdown.classList.add('sts-visible');
                checkStatuses();
            }
        });
        buttonContainer.addEventListener('mouseleave', () => {
            hideDropdownTimeout = setTimeout(() => {
                dropdown.classList.remove('sts-visible');
            }, 300);
        });

        document.querySelectorAll('.sts-site-div').forEach(siteDiv => {
            const subMenu = siteDiv.querySelector('.sts-submenu');
            let hideSubMenuTimeout;

            const show = () => {
                clearTimeout(hideSubMenuTimeout);
                document.querySelectorAll('.sts-submenu-visible').forEach(visibleSubMenu => {
                    if (visibleSubMenu !== subMenu) {
                        visibleSubMenu.classList.remove('sts-submenu-visible');
                    }
                });
                subMenu.classList.add('sts-submenu-visible');
            };

            const hide = () => {
                hideSubMenuTimeout = setTimeout(() => {
                    subMenu.classList.remove('sts-submenu-visible');
                }, 300);
            };

            siteDiv.addEventListener('mouseenter', show);
            siteDiv.addEventListener('mouseleave', hide);
            subMenu.addEventListener('mouseenter', show);
            subMenu.addEventListener('mouseleave', hide);
        });
    }

    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', createSearchButton);
    } else {
        createSearchButton();
    }
})();