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":