// ==UserScript== // @name Pixiv 显示信息增强脚本 // @name:en Pixiv Info Enhancer // @namespace http://tampermonkey.net/ // @version 2.1.8 // @description 给 pixiv 图片添加收藏数、日期、分辨率,支持用户主页、推荐和排行榜,支持识别列表类名以适应网站变动 // @description:en Add bookmarks count, date, and image resolution to pixiv images; supports user pages, recommendation lists, and ranking pages; supports identifying list class names to adapt to website changes // @author InMirrors // @license GPL-3.0-or-later // @icon https://www.pixiv.net/favicon20250122.ico // @match https://www.pixiv.net/* // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-idle // @downloadURL https://update.greasyfork.icu/scripts/538404/Pixiv%20%E6%98%BE%E7%A4%BA%E4%BF%A1%E6%81%AF%E5%A2%9E%E5%BC%BA%E8%84%9A%E6%9C%AC.user.js // @updateURL https://update.greasyfork.icu/scripts/538404/Pixiv%20%E6%98%BE%E7%A4%BA%E4%BF%A1%E6%81%AF%E5%A2%9E%E5%BC%BA%E8%84%9A%E6%9C%AC.meta.js // ==/UserScript== (function () { 'use strict'; // Cache for artwork data (bookmark count and create date) const artworkDataCache = {}; // ================================================================================= // Style Customization // ================================================================================= GM_addStyle(` /* 书签数量元素本体 */ .bmcount { position: absolute !important; /* 绝对定位 */ z-index: 10; /* 确保在图片上方显示 */ bottom: 2px; left : 2px; right : auto; top : auto; background-color: rgba(220, 220, 220, 0.5); border-radius: 8px; /* 移除旧的布局样式 */ text-align: initial !important; /* 取消居中 */ padding-bottom: 0 !important; /* 移除底部填充 */ } /* 书签数量链接和文本 */ .bmcount a { display: block; /* 使 padding 生效 */ height: initial !important; width: initial !important; border-radius: inherit !important; /* 继承父元素的圆角 */ padding: 3px 6px 3px 18px; /* 文本和链接内边距变量 */ font-family: sans-serif; font-size : 12px !important; font-weight: bold !important; color : rgba(0, 105, 177, 1) !important; opacity : 1.0; text-decoration: none !important; /* 图标样式 */ background-image: url("data:image/svg+xml;charset=utf8,") !important; background-position: center left 6px !important; /* 垂直居中 水平靠左 距离左侧6px */ background-size : 10px !important; background-repeat : no-repeat !important; } /* 在图片下方插入的元素的容器 */ .script-generated-footer { font-family: sans-serif; font-size : 12px !important; font-weight: normal !important; color : #000000 !important; opacity : 0.7; line-height: 20px; text-decoration: none !important; /* 移除可能的下划线 */ display: flex; flex-wrap: wrap; /* Allow wrapping to next line */ gap: 10px; /* Minimum gap between items */ justify-content: space-between; /* Distribute space between left and right */ width: 100%; /* Make sure the container spans full width */ } /* 日期元素 */ .createdate { white-space: nowrap; /* 防止日期换行 */ } /* 分辨率元素 */ .illust-res { white-space: nowrap; } `); const url = window.location.href; if (url.includes("ranking.php")) { GM_addStyle(` .script-generated-footer { display: block; } `) } // ================================================================================= // Storage and Selector Management // ================================================================================= const STORAGE_KEY = 'pixiv_bookmark_selectors'; // Selectors that seem relatively stable or cover specific cases const ALWAYS_INCLUDED_SELECTORS = [ '.ranking-item', // 排行榜 '.gtm-illust-recommend-zone[data-gtm-recommend-zone="discovery"] li', // 插图页面下方的推荐 ".sc-8d5ac044-6.iIVQGq", ".sc-8d5ac044-6.jUPNzm", ".sc-bf8cea3f-0.dKbaFf" ]; let dynamicArtworkSelectors = loadSelectors(); let currentCombinedSelector = buildSelectorString(); // Load selectors from storage function loadSelectors() { try { const selectors = GM_getValue(STORAGE_KEY, '[]'); if (Array.isArray(selectors) && selectors.every(s => typeof s === 'string')) { return selectors.filter(s => s.trim() !== ''); } console.error("Failed to parse stored selectors, returning empty array.", stored); return []; } catch (e) { console.error("Error loading selectors from storage:", e); return []; } } // Save selectors to storage function saveSelectors(selectors) { GM_setValue(STORAGE_KEY, selectors); dynamicArtworkSelectors = selectors; // Update in-memory variable currentCombinedSelector = buildSelectorString(); // Rebuild selector string // Note: The MutationObserver will pick up the new currentCombinedSelector // on its next execution cycle after a DOM change. } // Build the full CSS selector string for querySelectorAll function buildSelectorString() { // Note: .ranking-item (appears in the ranking page) is an item selector, not a container selector const containerSelectors = dynamicArtworkSelectors.filter(s => !s.endsWith(' li') && s !== '.ranking-item'); // Exclude .ranking-item from containers const itemSelectors = dynamicArtworkSelectors.filter(s => s.endsWith(' li') || s === '.ranking-item'); // Include .ranking-item as an item const alwaysIncludedContainerSelectors = ALWAYS_INCLUDED_SELECTORS.filter(s => !s.endsWith(' li') && s !== '.ranking-item'); const alwaysIncludedItemSelectors = ALWAYS_INCLUDED_SELECTORS.filter(s => s.endsWith(' li') || s === '.ranking-item'); const finalContainerSelectors = [...new Set([...containerSelectors, ...alwaysIncludedContainerSelectors])]; const finalItemSelectors = [...new Set([...itemSelectors, ...alwaysIncludedItemSelectors])]; // Build the query: all items + li descendants of all containers const queryParts = [ ...finalItemSelectors, // Items already selected (.ranking-item is here) ...finalContainerSelectors.map(s => s + ' li') // li inside containers ]; // Add the :not([data-dummybmc]) exclusion to each part const finalQuery = queryParts.map(s => s + ':not([data-dummybmc])').join(','); console.log("Built selector string:", finalQuery); return finalQuery; } // Find potential new container selectors on the current page // 基本只有用户主页会用到这个功能,排行榜和推荐都是固定类名 function findPotentialSelectors() { const allPotentialOnPage = new Set(); // 记录在页面上找到的所有潜在选择器 const existingSelectors = new Set([...dynamicArtworkSelectors, ...ALWAYS_INCLUDED_SELECTORS]); const newFound = new Set(); // 记录在页面上找到且是新的选择器 const artworkLinks = document.querySelectorAll('a[href*="/artworks/"]'); artworkLinks.forEach(link => { const li = link.closest('li'); if (!li) return; const ul = li.parentElement; if (!ul || ul.tagName !== 'UL') return; const container = ul.parentElement; if (container && (container.tagName === 'DIV' || container.tagName === 'SECTION')) { if (container.classList.length > 0) { const selector = '.' + Array.from(container.classList).join('.'); allPotentialOnPage.add(selector); // 添加到所有找到的集合 // 如果这个选择器不在现有列表中,则添加到新找到的集合 if (!existingSelectors.has(selector)) { newFound.add(selector); } } } }); // 返回包含详细信息的对象 return { totalFound: Array.from(allPotentialOnPage), // 页面上找到的所有潜在选择器 newFound: Array.from(newFound) // 页面上找到且是新的选择器 }; } // ================================================================================= // Context Menu Commands // ================================================================================= GM_registerMenuCommand("添加 (Add) 当前页面的 Pixiv 书签选择器", async () => { const result = findPotentialSelectors(); if (result.totalFound.length === 0) { // 情况 1: 页面上没有找到任何潜在的选择器 alert("未能在当前页面找到任何潜在的书签列表容器结构。请确保当前页面显示有插图列表。"); } else { // 页面上找到了潜在的选择器 if (result.newFound.length === 0) { // 情况 2: 找到了,但都是已存在的 alert("在当前页面找到了潜在的书签列表容器结构,但所有找到的选择器都已存在于列表中,无需添加新的。"); console.log("Found existing potential selectors:", result.totalFound.join(', ')); } else { // 情况 3: 找到了新的选择器 const currentSelectors = loadSelectors(); // 再次加载最新状态 const updatedSelectors = [...new Set([...currentSelectors, ...result.newFound])]; // 合并并去重 // 理论上 newFound 不为空时,updatedSelectors 长度应该大于 currentSelectors 长度,但为了严谨还是检查一下 if (updatedSelectors.length > currentSelectors.length) { saveSelectors(updatedSelectors); const addedList = result.newFound.join('\n'); alert(`成功添加了 ${result.newFound.length} 个新的书签列表容器选择器:\n\n${addedList}\n\n脚本将尝试使用这些新的选择器,请刷新页面使变更生效。`); console.log("Added new selectors:", result.newFound.join(', ')); } else { // 这通常不应该发生,除非 findPotentialSelectors 或 saveSelectors 逻辑有误 alert("找到了新的选择器,但在保存时未能实际增加列表项。请检查控制台输出。"); console.error("Logic error: newFound is not empty, but list size did not increase."); console.log("New found:", result.newFound); console.log("Current selectors:", currentSelectors); console.log("Updated selectors (after merge):", updatedSelectors); } } } }); GM_registerMenuCommand("清除 (Clear) 已添加的 Pixiv 书签选择器", () => { if (confirm("确定要清除所有动态学习到的 Pixiv 书签列表容器选择器吗?这可能导致脚本失效,直到重新添加。")) { saveSelectors([]); alert("已清除所有动态书签选择器。"); } }); // ================================================================================= // Utils // ================================================================================= /** * 基于 ISO 字符串快速生成 "yy-MM-dd HH:mm" * 因为 pixiv 的时间精度只到分,秒部分全是 0,所以本函数去掉秒和微秒部分 * @param {Date} date - 要格式化的 Date 对象 * @returns {string} 格式化后的字符串 */ function formatFromISO(date) { const iso = date.toISOString(); // e.g. "yyyy-MM-ddTHH:mm:ss.fffZ" const [datePart, timePart] = iso.split('T'); // ["yyyy-MM-dd", "HH:mm:ss.fffZ"] const dateShort = datePart.slice(2); // "yy-MM-dd" const time = timePart.slice(0, 5); // "HH:mm" return `${dateShort} ${time}`; // "yy-MM-dd HH:mm:ss" } /** * 按目标时区偏移后再格式化 * @param {Date} date - 原始 Date 对象(本地时区或任意时区) * @param {number} timeZoneCode - 目标时区,例如 +8。默认使用当前时区 * @returns {string} 格式化后的目标时区时间字符串 */ function formatWithTimezone(date, timeZoneCode = -date.getTimezoneOffset() / 60) { // 其实 getTime() 返回的已经是当前时区的时间戳了,但之后的 toISOString() 会引入偏移 const utcTimestamp = date.getTime(); // 所以加上一个偏移以抵消 toISOString() 引入的偏移 const targetTimestamp = utcTimestamp + timeZoneCode * 60 * 60000; const targetDate = new Date(targetTimestamp); return formatFromISO(targetDate); } // ================================================================================= // Configuration for elements to be inserted // ================================================================================= const elementConfigs = [ { keys: ['bookmarkCount'], // The key for the data in the artworkData object settingName: 'showBookmarkCount', // Unique name for storing the setting with GM_setValue menuLabel: '显示收藏数 (Show Bookmark Count)', // Label for the Tampermonkey menu isEnabledByDefault: true, // Default state if no setting is saved // Function to find the parent element for insertion getTarget: (listItem) => listItem.querySelector('a[href*="/artworks/"]'), // Position for insertAdjacentHTML (e.g., 'beforeend', 'afterend') position: 'beforeend', getHTMLValueNum: 1, // Function to generate the HTML string for the element getHTML: (bmCount, id) => `
` }, { keys: ['createDate'], settingName: 'showCreateDate', menuLabel: '显示创建日期 (Show Creation Date)', isEnabledByDefault: true, group: 'footer', // Each group corresponds to a CSS class, set the value to apply specific styles. getHTMLValueNum: 1, // The generated HTML does NOT include its own wrapper, as it will be placed inside the group container. getHTML: (createDate, id) => { try { // Format date to yyyy-MM-dd HH:mm const date = new Date(createDate); const formattedDate = formatWithTimezone(date); // Return just the content's HTML. The container is handled by the group. return `