// renderer.js — Explorer with repo drag-export and folder upload+git push + progress UI const $ = id => document.getElementById(id); let selectedFolder = null; let giteaCache = {}; // cache repo contents function setStatus(txt) { const s = $('status'); if (s) s.innerText = txt || ''; } /* ------------------------- Small dynamic progress UI (created if not present) ------------------------- */ function ensureProgressUI() { if (document.getElementById('folderProgressContainer')) return; const container = document.createElement('div'); container.id = 'folderProgressContainer'; container.style.position = 'fixed'; container.style.left = '50%'; container.style.top = '12px'; container.style.transform = 'translateX(-50%)'; container.style.zIndex = '10000'; container.style.width = '480px'; container.style.maxWidth = '90%'; container.style.padding = '6px'; container.style.background = 'rgba(20,20,30,0.95)'; container.style.borderRadius = '8px'; container.style.boxShadow = '0 6px 18px rgba(0,0,0,0.45)'; container.style.color = '#fff'; container.style.fontFamily = 'sans-serif'; container.style.display = 'none'; const text = document.createElement('div'); text.id = 'folderProgressText'; text.style.marginBottom = '6px'; text.style.fontSize = '13px'; container.appendChild(text); const barWrap = document.createElement('div'); barWrap.style.width = '100%'; barWrap.style.height = '10px'; barWrap.style.background = '#333'; barWrap.style.borderRadius = '6px'; barWrap.style.overflow = 'hidden'; const bar = document.createElement('div'); bar.id = 'folderProgressBar'; bar.style.width = '0%'; bar.style.height = '100%'; bar.style.background = '#4caf50'; bar.style.transition = 'width 150ms linear'; barWrap.appendChild(bar); container.appendChild(barWrap); document.body.appendChild(container); } function showProgress(percent, text) { ensureProgressUI(); const container = document.getElementById('folderProgressContainer'); const bar = document.getElementById('folderProgressBar'); const txt = document.getElementById('folderProgressText'); txt.innerText = text || ''; bar.style.width = `${percent}%`; container.style.display = 'block'; } function hideProgress() { const container = document.getElementById('folderProgressContainer'); if (container) container.style.display = 'none'; } /* ------------------------- Initialization & wiring ------------------------- */ async function init() { const creds = await window.electronAPI.loadCredentials(); if (creds) { if ($('githubToken')) $('githubToken').value = creds.githubToken || ''; if ($('giteaToken')) $('giteaToken').value = creds.giteaToken || ''; if ($('giteaURL')) $('giteaURL').value = creds.giteaURL || ''; } if ($('btnLoadGiteaRepos')) $('btnLoadGiteaRepos').addEventListener('click', loadGiteaRepos); if ($('btnSelectFolder')) $('btnSelectFolder').addEventListener('click', selectLocalFolder); if ($('btnPush')) $('btnPush').addEventListener('click', pushLocalFolder); if ($('btnCreateRepo')) $('btnCreateRepo').addEventListener('click', createRepoHandler); if ($('btnSettings')) $('btnSettings').addEventListener('click', () => $('settingsModal').classList.remove('hidden')); if ($('btnCloseSettings')) $('btnCloseSettings').addEventListener('click', () => $('settingsModal').classList.add('hidden')); if ($('btnSaveSettings')) $('btnSaveSettings').addEventListener('click', saveSettings); // global drag-over/drop: prevent default document.addEventListener('dragover', e => e.preventDefault()); document.addEventListener('drop', e => e.preventDefault()); // subscribe to progress events window.electronAPI.onFolderUploadProgress((payload) => { const { processed, total, percent } = payload; showProgress(percent, `Upload: ${processed}/${total} (${percent}%)`); if (processed >= total) setTimeout(hideProgress, 600); }); window.electronAPI.onFolderDownloadProgress((payload) => { const { processed, total, percent } = payload; showProgress(percent, `Download: ${processed}/${total} (${percent}%)`); if (processed >= total) setTimeout(hideProgress, 600); }); window.electronAPI.onPushProgress(p => setStatus('Pushing... ' + p + '%')); ensureProgressUI(); } /* ------------------------- Settings / buttons ------------------------- */ async function saveSettings() { const data = { githubToken: $('githubToken')?.value, giteaToken: $('giteaToken')?.value, giteaURL: $('giteaURL')?.value }; const r = await window.electronAPI.saveCredentials(data); setStatus(r.ok ? 'Settings saved' : 'Save failed: ' + r.error); $('settingsModal').classList.add('hidden'); } async function loadGiteaRepos() { setStatus('Loading Gitea repos...'); const creds = await window.electronAPI.loadCredentials(); if (!creds || !creds.giteaToken || !creds.giteaURL) { setStatus('Set Gitea token & URL in Settings'); return; } const res = await window.electronAPI.listGiteaRepos({ token: creds.giteaToken, url: creds.giteaURL }); if (!res.ok) { setStatus('Failed to load repos: ' + res.error); return; } renderGiteaRoots(res.repos); setStatus(`Loaded ${res.repos.length} repos`); } async function selectLocalFolder() { const folder = await window.electronAPI.selectFolder(); if (!folder) return; selectedFolder = folder; setStatus('Selected local folder: ' + folder); await refreshLocalTree(folder); await loadBranches(folder); await loadCommitLogs(folder); } async function pushLocalFolder() { if (!selectedFolder) return alert('Select local folder first'); const branch = $('branchSelect')?.value || 'main'; setStatus('Pushing...'); const res = await window.electronAPI.pushProject({ folder: selectedFolder, branch, repoName: $('repoName')?.value, platform: $('platform')?.value }); setStatus(res.ok ? 'Push succeeded' : 'Push failed: ' + res.error); if (res.ok) loadCommitLogs(selectedFolder); } async function createRepoHandler() { const name = $('repoName')?.value?.trim(); const platform = $('platform')?.value; const license = $('licenseSelect')?.value || ''; const autoInit = $('createReadme')?.checked; if (!name) return alert('Repo name required'); setStatus('Creating repo...'); const res = await window.electronAPI.createRepo({ name, platform, license, autoInit }); setStatus(res.ok ? 'Repo created' : 'Create failed: ' + res.error); } /* ------------------------- Render Gitea roots + draggable/droppable nodes ------------------------- */ function renderGiteaRoots(repos) { const tree = $('fileTree'); const prev = tree.querySelector('[data-is-gitea-root="1"]'); if (prev) { const next = prev.nextSibling; prev.remove(); if (next && next.classList && next.classList.contains('gitea-children-wrapper')) next.remove(); } const groot = document.createElement('div'); groot.className = 'file-node folder'; groot.dataset.isGiteaRoot = '1'; groot.innerText = 'Gitea Repositories'; groot.style.fontWeight = '800'; const wrapper = document.createElement('div'); wrapper.className = 'gitea-children-wrapper'; wrapper.style.marginLeft = '6px'; repos.forEach(repo => { let owner = (repo.owner && (repo.owner.login || repo.owner.username)) || null; let repoName = repo.name; if (!owner && repo.full_name && repo.full_name.includes('/')) { const parts = repo.full_name.split('/'); owner = parts[0]; repoName = parts[1]; } const rdiv = document.createElement('div'); rdiv.className = 'file-node folder'; rdiv.innerText = (repo.full_name || `${owner}/${repoName}`); rdiv.dataset.giteaOwner = owner; rdiv.dataset.giteaRepo = repoName; rdiv.dataset.giteaClone = repo.clone_url || repo.clone_url_ssh || (repo.html_url ? `${repo.html_url}.git` : ''); rdiv.style.paddingLeft = '8px'; rdiv.draggable = true; // dragstart rdiv.addEventListener('dragstart', async (ev) => { ev.preventDefault(); setStatus(`Preparing download for ${owner}/${repoName} ...`); showProgress(0, `Preparing ${owner}/${repoName}...`); const res = await window.electronAPI.prepareDownloadDrag({ owner, repo: repoName, path: '' }); if (!res.ok) { setStatus('Prepare failed: ' + res.error); hideProgress(); return; } const tempPath = res.tempPath; window.electronAPI.startNativeDrag(tempPath); setStatus('Drag started — drop to desktop or file manager.'); hideProgress(); }); // drop rdiv.addEventListener('dragover', (ev) => { ev.preventDefault(); rdiv.classList.add('drag-target'); }); rdiv.addEventListener('dragleave', () => { rdiv.classList.remove('drag-target'); }); rdiv.addEventListener('drop', async (ev) => { ev.preventDefault(); rdiv.classList.remove('drag-target'); const dt = ev.dataTransfer; if (!dt || !dt.files || dt.files.length === 0) { setStatus('No files/folders dropped.'); return; } const paths = Array.from(dt.files).map(f => f.path); for (const p of paths) { setStatus(`Uploading ${p} to ${owner}/${repoName} ...`); showProgress(0, `Uploading ${ppathBasename(p)} → ${owner}/${repoName}`); const res = await window.electronAPI.uploadAndPush({ localFolder: p, owner, repo: repoName, destPath: '', cloneUrl: rdiv.dataset.giteaClone, branch: 'main' }); if (!res.ok) setStatus('Upload failed: ' + res.error); else setStatus(res.usedGit ? `Upload+Push OK (git used)` : `Upload OK (API used)`); hideProgress(); } try { await expandGiteaDir(rdiv, owner, repoName, rdiv.dataset.giteaPath || ''); } catch (_) {} }); // Click - FIXED: ref: 'main' explicitly added rdiv.addEventListener('click', async (ev) => { ev.stopPropagation(); if (rdiv._expanded) { const next = rdiv.nextSibling; if (next && next.classList && next.classList.contains('gitea-children')) next.remove(); rdiv._expanded = false; return; } setStatus(`Loading ${owner}/${repoName}...`); const res = await window.electronAPI.getGiteaRepoContents({ owner, repo: repoName, path: '', ref: 'main' }); if (!res.ok) { setStatus('Failed to load repo: ' + res.error); return; } const cont = document.createElement('div'); cont.className = 'gitea-children'; cont.style.marginLeft = '12px'; (res.items || []).forEach(item => { const node = document.createElement('div'); node.className = 'file-node ' + (item.type === 'dir' ? 'folder' : 'file'); node.innerText = item.name; node.dataset.giteaOwner = owner; node.dataset.giteaRepo = repoName; node.dataset.giteaPath = item.path; node.style.paddingLeft = '8px'; node.addEventListener('dragover', e => e.preventDefault()); node.addEventListener('drop', async (e) => { e.preventDefault(); const dt = e.dataTransfer; if (!dt || !dt.files || dt.files.length === 0) { setStatus('No files dropped'); return; } const dropped = Array.from(dt.files).map(f => f.path); for (const p of dropped) { setStatus(`Uploading ${p} to ${owner}/${repoName}/${item.path} ...`); showProgress(0, `Uploading ${ppathBasename(p)} → ${owner}/${repoName}/${item.path}`); const res = await window.electronAPI.uploadAndPush({ localFolder: p, owner, repo: repoName, destPath: item.path, cloneUrl: rdiv.dataset.giteaClone, branch: 'main' }); if (!res.ok) setStatus('Upload failed: ' + res.error); else setStatus(res.usedGit ? `Upload+Push OK (git used)` : `Upload OK (API used)`); hideProgress(); } await expandGiteaDir(node, owner, repoName, item.path); }); node.addEventListener('click', async (e) => { e.stopPropagation(); if (item.type === 'dir') await expandGiteaDir(node, owner, repoName, item.path); else await previewGiteaFile(owner, repoName, item.path); document.querySelectorAll('.file-node').forEach(n => n.classList.remove('active')); node.classList.add('active'); }); node.addEventListener('contextmenu', (ev) => { ev.preventDefault(); showGiteaContextMenu(node, ev.clientX, ev.clientY); }); cont.appendChild(node); }); rdiv.after(cont); rdiv._expanded = true; setStatus(''); }); // Context menu for Repo rdiv.addEventListener('contextmenu', (ev) => { ev.preventDefault(); showRepoContextMenu(rdiv, ev.clientX, ev.clientY, owner, repoName, rdiv.dataset.giteaClone); }); wrapper.appendChild(rdiv); }); tree.prepend(groot); groot.after(wrapper); } function ppathBasename(p) { try { return p.split(/[\\/]/).pop(); } catch (_) { return p; } } /* Expand dir / render children / preview */ async function expandGiteaDir(nodeEl, owner, repo, dirPath) { if (nodeEl._expanded) { const next = nodeEl.nextSibling; if (next && next.classList && next.classList.contains('gitea-sub')) next.remove(); nodeEl._expanded = false; return; } const key = `${owner}/${repo}:${dirPath}`; const ref = 'main'; // FIXED: Always use 'main' if (giteaCache[key]) { renderGiteaChildren(nodeEl, giteaCache[key], owner, repo); nodeEl._expanded = true; return; } setStatus(`Loading ${dirPath}...`); const res = await window.electronAPI.getGiteaRepoContents({ owner, repo, path: dirPath, ref }); if (!res.ok) { setStatus('Failed: ' + res.error); return; } giteaCache[key] = res.items; renderGiteaChildren(nodeEl, res.items, owner, repo); nodeEl._expanded = true; setStatus(''); } function renderGiteaChildren(parentNode, items, owner, repo) { const container = document.createElement('div'); container.className = 'gitea-sub'; container.style.marginLeft = '12px'; items.forEach(item => { const el = document.createElement('div'); el.className = 'file-node ' + (item.type === 'dir' ? 'folder' : 'file'); el.innerText = item.name; el.dataset.giteaOwner = owner; el.dataset.giteaRepo = repo; el.dataset.giteaPath = item.path; el.style.paddingLeft = '8px'; el.addEventListener('click', async (ev) => { ev.stopPropagation(); if (item.type === 'dir') await expandGiteaDir(el, owner, repo, item.path); else await previewGiteaFile(owner, repo, item.path); document.querySelectorAll('.file-node').forEach(n => n.classList.remove('active')); el.classList.add('active'); }); el.addEventListener('dragover', e => e.preventDefault()); el.addEventListener('drop', async (e) => { e.preventDefault(); const paths = Array.from(e.dataTransfer.files).map(f => f.path); for (const p of paths) { setStatus(`Uploading ${p} to ${owner}/${repo}/${item.path} ...`); showProgress(0, `Uploading ${ppathBasename(p)} → ${owner}/${repo}/${item.path}`); const res = await window.electronAPI.uploadAndPush({ localFolder: p, owner, repo, destPath: item.path, cloneUrl: parentNode.previousSibling?.dataset?.giteaClone, branch: 'main' }); if (!res.ok) setStatus('Upload failed: ' + res.error); else setStatus(res.usedGit ? `Upload+Push OK (git used)` : `Upload OK (API used)`); hideProgress(); } await expandGiteaDir(el, owner, repo, item.path); }); el.addEventListener('contextmenu', (ev) => { ev.preventDefault(); showGiteaContextMenu(el, ev.clientX, ev.clientY); }); container.appendChild(el); }); parentNode.after(container); } async function previewGiteaFile(owner, repo, filePath) { setStatus(`Loading file ${filePath}...`); const res = await window.electronAPI.getGiteaFileContent({ owner, repo, path: filePath, ref: 'main' }); if (!res.ok) { setStatus('Failed: ' + res.error); $('previewTitle').innerText = filePath; $('previewContent').innerText = ''; return; } $('previewTitle').innerText = `${owner}/${repo}:${filePath}`; $('previewContent').innerText = res.content; setStatus(''); } /* Local tree */ async function refreshLocalTree(folder) { const res = await window.electronAPI.getFileTree({ folder, exclude: ['node_modules'], maxDepth: 5 }); const container = $('fileTree'); const existingLocal = container.querySelector('[data-local-root="1"]'); if (existingLocal) { const next = existingLocal.nextSibling; existingLocal.remove(); if (next && next.classList && next.classList.contains('local-children')) next.remove(); } if (!res.ok) { setStatus('Local tree error: ' + res.error); return; } const root = document.createElement('div'); root.className = 'file-node folder'; root.dataset.localRoot = '1'; root.innerText = 'Local: ' + folder.split(/[\\/]/).pop(); root.style.fontWeight = '800'; root.addEventListener('click', () => { const next = root.nextSibling; if (next && next.classList && next.classList.contains('local-children')) next.classList.toggle('hidden'); }); const wrapper = document.createElement('div'); wrapper.className = 'local-children'; wrapper.style.marginLeft = '8px'; function build(node, parent) { const el = document.createElement('div'); el.className = 'file-node ' + (node.isDirectory ? 'folder' : 'file'); el.innerText = node.name; el.dataset.path = node.path; el.style.paddingLeft = (node.depth * 10 + 8) + 'px'; el.addEventListener('click', async (ev) => { ev.stopPropagation(); if (!node.isDirectory) { const r = await window.electronAPI.readFile({ path: node.path }); if (r.ok) { $('previewTitle').innerText = node.path; $('previewContent').innerText = r.content; } else setStatus('Read failed: ' + r.error); } }); parent.appendChild(el); if (node.children && node.children.length) node.children.forEach(c => build(c, parent)); } res.tree.forEach(n => build(n, wrapper)); const tree = $('fileTree'); tree.appendChild(root); root.after(wrapper); } /* Branches & commits */ async function loadBranches(folder) { const res = await window.electronAPI.getBranches({ folder }); const sel = $('branchSelect'); sel.innerHTML = ''; if (res.ok) res.branches.forEach(b => sel.appendChild(new Option(b, b))); else sel.appendChild(new Option('main','main')); } async function loadCommitLogs(folder) { const res = await window.electronAPI.getCommitLogs({ folder }); const container = $('logs'); container.innerHTML = ''; if (!res.ok) return container.innerText = 'No logs or error: ' + res.error; res.logs.forEach(l => { const d = document.createElement('div'); d.innerText = l; container.appendChild(d); }); } /* Context menu */ function showGiteaContextMenu(node, x, y) { const old = document.getElementById('ctxMenu'); if (old) old.remove(); const menu = document.createElement('div'); menu.id = 'ctxMenu'; menu.style.position = 'fixed'; menu.style.left = x + 'px'; menu.style.top = y + 'px'; menu.style.background = '#1b1b2a'; menu.style.border = '1px solid #333'; menu.style.padding = '6px'; menu.style.zIndex = '9999'; menu.style.borderRadius = '6px'; menu.style.color = '#fff'; const owner = node.dataset.giteaOwner; const repo = node.dataset.giteaRepo; const filePath = node.dataset.giteaPath || ''; if (node.classList.contains('file')) { const dl = document.createElement('div'); dl.innerText = 'Download File'; dl.style.padding = '6px'; dl.style.cursor = 'pointer'; dl.onclick = async () => { const res = await window.electronAPI.downloadGiteaFile({ owner, repo, path: filePath }); setStatus(res.ok ? `Saved to ${res.savedTo}` : 'Download failed: ' + res.error); menu.remove(); }; menu.appendChild(dl); } if (node.classList.contains('folder')) { const dlf = document.createElement('div'); dlf.innerText = 'Download Folder (recursive)'; dlf.style.padding = '6px'; dlf.style.cursor = 'pointer'; dlf.onclick = async () => { setStatus(`Choose local folder to save ${owner}/${repo}/${filePath} ...`); showProgress(0, 'Starting download...'); const res = await window.electronAPI.downloadGiteaFolder({ owner, repo, path: filePath }); setStatus(res.ok ? `Folder downloaded to ${res.savedTo}` : 'Folder download failed: ' + res.error); hideProgress(); menu.remove(); }; menu.appendChild(dlf); } const up = document.createElement('div'); up.innerText = 'Upload Local Folder Here'; up.style.padding = '6px'; up.style.cursor = 'pointer'; up.onclick = async () => { const folderSel = await window.electronAPI.selectFolder(); if (!folderSel) { setStatus('No folder selected'); menu.remove(); return; } setStatus(`Uploading local folder ${folderSel} to ${owner}/${repo}/${filePath} ...`); showProgress(0, 'Starting upload...'); const res = await window.electronAPI.uploadAndPush({ localFolder: folderSel, owner, repo, destPath: filePath, cloneUrl: node.dataset.giteaClone, branch: 'main' }); setStatus(res.ok ? (res.usedGit ? 'Folder uploaded and pushed (git used)' : 'Folder uploaded (API)') : 'Upload failed: ' + res.error); hideProgress(); menu.remove(); if (node.classList.contains('folder')) await expandGiteaDir(node, owner, repo, filePath); }; menu.appendChild(up); if (node.classList.contains('file')) { const upf = document.createElement('div'); upf.innerText = 'Upload & Overwrite File'; upf.style.padding = '6px'; upf.style.cursor = 'pointer'; upf.onclick = async () => { const sel = await window.electronAPI.selectFile(); if (!sel.ok || !sel.files || sel.files.length === 0) { setStatus('No file selected'); menu.remove(); return; } const files = sel.files; setStatus(`Uploading ${files.length} file(s) to ${owner}/${repo}/${filePath} ...`); showProgress(0, 'Uploading files...'); const res = await window.electronAPI.uploadGiteaFile({ localPath: files, owner, repo, destPath: filePath, message: 'Upload via GUI', branch: 'main' }); setStatus(res.ok ? 'Upload complete' : 'Upload failed: ' + res.error); hideProgress(); menu.remove(); if (node.classList.contains('folder')) await expandGiteaDir(node, owner, repo, filePath); }; menu.appendChild(upf); } document.body.appendChild(menu); document.addEventListener('click', function handler() { menu.remove(); document.removeEventListener('click', handler); }); } /* Context menu for Repository Root */ function showRepoContextMenu(repoEl, x, y, owner, repoName, cloneUrl) { const old = document.getElementById('ctxMenu'); if (old) old.remove(); const menu = document.createElement('div'); menu.id = 'ctxMenu'; menu.style.position = 'fixed'; menu.style.left = x + 'px'; menu.style.top = y + 'px'; menu.style.background = '#1b1b2a'; menu.style.border = '1px solid #333'; menu.style.padding = '6px'; menu.style.zIndex = '9999'; menu.style.borderRadius = '6px'; menu.style.color = '#fff'; // Option 1: Download Repository const dlRepo = document.createElement('div'); dlRepo.innerText = 'Download Repository'; dlRepo.style.padding = '6px'; dlRepo.style.cursor = 'pointer'; dlRepo.style.borderBottom = '1px solid #333'; dlRepo.onclick = async () => { setStatus(`Downloading repository ${owner}/${repoName}...`); showProgress(0, 'Starting download...'); const res = await window.electronAPI.downloadGiteaFolder({ owner, repo: repoName, path: '' }); setStatus(res.ok ? `Repository downloaded to ${res.savedTo}` : 'Download failed: ' + res.error); hideProgress(); menu.remove(); }; menu.appendChild(dlRepo); // Option 2: Upload Folder to Repository const upRepo = document.createElement('div'); upRepo.innerText = 'Upload Folder to Repository'; upRepo.style.padding = '6px'; upRepo.style.cursor = 'pointer'; upRepo.style.borderBottom = '1px solid #333'; upRepo.onclick = async () => { const folderSel = await window.electronAPI.selectFolder(); if (!folderSel) { setStatus('No folder selected'); menu.remove(); return; } setStatus(`Uploading local folder to ${owner}/${repoName} ...`); showProgress(0, 'Starting upload...'); const res = await window.electronAPI.uploadAndPush({ localFolder: folderSel, owner, repo: repoName, destPath: '', cloneUrl: cloneUrl, branch: 'main' }); setStatus(res.ok ? (res.usedGit ? 'Upload & Push successful (git)' : 'Upload successful (API)') : 'Upload failed: ' + res.error); hideProgress(); menu.remove(); if (repoEl._expanded) { const next = repoEl.nextSibling; if (next && next.classList && next.classList.contains('gitea-children')) { next.remove(); repoEl._expanded = false; repoEl.click(); // Re-expand } } }; menu.appendChild(upRepo); // Option 3: Delete Repository const delRepo = document.createElement('div'); delRepo.innerText = 'Delete Repository'; delRepo.style.padding = '6px'; delRepo.style.cursor = 'pointer'; delRepo.style.color = '#ff5555'; delRepo.style.fontWeight = 'bold'; delRepo.onclick = async () => { if (!confirm(`Are you sure you want to DELETE ${owner}/${repoName}? This cannot be undone.`)) { menu.remove(); return; } setStatus(`Deleting repository ${owner}/${repoName}...`); const res = await window.electronAPI.deleteGiteaRepo({ owner, repo: repoName }); if (res.ok) { setStatus('Repository deleted successfully'); repoEl.remove(); } else { setStatus('Delete failed: ' + res.error); } menu.remove(); }; menu.appendChild(delRepo); document.body.appendChild(menu); const closeHandler = () => { menu.remove(); document.removeEventListener('click', closeHandler); }; setTimeout(() => document.addEventListener('click', closeHandler), 10); } /* Start */ window.addEventListener('DOMContentLoaded', init);