Greasy Fork 支持简体中文。

巴哈圖片預覽

pixiv/twitter網址開圖

安裝腳本?
作者推薦腳本

您可能也會喜歡 巴友暱稱紀錄

安裝腳本
// ==UserScript==
// @name         巴哈圖片預覽
// @description  pixiv/twitter網址開圖
// @namespace    https://smilin.net
// @author       smilin
// @version      0.01
// @license MIT
// @homepage     https://home.gamer.com.tw/homeindex.php?owner=a33073307
// @match        https://forum.gamer.com.tw/C.php*
// @match        https://forum.gamer.com.tw/Co.php*
// @icon         https://forum.gamer.com.tw/favicon.ico
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      pixiv.net
// @connect      www.pixiv.net
// @connect      i.pximg.net
// @connect      twitter.com
// @connect      x.com
// @connect      api.fxtwitter.com
// ==/UserScript==

(function () {
	"use strict";

	const SCRIPT_NAME = "bahamut-pic-preview";

	//#region  注入 CSS 樣式
	GM_addStyle(`
		.bpp-settings {
			position: fixed;
			top: 50%;
			left: 50%;
			transform: translate(-50%, -50%);
			background-color: var(--f1-bg);
			border: 1px solid #ccc;
			border-radius: 4px;
			padding: 10px;
			width: 200px;
			box-shadow: 0 2px 5px var(--secondary-text);
			z-index: 1000;
			display: none;
        	opacity: 0;
			animation: fadeIn 0.3s ease-out forwards;
		}
		.bpp-settings.active {
			display: block;
			opacity: 1;
		}
		@keyframes fadeIn {
			from {
				opacity: 0;
				transform: translate(-50%, -60%);
			}
			to {
				opacity: 1;
				transform: translate(-50%, -50%);
			}
		}
		.bpp-overlay {
			position: fixed;
			top: 0;
			left: 0;
			right: 0;
			bottom: 0;
			background-color: rgba(0, 0, 0, 0.5);
			z-index: 999;
			display: none;
		}
		.bpp-overlay.active {
			display: block;
		}
		.bpp-option {
			margin-bottom: 8px;
		}
		.bpp-option label {
			display: flex;
			align-items: center;
			font-size: 14px;
		}
		.bpp-option input[type="checkbox"] {
			margin-right: 8px;
		}
        .bpp-photo-box {
            margin: 15px 0;
			border: 1px solid var(--quaternary-text);
            border-radius: 4px;
            padding: 10px;
			background-color: var(--b1-bg);
        }
        .bpp-photo-container {
            position: relative;
            width: 100%;
            height: 400px;
            overflow: hidden;
        }
        .bpp-photo {
            width: 100%;
            height: 100%;
            object-fit: contain;
			cursor: pointer;
			transition: opacity 0.3s ease;
        }
		.bpp-photo:hover {
			opacity: 0.8;
		}
        .bpp-nav {
            position: absolute;
            top: 50%;
            transform: translateY(-50%);
			display: flex;
			align-items: center;
            background-color: rgba(0, 0, 0, 0.5);
            color: white;
            padding: 15px;
            cursor: pointer;
			transition: transform 0.3s ease;
			font-weight: 800;
        }
		.bpp-nav:hover {
			transform: translateY(-50%) scale(1.1);
		}
        .bpp-nav-left { left: 0px; }
        .bpp-nav-right { right: 0px; }
        .bpp-info {
            display: flex;
            justify-content: space-between;
			margin-top: 10px;
            padding-top: 10px;
			border-top: 1px solid var(--quaternary-text);
        }
        .bpp-fullscreen {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.9);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 9999;
        }
        .bpp-fullscreen-image {
            max-width: 90%;
            max-height: 90%;
            object-fit: contain;
        }
        .bpp-close-fullscreen {
            position: absolute;
            top: 20px;
            right: 20px;
            color: white;
            font-size: 30px;
            cursor: pointer;
        }
    `);
	//#endregion

	//#region  獲取與保存設置
	// 設置默認配置
	const defaultSettings = {
		enabled: true,
		showNSFW: false,
	};

	// 從 localStorage 中獲取設置
	function getSettings() {
		const storedSettings = localStorage.getItem(SCRIPT_NAME);
		return storedSettings ? JSON.parse(storedSettings) : defaultSettings;
	}

	// 將設置保存到 localStorage
	function saveSettings(settings) {
		localStorage.setItem(SCRIPT_NAME, JSON.stringify(settings));
	}

	let settings = getSettings();
	//#endregion

	//#region  創建控制按鈕和設置面板
	function createUI() {
		const maxAttempts = 10; // 最大嘗試次數
		const delayBetweenAttempts = 500; // 每次嘗試之間的延遲(毫秒)
		let attempts = 0;

		function attemptCreate() {
			const navbar = document.querySelector(
				".BH-menu.box-shadow__soft .BH-menuE"
			);
			if (navbar) {
				// Navbar 已找到,執行創建 UI 的邏輯
				const containerLi = document.createElement("li");
				containerLi.innerHTML = `
					<a href="javascript:void(0)" id="bpp-toggle">圖片預覽設置</a>
				`;
				navbar.appendChild(containerLi);

				// 創建設置面板和遮罩
				const settingsPanel = document.createElement("div");
				settingsPanel.className = "bpp-settings";
				settingsPanel.id = "bpp-settings";
				settingsPanel.innerHTML = `
					<div class="bpp-option">
						<label>
							<input type="checkbox" id="bpp-enable" ${settings.enabled ? "checked" : ""}>
							啟用功能
						</label>
					</div>
					<div class="bpp-option">
						<label>
							<input type="checkbox" id="bpp-nsfw" ${settings.showNSFW ? "checked" : ""}>
							顯示限制級內容
						</label>
					</div>
				`;
				document.body.appendChild(settingsPanel);

				const overlay = document.createElement("div");
				overlay.className = "bpp-overlay";
				document.body.appendChild(overlay);

				// 添加事件監聽器
				document
					.getElementById("bpp-toggle")
					.addEventListener("click", toggleSettings);
				document
					.getElementById("bpp-enable")
					.addEventListener("change", updateSettings);
				document
					.getElementById("bpp-nsfw")
					.addEventListener("change", updateSettings);

				// 點擊遮罩關閉設置面板
				overlay.addEventListener("click", closeSettings);

				console.log("圖片預覽設置 UI 已成功創建");
			} else {
				// Navbar 未找到,如果未達到最大嘗試次數,則再次嘗試
				attempts++;
				if (attempts < maxAttempts) {
					setTimeout(attemptCreate, delayBetweenAttempts);
				} else {
					console.log("無法找到 navbar,圖片預覽設置 UI 創建失敗");
				}
			}
		}

		// 開始第一次嘗試
		attemptCreate();
	}

	// 切換設置面板顯示
	function toggleSettings(event) {
		event.preventDefault();
		event.stopPropagation();
		const settingsPanel = document.getElementById("bpp-settings");
		const overlay = document.querySelector(".bpp-overlay");
		settingsPanel.classList.toggle("active");
		overlay.classList.toggle("active");
	}

	// 關閉設置面板
	function closeSettings() {
		const settingsPanel = document.getElementById("bpp-settings");
		const overlay = document.querySelector(".bpp-overlay");
		settingsPanel.classList.remove("active");
		overlay.classList.remove("active");
	}

	// 更新設置
	function updateSettings() {
		settings.enabled = document.getElementById("bpp-enable").checked;
		settings.showNSFW = document.getElementById("bpp-nsfw").checked;
		saveSettings(settings);
	}
	//#endregion

	//#region 照片盒子
	function createPhotoBox(imagesData) {
		const photoBox = document.createElement("div");
		photoBox.className = "bpp-photo-box";

		console.log("ssss", imagesData);

		let currentIndex = 0;
		const totalImages = imagesData.reduce(
			(sum, data) => sum + data.urls.length,
			0
		);

		const createPhotoContainer = () => {
			const currentData = imagesData.find((data, index) => {
				const prevCount = imagesData
					.slice(0, index)
					.reduce((sum, d) => sum + d.urls.length, 0);
				return (
					currentIndex >= prevCount &&
					currentIndex < prevCount + data.urls.length
				);
			});
			const currentImageIndex =
				currentIndex -
				imagesData
					.slice(0, imagesData.indexOf(currentData))
					.reduce((sum, d) => sum + d.urls.length, 0);

			return `
            <div class="bpp-photo-container">
                <img class="bpp-photo" src="${currentData.urls[currentImageIndex]}" alt="Preview">
                <div class="bpp-source-tag">${currentData.name}</div>
                <div class="bpp-nav bpp-nav-left">❮</div>
                <div class="bpp-nav bpp-nav-right">❯</div>
            </div>
        `;
		};

		const createInfoPanel = () => {
			const currentData = imagesData.find((data, index) => {
				const prevCount = imagesData
					.slice(0, index)
					.reduce((sum, d) => sum + d.urls.length, 0);
				return (
					currentIndex >= prevCount &&
					currentIndex < prevCount + data.urls.length
				);
			});
			const currentImageIndex =
				currentIndex -
				imagesData
					.slice(0, imagesData.indexOf(currentData))
					.reduce((sum, d) => sum + d.urls.length, 0);

			return `
            <div class="bpp-info">
                <span>${currentIndex + 1} / ${totalImages} - 標題: ${
				currentData.title
			}</span>
                <span>作者: <a href="${
									currentData.authorUrl
								}" target="_blank">${currentData.author}</a></span>
                <a href="${currentData.sourceUrl}" target="_blank">在 ${
				currentData.name
			} 上查看</a>
            </div>
        `;
		};

		const updatePhotoBox = () => {
			photoBox.innerHTML = createPhotoContainer() + createInfoPanel();
			setupNavigation();
		};

		const setupNavigation = () => {
			photoBox.querySelector(".bpp-nav-left").addEventListener("click", () => {
				currentIndex = (currentIndex - 1 + totalImages) % totalImages;
				updatePhotoBox();
			});

			photoBox.querySelector(".bpp-nav-right").addEventListener("click", () => {
				currentIndex = (currentIndex + 1) % totalImages;
				updatePhotoBox();
			});

			photoBox.querySelector(".bpp-photo").addEventListener("click", () => {
				const currentData = imagesData.find((data, index) => {
					const prevCount = imagesData
						.slice(0, index)
						.reduce((sum, d) => sum + d.urls.length, 0);
					return (
						currentIndex >= prevCount &&
						currentIndex < prevCount + data.urls.length
					);
				});
				const currentImageIndex =
					currentIndex -
					imagesData
						.slice(0, imagesData.indexOf(currentData))
						.reduce((sum, d) => sum + d.urls.length, 0);
				openFullscreen(currentData.urls[currentImageIndex]);
			});
		};

		updatePhotoBox();

		return photoBox;
	}

	function openFullscreen(imageUrl) {
		const fullscreen = document.createElement("div");
		fullscreen.className = "bpp-fullscreen";
		fullscreen.innerHTML = `
            <img class="bpp-fullscreen-image" src="${imageUrl}" alt="Fullscreen">
            <div class="bpp-close-fullscreen">×</div>
        `;

		fullscreen
			.querySelector(".bpp-close-fullscreen")
			.addEventListener("click", () => {
				document.body.removeChild(fullscreen);
			});

		document.body.appendChild(fullscreen);
	}
	//#endregion

	//#region handlers
	const siteHandlers = [
		{
			name: "pixiv",
			enabled: true,
			selector: 'a[href*="ref.gamer.com.tw/redir.php"]',
			extractId: (url) => {
				const decodedUrl = decodeURIComponent(url);
				const match = decodedUrl.match(/pixiv\.net\/artworks\/(\d+)/);
				return match ? match[1] : null;
			},
			fetchImage: (id) => {
				return new Promise((resolve, reject) => {
					GM_xmlhttpRequest({
						method: "GET",
						url: `https://www.pixiv.net/ajax/illust/${id}`,
						onload: function (response) {
							if (response.status === 200) {
								const data = JSON.parse(response.responseText);
								if (data.body) {
									const pageCount = data.body.pageCount || 1;
									const urls = [];
									for (let i = 0; i < pageCount; i++) {
										if (data.body.urls && data.body.urls.regular) {
											urls.push(
												data.body.urls.regular
													.replace("i.pximg.net", "pixiv.canaria.cc")
													.replace("_p0", `_p${i}`)
											);
										}
									}
									if (urls.length > 0) {
										resolve({
											name: "pixiv",
											urls: urls,
											sourceUrl: "https://www.pixiv.net/artworks/" + id,
											nsfw: data.body?.xRestrict > 0,
											title: data.body?.title,
											userId: data.body?.userId,
											author: data.body?.userName,
											authorUrl: `https://www.pixiv.net/users/${data.body?.userId}`,
										});
									} else {
										reject("Image URLs not found");
									}
								} else {
									reject("Invalid response data");
								}
							} else {
								reject("Failed to fetch image data");
							}
						},
						onerror: function (error) {
							reject(error);
						},
					});
				});
			},
		},
		{
			name: "twitter",
			enabled: true,
			selector: 'a[href*="ref.gamer.com.tw/redir.php"]',
			extractId: (url) => {
				const decodedUrl = decodeURIComponent(url);
				const match = decodedUrl.match(
					/(?:twitter\.com|x\.com)\/\w+\/status\/(\d+)/
				);
				return match ? match[1] : null;
			},
			fetchImage: (id) => {
				return new Promise((resolve, reject) => {
					GM_xmlhttpRequest({
						method: "GET",
						url: `https://api.fxtwitter.com/i/status/${id}`,
						onload: function (response) {
							if (response.status === 200) {
								try {
									const data = JSON.parse(response.responseText);
									if (
										data.tweet &&
										data.tweet.media &&
										data.tweet.media.all.length > 0
									) {
										const imageUrls = data.tweet.media.all
											.filter((media) => media.type === "photo")
											.map((photo) => photo.url);

										if (imageUrls.length > 0) {
											resolve({
												name: "twitter",
												urls: imageUrls,
												sourceUrl: data.tweet.url,
												nsfw: data.tweet.possibly_sensitive,
												title: data.tweet.text,
												userId: data.tweet.author.screen_name,
												author: data.tweet.author.name,
												authorUrl: `https://x.com/${data.tweet.author.screen_name}`,
											});
										} else {
											reject("No photos found in tweet");
										}
									} else {
										reject("No media found in tweet");
									}
								} catch (error) {
									reject("Error parsing tweet data: " + error.message);
								}
							} else {
								reject("Failed to fetch tweet data");
							}
						},
						onerror: function (error) {
							reject(error);
						},
					});
				});
			},
		},
		// 可以在這裡添加其他站點的處理器...
	];
	//#endregion

	function processLinks() {
		if (!settings.enabled) return;
		const posts = document.querySelectorAll(".c-post__body");
		posts.forEach((post) => {
			const imageDataPromises = [];
			siteHandlers.forEach((handler) => {
				if (handler.enabled) {
					if (post.dataset.processed) return;
					const links = post.querySelectorAll(handler.selector);

					links.forEach((link) => {
						if (!link.closest(".bpp-photo-box") && !link.dataset.processed) {
							const id = handler.extractId(link.href);
							if (id) {
								imageDataPromises.push(
									handler
										.fetchImage(id)
										.then((data) => {
											link.dataset.processed = "true";
											return data;
										})
										.catch((error) => {
											console.error(
												`Error fetching ${handler.name} image:`,
												error
											);
											return null;
										})
								);
							}
						}
					});
				}
			});
			if (imageDataPromises.length > 0) {
				Promise.all(imageDataPromises).then((imagesData) => {
					const validImagesData = imagesData.filter(
						(data) =>
							data !== null && (!data.nsfw || (data.nsfw && settings.showNSFW))
					);
					if (validImagesData.length > 0) {
						const photoBoxId = `bpp-photo-box-${
							post.id || Math.random().toString(36).substr(2, 9)
						}`;
						if (!post.querySelector(`#${photoBoxId}`)) {
							const photoBox = createPhotoBox(validImagesData);
							const buttonBar = post.querySelector(".c-post__body__buttonbar");
							if (buttonBar) {
								buttonBar.parentNode.insertBefore(photoBox, buttonBar);
							}
						}
					}
					post.dataset.processed = "true";
				});
			} else {
				// 如果沒有需要處理的鏈接,也標記這個 post 為已處理
				post.dataset.processed = "true";
			}
		});
	}
	// 初始化
	createUI();
	processLinks();

	// 使用防抖函數來限制 processLinks 的調用頻率
	function debounce(func, wait) {
		let timeout;
		return function (...args) {
			clearTimeout(timeout);
			timeout = setTimeout(() => func.apply(this, args), wait);
		};
	}

	// 監聽頁面變化
	const debouncedProcessLinks = debounce(processLinks, 1000);
	const observer = new MutationObserver(debouncedProcessLinks);
	observer.observe(document.body, { childList: true, subtree: true });
})();