diff --git a/renderer/index.html b/renderer/index.html index 5e1c8dc..6ef3ca5 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -47,7 +47,7 @@ - +
@@ -82,8 +82,8 @@
- - + + @@ -226,6 +226,18 @@ +
+
+
+

Diagnose

+

Erstellt ein Diagnosepaket mit Logs, Plattform und letzten Fehlern für schnelles Debugging.

+
+
+
+ +
+
+
@@ -284,6 +296,25 @@
+ +
+
+
+

Backup & Transfer

+

Einstellungen, Favoriten und Verlauf exportieren oder auf einem anderen PC importieren.

+
+
+
+
+ + +
+
+ Export enthält Tokens nur verschlüsselt, nie als Klartext. Der Import ist für andere PCs geeignet. +
+
+
+ diff --git a/renderer/renderer.js b/renderer/renderer.js index 90e583e..a06e04d 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -9,7 +9,7 @@ let currentLocalProjects = []; FAVORITEN & ZULETZT GEÖFFNET — State & Helpers ================================================ */ -let favorites = []; // [{ owner, repo, cloneUrl, addedAt }] +let favorites = []; // [{ owner, repo, cloneUrl, platform, displayName, addedAt }] let recentRepos = []; // [{ owner, repo, cloneUrl, openedAt }] // Feature-Flags (aus Settings, persistent via Credentials) @@ -17,6 +17,8 @@ let featureFavorites = true; let featureRecent = true; let compactMode = false; let featureAutostart = false; +const MAX_PINBOARD_REPOS = 5; +let pinnedRepos = []; // [{ owner, repo, cloneUrl, platform, pinnedAt }] let repoNameValidationTimer = null; let batchCloneValidationTimer = null; let activityHeatmapCollapsed = true; @@ -30,7 +32,31 @@ let repoTopicsByFullName = {}; // owner/repo -> string[] let repoKnownTopics = []; // all known topics across repos let repoKnownTopicsLoadedAt = 0; let currentGiteaUsername = ''; -let activeRepoOwnerFilter = 'mine'; // mine | all | owner: +let activeRepoOwnerFilter = 'mine'; // mine | all | archived | owner: + +function reportUnhandledRendererIssue(kind, errorLike, extra = {}) { + try { + const errorObj = errorLike instanceof Error + ? { name: errorLike.name, message: errorLike.message, stack: errorLike.stack } + : { message: String(errorLike || 'Unknown renderer error') }; + window.electronAPI?.debugToMain?.('error', `renderer:${kind}`, { ...extra, error: errorObj }); + } catch (_) { + // never break UI because of diagnostics reporting + } +} + +window.addEventListener('error', (event) => { + reportUnhandledRendererIssue('window-error', event?.error || event?.message || 'window error', { + fileName: event?.filename || '', + line: event?.lineno || 0, + column: event?.colno || 0 + }); +}); + +window.addEventListener('unhandledrejection', (event) => { + const reason = event?.reason; + reportUnhandledRendererIssue('unhandledrejection', reason || 'Unhandled rejection'); +}); function normalizePlatform(value) { return value === 'github' ? 'github' : 'gitea'; @@ -50,13 +76,178 @@ function platformEntries(items, platform = currentPlatformKey()) { return (Array.isArray(items) ? items : []).filter(e => normalizePlatform(e?.platform || 'gitea') === p); } +function getPinnedRepoKey(owner, repo, platform = currentPlatformKey()) { + return `${normalizePlatform(platform)}::${String(owner || '').toLowerCase()}::${String(repo || '').toLowerCase()}`; +} + +function normalizePinnedRepoEntry(entry, fallbackPlatform = currentPlatformKey()) { + if (!entry || typeof entry !== 'object') return null; + const owner = String(entry.owner || '').trim(); + const repo = String(entry.repo || '').trim(); + if (!owner || !repo) return null; + return { + owner, + repo, + cloneUrl: String(entry.cloneUrl || '').trim(), + platform: normalizePlatform(entry.platform || fallbackPlatform), + pinnedAt: entry.pinnedAt || new Date().toISOString() + }; +} + +function getPinnedReposForPlatform(platform = currentPlatformKey()) { + const p = normalizePlatform(platform); + return pinnedRepos.filter(entry => normalizePlatform(entry.platform || 'gitea') === p); +} + +function isRepoPinned(owner, repo, platform = currentPlatformKey()) { + const key = getPinnedRepoKey(owner, repo, platform); + return pinnedRepos.some(entry => getPinnedRepoKey(entry.owner, entry.repo, entry.platform) === key); +} + +async function persistPinnedRepos() { + const creds = await window.electronAPI.loadCredentials() || {}; + await window.electronAPI.saveCredentials({ + ...creds, + pinnedRepos + }); +} + +async function toggleRepoPinned(owner, repo, cloneUrl = '', platform = currentPlatformKey()) { + const key = getPinnedRepoKey(owner, repo, platform); + const exists = pinnedRepos.some(entry => getPinnedRepoKey(entry.owner, entry.repo, entry.platform) === key); + + if (exists) { + pinnedRepos = pinnedRepos.filter(entry => getPinnedRepoKey(entry.owner, entry.repo, entry.platform) !== key); + await persistPinnedRepos(); + showInfo(`Pin entfernt: ${owner}/${repo}`); + loadGiteaRepos(); + return; + } + + const platformPins = getPinnedReposForPlatform(platform); + if (platformPins.length >= MAX_PINBOARD_REPOS) { + showWarning(`Maximal ${MAX_PINBOARD_REPOS} Pins pro Plattform erlaubt.`); + return; + } + + pinnedRepos.unshift({ + owner, + repo, + cloneUrl: String(cloneUrl || '').trim(), + platform: normalizePlatform(platform), + pinnedAt: new Date().toISOString() + }); + await persistPinnedRepos(); + showSuccess(`Angepinnt: ${owner}/${repo}`); + loadGiteaRepos(); +} + +function renderRepoPinboard(grid, allRepos) { + if (!grid) return; + + const platform = currentPlatformKey(); + const pins = getPinnedReposForPlatform(platform) + .slice() + .sort((a, b) => new Date(b.pinnedAt || 0).getTime() - new Date(a.pinnedAt || 0).getTime()) + .slice(0, MAX_PINBOARD_REPOS); + + if (pins.length === 0) return; + + const wrap = document.createElement('div'); + wrap.className = 'repo-pinboard-wrap'; + wrap.style.cssText = 'grid-column: 1/-1; margin-bottom: 12px;'; + + const title = document.createElement('div'); + title.className = 'repo-pinboard-title'; + title.textContent = `📌 Pinboard (${pins.length}/${MAX_PINBOARD_REPOS})`; + wrap.appendChild(title); + + const row = document.createElement('div'); + row.className = 'repo-pinboard-row'; + + pins.forEach(pin => { + const fullName = `${pin.owner}/${pin.repo}`; + const match = Array.isArray(allRepos) + ? allRepos.find(r => { + const owner = r?.owner?.login || r?.owner?.username || ''; + const name = r?.name || ''; + return owner === pin.owner && name === pin.repo; + }) + : null; + + const chip = document.createElement('button'); + chip.type = 'button'; + chip.className = 'repo-pinboard-chip' + (match ? '' : ' is-missing'); + chip.title = match ? `${fullName} öffnen` : `${fullName} (nicht in Liste gefunden)`; + + const icon = document.createElement('span'); + icon.className = 'repo-pinboard-chip-icon'; + icon.textContent = '📌'; + chip.appendChild(icon); + + const label = document.createElement('span'); + label.className = 'repo-pinboard-chip-label'; + label.textContent = pin.repo; + chip.appendChild(label); + + const ownerEl = document.createElement('span'); + ownerEl.className = 'repo-pinboard-chip-owner'; + ownerEl.textContent = pin.owner; + chip.appendChild(ownerEl); + + if (!match) { + const missingEl = document.createElement('span'); + missingEl.className = 'repo-pinboard-chip-missing'; + missingEl.textContent = 'nicht gefunden'; + chip.appendChild(missingEl); + } + + chip.addEventListener('click', async () => { + if (!match) { + showWarning(`${fullName} ist aktuell nicht in der Liste verfügbar.`); + return; + } + addToRecent(pin.owner, pin.repo, pin.cloneUrl || match?.clone_url || match?.clone_url_ssh || '', platform); + loadRepoContents(pin.owner, pin.repo, ''); + }); + + chip.addEventListener('contextmenu', (ev) => { + ev.preventDefault(); + showRepoContextMenu( + ev, + pin.owner, + pin.repo, + pin.cloneUrl || match?.clone_url || match?.clone_url_ssh || '', + chip, + !!match?.private, + match || null + ); + }); + + row.appendChild(chip); + }); + + wrap.appendChild(row); + grid.appendChild(wrap); +} + async function loadFavoritesAndRecent() { try { const [favRes, recRes] = await Promise.all([ window.electronAPI.loadFavorites(), window.electronAPI.loadRecent() ]); - if (favRes && favRes.ok) favorites = (favRes.favorites || []).map(e => withPlatform(e, 'gitea')); + if (favRes && favRes.ok) { + favorites = (favRes.favorites || []).map((e) => { + const base = withPlatform(e, 'gitea'); + if (!base || typeof base !== 'object') return base; + const normalizedAlias = String(base.displayName ?? base.alias ?? '').trim(); + return { + ...base, + displayName: normalizedAlias || null + }; + }); + } if (recRes && recRes.ok) recentRepos = (recRes.recent || []).map(e => withPlatform(e, 'gitea')); } catch(e) { console.error('loadFavoritesAndRecent:', e); @@ -73,12 +264,37 @@ async function toggleFavorite(owner, repo, cloneUrl, platform = currentPlatformK if (isFavorite(owner, repo, p)) { favorites = favorites.filter(f => !(f.owner === owner && f.repo === repo && normalizePlatform(f.platform || 'gitea') === p)); } else { - favorites.unshift({ owner, repo, cloneUrl, platform: p, addedAt: new Date().toISOString() }); + favorites.unshift({ owner, repo, cloneUrl, platform: p, displayName: null, addedAt: new Date().toISOString() }); } await window.electronAPI.saveFavorites(favorites); refreshFavHistoryUi(); } +async function setFavoriteAlias(owner, repo, platform = currentPlatformKey()) { + const targetPlatform = normalizePlatform(platform); + const fav = favorites.find(f => f.owner === owner && f.repo === repo && normalizePlatform(f.platform || 'gitea') === targetPlatform) + || favorites.find(f => f.owner === owner && f.repo === repo); + if (!fav) { + showError('Favorit nicht gefunden'); + return; + } + + const currentAlias = fav.displayName || ''; + showInputModal({ + title: '📝 Alias setzen', + label: `Alias für "${owner}/${repo}" (leer = entfernen):`, + defaultValue: currentAlias, + confirmText: '✓ Speichern', + onConfirm: async (newAlias) => { + fav.displayName = newAlias ? newAlias : null; + if (Object.prototype.hasOwnProperty.call(fav, 'alias')) delete fav.alias; + await window.electronAPI.saveFavorites(favorites); + refreshFavHistoryUi(); + showSuccess(newAlias ? `Alias gespeichert: ${newAlias}` : 'Alias entfernt'); + } + }); +} + async function addToRecent(owner, repo, cloneUrl, platform = currentPlatformKey()) { if (!featureRecent) return; const p = normalizePlatform(platform); @@ -352,7 +568,11 @@ function renderFavHistorySidebar(allRepos) { const name = document.createElement('span'); name.className = 'fav-history-item-name'; - name.textContent = entry.repo || '-'; + const sidebarDisplayName = (activeType === 'favorites' && (entry.displayName || entry.alias)) + ? (entry.displayName || entry.alias) + : (entry.repo || '-'); + name.textContent = sidebarDisplayName; + name.title = `${entry.owner || '-'} / ${entry.repo || '-'}`; const meta = document.createElement('span'); meta.className = 'fav-history-item-meta'; @@ -413,7 +633,9 @@ function makeChip(entry, type, allRepos) { const label = document.createElement('span'); label.className = 'fav-chip-label'; - label.textContent = `${entry.owner}/${entry.repo}`; + const displayName = entry.displayName || entry.alias || entry.repo; + label.textContent = (entry.displayName || entry.alias) ? `${displayName}` : `${entry.owner}/${entry.repo}`; + label.title = `${entry.owner}/${entry.repo}`; chip.appendChild(icon); chip.appendChild(label); @@ -517,6 +739,15 @@ function showChipContextMenu(ev, entry, type) { const fullName = `${entry.owner || ''}/${entry.repo || ''}`; const isPrivate = !!repoPrivacyByFullName[fullName]; + const matchedRepo = Array.isArray(currentGiteaRepos) + ? currentGiteaRepos.find(r => { + const o = r?.owner?.login || r?.owner?.username || ''; + const n = r?.name || ''; + return o === entry.owner && n === entry.repo; + }) + : null; + const isArchived = !!matchedRepo?.archived; + const canArchive = !!matchedRepo && isRepoWritable(matchedRepo, currentGiteaUsername); addItem( isPrivate ? '🌍' : '🔒', isPrivate ? 'Öffentlich machen' : 'Privat machen', @@ -525,6 +756,25 @@ function showChipContextMenu(ev, entry, type) { } ); + if (canArchive) { + addItem( + isArchived ? '📦' : '🗄️', + isArchived ? 'Archivierung rückgängig machen' : 'Archivieren', + async () => { + await toggleRepoArchived(entry.owner, entry.repo, isArchived); + } + ); + } + + const pinned = isRepoPinned(entry.owner, entry.repo, entry.platform || currentPlatformKey()); + addItem( + pinned ? '📍' : '📌', + pinned ? 'Vom Pinboard lösen' : 'An Pinboard anheften', + async () => { + await toggleRepoPinned(entry.owner, entry.repo, entry.cloneUrl || '', entry.platform || currentPlatformKey()); + } + ); + const chipTopics = repoTopicsByFullName[fullName] || []; addItem('🏷️', 'Tags bearbeiten', async () => { await editRepoTopics(entry.owner, entry.repo, chipTopics); @@ -536,6 +786,10 @@ function showChipContextMenu(ev, entry, type) { menu.appendChild(sep); if (type === 'favorite') { + addItem('📝', 'Alias setzen', async () => { + menu.remove(); + await setFavoriteAlias(entry.owner, entry.repo, entry.platform || currentPlatformKey()); + }); addItem('⭐', 'Aus Favoriten entfernen', async () => { await toggleFavorite(entry.owner, entry.repo, entry.cloneUrl, entry.platform || currentPlatformKey()); loadGiteaRepos(); @@ -714,6 +968,33 @@ async function toggleRepoVisibility(owner, repoName, currentPrivate) { } } +async function toggleRepoArchived(owner, repoName, currentlyArchived) { + try { + const targetArchived = !currentlyArchived; + const actionText = targetArchived ? 'archiviert' : 'entarchiviert'; + showProgress(35, `Repository wird ${targetArchived ? 'archiviert' : 'entarchiviert'}...`); + + const result = await window.electronAPI.updateRepoArchived({ + owner, + repo: repoName, + archived: targetArchived, + platform: currentState.platform + }); + hideProgress(); + + if (result?.ok) { + showSuccess(`Repository wurde ${actionText}.`); + loadGiteaRepos(); + } else { + showError('Archivierung fehlgeschlagen: ' + (result?.error || 'Unbekannter Fehler')); + } + } catch (error) { + hideProgress(); + console.error('Archive toggle error:', error); + showError('Archivierung fehlgeschlagen'); + } +} + function showTagsEditorModal(owner, repoName, seed, knownTopics) { return new Promise((resolve) => { const modal = document.createElement('div'); @@ -956,14 +1237,12 @@ function getDefaultBranch(owner, repo) { // Navigations-Status für die Explorer-Ansicht let currentState = { - view: 'none', // 'local', 'gitea-list', 'gitea-repo', 'gitea-trash' + view: 'none', // 'local', 'gitea-list', 'gitea-repo' owner: null, repo: null, path: '', platform: 'gitea' // 'gitea' | 'github' }; -let lastRepoPathBeforeTrash = ''; - let repoLoadRequestId = 0; const USER_CACHE_MS = 5 * 60 * 1000; let currentUserCache = { @@ -1667,16 +1946,20 @@ function renderSettingsHealth() { function syncSettingsPanelHeights() { const credentialsPanel = document.querySelector('#settingsModal .settings-panel--credentials'); const healthPanel = document.querySelector('#settingsModal .settings-panel--health'); - if (!credentialsPanel || !healthPanel) return; - healthPanel.style.minHeight = ''; + if (healthPanel) { + healthPanel.style.minHeight = ''; + healthPanel.style.height = ''; + } // In der einspaltigen Ansicht sollen die Karten natuerlich fliessen. if (window.matchMedia('(max-width: 1120px)').matches) return; - const targetHeight = Math.ceil(credentialsPanel.getBoundingClientRect().height); - if (targetHeight > 0) { - healthPanel.style.minHeight = `${targetHeight}px`; + if (credentialsPanel && healthPanel) { + const topRightTargetHeight = Math.ceil(credentialsPanel.getBoundingClientRect().height); + if (topRightTargetHeight > 0) { + healthPanel.style.minHeight = `${topRightTargetHeight}px`; + } } } @@ -1901,6 +2184,7 @@ function fuzzyScoreToken(token, text) { if (idx >= 0) { let score = 120 - Math.min(idx, 60); if (t.startsWith(q)) score += 20; + else if (idx > 0) score += 10; // Teilwort-Treffer, aber nicht am Anfang return score; } @@ -1915,6 +2199,9 @@ function fuzzyScoreToken(token, text) { ti = found + 1; } + // Für Teilwort-Suche: Wenn q ein echter Substring von t ist, aber nicht am Anfang, gibt es einen kleinen Score + if (t.includes(q)) return 25; + return Math.max(8, 80 - gaps * 2); } @@ -1947,16 +2234,18 @@ function getRepoCardSearchScore(card, query) { function applyRepoFuzzyFilter(grid, searchInput, searchMetaEl) { if (!grid) return; - const cards = Array.from(grid.querySelectorAll('.item-card')); + const cards = Array.from(grid.querySelectorAll('.item-card[data-repo-card="main"]')); const query = (searchInput?.value || '').trim(); const isOwnerMatch = (card) => { const owner = String(card.dataset.searchOwner || '').toLowerCase(); const isShared = String(card.dataset.shared || '') === 'true'; + const isArchived = String(card.dataset.archived || '') === 'true'; if (activeRepoOwnerFilter === 'all') return true; + if (activeRepoOwnerFilter === 'archived') return isArchived; if (activeRepoOwnerFilter === 'mine') { if (!currentGiteaUsername) return true; - return owner === String(currentGiteaUsername).toLowerCase(); + return owner === String(currentGiteaUsername).toLowerCase() && !isArchived; } if (activeRepoOwnerFilter === 'shared') { return isShared; @@ -2959,8 +3248,6 @@ function updateNavigationUI() { // Back Button zeigen, wenn wir in einem Repo oder tief in Ordnern sind if (currentState.view === 'gitea-repo' || - currentState.view === 'gitea-trash' || - currentState.view === 'gitea-trash-global' || (currentState.view === 'gitea-list' && currentState.path !== '')) { btnBack.classList.remove('hidden'); } else { @@ -2968,331 +3255,14 @@ function updateNavigationUI() { } } +// loadGlobalTrashView: Papierkorb entfernt 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`); + showError('Papierkorb wurde deaktiviert.'); } -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`); +// loadTrashView: Papierkorb entfernt +async function loadTrashView(_owner, _repo) { + showError('Papierkorb wurde deaktiviert.'); } /* ------------------------- @@ -3307,14 +3277,8 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) { // Verstecke Commits & Releases-Buttons in Repo-Liste const btnCommits = $('btnCommits'); const btnReleases = $('btnReleases'); - const btnTrash = $('btnTrash'); - const btnGlobalTrash = $('btnGlobalTrash'); - const btnPurgeTrash = $('btnPurgeTrash'); if (btnCommits) btnCommits.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 const grid = $('explorerGrid'); @@ -3341,11 +3305,6 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) { } currentGiteaRepos = Array.isArray(res.repos) ? res.repos : []; - const btnGlobalTrash = $('btnGlobalTrash'); - if (btnGlobalTrash && currentState.platform === 'gitea') { - btnGlobalTrash.classList.remove('hidden'); - btnGlobalTrash.onclick = () => loadGlobalTrashView(); - } if (!preloadedData) { try { const cachedName = getCachedUsername('gitea'); @@ -3385,9 +3344,16 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) { ownerCounts.set(owner, (ownerCounts.get(owner) || 0) + 1); } const myOwner = String(currentGiteaUsername || '').trim(); + const archivedCount = currentGiteaRepos.filter(r => !!r?.archived).length; + const mineActiveCount = currentGiteaRepos.filter(r => { + const owner = String(r?.owner?.login || r?.owner?.username || '').trim().toLowerCase(); + if (!myOwner) return !r?.archived; + return owner === myOwner.toLowerCase() && !r?.archived; + }).length; const tabDefs = [ - { key: 'mine', label: 'Meine', count: myOwner ? (ownerCounts.get(myOwner) || 0) : currentGiteaRepos.length }, + { key: 'mine', label: 'Meine', count: mineActiveCount }, + { key: 'archived', label: 'Archiviert', count: archivedCount }, { key: 'all', label: 'Alle', count: currentGiteaRepos.length } ]; Array.from(ownerCounts.keys()) @@ -3401,6 +3367,9 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) { if (activeRepoOwnerFilter === 'shared') { activeRepoOwnerFilter = 'all'; } + if (activeRepoOwnerFilter === 'archived' && archivedCount === 0) { + activeRepoOwnerFilter = 'all'; + } const makeTab = (tab) => { const btn = document.createElement('button'); @@ -3420,6 +3389,7 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) { return btn; }; tabDefs.forEach(tab => ownerTabsWrap.appendChild(makeTab(tab))); + renderRepoPinboard(grid, currentGiteaRepos); grid.appendChild(ownerTabsWrap); const searchContainer = document.createElement('div'); @@ -3531,6 +3501,7 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) { const card = document.createElement('div'); card.className = 'item-card'; card.style.position = 'relative'; + card.dataset.repoCard = 'main'; card.dataset.cloneUrl = cloneUrl; card.dataset.owner = owner; card.dataset.repo = repoName; @@ -3545,9 +3516,18 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) { const sharedByOthers = !!ownerLower && !!myOwnerLower && ownerLower !== myOwnerLower; card.dataset.shared = sharedByOthers ? 'true' : 'false'; card.dataset.searchSharedOwner = sharedByOthers ? `geteilt von ${owner}` : ''; + card.dataset.archived = repo?.archived ? 'true' : 'false'; const writable = isRepoWritable(repo, currentGiteaUsername); const readOnly = !writable; card.dataset.readOnly = readOnly ? 'true' : 'false'; + const metaFooter = document.createElement('div'); + metaFooter.className = 'repo-card-footer'; + const archivedBadge = repo?.archived + ? Object.assign(document.createElement('div'), { + className: 'repo-archived-badge', + textContent: 'Archiviert' + }) + : null; // Stern-Button (nur wenn Favoriten-Feature aktiv) if (featureFavorites) { @@ -3606,7 +3586,15 @@ async function loadGiteaRepos(preloadedData = null, requestId = null) { sizeEl.textContent = kb >= 1024 ? `${(kb / 1024).toFixed(1)} MB` : `${kb} KB`; - card.appendChild(sizeEl); + metaFooter.appendChild(sizeEl); + } + + if (archivedBadge) { + metaFooter.appendChild(archivedBadge); + } + + if (metaFooter.childElementCount > 0) { + card.appendChild(metaFooter); } // --- Nativer Drag Start (Download) --- @@ -3748,10 +3736,6 @@ async function loadRepoContents(owner, repo, path) { // Zeige Commits & Releases-Buttons wenn wir in einem Repo sind const btnCommits = $('btnCommits'); const btnReleases = $('btnReleases'); - const btnTrash = $('btnTrash'); - const btnGlobalTrash = $('btnGlobalTrash'); - const btnPurgeTrash = $('btnPurgeTrash'); - if (btnCommits) { btnCommits.classList.remove('hidden'); btnCommits.onclick = () => loadCommitHistory(owner, repo, getDefaultBranch(owner, repo)); @@ -3762,16 +3746,6 @@ async function loadRepoContents(owner, repo, path) { 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 const grid = $('explorerGrid'); if (grid) { @@ -3971,12 +3945,6 @@ async function selectLocalFolder() { await refreshLocalTree(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) { console.error('Error selecting folder:', error); showError('Error selecting folder'); @@ -4437,7 +4405,18 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element, isPrivate = else showError('Clone-URL konnte nicht kopiert werden'); })); + const pinned = isRepoPinned(owner, repoName, currentPlatformKey()); + menu.appendChild(createMenuItem( + pinned ? '📍' : '📌', + pinned ? 'Vom Pinboard lösen' : 'An Pinboard anheften', + async () => { + menu.remove(); + await toggleRepoPinned(owner, repoName, cloneUrl || '', currentPlatformKey()); + } + )); + const isOwnRepo = String(owner || '').toLowerCase() === String(currentGiteaUsername || '').toLowerCase(); + const isArchived = !!repoMeta?.archived; if (isOwnRepo) { const isGiteaView = currentState.platform === 'gitea'; const syncLabel = isGiteaView ? 'Gitea -> GitHub synchronisieren' : 'GitHub -> Gitea synchronisieren'; @@ -4548,6 +4527,15 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element, isPrivate = await toggleRepoVisibility(owner, repoName, isPrivate); } )); + + menu.appendChild(createMenuItem( + isArchived ? '📦' : '🗄️', + isArchived ? 'Archivierung rückgängig machen' : 'Archivieren', + async () => { + menu.remove(); + await toggleRepoArchived(owner, repoName, isArchived); + } + )); } addSep(); @@ -4663,7 +4651,7 @@ function showGiteaItemContextMenu(ev, item, owner, repo) { el.className = 'context-item'; el.textContent = `${icon} ${text}`; if (color) el.style.color = color; - el.onclick = () => { menu.remove(); onClick(); }; + el.onclick = () => { menu.remove(); Promise.resolve().then(() => onClick()).catch(e => console.error('context-menu error', e)); }; menu.appendChild(el); }; @@ -4687,7 +4675,7 @@ function showGiteaItemContextMenu(ev, item, owner, repo) { let done = 0; for (const p of selectedItems) { const isGithub = currentState.platform === 'github'; - await window.electronAPI.deleteFile({ path: p, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo), softDelete: true }); + await window.electronAPI.deleteFile({ path: p, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo) }); done++; showProgress(Math.round((done / selectedItems.size) * 100), `Lösche ${done}/${selectedItems.size}`); } @@ -4765,10 +4753,10 @@ function showGiteaItemContextMenu(ev, item, owner, repo) { if (!ok) return; showProgress(0, `Lösche ${item.name}...`); const isGithub = currentState.platform === 'github'; - const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo), softDelete: true }); + const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: !isGithub, isGithub, ref: getDefaultBranch(owner, repo) }); hideProgress(); if (res && res.ok) { - setStatus(`${item.name} gelöscht` + (res.softDeleteWarning ? ` (${res.softDeleteWarning})` : '')); + setStatus(`${item.name} gelöscht`); loadRepoContents(owner, repo, currentState.path); } else { showError('Löschen fehlgeschlagen: ' + (res?.error || '')); @@ -4811,7 +4799,7 @@ function showLocalItemContextMenu(ev, node) { el.className = 'context-item'; el.textContent = `${icon} ${text}`; if (color) el.style.color = color; - el.onclick = () => { menu.remove(); onClick(); }; + el.onclick = () => { menu.remove(); Promise.resolve().then(() => onClick()).catch(e => console.error('context-menu error', e)); }; menu.appendChild(el); }; @@ -5308,7 +5296,7 @@ function setupBackgroundContextMenu() { const el = document.createElement('div'); el.className = 'context-item'; el.textContent = `${icon} ${text}`; - el.onclick = () => { menu.remove(); onClick(); }; + el.onclick = () => { menu.remove(); Promise.resolve().then(() => onClick()).catch(e => console.error('context-menu error', e)); }; menu.appendChild(el); }; @@ -5545,6 +5533,11 @@ window.addEventListener('DOMContentLoaded', async () => { if (typeof creds.featureFavorites === 'boolean') featureFavorites = creds.featureFavorites; if (typeof creds.featureRecent === 'boolean') featureRecent = creds.featureRecent; if (typeof creds.compactMode === 'boolean') compactMode = creds.compactMode; + if (Array.isArray(creds.pinnedRepos)) { + pinnedRepos = creds.pinnedRepos + .map(entry => normalizePinnedRepoEntry(entry, currentPlatformKey())) + .filter(Boolean); + } if (typeof creds.featureColoredIcons === 'boolean') featureColoredIcons = creds.featureColoredIcons; document.body.classList.toggle('compact-mode', compactMode); @@ -5649,34 +5642,10 @@ window.addEventListener('DOMContentLoaded', async () => { parts.pop(); 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 if ($('btnWinMinimize')) $('btnWinMinimize').onclick = () => window.electronAPI.windowMinimize(); if ($('btnWinMaximize')) $('btnWinMaximize').onclick = () => window.electronAPI.windowMaximize(); @@ -6204,6 +6173,7 @@ window.addEventListener('DOMContentLoaded', async () => { featureRecent, compactMode, featureColoredIcons, + pinnedRepos, favCollapsedFavorites: favSectionCollapsed.favorites, favCollapsedRecent: favSectionCollapsed.recent }; @@ -6353,11 +6323,10 @@ window.addEventListener('DOMContentLoaded', async () => { repo, isGitea: !isGithub, isGithub, - ref: getDefaultBranch(owner, repo), - softDelete: true + ref: getDefaultBranch(owner, repo) }); if (res?.ok) { - showSuccess(`"${item.name}" gelöscht` + (res.softDeleteWarning ? ` (${res.softDeleteWarning})` : '')); + showSuccess(`"${item.name}" gelöscht`); loadRepoContents(owner, repo, currentState.path); lastSelectedItem = null; } @@ -7930,6 +7899,69 @@ async function initUpdater() { } }; } + + if ($('btnExportSettingsBundle')) { + $('btnExportSettingsBundle').onclick = async () => { + try { + const res = await window.electronAPI.exportSettingsBundle(); + if (res?.ok) { + showSuccess('Backup exportiert'); + setStatus('Backup exportiert'); + } else if (!res?.canceled) { + showError('Backup-Export fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler')); + setStatus('Backup-Export fehlgeschlagen'); + } + } catch (error) { + showError('Backup-Export fehlgeschlagen: ' + (error?.message || error)); + setStatus('Backup-Export fehlgeschlagen'); + } + }; + } + + if ($('btnImportSettingsBundle')) { + $('btnImportSettingsBundle').onclick = async () => { + const ok = await showActionConfirmModal({ + title: 'Backup importieren', + message: 'Vorhandene Einstellungen koennen ueberschrieben werden. Fortfahren?', + confirmText: 'Import starten', + danger: false + }); + if (!ok) return; + + try { + const res = await window.electronAPI.importSettingsBundle(); + if (res?.ok) { + showSuccess('Backup importiert. Ansicht wird aktualisiert...'); + setStatus('Backup importiert'); + setTimeout(() => window.location.reload(), 650); + } else if (!res?.canceled) { + showError('Backup-Import fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler')); + setStatus('Backup-Import fehlgeschlagen'); + } + } catch (error) { + showError('Backup-Import fehlgeschlagen: ' + (error?.message || error)); + setStatus('Backup-Import fehlgeschlagen'); + } + }; + } + + if ($('btnCreateDiagnostics')) { + $('btnCreateDiagnostics').onclick = async () => { + try { + const res = await window.electronAPI.createDiagnosticsPackage(); + if (res?.ok) { + showSuccess('Diagnosepaket erstellt'); + setStatus('Diagnosepaket erstellt'); + } else if (!res?.canceled) { + showError('Diagnosepaket fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler')); + setStatus('Diagnosepaket fehlgeschlagen'); + } + } catch (error) { + showError('Diagnosepaket fehlgeschlagen: ' + (error?.message || error)); + setStatus('Diagnosepaket fehlgeschlagen'); + } + }; + } } // Event-Listener für das Update-Modal @@ -7968,6 +8000,18 @@ if (window.electronAPI.onUpdateNotAvailable) { }); } +if (window.electronAPI.onUpdateError) { + window.electronAPI.onUpdateError((payload) => { + const message = String(payload?.message || 'Update-Fehler'); + const silent = !!payload?.details?.silent; + + if (!silent) { + showError(message); + } + setStatus(message); + }); +} + // AM ENDE DER DATEI: Initialisierung beim Start document.addEventListener('DOMContentLoaded', () => { // 1. Basis-Setup (Settings-Feld füllen etc.) diff --git a/renderer/style.css b/renderer/style.css index 2f8156d..e087bb2 100644 --- a/renderer/style.css +++ b/renderer/style.css @@ -651,8 +651,7 @@ body { } #btnOpenRepoActions, -#btnPush, -#btnGlobalTrash { +#btnPush { min-width: 92px; } @@ -1759,6 +1758,18 @@ input[type="checkbox"] { margin: 0; } +.settings-version-field .settings-readonly-input { + height: 40px; + padding-top: 0; + padding-bottom: 0; + line-height: 40px; + text-align: center; + font-size: 16px; + font-weight: 400; + letter-spacing: 0.01em; + color: var(--text-primary); +} + .settings-readonly-input { background: rgba(255,255,255,0.05); color: var(--text-secondary); @@ -1767,15 +1778,17 @@ input[type="checkbox"] { .settings-update-btn { min-width: 210px; - height: 42px; - padding: 0 16px; + height: 40px; + padding: 0 12px; border-radius: var(--radius-md); border: 1px solid rgba(88, 213, 255, 0.3); background: linear-gradient(135deg, rgba(88, 213, 255, 0.94), rgba(92, 135, 255, 0.9)); color: #08111f; - font-size: 13px; - font-weight: 800; - letter-spacing: 0.01em; + font-size: 12px; + font-weight: 700; + letter-spacing: 0; + white-space: nowrap; + line-height: 1; cursor: pointer; transition: transform var(--transition-normal), box-shadow var(--transition-normal), filter var(--transition-normal); box-shadow: 0 16px 28px rgba(24, 136, 255, 0.26); @@ -1791,6 +1804,121 @@ input[type="checkbox"] { transform: translateY(0); } +.settings-tools-grid { + margin-top: 0; + display: grid; + grid-template-columns: 1fr; + gap: 8px; +} + +.settings-backup-card { + margin-top: 6px; +} + +.settings-diagnostics-card { + margin-top: 4px; +} + +.settings-backup-title { + margin: 0 0 8px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-secondary); +} + +.settings-action-btn { + height: 40px; + border-radius: var(--radius-md); + padding: 0 14px; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.15px; + cursor: pointer; + border: 1px solid rgba(255, 255, 255, 0.18); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.06) 0%, rgba(0, 212, 255, 0.08) 100%); + color: var(--text-primary); + transition: transform var(--transition-normal), box-shadow var(--transition-normal), border-color var(--transition-normal), background var(--transition-normal); +} + +.settings-action-btn:hover { + transform: translateY(-1px); + border-color: rgba(88, 213, 255, 0.5); + background: linear-gradient(135deg, rgba(88, 213, 255, 0.17) 0%, rgba(92, 135, 255, 0.15) 100%); + box-shadow: 0 10px 20px rgba(8, 20, 40, 0.24); +} + +.settings-action-btn:active { + transform: translateY(0); +} + +.settings-tools-grid .settings-action-btn { + width: 100%; + justify-content: center; +} + +.settings-panel--app { + padding-bottom: 10px; +} + +.settings-panel--app .settings-panel-header { + margin-bottom: 8px; +} + +.settings-panel--app .settings-version-card { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + align-items: end; +} + +.settings-panel--app .settings-update-btn { + width: 100%; + min-width: 0; + height: 40px; +} + +.settings-panel--app .settings-inline-hint { + margin-top: 6px; +} + +.settings-panel--backup .settings-inline-hint { + margin-top: 6px; +} + +.settings-panel--backup .settings-tools-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.settings-panel--diagnostics { + padding: 11px; + height: 140px; +} + +.settings-panel--backup { + padding: 11px; + height: 165px; +} + +.settings-panel--diagnostics .settings-panel-header { + margin-bottom: 7px; +} + +.settings-panel--backup .settings-panel-header { + margin-bottom: 7px; +} + +.settings-panel--diagnostics .settings-panel-header p { + font-size: 10px; + line-height: 1.25; +} + +.settings-panel--backup .settings-panel-header p { + font-size: 10px; + line-height: 1.25; +} + .settings-modal-actions { margin-top: 12px; } @@ -1987,6 +2115,84 @@ input[type="checkbox"] { gap: 8px; } +.repo-pinboard-wrap { + border: 1px solid rgba(88, 213, 255, 0.22); + background: linear-gradient(135deg, rgba(88, 213, 255, 0.07), rgba(92, 135, 255, 0.08)); + border-radius: 14px; + padding: 10px 12px; +} + +.repo-pinboard-title { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #d8f7ff; + margin-bottom: 8px; +} + +.repo-pinboard-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.repo-pinboard-chip { + appearance: none; + border: 1px solid rgba(255, 255, 255, 0.16); + background: rgba(7, 17, 31, 0.56); + color: var(--text-primary); + border-radius: 999px; + min-height: 30px; + padding: 0 10px; + font-size: 12px; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 7px; + transition: border-color 140ms ease, background 140ms ease, transform 140ms ease; +} + +.repo-pinboard-chip:hover { + border-color: rgba(88, 213, 255, 0.6); + background: rgba(88, 213, 255, 0.12); + transform: translateY(-1px); +} + +.repo-pinboard-chip-icon { + font-size: 11px; + opacity: 0.9; +} + +.repo-pinboard-chip-label { + font-weight: 700; + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.repo-pinboard-chip-owner { + color: var(--text-muted); + font-size: 11px; + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.repo-pinboard-chip-missing { + color: #fca5a5; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; +} + +.repo-pinboard-chip.is-missing { + border-color: rgba(248, 113, 113, 0.4); + background: rgba(127, 29, 29, 0.2); +} + .repo-owner-tab { appearance: none; border: 1px solid rgba(255, 255, 255, 0.18); @@ -2021,59 +2227,8 @@ input[type="checkbox"] { } /* =========================== - TRASH VIEW + (trash view removed) =========================== */ -.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 { border-color: rgba(88, 213, 255, 0.65); @@ -3567,6 +3722,14 @@ progress::-moz-progress-bar { gap: 12px; } + .settings-panel--diagnostics { + height: auto; + } + + .settings-panel--backup { + height: auto; + } + .settings-credentials-grid { grid-template-columns: minmax(0, 1fr); } @@ -3591,11 +3754,13 @@ progress::-moz-progress-bar { .settings-credentials-grid, .settings-connection-tools, .settings-version-card, + .settings-tools-grid, .modal-buttons { grid-template-columns: minmax(0, 1fr); } .settings-update-btn, + .settings-action-btn, #btnTestGiteaConnection, #btnTestGithubConnection { width: 100%; @@ -3681,10 +3846,38 @@ body.compact-mode .file-type-badge { .repo-size-badge { font-size: 10px; color: var(--text-muted); - margin-top: 4px; + margin-top: 0; opacity: 0.7; } +.repo-card-footer { + margin-top: auto; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + gap: 4px; +} + +.repo-archived-badge { + display: inline-flex; + align-items: center; + justify-content: center; + margin-top: 0; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.02em; + color: #fcd34d; + background: rgba(120, 53, 15, 0.32); + border: 1px solid rgba(245, 158, 11, 0.55); + border-radius: 999px; + padding: 2px 8px; + pointer-events: none; + text-transform: uppercase; + white-space: nowrap; +} + /* =========================== FAVORITEN DRAG-REORDER =========================== */