// ==UserScript==
// @name 知识星球图片保存(支持原图)
// @namespace http://tampermonkey.net/
// @version 1.5
// @description 在知识星球帖子的大图和小图右上角添加保存按钮,优先下载原图,否则下载高质量图片,文件名包含作者、日期和序号。
// @author Wenston
// @match https://wx.zsxq.com/*
// @grant GM_xmlhttpRequest
// @grant GM_download
// @connect images.zsxq.com
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 创建固定的进度条容器
let fixedProgressContainer = document.getElementById('fixed-progress-container');
if (!fixedProgressContainer) {
fixedProgressContainer = document.createElement('div');
fixedProgressContainer.id = 'fixed-progress-container';
fixedProgressContainer.style.position = 'fixed';
fixedProgressContainer.style.right = '20px';
fixedProgressContainer.style.bottom = '20px';
fixedProgressContainer.style.zIndex = '10000';
document.body.appendChild(fixedProgressContainer);
}
function addSaveButton(imageElement, authorName, dateString, imageIndex, originalImageIndex) {
const saveButton = document.createElement('button');
saveButton.textContent = '保存';
// 检查是否为大图模式
const isLargeImage = imageElement.closest('app-image-full-screen');
if (isLargeImage) {
// 大图模式下的按钮样式
saveButton.style.position = 'fixed';
saveButton.style.top = '20px';
saveButton.style.right = '20px';
} else {
// 小图模式下的按钮样式
saveButton.style.position = 'absolute';
saveButton.style.top = '10px';
saveButton.style.right = '10px';
}
saveButton.style.zIndex = '10000'; // 确保按钮始终显示在最上层
saveButton.style.padding = '5px 10px';
saveButton.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
saveButton.style.color = 'white';
saveButton.style.border = 'none';
saveButton.style.borderRadius = '5px';
saveButton.style.cursor = 'pointer';
const progressContainer = document.createElement('div');
progressContainer.style.marginBottom = '10px';
progressContainer.style.padding = '5px';
progressContainer.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
progressContainer.style.borderRadius = '5px';
progressContainer.style.display = 'none';
const fileNameLabel = document.createElement('div');
fileNameLabel.style.color = 'white';
fileNameLabel.style.fontSize = '12px';
fileNameLabel.style.marginBottom = '5px';
progressContainer.appendChild(fileNameLabel);
const progressBarContainer = document.createElement('div');
progressBarContainer.style.width = '200px';
progressBarContainer.style.height = '10px';
progressBarContainer.style.backgroundColor = 'lightgray';
progressBarContainer.style.borderRadius = '5px';
progressContainer.appendChild(progressBarContainer);
const progressBar = document.createElement('div');
progressBar.style.width = '0%';
progressBar.style.height = '100%';
progressBar.style.backgroundColor = 'green';
progressBar.style.borderRadius = '5px';
progressBarContainer.appendChild(progressBar);
saveButton.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
let imageUrl = null;
const originalImageLink = imageElement.parentNode.querySelector('a.original-image');
if (originalImageLink) {
imageUrl = originalImageLink.href;
} else {
// 查找大图的高质量图片
const topicElement = imageElement.closest('app-topic');
const highQualityImage = topicElement.querySelector('img[src*="quality/100!"]');
if (highQualityImage) {
imageUrl = highQualityImage.src;
} else {
// 从小图的 URL 构建高质量 URL
imageUrl = imageElement.src;
if (imageUrl.includes('imageMogr2')) {
const urlParts = imageUrl.split('?');
const baseUrl = urlParts[0];
const params = new URLSearchParams(urlParts[1]);
imageUrl = baseUrl + '?imageMogr2/auto-orient/quality/100!/ignore-error/1';
if (params.has('token')) imageUrl += '&token=' + params.get('token');
if (params.has('e')) imageUrl += '&e=' + params.get('e');
if (params.has('s')) imageUrl += '&s=' + params.get('s');
}
}
}
// 使用 originalImageIndex,如果为 undefined 则使用 imageIndex
const indexToUse = (originalImageIndex !== undefined) ? originalImageIndex : imageIndex;
// 修改日期格式
const dateStr = dateString.replace(/ /g, '').replace(/:/g, '');
const match = dateStr.match(/(\d{4})-(\d{2})-(\d{2})(\d{4})/); // 匹配原始日期格式
const fileName = match ?
`${authorName}_${match[1].slice(2)}${match[2]}${match[3]}${match[4]}_${indexToUse + 1}.jpg` :
`${authorName}_${dateStr}_${indexToUse + 1}.jpg`;
fixedProgressContainer.appendChild(progressContainer);
fileNameLabel.textContent = fileName;
progressContainer.style.display = 'block';
GM_xmlhttpRequest({
method: 'GET',
url: imageUrl,
responseType: 'blob',
onload: function(response) {
GM_download({
url: URL.createObjectURL(response.response),
name: fileName,
saveAs: false,
onload: () => {
setTimeout(() => { progressContainer.remove(); }, 1000);
},
onerror: (err) => {
console.error("Download error:", err);
setTimeout(() => { progressContainer.remove(); }, 1000);
}
});
},
onprogress: function(event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
progressBar.style.width = percentComplete + '%';
}
},
onerror: (err) => {
console.error("Request error:", err);
setTimeout(() => { progressContainer.remove(); }, 1000);
}
});
});
imageElement.parentNode.appendChild(saveButton);
}
function processImages() {
const topicElements = document.querySelectorAll('app-topic');
topicElements.forEach(topicElement => {
const authorElement = topicElement.querySelector('.author .role.member');
const authorName = authorElement ? authorElement.textContent.trim() : 'unknown';
const dateElement = topicElement.querySelector('.date');
const dateString = dateElement ? dateElement.textContent.trim() : 'unknown';
const imageContainers = topicElement.querySelectorAll('app-image-full-screen .image-container, app-image-gallery .image-container');
let smallImageIndex = 0; // 小图索引计数器
imageContainers.forEach(container => {
const isOneImage = container.parentNode.classList.contains('one-image');
const image = isOneImage ? container.querySelector('img.item.single-img') : container.querySelector('img.can-scale, img.item');
if (image && !container.querySelector('button')) {
const isLargeImage = container.closest('app-image-full-screen');
let currentImageIndex = smallImageIndex; // 默认为小图索引
if (isLargeImage) {
// 如果是大图,找到它对应的小图container
// 通过比较src来找到大图小图的对应关系, 大图的src属性里会包含小图的src内容
const updateImageIndex = () => {
const smallImageContainers = topicElement.querySelectorAll('app-image-gallery .image-container');
for (let i = 0; i < smallImageContainers.length; i++) {
const smallImg = smallImageContainers[i].querySelector('img');
if (image.src.includes(smallImg.src.split('?')[0])) {
currentImageIndex = i;
break;
}
}
};
// 初始化时计算索引
updateImageIndex();
// 监听图片src变化
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
updateImageIndex();
// 更新保存按钮的事件处理程序
const saveButton = container.querySelector('button');
if (saveButton) {
container.removeChild(saveButton);
addSaveButton(image, authorName, dateString, 0, currentImageIndex);
}
}
});
});
observer.observe(image, {
attributes: true,
attributeFilter: ['src']
});
}
if (image.complete) {
addSaveButton(image, authorName, dateString, 0, currentImageIndex); // 始终传入 currentImageIndex
} else {
image.onload = () => addSaveButton(image, authorName, dateString, 0, currentImageIndex); // 始终传入 currentImageIndex
}
if (!isOneImage && !isLargeImage) {
smallImageIndex++; // 只有小图才增加索引
}
}
});
});
}
processImages();
const observer = new MutationObserver(processImages);
observer.observe(document.body, {
childList: true,
subtree: true
});
})();