// ==UserScript==
// @name 花瓣 - 添加下载按钮
// @namespace http://tampermonkey.net/
// @version 0.5.4
// @description 给花瓣的图加上“下载”按钮,方便下载
// @author 潘志城_Neo
// @match *://huaban.com/*
// @match *://hbimg.huabanimg.com/*
// @grant GM_download
// @grant GM_xmlhttpRequest
// @grant GM_notification
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @connect gd-hbimg.huaban.com
// ==/UserScript==
;(function () {
"use strict"
// 所有图片
var allImages = []
// 按钮样式
var btnStyleText =
"border:0; color:#ffffff ;background-color: rgb(26 179 125 / 75%);border-radius:8px;padding:3px 12px;cursor:pointer;pointer-events:all;"
var interval = null
var defaultSetting = {
prefix: "HB", // 前缀
show_notification: true, // 是否显示通知消息
rename: false, // 是否重命名
show_source_img: false, // 是否显示大图
show_img_title: false, // 是否显示图片标题
download_type: "gm_download", // 下载方式
}
// 配置信息
var setting = GM_getValue("setting")
if (!setting) {
setting = Object.assign({}, defaultSetting)
} else {
setting = Object.assign({}, defaultSetting, setting)
}
GM_setValue("setting", setting)
// 主函数
function main() {
document.body.addEventListener("click", function (e) {
// 点击img标签的时候才尝试添加下载按钮
if ((e, e.target.tagName === "IMG")) {
addDonwloadBtnToPreivew()
}
})
// 网页滚动的时候,检测图片是否有添加下载按钮,没有就添加
document.addEventListener("scroll", throttle(addDownloadBtn, 300))
// 添加设置选项
setSettingMenu()
addDownloadBtn()
interval = setInterval(() => {
if (allImages.length === 0) {
addDownloadBtn()
} else {
clearInterval(interval)
}
}, 1500)
}
main()
/**
* 添加设置选项
*/
function setSettingMenu() {
var menuCommandSetting = GM_registerMenuCommand(
"设置",
function (e) {
addMenu()
},
"S"
)
}
// 插入菜单到页面
function addMenu() {
var domMenu = document.getElementById("neo_huaban_menu")
if (domMenu !== null) {
return
}
domMenu = document.createElement("div")
domMenu.id = "neo_huaban_menu"
domMenu.style =
"z-index:2333; width:252px; min-height:120px; display:flex; flex-direction: column; position:fixed; top:50%; left: 50%; transform:translate(-50%,-50%); border-radius:8px; overflow:hidden; background:white;box-shadow: 2px 2px 6px 1px #5668577a;"
var domHtml = `
<div class="title" style="padding:7px; text-align:center; background-color:#1AB37D;color:white;cursor:default;">
设置
</div>
<div class="content" style="display: flex; flex-direction: column; padding:15px; flex:1;">
<div>
<div style="display:flex; align-items:center;">
<div style="display:flex; align-items:center;">
<span style="display: inline-block;">重命名</span>
<input type="checkbox" style="margin-left:5px;cursor:pointer;" class="rename" ${
setting.rename ? "checked" : ""
}>
</div>
<div style="display:flex; align-items:center;">
<span style="margin-left: 13px; outline: none;" >前缀</span>
<input style="margin-left:5px; outline:none; width:100px; height: 50%; border-radius: 4px; border: 1px solid #adadadd1;" class="prefix" value="${
setting.prefix
}">
</div>
</div>
<div style="font-size: 0.8em; color: #7a7a7a; margin-left: 20px;">
<div >格式:前缀-年月日-pid</div>
<div>示例:HB-20230216-<a title="https://huaban.com/pins/5073719443=>最后那串数字就是pid" href="https://huaban.com/pins/5073719443" target="_blank">5073719443</a></div>
</div>
</div>
<div style="height:1px; width:100%; background: #cdd3ce47; margin: 7px;"></div>
<div>
<div style="display:flex; align-items:center;">
<span style="display: inline-block;">显示提示信息</span>
<input type="checkbox" style="margin-left:5px;cursor:pointer;" class="show_notification" ${
setting.show_notification ? "checked" : ""
}>
</div>
<div style="font-size: 0.8em; color: #7a7a7a; margin-left: 20px;">
<div>只在360极速浏览器有用。</div>
</div>
</div>
<div style="height:1px; width:100%; background: #cdd3ce47; margin: 7px;"></div>
<div style="display:flex; align-items:center;">
<span style="display: inline-block;">显示大图按钮</span>
<input type="checkbox" style="margin-left:5px;cursor:pointer;" class="show_source_img" ${
setting.show_source_img ? "checked" : ""
}>
</div>
<div style="height:1px; width:100%; background: #cdd3ce47; margin: 7px;"></div>
<div style="display:flex; align-items:center;">
<span style="display: inline-block;">显示图片标题</span>
<input type="checkbox" style="margin-left:5px;cursor:pointer;" class="show_img_title" ${
setting.show_img_title ? "checked" : ""
}>
</div>
<div style="height:1px; width:100%; background: #cdd3ce47; margin: 7px;"></div>
<div style="display:flex; align-items:center;">
<span style="display: inline-block;margin-right:5px;">下载方式</span>
<select class="download_type">
<option value="gm_download" ${
setting.download_type === "gm_download" ? "selected" : ""
}>gm_download</option>
<option value="fetch" ${setting.download_type} ${
setting.download_type === "fetch" ? "selected" : ""
} >fetch</option>
<option value="xhr" ${
setting.download_type === "xhr" ? "selected" : ""
}>xhr</option>
<option value="xmlhttpRequest" ${
setting.download_type === "xmlhttpRequest" ? "selected" : ""
}>xmlhttpRequest</option>
</select>
</div>
<div style="color: #7a7a7a; text-align: center; margin-top: 8px;">
<span>刷新页面后生效</span>
<span class="reload_window" style="background-color: #1ab37d; color: white; border-radius: 5px; padding: 2px 6px 3px; cursor: pointer;">立即刷新</span>
</div>
</div>
<div style="display: flex; justify-content: space-between; text-align: center; background-color: #ebebeb; color: #333; cursor: pointer; align-items: center;">
<div class="reset" style="width:50%; text-align:center;">重置设置</div>
<div class="close" style="width:50%; text-align:center; background-color:#d0d0d0;">
关闭
</div>
</div>
`
domMenu.innerHTML = domHtml
document.body.appendChild(domMenu)
// 设置-添加事件
document
.querySelector("#neo_huaban_menu .content .rename")
.addEventListener("change", function (e) {
if (e.target.checked) {
e.target.removeAttribute("checked")
setting.rename = true
} else {
e.target.setAttribute("checked", true)
setting.rename = false
}
GM_setValue("setting", setting)
})
// 关闭按钮
document
.querySelector("#neo_huaban_menu .close")
.addEventListener("click", function (e) {
removeMenu()
})
// 修改前缀
var dom_prefix = document
.querySelector("#neo_huaban_menu .prefix")
.addEventListener("change", function (e) {
setting.prefix = dom_prefix.value
GM_setValue("setting", setting)
})
// 显示通知消息
document
.querySelector("#neo_huaban_menu .content .show_notification")
.addEventListener("change", function (e) {
if (e.target.checked) {
e.target.removeAttribute("checked")
setting.show_notification = true
} else {
e.target.setAttribute("checked", true)
setting.show_notification = false
}
GM_setValue("setting", setting)
})
// 显示大图
document
.querySelector("#neo_huaban_menu .content .show_source_img")
.addEventListener("change", function (e) {
if (e.target.checked) {
e.target.removeAttribute("checked")
setting.show_source_img = true
} else {
e.target.setAttribute("checked", true)
setting.show_source_img = false
}
GM_setValue("setting", setting)
})
// 显示图片标题
document
.querySelector("#neo_huaban_menu .content .show_img_title")
.addEventListener("change", function (e) {
if (e.target.checked) {
e.target.removeAttribute("checked")
setting.show_img_title = true
} else {
e.target.setAttribute("checked", true)
setting.show_img_title = false
}
GM_setValue("setting", setting)
})
// 修改下载方式
var dom_download_type = document.querySelector(
"#neo_huaban_menu .download_type"
)
addEventListener("change", function (e) {
setting.download_type = dom_download_type.value
GM_setValue("setting", setting)
})
// 重置设置
document
.querySelector("#neo_huaban_menu .reset")
.addEventListener("click", function (e) {
setting = Object.assign({}, defaultSetting)
GM_setValue("setting", setting)
})
// 立即刷新
document
.querySelector("#neo_huaban_menu .reload_window")
.addEventListener("click", function (e) {
location.reload();
})
}
// 从页面中移除菜单
function removeMenu() {
var domMenu = document.getElementById("neo_huaban_menu")
if (domMenu) {
domMenu.remove()
}
}
/**
* 添加下载按钮(如果有按钮,就不添加)
*/
function addDownloadBtn() {
// if(document.URL.includes('discovery') || document.URL.includes('domains') || document.URL.includes('boards') || document.URL.includes('follow') || document.URL.includes('search')){
// addDownloadBtnToDiscovery()
// }
if (document.URL.includes("pins")) {
addDonwloadBtnToPreivew()
} else {
if (!document.URL.includes("user")) {
addDownloadBtnToDiscovery()
}
}
}
function addDownloadBtnToDiscovery() {
allImages = document.querySelectorAll(
".transparent-img-bg.hb-image"
)
allImages.forEach((dom) => {
var pinInfo = dom.parentNode.href.split("/")
// 图片标题和样式
var imgInfo = {
title: dom.getAttribute("alt"),
src: dom.getAttribute("src"),
pin: pinInfo[pinInfo.length - 1],
}
// 和包含图片的a标签同级的节点
var tempList = dom.parentNode.parentNode.childNodes
// 图片dom
var imgNode = tempList[tempList.length - 1]
// 与图片父级a标签同级,并处于上方的元素
var lookNode = tempList[tempList.length - 2]
lookNode.setAttribute("hidden", true)
lookNode.className = ""
lookNode.style.cssText =
"position: absolute;bottom: 8px; right: 8px; display: flex; flex-direction: row;align-items: center;z-index:1"
// 添加鼠标悬停时的样式
lookNode.parentNode.addEventListener("mouseover", function () {
lookNode.removeAttribute("hidden")
})
// 移除鼠标悬停时的样式
lookNode.parentNode.addEventListener("mouseout", function () {
lookNode.setAttribute("hidden", true)
})
if (lookNode.querySelectorAll(".neo_add").length === 0) {
var btnContainer = document.createElement("div")
btnContainer.style = "display:flex;"
if (setting.show_source_img) {
// 添加打开大图按钮
var sourceBtn = document.createElement("div")
sourceBtn.className = "neo_add_source"
sourceBtn.innerText = "大图"
sourceBtn.addEventListener("click", () => {
window.open(imgInfo.src.replace("_fw240webp", ""))
})
sourceBtn.style.cssText = btnStyleText + "margin-left:3px;"
btnContainer.appendChild(sourceBtn)
}
// 添加下载图片按钮
var downloadBtn = document.createElement("div")
downloadBtn.className = "neo_add"
downloadBtn.innerText = "下载"
downloadBtn.addEventListener("click", () => {
downloadImage(imgInfo)
})
downloadBtn.style.cssText = btnStyleText + "margin-left:3px;"
btnContainer.appendChild(downloadBtn)
lookNode.insertBefore(btnContainer, null)
// 添加图片标题
if (setting.show_img_title) {
var domTitle = document.createElement("div")
domTitle.innerText = imgInfo.title
domTitle.title = imgInfo.title
domTitle.style.cssText =
"padding-left:5px;text-overflow: ellipsis;white-space: nowrap;overflow: hidden; color: rgba(30,32,35,.65);height:3em;"
dom.parentNode.parentNode.parentNode.appendChild(domTitle)
}
}
})
}
function addDonwloadBtnToPreivew() {
var newBtn = document.createElement("button")
newBtn.innerText = "下载"
newBtn.style.cssText = btnStyleText + "border-radius:12px;padding:9px 12px;"
newBtn.className = "neo_add_btn"
newBtn.addEventListener("click", function () {
download()
})
function download() {
var imgDom = document.querySelector("#pin_detail div img")
var pinInfo = document.URL.split("/")
var imgInfo = {}
imgInfo.title = imgDom.alt
imgInfo.src = imgDom.src
imgInfo.pin = pinInfo[pinInfo.length - 1]
downloadImage(imgInfo)
}
var count = 0 // 尝试添加下载按钮的次数
var maxCount = 8 // 最大尝试次数
var interval = setInterval(function () {
var btnDom = document.querySelector("#pin_detail div button")
if (btnDom) {
clearInterval(interval)
var neoAddDom = document.querySelector(
"#pin_detail div button.neo_add_btn"
)
// 如果存在就不继续添加了
if (neoAddDom) {
return
}
btnDom.parentNode.appendChild(newBtn)
}
if (count >= maxCount) {
clearInterval(interval)
} else {
count++
}
}, 1000)
}
/**
* 下载图片
* @param {Object} imgInfo src:图片链接; title:图片标题
*/
function downloadImage(imgInfo) {
//替换文件名中不能有的字符
var sign_list = ["\\*", "\\'", '\\"', "<", ">", "\\?", "\\.", "\\|", "\\/"]
for (var i = 0; i < sign_list.length; i++) {
var reg = "/" + sign_list[i] + "/g"
var title = imgInfo.title
if (title) {
imgInfo.title = imgInfo.title.replace(eval(reg), "_")
} else {
imgInfo.title = "无标题"
}
}
imgInfo.src = imgInfo.src.replace(/_fw240.*/, "")
imgInfo.src = imgInfo.src.replace(/_fw658.*/, "")
var imgTitle = imgInfo.title
if (setting.rename) {
imgTitle =
(setting.prefix ? setting.prefix + "-" : "") +
formatDate(new Date()) +
"-" +
imgInfo.pin
}
show_notification({
text: imgTitle,
title: "图片已添加下载",
timeout: 2000,
})
switch (setting.download_type) {
case "gm_download":
imageDownload_with_gm_download(imgInfo.src, imgTitle)
break
case "fetch":
imageDownload_with_fetch(imgInfo.src, imgTitle)
break
case "xhr":
imageDownload_with_Xhr_download(imgInfo.src,imgTitle)
break
case "xmlhttpRequest":
imageDownload_with_xmlhttpRequest_download(imgInfo.src,imgTitle)
break
default:
imageDownload_with_Xhr_download(imgInfo.src, imgTitle)
break
}
}
function show_notification(item) {
if (setting.show_notification) {
GM_notification(item)
}
}
function throttle(cb, wait = 300) {
var last = 0
return function () {
var now = new Date().getTime()
if (now - last > wait) {
cb.call(this)
last = new Date().getTime()
}
}
}
//格式化时间
function formatDate(dat) {
//获取年月日,时间
var year = dat.getFullYear()
var mon =
dat.getMonth() + 1 < 10 ? "0" + (dat.getMonth() + 1) : dat.getMonth() + 1
var data = dat.getDate() < 10 ? "0" + dat.getDate() : dat.getDate()
var newDate = year + mon + data
return newDate
}
/**
* 用fecth下载图片
*/
function imageDownload_with_fetch(src, title) {
fetch(src)
.then((response) => response.blob())
.then((blob) => {
// 2. 创建Blob对象
// 3. 创建URL
const url = URL.createObjectURL(blob)
// 4. 创建下载链接
const a = document.createElement("a")
a.href = url
a.download = title
// 5. 模拟点击下载
document.body.appendChild(a)
a.click()
// 清理创建的URL对象
URL.revokeObjectURL(url)
document.body.removeChild(a)
})
.catch((error) => {
//下载出错,右下角弹窗通知。
show_notification({
text: title + "\n" + error,
title: "下载出错",
timeout: 5000,
})
console.error(error)
})
}
/**
* 用GM_download 下载图片
*/
function imageDownload_with_gm_download(src, title) {
//启用油猴的增强下载函数,可跨域
GM_download({
url: src,
name: title,
onprogress: function () {
if (setting.show_notification) {
var isNotice = false
return function () {
if (!isNotice) {
show_notification({
text: title,
title: "图片已添加下载",
timeout: 2000,
})
isNotice = true
}
}
}
},
onload: function () {
//下载完成之后,右下角弹窗通知。
show_notification({
text: title,
title: "图片已完成下载",
timeout: 5000,
})
},
onerror: function () {
//下载出错,右下角弹窗通知。
show_notification({
text: title + "\n" + imgInfo.src,
title: "下载出错",
timeout: 5000,
})
console.error(error)
},
})
}
/**
* 用原始的ajax方法下载
*/
function imageDownload_with_Xhr_download(src,title) {
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = 'blob';
xhr.onload = function() {
if (xhr.status === 200) {
// 创建一个Blob对象
var blob = xhr.response;
// 创建一个URL对象
var url = window.URL.createObjectURL(blob);
// 创建一个下载链接
var a = document.createElement('a');
a.href = url;
a.download = title;
// 将链接添加到页面并模拟点击进行下载
document.body.appendChild(a);
a.click();
// 释放URL对象
window.URL.revokeObjectURL(url);
a.remove()
}
};
// 错误处理
xhr.onerror = function() {
//下载出错,右下角弹窗通知。
show_notification({
text: title + "\n" + src,
title: "下载出错",
timeout: 5000,
})
console.error(error)
};
xhr.send();
}
/**
* 用 GM_xmlhttpRequest 方式下载
*/
function imageDownload_with_xmlhttpRequest_download(src,title){
GM_xmlhttpRequest({
method: 'GET',
url: src,
responseType: 'blob',
onload: function(response) {
const blob = new Blob([response.response], {type: response.response.type});
const contentType = response.response.type;
let extension = '';
// 根据内容类型确定扩展名
switch(contentType) {
case 'image/jpeg':
extension = '.jpg';
break;
case 'image/png':
extension = '.png';
break;
case 'image/gif':
extension = '.gif';
break;
default:
extension = '.jpg'; // 默认使用 .jpg
}
// 创建下载链接并添加扩展名
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = title + extension;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
},
onerror: function(error) {
//下载出错,右下角弹窗通知。
show_notification({
text: title + "\n" + imgInfo.src,
title: "下载出错",
timeout: 5000,
})
console.error(error)
}
});
}
})()