您需要先安装一个扩展,例如 篡改猴、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.5.4 // @license MIT // @match https://www.kobo.com/*/library/books // @match https://www.kobo.com/*/library/books?* // @run-at document-end // @grant GM.setClipboard // @require https://update.gf.qytechs.cn/scripts/483122/1303118/style-shims.js // @require https://unpkg.com/[email protected]/dist/i18n.object.min.js // @require https://update.gf.qytechs.cn/scripts/482311/1297431/queue.js // @supportURL https://gf.qytechs.cn/scripts/482410/feedback // ==/UserScript== const LL = (function() { const translations = { en: { HEADER: { MESSAGE: "Message", }, BUTTON: { OKAY: "Okay", CHECK_PAGE: "Check Update for Page", CHECKING_PAGE: "Checking Update...", 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: { HEADER: { MESSAGE: "訊息", }, BUTTON: { OKAY: "確定", CHECK_PAGE: "為本頁檢查更新", CHECKING_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:disabled { background-color: "#dcdcdc"; color: rgba(0,0,0,.42); pointer-events: none; } .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; } `); function showModal(message) { const modal = document.createElement("div"); modal.id = "modal"; modal.addEventListener("click", (event) => ((event.target === modal) && closeModal())); const modalContent = document.createElement("div"); modalContent.id = "modal-content"; modalContent.classList.add("library-modal"); const modalContainer = document.createElement("div"); const closeWrapper = document.createElement("div"); closeWrapper.classList.add("wrapper"); const closeButton = document.createElement("button"); closeButton.classList.add("modal-x", "close"); closeButton.addEventListener("click", closeModal); const actionWrapper = document.createElement("div"); actionWrapper.classList.add("wrapper"); const actionContainer = document.createElement("div"); actionContainer.classList.add("action-container"); const actionHeader = document.createElement("h2"); actionHeader.classList.add("confirm"); actionHeader.textContent = LL.HEADER.MESSAGE(); const actionMessage = document.createElement("p"); actionMessage.style = "display: inline-block; text-align: left;"; actionMessage.innerHTML = String(message).replaceAll("\n", "<br />"); const actionButtons = document.createElement("div"); actionButtons.classList.add("cta"); actionButtons.style = "display: block;"; const okayButton = document.createElement("button"); okayButton.classList.add("primary-button", "okay"); okayButton.textContent = LL.BUTTON.OKAY(); okayButton.addEventListener("click", closeModal); closeWrapper.append(closeButton); actionButtons.append(okayButton); actionContainer.append(actionHeader, actionMessage, actionButtons); actionWrapper.append(actionContainer); modalContainer.append(closeWrapper, actionWrapper); modalContent.append(modalContainer); modal.append(modalContent); document.body.append(modal); document.body.classList.add("show-modal"); function closeModal() { if (document.querySelectorAll("#modal").length > 1) { modal.remove() } else { document.body.classList.remove("show-modal"); setTimeout(() => modal.remove(), 250); } } } 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) => showModal(e.message)); message.replaceChildren(link); } }); } function checkUpdateForBooks(books) { checkButton.disabled = true; checkButton.textContent = LL.BUTTON.CHECKING_PAGE(); 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; } } showModal(LL.MESSAGE.FINISHED_CHECKING_PAGE({ latest, outdated, skipped, failed })); checkButton.disabled = false; checkButton.textContent = LL.BUTTON.CHECK_PAGE(); }, { 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)) { showModal(LL.MESSAGE.NO_BOOKS_BEEN_CHECKED()); return; } const outdated = Array.prototype.filter.call(books, (book) => (book.dataset.checkStatus === Status.OUTDATED)); if (outdated.length > 0) { GM.setClipboard(Array.prototype.map.call(outdated, getBookTitle).join("\n")); showModal(LL.MESSAGE.COPIED_BOOKS(outdated.length)); } else { showModal(LL.MESSAGE.NO_BOOKS_WERE_OUTDATED()); } }); updateControls.append(checkButton, copyButton); updateContainer.appendChild(updateControls); secondaryControls.insertBefore(updateContainer, secondaryControls.firstChild);
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址