您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
将 jpnkn 论坛帖子转换为类似 Reddit 的嵌套楼中楼视图,支持图片预览懒加载、处理失效图片链接以及嵌入 YouTube 视频。
当前为
// ==UserScript== // @name jpnkn to Reddit Style (Optimized) // @name:en jpnkn to Reddit Style (Optimized) // @name:ja jpnkn を Reddit 風に (最適化版) // @name:ko jpnkn Reddit 스타일로 (최적화됨) // @name:zh-CN jpnkn 论坛转 Reddit 风格 (优化版) // @name:zh-TW jpnkn 論壇轉 Reddit 風格 (優化版) // @license MIT // @namespace http://tampermonkey.net/ // @version 0.9.1 // @description Transforms jpnkn threads into a Reddit-like nested view, lazy loads image previews, handles broken images, and embeds YouTube videos. // @description:en Transforms jpnkn threads into a Reddit-like nested view, lazy loads image previews, handles broken images, and embeds YouTube videos. // @description:ja jpnknのスレッドをRedditのようなネスト表示に変換し、画像プレビューを遅延読み込みし、壊れた画像を処理し、YouTube動画を埋め込みます。 // @description:ko jpnkn 스레드를 Reddit과 유사한 중첩 보기로 변환하고, 이미지 미리보기를 지연 로드하며, 깨진 이미지를 처리하고, YouTube 비디오를 삽입합니다. // @description:zh-CN 将 jpnkn 论坛帖子转换为类似 Reddit 的嵌套楼中楼视图,支持图片预览懒加载、处理失效图片链接以及嵌入 YouTube 视频。 // @description:zh-TW 將 jpnkn 論壇帖子轉換為類似 Reddit 的巢狀樓中樓檢視,支援圖片預覽延遲載入、處理失效圖片連結以及嵌入 YouTube 影片。 // @author NBXX // @match https://bbs.jpnkn.com/test/read.cgi/*/*/* // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // --- Configuration --- const MAX_PREVIEW_HEIGHT = '400px'; const INDENTATION_SIZE = 20; const SHOW_PREVIEWS_BY_DEFAULT = true; // For images (videos will have a button) const LAZY_LOAD_OFFSET = '200px'; // Load images when they are 200px away from viewport GM_addStyle(` .reddit-style-container { padding: 10px; } .reddit-post { border: 1px solid #ccc; border-radius: 4px; margin-bottom: 8px; background-color: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.05); } .reddit-post > .original-post-content { padding: 8px; } .reddit-post > .original-post-content dt { font-size: 0.9em; color: #555; } .reddit-post > .original-post-content dd { margin-left: 1.5em; font-size: 1em; line-height: 1.4; } .replies-wrapper { margin-left: ${INDENTATION_SIZE}px; padding-left: 10px; border-left: 2px solid #e0e0e0; margin-top: 5px; } .media-preview-container { margin-top: 8px; } .media-preview-container img { max-width: 100%; max-height: ${MAX_PREVIEW_HEIGHT}; display: block; border: 1px solid #ddd; border-radius: 3px; background-color: #f9f9f9; cursor: zoom-in; min-height: 50px; /* Placeholder height before loading */ } .media-preview-container img.expanded { max-height: none; cursor: zoom-out; } .media-toggle-btn, .video-toggle-btn { font-size: 0.8em; color: #007bff; cursor: pointer; margin-left: 5px; text-decoration: underline; display: inline-block; /* Keep it on the same line */ } .toggle-replies-btn { cursor: pointer; color: #777; font-size: 0.8em; margin-left: 10px; } .original-link.broken-link { text-decoration: line-through; color: #d9534f; /* Bootstrap's danger color */ } .youtube-embed-container { margin-top: 8px; position: relative; padding-bottom: 56.25%; /* 16:9 aspect ratio */ height: 0; overflow: hidden; max-width: 100%; background: #000; } .youtube-embed-container iframe { position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0; } `); let imageObserver; function initializeImageObserver() { if (imageObserver) { imageObserver.disconnect(); } imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; const src = img.dataset.src; if (src) { img.src = src; img.removeAttribute('data-src'); // No need to load again } observer.unobserve(img); } }); }, { rootMargin: LAZY_LOAD_OFFSET }); } function isImageLink(url) { if (!url) return false; try { const path = new URL(url).pathname.toLowerCase(); return /\.(jpeg|jpg|gif|png|webp)$/.test(path); } catch (e) { return false; } } function getYouTubeVideoId(url) { if (!url) return null; try { const parsedUrl = new URL(url); let videoId = null; if (parsedUrl.hostname === 'youtu.be') { videoId = parsedUrl.pathname.slice(1); } else if (parsedUrl.hostname.includes('youtube.com') && parsedUrl.pathname === '/watch') { videoId = parsedUrl.searchParams.get('v'); } else if (parsedUrl.hostname.includes('youtube.com') && parsedUrl.pathname.startsWith('/embed/')) { videoId = parsedUrl.pathname.split('/embed/')[1].split('?')[0]; } // Basic check for valid ID format if (videoId && /^[a-zA-Z0-9_-]{11}$/.test(videoId)) { return videoId; } return null; } catch (e) { return null; } } function processPostContent(ddElement) { const links = ddElement.querySelectorAll('a'); links.forEach(link => { const href = link.href; // 1. Image Previews (Lazy Loaded) if (isImageLink(href)) { link.classList.add('original-link'); const container = document.createElement('div'); container.className = 'media-preview-container'; container.style.display = SHOW_PREVIEWS_BY_DEFAULT ? 'block' : 'none'; const img = document.createElement('img'); img.dataset.src = href; // Store real src in data attribute for lazy loading img.alt = 'Image Preview'; // img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; // 1x1 transparent gif img.addEventListener('click', () => img.classList.toggle('expanded')); img.onerror = () => { container.style.display = 'none'; // Hide preview container link.classList.add('broken-link'); link.title = '画像読み込み失敗'; if (toggleBtn) toggleBtn.style.display = 'none'; // Hide toggle if it exists }; const toggleBtn = document.createElement('span'); toggleBtn.className = 'media-toggle-btn'; toggleBtn.textContent = SHOW_PREVIEWS_BY_DEFAULT ? '[画像を隠す]' : '[画像を表示]'; toggleBtn.addEventListener('click', (e) => { e.preventDefault(); const isVisible = container.style.display !== 'none'; container.style.display = isVisible ? 'none' : 'block'; toggleBtn.textContent = isVisible ? '[画像を表示]' : '[画像を隠す]'; }); container.appendChild(img); // Insert elements: toggle after link, container after that or parent link.insertAdjacentElement('afterend', toggleBtn); let insertTarget = link.nextSibling; // The toggle button if(insertTarget) { insertTarget.insertAdjacentElement('afterend', container); } else { link.parentElement.appendChild(container); } if (SHOW_PREVIEWS_BY_DEFAULT) { // Only observe if initially visible imageObserver.observe(img); } else { // If not shown by default, only observe when user clicks "show" toggleBtn.addEventListener('click', () => { if (container.style.display !== 'none' && img.dataset.src) { imageObserver.observe(img); } }, { once: true }); // Only need to set this up once } } // End Image Preview // 2. YouTube Embeds else { const videoId = getYouTubeVideoId(href); if (videoId) { link.classList.add('original-link'); const videoContainer = document.createElement('div'); // videoContainer.className = 'youtube-embed-container'; // Apply this when iframe is added videoContainer.style.display = 'none'; // Initially hidden const toggleVideoBtn = document.createElement('span'); toggleVideoBtn.className = 'video-toggle-btn'; toggleVideoBtn.textContent = '[動画を再生]'; toggleVideoBtn.addEventListener('click', (e) => { e.preventDefault(); if (videoContainer.style.display === 'none') { // Create iframe on demand if (!videoContainer.querySelector('iframe')) { videoContainer.innerHTML = ''; // Clear previous content if any videoContainer.className = 'youtube-embed-container'; // Set class for aspect ratio const iframe = document.createElement('iframe'); iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1`; // Added autoplay iframe.setAttribute('frameborder', '0'); iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'); iframe.setAttribute('allowfullscreen', ''); videoContainer.appendChild(iframe); } videoContainer.style.display = 'block'; toggleVideoBtn.textContent = '[動画を隠す]'; } else { videoContainer.style.display = 'none'; toggleVideoBtn.textContent = '[動画を再生]'; // Optional: videoContainer.innerHTML = ''; // To stop video and free resources } }); link.insertAdjacentElement('afterend', toggleVideoBtn); let insertTarget = link.nextSibling; // The toggle button if(insertTarget) { insertTarget.insertAdjacentElement('afterend', videoContainer); } else { link.parentElement.appendChild(videoContainer); } } // End YouTube } }); } function transformThread() { const threadElement = document.getElementById('thread'); if (!threadElement || threadElement.dataset.transformed === 'true') { // console.log("5ch Reddit Style: Thread element not found or already transformed."); return; } const originalPosts = Array.from(threadElement.querySelectorAll('div.res')); if (originalPosts.length === 0) { // console.log("5ch Reddit Style: No posts found to transform."); return; } console.log(`5ch Reddit Style: Found ${originalPosts.length} posts to process.`); initializeImageObserver(); // Initialize or re-initialize observer const postsData = new Map(); originalPosts.forEach(postEl => { const resIndex = postEl.dataset.resIndex; if (!resIndex) return; const dtElement = postEl.querySelector('dt.info'); const ddElement = postEl.querySelector('dd'); if (!dtElement || !ddElement) return; postsData.set(resIndex, { id: resIndex, dtElement: dtElement.cloneNode(true), ddElement: ddElement.cloneNode(true), children: [], replyToIds: [] }); }); postsData.forEach(post => { const replyLinks = post.ddElement.querySelectorAll('a'); replyLinks.forEach(link => { const href = link.getAttribute('href'); if (href) { const parts = href.split('/'); const targetPart = parts[parts.length - 1]; if (targetPart) { const match = targetPart.match(/^(\d+)/); if (match && match[1]) { const targetId = match[1]; if (postsData.has(targetId) && targetId !== post.id) { post.replyToIds.push(targetId); } } } } }); if (post.replyToIds.length > 0) { const parentId = post.replyToIds[0]; if (postsData.has(parentId)) { postsData.get(parentId).children.push(post); } } }); const newThreadContainer = document.createElement('div'); newThreadContainer.className = 'reddit-style-container'; function renderPostRecursive(post, parentDomElement) { const postWrapper = document.createElement('div'); postWrapper.className = 'reddit-post'; postWrapper.dataset.postId = post.id; const originalContentDiv = document.createElement('div'); originalContentDiv.className = 'original-post-content'; originalContentDiv.appendChild(post.dtElement); originalContentDiv.appendChild(post.ddElement); postWrapper.appendChild(originalContentDiv); processPostContent(post.ddElement); // Process for images/videos on the cloned dd parentDomElement.appendChild(postWrapper); if (post.children.length > 0) { const repliesWrapper = document.createElement('div'); repliesWrapper.className = 'replies-wrapper'; const toggleRepliesBtn = document.createElement('span'); toggleRepliesBtn.className = 'toggle-replies-btn'; toggleRepliesBtn.textContent = `[-] (${post.children.length} replies)`; let repliesVisible = true; toggleRepliesBtn.addEventListener('click', () => { repliesVisible = !repliesVisible; repliesWrapper.style.display = repliesVisible ? 'block' : 'none'; toggleRepliesBtn.textContent = repliesVisible ? `[-] (${post.children.length} replies)` : `[+] (${post.children.length} replies)`; }); post.dtElement.appendChild(toggleRepliesBtn); postWrapper.appendChild(repliesWrapper); post.children.sort((a, b) => parseInt(a.id) - parseInt(b.id)); post.children.forEach(childPost => { renderPostRecursive(childPost, repliesWrapper); }); } } const rootPosts = []; postsData.forEach(p => { let isChild = false; if (p.replyToIds.length > 0) { const parentId = p.replyToIds[0]; if (postsData.has(parentId) && postsData.get(parentId).children.includes(p)) { isChild = true; } } if (!isChild) { rootPosts.push(p); } }); rootPosts.sort((a, b) => parseInt(a.id) - parseInt(b.id)); rootPosts.forEach(post => renderPostRecursive(post, newThreadContainer)); threadElement.innerHTML = ''; threadElement.appendChild(newThreadContainer); threadElement.dataset.transformed = 'true'; // Mark as transformed console.log("5ch Reddit Style: Transformation complete."); } const observerTarget = document.getElementById('thread'); if (observerTarget) { let transformTimeout = null; const mainObserver = new MutationObserver((mutationsList, obs) => { if (observerTarget.dataset.transformed === 'true') { // If content is already transformed, we might want to handle new posts differently // For now, we'll just prevent re-transforming the whole thing. // A more advanced version would append new posts into the existing tree. // For example, if MQTT adds new .res elements, they should be processed and appended. // This basic script doesn't handle that incremental update gracefully yet. return; } for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { let hasResAdded = false; for(const node of mutation.addedNodes){ if(node.nodeType === Node.ELEMENT_NODE && node.classList && node.classList.contains('res')){ hasResAdded = true; break; } } if(hasResAdded){ clearTimeout(transformTimeout); transformTimeout = setTimeout(() => { console.log("5ch Reddit Style: Detected content change, attempting transformation."); transformThread(); }, 500); // Shortened debounce return; } } } }); mainObserver.observe(observerTarget, { childList: true, subtree: false }); // Fallback: setTimeout(() => { if (observerTarget.dataset.transformed !== 'true') { console.log("5ch Reddit Style: Fallback timeout, attempting transformation."); transformThread(); } }, 1500); } else { console.error("5ch Reddit Style: #thread element not found for MutationObserver."); } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址