您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Automatically add comics to reading list when reaching the end of a chapter
// ==UserScript== // @name Comick Auto Follow on Read // @namespace https://github.com/GooglyBlox // @version 1.0 // @description Automatically add comics to reading list when reaching the end of a chapter // @author GooglyBlox // @match https://comick.io/* // @connect api.comick.io // @connect api.comick.fun // @grant none // @license MIT // ==/UserScript== (function() { 'use strict'; const API_ENDPOINTS = { comic: 'https://api.comick.fun/comic/', follow: 'https://api.comick.io/follow', whoami: 'https://api.comick.io/v1.0/sessions/whoami/get' }; const READING_LIST_ID = 1; const state = { observer: null, scrollListener: null, currentComicId: null, currentComicHid: null, currentComicSlug: null, hasTriggered: false, isProcessing: false, userList: null }; function isMobile() { return window.innerWidth <= 768 || /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } function extractComicSlug() { const pathMatch = window.location.pathname.match(/\/comic\/([^\/]+)\//); return pathMatch ? pathMatch[1] : null; } function isChapterPage() { return /^\/comic\/[^\/]+\/[^\/]+-chapter-/.test(window.location.pathname); } async function getComicInfo(slug) { try { const response = await fetch(`${API_ENDPOINTS.comic}${slug}/?tachiyomi=true`); if (!response.ok) { throw new Error(`Comic API failed: HTTP ${response.status}`); } const data = await response.json(); return data.comic; } catch (error) { console.error('Comic info error:', error); return null; } } async function getUserList() { try { const response = await fetch(API_ENDPOINTS.whoami, { method: 'POST', credentials: 'include' }); if (!response.ok) { return null; } const data = await response.json(); return data.info?.list || []; } catch (error) { console.error('Error fetching user list:', error); return null; } } async function followComic(comicId, listType = READING_LIST_ID) { try { const response = await fetch(API_ENDPOINTS.follow, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ id: comicId, t: listType }), credentials: 'include' }); return { success: response.ok, status: response.status, data: response.ok ? await response.json() : null }; } catch (error) { console.error('Follow API error:', error); return { success: false, error: error.message }; } } function showNotification(message, isSuccess = true) { const notification = document.createElement('div'); notification.className = `fixed top-4 right-4 z-50 p-3 rounded-lg text-white font-medium transition-all duration-300 ${ isSuccess ? 'bg-green-600' : 'bg-red-600' }`; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.style.opacity = '0'; notification.style.transform = 'translateX(100%)'; setTimeout(() => notification.remove(), 300); }, 3000); } async function handleAutoFollow() { if (state.isProcessing || state.hasTriggered || !state.currentComicSlug) { return; } state.isProcessing = true; try { if (state.userList === null) { state.userList = await getUserList(); } if (state.userList === null) { showNotification('Unable to check reading list - please login', false); state.isProcessing = false; return; } if (!state.currentComicId || !state.currentComicHid) { const comicInfo = await getComicInfo(state.currentComicSlug); if (!comicInfo) { showNotification('Comic info not found', false); state.isProcessing = false; return; } state.currentComicId = comicInfo.id; state.currentComicHid = comicInfo.hid; } if (state.userList.includes(state.currentComicHid)) { state.hasTriggered = true; state.isProcessing = false; return; } const result = await followComic(state.currentComicId); if (result.success) { showNotification('Added to Reading list!', true); state.hasTriggered = true; if (state.userList) { state.userList.push(state.currentComicHid); } } else if (result.status === 409) { showNotification('Already in your reading list', true); state.hasTriggered = true; if (state.userList && !state.userList.includes(state.currentComicHid)) { state.userList.push(state.currentComicHid); } } else { showNotification(`Failed to add to reading list (${result.status})`, false); } } catch (error) { console.error('Auto-follow error:', error); showNotification('Error adding to reading list', false); } finally { state.isProcessing = false; } } function checkScrollPosition() { if (state.hasTriggered || state.isProcessing) { return; } if (isMobile()) { const navElement = document.querySelector('div.flex.items-center > a[href*="chapter-"]'); if (navElement) { const rect = navElement.getBoundingClientRect(); const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight; if (isVisible) { handleAutoFollow(); } } } else { const scrollPosition = window.scrollY + window.innerHeight; const documentHeight = document.documentElement.scrollHeight; const threshold = documentHeight - 100; if (scrollPosition >= threshold) { handleAutoFollow(); } } } function setupScrollDetection() { if (state.scrollListener) { window.removeEventListener('scroll', state.scrollListener); } state.scrollListener = function() { requestAnimationFrame(checkScrollPosition); }; window.addEventListener('scroll', state.scrollListener, { passive: true }); } function resetState() { state.hasTriggered = false; state.isProcessing = false; state.currentComicId = null; state.currentComicHid = null; state.currentComicSlug = null; } function initializeChapterPage() { if (!isChapterPage()) { return; } resetState(); state.currentComicSlug = extractComicSlug(); if (state.currentComicSlug) { setupScrollDetection(); } } function handleNavigation() { setTimeout(() => { initializeChapterPage(); }, 500); } function startObserver() { if (state.observer) { state.observer.disconnect(); } state.observer = new MutationObserver((mutations) => { const hasContentChanges = mutations.some(mutation => mutation.type === 'childList' && mutation.addedNodes.length > 0 ); if (hasContentChanges) { handleNavigation(); } }); state.observer.observe(document.body, { childList: true, subtree: true }); } function cleanup() { if (state.observer) { state.observer.disconnect(); } if (state.scrollListener) { window.removeEventListener('scroll', state.scrollListener); } } function init() { initializeChapterPage(); startObserver(); window.addEventListener('popstate', handleNavigation); window.addEventListener('beforeunload', cleanup); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();