FoolFuuka Video Player + External Sounds

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