Reddit Media Zoom on Hover (v3.1 Fix Attempt - VI: Phóng to ảnh/video)

Zooms images, GIFs, and videos when hovering on old.reddit.com and www.reddit.com. (Fix attempt)

// ==UserScript==
// @name         Reddit Media Zoom on Hover (v3.1 Fix Attempt - VI: Phóng to ảnh/video)
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  Zooms images, GIFs, and videos when hovering on old.reddit.com and www.reddit.com. (Fix attempt)
// @author       ChatGPT & Bạn
// @match        *://old.reddit.com/*
// @match        *://www.reddit.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let zoomContainer = null;
    let zoomImageElement = null;
    let zoomVideoElement = null;
    let hoverTimeout = null;
    let currentMouseEvent = null;
    let currentMediaInfo = null; // Lưu thông tin media đang hiển thị/chờ hiển thị

    const DELAY_MS = 150; // Giảm nhẹ delay xem có giúp không
    const OFFSET_X = 15;
    const OFFSET_Y = 15;
    const IMAGE_EXTENSIONS_REGEX = /\.(jpg|jpeg|png|gif|webp)([\?#].*)?$/i;
    const VIDEO_EXTENSIONS_REGEX = /\.(mp4|webm)([\?#].*)?$/i;
    const VIDEO_DOMAIN_REGEX = /v\.redd\.it/i;
    const GIFV_REGEX = /\.gifv([\?#].*)?$/i;

    // --- Tạo các phần tử hiển thị (Không thay đổi nhiều) ---
    function createZoomElements() {
        if (zoomContainer) return;
        // (Giữ nguyên code tạo container, img, video như v3.0)
        zoomContainer = document.createElement('div');
        zoomContainer.id = 'userscript-media-zoom-popup';
        zoomContainer.style.position = 'fixed';
        zoomContainer.style.border = '2px solid #ccc';
        zoomContainer.style.borderRadius = '4px';
        zoomContainer.style.boxShadow = '3px 3px 10px rgba(0,0,0,0.5)';
        zoomContainer.style.zIndex = '99999';
        zoomContainer.style.display = 'none';
        zoomContainer.style.pointerEvents = 'none';
        zoomContainer.style.backgroundColor = 'black';
        zoomContainer.style.maxWidth = '85vw';
        zoomContainer.style.maxHeight = '85vh';
        zoomContainer.style.overflow = 'hidden';

        zoomImageElement = document.createElement('img');
        zoomImageElement.style.display = 'none'; // Bắt đầu ẩn
        zoomImageElement.style.maxWidth = '100%';
        zoomImageElement.style.maxHeight = 'calc(85vh - 4px)';
        zoomImageElement.style.width = 'auto';
        zoomImageElement.style.height = 'auto';
        zoomImageElement.style.objectFit = 'contain';
        zoomImageElement.style.verticalAlign = 'bottom';
        zoomImageElement.onload = () => { if (zoomContainer.style.display === 'block' && currentMouseEvent) positionZoomElement(currentMouseEvent); };
        zoomImageElement.onerror = () => { console.warn("Zoom Error: Failed to load image", zoomImageElement.src); hideZoom(); };

        zoomVideoElement = document.createElement('video');
        zoomVideoElement.style.display = 'none'; // Bắt đầu ẩn
        zoomVideoElement.style.maxWidth = '100%';
        zoomVideoElement.style.maxHeight = 'calc(85vh - 4px)';
        zoomVideoElement.style.width = 'auto';
        zoomVideoElement.style.height = 'auto';
        zoomVideoElement.style.objectFit = 'contain';
        zoomVideoElement.style.verticalAlign = 'bottom';
        zoomVideoElement.muted = true;
        zoomVideoElement.loop = true;
        zoomVideoElement.autoplay = true;
        zoomVideoElement.playsInline = true;
        zoomVideoElement.onloadeddata = () => { if (zoomContainer.style.display === 'block' && currentMouseEvent) positionZoomElement(currentMouseEvent); zoomVideoElement.play().catch(()=>{}); };
        zoomVideoElement.onerror = () => { console.warn("Zoom Error: Failed to load video", zoomVideoElement.src); hideZoom(); };

        zoomContainer.appendChild(zoomImageElement);
        zoomContainer.appendChild(zoomVideoElement);
        document.body.appendChild(zoomContainer);
        console.log("Zoom elements created."); // Log khi tạo xong
    }

    // --- Định vị (Không thay đổi) ---
    function positionZoomElement(event) {
        if (!zoomContainer || zoomContainer.style.display === 'none' || !event) return;
        const vw = window.innerWidth;
        const vh = window.innerHeight;
        const mouseX = event.clientX;
        const mouseY = event.clientY;
        const containerWidth = zoomContainer.offsetWidth;
        const containerHeight = zoomContainer.offsetHeight;

        // Tạm thời bỏ qua định vị nếu kích thước chưa có (chờ load)
        if (containerWidth === 0 || containerHeight === 0) return;

        let newX = mouseX + OFFSET_X;
        let newY = mouseY + OFFSET_Y;
        if (newX + containerWidth > vw - OFFSET_X) { newX = mouseX - containerWidth - OFFSET_X; if (newX < OFFSET_X) newX = OFFSET_X; }
        if (newY + containerHeight > vh - OFFSET_Y) { newY = mouseY - containerHeight - OFFSET_Y; if (newY < OFFSET_Y) newY = OFFSET_Y; }
        zoomContainer.style.left = newX + 'px';
        zoomContainer.style.top = newY + 'px';
    }

    // --- Lấy URL và loại media (Giữ nguyên logic, nhưng thêm log) ---
    function getMediaInfo(targetElement) {
        // console.log("getMediaInfo for:", targetElement); // Log đầu vào
        const hostname = window.location.hostname;
        let sourceUrl = null;
        let mediaType = 'image';

        try {
            // (Logic tìm sourceUrl như v3.0)
             // --- Logic cho old.reddit.com ---
            if (hostname === 'old.reddit.com') {
                const linkElement = targetElement.closest('a.thumbnail');
                if (linkElement && linkElement.href) {
                    sourceUrl = linkElement.href;
                    if (!IMAGE_EXTENSIONS_REGEX.test(sourceUrl) && !VIDEO_EXTENSIONS_REGEX.test(sourceUrl) && !GIFV_REGEX.test(sourceUrl)) {
                         const postLink = targetElement.closest('.thing')?.querySelector('a.title')?.href;
                         if (postLink && postLink.includes('/comments/')) return null;
                         // Nếu không phải link media đã biết, thử giả định là ảnh nếu là link trực tiếp
                         if (!sourceUrl.includes('/comments/') && !sourceUrl.includes('/gallery/')) {
                            // Keep sourceUrl, maybe it's an image without extension? Risky.
                         } else {
                             return null; // Là link post/gallery
                         }
                    }
                }
            }
            // --- Logic cho www.reddit.com ---
            else {
                let parentLinkHref = null;
                const parentLink = targetElement.closest('a[href]');
                if (parentLink) parentLinkHref = parentLink.href;

                // 1. Ưu tiên link trực tiếp từ thẻ A cha (không phải link comment/gallery)
                if (parentLinkHref && !parentLinkHref.includes('/comments/') && !parentLinkHref.includes('/gallery/')) {
                     if (IMAGE_EXTENSIONS_REGEX.test(parentLinkHref) || VIDEO_EXTENSIONS_REGEX.test(parentLinkHref) || GIFV_REGEX.test(parentLinkHref)) {
                         sourceUrl = parentLinkHref;
                     }
                 }

                // 2. Thử lấy từ src của IMG nếu target là IMG và chưa có URL
                if (!sourceUrl && targetElement.tagName === 'IMG' && targetElement.src && !targetElement.src.startsWith('data:')) {
                     if (targetElement.naturalWidth > 10 || targetElement.naturalHeight > 10 || targetElement.src.includes('redd.it') || targetElement.src.includes('imgur.com')) {
                         sourceUrl = targetElement.src;
                     }
                }

                // 3. Thử lấy từ background-image nếu target là DIV
                if (!sourceUrl && targetElement.tagName === 'DIV' && targetElement.style.backgroundImage.includes('url(')) {
                    const style = targetElement.style.backgroundImage;
                    const match = style.match(/url\("?([^"\)]+)"?\)/);
                    if (match && match[1] && !match[1].startsWith('data:')) {
                        sourceUrl = match[1];
                    }
                }

                 // 4. Tìm media lớn hơn trong post container nếu URL hiện tại là preview
                 if (sourceUrl && (sourceUrl.includes('preview.redd.it') || sourceUrl.includes('styles.redditmedia'))) {
                     const postContainer = targetElement.closest('[data-testid="post-container"], .scrollerItem, [data-testid="post-content"]');
                     if (postContainer) {
                         const videoPlayer = postContainer.querySelector('shreddit-player, video[src]');
                         if (videoPlayer) {
                             let videoSrc = videoPlayer.getAttribute('src'); // Thử lấy src trực tiếp
                             if(!videoSrc && videoPlayer.tagName === 'VIDEO') videoSrc = videoPlayer.currentSrc || videoPlayer.src;
                             // Lấy từ source tag nếu không có src trực tiếp
                             if (!videoSrc && videoPlayer.tagName.toLowerCase() === 'shreddit-player') {
                                const sources = videoPlayer.querySelectorAll('source[src]');
                                for(const source of sources) { if (source.src.endsWith('.mp4')) { videoSrc = source.src; break; } }
                                if (!videoSrc && sources.length > 0) videoSrc = sources[0].src;
                             }
                             if (videoSrc && (VIDEO_EXTENSIONS_REGEX.test(videoSrc) || VIDEO_DOMAIN_REGEX.test(videoSrc))) {
                                 sourceUrl = videoSrc;
                             }
                         }
                         // Tìm ảnh chính nếu không tìm thấy video hợp lệ
                         if (!sourceUrl || !(VIDEO_EXTENSIONS_REGEX.test(sourceUrl) || VIDEO_DOMAIN_REGEX.test(sourceUrl))) {
                             const mainImage = postContainer.querySelector('img[alt="Post image"], img[data-testid="post-image"], .media-element img');
                              if (mainImage && mainImage.src && !mainImage.src.startsWith('data:') && !mainImage.src.includes('blur=')) {
                                 if (!mainImage.src.includes('preview.redd.it') || sourceUrl.includes('styles.redditmedia')) {
                                     sourceUrl = mainImage.src;
                                 }
                             }
                         }
                     }
                 }

                // 5. Fallback: Kiểm tra lại parentLinkHref
                 if (!sourceUrl && parentLinkHref && !parentLinkHref.includes('/comments/') && !parentLinkHref.includes('/gallery/')) {
                     if (IMAGE_EXTENSIONS_REGEX.test(parentLinkHref) || VIDEO_EXTENSIONS_REGEX.test(parentLinkHref) || GIFV_REGEX.test(parentLinkHref)) {
                         sourceUrl = parentLinkHref;
                     }
                 }
            } // End logic www.reddit.com

            // --- Xử lý URL tìm được ---
            if (sourceUrl && sourceUrl.startsWith('http')) {
                sourceUrl = sourceUrl.replace(/&/g, '&');

                if (GIFV_REGEX.test(sourceUrl)) {
                    mediaType = 'video';
                    sourceUrl = sourceUrl.replace(GIFV_REGEX, '.mp4');
                } else if (VIDEO_EXTENSIONS_REGEX.test(sourceUrl)) {
                    mediaType = 'video';
                } else if (VIDEO_DOMAIN_REGEX.test(sourceUrl)) {
                    mediaType = 'video';
                     if (!sourceUrl.includes('/DASH_') && !sourceUrl.includes('m3u8') && !sourceUrl.endsWith('.mp4') && !sourceUrl.endsWith('.webm')) {
                         console.log("Unsupported v.redd.it link:", sourceUrl); return null;
                    }
                } else if (IMAGE_EXTENSIONS_REGEX.test(sourceUrl)) {
                    mediaType = 'image';
                } else if (sourceUrl.includes('external-preview.redd.it')) {
                     try {
                        const urlObj = new URL(sourceUrl);
                        const externalUrl = urlObj.searchParams.get('url');
                        if (externalUrl) {
                            sourceUrl = decodeURIComponent(externalUrl);
                            if (VIDEO_EXTENSIONS_REGEX.test(sourceUrl)) mediaType = 'video';
                            else if (IMAGE_EXTENSIONS_REGEX.test(sourceUrl)) mediaType = 'image';
                            else return null;
                        } else { return null; }
                    } catch(e) { return null; }
                } else {
                     // Nếu không khớp đuôi file nào, và không phải preview, thử giả định là ảnh
                     if (!sourceUrl.includes('/comments/') && !sourceUrl.includes('/gallery/') && !sourceUrl.includes('reddit.com/user/')) {
                         mediaType = 'image'; // Thử coi là ảnh
                         console.log("Assuming image type for:", sourceUrl);
                     } else {
                         // console.log("URL type unknown or excluded:", sourceUrl);
                         return null;
                     }
                }

                // --- Dọn dẹp URL ---
                if (mediaType === 'image' && sourceUrl.includes('preview.redd.it')) {
                     try {
                         const urlObj = new URL(sourceUrl);
                         const pathSegments = urlObj.pathname.split('/');
                         const imageIdWithExt = pathSegments[pathSegments.length - 1];
                         if (imageIdWithExt && imageIdWithExt.includes('.')) {
                             sourceUrl = `https://i.redd.it/${imageIdWithExt}`;
                         }
                     } catch(e) {/* ignore */}
                 }
                 if (mediaType === 'image' && (sourceUrl.includes('//i.redd.it/') || sourceUrl.includes('//i.imgur.com/'))) {
                     sourceUrl = sourceUrl.split('?')[0];
                 }

                // console.log("Found media:", { url: sourceUrl, type: mediaType }); // Log kết quả
                return { url: sourceUrl, type: mediaType };
            }

        } catch (error) {
            console.error("Zoom Error in getMediaInfo:", error, "Element:", targetElement);
        }

        // console.log("No valid media found."); // Log nếu không tìm thấy
        return null;
    }

    // --- Hàm hiển thị media zoom (Đơn giản hóa logic) ---
    function showZoom(event, targetElement) {
        // Hủy timeout cũ ngay lập tức
        clearTimeout(hoverTimeout);
        // console.log("showZoom called for:", targetElement); // Log

        const mediaInfo = getMediaInfo(targetElement);
        // console.log("Media info received:", mediaInfo); // Log

        if (mediaInfo) {
            createZoomElements(); // Đảm bảo element tồn tại

            // Lưu thông tin media sẽ hiển thị và event để định vị
            currentMediaInfo = mediaInfo;
            currentMouseEvent = event;

            // Đặt timeout để hiển thị
            hoverTimeout = setTimeout(() => {
                // Kiểm tra lại xem currentMediaInfo có còn là cái này không
                // (Phòng trường hợp di chuột quá nhanh, chỉ hiện cái cuối cùng)
                if (currentMediaInfo !== mediaInfo) {
                    // console.log("Hover changed before timeout, aborting display for:", mediaInfo.url);
                    return;
                }

                // console.log(`Timeout: Displaying ${mediaInfo.type}:`, mediaInfo.url); // Log

                // Reset cả hai element trước
                zoomImageElement.style.display = 'none';
                zoomImageElement.src = '';
                zoomVideoElement.style.display = 'none';
                if (!zoomVideoElement.paused) zoomVideoElement.pause();
                zoomVideoElement.removeAttribute('src'); // Dùng removeAttribute tốt hơn là src = '' cho video

                 // Set src và hiển thị element tương ứng
                if (mediaInfo.type === 'image') {
                    zoomImageElement.src = mediaInfo.url;
                    zoomImageElement.style.display = 'block';
                } else if (mediaInfo.type === 'video') {
                     zoomVideoElement.src = mediaInfo.url;
                     zoomVideoElement.style.display = 'block';
                     // Thử play lại, catch lỗi nếu có
                     zoomVideoElement.play().catch(e => console.warn("Video play() failed:", e));
                 }

                // Hiển thị container và định vị
                zoomContainer.style.display = 'block';
                positionZoomElement(currentMouseEvent); // Định vị dùng event đã lưu

            }, DELAY_MS);
        } else {
             // Nếu không có media info, đảm bảo không có gì đang chờ hiển thị
             currentMediaInfo = null; // Xóa media đang chờ (nếu có)
             // Không cần gọi hideZoom ở đây vì có thể người dùng chỉ lướt qua vùng không có ảnh
        }
    }

    // --- Hàm ẩn media zoom (Đơn giản hóa) ---
    function hideZoom() {
        // console.log("hideZoom called"); // Log
        clearTimeout(hoverTimeout); // Luôn xóa timeout
        currentMediaInfo = null;    // Xóa media đang chờ/hiển thị
        currentMouseEvent = null; // Xóa event

        if (zoomContainer && zoomContainer.style.display !== 'none') {
            // console.log("Hiding zoom container"); // Log
            zoomContainer.style.display = 'none';
            zoomImageElement.src = '';
            zoomImageElement.style.display = 'none';
            if (!zoomVideoElement.paused) zoomVideoElement.pause();
            zoomVideoElement.removeAttribute('src');
            zoomVideoElement.load(); // Yêu cầu trình duyệt dừng tải thêm data
            zoomVideoElement.style.display = 'none';
        }
    }

    // --- Các bộ chọn CSS (Giữ nguyên) ---
    const targetSelectors = [
        'a.thumbnail img', 'a.thumbnail[href*=".gif"]', 'a.thumbnail[href*=".mp4"]', 'a.thumbnail[href*=".webm"]', 'a.thumbnail[href*=".gifv"]',
        'img[alt="Post image"]', 'img[data-testid="post-image"]', 'div[data-testid="post-container"] .media-element', 'a[data-testid="post-image-container"] img', 'div[data-testid="post-image-container"] > div[style*="background-image"]',
        'shreddit-player', 'video[data-testid="video-player"]', 'video.media-element',
        'div[role="img"] img', 'img[data-event-action="thumbnail"]', 'div[data-click-id="media"] > div[style*="background-image"]', 'a[data-testid="external-link"] > div[style*="background-image"]',
        'figure[data-testid="image-widget"] img', 'figure[data-testid="video-widget"] video',
    ].join(', ');

    // --- Event Listeners (Điều chỉnh nhẹ) ---
    document.body.addEventListener('mouseover', (event) => {
        const targetElement = event.target;
        const matchingElement = targetElement.closest(targetSelectors);

        if (matchingElement) {
            // Gọi showZoom với event và phần tử khớp được
            // Logic xử lý chờ và hiển thị sẽ nằm trong showZoom
            showZoom(event, matchingElement);
        }
        // Không cần else ở đây, mouseout sẽ xử lý việc ẩn khi rời khỏi
    });

    document.body.addEventListener('mouseout', (event) => {
        const targetElement = event.target;
        const relatedTarget = event.relatedTarget; // Element chuột di chuyển TỚI

        // Tìm xem có phải đang rời khỏi một thumbnail/media hợp lệ không
        const matchingElement = targetElement.closest(targetSelectors);

        if (matchingElement) {
            // Chỉ ẩn NẾU chuột di ra ngoài HOÀN TOÀN khỏi matchingElement VÀ KHÔNG đi vào popup
            // (Popup có pointer-events: none nên kiểm tra popup không cần thiết)
            if (!relatedTarget || !matchingElement.contains(relatedTarget)) {
                 // console.log("Mouseout detected, hiding zoom."); // Log
                 hideZoom();
            }
        } else {
             // Nếu di chuột ra khỏi một phần tử KHÔNG phải là targetSelectors
             // và chuột KHÔNG đi vào một targetSelectors khác, thì cũng nên ẩn
             if (!relatedTarget || !relatedTarget.closest(targetSelectors)) {
                 // console.log("Mouseout from non-target, hiding zoom."); // Log
                  hideZoom();
             }
        }
    });

    document.body.addEventListener('mousemove', (event) => {
        // Chỉ định vị lại nếu popup đang hiển thị
        if (zoomContainer && zoomContainer.style.display === 'block') {
            // Cập nhật mouse event để hàm position dùng tọa độ mới nhất
            currentMouseEvent = event;
            positionZoomElement(event);
        }
    });

    console.log("Reddit Media Zoom script (v3.1 Fix Attempt) loaded.");
    // Tạo sẵn element khi script load để tránh delay lần đầu
    createZoomElements();

})();