Thêm liên kết bìa video vào trang chi tiết video Bilibili

Hiển thị một liên kết trực tiếp đến ảnh bìa video trên trang chi tiết video Bilibili.

// ==UserScript==
// @name         Bilibili视频详情页追加视频封面链接
// @name:zh-CN         Bilibili视频详情页追加视频封面链接
// @name:en      Add video cover link to Bilibili video detail page
// @name:ar          إضافة رابط صورة غلاف الفيديو إلى صفحة تفاصيل فيديو Bilibili
// @description:ar    يعرض رابطًا مباشرًا لصورة غلاف الفيديو في صفحة تفاصيل الفيديو على Bilibili.
// @name:bg          Добавяне на връзка към обложката на видеото към страницата с подробности за видеоклипа в Bilibili
// @description:bg    Показва директна връзка към обложката на видеото на страницата с подробности за видеоклипа в Bilibili.
// @name:cs          Přidat odkaz na obálku videa na stránku s podrobnostmi o videu Bilibili
// @description:cs    Zobrazí přímý odkaz na obálku videa na stránce s podrobnostmi o videu na Bilibili.
// @name:da          Tilføj videocoverlink til Bilibili-videodetaljeside
// @description:da    Viser et direkte link til videocoveret på Bilibilis videodetaljeside.
// @name:de          Video-Cover-Link zur Bilibili-Videodetailseite hinzufügen
// @description:de    Zeigt einen direkten Link zum Video-Cover auf der Bilibili-Videodetailseite an.
// @name:el          Προσθήκη συνδέσμου εξωφύλλου βίντεο στη σελίδα λεπτομερειών βίντεο Bilibili
// @description:el    Εμφανίζει έναν άμεσο σύνδεσμο προς το εξώφυλλο βίντεο στη σελίδα λεπτομερειών βίντεο του Bilibili.
// @name:eo          Aldoni videokovrilan ligilon al Bilibili-videa detala paĝo
// @description:eo    Montras rektan ligilon al la videokovrilo sur la Bilibili-videa detala paĝo.
// @name:es          Agregar enlace de portada de video a la página de detalles del video de Bilibili
// @description:es    Muestra un enlace directo a la portada del video en la página de detalles del video de Bilibili.
// @name:fi          Lisää videon kansikuvalinkki Bilibilin videotietosivulle
// @description:fi    Näyttää suoran linkin videon kansikuvaan Bilibilin videotietosivulla.
// @name:fr          Ajouter un lien de couverture vidéo à la page de détails de la vidéo Bilibili
// @description:fr    Affiche un lien direct vers la couverture de la vidéo sur la page de détails de la vidéo Bilibili.
// @name:fr-CA       Ajouter un lien de couverture vidéo à la page de détails de la vidéo Bilibili
// @description:fr-CA    Affiche un lien direct vers la couverture de la vidéo sur la page de détails de la vidéo Bilibili.
// @name:he          הוסף קישור לעטיפת וידאו לדף הפרטים של סרטון Bilibili
// @description:he    מציג קישור ישיר לעטיפת הוידאו בדף הפירוט של סרטון בביליבילי.
// @name:hr          Dodaj vezu naslovnice videa na stranicu s detaljima videozapisa Bilibili
// @description:hr    Prikazuje izravnu vezu na naslovnicu videa na stranici s detaljima videozapisa na Bilibili.
// @name:hu          Videóborító link hozzáadása a Bilibili videó részletező oldalához
// @description:hu    Közvetlen linket jelenít meg a videó borítójához a Bilibili videó részletező oldalán.
// @name:id          Tambahkan tautan sampul video ke halaman detail video Bilibili
// @description:id    Menampilkan tautan langsung ke sampul video di halaman detail video Bilibili.
// @name:it          Aggiungi link di copertina video alla pagina dei dettagli del video di Bilibili
// @description:it    Mostra un link diretto alla copertina del video nella pagina dei dettagli del video di Bilibili.
// @name:ja          Bilibiliビデオ詳細ページにビデオカバーリンクを追加
// @description:ja    Bilibiliのビデオ詳細ページにビデオカバーへの直接リンクを表示します。
// @name:ka          Bilibili ვიდეოს დეტალური გვერდზე ვიდეოს ყდის ბმულის დამატება
// @description:ka    აჩვენებს ვიდეოს ყდის პირდაპირ ბმულს Bilibili-ის ვიდეოს დეტალურ გვერდზე.
// @name:ko          Bilibili 비디오 세부 정보 페이지에 비디오 커버 링크 추가
// @description:ko    Bilibili 비디오 세부 정보 페이지에서 비디오 커버에 대한 직접 링크를 표시합니다.
// @name:nb          Legg til videocoverlenke til Bilibili videodetaljside
// @description:nb    Viser en direkte lenke til videocoveret på Bilibili videodetaljside.
// @name:nl          Voeg een video-coverlink toe aan de Bilibili-videodetailpagina
// @description:nl    Toont een directe link naar de video-cover op de Bilibili-videodetailpagina.
// @name:pl          Dodaj link do okładki wideo na stronie szczegółów filmu Bilibili
// @description:pl    Wyświetla bezpośredni link do okładki wideo na stronie szczegółów filmu Bilibili.
// @name:pt-BR       Adicionar link da capa do vídeo à página de detalhes do vídeo Bilibili
// @description:pt-BR    Exibe um link direto para a capa do vídeo na página de detalhes do vídeo no Bilibili.
// @name:ro          Adăugați un link de copertă video la pagina de detalii video Bilibili
// @description:ro    Afișează un link direct către coperta video pe pagina de detalii video Bilibili.
// @name:ru          Добавить ссылку на обложку видео на страницу сведений о видео Bilibili
// @description:ru    Отображает прямую ссылку на обложку видео на странице сведений о видео Bilibili.
// @name:sk          Pridať odkaz na obálku videa na stránku s podrobnosťami o videu Bilibili
// @description:sk    Zobrazuje priamy odkaz na obálku videa na stránke s podrobnosťami o videu na Bilibili.
// @name:sr          Додај везу омота видео снимка на страницу са детаљима видео снимка Bilibili
// @description:sr    Приказује директну везу до омота видео снимка на страници са детаљима видео снимка на Bilibili.
// @name:sv          Lägg till videocoverlänk till Bilibili videodetaljsida
// @description:sv    Visar en direktlänk till videocoveret på Bilibili videodetaljsida.
// @name:th          เพิ่มลิงก์หน้าปกวิดีโอไปยังหน้าข้อมูลวิดีโอ Bilibili
// @description:th    แสดงลิงก์ตรงไปยังหน้าปกวิดีโอบนหน้าข้อมูลวิดีโอของ Bilibili
// @name:tr          Bilibili video ayrıntı sayfasına video kapak bağlantısı ekle
// @description:tr    Bilibili video ayrıntı sayfasında video kapağına doğrudan bağlantı görüntüler.
// @name:ug          Bilibili سىن تەپسىلىي بېتىگە سىن مۇقاۋىسى ئۇلىنىشى قوشۇڭ
// @description:ug    Bilibili سىن تەپسىلىي بېتىدە سىن مۇقاۋىسىغا بىۋاسىتە ئۇلىنىش كۆرسىتىدۇ.
// @name:uk          Додати посилання на обкладинку відео на сторінку відомостей про відео Bilibili
// @description:uk    Відображає пряме посилання на обкладинку відео на сторінці відомостей про відео Bilibili.
// @name:vi          Thêm liên kết bìa video vào trang chi tiết video Bilibili
// @description:vi    Hiển thị một liên kết trực tiếp đến ảnh bìa video trên trang chi tiết video Bilibili.
// @name:zh          Bilibili视频详情页追加视频封面链接
// @description:zh    在Bilibili视频详情页面显示视频封面图片的直接链接。
// @description:zh-CN    在Bilibili视频详情页面显示视频封面图片的直接链接。
// @name:zh-HK       Bilibili視頻詳情頁追加視頻封面鏈接
// @description:zh-HK    在Bilibili視頻詳情頁面顯示視頻封面圖片的直接鏈接。
// @name:zh-SG       Bilibili视频详情页追加视频封面链接
// @description:zh-SG    在Bilibili视频详情页面显示视频封面图片的直接链接。
// @name:zh-TW       Bilibili視頻詳情頁追加視頻封面鏈接
// @description:zh-TW    在Bilibili視頻詳情頁面顯示視頻封面圖片的直接鏈接。
// @namespace    http://tampermonkey.net/
// @version      0.1.0
// @description  在视频详情页追加视频封面链接
// @description:en  Add video cover link to video detail page
// @namespace    http://tampermonkey.net/
// @description  Adds a link to the video cover in the info section and displays the cover image below the recommendations footer on Bilibili video pages.
// @author       aspen138
// @match        https://www.bilibili.com/video/*
// @icon         https://www.bilibili.com/favicon.ico
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_notification
// @grant        window.onurlchange
// @license      MIT
// ==/UserScript==


