小窗预览 DX

左键长按 (或拖曳) 以开启窗口,首次使用进入菜单按需调整,并开启一个窗口拖曳位置和大小后关闭,至此全域基础设定完成,后续按需取消通用设定,即可在各式网站以各自窗口加速浏览。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name               Small Window Preview DX
// @name:zh-TW         小窗預覽 DX
// @name:zh-CN         小窗预览 DX
// @name:zh-HK         小窗預覽 DX
// @description        Left-click long-press (or drag) to open window. First use: enter menu to adjust, open a window, drag to set position/size, then close. Global base settings done. Later, disable global settings to customize per site for accelerated browsing.
// @description:zh-TW  左鍵長按 (或拖曳) 以開啟視窗,首次使用進入選單按需調整,並開啟一個視窗拖曳位置和大小後關閉,至此全域基礎設定完成,後續按需取消通用設定,即可在各式網站以各自視窗加速瀏覽。
// @description:zh-CN  左键长按 (或拖曳) 以开启窗口,首次使用进入菜单按需调整,并开启一个窗口拖曳位置和大小后关闭,至此全域基础设定完成,后续按需取消通用设定,即可在各式网站以各自窗口加速浏览。
// @description:zh-HK  左鍵長按 (或拖曳) 以開啟視窗,首次使用進入選單按需調整,並開啟一個視窗拖曳位置和大小後關閉,至此全域基礎設定完成,後續按需取消通用設定,即可在各式網站以各自視窗加速瀏覽。
// @version            2026
// @author             hiisme,人民的勤务员 <[email protected]>,Dxzy(AI)
// @match              *://*/*
// @grant              GM_registerMenuCommand
// @grant              GM_unregisterMenuCommand
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_deleteValue
// @namespace          https://github.com/ChinaGodMan/UserScripts
// @license            MIT
// ==/UserScript==

