`
},
{
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 `
${formattedDate}
`;
} catch (e) {
console.error("Error formatting or inserting create date for ID", id, ":", e);
return null; // Return null on error to prevent insertion
}
},
// Whether to exclude this element from the normal list items (normal pages)
excludeNormal: false,
// Whether to exclude this element from the ranking pages
excludeRanking: true
// Note: No 'getTarget' or 'position' is needed here, as the group config handles placement.
},
{
keys: ['width', 'height'],
settingName: 'showResolution',
menuLabel: 'Show Image Resolution (显示图像分辨率)',
isEnabledByDefault: true,
group: 'footer',
getHTMLValueNum: 2,
getHTML: (width, height, id) => {
const widthInt = parseInt(width);
const heightInt = parseInt(height);
let orientationMark = '';
if (Math.abs(widthInt - heightInt) <= Math.max(widthInt, heightInt) * 0.20) {
orientationMark = '='; // Approximately square
} else if (widthInt > heightInt) {
orientationMark = '–'; // Landscape
} else {
orientationMark = '|'; // Portrait
}
return `
${orientationMark}${width}x${height}
`;
}
},
];
// Object to hold the current state of all settings
const SCRIPT_SETTINGS = {};
/**
* Initializes settings from GM_getValue and registers menu commands.
* Call this function once when the script starts.
*/
function initializeSettings() {
console.log("Initializing script settings and menu...");
elementConfigs.forEach(config => {
// Load the saved setting, or use the default value
SCRIPT_SETTINGS[config.settingName] = GM_getValue(config.settingName, config.isEnabledByDefault);
// Register a command in the Tampermonkey menu for each element
GM_registerMenuCommand(
`${SCRIPT_SETTINGS[config.settingName] ? '✅' : '❌'} ${config.menuLabel}`,
() => {
// Toggle the setting
const newValue = !SCRIPT_SETTINGS[config.settingName];
GM_setValue(config.settingName, newValue);
alert(`'${config.menuLabel}' 已${newValue ? '开启' : '关闭'}。\n请刷新页面以应用更改。`);
}
);
});
}
initializeSettings();
// Configuration for element groups/containers
// Defines shared containers for multiple elements.
const groupConfigs = {
// A group for elements to be placed at the end of the list item
footer: {
selector: `.${CLASSES.footer}`, // The CSS selector for the container div
// Function to find the anchor element to insert the container relative to
getTarget: (listItem) => listItem.querySelector(':scope > div'),
// Where to insert the container relative to the target ('afterend', 'beforebegin', etc.)
position: 'afterend',
// The HTML for the container itself. Elements will be inserted inside this.
containerHTML: ``
}
};
// =================================================================================
// Core
// =================================================================================
async function fetchArtworkData(id) {
//console.log(`Attempting to fetch data for ID: ${id}`);
// Check cache first
if (artworkDataCache[id]) {
//console.log(`Cache hit for ID: ${id}`);
return artworkDataCache[id];
}
try {
const response = await fetch("https://www.pixiv.net/ajax/illust/" + id, { credentials: "omit" });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const artworkData = data?.body;
// Store in cache
artworkDataCache[id] = artworkData;
//console.log(`Fetched and cached data for ID ${id}:`, artworkData);
return artworkData;
} catch (error) {
console.error("Error fetching artwork data for ID", id, ":", error);
// Store error state in cache to avoid repeated failed requests
artworkDataCache[id] = { error: true };
throw error; // Re-throw to be caught by the caller
}
} // fetchArtworkData
/**
* Inserts various elements into an artwork list item based on configuration.
* This function is data-driven by the 'elementConfigs' array.
* @param {HTMLElement} listItem - The list item element (e.g.,
or
).
* @param {string} id - The artwork ID.
* @param {object} artworkData - The object containing artwork details.
*/
function insertArtworkElements(listItem, id, artworkData) {
// Check if elements already exist within this item.
// Use a generic data attribute to mark as processed by this script.
if (listItem.hasAttribute(ATTR_PROCESSED)) {
return;
}
// Mark the listItem as processed to prevent re-insertion
listItem.dataset.insertionProcessed = 'true';
// Iterate over all configured elements
elementConfigs.forEach(config => {
// 1. Check if this element is enabled in the settings
if (!SCRIPT_SETTINGS[config.settingName]) return;
// 2. Check for exclusion conditions
if (config.excludeRanking && listItem.tagName === 'SECTION') return;
if (config.excludeNormal && listItem.tagName === 'LI') return;
// 3. Check if the required data is available in artworkData
// Get all missing keys (i.e., keys not present in artworkData)
const missingKeys = config.keys.filter(key => !(key in artworkData));
if (missingKeys.length > 0) {
console.warn("Missing keys:", missingKeys);
return;
}
const dataValues = config.keys.map(key => artworkData[key]);
// 4. Generate the element's inner HTML
function getHTML(config, id) {
if (config.getHTMLValueNum === 2) {
return config.getHTML(dataValues[0], dataValues[1], id);
} else { // Defaults to one value
return config.getHTML(dataValues[0], id);
}
}
const elementHTML = getHTML(config, id);
if (!elementHTML) return; // Skip if HTML generation failed
// 5. Determine insertion logic: Grouped or Standalone
if (config.group) {
// --- Logic for Grouped Elements ---
// Elements in the same group share the same CSS
const groupConfig = groupConfigs[config.group];
if (!groupConfig) {
console.warn(`Group '${config.group}' is not defined in groupConfigs.`);
return;
}
// Find or create the container for this group within the listItem
let container = listItem.querySelector(groupConfig.selector);
if (!container) {
// Container does not exist, so create it.
const parentForContainer = groupConfig.getTarget(listItem);
if (parentForContainer) {
parentForContainer.insertAdjacentHTML(groupConfig.position, groupConfig.containerHTML);
// Now that it's created, find it to get the element reference.
container = listItem.querySelector(groupConfig.selector);
}
}
// If container exists (or was just created), insert the element's HTML into it.
if (container) {
container.insertAdjacentHTML('beforeend', elementHTML);
} else {
console.warn(`Could not find or create container for group '${config.group}' in item ID ${id}.`);
}
}
// --- Logic for Standalone Elements ---
else {
const insertionParent = config.getTarget(listItem);
if (insertionParent) {
insertionParent.insertAdjacentHTML(config.position, elementHTML);
} else {
console.warn(`Insertion parent for '${config.key}' not found for item ID ${id}.`);
}
}
});
} // insertArtworkElements
// 处理元素,添加书签数和创建日期
async function processSingleArtworkElement(Each) {
// Each could be an LI or a SECTION.ranking-item
const listItem = (Each.tagName === 'LI' || Each.tagName === 'SECTION') ? Each : Each.closest('li, section');
if (!listItem) {
return;
}
// Not a valid li
if (!listItem.querySelector('img')) {
return;
}
// Check if it's a valid item and hasn't been fully processed
if (listItem.hasAttribute(ATTR_PROCESSED)) {
return;
}
const artworkLinkElem = listItem.querySelector('a[href*="/artworks/"]');
if (!artworkLinkElem) {
return;
}
let id = null;
// Extract ID from the href attribute
// Format: "/artworks/ID" or "/lang/artworks/ID"
id = /\d+$/.exec(artworkLinkElem.href)?.[0];
if (!id) {
return;
}
debugLog("Processing valid item:", listItem);
// Check cache first
let artworkData = artworkDataCache[id];
// If data is in cache (even error state), attempt insertion
if (artworkData) {
if (artworkData.error) {
// If cached data indicates error, mark item as processed to avoid retries
listItem.dataset.insertionProcessed = 'error';
} else {
insertArtworkElements(listItem, id, artworkData);
}
} else {
// Data not in cache, fetch it
// Use a temporary marker to prevent duplicate fetches for the same element
if (listItem.hasAttribute("data-fetching-bmc-cd")) {
return;
}
listItem.dataset.fetchingBmcCd = 'true';
try {
artworkData = await fetchArtworkData(id);
insertArtworkElements(listItem, id, artworkData);
} catch (error) {
// Error already logged in fetchArtworkData
listItem.dataset.insertionProcessed = 'error'; // Mark item as processed with error
} finally {
// Remove temporary fetching marker
delete listItem.dataset.fetchingBmcCd;
}
}
} // processSingleArtworkElement()
// =========================================================================
// Bootstrap
// =========================================================================
// The core function that finds and processes all relevant artwork elements on the page.
function processAllArtworks() {
debugLog("Scanning for new artwork elements...");
document.querySelectorAll(BASE_SELECTOR).forEach(processSingleArtworkElement);
}
// Create a debounced version of the processing function to avoid excessive runs
// during rapid DOM changes (e.g., fast scrolling).
const debouncedProcessAllArtworks = debounce(processAllArtworks, 50);
const observer = new MutationObserver((mutations) => {
debouncedProcessAllArtworks();
});
// Initial run: Process any elements that are already on the page when the script loads.
processAllArtworks();
observer.observe(document.body, { childList: true, subtree: true });
console.log("[Pixiv Info Enhancer] script started.");
})();