JPMD Blog 快速查找

在 jpmdblog.com 的貼文與列表頁增加勾選方塊,懸浮清單顯示目前標記,計數器顯示「N則未讀」,頁碼可直接輸入跳轉。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         JPMD Blog 快速查找
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  在 jpmdblog.com 的貼文與列表頁增加勾選方塊,懸浮清單顯示目前標記,計數器顯示「N則未讀」,頁碼可直接輸入跳轉。
// @author       Gemini
// @match        https://jpmdblog.com/*
// @match        https://www.jpmdblog.com/*
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // --- 設定區 ---
    const CONFIG = {
        // 偵測文章的選擇器
        articleSelector: [
            'article',
            '.post',
            '.type-post',
            '.status-publish',
            '.post-card',
            '.blog-post',
            '.entry',
            'div[class*="post-"]',
            'div[class*="entry-"]',
            '.archive-item'
        ].join(', '),

        // 忽略的選擇器
        ignoreSelector: [
            '.post-content',
            '.entry-content',
            '.article-content',
            '.post-inner',
            '.post-navigation',
            '.post-author',
            '.related-posts'
        ].join(', '),

        // 嘗試抓取日期的選擇器
        dateSelector: [
            'time',
            '.entry-date',
            '.post-date',
            '.date',
            '.published',
            '.meta-date',
            '.post-meta',
            '.entry-meta',
            '.meta',
            'span[class*="date"]',
            'div[class*="date"]'
        ].join(', '),

        // 尋找文章唯一標識符 (ID) 的方法
        linkSelector: 'a[href*="/"]',

        // 高亮顏色
        highlightColor: '#fff9c4', // 淺黃色
        borderColor: '#fbc02d',    // 深黃色邊框

        // 儲存空間的 Key
        storageKey: 'jpmd_highlighted_posts_v1',

        // 最大往後搜尋頁數 (避免無限請求)
        maxSearchPages: 50,

        // 除錯模式
        debug: true
    };

    // --- CSS 樣式注入 ---
    const styles = `
        /* 讓文章容器變成相對定位 */
        ${CONFIG.articleSelector} {
            position: relative !important;
            transition: background-color 0.3s ease, box-shadow 0.3s ease;
        }

        /* 隱藏不該出現的區域 */
        .sidebar .jpmd-checkbox-wrapper,
        .widget .jpmd-checkbox-wrapper,
        #secondary .jpmd-checkbox-wrapper,
        footer .jpmd-checkbox-wrapper {
            display: none !important;
        }

        /* 高亮狀態 */
        .jpmd-highlighted {
            background-color: ${CONFIG.highlightColor} !important;
            box-shadow: 0 0 15px rgba(251, 192, 45, 0.5) !important;
            border: 2px solid ${CONFIG.borderColor} !important;
            border-radius: 8px;
        }

        /* 定位時的閃爍效果 */
        @keyframes jpmd-flash {
            0% { box-shadow: 0 0 15px rgba(251, 192, 45, 0.5); }
            50% { box-shadow: 0 0 30px rgba(251, 192, 45, 1); transform: scale(1.02); }
            100% { box-shadow: 0 0 15px rgba(251, 192, 45, 0.5); transform: scale(1); }
        }
        .jpmd-scroll-target {
            animation: jpmd-flash 1s ease-in-out 3; /* 閃爍三次 */
        }

        /* Checkbox 容器 - 35px 圓角方形 */
        .jpmd-checkbox-wrapper {
            position: absolute;
            top: 10px;
            right: 10px;
            z-index: 999990;
            background: #ffffff;
            padding: 0;
            border-radius: 6px;
            border: 3px solid #fbc02d;
            box-shadow: 0 4px 8px rgba(0,0,0,0.4);
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            width: 35px;
            height: 35px;
            transition: transform 0.2s, box-shadow 0.2s;
        }

        .jpmd-checkbox-wrapper:hover {
            transform: scale(1.15);
            box-shadow: 0 6px 12px rgba(0,0,0,0.5);
        }

        /* 隱藏原生 Checkbox 外觀,改用自定義樣式 */
        .jpmd-checkbox-wrapper input[type="checkbox"] {
            appearance: none;
            -webkit-appearance: none;
            width: 100%;
            height: 100%;
            margin: 0;
            cursor: pointer;
            outline: none;
            position: relative;
        }

        /* 自定義打勾符號 (純 CSS 繪製) */
        .jpmd-checkbox-wrapper input[type="checkbox"]:checked::after {
            content: '';
            position: absolute;
            left: 50%;
            top: 45%;
            width: 8px;
            height: 16px;
            border: solid #fbc02d;
            border-width: 0 4px 4px 0;
            transform: translate(-50%, -60%) rotate(45deg);
        }

        .jpmd-checkbox-wrapper:hover::after {
            content: "標記";
            position: absolute;
            top: 110%;
            right: 50%;
            transform: translateX(50%);
            background: #333;
            color: #fff;
            font-size: 12px;
            padding: 4px 8px;
            border-radius: 4px;
            white-space: nowrap;
            pointer-events: none;
            z-index: 1000;
        }

        /* --- 懸浮框樣式 --- */
        .jpmd-float-container {
            position: fixed;
            bottom: 30px;
            right: 30px;
            z-index: 1000000;
            display: flex;
            align-items: center;
        }

        /* 轉頁按鈕群組 */
        .jpmd-nav-group {
            display: flex;
            align-items: center;
            margin-right: 15px;
            background: rgba(255,255,255,0.95);
            padding: 0 5px;
            border-radius: 25px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            height: 50px;
            font-family: "Microsoft JhengHei", sans-serif;
        }

        .jpmd-nav-btn {
            width: 40px;
            height: 40px;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            font-size: 24px;
            color: #fbc02d;
            font-weight: bold;
            user-select: none;
            border-radius: 50%;
            transition: background 0.2s, color 0.2s;
            line-height: 1;
        }
        .jpmd-nav-btn:hover {
            background: #fff9c4;
        }
        .jpmd-nav-btn.disabled {
            color: #ccc;
            cursor: default;
            background: transparent !important;
        }

        /* 頁碼輸入框樣式 */
        .jpmd-nav-curr-input {
            width: 45px;
            border: none;
            text-align: center;
            font-size: 16px;
            font-weight: bold;
            color: #555;
            outline: none;
            background: transparent;
            font-family: inherit;
            padding: 0;
            height: 30px;
            border-left: 1px solid #eee;
            border-right: 1px solid #eee;
            -moz-appearance: textfield; /* Firefox 移除上下箭頭 */
        }
        /* Chrome/Safari/Edge 移除上下箭頭 */
        .jpmd-nav-curr-input::-webkit-outer-spin-button,
        .jpmd-nav-curr-input::-webkit-inner-spin-button {
            -webkit-appearance: none;
            margin: 0;
        }
        .jpmd-nav-curr-input:focus {
            color: #fbc02d;
            background: #fafafa;
        }

        .jpmd-float-btn {
            width: 50px;
            height: 50px;
            background: #fbc02d;
            border-radius: 50%;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            cursor: pointer;
            font-size: 24px;
            transition: transform 0.2s;
            user-select: none;
            position: relative;
        }
        .jpmd-float-btn:hover {
            transform: scale(1.1);
            background: #f9a825;
        }

        /* 新增貼文計數氣泡 */
        .jpmd-new-counter {
            font-family: "Microsoft JhengHei", "微軟正黑體", sans-serif;
            background: #9e9e9e;
            color: white;
            font-size: 18px;
            font-weight: bold;
            height: 50px;
            min-width: 50px;
            padding: 0 15px;
            border-radius: 25px;
            margin-right: 15px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            transition: transform 0.3s, background-color 0.3s;
            pointer-events: auto;
            cursor: pointer;
            white-space: nowrap;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .jpmd-new-counter:hover {
            transform: scale(1.05);
        }
        .jpmd-new-counter.show {
            opacity: 1;
            transform: translateX(0);
        }

        .jpmd-float-panel {
            position: fixed;
            bottom: 90px;
            right: 30px;
            width: 320px;
            max-height: 50vh;
            background: white;
            border: 1px solid #ddd;
            border-radius: 8px;
            box-shadow: 0 5px 20px rgba(0,0,0,0.2);
            z-index: 1000000;
            display: none;
            flex-direction: column;
            overflow: hidden;
            font-family: sans-serif;
        }
        .jpmd-float-panel.open {
            display: flex;
        }
        .jpmd-panel-header {
            background: #fbc02d;
            color: #333;
            padding: 10px 15px;
            font-weight: bold;
            display: flex;
            justify-content: space-between;
            align-items: center;
            border-bottom: 1px solid #eee;
        }
        .jpmd-panel-body {
            flex: 1;
            overflow-y: auto;
            padding: 0;
            background: #fcfcfc;
        }
        .jpmd-list-item {
            padding: 10px 15px;
            border-bottom: 1px solid #eee;
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-size: 13px;
            background: white;
            transition: background 0.2s;
        }
        .jpmd-list-item:hover {
            background: #fffde7;
        }
        .jpmd-list-item span.jpmd-title {
            color: #333;
            flex: 1;
            margin-right: 10px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            display: block;
            font-weight: 500;
        }
        .jpmd-remove-btn {
            color: #ef5350;
            cursor: pointer;
            font-weight: bold;
            font-size: 16px;
            line-height: 1;
            padding: 0 5px;
        }
        .jpmd-remove-btn:hover {
            color: #d32f2f;
        }
        .jpmd-empty-msg {
            padding: 30px;
            text-align: center;
            color: #999;
            font-size: 13px;
        }
        .jpmd-panel-body::-webkit-scrollbar {
            width: 6px;
        }
        .jpmd-panel-body::-webkit-scrollbar-thumb {
            background: #ccc;
            border-radius: 3px;
        }
    `;

    // 注入 CSS
    if (typeof GM_addStyle !== 'undefined') {
        GM_addStyle(styles);
    } else {
        const styleEl = document.createElement('style');
        styleEl.innerHTML = styles;
        document.head.appendChild(styleEl);
    }

    // --- 邏輯處理 ---
    let newPostCount = 0;
    let firstNewPostId = null;
    let isCalculating = false;

    function log(...args) {
        if (CONFIG.debug) console.log('[JPMD Highlighter]', ...args);
    }

    function getStoredState() {
        const stored = localStorage.getItem(CONFIG.storageKey);
        return stored ? JSON.parse(stored) : {};
    }

    function saveState(id, isChecked, title = null, sourceUrl = null, articleDate = 0) {
        let state = {};

        if (isChecked) {
            state[id] = {
                title: title || id,
                timestamp: Date.now(),
                sourceUrl: sourceUrl,
                articleDate: articleDate
            };
        }

        localStorage.setItem(CONFIG.storageKey, JSON.stringify(state));
        updatePanelList();
        startGlobalCounting();
    }

    function normalizePath(path) {
        if (!path) return '';
        return path.endsWith('/') ? path.slice(0, -1) : path;
    }

    function getArticleTitle(article) {
        const titleEl = article.querySelector('h1, h2, h3, .entry-title, .post-title, .card-title, .title');
        return titleEl ? titleEl.innerText.trim() : '未命名文章';
    }

    function getArticleDate(article) {
        const dateEl = article.querySelector(CONFIG.dateSelector);
        let dateText = '';

        if (dateEl) {
            const datetimeAttr = dateEl.getAttribute('datetime');
            if (datetimeAttr) {
                const ts = Date.parse(datetimeAttr);
                if (!isNaN(ts)) return ts;
            }
            const titleAttr = dateEl.getAttribute('title');
            if (titleAttr) {
                const ts = Date.parse(titleAttr);
                if (!isNaN(ts)) return ts;
            }
            dateText = dateEl.innerText.trim();
        } else {
            dateText = article.innerText;
        }

        const datePattern = /(\d{4}[-./]\d{1,2}[-./]\d{1,2})|((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{1,2},?\s+\d{4})/i;
        const match = dateText.match(datePattern);

        if (match) {
            let foundDateStr = match[0];
            foundDateStr = foundDateStr.replace(/\./g, '/');
            const ts = Date.parse(foundDateStr);
            if (!isNaN(ts)) return ts;
        }

        return 0;
    }

    function buildSourceUrl(article) {
        let currentSourceUrl = window.location.href;
        if (article.id) {
            const hashIndex = currentSourceUrl.indexOf('#');
            const base = hashIndex > -1 ? currentSourceUrl.substring(0, hashIndex) : currentSourceUrl;
            currentSourceUrl = base + '#' + article.id;
        }
        return currentSourceUrl;
    }

    function getArticleId(article) {
        let link = article.querySelector('h1 a, h2 a, h3 a, .entry-title a, a.post-link, a.card-link, .post-thumbnail a');

        if (!link) {
            const candidates = article.querySelectorAll(CONFIG.linkSelector);
            for (let cand of candidates) {
                const href = cand.getAttribute('href');
                if (href && !href.includes('/tag/') && !href.includes('/category/') && !href.includes('/author/') && !href.includes('#')) {
                    link = cand;
                    break;
                }
            }
        }

        if (link) {
            try {
                return new URL(link.href).pathname;
            } catch (e) {
                return link.getAttribute('href');
            }
        }

        const isMainContent = article.querySelector('h1');
        if (isMainContent && window.location.pathname.length > 1) {
            return window.location.pathname;
        }

        return null;
    }

    function isInIgnoredArea(element) {
        return element.closest('.sidebar') ||
               element.closest('.widget') ||
               element.closest('#secondary') ||
               element.closest('.comments') ||
               element.closest('footer') ||
               element.matches(CONFIG.ignoreSelector) ||
               element.closest(CONFIG.ignoreSelector);
    }

    function getCurrentPageNumber() {
        const match = window.location.pathname.match(/\/page\/(\d+)\/?/);
        return match ? parseInt(match[1], 10) : 1;
    }

    // --- 全域計數核心邏輯 ---
    async function startGlobalCounting() {
        if (isCalculating) return;
        isCalculating = true;

        const state = getStoredState();
        const keys = Object.keys(state);
        let thresholdDate = 0;
        let markedArticleId = null;

        if (keys.length > 0) {
            markedArticleId = keys[0];
            const data = state[markedArticleId];
            thresholdDate = data.articleDate || 0;
        }

        if (!markedArticleId) {
            newPostCount = 0;
            firstNewPostId = null;
            updateNewPostCounterUI();
            isCalculating = false;
            return;
        }

        log('Starting Global Count. Marked ID:', markedArticleId);
        updateNewPostCounterUI(true);

        let count = 0;
        let firstId = null;
        let foundMark = false;
        let page = 1;
        const currentPage = getCurrentPageNumber();
        const countedIds = new Set();

        while (page <= CONFIG.maxSearchPages && !foundMark) {
            let doc = null;

            if (page === currentPage) {
                doc = document;
            } else {
                try {
                    const url = page === 1 ? window.location.origin : `${window.location.origin}/page/${page}/`;
                    const response = await fetch(url);
                    const text = await response.text();
                    const parser = new DOMParser();
                    doc = parser.parseFromString(text, 'text/html');
                } catch (e) {
                    console.error(`Error fetching page ${page}`, e);
                    break;
                }
            }

            const pageArticles = doc.querySelectorAll(CONFIG.articleSelector);
            if (pageArticles.length === 0) {
                break;
            }

            for (let i = 0; i < pageArticles.length; i++) {
                const article = pageArticles[i];
                if (isInIgnoredArea(article)) continue;

                const currentId = article.dataset.jpmdId || getArticleId(article);
                if (!currentId) continue;

                if (countedIds.has(currentId)) continue;
                countedIds.add(currentId);

                if (currentId === markedArticleId) {
                    foundMark = true;
                    break;
                }

                const d = getArticleDate(article);

                if (d > thresholdDate) {
                    count++;
                    if (!firstId) firstId = currentId;
                } else if (d === thresholdDate && !foundMark) {
                    count++;
                    if (!firstId) firstId = currentId;
                }
            }

            page++;
        }

        newPostCount = count;
        firstNewPostId = firstId;
        isCalculating = false;
        updateNewPostCounterUI();
    }

    function processArticle(article) {
        if (article.dataset.jpmdProcessed) return;
        if (article.offsetHeight < 100 || article.offsetWidth < 100) return;
        if (isInIgnoredArea(article)) return;

        const articleId = getArticleId(article);
        if (!articleId) return;

        const currentPath = normalizePath(window.location.pathname);
        const targetId = normalizePath(articleId);

        if (targetId === currentPath && currentPath !== '' && currentPath !== '/' && currentPath !== '/index.html') {
            return;
        }
        if (article.querySelector('h1') && currentPath !== '' && currentPath !== '/' && currentPath !== '/index.html') {
            return;
        }

        const parentMatch = article.parentElement.closest(CONFIG.articleSelector);
        if (parentMatch) {
            if (parentMatch.querySelector('.jpmd-checkbox-wrapper') || parentMatch.dataset.jpmdProcessed) {
                return;
            }
        }

        article.dataset.jpmdProcessed = 'true';
        article.dataset.jpmdId = articleId;

        if (!article.id) {
            const safeIdSuffix = articleId.replace(/[^a-zA-Z0-9-_]/g, '-').replace(/^-+|-+$/g, '');
            const finalSuffix = safeIdSuffix.length > 2 ? safeIdSuffix : Math.random().toString(36).substr(2, 9);
            article.id = 'jpmd-' + finalSuffix;
        }

        const currentHash = window.location.hash;
        if (currentHash === '#' + article.id) {
            setTimeout(() => {
                article.scrollIntoView({ behavior: 'smooth', block: 'center' });
                article.classList.add('jpmd-scroll-target');
                setTimeout(() => {
                    article.classList.remove('jpmd-scroll-target');
                }, 3000);
            }, 600);
        }

        const wrapper = document.createElement('div');
        wrapper.className = 'jpmd-checkbox-wrapper';

        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';

        const state = getStoredState();
        if (state[articleId]) {
            checkbox.checked = true;
            article.classList.add('jpmd-highlighted');

            const currentSourceUrl = buildSourceUrl(article);
            const savedData = state[articleId];
            const savedUrl = (typeof savedData === 'object') ? savedData.sourceUrl : null;
            const articleDate = getArticleDate(article);

            if (savedUrl !== currentSourceUrl) {
                 const currentTitle = getArticleTitle(article);
                 saveState(articleId, true, currentTitle, currentSourceUrl, articleDate);
            }
        }

        checkbox.addEventListener('change', (e) => {
            const isChecked = e.target.checked;
            const title = getArticleTitle(article);
            const currentSourceUrl = buildSourceUrl(article);
            const articleDate = getArticleDate(article);

            if (isChecked) {
                const allArticles = document.querySelectorAll(CONFIG.articleSelector);
                allArticles.forEach(art => {
                    if (art.dataset.jpmdId !== articleId) {
                        art.classList.remove('jpmd-highlighted');
                        const cb = art.querySelector('input[type="checkbox"]');
                        if (cb) cb.checked = false;
                    }
                });
            }

            saveState(articleId, isChecked, title, currentSourceUrl, articleDate);

            const allSameArticles = document.querySelectorAll(`[data-jpmd-id="${CSS.escape(articleId)}"]`);
            allSameArticles.forEach(otherArticle => {
                if (isChecked) {
                    otherArticle.classList.add('jpmd-highlighted');
                    const cb = otherArticle.querySelector('input[type="checkbox"]');
                    if (cb) cb.checked = true;
                } else {
                    otherArticle.classList.remove('jpmd-highlighted');
                    const cb = otherArticle.querySelector('input[type="checkbox"]');
                    if (cb) cb.checked = false;
                }
            });
        });

        wrapper.addEventListener('click', (e) => {
            e.stopPropagation();
        });

        wrapper.appendChild(checkbox);
        article.appendChild(wrapper);
    }

    function scanPosts() {
        const articles = document.querySelectorAll(CONFIG.articleSelector);
        articles.forEach(processArticle);
    }

    // --- 懸浮框 UI 邏輯 ---

    function updateNewPostCounterUI(loading = false) {
        const counter = document.getElementById('jpmd-new-counter');
        if (counter) {
            if (loading) {
                counter.innerText = '計算中...';
                counter.style.background = '#9e9e9e';
            } else {
                counter.innerText = `${newPostCount}則未讀`;
                if (newPostCount > 0) {
                    counter.style.background = '#ff5252';
                    counter.title = '點擊捲動到第一篇新貼文';
                } else {
                    counter.style.background = '#9e9e9e';
                    counter.title = '沒有新貼文';
                }
            }
        }
    }

    function createFloatingUI() {
        if (document.querySelector('.jpmd-float-container')) return;

        const container = document.createElement('div');
        container.className = 'jpmd-float-container';

        // 新增轉頁按鈕群組
        const navGroup = document.createElement('div');
        navGroup.className = 'jpmd-nav-group';

        const currentPage = getCurrentPageNumber();

        // 上一頁按鈕
        const prevBtn = document.createElement('div');
        prevBtn.className = 'jpmd-nav-btn prev';
        prevBtn.innerHTML = '‹';
        prevBtn.title = '上一頁';
        if (currentPage > 1) {
            prevBtn.onclick = () => {
                const target = currentPage - 1;
                window.location.href = target === 1 ? '/' : `/page/${target}/`;
            };
        } else {
            prevBtn.classList.add('disabled');
        }

        // 當前頁數/跳轉輸入框
        const currInput = document.createElement('input');
        currInput.type = 'number';
        currInput.className = 'jpmd-nav-curr-input';
        currInput.value = currentPage;
        currInput.title = '輸入頁碼後按 Enter 跳轉';

        // 按下 Enter 觸發跳轉
        currInput.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                const target = parseInt(currInput.value, 10);
                if (!isNaN(target) && target > 0) {
                    window.location.href = target === 1 ? '/' : `/page/${target}/`;
                }
            }
        });

        // 點擊自動選取,方便輸入
        currInput.addEventListener('focus', () => {
            currInput.select();
        });

        // 下一頁按鈕
        const nextBtn = document.createElement('div');
        nextBtn.className = 'jpmd-nav-btn next';
        nextBtn.innerHTML = '›';
        nextBtn.title = '下一頁';
        nextBtn.onclick = () => {
             window.location.href = `/page/${currentPage + 1}/`;
        };

        navGroup.appendChild(prevBtn);
        navGroup.appendChild(currInput); // 插入輸入框
        navGroup.appendChild(nextBtn);

        // 計數器
        const counter = document.createElement('div');
        counter.id = 'jpmd-new-counter';
        counter.className = 'jpmd-new-counter';
        counter.innerText = '計算中...';

        counter.addEventListener('click', () => {
            if (firstNewPostId) {
                const target = document.querySelector(`[data-jpmd-id="${CSS.escape(firstNewPostId)}"]`);
                if (target) {
                    target.scrollIntoView({ behavior: 'smooth', block: 'center' });
                    target.classList.add('jpmd-scroll-target');
                    setTimeout(() => {
                        target.classList.remove('jpmd-scroll-target');
                    }, 3000);
                } else {
                    if (confirm('第一篇未讀貼文位於其他頁面(例如首頁),是否前往該文章?')) {
                        if (firstNewPostId.startsWith('http') || firstNewPostId.startsWith('/')) {
                             window.location.href = firstNewPostId;
                        }
                    }
                }
            }
        });

        // 標記清單按鈕
        const btn = document.createElement('div');
        btn.className = 'jpmd-float-btn';
        btn.innerHTML = '★';
        btn.title = '顯示已標記文章';

        const panel = document.createElement('div');
        panel.className = 'jpmd-float-panel';
        panel.innerHTML = `
            <div class="jpmd-panel-header">
                <span>目前標記</span>
            </div>
            <div class="jpmd-panel-body" id="jpmd-panel-list">
                <!-- 列表內容 -->
            </div>
        `;

        container.appendChild(navGroup);
        container.appendChild(counter);
        container.appendChild(btn);
        document.body.appendChild(container);
        document.body.appendChild(panel);

        btn.addEventListener('click', () => {
            panel.classList.toggle('open');
            if (panel.classList.contains('open')) {
                updatePanelList();
            }
        });

        document.addEventListener('click', (e) => {
            if (!panel.contains(e.target) && !container.contains(e.target) && panel.classList.contains('open')) {
                panel.classList.remove('open');
            }
        });

        // 啟動全域計算
        startGlobalCounting();
    }

    function updatePanelList() {
        const listContainer = document.getElementById('jpmd-panel-list');
        if (!listContainer) return;

        const state = getStoredState();
        const keys = Object.keys(state).reverse();

        if (keys.length === 0) {
            listContainer.innerHTML = '<div class="jpmd-empty-msg">尚無標記文章</div>';
            return;
        }

        let html = '';
        keys.forEach(key => {
            const data = state[key];
            let title = '未知標題';

            if (typeof data === 'object') {
                title = data.title || key;
            } else {
                title = key;
            }

            html += `
                <div class="jpmd-list-item">
                    <span class="jpmd-title" title="${title}">${title}</span>
                    <span class="jpmd-remove-btn" data-id="${key}" title="移除">×</span>
                </div>
            `;
        });

        listContainer.innerHTML = html;

        listContainer.querySelectorAll('.jpmd-remove-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const id = e.target.dataset.id;
                saveState(id, false);

                const allSameArticles = document.querySelectorAll(`[data-jpmd-id="${CSS.escape(id)}"]`);
                allSameArticles.forEach(article => {
                    article.classList.remove('jpmd-highlighted');
                    const cb = article.querySelector('input[type="checkbox"]');
                    if (cb) cb.checked = false;
                });
            });
        });
    }

    // --- 啟動流程 ---
    createFloatingUI();
    setTimeout(scanPosts, 1000);

    const observer = new MutationObserver((mutations) => {
        let shouldScan = false;
        mutations.forEach((mutation) => {
            if (mutation.addedNodes.length > 0) {
                shouldScan = true;
            }
        });
        if (shouldScan) {
            setTimeout(scanPosts, 500);
        }
    });

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

})();