// ==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 '';
}
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 站点可能拦截内联