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 { initModalControls } from './modal.js'; import * as deleteHandler from './delete.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); initModalControls(); const callbacks = { refreshCardSelection, updateSelCount, updateNumberBadges, updateNewNameLabels, pushHistory: (affectedPaths) => history.pushReorder(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(); deleteHandler.init(grid); 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.metaKey && !e.shiftKey && e.code === 'KeyZ') { e.preventDefault(); performUndo(); } if (e.metaKey && e.shiftKey && e.code === 'KeyZ') { e.preventDefault(); performRedo(); } if (e.code === 'KeyP') btnPreviewToggle.click(); if (e.code === 'Backspace' || e.code === 'Delete') deleteHandler.deleteSelected(); }); // ── 폴더 열기 (다이얼로그) ─────────────────────── 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.pushReorder(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.pushReorder(state.files); } function updateSortButtons() { btnSortName.classList.toggle('active', state.sortMode === 'name'); btnSortDate.classList.toggle('active', state.sortMode === 'date'); } // ── Undo / Redo ─────────────────────────────────── let _historyBusy = false; function updateHistoryButtons() { btnUndo.disabled = !history.canUndo(); btnRedo.disabled = !history.canRedo(); } async function performUndo() { if (_historyBusy || !history.canUndo()) { if (!history.canUndo()) showToast('더 이상 되돌릴 수 없습니다'); return; } _historyBusy = true; try { const { entry, targetFiles } = history.undo(); if (entry.type === 'delete') { const { restored, errors } = await window.api.restoreStagedFiles(entry.deletedEntries); if (errors.length) showToast('일부 파일 복원 실패: ' + errors[0]); _applyHistorySnapshot(targetFiles, new Set(restored)); if (restored.length) setStatus(`${restored.length}개 파일 복원됨`, true); } else { _applyHistorySnapshot(targetFiles, entry.affectedPaths); } } finally { _historyBusy = false; } } async function performRedo() { if (_historyBusy || !history.canRedo()) { if (!history.canRedo()) showToast('마지막 작업 상태입니다'); return; } _historyBusy = true; try { const nextEntry = history.peekRedo(); if (nextEntry.type === 'delete') { const pathsToRedo = nextEntry.deletedEntries.map(e => e.originalPath); const { staged, errors } = await window.api.stageDeleteFiles(pathsToRedo); if (errors.length) showToast('일부 파일 삭제 실패: ' + errors[0]); const stagedByPath = new Map(staged.map(s => [s.originalPath, s.stagingPath])); nextEntry.deletedEntries.forEach(e => { if (stagedByPath.has(e.originalPath)) e.stagingPath = stagedByPath.get(e.originalPath); }); const { targetFiles } = history.redo(); _applyHistorySnapshot(targetFiles, null); if (staged.length) setStatus(`${staged.length}개 파일 삭제됨`, true); } else { const { entry, targetFiles } = history.redo(); _applyHistorySnapshot(targetFiles, entry.affectedPaths); } } finally { _historyBusy = false; } } function _applyHistorySnapshot(targetFiles, affectedPaths = null) { state.files.length = 0; state.files.push(...targetFiles); state.selectedIdxs.clear(); if (affectedPaths && affectedPaths.size > 0) { const pathToIdx = new Map(state.files.map((f, i) => [f.path, i])); affectedPaths.forEach(p => { const idx = pathToIdx.get(p); if (idx !== undefined) state.selectedIdxs.add(idx); }); } state.sortMode = null; updateSortButtons(); if (!state.files.length) { showEmptyGrid(); btnRename.disabled = true; } else { renderGrid(); btnRename.disabled = false; } 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(); }