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.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

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

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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