// ==UserScript==
// @name Flickr Download Link Plus
// @name:zh-CN Flickr下载助手增强版
// @name:zh-TW Flickr下載助手增強版
// @version 4.2
// @description Adds download links to photos with size selection and batch download support for Flickr
// @description:zh-CN 为Flickr添加图片下载按钮,支持选择不同尺寸和批量下载功能
// @description:zh-TW 為Flickr添加圖片下載按鈕,支持選擇不同尺寸和批量下載功能
// @author powcai
// @namespace cuzi
// @license MIT
// @grant none
// @icon https://combo.staticflickr.com/pw/images/favicons/favicon-228.png
// @match https://flickr.com/*
// @match https://*.flickr.com/*
// @require https://cdn.jsdelivr.net/npm/sweetalert2@11
// @run-at document-end
// @noframes
// @connect flickr.com
// @connect staticflickr.com
// ==/UserScript==
/* jshint asi: true, esversion: 8 */
(function () {
'use strict'
// 下载图片函数
const downloadImage = async (imageUrl) => {
try {
const response = await fetch('https:' + imageUrl);
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = blobUrl;
link.download = imageUrl.split('/').pop();
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setTimeout(() => URL.revokeObjectURL(blobUrl), 100);
} catch (error) {
console.error('Download error:', error);
}
};
// 获取最大尺寸的图片URL
const getLargestSize = (sizes) => {
const availableSizes = Object.entries(sizes)
.map(([key, data]) => ({
src: data.src,
pixels: data.width * data.height
}))
.sort((a, b) => b.pixels - a.pixels);
return availableSizes[0]?.src;
};
const createBatchButton = () => {
const buttonId = 'batch-download-button';
if (!document.getElementById(buttonId)) {
const batchButton = document.createElement('div');
batchButton.id = buttonId;
batchButton.innerHTML = `
<div style="
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
background: rgba(0,0,0,0.8);
color: white;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
z-index: 9999;
display: flex;
align-items: center;
gap: 5px;
">
<span>⬇️</span>
<span>Download All (Max Size)</span>
</div>
`;
document.body.appendChild(batchButton);
return batchButton;
}
return document.getElementById(buttonId);
};
// 添加下载按钮的通用函数
function addDownloadButtons(containers, linkClass) {
containers.forEach(container => {
if (!container.querySelector('.download-button')) {
const detailLink = container.querySelector(`.${linkClass}`).href;
const photoName = container.querySelector(`.${linkClass}`).getAttribute('title') || '';
const downloadButton = document.createElement('div');
downloadButton.className = 'download-button';
downloadButton.innerHTML = `
<span style="
position: fixed;
right: 10px;
top: 10px;
background: rgba(0,0,0,0.5);
color: white;
padding: 5px;
border-radius: 3px;
cursor: pointer;
z-index: 9999
">⬇️</span>
<div class="size-menu" style="
display: none;
position: fixed;
right: 45px;
top: 10px;
background: rgba(0,0,0,0.8);
border-radius: 3px;
padding: 5px;
z-index: 9999;
max-height: 300px;
overflow-y: auto;
"></div>
`;
// 点击下载按钮显示尺寸菜单
downloadButton.onclick = async function(e) {
e.preventDefault();
e.stopPropagation();
const menu = downloadButton.querySelector('.size-menu');
if (menu.style.display === 'none') {
try {
const response = await fetch(detailLink);
const text = await response.text();
const regex = /"sizes":\s*({[^}]*(?:}[^}]*)*?}),"descendingSizes"/;
const match = text.match(regex);
if (match) {
const sizes = JSON.parse(match[1]);
createSizeMenu(sizes, menu);
menu.style.display = 'block';
}
} catch (error) {
console.error('Error:', error);
}
} else {
menu.style.display = 'none';
}
};
// 点击其他地方关闭菜单
document.addEventListener('click', (e) => {
if (!downloadButton.contains(e.target)) {
downloadButton.querySelector('.size-menu').style.display = 'none';
}
});
container.style.position = 'relative';
container.appendChild(downloadButton);
}
});
}
// 创建尺寸选择菜单内容
const createSizeMenu = (sizes, menu) => {
menu.innerHTML = '';
const sizeNames = {
'o': 'Original',
'6k': '6K',
'5k': '5K',
'4k': '4K',
'3k': '3K',
'k': '2K',
'h': 'Large 1600',
'l': 'Large',
'c': 'Medium 800',
'z': 'Medium 640',
'm': 'Medium'
};
const availableSizes = Object.entries(sizes)
.map(([key, data]) => ({
key,
width: data.width,
height: data.height,
src: data.src,
pixels: data.width * data.height
}))
.sort((a, b) => b.pixels - a.pixels);
availableSizes.forEach(size => {
const button = document.createElement('div');
button.style.cssText = `
color: white;
padding: 5px 10px;
cursor: pointer;
white-space: nowrap;
margin: 2px 0;
`;
const sizeName = sizeNames[size.key] || `Size ${size.key.toUpperCase()}`;
button.textContent = `${sizeName} (${size.width}×${size.height})`;
button.onmouseover = () => {
button.style.backgroundColor = 'rgba(255,255,255,0.2)';
};
button.onmouseout = () => {
button.style.backgroundColor = 'transparent';
};
button.onclick = (e) => {
e.stopPropagation();
downloadImage(size.src);
menu.style.display = 'none';
};
menu.appendChild(button);
});
};
// 处理列表视图
function page_photo_list() {
const photoContainers = document.querySelectorAll('.photo-list-photo-container');
if (photoContainers.length === 0) return;
const batchButton = createBatchButton('right');
// 批量下载处理
batchButton.onclick = async () => {
for (const container of photoContainers) {
try {
const detailLink = container.querySelector('.overlay').href;
const response = await fetch(detailLink);
const text = await response.text();
const regex = /"sizes":\s*({[^}]*(?:}[^}]*)*?}),"descendingSizes"/;
const match = text.match(regex);
if (match) {
const sizes = JSON.parse(match[1]);
const largestSrc = getLargestSize(sizes);
if (largestSrc) {
await downloadImage(largestSrc);
}
}
} catch (error) {
console.error('Error downloading image:', error);
}
}
};
addDownloadButtons(photoContainers, 'overlay');
}
// 处理卡片视图
function card_photo_list() {
const photoContainers = document.querySelectorAll('.photo-card-content');
if (photoContainers.length === 0) return;
const batchButton = createBatchButton('left');
// 批量下载处理
batchButton.onclick = async () => {
for (const container of photoContainers) {
try {
const detailLink = container.querySelector('.photo-link').href;
const response = await fetch(detailLink);
const text = await response.text();
const regex = /"sizes":\s*({[^}]*(?:}[^}]*)*?}),"descendingSizes"/;
const match = text.match(regex);
if (match) {
const sizes = JSON.parse(match[1]);
const largestSrc = getLargestSize(sizes);
if (largestSrc) {
await downloadImage(largestSrc);
}
}
} catch (error) {
console.error('Error downloading image:', error);
}
}
};
addDownloadButtons(photoContainers, 'photo-link');
}
function main() {
page_photo_list();
card_photo_list();
}
const observer = new MutationObserver((mutations) => {
main();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
main();
})();