您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
View all media (images+videos) from a 4chan thread in a masonry grid layout.
// ==UserScript== // @name 4chan-masonry // @namespace 0000xFFFF // @version 1.3.3 // @description View all media (images+videos) from a 4chan thread in a masonry grid layout. // @author 0000xFFFF // @license MIT // @match *://boards.4chan.org/*/thread/* // @match *://boards.4channel.org/*/thread/* // @grant GM_addStyle // @icon data:image/ico;base64,AAABAAEAEBAAAAEAIACaAAAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAQAAAAEAgGAAAAH/P/YQAAAGFJREFUeJxjYICA/0iYEMBQ+z/tjDEcEzAEp1piDCFOM7EGYBiCxW/EihH2KxFhg10zzCZSDMHQTKohFBtAkRdQQhtPIGI1CJ9CrAYjG0JxUqaOAcg0IQOwqf2PRuMDKGoBmcLsrWcgpCUAAAAASUVORK5CYII= // ==/UserScript== const GRID_ROWS_MIN = 1; const GRID_ROWS_MAX = 15; const GRID_ROWS_DEFAULT = 4; const CONCURRENT_LOADS_IMAGE = 3; const CONCURRENT_LOADS_VIDEO = 1; const LOAD_DELAY_IMAGE = 10; const LOAD_DELAY_VIDEO = 300; const PRELOAD_VIEWPORT_BUFFER = 200; // Load images within 200px of viewport function GM_addStyle(css) { const style = document.createElement("style"); style.textContent = css; (document.head || document.documentElement).appendChild(style); return style; } // Usage var MasonryCss = ` #fcm_overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.95); z-index: 10000; overflow: auto; box-sizing: border-box; } #fcm_topbar { position: fixed; top: 0; left: 0; right: 0; display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; background-color: rgba(0, 0, 0, 0.8); color: #fff; z-index: 999; transition: transform 0.3s ease-in-out; will-change: transform; backdrop-filter: blur(5px); } .fcm_slider_container { display: flex; align-items: center; gap: 15px; } .fcm_close { padding: 8px 12px; background: #d32f2f; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: bold; transition: all 0.2s ease; min-width: 40px; display: flex; align-items: center; justify-content: center; } .fcm_close:hover { background: #b71c1c; transform: scale(1.05); } #fcm_masonry { display: block; gap: 10px; margin-top: 90px; column-gap: 5px; } .fcm_shortcut_4chanx { margin: 0 0 0 5px; padding: 0; cursor: pointer; justify-content: center; } .fcm_shortcut_4chanx img { width: 16px; height: 13px; } .fcm_button_regular { padding: 12px 18px; display: flex; gap: 5px; background: #2d5016; color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: bold; box-shadow: 0 4px 15px rgba(0,0,0,0.3); transition: all 0.3s ease; white-space: nowrap; } .fcm_button_regular:hover { background: #4a7c21; transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.4); } .fcm_button_regular img { height: 15px; } .fcm_container { display: flex; margin: 15px 0 15px 0; } .fcm_grid_container { max-width: 97vw; margin: 0 auto; } .fcm_value_display { color: white; font-weight: bold; font-size: 16px; min-width: 20px; } .fcm_media_wrapper { position: relative; overflow: hidden; border-radius: 8px; cursor: pointer; transition: transform 0.1s ease, box-shadow 0.1s ease; } .fcm_media_wrapper:hover { transform: scale(1.05); box-shadow: 0 8px 25px rgba(0,0,0,0.6); z-index: 100; } .fcm_media_img, .fcm_media_thumb, .fcm_media_video { width: 100%; display: block; object-fit: cover; cursor: pointer; } .fcm_media_loading, .fcm_media_thumb { } .fcm_media_img { object-fit: contain; transition: opacity 0.3s ease; } .fcm_media_video { object-fit: contain; display: none; } .fcm_play_btn { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 50px; height: 50px; background: rgba(0,0,0,0.5); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; color: white; pointer-events: none; opacity: 0.4; } .fcm_tooltip { position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.8); color: white; padding: 8px; font-size: 12px; transform: translateY(100%); transition: transform 0.3s ease; word-break: break-all; } .fcm_media_wrapper:hover .fcm_tooltip { transform: translateY(0); } `; GM_addStyle(MasonryCss); const userscript_icon_1 = "data:image/ico;base64,AAABAAEAEBAAAAEAIACFAAAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAQAAAAEAgGAAAAH/P/YQAAAExJREFUeJxjYBhM4D8SJlnN/7QzxnCMwxCCavApIE0zIQNwGoLFj9jYhMIJ01ZiNVDNAEJeIMtWolxDUTRSzwBkmkg5VEPQaGLlyAcAWwCk9UAWSQAAAAAASUVORK5CYII=" const userscript_icon_2 = "data:image/ico;base64,AAABAAEAEBAAAAEAIACaAAAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAQAAAAEAgGAAAAH/P/YQAAAGFJREFUeJxjYICA/0iYEMBQ+z/tjDEcEzAEp1piDCFOM7EGYBiCxW/EihH2KxFhg10zzCZSDMHQTKohFBtAkRdQQhtPIGI1CJ9CrAYjG0JxUqaOAcg0IQOwqf2PRuMDKGoBmcLsrWcgpCUAAAAASUVORK5CYII="; const userscript_icon_3 = "data:image/ico;base64,AAABAAEACggAAAEAIABkAAAAFgAAAIlQTkcNChoKAAAADUlIRFIAAAAKAAAACAgGAAAAwPputgAAACtJREFUeJxjYEAF/9EwVvA/7YwxCsamGF0Sq2IMRbgUE62QaKtJ8gzB4AEA82hZQXIdFlEAAAAASUVORK5CYII=" let gridRows = GRID_ROWS_DEFAULT; let isGridOpen = false; let gridOverlay = null; let loadQueue = []; let activeLoadsImage = 0; let activeLoadsVideo = 0; let preloadCache = new Map(); // Cache to avoid duplicate requests // Rate limiting function function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Check if element is near viewport function isNearViewport(element, buffer = PRELOAD_VIEWPORT_BUFFER) { const rect = element.getBoundingClientRect(); const windowHeight = window.innerHeight; const windowWidth = window.innerWidth; return ( rect.bottom >= -buffer && rect.top <= windowHeight + buffer && rect.right >= -buffer && rect.left <= windowWidth + buffer ); } // Queue-based image preloader async function preloadMedia(url, priority = 'low', type = 'image') { if (preloadCache.has(url)) { return preloadCache.get(url); } return new Promise((resolve, reject) => { loadQueue.push({ url, resolve, reject, priority, type }); processLoadQueue(); }); } // Process the load queue with rate limiting async function processLoadQueue() { if ((activeLoadsImage >= CONCURRENT_LOADS_IMAGE || activeLoadsVideo >= CONCURRENT_LOADS_VIDEO) || loadQueue.length === 0) { return; } // Sort by priority (high priority first) loadQueue.sort((a, b) => { const priorityOrder = { 'high': 3, 'medium': 2, 'low': 1 }; return priorityOrder[b.priority] - priorityOrder[a.priority]; }); const item = loadQueue.shift(); let delayMs = 0; if (item.type === 'image') { activeLoadsImage++; delayMs = LOAD_DELAY_IMAGE; } else { activeLoadsVideo++; delayMs = LOAD_DELAY_VIDEO; } try { await delay(delayMs); // Rate limiting delay const result = await loadMedia(item.url, item.type); preloadCache.set(item.url, result); item.resolve(result); } catch (error) { console.warn(`Failed to load ${item.url}:`, error); item.reject(error); } finally { if (item.type === 'image') { activeLoadsImage--; } else { activeLoadsVideo--; } // Process next item in queue setTimeout(processLoadQueue, 50); } } function updateQueuePriority(url, newPriority) { const item = loadQueue.find(i => i.url === url); if (item) { item.priority = newPriority; } } function updatePriorities() { document.querySelectorAll('.fcm_media_img').forEach(img => { const url = img.dataset.fullUrl; if (!url) return; // Already loaded if (preloadCache.has(url)) return; // Already done const item = loadQueue.find(i => i.url === url); if (item) { item.priority = isNearViewport(img) ? 'medium' : 'low'; } }); } function throttle(fn, delay) { let lastCall = 0; let timeout; return function (...args) { const now = Date.now(); if (now - lastCall < delay) { clearTimeout(timeout); timeout = setTimeout(() => { lastCall = Date.now(); fn.apply(this, args); }, delay - (now - lastCall)); } else { lastCall = now; fn.apply(this, args); } }; } const throttledUpdatePriorities = throttle(updatePriorities, 800); window.addEventListener('scroll', throttledUpdatePriorities); window.addEventListener('resize', throttledUpdatePriorities); // Actual media loading function function loadMedia(url, type) { return new Promise((resolve, reject) => { if (type === 'image') { const img = new Image(); const timeout = setTimeout(() => { reject(new Error('Image load timeout')); }, 10000); // 10 second timeout img.onload = () => { clearTimeout(timeout); resolve(img); }; img.onerror = () => { clearTimeout(timeout); reject(new Error('Image load failed')); }; img.src = url; } else if (type === 'video') { const video = document.createElement('video'); const timeout = setTimeout(() => { reject(new Error('Video load timeout')); }, 15000); // 15 second timeout for videos video.addEventListener('loadeddata', () => { clearTimeout(timeout); resolve(video); }); video.addEventListener('error', () => { clearTimeout(timeout); reject(new Error('Video load failed')); }); video.src = url; video.preload = 'metadata'; // Only load metadata initially } }); } // Intersection Observer for lazy loading const imageObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; const fullUrl = img.dataset.fullUrl; if (fullUrl && !img.dataset.loading) { img.dataset.loading = 'true'; preloadMedia(fullUrl, isNearViewport(img) ? 'medium' : 'low').then((fullImg) => { img.style.opacity = '0'; setTimeout(() => { img.src = fullImg.src; img.style.opacity = '1'; img.removeAttribute('data-full-url'); img.removeAttribute('data-loading'); img.classList.remove("fcm_media_loading"); }, 200); }).catch(() => { img.removeAttribute('data-loading'); img.classList.remove("fcm_media_loading"); }); } } }); }, { rootMargin: '200px' // Start loading 200px before entering viewport }); function createOptimizedVideoElement(mediaData, thumbImg, playBtn, mediaWrapper) { let video = null; let videoLoaded = false; let hoverTimeout = null; let isHovering = false; const createVideo = () => { if (!video) { video = document.createElement('video'); video.className = "fcm_media_video"; video.loop = true; video.muted = true; video.playsInline = true; video.style.display = 'none'; video.addEventListener('canplay', () => { videoLoaded = true; if (isNearViewport(video) && !video.controls) { showVideo(); } }); video.addEventListener('loadstart', () => { console.log('Video loading started:', mediaData.url); }); mediaWrapper.appendChild(video); } return video; }; const showVideo = () => { if (video && videoLoaded) { thumbImg.style.display = 'none'; playBtn.style.display = 'none'; video.style.display = 'block'; video.play().catch(() => { }); } }; const hideVideo = () => { if (video && !video.controls) { video.pause(); video.style.display = 'none'; thumbImg.style.display = 'block'; playBtn.style.display = 'flex'; } }; // Hover enter - start loading after delay mediaWrapper.addEventListener('mouseenter', () => { isHovering = true; clearTimeout(hoverTimeout); hoverTimeout = setTimeout(() => { if (isHovering) { // Double-check still hovering const vid = createVideo(); if (videoLoaded) { // Video already loaded, show immediately showVideo(); } else if (!vid.src) { // Queue the video load instead of hitting server immediately preloadMedia(mediaData.url, 'high', 'video') .then((videoEl) => { vid.src = mediaData.url; }) .catch(() => { console.warn("Failed to queue video load:", mediaData.url); }); } // If video is loading, showVideo() will be called from 'canplay' event } }, 100); // Reduced delay for better responsiveness }); // Hover leave - hide video and cancel loading if needed mediaWrapper.addEventListener('mouseleave', () => { isHovering = false; clearTimeout(hoverTimeout); //hideVideo(); }); // Click handler - permanent video with controls mediaWrapper.addEventListener('click', (e) => { if (!video || video.controls) return; // Already clicked or no video e.preventDefault(); const vid = createVideo(); // Set up for permanent display vid.muted = false; vid.controls = true; vid.style.display = 'block'; thumbImg.style.display = 'none'; playBtn.style.display = 'none'; if (!vid.src) { vid.src = mediaData.url; } // Play when ready if (videoLoaded) { vid.play().catch(() => { }); } else { vid.addEventListener('canplay', () => { vid.play().catch(() => { }); }, { once: true }); } }); } // Replace the image creation part in createMasonryGrid function function createOptimizedMediaElement(mediaData) { const mediaWrapper = document.createElement('div'); mediaWrapper.className = "fcm_media_wrapper"; if (mediaData.isVideo) { // Thumbnail image const thumbImg = document.createElement('img'); thumbImg.src = mediaData.thumbnail; thumbImg.className = "fcm_media_thumb"; thumbImg.loading = 'lazy'; mediaWrapper.appendChild(thumbImg); // Play button overlay const playBtn = document.createElement('div'); playBtn.className = "fcm_play_btn"; playBtn.innerHTML = '►'; mediaWrapper.appendChild(playBtn); // Use optimized video creation createOptimizedVideoElement(mediaData, thumbImg, playBtn, mediaWrapper); } else { const img = document.createElement('img'); img.className = "fcm_media_img fcm_media_loading"; img.src = mediaData.thumbnail; img.loading = 'lazy'; img.dataset.fullUrl = mediaData.url; mediaWrapper.addEventListener('mouseenter', () => { updateQueuePriority(mediaData.url, 'high'); }); // Use Intersection Observer for lazy loading imageObserver.observe(img); mediaWrapper.appendChild(img); } // Filename tooltip const tooltip = document.createElement('div'); tooltip.textContent = mediaData.originalName; tooltip.className = "fcm_tooltip"; // Middle click handler mediaWrapper.addEventListener('mousedown', (event) => { if (event.button === 1) { window.open(mediaData.url, '_blank'); } }); mediaWrapper.appendChild(tooltip); return mediaWrapper; } // Add cleanup function for when grid is closed function cleanupPreloading() { // Cancel pending loads loadQueue.forEach(item => { item.reject(new Error('Cleanup cancelled')); }); loadQueue = []; activeLoads = 0; // this is handles with refresh // // Clear cache periodically to prevent memory leaks // if (preloadCache.size > 100) { // preloadCache.clear(); // } // Disconnect observer imageObserver.disconnect(); } function initUI() { const button = document.createElement('span'); button.title = "Masonry Grid"; button.addEventListener('click', function(e) { e.preventDefault(); openGrid(); }); const forchanX_header = document.getElementById("header-bar") if (forchanX_header) { // if 4chan X is detected const element = document.getElementById("shortcut-watcher"); button.id = "shortcut-masonry"; button.className = "shortcut brackets-wrap fcm_shortcut_4chanx"; const img = document.createElement("img"); img.src = userscript_icon_1; button.appendChild(img); img.addEventListener('mouseover', () => { if (!img.classList.contains('clicked')) { img.src = userscript_icon_2; } }); img.addEventListener('mouseout', () => { if (!img.classList.contains('clicked')) { img.src = userscript_icon_1; } }); img.addEventListener('mousedown', () => { img.classList.add('pressed'); img.src = userscript_icon_3; }); img.addEventListener('mouseup', () => { img.classList.remove('pressed'); if (img.matches(':hover')) { img.src = userscript_icon_2; } else { img.src = userscript_icon_1; } }); // also handle case where mouse is released outside the image document.addEventListener('mouseup', () => { if (img.classList.contains('pressed')) { img.classList.remove('pressed'); if (img.matches(':hover')) { img.src = 'icon2.png'; } else { img.src = 'icon1.png'; } } }); forchanX_header.appendChild(button); element.parentElement.insertBefore(button, element); } else { button.className = "fcm_button_regular"; const img = document.createElement("img"); img.src = userscript_icon_3; button.appendChild(img); const span = document.createElement("span"); span.innerHTML = "Masonry Grid"; button.appendChild(span); const containerDiv = document.createElement('div'); containerDiv.id = "4chan_grid_cont"; containerDiv.className = "fcm_container"; containerDiv.appendChild(button); const threadElement = document.querySelector(".thread"); threadElement.parentElement.insertBefore(containerDiv, threadElement); } findMediaLinks(button); } function findMediaLinks(button = null) { const mediaLinks = []; const files = document.querySelectorAll('div.file'); files.forEach((fileDiv, index) => { const fileText = fileDiv.querySelector('.fileText'); const fileThumb = fileDiv.querySelector('.fileThumb'); const fileThumbImage = fileThumb.querySelector("img"); let link = null; if (fileText) { link = fileText.querySelector('a'); } if (link && link.href) { const url = link.href.startsWith('//') ? 'https:' + link.href : link.href; const isImage = /\.(jpg|jpeg|png|gif|webp|bmp|svg)(\?|$)/i.test(url); const isVideo = /\.(mp4|webm|mkv|avi|mov)(\?|$)/i.test(url); if (isImage || isVideo) { const postId = url.split('/').pop().split('?')[0]; let originalName = link.title.trim() || link.textContent.trim() || postId; // if 4chan-X is used fix the name fetching const fnfull = link.querySelector('.fnfull'); if (fnfull) { originalName = fnfull.textContent.trim(); } // get file info text const fileInfo = fileDiv.querySelector(".file-info"); let width = null; let height = null; if (fileInfo) { const fileInfoClone = fileInfo.cloneNode(true); fileInfoClone.querySelectorAll("a").forEach(a => a.remove()); const info = fileInfoClone.textContent.trim(); const size = info.split(", ")[1].replace(")", ""); const width_and_height = size.split("x"); width = width_and_height[0]; height = width_and_height[1]; } const newElement = { url: url, originalName: originalName, postId: postId, index: index + 1, isVideo: isVideo, thumbnail: (fileThumbImage.src), width: width, height: height }; mediaLinks.push(newElement); } } }); if (button) { button.title = `Masonry Grid (${mediaLinks.length})`; } return mediaLinks; } function findMediaLinksFromImgAndVideoElements() { const mediaLinks = [] const imgElements = document.querySelectorAll('img[src*="jpg"], img[src*="jpeg"], img[src*="png"], img[src*="gif"], img[src*="webp"], img[src*="bmp"]'); const videoElements = document.querySelectorAll('video[src*="mp4"], video[src*="webm"], video[src*="mkv"], video[src*="avi"], video[src*="mov"]'); const mediaElements = [...imgElements, ...videoElements]; mediaElements.forEach((img_or_vid, index) => { const url = img_or_vid.src; const filename = url.split('/').pop().split('?')[0]; const isVideo = /\.(mp4|webm|mkv|avi|mov)(\?|$)/i.test(url); mediaLinks.push({ url: url, originalName: filename, postId: filename, index: index + 1, isVideo: isVideo }); }); return mediaLinks; } function updateMasonryGrid() { const gridContainer = document.getElementById('fcm_masonry'); if (gridContainer) { gridContainer.style.columnCount = gridRows; } } function createTopBar() { const topbar = document.createElement("div"); topbar.id = "fcm_topbar"; const sliderContainer = document.createElement('div'); sliderContainer.className = "fcm_slider_container"; const slider = document.createElement('input'); slider.type = 'range'; slider.id = "setting_slider_cols"; slider.min = GRID_ROWS_MIN; slider.max = GRID_ROWS_MAX; slider.step = "1"; slider.value = gridRows; const valueDisplay = document.createElement('span'); valueDisplay.textContent = gridRows; valueDisplay.className = "fcm_value_display"; slider.addEventListener('input', (e) => { gridRows = parseInt(e.target.value); valueDisplay.textContent = gridRows; updateMasonryGrid(); }); function slider_left() { slider.value = Math.max(Number(slider.value) - Number(slider.step), slider.min); } function slider_right() { slider.value = Math.min(Number(slider.value) + Number(slider.step), slider.max); } slider.addEventListener('keydown', (event) => { switch (event.key) { case 'ArrowLeft': slider_left(); break; case 'ArrowRight': slider_right(); break; } }); slider.focus(); slider.select(); sliderContainer.appendChild(slider); sliderContainer.appendChild(valueDisplay); topbar.appendChild(sliderContainer); // Close button const closeButton = document.createElement('button'); closeButton.className = "fcm_close"; closeButton.innerHTML = '✕'; closeButton.addEventListener('click', closeGrid); topbar.appendChild(closeButton); return topbar; } function createMasonryGrid(mediaLinks) { const overlay = document.createElement('div'); overlay.id = 'fcm_overlay'; const topbar = createTopBar(); let lastScroll = 0; overlay.addEventListener("scroll", () => { const currentScroll = overlay.scrollTop; if (currentScroll > lastScroll) { topbar.style.transform = "translateY(-100%)"; } else { topbar.style.transform = "translateY(0)"; } lastScroll = currentScroll; }); overlay.appendChild(topbar); const container = document.createElement('div'); container.className = "fcm_grid_container"; const handleParentClick = (event) => { event.preventDefault(); if (event.target === event.currentTarget) { closeGrid(); } }; overlay.addEventListener('click', handleParentClick); container.addEventListener('click', handleParentClick); topbar.addEventListener('click', handleParentClick); // Grid container const gridContainer = document.createElement('div'); gridContainer.id = 'fcm_masonry'; gridContainer.style.columnCount = gridRows; // Create image elements mediaLinks.forEach((mediaData, index) => { const mediaWrapper = createOptimizedMediaElement(mediaData); gridContainer.appendChild(mediaWrapper); }); container.appendChild(gridContainer); overlay.appendChild(container); return overlay; } function openGrid() { if (isGridOpen) return; const mediaLinks = findMediaLinks(); if (mediaLinks.length === 0) { alert('No media found on this page!'); return; } gridOverlay = createMasonryGrid(mediaLinks); document.body.appendChild(gridOverlay); isGridOpen = true; // Prevent body scrolling document.body.style.overflow = 'hidden'; // Close on escape key document.addEventListener('keydown', handleEscapeKey); } function closeGrid() { if (!isGridOpen || !gridOverlay) return; cleanupPreloading(); document.body.removeChild(gridOverlay); gridOverlay = null; isGridOpen = false; document.body.style.overflow = 'auto'; document.removeEventListener('keydown', handleEscapeKey); } function handleEscapeKey(e) { if (e.key === 'Escape') { closeGrid(); } } async function init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); return; } setTimeout(async () => { try { initUI(); } catch (error) { console.error('Error initializing userscript:', error); } }, 500); } init();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址