20240803.3 [油猴脚本][Discourse]论坛内链接预览 "Arc"版 !适配 🍎移动端;匹配 linux.do 和meta.appinn.net (小众软件)

:fire: 持续更新…

:ladder: 施工计划:

  1. :white_check_mark: 加载动画;
  2. :white_check_mark: 适配黑暗模式;
  3. 自定义(宽度 + 高度 /加载动画相关…);
  4. 预览窗口的 锁定(防误触) 功能;
  5. 预览窗口的 “台前调度” 功能;

—分割线—

太长不看

碎碎念:
由于不喜欢在新标签页中打开网页,我一直在寻找优雅的预览方法:
(这也是我用arc浏览器的主要原因)

桌面端可以使用的预览插件有很多,但是想在移动设备上实现预览,基本没有可用的方法;
(注:仅折腾过iOS/iPadOS的浏览器);

在Discourse论坛点击帖子后,默认是改变直接改变当前页面的链接;
那么在回退后,原页面有可能会刷新;
(注:仅个人观察结果,未排除其他插件等因素)

某日,我在Greasy Fork上,发现了一个链接预览的脚本
试了一下,移动端也可以正常使用;

太长不看

碎碎念:
使用一段时间后,发现预览窗口与原页面之间的边界比较模糊;
于是我就在这个脚本的基础上,修修改改;
目前为止还没有折腾出啥好结果,bug有点多,留着自用了 :smiling_face_with_tear:
(具体折腾过程略)

下面分享的代码,我仅做了小小的修改:

  1. :white_check_mark: 匹配了linux.do 和meta.appinn.net(小众软件);
  2. :white_check_mark: 外观/功能 上:修改了iframe样式,效果类似Arc浏览器;
  3. :white_check_mark: 加载动画;
  4. :white_check_mark: 适配黑暗模式;
  5. 未完待续…欢迎留言建议 :smiling_face_with_three_hearts:

image

点击查看 脚本
// ==UserScript==
// @name         [Discourse] 论坛内链接预览 "Arc"版 -20240803.3
// @version      20240803.3
// @description  点击论坛内链接 -> 在像Arc浏览器的窗口中预览  (阻止默认的页面跳转)
// @match        https://linux.do/*
// @match        https://meta.appinn.net/*
// @grant        none
//
// @icon         https://www.svgrepo.com/show/330308/discourse.svg
// ==/UserScript==

(function() {
    'use strict';
    console.log("脚本初始化...");
    // 添加样式到页面头部
    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 {
            width: 90%;
            height: 90%;
            position: relative;
        }
        iframe {
            width: calc(100% - 16px);
            height: calc(100% - 16px);
            border: 8px solid #e0e0e0;
            border-radius: 16px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
        }
        .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); }
        }
        /* 滚动条样式 */
        ::-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;
        }
        /* 黑暗模式 */
        @media (prefers-color-scheme: dark) {
            iframe {
                border: 8px solid #333333;
                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;
            }
        }
    `;
    document.head.appendChild(style);

    // 创建模态窗口、iframe容器和加载动画
    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(iframe);
    iframeContainer.appendChild(loader);
    modal.appendChild(iframeContainer);
    document.body.appendChild(modal);

    // 打开模态窗口的函数
    function openModal(url) {
        modal.style.display = 'flex';
        loader.style.display = 'block';
        iframe.style.display = 'none';
        iframe.src = url;
        // 监听 iframe 的 onload 事件
        iframe.onload = function() {
            // 当 iframe 内容加载完成后,隐藏加载动画,显示iframe
            loader.style.display = 'none';
            iframe.style.display = 'block';
        };
    }
    // 点击模态窗口以外的区域关闭模态窗口
    modal.addEventListener('click', function(event) {
        if (event.target === modal) {
            iframe.src = ''; // 清除iframe内容
            modal.style.display = 'none';
            loader.style.display = 'none';
            iframe.style.display = 'none';
        }
    });

    // 检查链接是否为内部话题链接
    function isInternalTopicLink(url) {
        const currentDomain = window.location.hostname;
        const urlObject = new URL(url, window.location.origin);
        return (urlObject.hostname === currentDomain) && urlObject.pathname.includes('/t/');
    }

    // 处理链接点击的函数
    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')) {
                return;  // 如果在这些区域,不处理链接
            }

            e.preventDefault();
            e.stopPropagation();  // 防止事件冒泡
            let url = target.href;
            if (url.startsWith('/')) {
                url = window.location.origin + url;
            }
            console.log("Opening URL:", url);  // 调试日志
            openModal(url);
        }
    }

    // 使用事件委托来监听整个文档的点击事件
    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("脚本加载完成");  // 调试日志
})();
点击查看 测试结果

经个人测试:

浏览器 运行 功能
桌面端 ☐ 是否正常? ☐ 可嵌套不止1层?
Chrome + Violentmonkey :white_check_mark: :white_check_mark:
Firefox + Violentmonkey :white_check_mark: :white_check_mark:
Safari + Stay :white_check_mark: :white_check_mark:
Orion + Violentmonkey :white_check_mark:
移动端 ☐ 是否正常? ☐ 可嵌套不止1层?
Orion + Violentmonkey :white_check_mark: :x:
Safari + Stay :white_check_mark: :white_check_mark:
Safari + Makeover
未完待续…
点击查看 桌面端
普通模式


黑暗模式


点击查看 移动端
平板
竖屏
普通模式

黑暗模式

横屏
普通模式

黑暗模式

手机
竖屏
普通模式

黑暗模式
横屏
普通模式

黑暗模式

—分割线—

致谢

感谢论坛里的佬们,
你们经常分享自己编写的脚本,质量普遍很高 :yum:

—分割线—

其他:
如果有对 链接预览 感兴趣的佬,可以一起交流;

----20240803.3

8 个赞

感谢始皇 :smiling_face_with_three_hearts:

2 个赞

不错 很好用,请问怎么对其他网站也添加?

2 个赞

感谢分享。

  • 把你想适配的网页的源码,发送给ChatGPT/Claude,
    让它们分析网页结构/链接…;

  • 把油猴脚本也发给ChatGPT/Claude,
    让它们理解这个油猴脚本中,实现预览功能的方法;

  • 让ChatGPT/Claude编写适配你提供的网页,
    并实现你所需功能的新的油猴脚本;

1 个赞

arc 浏览器能总结内容啊,按住 shift, 鼠标悬停在链接上

是啊

好用!感谢

1 个赞

刚刚更新了,你可以参考一下思路 :innocent:

看了你的贴子去下了个arc想体验一下,结果卡在注册账号上了

1 个赞