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.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu budete musieť nainštalovať rozšírenie, ako je napríklad Tampermonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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