Neopets Read Books Tracker

Keeps track of books read by your Neopet and removes their name from the dropdown list in inventory for reading a book if they have already read it. On the home page (https://www.neopets.com/home/index.phtml), it will remove books from the list of options when you select "Read" on a pet, and re-calculate the actual number of unread books you have in your inventory and available to read to them.

当前为 2025-02-20 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Neopets Read Books Tracker
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Keeps track of books read by your Neopet and removes their name from the dropdown list in inventory for reading a book if they have already read it. On the home page (https://www.neopets.com/home/index.phtml), it will remove books from the list of options when you select "Read" on a pet, and re-calculate the actual number of unread books you have in your inventory and available to read to them.
// @author       darknstormy
// @match        *://*.neopets.com/books_read.phtml?*
// @match        *://*.neopets.com/moon/books_read.phtml?*
// @match        *://*.neopets.com/inventory.phtml
// @match        *://*.neopets.com/home/index.phtml
// @icon         https://www.google.com/s2/favicons?sz=64&domain=neopets.com
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

// Home Constants
const petCareListObserver = new MutationObserver((mutations) => {
    var removedBookCount = removeReadBooksFromPetCareList()

    if (removedBookCount > 0) {
        correctUnreadBookCount(removedBookCount)
    }

    listenForReadToPetAction()
})

// Inventory Constants
const SUCCESS_TEXT = "Success"

const itemDescriptionObserver = new MutationObserver((mutations) => {
         if (hasReadToOptions()) {
             let bookTitle = $("#invItemName").text().toString()
             removeOptionsToReadBookToPetsThatHaveAlreadyRead(bookTitle)
             listenForReadActionSubmission(bookTitle)
         }
     })

const inventoryActionResultObserver = new MutationObserver((mutations) => {
     let result = $("#invResult").find("h3")[0].innerHTML

     if (result) {
         if (result.includes(SUCCESS_TEXT)) {
             let readActionSubmitted = GM_getValue("pendingReadAction", {})

             let bookTitle = readActionSubmitted.bookTitle
             let petSelectedForReading = readActionSubmitted.petName

             if (bookTitle && petSelectedForReading) {
                 onBookReadToPet(bookTitle, petSelectedForReading)
             }
         }

         GM_setValue("pendingReadAction", {})
     }
 })

// URL helper functions
function isPetsReadBookListUrl() {
   return window.location.href.includes("books_read.phtml")
}

function isInventoryUrl() {
    return window.location.href.includes("inventory.phtml")
}

function isHomeUrl() {
    return window.location.href.includes("home/index.phtml")
}

// Initialize the reading tracker code!
$(document).ready(function() {
     if (isPetsReadBookListUrl()) {
         storeReadBooks()
     } else if (isInventoryUrl()) {
         setupBookReadingTrackerForInventory()
    } else if (isHomeUrl()) {
         setupBookReadingTrackerForHomePage()
    } else {
         cleanUpUnfinalizedActions()
    }
})

// Inventory Functions
function setupBookReadingTrackerForInventory() {
     itemDescriptionObserver.observe(document.getElementById("invDesc"), {childList: true, subtree: true})
     inventoryActionResultObserver.observe(document.getElementById("invResult"), {childList: true, subtree: true})
}

function hasReadToOptions() {
    return $('#iteminfo_select_action option:contains("Read to")') != undefined
}

function listenForReadActionSubmission(title) {
   $(".invitem-submit").on("click", function () {
       let option = $("#iteminfo_select_action").find("option:selected").val()
       if (option.includes("Read")) {
           let selectedPet = option.split(" ").pop()
           GM_setValue("pendingReadAction", { bookTitle: title,
                                             petName: selectedPet })
       }
   })
}

function removeOptionsToReadBookToPetsThatHaveAlreadyRead(bookTitle) {
    let readByList = getReadBooks()[bookTitle.toLowerCase()]

    if (readByList) {
        readByList.forEach(pet => {
            $("#iteminfo_select_action option:contains(" + pet + ")").remove()
        });
    }
}

// Home Page Functions
function setupBookReadingTrackerForHomePage() {
     resetStoredValuesWhenNotReadingToPet()
     listenForReadingActionFlowTriggered()
}

function listenForReadingActionFlowTriggered() {
     $("#petCareLinkRead").on("click", function() {
        petCareListObserver.observe(document.getElementById("petCareList"), {childList: true, subtree: true})
     })
}

function resetStoredValuesWhenNotReadingToPet() {
    $("div.petCare-buttons").children().not("#petCareLinkRead").each(function() {
        // Disconnect from observing, so that the on-click isn't performed for non-read actions
        $(this).on("click", function() {
            GM_setValue("selectedUnreadBook", {})
            petCareListObserver.disconnect()
        })
    })
}

function correctUnreadBookCount(removedBookCount) {
     let itemCount = $("div.petCare-itemcount")

     if (itemCount) {
         let unreadBookCountTextHtml = itemCount.text()
         let bookCountBeforeRemoval = unreadBookCountTextHtml.match(/\d+$/)
         let actualUnreadCount = Number(bookCountBeforeRemoval) - Number(removedBookCount)
         let replacementText = unreadBookCountTextHtml.replace(/\d+$/, actualUnreadCount)
         itemCount.html(replacementText)
     }
}

function listenForReadToPetAction() {
   // Start by storing any selected book that is clicked from the Read list
   $('div.petCare-itemgrid').find('[data-action~="Read"]').each(function () {
       $(this).on("click", function() {
           let title = $(this).data("itemname")
           let selectedPet = $(this).data("action").split(" ").pop()
           GM_setValue("selectedUnreadBook", {
               bookTitle: title,
               petName: selectedPet })
       })
   })

   // Listen for clicks on the action button
   $("#petCareUseItem").on("click", function () {
       let selectedUnreadBook = GM_getValue("selectedUnreadBook", {})
       let bookTitle = selectedUnreadBook.bookTitle
       let petName = selectedUnreadBook.petName

       if (bookTitle && petName) {
           onBookReadToPet(bookTitle, petName)
       }

       GM_setValue("selectedUnreadBook", {})
   })
}

function removeReadBooksFromPetCareList() {
    var countRemovedBooks = 0

    $('[data-action~="Read"]').each(function() {
        var bookTitle = $(this).data("itemname")

        if (bookTitle) {
            bookTitle = bookTitle.toString()

            let petName = $(this).data("action").toString().split(" ").pop()

            if (petHasAlreadyReadBook(petName, bookTitle)) {
                $(this).remove()
                countRemovedBooks++
            }
        }
    })

    return countRemovedBooks
}

// Clean Up Functions
function cleanUpUnfinalizedActions() {
    GM_setValue("pendingReadAction", {})
    GM_setValue("selectedUnreadBook", {})
}

// Storage Functions (this is how we track what has been read so far)
function onBookReadToPet(bookTitle, petName) {
    // This shouldn't ever happen but if it does, we'll short-circuit here
    if (petHasAlreadyReadBook(petName, bookTitle)) {
       return
    }

    let bookTitleSanitized = bookTitle.toLowerCase()
    var bookList = getReadBooks()

    var readPetList = bookList[bookTitleSanitized]

    if (readPetList) {
        readPetList.push(petName)
    } else {
        readPetList = [petName]
    }

    bookList[bookTitleSanitized] = readPetList
    GM_setValue("readBooks", bookList)
}

function petHasAlreadyReadBook(petName, bookTitle) {
    let readByList = getReadBooks()[bookTitle.toLowerCase()]
    return readByList && readByList.includes(petName)
}

function getReadBooks() {
    return GM_getValue("readBooks", {})
}

function storeReadBooks() {
    let pet = new URLSearchParams(window.location.search).get('pet_name')

    let bookList = getReadBooks()

    $("td.content").find("td:nth-child(2)").each(function(i) {
        // Skip the first tr because it is the table "header" of the read books list
        if (i == 0) {
            return
        }

       let bookTitle = $(this).html().match(/(?:(?!:).)*/).toString()
       onBookReadToPet(bookTitle, pet)
    })
}