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