您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
sounds player script for 4chan archive websites
// ==UserScript== // @name FoolFuuka Video Player + External Sounds // @namespace kwlNjR37xBCMkr76P5eKA88apmOClCfZ // @version 0004 // @description sounds player script for 4chan archive websites // @author soundboy_1459944 // @website https://greasyfork.org/en/scripts/546929 // @match *://b4k.co/* // @match *://b4k.dev/* // @match *://arch.b4k.co/* // @match *://arch.b4k.dev/* // @match *://desuarchive.org/* // @connect 4chan.org // @connect 4channel.org // @connect a.4cdn.org // @connect 8chan.moe // @connect 8chan.se // @connect desu-usergeneratedcontent.xyz // @connect arch-img.b4k.co // @connect archive-media-0.nyafuu.org // @connect 4cdn.org // @connect a.pomf.cat // @connect pomf.cat // @connect litter.catbox.moe // @connect files.catbox.moe // @connect catbox.moe // @connect share.dmca.gripe // @connect z.zz.ht // @connect z.zz.fo // @connect zz.ht // @connect too.lewd.se // @connect lewd.se // @connect b4k.co // @connect b4k.dev // @connect arch.b4k.co // @connect arch.b4k.dev // @connect desuarchive.org // @connect * // @license CC0 1.0 // @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgdmlld0JveD0iMCAwIDI0IDI0Ij4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmNscy0xIHsKICAgICAgICBmaWxsOiAjMGExZTFlOwogICAgICB9CgogICAgICAuY2xzLTIgewogICAgICAgIGZpbGw6IG5vbmU7CiAgICAgICAgc3Ryb2tlOiAjZmZmOwogICAgICAgIHN0cm9rZS1taXRlcmxpbWl0OiAxMDsKICAgICAgICBzdHJva2Utd2lkdGg6IDNweDsKICAgICAgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI4LjcuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDEuMi4wIEJ1aWxkIDE0MikgIC0tPgogIDxnPgogICAgPGcgaWQ9IkxheWVyXzEiPgogICAgICA8cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0yMC40LDBIMy42QzEuNiwwLDAsMS42LDAsMy42djE2LjhjMCwyLDEuNiwzLjYsMy42LDMuNmgxNi44YzIsMCwzLjYtMS42LDMuNi0zLjZWMy42YzAtMi0xLjYtMy42LTMuNi0zLjZaIi8+CiAgICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTUsNHYxNmMwLC41NTIuNDQ3LDEsMSwxLC4xODUsMCwuMzY3LS4wNTEuNTI0LS4xNDhsMTMtOGMuNDcxLS4yODkuNjE4LS45MDUuMzI4LTEuMzc2LS4wODItLjEzNC0uMTk1LS4yNDYtLjMyOC0uMzI4TDYuNTI0LDMuMTQ4Yy0uNDctLjI4OS0xLjA4Ni0uMTQzLTEuMzc2LjMyOC0uMDk3LjE1OC0uMTQ4LjMzOS0uMTQ4LjUyNFoiLz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPg== // ==/UserScript== const MEDIA_INITIAL_WIDTH = 350; const MEDIA_INITIAL_HEIGHT = 350; const DURATION_MATCH_TOLERANCE = 2; // seconds const SUPPORTED_VIDEO_EXTS = ['.webm', '.mp4']; const SUPPORTED_IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.gif']; (function () { 'use strict'; // Store all media items for the list const mediaItems = []; function createElement(html, parent, events = {}) { const container = document.createElement('div'); container.innerHTML = html; const el = container.children[0]; parent && parent.appendChild(el); for (let event in events) { el.addEventListener(event, events[event]); } return el; }; const div = createElement(`<div class="post_wrapper"></div>`, document.body); const style_post_wrapper = document.defaultView.getComputedStyle(div); const div2 = createElement(`<div class="letters"></div>`, document.body); const style_navbar = document.defaultView.getComputedStyle(div2); // CSS Styles const styles = ` .draggable-window { position: fixed; z-index: 1; width: 400px; height: 400px; min-width: 230px; min-height: 150px; background-color: ${style_post_wrapper.background}; border: 1px solid gray; border-radius: 5px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); overflow: hidden; resize: both; display: flex; flex-direction: column; } .draggable-window-titlebar { padding: 8px; background-color: ${style_navbar.background}; color: ${style_navbar.color}; cursor: move; display: flex; justify-content: space-between; align-items: center; user-select: none; } .draggable-window-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 10px; font-weight: bold; } .draggable-window-title:not(.draggable-window-title:hover) { color: ${style_navbar.color} !important; } .draggable-window-close { background: none; border: none; color: ${style_navbar.color}; cursor: pointer; font-size: 16px; padding: 0 5px; } .draggable-window-content { flex: 1; overflow: hidden; padding-bottom: 10px; } .draggable-window-list .draggable-window-content { flex: 1; overflow-y: scroll; padding-bottom: 10px; } .media-container { text-align: center; display: flex; justify-items: center; justify-content: center; position: relative; display: flex; flex-direction: column; align-items: center; object-fit: contain; } .media-container-inline { text-align: center; justify-content: center; position: relative; display: flex; align-items: center; margin-top: 5px; resize: both; overflow: auto; object-fit: contain; width: ${MEDIA_INITIAL_WIDTH}px; min-width: 240px; min-height: 180px; } .media-player { display: flex; flex-direction: column; align-items: center; object-fit: cover; } .media-player img, .media-player video { display: flex; width: 100%; height: 100%; object-fit: cover; } .draggable-window-content .media-player img, .media-player video { object-fit: contain !important; } .media-player audio { display: flex; width: 100%; } .play-button { padding: 0px 3px 1px; border: 1px solid; padding: 0px 6px; cursor: pointer; font-size: 10px; font-weight: bold; } .play-button:hover { border: 1px solid /*white*/; } .play-button-draggable { padding: 0px 3px 1px; border: 1px solid; padding: 0px 6px; cursor: pointer; font-size: 10px; font-weight: bold; } .play-button-draggable:hover { border: 1px solid; } /* Media List Styles */ .media-list { list-style: none; padding: 0; margin: 0; } .media-list-item { padding: 8px 12px; border-bottom: 1px solid gray; cursor: pointer; display: flex; align-items: center; gap: 8px; font-size: 12px; } .media-list-item:hover { background-color: rgba(255, 255, 255, 0.1); } .media-list-item-icon { width: 34px; /*height: 16px;*/ flex-shrink: 0; text-align: center; } .media-list-item-text { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 10px; } .media-list-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 2px solid gray; background-color: ${style_navbar.background}; } .media-list-count { font-size: 11px; opacity: 0.8; } #media-list-toggle-btn { position: fixed !important; right: 8px; bottom: 8px; left: auto; top: auto; z-index: 3; padding: 6px 8px 7px 7px; } `; // Add styles to the document const styleElement = document.createElement('style'); styleElement.textContent = styles; document.head.appendChild(styleElement); document.body.removeChild(div); document.body.removeChild(div2); function extractSoundUrl(title) { const match = title.match(/\[sound=([^\]]+)\]/); if (!match) return null; if (match[1].includes('_') && !match[1].includes('%')) match[1] = match[1].replace(/_/g, '%'); // Fix for Firefox filenames: replace underscores that were mistakenly used instead of percent-encoding let url = decodeURIComponent(match[1]); return !/^https?:\/\//.test(url) ? 'https://' + url : url; } function extractPostIdFromLink(linkElement) { if (!linkElement || !linkElement.href) return 0; // Extract the post ID from the URL hash (e.g., #536595752) const hashMatch = linkElement.href.match(/#(\d+)$/); if (hashMatch && hashMatch[1]) { return parseInt(hashMatch[1], 10); } // Fallback: try to extract from data attributes or other parts of the URL const urlMatch = linkElement.href.match(/\/(\d+)\/?$/); if (urlMatch && urlMatch[1]) { return parseInt(urlMatch[1], 10); } return 0; } function createDraggableWindow(windowTitle, content, linkToThisPost, title, isMediaList = false) { const windowId = 'media-window-' + Math.random().toString(36).substr(2, 9); const windowElement = document.createElement('div'); windowElement.id = windowId; windowElement.className = isMediaList ? 'draggable-window draggable-window-list' : 'draggable-window'; // Title bar const titleBar = document.createElement('div'); titleBar.className = 'draggable-window-titlebar'; const titleText = document.createElement(isMediaList ? 'div' : 'a'); titleText.className = 'draggable-window-title'; titleText.textContent = decodeURIComponent(title); if (!isMediaList) { titleText.title = "Jump to the post for the current sound"; titleText.href = linkToThisPost; } const closeButton = document.createElement('button'); closeButton.className = 'draggable-window-close icon-remove'; titleBar.appendChild(titleText); titleBar.appendChild(closeButton); // Content area const contentArea = document.createElement('div'); contentArea.className = 'draggable-window-content'; contentArea.appendChild(content); windowElement.appendChild(titleBar); windowElement.appendChild(contentArea); // Position the window initially const windowCount = document.querySelectorAll('[id^="media-window-"]').length; windowElement.style.left = (20 + (windowCount * 20)) + 'px'; windowElement.style.top = (20 + (windowCount * 20)) + 'px'; document.body.appendChild(windowElement); // Make draggable let isDragging = false; let offsetX, offsetY; titleBar.addEventListener('mousedown', (e) => { if (e.target === closeButton) return; isDragging = true; offsetX = e.clientX - windowElement.getBoundingClientRect().left; offsetY = e.clientY - windowElement.getBoundingClientRect().top; windowElement.style.cursor = 'grabbing'; e.preventDefault(); }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; windowElement.style.left = (e.clientX - offsetX) + 'px'; windowElement.style.top = (e.clientY - offsetY) + 'px'; }); document.addEventListener('mouseup', () => { isDragging = false; windowElement.style.cursor = ''; ensureOnScreen(windowElement); }); // Close button closeButton.addEventListener('click', () => { if (!isMediaList) { const video = contentArea.querySelector('video'); const audio = contentArea.querySelector('audio'); if (video) video.pause(); if (audio) audio.pause(); } document.body.removeChild(windowElement); }); // Bring to front on click windowElement.addEventListener('mousedown', () => { const allWindows = document.querySelectorAll('[id^="media-window-"]'); let maxZIndex = 1; allWindows.forEach(win => { const zIndex = parseInt(win.style.zIndex) || 1; if (zIndex > maxZIndex) maxZIndex = zIndex; win.style.zIndex = '1'; }); //windowElement.style.zIndex = (maxZIndex + 1).toString(); windowElement.style.zIndex = '2'; }); if (!isMediaList) { // Resize observer for media elements const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { const { width, height } = entry.contentRect; ['video', 'audio', 'img'].forEach(selector => { const audio = contentArea.querySelector('audio'); const element = contentArea.querySelector(selector); if (element) { element.style.width = '100%'; element.style.maxWidth = `${width}px`; if (selector !== 'audio') { if(audio) { element.style.maxHeight = `${height-30}px`; element.style.height = 'auto'; } else { element.style.maxHeight = `${height+10}px`; element.style.height = 'auto'; } } } }); } }); resizeObserver.observe(contentArea); windowElement.addEventListener('close', () => resizeObserver.disconnect()); } // resize observer for the window itself to keep it on screen const windowResizeObserver = new ResizeObserver(() => { ensureOnScreen(windowElement); }); windowResizeObserver.observe(windowElement); windowResizeObserver.observe(document.body); return windowElement; } function createMediaListWindow() { if (mediaItems.length === 0) return; const listContainer = document.createElement('div'); // Header with count /*const header = document.createElement('div'); header.className = 'media-list-header'; header.innerHTML = ` <strong>Media List</strong> <span class="media-list-count">${mediaItems.length} items</span> `; listContainer.appendChild(header);*/ // Sort media items by post ID from lowest to highest const sortedMediaItems = [...mediaItems].sort((a, b) => { const aId = extractPostIdFromLink(a.linkToThisPost); const bId = extractPostIdFromLink(b.linkToThisPost); return aId - bId; }); // List of media items const list = document.createElement('ul'); list.className = 'media-list'; sortedMediaItems.forEach((item, index) => { const listItem = document.createElement('li'); listItem.className = 'media-list-item'; listItem.dataset.index = index; // Icon based on media type const icon = document.createElement('span'); icon.className = 'media-list-item-icon'; icon.innerHTML = item.isVideo ? '🎬' : '🖼️'; icon.innerHTML += (item.title.match(/\[sound=([^\]]+)\]/)) ? '🔊' : ''; const text = document.createElement('span'); text.className = 'media-list-item-text'; // Show post ID in the text if available const postId = extractPostIdFromLink(item.linkToThisPost); const displayText = postId ? `${postId} ▪ ${item.title}` : item.title; text.textContent = displayText; text.title = item.title; listItem.appendChild(icon); listItem.appendChild(text); listItem.addEventListener('click', () => { // Scroll to the post if (item.linkToThisPost) { item.linkToThisPost.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Highlight the post briefly const postWrapper = item.linkToThisPost.closest('.post_wrapper'); if (postWrapper) { const originalBackground = postWrapper.style.backgroundColor; postWrapper.style.backgroundColor = 'rgba(255, 255, 0, 0.2)'; setTimeout(() => { postWrapper.style.backgroundColor = originalBackground; }, 2000); } } }); list.appendChild(listItem); }); listContainer.appendChild(list); // Create the draggable window createDraggableWindow('Media List', listContainer, null, `Media List - ${mediaItems.length} items`, true); } function addMediaListItem(mediaUrl, soundUrl, linkToThisPost, title) { const isVideo = SUPPORTED_VIDEO_EXTS.some(ext => mediaUrl.toLowerCase().endsWith(ext)); mediaItems.push({ mediaUrl, soundUrl, linkToThisPost, title: decodeURIComponent(title), isVideo, timestamp: Date.now() }); } function ensureOnScreen(windowElement) { const containerRect = windowElement.getBoundingClientRect(); const viewportWidth = document.documentElement.clientWidth; const viewportHeight = document.documentElement.clientHeight; // Check if window is completely offscreen const isOffscreen = containerRect.right < 0 || containerRect.bottom < 0 || containerRect.left > viewportWidth || containerRect.top > viewportHeight; if (isOffscreen) { // Move to default position if completely offscreen windowElement.style.left = '20px'; windowElement.style.top = '20px'; } else { // Adjust position if partially offscreen let newLeft = parseFloat(windowElement.style.left) || 0; let newTop = parseFloat(windowElement.style.top) || 0; if (containerRect.left < 0) { newLeft = 0; } else if (containerRect.right > viewportWidth) { newLeft = viewportWidth - containerRect.width; } if (containerRect.top < 0) { newTop = 0; } else if (containerRect.bottom > viewportHeight) { newTop = viewportHeight - containerRect.height; } if (newLeft !== (parseFloat(windowElement.style.left) || 0) || newTop !== (parseFloat(windowElement.style.top) || 0)) { windowElement.style.left = newLeft + 'px'; windowElement.style.top = newTop + 'px'; } } } function createMediaPlayer(mediaUrl, soundUrl, isDraggableWindow = false) { const extension = mediaUrl.split('.').pop().toLowerCase(); const isImage = SUPPORTED_IMAGE_EXTS.some(ext => mediaUrl.toLowerCase().endsWith(ext)); const wrapper = document.createElement('div'); wrapper.className = 'media-player'; if (isImage) { const img = document.createElement('img'); img.src = mediaUrl; wrapper.appendChild(img); if (soundUrl) { const audio = createAudioElement(soundUrl, true, true); wrapper.appendChild(audio); img.style.cursor = 'pointer'; img.addEventListener('click', () => { audio.paused ? audio.play().catch(console.error) : audio.pause(); }); } } else { const video = document.createElement('video'); video.src = mediaUrl; video.controls = true; video.autoplay = false; video.loop = true; video.preload = 'auto'; wrapper.appendChild(video); if (soundUrl) { const audio = createAudioElement(soundUrl, false, false); syncMediaElements(video, audio); wrapper.appendChild(audio); } else { video.addEventListener('loadedmetadata', () => { video.play().catch(console.error); }); } } const container = document.createElement('div'); container.className = isDraggableWindow ? 'media-container' : 'media-container-inline'; container.appendChild(wrapper); return container; } function createAudioElement(soundUrl, autoplay, isDraggableWindow = false) { const audio = document.createElement('audio'); audio.src = soundUrl; audio.loop = true; audio.autoplay = autoplay; audio.preload = 'auto'; audio.controls = true; audio.style.width = '100%'; audio.style.maxWidth = '100%'; return audio; } function syncMediaElements(video, audio) { let durationsMatch = false; let videoReady = false; let audioReady = false; let isSeeking = false; const checkDurations = () => { if (isFinite(video.duration) && isFinite(audio.duration)) { durationsMatch = Math.abs(video.duration - audio.duration) <= DURATION_MATCH_TOLERANCE; if (!durationsMatch) { console.log(`Not syncing audio: duration mismatch (video: ${video.duration.toFixed(2)}s, audio: ${audio.duration.toFixed(2)}s)`); } return true; } return false; }; const checkReady = () => { if (videoReady && audioReady) { const checkInterval = setInterval(() => { if (checkDurations()) { clearInterval(checkInterval); video.play().catch(console.error); audio.play().catch(console.error); } }, 100); } }; video.addEventListener('loadedmetadata', () => { videoReady = true; checkReady(); }); audio.addEventListener('loadedmetadata', () => { audioReady = true; checkReady(); }); // Sync play/pause const syncPlayPause = (source, target) => { if (durationsMatch) { if (source.paused) target.pause(); else target.play().catch(console.error); } }; video.addEventListener('play', () => syncPlayPause(video, audio)); video.addEventListener('pause', () => syncPlayPause(video, audio)); video.addEventListener('ended', () => durationsMatch && !video.loop && audio.pause()); audio.addEventListener('play', () => syncPlayPause(audio, video)); audio.addEventListener('pause', () => syncPlayPause(audio, video)); audio.addEventListener('ended', () => durationsMatch && !audio.loop && video.pause()); // Sync volume/mute const syncVolume = (source, target) => { target.muted = source.muted; target.volume = source.volume; }; video.addEventListener('volumechange', () => syncVolume(video, audio)); audio.addEventListener('volumechange', () => syncVolume(audio, video)); // Sync seeking const syncSeek = (source, target) => { if (durationsMatch && !isSeeking) { isSeeking = true; target.currentTime = source.currentTime; setTimeout(() => isSeeking = false, 100); } }; video.addEventListener('seeked', () => syncSeek(video, audio)); audio.addEventListener('seeked', () => syncSeek(audio, video)); // Sync time updates const syncTimeUpdate = (source, target) => { if (durationsMatch && !isSeeking && Math.abs(source.currentTime - target.currentTime) > 0.1) { target.currentTime = source.currentTime; } }; video.addEventListener('timeupdate', () => syncTimeUpdate(video, audio)); audio.addEventListener('timeupdate', () => syncTimeUpdate(audio, video)); // Sync playback rate const syncRate = (source, target) => { if (durationsMatch) target.playbackRate = source.playbackRate; }; video.addEventListener('ratechange', () => syncRate(video, audio)); audio.addEventListener('ratechange', () => syncRate(audio, video)); // Sync loop const syncLoop = (source, target) => { if (durationsMatch) target.loop = source.loop; }; video.addEventListener('change', (e) => e.target === video && e.target.hasAttribute('loop') && syncLoop(video, audio)); audio.addEventListener('change', (e) => e.target === audio && e.target.hasAttribute('loop') && syncLoop(audio, video)); // Initial sync syncVolume(video, audio); syncRate(video, audio); } function createToggleButton(mediaUrl, soundUrl, mediaTarget, originalThumbHTML, linkToThisPost, title, isDraggableButton = false) { const btn = document.createElement('button'); btn.title = soundUrl ? (isDraggableButton ? 'Play with sound in a draggable window' : 'Play with sound') : (isDraggableButton ? 'Play in a draggable window' : 'Play'); btn.className = isDraggableButton ? 'btnr parent play-file play-button-draggable icon-film' : 'btnr parent play-file play-button icon-play'; btn.dataset.playButton = 'true'; if (isDraggableButton) { btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); const mediaPlayer = createMediaPlayer(mediaUrl, soundUrl, true); const windowTitle = mediaUrl.split('/').pop() || (soundUrl ? 'Media with Sound' : 'Media Player'); createDraggableWindow(windowTitle, mediaPlayer, linkToThisPost, title); }); } else { let mediaInserted = false; let mediaPlayer = null; btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); if (!mediaInserted) { mediaTarget.innerHTML = ''; mediaPlayer = createMediaPlayer(mediaUrl, soundUrl); mediaTarget.appendChild(mediaPlayer); btn.classList.remove("icon-play"); btn.classList.remove("icon-remove"); btn.classList.add("icon-remove"); btn.title = 'Hide'; mediaInserted = true; } else { if (mediaPlayer) { const video = mediaPlayer.querySelector('video'); const audio = mediaPlayer.querySelector('audio'); if (video) video.pause(); if (audio) audio.pause(); } mediaTarget.innerHTML = originalThumbHTML; btn.classList.remove("icon-play"); btn.classList.remove("icon-remove"); btn.classList.add("icon-play"); btn.title = soundUrl ? 'Play with Sound' : 'Play'; mediaInserted = false; } }); } return btn; } function injectToggleButtons() { // Process both video and image files with a single function processMediaFiles(SUPPORTED_VIDEO_EXTS, false);// Videos always get buttons processMediaFiles(SUPPORTED_IMAGE_EXTS, true); // Images only if they have sound // Add media list button if we have media items addMediaListButton(); } function addMediaListButton() { if (mediaItems.length > 0 && !document.querySelector('#media-list-toggle-btn')) { const listBtn = document.createElement('button'); listBtn.id = 'media-list-toggle-btn'; listBtn.innerHTML = '🎬'; listBtn.title = `Show Media List (${mediaItems.length} items)`; listBtn.style.marginLeft = '10px'; listBtn.addEventListener('click', createMediaListWindow); document.body.appendChild(listBtn); } } function processMediaFiles(extensions, requireSound = false) { extensions.forEach(ext => { document.querySelectorAll(`a.post_file_filename[href$="${ext}"]`).forEach(link => { const postWrapper = link.closest(".post_wrapper"); if (!postWrapper) return; const fileControls = postWrapper.querySelector(".post_file_controls"); if (!fileControls || fileControls.dataset.hasToggleBtn) return; const soundUrl = extractSoundUrl(link.getAttribute("title") || ''); // Skip images without sound if required if (requireSound && !soundUrl) return; const mediaUrl = link.href; const linkToThisPost = postWrapper.querySelector('a[data-function="highlight"]'); const title = link.title; const thumbBox = postWrapper.querySelector(".thread_image_box"); if (!thumbBox) return; const originalThumbHTML = thumbBox.innerHTML; // Add to media items list addMediaListItem(mediaUrl, soundUrl, linkToThisPost, title); // Create and insert buttons fileControls.insertAdjacentElement('afterend', createToggleButton(mediaUrl, soundUrl, thumbBox, originalThumbHTML, linkToThisPost, title, true)); fileControls.insertAdjacentElement('afterend', createToggleButton(mediaUrl, soundUrl, thumbBox, originalThumbHTML, linkToThisPost, title)); fileControls.dataset.hasToggleBtn = 'true'; }); }); } // Initial injection setTimeout(() => {injectToggleButtons()}, 2500); // Observer for new content const observer = new MutationObserver(injectToggleButtons); observer.observe(document.body, { childList: true, subtree: true }); })();