// Acknowledgement: Gemini 2.5 Pro 03-25


(function() {
    'use strict';

    // ↓↓↓↓↓↓↓↓↓模板,建议直接复制 //

    // 自定义 urlchange 事件(用来监听 URL 变化)
    function addUrlChangeEvent() {
        if (window.onurlchange === undefined) { // Only add if Tampermonkey hasn't provided it
            history.pushState = ( f => function pushState(){
                var ret = f.apply(this, arguments);
                window.dispatchEvent(new Event('pushstate'));
                window.dispatchEvent(new Event('urlchange'));
                return ret;
            })(history.pushState);

            history.replaceState = ( f => function replaceState(){
                var ret = f.apply(this, arguments);
                window.dispatchEvent(new Event('replacestate'));
                window.dispatchEvent(new Event('urlchange'));
                return ret;
            })(history.replaceState);

            window.addEventListener('popstate',()=>{
                window.dispatchEvent(new Event('urlchange'))
            });
            console.log("Custom 'urlchange' event listener added.");
        } else {
             console.log("Using built-in 'urlchange' event listener.");
        }
    }


    var menu_ALL = [
        ['menu_isEnableAppendCover', '添加视频封面链接和图片', '添加视频封面链接和图片功能', true] // Default changed to true for convenience
    ], menu_ID = [];

    // Initialize default values
    for (let i=0; i<menu_ALL.length; i++){
        if (GM_getValue(menu_ALL[i][0]) === null){ // Use strict comparison for null
            GM_setValue(menu_ALL[i][0], menu_ALL[i][3]);
        }
    }

    // 注册脚本菜单
    function registerMenuCommand() {
        // Clear existing menus before re-registering
        for (let i = 0; i < menu_ID.length; i++) {
            if (menu_ID[i]) { // Check if ID exists before trying to unregister
                 try {
                    GM_unregisterMenuCommand(menu_ID[i]);
                 } catch (e) {
                    console.warn("Could not unregister menu command:", menu_ID[i], e);
                 }
            }
        }
        menu_ID = []; // Reset the array

        for (let i = 0; i < menu_ALL.length; i++) {
            menu_ALL[i][3] = GM_getValue(menu_ALL[i][0]); // Update current status from storage
            const isEnabled = menu_ALL[i][3];
            const commandLabel = `${isEnabled ? '✅' : '❌'} ${menu_ALL[i][1]}`;
            const menuName = menu_ALL[i][0];
            const menuTips = menu_ALL[i][2];

             // Use a closure to capture the correct variables for the callback
            (function(currentStatus, name, tips) {
                menu_ID[i] = GM_registerMenuCommand(commandLabel, function() {
                    menu_switch(currentStatus, name, tips);
                });
            })(isEnabled, menuName, menuTips);
        }
    }

    // 菜单开关
    function menu_switch(menu_status, Name, Tips) {
        const newState = !menu_status; // Toggle the state
        GM_setValue(Name, newState);
        GM_notification({
            text: `已${newState ? '开启' : '关闭'} [${Tips}] 功能\n(刷新网页后生效)`, // Simplified message
            timeout: 3500,
            onclick: function(){ location.reload(); }
        });
        registerMenuCommand(); // Update menu state immediately
    };

    // ↑↑↑↑↑↑↑↑↑↑↑↑模板,建议直接复制 //

    // --- Script Core Logic ---

    const INFO_CONTAINER_SELECTOR = '.video-info-detail-list.video-info-detail-content';
    const FOOTER_SELECTOR = '.rec-footer[data-v-17ce950e]'; // More specific selector if needed
    const COVER_LINK_CLASS = 'bili-cover-link-item'; // Custom class for the link container
    const COVER_IMAGE_CONTAINER_CLASS = 'bili-cover-image-container'; // Custom class for the image container

    let currentBVid = null; // Keep track of the current video ID
    let checkInterval = null; // Interval timer handle
    const CHECK_INTERVAL_MS = 500; // Check every 500ms
    const MAX_CHECKS = 40; // Try for 20 seconds max

    // Main function to add cover link and image
    function addCoverElements() {
        const infoContainer = document.querySelector(INFO_CONTAINER_SELECTOR);
        const recFooter = document.querySelector(FOOTER_SELECTOR);
        const imageMetaTag = document.head.querySelector('meta[itemprop="image"]');

        // Check if all necessary elements are present
        if (!infoContainer || !recFooter || !imageMetaTag) {
            // console.log('Waiting for elements...');
            return false; // Indicate elements are not ready
        }

        // Check if we've already processed this specific info container
        if (infoContainer.dataset.coverProcessed === 'true') {
            // console.log('Already processed this container.');
            return true; // Indicate processing is done or already happened
        }

        // Extract cover URL
        const coverImgUrlRaw = imageMetaTag.getAttribute('content');
        if (!coverImgUrlRaw) {
            console.warn('Cover image meta tag found, but content is empty.');
            return false; // Cannot proceed without URL
        }
        const coverImgUrl = 'https://' + coverImgUrlRaw.replace(/^https?:?\/\//, '').split('@')[0];
        console.log("Cover URL found:", coverImgUrl);

        // --- 1. Add the Cover Link to Info Section ---
        // Check if link already exists (belt-and-suspenders check)
        if (!infoContainer.querySelector(`.${COVER_LINK_CLASS}`)) {
            const coverItem = document.createElement('div');
            coverItem.classList.add(COVER_LINK_CLASS, 'item'); // Add custom class and 'item' class

            // Optional: Add an icon (simplified)
            const coverIcon = document.createElement('span');
            coverIcon.textContent = '🖼️'; // Emoji icon
            coverIcon.style.marginRight = '5px';
            coverIcon.style.fontSize = '16px';
            coverIcon.style.verticalAlign = 'middle';

            const coverLink = document.createElement('a');
            coverLink.href = coverImgUrl;
            coverLink.target = '_blank';
            coverLink.rel = 'noopener noreferrer';
            coverLink.title = '点击查看封面原图 (Click to view original cover)';
            coverLink.style.verticalAlign = 'middle';

            const linkText = document.createElement('span');
            linkText.textContent = '封面 (Cover)';

            coverLink.appendChild(coverIcon);
            coverLink.appendChild(linkText);
            coverItem.appendChild(coverLink);

            infoContainer.appendChild(coverItem); // Append the new item
            console.log('Cover link appended to info section.');
        }

        // --- 2. Add the Cover Image below the Footer ---
        // Check if image container already exists as the next sibling of the footer
        if (!recFooter.nextElementSibling || !recFooter.nextElementSibling.classList.contains(COVER_IMAGE_CONTAINER_CLASS)) {
            const imageContainer = document.createElement('div');
            imageContainer.className = COVER_IMAGE_CONTAINER_CLASS;
            imageContainer.style.marginTop = '15px'; // Add some space above the image
            imageContainer.style.textAlign = 'center'; // Center the image container

            const coverImage = document.createElement('img');
            coverImage.src = coverImgUrl;
            coverImage.alt = 'Video Cover Image';
            coverImage.style.maxWidth = '100%'; // Ensure image is responsive
            coverImage.style.height = 'auto';
            coverImage.style.borderRadius = '4px'; // Optional: slightly rounded corners
            coverImage.style.cursor = 'pointer'; // Indicate it's clickable
            coverImage.title = '点击查看封面原图 (Click to view original cover)';

            // Make the image itself a link to the cover
            const imageLink = document.createElement('a');
            imageLink.href = coverImgUrl;
            imageLink.target = '_blank';
            imageLink.rel = 'noopener noreferrer';
            imageLink.appendChild(coverImage);

            imageContainer.appendChild(imageLink);

            // Insert the container *after* the footer element
            recFooter.parentNode.insertBefore(imageContainer, recFooter.nextSibling);
            console.log('Cover image appended below footer.');
        }

        // Mark the info container as processed to prevent duplicates if this function runs again
        infoContainer.dataset.coverProcessed = 'true';

        return true; // Indicate success
    }

    // Function to start the process of adding elements
    function runLogic() {
        if (!GM_getValue('menu_isEnableAppendCover', true)) {
            console.log('Cover feature is disabled.');
            return; // Exit if the feature is turned off
        }

        const newBVid = location.pathname.match(/BV[^/]+/)?.[0];
        if (!newBVid) {
            // console.log("Not a video page or BVid not found.");
            return;
        }

        // Only reset and run if the BVid changed or it's the first time
        if (newBVid !== currentBVid) {
            console.log(`New video detected (BVid: ${newBVid}). Running cover logic.`);
            currentBVid = newBVid;

            // Clear any previous interval
            if (checkInterval) {
                clearInterval(checkInterval);
                checkInterval = null;
            }

            // Reset processed flags on potential old elements (though ideally elements are replaced on navigation)
             const oldInfo = document.querySelector(`${INFO_CONTAINER_SELECTOR}[data-cover-processed="true"]`);
             if(oldInfo) delete oldInfo.dataset.coverProcessed;
             const oldImage = document.querySelector(`.${COVER_IMAGE_CONTAINER_CLASS}`);
             if(oldImage) oldImage.remove();


            let checks = 0;
            checkInterval = setInterval(() => {
                checks++;
                // console.log(`Check ${checks}/${MAX_CHECKS}`);
                try {
                    if (addCoverElements() || checks >= MAX_CHECKS) {
                        clearInterval(checkInterval);
                        checkInterval = null;
                        if (checks >= MAX_CHECKS) {
                             console.warn("Cover script timed out waiting for elements.");
                        } else {
                             console.log("Cover elements added successfully or already present.");
                        }
                    }
                } catch (error) {
                    console.error("Error during cover element check/addition:", error);
                    clearInterval(checkInterval); // Stop on error
                    checkInterval = null;
                }
            }, CHECK_INTERVAL_MS);
        } else {
             // console.log("Same video page (BVid: " + currentBVid + "), no immediate action needed unless elements reappear.");
             // Optional: Could add a check here to see if elements disappeared and need re-adding,
             // but usually page navigation handles this.
        }
    }

    // --- Initialization ---
    addUrlChangeEvent(); // Ensure URL change listener is set up
    registerMenuCommand(); // Set up the script menu

    // Run the logic on initial load
    // Use DOMContentLoaded or a small delay to ensure basic structure exists
    if (document.readyState === 'loading') {
        window.addEventListener('DOMContentLoaded', runLogic);
    } else {
        // Small delay in case runLogic depends on elements added dynamically shortly after load
        setTimeout(runLogic, 200);
    }

    // Run the logic whenever the URL changes
    window.addEventListener('urlchange', () => {
        console.log("URL change detected by script.");
        // Use a small delay to allow the page content to potentially update
        setTimeout(runLogic, 500); // Delay slightly after URL change
    });

})();