您需要先安装一个扩展,例如 篡改猴、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
- }
- ` );
- })();