您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add button to download full resolution images from WikiArt
// ==UserScript== // @name WikiArt Downloader // @namespace https://greasyfork.org/en/scripts/492666-wikiart-downloader // @version 1.0 // @description Add button to download full resolution images from WikiArt // @author CertifiedDiplodocus // @match https://www.wikiart.org/* // @icon https://www.google.com/s2/favicons?sz=64&domain=wikiart.org // @grant GM_addStyle // @license GPL-3.0-or-later // ==/UserScript== /* PURPOSE: Adds a "download" button to WikiArt gallery, single painting, and fullscreen views. - Default filename from painting info. - To change the default formatting, edit the variable saveAsName in the function savePaintingAs() below. - Verbose logging for debugging purposes. Errors will always be logged. LIST OF JSON ATTRIBUTES (e.g. painting.title): title, year, width, height, artistName, imageUrl (key renamed from "image"), paintingUrl (url on WikiArt, e.g. "/en/zdzislaw-beksinski/untitled-1972-0"), artistUrl (on WikiArt, e.g. "/en/zdzislaw-beksinski") ---------------------------------------------------------------------------------------------------------------------- */ (function() { 'use strict'; const enableVerboseLogging = false // set to 'true' for debugging purposes // Attempt to find paintings verboseLog ('Page loading. Starting to search for painting information...') let painting let paintingInfoDiv = document.querySelector ('div[ng-init^="paintingJson"]') // why doesn't this detect the gallery items? (which I appreciate, but...) let galleryOuterContainer = document.querySelector ('.masonry-content') if (!paintingInfoDiv && !galleryOuterContainer) {verboseLog ('No gallery or painting info found.'); return null} // EXIT // Create download button in fullscreen mode const newButtonFullscreen = createButton ('a','download-button-fullscreen', downloadFromFullscreen); document.getElementsByClassName('sueprsized-navigation-panel-right')[0].appendChild(newButtonFullscreen); // typo in source code // Create download button on the main artwork page if (paintingInfoDiv) { verboseLog('Found div with painting information.') const newButtonStandard = createButton ('div', 'download-button-standard', downloadFromDetailedView) document.getElementsByClassName('fav-controls-wrapper')[0].appendChild (newButtonStandard) } if (galleryOuterContainer) { verboseLog ('Gallery found! Waiting for user to select a painting...'); // On page load (before the user does anything) get the container and watch for paintings being added or removed let typeOfGallery = document.querySelector('.masonry-content').firstElementChild.getAttribute('ng-switch-when') let galleryContainer = getGalleryContainer () if (galleryContainer) { createGalleryObserver() } // If the user switches between details / masonry / text views, identify and create an observer for the new gallery const galleryTypeObserver = new MutationObserver (galleryTypeChange) galleryTypeObserver.observe (galleryOuterContainer, {childList: true}) function galleryTypeChange (mutations, galleryTypeObserver) { for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { if (addedNode.nodeType === 1) { // select elements, ignore comments typeOfGallery = addedNode.getAttribute('ng-switch-when') galleryContainer = getGalleryContainer () if (galleryContainer) { createGalleryObserver() } } } } } function getGalleryContainer () { let galleryMasonryContainer = document.querySelector ('.wiki-masonry-container') let galleryDetailedContainer = document.querySelector('.wiki-detailed-container') switch (typeOfGallery) { case 'detailed': case 'masonry': verboseLog (`A valid (${typeOfGallery}) gallery view was found.`); return galleryMasonryContainer || galleryDetailedContainer; case 'text': verboseLog ('Cannot download from gallery text view. Waiting for valid gallery type...'); return false; default: console.error ('Could not evaluate the type of gallery.'); return null } } // If gallery items change... function createGalleryObserver() { const galleryObserver = new MutationObserver (galleryItemChanges) galleryObserver.observe (galleryContainer, {childList: true}) } // ...create download buttons for each newly-added painting. Buttons are appended to a <div> with the painting's unique ID or JSON. function galleryItemChanges(mutations, galleryObserver) { let a = 0 let r = 0 let newGalleryItems = [] // empty array let classOfButtonParent typeOfGallery = document.querySelector('[ng-switch-when]').getAttribute('ng-switch-when') switch (typeOfGallery) { case 'masonry': classOfButtonParent = '.title-block'; break; case 'detailed': classOfButtonParent = '.wiki-layout-painting-info-bottom'; break; } // Select the added paintings (element type). Track removed paintings for debugging purposes. for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { if (addedNode.nodeType === 1) { let buttonParent = addedNode.querySelector(classOfButtonParent) newGalleryItems.push(buttonParent) a++ } } for (const removedNode of mutation.removedNodes) { if (removedNode.nodeType === 1) {r++} } } verboseLog (`Loading: removed ${r} paintings, added ${a} paintings.`) if (a > 0) { createButtonsInGallery(newGalleryItems) } } // Create download buttons in gallery view function createButtonsInGallery (galleryItemList) { verboseLog (`Created buttons in ${typeOfGallery} view.`) switch (typeOfGallery) { case 'masonry': galleryItemList.forEach(item => { const newButtonGallery = createButton ('div', 'download-button-gallery like-overlay', downloadFromGalleryMasonry) item.appendChild (newButtonGallery) }) break; case 'detailed': galleryItemList.forEach(item => { const newButtonGallery = createButton ('div', 'download-button-standard', downloadFromDetailedView) item.querySelector('.fav-controls-wrapper').appendChild(newButtonGallery) // needs to be in the fav wrapper or it will not be clickable }) } } } // EVENT LISTENER HANDLERS ------------------------------------------------------- // 'this' = the 'buttonGallery' element (https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#the_value_of_this_within_the_handler) function downloadFromDetailedView() { // single and gallery modes parsePaintingJson (this.parentElement.parentElement) savePaintingAs() } function downloadFromGalleryMasonry() { let parentId = this.parentElement.id painting = { title: document.querySelector(`#${parentId} .artwork-name`).innerText, year: document.querySelector(`#${parentId} .artwork-year`).innerText, artistName: document.querySelector(`#${parentId} .artist-name`).innerHTML.trim().split(' ')[0], imageUrl: this.parentElement.parentElement.querySelector('img').src.split('!')[0] } savePaintingAs() } function downloadFromFullscreen() { painting = { title: document.querySelector('.supersized-slide-name').title, year: document.querySelector('span.year').innerText.slice(1).slice(0,-1), // remove the first and last characters: (1956) -> 1956 artistName: document.querySelector('.supersized-slide-header').title, imageUrl: document.querySelector('.primary-image').src } savePaintingAs() } function parsePaintingJson (sourceDiv) { let initContent = sourceDiv.getAttribute('ng-init') let jsonString = initContent.match('paintingJson = ({.*?})')[1] // clean up jsonString = jsonString.replace('"image" :','"imageUrl" :') // better key name if (!jsonString || jsonString.length<=1) {console.error ("Could not extract JSON from the element."); return null} // EXIT verboseLog ('Extracted JSON string:', jsonString); try { painting = JSON.parse(jsonString); verboseLog (painting.title + ' - ' + painting.year + ' (' + painting.artistName + ')'); verboseLog ('Painting information parsed and extracted!'); } catch (e) { console.error ('Error parsing JSON:', e); alert ('There was an error parsing the painting information. Check the console for more details.'); }; } // When a download button is clicked, save URL with the default filename = ARTIST - TITLE (YEAR).EXT function savePaintingAs () { const imgExtension = painting.imageUrl.split('.').pop(); // pop() returns the last element from an array; in this case, the extension let saveAsName = `${painting.artistName} - ${painting.title} (${painting.year}).${imgExtension}`; saveAsName = decodeHTML(saveAsName); // handle special characters (í, ç etc) downloadImage(painting.imageUrl, saveAsName); } // FUNCTIONS AND STYLES --------------------------------------------------------------- // Create a button with an event listener function createButton (elementType, buttonClass, callbackFunctionName) { let newButton = document.createElement (elementType) newButton.className = buttonClass newButton.addEventListener ('click', callbackFunctionName, false) return newButton } // Check for HTML codes in text and convert to characters function decodeHTML (stringInput) { let txt = document.createElement("textarea"); txt.innerHTML = stringInput; return txt.value; } // Save image using fetch (a workaround because Chrome and Firefox both block the "download" attribute for images on different domains) async function downloadImage(imageSrc, imageName) { const image = await fetch(imageSrc) const imageBlob = await image.blob() const imageURL = URL.createObjectURL(imageBlob) const link = document.createElement('a') link.href = imageURL link.download = imageName document.body.appendChild(link) link.click() document.body.removeChild(link) } function verboseLog (textForConsole) { if (!enableVerboseLogging) {return null} console.log (textForConsole) } //--- Style newly-added button in CSS. Modify the page CSS to make room for the button (overwrite with !important flag). GM_addStyle ( ` .download-button-fullscreen { display:block; height:40px; width:40px; background:url(https://upload.wikimedia.org/wikipedia/commons/8/8a/Download-icon.svg) center center no-repeat; margin:0 10px; cursor:pointer } .download-button-standard { display:block; height:40px; width:40px; position:absolute; right:0px; background:url(https://upload.wikimedia.org/wikipedia/commons/7/72/Download-icon-green.svg) center center no-repeat; background-size:24px; cursor:pointer } .download-button-gallery { left:calc(50% - 20px - 8px - 40px) !important; background:url(https://upload.wikimedia.org/wikipedia/commons/7/72/Download-icon-green.svg) center center no-repeat !important; background-size:24px !important; /*; background-color:#e9e9eb !important */ } .wiki-masonry-container>li:hover .title-block .like-overlay.like-overlay-left { left:calc(50% - 20px) !important } .wiki-masonry-container>li:hover .title-block .like-overlay.like-overlay-right { left:calc(50% + 20px + 8px) !important } /* Fix weird formatting in gallery detailed view */ .fav-controls-wrapper { width:120px !important } .copyright-wrapper { width:calc(100% - 120px) !important } .fav-controls-heart { top:0px !important } .fav-controls-folder { top:0px !important; right:40px !important } ` ); })();