用腳本插件解決NicoNico網站提示訊息不夠醒目的問題

前陣子刷歌時有件事滿困擾我的
我偶爾會開VPN 然後可能就放著不關繼續上網
但在開著VPN的時候如果按nico的影片加入清單或是加入あとで見る的話
就會跳出加入失敗的訊息
開著VPN按いいね或是清單超過上限時好像也會
但是nico的通知訊息都是跳在右下超小一個
成功跟失敗看起來又沒有差別 很容易被忽略

之前還有個小問題是nico免費會員只允許同時開三個分頁
一開始我是用vivaldi的休眠分頁功能讓我開在背景的分頁不被抓到
之後我發現有個tampermonkey腳本叫niconico unlimit tabs可以繞過限制
這個訊息太小的問題我就想能不能也用腳本解決
就叫grok幫我寫看看(最近免費可以用4.1的beta版算好用
失敗幾個版本之後終於成功讓他訊息至少是醒目的了

既然弄出來了就想說丟在這裡留個紀錄也當做分享
但要先講一下這腳本潛在的問題

  • 看他code寫法好像是抓訊息裡的關鍵字,有符合就會變成醒目顯示,所以搞不好會偶爾有誤抓不是錯誤訊息卻醒目顯示的情況,相反的也可能明明沒成功但沒有醒目顯示,像我剛測如果那首歌已經在歌單裡又按一次加清單的話那個訊息就不會醒目顯示,其實直接在code裡改就好了但我懶,反正這版可以用就好
  • 還有發現一個情況是播放器載入失敗的情況顯示在播放器上的失敗訊息也會變成這個放大紅框的樣式
  • 因為是叫AI寫的所以我對這個腳本說真的也不太有把握,像是他會不會吃太多效能,或是他另外有沒有又改了什麼背景裡的屬性是我沒發現的,所以要使用還是斟酌看看風險

以下分享腳本code
至於tampermonkey插件的安裝用法什麼的這邊就不多贅述
這邊多說一點以免我以後忘記
就是vivaldi裝好tampermonkey之後要去把權限打開才會生效
我是把研究人員模式也打開了 但好像其實不用
之前用edge好像不用多這個步驟

// ==UserScript==
// @name         Niconico あとで見る 失敗通知醒目(中央大字+紅框版)
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  失敗通知移到中央、超大字、紅色邊框(無殘留黑底)
// @author       Grok
// @match        https://www.nicovideo.jp/watch/*
// @grant        GM_addStyle
// @run-at       document-body
// ==/UserScript==

(function() {
    'use strict';

    // 注入 CSS:黑半透明背景、白大字、紅色邊框
    GM_addStyle(`
        .enhanced-failure-toast {
            background-color: rgba(0, 0, 0, 0.85) !important;
            color: white !important;
            font-size: 42px !important;
            font-weight: bolder !important;
            padding: 40px 60px !important;
            border-radius: 20px !important;
            border: 6px solid red !important;  /* 紅色粗邊框 */
            position: fixed !important;
            top: 50% !important;
            left: 50% !important;
            transform: translate(-50%, -50%) !important;
            z-index: 9999999 !important;
            text-align: center !important;
            opacity: 1 !important;
            animation: none !important;
            min-width: 320px !important;
            max-width: 90vw !important;
            pointer-events: auto !important;
            box-shadow: none !important;
        }
        .enhanced-failure-toast-small {
            background-color: rgba(0, 0, 0, 0.85) !important;
            color: white !important;
            font-size: 28px !important;
            font-weight: bold !important;
            padding: 20px 30px !important;
            border-radius: 12px !important;
            border: 4px solid red !important;  /* 小版也加紅框 */
            z-index: 999999 !important;
            opacity: 1 !important;
            box-shadow: none !important;
        }
    `);

    const failureKeywords = ['失敗', 'エラー', '追加できません', '登録できません', '上限', '制限', '達しました', 'できませんでした', '上限に達しました'];

    function isFailureText(text) {
        return text && failureKeywords.some(kw => text.includes(kw));
    }

    function cleanupTarget(target) {
        if (target) {
            target.classList.remove('enhanced-failure-toast', 'enhanced-failure-toast-small');
            target.style.cssText = '';  // 強制清除所有內聯樣式
            delete target.dataset.enhanced;
        }
    }

    function applyEnhance(element) {
        if (element.dataset.enhanced) return;
        element.dataset.enhanced = 'true';

        // 嚴格向上找小型 fixed/absolute 容器(避免抓外層)
        let target = element;
        let depth = 0;
        while (target && target !== document.body && depth < 8) {
            const style = window.getComputedStyle(target);
            const width = target.offsetWidth;
            const height = target.offsetHeight;
            if ((style.position === 'fixed' || style.position === 'absolute') && width > 100 && width < 600 && height < 300) {
                break;  // 只抓典型 toast 大小
            }
            target = target.parentElement;
            depth++;
        }
        if (!target || target === document.body || target.offsetWidth > 800) {
            // fallback: 只強化文字 + 紅框,不移動
            element.style.fontSize = '32px !important';
            element.style.fontWeight = 'bolder !important';
            element.style.color = 'white !important';
            element.style.border = '4px solid red !important';
            element.style.padding = '10px !important';
            console.log('容器不適合移動,只強化文字+紅框');
            return;
        }

        const rect = target.getBoundingClientRect();
        if (rect.width > 550 || rect.height > 250) {
            target.classList.add('enhanced-failure-toast-small');
            console.log('稍大容器,強化原位置+紅框');
        } else {
            target.classList.add('enhanced-failure-toast');
            console.log('成功移到中央 + 大字 + 紅框');
        }

        // 強力監測移除,清除樣式防止殘留
        const removalObserver = new MutationObserver(() => {
            if (!document.body.contains(target)) {
                cleanupTarget(target);
                removalObserver.disconnect();
            }
        });
        removalObserver.observe(document.body, { childList: true, subtree: true });

        // 額外定時檢查(如果 toast 淡出移除)
        const checkInterval = setInterval(() => {
            if (!document.body.contains(target)) {
                cleanupTarget(target);
                clearInterval(checkInterval);
            }
        }, 500);
    }

    const observer = new MutationObserver((mutations) => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === 1) {
                    const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
                    let textNode;
                    while (textNode = walker.nextNode()) {
                        if (isFailureText(textNode.textContent.trim())) {
                            applyEnhance(textNode.parentElement || node);
                        }
                    }
                    if (node.textContent && isFailureText(node.textContent.trim())) {
                        applyEnhance(node);
                    }
                }
            });
        });
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // 輪詢補救
    let checkCount = 0;
    const interval = setInterval(() => {
        document.querySelectorAll('div, span, p, div[style*="fixed"], div[style*="absolute"]').forEach(el => {
            if (!el.dataset.enhanced && el.textContent && isFailureText(el.textContent.trim())) {
                applyEnhance(el);
            }
        });
        checkCount++;
        if (checkCount > 80) clearInterval(interval);
    }, 600);

})();

留言

這個網誌中的熱門文章

小魔女DoReMi 第二部+劇場版