KaraKeep + SingleFile: X.com Preview Image Fixer

Optimizes X.com metadata for SingleFile captures, ensuring KaraKeep displays the correct main image (photos/videos/cards) instead of avatars or reply images.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         KaraKeep + SingleFile: X.com Preview Image Fixer
// @namespace    http://tampermonkey.net/
// @version      5.2
// @description  Optimizes X.com metadata for SingleFile captures, ensuring KaraKeep displays the correct main image (photos/videos/cards) instead of avatars or reply images.
// @author       You
// @match        https://x.com/*
// @match        https://twitter.com/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    let currentBestImage = null;

    // --- 核心工具:从 CSS 背景图中提取 URL ---
    function extractUrlFromStyle(style) {
        if (!style) return null;
        const match = style.match(/url\(['"]?([^'"]+)['"]?\)/);
        if (match && match[1]) {
            // 忽略 blob: 或 data: 这种无效/临时地址,只取 http 开头的
            if (match[1].startsWith('http')) {
                return match[1];
            }
        }
        return null;
    }

    // --- 核心工具:锁定“主题帖”区域 ---
    function getMainTweetScope() {
        // 1. 获取当前 URL 中的 Tweet ID (例如 18645...)
        const match = window.location.pathname.match(/status\/(\d+)/);
        if (!match) return document; // 如果不是帖子详情页,就退回全局查找

        const tweetId = match[1];

        // 2. 找到页面上所有的推文容器
        const articles = document.querySelectorAll('article[data-testid="tweet"]');

        // 3. 遍历找到那个链接指向当前 URL 的推文 (这就是主题帖)
        for (let article of articles) {
            // 检查 article 内部所有的链接
            const links = article.querySelectorAll('a');
            for (let link of links) {
                if (link.href.includes(tweetId) && link.href.includes('/status/')) {
                    // 找到了包含当前推文 ID 链接的容器,这就是主帖区域!
                    return article;
                }
            }
        }

        // 如果实在找不到,返回 document 作为一个保底,但通常不会走到这里
        return document;
    }

    // --- 核心功能:查找最佳图片 ---
    function findBestImage() {
        // 关键步骤:先锁定搜索范围!
        const scope = getMainTweetScope();
        if (!scope) return null;

        let imageUrl = null;

        // === 优先级 1: 视频封面 (Video Thumbnail) ===
        // 视频通常在 videoComponent 或 videoPlayer 中
        const videoComponents = scope.querySelectorAll('[data-testid="videoComponent"], [data-testid="videoPlayer"]');
        for (let component of videoComponents) {
            // 1.1 尝试找 <video poster="...">
            const video = component.querySelector('video');
            if (video && video.poster && video.poster.startsWith('http')) {
                imageUrl = video.poster;
                break;
            }
            // 1.2 尝试找背景图 (通常 X 用 div 做封面)
            const bgDivs = component.querySelectorAll('div[style*="background-image"]');
            for (let div of bgDivs) {
                const extracted = extractUrlFromStyle(div.getAttribute('style'));
                if (extracted) {
                    imageUrl = extracted;
                    break;
                }
            }
            if (imageUrl) break;
        }

        // === 优先级 2: 普通配图 (Tweet Photo) ===
        if (!imageUrl) {
            const tweetPhotos = scope.querySelectorAll('div[data-testid="tweetPhoto"] img');
            for (let img of tweetPhotos) {
                if (img.src && img.src.startsWith('http')) {
                    imageUrl = img.src;
                    break;
                }
            }
        }

        // === 优先级 3: 外部链接卡片 (Link Card) ===
        if (!imageUrl) {
            const cardMedia = scope.querySelector('[data-testid="card.layoutLarge.media"]');
            if (cardMedia) {
                // 3.1 卡片背景图
                const bgDivs = cardMedia.querySelectorAll('div[style*="background-image"]');
                for (let div of bgDivs) {
                    const extracted = extractUrlFromStyle(div.getAttribute('style'));
                    if (extracted) {
                        imageUrl = extracted;
                        break;
                    }
                }
                // 3.2 卡片 img 标签
                if (!imageUrl) {
                    const img = cardMedia.querySelector('img');
                    if (img && img.src) imageUrl = img.src;
                }
            }
        }

        // === 后期处理:高清化 ===
        if (imageUrl && imageUrl.includes('name=small')) {
            imageUrl = imageUrl.replace('name=small', 'name=large');
        }

        return imageUrl;
    }

    // --- 暴力替换 Meta 标签 ---
    function forceUpdateMeta() {
        const newImage = findBestImage();

        // 如果没找到符合条件的图(说明主帖可能纯文字),不要去用回复的图,保持 null
        if (!newImage && currentBestImage !== null) return;

        if (newImage && newImage !== currentBestImage) {
            currentBestImage = newImage;
            console.log('🎯 [X-Fix] Locked & Loaded Image:', newImage);

            // 1. 清理 React 的标签
            const existingMetas = document.querySelectorAll('meta[property="og:image"]');
            existingMetas.forEach(meta => {
                if (meta.getAttribute('data-sf-override') !== 'true') {
                    meta.remove();
                }
            });

            // 2. 写入我们的标签
            let myMeta = document.querySelector('meta[data-sf-override="true"]');
            if (!myMeta) {
                myMeta = document.createElement('meta');
                myMeta.setAttribute('property', 'og:image');
                myMeta.setAttribute('data-sf-override', 'true');
                document.head.appendChild(myMeta);
            }
            myMeta.content = newImage;

            // 3. 顺便修补 twitter:image
            const twitterImg = document.querySelector('meta[name="twitter:image"]');
            if (twitterImg) twitterImg.content = newImage;
        }
    }

    // --- 执行循环 ---
    setInterval(forceUpdateMeta, 1000);

    // SingleFile 保存前强制执行一次
    window.addEventListener('single-file-on-before-capture-request', (e) => {
        forceUpdateMeta();
    });

})();