您需要先安装一个扩展,例如 篡改猴、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.4.3 // @license MIT // @match https://www.kobo.com/*/library/books // @run-at document-end // @grant GM.addStyle // @grant GM.setClipboard // @require https://unpkg.com/[email protected]/dist/i18n.object.min.js // @require https://update.greasyfork.org/scripts/482311/1297431/queue.js // @supportURL https://greasyfork.org/scripts/482410/feedback // ==/UserScript== const LL = (function() { const translations = { en: { BUTTON: { CHECK_PAGE: "Check Update for Page", CHECK_SINGLE: "Check Update", COPY_OUTDATED: "Copy Outdated Books", }, STATUS: { PENDING: "Pending...", CHECKING: "Checking...", LATEST: "Latest", OUTDATED: "Outdated", PREVIEW: "Preview", SKIPPED: "Skipped", FAILED: "Failed", }, ERROR: { UNLISTED: "This book was unlisted, there’s no way to check update for this type of books at the moment.", PARSING: "Failed to parse the latest product ID, please contact the developer for further investigations.", UNKNOWN: "Unknown error, please contact the developer for further investigations.", }, MESSAGE: { FINISHED_CHECKING_PAGE: "Finished checking all books for this page.\n\nLatest: {latest}\nOutdated: {outdated}\nSkipped: {skipped}\nFailed: {failed}", NO_BOOKS_BEEN_CHECKED: "No books have been checked.", NO_BOOKS_WERE_OUTDATED: "No books were outdated.", COPIED_BOOKS: "Copied {0} book{{s}} into the clipboard.", }, }, zh: { BUTTON: { CHECK_PAGE: "為本頁檢查更新", CHECK_SINGLE: "檢查更新", COPY_OUTDATED: "複製過時書籍", }, STATUS: { PENDING: "等待中…", CHECKING: "檢查中…", LATEST: "最新", OUTDATED: "過時", PREVIEW: "預覽", SKIPPED: "已略過", FAILED: "檢查失敗", }, ERROR: { UNLISTED: "該書已下架,目前尚未有方法為這類書籍檢查更新。", PARSING: "無法解析最新的產品編號,請聯絡開發者以進一步調查。", UNKNOWN: "未知錯誤,請聯絡開發者以進一步調查。", }, MESSAGE: { FINISHED_CHECKING_PAGE: "完成檢查本頁的書籍。\n\n最新:{latest}\n過時:{outdated}\n已略過:{skipped}\n檢查失敗:{failed}", NO_BOOKS_BEEN_CHECKED: "沒有已檢查的書籍。", NO_BOOKS_WERE_OUTDATED: "沒有過時的書籍。", COPIED_BOOKS: "已複製 {0} 本書到剪貼簿。", }, }, }; let locale = location.pathname.match(/\/[a-z]{2}\/([a-z]{2})\//)?.[1] ?? "en"; if (!Object.keys(translations).includes(locale)) { console.warn("No translations available for this locale.") locale = "en"; } return i18nObject(locale, translations[locale]); })(); GM.addStyle(` .library-container .update-container { text-align: right; } .library-container .update-controls { position: relative; display: inline-block; margin-top: 18px; min-width: 13rem; width: auto; } .library-container .update-button { border-radius: 20px; min-width: 0; max-width: 100%; width: auto; 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 .update-button:not(:first-child) { margin-left: 5px; } .library-container .update-button:not(:last-child) { margin-right: 5px; } .library-container .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 .update-button:hover { background-color: rgba(0, 0, 0, .04); } .library-container .update-button:focus::before { opacity: 1; transform: scale(1); } .library-container .update-button:active { background-color: #000; color: #fff; } @media (max-width: 568px) { .library-container .secondary-controls { margin-right: 18px; } .library-container .update-container { text-align: left; } .library-container .update-controls { margin-right: 0; width: 100%; white-space: break-spaces; } .library-container .update-button { margin-left: 0 !important; margin-right: 0 !important; width: 100%; text-align: center; } .library-container .update-button:not(:first-child) { margin-top: 18px; } .library-container .library-content.grid .more-actions:not(.open) { width: fit-content; transform: translateY(35px); } } .item-wrapper.book[data-check-status=outdated] .product-field.item-status { background: #FE8484; } .item-wrapper.book[data-check-status=skipped] .product-field.item-status { background: #B5B5B5; } .item-wrapper.book[data-check-status=failed] .product-field.item-status { background: #FFA700; } .item-wrapper.book:is([data-check-status=skipped], [data-check-status=failed]) .product-field.item-status a { text-decoration-line: underline; cursor: pointer; } `); const Status = { PENDING: "pending", CHECKING: "checking", LATEST: "latest", OUTDATED: "outdated", SKIPPED: "skipped", FAILED: "failed", }; const queue = new Queue({ autostart: true, concurrency: 6 }); function isAudiobook(book) { return STORE_AUDIOBOOK_URL_PATTERN.test(book.querySelector(".product-field.title a").href); } function getBookTitle(book) { return book.querySelector(".product-field.title").innerText; } function getCurrentProductId(book) { const config = JSON.parse(book.querySelector(".library-action.mark-as-finished").dataset.koboGizmoConfig); return config.productId; } async function getLatestProductId(book) { const response = await fetch(book.querySelector(".product-field.title a").href); if (!response.ok) { if (response.status === 404) { throw new Error(LL.ERROR.UNLISTED()); } throw new Error(LL.ERROR.UNKNOWN()); } 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; } const config = page.querySelector(".item-detail"); if (config) { return JSON.parse(config.dataset.koboGizmoConfig).productId; } throw new Error(LL.ERROR.PARSING()); } function buildMessage(message, replacements) { return message.replaceAll(/\{(\d+)\}/g, (_, index) => replacements[index]); } function checkUpdate(book) { const message = book.querySelector(".product-field.item-status"); book.dataset.checkStatus = Status.PENDING; message.replaceChildren(LL.STATUS.PENDING()); queue.push(async () => { book.dataset.checkStatus = Status.CHECKING; message.textContent = LL.STATUS.CHECKING(); if (book.dataset.koboGizmo === "PreviewLibraryItem") { book.dataset.checkStatus = Status.SKIPPED; message.classList.remove("buy-now"); message.replaceChildren(LL.STATUS.PREVIEW()); return; } try { const currentId = getCurrentProductId(book); const latestId = await getLatestProductId(book); console.debug(`${getBookTitle(book)}\n Current: ${currentId}\n Latest : ${latestId}`); if (currentId === latestId) { book.dataset.checkStatus = Status.LATEST; message.replaceChildren(LL.STATUS.LATEST()); } else { book.dataset.checkStatus = Status.OUTDATED; message.replaceChildren(LL.STATUS.OUTDATED()); } } catch (e) { book.dataset.checkStatus = Status.FAILED; const link = document.createElement("a"); link.textContent = LL.STATUS.FAILED(); link.addEventListener("click", (event) => alert(e.message)); message.replaceChildren(link); } }); } function checkUpdateForBooks(books) { queue.addEventListener("end", () => { let latest = 0; let outdated = 0; let skipped = 0; let failed = 0; for (const book of books) { switch (book.dataset.checkStatus) { case Status.LATEST: latest++; break; case Status.OUTDATED: outdated++; break; case Status.SKIPPED: skipped++ break; case Status.FAILED: failed++; break; } } alert(LL.MESSAGE.FINISHED_CHECKING_PAGE({ latest, outdated, skipped, failed })); }, { once: true }); books.forEach(checkUpdate); } const books = document.querySelectorAll(".item-wrapper.book"); for (const book of books) { const actions = book.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 = LL.BUTTON.CHECK_SINGLE(); action.addEventListener("click", () => checkUpdate(book)); actionContainer.appendChild(action); actions.appendChild(actionContainer); } const secondaryControls = document.querySelector(".secondary-controls"); const updateContainer = document.createElement("div"); updateContainer.classList.add("update-container"); const updateControls = document.createElement("div"); updateControls.classList.add("update-controls"); const checkButton = document.createElement("button"); checkButton.classList.add("update-button"); checkButton.textContent = LL.BUTTON.CHECK_PAGE(); checkButton.addEventListener("click", () => checkUpdateForBooks(books)); const copyButton = document.createElement("button"); copyButton.classList.add("update-button"); copyButton.textContent = LL.BUTTON.COPY_OUTDATED(); copyButton.addEventListener("click", () => { if (!Array.prototype.some.call(books, (book) => book.dataset.checkStatus)) { alert(LL.MESSAGE.NO_BOOKS_BEEN_CHECKED()); return; } const outdated = Array.prototype.filter.call(books, (book) => (book.dataset.checkStatus === Status.OUTDATED)); GM.setClipboard(Array.prototype.map.call(outdated, getBookTitle).join("\n")); alert((outdated.length === 0) ? LL.MESSAGE.NO_BOOKS_WERE_OUTDATED() : LL.MESSAGE.COPIED_BOOKS(outdated.length)); }); updateControls.append(checkButton, copyButton); updateContainer.appendChild(updateControls); secondaryControls.insertBefore(updateContainer, secondaryControls.firstChild);