diff --git a/renderer/App.jsx b/renderer/App.jsx new file mode 100644 index 0000000..6491792 --- /dev/null +++ b/renderer/App.jsx @@ -0,0 +1,104 @@ +import React, { useState, useEffect } from 'react'; +import Settings from './Settings.jsx'; + +export default function App() { + const [folder, setFolder] = useState(''); + const [repoName, setRepoName] = useState(''); + const [platform, setPlatform] = useState('github'); + const [status, setStatus] = useState(''); + const [showSettings, setShowSettings] = useState(false); + const [branches, setBranches] = useState([]); + const [selectedBranch, setSelectedBranch] = useState('master'); + const [logs, setLogs] = useState([]); + const [progress, setProgress] = useState(0); + + async function selectFolder() { + const selected = await window.electronAPI.selectFolder(); + if (selected) setFolder(selected); + // Branches laden + if (selected) { + const branchList = await window.electronAPI.getBranches({ folder: selected }); + setBranches(branchList); + if (branchList.includes('master')) setSelectedBranch('master'); + } + } + + async function createRepoHandler() { + if (!repoName) return alert('Repo Name required!'); + setStatus('Creating repository...'); + const result = await window.electronAPI.createRepo({ name: repoName, platform }); + setStatus(result ? 'Repository created!' : 'Failed to create repository.'); + } + + async function pushProjectHandler() { + if (!folder) return alert('Select a project folder first!'); + setStatus('Pushing project...'); + setProgress(0); + const onProgress = (p) => setProgress(p); // Callback für Fortschritt + const result = await window.electronAPI.pushProject({ folder, branch: selectedBranch, onProgress }); + setStatus(result ? 'Project pushed!' : 'Failed to push project.'); + if (result) { + const logList = await window.electronAPI.getCommitLogs({ folder }); + setLogs(logList); + } + } + + return ( +
+

Git Manager GUI - High-End

+ + {showSettings && } + +
+ + +
+ +
+ + {folder} +
+ +
+ + +
+ +
+ setRepoName(e.target.value)} /> + +
+ +
+ +
+ +
+ + +
+ +
+ Status: {status} +
+ +
+

Commit Logs:

+
    + {logs.map((log, i) => ( +
  • {log}
  • + ))} +
+
+
+ ); +} diff --git a/renderer/Settings.jsx b/renderer/Settings.jsx new file mode 100644 index 0000000..0c5450e --- /dev/null +++ b/renderer/Settings.jsx @@ -0,0 +1,41 @@ +import React, { useState, useEffect } from 'react'; + +export default function Settings() { + const [githubToken, setGithubToken] = useState(''); + const [giteaToken, setGiteaToken] = useState(''); + const [giteaURL, setGiteaURL] = useState(''); + + useEffect(() => { + window.electronAPI.loadCredentials().then(data => { + if (data) { + setGithubToken(data.githubToken || ''); + setGiteaToken(data.giteaToken || ''); + setGiteaURL(data.giteaURL || ''); + } + }); + }, []); + + const save = () => { + window.electronAPI.saveCredentials({ githubToken, giteaToken, giteaURL }); + alert('Settings saved securely!'); + } + + return ( +
+

Settings

+
+ + setGithubToken(e.target.value)} /> +
+
+ + setGiteaToken(e.target.value)} /> +
+
+ + setGiteaURL(e.target.value)} /> +
+ +
+ ); +} diff --git a/renderer/index.html b/renderer/index.html new file mode 100644 index 0000000..cf0be02 --- /dev/null +++ b/renderer/index.html @@ -0,0 +1,89 @@ + + + + + Git Manager Explorer + + + + +
+
+ + + + + +
+ +
+ + +
+
+
+
+ + +
+ + +
+ + +
+ +
+ +
+
+
+
+
+ +
+

Preview

+
Select a file to preview
+
+ +
+

Commit Logs

+
No commits yet.
+
+
+
+ + + +
+ + + + \ No newline at end of file diff --git a/renderer/index.js b/renderer/index.js new file mode 100644 index 0000000..63a54d1 --- /dev/null +++ b/renderer/index.js @@ -0,0 +1,7 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.jsx'; + +const container = document.getElementById('root'); +const root = createRoot(container); +root.render(); diff --git a/renderer/renderer.js b/renderer/renderer.js new file mode 100644 index 0000000..891e681 --- /dev/null +++ b/renderer/renderer.js @@ -0,0 +1,621 @@ +// 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); \ No newline at end of file diff --git a/renderer/style.css b/renderer/style.css new file mode 100644 index 0000000..544648c --- /dev/null +++ b/renderer/style.css @@ -0,0 +1,53 @@ +/* Dark Explorer Style */ +:root { + --bg: #141423; + --panel: #1f1f2f; + --muted: #9aa0b4; + --accent: #00bcd4; + --card: #242438; + --panel-2: #2b2b3d; +} + +html,body { height:100%; margin:0; background:var(--bg); color:#e6e6e6; font-family: "Segoe UI", Roboto, Arial; } +#toolbar { display:flex; align-items:center; gap:8px; padding:10px; background:var(--panel); border-bottom:1px solid #2f2f4f; } +#toolbar button, #toolbar select { background:var(--accent); color:#022; border:none; padding:6px 10px; border-radius:6px; cursor:pointer; } +#toolbar .status { margin-left:8px; color:var(--muted); font-weight:600; } + +#main { display:flex; height: calc(100vh - 52px); gap:10px; padding:12px; box-sizing:border-box; } +aside#explorerPanel { width:320px; background:var(--panel-2); border-radius:8px; padding:10px; overflow:auto; box-shadow: 0 6px 18px rgba(0,0,0,0.6); } +#fileTree { font-family: monospace; font-size:13px; } + +#detailsPanel { flex:1; display:flex; flex-direction:column; gap:12px; overflow:auto; } +.card { background:var(--card); padding:12px; border-radius:8px; box-shadow: 0 6px 18px rgba(0,0,0,0.6); } +input, select { background:#222231; color:#e6e6e6; border:1px solid #3a3a5a; padding:6px; border-radius:6px; width:100%; box-sizing:border-box; } + +/* FIX: Styling für Checkboxen, damit sie nicht die volle Breite einnehmen */ +input[type="checkbox"] { + width: auto; + margin-right: 8px; + vertical-align: middle; + cursor: pointer; +} + +.file-node { padding:6px 8px; border-radius:6px; margin:2px 0; } +.file-node:hover { background: rgba(255,255,255,0.02); cursor:pointer; } +.file-node.folder { font-weight:700; color:#cfefff; } +.file-node.file { color:#dfe7ff; } + +.preview-content { background:#0f1020; color:#dcecff; padding:10px; border-radius:6px; height:260px; overflow:auto; white-space:pre-wrap; font-family: Consolas, Monaco, monospace; } + +#logs { max-height:220px; overflow:auto; font-family:monospace; font-size:13px; background:#0b0b12; padding:8px; border-radius:6px; } +#logs div { padding:6px; border-bottom:1px solid #1b1b2a; } + +.modal { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,0.6); } +.modalContent { width:420px; padding:16px; } +.modal.hidden { display:none; } + +.context-menu { + font-family: sans-serif; + font-size: 13px; + box-shadow: 0 2px 10px rgba(0,0,0,0.5); +} +.context-menu div:hover { + background-color: #555; +} \ No newline at end of file