eBay Show Total Price (Item + Shipping) with Settings

Adds shipping price to item price on eBay search results, with live settings for tax, color, and size. Enhanced UI with toggle and click-off-to-close.

// ==UserScript==
// @name         eBay Show Total Price (Item + Shipping) with Settings
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds shipping price to item price on eBay search results, with live settings for tax, color, and size. Enhanced UI with toggle and click-off-to-close.
// @author       You
// @match        https://www.ebay.*/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // --- Settings ---
    const SETTINGS_KEY = 'ebay-total-settings';
    const defaultSettings = {
        taxRate: 0,
        textColor: '#d0021b',
        textSize: '16px'
    };

    function loadSettings() {
        try {
            return Object.assign({}, defaultSettings, JSON.parse(localStorage.getItem(SETTINGS_KEY)));
        } catch {
            return {...defaultSettings};
        }
    }

    function saveSettings(settings) {
        localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
    }

    let settings = loadSettings();

    // --- Settings Window ---
    function createSettingsWindow() {
        const win = document.createElement('div');
        win.id = 'ebay-settings';
        win.style.position = 'fixed';
        win.style.top = '20px';
        win.style.right = '20px';
        win.style.zIndex = '2147483647'; // Max z-index
        win.style.background = '#fff';
        win.style.border = '1px solid #ccc';
        win.style.padding = '12px 18px 12px 12px';
        win.style.borderRadius = '8px';
        win.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
        win.style.fontFamily = 'sans-serif';
        win.style.fontSize = '14px';
        win.style.userSelect = 'none';
        win.style.cursor = 'move';
        win.style.display = 'none';

        win.innerHTML = `
            <div style="font-weight:bold;margin-bottom:8px;cursor:move;">eBay Total Price Settings</div>
            <label>Tax Rate (%): <input type="number" min="0" max="100" step="0.01" id="tax-rate" style="width:60px" value="${settings.taxRate}"></label><br>
            <label>Text Color: <input type="color" id="text-color" value="${settings.textColor}"></label><br>
            <label>Text Size: <input type="number" min="10" max="48" id="text-size" style="width:60px" value="${parseInt(settings.textSize)}"> px</label><br>
            <button id="close-settings" style="margin-top:8px;">×</button>
        `;

        document.body.appendChild(win);

        // Drag functionality
        let isDragging = false, offsetX = 0, offsetY = 0;
        win.addEventListener('mousedown', function(e) {
            if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') return;
            isDragging = true;
            offsetX = e.clientX - win.offsetLeft;
            offsetY = e.clientY - win.offsetTop;
        });
        document.addEventListener('mousemove', function(e) {
            if (isDragging) {
                win.style.left = (e.clientX - offsetX) + 'px';
                win.style.top = (e.clientY - offsetY) + 'px';
                win.style.right = 'auto';
            }
        });
        document.addEventListener('mouseup', function() { isDragging = false; });

        // Live update handlers
        win.querySelector('#tax-rate').addEventListener('input', function(e) {
            settings.taxRate = parseFloat(e.target.value) || 0;
            saveSettings(settings);
            processedItems.clear();
            updatePrices();
        });
        win.querySelector('#text-color').addEventListener('input', function(e) {
            settings.textColor = e.target.value;
            saveSettings(settings);
            processedItems.clear();
            updatePrices();
        });
        win.querySelector('#text-size').addEventListener('input', function(e) {
            settings.textSize = e.target.value + 'px';
            saveSettings(settings);
            processedItems.clear();
            updatePrices();
        });

        // Close button
        win.querySelector('#close-settings').onclick = function() {
            win.style.display = 'none';
        };

        // Click-off-to-close
        document.addEventListener('mousedown', function(e) {
            const btn = document.getElementById('ebay-settings-btn');
            if (
                win.style.display !== 'none' &&
                !win.contains(e.target) &&
                (!btn || !btn.contains(e.target))
            ) {
                win.style.display = 'none';
            }
        });
    }

    // --- Settings Button ---
    function createSettingsButton() {
        const btn = document.createElement('button');
        btn.id = 'ebay-settings-btn';
        btn.textContent = '⚙️';
        btn.title = 'eBay Total Price Settings';
        btn.style.position = 'fixed';
        btn.style.top = '20px';
        btn.style.right = '20px';
        btn.style.zIndex = '2147483647';
        btn.style.background = '#fff';
        btn.style.border = '1px solid #ccc';
        btn.style.borderRadius = '50%';
        btn.style.width = '36px';
        btn.style.height = '36px';
        btn.style.fontSize = '18px';
        btn.style.cursor = 'pointer';
        btn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.10)';
        btn.onclick = function() {
            const win = document.getElementById('ebay-settings');
            win.style.display = (win.style.display === 'none' || win.style.display === '') ? 'block' : 'none';
        };
        document.body.appendChild(btn);
    }

    // --- Price Calculation ---
    function parsePrice(str) {
        if (!str) return 0;
        let match = str.replace(/,/g, '').match(/[\d,.]+/);
        return match ? parseFloat(match[0]) : 0;
    }

    // --- Optimized Update ---
    const processedItems = new Set();

    function updatePrices() {
        settings = loadSettings();
        const items = document.querySelectorAll('[data-view*="mi:1686"]')
            .length ? document.querySelectorAll('[data-view*="mi:1686"]')
            : document.querySelectorAll('.s-item');

        items.forEach(item => {
            if (processedItems.has(item)) return;

            const priceElem = item.querySelector('.s-item__price');
            if (!priceElem) return;

            let shippingElem = item.querySelector('.s-item__shipping, .s-item__logisticsCost');
            let shippingText = shippingElem ? shippingElem.textContent.trim() : '';

            let shippingPrice = 0;
            if (/free shipping/i.test(shippingText) || /shipping in \d+ day/i.test(shippingText)) {
                shippingPrice = 0;
            } else {
                shippingPrice = parsePrice(shippingText);
            }

            let itemPrice = parsePrice(priceElem.textContent);

            // Apply tax
            let total = itemPrice + shippingPrice;
            if (settings.taxRate > 0) {
                total += total * (settings.taxRate / 100);
            }

            // Remove old total price if present
            let old = item.querySelector('.total-price');
            if (old) old.remove();

            // Add total price display
            const totalElem = document.createElement('div');
            totalElem.className = 'total-price';
            totalElem.style.fontWeight = 'bold';
            totalElem.style.color = settings.textColor;
            totalElem.style.fontSize = settings.textSize;
            totalElem.textContent = `Total: $${total.toFixed(2)}`;
            priceElem.parentNode.insertBefore(totalElem, priceElem.nextSibling);

            processedItems.add(item);
        });
    }

    // Debounce utility
    function debounce(fn, delay) {
        let timer = null;
        return function() {
            clearTimeout(timer);
            timer = setTimeout(fn, delay);
        };
    }

    // --- Initialize ---
    createSettingsButton();
    createSettingsWindow();
    updatePrices();

    // Observe for dynamic content, but debounce updates
    const debouncedUpdate = debounce(updatePrices, 400);
    const observer = new MutationObserver(debouncedUpdate);
    observer.observe(document.body, { childList: true, subtree: true });
})();