JavDB列表页显示是否已看

在演员列表页,显示每部影片是否已看,就难得点进去看了

// ==UserScript==
// @name         JavDB列表页显示是否已看
// @namespace     http://tampermonkey.net/
// @version      2024-11-20-1630
// @description   在演员列表页,显示每部影片是否已看,就难得点进去看了
// @author       Ryen
// @match        https://javdb.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=javdb.com
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @license      GPL-3.0-only
// ==/UserScript==


const fadeDuration = 500; // 消息渐变持续时间
const displayDuration = 3000; // 消息显示持续时间
const maxMessages = 3; // 最大显示消息数量
let counter = 0; // 初始化计数器
let lastItemCount = 0; // 存储上一次的电影项目数量

let storedIds = new Set(); // 使用 Set 存储唯一 ID

const styleMap = {
    '我看過這部影片': 'tag is-success is-light',
    '我想看這部影片': 'tag is-info is-light',
    '未看过': 'tag is-gray',
};

const indicatorTexts = ['我看過這部影片', '我想看這部影片'];

const validUrlPatterns = [
    /https:\/\/javdb\.com\/users\/want_watch_videos.*/,
    /https:\/\/javdb\.com\/users\/watched_videos.*/,
    /https:\/\/javdb\.com\/users\/list_detail.*/,
    /https:\/\/javdb\.com\/lists.*/
];

// 消息容器
const messageContainer = document.createElement('div');
messageContainer.style.position = 'fixed';
messageContainer.style.bottom = '20px';
messageContainer.style.right = '20px';
messageContainer.style.zIndex = '9999';
messageContainer.style.pointerEvents = 'none';
messageContainer.style.maxWidth = '500px';
messageContainer.style.display = 'flex';
messageContainer.style.flexDirection = 'column';
document.body.appendChild(messageContainer);

// 渐入效果
function fadeIn(el) {
    el.style.opacity = 0;
    el.style.display = 'block';

    const startTime = performance.now();
    function animate(time) {
        const elapsed = time - startTime;
        el.style.opacity = Math.min((elapsed / fadeDuration), 1);
        if (elapsed < fadeDuration) {
            requestAnimationFrame(animate);
        }
    }
    requestAnimationFrame(animate);
}

// 渐出效果
function fadeOut(el) {
    const startTime = performance.now();
    function animate(time) {
        const elapsed = time - startTime;
        el.style.opacity = 1 - Math.min((elapsed / fadeDuration), 1);
        if (elapsed < fadeDuration) {
            requestAnimationFrame(animate);
        } else {
            el.remove();
        }
    }
    requestAnimationFrame(animate);
}

// 显示信息
function logToScreen(message, bgColor = 'rgba(169, 169, 169, 0.8)', textColor = 'white') {

    const messageBox = document.createElement('div');
    messageBox.style.padding = '10px';
    messageBox.style.borderRadius = '5px';
    messageBox.style.backgroundColor = bgColor;
    messageBox.style.color = textColor;
    messageBox.style.fontSize = '12px';
    messageBox.style.marginBottom = '10px';
    messageBox.style.pointerEvents = 'none';
    messageBox.style.wordWrap = 'break-word';
    messageBox.style.maxWidth = '100%';

    messageBox.innerHTML = message;
    messageContainer.appendChild(messageBox);


    fadeIn(messageBox);


    setTimeout(() => {
        fadeOut(messageBox);
    }, displayDuration);


    if (messageContainer.childElementCount > maxMessages) {
        fadeOut(messageContainer.firstChild);
    }
}
async function sleep(ms) {
            return new Promise((resolve) => setTimeout(resolve, ms));
        }


