接上一版:
注意:
本次更新可能与部分移动端设备不兼容,
故另起一新话题!
谨慎更新:
(论坛近期升级了,
许多链接需要排除,
早知道搞白名单了 )
持续更新…
施工进展:
- 加载动画;
- 适配黑暗模式;
- 自定义预览窗口 (宽度 + 高度)
- 预览窗口 锁定(防误触) 功能;
- 适配超高(宽)屏+超高分辨率
计划:
- 站内链接改用白名单判断;
- 阻止预览窗口下方的网页滚动;
- 加载动画 + 实用功能(提醒);
- 预览窗口的 “台前调度” 功能;
…
—分割线—
太长不看
碎碎念:
由于不喜欢在新标签页中打开网页,我一直在寻找优雅的预览方法:
(这也是我用arc浏览器的主要原因)
桌面端可以使用的预览插件有很多,但是想在移动设备上实现预览,基本没有可用的方法;
(注:仅折腾过iOS/iPadOS的浏览器);
在Discourse论坛点击帖子后,默认是改变直接改变当前页面的链接;
那么在回退后,原页面有可能会刷新;
(注:仅个人观察结果,未排除其他插件等因素)
某日,我在Greasy Fork上,发现了一个链接预览的脚本,
试了一下,移动端也可以正常使用;
太长不看
碎碎念:
使用一段时间后,发现预览窗口与原页面之间的边界比较模糊;
于是我就在这个脚本的基础上,修修改改;
目前为止还没有折腾出啥好结果,bug有点多,留着自用了 ;
(具体折腾过程略)
下面分享的代码,我仅做了小小的修改:
- 匹配了linux.do 和meta.appinn.net(小众软件);
- 外观/功能 上:修改了iframe样式,效果类似Arc浏览器;
- 加载动画;
- 适配黑暗模式;
- 未完待续…欢迎留言建议
—分割线—
点击查看 (新版) 脚本
// ==UserScript==
// @name [Discourse] 论坛内链接预览 "Arc"版 -20240811.1
// @version 20240811.1
// @description 更新:适配超高(宽)屏+超高分辨率
//
// @match https://linux.do/*
// @match https://meta.appinn.net/*
//
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
//
// @icon https://www.svgrepo.com/show/330308/discourse.svg
// ==/UserScript==
(function() {
'use strict';
console.log("脚本初始化...");
// 获取用户设置
let previewWidth = GM_getValue('previewWidth', 90);
let previewHeight = GM_getValue('previewHeight', 90);
let currentLockMode = GM_getValue('lockMode', false);
let closeButtonPosition = GM_getValue('closeButtonPosition', 'right');
// 存储所有创建的模态窗口
let allModals = [];
// 添加样式到页面头部
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = `
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.iframe-container {
position: relative;
width: ${previewWidth}%;
height: ${previewHeight}%;
display: grid;
}
iframe {
position: absolute;
min-width: 100%;
min-height: 100%;
border: 8px solid rgba(224, 224, 224, 0.9);
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
display: none;
}
.loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
border: 5px solid #f3f3f3;
border-top: 5px solid #3498db;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
display: none;
}
@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
.close-button {
position: absolute;
top: calc(50% - ${previewHeight/2}% + 10px);
${closeButtonPosition === 'left' ? 'left: calc(50% - ' + previewWidth/2 + '% - 35px);' : 'right: calc(50% - ' + previewWidth/2 + '% - 35px);'}
background-color: rgba(255, 255, 255, 0.7);
color: #333;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
font-size: 16px;
line-height: 30px;
text-align: center;
cursor: pointer;
opacity: 0.7;
transition: all 0.3s;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
display: none;
z-index: 1001;
}
.close-button:hover {
opacity: 1;
background-color: rgba(255, 255, 255, 0.9);
transform: scale(1.1);
}
.close-button:active {
transform: scale(0.95);
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
20%, 40%, 60%, 80% { transform: translateX(10px); }
}
.shake {
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 7px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* 设置弹窗样式 */
.settings-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
z-index: 1001;
display: none;
width: 300px;
}
.settings-modal h2 {
margin-top: 0;
color: #333;
font-size: 18px;
text-align: center;
}
.settings-modal .input-group {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.settings-modal label {
flex: 1;
margin-right: 10px;
}
.settings-modal input[type="number"] {
width: 50px;
padding: 5px;
margin-right: 5px;
}
.settings-modal .percent {
margin-left: 5px;
}
.settings-modal .checkbox-group {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.settings-modal .checkbox-group label {
margin-left: 5px;
}
.settings-modal .radio-group {
display: flex;
flex-direction: column;
margin-bottom: 10px;
}
.settings-modal .radio-group label {
margin-bottom: 5px;
}
.settings-modal .buttons {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.settings-modal button {
padding: 5px 10px;
border: none;
border-radius: 5px;
cursor: pointer;
flex: 1;
margin: 0 5px;
}
.settings-modal button.reset {
background-color: #3498db;
color: white;
}
.settings-modal button.cancel {
background-color: #e74c3c;
color: white;
}
.settings-modal button.confirm {
background-color: #2ecc71;
color: white;
}
/* 黑暗模式 */
@media (prefers-color-scheme: dark) {
iframe {
border: 8px solid rgba(51, 51, 51, 0.9);
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.05);
}
::-webkit-scrollbar-track {
background: #333333;
}
::-webkit-scrollbar-thumb {
background: #666666;
}
::-webkit-scrollbar-thumb:hover {
background: #888888;
}
.loader {
border: 5px solid #333;
border-top: 5px solid #3498db;
}
.settings-modal {
background-color: #333;
color: #fff;
}
.settings-modal h2 {
color: #fff;
}
.settings-modal input {
background-color: #444;
color: #fff;
border: 1px solid #555;
}
.close-button {
background-color: rgba(51, 51, 51, 0.7);
color: #fff;
}
.close-button:hover {
background-color: rgba(51, 51, 51, 0.9);
}
}
`;
document.head.appendChild(style);
// 创建模态窗口、iframe容器和加载动画
function createModal() {
const modal = document.createElement('div');
modal.className = 'modal';
const iframeContainer = document.createElement('div');
iframeContainer.className = 'iframe-container';
const iframe = document.createElement('iframe');
const loader = document.createElement('div');
loader.className = 'loader';
iframeContainer.appendChild(loader);
iframeContainer.appendChild(iframe);
modal.appendChild(iframeContainer);
// 创建关闭按钮
const closeButton = document.createElement('button');
closeButton.className = 'close-button';
closeButton.innerHTML = '✕';
closeButton.style.display = 'none';
modal.appendChild(closeButton);
document.body.appendChild(modal);
// 将新创建的模态窗口添加到数组中
allModals.push(modal);
return { modal, iframe, loader, closeButton };
}
// 创建设置弹窗
const settingsModal = document.createElement('div');
settingsModal.className = 'settings-modal';
settingsModal.innerHTML = `
<h2>预览窗口设置</h2>
<div class="input-group">
<label for="widthInput">宽度:</label>
<input type="number" id="widthInput" min="10" max="100">
<span class="percent">%</span>
</div>
<div class="input-group">
<label for="heightInput">高度:</label>
<input type="number" id="heightInput" min="10" max="100">
<span class="percent">%</span>
</div>
<div class="checkbox-group">
<input type="checkbox" id="lockModeCheckbox">
<label for="lockModeCheckbox">启用锁定(防误触)模式</label>
</div>
<div class="radio-group">
<label>关闭按钮位置:</label>
<label>
<input type="radio" name="closeButtonPosition" value="left"> 左上角
</label>
<label>
<input type="radio" name="closeButtonPosition" value="right"> 右上角
</label>
</div>
<div class="buttons">
<button class="reset">重置</button>
<button class="cancel">取消</button>
<button class="confirm">确认</button>
</div>
`;
document.body.appendChild(settingsModal);
// 注册菜单命令
GM_registerMenuCommand("设置预览窗口", openSettingsModal);
// 打开设置弹窗
function openSettingsModal() {
document.getElementById('widthInput').value = previewWidth;
document.getElementById('heightInput').value = previewHeight;
document.getElementById('lockModeCheckbox').checked = currentLockMode;
document.querySelector(`input[name="closeButtonPosition"][value="${closeButtonPosition}"]`).checked = true;
settingsModal.style.display = 'block';
}
// 事件监听器
settingsModal.querySelector('.reset').addEventListener('click', () => {
document.getElementById('widthInput').value = 90;
document.getElementById('heightInput').value = 90;
document.getElementById('lockModeCheckbox').checked = false;
document.querySelector('input[name="closeButtonPosition"][value="right"]').checked = true;
});
settingsModal.querySelector('.cancel').addEventListener('click', () => {
settingsModal.style.display = 'none';
});
settingsModal.querySelector('.confirm').addEventListener('click', () => {
const newWidth = parseInt(document.getElementById('widthInput').value);
const newHeight = parseInt(document.getElementById('heightInput').value);
if (newWidth && newHeight && newWidth >= 10 && newWidth <= 100 && newHeight >= 10 && newHeight <= 100) {
previewWidth = newWidth;
previewHeight = newHeight;
GM_setValue('previewWidth', newWidth);
GM_setValue('previewHeight', newHeight);
const newLockMode = document.getElementById('lockModeCheckbox').checked;
if (newLockMode !== currentLockMode) {
currentLockMode = newLockMode;
GM_setValue('lockMode', currentLockMode);
updateAllModalsLockMode();
}
closeButtonPosition = document.querySelector('input[name="closeButtonPosition"]:checked').value;
GM_setValue('closeButtonPosition', closeButtonPosition);
updateStyles();
} else {
alert('请输入10到100之间的有效数值。');
return;
}
settingsModal.style.display = 'none';
});
// 更新所有模态窗口的锁定模式
function updateAllModalsLockMode() {
allModals.forEach(modal => {
updateModalLockMode(modal);
});
// 向所有iframe发送更新消息
allModals.forEach(modal => {
const iframe = modal.querySelector('iframe');
if (iframe) {
iframe.contentWindow.postMessage({ type: 'updateLockMode', lockMode: currentLockMode }, '*');
}
});
}
// 更新单个模态窗口的锁定模式
function updateModalLockMode(modal) {
const closeButton = modal.querySelector('.close-button');
const iframe = modal.querySelector('iframe');
if (iframe.style.display === 'inline-block') {
closeButton.style.display = currentLockMode ? 'block' : 'none';
}
updateModalClickBehavior(modal);
}
// 更新模态窗口的点击行为
function updateModalClickBehavior(modal) {
modal.onclick = function(event) {
if (event.target === modal) {
if (currentLockMode) {
shakeModal(modal.querySelector('.iframe-container'));
} else {
closeModal(modal);
}
}
};
}
// 更新样式
function updateStyles() {
const newStyle = `
.iframe-container {
width: ${previewWidth}%;
height: ${previewHeight}%;
}
.close-button {
top: calc(50% - ${previewHeight/2}% + 10px);
${closeButtonPosition === 'left' ? 'left: calc(50% - ' + previewWidth/2 + '% - 35px); right: auto;' : 'right: calc(50% - ' + previewWidth/2 + '% - 35px); left: auto;'}
}
`;
style.innerHTML += newStyle;
allModals.forEach(modal => {
const iframeContainer = modal.querySelector('.iframe-container');
const closeButton = modal.querySelector('.close-button');
iframeContainer.style.width = `${previewWidth}%`;
iframeContainer.style.height = `${previewHeight}%`;
closeButton.style.top = `calc(50% - ${previewHeight/2}% + 10px)`;
if (closeButtonPosition === 'left') {
closeButton.style.left = `calc(50% - ${previewWidth/2}% - 35px)`;
closeButton.style.right = 'auto';
} else {
closeButton.style.right = `calc(50% - ${previewWidth/2}% - 35px)`;
closeButton.style.left = 'auto';
}
});
}
// 打开模态窗口的函数
function openModal(url) {
const { modal, iframe, loader, closeButton } = createModal();
modal.style.display = 'flex';
loader.style.display = 'block';
iframe.style.display = 'none';
closeButton.style.display = 'none';
iframe.src = url;
iframe.onload = function() {
loader.style.display = 'none';
iframe.style.display = 'inline-block';
if (currentLockMode) {
closeButton.style.display = 'block';
}
setupIframeContentListener(iframe);
};
// 更新模态窗口的点击行为
updateModalClickBehavior(modal);
// 关闭按钮点击事件
closeButton.addEventListener('click', () => closeModal(modal));
}
// 关闭模态窗口的函数
function closeModal(modal) {
const iframe = modal.querySelector('iframe');
const loader = modal.querySelector('.loader');
const closeButton = modal.querySelector('.close-button');
iframe.src = '';
modal.style.display = 'none';
loader.style.display = 'none';
iframe.style.display = 'none';
closeButton.style.display = 'none';
// 从数组中移除关闭的模态窗口
const index = allModals.indexOf(modal);
if (index > -1) {
allModals.splice(index, 1);
}
// 移除模态窗口元素
modal.remove();
}
// 抖动窗口的函数
function shakeModal(element) {
element.classList.add('shake');
setTimeout(() => {
element.classList.remove('shake');
}, 820); // 抖动动画持续时间为 820ms
}
// 检查链接是否为内部话题链接
function isInternalTopicLink(url) {
const currentDomain = window.location.hostname;
const urlObject = new URL(url, window.location.origin);
// // 获取链接元素
// let linkElement = document.querySelector(`a[href="${urlObject.pathname}"]`);
// 获取当前页面的 topic ID
const currentTopicMatch = window.location.pathname.match(/\/t\/topic\/(\d+)(\/|\b)/);
const currentTopicId = currentTopicMatch ? currentTopicMatch[1] : null;
// 获取链接中的 topic ID
const linkTopicMatch = urlObject.pathname.match(/\/t\/topic\/(\d+)(\/|\b)/);
const linkTopicId = linkTopicMatch ? linkTopicMatch[1] : null;
// 检查是否为相同 domain 且不同 topic
return (urlObject.hostname === currentDomain) &&
linkTopicId !== null &&
linkTopicId !== currentTopicId;
}
// 处理链接点击的函数
function handleLinkClick(e) {
console.log("Link clicked:", e.target); // 调试日志
let target = e.target.closest('a'); // 找到最近的 <a> 标签
if (!target) return; // 如果点击的不是链接或其子元素,直接返回
// 检查是否是我们想要处理的链接类型
if (target.classList.contains('raw-link') ||
(target.closest('.fps-topic') && target.classList.contains('search-link')) ||
isInternalTopicLink(target.href)) {
// // 检查链接是否在导航栏或其他功能区域
// if (target.closest('header') || target.closest('nav') || target.closest('.d-header')|| target.closest('.d-sidebar')|| target.closest('.user-menu')) {
// return; // 如果在这些区域,不处理链接
// }
e.preventDefault();
e.stopPropagation(); // 防止事件冒泡
let url = target.href;
if (url.startsWith('/')) {
url = window.location.origin + url;
}
console.log("Opening URL:", url); // 调试日志
openModal(url);
}
}
// 设置iframe内容的事件监听器
function setupIframeContentListener(iframe) {
iframe.addEventListener('load', function() {
try {
const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
iframeDocument.addEventListener('click', handleIframeLinkClick);
// 向iframe发送当前的锁定模式
iframe.contentWindow.postMessage({ type: 'updateLockMode', lockMode: currentLockMode }, '*');
} catch (e) {
console.error("无法访问iframe内容:", e);
}
});
}
// 处理iframe内链接点击的函数
function handleIframeLinkClick(e) {
let target = e.target.closest('a');
if (!target) return;
if (target.classList.contains('raw-link') ||
(target.closest('.fps-topic') && target.classList.contains('search-link')) ||
isInternalTopicLink(target.href)) {
e.preventDefault();
e.stopPropagation();
let url = target.href;
if (url.startsWith('/')) {
url = new URL(url, e.target.baseURI).href;
}
openModal(url);
}
}
// 监听来自iframe的消息
window.addEventListener('message', function(event) {
if (event.data.type === 'updateLockMode') {
currentLockMode = event.data.lockMode;
updateAllModalsLockMode();
}
});
// 使用事件委托来监听整个文档的点击事件
document.addEventListener('click', handleLinkClick, true);
// 添加 MutationObserver 来处理动态加载的内容
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const links = node.querySelectorAll('a');
links.forEach(link => {
if (link.classList.contains('raw-link') ||
(link.closest('.fps-topic') && link.classList.contains('search-link')) ||
isInternalTopicLink(link.href)) {
link.addEventListener('click', handleLinkClick);
}
});
}
});
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
console.log("脚本加载完成"); // 调试日志
})();
点击查看 测试结果
经个人测试:
202408011.1 (新版)
浏览器 | 运行 | 功能 |
---|---|---|
桌面端 | ☐ 是否正常? | ☐ 可嵌套不止1层? |
Chrome + Violentmonkey | ||
Firefox + Violentmonkey | ||
Safari + Stay | ||
Orion + Violentmonkey | ||
移动端 | ☐ 是否正常? | ☐ 可嵌套不止1层? |
Orion + Violentmonkey | ||
Safari + Stay | ||
Safari + Addons | ||
Safari + Makeover | … | … |
未完待续… | … | … |
点击查看 移动端 -待更新
平板
竖屏
普通模式
黑暗模式
横屏
普通模式
黑暗模式
手机
竖屏
普通模式
黑暗模式
横屏
普通模式
黑暗模式
—分割线—
致谢
感谢论坛里的佬们,
你们经常分享自己编写的脚本,质量普遍很高
—分割线—
其他:
如果有对 链接预览 感兴趣的佬,可以一起交流;
----202408011.1