diff --git a/src/renderer/handlers/file-explorer.js b/src/renderer/handlers/file-explorer.js index c3a5607..c1c0a0f 100644 --- a/src/renderer/handlers/file-explorer.js +++ b/src/renderer/handlers/file-explorer.js @@ -1,7 +1,8 @@ // Directory tree for the sidebar file-explorer pane. // Lazy-loads children when a node is expanded. Tracks one selected path. +// Row click = select + always expand. Chevron click = toggle collapse/expand. -let _onSelect = null; // callback(folderPath) +let _onSelect = null; let _selectedPath = null; const ICON_CLOSED = ``; @@ -43,9 +44,9 @@ function createNode({ name, path: nodePath, hasChildren, isRoot = false }) { const chevron = document.createElement('span'); chevron.className = 'tree-chevron'; - chevron.innerHTML = hasChildren - ? `` - : ''; + if (hasChildren) { + chevron.innerHTML = ``; + } const icon = document.createElement('span'); icon.className = 'tree-icon'; @@ -67,15 +68,32 @@ function createNode({ name, path: nodePath, hasChildren, isRoot = false }) { wrap.appendChild(children); } - row.addEventListener('click', (e) => { + // Chevron click: toggle collapse/expand without changing selection + chevron.addEventListener('click', (e) => { e.stopPropagation(); + if (!hasChildren) return; + if (wrap.dataset.expanded === 'true') { + collapseNode(wrap); + } else { + expandNode(wrap, null); + } + }); + + // Row click: select + always expand (never collapse) + row.addEventListener('click', () => { selectNode(wrap); - if (hasChildren) toggleNode(wrap); + if (hasChildren && wrap.dataset.expanded !== 'true') { + expandNode(wrap, null); + } }); return wrap; } +function nodeIcon(nodeEl) { + return nodeEl.dataset.isRoot === 'true' ? ICON_HOME : ICON_OPEN; +} + function selectNode(nodeEl) { const path = nodeEl.dataset.path; document.querySelectorAll('.tree-row.active').forEach(r => r.classList.remove('active')); @@ -84,20 +102,11 @@ function selectNode(nodeEl) { if (_onSelect) _onSelect(path); } -async function toggleNode(nodeEl) { - const expanded = nodeEl.dataset.expanded === 'true'; - if (expanded) { - collapseNode(nodeEl); - } else { - await expandNode(nodeEl, null); - } -} - function collapseNode(nodeEl) { nodeEl.dataset.expanded = 'false'; const row = nodeEl.querySelector(':scope > .tree-row'); row.querySelector('.tree-chevron').classList.remove('open'); - row.querySelector('.tree-icon').innerHTML = ICON_CLOSED; + row.querySelector('.tree-icon').innerHTML = nodeEl.dataset.isRoot === 'true' ? ICON_HOME : ICON_CLOSED; const children = nodeEl.querySelector(':scope > .tree-children'); if (children) children.style.display = 'none'; } @@ -106,41 +115,46 @@ async function expandNode(nodeEl, autoExpandPath) { nodeEl.dataset.expanded = 'true'; const row = nodeEl.querySelector(':scope > .tree-row'); row.querySelector('.tree-chevron').classList.add('open'); - row.querySelector('.tree-icon').innerHTML = nodeEl.dataset.isRoot === 'true' ? ICON_HOME : ICON_OPEN; + row.querySelector('.tree-icon').innerHTML = nodeIcon(nodeEl); const childrenEl = nodeEl.querySelector(':scope > .tree-children'); if (!childrenEl) return; childrenEl.style.display = ''; - // Load children if not yet loaded + // Lazy-load children on first expansion if (!childrenEl.dataset.loaded) { childrenEl.dataset.loaded = 'true'; const dirs = await window.api.listDirectories(nodeEl.dataset.path); - dirs.forEach(dir => { - const child = createNode(dir); - childrenEl.appendChild(child); - }); + dirs.forEach(dir => childrenEl.appendChild(createNode(dir))); } - // Auto-expand path to target + // Auto-expand toward the target path if (autoExpandPath && autoExpandPath !== nodeEl.dataset.path) { const childNodes = childrenEl.querySelectorAll(':scope > .tree-node'); for (const child of childNodes) { - if (autoExpandPath === child.dataset.path || autoExpandPath.startsWith(child.dataset.path + '/')) { + const cp = child.dataset.path; + if (autoExpandPath === cp || autoExpandPath.startsWith(cp + '/')) { await expandNode(child, autoExpandPath); break; } } } + // Reached the target — select it if (autoExpandPath === nodeEl.dataset.path) { selectNode(nodeEl); } } -export function setSelectedPath(folderPath) { +// Mark a path as selected and ensure the node is expanded to show its children. +// Called from renderer when a folder is loaded externally (e.g. "폴더 열기" dialog). +export async function setSelectedPath(folderPath) { _selectedPath = folderPath; document.querySelectorAll('.tree-row.active').forEach(r => r.classList.remove('active')); const node = document.querySelector(`.tree-node[data-path="${CSS.escape(folderPath)}"]`); - if (node) node.querySelector(':scope > .tree-row').classList.add('active'); + if (!node) return; + node.querySelector(':scope > .tree-row').classList.add('active'); + if (node.dataset.hasChildren === 'true' && node.dataset.expanded !== 'true') { + await expandNode(node, null); + } } diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js index 862d63f..cb3610a 100644 --- a/src/renderer/renderer.js +++ b/src/renderer/renderer.js @@ -100,7 +100,7 @@ async function loadFolder(folder) { state.currentFolder = folder; dirPath.textContent = folder; await window.api.setLastFolder(folder); - fileExplorer.setSelectedPath(folder); + await fileExplorer.setSelectedPath(folder); showLoading('이미지 목록 불러오는 중...'); const list = await window.api.listImages(folder); diff --git a/src/renderer/style.css b/src/renderer/style.css index 8740f68..887a0d0 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -450,9 +450,12 @@ footer span { font-size: 11px; color: var(--muted); } display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--muted); transition: transform .15s; + cursor: pointer; + border-radius: 3px; } +.tree-chevron:hover { background: rgba(255,255,255,.08); color: var(--text); } .tree-chevron.open { transform: rotate(90deg); } -.tree-chevron svg { display: block; } +.tree-chevron svg { display: block; pointer-events: none; } .tree-icon { width: 16px; height: 16px;