您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Count illustrations per tag in bookmarks
// ==UserScript== // @name Pixiv Bookmark Tag Summary // @namespace http://tampermonkey.net/ // @version 0.6.3 // @description Count illustrations per tag in bookmarks // @match https://www.pixiv.net/*/bookmarks* // @grant unsafeWindow // @license MIT // ==/UserScript== (function() { 'use strict'; const turboMode = false; const bookmarkBatchSize = 100; const BANNER = ".sc-x1dm5r-0"; let uid, lang, token; let pageInfo = {}; let userTags = []; // Function to count bookmarks by tag let tags = {}; let sortedTags = []; let illustrations = {}; let bookmarks = {}; let translations = {}; let debounceTimer = null; let fetchedAll = false; let tagTiles = {}; let globalObjects = { userTags: userTags, tags: tags, sortedTags: sortedTags, illustrations: illustrations, bookmarks: bookmarks, translations: translations, uid: uid, lang: lang, token: token, bookmarkBatchSize: bookmarkBatchSize, tagTiles: tagTiles } let unsafeWindow_ = unsafeWindow; const delay = (ms) => { return new Promise((res) => setTimeout(res, ms)); } const splitIntoBatches = (array, batchSize) => { const batches = []; for (let i = 0; i < array.length; i += batchSize) { batches.push(array.slice(i, i + batchSize)); } return batches; } const reverseObject = (obj) => { const reversed = {}; for (let [key, value] of Object.entries(obj)) { if (reversed[value]) { reversed[value].push(key); } else { reversed[value] = [key]; } } return reversed; } const downloadObject = (obj, name="object.json") => { // Convert the transformed dictionary to a JSON string let jsonStr = JSON.stringify(obj, null, 4); // Create a Blob from the JSON string let blob = new Blob([jsonStr], { type: 'application/json' }); let url = URL.createObjectURL(blob); let a = document.createElement('a'); a.href = url; a.download = name; document.body.appendChild(a); // Append the link to the body a.click(); // Trigger the download document.body.removeChild(a); // Remove the link after the download URL.revokeObjectURL(url); } const fixTagName = (str) => { return str.replace(/ /g, "_").slice(0, 20).trim(); } const getTag = (tag) => { if (typeof tag == "string"){ return tags[tag]; } return tag; } const getTagName = (tag) => { if (typeof tag !== "string"){ return tag.name; } return tag; } const remove = async (tagNames, bookmarkIds, isBatch=false) => { if (!tagNames || !bookmarkIds || tagNames.length == 0 || bookmarkIds.length == 0){ console.log(`Skip removing tag ${tagNames} from ${bookmarkIds.length} illustrations`); return; } tagNames = tagNames.map(getTagName); if (tagNames.length > 1){ for (const tagName of tagNames) { await remove([tagName], bookmarkIds); } return; } bookmarkIds = filterBookmarks(tagNames[0], bookmarkIds, false); bookmarkIds = filterBookmarks2(tagNames[0], bookmarkIds, false); if (!bookmarkIds || bookmarkIds.length == 0){ console.log(`Skip removing tag ${tagNames} because it has no illustrations`); return; } tagNames = tagNames.map(fixTagName); if (bookmarkIds.length > bookmarkBatchSize){ const batches = splitIntoBatches(bookmarkIds, bookmarkBatchSize); for (const batch of batches) { await remove(tagNames, batch, true); } return; } const payload = { removeTags: tagNames, bookmarkIds: bookmarkIds, }; const response = await fetch("https://www.pixiv.net/ajax/illusts/bookmarks/remove_tags", { headers: { accept: "application/json", "content-type": "application/json; charset=utf-8", //"content-type": "application/x-www-form-urlencoded; charset=utf-8", "x-csrf-token": token, }, body: JSON.stringify(payload), //body: new URLSearchParams(payload).toString(), method: "POST", }); if (response.ok && !response.error) { tagNames.forEach((tagName) => { const bookmarks = bookmarkIds.map(id => globalObjects.bookmarks[id]); bookmarks.forEach(b => { removeIllust(tagName, b); }); }) console.log(`Removed ${tagNames} from ${bookmarkIds}`); }else{ throw Error(`Remove of ${tagNames} failed for ${bookmarkIds} with status ${response.status} and error ${response.error}: ${response.message}`); } await delay(500); } const includes = (source, negate=false) => { let f = x => true; if (typeof source === 'object' && !Array.isArray(source) && !(source instanceof Set)) { f = key => (key in source); } if (Array.isArray(source)) { f = key => source.includes(key); } if (source instanceof Set) { f = key => source.has(key); } let f2 = f; if (negate){ f2 = key => !f(key); } return f2; } const filterBookmarks = (source, keys, exclusion) => { if(typeof source == "string"){ source = globalObjects.tags[source]; if (!source) return keys; source = source.bookmarks; if (!source) return keys; } let f2 = includes(source, exclusion); return keys.filter(f2); } const filterBookmarks2 = (tagName, bookmarkIds, exclusion) => { let bookmarks = bookmarkIds.map(id => globalObjects.bookmarks[id]); bookmarks = bookmarks.filter(b => includes(b.associatedTags, exclusion)(tagName)); return bookmarks.map(b => b.bookmarkId); } const add = async (tagNames, bookmarkIds, isBatch=false) => { if (!tagNames || !bookmarkIds || tagNames.length == 0 || bookmarkIds.length == 0){ console.log(`Skip adding tag ${tagNames} to ${bookmarkIds.length} illustrations`); return tagNames; } tagNames = tagNames.map(getTagName); if (tagNames.length > 1){ for (const tagName of tagNames) { await add([tagName], bookmarkIds); } return tagNames; } bookmarkIds = filterBookmarks(tagNames[0], bookmarkIds, true); bookmarkIds = filterBookmarks2(tagNames[0], bookmarkIds, true); if (!bookmarkIds || bookmarkIds.length == 0){ console.log(`Skip adding tag ${tagNames} because the illustrations already have it`); return tagNames; } tagNames = tagNames.map(fixTagName); if (bookmarkIds.length > bookmarkBatchSize){ const batches = splitIntoBatches(bookmarkIds, bookmarkBatchSize); for (const batch of batches) { await add(tagNames, batch, true); } return tagNames; } const restrict = window.location.href.includes("rest=hide") ? 1 : 0; const payload = { tags: tagNames, bookmarkIds: bookmarkIds, }; const response = await fetch("https://www.pixiv.net/ajax/illusts/bookmarks/add_tags", { headers: { accept: "application/json", "content-type": "application/json; charset=utf-8", //"content-type": "application/x-www-form-urlencoded; charset=utf-8", "x-csrf-token": token, }, body: JSON.stringify(payload), //body: new URLSearchParams(payload).toString(), method: "POST", }); if (response.ok && !response.error) { tagNames.forEach((tagName) => { const tag = saveTag(tagName); const bookmarks = bookmarkIds.map(id => globalObjects.bookmarks[id]); bookmarks.forEach(b => { saveIllust(tag, b); }); }) console.log(`Added ${tagNames} to ${bookmarkIds}`); }else{ throw Error(`Add of ${tagNames} failed for ${bookmarkIds} with status ${response.status} and error ${response.error}: ${response.message}`); } await delay(500); return tagNames; } const getTagTranslation = async (tag) => { const apiBaseUrl = "https://www.pixiv.net/ajax/search/tags/"; const apiUrl = `${apiBaseUrl}${encodeURIComponent(tag)}?lang=${lang}`; let translation = null; if (includes(globalObjects.translations)(tag)){ translation = globalObjects.translations[tag]; if (translation){ console.log(`Cached translation: ${tag} -> ${translation}`); return translation; } } try { // Fetch the tag translation from the API if (!translation){ const response = await fetch(apiUrl, { credentials: 'same-origin' }); if (!response.ok) { console.error(`Failed to fetch translation for tag: ${tag}`); return null; } // Parse the JSON response const json = await response.json(); if (json.error) { console.error(`API error for tag: ${tag}`, json.message || "Unknown error"); return null; } // Extract translation based on the order of preference translation = json.body?.tagTranslation?.[tag]?.[lang] || json.body?.breadcrumbs?.successor?.pop()?.translation?.[lang] || json.body?.tagTranslation?.[tag]?.en || json.body?.breadcrumbs?.successor?.pop()?.translation?.en || json.body?.tagTranslation?.[tag]?.romaji || json.body?.pixpedia?.tag || null; // No translation available } if (translation) { translation = fixTagName(translation); console.log(`Translated: ${tag} -> ${translation}`); translations[tag] = translation; return translation; } else { console.warn(`No translation found for tag: ${tag}`); return null; } // Delay to avoid overloading the server } catch (err) { console.error(`Error fetching translation for tag: ${tag}`, err); return null; }finally { await delay(500); } return translation; } const renameTag = async (tag, newTagName, publicationType=null) => { if (!publicationType){ publicationType = window.location.href.includes("rest=hide") ? "hide" : "show"; } tag = getTag(tag); const tagTile = globalObjects.tagTiles[tag.name]; let bookmarkIds = Object.values(tag.bookmarks).map(illust => illust.bookmarkId); if (!bookmarkIds || bookmarkIds.length == 0){ console.log(`Skip renaming tag ${tag.name} -> ${newTagName} because it has no illustrations`); return; } newTagName = fixTagName(newTagName); if (includes(tagTiles)(newTagName)){ if (confirm(`Tag ${newTagName} already exists. Do you want to move ${tag.name} into ${newTagName}?`)){ moveTag(tag.name, newTagName, true); } return; } // Update server-side: Add the new tag and remove the old tag newTagName = (await add([newTagName], bookmarkIds))[0]; await remove([tag.name], bookmarkIds); // Update the `tags` object const oldTagName = tag.name; tag.name = newTagName; // Update the DOM: Modify the tag tile if (tagTile){ const tagLink = tagTile.querySelector('a'); // Tag link element const tagCount = tagTile.querySelector('div:last-child'); // Tag count element if (tagLink) tagLink.innerText = newTagName; // Update tag name if (tagLink) tagLink.href = `https://www.pixiv.net/en/users/${uid}/bookmarks/artworks/${newTagName}?rest=${publicationType}`; // Update URL if (tagCount) tagCount.innerText = `(${Object.keys(tag.illustrations).length})`; // Count remains unchanged tagTiles[newTagName] = tagTile; } removeTagLocal(oldTagName, true); console.log(`Renamed tag "${oldTagName}" to "${newTagName}".`); } const deleteArrayElement = (arr, el) => { let index = arr.indexOf(el); if (index !== -1) { arr.splice(index, 1); } } const removeTagLocal = (tagName, isRename=false) => { deleteArrayElement(userTags, tagName); if (!isRename){ deleteArrayElement(sortedTags, tags[tagName]); } delete tags[tagName]; if (tagTiles[tagName]){ if (!isRename){ tagTiles[tagName].remove(); } delete tagTiles[tagName]; } } const removeTags = async (tags) => { for (let tag of tags) { tag = getTag(tag); const illusts = Object.values(tag.illustrations).map((illust) => illust.bookmarkId); await remove([tag.name], illusts); removeTagLocal(tag.name); } } const sortByParody = (array) => { const sortFunc = (a, b) => { let reg = /^[a-zA-Z0-9]/; if (reg.test(a) && !reg.test(b)) return -1; else if (!reg.test(a) && reg.test(b)) return 1; else return a.localeCompare(b, "zh"); }; const withParody = array.filter((key) => key.includes("(")); const withoutParody = array.filter((key) => !key.includes("(")); withoutParody.sort(sortFunc); withParody.sort(sortFunc); withParody.sort((a, b) => sortFunc(a.split("(")[1], b.split("(")[1])); return withoutParody.concat(withParody); } const fetchUserTags = async () => { const tagsRaw = await fetch( `/ajax/user/${uid}/illusts/bookmark/tags?lang=${lang}` ); const tagsObj = await tagsRaw.json(); if (tagsObj.error === true) return alert( `获取tags失败 Fail to fetch user tags` + "\n" + decodeURI(tagsObj.message) ); let userTagDict = tagsObj.body; const userTagsSet = new Set(); const addTag2Set = (tag) => { try { userTagsSet.add(decodeURI(tag)); } catch (err) { userTagsSet.add(tag); if (err.message !== "URI malformed") { console.log("[Label Pixiv] Error!"); console.log(err.name, err.message); console.log(err.stack); } } }; for (let obj of userTagDict.public) { addTag2Set(obj.tag); } for (let obj of userTagDict["private"]) { addTag2Set(obj.tag); } userTagsSet.delete("未分類"); return sortByParody(Array.from(userTagsSet)); } // Function to bulk remove or remove tags based on minimum bookmark count const bulkRemove = async (minBookmarks) => { // Filter tags based on the bookmark count const selectedTags = sortedTags.filter((tag) => { const bookmarkCount = countIllusts(tag); return bookmarkCount < minBookmarks; }); if (selectedTags.length === 0) { alert('No tag meet the criteria.'); return; } // Show confirmation dialog const confirmation = confirm(`Are you sure you want to remove ${selectedTags.length} tags?`); if (!confirmation) return; await removeTags(selectedTags); alert(`Removed ${selectedTags.length} tags`); } const fetchTokenPolyfill = async () => { // get token const userRaw = await fetch( "/bookmark_add.php?type=illust&illust_id=83540927" ); if (!userRaw.ok) { console.log(`获取身份信息失败 Fail to fetch user information`); throw new Error(); } const userRes = await userRaw.text(); const tokenPos = userRes.indexOf("pixiv.context.token"); const tokenEnd = userRes.indexOf(";", tokenPos); return userRes.slice(tokenPos, tokenEnd).split('"')[1]; } const initializeVariables = async () => { const polyfill = async () => { try { const dataLayer = unsafeWindow_["dataLayer"][0]; uid = dataLayer["user_id"]; lang = dataLayer["lang"]; token = await fetchTokenPolyfill(); pageInfo.userId = window.location.href.match(/users\/(\d+)/)?.[1]; pageInfo.client = { userId: uid, lang, token }; } catch (err) { console.log(err); console.log("[Label Bookmarks] Initializing Failed"); } } try { pageInfo = Object.values(document.querySelector(BANNER))[0]["return"][ "return" ]["memoizedProps"]; uid = pageInfo["client"]["userId"]; token = pageInfo["client"]["token"]; lang = pageInfo["client"]["lang"]; if (!uid || !token || !lang) await polyfill(); } catch (err) { console.log(err); await polyfill(); } } const fetchBookmarks = async (uid, tagToQuery='', offset=0, publicationType=null) => { if (!publicationType){ publicationType = window.location.href.includes("rest=hide") ? "hide" : "show"; } const bookmarksRaw = await fetch( `/ajax/user/${uid}` + `/illusts/bookmarks?tag=${tagToQuery}` + `&offset=${offset}&limit=${bookmarkBatchSize}&rest=${publicationType}` ); if (!turboMode) await delay(500); const bookmarksRes = await bookmarksRaw.json(); if (!bookmarksRaw.ok || bookmarksRes.error === true) { return alert( `获取用户收藏夹列表失败\nFail to fetch user bookmarks\n` + decodeURI(bookmarksRes.message) ); } const bookmarks = bookmarksRes.body; bookmarks.count = bookmarks["works"].length; const works = bookmarks["works"] .map((work) => { if (work.title === "-----") return null; work.bookmarkId = work["bookmarkData"]["id"]; work.associatedTags = bookmarks["bookmarkTags"][work.bookmarkId] || []; work.associatedTags = work.associatedTags.filter( (tag) => tag != "未分類" ); return work; }) .filter((work) => work && work.associatedTags.length); bookmarks["works"] = works; return bookmarks; } const fetchAllBookmarks = async (uid, tagToQuery='', publicationType=null) => { let total, // total bookmarks of specific tag index = 0; // counter of do-while loop let finalBookmarks = null; let allWorks = []; let allTags = {} do { const bookmarks = await fetchBookmarks( uid, tagToQuery, index, publicationType ); if (!total) total = bookmarks.total; const works = bookmarks["works"]; allWorks = allWorks.concat(works); allTags = updateObject(allTags, bookmarks["bookmarkTags"]); index += bookmarks.count || bookmarks["works"].length; finalBookmarks = updateObject(finalBookmarks, bookmarks); console.log(`Fetching bookmarks... ${index}/${total}`) } while (index < total); finalBookmarks["works"] = allWorks; finalBookmarks["bookmarkTags"] = allTags; return finalBookmarks; } // Function to check if the bookmarks list has changed const countIllusts = (tag) => Object.keys(tag.illustrations).length; const countBookmarks = (tag) => Object.keys(tag.bookmarks).length; const illustComparator = (a, b) => countIllusts(b) - countIllusts(a); const updateObject = (target, source) => { if (!target) return source; //target = {...target, ...source}; Object.assign(target, source); return target; } const saveTag = (tag) => { tag = getTagName(tag); if (!tags[tag]){ userTags.push(tag); tags[tag] = { name: tag, illustrations: {}, bookmarks: {}, }; } return tags[tag]; } const saveIllust = (tag, illust) => { tag = saveTag(tag); let illustId = illust.id; if (tag.illustrations[illustId]) { tag.illustrations[illustId] = updateObject(tag.illustrations[illustId], illust); illust = tag.illustrations[illustId]; }else{ tag.illustrations[illustId] = illust; } illustrations[illustId] = illust; let bookmarkId = illust.bookmarkId; if (tag.bookmarks[bookmarkId]) { tag.bookmarks[bookmarkId] = updateObject(tag.bookmarks[bookmarkId], illust); illust = tag.bookmarks[bookmarkId]; }else{ tag.bookmarks[bookmarkId] = illust; } bookmarks[bookmarkId] = illust; return illust; } const removeIllust = (tag, illust) => { tag = getTag(tag); if (!tag) return; delete tag.illustrations[illust.id]; delete tag.bookmarks[illust.bookmarkId]; if (countIllusts(tag) == 0){ removeTags([tag]); } } const summarizeAllBookmarks = async () => { userTags = await fetchUserTags(); userTags.forEach((tag) => { saveTag(tag); }); const bookmarks = await fetchAllBookmarks(uid); console.log(`Fetched ${bookmarks.works.length} bookmarks`); let total = 0; bookmarks["works"].forEach((work) => { let illust = { id: work.id, title: work.title, alt: work.alt, img: work.url, }; illust = updateObject(illust, work); illust["url"] = `https://www.pixiv.net/${lang}/artworks/${work.id}`; work.associatedTags.forEach((tag) => { saveIllust(tag, illust); }); total += 1; }); console.log(`Processed ${total} illusts with ${Object.keys(tags).length} tags`); sortTags(); fetchedAll = true; requestAnimationFrame(renderSummary); //renderSummary(); } const sortTags = () =>{ sortedTags = Object.values(tags).sort(illustComparator); globalObjects.sortedTags = sortedTags; } const moveTag = async (tag, targetTagName, forceRemove=false) => { // Add the dragged tag (A) to all illustrations in the target tag (B) tag = getTag(tag); const targetTag = tags[targetTagName]; const bookmarkIds = Object.values(tag.illustrations).map(illust => illust.bookmarkId); await add([targetTagName], bookmarkIds); populateTagTile(targetTag); console.log(`Tag "${targetTagName}" added to all illustrations in "${tag.name}".`); if (forceRemove || confirm(`Do you want to remove the "${tag.name}" tag?`)) { await removeTags([tag.name]); if (!forceRemove) alert(`Tag "${tag.name}" removed.`); } sortTags(); } //populate tag tile const populateTagTile = (tag) => { tag = getTag(tag); const tagTile = tagTiles[tag.name]; if (!tagTile) return; const count = countIllusts(tag); const tagCount = tagTile.querySelector(".tag-count"); tagCount.innerText = `(${count})`; const illustContainer = tagTile.querySelector(".illust-container") illustContainer.innerHTML = ""; // Populate illustrations Object.values(tag.illustrations).forEach((illust) => { const illustItem = document.createElement('li'); illustItem.style.marginBottom = '5px'; illustItem.innerHTML = `<a href="${illust.url}" target="_blank">${illust.alt || illust.title}</a>`; illustContainer.appendChild(illustItem); }); } // Function to render the summary UI const renderSummary = () => { let publicationType = window.location.href.includes("rest=hide") ? "hide" : "show"; // Clear previous summary if exists const existingSummary = document.getElementById('tag-summary'); if (existingSummary) { existingSummary.remove(); } // Create a summary element const summaryDiv = document.createElement('div'); summaryDiv.id = 'tag-summary'; // Set an ID for easy removal summaryDiv.style.position = 'fixed'; summaryDiv.style.bottom = '10px'; summaryDiv.style.right = '10px'; summaryDiv.style.backgroundColor = '#fff'; summaryDiv.style.padding = '10px'; summaryDiv.style.border = '1px solid #ccc'; summaryDiv.style.zIndex = '9999'; summaryDiv.style.color = 'black'; const title = document.createElement('h3'); title.innerText = 'Tags'; title.style.cursor = 'pointer'; // Change cursor to pointer title.style.margin = '0'; // Remove default margin // Create a container for tag data const summaryContent = document.createElement('div'); summaryContent.style.display = 'none'; // Initially hidden summaryContent.style.padding = '10px'; summaryContent.style.maxHeight = '400px'; // Set a maximum height summaryContent.style.minWidth = '400px'; summaryContent.style.overflowY = 'auto'; // Enable vertical scrolling // Toggle visibility of the tag container when the title is clicked title.addEventListener('click', () => { if (summaryContent.style.display === 'none') { summaryContent.style.display = 'block'; title.innerText = 'Tags'; // Change title when expanded } else { summaryContent.style.display = 'none'; title.innerText = 'Tags'; // Reset title when collapsed } }); let totalCount = 0; const tagContainer = document.createElement('div'); tagContainer.style.display = 'grid'; tagContainer.style.gridTemplateColumns = 'repeat(auto-fill, minmax(100px, 1fr))'; tagContainer.style.gap = '10px'; // Keep track of the currently expanded tile let expandedTile = null; // Generate each tag as a tile with clickable illustration list Object.keys(tagTiles).forEach(key => delete tagTiles[key]); Object.values(sortedTags).forEach((tag) => { const count = countIllusts(tag); if (!count) return; const tagTile = document.createElement('div'); tagTile.style.backgroundColor = '#f0f0f0'; tagTile.style.padding = '5px'; tagTile.style.borderRadius = '5px'; tagTile.style.textAlign = 'center'; tagTile.style.cursor = 'pointer'; tagTile.style.transition = 'all 0.3s ease'; tagTile.style.position = "relative"; /*add this*/ tagTile.style.color = 'black'; tagTile.draggable = true; // Enable dragging // Store the tag name in the element's dataset for easy reference during drag and drop tagTile.dataset.tagName = tag.name; tagTiles[tag.name] = tagTile; tagTile.tag = tag; // Dragstart is triggered on the dragged element tagTile.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', tag.name); }); tagTile.addEventListener('dragover', (e) => { e.preventDefault(); }); // drop and dragover are triggered on the below element tagTile.addEventListener('drop', async (e) => { e.preventDefault(); const targetTagName = tag.name; const draggedTagName = e.dataTransfer.getData('text/plain'); console.log(draggedTagName, targetTagName); if (draggedTagName === targetTagName) return; // Confirm addition of the dragged tag to target tag's illustrations if (!confirm(`Do you want to move "${draggedTagName}" tag to "${targetTagName}" tag?\nThis will add "${targetTagName}" tag to all illustrations in "${draggedTagName}"`)) { return; } moveTag(draggedTagName, targetTagName); }); const buttonContainer = document.createElement('div'); buttonContainer.classList.add("button-container"); buttonContainer.style.position = 'absolute'; buttonContainer.style.top = '5px'; buttonContainer.style.right = '5px'; buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '5px'; // Space between buttons buttonContainer.style.display = 'none'; // Initially hidden tagTile.appendChild(buttonContainer); // Create a delete button for each tile const deleteButton = document.createElement('button'); deleteButton.innerText = '🗑'; deleteButton.style.backgroundColor = 'red'; deleteButton.style.color = 'white'; deleteButton.style.border = 'none'; deleteButton.style.cursor = 'pointer'; // Add click event for delete button deleteButton.addEventListener('click', async (e) => { e.stopPropagation(); // Prevent triggering tile click events const confirmDelete = confirm(`Are you sure you want to delete the "${tag.name}" tag?`); if (confirmDelete) { await removeTags([tag.name]); alert(`Tag "${tag.name}" has been deleted.`); } }); const copyButton = document.createElement('button'); copyButton.innerText = '🗎'; copyButton.style.backgroundColor = 'gray'; copyButton.style.color = 'white'; copyButton.style.border = 'none'; copyButton.style.cursor = 'pointer'; copyButton.addEventListener('click', (e) => { e.stopPropagation(); navigator.clipboard.writeText(tag.name).then(() => { console.log(`Copied "${tag.name}" to clipboard.`); }).catch((err) => { console.error(`Failed to copy tag: ${err}`); }); }); // Edit button const editButton = document.createElement('button'); editButton.innerText = '✏️'; // Unicode for edit icon editButton.style.border = 'none'; editButton.style.background = 'yellow'; editButton.style.color = 'white'; editButton.style.cursor = 'pointer'; editButton.addEventListener('click', async (e) => { e.stopPropagation(); let newTagName = prompt(`Edit tag "${tag.name}". Enter a new name:`, tag.name); if (!newTagName || newTagName.trim() === tag.name) return; const oldTagName = tag.name; newTagName = newTagName.trim(); await renameTag(tag, newTagName); alert(`Tag "${oldTagName}" has been renamed to "${newTagName}".`); }); const translateButton = document.createElement('button'); translateButton.innerText = '🌐'; // Unicode globe icon for translation translateButton.style.border = 'none'; translateButton.style.background = 'blue'; translateButton.style.color = 'white'; translateButton.style.cursor = 'pointer'; translateButton.addEventListener('click', async (e) => { e.stopPropagation(); // Fetch translation for the current tag const translatedTag = await getTagTranslation(tag.name); if (!translatedTag || translatedTag == tag.name) { alert(`No translation found for tag: ${tag.name}`); return; } // Confirm translation if (confirm(`Translate "${tag.name}" to "${translatedTag}"?`)) { const oldTagName = tag.name; await renameTag(tag, translatedTag); alert(`Tag "${oldTagName}" has been renamed to "${translatedTag}".`); } }); // Add buttons to the container buttonContainer.appendChild(translateButton); buttonContainer.appendChild(editButton); buttonContainer.appendChild(copyButton); buttonContainer.appendChild(deleteButton); const tagText = document.createElement('div'); tagTile.appendChild(tagText); const tagLink = document.createElement('a'); tagLink.href = `https://www.pixiv.net/en/users/${uid}/bookmarks/artworks/${tag.name}?rest=${publicationType}`; tagLink.innerText = `${tag.name}`; tagLink.classList.add("tag-link"); tagLink.style.textDecoration = 'none'; tagLink.style.display = 'block'; tagText.appendChild(tagLink); tagTile.tagLink = tagLink; const tagCount = document.createElement('div'); tagCount.classList.add("tag-count"); tagText.appendChild(tagCount); tagTile.tagCount = tagCount; // Create a container for illustrations (initially hidden) let illustContainer = document.createElement('ol'); illustContainer.classList.add("illust-container"); illustContainer.style.display = 'none'; illustContainer.style.paddingTop = '5px'; illustContainer.style.textAlign = 'left'; tagTile.appendChild(illustContainer); tagTile.illustContainer = illustContainer; populateTagTile(tag); // Toggle illustration visibility when tile is clicked tagTile.addEventListener('click', () => { if (expandedTile && expandedTile !== tagTile) { // Collapse the previously expanded tile expandedTile.style.gridColumn = ''; expandedTile.querySelector('.illust-container').style.display = 'none'; expandedTile.querySelector('.button-container').style.display = 'none'; } if (illustContainer.style.display === 'none') { // Expand this tile tagTile.style.gridColumn = '1 / -1'; // Full width in grid illustContainer.style.display = 'block'; buttonContainer.style.display = 'block'; // Show button expandedTile = tagTile; } else { // Collapse this tile if already expanded tagTile.style.gridColumn = ''; illustContainer.style.display = 'none'; buttonContainer.style.display = 'none'; // Hide button expandedTile = null; } }); tagContainer.appendChild(tagTile); totalCount += count; }); const totalContainer = document.createElement('p'); totalContainer.innerHTML = `<span>Total: ${totalCount}</span>`; const logButton = document.createElement('button'); logButton.innerHTML = `Log Items`; logButton.style.marginRight = '5px'; logButton.addEventListener('click', () => { console.log(JSON.stringify(sortedTags)); console.log(sortedTags); }); const fetchButton = document.createElement('button'); fetchButton.innerHTML = `Fetch All`; fetchButton.style.marginRight = '5px'; fetchButton.addEventListener('click', () => { setTimeout(summarizeAllBookmarks, 100); }); const loadTranslationsButton = document.createElement('button'); loadTranslationsButton.innerHTML = `Load Translations`; loadTranslationsButton.style.marginRight = '5px'; loadTranslationsButton.addEventListener('click', async () => { const input = document.createElement('input'); input.type = 'file'; input.accept = 'application/json'; input.addEventListener('change', async (event) => { const file = event.target.files[0]; if (!file) return; try { const fileContent = await file.text(); const loadedTranslations = JSON.parse(fileContent); if (typeof loadedTranslations !== 'object') { alert('Invalid JSON file. Expected an object.'); return; } // Update the translations dictionary Object.assign(globalObjects.translations, loadedTranslations); console.log('Loaded translations:', loadedTranslations); alert(`Loaded ${Object.keys(loadedTranslations).length} translations.`); } catch (error) { console.error('Error loading translations:', error); alert('Failed to load translations. Please ensure the file is valid JSON.'); } finally { document.body.removeChild(input); //input.remove(); } }); document.body.appendChild(input); input.click(); }); const bulkActionDiv = document.createElement('div'); if (fetchedAll){ // Create UI for bulk add/remove bulkActionDiv.style.marginTop = '10px'; const minCountInput = document.createElement('input'); minCountInput.type = 'number'; minCountInput.placeholder = 'Min bookmarks'; minCountInput.style.width = '100px'; minCountInput.style.marginRight = '5px'; // Remove button const removeButton = document.createElement('button'); removeButton.innerText = 'Remove'; removeButton.onclick = () => { const minBookmarks = parseInt(minCountInput.value, 10); if (isNaN(minBookmarks)) { alert('Please enter a valid number for minimum bookmarks.'); return; } bulkRemove(minBookmarks); }; const translateTags = async () => { const translations = {}; // Dictionary to store tag translations for (const tagName in tags) { if (!tags[tagName].bookmarks || countIllusts(tags[tagName]) == 0){ continue; } // Skip tags that are purely alphanumeric, whitespace, or common symbols if (/^[a-zA-Z0-9\s~!@#\$%\^&*\(\)\-_=\+\[\]{}\\|;:'",\.\/<>\?]+$/.test(tagName)) { console.log(`Skipping tag (no translation needed): ${tagName}`); continue; } const translation = await getTagTranslation(tagName); if (translation){ translations[tagName] = translation; } } console.log(translations); console.log(JSON.stringify(translations)); let synonyms = reverseObject(translations); console.log(synonyms); console.log(JSON.stringify(synonyms)); if (confirm(`Save translations?`)) { downloadObject(translations, "translations.json"); } if (confirm(`Save translations as synonyms?`)) { downloadObject(synonyms, "synonyms.json"); } // Show an alert asking if the user wants to download the file const toTranslateCount = Object.keys(translations).length; let translatedCount = 0; if (confirm(`Are you sure to translate ${toTranslateCount} tags?`)) { // Trigger the download by clicking the link for (let tagName in translations) { let tag = tags[tagName]; try{ await renameTag(tag, translations[tag.name]); translatedCount += 1; }catch(ex){ continue; } } } alert(`Translated ${translatedCount}/${toTranslateCount} tags`); } const translateButton = document.createElement('button'); translateButton.innerHTML = `Translate Tags`; translateButton.style.marginRight = '5px'; translateButton.addEventListener('click', async () => { await translateTags(); }); // Append elements to the bulk action div bulkActionDiv.appendChild(translateButton); bulkActionDiv.appendChild(minCountInput); bulkActionDiv.appendChild(removeButton); } summaryContent.appendChild(tagContainer); summaryContent.appendChild(totalContainer); summaryContent.appendChild(logButton); summaryContent.appendChild(fetchButton); summaryContent.appendChild(loadTranslationsButton); // Add the bulk action div to the summary UI summaryContent.appendChild(bulkActionDiv); summaryDiv.appendChild(title); summaryDiv.appendChild(summaryContent); document.body.appendChild(summaryDiv); } // Initial summary calculation when the page loads window.addEventListener('load', () => { setTimeout(async () => { await initializeVariables(); renderSummary(); }, 1000); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址