Greasy Fork

Greasy Fork is available in English.

哔哩哔哩专栏动态显示原图

同屏显示哔哩哔哩专栏和动态中的原图

// ==UserScript==
// @name        哔哩哔哩专栏动态显示原图
// @namespace   hzhbest
// @include     http://www.bilibili.com/read/*
// @include     https://www.bilibili.com/read/*
// @include     http://t.bilibili.com/*
// @include     https://t.bilibili.com/*
// @include     http://space.bilibili.com/*/dynamic*
// @include     https://space.bilibili.com/*/dynamic*
// @include     http://www.bilibili.com/opus/*
// @include     https://www.bilibili.com/opus/*
// @description    同屏显示哔哩哔哩专栏和动态中的原图
// @version     1.2
// @grant       GM_xmlhttpRequest
// @grant       unsafeWindow
// @grant       GM_getValue
// @grant       GM_setValue
// @run-at      document-end
// @license     GNU GPLv3
// ==/UserScript==

(function () {
	// --这里是设置区-- //
	var topheight = 60; // 哔哩哔哩顶栏高度
	var topspare = 125; // 滚动预留顶部高度
	var autorefresh = true; // 是否定时自动检测页面变化
	var loadLargeGif = GM_getValue('BLimgAll') || false; // 是否载入大型动图

	// --以下是代码区,请不要随意改动-- //

	// 匹配哔哩哔哩专栏和动态页面
	var regexRead = /bilibili\.com\/read\/[cv]\d+/; // 专栏页面
	var regexOpus = /bilibili\.com\/opus\/\d+/; // 新版专栏页面
	var regexDynamic = /t\.bilibili\.com\/\d+/; // 单条动态页面
	var regexSpace = /space\.bilibili\.com\/\d+\/dynamic/; // 用户动态页面
	var cur = -1; // "当前图片"序
	pinit();

	function pinit() {
		// 检查是否已开大图,否则进入初始化进程;如果已开大图但网址已非匹配网址则去除按钮;一秒检测一次
		var bpimg = document.querySelector("img.big_pic");
		var isMatch = regexRead.test(document.location.href) || 
		              regexOpus.test(document.location.href) ||
		              regexDynamic.test(document.location.href) ||
		              regexSpace.test(document.location.href);
		console.log("matched?: ", isMatch);
		
		if (isMatch) {
			if (!bpimg) init();
		} else {
			var buttonbox = document.querySelector(".big_pic_b");
			if (!!buttonbox) buttonbox.parentNode.removeChild(buttonbox);
		}
		
		if (autorefresh) {
			setTimeout(pinit, 1000);
		}
	}

	function init() {
		// 根据不同页面类型执行不同的初始化
		if (regexRead.test(document.location.href)) {
			initRead();
		} else if (regexOpus.test(document.location.href)) {
			initOpus();
		} else if (regexDynamic.test(document.location.href) || regexSpace.test(document.location.href)) {
			initDynamic();
		}
	}

	function initRead() {
		// 初始化专栏页面
		var articleContainer = document.querySelector(".article-container");
		if (!articleContainer) {
			console.log("专栏容器未找到,等待加载");
			setTimeout(init, 1000);
			return;
		}

		var imgElements = articleContainer.querySelectorAll("img.normal-img");
		if (imgElements.length === 0) {
			console.log("未找到专栏图片,退出");
			return;
		}

		console.log("开始处理专栏图片");
		goRead(imgElements);
	}

	function initOpus() {
		// 初始化新版专栏页面(opus)
		console.log("开始初始化新版专栏页面");
		
		// 等待内容加载完成
		if (!document.querySelector(".opus-module-content")) {
			console.log("新版专栏容器未找到,等待加载");
			setTimeout(init, 1000);
			return;
		}
		
		// 只查找所有img标签
		var allImages = document.querySelectorAll(".opus-module-content img");
		
		if (allImages.length === 0) {
			console.log("未找到专栏图片,退出");
			return;
		}
		
		console.log("找到新版专栏图片,开始处理");
		goOpus(null, null, allImages);
	}

	function initDynamic() {
		// 初始化动态页面
		console.log("开始初始化动态页面");
		
		// 适配新版动态页面结构
		var dynamicContainer = document.querySelector(".bili-dyn-list__item") || 
		                      document.querySelector(".bili-dyn-item") ||
		                      document.querySelector(".card");
		
		if (!dynamicContainer) {
			console.log("动态容器未找到,等待加载");
			setTimeout(init, 1000);
			return;
		}

		// 只查找img标签
		var allImages = document.querySelectorAll(".bili-album__preview__picture__img img");
		
		if (allImages.length === 0) {
			console.log("未找到动态图片,尝试查找其他选择器");
			
			// 尝试查找其他可能的图片容器
			allImages = document.querySelectorAll("img[src*='hdslb.com/bfs/new_dyn/']");
			
			if (allImages.length === 0) {
				console.log("未找到任何动态图片,退出");
				return;
			}
		}
		
		console.log("找到动态图片,开始处理");
		goDynamic(null, null, allImages);
	}

	function goRead(imgElements) {
		// 处理专栏图片
		setupCSS();
		setupButtons();

		// 建立大图框架,用于插入大图
		var bpboxes = [],
			imgsrc,
			imgn,
			imgl = imgElements.length;
		var _limited = false;
		var root = document.querySelector(".article-container");
		var nslink = document.querySelector(".big_pic_ns");
		var nslinks = [];

		for (var i = 0; i < imgl; i++) {
			// 提取专栏大图
			// https://i0.hdslb.com/bfs/article/watermark/[email protected]
			// 转换为
			// https://i0.hdslb.com/bfs/article/xxxx 或 https://i0.hdslb.com/bfs/article/xxxx.jpg
			bpboxes[i] = creaElemIn("div", root);
			nslinks[i] = creaElemIn("div", nslink);
			nslinks[i].innerHTML = (i + 1);
			nslinks[i].name = i;
			creaElemIn("br", root);
            
			var imgSrc = imgElements[i].src;
			// 处理专栏图片URL,获取原图
			imgsrc = convertToOriginalImage(imgSrc);
			
			imgn = creaElemIn("img", bpboxes[i]);
			imgn.src = imgsrc;
			imgn.className = "big_pic";
			imgn.title = "[ " + (i + 1) + " / " + imgl + " ]";
			nslinks[i].style.backgroundImage = 'url("' + imgSrc + '")';
			
			nslinks[i].addEventListener(
				"click",
				function (e) {
					scrollto(getTop(bpboxes[e.target.name]) - topspare + 25);
				},
				false
			);
		}

		// 设置滚动事件和其他交互
		setupScrollEvent(bpboxes, nslinks, imgl);
	}

	function goOpus(allPictures, allSourceSets, allImages) {
		// 处理新版专栏图片
		setupCSS();
		setupButtons();

		// 建立大图框架,用于插入大图
		var bpboxes = [],
			imgsrc,
			imgn,
			imgl = 0;
		var _limited = false;
		var root = document.querySelector(".opus-module-content");
		var nslink = document.querySelector(".big_pic_ns");
		var nslinks = [];

		// 收集所有图片URL (只从img标签获取)
		var imageUrls = [];
		var processedOriginals = []; // 存储已处理过的原图URL,用于去重
		
		if (allImages.length > 0) {
			console.log("从img.src中获取图片URL");
			for (var i = 0; i < allImages.length; i++) {
				var src = allImages[i].getAttribute("src");
				if (src && src.includes("hdslb.com/bfs/")) {
					if (src.startsWith("//")) {
						src = "https:" + src;
					}
					
					// 提前计算原图URL用于去重
					var originalSrc = convertToOriginalImage(src);
					// 检查是否已经处理过相同的原图
					if (!processedOriginals.includes(originalSrc)) {
						imageUrls.push(src);
						processedOriginals.push(originalSrc);
					} else {
						console.log("跳过重复图片: " + src);
					}
				}
			}
		}
		
		console.log("找到图片URL数量: " + imageUrls.length);
		imgl = imageUrls.length;
		
		if (imgl === 0) {
			console.log("未能提取到有效的图片URL,退出");
			return;
		}

		for (var i = 0; i < imgl; i++) {
			bpboxes[i] = creaElemIn("div", root);
			nslinks[i] = creaElemIn("div", nslink);
			nslinks[i].innerHTML = (i + 1);
			nslinks[i].name = i;
			creaElemIn("br", root);
            
			var imgSrc = imageUrls[i];
			// 处理专栏图片URL,获取原图
			imgsrc = convertToOriginalImage(imgSrc);
			
			imgn = creaElemIn("img", bpboxes[i]);
			imgn.src = imgsrc;
			imgn.className = "big_pic";
			imgn.title = "[ " + (i + 1) + " / " + imgl + " ]";
			nslinks[i].style.backgroundImage = 'url("' + imgSrc + '")';
			
			nslinks[i].addEventListener(
				"click",
				function (e) {
					scrollto(getTop(bpboxes[e.target.name]) - topspare + 25);
				},
				false
			);
		}

		// 设置滚动事件和其他交互
		setupScrollEvent(bpboxes, nslinks, imgl);
	}

	function goDynamic(allPictures, allSourceSets, allImages) {
		// 处理动态图片
		setupCSS();
		setupButtons();

		// 建立大图框架,用于插入大图
		var bpboxes = [],
			imgsrc,
			imgn,
			imgl = 0;
		var _limited = false;
		var root = document.querySelector(".bili-dyn-list__item") || 
		          document.querySelector(".bili-dyn-item") ||
		          document.querySelector(".card");
		var nslink = document.querySelector(".big_pic_ns");
		var nslinks = [];

		// 收集所有图片URL (只从img标签获取)
		var imageUrls = [];
		var processedOriginals = []; // 存储已处理过的原图URL,用于去重
		
		if (allImages.length > 0) {
			console.log("从img.src中获取图片URL");
			for (var i = 0; i < allImages.length; i++) {
				var src = allImages[i].getAttribute("src");
				if (src && src.includes("hdslb.com/bfs/")) {
					if (src.startsWith("//")) {
						src = "https:" + src;
					}
					
					// 提前计算原图URL用于去重
					var originalSrc = convertToOriginalImage(src);
					// 检查是否已经处理过相同的原图
					if (!processedOriginals.includes(originalSrc)) {
						imageUrls.push(src);
						processedOriginals.push(originalSrc);
					} else {
						console.log("跳过重复图片: " + src);
					}
				}
			}
		}
		
		console.log("找到图片URL数量: " + imageUrls.length);
		imgl = imageUrls.length;
		
		if (imgl === 0) {
			console.log("未能提取到有效的图片URL,退出");
			return;
		}

		for (var i = 0; i < imgl; i++) {
			// 提取动态大图
			bpboxes[i] = creaElemIn("div", root);
			nslinks[i] = creaElemIn("div", nslink);
			nslinks[i].innerHTML = (i + 1);
			nslinks[i].name = i;
			creaElemIn("br", root);
            
			var imgSrc = imageUrls[i];
			// 处理动态图片URL,获取原图
			imgsrc = convertToOriginalImage(imgSrc);
			
			imgn = creaElemIn("img", bpboxes[i]);
			imgn.src = imgsrc;
			imgn.className = "big_pic";
			imgn.title = "[ " + (i + 1) + " / " + imgl + " ]";
			nslinks[i].style.backgroundImage = 'url("' + imgSrc + '")';
			
			nslinks[i].addEventListener(
				"click",
				function (e) {
					scrollto(getTop(bpboxes[e.target.name]) - topspare + 25);
				},
				false
			);
		}

		// 设置滚动事件和其他交互
		setupScrollEvent(bpboxes, nslinks, imgl);
	}

	function convertToOriginalImage(imgSrc) {
		// 转换图片链接为原图链接
		var originalSrc = imgSrc;
		
		// 处理专栏图片 - 老版格式
		// 移除watermark和后续参数
		if (imgSrc.includes("hdslb.com/bfs/article")) {
			originalSrc = imgSrc.replace(/\/watermark\/.*$/, "");
			// 移除@后的参数
			originalSrc = originalSrc.replace(/@.*$/, "");
			// 移除.webp后缀
			originalSrc = originalSrc.replace(/\.webp$/, "");
		}
		
		// 处理动态图片 - 老版格式
		// 替换@后的尺寸和格式参数
		if (imgSrc.includes("hdslb.com/bfs/album")) {
			// 移除@后的参数
			originalSrc = originalSrc.replace(/@.*$/, "");
			// 若没有扩展名,尝试添加.jpg
			if (!originalSrc.match(/\.(jpg|jpeg|png|gif)$/i)) {
				originalSrc += ".jpg";
			}
		}
		
		// 处理动态图片和新版专栏 - 新版格式
		// 替换@后的尺寸和格式参数
		if (imgSrc.includes("hdslb.com/bfs/new_dyn")) {
			// 移除@后的参数
			originalSrc = originalSrc.replace(/@.*$/, "");
			// 若没有扩展名,尝试添加.jpg
			if (!originalSrc.match(/\.(jpg|jpeg|png|gif)$/i)) {
				originalSrc += ".jpg";
			}
		}
		
		// 确保URL有协议头
		if (originalSrc.startsWith("//")) {
			originalSrc = "https:" + originalSrc;
		}
		
		console.log("原图URL: " + originalSrc);
		return originalSrc;
	}

	function setupCSS() {
		// 插入CSS
		var headID = document.getElementsByTagName("head")[0];
		var cssNode = creaElemIn("style", headID);
		cssNode.type = "text/css";
		cssNode.innerHTML = [
			".big_pic{max-width: 890px;}",
			".big_pic_n{max-width: 500px;}",
			".big_pic:hover, .big_pic_n:hover{box-shadow: 0 0 30px 2px #f1ecdf;}",
			".big_pic_poster{outline: 2px dashed #fcde44; outline-offset: -2px; cursor: pointer;}",
			".big_pic_v{max-width: 90%; max-height: 80vh; cursor: pointer;}"
		].join(""); //大图样式
		cssNode.innerHTML += [
			".big_pic_b{position: fixed; left: 10px; top: 200px; z-index: 99999;}",
			".big_pic_btn{height: 20px; min-width: 50px; width: fit-content; padding: 3px; margin-bottom: 20px; border: 1px solid white; color: white; background: rgba(133,133,133,0.6); cursor: pointer; user-select: none;}",
			".big_pic_btn:hover{background: rgba(133,133,200,0.6);}",
			".big_pic_ns{margin-bottom: 20px; width: 62px; display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; grid-gap: 3px;}",
			".big_pic_ns > div{font-size: 10px; line-height: 28px; text-align: right; height: 15px; width: 15px; background-clip: border-box; background-position: center; background-size: cover; padding: 3px; border: 1px solid #7a7a7a; color: white; text-shadow: 0 0 2px black,0 0 2px black,0 0 2px black; cursor: pointer; user-select: none; opacity: 0.7;}",
			".big_pic_ns > div:hover{font-size: 0px; outline: 1px solid #f8f87b; opacity: 1;}",
			".big_pic_ns > div.curr{outline: 3px solid #f87bce; opacity: 0.9;}"
		].join(""); //按钮样式
	}

	function setupButtons() {
		// 创建控制按钮
		var buttonbox = creaElemIn("div", document.body);
		buttonbox.className = "big_pic_b";
		var tplink = creaElemIn("div", buttonbox); // 直达页顶链接
		var nclink = creaElemIn("div", buttonbox); // 图片限宽链接
		var n1link = creaElemIn("div", buttonbox); // 首个图片链接
		var n2link = creaElemIn("div", buttonbox); // 上个图片链接
		var nslink = creaElemIn("div", buttonbox); // 图片导航按钮
		var n3link = creaElemIn("div", buttonbox); // 下个图片链接
		nslink.className = "big_pic_ns";

		tplink.className = "big_pic_btn";
		tplink.innerHTML = "回到页顶";
		tplink.addEventListener(
			"click",
			function () {
				var headerbox = document.querySelector("header");
				scrollto(getTop(headerbox) - topheight * 2);
			},
			false
		);

		nclink.className = "big_pic_btn";
		nclink.innerHTML = "图片限宽";
		nclink.addEventListener(
			"click",
			function () {
				var bpboxes = document.querySelectorAll("div > img.big_pic, div > img.big_pic_n");
				var i;
				if (bpboxes.length > 0) {
					if (bpboxes[0].className === "big_pic") {
						for (i = 0; i < bpboxes.length; i++) {
							bpboxes[i].className = "big_pic_n";
						}
					} else {
						for (i = 0; i < bpboxes.length; i++) {
							bpboxes[i].className = "big_pic";
						}
					}
				}
			},
			false
		);

		n1link.className = "big_pic_btn";
		n1link.innerHTML = "△首个图片";
		n1link.addEventListener(
			"click",
			function () {
				var bpboxes = document.querySelectorAll("div > img.big_pic, div > img.big_pic_n");
				if (bpboxes.length > 0) {
					scrollto(getTop(bpboxes[0].parentNode) - topspare + 25);
				}
			},
			false
		);

		n2link.className = "big_pic_btn";
		n2link.innerHTML = "▲上个图片";
		n2link.addEventListener(
			"click",
			function () {
				var bpboxes = document.querySelectorAll("div > img.big_pic, div > img.big_pic_n");
				var t = document.documentElement.scrollTop;
				for (var j = bpboxes.length - 1; j >= 0; j--) {
					if (t > getTop(bpboxes[j].parentNode) + bpboxes[j].parentNode.offsetHeight - topspare) {
						scrollto(getTop(bpboxes[j].parentNode) - topspare + 25);
						return;
					}
				}
			},
			false
		);

		n3link.className = "big_pic_btn";
		n3link.innerHTML = "▼下个图片";
		n3link.addEventListener(
			"click",
			function () {
				var bpboxes = document.querySelectorAll("div > img.big_pic, div > img.big_pic_n");
				var t = document.documentElement.scrollTop;
				for (var j = 0; j < bpboxes.length; j++) {
					if (t < getTop(bpboxes[j].parentNode) - topspare) {
						scrollto(getTop(bpboxes[j].parentNode) - topspare + 25);
						return;
					}
				}
			},
			false
		);
	}

	function setupScrollEvent(bpboxes, nslinks, imgl) {
		// 设置滚动事件以突出显示当前图片
		document.onscroll = function () {
			var t = document.documentElement.scrollTop; // 当前滚动位置(视框顶y)
			var w = window.innerHeight;	// 当前视框高度
			var percentage = 1 / 4;		// 视框内"注视框"距视框顶底距离(1-2*percentage 为注视框高度占比)
			var linetop = t + w * percentage - topspare;	// 注视框顶位置
			var linebtm = t + w * (1 - percentage);	// 注视框底位置
			var j, vh, vhmax = 0;	// 检查图片序、图片在注视框内高度、注视框内最大高度
			
			if (bpboxes.length > 0) {
				if (getTop(bpboxes[0]) >= linebtm || getTop(bpboxes[imgl - 1]) + bpboxes[imgl - 1].offsetHeight <= linetop) {
					cur = -1;	// 若首图在注视框底之下或末图在注视框顶之上,则无当前图
				} else {
					for (j = imgl - 1; j >= 0; j--) {	// 从底部检查各图片
						let ti = getTop(bpboxes[j]), hi = bpboxes[j].offsetHeight;	// 检查图片位置、检查图片高度
						if (ti < linebtm && (ti + hi) > linetop) {	// 若图片顶端在注视框底之上、底端在注视框顶之下
							vh = Math.min(linebtm, ti + hi) - Math.max(linetop, ti);	// 计算注视框内高度
							if (vh >= vhmax) {	// 当检查图片拥有更大的注视框内高度,则其为"当前图片"(等高则更前)
								vhmax = vh;
								cur = j;
							}
						}
					}
				}
				
				if (cur !== -1) {
					for (j = imgl - 1; j >= 0; j--) {
						if (j !== cur) {
							nslinks[j].classList.remove("curr");
						} else {
							nslinks[j].classList.add("curr");
						}
					}
				} else {
					for (j = imgl - 1; j >= 0; j--) {
						if (j !== cur) {
							nslinks[j].classList.remove("curr");
						}
					}
				}
			}
		};
	}

	function scrollto(pos) {
		// 滚动到指定位置
		document.documentElement.scrollTop = pos;
	}

	// 创建元素
	function creaElemIn(tagname, destin) {
		var theElem = destin.appendChild(document.createElement(tagname));
		return theElem;
	}

	// 通过xpath获取元素
	function getElem(xpath) {
		return document
			.evaluate(
				xpath,
				document,
				null,
				XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
				null
			)
			.snapshotItem(0);
	}

	// 获取元素的绝对顶部位置
	function getTop(e) {
		var offset = e.offsetTop;
		if (e.offsetParent != null) offset += getTop(e.offsetParent);
		return offset;
	}

})();