您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Track manhwa reading progress on Toonily.com
// ==UserScript== // @name Toonily Manhwa Tracker // @namespace https://github.com/Sheikhlipu123 // @version 1.0 // @description Track manhwa reading progress on Toonily.com // @author Sheikhlipu123 // @match https://toonily.com/* // @grant none // @run-at document-end // @homepage https://github.com/Sheikhlipu123/Toonily-Manhwa-Tracker/ // @supportURL https://github.com/Sheikhlipu123/Toonily-Manhwa-Tracker/issues // @license MIT // ==/UserScript== ;(() => { // Storage key for localStorage const STORAGE_KEY = "toonily_manhwa_tracker" // Initialize or get existing data function getTrackedData() { const stored = localStorage.getItem(STORAGE_KEY) return stored ? JSON.parse(stored) : {} } // Save data to localStorage function saveTrackedData(data) { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)) } // Extract manhwa name from URL function getManhwaNameFromUrl(url = window.location.href) { const match = url.match(/\/serie\/([^/]+)/) if (match) { return match[1].replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()) } return null } // Extract chapter number from URL function getChapterFromUrl(url = window.location.href) { const match = url.match(/\/chapter-(\d+(?:\.\d+)?)/) return match ? match[1] : null } // Get manhwa thumbnail function getManhwaThumbnail() { const imgElement = document.querySelector(".summary_image img") if (imgElement) { return imgElement.getAttribute("data-src") || imgElement.src } return null } // Get manhwa title from breadcrumb or page title function getManhwaTitle() { const breadcrumbLink = document.querySelector('.c-breadcrumb a[href*="/serie/"]') if (breadcrumbLink) { return breadcrumbLink.textContent.trim() } const titleElement = document.querySelector("h1, .post-title") if (titleElement) { return titleElement.textContent.trim() } return getManhwaNameFromUrl() } // Track current page function trackCurrentPage() { const manhwaName = getManhwaNameFromUrl() if (!manhwaName) return const chapter = getChapterFromUrl() const data = getTrackedData() const now = new Date().toISOString() const currentUrl = window.location.href if (!data[manhwaName]) { data[manhwaName] = { title: getManhwaTitle() || manhwaName, thumbnail: getManhwaThumbnail(), serieUrl: `https://toonily.com/serie/${manhwaName.toLowerCase().replace(/\s+/g, "-")}/`, chapters: {}, lastVisited: now, firstVisited: now, } } // Update last visited data[manhwaName].lastVisited = now // If we're on a chapter page, track it if (chapter) { if (!data[manhwaName].chapters[chapter]) { data[manhwaName].chapters[chapter] = { firstRead: now, readCount: 0, url: currentUrl, } } data[manhwaName].chapters[chapter].lastRead = now data[manhwaName].chapters[chapter].readCount++ data[manhwaName].chapters[chapter].url = currentUrl } // Update thumbnail if we're on serie page and don't have one if (!data[manhwaName].thumbnail && currentUrl.includes("/serie/")) { const thumbnail = getManhwaThumbnail() if (thumbnail) { data[manhwaName].thumbnail = thumbnail } } saveTrackedData(data) } // Create tracker panel function createTrackerPanel() { // Create panel container const panel = document.createElement("div") panel.id = "manhwa-tracker-panel" panel.innerHTML = ` <div id="tracker-toggle" style=" position: fixed; top: 20px; right: 20px; z-index: 10000; background: #2c3e50; color: white; padding: 10px 15px; border-radius: 5px; cursor: pointer; font-family: Arial, sans-serif; font-size: 14px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); user-select: none; "> 📚 Tracker </div> <div id="tracker-popup" style=" position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 10001; display: none; font-family: Arial, sans-serif; "> <div style=" position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; width: 90%; max-width: 1000px; height: 80%; border-radius: 10px; overflow: hidden; box-shadow: 0 5px 25px rgba(0,0,0,0.5); "> <div style=" background: #34495e; color: white; padding: 15px 20px; display: flex; justify-content: space-between; align-items: center; "> <h2 style="margin: 0; font-size: 18px;">Manhwa Tracker</h2> <div> <button id="export-data" style=" background: #27ae60; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; margin-right: 10px; font-size: 12px; ">Export</button> <button id="import-data" style=" background: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; margin-right: 10px; font-size: 12px; ">Import</button> <span id="close-tracker" style=" cursor: pointer; font-size: 24px; font-weight: bold; ">×</span> </div> </div> <div id="tracker-content" style=" padding: 20px; height: calc(100% - 70px); overflow-y: auto; "></div> </div> </div> ` document.body.appendChild(panel) // Event listeners document.getElementById("tracker-toggle").addEventListener("click", showTracker) document.getElementById("close-tracker").addEventListener("click", hideTracker) document.getElementById("export-data").addEventListener("click", exportData) document.getElementById("import-data").addEventListener("click", importData) // Close on background click document.getElementById("tracker-popup").addEventListener("click", function (e) { if (e.target === this) hideTracker() }) } // Show tracker popup function showTracker() { document.getElementById("tracker-popup").style.display = "block" updateTrackerContent() } // Hide tracker popup function hideTracker() { document.getElementById("tracker-popup").style.display = "none" } // Update tracker content function updateTrackerContent() { const data = getTrackedData() const content = document.getElementById("tracker-content") if (Object.keys(data).length === 0) { content.innerHTML = '<p style="text-align: center; color: #666; margin-top: 50px;">No manhwa tracked yet. Visit some manhwa pages to start tracking!</p>' return } let html = ` <div style="margin-bottom: 20px;"> <h3>Tracked Manhwa (${Object.keys(data).length})</h3> <input type="text" id="search-manhwa" placeholder="Search manhwa..." style=" width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; margin-bottom: 15px; font-size: 14px; "> </div> <div id="manhwa-list"> ` // Sort manhwa by last visited const sortedManhwa = Object.entries(data).sort((a, b) => new Date(b[1].lastVisited) - new Date(a[1].lastVisited)) sortedManhwa.forEach(([key, manhwa]) => { const chapterCount = Object.keys(manhwa.chapters).length const latestChapter = chapterCount > 0 ? Math.max(...Object.keys(manhwa.chapters).map(Number)) : 0 const lastVisited = new Date(manhwa.lastVisited).toLocaleDateString() html += ` <div class="manhwa-item" style=" border: 1px solid #ddd; border-radius: 8px; margin-bottom: 15px; padding: 15px; background: #f9f9f9; "> <div style="display: flex; gap: 15px;"> <div style="flex-shrink: 0;"> ${ manhwa.thumbnail ? ` <img src="${manhwa.thumbnail}" alt="${manhwa.title}" style=" width: 80px; height: 110px; object-fit: cover; border-radius: 5px; border: 1px solid #ddd; "> ` : ` <div style=" width: 80px; height: 110px; background: #ddd; border-radius: 5px; display: flex; align-items: center; justify-content: center; color: #666; font-size: 12px; ">No Image</div> ` } </div> <div style="flex: 1;"> <h4 style="margin: 0 0 10px 0; color: #2c3e50;"> <a href="${manhwa.serieUrl}" target="_blank" style="text-decoration: none; color: inherit;"> ${manhwa.title} </a> </h4> <div style="color: #666; font-size: 13px; margin-bottom: 10px;"> <div><strong>Chapters Read:</strong> ${chapterCount}</div> <div><strong>Latest Chapter:</strong> ${latestChapter || "None"}</div> <div><strong>Last Visited:</strong> ${lastVisited}</div> </div> <button onclick="showChapterDetails('${key}')" style=" background: #3498db; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; margin-right: 10px; ">View Chapters</button> <button onclick="deleteManhwa('${key}')" style=" background: #e74c3c; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; ">Delete</button> </div> </div> <div id="chapters-${key}" style="display: none; margin-top: 15px; padding-top: 15px; border-top: 1px solid #ddd;"></div> </div> ` }) html += "</div>" content.innerHTML = html // Add search functionality document.getElementById("search-manhwa").addEventListener("input", (e) => { const searchTerm = e.target.value.toLowerCase() const manhwaItems = document.querySelectorAll(".manhwa-item") manhwaItems.forEach((item) => { const title = item.querySelector("h4").textContent.toLowerCase() item.style.display = title.includes(searchTerm) ? "block" : "none" }) }) } // Show chapter details window.showChapterDetails = (manhwaKey) => { const data = getTrackedData() const manhwa = data[manhwaKey] const chaptersDiv = document.getElementById(`chapters-${manhwaKey}`) if (chaptersDiv.style.display === "none") { let chaptersHtml = '<h5 style="margin: 0 0 10px 0;">Chapter History:</h5>' if (Object.keys(manhwa.chapters).length === 0) { chaptersHtml += '<p style="color: #666; font-style: italic;">No chapters read yet.</p>' } else { chaptersHtml += '<div style="max-height: 200px; overflow-y: auto;">' // Sort chapters by number const sortedChapters = Object.entries(manhwa.chapters).sort((a, b) => Number(b[0]) - Number(a[0])) sortedChapters.forEach(([chapterNum, chapterData]) => { const firstRead = new Date(chapterData.firstRead).toLocaleDateString() const lastRead = chapterData.lastRead ? new Date(chapterData.lastRead).toLocaleDateString() : firstRead chaptersHtml += ` <div style=" background: white; padding: 8px 12px; margin-bottom: 5px; border-radius: 4px; border: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; "> <div> <strong>Chapter ${chapterNum}</strong> <div style="font-size: 11px; color: #666;"> First: ${firstRead} | Last: ${lastRead} | Read ${chapterData.readCount}x </div> </div> <a href="${chapterData.url}" target="_blank" style=" background: #27ae60; color: white; text-decoration: none; padding: 4px 8px; border-radius: 3px; font-size: 11px; ">Read</a> </div> ` }) chaptersHtml += "</div>" } chaptersDiv.innerHTML = chaptersHtml chaptersDiv.style.display = "block" } else { chaptersDiv.style.display = "none" } } // Delete manhwa window.deleteManhwa = (manhwaKey) => { if (confirm("Are you sure you want to delete this manhwa from tracking?")) { const data = getTrackedData() delete data[manhwaKey] saveTrackedData(data) updateTrackerContent() } } // Export data function exportData() { const data = getTrackedData() const dataStr = JSON.stringify(data, null, 2) const dataBlob = new Blob([dataStr], { type: "application/json" }) const link = document.createElement("a") link.href = URL.createObjectURL(dataBlob) link.download = `toonily-tracker-${new Date().toISOString().split("T")[0]}.json` link.click() } // Import data function importData() { const input = document.createElement("input") input.type = "file" input.accept = ".json" input.onchange = (e) => { const file = e.target.files[0] if (!file) return const reader = new FileReader() reader.onload = (e) => { try { const importedData = JSON.parse(e.target.result) const currentData = getTrackedData() // Merge data const mergedData = { ...currentData, ...importedData } saveTrackedData(mergedData) alert("Data imported successfully!") updateTrackerContent() } catch (error) { alert("Error importing data: Invalid JSON file") } } reader.readAsText(file) } input.click() } // Initialize function init() { // Track current page trackCurrentPage() // Create tracker panel createTrackerPanel() // Track navigation changes (for SPA-like behavior) let currentUrl = window.location.href setInterval(() => { if (window.location.href !== currentUrl) { currentUrl = window.location.href setTimeout(trackCurrentPage, 1000) // Delay to let page load } }, 1000) } // Wait for page to load completely if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init) } else { init() } })()