(function () {
    'use strict'
    const lang = navigator.language.startsWith('zh') ? 'zh' : 'en'
    const i18n = {
        zh: {
            menuSettings: '⚙️ 設定選單',
            menuGlobalPrefix: '🌐 通用設定:',
            on: '開',
            off: '關',
            promptTitle: '小窗預覽 DX 設定選單',
            promptExit: '0: 退出',
            promptInput: '請輸入選項 (0 退出):',
            optGlobal: '採取通用設定',
            optMode: '選擇觸發方式',
            optEffective: '長按生效時間',
            optDuration: '長按觸發時間',
            optDragTimeout: '拖曳逾時時間',
            optClickClose: '點擊關閉小窗',
            optScrollClose: '滾動關閉小窗',
            optProgress: '顯示倒數計時進度條',
            optDragProgress: '顯示拖曳逾時進度條',
            optSavePos: '記錄窗口位置',
            modeLong: '長按',
            modeDrag: '拖曳',
            modeBoth: '兩者都用',
            divider: '===================='
        },
        en: {
            menuSettings: '⚙️ Settings Menu',
            menuGlobalPrefix: '🌐 Global Settings: ',
            on: 'ON',
            off: 'OFF',
            promptTitle: 'Small Window Preview DX Settings',
            promptExit: '0: Exit',
            promptInput: 'Enter option (0 to exit):',
            optGlobal: 'Use Global Settings',
            optMode: 'Trigger Mode',
            optEffective: 'Long-press Effective Time',
            optDuration: 'Long-press Duration',
            optDragTimeout: 'Drag Timeout',
            optClickClose: 'Click to Close Window',
            optScrollClose: 'Scroll to Close Window',
            optProgress: 'Show Countdown Progress',
            optDragProgress: 'Show Drag Timeout Progress',
            optSavePos: 'Save Window Position',
            modeLong: 'Long-press',
            modeDrag: 'Drag',
            modeBoth: 'Both',
            divider: '===================='
        }
    }
    const t = i18n[lang]
    const state = {
        isDragging: false,
        linkToPreload: null,
        popupWindow: null,
        acrylicOverlay: null,
        progressBar: null,
        dragprogressBar: null,
        dragintervalId: null,
        startTime: null,
        pressTimer: null,
        popupChecker: null,
        resizeDebounceTimer: null,
        lastSavedConfig: null,
        popupReady: false,
        currentHostname: window.location.hostname,
        registeredMenuIds: [],
        dragMoveHandler: null
    }
    let config = {}
    let siteConfig = {}
    function updateConfig() {
        const globalUseGlobal = GM_getValue('useGlobalSettings', true)
        const storedSiteConfig = GM_getValue(`siteConfig_${state.currentHostname}`, {})
        const useGlobal = storedSiteConfig.hasOwnProperty('useGlobalSettings') ? storedSiteConfig.useGlobalSettings : globalUseGlobal
        const baseConfig = {
            closeOnMouseClick: GM_getValue('closeOnMouseClick', true),
            closeOnScroll: GM_getValue('closeOnScroll', true),
            longPressEffective: GM_getValue('longPressEffective', 0),
            longPressDuration: GM_getValue('longPressDuration', 500),
            dragTimeOut: GM_getValue('dragTimeOut', 700),
            actionMode: GM_getValue('actionMode', 1),
            showCountdown: GM_getValue('showCountdown', false),
            showCountdowndrag: GM_getValue('showCountdowndrag', false),
            saveWindowConfig: GM_getValue('saveWindowConfig', true),
            useGlobalSettings: useGlobal
        }
        if (useGlobal) {
            config = baseConfig
            siteConfig = {}
        } else {
            siteConfig = storedSiteConfig
            config = { ...baseConfig, ...siteConfig }
        }
        reWindowConfig()
    }
    function setConfigValue(key, value) {
        if (config.useGlobalSettings) {
            GM_setValue(key, value)
        } else {
            siteConfig[key] = value
            GM_setValue(`siteConfig_${state.currentHostname}`, siteConfig)
        }
        config[key] = value
    }
    function getWindowConfig() {
        if (config.useGlobalSettings) {
            const cw = GM_getValue('custom_windowWidth', 0)
            const ch = GM_getValue('custom_windowHeight', 0)
            const cl = GM_getValue('custom_screenLeft', 0)
            const ct = GM_getValue('custom_screenTop', 0)
            if (cw && ch && cl && ct) {
                return { width: cw, height: ch, top: ct, left: cl }
            }
            return {
                width: 870,
                height: 530,
                top: (window.screen.height - 530) / 3,
                left: (window.screen.width - 870) / 2
            }
        } else {
            const windowConfigs = GM_getValue('SitewindowConfigs', [])
            const currentHostName = state.currentHostname
            for (const cfg of windowConfigs) {
                const host = Array.isArray(cfg.hostName) ? cfg.hostName : [cfg.hostName]
                if (host.includes(currentHostName)) {
                    return {
                        width: cfg.width || 870,
                        height: cfg.height || 530,
                        top: cfg.top !== undefined ? cfg.top : (window.screen.height - 530) / 3,
                        left: cfg.left !== undefined ? cfg.left : (window.screen.width - 870) / 2
                    }
                }
            }
            return {
                width: 870,
                height: 530,
                top: (window.screen.height - 530) / 3,
                left: (window.screen.width - 870) / 2
            }
        }
    }
    function reWindowConfig() {
        const wc = getWindowConfig()
        config.windowWidth = wc.width
        config.windowHeight = wc.height
        config.screenLeft = wc.left
        config.screenTop = wc.top
    }
    function delay(ms) { return new Promise(r => setTimeout(r, ms)) }
    async function preloadLink(link) {
        const el = document.createElement('link')
        el.rel = 'preload'
        el.href = link
        el.as = '*/*'
        el.importance = 'high'
        document.head.appendChild(el)
        await delay(1)
    }
    function createAcrylicOverlay() {
        const ov = document.createElement('div')
        ov.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:9999;pointer-events:auto;background:rgba(0,0,0,0.01)'
        if (config.closeOnMouseClick) {
            ov.addEventListener('click', (e) => { if (e.target === ov) closePopupWindow() })
        }
        document.body.appendChild(ov)
        return ov
    }
    function removeAcrylicOverlay() {
        if (state.acrylicOverlay) {
            state.acrylicOverlay.remove()
            state.acrylicOverlay = null
        }
    }
    function debouncedSaveConfig(width, height, left, top, hostname) {
        if (state.resizeDebounceTimer) clearTimeout(state.resizeDebounceTimer)
        state.resizeDebounceTimer = setTimeout(() => {
            const key = `${width}x${height}@${left},${top}`
            if (state.lastSavedConfig === key) return
            state.lastSavedConfig = key
            saveWindowConfig(width, height, left, top, hostname)
        }, 500)
    }
    function saveWindowConfig(width, height, left, top, HostName = state.currentHostname) {
        config.windowWidth = width
        config.windowHeight = height
        config.screenLeft = left
        config.screenTop = top
        if (config.useGlobalSettings) {
            GM_setValue('custom_windowWidth', width)
            GM_setValue('custom_windowHeight', height)
            GM_setValue('custom_screenLeft', left)
            GM_setValue('custom_screenTop', top)
        } else {
            let configs = GM_getValue('SitewindowConfigs', [])
            let updated = false
            for (const cfg of configs) {
                const host = Array.isArray(cfg.hostName) ? cfg.hostName : [cfg.hostName]
                if (host.includes(HostName)) {
                    Object.assign(cfg, { width, height, top, left })
                    updated = true
                    break
                }
            }
            if (!updated) {
                configs.push({ name: HostName, hostName: HostName, width, height, top, left })
            }
            GM_setValue('SitewindowConfigs', configs)
        }
    }
    function safeReadPopupProps(win) {
        try {
            return {
                width: win.outerWidth,
                height: win.outerHeight,
                left: win.screenX,
                top: win.screenY,
                closed: win.closed
            }
        } catch (e) {
            return null
        }
    }
    function openPopupWindow(link) {
        reWindowConfig()
        if (state.popupWindow && (!config.closeOnMouseClick || !config.closeOnScroll)) {
            state.popupWindow.close()
        }
        if (!state.popupWindow || state.popupWindow.closed) {
            state.acrylicOverlay = createAcrylicOverlay()
            state.popupWindow = window.open(link, '_blank', `width=${config.windowWidth},height=${config.windowHeight},left=${config.screenLeft},top=${config.screenTop},resizable=yes,scrollbars=yes`)
            state.popupReady = false
            state.lastSavedConfig = null
            if (state.popupChecker) {
                clearInterval(state.popupChecker)
                state.popupChecker = null
            }
            state.popupChecker = setInterval(() => {
                if (!state.popupWindow || state.popupWindow.closed) {
                    removeAcrylicOverlay()
                    clearInterval(state.popupChecker)
                    state.popupChecker = null
                    state.popupWindow = null
                }
            }, 500)
            setTimeout(() => {
                const props = safeReadPopupProps(state.popupWindow)
                if (props && !props.closed && config.saveWindowConfig) {
                    debouncedSaveConfig(props.width, props.height, props.left, props.top, state.currentHostname)
                    state.popupReady = true
                    try {
                        state.popupWindow.addEventListener('resize', () => {
                            const p = safeReadPopupProps(state.popupWindow)
                            if (p) debouncedSaveConfig(p.width, p.height, p.left, p.top, state.currentHostname)
                        })
                        state.popupWindow.addEventListener('move', () => {
                            const p = safeReadPopupProps(state.popupWindow)
                            if (p) debouncedSaveConfig(p.width, p.height, p.left, p.top, state.currentHostname)
                        })
                    } catch (e) { }
                }
            }, 1500)
            state.popupWindow.postMessage({ type: 'qinwuyuan_init', hostname: state.currentHostname }, '*')
        }
    }
    function closePopupWindow() {
        if (state.popupWindow && !state.popupWindow.closed) {
            state.popupWindow.close()
            state.popupWindow = null
            removeAcrylicOverlay()
            if (state.popupChecker) {
                clearInterval(state.popupChecker)
                state.popupChecker = null
            }
            if (state.resizeDebounceTimer) {
                clearTimeout(state.resizeDebounceTimer)
                state.resizeDebounceTimer = null
            }
            if (state.linkToPreload) {
                const el = document.querySelector(`link[href="${state.linkToPreload}"]`)
                if (el) el.remove()
                state.linkToPreload = null
            }
            window.removeEventListener('scroll', closePopupOnScroll)
        }
    }
    function closePopupOnScroll() {
        if (state.popupWindow && !state.popupWindow.closed) closePopupWindow()
    }
    function createProgressBar(colorStart = '#4caf50', colorEnd = '#81c784') {
        if (!config.showCountdown && !config.showCountdowndrag) return null
        const pb = document.createElement('div')
        pb.style.cssText = `position:fixed;height:6px;width:5%;background:linear-gradient(to right,${colorStart},${colorEnd});border-radius:3px;box-shadow:0 2px 5px rgba(0,0,0,0.3);z-index:9999`
        document.body.appendChild(pb)
        return pb
    }
    function removeProgressBar() {
        if (state.progressBar) { state.progressBar.remove(); state.progressBar = null }
    }
    function handleDragMove(e) {
        if (state.dragprogressBar) {
            state.dragprogressBar.style.left = `${e.clientX}px`
            state.dragprogressBar.style.top = `${e.clientY + 30}px`
        }
    }
    function handleDragStart(event) {
        if (event.button !== 0) return
        const linkEl = event.target.tagName === 'A' ? event.target : event.target.closest('a')
        if (!linkEl) return
        if (config.showCountdowndrag && config.dragTimeOut > 0) {
            state.dragprogressBar = createProgressBar('#ff9800', '#f44336')
            if (state.dragprogressBar) {
                state.dragprogressBar.style.display = 'block'
                state.dragprogressBar.style.width = '5%'
                state.startTime = Date.now()
                clearInterval(state.dragintervalId)
                state.dragintervalId = setInterval(() => {
                    const elapsed = Date.now() - state.startTime
                    const progress = Math.max(5 - (elapsed / config.dragTimeOut) * 5, 0)
                    state.dragprogressBar.style.width = `${progress}%`
                    if (progress <= 0) {
                        state.isDragging = false
                        clearInterval(state.dragintervalId)
                        state.dragprogressBar.style.display = 'none'
                    }
                }, 100)
                if (!state.dragMoveHandler) {
                    state.dragMoveHandler = handleDragMove
                }
                window.addEventListener('drag', state.dragMoveHandler)
            }
        }
        const link = linkEl.href
        state.isDragging = true
        state.linkToPreload = link
        preloadLink(link).then(() => {
            if (config.closeOnScroll) window.addEventListener('scroll', closePopupOnScroll, { once: true })
        })
    }
    function handleDragEnd(event) {
        if (state.dragMoveHandler) {
            window.removeEventListener('drag', state.dragMoveHandler)
        }
        if (state.dragprogressBar) {
            clearInterval(state.dragintervalId)
            state.dragprogressBar.style.display = 'none'
            state.dragprogressBar = null
        }
        if (event.clientY < 1) { state.isDragging = false; return }
        if (state.isDragging && state.linkToPreload) {
            state.isDragging = false
            openPopupWindow(state.linkToPreload)
            state.linkToPreload = null
        }
    }
    function handleMouseDown(event) {
        if (event.button !== 0) return
        const linkEl = event.target.tagName === 'A' ? event.target : event.target.closest('a')
        if (!linkEl) return
        let isDragging = false
        let isMouseDown = true
        const onMove = () => { isDragging = true; clearTimeout(state.pressTimer); removeProgressBar() }
        const onUp = () => { isMouseDown = false; clearTimeout(state.pressTimer); removeProgressBar() }
        document.addEventListener('dragstart', onMove, { once: true })
        document.addEventListener('mouseup', onUp, { once: true })
        document.addEventListener('keydown', onUp, { once: true })
        setTimeout(() => {
            if (!isDragging && isMouseDown) {
                state.progressBar = createProgressBar()
                if (state.progressBar) {
                    const dur = Math.max(config.longPressDuration, 0) + 'ms'
                    state.progressBar.style.left = `${event.clientX}px`
                    state.progressBar.style.top = `${event.clientY + 20}px`
                    state.progressBar.style.transition = `width ${dur} linear`
                    requestAnimationFrame(() => { state.progressBar.style.width = '0' })
                }
            }
            state.pressTimer = setTimeout(() => {
                if (!isDragging && isMouseDown) {
                    const link = linkEl.href
                    state.linkToPreload = link
                    preloadLink(link).then(() => openPopupWindow(link))
                }
                removeProgressBar()
            }, config.longPressDuration)
        }, config.longPressEffective)
    }
    function handleMouseUp() {
        clearTimeout(state.pressTimer)
        removeProgressBar()
    }
    function handleMouseLeave() {
        clearTimeout(state.pressTimer)
        removeProgressBar()
    }
    function handleWheel() {
        if (config.closeOnScroll) closePopupWindow()
    }
    function handleClick(event) {
        if (event.target === state.acrylicOverlay) removeAcrylicOverlay()
    }
    function setupEventListeners() {
        document.body.removeEventListener('dragstart', handleDragStart)
        document.body.removeEventListener('dragend', handleDragEnd)
        document.body.removeEventListener('mousedown', handleMouseDown)
        document.body.removeEventListener('mouseup', handleMouseUp)
        document.body.removeEventListener('mouseleave', handleMouseLeave)
        document.body.removeEventListener('wheel', handleWheel)
        document.body.removeEventListener('click', handleClick)
        if (config.actionMode === 1 || config.actionMode === 0) {
            document.body.addEventListener('mousedown', handleMouseDown)
            document.body.addEventListener('mouseup', handleMouseUp)
            document.body.addEventListener('mouseleave', handleMouseLeave)
        }
        if (config.actionMode === 2 || config.actionMode === 0) {
            document.body.addEventListener('dragstart', handleDragStart)
            document.body.addEventListener('dragend', handleDragEnd)
        }
        document.body.addEventListener('wheel', handleWheel, { passive: true })
        document.body.addEventListener('click', handleClick)
    }
    function showSettingsMenu() {
        const menuText = `${t.promptTitle}
${t.divider}
1: ${t.optGlobal} (${config.useGlobalSettings ? t.on : t.off})
2: ${t.optMode} (${config.actionMode === 1 ? t.modeLong : config.actionMode === 2 ? t.modeDrag : t.modeBoth})
3: ${t.optEffective} (${config.longPressEffective}ms)
4: ${t.optDuration} (${config.longPressDuration}ms)
5: ${t.optDragTimeout} (${config.dragTimeOut}ms)
6: ${t.optClickClose} (${config.closeOnMouseClick ? t.on : t.off})
7: ${t.optScrollClose} (${config.closeOnScroll ? t.on : t.off})
8: ${t.optProgress} (${config.showCountdown ? t.on : t.off})
9: ${t.optDragProgress} (${config.showCountdowndrag ? t.on : t.off})
a: ${t.optSavePos} (${config.saveWindowConfig ? t.on : t.off})
0: ${t.promptExit}
${t.divider}
${t.promptInput}`
        const choice = prompt(menuText, '0')
        if (choice === null || choice === '0') return
        switch (choice) {
            case '1': {
                const newVal = !config.useGlobalSettings
                if (newVal) {
                    GM_deleteValue(`siteConfig_${state.currentHostname}`)
                    siteConfig = {}
                    config.useGlobalSettings = true
                } else {
                    siteConfig = GM_getValue(`siteConfig_${state.currentHostname}`, {})
                    siteConfig.useGlobalSettings = false
                    GM_setValue(`siteConfig_${state.currentHostname}`, siteConfig)
                    config.useGlobalSettings = false
                }
                updateMenuCommands()
                break
            }
            case '2': {
                const m = prompt(`${t.optMode}\n1: ${t.modeLong}\n2: ${t.modeDrag}\n0: ${t.modeBoth}`, config.actionMode)
                if (m !== null) { setConfigValue('actionMode', parseInt(m, 10)); setupEventListeners() }
                break
            }
            case '3': {
                const v = prompt(`${t.optEffective} (ms):`, config.longPressEffective)
                if (v !== null) setConfigValue('longPressEffective', parseInt(v, 10))
                break
            }
            case '4': {
                const v = prompt(`${t.optDuration} (ms):`, config.longPressDuration)
                if (v !== null) setConfigValue('longPressDuration', parseInt(v, 10))
                break
            }
            case '5': {
                const v = prompt(`${t.optDragTimeout} (ms):`, config.dragTimeOut)
                if (v !== null) setConfigValue('dragTimeOut', parseInt(v, 10))
                break
            }
            case '6': setConfigValue('closeOnMouseClick', !config.closeOnMouseClick); break
            case '7': {
                setConfigValue('closeOnScroll', !config.closeOnScroll)
                if (config.closeOnScroll) {
                    window.addEventListener('scroll', closePopupOnScroll, { once: true })
                } else {
                    window.removeEventListener('scroll', closePopupOnScroll)
                }
                break
            }
            case '8': setConfigValue('showCountdown', !config.showCountdown); break
            case '9': setConfigValue('showCountdowndrag', !config.showCountdowndrag); break
            case 'a': setConfigValue('saveWindowConfig', !config.saveWindowConfig); break
            default: break
        }
        showSettingsMenu()
    }
    function toggleGlobalSettings() {
        const newVal = !config.useGlobalSettings
        if (newVal) {
            GM_deleteValue(`siteConfig_${state.currentHostname}`)
            siteConfig = {}
            config.useGlobalSettings = true
        } else {
            siteConfig = GM_getValue(`siteConfig_${state.currentHostname}`, {})
            siteConfig.useGlobalSettings = false
            GM_setValue(`siteConfig_${state.currentHostname}`, siteConfig)
            config.useGlobalSettings = false
        }
        updateMenuCommands()
    }
    function updateMenuCommands() {
        while (state.registeredMenuIds.length > 0) {
            const id = state.registeredMenuIds.pop()
            try { GM_unregisterMenuCommand(id) } catch (e) { }
        }
        const menuId1 = GM_registerMenuCommand(t.menuSettings, showSettingsMenu)
        const menuId2 = GM_registerMenuCommand(`${t.menuGlobalPrefix}${config.useGlobalSettings ? t.on : t.off}`, toggleGlobalSettings)
        state.registeredMenuIds.push(menuId1, menuId2)
    }
    window.addEventListener('message', (e) => {
        if (e.data?.type === 'qinwuyuan' && config.saveWindowConfig) {
            debouncedSaveConfig(e.data.width, e.data.height, e.data.left, e.data.top, e.data.hostname)
        }
    })
    updateConfig()
    setupEventListeners()
    updateMenuCommands()
})()