Small Window Preview DX

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.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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