diff --git a/qbittorrent_performer_sorter.html b/qbittorrent_performer_sorter.html index 74e99a2..23129b4 100644 --- a/qbittorrent_performer_sorter.html +++ b/qbittorrent_performer_sorter.html @@ -822,16 +822,17 @@ cursor: pointer; flex-shrink: 0; background: #000; - transition: all 0.15s ease; + transition: all 0.18s cubic-bezier(0.16, 1, 0.3, 1); } .candidate-image-item:hover { border-color: var(--accent-cyan); - transform: translateY(-2px); + transform: scale(1.08) translateY(-4px); + z-index: 10; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.7), 0 0 16px rgba(6, 182, 212, 0.45); } .candidate-image-item.active { border-color: var(--accent-green); - box-shadow: 0 0 12px rgba(34, 197, 94, 0.6); - transform: translateY(-2px); + box-shadow: 0 0 14px rgba(34, 197, 94, 0.65); } .candidate-image-item img { width: 100%; @@ -854,6 +855,57 @@ align-items: center; justify-content: center; box-shadow: 0 2px 5px rgba(0,0,0,0.6); + z-index: 5; + } + + /* Floating Enlarged Image Zoom Preview */ + .image-zoom-preview-modal { + position: fixed; + z-index: 200000; + pointer-events: none; + background: rgba(15, 23, 42, 0.98); + border: 2px solid var(--accent-cyan); + border-radius: var(--radius-md); + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.85), 0 0 35px rgba(6, 182, 212, 0.35); + padding: 0.4rem; + width: 280px; + height: 380px; + opacity: 0; + visibility: hidden; + transform: scale(0.92); + transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s; + backdrop-filter: blur(16px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: hidden; + } + .image-zoom-preview-modal.visible { + opacity: 1; + visibility: visible; + transform: scale(1); + } + .image-zoom-preview-modal img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: var(--radius-sm); + display: block; + } + .image-zoom-caption { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: linear-gradient(transparent, rgba(0,0,0,0.85)); + color: #fff; + font-size: 0.75rem; + font-weight: 600; + padding: 1.2rem 0.6rem 0.45rem 0.6rem; + text-align: center; + text-shadow: 0 1px 3px rgba(0,0,0,0.8); + pointer-events: none; } .btn-preview-play { display: inline-flex; @@ -1632,6 +1684,12 @@ + +
+ Enlarged Preview +
+
+
@@ -1705,8 +1763,67 @@ hoverCard.style.top = `${top}px`; } - // Global event delegation for performer pills & inputs + // ==================== Floating Image Zoom Preview Controller ==================== + const imageZoomModal = document.getElementById('image-zoom-preview-modal'); + const imageZoomImg = document.getElementById('image-zoom-img'); + const imageZoomCaption = document.getElementById('image-zoom-caption'); + let zoomTimeout = null; + + function showImageZoomPreview(imageUrl, caption, targetEl, event) { + if (!imageZoomModal || !imageZoomImg || !imageUrl) return; + clearTimeout(zoomTimeout); + imageZoomImg.src = imageUrl; + if (imageZoomCaption) imageZoomCaption.textContent = caption || ''; + positionImageZoom(targetEl, event); + imageZoomModal.classList.add('visible'); + } + + function hideImageZoomPreview() { + clearTimeout(zoomTimeout); + if (!imageZoomModal) return; + imageZoomModal.classList.remove('visible'); + } + + function positionImageZoom(targetEl, event) { + const rect = targetEl ? targetEl.getBoundingClientRect() : { left: event.clientX, top: event.clientY, width: 20, height: 20 }; + const zoomWidth = 280; + const zoomHeight = 380; + const margin = 14; + + // Default: place to the right of thumbnail + let left = rect.right + margin; + let top = rect.top - 40; + + // If overflowing right boundary, place to left of thumbnail + if (left + zoomWidth > window.innerWidth - 10) { + left = rect.left - zoomWidth - margin; + } + + // If overflowing top or bottom, clamp within screen + if (top < 10) top = 10; + if (top + zoomHeight > window.innerHeight - 10) { + top = window.innerHeight - zoomHeight - 10; + } + if (left < 10) left = 10; + + imageZoomModal.style.left = `${left}px`; + imageZoomModal.style.top = `${top}px`; + } + + // Global event delegation for candidate images, performer pills & inputs document.addEventListener('mouseover', (e) => { + const candItem = e.target.closest('.candidate-image-item'); + if (candItem) { + const img = candItem.querySelector('img'); + const imgUrl = candItem.getAttribute('data-image-url') || (img ? img.src : ''); + const perfCard = candItem.closest('.performer-review-card'); + const perfName = perfCard ? (perfCard.querySelector('.stash-performer-checkbox')?.dataset?.name || '') : ''; + if (imgUrl) { + showImageZoomPreview(imgUrl, perfName, candItem, e); + return; + } + } + const pill = e.target.closest('.performer-pill, .smb-performer-pill, .modal-performer-pill'); if (pill) { const perf = pill.getAttribute('data-performer'); @@ -1717,6 +1834,12 @@ }); document.addEventListener('mouseout', (e) => { + const candItem = e.target.closest('.candidate-image-item'); + if (candItem) { + hideImageZoomPreview(); + return; + } + const pill = e.target.closest('.performer-pill, .smb-performer-pill, .modal-performer-pill'); if (pill) { hidePerformerHoverCard(); @@ -5225,7 +5348,7 @@ Return ONLY a valid JSON object with an array named "results":
- +
${escapeHtml(p.name)}