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