Skip to main content

Command Palette

Search for a command to run...

Pubu 電子書城優化腳本 (Pubu Helper)

Updated
6 min read

簡介

版本: 3.1.2.1
更新日期: 2026-01-29
功能摘要:

  1. 書名完整顯示:解決 Pubu 網頁版書名過長被截斷(...)的問題,並智慧調整行高。

  2. 已購書籍標記:自動偵測已購買的書,在書名與詳細頁標題加上「綠色背景」,避免重複購買。

  3. EPUB 下載警示:在書籍詳細頁,若該書不提供 EPUB 下載,會自動在標題旁顯示紅色的 (不提供EPUB下載) 警示。

  4. 智慧快取:減少伺服器請求,支援手動/自動更新已購書單。


📂 腳本原始碼 (Script Code)

// ==UserScript==
// @name         Pubu書名全顯示(智慧多行+搜尋頁與書籍頁已購書綠底+快取)
// @namespace    http://tampermonkey.net/
// @version      3.1.2.1
// @description  Pubu書名完整顯示,避免覆蓋,智能調整顯示行數,已購書綠底,本地快取可手動/自動更新;新增不提供EPUB警示
// @author       Your Name
// @match        https://www.pubu.com.tw/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=www.pubu.com.tw
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // ====== 全域CSS修正 ======
    const style = document.createElement('style');
    style.textContent = `
        /* 解除 line-clamp 限制 */
        .info-name.show h3 a.text-reset.js-ecGtmClick {
            -webkit-line-clamp: unset !important; /* 移除 WebKit 的行數限制 */
            -webkit-box-orient: unset !important; /* 移除 WebKit 的排列方向限制 */
            overflow: visible !important; /* 允許內容超出元素邊界 */
            text-overflow: clip !important; /* 超出部分直接裁剪,不顯示省略號 */
            display: block !important; /* 確保元素可以多行顯示 */
        }

        /* 動態調整 info-name 區塊的最小高度 */
        .info-name.show {
            min-height: auto !important; /* 高度自動,由內容決定 */
        }

        /* 確保 info-others (出版社、作者等資訊) 不會被書名覆蓋 */
        .info-others.text-truncate {
            margin-top: 3px !important; /* 增加一些頂部間距,避免與上方書名重疊 */
        }

        /* 已購書綠底樣式 */
        .pubu-owned-title {
            background: linear-gradient(90deg, #a8ff78 0%, #78ffd6 100%);
            color: #222 !important;
            border-radius: 4px;
            padding: 2px 4px;
            transition: background 0.3s;
        }

        /* 更新按鈕樣式 */
        .pubu-update-btn {
            font-size: 14px !important;
            line-height: 21px !important;
            color: #222 !important;
            cursor: pointer;
            padding: 6px 16px;
            border: none;
            background: none;
            width: 100%;
            text-align: left;
        }
        .pubu-update-btn:disabled {
            color: #aaa !important;
            cursor: not-allowed;
        }
    `;
    document.head.appendChild(style);

    // ====== 本地快取相關 ======
    const LS_KEY = 'pubu_owned_book_ids';
    const LS_TIME_KEY = 'pubu_owned_book_ids_time';
    const UPDATE_INTERVAL = 24 * 60 * 60 * 1000; // 24小時

    // 取得本地快取
    function getCachedOwnedIds() {
        try {
            const ids = JSON.parse(localStorage.getItem(LS_KEY) || '[]');
            return new Set(ids);
        } catch {
            return new Set();
        }
    }

    // 設定本地快取
    function setCachedOwnedIds(ids) {
        localStorage.setItem(LS_KEY, JSON.stringify([...ids]));
        localStorage.setItem(LS_TIME_KEY, Date.now().toString());
    }

    // 取得最後更新時間
    function getLastUpdateTime() {
        return parseInt(localStorage.getItem(LS_TIME_KEY) || '0', 10);
    }

    // 判斷是否需要自動更新
    function needAutoUpdate() {
        const last = getLastUpdateTime();
        return !last || (Date.now() - last > UPDATE_INTERVAL);
    }

    // ====== 取得所有已購電子書ID ======
    /**
     * 透過 AJAX 方式抓取 /all/bookshelf?page=1,2... 取得所有已購買電子書ID
     * 每頁 rows=50,加快抓取速度
     * 回傳 Set<string>,每個元素為電子書ID
     */
    async function fetchAllOwnedBookIds(onProgress) {
        const ownedIds = new Set();
        let page = 1;
        const rows = 50; // 每頁抓取50本,加快速度
        while (true) {
            const url = `/all/bookshelf?page=${page}&rows=${rows}&sort=0&queryShelf=&category=1`;
            try {
                const resp = await fetch(url, { credentials: 'include' });
                const html = await resp.text();
                // 解析所有 /ebook/123456 連結
                const matches = [...html.matchAll(/href="\/ebook\/(\d+)/g)];
                if (matches.length === 0) break; // 沒有更多書,結束
                matches.forEach(m => ownedIds.add(m[1]));
                if (typeof onProgress === 'function') onProgress(page, ownedIds.size);
                page++;
            } catch (e) {
                break;
            }
        }
        return ownedIds;
    }

    // ====== 移除樣式限制的核心函式 (主要移除造成截斷的樣式) ======
    const unlockTitles = (element) => {
        let parent = element;
        while (parent) {
            if (parent.style) {
                parent.style.whiteSpace = 'normal'; // 允許文字正常換行
                parent.style.overflow = 'visible'; // 允許內容溢出
                parent.style.textOverflow = 'clip'; // 溢出時裁剪
                //parent.style.maxWidth = 'none'; // 不限制最大寬度
                parent.style.webkitLineClamp = 'unset'; // 再次確保移除 WebKit 內核瀏覽器的行數截斷限制
                parent.style.webkitBoxOrient = 'unset'; // 再次確保移除 WebKit 內核瀏覽器的排列方向限制
            }
            parent = parent.parentElement;
        }
        void element.offsetHeight; // 強制瀏覽器進行重繪,以確保樣式更改立即生效
    };

    // ====== 主要處理邏輯:書名全顯示+已購書綠底+(新)不提供EPUB警示 ======
    function processTitles(ownedIds) {
        // ====== 通用,用於搜索結果,書名全顯示+已購書綠底 ======
        document.querySelectorAll('.info-name h3 a.text-reset').forEach(link => {
            // 如果連結文字包含省略號,或者文字內容與 title 屬性不符,則更新
            if (link.textContent.includes('...') || link.title && link.textContent !== link.title) {
                link.textContent = link.title; // 將連結文字設定為完整的書名
                unlockTitles(link); // 解除相關元素的樣式限制
            } else if (!link.title && link.textContent.length > 21) {
                unlockTitles(link); // 解除相關元素的樣式限制
            }

            // 動態調整 h3 (書名標題) 及其父容器的高度
            const titleLength = link.textContent.length;
            // 假設每行約 10 個中文字元來估算行數 (可依實際情況調整此數值)
            const estimatedLines = Math.ceil(titleLength / 10);
            // 預設行高約為 21px (可依實際情況調整此數值)
            const lineHeight = 21;
            const minHeight = estimatedLines * lineHeight;

            // 動態設定 h3 元素的最小高度
            link.parentNode.style.minHeight = minHeight + 'px';
            // 動態設定 .info-name 容器的最小高度,確保有足夠空間容納多行書名
            link.parentNode.parentNode.style.minHeight = minHeight + 'px';

            // ====== 標記已購買書籍:比對ID,命中即加上綠底class ======
            const match = link.href.match(/\/ebook\/(\d+)/);
            if (match && ownedIds.has(match[1])) {
                link.classList.add('pubu-owned-title');
            } else {
                link.classList.remove('pubu-owned-title');
            }
        });

        // ====== 檢查當前頁面是否為書籍詳細頁 ======
        const currentMatch = window.location.href.match(/\/ebook\/(\d+)/);
        if (currentMatch) {
            // 1. 若已購買,標記綠底
            if (ownedIds.has(currentMatch[1])) {
                const h1 = document.querySelector('h1.h4');
                if (h1) {
                    h1.classList.add('pubu-owned-title');
                }
            }

            // 2. 檢查是否不提供 EPUB 下載 (新增功能)
            // 尋找包含 "提供 Adobe DRM" 的標籤所在 col-4
            const drmCols = document.querySelectorAll('.col-4');
            for (const col of drmCols) {
                // 檢查是否包含 "提供 Adobe DRM" 文字
                if (col.textContent.trim().includes('提供 Adobe DRM')) {
                    // 找到同一 row 中的下一個 col-8 (內容區塊)
                    const contentCol = col.nextElementSibling;
                    if (contentCol && contentCol.classList.contains('col-8')) {
                        // 檢查內容是否包含 "不提供EPUB"
                        if (contentCol.textContent.includes('不提供EPUB')) {
                             const h1 = document.querySelector('h1.h4');
                             // 避免重複添加提示 (檢查是否已有 .pubu-no-epub-warning)
                             if (h1 && !h1.querySelector('.pubu-no-epub-warning')) {
                                 const warningSpan = document.createElement('span');
                                 warningSpan.className = 'pubu-no-epub-warning';
                                 warningSpan.style.cssText = 'color: red !important; font-size: 16px !important; font-weight: bold; margin-left: 10px; vertical-align: middle;';
                                 warningSpan.textContent = '(不提供EPUB下載)';
                                 h1.appendChild(warningSpan);
                             }
                        }
                    }
                    break; // 找到標籤後即可停止搜尋
                }
            }
        }

        // ====== 檢查當前頁面是否為購物車 ======
        if (window.location.href.match(/\/cart/)) {
            document.querySelectorAll('li.booktitle p a').forEach(link => {
                const cartMatch = link.href.match(/\/ebook\/(\d+)/);
                if (cartMatch && ownedIds.has(cartMatch[1])) {
                    link.classList.add('pubu-owned-title');
                    } else {
                        link.classList.remove('pubu-owned-title');
                    }
            });
        }
    }

    // ====== 高效能 MutationObserver 配置,用於監聽 DOM 變化 ======
    function setupObserver(ownedIds) {
        const process = () => processTitles(ownedIds);
        const observer = new MutationObserver(mutations => {
            mutations.some(mutation => {
                if (mutation.type === 'childList') {
                    process();
                    return true;
                }
                return false;
            });
        });

        const contentNode = document.querySelector('#search-list-content') || document.documentElement;
        observer.observe(contentNode, {
            childList: true,
            subtree: true,
            attributes: false,
            characterData: false
        });

        // 初始化執行,並兼容單頁應用程式 (SPA) 的路由變化
        const init = () => {
            process();
            window.requestAnimationFrame(process);
        };
        init();
        window.addEventListener('popstate', init);
        window.addEventListener('pushstate', init);
        window.addEventListener('replacestate', init);
    }

    // ====== UI:插入手動更新按鈕到會員下拉選單 ======
    function insertUpdateButton() {
        // 判斷是否為手機模式,寬度 <= 768px 視為手機
        const isMobile = /Mobi|Android/i.test(navigator.userAgent) || window.matchMedia("(max-width: 768px)").matches;
//         const isMobile = window.matchMedia("(max-width: 768px)").matches;

        // 嘗試尋找目標下拉選單
        const menu = isMobile
        ? document.querySelector('.pl-4')
        : document.querySelector('.dropdown-menu[aria-labelledby="dropdownMenu-login"]');

        // 防止重複插入
        if (!menu || menu.querySelector('.pubu-update-btn')) return;

        console.log('已插入按鈕');
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'dropdown-item pubu-update-btn';
        btn.textContent = '更新已購書(Pubu Userscript)';

        // 事件直接綁定到目前按鈕,避免 mutation observer dom 重建造成引用失效
        btn.addEventListener('click', async function(e) {
            const el = e.currentTarget; // 總是取得真正被點擊的這顆按鈕
            el.disabled = true;
            el.textContent = '更新中...';

            ownedIds = await fetchAllOwnedBookIds();
            setCachedOwnedIds(ownedIds);
            processTitles(ownedIds);

            el.textContent = '更新完成!';
            setTimeout(() => {
                el.textContent = '再次更新已購書';
                el.disabled = false;
            }, 2000);
        });

        // 插入到「登出」按鈕上方
        const logout = menu.querySelector('#logout');
        if (logout) {
            menu.insertBefore(btn, logout);
        } else {
            menu.appendChild(btn);
        }
    }

    // ====== 主流程 ======
    let ownedIds = getCachedOwnedIds();
    let observerStarted = false;

    // 處理搜尋頁面標記
    function startObserverIfNeeded() {
        //if (!observerStarted && document.querySelector('.info-name.show h3 a.text-reset.js-ecGtmClick')) {
        if (!observerStarted && document.querySelector('.info-name h3 a.text-reset')) {
            setupObserver(ownedIds);
            observerStarted = true;
        } else if(!observerStarted && window.location.href.match(/\/cart/)) {
            setupObserver(ownedIds);
            observerStarted = true;
        }
    }

    // 嘗試自動更新(每天一次)
    async function tryAutoUpdate() {
        if (needAutoUpdate()) {
            ownedIds = await fetchAllOwnedBookIds();
            setCachedOwnedIds(ownedIds);
            processTitles(ownedIds);
        }
    }

    // 嘗試插入更新按鈕(因為會員下拉選單是動態生成,需監控)
    function setupUpdateButton() {
        const tryInsert = () => {
            insertUpdateButton();
        };
        // 監控下拉選單出現
        const obs = new MutationObserver(tryInsert);
        obs.observe(document.body, { childList: true, subtree: true });

        // 頁面載入時先嘗試一次
        tryInsert();
    }

    // ====== 啟動 ======
    setupUpdateButton();
    startObserverIfNeeded();
    tryAutoUpdate();

    // SPA動態頁面切換時重新啟動標記
    window.addEventListener('popstate', startObserverIfNeeded);
    window.addEventListener('pushstate', startObserverIfNeeded);
    window.addEventListener('replacestate', startObserverIfNeeded);

})();

