您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Checks if updates were available for the e-books you own.
当前为
// ==UserScript== // @name Kobo e-Books Update Checker // @name:zh-TW Kobo 電子書更新檢查器 // @description Checks if updates were available for the e-books you own. // @description:zh-TW 檢查你購買的電子書是否有更新檔提供。 // @icon https://icons.duckduckgo.com/ip3/www.kobo.com.ico // @author Jason Kwok // @namespace https://jasonhk.dev/ // @version 1.0.0 // @license MIT // @match https://www.kobo.com/*/library/books // @run-at document-end // @grant GM.addStyle // @require https://update.greasyfork.org/scripts/482311/1296526/queue.js // ==/UserScript== let MESSAGES; { const PATHNAME_PREFIX_PATTERN = /\/(?<region>[a-z]{2})\/(?<language>[a-z]{2})\//; const I18N = { en: { CHECK_UPDATE_FOR_PAGE: "Check Update for Page", CHECK_UPDATE: "Check Update", QUEUED: "Queued", CHECKING: "Checking...", LATEST: "Latest", OUTDATED: "Outdated", PREVIEW: "Preview", FAILED: "Failed", NOT_SUPPORTED: "Not Supported", BOOK_WAS_UNLISTED: "The book was unlisted, there’s no way to check update for this type of books at the moment.", BOOK_IS_AUDIOBOOK: "The book is audiobook, there’s no way to check update for this type of books at the moment.", UNKNOWN_ERROR: "Unknown error, please contact the developer for further investigations.", }, zh: { CHECK_UPDATE_FOR_PAGE: "為本頁檢查更新", CHECK_UPDATE: "檢查更新", QUEUED: "等待中", CHECKING: "檢查中…", LATEST: "最新", OUTDATED: "過時", PREVIEW: "預覽", FAILED: "檢查失敗", NOT_SUPPORTED: "不支援", BOOK_WAS_UNLISTED: "該書籍已下架,目前尚未有方法為這類書籍檢查更新。", BOOK_IS_AUDIOBOOK: "該書籍為有聲書,目前尚未有方法為這類書籍檢查更新。", UNKNOWN_ERROR: "未知錯誤,請聯絡開發者以進一步調查。", }, }; const { language } = location.pathname.match(PATHNAME_PREFIX_PATTERN).groups; MESSAGES = Object.hasOwn(I18N, language) ? I18N[language] : I18N.en; } GM.addStyle(` .library-container .check-update-container { display: inline; } .library-container .check-update-controls.filter-chip, .library-container .check-update-controls .check-update-button { width: auto; } .library-container .check-update-button { display: flex; border-radius: 20px; min-width: 0; max-width: 100%; width: 100%; justify-content: space-between; align-items: center; overflow: hidden; background-color: #eee; color: #000; font-size: 1.6rem; font-family: "Rakuten Sans UI", "Trebuchet MS", Trebuchet, Arial, Helvetica, sans-serif; font-weight: 400; text-align: left; text-overflow: ellipsis; white-space: nowrap; position: relative; white-space: nowrap; transition: background-color .3s ease-in-out, color .15s ease-in-out 0s; } .library-container .check-update-button::before { position: absolute; top: calc(50% - 30px); border-radius: 80px; width: calc(100% - 30px); height: 60px; background-color: rgba(0, 0, 0, .1); content: ""; opacity: 0; transform: scale(0); } .library-container .check-update-button:hover { background-color: rgba(0, 0, 0, .04); } .library-container .check-update-button:active { background-color: #000; color: #fff; } .library-container .check-update-button:focus::before { opacity: 1; transform: scale(1); } @media (max-width: 745px) { .library-container .check-update-container { display: block; text-align: left; } .library-container .check-update-controls { margin-right: 18px; } } .product-field.item-status.outdated { background: #FE8484; } .product-field.item-status:is(.failed, .audiobook) a { cursor: pointer; } `); const READING_URL_PATTERN = /\/ReadNow\/(?<id>[0-9a-f-]{36})/; const STORE_AUDIOBOOK_URL_PATTERN = /\/(?<region>[a-z]{2})\/(?<language>[a-z]{2})\/audiobook\//;; const items = document.querySelectorAll(".library-items > li"); for (const item of items) { const actions = item.querySelector(".item-info + .item-bar .library-actions-list"); const actionContainer = document.createElement("li"); actionContainer.classList.add("library-actions-list-item"); const action = document.createElement("button"); action.classList.add("library-action"); action.textContent = MESSAGES.CHECK_UPDATE; action.addEventListener("click", () => checkUpdate(item)); actionContainer.appendChild(action); actions.appendChild(actionContainer); } { const controls = document.querySelector(".secondary-controls"); const container = document.createElement("div"); container.classList.add("check-update-container"); const wrapper = document.createElement("div"); wrapper.classList.add("check-update-controls", "filter-chip"); const button = document.createElement("button"); button.classList.add("check-update-button"); button.textContent = MESSAGES.CHECK_UPDATE_FOR_PAGE; button.addEventListener("click", () => items.forEach(checkUpdate)); wrapper.appendChild(button); container.appendChild(wrapper); controls.insertBefore(container, controls.firstChild); } const queue = new Queue({ autostart: true, concurrency: 6 }); function checkUpdate(item) { const message = item.querySelector(".product-field.item-status"); message.replaceChildren(MESSAGES.QUEUED); if (item.dataset.koboGizmo === "PreviewLibraryItem") { message.classList.remove("buy-now"); message.replaceChildren(MESSAGES.PREVIEW); return; } const storeLink = item.querySelector(".product-field.title a"); if (STORE_AUDIOBOOK_URL_PATTERN.test(storeLink.href)) { message.classList.add("audiobook"); const link = document.createElement("a"); link.textContent = MESSAGES.NOT_SUPPORTED; link.addEventListener("click", (event) => alert(MESSAGES.BOOK_IS_AUDIOBOOK)); message.replaceChildren(link); return; } queue.push(async () => { message.textContent = MESSAGES.CHECKING; try { const currentId = getCurrentProductId(); const latestId = await getLatestProductId(storeLink.href); console.debug(`${storeLink.innerText}\n Current: ${currentId}\n Latest : ${latestId}`); if (currentId === latestId) { message.classList.remove("outdated", "failed"); message.replaceChildren(MESSAGES.LATEST); } else { message.classList.add("failed"); message.replaceChildren(MESSAGES.OUTDATED); message.classList.add("outdated"); } } catch (e) { message.classList.remove("outdated"); message.classList.add("failed"); const link = document.createElement("a"); link.textContent = MESSAGES.FAILED; link.addEventListener("click", (event) => alert(e.message)); message.replaceChildren(link); } }); function getCurrentProductId() { const matches = item.querySelector(".library-action.readnow").href.match(READING_URL_PATTERN); return matches?.groups.id; } async function getLatestProductId(url) { const response = await fetch(url); if (!response.ok) { if (response.status === 404) { throw new Error(MESSAGES.BOOK_WAS_UNLISTED); } throw new Error(MESSAGES.UNKNOWN_ERROR); } const html = await response.text(); const parser = new DOMParser(); const page = parser.parseFromString(html, "text/html"); const itemId = page.querySelector("#ratItemId"); if (itemId) { return itemId.value; } throw new Error(MESSAGES.UNKNOWN_ERROR); } }