(function () {
    'use strict';


    let panelVisible = false; // 初始状态不显示
    const circlePosition = { left: -40, top: 60 }; // 默认圆位置
    let lastUploadTime = ""; // 最新的上传时间

    // 面板
    const panel = document.createElement('div');
    panel.style.position = 'fixed';
    panel.style.border = '1px solid #ccc';
    panel.style.backgroundColor = 'white';
    panel.style.padding = '10px';
    panel.style.boxShadow = '0px 0px 10px rgba(0, 0, 0, 0.5)';
    panel.style.width = '300px';
    panel.style.borderRadius = '10px';
    panel.style.display = 'none'; // 隐藏
    panel.style.zIndex = 10001;
    document.body.appendChild(panel);

    // 标题
    const title = document.createElement('div');
    title.textContent = '番号数据上传与搜索';
    title.style.fontWeight = 'normal';
    title.style.marginBottom = '10px';
    title.style.position = 'relative';
    panel.appendChild(title);

    // 帮助符号
    const titleHelpIcon = document.createElement('span');
    titleHelpIcon.textContent = 'ℹ️';
    titleHelpIcon.style.cursor = 'pointer';
    titleHelpIcon.style.marginLeft = '10px';
    titleHelpIcon.title = '目前只过滤“看过”,更新脚本数据会被清空';
    title.appendChild(titleHelpIcon);

    // 文件上传按钮
    const uploadButton = document.createElement('input');
    uploadButton.type = 'file';
    uploadButton.accept = '.json';
    panel.appendChild(uploadButton);

    // 帮助符号
    const uploadHelpIcon = document.createElement('span');
    uploadHelpIcon.textContent = 'ℹ️'; // 图标
    uploadHelpIcon.style.cursor = 'pointer';
    uploadHelpIcon.style.marginLeft = '10px';
    uploadHelpIcon.title = '前往“看过”页面进行导出文件';
    uploadButton.parentNode.insertBefore(uploadHelpIcon, uploadButton.nextSibling);

    // 导出按钮
    const exportButton = document.createElement('button');
    exportButton.textContent = '导出存储番号';
    exportButton.style.marginTop = '10px';
    panel.appendChild(exportButton);

    // 清除存储按钮
    const clearButton = document.createElement('button');
    clearButton.textContent = '清除存储番号';
    clearButton.style.marginTop = '10px';
    panel.appendChild(clearButton);

    // 唯一 ID 数量
    const idCountDisplay = document.createElement('div');
    idCountDisplay.style.marginTop = '10px';
    idCountDisplay.style.color = '#A9A9A9'; // 浅灰色
    panel.appendChild(idCountDisplay);

    // 最新上传时间
    const uploadTimeDisplay = document.createElement('div');
    uploadTimeDisplay.style.marginTop = '5px';
    uploadTimeDisplay.style.color = '#A9A9A9'; // 浅灰色
    panel.appendChild(uploadTimeDisplay);

    // 搜索框
    const searchBox = document.createElement('input');
    searchBox.type = 'text';
    searchBox.placeholder = '搜索存储的番号';
    searchBox.style.marginTop = '10px';
    panel.appendChild(searchBox);

    // 搜索结果
    const resultContainer = document.createElement('div');
    resultContainer.style.display = 'none'; // 初始为隐藏
    resultContainer.style.marginTop = '10px';
    resultContainer.style.maxHeight = '150px';
    resultContainer.style.overflowY = 'auto';
    resultContainer.style.border = '1px solid #ccc';
    resultContainer.style.backgroundColor = '#f9f9f9';
    resultContainer.style.padding = '5px';
    panel.appendChild(resultContainer);

    // 加载存储的 ID 和上传时间
    const rawData = GM_getValue('myIds');
    const savedUploadTime = GM_getValue('lastUploadTime');
    if (rawData) {
        storedIds = new Set(rawData); // 如果之前存过数据,加载到 Set
        updateIdCountDisplay(); // 更新 ID 计数显示
    }
    if (savedUploadTime) {
        lastUploadTime = savedUploadTime; // 恢复最新上传时间
        uploadTimeDisplay.textContent = `最新上传时间: ${lastUploadTime}`;
    }

    // 处理文件上传
    uploadButton.addEventListener('change', handleFileUpload);

    // 处理导出按钮点击事件
    exportButton.addEventListener('click', exportIds);

    // 处理清除存储 ID 点击事件
    clearButton.addEventListener('click', clearStoredIds);

    // 处理搜索
    searchBox.addEventListener('input', handleSearch);


    const circle = createCircle(circlePosition.left, circlePosition.top);

    function handleFileUpload(event) {
        const file = event.target.files[0];
        if (!file) {
            return;
        }

        const reader = new FileReader();
        reader.onload = function (e) {
            try {
                const jsonData = JSON.parse(e.target.result);

                jsonData.forEach(item => {
                    if (item.id) {
                        storedIds.add(item.id);
                    }
                });

                GM_setValue('myIds', Array.from(storedIds));


                lastUploadTime = new Date().toLocaleString();
                GM_setValue('lastUploadTime', lastUploadTime);
                uploadTimeDisplay.textContent = `最新上传时间: ${lastUploadTime}`;

                alert('数据已保存');
                updateIdCountDisplay(); // 更新 ID 计数显示
            } catch (error) {
                console.error('解析 JSON 失败:', error);
                alert('解析 JSON 失败');
            }
        };

        reader.readAsText(file);
    }

    function exportIds() {
        // 将存储的唯一 IDs 转换为 JSON 字符串
        const jsonData = JSON.stringify(Array.from(storedIds).map(id => ({ id })), null, 2);
        const blob = new Blob([jsonData], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'unique_ids.json'; // 下载的文件名
        document.body.appendChild(a);
        a.click();
        setTimeout(() => {
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }, 0);
    }

    function clearStoredIds() {
        if (confirm('确认要清除所有存储的番号吗?')) {
            storedIds.clear();
            GM_setValue('myIds', []); // 清空存储
            lastUploadTime = ""; // 清空上传时间
            GM_setValue('lastUploadTime', null); // 删除上传时间存储
            uploadTimeDisplay.textContent = ""; // 清空显示
            updateIdCountDisplay(); // 更新 ID 计数显示
            alert('所有存储的番号已清除');
        }
    }

    function handleSearch() {
        const query = searchBox.value.toLowerCase();
        resultContainer.innerHTML = '';

        if (query) {

            resultContainer.style.display = 'block';
            if (storedIds.size > 0) {
                const results = Array.from(storedIds).filter(id => id.toLowerCase().includes(query));
                // 显示搜索结果
                results.forEach(id => {
                    const div = document.createElement('div');
                    div.textContent = id;
                    resultContainer.appendChild(div);
                });
            }
        } else {

            resultContainer.style.display = 'none';
        }
    }

    function updateIdCountDisplay() {
        idCountDisplay.textContent = `已看番号总数: ${storedIds.size}`;
    }

    function createCircle(left, top) {

        const existingCircle = document.getElementById('unique-circle');
        if (existingCircle) {
            existingCircle.remove();
        }

        const circle = document.createElement('div');
        circle.id = 'unique-circle';
        circle.style.position = 'fixed';
        circle.style.width = '60px';
        circle.style.height = '60px';
        circle.style.borderRadius = '50%';
        circle.style.backgroundColor = '#ed0085'; // 初始颜色
        circle.style.cursor = 'pointer';
        circle.style.zIndex = 10000;
        circle.style.left = `${left}px`;
        circle.style.top = `${top}px`;
        document.body.appendChild(circle);


        const label = document.createElement('div');
        label.textContent = '番';
        label.style.fontSize = '20px';
        label.style.color = 'white';
        label.style.textAlign = 'center';
        label.style.lineHeight = '60px';
        circle.appendChild(label);

        // 悬停
        circle.addEventListener('mouseenter', function () {
            circle.style.transition = 'left 0.3s, background-color 0.3s';
            circle.style.left = '0px';
            circle.style.backgroundColor = '#ed0085'; // 恢复颜色
        });

        circle.addEventListener('mouseleave', function () {
            circle.style.transition = 'left 0.3s, background-color 0.3s';
            circle.style.left = '-40px';
            circle.style.backgroundColor = 'rgba(237, 0, 133, 0.7)'; // 改变颜色
        });

        // 展开面板
        circle.addEventListener('click', function () {
            panel.style.display = 'block'; // 显示面板
            panelVisible = true;

            // 设置面板位置
            panel.style.left = '10px';
            panel.style.top = `${top}px`;
            panel.style.transition = 'none';
            setupMouseLeave(panel);
        });

        return circle;
    }

    function setupMouseLeave(panelElement) {
        panelElement.addEventListener('mouseleave', function() {

            panelElement.style.display = 'none';
            panelVisible = false;
        });
    }


    createCircle(circlePosition.left, circlePosition.top);
})();


(function () {

	const url = window.location.href;


	const validUrlPatterns = [
		/https:\/\/javdb\.com\/users\/want_watch_videos.*/,
		/https:\/\/javdb\.com\/users\/watched_videos.*/,
		/https:\/\/javdb\.com\/users\/list_detail.*/,
		/https:\/\/javdb\.com\/lists.*/
	];


	const isValidUrl = validUrlPatterns.some(pattern => pattern.test(url));
	if (!isValidUrl) {
		return;
	}



	let allVideosInfo = JSON.parse(localStorage.getItem('allVideosInfo')) || [];
	let exportState = {
		allowExport: false,
		currentPage: 1,
		maxPage: null
	};
	let exportButton = null;

	function getVideosInfo() {
		const videoElements = document.querySelectorAll('.item');
		return Array.from(videoElements).map((element) => {
			const title = element.querySelector('.video-title').textContent.trim();
			const [id, ...titleWords] = title.split(' ');
			const releaseDate = element.querySelector('.meta').textContent.replace(/[^0-9-]/g, '');
			return { id, releaseDate };
		});
	}

	function scrapeAllPages() {

		if ((exportState.maxPage && exportState.currentPage > exportState.maxPage) ||
			!document.querySelector('.pagination-next')) {
			exportVideosInfo();
			return;
		}

		const videosInfo = getVideosInfo();
		allVideosInfo = allVideosInfo.concat(videosInfo);


		exportState.currentPage++;
		localStorage.setItem('exportState', JSON.stringify(exportState));
		localStorage.setItem('allVideosInfo', JSON.stringify(allVideosInfo));

		const nextPageButton = document.querySelector('.pagination-next');
		if (nextPageButton) {
			nextPageButton.click();
			setTimeout(() => scrapeAllPages(), 2000);
		} else {
			exportVideosInfo();
		}
	}

	function exportVideosInfo() {

		exportState.allowExport = false;
		exportState.currentPage = 1;
		exportState.maxPage = null;

		localStorage.setItem('exportState', JSON.stringify(exportState));

		allVideosInfo.sort((a, b) => a.id.localeCompare(b.id));
		const json = JSON.stringify(allVideosInfo);
		const jsonBlob = new Blob([json], { type: 'application/json' });
		const jsonUrl = URL.createObjectURL(jsonBlob);
		const downloadLink = document.createElement('a');
		const dateTime = new Date().toLocaleString('zh-CN', {
			year: 'numeric',
			month: '2-digit',
			day: '2-digit',
			hour: '2-digit',
			minute: '2-digit',
			second: '2-digit',
			hour12: false
		}).replace(/\//g, '-').replace(',', '');

		let fileName = '';
		if (url.includes('/watched_videos')) {
			fileName = 'watched-videos';
		} else if (url.includes('/want_watch_videos')) {
			fileName = 'want-watch-videos';
		} else if (url.includes('/list_detail')) {
			const breadcrumb = document.getElementsByClassName('breadcrumb')[0];
			const li = breadcrumb.parentNode.querySelectorAll('li');
			fileName = li[1].innerText;
		} else if (url.includes('/lists')) {
			fileName = document.querySelector('.actor-section-name').innerText;
		}

		downloadLink.href = jsonUrl;
		downloadLink.download = `${fileName} ${dateTime}.json`;
		document.body.appendChild(downloadLink);
		downloadLink.click();


		localStorage.removeItem('allVideosInfo');
		localStorage.removeItem('exportState');

		exportButton.textContent = '导出完毕';
		exportButton.disabled = false;
	}

	function startExport() {

		const allImages = document.querySelectorAll('img');
		allImages.forEach((image) => image.remove());


		const maxPageInput = document.getElementById('maxPageInput');
		exportState.maxPage = maxPageInput.value ? parseInt(maxPageInput.value) : null;
		exportState.allowExport = true;
		exportState.currentPage = 1;


		localStorage.setItem('exportState', JSON.stringify(exportState));


		exportButton.textContent = '导出中...';
		exportButton.disabled = true;


		allVideosInfo = [];


		scrapeAllPages();
	}

	function createExportButton() {

		const maxPageInput = document.createElement('input');
		maxPageInput.type = 'number';
		maxPageInput.id = 'maxPageInput';
		maxPageInput.placeholder = '往后获取多少页数';
		maxPageInput.style.marginRight = '10px';

		exportButton = document.createElement('button');
		exportButton.textContent = '导出 json';
		exportButton.className = 'button is-small';
		exportButton.addEventListener('click', startExport);

		const exportContainer = document.createElement('div');
		exportContainer.style.display = 'flex';
		exportContainer.style.alignItems = 'center';
		exportContainer.appendChild(maxPageInput);
		exportContainer.appendChild(exportButton);

		if (url.includes('/list_detail')) {
			document.querySelector('.breadcrumb').querySelector('ul').appendChild(exportContainer);
		} else {
			document.querySelector('.toolbar').appendChild(exportContainer);
		}
	}


	function checkExportState() {
		const savedExportState = localStorage.getItem('exportState');
		if (savedExportState) {
			exportState = JSON.parse(savedExportState);
			const savedVideosInfo = localStorage.getItem('allVideosInfo');

			if (exportState.allowExport) {
				if (savedVideosInfo) {
					allVideosInfo = JSON.parse(savedVideosInfo);
				}

				exportButton.textContent = '导出中...';
				exportButton.disabled = true;

				scrapeAllPages();
			}
		}
	}

	if (url.includes('/watched_videos')
		|| url.includes('/want_watch_videos')
		|| url.includes('/list_detail')
		|| url.includes('/lists')
	) {
		createExportButton();
		checkExportState();
	}
})();

function modifyItemAtCurrentPage(itemToModify, textIndicator) {
    let tags = itemToModify.closest('.item').querySelector('.tags.has-addons');
    let tagClass = styleMap[textIndicator];

    // 检查是否已经存在相同的标记
    let existingTags = Array.from(tags.querySelectorAll('span')); // 获取所有已存在的标签
    let tagExists = existingTags.some(tag => tag.textContent === textIndicator); // 检查是否有相同内容的标签

    // 如果不存在对应的标签,则添加新的标签
    if (!tagExists) {
        let newTag = document.createElement('span');
        newTag.className = tagClass;
        newTag.textContent = textIndicator;
        tags.appendChild(newTag);
    } else {
        console.log(`标签 "${textIndicator}" 已经存在于该项目中,未重复添加.`);
    }
}

async function processLoadedItems() {
    const url = window.location.href; // 获取当前 URL

    const isValidUrl = validUrlPatterns.some(pattern => pattern.test(url));
    if (isValidUrl) {
        return;
    }
    // 获取当前页面的影片元素
    let items = Array.from(document.querySelectorAll('.movie-list .item a'));

    // 从每个 item 的 <a> 中找到对应的 <strong> 内容
    let titles = items.map(item => {
        // 在当前 item 中查找 <strong> 元素,并获取其文本内容
        let strongElement = item.querySelector('div.video-title > strong');
        return strongElement ? strongElement.textContent.trim() : null; // 如果存在则返回内容,否则返回 null
    }).filter(title => title !== null); // 过滤掉 null 值

    // 输出获取到的标题
    //console.log('获取到的标题:', titles);
    if (titles.length > 0) {
        //logToScreen(`获取到的标题:${titles.join(', ')}`, 'rgba(0, 255, 255, 0.8)', 'white');

        // 对比 titles 和 storedIds,修改样式
        titles.forEach(title => {
            // 查找对应条目
            let itemToModify = items.find(item => {
                let strongElement = item.querySelector('div.video-title > strong');
                return strongElement && strongElement.textContent.trim() === title;
            });

            if (itemToModify) {
                if (storedIds.has(title)) {
                    console.log(`匹配到已看影片: ${title}`);
                    modifyItemAtCurrentPage(itemToModify, '我看過這部影片');
                    logToScreen(`我看過這部影片: ${title}`, 'rgba(76, 175, 80, 0.8)', 'white');
                } else {
                    // console.log(`未看过: ${title}`);
                    modifyItemAtCurrentPage(itemToModify, '未看过');
                    // logToScreen(`未看过: ${title}`);
                }
            }
        });
    } else {
        logToScreen('未找到任何标题', 'rgba(255, 0, 0, 0.8)', 'white');
    }
}

// 定时检查新加载的项目
setInterval(() => {
    let items = document.querySelectorAll('.movie-list .item a');
    let currentItemCount = items.length;

    if (currentItemCount > lastItemCount) {
        console.log(`发现新增项目:${currentItemCount - lastItemCount}`);
        logToScreen(`发现新增项目:${currentItemCount - lastItemCount}`, 'rgba(0, 255, 255, 0.8)', 'white');
        processLoadedItems(); // 处理新加载的项目
        lastItemCount = currentItemCount; // 更新上一次的项目计数
    }
}, 1000);

// 初始化时获取当前项目的数量
lastItemCount = document.querySelectorAll('.movie-list .item a').length;
processLoadedItems(); // 初始化已有元素