🔧 新手安裝指南 (電腦版 Chrome/Edge/Firefox)

若您是在電腦上使用,請依照以下步驟安裝:

  1. 安裝擴充功能

    • 請先安裝「Tampermonkey (油猴)」瀏覽器擴充功能。

    • Chrome/Edge 商店:連結

    • Firefox 商店:連結

  2. 新增腳本

    • 安裝完後,點擊瀏覽器右上角的 Tampermonkey 圖示(黑色正方形圖示)。

    • 選擇「添加新腳本 (Create a new script)」。

  3. 貼上程式碼

    • 刪除編輯器內原有的所有內容。

    • 複製上方「腳本原始碼」區塊內的所有程式碼。

    • 貼上到編輯器中。

  4. 儲存

    • 按下 Ctrl + S 或點擊左上角的「文件 (File)」>「保存 (Save)」。
  5. 完成

    • 回到 Pubu 網站重新整理頁面,即可看到效果。

📱 新手安裝指南 (Android Firefox 版)

若您想在手機上使用,目前以 Android 版 Firefox 支援度最好,請參考以下步驟:

  1. 下載瀏覽器

    • 請至 Google Play 商店下載並安裝 Firefox 瀏覽器
  2. 安裝擴充套件

    • 打開 Firefox App。

    • 點擊右下角(或右上角)的「三點選單」圖示。

    • 選擇「擴充套件 (Add-ons)」。

    • 在推薦列表中找到 Tampermonkey,點擊 + 安裝。

  3. 新增腳本

    • 安裝後,再次點擊「三點選單」,您會看到最下方出現了 Tampermonkey,點擊進入。

    • 點擊 Dashboard(儀表板)或「+」號來新增腳本。

    • (小技巧):由於手機複製貼上大量程式碼較不便,建議先將上方程式碼複製到手機的筆記本 App,再全選複製,貼入 Tampermonkey 的編輯視窗。

  4. 儲存並使用

    • 貼上後,點擊編輯器選單中的「保存 (Save)」。

    • 用 Firefox 開啟 Pubu 網站並登入,腳本即會自動運作。

    • 手機版也能在側邊欄選單底部看到「更新已購書」的按鈕。