// ==UserScript== // @name 全站链接小窗 // @namespace https://qq5855144.github.io/ // @version 1.0.1 // @description 全站链接小窗浏览器:多标签、历史导航、拖拽高度、站点记忆、媒体预览、黑白名单、沉浸式顶栏、手势滑动切换标签(屏蔽浏览器横滑手势,关闭后自动恢复);小窗内跳转地址同步(含跨域回传) // @match *://*/* // @run-at document-idle // @grant none // ==/UserScript== (function () { 'use strict'; var FRAME_MODE = window.top !== window.self; if (!FRAME_MODE) { if (window.__MT_GLOBAL_LINK_WINDOW__) return; window.__MT_GLOBAL_LINK_WINDOW__ = true; // 供同源 iframe 快速识别小窗宿主并读取当前配置 try { Object.defineProperty(window, '__MT_SW_PANEL_HOST__', { get: function () { return { openMode: cfg.openMode }; }, configurable: true }); } catch (e) {} } else { // 供其他用户脚本识别"我正运行在小窗 iframe 中" try { window.__MT_SW_FRAME__ = true; } catch (e) {} } var KEY = 'mt-global-link-window-v2'; var DEFAULTS = { height: 92, animation: 280, adaptiveColor: true, rememberHeight: true, multiTab: true, sameOriginHistory: true, mediaPreview: true, openExternal: false, openMode: 'window', // 'window' 小窗 iframe 预览 | 'tab' 浏览器新标签页(用户脚本100%生效) autoFallback: true, // iframe 被 X-Frame-Options/CSP 拦截时自动转新标签页 newTabUrl: 'https://qq5855144.github.io/Minimal-Desktop/', // 新建标签页默认打开的地址 swipeTab: true, // 手势滑动切换标签页(标题栏 + 内容区) noFrame: ['github.com','gist.github.com','gitee.com','gitlab.com'], // 已知禁止被嵌入 iframe 的站点,直接新标签页打开 whitelist: [], blacklist: [], searchEngines: [ 'google.com','google.ad','google.ae','google.am','google.at','google.az', 'google.ba','google.be','google.bg','google.ca','google.ch','google.cl', 'google.co.id','google.co.il','google.co.in','google.co.jp','google.co.kr', 'google.co.nz','google.pl','google.pt','google.ro','google.ru','google.sa', 'google.se','google.sg','google.th','google.tr','google.tw','google.ua', 'google.co.uk','google.com.au','google.com.br','google.com.hk', 'google.com.sg','google.com.tw','google.com.vn','bing.com','bing.cn', 'baidu.com','baidu.cn','sogou.com','sogou.cn','so.com','360.cn', 'haosou.com','sm.cn','shenma.com','duckduckgo.com','search.yahoo.com', 'yahoo.com','yandex.com','yandex.ru','yandex.eu','naver.com', 'search.brave.com','brave.com','startpage.com','ecosia.org','qwant.com', 'kagi.com','searx.be','searxng.org','perplexity.ai' ] }; var cfg = loadConfig(); var state = { opened: false, tabs: [], active: -1, panel: null, mask: null, head: null, tabbar: null, content: null, track: null, color: {r:83,g:188,b:245,a:1}, noFrameHosts: {}, historyMarker: false, closingByHistory: false, drag: null }; function loadConfig() { try { return merge(DEFAULTS, JSON.parse(localStorage.getItem(KEY) || 'null') || {}); } catch (e) { return merge(DEFAULTS, {}); } } function merge(base, extra) { var out = {}; Object.keys(base).forEach(function (k) { out[k] = Array.isArray(base[k]) ? base[k].slice() : base[k]; }); Object.keys(extra || {}).forEach(function (k) { out[k] = Array.isArray(extra[k]) ? extra[k].slice() : extra[k]; }); return out; } function saveConfig() { try { localStorage.setItem(KEY, JSON.stringify(cfg)); } catch (e) {} } function hostOf(url) { try { return new URL(url, location.href).hostname.toLowerCase().replace(/^www\./, ''); } catch (e) { return ''; } } function domainMatch(host, domain) { domain = String(domain || '').toLowerCase().replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0]; return !!domain && (host === domain || host.endsWith('.' + domain)); } function inList(host, list) { return (list || []).some(function (d) { return domainMatch(host, d); }); } function isSearchEngine(url) { return inList(hostOf(url), cfg.searchEngines); } // 禁止嵌入小窗的站点(默认名单 + 本次会话学到的) function isNoFrameSite(url) { var h = hostOf(url); return !!h && (inList(h, cfg.noFrame) || !!state.noFrameHosts[h]); } function learnNoFrame(host) { if (!host || state.noFrameHosts[host]) return; state.noFrameHosts[host] = true; try { if (!inList(host, cfg.noFrame)) { cfg.noFrame = (cfg.noFrame || []).concat([host]); saveConfig(); } } catch (e) {} } function isWeb(url) { try { var p = new URL(url, location.href).protocol; return p === 'http:' || p === 'https:'; } catch (e) { return false; } } function shouldIntercept(a) { if (!a || !a.href || !isWeb(a.href)) return false; if (a.closest && a.closest('.mt-sw-root')) return false; if (a.hasAttribute('download')) return false; if ((a.getAttribute('rel') || '').toLowerCase().split(/\s+/).indexOf('external') >= 0) return false; var url = a.href, host = hostOf(url); // 用户脚本/样式安装文件放行:交给浏览器顶层打开(Via/Tampermonkey 会弹出安装确认), // 若被小窗 iframe 拦截则只是显示源码文本,无法安装 if (/\.user\.(js|css)($|[?#])/i.test(url)) return false; if (isSearchEngine(url) || inList(host, cfg.whitelist) || inList(host, cfg.blacklist)) return false; try { var u = new URL(url), c = new URL(location.href); if (u.origin === c.origin && u.pathname === c.pathname && u.search === c.search && u.hash && u.hash !== c.hash) return false; } catch (e) {} return true; } function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return {'&':'&','<':'<','>':'>','"':'"'}[c]; }); } function icon(name) { var map = { back:'', forward:'', reload:'', external:'', close:'', plus:'', settings:'' }; return '' + map[name] + ''; } function addStyle() { if (document.getElementById('mt-sw-style-v2')) return; var s = document.createElement('style'); s.id = 'mt-sw-style-v2'; s.textContent = '.mt-sw-root{position:fixed;inset:0;z-index:2147483646;pointer-events:none;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}' + '.mt-sw-mask{position:absolute;inset:0;background:rgba(0,0,0,.45);opacity:0;transition:opacity .25s ease;pointer-events:auto}' + '.mt-sw-panel{position:absolute;left:0;right:0;bottom:0;height:var(--mt-sw-height,92vh);max-height:98vh;min-height:45vh;background:#fff;border-radius:18px 18px 0 0;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 -5px 28px rgba(0,0,0,.22);transform:translateY(100%);transition:transform .28s cubic-bezier(.32,.72,.36,1);pointer-events:auto}' + '.mt-sw-show .mt-sw-mask{opacity:1}.mt-sw-show .mt-sw-panel{transform:translateY(0)}' + '.mt-sw-resize{height:7px;flex:none;cursor:ns-resize;touch-action:none;background:transparent;position:relative;z-index:4}.mt-sw-resize:after{content:"";position:absolute;left:50%;top:3px;width:42px;height:4px;border-radius:4px;transform:translateX(-50%);background:rgba(0,0,0,.18)}' + '.mt-sw-head{display:flex;align-items:center;gap:5px;padding:4px 8px;background:var(--mt-sw-color,#53BCF5);color:var(--mt-sw-fg,#fff);transition:background .22s ease,color .22s ease;flex:none}' + '.mt-sw-nav{display:flex;gap:4px;flex:none}.mt-sw-btn{width:32px;height:32px;border:0;border-radius:50%;padding:0;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,.2);color:inherit;cursor:pointer}.mt-sw-btn:active{transform:scale(.94)}.mt-sw-btn:disabled{opacity:.38}' + '.mt-sw-title{min-width:0;flex:1;font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding:0 4px}' + '.mt-sw-tabbar{display:flex;align-items:center;gap:4px;padding:4px 7px;background:var(--mt-sw-color,#53BCF5);color:var(--mt-sw-fg,#fff);border-bottom:1px solid rgba(255,255,255,.24);overflow-x:auto;scrollbar-width:none;flex:none;transition:background .22s ease,color .22s ease}.mt-sw-tabbar::-webkit-scrollbar{display:none}' + '.mt-sw-tab{height:30px;min-width:64px;max-width:76px;display:flex;align-items:center;gap:3px;padding:0 7px;border:0;border-radius:8px;background:rgba(255,255,255,.18);font-size:12px;color:inherit;flex:none;cursor:pointer;transition:background .18s ease,color .18s ease}.mt-sw-tab.active{background:rgba(255,255,255,.92);color:#222;box-shadow:none}.mt-sw-tab span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;text-align:center}.mt-sw-tab b{font-weight:400;font-size:15px;line-height:1;color:inherit;opacity:.72}.mt-sw-add{width:30px;height:30px;border:0;border-radius:8px;background:rgba(255,255,255,.14);color:inherit;flex:none}' + '.mt-sw-content{position:relative;flex:1;min-height:0;background:#fff}.mt-sw-frame{position:absolute;inset:0;width:100%;height:100%;border:0;background:#fff}.mt-sw-media{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#111}.mt-sw-audio{position:absolute;left:16px;right:16px;top:50%;transform:translateY(-50%);width:calc(100% - 32px)}' + '.mt-sw-error{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;color:#666;background:#fff;padding:24px;text-align:center}.mt-sw-error button{border:0;border-radius:9px;padding:9px 14px;background:#eee}' + '.mt-sw-settings{position:absolute;inset:0;background:rgba(255,255,255,.98);z-index:8;overflow:auto;padding:18px;display:none}.mt-sw-settings.show{display:block}.mt-sw-settings h3{margin:0 0 16px;font-size:18px}.mt-sw-setting{padding:12px 0;border-bottom:1px solid #eee}.mt-sw-setting label{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:14px}.mt-sw-setting input[type=number]{width:76px}.mt-sw-setting select{font-size:14px;padding:6px 8px;border:1px solid #ddd;border-radius:8px;background:#fff;color:#333}.mt-sw-setting textarea{width:100%;box-sizing:border-box;min-height:70px;margin-top:8px;border:1px solid #ddd;border-radius:8px;padding:8px;resize:vertical}.mt-sw-settings-actions{display:flex;gap:8px;margin-top:16px}.mt-sw-settings-actions button{flex:1;border:0;border-radius:9px;padding:10px}.mt-sw-primary{background:#53BCF5;color:#fff}.mt-sw-secondary{background:#eee}.mt-sw-tip{font-size:11px;color:#999;line-height:1.5;margin-top:5px}'; (document.head || document.documentElement).appendChild(s); // 主题变量覆盖样式:让标签/按钮跟随顶栏明暗(主样式行过长,单独注入) var tv = document.createElement('style'); tv.textContent = '.mt-sw-tab{background:var(--mt-sw-tab-bg,rgba(255,255,255,.18));color:var(--mt-sw-tab-fg,#fff)}.mt-sw-tab.active{background:var(--mt-sw-tab-active-bg,rgba(255,255,255,.92));color:var(--mt-sw-tab-active-fg,#222)}.mt-sw-tab b{opacity:.72}.mt-sw-add{background:var(--mt-sw-add-bg,rgba(255,255,255,.14));color:var(--mt-sw-tab-fg,#fff)}.mt-sw-btn{background:var(--mt-sw-btn-bg,rgba(255,255,255,.2))}.mt-sw-tabbar{border-bottom-color:var(--mt-sw-tabline,rgba(255,255,255,.24))}'; (document.head || document.documentElement).appendChild(tv); // 标签页轨道:并排布局 + 平滑平移切换动画 var tw = document.createElement('style'); tw.textContent = '.mt-sw-track{position:absolute;inset:0;display:flex;height:100%;will-change:transform;transition:transform .34s cubic-bezier(.22,.68,.28,1)}.mt-sw-track>.mt-sw-frame,.mt-sw-track>.mt-sw-media,.mt-sw-track>.mt-sw-audio,.mt-sw-track>.mt-sw-error{position:relative;inset:auto;left:auto;right:auto;top:auto;bottom:auto;flex:0 0 100%;width:100%;min-width:100%;height:100%;margin:0}.mt-sw-track>.mt-sw-audio{display:flex;align-items:center;justify-content:center}.mt-sw-track>.mt-sw-audio{width:100%}.mt-sw-track>.mt-sw-error{display:flex}'; (document.head || document.documentElement).appendChild(tw); // 严格 CSP 站点可能拦截内联