MWI Tooltip Float Window

Displays mouseover-only tooltips (likely bloated by various extensions) in a draggable float window that stays visible at your preferred position.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         MWI Tooltip Float Window
// @namespace    http://tampermonkey.net/
// @version      8.0
// @description  Displays mouseover-only tooltips (likely bloated by various extensions) in a draggable float window that stays visible at your preferred position.
// @author       syabusyabukun
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @match        https://www.milkywayidlecn.com/*
// @match        https://test.milkywayidlecn.com/*
// @grant        GM_addStyle
// @run-at       document-start
// @license MIT
// ==/UserScript==

(function () {
    'use strict';

    const FLOAT_WINDOW_ID = 'mwi-tooltip-float-window';
    const STORAGE_KEY_POS = 'mwi-tooltip-float-pos';
    const HIDE_CLASS = 'mwi-float-hidden-tooltip';

    GM_addStyle(`
        /* 元tooltipの一時非表示用クラス
           visibility:hidden でレイアウトを壊さずに視覚的に消す。
           MUIのstyle属性(position/transform等)には干渉しない。 */
        .${HIDE_CLASS} {
            visibility: hidden !important;
            opacity: 0 !important;
            pointer-events: none !important;
        }

        #${FLOAT_WINDOW_ID} {
            position: fixed;
            z-index: 100000;
            min-width: 200px;
            max-width: 90vw;
            max-height: 85vh;
            background: var(--color-midnight-900, #1a1f36);
            border: 1px solid var(--color-neutral-200, #555);
            border-radius: 6px;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
            display: none;
            flex-direction: column;
            overflow: hidden;
            font-family: "Roboto", sans-serif;
            color: var(--color-text-dark-mode, #e0e0e0);
        }
        #${FLOAT_WINDOW_ID}.visible {
            display: flex;
        }
        #${FLOAT_WINDOW_ID} .mwi-float-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 4px 8px;
            background: var(--color-space-700, #2a2f4a);
            cursor: move;
            user-select: none;
            flex-shrink: 0;
            min-height: 24px;
            border-bottom: 1px solid var(--color-neutral-200, #555);
        }
        #${FLOAT_WINDOW_ID} .mwi-float-header .title-text {
            font-size: 11px;
            font-weight: 600;
            color: var(--color-neutral-200, #aaa);
            pointer-events: none;
        }
        #${FLOAT_WINDOW_ID} .mwi-float-close-btn {
            width: 20px;
            height: 20px;
            line-height: 20px;
            text-align: center;
            border-radius: 50%;
            background: rgba(255, 255, 255, 0.1);
            color: #ccc;
            cursor: pointer;
            font-size: 14px;
            font-weight: bold;
            transition: all 0.15s ease;
            flex-shrink: 0;
        }
        #${FLOAT_WINDOW_ID} .mwi-float-close-btn:hover {
            background: #e74c3c;
            color: #fff;
        }
        #${FLOAT_WINDOW_ID} .mwi-float-body {
            overflow-y: auto;
            overflow-x: hidden;
            flex-grow: 1;
            padding: 0;
            font-size: var(--font-size-base, 0.875rem);
            line-height: 1.4;
            user-select: text;
            -webkit-user-select: text;
            cursor: auto;
        }
        #${FLOAT_WINDOW_ID} .mwi-float-body * {
            user-select: text !important;
            -webkit-user-select: text !important;
        }
        #${FLOAT_WINDOW_ID} .mwi-float-resize {
            position: absolute;
            bottom: 0;
            right: 0;
            width: 16px;
            height: 16px;
            cursor: nwse-resize;
            z-index: 1;
        }
        #${FLOAT_WINDOW_ID} .mwi-float-resize::after {
            content: '';
            position: absolute;
            bottom: 3px;
            right: 3px;
            width: 8px;
            height: 8px;
            border-right: 2px solid rgba(255,255,255,0.3);
            border-bottom: 2px solid rgba(255,255,255,0.3);
        }
    `);

    // ─── 状態 ───
    let floatWindow = null;
    let isFloatVisible = false;
    let syncObserver = null;
    let trackedTooltip = null;
    let captureTimer = null;
    let frozen = false;

    let lastClickTime = 0;
    const CLICK_THRESHOLD_MS = 600;

    document.addEventListener('mousedown', (e) => {
        if (e.target.closest?.(`#${FLOAT_WINDOW_ID}`)) return;
        lastClickTime = Date.now();
    }, true);

    // ─── 非hover tooltip除外判定 ───
    function shouldExclude(popperNode) {
        if (Date.now() - lastClickTime < CLICK_THRESHOLD_MS) return true;

        const buttons = popperNode.querySelectorAll(
            'button, [role="button"], [role="menuitem"], input[type="button"], input[type="submit"], .MuiButton-root'
        );
        if (buttons.length >= 2) return true;

        if (popperNode.querySelector('[class*="ItemSelector"]')) return true;
        if (popperNode.querySelector('[class*="Item_clickable"]')) return true;

        const itemContainers = popperNode.querySelectorAll('[class*="Item_itemContainer"]');
        if (itemContainers.length >= 2) return true;

        if (popperNode.querySelector('input[type="text"], input:not([type])')) return true;

        return false;
    }

    // ─── フロートウィンドウ作成 ───
    function createFloatWindow() {
        const win = document.createElement('div');
        win.id = FLOAT_WINDOW_ID;
        win.innerHTML = `
            <div class="mwi-float-header">
                <span class="title-text">Tooltip</span>
                <span class="mwi-float-close-btn" title="Close">×</span>
            </div>
            <div class="mwi-float-body"></div>
            <div class="mwi-float-resize"></div>
        `;
        document.body.appendChild(win);

        win.querySelector('.mwi-float-close-btn').addEventListener('click', (e) => {
            e.stopPropagation();
            hideFloatWindow();
        });

        setupDrag(win);
        setupResize(win);
        restorePosition(win);
        return win;
    }

    function setupDrag(win) {
        const header = win.querySelector('.mwi-float-header');
        let isDragging = false;
        let startX, startY, origLeft, origTop;

        header.addEventListener('mousedown', (e) => {
            if (e.target.classList.contains('mwi-float-close-btn')) return;
            e.preventDefault();
            isDragging = true;
            startX = e.clientX;
            startY = e.clientY;
            const rect = win.getBoundingClientRect();
            origLeft = rect.left;
            origTop = rect.top;
        });
        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;
            e.preventDefault();
            let newLeft = origLeft + (e.clientX - startX);
            let newTop = origTop + (e.clientY - startY);
            newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - win.offsetWidth));
            newTop = Math.max(0, Math.min(newTop, window.innerHeight - 30));
            win.style.left = newLeft + 'px';
            win.style.top = newTop + 'px';
            win.style.right = 'auto';
            win.style.bottom = 'auto';
        });
        document.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                savePosition(win);
            }
        });
    }

    function setupResize(win) {
        const handle = win.querySelector('.mwi-float-resize');
        let isResizing = false;
        let startX, startY, origW, origH;

        handle.addEventListener('mousedown', (e) => {
            e.preventDefault();
            e.stopPropagation();
            isResizing = true;
            startX = e.clientX;
            startY = e.clientY;
            origW = win.offsetWidth;
            origH = win.offsetHeight;
            win.dataset.manualResize = 'true';
        });
        document.addEventListener('mousemove', (e) => {
            if (!isResizing) return;
            e.preventDefault();
            win.style.width = Math.max(200, origW + (e.clientX - startX)) + 'px';
            win.style.height = Math.max(100, origH + (e.clientY - startY)) + 'px';
        });
        document.addEventListener('mouseup', () => {
            if (isResizing) isResizing = false;
        });
    }

    function savePosition(win) {
        try {
            const rect = win.getBoundingClientRect();
            localStorage.setItem(STORAGE_KEY_POS, JSON.stringify({ left: rect.left, top: rect.top }));
        } catch (e) { }
    }
    function restorePosition(win) {
        try {
            const saved = localStorage.getItem(STORAGE_KEY_POS);
            if (saved) {
                const pos = JSON.parse(saved);
                win.style.left = Math.max(0, Math.min(pos.left, window.innerWidth - 100)) + 'px';
                win.style.top = Math.max(0, Math.min(pos.top, window.innerHeight - 50)) + 'px';
            } else {
                win.style.left = (window.innerWidth - 450) + 'px';
                win.style.top = '60px';
            }
            win.style.right = 'auto';
            win.style.bottom = 'auto';
        } catch (e) {
            win.style.left = '100px';
            win.style.top = '100px';
        }
    }

    // ─── スナップショット ───
    function snapshotToFloat(sourceEl) {
        if (!floatWindow) floatWindow = createFloatWindow();
        const body = floatWindow.querySelector('.mwi-float-body');
        const scrollTop = body.scrollTop;

        const clone = sourceEl.cloneNode(true);
        clone.classList.remove(HIDE_CLASS);
        clone.style.cssText = `
            position: static !important;
            transform: none !important;
            top: auto !important; left: auto !important;
            right: auto !important; bottom: auto !important;
            width: auto !important; max-width: 100% !important;
            visibility: visible !important; opacity: 1 !important;
            pointer-events: auto !important; z-index: auto !important;
        `;

        body.innerHTML = '';
        body.appendChild(clone);
        body.scrollTop = scrollTop;

        delete floatWindow.dataset.manualResize;
        floatWindow.style.width = '';
        floatWindow.style.height = '';

        isFloatVisible = true;
        floatWindow.classList.add('visible');
    }

    // ─── 同期 ───
    function startSync(sourceEl) {
        stopSync();
        trackedTooltip = sourceEl;
        frozen = false;

        syncObserver = new MutationObserver(() => {
            if (frozen) return;
            const text = sourceEl.textContent || '';
            if (text.trim().length < 5) {
                frozen = true;
                stopSync();
                return;
            }
            snapshotToFloat(sourceEl);
        });
        syncObserver.observe(sourceEl, {
            childList: true,
            subtree: true,
            characterData: true
        });
    }

    function stopSync() {
        if (syncObserver) {
            syncObserver.disconnect();
            syncObserver = null;
        }
        trackedTooltip = null;
    }

    function hideFloatWindow() {
        isFloatVisible = false;
        frozen = false;
        stopSync();
        if (captureTimer) {
            clearTimeout(captureTimer);
            captureTimer = null;
        }
        if (floatWindow) {
            floatWindow.classList.remove('visible');
            floatWindow.querySelector('.mwi-float-body').innerHTML = '';
            delete floatWindow.dataset.manualResize;
            floatWindow.style.width = '';
            floatWindow.style.height = '';
        }
    }

    // ─── body直下のtooltip出現を監視 ───
    const skippedPoppers = new WeakSet();

    const bodyObserver = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            for (const node of mutation.addedNodes) {
                if (node.nodeType !== Node.ELEMENT_NODE) continue;
                if (node.id === FLOAT_WINDOW_ID) continue;

                let tooltipWrapper = null;
                if (node.getAttribute?.('role') === 'tooltip') {
                    tooltipWrapper = node;
                } else if (node.querySelector?.('[role="tooltip"]')) {
                    tooltipWrapper = node;
                }
                if (!tooltipWrapper) continue;
                if (skippedPoppers.has(tooltipWrapper)) continue;

                // ★ 即座にCSSクラスで非表示(インラインstyleに干渉しない)
                tooltipWrapper.classList.add(HIDE_CLASS);

                if (captureTimer) clearTimeout(captureTimer);
                const target = tooltipWrapper;
                captureTimer = setTimeout(() => {
                    captureTimer = null;
                    if (!target.isConnected) {
                        target.classList.remove(HIDE_CLASS);
                        return;
                    }

                    if (shouldExclude(target)) {
                        // ★ hover tooltipではない → クラスを外すだけで完全に元通り
                        target.classList.remove(HIDE_CLASS);
                        skippedPoppers.add(target);
                        return;
                    }

                    // hover tooltipと確定
                    snapshotToFloat(target);
                    startSync(target);
                    // 元のtooltipは非表示クラスを付けたまま維持
                }, 150);
            }

            for (const node of mutation.removedNodes) {
                if (node.nodeType !== Node.ELEMENT_NODE) continue;
                if (trackedTooltip && (node === trackedTooltip || node.contains?.(trackedTooltip))) {
                    frozen = true;
                    stopSync();
                }
            }
        }
    });

    // ─── 初期化 ───
    function init() {
        const waitForGame = setInterval(() => {
            const root = document.getElementById('root');
            if (root && root.children.length > 0) {
                clearInterval(waitForGame);
                floatWindow = createFloatWindow();
                bodyObserver.observe(document.body, { childList: true, subtree: false });
                console.log('[MWI Tooltip Float v8] Initialized');
            }
        }, 500);
    }

    init();
})();