Amazon Plus (UK)

Auto-sort popular by default; re-applies on every search results update (SPA/pagination).

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Amazon Plus (UK)
// @namespace    http://tampermonkey.net/
// @version      5.5
// @description  Auto-sort popular by default; re-applies on every search results update (SPA/pagination).
// @author       Gemini (and You!)
// @license      MIT
// @match        *://www.amazon.co.uk/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_setClipboard
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- UTILS ---
    // Safe wrappers so the script degrades gracefully if GM_* APIs are missing
    const safeGMGet = (key, def) => {
        try {
            if (typeof GM_getValue === 'function') {
                return GM_getValue(key, def);
            }
        } catch (e) {
            console.warn('GM_getValue failed, falling back to localStorage.', e);
        }
        try {
            const raw = window.localStorage.getItem(`amz_plus_${key}`);
            if (raw === null) return def;
            return JSON.parse(raw);
        } catch {
            return def;
        }
    };

    const safeGMSet = (key, value) => {
        try {
            if (typeof GM_setValue === 'function') {
                GM_setValue(key, value);
            }
        } catch (e) {
            console.warn('GM_setValue failed, falling back to localStorage.', e);
        }
        try {
            window.localStorage.setItem(`amz_plus_${key}`, JSON.stringify(value));
        } catch {
            // ignore storage failures
        }
    };

    const getVal = (id, def) => safeGMGet(id, def);
    const setVal = (id, val) => safeGMSet(id, val);

    const FEATURE_DEFAULTS = {
        amz_master: true, // global ON/OFF switch (ON by default)
        amz_sort: true, // Auto-Sort Popular: ON by default on every search page
        amz_ads: true,
        amz_purist: false
    };

    /** Debounced re-run so SPA / pagination / lazy results don’t miss a sort pass */
    let searchApplyTimer = null;
    let searchObserver = null;
    const SEARCH_APPLY_DEBOUNCE_MS = 120;

    function scheduleSearchApply() {
        if (isProductPage()) return;
        if (searchApplyTimer) clearTimeout(searchApplyTimer);
        searchApplyTimer = setTimeout(() => {
            searchApplyTimer = null;
            try {
                setupSearchResultsWatcher(); // attach once `.s-main-slot` exists (late DOM)
                applySortAndFilter();
            } catch (e) {
                console.warn('Amazon Plus: applySortAndFilter failed', e);
            }
        }, SEARCH_APPLY_DEBOUNCE_MS);
    }

    function setupSearchResultsWatcher() {
        if (isProductPage()) return;
        const container = document.querySelector('.s-main-slot') || document.querySelector('.s-result-list');
        if (!container || searchObserver) return;

        searchObserver = new MutationObserver(() => scheduleSearchApply());
        searchObserver.observe(container, { childList: true, subtree: true });
    }

    let historyHooksInstalled = false;
    function hookHistoryForSearchPages() {
        if (historyHooksInstalled) return;
        historyHooksInstalled = true;
        const notify = () => scheduleSearchApply();
        window.addEventListener('popstate', notify);
        const wrap = (fn) =>
            function stateWrapped() {
                const ret = fn.apply(this, arguments);
                scheduleSearchApply();
                return ret;
            };
        if (history.pushState) history.pushState = wrap(history.pushState);
        if (history.replaceState) history.replaceState = wrap(history.replaceState);
    }

    const getFlag = (key) => {
        const def = Object.prototype.hasOwnProperty.call(FEATURE_DEFAULTS, key)
            ? FEATURE_DEFAULTS[key]
            : false;
        const stored = getVal(key, null);
        return stored === null ? def : stored;
    };

    function isProductPage() {
        return window.location.pathname.includes('/dp/') || window.location.pathname.includes('/gp/product/');
    }

    function extractRatingCount(el) {
        const node =
            el.querySelector('.haul-puis-ratings-container .a-size-micro:last-child') ||
            el.querySelector('span[aria-label*="ratings"]') ||
            el.querySelector('.s-underline-text') ||
            el.querySelector('[aria-label*="ratings"]');

        if (!node) return 0;

        const raw = (node.innerText || node.getAttribute('aria-label') || '0').replace(/,/g, '');
        const re = /([\d.]+)\s*(K)?/gi;
        let max = 0;
        let m;
        while ((m = re.exec(raw)) !== null) {
            let val = parseFloat(m[1]);
            if (!Number.isFinite(val)) continue;
            if (m[2]) val *= 1000;
            if (val > max) max = val;
        }
        return Math.round(max) || 0;
    }

    // --- SEARCH PAGE ENGINE ---
    function applySortAndFilter() {
        if (isProductPage()) return;
        const container = document.querySelector('.s-main-slot') || document.querySelector('.s-result-list');
        if (!container) return;

        const items = container.querySelectorAll('.s-result-item[data-asin]');
        if (items.length === 0) return;

        // Master switch off: restore defaults and exit.
        if (!getFlag('amz_master')) {
            items.forEach((item) => {
                if (!item.getAttribute('data-asin')) return;
                item.style.display = '';
                item.style.order = '';
            });
            return;
        }

        const doSort = getFlag('amz_sort');
        const doAds = getFlag('amz_ads');
        const doPurist = getFlag('amz_purist');

        items.forEach(item => {
            if (!item.getAttribute('data-asin')) return;
            const text = item.innerText;
            const isSponsored = item.classList.contains('AdHolder') || text.includes('Sponsored');
            const isThirdParty = doPurist && !text.includes('Prime') && !text.includes('Amazon');

            if ((doAds && isSponsored) || isThirdParty) {
                item.style.display = 'none';
            } else {
                item.style.display = ''; 
            }

            item.style.order = doSort ? -extractRatingCount(item) : ''; 
        });
    }

    // --- SIDEBAR UI (SEARCH PAGE) ---
    function injectSidebar() {
        if (isProductPage() || document.getElementById('god-mode-panel')) return;
        const sidebar = document.querySelector('#s-refinements') || document.querySelector('.sf-left-nav-reflow');
        if (!sidebar) return;

        const panel = document.createElement('div');
        panel.id = 'god-mode-panel';
        panel.style.cssText = "background: #fdfdfd; padding: 12px; border: 2px solid #B12704; border-radius: 6px; margin-bottom: 15px; font-family: Arial;";

        const createToggle = (id, label) => `
            <div style="display:flex; justify-content:space-between; margin-bottom:8px; font-size:12px; color:#0f1111;">
                <b>${label}</b>
                <input type="checkbox" id="${id}" ${getFlag(id) ? 'checked' : ''} style="cursor:pointer;">
            </div>`;

        panel.innerHTML = `
            <div style="font-weight:900; border-bottom:1px solid #ddd; margin-bottom:10px; padding-bottom:5px; color:#B12704;">⚡ GOD MODE TOOLS</div>
            ${createToggle('amz_master', '⏻ On/Off Switch')}
            ${createToggle('amz_sort', '📊 Auto-Sort Popular')}
            ${createToggle('amz_ads', '🚫 Block Sponsored Ads')}
            ${createToggle('amz_purist', '🏭 STRICT: Amazon/Prime Only')}
        `;

        sidebar.insertAdjacentElement('afterbegin', panel);
        
        panel.querySelectorAll('input').forEach(input => {
            input.addEventListener('change', (e) => {
                const id = e.target.id;
                setVal(id, e.target.checked);
                if (id === 'amz_master') {
                    // When master toggles, rerun engines so both product/search behavior update.
                    runEngines();
                } else {
                    applySortAndFilter();
                }
            });
        });
        
        // Ensure defaults are written once so future checks are consistent
        Object.keys(FEATURE_DEFAULTS).forEach((key) => {
            if (getVal(key, null) === null) {
                setVal(key, FEATURE_DEFAULTS[key]);
            }
        });
    }

    // --- PRODUCT PAGE: DASHBOARD ---
    function injectProductDashboard() {
        if (!isProductPage() || document.getElementById('god-mode-dashboard')) return;
        
        const targetContainer = document.querySelector('#titleSection') || document.querySelector('#centerCol');
        if (!targetContainer) return;

        const asinMatch = window.location.href.match(/\/(?:dp|gp\/product)\/([A-Z0-9]{10})/);
        const asin = asinMatch ? asinMatch[1] : '';
        if (!asin) return;

        const pageText = document.documentElement.textContent;
        
        const bsrMatch = pageText.match(/Best Sellers Rank:?\s*#?([0-9,]+)\s+in\s+([A-Za-z&\s]+)/i);
        const bsrText = bsrMatch ? `🏆 <b>BSR:</b> #${bsrMatch[1]} in ${bsrMatch[2].trim().substring(0, 20)}...` : '<span style="color:#888;">No BSR found</span>';

        const isNonReturnable = pageText.includes('Non-returnable') || pageText.includes('not returnable');
        const returnHtml = isNonReturnable ? `<div style="width:100%; background:#ffe3e3; color:#B12704; padding:6px; border:1px solid #B12704; font-weight:bold; text-align:center; margin-bottom:8px; border-radius:4px; font-size:12px;">🚨 WARNING: This item is NON-RETURNABLE</div>` : '';

        const hasUsed = pageText.includes('Used from') || document.querySelector('#olpLinkWidget_feature_div');
        const warehouseHtml = hasUsed ? `<a href="https://www.amazon.co.uk/dp/${asin}#aod" style="background:#007185; color:white; padding:4px 8px; border-radius:4px; text-decoration:none; font-weight:bold;">♻️ Open Box Deals</a>` : '';

        let ppuHtml = '';
        const ppuNode = document.querySelector('.a-size-small.a-color-price');
        if (ppuNode && ppuNode.textContent.includes('/')) {
            ppuHtml = `<span style="background:#fff3d4; color:#B12704; font-weight:900; padding:2px 6px; border-radius:4px; border:1px solid #f4bf42;">⚖️ ${ppuNode.textContent.trim()}</span>`;
            ppuNode.style.display = 'none'; 
        }

        const mainImg = document.querySelector('#landingImage') || document.querySelector('#imgBlkFront');
        const lensLink = mainImg ? `https://lens.google.com/uploadbyurl?url=${encodeURIComponent(mainImg.src)}` : '#';

        const dash = document.createElement('div');
        dash.id = 'god-mode-dashboard';
        dash.style.cssText = "margin: 10px 0; padding: 12px; background: #fdfdfd; border: 1px solid #ccc; border-top: 3px solid #007185; border-radius: 4px; display: flex; flex-direction: column; gap: 8px; font-size: 12px; width: 100%; box-sizing: border-box;";

        dash.innerHTML = `
            ${returnHtml}
            <div style="display:flex; flex-wrap:wrap; gap:12px; align-items:center;">
                ${ppuHtml}
                <span style="color:#333;">${bsrText}</span>
            </div>
            <div style="display:flex; flex-wrap:wrap; gap:10px; align-items:center; border-top:1px solid #eee; padding-top:8px;">
                <span id="as-copy-link" style="color:#007185; cursor:pointer; font-weight:bold;" title="Copy tracking-free URL">🔗 Copy Clean Link (${asin})</span>
                <span style="color:#ccc;">|</span>
                <a href="${lensLink}" target="_blank" style="color:#B12704; font-weight:bold; text-decoration:none;" title="Reverse image search on Google to find AliExpress/Dropshippers">🕵️ AliExpress Spotter</a>
                <span style="color:#ccc;">|</span>
                <a href="https://www.hagglezon.com/en/s/${asin}" target="_blank" style="color:#007185; font-weight:bold; text-decoration:none;">🌍 Compare EU Prices</a>
                ${warehouseHtml !== '' ? '<span style="color:#ccc;">|</span>' + warehouseHtml : ''}
            </div>
        `;

        if (targetContainer.id === 'titleSection') {
            targetContainer.insertAdjacentElement('afterend', dash);
        } else {
            targetContainer.insertAdjacentElement('afterbegin', dash);
        }

        document.getElementById('as-copy-link').addEventListener('click', (e) => {
            GM_setClipboard(`https://www.amazon.co.uk/dp/${asin}`);
            e.target.innerText = '✅ Clean Link Copied!';
            setTimeout(() => e.target.innerText = `🔗 Copy Clean Link (${asin})`, 1500);
        });
    }

    // --- PRODUCT PAGE: KEEPA (BOTTOM) ---
    function injectKeepa() {
        if (!isProductPage() || document.getElementById('as-keepa-box')) return;
        
        const targetContainer = document.querySelector('#feature-bullets') || document.querySelector('#centerCol');
        if (!targetContainer) return; 

        const asinMatch = window.location.href.match(/\/(?:dp|gp\/product)\/([A-Z0-9]{10})/);
        const asin = asinMatch ? asinMatch[1] : '';
        if (!asin) return;

        const box = document.createElement('div');
        box.id = 'as-keepa-box';
        box.style.cssText = "margin-top: 15px; border: 1px solid #ddd; padding: 15px; background: #fff; text-align: center; width: 100%; box-sizing: border-box; border-radius: 4px;";
        
        box.innerHTML = `
            <div style="font-weight:bold; margin-bottom:10px; font-size:13px; color:#0f1111; text-align:left;">📉 Price History (Keepa)</div>
            <img id="keepa-graph-img" src="https://graph.keepa.com/pricehistory.png?asin=${asin}&domain=co.uk" style="width:100%; max-width: 600px; cursor:pointer; border-radius:4px; border: 1px solid #eee;">
        `;
        
        if (targetContainer.id === 'feature-bullets') {
            targetContainer.insertAdjacentElement('afterend', box);
        } else {
            targetContainer.insertAdjacentElement('beforeend', box);
        }

        document.getElementById('keepa-graph-img').addEventListener('click', () => {
            window.open(`https://keepa.com/#!product/2-${asin}`);
        });
    }

    // --- RELIABLE EVENT LOOP ---
    function runEngines() {
        try {
            if (isProductPage()) {
                // On product pages, master OFF removes enhancements; ON injects them.
                if (!getFlag('amz_master')) {
                    const dash = document.getElementById('god-mode-dashboard');
                    if (dash && dash.parentElement) dash.parentElement.removeChild(dash);
                    const keepa = document.getElementById('as-keepa-box');
                    if (keepa && keepa.parentElement) keepa.parentElement.removeChild(keepa);
                    return;
                }
                injectProductDashboard();
                injectKeepa();
            } else {
                // Always show sidebar on search pages so the master switch is reachable.
                injectSidebar();
                applySortAndFilter();
                setupSearchResultsWatcher();
                scheduleSearchApply(); // catch late DOM (sidebar sometimes before results)
            }
        } catch (error) {
            console.warn("God Mode Script Encountered a DOM Error:", error);
        }
    }

    // Re-apply when Amazon swaps results (pagination, filters, SPA navigation).
    hookHistoryForSearchPages();

    runEngines();

})();