// ==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 });
})();