您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); })();