import { state } from './state.js'; import { showLoading, hideLoading, setStatus, showToast } from './ui.js'; import * as history from './history.js'; import { renderGrid, updateSelCount, updateNumberBadges, updateNewNameLabels, refreshCardSelection, showEmptyGrid, } from './grid.js'; import { doRename } from './rename.js'; import { handleFolderDrop } from './move.js'; import * as pinchZoom from './handlers/pinch-zoom.js'; import * as selection from './handlers/selection.js'; import * as dragDrop from './handlers/drag-drop.js'; import * as hoverPreview from './handlers/image-preview.js'; import * as fileExplorer from './handlers/file-explorer.js'; let _homeDir = null; // ── DOM ────────────────────────────────────────────── const btnOpen = document.getElementById('btn-open'); const btnSortName = document.getElementById('btn-sort-name'); const btnSortDate = document.getElementById('btn-sort-date'); const btnRename = document.getElementById('btn-rename'); const inputPrefix = document.getElementById('input-prefix'); const inputDigits = document.getElementById('input-digits'); const previewName = document.getElementById('preview-name'); const dirPath = document.getElementById('dir-path'); const rubberBand = document.getElementById('rubber-band'); const grid = document.getElementById('grid'); const gridContainer = document.getElementById('grid-container'); const dragGhostEl = document.getElementById('drag-ghost'); const thumbSizeHint = document.getElementById('thumb-size-hint'); const btnPreviewToggle = document.getElementById('btn-preview-toggle'); const btnUndo = document.getElementById('btn-undo'); const btnRedo = document.getElementById('btn-redo'); const fileExplorerContainer = document.querySelector('.file-explorer'); // ── 핸들러 초기화 ───────────────────────────────── history.setOnUpdate(updateHistoryButtons); const callbacks = { refreshCardSelection, updateSelCount, updateNumberBadges, updateNewNameLabels, pushHistory: (affectedPaths) => history.push(state.files, affectedPaths), }; pinchZoom.init({ gridContainer, thumbSizeHint }); selection.init({ gridContainer, rubberBand, grid, state, callbacks }); dragDrop.init({ grid, gridContainer, dragGhostEl, state, callbacks, fileExplorerEl: fileExplorerContainer, onFolderDrop: handleFolderDrop }); hoverPreview.init(); fileExplorer.init(fileExplorerContainer, loadFolder); window.api.onFullscreenChange((isFullscreen) => { document.body.classList.toggle('fullscreen', isFullscreen); }); // ── 앱 시작 ─────────────────────────────────────── (async () => { const [lastFolder, homeDir, previewOn] = await Promise.all([ window.api.getLastFolder(), window.api.getHomeDir(), window.api.getPreviewOn(), ]); _homeDir = homeDir; btnPreviewToggle.classList.toggle('active', previewOn); hoverPreview.setEnabled(previewOn); const target = lastFolder || homeDir; await fileExplorer.loadTree(homeDir, target); await loadFolder(target); })(); // ── 버튼 / 키보드 이벤트 ───────────────────────── inputPrefix.addEventListener('input', updatePreview); inputDigits.addEventListener('input', updatePreview); btnOpen.addEventListener('click', openFolder); btnSortName.addEventListener('click', () => sortFiles('name')); btnSortDate.addEventListener('click', () => sortFiles('date')); btnRename.addEventListener('click', doRename); btnUndo.addEventListener('click', performUndo); btnRedo.addEventListener('click', performRedo); btnPreviewToggle.addEventListener('click', () => { const on = btnPreviewToggle.classList.toggle('active'); hoverPreview.setEnabled(on); window.api.setPreviewOn(on); }); document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT') return; if (e.code === 'KeyO') btnOpen.click(); if (e.code === 'KeyS' && !btnRename.disabled) btnRename.click(); if (e.code === 'KeyA') { state.files.forEach((_, i) => state.selectedIdxs.add(i)); refreshCardSelection(); updateSelCount(); } if (e.code === 'KeyU') performUndo(); if (e.code === 'KeyR') performRedo(); }); // ── 폴더 열기 (다이얼로그) ─────────────────────── async function openFolder() { const folder = await window.api.selectFolder(); if (!folder) return; const homeDir = _homeDir; if (folder.startsWith(homeDir)) { await fileExplorer.loadTree(homeDir, folder); } else { await fileExplorer.loadTree(folder, folder); } await loadFolder(folder); } // ── 폴더 로드 (트리 클릭 또는 다이얼로그 공통) ── async function loadFolder(folder) { if (!folder) return; state.currentFolder = folder; dirPath.textContent = folder; await window.api.setLastFolder(folder); await fileExplorer.setSelectedPath(folder); showLoading('이미지 목록 불러오는 중...'); const list = await window.api.listImages(folder); hideLoading(); if (!list.length) { state.files.length = 0; state.selectedIdxs.clear(); showEmptyGrid(); btnRename.disabled = true; setStatus('이미지 파일 없음', false); return; } state.files.length = 0; state.files.push(...list); state.selectedIdxs.clear(); state.sortMode = 'name'; updateSortButtons(); setStatus(`${state.files.length}개 파일 로드됨`, true); const folderName = folder.replace(/\\/g, '/').split('/').pop(); if (folderName) inputPrefix.value = folderName; renderGrid(); updatePreview(); btnRename.disabled = false; history.reset(); history.push(state.files); } // ── 정렬 ───────────────────────────────────────── async function sortFiles(mode) { if (!state.files.length) return; if (mode === 'name') state.files.sort((a, b) => a.name.localeCompare(b.name, 'ko')); else state.files.sort((a, b) => a.mtime - b.mtime); state.sortMode = mode; state.selectedIdxs.clear(); updateSortButtons(); await renderGrid(); setStatus(`${mode === 'name' ? '이름순' : '날짜순'}으로 정렬됨`, true); history.push(state.files); } function updateSortButtons() { btnSortName.classList.toggle('active', state.sortMode === 'name'); btnSortDate.classList.toggle('active', state.sortMode === 'date'); } // ── Undo / Redo ─────────────────────────────────── function updateHistoryButtons() { btnUndo.disabled = !history.canUndo(); btnRedo.disabled = !history.canRedo(); } function performUndo() { if (!history.canUndo()) { showToast('더 이상 되돌릴 수 없습니다'); return; } _applyHistorySnapshot(history.undo()); } function performRedo() { if (!history.canRedo()) { showToast('마지막 작업 상태입니다'); return; } _applyHistorySnapshot(history.redo()); } function _applyHistorySnapshot({ files, affectedPaths }) { state.files.length = 0; state.files.push(...files); state.selectedIdxs.clear(); if (affectedPaths) { const pathToIdx = new Map(state.files.map((f, i) => [f.path, i])); affectedPaths.forEach(path => { const idx = pathToIdx.get(path); if (idx !== undefined) state.selectedIdxs.add(idx); }); } state.sortMode = null; updateSortButtons(); renderGrid(); updateHistoryButtons(); } // ── 포맷 미리보기 ───────────────────────────────── function updatePreview() { const prefix = inputPrefix.value.trim() || 'photo'; const digits = Math.max(1, Math.min(6, parseInt(inputDigits.value) || 3)); const ext = state.files.length ? state.files[0].name.split('.').pop().toLowerCase() : 'jpg'; previewName.textContent = `${prefix}_${'1'.padStart(digits, '0')}.${ext}`; updateNewNameLabels(); }