Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
013a03424d | ||
|
|
46f0c0c56b | ||
|
|
54b87bec66 | ||
|
|
069f1480b9 | ||
| 6f11730fc7 | |||
| e0827faf42 | |||
| 1041f39ced | |||
| 7a43d24a32 | |||
| d6968a4954 | |||
| 464d15464a | |||
| da47343b2e | |||
| d41865608f | |||
| e4b1215aa7 | |||
| 1d7b5e8d6e | |||
| e79c0f411d | |||
| 9da186e5d2 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "git-manager-gui",
|
"name": "git-manager-gui",
|
||||||
"version": "2.0.6",
|
"version": "2.0.9",
|
||||||
"description": "Git Manager GUI - Verwaltung von Git Repositories",
|
"description": "Git Manager GUI - Verwaltung von Git Repositories",
|
||||||
"author": "M_Viper",
|
"author": "M_Viper",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
getGithubCurrentUser: () => ipcRenderer.invoke('get-github-current-user'),
|
getGithubCurrentUser: () => ipcRenderer.invoke('get-github-current-user'),
|
||||||
getGithubUserHeatmap: (data) => ipcRenderer.invoke('get-github-user-heatmap', data),
|
getGithubUserHeatmap: (data) => ipcRenderer.invoke('get-github-user-heatmap', data),
|
||||||
getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data),
|
getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data),
|
||||||
|
listGiteaTrash: (data) => ipcRenderer.invoke('list-gitea-trash', data),
|
||||||
|
purgeGiteaTrash: (data) => ipcRenderer.invoke('purge-gitea-trash', data),
|
||||||
|
restoreGiteaTrashItem: (data) => ipcRenderer.invoke('restore-gitea-trash-item', data),
|
||||||
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data),
|
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data),
|
||||||
readGiteaFile: (data) => ipcRenderer.invoke('read-gitea-file', data),
|
readGiteaFile: (data) => ipcRenderer.invoke('read-gitea-file', data),
|
||||||
writeGiteaFile: (data) => ipcRenderer.invoke('write-gitea-file', data),
|
writeGiteaFile: (data) => ipcRenderer.invoke('write-gitea-file', data),
|
||||||
@@ -175,5 +178,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
// Utility
|
// Utility
|
||||||
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
|
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
|
||||||
openExternalUrl: (url) => ipcRenderer.invoke('open-external-url', url),
|
openExternalUrl: (url) => ipcRenderer.invoke('open-external-url', url),
|
||||||
debugToMain: (level, message, payload) => ipcRenderer.send('renderer-debug-log', { level, message, payload })
|
debugToMain: (level, message, payload) => ipcRenderer.send('renderer-debug-log', { level, message, payload }),
|
||||||
|
|
||||||
|
// Debugging & Diagnostics
|
||||||
|
getDebugInfo: () => ipcRenderer.invoke('get-debug-info'),
|
||||||
|
clearCache: (type) => ipcRenderer.invoke('clear-cache', type || 'all')
|
||||||
});
|
});
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
<span class="toolbar-kicker">Workspace Control</span>
|
<span class="toolbar-kicker">Workspace Control</span>
|
||||||
<strong>Git Manager Explorer Pro</strong>
|
<strong>Git Manager Explorer Pro</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<span id="project-toolbar-title" class="toolbar-project-title"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="toolbar-top-actions">
|
<div class="toolbar-top-actions">
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
|
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
|
||||||
<button id="btnOpenMigration" title="Repository von GitHub/GitLab zu Gitea migrieren">📥 Migrieren</button>
|
<button id="btnOpenMigration" title="Repository von GitHub/GitLab zu Gitea migrieren">📥 Migrieren</button>
|
||||||
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
|
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
|
||||||
|
<button id="btnGlobalTrash" class="hidden" title="Globalen Papierkorb über alle Repositories anzeigen">🗂️ Global Trash</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tool-group tool-group--utility">
|
<div class="tool-group tool-group--utility">
|
||||||
@@ -80,10 +82,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<button id="btnCommits" class="hidden" title="Commit History anzeigen">📊 Commits</button>
|
<button id="btnCommits" class="hidden" title="Commit History anzeigen">📊 Commits</button>
|
||||||
<button id="btnReleases" class="hidden" title="Releases anzeigen">📦 Releases</button>
|
<button id="btnReleases" class="hidden" title="Releases anzeigen">📦 Releases</button>
|
||||||
|
<button id="btnTrash" class="hidden" title="Papierkorb anzeigen">🗑️ Papierkorb</button>
|
||||||
|
<button id="btnPurgeTrash" class="hidden" title="Papierkorb älter als 7 Tage leeren">🧹 Purge 7d</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="project-gravur-bar"><span id="project-gravur-title" class="project-gravur-title"></span></div>
|
||||||
|
<div class="project-gravur-separator"></div>
|
||||||
<div id="contentArea" class="content-area">
|
<div id="contentArea" class="content-area">
|
||||||
<aside id="favHistorySidebar" class="fav-history-sidebar" aria-label="Favoriten und Verlauf"></aside>
|
<aside id="favHistorySidebar" class="fav-history-sidebar" aria-label="Favoriten und Verlauf"></aside>
|
||||||
<main id="main">
|
<main id="main">
|
||||||
|
|||||||
@@ -595,9 +595,16 @@ async function uploadDroppedPaths({ paths, owner, repo, destPath = '', cloneUrl
|
|||||||
try {
|
try {
|
||||||
let res = null;
|
let res = null;
|
||||||
|
|
||||||
// Fast path: treat dropped item as file upload first.
|
const pathType = await withTimeout(window.electronAPI.getPathType(p), 5000, 'get-path-type');
|
||||||
// If it is actually a directory, the call returns ok=false and we fall back to uploadAndPush.
|
console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:pathType', { path: p, pathType });
|
||||||
const fileTry = await withTimeout(window.electronAPI.uploadGiteaFile({
|
try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:pathType', { path: p, pathType }); } catch (_) {}
|
||||||
|
|
||||||
|
if (!pathType?.ok) {
|
||||||
|
throw new Error(pathType?.error || 'path-type-failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathType.type === 'file') {
|
||||||
|
res = await withTimeout(window.electronAPI.uploadGiteaFile({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
localPath: [p],
|
localPath: [p],
|
||||||
@@ -605,22 +612,17 @@ async function uploadDroppedPaths({ paths, owner, repo, destPath = '', cloneUrl
|
|||||||
branch,
|
branch,
|
||||||
platform: currentState.platform
|
platform: currentState.platform
|
||||||
}), 15000, 'upload-gitea-file');
|
}), 15000, 'upload-gitea-file');
|
||||||
|
} else if (pathType.type === 'dir') {
|
||||||
console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:fileTry', { path: p, fileTry });
|
res = await withTimeout(window.electronAPI.uploadLocalFolderToGitea({
|
||||||
try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:fileTry', { path: p, fileTry }); } catch (_) {}
|
|
||||||
|
|
||||||
const fileFailed = !fileTry?.ok || (Array.isArray(fileTry.results) && fileTry.results.some(r => !r.ok));
|
|
||||||
if (!fileFailed) {
|
|
||||||
res = { ok: true, via: 'upload-gitea-file', results: fileTry.results || [] };
|
|
||||||
} else {
|
|
||||||
res = await withTimeout(window.electronAPI.uploadAndPush({
|
|
||||||
localFolder: p,
|
localFolder: p,
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
destPath,
|
destPath,
|
||||||
cloneUrl,
|
branch,
|
||||||
branch
|
messagePrefix: 'Upload folder via GUI'
|
||||||
}), 30000, 'upload-and-push');
|
}), 600000, 'upload-local-folder-to-gitea');
|
||||||
|
} else {
|
||||||
|
throw new Error(`Nicht unterstuetzter Pfadtyp: ${pathType.type || 'unknown'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:itemResult', { path: p, res });
|
console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:itemResult', { path: p, res });
|
||||||
@@ -628,7 +630,10 @@ async function uploadDroppedPaths({ paths, owner, repo, destPath = '', cloneUrl
|
|||||||
|
|
||||||
if (!res?.ok) {
|
if (!res?.ok) {
|
||||||
failedCount++;
|
failedCount++;
|
||||||
errors.push(`${baseName}: ${res?.error || 'Unbekannter Fehler'}`);
|
const detailedFailure = Array.isArray(res?.failedFiles) && res.failedFiles.length > 0
|
||||||
|
? `${baseName}: ${res.failedFiles[0].targetPath} - ${res.failedFiles[0].error || 'Unbekannter Fehler'}`
|
||||||
|
: `${baseName}: ${res?.error || 'Unbekannter Fehler'}`;
|
||||||
|
errors.push(detailedFailure);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -951,12 +956,13 @@ function getDefaultBranch(owner, repo) {
|
|||||||
|
|
||||||
// Navigations-Status für die Explorer-Ansicht
|
// Navigations-Status für die Explorer-Ansicht
|
||||||
let currentState = {
|
let currentState = {
|
||||||
view: 'none', // 'local', 'gitea-list', 'gitea-repo'
|
view: 'none', // 'local', 'gitea-list', 'gitea-repo', 'gitea-trash'
|
||||||
owner: null,
|
owner: null,
|
||||||
repo: null,
|
repo: null,
|
||||||
path: '',
|
path: '',
|
||||||
platform: 'gitea' // 'gitea' | 'github'
|
platform: 'gitea' // 'gitea' | 'github'
|
||||||
};
|
};
|
||||||
|
let lastRepoPathBeforeTrash = '';
|
||||||
|
|
||||||
let repoLoadRequestId = 0;
|
let repoLoadRequestId = 0;
|
||||||
const USER_CACHE_MS = 5 * 60 * 1000;
|
const USER_CACHE_MS = 5 * 60 * 1000;
|
||||||
@@ -1852,6 +1858,31 @@ function showSuccess(msg) { setStatus(msg); showToast(msg, 'success', 3000); log
|
|||||||
function showWarning(msg) { setStatus(msg); showToast(msg, 'warning'); logActivity('warning', msg); }
|
function showWarning(msg) { setStatus(msg); showToast(msg, 'warning'); logActivity('warning', msg); }
|
||||||
function showInfo(msg) { setStatus(msg); showToast(msg, 'info', 2500); }
|
function showInfo(msg) { setStatus(msg); showToast(msg, 'info', 2500); }
|
||||||
|
|
||||||
|
function formatUploadFailureDetails(result, fallbackMessage = 'Upload fehlgeschlagen') {
|
||||||
|
const lines = [];
|
||||||
|
const summary = result?.error || fallbackMessage;
|
||||||
|
lines.push(summary);
|
||||||
|
|
||||||
|
if (Array.isArray(result?.failedFiles) && result.failedFiles.length > 0) {
|
||||||
|
lines.push('');
|
||||||
|
lines.push('Betroffene Dateien:');
|
||||||
|
result.failedFiles.slice(0, 8).forEach((entry) => {
|
||||||
|
const target = entry?.targetPath || entry?.localFile || 'unbekannter Pfad';
|
||||||
|
const error = entry?.error || 'Unbekannter Fehler';
|
||||||
|
lines.push(`- ${target}: ${error}`);
|
||||||
|
});
|
||||||
|
if (result.failedFiles.length > 8) {
|
||||||
|
lines.push(`- ... und ${result.failedFiles.length - 8} weitere`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showUploadFailureModal(title, result, fallbackMessage = 'Upload fehlgeschlagen') {
|
||||||
|
await showInfoModal(title, formatUploadFailureDetails(result, fallbackMessage), true);
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSearchText(value) {
|
function normalizeSearchText(value) {
|
||||||
return String(value || '')
|
return String(value || '')
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -2928,6 +2959,8 @@ function updateNavigationUI() {
|
|||||||
|
|
||||||
// Back Button zeigen, wenn wir in einem Repo oder tief in Ordnern sind
|
// Back Button zeigen, wenn wir in einem Repo oder tief in Ordnern sind
|
||||||
if (currentState.view === 'gitea-repo' ||
|
if (currentState.view === 'gitea-repo' ||
|
||||||
|
currentState.view === 'gitea-trash' ||
|
||||||
|
currentState.view === 'gitea-trash-global' ||
|
||||||
(currentState.view === 'gitea-list' && currentState.path !== '')) {
|
(currentState.view === 'gitea-list' && currentState.path !== '')) {
|
||||||
btnBack.classList.remove('hidden');
|
btnBack.classList.remove('hidden');
|
||||||
} else {
|
} else {
|
||||||
@@ -2935,6 +2968,333 @@ function updateNavigationUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadGlobalTrashView() {
|
||||||
|
if (currentState.platform !== 'gitea') {
|
||||||
|
showError('Globaler Papierkorb ist aktuell nur für Gitea verfügbar.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentState.view = 'gitea-trash-global';
|
||||||
|
currentState.path = '_trash';
|
||||||
|
updateNavigationUI();
|
||||||
|
|
||||||
|
const btnCommits = $('btnCommits');
|
||||||
|
const btnReleases = $('btnReleases');
|
||||||
|
const btnPurgeTrash = $('btnPurgeTrash');
|
||||||
|
if (btnCommits) btnCommits.classList.add('hidden');
|
||||||
|
if (btnReleases) btnReleases.classList.add('hidden');
|
||||||
|
|
||||||
|
const grid = $('explorerGrid');
|
||||||
|
if (!grid) return;
|
||||||
|
grid.innerHTML = '';
|
||||||
|
setStatus('Lade globalen Papierkorb...');
|
||||||
|
|
||||||
|
let repos = Array.isArray(currentGiteaRepos) ? currentGiteaRepos.slice() : [];
|
||||||
|
if (repos.length === 0) {
|
||||||
|
const reposRes = await window.electronAPI.listGiteaRepos();
|
||||||
|
if (!reposRes?.ok) {
|
||||||
|
showError('Globaler Papierkorb konnte nicht geladen werden: Repo-Liste fehlgeschlagen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
repos = Array.isArray(reposRes.repos) ? reposRes.repos : [];
|
||||||
|
currentGiteaRepos = repos;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allItems = [];
|
||||||
|
let purgedFilesTotal = 0;
|
||||||
|
|
||||||
|
for (const r of repos) {
|
||||||
|
const owner = r?.owner?.login || r?.owner?.username;
|
||||||
|
const repoName = r?.name;
|
||||||
|
if (!owner || !repoName) continue;
|
||||||
|
|
||||||
|
const ref = getDefaultBranch(owner, repoName);
|
||||||
|
const res = await window.electronAPI.listGiteaTrash({
|
||||||
|
owner,
|
||||||
|
repo: repoName,
|
||||||
|
ref,
|
||||||
|
autoPurge: true,
|
||||||
|
autoPurgeDays: 7
|
||||||
|
});
|
||||||
|
if (!res?.ok) continue;
|
||||||
|
|
||||||
|
purgedFilesTotal += Number(res?.purgeSummary?.purgedFiles || 0);
|
||||||
|
const items = Array.isArray(res.items) ? res.items : [];
|
||||||
|
items.forEach(item => {
|
||||||
|
allItems.push({
|
||||||
|
...item,
|
||||||
|
owner,
|
||||||
|
repo: repoName,
|
||||||
|
ref
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (btnPurgeTrash) {
|
||||||
|
btnPurgeTrash.classList.remove('hidden');
|
||||||
|
btnPurgeTrash.textContent = '🧹 Global Purge 7d';
|
||||||
|
btnPurgeTrash.title = 'Papierkorb älter als 7 Tage in allen Repositories leeren';
|
||||||
|
btnPurgeTrash.onclick = async () => {
|
||||||
|
const ok = await showActionConfirmModal({
|
||||||
|
title: 'Globaler Purge',
|
||||||
|
message: 'Alle Papierkorb-Einträge älter als 7 Tage in ALLEN Repositories löschen?',
|
||||||
|
confirmText: 'Global Purge',
|
||||||
|
danger: true
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
showProgress(35, 'Globaler Purge läuft...');
|
||||||
|
let totalPurged = 0;
|
||||||
|
for (const r of repos) {
|
||||||
|
const owner = r?.owner?.login || r?.owner?.username;
|
||||||
|
const repoName = r?.name;
|
||||||
|
if (!owner || !repoName) continue;
|
||||||
|
const ref = getDefaultBranch(owner, repoName);
|
||||||
|
const purgeRes = await window.electronAPI.purgeGiteaTrash({
|
||||||
|
owner,
|
||||||
|
repo: repoName,
|
||||||
|
ref,
|
||||||
|
olderThanDays: 7
|
||||||
|
});
|
||||||
|
if (purgeRes?.ok) totalPurged += Number(purgeRes.purgedFiles || 0);
|
||||||
|
}
|
||||||
|
hideProgress();
|
||||||
|
showSuccess(`Global Purge abgeschlossen: ${totalPurged} Dateien entfernt`);
|
||||||
|
await loadGlobalTrashView();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (purgedFilesTotal > 0) {
|
||||||
|
setStatus(`Auto-Purge (global): ${purgedFilesTotal} Dateien entfernt`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allItems.length === 0) {
|
||||||
|
const emptyEl = document.createElement('div');
|
||||||
|
emptyEl.style.cssText = 'grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-muted);';
|
||||||
|
emptyEl.textContent = '🧺 Globaler Papierkorb ist leer';
|
||||||
|
grid.appendChild(emptyEl);
|
||||||
|
setStatus('Globaler Papierkorb ist leer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
allItems.sort((a, b) => String(b.restoreTimestamp).localeCompare(String(a.restoreTimestamp)));
|
||||||
|
for (const item of allItems) {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'item-card';
|
||||||
|
|
||||||
|
const iconEl = makeFileIconEl(item.name || item.originalPath || 'item', false);
|
||||||
|
const nameEl = document.createElement('div');
|
||||||
|
nameEl.className = 'item-name';
|
||||||
|
nameEl.textContent = item.originalPath || item.name || '(unbekannt)';
|
||||||
|
|
||||||
|
const metaEl = document.createElement('div');
|
||||||
|
metaEl.className = 'trash-card-meta';
|
||||||
|
metaEl.textContent = `${item.owner}/${item.repo} | Trash: ${item.trashPath} | Zeit: ${item.restoreTimestamp}`;
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'trash-card-actions';
|
||||||
|
actions.style.position = 'relative';
|
||||||
|
actions.style.zIndex = '3';
|
||||||
|
|
||||||
|
const restoreBtn = document.createElement('button');
|
||||||
|
restoreBtn.type = 'button';
|
||||||
|
restoreBtn.className = 'trash-restore-btn';
|
||||||
|
restoreBtn.textContent = '♻️ Restore';
|
||||||
|
restoreBtn.style.position = 'relative';
|
||||||
|
restoreBtn.style.zIndex = '4';
|
||||||
|
restoreBtn.addEventListener('click', async (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
try {
|
||||||
|
restoreBtn.disabled = true;
|
||||||
|
restoreBtn.textContent = '⏳ Restore...';
|
||||||
|
setStatus(`Restore: ${item.owner}/${item.repo} -> ${item.originalPath}`);
|
||||||
|
showProgress(25, 'Wiederherstellung läuft...');
|
||||||
|
const restoreRes = await window.electronAPI.restoreGiteaTrashItem({
|
||||||
|
owner: item.owner,
|
||||||
|
repo: item.repo,
|
||||||
|
trashPath: item.trashPath,
|
||||||
|
restorePath: item.originalPath,
|
||||||
|
ref: item.ref
|
||||||
|
});
|
||||||
|
hideProgress();
|
||||||
|
if (restoreRes?.ok) {
|
||||||
|
const warning = restoreRes.warning ? ` (${restoreRes.warning})` : '';
|
||||||
|
showSuccess(`Wiederhergestellt: ${item.owner}/${item.repo}/${item.originalPath}${warning}`);
|
||||||
|
await loadGlobalTrashView();
|
||||||
|
} else {
|
||||||
|
showError('Restore fehlgeschlagen: ' + (restoreRes?.error || 'Unbekannter Fehler'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
hideProgress();
|
||||||
|
showError('Restore fehlgeschlagen: ' + (e?.message || String(e)));
|
||||||
|
} finally {
|
||||||
|
restoreBtn.disabled = false;
|
||||||
|
restoreBtn.textContent = '♻️ Restore';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.appendChild(restoreBtn);
|
||||||
|
card.appendChild(iconEl);
|
||||||
|
card.appendChild(nameEl);
|
||||||
|
card.appendChild(metaEl);
|
||||||
|
card.appendChild(actions);
|
||||||
|
grid.appendChild(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(`Globaler Papierkorb: ${allItems.length} Einträge`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTrashView(owner, repo) {
|
||||||
|
if (!owner || !repo) {
|
||||||
|
showError('Papierkorb nur innerhalb eines Repositories verfügbar.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRepoPathBeforeTrash = currentState.path || '';
|
||||||
|
currentState.view = 'gitea-trash';
|
||||||
|
currentState.owner = owner;
|
||||||
|
currentState.repo = repo;
|
||||||
|
currentState.path = '_trash';
|
||||||
|
updateNavigationUI();
|
||||||
|
|
||||||
|
const btnCommits = $('btnCommits');
|
||||||
|
const btnReleases = $('btnReleases');
|
||||||
|
const btnPurgeTrash = $('btnPurgeTrash');
|
||||||
|
if (btnCommits) btnCommits.classList.add('hidden');
|
||||||
|
if (btnReleases) btnReleases.classList.add('hidden');
|
||||||
|
if (btnPurgeTrash) {
|
||||||
|
btnPurgeTrash.classList.remove('hidden');
|
||||||
|
btnPurgeTrash.textContent = '🧹 Purge 7d';
|
||||||
|
btnPurgeTrash.title = 'Papierkorb älter als 7 Tage leeren';
|
||||||
|
btnPurgeTrash.onclick = async () => {
|
||||||
|
const ok = await showActionConfirmModal({
|
||||||
|
title: 'Papierkorb leeren',
|
||||||
|
message: 'Alle Papierkorb-Einträge älter als 7 Tage löschen?',
|
||||||
|
confirmText: 'Purge 7d',
|
||||||
|
danger: true
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
|
showProgress(30, 'Papierkorb wird bereinigt...');
|
||||||
|
const purgeRes = await window.electronAPI.purgeGiteaTrash({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref,
|
||||||
|
olderThanDays: 7
|
||||||
|
});
|
||||||
|
hideProgress();
|
||||||
|
if (purgeRes?.ok) {
|
||||||
|
showSuccess(`Papierkorb bereinigt: ${purgeRes.purgedFiles || 0} Dateien entfernt`);
|
||||||
|
await loadTrashView(owner, repo);
|
||||||
|
} else {
|
||||||
|
showError('Purge fehlgeschlagen: ' + (purgeRes?.error || 'Unbekannter Fehler'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const grid = $('explorerGrid');
|
||||||
|
if (!grid) return;
|
||||||
|
grid.innerHTML = '';
|
||||||
|
setStatus('Lade Papierkorb...');
|
||||||
|
|
||||||
|
const ref = getDefaultBranch(owner, repo);
|
||||||
|
const res = await window.electronAPI.listGiteaTrash({ owner, repo, ref });
|
||||||
|
if (!res?.ok) {
|
||||||
|
showError('Papierkorb konnte nicht geladen werden: ' + (res?.error || 'Unbekannter Fehler'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((res?.purgeSummary?.purgedFiles || 0) > 0) {
|
||||||
|
setStatus(`Auto-Purge: ${res.purgeSummary.purgedFiles} alte Papierkorb-Dateien entfernt`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = Array.isArray(res.items) ? res.items : [];
|
||||||
|
if (items.length === 0) {
|
||||||
|
const emptyEl = document.createElement('div');
|
||||||
|
emptyEl.style.cssText = 'grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-muted);';
|
||||||
|
emptyEl.textContent = '🧺 Papierkorb ist leer';
|
||||||
|
grid.appendChild(emptyEl);
|
||||||
|
setStatus('Papierkorb ist leer');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.forEach((item) => {
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'item-card';
|
||||||
|
|
||||||
|
const iconEl = makeFileIconEl(item.name || item.originalPath || 'item', false);
|
||||||
|
const nameEl = document.createElement('div');
|
||||||
|
nameEl.className = 'item-name';
|
||||||
|
nameEl.textContent = item.originalPath || item.name || '(unbekannt)';
|
||||||
|
|
||||||
|
const metaEl = document.createElement('div');
|
||||||
|
metaEl.className = 'trash-card-meta';
|
||||||
|
metaEl.textContent = `Trash: ${item.trashPath} | Zeit: ${item.restoreTimestamp}`;
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'trash-card-actions';
|
||||||
|
actions.style.position = 'relative';
|
||||||
|
actions.style.zIndex = '3';
|
||||||
|
|
||||||
|
const restoreBtn = document.createElement('button');
|
||||||
|
restoreBtn.type = 'button';
|
||||||
|
restoreBtn.className = 'trash-restore-btn';
|
||||||
|
restoreBtn.textContent = '♻️ Restore';
|
||||||
|
restoreBtn.style.position = 'relative';
|
||||||
|
restoreBtn.style.zIndex = '4';
|
||||||
|
restoreBtn.addEventListener('click', async (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
try {
|
||||||
|
restoreBtn.disabled = true;
|
||||||
|
restoreBtn.textContent = '⏳ Restore...';
|
||||||
|
setStatus(`Restore: ${item.originalPath}`);
|
||||||
|
showProgress(25, 'Wiederherstellung läuft...');
|
||||||
|
if (window.electronAPI?.debugToMain) {
|
||||||
|
window.electronAPI.debugToMain('info', 'trash-restore-clicked', {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
trashPath: item.trashPath,
|
||||||
|
restorePath: item.originalPath,
|
||||||
|
ref
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreRes = await window.electronAPI.restoreGiteaTrashItem({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
trashPath: item.trashPath,
|
||||||
|
restorePath: item.originalPath,
|
||||||
|
ref
|
||||||
|
});
|
||||||
|
|
||||||
|
hideProgress();
|
||||||
|
if (restoreRes?.ok) {
|
||||||
|
const warning = restoreRes.warning ? ` (${restoreRes.warning})` : '';
|
||||||
|
showSuccess(`Wiederhergestellt: ${item.originalPath}${warning}`);
|
||||||
|
await loadTrashView(owner, repo);
|
||||||
|
} else {
|
||||||
|
showError('Restore fehlgeschlagen: ' + (restoreRes?.error || 'Unbekannter Fehler'));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
hideProgress();
|
||||||
|
showError('Restore fehlgeschlagen: ' + (e?.message || String(e)));
|
||||||
|
console.error('trash restore click handler error', e);
|
||||||
|
} finally {
|
||||||
|
restoreBtn.disabled = false;
|
||||||
|
restoreBtn.textContent = '♻️ Restore';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
actions.appendChild(restoreBtn);
|
||||||
|
card.appendChild(iconEl);
|
||||||
|
card.appendChild(nameEl);
|
||||||
|
card.appendChild(metaEl);
|
||||||
|
card.appendChild(actions);
|
||||||
|
grid.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
setStatus(`Papierkorb: ${items.length} Einträge`);
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------
|
/* -------------------------
|
||||||
GITEA CORE LOGIK (GRID)
|
GITEA CORE LOGIK (GRID)
|
||||||
------------------------- */
|
------------------------- */
|
||||||
@@ -2947,8 +3307,14 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) {
|
|||||||
// Verstecke Commits & Releases-Buttons in Repo-Liste
|
// Verstecke Commits & Releases-Buttons in Repo-Liste
|
||||||
const btnCommits = $('btnCommits');
|
const btnCommits = $('btnCommits');
|
||||||
const btnReleases = $('btnReleases');
|
const btnReleases = $('btnReleases');
|
||||||
|
const btnTrash = $('btnTrash');
|
||||||
|
const btnGlobalTrash = $('btnGlobalTrash');
|
||||||
|
const btnPurgeTrash = $('btnPurgeTrash');
|
||||||
if (btnCommits) btnCommits.classList.add('hidden');
|
if (btnCommits) btnCommits.classList.add('hidden');
|
||||||
if (btnReleases) btnReleases.classList.add('hidden');
|
if (btnReleases) btnReleases.classList.add('hidden');
|
||||||
|
if (btnTrash) btnTrash.classList.add('hidden');
|
||||||
|
if (btnGlobalTrash) btnGlobalTrash.classList.add('hidden');
|
||||||
|
if (btnPurgeTrash) btnPurgeTrash.classList.add('hidden');
|
||||||
|
|
||||||
// WICHTIG: Grid-Layout zurücksetzen
|
// WICHTIG: Grid-Layout zurücksetzen
|
||||||
const grid = $('explorerGrid');
|
const grid = $('explorerGrid');
|
||||||
@@ -2975,6 +3341,11 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentGiteaRepos = Array.isArray(res.repos) ? res.repos : [];
|
currentGiteaRepos = Array.isArray(res.repos) ? res.repos : [];
|
||||||
|
const btnGlobalTrash = $('btnGlobalTrash');
|
||||||
|
if (btnGlobalTrash && currentState.platform === 'gitea') {
|
||||||
|
btnGlobalTrash.classList.remove('hidden');
|
||||||
|
btnGlobalTrash.onclick = () => loadGlobalTrashView();
|
||||||
|
}
|
||||||
if (!preloadedData) {
|
if (!preloadedData) {
|
||||||
try {
|
try {
|
||||||
const cachedName = getCachedUsername('gitea');
|
const cachedName = getCachedUsername('gitea');
|
||||||
@@ -3328,6 +3699,7 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) {
|
|||||||
console.log('[UPLOAD_DEBUG][renderer] repoCard:dropResult', res);
|
console.log('[UPLOAD_DEBUG][renderer] repoCard:dropResult', res);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
showError('Fehler: ' + (res.error || 'Upload fehlgeschlagen'));
|
showError('Fehler: ' + (res.error || 'Upload fehlgeschlagen'));
|
||||||
|
await showUploadFailureModal('Upload fehlgeschlagen', res, 'Upload fehlgeschlagen');
|
||||||
setStatus('Upload fehlgeschlagen');
|
setStatus('Upload fehlgeschlagen');
|
||||||
} else {
|
} else {
|
||||||
setStatus('Upload abgeschlossen');
|
setStatus('Upload abgeschlossen');
|
||||||
@@ -3362,6 +3734,11 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadRepoContents(owner, repo, path) {
|
async function loadRepoContents(owner, repo, path) {
|
||||||
|
// Projekttitel als Gravur unterhalb der Toolbar setzen (nur Repo-Name)
|
||||||
|
const gravurTitle = document.getElementById('project-gravur-title');
|
||||||
|
if (gravurTitle) {
|
||||||
|
gravurTitle.textContent = repo;
|
||||||
|
}
|
||||||
currentState.view = 'gitea-repo';
|
currentState.view = 'gitea-repo';
|
||||||
currentState.owner = owner;
|
currentState.owner = owner;
|
||||||
currentState.repo = repo;
|
currentState.repo = repo;
|
||||||
@@ -3371,6 +3748,9 @@ async function loadRepoContents(owner, repo, path) {
|
|||||||
// Zeige Commits & Releases-Buttons wenn wir in einem Repo sind
|
// Zeige Commits & Releases-Buttons wenn wir in einem Repo sind
|
||||||
const btnCommits = $('btnCommits');
|
const btnCommits = $('btnCommits');
|
||||||
const btnReleases = $('btnReleases');
|
const btnReleases = $('btnReleases');
|
||||||
|
const btnTrash = $('btnTrash');
|
||||||
|
const btnGlobalTrash = $('btnGlobalTrash');
|
||||||
|
const btnPurgeTrash = $('btnPurgeTrash');
|
||||||
|
|
||||||
if (btnCommits) {
|
if (btnCommits) {
|
||||||
btnCommits.classList.remove('hidden');
|
btnCommits.classList.remove('hidden');
|
||||||
@@ -3382,6 +3762,16 @@ async function loadRepoContents(owner, repo, path) {
|
|||||||
btnReleases.onclick = () => loadRepoReleases(owner, repo);
|
btnReleases.onclick = () => loadRepoReleases(owner, repo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (btnTrash) {
|
||||||
|
btnTrash.classList.remove('hidden');
|
||||||
|
btnTrash.onclick = () => loadTrashView(owner, repo);
|
||||||
|
}
|
||||||
|
if (btnGlobalTrash) {
|
||||||
|
btnGlobalTrash.classList.remove('hidden');
|
||||||
|
btnGlobalTrash.onclick = () => loadGlobalTrashView();
|
||||||
|
}
|
||||||
|
if (btnPurgeTrash) btnPurgeTrash.classList.add('hidden');
|
||||||
|
|
||||||
// WICHTIG: Grid-Layout zurücksetzen
|
// WICHTIG: Grid-Layout zurücksetzen
|
||||||
const grid = $('explorerGrid');
|
const grid = $('explorerGrid');
|
||||||
if (grid) {
|
if (grid) {
|
||||||
@@ -3399,13 +3789,31 @@ async function loadRepoContents(owner, repo, path) {
|
|||||||
repo,
|
repo,
|
||||||
path,
|
path,
|
||||||
ref,
|
ref,
|
||||||
platform
|
platform,
|
||||||
|
noCache: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
// Bei Fehler Gravur zurücksetzen
|
||||||
|
const gravurTitle = document.getElementById('project-gravur-title');
|
||||||
|
if (gravurTitle) {
|
||||||
|
gravurTitle.textContent = '';
|
||||||
|
}
|
||||||
showError('Error: ' + (res.error || 'Unknown error'));
|
showError('Error: ' + (res.error || 'Unknown error'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Wenn zur Übersicht gewechselt wird, Gravur zurücksetzen
|
||||||
|
function resetProjectGravurTitle() {
|
||||||
|
const gravurTitle = document.getElementById('project-gravur-title');
|
||||||
|
if (gravurTitle) gravurTitle.textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nach dem Laden der Repo-Liste oder beim Klick auf "Zurück" rufe resetProjectGravurTitle() auf
|
||||||
|
const origLoadRepos = loadRepos;
|
||||||
|
loadRepos = function(...args) {
|
||||||
|
resetProjectGravurTitle();
|
||||||
|
return origLoadRepos.apply(this, args);
|
||||||
|
};
|
||||||
|
|
||||||
const grid = $('explorerGrid');
|
const grid = $('explorerGrid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
@@ -3501,6 +3909,7 @@ async function loadRepoContents(owner, repo, path) {
|
|||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
showError('Upload error: ' + (res.error || 'Unbekannter Fehler'));
|
showError('Upload error: ' + (res.error || 'Unbekannter Fehler'));
|
||||||
|
await showUploadFailureModal('Upload fehlgeschlagen', res, 'Upload fehlgeschlagen');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
@@ -3561,6 +3970,13 @@ async function selectLocalFolder() {
|
|||||||
|
|
||||||
await refreshLocalTree(folder);
|
await refreshLocalTree(folder);
|
||||||
await loadBranches(folder);
|
await loadBranches(folder);
|
||||||
|
|
||||||
|
const btnTrash = $('btnTrash');
|
||||||
|
if (btnTrash) btnTrash.classList.add('hidden');
|
||||||
|
const btnGlobalTrash = $('btnGlobalTrash');
|
||||||
|
if (btnGlobalTrash) btnGlobalTrash.classList.add('hidden');
|
||||||
|
const btnPurgeTrash = $('btnPurgeTrash');
|
||||||
|
if (btnPurgeTrash) btnPurgeTrash.classList.add('hidden');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error selecting folder:', error);
|
console.error('Error selecting folder:', error);
|
||||||
showError('Error selecting folder');
|
showError('Error selecting folder');
|
||||||
@@ -4160,21 +4576,26 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element, isPrivate =
|
|||||||
const sel = await window.electronAPI.selectFolder();
|
const sel = await window.electronAPI.selectFolder();
|
||||||
if (sel) {
|
if (sel) {
|
||||||
showProgress(0, 'Upload...');
|
showProgress(0, 'Upload...');
|
||||||
await window.electronAPI.uploadAndPush({
|
const res = await window.electronAPI.uploadLocalFolderToGitea({
|
||||||
localFolder: sel,
|
localFolder: sel,
|
||||||
owner,
|
owner,
|
||||||
repo: repoName,
|
repo: repoName,
|
||||||
destPath: '',
|
destPath: '',
|
||||||
cloneUrl,
|
branch: getDefaultBranch(owner, repoName),
|
||||||
branch: getDefaultBranch(owner, repoName)
|
messagePrefix: 'Upload folder via GUI'
|
||||||
});
|
});
|
||||||
|
if (!res?.ok) {
|
||||||
|
showError('Upload fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler'));
|
||||||
|
await showUploadFailureModal('Upload fehlgeschlagen', res, 'Upload fehlgeschlagen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
hideProgress();
|
hideProgress();
|
||||||
setStatus('Upload complete');
|
setStatus('Upload complete');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Upload error:', error);
|
console.error('Upload error:', error);
|
||||||
hideProgress();
|
hideProgress();
|
||||||
showError('Upload failed');
|
showError('Upload failed: ' + (error?.message || 'Unbekannter Fehler'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4266,7 +4687,7 @@ function showGiteaItemContextMenu(ev, item, owner, repo) {
|
|||||||
let done = 0;
|
let done = 0;
|
||||||
for (const p of selectedItems) {
|
for (const p of selectedItems) {
|
||||||
const isGithub = currentState.platform === 'github';
|
const isGithub = currentState.platform === 'github';
|
||||||
await window.electronAPI.deleteFile({ path: p, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo) });
|
await window.electronAPI.deleteFile({ path: p, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo), softDelete: true });
|
||||||
done++;
|
done++;
|
||||||
showProgress(Math.round((done / selectedItems.size) * 100), `Lösche ${done}/${selectedItems.size}`);
|
showProgress(Math.round((done / selectedItems.size) * 100), `Lösche ${done}/${selectedItems.size}`);
|
||||||
}
|
}
|
||||||
@@ -4344,10 +4765,10 @@ function showGiteaItemContextMenu(ev, item, owner, repo) {
|
|||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
showProgress(0, `Lösche ${item.name}...`);
|
showProgress(0, `Lösche ${item.name}...`);
|
||||||
const isGithub = currentState.platform === 'github';
|
const isGithub = currentState.platform === 'github';
|
||||||
const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo) });
|
const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo), softDelete: true });
|
||||||
hideProgress();
|
hideProgress();
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
setStatus(`${item.name} gelöscht`);
|
setStatus(`${item.name} gelöscht` + (res.softDeleteWarning ? ` (${res.softDeleteWarning})` : ''));
|
||||||
loadRepoContents(owner, repo, currentState.path);
|
loadRepoContents(owner, repo, currentState.path);
|
||||||
} else {
|
} else {
|
||||||
showError('Löschen fehlgeschlagen: ' + (res?.error || ''));
|
showError('Löschen fehlgeschlagen: ' + (res?.error || ''));
|
||||||
@@ -4534,7 +4955,7 @@ function showNewGiteaItemModal(owner, repo, parentPath, type) {
|
|||||||
type
|
type
|
||||||
});
|
});
|
||||||
if (res?.ok) {
|
if (res?.ok) {
|
||||||
setStatus(`"${name}" erstellt`);
|
setStatus(res?.exists ? `"${name}" existiert bereits` : `"${name}" erstellt`);
|
||||||
loadRepoContents(owner, repo, currentState.path);
|
loadRepoContents(owner, repo, currentState.path);
|
||||||
} else {
|
} else {
|
||||||
await showInfoModal('Erstellen fehlgeschlagen', 'Erstellen fehlgeschlagen:\n' + (res?.error || ''), true);
|
await showInfoModal('Erstellen fehlgeschlagen', 'Erstellen fehlgeschlagen:\n' + (res?.error || ''), true);
|
||||||
@@ -4914,12 +5335,23 @@ function setupBackgroundContextMenu() {
|
|||||||
addItem('📋', `⬆️ Von Lokal einfügen: "${clipboard.item.name}"`, async () => {
|
addItem('📋', `⬆️ Von Lokal einfügen: "${clipboard.item.name}"`, async () => {
|
||||||
showProgress(0, `Lade "${clipboard.item.name}" hoch...`);
|
showProgress(0, `Lade "${clipboard.item.name}" hoch...`);
|
||||||
try {
|
try {
|
||||||
await window.electronAPI.uploadAndPush({
|
const res = clipboard.item.isDirectory
|
||||||
|
? await window.electronAPI.uploadLocalFolderToGitea({
|
||||||
localFolder: clipboard.item.path,
|
localFolder: clipboard.item.path,
|
||||||
owner, repo,
|
owner,
|
||||||
|
repo,
|
||||||
|
destPath: currentPath,
|
||||||
|
branch: getDefaultBranch(owner, repo),
|
||||||
|
messagePrefix: 'Upload folder via GUI'
|
||||||
|
})
|
||||||
|
: await window.electronAPI.uploadAndPush({
|
||||||
|
localFolder: clipboard.item.path,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
destPath: currentPath,
|
destPath: currentPath,
|
||||||
branch: getDefaultBranch(owner, repo)
|
branch: getDefaultBranch(owner, repo)
|
||||||
});
|
});
|
||||||
|
if (!res?.ok) throw new Error(res?.error || 'Upload fehlgeschlagen');
|
||||||
showSuccess(`"${clipboard.item.name}" nach Gitea kopiert`);
|
showSuccess(`"${clipboard.item.name}" nach Gitea kopiert`);
|
||||||
loadRepoContents(owner, repo, currentState.path);
|
loadRepoContents(owner, repo, currentState.path);
|
||||||
} catch(e) { showError('Cross-Paste fehlgeschlagen'); }
|
} catch(e) { showError('Cross-Paste fehlgeschlagen'); }
|
||||||
@@ -5217,10 +5649,34 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
parts.pop();
|
parts.pop();
|
||||||
loadRepoContents(currentState.owner, currentState.repo, parts.join('/'));
|
loadRepoContents(currentState.owner, currentState.repo, parts.join('/'));
|
||||||
}
|
}
|
||||||
|
} else if (currentState.view === 'gitea-trash') {
|
||||||
|
loadRepoContents(currentState.owner, currentState.repo, lastRepoPathBeforeTrash || '');
|
||||||
|
} else if (currentState.view === 'gitea-trash-global') {
|
||||||
|
loadGiteaRepos();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($('btnTrash')) {
|
||||||
|
$('btnTrash').onclick = () => {
|
||||||
|
if (!currentState.owner || !currentState.repo) {
|
||||||
|
showError('Bitte zuerst ein Repository öffnen.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadTrashView(currentState.owner, currentState.repo);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($('btnGlobalTrash')) {
|
||||||
|
$('btnGlobalTrash').onclick = () => {
|
||||||
|
if (currentState.platform !== 'gitea') {
|
||||||
|
showError('Globaler Papierkorb ist nur für Gitea verfügbar.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadGlobalTrashView();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Modal controls
|
// Modal controls
|
||||||
if ($('btnWinMinimize')) $('btnWinMinimize').onclick = () => window.electronAPI.windowMinimize();
|
if ($('btnWinMinimize')) $('btnWinMinimize').onclick = () => window.electronAPI.windowMinimize();
|
||||||
if ($('btnWinMaximize')) $('btnWinMaximize').onclick = () => window.electronAPI.windowMaximize();
|
if ($('btnWinMaximize')) $('btnWinMaximize').onclick = () => window.electronAPI.windowMaximize();
|
||||||
@@ -5890,8 +6346,21 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
if (lastSelectedItem.type === 'gitea') {
|
if (lastSelectedItem.type === 'gitea') {
|
||||||
const { item, owner, repo } = lastSelectedItem;
|
const { item, owner, repo } = lastSelectedItem;
|
||||||
showDeleteConfirm(`"${item.name}" wirklich löschen?`, async () => {
|
showDeleteConfirm(`"${item.name}" wirklich löschen?`, async () => {
|
||||||
const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: true });
|
const isGithub = currentState.platform === 'github';
|
||||||
if (res?.ok) { showSuccess(`"${item.name}" gelöscht`); loadRepoContents(owner, repo, currentState.path); lastSelectedItem = null; }
|
const res = await window.electronAPI.deleteFile({
|
||||||
|
path: item.path,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
isGitea: !isGithub,
|
||||||
|
isGithub,
|
||||||
|
ref: getDefaultBranch(owner, repo),
|
||||||
|
softDelete: true
|
||||||
|
});
|
||||||
|
if (res?.ok) {
|
||||||
|
showSuccess(`"${item.name}" gelöscht` + (res.softDeleteWarning ? ` (${res.softDeleteWarning})` : ''));
|
||||||
|
loadRepoContents(owner, repo, currentState.path);
|
||||||
|
lastSelectedItem = null;
|
||||||
|
}
|
||||||
else showError('Löschen fehlgeschlagen: ' + (res?.error || ''));
|
else showError('Löschen fehlgeschlagen: ' + (res?.error || ''));
|
||||||
});
|
});
|
||||||
} else if (lastSelectedItem.type === 'local') {
|
} else if (lastSelectedItem.type === 'local') {
|
||||||
|
|||||||
@@ -1,3 +1,44 @@
|
|||||||
|
.project-gravur-bar {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
background: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.project-gravur-title {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(174, 189, 216, 0.18);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
font-style: italic;
|
||||||
|
user-select: none;
|
||||||
|
text-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 1.5px 0 rgba(0,0,0,0.13);
|
||||||
|
transition: color 0.2s;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.project-gravur-separator {
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: linear-gradient(90deg, rgba(88,213,255,0.10) 0%, rgba(92,135,255,0.10) 100%);
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
.toolbar-project-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(174, 189, 216, 0.22);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
margin-left: 32px;
|
||||||
|
font-style: italic;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
text-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 1.5px 0 rgba(0,0,0,0.13);
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
:root {
|
:root {
|
||||||
/* Moderne Farbpalette */
|
/* Moderne Farbpalette */
|
||||||
--bg-primary: #07111f;
|
--bg-primary: #07111f;
|
||||||
@@ -96,12 +137,15 @@
|
|||||||
|
|
||||||
.titlebar-strip-title {
|
.titlebar-strip-title {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: rgba(174, 189, 216, 0.68);
|
color: rgba(174, 189, 216, 0.32); /* viel dezenter */
|
||||||
letter-spacing: 0.045em;
|
letter-spacing: 0.045em;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-shadow: 0 1px 0 rgba(255,255,255,0.04), 0 1.5px 0 rgba(0,0,0,0.13);
|
||||||
|
font-style: italic;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#titlebar-strip .win-controls {
|
#titlebar-strip .win-controls {
|
||||||
@@ -383,6 +427,11 @@ body {
|
|||||||
|
|
||||||
.tool-group--quick-actions {
|
.tool-group--quick-actions {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-group--utility {
|
||||||
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-group--repo {
|
.tool-group--repo {
|
||||||
@@ -602,7 +651,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#btnOpenRepoActions,
|
#btnOpenRepoActions,
|
||||||
#btnPush {
|
#btnPush,
|
||||||
|
#btnGlobalTrash {
|
||||||
min-width: 92px;
|
min-width: 92px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1970,6 +2020,61 @@ input[type="checkbox"] {
|
|||||||
color: #d9f7ff;
|
color: #d9f7ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===========================
|
||||||
|
TRASH VIEW
|
||||||
|
=========================== */
|
||||||
|
.item-card .trash-card-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.84;
|
||||||
|
margin-top: 8px;
|
||||||
|
line-height: 1.45;
|
||||||
|
word-break: break-all;
|
||||||
|
color: rgba(226, 232, 240, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card .trash-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card button.trash-restore-btn {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.45);
|
||||||
|
background: linear-gradient(135deg, rgba(10, 88, 122, 0.58), rgba(30, 64, 175, 0.55));
|
||||||
|
color: #e0f2fe;
|
||||||
|
border-radius: 9px;
|
||||||
|
padding: 7px 12px;
|
||||||
|
min-height: 32px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 8px 18px rgba(3, 19, 42, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||||
|
transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease, background 0.12s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card button.trash-restore-btn:hover {
|
||||||
|
border-color: rgba(56, 189, 248, 0.78);
|
||||||
|
background: linear-gradient(135deg, rgba(14, 116, 144, 0.64), rgba(37, 99, 235, 0.62));
|
||||||
|
box-shadow: 0 10px 22px rgba(7, 42, 90, 0.3), 0 0 0 2px rgba(56, 189, 248, 0.16);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card button.trash-restore-btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-card button.trash-restore-btn:disabled {
|
||||||
|
opacity: 0.62;
|
||||||
|
cursor: wait;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.repo-owner-tab.active {
|
.repo-owner-tab.active {
|
||||||
border-color: rgba(88, 213, 255, 0.65);
|
border-color: rgba(88, 213, 255, 0.65);
|
||||||
background: linear-gradient(135deg, rgba(88, 213, 255, 0.2), rgba(92, 135, 255, 0.18));
|
background: linear-gradient(135deg, rgba(88, 213, 255, 0.2), rgba(92, 135, 255, 0.18));
|
||||||
|
|||||||
@@ -682,9 +682,29 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
|
|||||||
repo = parts[1];
|
repo = parts[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Behalte den branch so wie übergeben - keine Konvertierung
|
// Behalte den branch so wie übergeben - aber 'HEAD' muss zum echten Branch aufgelöst werden
|
||||||
let branchName = branch || 'HEAD';
|
let branchName = branch || 'HEAD';
|
||||||
|
|
||||||
|
// HEAD-Auflösung: Wenn branch === 'HEAD', den Default-Branch des Repos abrufen
|
||||||
|
if (branchName === 'HEAD') {
|
||||||
|
try {
|
||||||
|
const repoInfoUrl = `${base}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}`;
|
||||||
|
const repoInfo = await tryRequest(repoInfoUrl, token);
|
||||||
|
if (repoInfo.ok && repoInfo.data.default_branch) {
|
||||||
|
branchName = repoInfo.data.default_branch;
|
||||||
|
console.log(`[Upload Debug] HEAD aufgelöst zu: ${branchName}`);
|
||||||
|
} else {
|
||||||
|
// Fallback auf 'main' wenn Auflösung fehlschlägt
|
||||||
|
branchName = 'main';
|
||||||
|
console.warn(`[Upload Debug] HEAD-Auflösung fehlgeschlagen, verwende Fallback: ${branchName}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback auf 'main' wenn Fehler
|
||||||
|
branchName = 'main';
|
||||||
|
console.warn(`[Upload Debug] HEAD-Auflösung fehlgeschlagen (${e.message}), verwende Fallback: ${branchName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fetchSha = async () => {
|
const fetchSha = async () => {
|
||||||
try {
|
try {
|
||||||
const existing = await getGiteaRepoContents({ token, url: base, owner, repo, path, ref: branchName });
|
const existing = await getGiteaRepoContents({ token, url: base, owner, repo, path, ref: branchName });
|
||||||
@@ -720,7 +740,7 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
|
|||||||
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
let retryCount = 0;
|
let retryCount = 0;
|
||||||
const MAX_RETRIES = 2; // Reduziert auf 2 Retries für schnelleren Fallback
|
const MAX_RETRIES = 3;
|
||||||
|
|
||||||
while (retryCount <= MAX_RETRIES) {
|
while (retryCount <= MAX_RETRIES) {
|
||||||
let sha = await fetchSha();
|
let sha = await fetchSha();
|
||||||
@@ -785,13 +805,11 @@ async function uploadGiteaFile({ token, url, owner, repo, path, contentBase64, m
|
|||||||
|
|
||||||
if (isShaRequired && retryCount < MAX_RETRIES) {
|
if (isShaRequired && retryCount < MAX_RETRIES) {
|
||||||
retryCount++;
|
retryCount++;
|
||||||
console.warn(`-> 422 SHA Required. Waiting 1.5 seconds for server index update... (Retry ${retryCount}/${MAX_RETRIES})`);
|
console.warn(`-> 422 SHA Required. Waiting 2.5 seconds for server index update... (Retry ${retryCount}/${MAX_RETRIES})`);
|
||||||
await sleep(1500); // Reduzierte Wartezeit für schnelleren Fallback
|
await sleep(2500);
|
||||||
// Schleife wird neu gestartet, SHA wird erneut gesucht
|
|
||||||
continue;
|
continue;
|
||||||
} else if (isShaRequired && retryCount >= MAX_RETRIES) {
|
} else if (isShaRequired && retryCount >= MAX_RETRIES) {
|
||||||
// Verbesserte Fehlermeldung mit Hinweis auf Git-Fallback
|
const error = new Error(`API-Upload fehlgeschlagen: Repository-Index nach vorherigem Upload noch nicht bereit. Bitte erneut versuchen.`);
|
||||||
const error = new Error(`API-Upload fehlgeschlagen: Repository wurde gerade erstellt, Index noch nicht bereit. Verwende Git-Fallback.`);
|
|
||||||
error.code = 'SHA_NOT_FOUND';
|
error.code = 'SHA_NOT_FOUND';
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
293
src/utils/helpers.js
Normal file
293
src/utils/helpers.js
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
/**
|
||||||
|
* Gemeinsame Utility-Funktionen für Git Manager GUI
|
||||||
|
* - Branch Handling
|
||||||
|
* - API Error Handling
|
||||||
|
* - Standardisiertes Logging
|
||||||
|
* - Caching
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const ppath = require('path');
|
||||||
|
|
||||||
|
// ===== LOGGING SYSTEM =====
|
||||||
|
const LOG_LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
||||||
|
let currentLogLevel = process.env.NODE_ENV === 'production' ? LOG_LEVELS.INFO : LOG_LEVELS.DEBUG;
|
||||||
|
let logQueue = [];
|
||||||
|
const MAX_LOG_BUFFER = 100;
|
||||||
|
|
||||||
|
function formatLog(level, context, message, details = null) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const levelStr = Object.keys(LOG_LEVELS).find(k => LOG_LEVELS[k] === level);
|
||||||
|
return {
|
||||||
|
timestamp,
|
||||||
|
level: levelStr,
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
details,
|
||||||
|
pid: process.pid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLog(logEntry) {
|
||||||
|
logQueue.push(logEntry);
|
||||||
|
if (logQueue.length > MAX_LOG_BUFFER) {
|
||||||
|
logQueue.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auch in Console schreiben
|
||||||
|
const { level, timestamp, context, message, details } = logEntry;
|
||||||
|
const prefix = `[${timestamp}] [${level}] [${context}]`;
|
||||||
|
|
||||||
|
if (level === 'ERROR' && details?.error) {
|
||||||
|
console.error(prefix, message, details.error);
|
||||||
|
} else if (level === 'WARN') {
|
||||||
|
console.warn(prefix, message, details ? JSON.stringify(details) : '');
|
||||||
|
} else if (level !== 'DEBUG' || process.env.DEBUG) {
|
||||||
|
console.log(prefix, message, details ? JSON.stringify(details) : '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logger = {
|
||||||
|
debug: (context, message, details) => writeLog(formatLog(LOG_LEVELS.DEBUG, context, message, details)),
|
||||||
|
info: (context, message, details) => writeLog(formatLog(LOG_LEVELS.INFO, context, message, details)),
|
||||||
|
warn: (context, message, details) => writeLog(formatLog(LOG_LEVELS.WARN, context, message, details)),
|
||||||
|
error: (context, message, details) => writeLog(formatLog(LOG_LEVELS.ERROR, context, message, details)),
|
||||||
|
getRecent: (count = 20) => logQueue.slice(-count),
|
||||||
|
setLevel: (level) => { currentLogLevel = LOG_LEVELS[level] || LOG_LEVELS.INFO; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== BRANCH HANDLING =====
|
||||||
|
const BRANCH_DEFAULTS = {
|
||||||
|
gitea: 'main',
|
||||||
|
github: 'main'
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeBranch(branch = 'HEAD', platform = 'gitea') {
|
||||||
|
const value = String(branch || '').trim();
|
||||||
|
|
||||||
|
// HEAD sollte immer zu Standard konvertiert werden
|
||||||
|
if (value.toLowerCase() === 'head') {
|
||||||
|
return BRANCH_DEFAULTS[platform] || 'main';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validierung: nur sichere Git-Referenzen
|
||||||
|
if (/^[a-zA-Z0-9._\-/]+$/.test(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return BRANCH_DEFAULTS[platform] || 'main';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSafeBranch(branch) {
|
||||||
|
return /^[a-zA-Z0-9._\-/]+$/.test(String(branch || ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== ERROR HANDLING =====
|
||||||
|
const ERROR_CODES = {
|
||||||
|
NETWORK: 'NETWORK_ERROR',
|
||||||
|
AUTH_FAILED: 'AUTH_FAILED',
|
||||||
|
NOT_FOUND: 'NOT_FOUND',
|
||||||
|
VALIDATION: 'VALIDATION_ERROR',
|
||||||
|
RATE_LIMIT: 'RATE_LIMIT',
|
||||||
|
SERVER_ERROR: 'SERVER_ERROR',
|
||||||
|
UNKNOWN: 'UNKNOWN_ERROR'
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseApiError(error, defaultCode = ERROR_CODES.UNKNOWN) {
|
||||||
|
if (!error) {
|
||||||
|
return { code: defaultCode, message: 'Unknown error', statusCode: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Axios-style error
|
||||||
|
if (error.response) {
|
||||||
|
const status = error.response.status;
|
||||||
|
const data = error.response.data;
|
||||||
|
let code = defaultCode;
|
||||||
|
|
||||||
|
if (status === 401 || status === 403) {
|
||||||
|
code = ERROR_CODES.AUTH_FAILED;
|
||||||
|
} else if (status === 404) {
|
||||||
|
code = ERROR_CODES.NOT_FOUND;
|
||||||
|
} else if (status === 429) {
|
||||||
|
code = ERROR_CODES.RATE_LIMIT;
|
||||||
|
} else if (status >= 500) {
|
||||||
|
code = ERROR_CODES.SERVER_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
message: data?.message || error.message || `HTTP ${status}`,
|
||||||
|
statusCode: status,
|
||||||
|
rawMessage: data?.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network error
|
||||||
|
if (error.message?.includes('timeout') || error.code?.includes('TIMEOUT')) {
|
||||||
|
return { code: ERROR_CODES.NETWORK, message: 'Request timeout', statusCode: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code?.includes('ECONNREFUSED') || error.message?.includes('ECONNREFUSED')) {
|
||||||
|
return { code: ERROR_CODES.NETWORK, message: 'Connection refused', statusCode: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: ERROR_CODES.UNKNOWN,
|
||||||
|
message: error.message || String(error),
|
||||||
|
statusCode: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatErrorForUser(error, context = 'Operation') {
|
||||||
|
const parsed = parseApiError(error);
|
||||||
|
const messages = {
|
||||||
|
[ERROR_CODES.AUTH_FAILED]: `Authentifizierung fehlgeschlagen. Bitte Token überprüfen.`,
|
||||||
|
[ERROR_CODES.NOT_FOUND]: `Ressource nicht gefunden.`,
|
||||||
|
[ERROR_CODES.NETWORK]: `Netzwerkfehler. Bitte Verbindung überprüfen.`,
|
||||||
|
[ERROR_CODES.RATE_LIMIT]: `Zu viele Anfragen. Bitte später versuchen.`,
|
||||||
|
[ERROR_CODES.SERVER_ERROR]: `Server-Fehler. Bitte später versuchen.`,
|
||||||
|
[ERROR_CODES.UNKNOWN]: `${context} fehlgeschlagen.`
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
userMessage: messages[parsed.code],
|
||||||
|
technicalMessage: parsed.message,
|
||||||
|
code: parsed.code,
|
||||||
|
details: parsed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== CACHING SYSTEM =====
|
||||||
|
class Cache {
|
||||||
|
constructor(ttl = 300000) { // 5 min default
|
||||||
|
this.store = new Map();
|
||||||
|
this.ttl = ttl;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key, value, customTtl = null) {
|
||||||
|
const expiry = Date.now() + (customTtl || this.ttl);
|
||||||
|
this.store.set(key, { value, expiry });
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key) {
|
||||||
|
const item = this.store.get(key);
|
||||||
|
if (!item) return null;
|
||||||
|
if (Date.now() > item.expiry) {
|
||||||
|
this.store.delete(key);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return item.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate(keyPattern) {
|
||||||
|
for (const [key] of this.store) {
|
||||||
|
if (key.includes(keyPattern)) {
|
||||||
|
this.store.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.store.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
size() {
|
||||||
|
return this.store.size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard Caches
|
||||||
|
const caches = {
|
||||||
|
repos: new Cache(600000), // 10 min
|
||||||
|
fileTree: new Cache(300000), // 5 min
|
||||||
|
api: new Cache(120000) // 2 min
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== PARALLEL OPERATIONS =====
|
||||||
|
async function runParallel(operations, concurrency = 4, onProgress = null) {
|
||||||
|
const results = new Array(operations.length);
|
||||||
|
let completed = 0;
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
async function worker() {
|
||||||
|
while (index < operations.length) {
|
||||||
|
const i = index++;
|
||||||
|
try {
|
||||||
|
results[i] = { ok: true, result: await operations[i]() };
|
||||||
|
} catch (e) {
|
||||||
|
results[i] = { ok: false, error: e };
|
||||||
|
}
|
||||||
|
completed++;
|
||||||
|
if (onProgress) {
|
||||||
|
try { onProgress(completed, operations.length); } catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workers = Array.from({ length: Math.min(concurrency, operations.length) }, () => worker());
|
||||||
|
await Promise.all(workers);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== RETRY LOGIC =====
|
||||||
|
async function retryWithBackoff(fn, maxAttempts = 3, baseDelay = 1000) {
|
||||||
|
let lastError;
|
||||||
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
return await fn();
|
||||||
|
} catch (e) {
|
||||||
|
lastError = e;
|
||||||
|
if (attempt < maxAttempts - 1) {
|
||||||
|
const delay = baseDelay * Math.pow(2, attempt);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== FILE OPERATIONS =====
|
||||||
|
function ensureDirectory(dirPath) {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeReadFile(filePath, defaultValue = null) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) return defaultValue;
|
||||||
|
return fs.readFileSync(filePath, 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('safeReadFile', `Failed to read ${filePath}`, { error: e.message });
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeWriteFile(filePath, content) {
|
||||||
|
try {
|
||||||
|
ensureDirectory(ppath.dirname(filePath));
|
||||||
|
fs.writeFileSync(filePath, content, 'utf8');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('safeWriteFile', `Failed to write ${filePath}`, { error: e.message });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== EXPORTS =====
|
||||||
|
module.exports = {
|
||||||
|
logger,
|
||||||
|
normalizeBranch,
|
||||||
|
isSafeBranch,
|
||||||
|
parseApiError,
|
||||||
|
formatErrorForUser,
|
||||||
|
ERROR_CODES,
|
||||||
|
Cache,
|
||||||
|
caches,
|
||||||
|
runParallel,
|
||||||
|
retryWithBackoff,
|
||||||
|
ensureDirectory,
|
||||||
|
safeReadFile,
|
||||||
|
safeWriteFile,
|
||||||
|
LOG_LEVELS
|
||||||
|
};
|
||||||
@@ -94,7 +94,8 @@ class Updater {
|
|||||||
const ext = process.platform === 'win32' ? '.exe' : '.AppImage';
|
const ext = process.platform === 'win32' ? '.exe' : '.AppImage';
|
||||||
return assets.find(a => {
|
return assets.find(a => {
|
||||||
const name = String(a?.name || '').toLowerCase();
|
const name = String(a?.name || '').toLowerCase();
|
||||||
const validName = /^[a-z0-9._-]+$/.test(name);
|
// Leerzeichen im Namen erlauben!
|
||||||
|
const validName = /^[a-z0-9._\- ]+$/i.test(name);
|
||||||
return validName && name.endsWith(ext);
|
return validName && name.endsWith(ext);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user