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.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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