/** Unlimited-depth tree library UI + admin management. */
let treeData = null;
let imageTypes = [];
const librarySidebar = {
containerId: 'lib-tree',
searchId: 'lib-tree-search',
allBtnId: 'btn-tree-all',
getSelectedNode: () => window.ddaState?.selectedNode,
onNodeSelect: (node) => {
window.ddaState.setNode(node);
window.ddaState.refreshImages();
syncUploadNodeSelect();
},
onClearNode: () => {
window.ddaState.clearNode();
window.ddaState.refreshImages();
syncUploadNodeSelect();
},
};
let compareSidebar = null;
function isAdmin() {
return window.ddaState?.userRole === 'admin';
}
function getSidebars() {
const list = [librarySidebar];
if (compareSidebar) list.push(compareSidebar);
return list;
}
function renderTreeNodes(nodes, sidebar, depth = 0) {
if (!nodes || !nodes.length) return '';
const filter = (document.getElementById(sidebar.searchId)?.value || '').toLowerCase();
const sel = sidebar.getSelectedNode?.();
return nodes.map((node) => {
const name = node.name || node.nodeName;
const children = node.children || [];
const matches = !filter ||
name.toLowerCase().includes(filter) ||
(node.nodePath || '').toLowerCase().includes(filter) ||
children.some((c) => JSON.stringify(c).toLowerCase().includes(filter));
if (!matches && filter) return '';
const active = sel?.id === node.id;
const hasKids = children.length > 0;
const count = node.imageCount ? ` (${node.imageCount})` : '';
const indent = depth > 0 ? ` style="padding-left:${depth * 0.5}rem"` : '';
if (hasKids) {
return `
No nodes yet. Create folders on disk or use Manage to add zones.
'; } el.innerHTML = html; document.getElementById(allBtnId)?.addEventListener('click', () => { sidebar.onClearNode?.(); renderAllTrees(treeData, imageTypes); }); el.querySelectorAll('.dda-tree-node-btn').forEach((btn) => { btn.addEventListener('click', (e) => { e.stopPropagation(); sidebar.onNodeSelect?.({ id: parseInt(btn.dataset.nodeId, 10), path: btn.dataset.nodePath, }); renderAllTrees(treeData, imageTypes); }); }); } function renderAllTrees(tree, types) { if (tree) treeData = tree; getSidebars().forEach((sidebar) => renderTreeSidebar(sidebar, treeData, types)); } function renderTree(tree, types) { renderAllTrees(tree, types); } function registerCompareTreeSidebar(config) { compareSidebar = config; if (treeData) renderAllTrees(treeData, imageTypes); } document.getElementById('lib-tree-search')?.addEventListener('input', () => { if (treeData) renderAllTrees(treeData, imageTypes); }); document.getElementById('compare-tree-search')?.addEventListener('input', () => { if (treeData) renderAllTrees(treeData, imageTypes); }); async function loadTree() { const data = await ddaApi('GET', '/api/dda/tree'); renderAllTrees(data, data.imageTypes); populateNodeSelects(data.tree); if (typeof populateManageNodeSelect === 'function') populateManageNodeSelect(); return data; } window.loadTree = loadTree; window.renderTree = renderTree; window.renderAllTrees = renderAllTrees; window.registerCompareTreeSidebar = registerCompareTreeSidebar; function flattenNodes(nodes, out = []) { (nodes || []).forEach((n) => { out.push(n); flattenNodes(n.children, out); }); return out; } function populateNodeSelects(tree) { const flat = flattenNodes(tree || []); const opts = '' + flat.map((n) => ``).join(''); ['upload-node', 'manage-parent', 'move-parent', 'add-child-parent'].forEach((id) => { const el = document.getElementById(id); if (el) el.innerHTML = id === 'manage-parent' ? '' + flat.map((n) => ``).join('') : opts; }); const typeSel = document.getElementById('upload-image-type'); if (typeSel && imageTypes.length) { typeSel.innerHTML = imageTypes.map((t) => ``).join(''); } syncUploadNodeSelect(); } function syncUploadNodeSelect() { const sel = document.getElementById('upload-node'); const node = window.ddaState?.selectedNode; if (sel && node?.id) sel.value = String(node.id); } /* Manage modal */ function openManage() { document.getElementById('dda-manage-modal')?.classList.remove('hidden'); if (treeData) populateNodeSelects(treeData.tree || treeData); } function closeManage() { document.getElementById('dda-manage-modal')?.classList.add('hidden'); } document.getElementById('btn-manage-library')?.addEventListener('click', openManage); document.getElementById('dda-manage-close')?.addEventListener('click', closeManage); document.getElementById('btn-add-node')?.addEventListener('click', async () => { const parentVal = document.getElementById('add-child-parent')?.value; const name = document.getElementById('add-node-name')?.value?.trim(); const type = document.getElementById('add-node-type')?.value || 'Folder'; if (!name) return showDdaError('Enter a node name.'); const body = { node_name: name, node_type: type, parent_id: parentVal ? parseInt(parentVal, 10) : null }; try { await ddaApi('POST', '/api/dda/tree/nodes', { body: JSON.stringify(body) }); document.getElementById('add-node-name').value = ''; showDdaSuccess('Node created.'); await window.ddaState.rescan(); } catch (err) { showDdaError(err.message); } }); document.getElementById('btn-rename-node')?.addEventListener('click', async () => { const id = parseInt(document.getElementById('manage-node-select')?.value || '0', 10); const name = document.getElementById('rename-node-name')?.value?.trim(); if (!id || !name) return showDdaError('Select a node and enter a new name.'); try { await ddaApi('PUT', `/api/dda/tree/nodes/${id}/rename`, { body: JSON.stringify({ node_name: name }) }); showDdaSuccess('Node renamed.'); await window.ddaState.rescan(); } catch (err) { showDdaError(err.message); } }); document.getElementById('btn-delete-node')?.addEventListener('click', async () => { const id = parseInt(document.getElementById('manage-node-select')?.value || '0', 10); if (!id || !confirm('Delete this node? Check delete files if removing images.')) return; const deleteFiles = document.getElementById('delete-files-check')?.checked; try { await ddaApi('DELETE', `/api/dda/tree/nodes/${id}`, { body: JSON.stringify({ delete_files: !!deleteFiles }) }); showDdaSuccess('Node deleted.'); await window.ddaState.rescan(); } catch (err) { showDdaError(err.message); } }); document.getElementById('btn-move-node')?.addEventListener('click', async () => { const id = parseInt(document.getElementById('manage-node-select')?.value || '0', 10); const parentVal = document.getElementById('move-parent')?.value; if (!id) return showDdaError('Select a node to move.'); try { await ddaApi('POST', `/api/dda/tree/nodes/${id}/move`, { body: JSON.stringify({ parent_id: parentVal ? parseInt(parentVal, 10) : null }), }); showDdaSuccess('Node moved.'); await window.ddaState.rescan(); } catch (err) { showDdaError(err.message); } }); function populateManageNodeSelect() { if (!treeData) return; const flat = flattenNodes(treeData.tree || treeData || []); const sel = document.getElementById('manage-node-select'); if (sel) sel.innerHTML = flat.map((n) => ``).join(''); } document.getElementById('dda-manage-modal')?.addEventListener('click', (e) => { if (e.target.id === 'dda-manage-modal') closeManage(); }); window.populateManageNodeSelect = populateManageNodeSelect;