PTT Web Image Preview (term.ptt.cc)

在 term.ptt.cc 預覽圖片

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         PTT Web Image Preview (term.ptt.cc)
// @namespace    http://tampermonkey.net/
// @version      1.6
// @description  在 term.ptt.cc 預覽圖片
// @author       Gemini
// @match        https://term.ptt.cc/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const PREVIEW_ID = 'ptt-center-preview-img';

    // 1. 標準圖片副檔名檢查
    const IMG_EXTENSIONS = /\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?.*)?$/i;

    // 2. Imgur 網址特徵 (針對沒有副檔名的連結)
    // 支援: imgur.com/ID, i.imgur.com/ID, m.imgur.com/ID
    const IMGUR_PATTERN = /^https?:\/\/(?:i\.|m\.)?imgur\.com\/([a-zA-Z0-9]{5,})(\/?)$/;

    let activeLinkElement = null;
    let watchDogTimer = null;

    /**
     * 判斷並取得真實圖片網址
     * @param {string} url 原始連結網址
     * @returns {string|null} 如果是圖片則回傳直連網址,否則回傳 null
     */
    function getImageUrl(url) {
        // Case A: 網址本身就有圖片副檔名 (例如 .jpg)
        if (IMG_EXTENSIONS.test(url)) {
            return url;
        }

        // Case B: Imgur 網址但沒有副檔名 (例如 https://imgur.com/PA3rQqo)
        const match = url.match(IMGUR_PATTERN);
        if (match) {
            // match[1] 是 ID (例如 PA3rQqo)
            // 強制轉為 i.imgur.com 並加上 .jpg,這樣 Imgur 才會給圖片而不是網頁
            return `https://i.imgur.com/${match[1]}.jpg`;
        }

        return null;
    }

    // 建立或取得預覽圖片元素
    function getPreviewElement() {
        let img = document.getElementById(PREVIEW_ID);
        if (!img) {
            img = document.createElement('img');
            img.id = PREVIEW_ID;

            // 解決 Imgur 403 Forbidden
            img.referrerPolicy = "no-referrer";

            // CSS 樣式
            img.style.position = 'fixed';
            img.style.top = '50%';
            img.style.left = '50%';
            img.style.transform = 'translate(-50%, -50%)';
            img.style.zIndex = '2147483647';
            img.style.maxWidth = '95vw';
            img.style.maxHeight = '95vh';
            img.style.objectFit = 'contain';
            img.style.border = '2px solid rgba(255, 255, 255, 0.5)';
            img.style.borderRadius = '8px';
            img.style.backgroundColor = '#000';
            img.style.boxShadow = '0 0 30px rgba(0,0,0,0.9)';
            img.style.pointerEvents = 'none';
            img.style.display = 'none';

            document.body.appendChild(img);
        }
        return img;
    }

    // 關閉預覽
    function removePreviewImage() {
        const img = document.getElementById(PREVIEW_ID);
        if (img) {
            img.style.display = 'none';
            img.src = '';
        }
        activeLinkElement = null;
        if (watchDogTimer) {
            clearInterval(watchDogTimer);
            watchDogTimer = null;
        }
    }

    // 滑鼠進入連結
    document.addEventListener('mouseover', function(e) {
        const target = e.target.closest('a');

        if (target && target.href) {
            // 透過 getImageUrl 檢查是否為支援的圖片連結
            const imageUrl = getImageUrl(target.href);

            if (imageUrl) {
                if (activeLinkElement === target) return;

                activeLinkElement = target;

                const img = getPreviewElement();
                img.src = imageUrl; // 使用處理過後的網址
                img.style.display = 'block';

                // WatchDog
                if (watchDogTimer) clearInterval(watchDogTimer);
                watchDogTimer = setInterval(() => {
                    if (activeLinkElement && !activeLinkElement.isConnected) {
                        removePreviewImage();
                    }
                }, 200);
            }
        }
    }, true);

    // 滑鼠離開連結
    document.addEventListener('mouseout', function(e) {
        const target = e.target.closest('a');
        if (target && target === activeLinkElement) {
            removePreviewImage();
        }
    }, true);

    // 安全機制
    window.addEventListener('keydown', function() {
        if (activeLinkElement) removePreviewImage();
    }, true);

    window.addEventListener('wheel', function() {
        if (activeLinkElement) removePreviewImage();
    }, true);

})();