const SHOW_DELAY = 1000; const HIDE_DELAY = 100; const SVG_NS = 'http://www.w3.org/2000/svg'; const RADIUS = 20; const CIRC = 2 * Math.PI * RADIUS; // ~125.66 let _el = null; let _imgEl = null; let _showTimer = null; let _hideTimer = null; let _visible = false; let _loaderEl = null; let _activeCard = null; let _enabled = true; export function init() { _el = document.createElement('div'); _el.className = 'hover-preview'; _imgEl = document.createElement('img'); _el.appendChild(_imgEl); document.body.appendChild(_el); } export function setEnabled(v) { _enabled = v; if (!v) { clearTimeout(_showTimer); _showTimer = null; clearTimeout(_hideTimer); _hideTimer = null; _removeLoader(); _activeCard = null; _visible = false; _el.style.display = 'none'; _el.style.visibility = 'hidden'; } } export function setupCard(card) { card.addEventListener('mouseenter', _onEnter); card.addEventListener('mouseleave', _onLeave); } function _onEnter() { if (!_enabled) return; const card = this; _activeCard = card; // 팝업이 사라지려고 대기 중이면 취소 if (_hideTimer) { clearTimeout(_hideTimer); _hideTimer = null; } if (_visible) { // 이미 팝업이 열려 있으면 즉시 내용 전환 _switchTo(card); return; } // 팝업이 닫힌 상태 — 1초 대기 후 표시 clearTimeout(_showTimer); _showLoader(card); _showTimer = setTimeout(async () => { _showTimer = null; _removeLoader(); if (_activeCard !== card) return; const src = await _loadSrc(card); if (!src || _activeCard !== card) return; _imgEl.src = src; _visible = true; _show(card); }, SHOW_DELAY); } function _onLeave() { if (!_visible) { // 팝업 미표시 상태 — 로더·타이머만 정리 clearTimeout(_showTimer); _showTimer = null; _removeLoader(); return; } // 팝업 표시 중 — 0.1초 후 닫기 (다른 카드 진입 시 취소됨) _hideTimer = setTimeout(() => { _hideTimer = null; _visible = false; _el.style.display = 'none'; _el.style.visibility = 'hidden'; }, HIDE_DELAY); } // ── 팝업 내용 전환 (이미 열린 상태에서 카드 변경) ── async function _switchTo(card) { const src = await _loadSrc(card); if (!src || _activeCard !== card) return; _imgEl.src = src; _show(card); } // ── 이미지 소스 반환 (카드 썸네일 캐시 우선) ── async function _loadSrc(card) { const thumb = card.querySelector('img.card-thumb'); let src = thumb?.src || ''; if (!src) { src = await window.api.readImage(card.dataset.filepath) || ''; if (src && thumb) thumb.src = src; } return src; } // ── 원형 로딩바 ────────────────────────────────── function _showLoader(card) { _removeLoader(); const svg = document.createElementNS(SVG_NS, 'svg'); svg.classList.add('card-hover-loader'); svg.setAttribute('viewBox', '0 0 48 48'); const bg = document.createElementNS(SVG_NS, 'circle'); bg.classList.add('loader-bg'); bg.setAttribute('cx', '24'); bg.setAttribute('cy', '24'); bg.setAttribute('r', '24'); const track = document.createElementNS(SVG_NS, 'circle'); track.classList.add('loader-track'); track.setAttribute('cx', '24'); track.setAttribute('cy', '24'); track.setAttribute('r', String(RADIUS)); const progress = document.createElementNS(SVG_NS, 'circle'); progress.classList.add('loader-progress'); progress.setAttribute('cx', '24'); progress.setAttribute('cy', '24'); progress.setAttribute('r', String(RADIUS)); progress.style.strokeDasharray = CIRC; progress.style.strokeDashoffset = CIRC; svg.appendChild(bg); svg.appendChild(track); svg.appendChild(progress); card.appendChild(svg); _loaderEl = svg; } function _removeLoader() { if (_loaderEl) { _loaderEl.remove(); _loaderEl = null; } } // ── 이미지 팝업 위치 계산 및 표시 ──────────────── function _show(card) { _el.style.display = 'block'; _el.style.visibility = 'hidden'; _el.style.left = '0px'; _el.style.top = '0px'; requestAnimationFrame(() => { if (!_visible) return; const popup = _el.getBoundingClientRect(); const cardRect = card.getBoundingClientRect(); const vw = window.innerWidth; const vh = window.innerHeight; const gap = 16; let left = cardRect.right + gap; if (left + popup.width > vw - 8) { left = cardRect.left - popup.width - gap; } left = Math.max(8, Math.min(left, vw - popup.width - 8)); let top = cardRect.top + (cardRect.height - popup.height) / 2; top = Math.max(8, Math.min(top, vh - popup.height - 8)); _el.style.left = left + 'px'; _el.style.top = top + 'px'; _el.style.visibility = 'visible'; }); }