From 2a9812575c5ec75ad57051693aeaa0a3492e5404 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Wed, 25 Mar 2026 23:07:07 +0100 Subject: [PATCH] Update from Git Manager GUI --- renderer/Settings.jsx | 69 +- renderer/index.html | 220 ++- renderer/renderer.js | 3460 +++++++++++++++++++++++++---------------- renderer/style.css | 737 ++++----- 4 files changed, 2624 insertions(+), 1862 deletions(-) diff --git a/renderer/Settings.jsx b/renderer/Settings.jsx index 4e230ea..b42fea2 100644 --- a/renderer/Settings.jsx +++ b/renderer/Settings.jsx @@ -1,10 +1,13 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; export default function Settings({ onClose }) { const [githubToken, setGithubToken] = useState(''); const [giteaToken, setGiteaToken] = useState(''); const [giteaURL, setGiteaURL] = useState(''); + const [avatarB64, setAvatarB64] = useState(null); const [savedOk, setSavedOk] = useState(false); + const [avatarUploading, setAvatarUploading] = useState(false); + const fileInputRef = useRef(null); function normalizeAndValidateGiteaUrl(rawUrl) { const value = (rawUrl || '').trim(); @@ -36,17 +39,46 @@ export default function Settings({ onClose }) { setGithubToken(data.githubToken || ''); setGiteaToken(data.giteaToken || ''); setGiteaURL(data.giteaURL || ''); + setAvatarB64(data.avatarB64 || null); } }); }, []); - function save() { + function handleAvatarFileChange(e) { + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => setAvatarB64(ev.target.result); + reader.readAsDataURL(file); + } + + async function save() { const checkedUrl = normalizeAndValidateGiteaUrl(giteaURL); if (!checkedUrl.ok) { alert(checkedUrl.error); return; } - window.electronAPI.saveCredentials({ githubToken, giteaToken, giteaURL: checkedUrl.value }); + window.electronAPI.saveCredentials({ + githubToken, + giteaToken, + giteaURL: checkedUrl.value, + avatarB64: avatarB64 || null + }); + + // Avatar automatisch zu Gitea pushen, wenn Token + URL vorhanden + if (avatarB64 && giteaToken && checkedUrl.value) { + setAvatarUploading(true); + const result = await window.electronAPI.updateGiteaAvatar({ + token: giteaToken, + url: checkedUrl.value, + imageBase64: avatarB64 + }); + setAvatarUploading(false); + if (!result.ok) { + console.warn('Avatar-Upload fehlgeschlagen:', result.error); + } + } + setSavedOk(true); setTimeout(() => setSavedOk(false), 2500); } @@ -55,12 +87,28 @@ export default function Settings({ onClose }) {
-
-
Konfiguration
-

⚙️ Einstellungen

-

- Zugangsdaten für GitHub und Gitea hinterlegen. -

+
+
fileInputRef.current?.click()} title="Profilbild ändern"> + {avatarB64 + ? Avatar + :
👤
+ } +
✏️
+ +
+
+
Konfiguration
+

⚙️ Einstellungen

+

+ Zugangsdaten für GitHub und Gitea hinterlegen. +

+
@@ -121,9 +169,10 @@ export default function Settings({ onClose }) { {onClose && ( diff --git a/renderer/index.html b/renderer/index.html index 133935e..665d119 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -4,6 +4,7 @@ Git Manager Explorer Pro + @@ -43,6 +44,7 @@
+
@@ -66,7 +68,7 @@
Quelle - +
@@ -104,10 +106,19 @@
-
-
Konfiguration
-

⚙️ Einstellungen

-

Alle wichtigen Optionen auf einer Seite: Zugangsdaten, Verbindungscheck, Darstellung und Updates.

+
+
+ +
👤
+
✏️
+ +
+ +
+
Konfiguration
+

⚙️ Einstellungen

+

Alle wichtigen Optionen auf einer Seite: Zugangsdaten, Verbindungscheck, Darstellung und Updates.

+
@@ -121,25 +132,35 @@
-
-
- - -
+
+
+
+

GitHub

+ +
+
+ + +
+ +
Hinweis: Der Token wird direkt über api.github.com geprüft.
+
-
- - -
-
- -
- - -
+
+
+

Gitea

+ +
+
+ + +
+
+ + +
Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000
- -
+
@@ -195,43 +216,8 @@ -
- -
-
-
-

💽 Lokale Backups

-

Automatische lokale Backups in einen Zielordner.

-
- -
- -
- -
@@ -350,6 +336,60 @@
+ + + - - - \ No newline at end of file diff --git a/renderer/renderer.js b/renderer/renderer.js index 3a24124..c7f141b 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -4,7 +4,6 @@ const $ = id => document.getElementById(id); let selectedFolder = null; let giteaCache = {}; let currentLocalProjects = []; -let backupGitRepos = []; /* ================================================ FAVORITEN & ZULETZT GEÖFFNET — State & Helpers @@ -26,6 +25,30 @@ let activityHeatmapRangeMonths = 20; // Sidebar-State für die linke Favoriten/Verlauf-Fläche let sidebarMode = 'favorites'; let currentGiteaRepos = []; +let repoPrivacyByFullName = {}; // owner/repo -> boolean(private) +let repoTopicsByFullName = {}; // owner/repo -> string[] +let repoKnownTopics = []; // all known topics across repos +let repoKnownTopicsLoadedAt = 0; +let currentGiteaUsername = ''; +let activeRepoOwnerFilter = 'mine'; // mine | all | owner: + +function normalizePlatform(value) { + return value === 'github' ? 'github' : 'gitea'; +} + +function currentPlatformKey() { + return normalizePlatform(currentState.platform); +} + +function withPlatform(entry, fallback = 'gitea') { + if (!entry || typeof entry !== 'object') return entry; + return { ...entry, platform: normalizePlatform(entry.platform || fallback) }; +} + +function platformEntries(items, platform = currentPlatformKey()) { + const p = normalizePlatform(platform); + return (Array.isArray(items) ? items : []).filter(e => normalizePlatform(e?.platform || 'gitea') === p); +} async function loadFavoritesAndRecent() { try { @@ -33,31 +56,34 @@ async function loadFavoritesAndRecent() { window.electronAPI.loadFavorites(), window.electronAPI.loadRecent() ]); - if (favRes && favRes.ok) favorites = favRes.favorites || []; - if (recRes && recRes.ok) recentRepos = recRes.recent || []; + if (favRes && favRes.ok) favorites = (favRes.favorites || []).map(e => withPlatform(e, 'gitea')); + if (recRes && recRes.ok) recentRepos = (recRes.recent || []).map(e => withPlatform(e, 'gitea')); } catch(e) { console.error('loadFavoritesAndRecent:', e); } } -function isFavorite(owner, repo) { - return favorites.some(f => f.owner === owner && f.repo === repo); +function isFavorite(owner, repo, platform = currentPlatformKey()) { + const p = normalizePlatform(platform); + return favorites.some(f => f.owner === owner && f.repo === repo && normalizePlatform(f.platform || 'gitea') === p); } -async function toggleFavorite(owner, repo, cloneUrl) { - if (isFavorite(owner, repo)) { - favorites = favorites.filter(f => !(f.owner === owner && f.repo === repo)); +async function toggleFavorite(owner, repo, cloneUrl, platform = currentPlatformKey()) { + const p = normalizePlatform(platform); + 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, addedAt: new Date().toISOString() }); + favorites.unshift({ owner, repo, cloneUrl, platform: p, addedAt: new Date().toISOString() }); } await window.electronAPI.saveFavorites(favorites); refreshFavHistoryUi(); } -async function addToRecent(owner, repo, cloneUrl) { +async function addToRecent(owner, repo, cloneUrl, platform = currentPlatformKey()) { if (!featureRecent) return; - recentRepos = recentRepos.filter(r => !(r.owner === owner && r.repo === repo)); - recentRepos.unshift({ owner, repo, cloneUrl, openedAt: new Date().toISOString() }); + const p = normalizePlatform(platform); + recentRepos = recentRepos.filter(r => !(r.owner === owner && r.repo === repo && normalizePlatform(r.platform || 'gitea') === p)); + recentRepos.unshift({ owner, repo, cloneUrl, platform: p, openedAt: new Date().toISOString() }); recentRepos = recentRepos.slice(0, 20); await window.electronAPI.saveRecent(recentRepos); refreshFavHistoryUi(); @@ -76,7 +102,8 @@ function formatRelDate(iso) { return new Date(iso).toLocaleDateString('de-DE'); } -function setPlatformSelection(platform) { +async function setPlatformSelection(platform) { + currentState.platform = platform; const platformInput = $('platform'); if (platformInput) { platformInput.value = platform; @@ -87,6 +114,9 @@ function setPlatformSelection(platform) { button.classList.toggle('active', isActive); button.setAttribute('aria-pressed', isActive ? 'true' : 'false'); }); + + // Direkt laden; fehlende Credentials werden im Loader selbst sauber behandelt. + loadRepos(); } function initializePlatformSelection() { @@ -96,11 +126,81 @@ function initializePlatformSelection() { document.querySelectorAll('.platform-option').forEach(button => { button.addEventListener('click', () => { - setPlatformSelection(button.dataset.platform || 'gitea'); + const p = button.dataset.platform || 'gitea'; + if (p !== currentState.platform) { + setPlatformSelection(p); + } }); }); } +/** Dispatches to the correct repo loader based on the active platform */ +function loadRepos() { + const requestId = ++repoLoadRequestId; + if (currentState.platform === 'github') { + loadGithubRepos(requestId); + } else { + loadGiteaRepos(null, requestId); + } +} + +/** Load repositories from GitHub and render the grid using loadGiteaRepos's renderer */ +async function loadGithubRepos(requestId = null) { + const activeRequestId = requestId || ++repoLoadRequestId; + currentState.view = 'gitea-list'; + currentState.path = ''; + updateNavigationUI(); + + const btnCommits = $('btnCommits'); + const btnReleases = $('btnReleases'); + if (btnCommits) btnCommits.classList.add('hidden'); + if (btnReleases) btnReleases.classList.add('hidden'); + + const grid = $('explorerGrid'); + if (grid) grid.style.gridTemplateColumns = ''; + + setStatus('Loading GitHub repos...'); + + try { + const res = await window.electronAPI.listGithubRepos(); + if (activeRequestId !== repoLoadRequestId) return; + if (!res.ok) { + showError('GitHub: ' + (res.error || 'Unbekannter Fehler')); + return; + } + + // Normalize GitHub repo fields to match what the Gitea renderer expects + const normalizedRepos = (Array.isArray(res.repos) ? res.repos : []).map(r => ({ + ...r, + stars_count: r.stargazers_count || 0, + updated: r.updated_at || r.pushed_at || '', + clone_url: r.clone_url || r.html_url || '' + })); + + try { + const cachedName = getCachedUsername('github'); + if (cachedName) { + currentGiteaUsername = cachedName; + } else { + const meRes = await window.electronAPI.getGithubCurrentUser(); + if (activeRequestId !== repoLoadRequestId) return; + currentGiteaUsername = meRes?.ok ? (meRes.user?.login || '') : ''; + setCachedUsername('github', currentGiteaUsername); + } + } catch (_) { + currentGiteaUsername = ''; + } + + // Delegate rendering to loadGiteaRepos with pre-loaded data + await loadGiteaRepos({ ok: true, repos: normalizedRepos }, activeRequestId); + + } catch (error) { + if (activeRequestId !== repoLoadRequestId) return; + console.error('Error loading GitHub repos:', error); + showError('GitHub Fehler: ' + error.message); + } +} + /* Rendert Favoriten + Zuletzt-geöffnet-Bereich in ein beliebiges Container-Element */ // Collapse-Zustand (wird in Credentials persistiert) const favSectionCollapsed = { favorites: false, recent: false }; @@ -200,7 +300,10 @@ function renderFavHistorySidebar(allRepos) { if (!hasRecFeature && sidebarMode === 'recent') sidebarMode = 'favorites'; const activeType = sidebarMode; - const items = activeType === 'favorites' ? favorites : recentRepos; + const currentPlatform = currentPlatformKey(); + const favoritesCurrent = platformEntries(favorites, currentPlatform); + const recentCurrent = platformEntries(recentRepos, currentPlatform); + const items = activeType === 'favorites' ? favoritesCurrent : recentCurrent; const inner = document.createElement('div'); inner.className = 'fav-history-sidebar-inner'; @@ -211,7 +314,7 @@ function renderFavHistorySidebar(allRepos) { if (hasFavFeature) { const btnFav = document.createElement('button'); btnFav.className = 'fav-history-tab' + (activeType === 'favorites' ? ' active' : ''); - btnFav.textContent = `⭐ Favoriten (${favorites.length})`; + btnFav.textContent = `⭐ Favoriten (${favoritesCurrent.length})`; btnFav.onclick = () => { sidebarMode = 'favorites'; renderFavHistorySidebar(allRepos); @@ -222,7 +325,7 @@ function renderFavHistorySidebar(allRepos) { if (hasRecFeature) { const btnRec = document.createElement('button'); btnRec.className = 'fav-history-tab' + (activeType === 'recent' ? ' active' : ''); - btnRec.textContent = `🕐 Verlauf (${recentRepos.length})`; + btnRec.textContent = `🕐 Verlauf (${recentCurrent.length})`; btnRec.onclick = () => { sidebarMode = 'recent'; renderFavHistorySidebar(allRepos); @@ -262,7 +365,7 @@ function renderFavHistorySidebar(allRepos) { itemBtn.appendChild(name); itemBtn.appendChild(meta); itemBtn.onclick = () => { - addToRecent(entry.owner, entry.repo, entry.cloneUrl); + addToRecent(entry.owner, entry.repo, entry.cloneUrl, entry.platform || currentPlatform); loadRepoContents(entry.owner, entry.repo, ''); }; @@ -289,7 +392,7 @@ function refreshFavHistoryUi() { const card = btn.closest('.item-card'); if (!card) return; const owner = card.dataset.owner; - const repo = card.dataset.repo; + const repo = card.dataset.repo; if (!owner || !repo) return; const active = isFavorite(owner, repo); btn.classList.toggle('active', active); @@ -337,12 +440,12 @@ function makeChip(entry, type, allRepos) { if (isFav) { chip.draggable = true; chip.dataset.owner = entry.owner; - chip.dataset.repo = entry.repo; + chip.dataset.repo = entry.repo; chip.addEventListener('dragstart', (ev) => { ev.dataTransfer.effectAllowed = 'move'; ev.dataTransfer.setData('text/fav-owner', entry.owner); - ev.dataTransfer.setData('text/fav-repo', entry.repo); + ev.dataTransfer.setData('text/fav-repo', entry.repo); chip.classList.add('fav-chip--dragging'); }); chip.addEventListener('dragend', () => chip.classList.remove('fav-chip--dragging')); @@ -358,11 +461,11 @@ function makeChip(entry, type, allRepos) { ev.preventDefault(); chip.classList.remove('fav-chip--drop-target'); const srcOwner = ev.dataTransfer.getData('text/fav-owner'); - const srcRepo = ev.dataTransfer.getData('text/fav-repo'); + const srcRepo = ev.dataTransfer.getData('text/fav-repo'); if (srcOwner === entry.owner && srcRepo === entry.repo) return; const fromIdx = favorites.findIndex(f => f.owner === srcOwner && f.repo === srcRepo); - const toIdx = favorites.findIndex(f => f.owner === entry.owner && f.repo === entry.repo); + const toIdx = favorites.findIndex(f => f.owner === entry.owner && f.repo === entry.repo); if (fromIdx < 0 || toIdx < 0) return; // Reorder @@ -396,23 +499,37 @@ function showChipContextMenu(ev, entry, type) { const menu = document.createElement('div'); menu.id = 'ctxMenu'; menu.className = 'context-menu'; - menu.style.left = Math.min(ev.clientX, window.innerWidth - 240) + 'px'; - menu.style.top = Math.min(ev.clientY, window.innerHeight - 160) + 'px'; - + menu.style.left = Math.min(ev.clientX, window.innerWidth - 240) + 'px'; + menu.style.top = Math.min(ev.clientY, window.innerHeight - 160) + 'px'; const addItem = (icon, text, cb, color) => { const el = document.createElement('div'); el.className = 'context-item'; - el.innerHTML = `${icon} ${text}`; + el.textContent = `${icon} ${text}`; if (color) el.style.color = color; el.onclick = () => { menu.remove(); cb(); }; menu.appendChild(el); }; addItem('📂', 'Öffnen', () => { - addToRecent(entry.owner, entry.repo, entry.cloneUrl); + addToRecent(entry.owner, entry.repo, entry.cloneUrl, entry.platform || currentPlatformKey()); loadRepoContents(entry.owner, entry.repo, ''); }); + const fullName = `${entry.owner || ''}/${entry.repo || ''}`; + const isPrivate = !!repoPrivacyByFullName[fullName]; + addItem( + isPrivate ? '🌍' : '🔒', + isPrivate ? 'Öffentlich machen' : 'Privat machen', + async () => { + await toggleRepoVisibility(entry.owner, entry.repo, isPrivate); + } + ); + + const chipTopics = repoTopicsByFullName[fullName] || []; + addItem('🏷️', 'Tags bearbeiten', async () => { + await editRepoTopics(entry.owner, entry.repo, chipTopics); + }); + // Separator const sep = document.createElement('div'); sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; @@ -420,16 +537,17 @@ function showChipContextMenu(ev, entry, type) { if (type === 'favorite') { addItem('⭐', 'Aus Favoriten entfernen', async () => { - await toggleFavorite(entry.owner, entry.repo, entry.cloneUrl); + await toggleFavorite(entry.owner, entry.repo, entry.cloneUrl, entry.platform || currentPlatformKey()); loadGiteaRepos(); }, '#f59e0b'); } else { addItem('⭐', 'Zu Favoriten hinzufügen', async () => { - await toggleFavorite(entry.owner, entry.repo, entry.cloneUrl); + await toggleFavorite(entry.owner, entry.repo, entry.cloneUrl, entry.platform || currentPlatformKey()); loadGiteaRepos(); }); addItem('✕', 'Aus Verlauf entfernen', async () => { - recentRepos = recentRepos.filter(r => !(r.owner === entry.owner && r.repo === entry.repo)); + const p = normalizePlatform(entry.platform || currentPlatformKey()); + recentRepos = recentRepos.filter(r => !(r.owner === entry.owner && r.repo === entry.repo && normalizePlatform(r.platform || 'gitea') === p)); await window.electronAPI.saveRecent(recentRepos); loadGiteaRepos(); }, '#ef4444'); @@ -439,6 +557,391 @@ function showChipContextMenu(ev, entry, type) { setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); } +async function uploadDroppedPaths({ paths, owner, repo, destPath = '', cloneUrl = null, branch = 'HEAD' }) { + try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:start', { owner, repo, destPath, branch, pathCount: Array.isArray(paths) ? paths.length : 0 }); } catch (_) {} + console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:start', { + owner, + repo, + destPath, + branch, + pathCount: Array.isArray(paths) ? paths.length : 0, + platform: currentState.platform + }); + const safePaths = (Array.isArray(paths) ? paths : []).filter(Boolean); + if (safePaths.length === 0) { + try { window.electronAPI.debugToMain('warn', 'uploadDroppedPaths:no-safe-paths', { owner, repo, destPath, branch, rawPaths: paths }); } catch (_) {} + return { ok: false, error: 'Keine gueltigen lokalen Dateipfade aus dem Drop ermittelt.' }; + } + + if (currentState.platform === 'github') { + return { ok: false, error: 'Drag-and-Drop Upload ist aktuell nur für Gitea aktiv.' }; + } + + let successCount = 0; + let failedCount = 0; + const errors = []; + + const withTimeout = (promise, ms, label) => { + return Promise.race([ + promise, + new Promise((_, reject) => setTimeout(() => reject(new Error(`${label}-timeout-${ms}ms`)), ms)) + ]); + }; + + for (let i = 0; i < safePaths.length; i++) { + const p = safePaths[i]; + const baseName = p.split(/[\\/]/).pop() || p; + showProgress(Math.round(((i + 1) / safePaths.length) * 100), `Upload ${i + 1}/${safePaths.length}: ${baseName}`); + try { + let res = null; + + // Fast path: treat dropped item as file upload first. + // If it is actually a directory, the call returns ok=false and we fall back to uploadAndPush. + const fileTry = await withTimeout(window.electronAPI.uploadGiteaFile({ + owner, + repo, + localPath: [p], + destPath, + branch, + platform: currentState.platform + }), 15000, 'upload-gitea-file'); + + console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:fileTry', { path: p, fileTry }); + 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, + owner, + repo, + destPath, + cloneUrl, + branch + }), 30000, 'upload-and-push'); + } + + console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:itemResult', { path: p, res }); + try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:itemResult', { path: p, res }); } catch (_) {} + + if (!res?.ok) { + failedCount++; + errors.push(`${baseName}: ${res?.error || 'Unbekannter Fehler'}`); + continue; + } + + const failedEntries = Array.isArray(res.results) ? res.results.filter(r => !r.ok) : []; + if (failedEntries.length > 0) { + failedCount++; + errors.push(`${baseName}: ${failedEntries[0]?.error || 'Teilweise fehlgeschlagen'}`); + } else { + successCount++; + } + } catch (err) { + failedCount++; + const errMsg = String(err && err.message ? err.message : err); + errors.push(`${baseName}: ${errMsg}`); + console.error('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:itemError', { path: p, err: errMsg }); + try { window.electronAPI.debugToMain('error', 'uploadDroppedPaths:itemError', { path: p, err: errMsg }); } catch (_) {} + } + } + + if (failedCount > 0) { + return { ok: false, error: errors[0] || `${failedCount} Upload(s) fehlgeschlagen`, successCount, failedCount }; + } + + const result = { ok: true, uploadedFiles: successCount, uploadedDirs: 0 }; + try { window.electronAPI.debugToMain('log', 'uploadDroppedPaths:done', result); } catch (_) {} + console.log('[UPLOAD_DEBUG][renderer] uploadDroppedPaths:done', result); + return result; +} + +function extractDroppedPaths(files) { + const list = Array.isArray(files) ? files : Array.from(files || []); + const out = []; + for (const f of list) { + const directPath = f && typeof f.path === 'string' ? f.path : ''; + if (directPath) { + out.push(directPath); + continue; + } + let resolvedPath = ''; + try { + resolvedPath = window.electronAPI.getPathForFile ? window.electronAPI.getPathForFile(f) : ''; + } catch (_) {} + if (resolvedPath) out.push(resolvedPath); + } + return out.filter(Boolean); +} + +async function toggleRepoVisibility(owner, repoName, currentPrivate) { + try { + const creds = await window.electronAPI.loadCredentials(); + if (!creds?.giteaToken || !creds?.giteaURL) { + showError('Gitea Token oder URL fehlt. Bitte zuerst in den Einstellungen speichern.'); + return; + } + + const targetPrivate = !currentPrivate; + const actionText = targetPrivate ? 'privat' : 'oeffentlich'; + showProgress(35, `Repository wird ${actionText} gesetzt...`); + const result = await window.electronAPI.updateGiteaRepoVisibility({ + token: creds.giteaToken, + url: creds.giteaURL, + owner, + repo: repoName, + isPrivate: targetPrivate + }); + hideProgress(); + + if (result?.ok) { + showSuccess(`Repository ist jetzt ${actionText}.`); + loadGiteaRepos(); + } else { + showError('Umschalten fehlgeschlagen: ' + (result?.error || 'Unbekannter Fehler')); + } + } catch (error) { + hideProgress(); + console.error('Visibility toggle error:', error); + showError('Umschalten fehlgeschlagen'); + } +} + +function showTagsEditorModal(owner, repoName, seed, knownTopics) { + return new Promise((resolve) => { + const modal = document.createElement('div'); + modal.className = 'modal'; + modal.style.zIndex = '99999'; + const initial = Array.isArray(seed) + ? seed.filter(Boolean) + : String(seed || '').split(',').map(t => t.trim()).filter(Boolean); + const known = Array.from(new Set([...(knownTopics || []), ...initial])) + .filter(Boolean) + .sort((a, b) => a.localeCompare(b, 'de')); + + const card = document.createElement('div'); + card.className = 'modalContent card'; + card.style.maxWidth = '620px'; + + const title = document.createElement('h2'); + title.textContent = '🏷️ Tags bearbeiten'; + + const group = document.createElement('div'); + group.className = 'input-group'; + + const repoLabel = document.createElement('label'); + repoLabel.textContent = `${owner}/${repoName}`; + + const selectedHostEl = document.createElement('div'); + selectedHostEl.id = 'repoTagsSelected'; + selectedHostEl.className = 'tags-editor-selected'; + + const row = document.createElement('div'); + row.className = 'tags-editor-row'; + + const tagInput = document.createElement('input'); + tagInput.id = 'repoTagInput'; + tagInput.className = 'tags-editor-input'; + tagInput.type = 'text'; + tagInput.placeholder = 'Vorhandene Tags suchen oder neuen Tag eingeben'; + tagInput.autocomplete = 'off'; + + const tagAddBtn = document.createElement('button'); + tagAddBtn.id = 'btnRepoTagAdd'; + tagAddBtn.className = 'tags-editor-add-btn'; + tagAddBtn.textContent = 'Hinzufügen'; + + row.appendChild(tagInput); + row.appendChild(tagAddBtn); + + const suggestionsHostEl = document.createElement('div'); + suggestionsHostEl.id = 'repoTagSuggestions'; + suggestionsHostEl.className = 'tags-editor-suggestions'; + + const hint = document.createElement('div'); + hint.className = 'settings-inline-hint'; + hint.textContent = 'Vorschlaege kommen live von deiner Gitea-Seite. Neue Tags sind ebenfalls erlaubt.'; + + group.appendChild(repoLabel); + group.appendChild(selectedHostEl); + group.appendChild(row); + group.appendChild(suggestionsHostEl); + group.appendChild(hint); + + const buttons = document.createElement('div'); + buttons.className = 'modal-buttons'; + buttons.style.marginTop = '16px'; + + const saveButton = document.createElement('button'); + saveButton.id = 'btnRepoTagsSave'; + saveButton.className = 'accent-btn'; + saveButton.textContent = 'Speichern'; + + const cancelButton = document.createElement('button'); + cancelButton.id = 'btnRepoTagsCancel'; + cancelButton.className = 'secondary'; + cancelButton.textContent = 'Abbrechen'; + + buttons.appendChild(saveButton); + buttons.appendChild(cancelButton); + + card.appendChild(title); + card.appendChild(group); + card.appendChild(buttons); + modal.appendChild(card); + document.body.appendChild(modal); + + const selectedHost = modal.querySelector('#repoTagsSelected'); + const input = modal.querySelector('#repoTagInput'); + const addBtn = modal.querySelector('#btnRepoTagAdd'); + const suggestionsHost = modal.querySelector('#repoTagSuggestions'); + const selected = [...initial]; + + const renderSelected = () => { + if (!selectedHost) return; + if (selected.length === 0) { + selectedHost.innerHTML = 'Noch keine Tags gesetzt'; + return; + } + selectedHost.innerHTML = ''; + selected.forEach((tag, idx) => { + const tagChip = document.createElement('span'); + tagChip.style.cssText = 'display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border-radius:999px;border:1px solid rgba(88,213,255,0.35);background:rgba(88,213,255,0.12);font-size:12px;'; + tagChip.appendChild(document.createTextNode(String(tag || ''))); + + const removeBtn = document.createElement('button'); + removeBtn.style.cssText = 'all:unset;cursor:pointer;font-size:12px;opacity:0.85;'; + removeBtn.textContent = '✕'; + removeBtn.onclick = () => { + if (Number.isInteger(idx) && idx >= 0 && idx < selected.length) { + selected.splice(idx, 1); + renderSelected(); + } + }; + + tagChip.appendChild(removeBtn); + selectedHost.appendChild(tagChip); + }); + }; + + const addTag = (rawTag) => { + const tag = String(rawTag || '').trim(); + if (!tag) return; + if (selected.length >= 30) return; + const exists = selected.some(t => t.toLowerCase() === tag.toLowerCase()); + if (!exists) selected.push(tag); + renderSelected(); + }; + + const renderSuggestions = (queryRaw) => { + if (!suggestionsHost) return; + const query = String(queryRaw || '').trim().toLowerCase(); + let list = known; + if (query) { + list = known.filter(t => t.toLowerCase().includes(query)); + } + list = list.filter(t => !selected.some(s => s.toLowerCase() === t.toLowerCase())).slice(0, 50); + if (list.length === 0) { + suggestionsHost.innerHTML = '
Keine Vorschlaege
'; + return; + } + suggestionsHost.innerHTML = ''; + list.forEach(tag => { + const btn = document.createElement('button'); + btn.className = 'tags-editor-suggestion-item'; + btn.textContent = String(tag || ''); + btn.onclick = () => { + addTag(tag); + if (input) { + input.value = ''; + input.focus(); + } + renderSuggestions(''); + }; + suggestionsHost.appendChild(btn); + }); + }; + + const closeWith = (value) => { + modal.remove(); + resolve(value); + }; + + if (input) { + input.focus(); + input.addEventListener('input', () => renderSuggestions(input.value)); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + addTag(input.value); + input.value = ''; + renderSuggestions(''); + } + if (e.key === 'Escape') closeWith(null); + }); + } + + renderSelected(); + renderSuggestions(''); + + const saveBtn = modal.querySelector('#btnRepoTagsSave'); + const cancelBtn = modal.querySelector('#btnRepoTagsCancel'); + if (addBtn) addBtn.onclick = (e) => { + e.preventDefault(); + addTag(input ? input.value : ''); + if (input) { + input.value = ''; + input.focus(); + } + renderSuggestions(''); + }; + if (saveBtn) saveBtn.onclick = () => { + if (input && input.value.trim()) addTag(input.value.trim()); + closeWith(selected.slice(0, 30)); + }; + if (cancelBtn) cancelBtn.onclick = () => closeWith(null); + modal.onclick = (e) => { if (e.target === modal) closeWith(null); }; + }); +} + +async function fetchKnownTopicsFromGitea(force = false) { + const now = Date.now(); + if (!force && repoKnownTopics.length > 0 && (now - repoKnownTopicsLoadedAt) < 5 * 60 * 1000) { + return repoKnownTopics; + } + if (!window.electronAPI.getGiteaTopicsCatalog) return repoKnownTopics; + try { + const res = await window.electronAPI.getGiteaTopicsCatalog(); + if (res?.ok && Array.isArray(res.topics)) { + repoKnownTopics = res.topics; + repoKnownTopicsLoadedAt = now; + } + } catch (_) {} + return repoKnownTopics; +} + +async function editRepoTopics(owner, repoName, currentTopics = []) { + const seed = Array.isArray(currentTopics) ? currentTopics : []; + const known = await fetchKnownTopicsFromGitea(); + const raw = await showTagsEditorModal(owner, repoName, seed, known); + if (raw === null) return; + + const topics = Array.isArray(raw) ? raw : []; + + showProgress(35, 'Tags werden aktualisiert...'); + const result = await window.electronAPI.updateGiteaRepoTopics({ owner, repo: repoName, topics }); + hideProgress(); + + if (result?.ok) { + showSuccess('Tags aktualisiert'); + loadGiteaRepos(); + } else { + showError('Tags konnten nicht gespeichert werden: ' + (result?.error || 'Unbekannter Fehler')); + } +} + // Speichert den default_branch pro Repo (owner/repo -> 'main' oder 'master') let repoDefaultBranches = {}; @@ -451,9 +954,33 @@ let currentState = { view: 'none', // 'local', 'gitea-list', 'gitea-repo' owner: null, repo: null, - path: '' + path: '', + platform: 'gitea' // 'gitea' | 'github' }; +let repoLoadRequestId = 0; +const USER_CACHE_MS = 5 * 60 * 1000; +let currentUserCache = { + gitea: { name: '', ts: 0 }, + github: { name: '', ts: 0 } +}; + +function getCachedUsername(platform) { + const p = platform === 'github' ? 'github' : 'gitea'; + const item = currentUserCache[p]; + if (!item?.name) return ''; + if ((Date.now() - Number(item.ts || 0)) > USER_CACHE_MS) return ''; + return item.name; +} + +function setCachedUsername(platform, name) { + const p = platform === 'github' ? 'github' : 'gitea'; + currentUserCache[p] = { + name: String(name || ''), + ts: Date.now() + }; +} + const MAX_ACTIVITY_ITEMS = 300; let activityEntries = []; let retryQueueCount = 0; @@ -462,6 +989,12 @@ let remoteHeatmapFetchState = 'idle'; // idle | ok | error let remoteHeatmapFetchedAt = 0; let remoteHeatmapCounts = new Map(); let remoteHeatmapUsername = ''; +let remoteHeatmapPlatform = ''; +let remoteHeatmapMonths = 0; + +function getActiveHeatmapMonths() { + return activityHeatmapRangeMonths; +} function logActivity(level, message) { const entry = { @@ -520,29 +1053,112 @@ function setRemoteHeatmapEntries(entries) { remoteHeatmapCounts = next; } +function hasPositiveHeatmapEntries(entries) { + return Array.isArray(entries) && entries.some(item => Number(item?.count || 0) > 0); +} + +async function buildGithubHeatmapFromRepoCommits(monthsBack) { + if (currentPlatformKey() !== 'github') return []; + + const repos = Array.isArray(currentGiteaRepos) ? currentGiteaRepos.slice(0, 60) : []; + if (repos.length === 0) return []; + + const since = new Date(); + since.setMonth(since.getMonth() - Math.max(1, Number(monthsBack) || 20)); + const sinceTs = since.getTime(); + + const myLogin = String(currentGiteaUsername || '').trim().toLowerCase(); + const dayMap = new Map(); + const concurrency = 4; + let repoIndex = 0; + + const worker = async () => { + while (repoIndex < repos.length) { + const repo = repos[repoIndex++]; + const owner = repo?.owner?.login || repo?.owner?.username; + const repoName = repo?.name; + if (!owner || !repoName) continue; + + try { + const res = await window.electronAPI.getCommits({ + owner, + repo: repoName, + branch: repo?.default_branch || 'HEAD', + limit: 100, + platform: 'github' + }); + if (!res?.ok || !Array.isArray(res.commits)) continue; + + for (const commit of res.commits) { + const authorLogin = String(commit?.author?.login || '').toLowerCase(); + if (myLogin && authorLogin && authorLogin !== myLogin) continue; + + const dateValue = commit?.commit?.author?.date || commit?.commit?.committer?.date; + if (!dateValue) continue; + const ts = new Date(dateValue).getTime(); + if (!Number.isFinite(ts) || ts < sinceTs) continue; + + const key = normalizeHeatmapEntryDate(dateValue); + if (!key) continue; + dayMap.set(key, (dayMap.get(key) || 0) + 1); + } + } catch (_) { + // einzelne Repos koennen fehlschlagen ohne den Gesamtprozess zu blockieren + } + } + }; + + await Promise.all(Array.from({ length: concurrency }, () => worker())); + + return Array.from(dayMap.entries()) + .map(([date, count]) => ({ date, count })) + .sort((a, b) => a.date.localeCompare(b.date)); +} + async function loadRemoteHeatmapData(force = false) { + const platform = currentState.platform === 'github' ? 'github' : 'gitea'; + const monthsBack = getActiveHeatmapMonths(); const now = Date.now(); - if (!force && remoteHeatmapFetchState === 'ok' && (now - remoteHeatmapFetchedAt) < HEATMAP_CACHE_MS) { + if (!force && remoteHeatmapFetchState === 'ok' && remoteHeatmapPlatform === platform && remoteHeatmapMonths === monthsBack && (now - remoteHeatmapFetchedAt) < HEATMAP_CACHE_MS) { return; } - if (!window.electronAPI || typeof window.electronAPI.getGiteaUserHeatmap !== 'function') { + const loadFn = platform === 'github' ? window.electronAPI?.getGithubUserHeatmap : window.electronAPI?.getGiteaUserHeatmap; + if (!window.electronAPI || typeof loadFn !== 'function') { remoteHeatmapFetchState = 'error'; + remoteHeatmapPlatform = platform; + remoteHeatmapMonths = monthsBack; + remoteHeatmapUsername = ''; + setRemoteHeatmapEntries([]); return; } try { - const res = await window.electronAPI.getGiteaUserHeatmap(); + const res = await loadFn({ monthsBack }); if (res && res.ok) { - remoteHeatmapUsername = res.username || ''; - setRemoteHeatmapEntries(res.entries || []); + remoteHeatmapUsername = res.username || currentGiteaUsername || ''; + let entries = Array.isArray(res.entries) ? res.entries : []; + if (platform === 'github' && !hasPositiveHeatmapEntries(entries)) { + entries = await buildGithubHeatmapFromRepoCommits(monthsBack); + } + setRemoteHeatmapEntries(entries); remoteHeatmapFetchedAt = now; remoteHeatmapFetchState = 'ok'; + remoteHeatmapPlatform = platform; + remoteHeatmapMonths = monthsBack; return; } remoteHeatmapFetchState = 'error'; + remoteHeatmapPlatform = platform; + remoteHeatmapMonths = monthsBack; + remoteHeatmapUsername = ''; + setRemoteHeatmapEntries([]); } catch (_) { remoteHeatmapFetchState = 'error'; + remoteHeatmapPlatform = platform; + remoteHeatmapMonths = monthsBack; + remoteHeatmapUsername = ''; + setRemoteHeatmapEntries([]); } } @@ -634,7 +1250,20 @@ function renderActivityHeatmap(host) { if (!host) return; host.innerHTML = ''; - const { weeks, total, maxCount, source } = buildHeatmapData(activityHeatmapRangeMonths); + const monthsBack = getActiveHeatmapMonths(); + const { weeks, total, maxCount, source } = buildHeatmapData(monthsBack); + + // Dynamische Zellgröße: passt die Heatmap an die verfügbare Kartenbreite an, + // damit rechts keine Monate/Tage abgeschnitten werden. + const weekCount = Math.max(1, weeks.length); + const weekdayCol = 22; + const gridGap = 8; + const weekGap = 2; + const hostWidth = Math.max(320, host.clientWidth || 0); + const horizontalPadding = 24; + const availableForGrid = Math.max(220, hostWidth - horizontalPadding - weekdayCol - gridGap); + const computedCell = (availableForGrid - ((weekCount - 1) * weekGap)) / weekCount; + const cellSize = Math.max(6, Math.min(12, Number(computedCell.toFixed(2)))); const card = document.createElement('section'); card.className = 'activity-heatmap-card' + (activityHeatmapCollapsed ? ' collapsed' : ''); @@ -664,6 +1293,8 @@ function renderActivityHeatmap(host) { const body = document.createElement('div'); body.className = 'activity-heatmap-body'; + body.style.setProperty('--hm-cell-size', `${cellSize}px`); + body.style.setProperty('--hm-week-gap', `${weekGap}px`); const months = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']; const monthsRow = document.createElement('div'); @@ -722,22 +1353,25 @@ function renderActivityHeatmap(host) { const summary = document.createElement('span'); if (source === 'remote') { - summary.textContent = `${total.toLocaleString('de-DE')} Beiträge vom Git-Profil in den letzten ${activityHeatmapRangeMonths} Monaten${remoteHeatmapUsername ? ` (${remoteHeatmapUsername})` : ''}`; + const profileLabel = currentState.platform === 'github' ? 'GitHub-Profil' : 'Git-Profil'; + summary.textContent = `${total.toLocaleString('de-DE')} Beiträge vom ${profileLabel} in den letzten ${monthsBack} Monaten${remoteHeatmapUsername ? ` (${remoteHeatmapUsername})` : ''}`; } else { - summary.textContent = `${total.toLocaleString('de-DE')} lokale Einträge in den letzten ${activityHeatmapRangeMonths} Monaten`; + summary.textContent = `${total.toLocaleString('de-DE')} lokale Einträge in den letzten ${monthsBack} Monaten`; } const legend = document.createElement('div'); legend.className = 'activity-heatmap-legend'; - legend.innerHTML = ` - Weniger - - - - - - Mehr - `; + const less = document.createElement('span'); + less.textContent = 'Weniger'; + legend.appendChild(less); + [0, 1, 2, 3, 4].forEach((lv) => { + const cell = document.createElement('i'); + cell.className = `activity-heatmap-cell lv${lv}`; + legend.appendChild(cell); + }); + const more = document.createElement('span'); + more.textContent = 'Mehr'; + legend.appendChild(more); footer.appendChild(summary); footer.appendChild(legend); @@ -766,20 +1400,39 @@ function renderActivityLog() { const visible = activityEntries.filter(e => filter === 'all' || e.level === filter); if (visible.length === 0) { - list.innerHTML = '
Noch keine Einträge.
'; + list.innerHTML = ''; + const row = document.createElement('div'); + row.className = 'activity-log-item info'; + const msg = document.createElement('span'); + msg.className = 'activity-log-message'; + msg.textContent = 'Noch keine Einträge.'; + row.appendChild(msg); + list.appendChild(row); return; } - list.innerHTML = visible.map(e => { - const lvl = (e.level || 'info').toUpperCase(); - return ` -
- ${formatActivityTimestamp(e.ts)} - ${lvl} - ${escapeHtml(e.message)} -
- `; - }).join(''); + list.innerHTML = ''; + for (const e of visible) { + const row = document.createElement('div'); + row.className = `activity-log-item ${e.level || 'info'}`; + + const time = document.createElement('span'); + time.className = 'activity-log-time'; + time.textContent = formatActivityTimestamp(e.ts); + + const level = document.createElement('span'); + level.className = 'activity-log-level'; + level.textContent = (e.level || 'info').toUpperCase(); + + const msg = document.createElement('span'); + msg.className = 'activity-log-message'; + msg.textContent = String(e.message || ''); + + row.appendChild(time); + row.appendChild(level); + row.appendChild(msg); + list.appendChild(row); + } } function updateRetryQueueBadge(count) { @@ -1051,25 +1704,27 @@ function normalizeAndValidateGiteaUrl(rawUrl) { return { ok: true, value: value.replace(/\/$/, '') }; } -function renderGiteaUrlHint(rawValue) { +function renderGiteaUrlHint(rawValue, rawToken = '') { const hint = $('giteaUrlHint'); if (!hint) return; - const result = normalizeAndValidateGiteaUrl(rawValue); - if (!rawValue || !rawValue.trim()) { - hint.className = 'settings-inline-hint'; - hint.textContent = 'Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000'; - return; - } + const url = String(rawValue || '').trim(); + const token = String(rawToken || '').trim(); + const result = normalizeAndValidateGiteaUrl(url); + const connected = !!token && !!url && !!result.ok; - if (!result.ok) { - hint.className = 'settings-inline-hint error'; - hint.textContent = result.error; - return; - } + hint.className = `settings-inline-hint ${connected ? 'success' : 'error'}`; + hint.textContent = connected ? 'Verbunden' : 'Nicht verbunden'; +} - hint.className = 'settings-inline-hint success'; - hint.textContent = `Gültige URL: ${result.value}`; +function renderGithubTokenHint(rawToken) { + const hint = $('githubTokenHint'); + if (!hint) return; + + const token = String(rawToken || '').trim(); + const connected = !!token; + hint.className = `settings-inline-hint ${connected ? 'success' : 'error'}`; + hint.textContent = connected ? 'Verbunden' : 'Nicht verbunden'; } function mapErrorMessage(message) { @@ -1264,18 +1919,38 @@ function applyRepoFuzzyFilter(grid, searchInput, searchMetaEl) { const cards = Array.from(grid.querySelectorAll('.item-card')); const query = (searchInput?.value || '').trim(); + const isOwnerMatch = (card) => { + const owner = String(card.dataset.searchOwner || '').toLowerCase(); + const isShared = String(card.dataset.shared || '') === 'true'; + if (activeRepoOwnerFilter === 'all') return true; + if (activeRepoOwnerFilter === 'mine') { + if (!currentGiteaUsername) return true; + return owner === String(currentGiteaUsername).toLowerCase(); + } + if (activeRepoOwnerFilter === 'shared') { + return isShared; + } + if (activeRepoOwnerFilter.startsWith('owner:')) { + const target = activeRepoOwnerFilter.slice('owner:'.length).toLowerCase(); + return owner === target; + } + return true; + }; + if (!query) { cards.forEach(card => { - card.style.display = 'flex'; + card.style.display = isOwnerMatch(card) ? 'flex' : 'none'; card.style.order = ''; }); if (searchMetaEl) { - searchMetaEl.textContent = `${cards.length} Repositories`; + const visibleCount = cards.filter(card => isOwnerMatch(card)).length; + searchMetaEl.textContent = `${visibleCount} Repositories`; } return; } const ranked = cards + .filter(card => isOwnerMatch(card)) .map(card => ({ card, score: getRepoCardSearchScore(card, query) })) .filter(entry => entry.score > 0) .sort((a, b) => b.score - a.score || a.card.dataset.searchName.localeCompare(b.card.dataset.searchName)); @@ -1293,6 +1968,17 @@ function applyRepoFuzzyFilter(grid, searchInput, searchMetaEl) { } } +function isRepoWritable(repo, currentUsername) { + const owner = String(repo?.owner?.login || repo?.owner?.username || '').toLowerCase(); + const me = String(currentUsername || '').toLowerCase(); + if (owner && me && owner === me) return true; + + const perms = repo?.permissions || repo?.permission || {}; + if (perms.admin === true) return true; + if (perms.push === true) return true; + return false; +} + function buildRepoWebUrl(owner, repoName) { const urlInput = $('giteaURL'); const normalized = normalizeAndValidateGiteaUrl(urlInput?.value || ''); @@ -1417,132 +2103,6 @@ function ensureProgressUI() { document.body.appendChild(container); } -function ensureBackupRunOverlay() { - if ($('backupRunOverlay')) return; - - const overlay = document.createElement('div'); - overlay.id = 'backupRunOverlay'; - overlay.style.cssText = ` - position: fixed; - inset: 0; - z-index: 10001; - background: rgba(5, 10, 18, 0.58); - backdrop-filter: blur(4px); - display: none; - align-items: center; - justify-content: center; - padding: 16px; - `; - - const card = document.createElement('div'); - card.style.cssText = ` - width: min(520px, 96vw); - background: linear-gradient(180deg, rgba(16, 26, 44, 0.96), rgba(12, 20, 34, 0.96)); - border: 1px solid rgba(88, 213, 255, 0.26); - border-radius: 12px; - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.45); - padding: 18px 18px 16px; - color: #fff; - `; - - const header = document.createElement('div'); - header.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;font-weight:700;font-size:14px;color:#9de8ff;'; - - const spinner = document.createElement('div'); - spinner.style.cssText = ` - width: 16px; - height: 16px; - border: 2px solid rgba(88, 213, 255, 0.25); - border-top-color: #58d5ff; - border-radius: 50%; - animation: backupSpin 0.9s linear infinite; - `; - - const title = document.createElement('span'); - title.textContent = 'Backup laeuft... bitte warten'; - header.appendChild(spinner); - header.appendChild(title); - - const detail = document.createElement('div'); - detail.id = 'backupRunOverlayText'; - detail.style.cssText = 'font-size:13px;color:#d8eaff;margin-bottom:10px;min-height:20px;'; - detail.textContent = 'Initialisiere Backup...'; - - const percent = document.createElement('div'); - percent.id = 'backupRunOverlayPercent'; - percent.style.cssText = 'font-size:12px;color:#9fc6dc;margin-bottom:8px;text-align:right;'; - percent.textContent = '0%'; - - const barWrap = document.createElement('div'); - barWrap.style.cssText = 'height:10px;background:rgba(255,255,255,0.10);border-radius:999px;overflow:hidden;'; - - const bar = document.createElement('div'); - bar.id = 'backupRunOverlayBar'; - bar.style.cssText = 'height:100%;width:0%;background:linear-gradient(90deg, #58d5ff, #5c87ff);transition:width 200ms ease-out;'; - barWrap.appendChild(bar); - - card.appendChild(header); - card.appendChild(detail); - card.appendChild(percent); - card.appendChild(barWrap); - overlay.appendChild(card); - document.body.appendChild(overlay); - - if (!$('backupOverlayAnimationStyle')) { - const style = document.createElement('style'); - style.id = 'backupOverlayAnimationStyle'; - style.textContent = '@keyframes backupSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }'; - document.head.appendChild(style); - } -} - -function mapBackupProgressText(rawText, percentValue) { - const text = String(rawText || '').toLowerCase(); - const percent = Math.min(100, Math.max(0, Math.round(Number(percentValue) || 0))); - - if (percent >= 100) return 'Finalisiere Backup...'; - if (text.includes('upload')) return 'Upload laeuft...'; - if (text.includes('download')) return 'Dateien werden heruntergeladen...'; - if (text.includes('git')) return 'Git-Projekte werden gesammelt...'; - if (text.includes('erstelle backup') || text.includes('backup wird erstellt')) return 'Backup wird erstellt...'; - if (percent <= 8) return 'Vorbereitung laeuft...'; - if (percent <= 35) return 'Dateien werden gesammelt...'; - if (percent <= 85) return 'Daten werden verarbeitet...'; - return 'Finalisiere Backup...'; -} - -function showBackupRunOverlay(text) { - ensureBackupRunOverlay(); - const overlay = $('backupRunOverlay'); - const detail = $('backupRunOverlayText'); - const percent = $('backupRunOverlayPercent'); - const bar = $('backupRunOverlayBar'); - if (detail) detail.textContent = text || 'Backup wird vorbereitet...'; - if (percent) percent.textContent = '0%'; - if (bar) bar.style.width = '0%'; - if (overlay) overlay.style.display = 'flex'; -} - -function updateBackupRunOverlay(percentValue, text) { - const overlay = $('backupRunOverlay'); - if (!overlay || overlay.style.display === 'none') return; - const clamped = Math.min(100, Math.max(0, Math.round(Number(percentValue) || 0))); - const detail = $('backupRunOverlayText'); - const percent = $('backupRunOverlayPercent'); - const bar = $('backupRunOverlayBar'); - if (detail) detail.textContent = mapBackupProgressText(text, clamped); - if (percent) percent.textContent = `${clamped}%`; - if (bar) bar.style.width = `${clamped}%`; -} - -function hideBackupRunOverlay() { - const overlay = $('backupRunOverlay'); - if (!overlay) return; - setTimeout(() => { - overlay.style.display = 'none'; - }, 250); -} - function showProgress(percent, text) { ensureProgressUI(); const container = $('folderProgressContainer'); @@ -1551,7 +2111,6 @@ function showProgress(percent, text) { if (txt) txt.innerText = text || ''; if (bar) bar.style.width = `${Math.min(100, Math.max(0, percent))}%`; if (container) container.style.display = 'block'; - updateBackupRunOverlay(percent, text); } function hideProgress() { @@ -1616,7 +2175,7 @@ function initEditor() { } // Add new tab -function addTab(filePath, fileName, content, isGitea = false, owner = null, repo = null) { +function addTab(filePath, fileName, content, isGitea = false, owner = null, repo = null, platform = null) { openTabs[filePath] = { name: fileName, content: content, @@ -1626,6 +2185,7 @@ function addTab(filePath, fileName, content, isGitea = false, owner = null, repo isGitea, owner, repo, + platform: platform || (isGitea ? currentState.platform : null), history: [content], historyIndex: 0 }; @@ -1693,10 +2253,16 @@ function renderTabs() { const closeBtn = document.createElement('button'); closeBtn.className = 'editor-tab-close'; closeBtn.textContent = '✕'; - closeBtn.addEventListener('click', (e) => { + closeBtn.addEventListener('click', async (e) => { e.stopPropagation(); - if (tab.dirty && !confirm(`${tab.name} hat ungespeicherte Änderungen. Wirklich schließen?`)) { - return; + if (tab.dirty) { + const ok = await showActionConfirmModal({ + title: 'Ungespeicherte Aenderungen', + message: `${tab.name} hat ungespeicherte Aenderungen. Wirklich schliessen?`, + confirmText: 'Schliessen', + danger: true + }); + if (!ok) return; } removeTab(filePath); }); @@ -1752,7 +2318,8 @@ async function updateEditor() { owner: tab.owner, repo: tab.repo, path: filePath, - ref: getDefaultBranch(tab.owner, tab.repo) + ref: getDefaultBranch(tab.owner, tab.repo), + platform: tab.platform || 'gitea' }); if (response.ok) { @@ -1951,14 +2518,18 @@ function showAutoSaveIndicator() { } } -function closeFileEditor() { +async function closeFileEditor() { // Überprüfe auf ungespeicherte Änderungen const unsaved = Object.entries(openTabs).filter(([_, tab]) => tab.dirty); if (unsaved.length > 0) { - if (!confirm(`${unsaved.length} Datei(en) haben ungespeicherte Änderungen. Wirklich schließen?`)) { - return; - } + const ok = await showActionConfirmModal({ + title: 'Ungespeicherte Aenderungen', + message: `${unsaved.length} Datei(en) haben ungespeicherte Aenderungen. Wirklich schliessen?`, + confirmText: 'Schliessen', + danger: true + }); + if (!ok) return; } openTabs = {}; @@ -1993,7 +2564,7 @@ async function openFileEditor(filePath, fileName) { if (response.ok) { addTab(filePath, fileName, response.content); } else { - alert(`Fehler: ${response.error}`); + await showInfoModal('Datei konnte nicht geoeffnet werden', `Fehler: ${response.error || 'Unbekannter Fehler'}`, true); return; } } @@ -2009,7 +2580,7 @@ async function openFileEditor(filePath, fileName) { console.log('✅ File opened'); } catch (error) { console.error('Error opening file:', error); - alert('Fehler beim Öffnen der Datei'); + await showInfoModal('Datei konnte nicht geoeffnet werden', 'Fehler beim Oeffnen der Datei.', true); } } @@ -2027,16 +2598,17 @@ async function openGiteaFileInEditor(owner, repo, filePath, fileName) { return; } - // Lade Datei-Content vom Gitea Handler + // Lade Datei-Content vom Gitea/GitHub Handler const response = await window.electronAPI.readGiteaFile({ owner, repo, path: filePath, - ref: getDefaultBranch(owner, repo) + ref: getDefaultBranch(owner, repo), + platform: currentState.platform }); if (response.ok) { - addTab(vPath, fileName, response.content, true, owner, repo); + addTab(vPath, fileName, response.content, true, owner, repo, currentState.platform); const modal = $('fileEditorModal'); if (modal) { @@ -2048,12 +2620,12 @@ async function openGiteaFileInEditor(owner, repo, filePath, fileName) { setStatus(`Editiere: ${fileName}`); console.log('✅ Gitea file opened'); } else { - alert(`Fehler: ${response.error}`); + await showInfoModal('Datei konnte nicht geladen werden', `Fehler: ${response.error || 'Unbekannter Fehler'}`, true); showError('Fehler beim Laden der Datei'); } } catch (error) { console.error('Error opening Gitea file:', error); - alert('Fehler beim Öffnen der Datei'); + await showInfoModal('Datei konnte nicht geoeffnet werden', 'Fehler beim Oeffnen der Datei.', true); showError('Fehler'); } } @@ -2220,7 +2792,7 @@ async function saveCurrentFile(isAutoSave = false) { // Prüfe ob es eine Bilddatei ist if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(currentActiveTab)) { - alert('Bilder können nicht bearbeitet werden'); + await showInfoModal('Nicht bearbeitbar', 'Bilder koennen nicht bearbeitet werden.'); return; } @@ -2232,14 +2804,15 @@ async function saveCurrentFile(isAutoSave = false) { try { let response; - // Prüfe ob es eine Gitea-Datei ist + // Prüfe ob es eine Gitea/GitHub-Datei ist if (tab.isGitea) { response = await window.electronAPI.writeGiteaFile({ owner: tab.owner, repo: tab.repo, path: currentActiveTab.replace(`gitea://${tab.owner}/${tab.repo}/`, ''), content: content, - ref: getDefaultBranch(tab.owner, tab.repo) + ref: getDefaultBranch(tab.owner, tab.repo), + platform: tab.platform || 'gitea' }); } else { // Lokale Datei @@ -2270,11 +2843,11 @@ async function saveCurrentFile(isAutoSave = false) { showWarning(response.message || 'Änderung in Retry-Queue gelegt und wird später hochgeladen.'); updateRetryQueueBadge(retryQueueCount + 1); } else { - alert(`Fehler: ${response.error}`); + await showInfoModal('Speichern fehlgeschlagen', `Fehler: ${response.error || 'Unbekannter Fehler'}`, true); } } catch (error) { console.error('Error saving file:', error); - alert('Fehler beim Speichern'); + await showInfoModal('Speichern fehlgeschlagen', 'Fehler beim Speichern.', true); } } @@ -2297,17 +2870,11 @@ function updateEditorStats() { function parseMarkdownToHTML(markdown) { if (!markdown) return ''; - let html = markdown; - - // Check if content already contains HTML (starts with < or has closing tags) - const hasHTML = /<[a-zA-Z][\s\S]*>/.test(html); - - if (!hasHTML) { - // Only escape and parse if no HTML present - html = html.replace(/&/g, '&') - .replace(//g, '>'); - } + // Immer escapen: verhindert Script-/HTML-Injection aus Release-Texten. + let html = String(markdown) + .replace(/&/g, '&') + .replace(//g, '>'); // Convert markdown patterns // Headings: ### Title →

Title

@@ -2371,7 +2938,8 @@ function updateNavigationUI() { /* ------------------------- GITEA CORE LOGIK (GRID) ------------------------- */ -async function loadGiteaRepos() { +async function loadGiteaRepos(preloadedData = null, requestId = null) { + const activeRequestId = requestId || ++repoLoadRequestId; currentState.view = 'gitea-list'; currentState.path = ''; updateNavigationUI(); @@ -2388,20 +2956,40 @@ async function loadGiteaRepos() { grid.style.gridTemplateColumns = ''; } - setStatus('Loading Gitea repos...'); - updateSettingsHealth({ lastError: '-' }); + setStatus('Loading repos...'); + if (!preloadedData) updateSettingsHealth({ lastError: '-' }); try { - const res = await window.electronAPI.listGiteaRepos(); - if (!res.ok) { - showError('Failed to load repos: ' + (res.error || 'Unknown error')); - updateSettingsHealth({ api: 'Fehler', auth: 'Fehler' }); - return; + let res; + if (preloadedData) { + res = preloadedData; + } else { + res = await window.electronAPI.listGiteaRepos(); + if (activeRequestId !== repoLoadRequestId) return; + if (!res.ok) { + showError('Failed to load repos: ' + (res.error || 'Unknown error')); + updateSettingsHealth({ api: 'Fehler', auth: 'Fehler' }); + return; + } + updateSettingsHealth({ api: 'Erreichbar', auth: 'OK', lastError: '-' }); } - updateSettingsHealth({ api: 'Erreichbar', auth: 'OK', lastError: '-' }); - currentGiteaRepos = Array.isArray(res.repos) ? res.repos : []; + if (!preloadedData) { + try { + const cachedName = getCachedUsername('gitea'); + if (cachedName) { + currentGiteaUsername = cachedName; + } else { + const meRes = await window.electronAPI.getGiteaCurrentUser?.(); + if (activeRequestId !== repoLoadRequestId) return; + currentGiteaUsername = meRes?.ok ? (meRes.user?.login || meRes.user?.username || '') : ''; + setCachedUsername('gitea', currentGiteaUsername); + } + } catch (_) { + currentGiteaUsername = ''; + } + } const grid = $('explorerGrid'); if (!grid) return; @@ -2415,6 +3003,54 @@ async function loadGiteaRepos() { } // --- Fuzzy-Suchfeld für Repositories --- + const ownerTabsWrap = document.createElement('div'); + ownerTabsWrap.className = 'repo-owner-tabs'; + ownerTabsWrap.style.cssText = 'grid-column: 1/-1; margin-bottom: 12px;'; + + const ownerCounts = new Map(); + for (const r of currentGiteaRepos) { + const owner = (r?.owner?.login || r?.owner?.username || '').trim(); + if (!owner) continue; + ownerCounts.set(owner, (ownerCounts.get(owner) || 0) + 1); + } + const myOwner = String(currentGiteaUsername || '').trim(); + + const tabDefs = [ + { key: 'mine', label: 'Meine', count: myOwner ? (ownerCounts.get(myOwner) || 0) : currentGiteaRepos.length }, + { key: 'all', label: 'Alle', count: currentGiteaRepos.length } + ]; + Array.from(ownerCounts.keys()) + .filter(o => !myOwner || o.toLowerCase() !== myOwner.toLowerCase()) + .sort((a, b) => a.localeCompare(b, 'de')) + .forEach(owner => tabDefs.push({ key: `owner:${owner}`, label: owner, count: ownerCounts.get(owner) || 0 })); + + if (activeRepoOwnerFilter === 'mine' && tabDefs[0].count === 0) { + activeRepoOwnerFilter = 'all'; + } + if (activeRepoOwnerFilter === 'shared') { + activeRepoOwnerFilter = 'all'; + } + + const makeTab = (tab) => { + const btn = document.createElement('button'); + btn.className = 'repo-owner-tab' + (activeRepoOwnerFilter === tab.key ? ' active' : ''); + btn.textContent = String(tab.label || ''); + btn.appendChild(document.createTextNode(' ')); + const countSpan = document.createElement('span'); + countSpan.textContent = String(tab.count || 0); + btn.appendChild(countSpan); + btn.onclick = () => { + activeRepoOwnerFilter = tab.key; + ownerTabsWrap.querySelectorAll('.repo-owner-tab').forEach(el => { + el.classList.toggle('active', el === btn); + }); + applyRepoFuzzyFilter(grid, searchInput, null); + }; + return btn; + }; + tabDefs.forEach(tab => ownerTabsWrap.appendChild(makeTab(tab))); + grid.appendChild(ownerTabsWrap); + const searchContainer = document.createElement('div'); searchContainer.className = 'repo-search-wrap'; searchContainer.style.cssText = 'grid-column: 1/-1; margin-bottom: 20px;'; @@ -2466,8 +3102,11 @@ async function loadGiteaRepos() { heatmapHost.style.cssText = 'grid-column: 1/-1; margin-bottom: 18px;'; grid.appendChild(heatmapHost); - await loadRemoteHeatmapData(); renderActivityHeatmap(heatmapHost); + loadRemoteHeatmapData(false).finally(() => { + if (activeRequestId !== repoLoadRequestId) return; + renderActivityHeatmap(heatmapHost); + }); // Fuzzy Search Logic searchInput.addEventListener('input', (e) => { @@ -2498,10 +3137,21 @@ async function loadGiteaRepos() { // ── Sidebar links einblenden ── renderFavHistorySidebar(res.repos); + repoPrivacyByFullName = {}; + repoTopicsByFullName = {}; + const knownTopicSet = new Set(); + res.repos.forEach(repo => { let owner = (repo.owner && (repo.owner.login || repo.owner.username)) || null; let repoName = repo.name; let cloneUrl = repo.clone_url || repo.clone_url_ssh; + repoPrivacyByFullName[`${owner || ''}/${repoName || ''}`] = !!repo.private; + const repoTopics = Array.isArray(repo.topics) ? repo.topics : []; + repoTopicsByFullName[`${owner || ''}/${repoName || ''}`] = repoTopics; + repoTopics.forEach(t => { + const s = String(t || '').trim(); + if (s) knownTopicSet.add(s); + }); // default_branch speichern (main ODER master je nach Repo) const defaultBranch = repo.default_branch || 'HEAD'; @@ -2519,6 +3169,14 @@ async function loadGiteaRepos() { card.dataset.searchLanguage = repo.language || ''; card.dataset.searchTopics = Array.isArray(repo.topics) ? repo.topics.join(' ') : ''; card.dataset.searchDescription = repo.description || ''; + const ownerLower = String(owner || '').toLowerCase(); + const myOwnerLower = String(currentGiteaUsername || '').toLowerCase(); + const sharedByOthers = !!ownerLower && !!myOwnerLower && ownerLower !== myOwnerLower; + card.dataset.shared = sharedByOthers ? 'true' : 'false'; + card.dataset.searchSharedOwner = sharedByOthers ? `geteilt von ${owner}` : ''; + const writable = isRepoWritable(repo, currentGiteaUsername); + const readOnly = !writable; + card.dataset.readOnly = readOnly ? 'true' : 'false'; // Stern-Button (nur wenn Favoriten-Feature aktiv) if (featureFavorites) { @@ -2535,13 +3193,40 @@ async function loadGiteaRepos() { const iconEl = document.createElement('div'); iconEl.className = 'item-icon'; - iconEl.textContent = '📦'; + if (repo.avatar_url) { + const avatarImg = document.createElement('img'); + avatarImg.src = repo.avatar_url; + avatarImg.className = 'repo-avatar-img'; + avatarImg.alt = ''; + avatarImg.onerror = () => { iconEl.textContent = '📦'; avatarImg.remove(); }; + iconEl.appendChild(avatarImg); + } else { + iconEl.textContent = '📦'; + } card.appendChild(iconEl); const nameEl = document.createElement('div'); nameEl.className = 'item-name'; nameEl.textContent = repoName; card.appendChild(nameEl); + if (sharedByOthers) { + const sharedByEl = document.createElement('div'); + sharedByEl.className = 'item-submeta'; + sharedByEl.textContent = `geteilt von ${owner}`; + card.appendChild(sharedByEl); + } + + if (readOnly) { + const roBadge = document.createElement('div'); + roBadge.className = 'repo-size-badge'; + roBadge.style.top = '10px'; + roBadge.style.right = '42px'; + roBadge.style.background = 'rgba(59,130,246,0.16)'; + roBadge.style.borderColor = 'rgba(59,130,246,0.45)'; + roBadge.textContent = 'Nur Ansicht'; + card.appendChild(roBadge); + } + // Repo-Größe anzeigen if (repo.size != null) { const sizeEl = document.createElement('div'); @@ -2600,54 +3285,72 @@ async function loadGiteaRepos() { card.classList.remove('drag-target'); const files = ev.dataTransfer.files; + try { + window.electronAPI.debugToMain('log', 'repoCard:drop:event', { + owner, + repo: repoName, + fileCount: files ? files.length : 0, + itemCount: ev.dataTransfer?.items ? ev.dataTransfer.items.length : 0 + }); + } catch (_) {} if (!files || files.length === 0) { showWarning("Keine Dateien zum Upload gefunden."); return; } - const paths = Array.from(files).map(f => f.path); - const uploadSessionId = `drop-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - setStatus(`Starte Upload von ${paths.length} Elementen...`); - - for (let i = 0; i < paths.length; i++) { - const p = paths[i]; - const baseName = p.split(/[\\/]/).pop(); - showProgress(0, `Sende: ${baseName}`); - + const paths = extractDroppedPaths(files); + if (paths.length === 0) { try { - const res = await window.electronAPI.uploadAndPush({ - localFolder: p, - owner, - repo: repoName, - destPath: '', - cloneUrl, - branch: getDefaultBranch(owner, repoName), - skipBackup: i > 0, // Backup nur beim ersten Element - uploadSessionId + window.electronAPI.debugToMain('warn', 'repoCard:drop:no-paths', { + fileNames: Array.from(files).map(f => f.name) }); - - if (!res.ok) { - console.error("Upload Fehler:", res.error); - showError("Fehler: " + res.error); - } - } catch (err) { - console.error("Kritischer Upload Fehler:", err); - setStatus("Upload fehlgeschlagen"); + } catch (_) {} + showError('Dateipfade konnten nicht gelesen werden (Sandbox/Drag-Quelle). Bitte Upload über Kontextmenü testen.'); + return; + } + console.log('[UPLOAD_DEBUG][renderer] repoCard:drop', { + owner, + repo: repoName, + branch: getDefaultBranch(owner, repoName), + paths + }); + setStatus(`Starte Upload von ${paths.length} Elementen...`); + + try { + const res = await uploadDroppedPaths({ + paths, + owner, + repo: repoName, + destPath: '', + cloneUrl, + branch: getDefaultBranch(owner, repoName) + }); + console.log('[UPLOAD_DEBUG][renderer] repoCard:dropResult', res); + if (!res.ok) { + showError('Fehler: ' + (res.error || 'Upload fehlgeschlagen')); + setStatus('Upload fehlgeschlagen'); + } else { + setStatus('Upload abgeschlossen'); } + } catch (err) { + console.error('Kritischer Upload Fehler:', err); + setStatus('Upload fehlgeschlagen'); } hideProgress(); - setStatus('Upload abgeschlossen'); }); card.onclick = () => { addToRecent(owner, repoName, cloneUrl); loadRepoContents(owner, repoName, ''); }; - card.oncontextmenu = (ev) => showRepoContextMenu(ev, owner, repoName, cloneUrl, card); + card.oncontextmenu = (ev) => showRepoContextMenu(ev, owner, repoName, cloneUrl, card, !!repo.private, repo); grid.appendChild(card); }); + repoKnownTopics = Array.from(knownTopicSet).sort((a, b) => a.localeCompare(b, 'de')); + repoKnownTopicsLoadedAt = Date.now(); + applyRepoFuzzyFilter(grid, searchInput, null); setStatus(`Loaded ${res.repos.length} repos`); @@ -2688,13 +3391,15 @@ async function loadRepoContents(owner, repo, path) { setStatus(`Loading: /${path || 'root'}`); const ref = getDefaultBranch(owner, repo); + const platform = currentState.platform; try { const res = await window.electronAPI.getGiteaRepoContents({ owner, repo, path, - ref + ref, + platform }); if (!res.ok) { @@ -2710,7 +3415,10 @@ async function loadRepoContents(owner, repo, path) { const emptyMsg = res.empty ? '📭 Leeres Repository — noch keine Commits' : '📂 Leerer Ordner'; - grid.innerHTML = `
${emptyMsg}
`; + const emptyEl = document.createElement('div'); + emptyEl.style.cssText = 'grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-muted);'; + emptyEl.textContent = emptyMsg; + grid.appendChild(emptyEl); setStatus(res.empty ? 'Leeres Repository' : 'Leerer Ordner'); return; } @@ -2776,28 +3484,27 @@ async function loadRepoContents(owner, repo, path) { const files = ev.dataTransfer.files; if (!files || files.length === 0) return; - const paths = Array.from(files).map(f => f.path); + const paths = extractDroppedPaths(files); + if (paths.length === 0) { + showError('Dateipfade konnten nicht gelesen werden (Sandbox/Drag-Quelle).'); + return; + } const targetPath = item.path; - const uploadSessionId = `drop-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - - for (let i = 0; i < paths.length; i++) { - const p = paths[i]; - const baseName = p.split(/[\\/]/).pop(); - showProgress(0, `Uploading ${baseName} to ${targetPath}...`); - - try { - await window.electronAPI.uploadAndPush({ - localFolder: p, - owner, - repo, - destPath: targetPath, - branch: getDefaultBranch(owner, repo), - skipBackup: i > 0, // Backup nur beim ersten Element - uploadSessionId - }); - } catch (error) { - console.error('Upload error:', error); + + try { + const res = await uploadDroppedPaths({ + paths, + owner, + repo, + destPath: targetPath, + branch: getDefaultBranch(owner, repo) + }); + if (!res.ok) { + showError('Upload error: ' + (res.error || 'Unbekannter Fehler')); } + } catch (error) { + console.error('Upload error:', error); + showError('Upload failed'); } hideProgress(); @@ -2871,7 +3578,6 @@ async function refreshLocalTree(folder) { currentLocalProjects = Array.isArray(res.tree) ? res.tree.filter(node => node && node.isDirectory) : []; - populateBackupSourceOptions(); const grid = $('explorerGrid'); if (!grid) return; @@ -2884,7 +3590,6 @@ async function refreshLocalTree(folder) { if (!res.tree || res.tree.length === 0) { currentLocalProjects = []; - populateBackupSourceOptions(); grid.innerHTML = '
Keine Dateien gefunden
'; return; } @@ -2938,7 +3643,12 @@ async function refreshLocalTree(folder) { ------------------------- */ async function pushLocalFolder() { if (!selectedFolder) { - alert('Select local folder first'); + await showActionConfirmModal({ + title: 'Lokaler Ordner fehlt', + message: 'Bitte waehle zuerst einen lokalen Ordner aus.', + confirmText: 'OK', + danger: false + }); return; } @@ -2949,17 +3659,9 @@ async function pushLocalFolder() { const branch = $('branchSelect')?.value || 'main'; const repoName = $('repoName')?.value; const platform = $('platform')?.value; - const savedCreds = await window.electronAPI.loadCredentials(); - const autoBackup = Boolean(savedCreds && savedCreds.autoBackupEnabled); - const backupTarget = String((savedCreds && savedCreds.backupPrefLocalFolder) || '').trim(); setStatus('Pushing...'); - if (autoBackup) { - showBackupRunOverlay('Auto-Backup wird erstellt...'); - showProgress(0, 'Auto-Backup wird erstellt...'); - } else { - showProgress(0, 'Starting push...'); - } + showProgress(0, 'Starting push...'); try { const res = await window.electronAPI.pushProject({ @@ -2967,17 +3669,11 @@ async function pushLocalFolder() { branch, repoName, platform, - commitMessage: message, - autoBackup, - backupTarget + commitMessage: message }); if (res.ok) { - if (autoBackup) { - showSuccess('Vorab-Backup OK, Upload erfolgreich ✓'); - } else { - showSuccess('Upload erfolgreich ✓'); - } + showSuccess('Upload erfolgreich ✓'); } else { showError('Upload fehlgeschlagen: ' + (res.error || 'Unbekannter Fehler')); } @@ -2986,7 +3682,6 @@ async function pushLocalFolder() { showError('Upload fehlgeschlagen'); } finally { hideProgress(); - hideBackupRunOverlay(); } } @@ -2996,36 +3691,73 @@ function showCommitMessageModal() { const modal = document.createElement('div'); modal.className = 'modal'; modal.style.zIndex = '99999'; - modal.innerHTML = ` -
-

💬 Commit-Nachricht

-
- - -
-
- ${['🐛 Fix Bug', '✨ Neues Feature', '📝 Dokumentation', '♻️ Refactoring', '🚀 Release'].map(t => - `` - ).join('')} -
- -
- `; + const card = document.createElement('div'); + card.className = 'modalContent card'; + card.style.maxWidth = '500px'; + + const title = document.createElement('h2'); + title.textContent = '💬 Commit-Nachricht'; + + const inputGroup = document.createElement('div'); + inputGroup.className = 'input-group'; + + const label = document.createElement('label'); + label.textContent = 'Was wurde geändert?'; + + const input = document.createElement('input'); + input.id = 'commitMsgInput'; + input.type = 'text'; + input.placeholder = 'z.B. Fix: Button-Farbe angepasst'; + input.style.fontSize = '15px'; + input.autocomplete = 'off'; + + inputGroup.appendChild(label); + inputGroup.appendChild(input); + + const quickBtns = document.createElement('div'); + quickBtns.id = 'commitQuickBtns'; + quickBtns.style.cssText = 'margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px;'; + + ['🐛 Fix Bug', '✨ Neues Feature', '📝 Dokumentation', '♻️ Refactoring', '🚀 Release'].forEach((text) => { + const btn = document.createElement('button'); + btn.className = 'commit-quick-btn'; + btn.style.cssText = ` + background: var(--bg-tertiary); + border: 1px solid rgba(255,255,255,0.1); + color: var(--text-secondary); + padding: 6px 12px; + border-radius: 20px; + cursor: pointer; + font-size: 12px; + `; + btn.textContent = text; + quickBtns.appendChild(btn); + }); + + const modalButtons = document.createElement('div'); + modalButtons.className = 'modal-buttons'; + modalButtons.style.marginTop = '20px'; + + const okButton = document.createElement('button'); + okButton.id = 'btnCommitOk'; + okButton.className = 'accent-btn'; + okButton.textContent = '⬆️ Pushen'; + + const cancelButton = document.createElement('button'); + cancelButton.id = 'btnCommitCancel'; + cancelButton.className = 'secondary'; + cancelButton.textContent = 'Abbrechen'; + + modalButtons.appendChild(okButton); + modalButtons.appendChild(cancelButton); + + card.appendChild(title); + card.appendChild(inputGroup); + card.appendChild(quickBtns); + card.appendChild(modalButtons); + modal.appendChild(card); document.body.appendChild(modal); - const input = modal.querySelector('#commitMsgInput'); input.focus(); // Quick-Buttons @@ -3054,6 +3786,100 @@ function showCommitMessageModal() { }); } +function escapeHtml(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function showSyncConfirmModal({ title = 'Bestaetigen', message = '', confirmText = 'Fortfahren', details = [] }) { + return new Promise((resolve) => { + const modal = document.createElement('div'); + modal.className = 'modal'; + + const card = document.createElement('div'); + card.className = 'card'; + card.style.width = 'min(520px, 92vw)'; + + const titleEl = document.createElement('h2'); + titleEl.style.marginBottom = '8px'; + titleEl.textContent = String(title || 'Bestaetigen'); + + const messageEl = document.createElement('p'); + messageEl.style.cssText = 'margin: 0; color: var(--text-secondary); line-height: 1.5;'; + messageEl.textContent = String(message || ''); + + card.appendChild(titleEl); + card.appendChild(messageEl); + + if (Array.isArray(details)) { + const rows = details.filter(d => d && d.label); + if (rows.length > 0) { + const detailBlock = document.createElement('div'); + detailBlock.style.cssText = 'margin-top:12px;padding:10px 12px;border-radius:10px;border:1px solid rgba(255,255,255,0.08);background:linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01));'; + + const detailTitle = document.createElement('div'); + detailTitle.style.cssText = 'font-size:12px;font-weight:700;letter-spacing:0.06em;text-transform:uppercase;color:#b9c9e8;margin-bottom:4px;'; + detailTitle.textContent = 'Was wird uebernommen'; + detailBlock.appendChild(detailTitle); + + rows.forEach((d) => { + const row = document.createElement('div'); + row.style.cssText = 'display:flex;justify-content:space-between;gap:10px;padding:6px 0;border-bottom:1px dashed rgba(255,255,255,0.08);'; + + const left = document.createElement('span'); + left.style.cssText = 'color:var(--text-secondary);font-size:12px;'; + left.textContent = String(d.label || ''); + + const right = document.createElement('strong'); + right.style.cssText = 'color:var(--text-primary);font-size:12px;text-align:right;word-break:break-word;'; + right.textContent = String(d.value || ''); + + row.appendChild(left); + row.appendChild(right); + detailBlock.appendChild(row); + }); + + card.appendChild(detailBlock); + } + } + + const btnRow = document.createElement('div'); + btnRow.className = 'modal-buttons'; + btnRow.style.marginTop = '18px'; + + const confirmBtn = document.createElement('button'); + confirmBtn.id = 'btnSyncConfirm'; + confirmBtn.className = 'accent-btn'; + confirmBtn.textContent = String(confirmText || 'Fortfahren'); + + const cancelBtn = document.createElement('button'); + cancelBtn.id = 'btnSyncCancel'; + cancelBtn.className = 'secondary'; + cancelBtn.textContent = 'Abbrechen'; + + btnRow.appendChild(confirmBtn); + btnRow.appendChild(cancelBtn); + card.appendChild(btnRow); + modal.appendChild(card); + + const close = (result) => { + modal.remove(); + resolve(!!result); + }; + + document.body.appendChild(modal); + modal.querySelector('#btnSyncConfirm').addEventListener('click', () => close(true)); + modal.querySelector('#btnSyncCancel').addEventListener('click', () => close(false)); + modal.addEventListener('click', (e) => { + if (e.target === modal) close(false); + }); + }); +} + async function loadBranches(folder) { try { const res = await window.electronAPI.getBranches({ folder }); @@ -3099,7 +3925,7 @@ async function loadCommitLogs(folder) { /* ------------------------- CONTEXT MENÜS ------------------------- */ -function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { +function showRepoContextMenu(ev, owner, repoName, cloneUrl, element, isPrivate = false, repoMeta = null) { ev.preventDefault(); ev.stopPropagation(); @@ -3115,7 +3941,7 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { const createMenuItem = (icon, text, onClick, color = null) => { const item = document.createElement('div'); item.className = 'context-item'; - item.innerHTML = `${icon} ${text}`; + item.textContent = `${icon} ${text}`; if (color) item.style.color = color; item.onclick = onClick; return item; @@ -3135,6 +3961,59 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { addSep(); + const writable = isRepoWritable(repoMeta || {}, currentGiteaUsername); + + // ── Projektbild ändern ── + if (writable && currentState.platform === 'gitea') { + menu.appendChild(createMenuItem('🖼️', 'Projektbild ändern', () => { + menu.remove(); + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = 'image/*'; + fileInput.style.display = 'none'; + document.body.appendChild(fileInput); + fileInput.onchange = async (e) => { + const file = e.target.files[0]; + fileInput.remove(); + if (!file) return; + const reader = new FileReader(); + reader.onload = async (ev) => { + const creds = await window.electronAPI.loadCredentials(); + if (!creds?.giteaToken || !creds?.giteaURL) { + showError('Gitea Token oder URL fehlt. Bitte zuerst in den Einstellungen speichern.'); + return; + } + showProgress(30, `Bild für "${repoName}" hochladen…`); + const result = await window.electronAPI.updateGiteaRepoAvatar({ + token: creds.giteaToken, + url: creds.giteaURL, + owner, + repo: repoName, + imageBase64: ev.target.result + }); + hideProgress(); + if (result?.ok) { + showSuccess(`Projektbild für "${repoName}" aktualisiert`); + loadGiteaRepos(); + } else { + showError('Upload fehlgeschlagen: ' + (result?.error || 'Unbekannter Fehler')); + } + }; + reader.readAsDataURL(file); + }; + fileInput.click(); + })); + } + + const fullName = `${owner || ''}/${repoName || ''}`; + const currentTopics = repoTopicsByFullName[fullName] || []; + if (writable) { + menu.appendChild(createMenuItem('🏷️', 'Tags bearbeiten', async () => { + menu.remove(); + await editRepoTopics(owner, repoName, currentTopics); + })); + } + menu.appendChild(createMenuItem('🔗', 'Clone-URL kopieren', async () => { menu.remove(); const res = await window.electronAPI.copyToClipboard(cloneUrl || ''); @@ -3142,6 +4021,89 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { else showError('Clone-URL konnte nicht kopiert werden'); })); + const isOwnRepo = String(owner || '').toLowerCase() === String(currentGiteaUsername || '').toLowerCase(); + if (isOwnRepo) { + const isGiteaView = currentState.platform === 'gitea'; + const syncLabel = isGiteaView ? 'Gitea -> GitHub synchronisieren' : 'GitHub -> Gitea synchronisieren'; + const syncIcon = isGiteaView ? '🔄' : '🔁'; + + menu.appendChild(createMenuItem(syncIcon, syncLabel, async () => { + menu.remove(); + + if (isGiteaView) { + const ok = await showSyncConfirmModal({ + title: 'Sync bestaetigen', + message: `Projekt "${repoName}" von Gitea nach GitHub synchronisieren?`, + confirmText: 'Jetzt synchronisieren', + details: [ + { label: 'Richtung', value: 'Gitea -> GitHub' }, + { label: 'Repository', value: `${owner}/${repoName}` }, + { label: 'Sichtbarkeit', value: isPrivate ? 'Privat' : 'Oeffentlich' }, + { label: 'Beschreibung', value: (repoMeta?.description || '-').slice(0, 160) }, + { label: 'Topics', value: (Array.isArray(repoMeta?.topics) && repoMeta.topics.length > 0) ? repoMeta.topics.join(', ') : '-' } + ] + }); + if (!ok) return; + showProgress(20, `Synchronisiere ${owner}/${repoName} nach GitHub...`); + const res = await window.electronAPI.syncRepoToGitHub({ + owner, + repo: repoName, + cloneUrl, + isPrivate, + description: repoMeta?.description || '', + homepage: repoMeta?.website || repoMeta?.homepage || '', + topics: Array.isArray(repoMeta?.topics) ? repoMeta.topics : [] + }); + hideProgress(); + + if (res?.ok) { + const createdText = res.repoCreated ? ' (neu erstellt)' : ' (bereits vorhanden)'; + showSuccess(`Sync erfolgreich: ${res.githubRepo}${createdText}`); + setStatus(`GitHub Sync OK: ${res.githubRepo}`); + } else { + showError('GitHub Sync fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler')); + } + } else { + const ok = await showSyncConfirmModal({ + title: 'Sync bestaetigen', + message: `Projekt "${repoName}" von GitHub nach Gitea synchronisieren?`, + confirmText: 'Jetzt synchronisieren', + details: [ + { label: 'Richtung', value: 'GitHub -> Gitea' }, + { label: 'Repository', value: `${owner}/${repoName}` }, + { label: 'Sichtbarkeit', value: isPrivate ? 'Privat' : 'Oeffentlich' }, + { label: 'Beschreibung', value: (repoMeta?.description || '-').slice(0, 160) }, + { label: 'Topics', value: (Array.isArray(repoMeta?.topics) && repoMeta.topics.length > 0) ? repoMeta.topics.join(', ') : '-' } + ] + }); + if (!ok) return; + const creds = await window.electronAPI.loadCredentials(); + if (!creds?.giteaToken || !creds?.giteaURL) { + showError('Gitea Token oder URL fehlt. Bitte in den Einstellungen speichern.'); + return; + } + + showProgress(20, `Synchronisiere ${owner}/${repoName} nach Gitea...`); + const res = await window.electronAPI.migrateRepoToGitea({ + cloneUrl, + repoName, + description: repoMeta?.description || `Synced from GitHub ${owner}/${repoName}`, + isPrivate: !!isPrivate, + authToken: creds.githubToken || '', + authUsername: 'x-access-token' + }); + hideProgress(); + + if (res?.ok) { + showSuccess(`Sync erfolgreich: Gitea/${repoName}`); + setStatus(`Gitea Sync OK: ${repoName}`); + } else { + showError('Gitea Sync fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler')); + } + } + })); + } + menu.appendChild(createMenuItem('📋', 'owner/repo kopieren', async () => { menu.remove(); const res = await window.electronAPI.copyToClipboard(`${owner}/${repoName}`); @@ -3149,7 +4111,7 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { else showError('owner/repo konnte nicht kopiert werden'); })); - menu.appendChild(createMenuItem('🌐 Im Browser öffnen', async () => { + menu.appendChild(createMenuItem('🌐', 'Im Browser öffnen', async () => { menu.remove(); const url = buildRepoWebUrl(owner, repoName); if (!url) { @@ -3161,6 +4123,17 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { else showError('Browser konnte nicht geöffnet werden'); })); + if (writable) { + menu.appendChild(createMenuItem( + isPrivate ? '🌍' : '🔒', + isPrivate ? 'Öffentlich machen' : 'Privat machen', + async () => { + menu.remove(); + await toggleRepoVisibility(owner, repoName, isPrivate); + } + )); + } + addSep(); // ── Favorit ── @@ -3207,9 +4180,15 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { const deleteItem = createMenuItem('🗑️', 'Repo löschen', async () => { menu.remove(); - if (confirm(`Delete ${repoName}?`)) { + const ok = await showActionConfirmModal({ + title: 'Repository loeschen', + message: `Delete ${repoName}?`, + confirmText: 'Loeschen', + danger: true + }); + if (ok) { try { - const res = await window.electronAPI.deleteGiteaRepo({ owner, repo: repoName }); + const res = await window.electronAPI.deleteGiteaRepo({ owner, repo: repoName, platform: currentState.platform }); if (res.ok) { element.remove(); showSuccess('Repository deleted'); @@ -3223,8 +4202,10 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { } }, '#ef4444'); - menu.appendChild(uploadItem); - menu.appendChild(deleteItem); + if (writable) { + menu.appendChild(uploadItem); + menu.appendChild(deleteItem); + } document.body.appendChild(menu); setTimeout(() => { @@ -3259,7 +4240,7 @@ function showGiteaItemContextMenu(ev, item, owner, repo) { const addItem = (icon, text, onClick, color = null) => { const el = document.createElement('div'); el.className = 'context-item'; - el.innerHTML = `${icon} ${text}`; + el.textContent = `${icon} ${text}`; if (color) el.style.color = color; el.onclick = () => { menu.remove(); onClick(); }; menu.appendChild(el); @@ -3274,11 +4255,18 @@ function showGiteaItemContextMenu(ev, item, owner, repo) { addSep(); addItem('🗑️', `Alle ${selectedItems.size} löschen`, async () => { - if (!confirm(`${selectedItems.size} Elemente wirklich löschen?`)) return; + const ok = await showActionConfirmModal({ + title: 'Mehrfach-Loeschen', + message: `${selectedItems.size} Elemente wirklich loeschen?`, + confirmText: 'Loeschen', + danger: true + }); + if (!ok) return; showProgress(0, 'Lösche...'); let done = 0; for (const p of selectedItems) { - await window.electronAPI.deleteFile({ path: p, owner, repo, isGitea: true }); + const isGithub = currentState.platform === 'github'; + 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}`); } @@ -3316,7 +4304,7 @@ function showGiteaItemContextMenu(ev, item, owner, repo) { // --- CUT & PASTE --- addItem('✂️', 'Ausschneiden (Cut)', () => { - clipboard = { item: { ...item, owner, repo, isGitea: true }, action: 'cut' }; + clipboard = { item: { ...item, owner, repo, isGitea: currentState.platform !== 'github', isGithub: currentState.platform === 'github' }, action: 'cut' }; setStatus(`✂️ "${item.name}" ausgeschnitten — Zielordner öffnen und Einfügen wählen`); }); @@ -3347,16 +4335,23 @@ function showGiteaItemContextMenu(ev, item, owner, repo) { // --- LÖSCHEN --- addItem('🗑️', 'Löschen', async () => { - if (!confirm(`"${item.name}" wirklich löschen?`)) return; + const ok = await showActionConfirmModal({ + title: 'Element loeschen', + message: `"${item.name}" wirklich loeschen?`, + confirmText: 'Loeschen', + danger: true + }); + if (!ok) return; showProgress(0, `Lösche ${item.name}...`); - const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: true }); + const isGithub = currentState.platform === 'github'; + 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`); loadRepoContents(owner, repo, currentState.path); } else { showError('Löschen fehlgeschlagen: ' + (res?.error || '')); - alert('Löschen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler')); + await showInfoModal('Loeschen fehlgeschlagen', 'Loeschen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler'), true); } }, '#ef4444'); @@ -3393,7 +4388,7 @@ function showLocalItemContextMenu(ev, node) { const addItem = (icon, text, onClick, color = null) => { const el = document.createElement('div'); el.className = 'context-item'; - el.innerHTML = `${icon} ${text}`; + el.textContent = `${icon} ${text}`; if (color) el.style.color = color; el.onclick = () => { menu.remove(); onClick(); }; menu.appendChild(el); @@ -3408,7 +4403,13 @@ function showLocalItemContextMenu(ev, node) { addSep(); addItem('🗑️', `Alle ${selectedItems.size} löschen`, async () => { - if (!confirm(`${selectedItems.size} Elemente wirklich löschen?`)) return; + const ok = await showActionConfirmModal({ + title: 'Mehrfach-Loeschen', + message: `${selectedItems.size} Elemente wirklich loeschen?`, + confirmText: 'Loeschen', + danger: true + }); + if (!ok) return; for (const p of selectedItems) { await window.electronAPI.deleteFile({ path: p }); } @@ -3464,7 +4465,13 @@ function showLocalItemContextMenu(ev, node) { // --- LÖSCHEN --- addItem('🗑️', 'Löschen', async () => { - if (!confirm(`"${node.name}" wirklich löschen?`)) return; + const ok = await showActionConfirmModal({ + title: 'Element loeschen', + message: `"${node.name}" wirklich loeschen?`, + confirmText: 'Loeschen', + danger: true + }); + if (!ok) return; const res = await window.electronAPI.deleteFile({ path: node.path }); if (res && res.ok) { setStatus(`${node.name} gelöscht`); @@ -3504,7 +4511,7 @@ function showGiteaRenameModal(item, owner, repo) { setStatus(`Umbenannt in "${newName}"`); loadRepoContents(owner, repo, currentState.path); } else { - alert('Umbenennen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler')); + await showInfoModal('Umbenennen fehlgeschlagen', 'Umbenennen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler'), true); showError('Fehler beim Umbenennen'); } } @@ -3530,7 +4537,7 @@ function showNewGiteaItemModal(owner, repo, parentPath, type) { setStatus(`"${name}" erstellt`); loadRepoContents(owner, repo, currentState.path); } else { - alert('Erstellen fehlgeschlagen:\n' + (res?.error || '')); + await showInfoModal('Erstellen fehlgeschlagen', 'Erstellen fehlgeschlagen:\n' + (res?.error || ''), true); } } }); @@ -3550,7 +4557,7 @@ function showLocalRenameModal(node) { setStatus(`Umbenannt in "${newName}"`); if (selectedFolder) refreshLocalTree(selectedFolder); } else { - alert('Umbenennen fehlgeschlagen:\n' + (res?.error || '')); + await showInfoModal('Umbenennen fehlgeschlagen', 'Umbenennen fehlgeschlagen:\n' + (res?.error || ''), true); } } }); @@ -3570,7 +4577,7 @@ function showNewLocalItemModal(parentDir, type) { setStatus(`"${name}" erstellt`); if (selectedFolder) refreshLocalTree(selectedFolder); } else { - alert('Erstellen fehlgeschlagen:\n' + (res?.error || '')); + await showInfoModal('Erstellen fehlgeschlagen', 'Erstellen fehlgeschlagen:\n' + (res?.error || ''), true); } } }); @@ -3595,7 +4602,7 @@ async function pasteGiteaItem(owner, repo, destFolderPath) { setStatus(`"${src.name}" verschoben`); loadRepoContents(owner, repo, currentState.path); } else { - alert('Verschieben fehlgeschlagen:\n' + (res?.error || '')); + await showInfoModal('Verschieben fehlgeschlagen', 'Verschieben fehlgeschlagen:\n' + (res?.error || ''), true); showError('Fehler beim Verschieben'); } } @@ -3611,7 +4618,7 @@ async function pasteLocalItem(destDir) { setStatus(`"${src.name}" verschoben`); if (selectedFolder) refreshLocalTree(selectedFolder); } else { - alert('Verschieben fehlgeschlagen:\n' + (res?.error || '')); + await showInfoModal('Verschieben fehlgeschlagen', 'Verschieben fehlgeschlagen:\n' + (res?.error || ''), true); } } @@ -3620,19 +4627,49 @@ function showInputModal({ title, label, defaultValue, confirmText, onConfirm }) const modal = document.createElement('div'); modal.className = 'modal'; modal.style.zIndex = '99999'; - modal.innerHTML = ` -
-

${title}

-
- - -
- -
- `; + const card = document.createElement('div'); + card.className = 'modalContent card'; + card.style.maxWidth = '420px'; + + const titleEl = document.createElement('h2'); + titleEl.textContent = String(title || ''); + + const group = document.createElement('div'); + group.className = 'input-group'; + + const labelEl = document.createElement('label'); + labelEl.textContent = String(label || ''); + + const inputEl = document.createElement('input'); + inputEl.id = 'inputModalField'; + inputEl.type = 'text'; + inputEl.value = String(defaultValue || ''); + inputEl.autocomplete = 'off'; + + group.appendChild(labelEl); + group.appendChild(inputEl); + + const buttons = document.createElement('div'); + buttons.className = 'modal-buttons'; + buttons.style.marginTop = '16px'; + + const okBtn = document.createElement('button'); + okBtn.id = 'inputModalOk'; + okBtn.className = 'accent-btn'; + okBtn.textContent = String(confirmText || 'Bestätigen'); + + const cancelBtn = document.createElement('button'); + cancelBtn.id = 'inputModalCancel'; + cancelBtn.className = 'secondary'; + cancelBtn.textContent = 'Abbrechen'; + + buttons.appendChild(okBtn); + buttons.appendChild(cancelBtn); + + card.appendChild(titleEl); + card.appendChild(group); + card.appendChild(buttons); + modal.appendChild(card); document.body.appendChild(modal); const input = modal.querySelector('#inputModalField'); input.focus(); @@ -3656,30 +4693,50 @@ function showActionConfirmModal({ title, message, confirmText = 'Bestätigen', d const modal = document.createElement('div'); modal.className = 'modal confirm-modal'; modal.style.zIndex = '99999'; - modal.innerHTML = ` -
-

${danger ? '🗑️' : 'ℹ️'} ${escapeHtml(title || 'Bestätigung')}

-

${escapeHtml(message || '')}

- -
- `; + const card = document.createElement('div'); + card.className = 'modalContent card confirm-modal-content'; + + const titleEl = document.createElement('h2'); + titleEl.className = 'confirm-modal-title'; + titleEl.textContent = `${danger ? '🗑️' : 'ℹ️'} ${title || 'Bestätigung'}`; + + const msgEl = document.createElement('p'); + msgEl.className = 'confirm-modal-message'; + msgEl.textContent = String(message || ''); + + const btnWrap = document.createElement('div'); + btnWrap.className = 'modal-buttons confirm-modal-buttons'; + + const okBtn = document.createElement('button'); + okBtn.id = 'actionConfirmOk'; + okBtn.className = `confirm-modal-btn ${danger ? 'confirm-modal-btn--danger' : 'confirm-modal-btn--primary'}`; + okBtn.textContent = String(confirmText || 'Bestätigen'); + + const cancelBtn = document.createElement('button'); + cancelBtn.id = 'actionConfirmCancel'; + cancelBtn.className = 'confirm-modal-btn confirm-modal-btn--secondary'; + cancelBtn.textContent = 'Abbrechen'; + + btnWrap.appendChild(okBtn); + btnWrap.appendChild(cancelBtn); + card.appendChild(titleEl); + card.appendChild(msgEl); + card.appendChild(btnWrap); + modal.appendChild(card); document.body.appendChild(modal); - const okBtn = modal.querySelector('#actionConfirmOk'); - const cancelBtn = modal.querySelector('#actionConfirmCancel'); - if (okBtn) okBtn.focus(); + const okBtnNode = modal.querySelector('#actionConfirmOk'); + const cancelBtnNode = modal.querySelector('#actionConfirmCancel'); + if (okBtnNode) okBtnNode.focus(); const closeWith = (result) => { modal.remove(); resolve(result); }; - if (okBtn) okBtn.onclick = () => closeWith(true); - if (cancelBtn) cancelBtn.onclick = () => closeWith(false); + if (okBtnNode) okBtnNode.onclick = () => closeWith(true); + if (cancelBtnNode) cancelBtnNode.onclick = () => closeWith(false); modal.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeWith(false); @@ -3692,6 +4749,15 @@ function showActionConfirmModal({ title, message, confirmText = 'Bestätigen', d }); } +function showInfoModal(title, message, danger = false) { + return showActionConfirmModal({ + title, + message, + confirmText: 'OK', + danger + }); +} + /* ------------------------- HELPER FUNCTIONS ------------------------- */ @@ -3709,11 +4775,11 @@ async function previewGiteaFile(owner, repo, filePath) { owner, repo, path: filePath, - ref: getDefaultBranch(owner, repo) + ref: getDefaultBranch(owner, repo), + platform: currentState.platform }); if (res.ok) { - console.log("Content of", filePath, ":", res.content); setStatus(`Previewed: ${filePath}`); } else { showError('Preview failed'); @@ -3738,7 +4804,12 @@ async function createRepoHandler() { } if (check.similar && check.similar.length > 0) { - const proceed = confirm(`Ähnliche Repository-Namen gefunden: ${check.similar.slice(0, 3).join(', ')}\n\nTrotzdem erstellen?`); + const proceed = await showActionConfirmModal({ + title: 'Aehnliche Namen gefunden', + message: `Aehnliche Repository-Namen gefunden: ${check.similar.slice(0, 3).join(', ')}\n\nTrotzdem erstellen?`, + confirmText: 'Trotzdem erstellen', + danger: false + }); if (!proceed) return; } @@ -3815,7 +4886,7 @@ function setupBackgroundContextMenu() { const addItem = (icon, text, onClick) => { const el = document.createElement('div'); el.className = 'context-item'; - el.innerHTML = `${icon} ${text}`; + el.textContent = `${icon} ${text}`; el.onclick = () => { menu.remove(); onClick(); }; menu.appendChild(el); }; @@ -3948,36 +5019,28 @@ function setupGlobalDropZone() { const repo = currentState.repo; const targetPath = currentState.path || ''; - const paths = Array.from(files).map(f => f.path); - const uploadSessionId = `drop-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + const paths = extractDroppedPaths(files); + if (paths.length === 0) { + showError('Dateipfade konnten nicht gelesen werden (Sandbox/Drag-Quelle).'); + return; + } setStatus(`Uploading ${paths.length} items to /${targetPath || 'root'}...`); - - for (let i = 0; i < paths.length; i++) { - const p = paths[i]; - const baseName = p.split(/[\\/]/).pop(); - showProgress(0, `Uploading: ${baseName}`); - - try { - const res = await window.electronAPI.uploadAndPush({ - localFolder: p, - owner, - repo, - destPath: targetPath, - branch: getDefaultBranch(owner, repo), - skipBackup: i > 0, - uploadSessionId - }); - - if (!res.ok) { - console.error("Upload error:", res.error); - showError("Error: " + res.error); - } else { - setStatus(`Uploaded: ${baseName}`); - } - } catch (err) { - console.error("Critical upload error:", err); - showError("Upload failed"); + + try { + const res = await uploadDroppedPaths({ + paths, + owner, + repo, + destPath: targetPath, + branch: getDefaultBranch(owner, repo) + }); + if (!res.ok) { + console.error('Upload error:', res.error); + showError('Error: ' + (res.error || 'Upload failed')); } + } catch (err) { + console.error('Critical upload error:', err); + showError('Upload failed'); } hideProgress(); @@ -4006,6 +5069,13 @@ window.addEventListener('DOMContentLoaded', async () => { }); document.addEventListener('drop', e => { + try { + window.electronAPI.debugToMain('log', 'document:drop', { + currentView: currentState.view, + fileCount: e.dataTransfer?.files ? e.dataTransfer.files.length : 0, + itemCount: e.dataTransfer?.items ? e.dataTransfer.items.length : 0 + }); + } catch (_) {} if (currentState.view !== 'gitea-repo') { e.preventDefault(); } @@ -4014,12 +5084,21 @@ window.addEventListener('DOMContentLoaded', async () => { // Load credentials and auto-login if available try { const creds = await window.electronAPI.loadCredentials(); + const credentialStatus = await window.electronAPI.getCredentialsStatus(); if (creds) { // Fülle Settings-Felder if ($('githubToken')) $('githubToken').value = creds.githubToken || ''; if ($('giteaToken')) $('giteaToken').value = creds.giteaToken || ''; if ($('giteaURL')) $('giteaURL').value = creds.giteaURL || ''; - renderGiteaUrlHint(creds.giteaURL || ''); + renderGithubTokenHint(creds.githubToken || ''); + renderGiteaUrlHint(creds.giteaURL || '', creds.giteaToken || ''); + // Avatar anzeigen + if (creds.avatarB64) { + const img = $('settingsAvatarImg'); + const ph = $('settingsAvatarPlaceholder'); + if (img) { img.src = creds.avatarB64; img.style.display = 'block'; } + if (ph) ph.style.display = 'none'; + } const checkedUrl = normalizeAndValidateGiteaUrl(creds.giteaURL || ''); updateSettingsHealth({ @@ -4060,26 +5139,28 @@ window.addEventListener('DOMContentLoaded', async () => { if (cbColorIcons) cbColorIcons.checked = featureColoredIcons; const cbAutostart = $('settingAutostart'); if (cbAutostart) cbAutostart.checked = featureAutostart; - const cbAutoBackup = $('settingAutoBackup'); - if (cbAutoBackup) cbAutoBackup.checked = Boolean(creds.autoBackupEnabled); - // 🆕 AUTO-LOGIN: Wenn Gitea-Credentials vorhanden sind, lade sofort die Repos - if (creds.giteaToken && creds.giteaURL) { - console.log('✅ Credentials gefunden - Auto-Login wird gestartet...'); + // Standard beim App-Start: zuerst "Meine" Repositories anzeigen. + activeRepoOwnerFilter = 'mine'; + + // AUTO-LOGIN: je nach ausgewaehlter Plattform nur mit passenden Credentials laden + const selectedPlatform = ($('platform')?.value || currentState.platform || 'gitea'); + const canAutoload = selectedPlatform === 'github' + ? !!creds.githubToken + : !!(creds.giteaToken && creds.giteaURL); + + if (canAutoload) { setStatus('Lade deine Projekte...'); - - // Kurze Verzögerung damit UI fertig geladen ist setTimeout(() => { - loadGiteaRepos(); - }, 500); + loadRepos(); + }, 350); } else { - console.log('ℹ️ Keine vollständigen Gitea-Credentials - bitte in Settings eintragen'); setStatus('Bereit - bitte Settings konfigurieren'); } } else { - console.log('ℹ️ Keine Credentials gespeichert'); setStatus('Bereit - bitte Settings konfigurieren'); - renderGiteaUrlHint(''); + renderGithubTokenHint(''); + renderGiteaUrlHint('', ''); updateSettingsHealth({ url: 'Nicht konfiguriert', api: 'Nicht konfiguriert', @@ -4088,6 +5169,19 @@ window.addEventListener('DOMContentLoaded', async () => { version: '-', lastError: '-' }); + if (credentialStatus && credentialStatus.reason === 'safeStorage-decrypt-failed') { + showError('Gespeicherte Zugangsdaten konnten nicht entschlüsselt werden. Bitte GitHub- und Gitea-Token neu eingeben und speichern.'); + updateSettingsHealth({ + auth: 'Nicht lesbar', + lastError: 'Credentials nicht entschlüsselbar' + }); + } else if (credentialStatus && credentialStatus.reason === 'safeStorage-unavailable') { + showError('Die aktuelle Sitzung kann die gespeicherten Zugangsdaten nicht über safeStorage lesen. Bitte erneut eingeben und speichern.'); + updateSettingsHealth({ + auth: 'Nicht lesbar', + lastError: 'safeStorage nicht verfügbar' + }); + } } } catch (error) { console.error('Error loading credentials:', error); @@ -4098,7 +5192,7 @@ window.addEventListener('DOMContentLoaded', async () => { // Event Handlers if ($('btnLoadGiteaRepos')) { - $('btnLoadGiteaRepos').onclick = loadGiteaRepos; + $('btnLoadGiteaRepos').onclick = loadRepos; } if ($('btnSelectFolder')) { @@ -4327,7 +5421,7 @@ window.addEventListener('DOMContentLoaded', async () => { if ($('giteaURL')) { $('giteaURL').addEventListener('input', (e) => { const raw = e.target.value; - renderGiteaUrlHint(raw); + renderGiteaUrlHint(raw, $('giteaToken')?.value || ''); const checked = normalizeAndValidateGiteaUrl(raw); updateSettingsHealth({ url: checked.ok && checked.value ? 'Gültig' : (checked.ok ? 'Leer' : 'Ungültig') @@ -4335,6 +5429,18 @@ window.addEventListener('DOMContentLoaded', async () => { }); } + if ($('giteaToken')) { + $('giteaToken').addEventListener('input', (e) => { + renderGiteaUrlHint($('giteaURL')?.value || '', e.target.value || ''); + }); + } + + if ($('githubToken')) { + $('githubToken').addEventListener('input', (e) => { + renderGithubTokenHint(e.target.value || ''); + }); + } + if ($('btnTestGiteaConnection')) { $('btnTestGiteaConnection').onclick = async () => { const token = $('giteaToken')?.value || ''; @@ -4363,7 +5469,7 @@ window.addEventListener('DOMContentLoaded', async () => { }); if (!res.ok) { - showError(res.error || 'Verbindungstest fehlgeschlagen'); + showError('Gitea: Nicht verbunden'); updateSettingsHealth({ url: 'Gültig', api: 'Fehler', @@ -4388,11 +5494,44 @@ window.addEventListener('DOMContentLoaded', async () => { lastError: '-' }); - if (result.ok) showSuccess('Verbindung erfolgreich getestet'); - else showWarning('Server erreichbar, aber Auth/Teilcheck fehlgeschlagen'); + if (result.ok) showSuccess('Gitea: Verbunden'); + else showError('Gitea: Nicht verbunden'); } catch (error) { console.error('test-gitea-connection error:', error); - showError(error && error.message ? error.message : String(error)); + showError('Gitea: Nicht verbunden'); + } finally { + btn.disabled = false; + btn.textContent = oldText; + } + }; + } + + if ($('btnTestGithubConnection')) { + $('btnTestGithubConnection').onclick = async () => { + const token = $('githubToken')?.value || ''; + if (!token) { + showError('GitHub: Nicht verbunden'); + return; + } + + setStatus('Teste GitHub-Verbindung...'); + const btn = $('btnTestGithubConnection'); + const oldText = btn.textContent; + btn.disabled = true; + btn.textContent = 'Teste...'; + + try { + const res = await window.electronAPI.testGithubConnection({ token }); + if (!res.ok) { + showError('GitHub: Nicht verbunden'); + return; + } + + showSuccess('GitHub: Verbunden'); + updateSettingsHealth({ auth: 'OK', lastError: '-' }); + } catch (error) { + console.error('test-github-connection error:', error); + showError('GitHub: Nicht verbunden'); } finally { btn.disabled = false; btn.textContent = oldText; @@ -4420,6 +5559,76 @@ window.addEventListener('DOMContentLoaded', async () => { }; } + // ── Migration Modal ── + if ($('btnOpenMigration')) { + $('btnOpenMigration').onclick = () => { + $('migrationModal').classList.remove('hidden'); + }; + } + if ($('btnCloseMigration')) { + $('btnCloseMigration').onclick = () => { + $('migrationModal').classList.add('hidden'); + $('migrationStatus').classList.add('hidden'); + $('migrationStatus').textContent = ''; + }; + } + if ($('migrateCloneUrl')) { + $('migrateCloneUrl').addEventListener('input', () => { + // Repo-Name aus URL automatisch ableiten + const url = $('migrateCloneUrl').value.trim(); + const match = url.match(/\/([^/]+?)(\.git)?$/); + if (match && !$('migrateRepoName').value) { + $('migrateRepoName').value = match[1]; + } + }); + } + if ($('btnStartMigration')) { + $('btnStartMigration').onclick = async () => { + const cloneUrl = $('migrateCloneUrl')?.value.trim(); + const repoName = $('migrateRepoName')?.value.trim(); + if (!cloneUrl) { showError('Bitte eine Quell-URL eingeben.'); return; } + if (!repoName) { showError('Bitte einen Repository-Namen eingeben.'); return; } + + const statusEl = $('migrationStatus'); + const btn = $('btnStartMigration'); + btn.disabled = true; + btn.textContent = '⏳ Migration läuft…'; + statusEl.className = 'migration-status migration-status--running'; + statusEl.textContent = `⏳ Migriere "${repoName}" von ${cloneUrl} …`; + statusEl.classList.remove('hidden'); + + const result = await window.electronAPI.migrateRepoToGitea({ + cloneUrl, + repoName, + description: $('migrateDescription')?.value.trim() || '', + isPrivate: $('migratePrivate')?.checked || false, + authUsername: $('migrateAuthUsername')?.value.trim() || '', + authToken: $('migrateAuthToken')?.value.trim() || '' + }); + + btn.disabled = false; + btn.textContent = '📥 Migration starten'; + + if (result?.ok) { + statusEl.className = 'migration-status migration-status--ok'; + statusEl.textContent = `✅ "${repoName}" wurde erfolgreich migriert!`; + showSuccess(`Repository "${repoName}" migriert`); + // Felder leeren + $('migrateCloneUrl').value = ''; + $('migrateRepoName').value = ''; + $('migrateDescription').value = ''; + $('migrateAuthUsername').value = ''; + $('migrateAuthToken').value = ''; + $('migratePrivate').checked = false; + loadGiteaRepos(); + } else { + statusEl.className = 'migration-status migration-status--error'; + statusEl.textContent = `❌ Fehler: ${result?.error || 'Unbekannter Fehler'}`; + showError('Migration fehlgeschlagen: ' + (result?.error || '')); + } + }; + } + try { const queue = await window.electronAPI.getRetryQueue(); if (queue && queue.ok) { @@ -4428,6 +5637,61 @@ window.addEventListener('DOMContentLoaded', async () => { } } catch (_) {} + // Avatar-Picker + if ($('settingsAvatarWrap')) { + $('settingsAvatarWrap').onclick = () => $('settingsAvatarInput')?.click(); + } + if ($('settingsAvatarInput')) { + $('settingsAvatarInput').onchange = (e) => { + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + const img = $('settingsAvatarImg'); + const ph = $('settingsAvatarPlaceholder'); + if (img) { img.src = ev.target.result; img.style.display = 'block'; } + if (ph) ph.style.display = 'none'; + }; + reader.readAsDataURL(file); + }; + } + + // Avatar direkt auf Gitea hochladen + if ($('btnUploadAvatar')) { + $('btnUploadAvatar').onclick = async () => { + const btn = $('btnUploadAvatar'); + const img = $('settingsAvatarImg'); + const avatarB64 = img && img.style.display !== 'none' ? img.src : null; + if (!avatarB64 || !avatarB64.startsWith('data:')) { + showError('Kein Profilbild ausgewählt. Bitte zuerst ein Bild auswählen.'); + return; + } + const token = $('giteaToken')?.value || ''; + const url = $('giteaURL')?.value || ''; + if (!token || !url) { + showError('Gitea Token und URL müssen eingetragen sein.'); + return; + } + btn.disabled = true; + btn.textContent = '⏳ Wird hochgeladen…'; + try { + const result = await window.electronAPI.updateGiteaAvatar({ token, url, imageBase64: avatarB64 }); + if (result && result.ok) { + btn.textContent = '✅ Aktualisiert'; + setTimeout(() => { btn.textContent = '📤 Auf Gitea aktualisieren'; btn.disabled = false; }, 2500); + } else { + showError('Upload fehlgeschlagen: ' + (result?.error || 'Unbekannter Fehler')); + btn.textContent = '📤 Auf Gitea aktualisieren'; + btn.disabled = false; + } + } catch (err) { + showError('Upload fehlgeschlagen: ' + err.message); + btn.textContent = '📤 Auf Gitea aktualisieren'; + btn.disabled = false; + } + }; + } + if ($('btnSaveSettings')) { $('btnSaveSettings').onclick = async () => { try { @@ -4454,12 +5718,32 @@ window.addEventListener('DOMContentLoaded', async () => { return; } + const credentialStatus = await window.electronAPI.getCredentialsStatus(); + const githubTokenValue = $('githubToken').value; + const giteaTokenValue = $('giteaToken').value; + if ( + credentialStatus && + (credentialStatus.reason === 'safeStorage-decrypt-failed' || credentialStatus.reason === 'safeStorage-unavailable') && + !String(githubTokenValue || '').trim() && + !String(giteaTokenValue || '').trim() && + !String(checkedUrl.value || '').trim() + ) { + showError('Gespeicherte Zugangsdaten sind nicht lesbar. Bitte Token und URL neu eingeben, bevor du speicherst.'); + return; + } + const existingCreds = await window.electronAPI.loadCredentials() || {}; + // aktuell ausgewählten Avatar-Base64 aus dem img-Element lesen + const avatarImg = $('settingsAvatarImg'); + const avatarB64 = (avatarImg && avatarImg.src && avatarImg.style.display !== 'none') + ? avatarImg.src + : (existingCreds.avatarB64 || null); const data = { ...existingCreds, - githubToken: $('githubToken').value, - giteaToken: $('giteaToken').value, + githubToken: githubTokenValue, + giteaToken: giteaTokenValue, giteaURL: checkedUrl.value, + avatarB64, featureFavorites, featureRecent, compactMode, @@ -4468,9 +5752,31 @@ window.addEventListener('DOMContentLoaded', async () => { favCollapsedRecent: favSectionCollapsed.recent }; await window.electronAPI.saveCredentials(data); + // Avatar automatisch zu Gitea pushen + if (avatarB64 && avatarB64.startsWith('data:') && data.giteaToken && data.giteaURL) { + const btnSave = $('btnSaveSettings'); + if (btnSave) btnSave.textContent = '⏳ Bild wird hochgeladen…'; + try { + const avatarResult = await window.electronAPI.updateGiteaAvatar({ + token: data.giteaToken, + url: data.giteaURL, + imageBase64: avatarB64 + }); + if (avatarResult && !avatarResult.ok) { + console.error('Avatar-Upload Fehler:', avatarResult.error); + showError('Profilbild konnte nicht hochgeladen werden: ' + (avatarResult.error || 'Unbekannter Fehler')); + } + } catch (avatarErr) { + console.error('Avatar-Upload Exception:', avatarErr); + showError('Profilbild-Upload fehlgeschlagen: ' + avatarErr.message); + } finally { + if (btnSave) btnSave.textContent = 'Speichern'; + } + } $('settingsModal').classList.add('hidden'); showSuccess('Settings saved'); - renderGiteaUrlHint(checkedUrl.value); + renderGithubTokenHint(data.githubToken || ''); + renderGiteaUrlHint(checkedUrl.value, data.giteaToken || ''); updateSettingsHealth({ url: checkedUrl.value ? 'Gültig' : 'Leer', auth: data.giteaToken ? 'Token vorhanden' : 'Kein Token', @@ -4608,30 +5914,6 @@ window.addEventListener('DOMContentLoaded', async () => { showProgress(p.percent, `Download: ${p.processed}/${p.total}`); }); - if (window.electronAPI.onPrePushBackupStatus) { - window.electronAPI.onPrePushBackupStatus((payload) => { - if (!payload || !payload.stage) return; - - if (payload.stage === 'backup-start') { - setStatus('Auto-Backup wird erstellt...'); - showBackupRunOverlay('Auto-Backup wird erstellt...'); - showProgress(8, 'Auto-Backup wird erstellt...'); - } else if (payload.stage === 'backup-done') { - setStatus('Vorab-Backup erstellt. Upload startet...'); - showProgress(22, 'Vorab-Backup erstellt. Upload startet...'); - showInfo('Vorab-Backup OK, Upload startet...'); - } else if (payload.stage === 'upload-start') { - setStatus('Upload läuft...'); - showProgress(30, 'Upload läuft...'); - } else if (payload.stage === 'backup-failed') { - const errorMsg = payload.error ? `Auto-Backup fehlgeschlagen: ${payload.error}` : 'Auto-Backup fehlgeschlagen.'; - setStatus('Auto-Backup fehlgeschlagen'); - showError(errorMsg); - hideBackupRunOverlay(); - } - }); - } - if (window.electronAPI.onRetryQueueUpdated) { window.electronAPI.onRetryQueueUpdated((payload) => { const size = payload && typeof payload.size === 'number' ? payload.size : 0; @@ -4664,605 +5946,8 @@ window.addEventListener('DOMContentLoaded', async () => { initUpdater(); // Updater initialisieren updateNavigationUI(); - // ═══════════════════════════════════════════════════════════ - // ✅ BACKUP MANAGEMENT MODAL - EVENT HANDLERS - // ═══════════════════════════════════════════════════════════ - - // Button zum Öffnen des Backup-Modals - if ($('btnOpenBackupManagement')) { - $('btnOpenBackupManagement').onclick = async () => { - const modal = $('backupManagementModal'); - if (modal) modal.classList.remove('hidden'); - await loadBackupUiPrefs(); - await ensureBackupSourcesLoaded(); - populateBackupSourceOptions(); - const repoName = getActiveBackupRepoName(); - if (repoName) { - setStatus(`Backup-Verwaltung: ${repoName}`); - } - await refreshBackupAuthStatus(); - loadBackupList(); - }; - } - - // Schließen-Button im Modal - if ($('btnCloseBackupModal')) { - $('btnCloseBackupModal').onclick = () => { - const modal = $('backupManagementModal'); - if (modal) modal.classList.add('hidden'); - }; - } - - const localCredentialsSection = $('localCredentials'); - if (localCredentialsSection) { - localCredentialsSection.style.display = 'flex'; - } - - // 🚀 CREATE BACKUP NOW - Button - if ($('btnCreateBackupNow')) { - $('btnCreateBackupNow').onclick = async () => { - const source = getSelectedBackupSource(); - const repoName = source?.repoName || getActiveBackupRepoName(); - if (!repoName) { - showWarning('Kein Repository erkannt. Bitte zuerst ein Repository öffnen oder einen Projektordner wählen.'); - return; - } - - const provider = 'local'; - - const closeBackupModalForRun = () => { - const modal = $('backupManagementModal'); - if (modal) modal.classList.add('hidden'); - }; - - if (provider === 'local' && source && (source.kind === 'gitea-repo' || source.kind === 'gitea-all')) { - const destination = $('localBackupFolder')?.value?.trim() || ''; - if (!destination) { - showWarning('Bitte zuerst den Backup-Zielordner auswählen.'); - return; - } - - closeBackupModalForRun(); - setStatus('Lade Projekte von Git und sichere lokal...'); - const btn = $('btnCreateBackupNow'); - const oldText = btn.textContent; - btn.disabled = true; - showBackupRunOverlay('Lade Projekte von Git und sichere lokal...'); - showProgress(0, 'Lade Projekte von Git...'); - - try { - const res = await window.electronAPI.exportGiteaProjectsToLocal( - source.kind === 'gitea-all' - ? { mode: 'all', destination } - : { mode: 'single', owner: source.owner, repo: source.repo, destination } - ); - - if (res?.ok) { - updateBackupRunOverlay(100, 'Finalisiere Backup...'); - const reposCount = res.repositoryCount || (Array.isArray(res.repositories) ? res.repositories.length : 0); - showSuccess(`✓ Git-Backup abgeschlossen: ${reposCount} Projekte in ${destination}`); - setStatus('Komplett-Backup abgeschlossen'); - } else { - showError('Komplett-Backup fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler')); - } - } catch (error) { - showError('Komplett-Backup fehlgeschlagen: ' + (error?.message || String(error))); - } finally { - hideProgress(); - hideBackupRunOverlay(); - btn.disabled = false; - btn.textContent = oldText; - } - return; - } - - const sourcePath = source?.projectPath || selectedFolder || ''; - if (!sourcePath) { - showWarning('Bitte Backup-Quelle aus der Liste auswählen.'); - return; - } - - closeBackupModalForRun(); - setStatus('Erstelle Backup...'); - const btn = $('btnCreateBackupNow'); - const oldText = btn.textContent; - btn.disabled = true; - showBackupRunOverlay('Lokales Backup wird erstellt...'); - showProgress(0, 'Erstelle Backup...'); - - try { - const res = await window.electronAPI.createCloudBackup({ - repoName, - projectPath: sourcePath - }); - - if (res?.ok) { - updateBackupRunOverlay(100, 'Finalisiere Backup...'); - showSuccess('✓ Backup erfolgreich erstellt!'); - setStatus('Backup abgeschlossen'); - // Lade Backup-Liste neu - setTimeout(loadBackupList, 500); - } else { - showError('Backup fehlgeschlagen: ' + (res?.error || 'Unbekannter Fehler')); - } - } catch (error) { - showError('Fehler: ' + (error?.message || String(error))); - } finally { - hideProgress(); - hideBackupRunOverlay(); - btn.disabled = false; - btn.textContent = oldText; - } - }; - } - - // 🔄 REFRESH BACKUP LIST - Button - if ($('btnRefreshBackupsList')) { - $('btnRefreshBackupsList').onclick = loadBackupList; - } - - if ($('backupSourceSelect')) { - $('backupSourceSelect').addEventListener('change', async () => { - const key = $('backupSourceSelect')?.value || ''; - if (key) { - await saveBackupUiPrefs({ backupPrefSourceSelection: key }); - } - await refreshBackupAuthStatus(); - loadBackupList(); - }); - } - - if ($('localBackupFolder')) { - $('localBackupFolder').addEventListener('change', async (e) => { - const folder = e.target.value.trim(); - if (!folder) return; - await saveBackupUiPrefs({ backupPrefLocalFolder: folder }); - await refreshBackupAuthStatus(); - await loadBackupList(); - }); - } - - if ($('btnPickLocalBackupFolder')) { - $('btnPickLocalBackupFolder').onclick = async () => { - try { - const folder = await window.electronAPI.selectFolder(); - if (!folder) return; - const input = $('localBackupFolder'); - if (input) input.value = folder; - await saveBackupUiPrefs({ backupPrefLocalFolder: folder }); - await ensureBackupSourcesLoaded(); - populateBackupSourceOptions(); - await refreshBackupAuthStatus(); - await loadBackupList(); - } catch (error) { - showError('Ordnerauswahl fehlgeschlagen: ' + (error?.message || String(error))); - } - }; - } - - // 📋 Handler für Auto-Backup Toggle - if ($('settingAutoBackup')) { - $('settingAutoBackup').addEventListener('change', async (e) => { - try { - const enabled = e.target.checked; - const creds = await window.electronAPI.loadCredentials(); - if (creds) { - await window.electronAPI.saveCredentials({ - ...creds, - autoBackupEnabled: enabled - }); - showInfo(enabled ? '✓ Auto-Backup aktiviert' : '✓ Auto-Backup deaktiviert'); - } - } catch (error) { - console.error('Error toggling auto-backup:', error); - } - }); - } - - // Listener für Backup Created Events (von IPC) - if (window.electronAPI.onBackupCreated) { - window.electronAPI.onBackupCreated((data) => { - if (data?.ok) { - showSuccess(`📦 Backup erstellt: ${data.filename}`); - logActivity('info', `Backup erstellt: ${data.filename} (${data.size} bytes)`); - loadBackupList(); - } - }); - } - }); -// ═══════════════════════════════════════════════════════════ -// HELPER FUNCTIONS - BACKUP -// ═══════════════════════════════════════════════════════════ - -function getBackupCredentialsFromUI(provider) { - if (!provider) return null; - - const creds = {}; - let isValid = true; - - if (provider === 'local') { - creds.basePath = $('localBackupFolder')?.value?.trim(); - isValid = !!creds.basePath; - } - - return isValid ? creds : null; -} - -async function refreshBackupAuthStatus() { - const el = $('backupAuthStatus'); - if (!el) return; - - const repoName = getActiveBackupRepoName(); - const provider = 'local'; - - if (!repoName) { - el.textContent = 'Status: Kein Repository erkannt'; - el.style.color = 'var(--warning)'; - return; - } - - try { - const res = await window.electronAPI.getBackupAuthStatus({ repoName, provider }); - if (res?.ok && res.connected) { - const sourceLabel = res.source === 'repo' - ? 'projektbezogen' - : (res.source === 'global' ? 'global gespeichert' : 'aktiv'); - el.textContent = `Status: Verbunden (${sourceLabel})`; - el.style.color = 'var(--success)'; - } else { - el.textContent = 'Status: Nicht verbunden - einmalig verbinden erforderlich'; - el.style.color = 'var(--warning)'; - } - } catch (_) { - el.textContent = 'Status: Verbindung konnte nicht geprüft werden'; - el.style.color = 'var(--danger)'; - } -} - -function getActiveBackupRepoName() { - const selectedSource = getSelectedBackupSource(); - if (selectedSource && selectedSource.repoName) return selectedSource.repoName; - - if (currentState && currentState.repo) return currentState.repo; - const inputRepo = $('repoName')?.value?.trim(); - if (inputRepo) return inputRepo; - if (selectedFolder) { - const folderName = selectedFolder.split(/[\\/]/).pop(); - if (folderName) return folderName; - } - return ''; -} - -function getSelectedBackupSource() { - const key = $('backupSourceSelect')?.value || ''; - if (!key) return null; - - if (key === '__ALL_GIT__') { - return { - key, - kind: 'gitea-all', - repoName: 'all-git-projects', - label: 'Alles komplett sichern (Git)' - }; - } - - if (key.startsWith('repo::')) { - const payload = key.slice(6); - const [owner, repo] = payload.split('/'); - const name = repo || 'projekt'; - return { - key, - kind: 'gitea-repo', - owner, - repo, - repoName: name, - label: name - }; - } - - return null; -} - -async function ensureBackupSourcesLoaded() { - try { - const res = await window.electronAPI.listGiteaRepos(); - if (!res || !res.ok || !Array.isArray(res.repos)) { - backupGitRepos = []; - return; - } - - backupGitRepos = res.repos.map(r => ({ - owner: (r.owner && (r.owner.login || r.owner.username)) || '', - repo: r.name || '' - })).filter(r => r.owner && r.repo); - } catch (_) {} -} - -function populateBackupSourceOptions() { - const select = $('backupSourceSelect'); - if (!select) return; - - const prevValue = select.value || ''; - const prefValue = select.dataset.prefValue || ''; - const keepValue = prevValue || prefValue; - - const opts = []; - opts.push({ value: '', text: '-- Wähle aus vorhandenen Projekten --' }); - - if (backupGitRepos.length > 0) { - opts.push({ value: '__ALL_GIT__', text: '🧩 Alles komplett sichern (alle Git-Projekte)' }); - } - - const repos = [...backupGitRepos].sort((a, b) => `${a.owner}/${a.repo}`.localeCompare(`${b.owner}/${b.repo}`, 'de')); - repos.forEach(node => { - opts.push({ - value: `repo::${node.owner}/${node.repo}`, - text: `📦 ${node.owner}/${node.repo}` - }); - }); - - if (keepValue && keepValue.startsWith('repo::') && !opts.some(o => o.value === keepValue)) { - try { - const decoded = keepValue.slice(6); - opts.push({ value: keepValue, text: `📌 ${decoded} (zuletzt)` }); - } catch (_) {} - } - - select.innerHTML = opts - .map(o => ``) - .join(''); - - if (keepValue && opts.some(o => o.value === keepValue)) { - select.value = keepValue; - } else if (opts.length > 1) { - select.value = opts[1].value; - } else { - select.value = ''; - } -} - -async function loadBackupUiPrefs() { - try { - const creds = await window.electronAPI.loadCredentials(); - if (!creds) return; - - const localFolder = String(creds.backupPrefLocalFolder || '').trim(); - const sourceSelection = String(creds.backupPrefSourceSelection || '').trim(); - - const localFolderEl = $('localBackupFolder'); - if (localFolderEl && localFolder) { - localFolderEl.value = localFolder; - } - - const sourceSelectEl = $('backupSourceSelect'); - if (sourceSelectEl && sourceSelection) { - sourceSelectEl.dataset.prefValue = sourceSelection; - } - } catch (_) {} -} - -async function saveBackupUiPrefs(patch) { - try { - const creds = await window.electronAPI.loadCredentials(); - const current = creds || {}; - await window.electronAPI.saveCredentials({ ...current, ...patch }); - } catch (_) {} -} - -async function loadBackupList() { - const repoName = getActiveBackupRepoName(); - if (!repoName) { - const container = $('backupListContainer'); - if (container) { - container.innerHTML = '
Kein Repository erkannt
'; - } - return; - } - - const provider = 'local'; - const credentials = getBackupCredentialsFromUI(provider); - - // Optionaler Komfort: Wenn neue Credentials eingetragen sind, automatisch speichern. - if (credentials) { - try { - await window.electronAPI.setupBackupProvider({ repoName, provider, credentials }); - } catch (_) { - // Wenn kein vollständiges Formular vorhanden ist, versuchen wir mit gespeicherter Konfiguration weiter. - } - } - - setStatus('Lade Backup-Liste...'); - showProgress(0, 'Lade Backups...'); - - try { - const res = await window.electronAPI.listCloudBackups({ repoName, provider }); - - hideProgress(); - - const container = $('backupListContainer'); - if (!container) return; - - if (!res?.ok) { - container.innerHTML = `
Fehler beim Laden: ${res?.error || 'Unbekannt'}
`; - return; - } - - const backups = res.backups || []; - - if (backups.length === 0) { - container.innerHTML = '
Noch keine Backups vorhanden
'; - return; - } - - container.innerHTML = ''; - - const baseNameCounts = new Map(); - backups.forEach((b) => { - const name = getBackupDisplayName(b.filename || b.name || ''); - if (!name) return; - baseNameCounts.set(name, (baseNameCounts.get(name) || 0) + 1); - }); - - backups.forEach((backup, index) => { - const backupFilename = backup.filename || backup.name || ''; - const baseDisplayName = getBackupDisplayName(backupFilename) || `Backup ${index + 1}`; - const duplicateCount = baseNameCounts.get(baseDisplayName) || 0; - const displaySuffix = duplicateCount > 1 ? ` • ${formatTimeOnly(backup.modifiedTime || backup.date)}` : ''; - const backupDisplayName = `${baseDisplayName}${displaySuffix}`; - const item = document.createElement('div'); - item.className = 'backup-list-item'; - - const info = document.createElement('div'); - info.className = 'backup-list-info'; - - const filename = document.createElement('div'); - filename.textContent = backupDisplayName; - filename.title = backupFilename || backupDisplayName; - filename.className = 'backup-list-name'; - - const meta = document.createElement('div'); - meta.className = 'backup-list-meta'; - meta.textContent = `Größe: ${formatBytes(backup.size)} • ${new Date(backup.modifiedTime || new Date()).toLocaleString('de-DE')}`; - - info.appendChild(filename); - info.appendChild(meta); - - const buttons = document.createElement('div'); - buttons.className = 'backup-list-actions'; - - // Download-Button - const btnRestore = document.createElement('button'); - btnRestore.className = 'backup-list-action backup-list-action--restore'; - btnRestore.textContent = '⬇️ Wiederherstellen'; - btnRestore.onclick = async () => { - if (!backupFilename) { - showError('Backup-Dateiname fehlt.'); - return; - } - const proceedRestore = await showActionConfirmModal({ - title: 'Backup wiederherstellen', - message: `Backup "${backupFilename}" wirklich wiederherstellen? Vorhandene Dateien werden überschrieben.`, - confirmText: 'Wiederherstellen' - }); - if (!proceedRestore) return; - await restoreBackup(backupFilename, repoName); - }; - - // Delete-Button - const btnDelete = document.createElement('button'); - btnDelete.className = 'backup-list-action backup-list-action--delete'; - btnDelete.textContent = '🗑️ Löschen'; - btnDelete.onclick = async () => { - if (!backupFilename) { - showError('Backup-Dateiname fehlt.'); - return; - } - const proceedDelete = await showActionConfirmModal({ - title: 'Backup löschen', - message: `Backup "${backupFilename}" wirklich löschen?`, - confirmText: 'Löschen', - danger: true - }); - if (!proceedDelete) return; - await deleteBackup(backupFilename, repoName); - }; - - buttons.appendChild(btnRestore); - buttons.appendChild(btnDelete); - item.appendChild(info); - item.appendChild(buttons); - container.appendChild(item); - }); - - setStatus(`${backups.length} Backups gefunden`); - } catch (error) { - hideProgress(); - setStatus('Fehler beim Laden der Backups'); - const container = $('backupListContainer'); - if (container) container.innerHTML = `
Fehler: ${error?.message || 'Unbekannt'}
`; - } -} - -async function restoreBackup(filename, repoName) { - if (!selectedFolder) { - showWarning('Bitte zuerst einen lokalen Ordner auswählen'); - return; - } - - setStatus('Stelle Backup wieder her...'); - showProgress(0, `Lade ${filename}...`); - - try { - const res = await window.electronAPI.restoreCloudBackup({ - repoName, - filename, - targetPath: selectedFolder - }); - - hideProgress(); - - if (res?.ok) { - showSuccess(`✓ Backup "${filename}" wiederhergestellt`); - if (selectedFolder) refreshLocalTree(selectedFolder); - } else { - showError('Wiederherstellung fehlgeschlagen: ' + (res?.error || 'Unbekannt')); - } - } catch (error) { - hideProgress(); - showError('Fehler: ' + (error?.message || String(error))); - } -} - -async function deleteBackup(filename, repoName) { - setStatus('Lösche Backup...'); - - try { - const res = await window.electronAPI.deleteCloudBackup({ - repoName, - filename - }); - - if (res?.ok) { - showSuccess(`✓ Backup gelöscht`); - loadBackupList(); - } else { - showError('Löschen fehlgeschlagen: ' + (res?.error || 'Unbekannt')); - } - } catch (error) { - showError('Fehler: ' + (error?.message || String(error))); - } -} - -function formatBytes(bytes) { - if (!bytes || bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; -} - -function getBackupDisplayName(filename) { - const raw = String(filename || '').trim(); - if (!raw) return ''; - - const normalized = raw.replace(/\.zip$/i, ''); - const match = normalized.match(/^(.*?)-backup-\d{8}-\d{6}(?:-[^-]+)?$/i); - if (match && match[1]) { - return match[1]; - } - - return normalized; -} - -function formatTimeOnly(input) { - if (!input) return '--:--:--'; - const date = new Date(input); - if (Number.isNaN(date.getTime())) return '--:--:--'; - return date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); -} - /* ================================ RELEASE MANAGEMENT UI FUNCTIONS Füge dies zu renderer.js hinzu @@ -5279,11 +5964,12 @@ let currentReleaseView = { async function loadRepoReleases(owner, repo) { currentReleaseView.owner = owner; currentReleaseView.repo = repo; + currentReleaseView.platform = currentState.platform; setStatus('Loading releases...'); try { - const res = await window.electronAPI.listReleases({ owner, repo }); + const res = await window.electronAPI.listReleases({ owner, repo, platform: currentState.platform }); if (!res.ok) { showError('Error loading releases: ' + res.error); @@ -5294,36 +5980,37 @@ async function loadRepoReleases(owner, repo) { if (!grid) return; // Header mit "New Release" Button - grid.innerHTML = ` -
-

📦 Releases für ${repo}

- -
+ grid.innerHTML = ''; + const releaseHeader = document.createElement('div'); + releaseHeader.style.cssText = 'grid-column: 1/-1; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;'; + + const releaseTitle = document.createElement('h2'); + releaseTitle.style.cssText = 'margin: 0; color: var(--text-primary);'; + releaseTitle.textContent = `📦 Releases für ${repo}`; + + const newBtn = document.createElement('button'); + newBtn.className = 'btn-new-release'; + newBtn.style.cssText = ` + background: var(--accent-gradient); + color: #000; + border: none; + padding: 10px 20px; + border-radius: var(--radius-md); + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; `; - - // Event-Listener MUSS VOR innerHTML += gesetzt werden - const newBtn = grid.querySelector('.btn-new-release'); - if (newBtn) { - newBtn.onclick = () => { - console.log('New Release button clicked'); - showCreateReleaseModal(owner, repo); - }; - } else { - console.error('New Release button not found in DOM'); - } + newBtn.textContent = '🚀 New Release'; + newBtn.onclick = () => { + console.log('New Release button clicked'); + showCreateReleaseModal(owner, repo); + }; + + releaseHeader.appendChild(releaseTitle); + releaseHeader.appendChild(newBtn); + grid.appendChild(releaseHeader); if (!res.releases || res.releases.length === 0) { // WICHTIG: appendChild statt innerHTML +=, um Event-Listener zu erhalten @@ -5500,7 +6187,13 @@ function createReleaseCard(release, isLatest) { margin-left: 8px; `; deleteAssetBtn.onclick = async () => { - if (confirm(`Delete asset "${asset.name}"?`)) { + const ok = await showActionConfirmModal({ + title: 'Asset loeschen', + message: `Delete asset "${asset.name}"?`, + confirmText: 'Loeschen', + danger: true + }); + if (ok) { const res = await window.electronAPI.deleteReleaseAsset({ owner: currentReleaseView.owner, repo: currentReleaseView.repo, @@ -5550,10 +6243,12 @@ function createReleaseCard(release, isLatest) { day: 'numeric' }); - meta.innerHTML = ` - 📅 ${dateStr} - 👤 ${release.author?.login || 'Unknown'} - `; + const dateMeta = document.createElement('span'); + dateMeta.textContent = `📅 ${dateStr}`; + const authorMeta = document.createElement('span'); + authorMeta.textContent = `👤 ${release.author?.login || 'Unknown'}`; + meta.appendChild(dateMeta); + meta.appendChild(authorMeta); card.appendChild(meta); // Action Buttons @@ -5608,11 +6303,18 @@ function createReleaseCard(release, isLatest) { margin-left: auto; `; deleteBtn.onclick = async () => { - if (confirm(`Delete release "${release.name || release.tag_name}"?`)) { + const ok = await showActionConfirmModal({ + title: 'Release loeschen', + message: `Delete release "${release.name || release.tag_name}"?`, + confirmText: 'Loeschen', + danger: true + }); + if (ok) { const res = await window.electronAPI.deleteRelease({ owner: currentReleaseView.owner, repo: currentReleaseView.repo, - releaseId: release.id + releaseId: release.id, + platform: currentReleaseView.platform || currentState.platform }); if (res.ok) { card.remove(); @@ -5635,83 +6337,124 @@ function createReleaseCard(release, isLatest) { function showCreateReleaseModal(owner, repo) { const modal = document.createElement('div'); modal.className = 'modal'; - modal.innerHTML = ` -
-

🚀 Neues Release erstellen

- -
- - -
- -
- - -
- -
- - -
- -
- - -
+ const title = document.createElement('h2'); + title.textContent = '🚀 Neues Release erstellen'; + card.appendChild(title); - -
- -
- - -
-
- -
- - -
- - -
- `; + const tagGroup = document.createElement('div'); + tagGroup.className = 'input-group'; + const tagLabel = document.createElement('label'); + tagLabel.textContent = 'Tag Version *'; + const tagInput = document.createElement('input'); + tagInput.id = 'releaseTag'; + tagInput.type = 'text'; + tagInput.placeholder = 'v1.0.0'; + tagGroup.appendChild(tagLabel); + tagGroup.appendChild(tagInput); + card.appendChild(tagGroup); + + const nameGroup = document.createElement('div'); + nameGroup.className = 'input-group'; + const nameLabel = document.createElement('label'); + nameLabel.textContent = 'Release Name'; + const nameInput = document.createElement('input'); + nameInput.id = 'releaseName'; + nameInput.type = 'text'; + nameInput.placeholder = 'Version 1.0.0'; + nameGroup.appendChild(nameLabel); + nameGroup.appendChild(nameInput); + card.appendChild(nameGroup); + + const bodyGroup = document.createElement('div'); + bodyGroup.className = 'input-group'; + const bodyLabel = document.createElement('label'); + bodyLabel.textContent = 'Release Notes'; + const bodyInput = document.createElement('textarea'); + bodyInput.id = 'releaseBody'; + bodyInput.rows = 8; + bodyInput.placeholder = '## Was ist neu?\n\n- Feature 1\n- Feature 2\n- Bug Fixes'; + bodyInput.style.cssText = 'width: 100%; padding: 12px; border-radius: var(--radius-md); border: 1px solid rgba(255, 255, 255, 0.1); background: var(--bg-tertiary); color: var(--text-primary); font-family: monospace; resize: vertical;'; + bodyGroup.appendChild(bodyLabel); + bodyGroup.appendChild(bodyInput); + card.appendChild(bodyGroup); + + const targetGroup = document.createElement('div'); + targetGroup.className = 'input-group'; + const targetLabel = document.createElement('label'); + targetLabel.textContent = 'Target Branch'; + const targetInput = document.createElement('input'); + targetInput.id = 'releaseTarget'; + targetInput.type = 'text'; + targetInput.value = 'main'; + targetInput.placeholder = 'main'; + targetGroup.appendChild(targetLabel); + targetGroup.appendChild(targetInput); + card.appendChild(targetGroup); + + const assetGroup = document.createElement('div'); + assetGroup.className = 'input-group'; + const assetLabel = document.createElement('label'); + assetLabel.textContent = 'Release Asset (Optional)'; + const assetRow = document.createElement('div'); + assetRow.style.cssText = 'display: flex; gap: 10px;'; + const assetInput = document.createElement('input'); + assetInput.id = 'releaseAssetInput'; + assetInput.type = 'text'; + assetInput.readOnly = true; + assetInput.placeholder = 'Keine Datei gewählt'; + assetInput.style.cssText = 'flex: 1; padding: 10px; border-radius: var(--radius-md); border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(0, 0, 0, 0.2); color: var(--text-muted); cursor: not-allowed;'; + const assetBtn = document.createElement('button'); + assetBtn.id = 'btnSelectReleaseAsset'; + assetBtn.type = 'button'; + assetBtn.textContent = '📎 Datei wählen'; + assetBtn.style.cssText = 'padding: 10px 20px; border-radius: var(--radius-md); background: var(--bg-secondary); color: var(--text-primary); border: 1px solid rgba(255, 255, 255, 0.1); cursor: pointer; font-weight: 600;'; + assetRow.appendChild(assetInput); + assetRow.appendChild(assetBtn); + assetGroup.appendChild(assetLabel); + assetGroup.appendChild(assetRow); + card.appendChild(assetGroup); + + const optionsGroup = document.createElement('div'); + optionsGroup.className = 'input-group'; + optionsGroup.style.cssText = 'display: flex; gap: 20px;'; + + const preLabel = document.createElement('label'); + preLabel.style.cssText = 'display: flex; align-items: center; gap: 8px; cursor: pointer;'; + const preCheck = document.createElement('input'); + preCheck.type = 'checkbox'; + preCheck.id = 'releasePrerelease'; + preLabel.appendChild(preCheck); + preLabel.appendChild(document.createTextNode(' Pre-Release')); + + const draftLabel = document.createElement('label'); + draftLabel.style.cssText = 'display: flex; align-items: center; gap: 8px; cursor: pointer;'; + const draftCheck = document.createElement('input'); + draftCheck.type = 'checkbox'; + draftCheck.id = 'releaseDraft'; + draftLabel.appendChild(draftCheck); + draftLabel.appendChild(document.createTextNode(' Draft (nicht veröffentlichen)')); + + optionsGroup.appendChild(preLabel); + optionsGroup.appendChild(draftLabel); + card.appendChild(optionsGroup); + + const modalButtons = document.createElement('div'); + modalButtons.className = 'modal-buttons'; + const createBtn = document.createElement('button'); + createBtn.id = 'btnCreateRelease'; + createBtn.textContent = 'Erstellen & Veröffentlichen'; + const cancelBtn = document.createElement('button'); + cancelBtn.id = 'btnCancelRelease'; + cancelBtn.textContent = 'Abbrechen'; + modalButtons.appendChild(createBtn); + modalButtons.appendChild(cancelBtn); + card.appendChild(modalButtons); + + modal.appendChild(card); document.body.appendChild(modal); @@ -5731,7 +6474,7 @@ function showCreateReleaseModal(owner, repo) { } } catch (error) { console.error('Fehler beim Auswählen der Datei:', error); - alert('Konnte Dateidialog nicht öffnen.'); + await showInfoModal('Dateidialog', 'Konnte Dateidialog nicht oeffnen.', true); } }; @@ -5745,7 +6488,7 @@ function showCreateReleaseModal(owner, repo) { const draft = $('releaseDraft').checked; if (!tag) { - alert('Tag Version ist erforderlich!'); + await showInfoModal('Eingabe fehlt', 'Tag Version ist erforderlich!'); return; } @@ -5761,7 +6504,8 @@ function showCreateReleaseModal(owner, repo) { body, target_commitish: target, prerelease, - draft + draft, + platform: currentState.platform }); if (res.ok) { @@ -5784,12 +6528,12 @@ function showCreateReleaseModal(owner, repo) { setStatus(`Release "${tag}" und Asset erstellt!`); } else { console.error('Asset Upload fehlgeschlagen:', uploadRes.error); - alert(`Release erstellt, aber Asset Upload fehlgeschlagen: ${uploadRes.error}`); + await showInfoModal('Asset Upload fehlgeschlagen', `Release erstellt, aber Asset Upload fehlgeschlagen: ${uploadRes.error || 'Unbekannter Fehler'}`, true); showWarning('Release erstellt (Upload Fehler)'); } } catch (uploadErr) { console.error('Upload error:', uploadErr); - alert('Release erstellt, aber Fehler beim Hochladen der Datei.'); + await showInfoModal('Upload fehlgeschlagen', 'Release erstellt, aber Fehler beim Hochladen der Datei.', true); showWarning('Release erstellt (Upload Fehler)'); } finally { hideProgress(); @@ -5802,12 +6546,12 @@ function showCreateReleaseModal(owner, repo) { loadRepoReleases(owner, repo); // Liste neu laden } else { showError('Failed: ' + res.error); - alert('Fehler beim Erstellen des Releases: ' + res.error); + await showInfoModal('Release erstellen fehlgeschlagen', 'Fehler beim Erstellen des Releases: ' + (res.error || 'Unbekannter Fehler'), true); } } catch (error) { console.error('Create release error:', error); showError('Create failed'); - alert('Ein unerwarteter Fehler ist aufgetreten.'); + await showInfoModal('Unerwarteter Fehler', 'Ein unerwarteter Fehler ist aufgetreten.', true); } }; @@ -5898,6 +6642,7 @@ async function loadCommitHistory(owner, repo, branch = 'main') { currentCommitView.owner = owner; currentCommitView.repo = repo; currentCommitView.branch = branch; + currentCommitView.platform = currentState.platform; setStatus('Loading commit history...'); @@ -5906,7 +6651,8 @@ async function loadCommitHistory(owner, repo, branch = 'main') { owner, repo, branch, - limit: 100 + limit: 100, + platform: currentState.platform }); if (!res.ok) { @@ -5942,47 +6688,53 @@ function renderCommitHistoryView() { flex-wrap: wrap; `; - header.innerHTML = ` -

- 📊 Commit History - - ${currentCommitView.repo} / ${currentCommitView.branch} - -

- -
- - -
+ const title = document.createElement('h2'); + title.style.cssText = 'margin: 0; color: var(--text-primary); display: flex; align-items: center; gap: 12px;'; + title.textContent = '📊 Commit History'; + + const titleMeta = document.createElement('span'); + titleMeta.style.cssText = 'font-size: 14px; color: var(--text-muted); font-weight: 400;'; + titleMeta.textContent = `${currentCommitView.repo} / ${currentCommitView.branch}`; + title.appendChild(titleMeta); + + const searchWrap = document.createElement('div'); + searchWrap.style.cssText = 'display: flex; gap: 12px; flex: 1; max-width: 600px;'; + + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.id = 'commitSearch'; + searchInput.placeholder = '🔍 Search commits (message, author)...'; + searchInput.style.cssText = ` + flex: 1; + padding: 10px 16px; + border-radius: var(--radius-md); + border: 1px solid rgba(255, 255, 255, 0.1); + background: var(--bg-tertiary); + color: var(--text-primary); + font-size: 14px; `; + + const clearBtn = document.createElement('button'); + clearBtn.id = 'btnClearSearch'; + clearBtn.textContent = 'Clear'; + clearBtn.style.cssText = ` + padding: 10px 16px; + border-radius: var(--radius-md); + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid rgba(255, 255, 255, 0.1); + cursor: pointer; + font-weight: 600; + `; + + searchWrap.appendChild(searchInput); + searchWrap.appendChild(clearBtn); + header.appendChild(title); + header.appendChild(searchWrap); grid.appendChild(header); // Search-Handler - const searchInput = header.querySelector('#commitSearch'); - const clearBtn = header.querySelector('#btnClearSearch'); - let searchTimeout; searchInput.addEventListener('input', (e) => { clearTimeout(searchTimeout); @@ -6116,17 +6868,24 @@ function createCommitCard(commit, index) { const dateStr = formatRelativeTime(date); const sha = commit.sha?.substring(0, 7) || '???????'; - meta.innerHTML = ` - - 👤 ${author} - - - 🕐 ${dateStr} - - - #${sha} - - `; + const authorMeta = document.createElement('span'); + authorMeta.style.cssText = 'display: flex; align-items: center; gap: 6px;'; + authorMeta.appendChild(document.createTextNode('👤 ')); + const authorStrong = document.createElement('strong'); + authorStrong.textContent = author; + authorMeta.appendChild(authorStrong); + + const dateMeta = document.createElement('span'); + dateMeta.style.cssText = 'display: flex; align-items: center; gap: 6px;'; + dateMeta.textContent = `🕐 ${dateStr}`; + + const shaMeta = document.createElement('span'); + shaMeta.style.cssText = 'display: flex; align-items: center; gap: 6px; font-family: monospace; background: rgba(255,255,255,0.05); padding: 2px 8px; border-radius: 4px;'; + shaMeta.textContent = `#${sha}`; + + meta.appendChild(authorMeta); + meta.appendChild(dateMeta); + meta.appendChild(shaMeta); content.appendChild(meta); // Stats (if available) @@ -6140,11 +6899,21 @@ function createCommitCard(commit, index) { border-top: 1px solid rgba(255, 255, 255, 0.05); `; - stats.innerHTML = ` - +${commit.stats.additions || 0} - -${commit.stats.deletions || 0} - ${commit.stats.total || 0} changes - `; + const addStat = document.createElement('span'); + addStat.style.color = 'var(--success)'; + addStat.textContent = `+${commit.stats.additions || 0}`; + + const delStat = document.createElement('span'); + delStat.style.color = 'var(--danger)'; + delStat.textContent = `-${commit.stats.deletions || 0}`; + + const totalStat = document.createElement('span'); + totalStat.style.color = 'var(--text-muted)'; + totalStat.textContent = `${commit.stats.total || 0} changes`; + + stats.appendChild(addStat); + stats.appendChild(delStat); + stats.appendChild(totalStat); content.appendChild(stats); } @@ -6177,28 +6946,41 @@ async function showCommitDetails(commit) { const modal = document.createElement('div'); modal.className = 'modal commit-modal'; - modal.innerHTML = ` -
-
-

📋 Commit Details

- -
- -
-
-
- Loading commit details... -
-
-
-
- `; + const card = document.createElement('div'); + card.className = 'card'; + card.style.cssText = 'width: 90vw; max-width: 1200px; max-height: 90vh; overflow: hidden; display: flex; flex-direction: column;'; + + const header = document.createElement('div'); + header.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;'; + + const title = document.createElement('h2'); + title.style.margin = '0'; + title.textContent = '📋 Commit Details'; + + const closeBtn = document.createElement('button'); + closeBtn.id = 'btnCloseCommitModal'; + closeBtn.style.cssText = 'background: transparent; border: none; font-size: 24px; cursor: pointer; color: var(--text-muted);'; + closeBtn.textContent = '✕'; + + header.appendChild(title); + header.appendChild(closeBtn); + + const scroller = document.createElement('div'); + scroller.style.cssText = 'overflow-y: auto; flex: 1;'; + + const detailsContent = document.createElement('div'); + detailsContent.id = 'commitDetailsContent'; + detailsContent.style.paddingBottom = '20px'; + + const loading = document.createElement('div'); + loading.style.cssText = 'text-align: center; padding: 40px; color: var(--text-muted);'; + loading.textContent = 'Loading commit details...'; + + detailsContent.appendChild(loading); + scroller.appendChild(detailsContent); + card.appendChild(header); + card.appendChild(scroller); + modal.appendChild(card); document.body.appendChild(modal); @@ -6229,17 +7011,19 @@ async function loadCommitDetailsContent(commit) { diffRes = { ok: true, diff: details?.diff || '' }; filesRes = { ok: true, files: details?.fileChanges?.files || [], stats: { additions: details?.fileChanges?.insertions || 0, deletions: details?.fileChanges?.deletions || 0 } }; } else { - // Gitea repository + // Remote repository (Gitea or GitHub) const [diff, files] = await Promise.all([ window.electronAPI.getCommitDiff({ owner: currentCommitView.owner, repo: currentCommitView.repo, - sha: commit.sha + sha: commit.sha, + platform: currentCommitView.platform || currentState.platform }), window.electronAPI.getCommitFiles({ owner: currentCommitView.owner, repo: currentCommitView.repo, - sha: commit.sha + sha: commit.sha, + platform: currentCommitView.platform || currentState.platform }) ]); @@ -6264,14 +7048,34 @@ async function loadCommitDetailsContent(commit) { const date = new Date(commit.commit?.author?.date || commit.created_at); const sha = commit.sha || ''; - header.innerHTML = ` -

${escapeHtml(message)}

-
- 👤 ${escapeHtml(author)} ${email ? `<${escapeHtml(email)}>` : ''} - 🕐 ${date.toLocaleString()} - ${sha.substring(0, 7)} -
- `; + const titleEl = document.createElement('h3'); + titleEl.style.cssText = 'margin: 0 0 16px 0; font-size: 20px; line-height: 1.4;'; + titleEl.textContent = message; + + const metaRow = document.createElement('div'); + metaRow.style.cssText = 'display: flex; gap: 24px; font-size: 14px; color: var(--text-muted); flex-wrap: wrap;'; + + const authorEl = document.createElement('span'); + authorEl.appendChild(document.createTextNode('👤 ')); + const authorStrong = document.createElement('strong'); + authorStrong.textContent = author; + authorEl.appendChild(authorStrong); + if (email) { + authorEl.appendChild(document.createTextNode(` <${email}>`)); + } + + const dateEl = document.createElement('span'); + dateEl.textContent = `🕐 ${date.toLocaleString()}`; + + const shaEl = document.createElement('span'); + shaEl.style.cssText = 'font-family: monospace; background: rgba(255,255,255,0.05); padding: 4px 12px; border-radius: 6px;'; + shaEl.textContent = sha.substring(0, 7); + + metaRow.appendChild(authorEl); + metaRow.appendChild(dateEl); + metaRow.appendChild(shaEl); + header.appendChild(titleEl); + header.appendChild(metaRow); container.appendChild(header); // File changes summary @@ -6284,15 +7088,25 @@ async function loadCommitDetailsContent(commit) { margin-bottom: 16px; `; - filesHeader.innerHTML = ` -

- 📁 Changed Files (${filesRes.files.length}) -

-
- +${filesRes.stats?.additions || 0} - -${filesRes.stats?.deletions || 0} -
- `; + const filesTitle = document.createElement('h4'); + filesTitle.style.cssText = 'margin: 0; display: flex; align-items: center; gap: 8px;'; + filesTitle.textContent = `📁 Changed Files (${filesRes.files.length})`; + + const filesStats = document.createElement('div'); + filesStats.style.cssText = 'display: flex; gap: 16px; font-size: 13px;'; + + const additionsEl = document.createElement('span'); + additionsEl.style.color = 'var(--success)'; + additionsEl.textContent = `+${filesRes.stats?.additions || 0}`; + + const deletionsEl = document.createElement('span'); + deletionsEl.style.color = 'var(--danger)'; + deletionsEl.textContent = `-${filesRes.stats?.deletions || 0}`; + + filesStats.appendChild(additionsEl); + filesStats.appendChild(deletionsEl); + filesHeader.appendChild(filesTitle); + filesHeader.appendChild(filesStats); container.appendChild(filesHeader); // File list @@ -6317,13 +7131,25 @@ async function loadCommitDetailsContent(commit) { const icon = changeType === 'added' ? '🆕' : changeType === 'deleted' ? '🗑️' : '📝'; - fileItem.innerHTML = ` - ${icon} ${escapeHtml(file.file)} - - +${file.insertions} - -${file.deletions} - - `; + const fileNameEl = document.createElement('span'); + fileNameEl.style.cssText = 'font-family: monospace; color: var(--text-primary);'; + fileNameEl.textContent = `${icon} ${String(file.file || '')}`; + + const fileStatsEl = document.createElement('span'); + fileStatsEl.style.color = 'var(--text-muted)'; + + const fileAddEl = document.createElement('span'); + fileAddEl.style.color = 'var(--success)'; + fileAddEl.textContent = `+${file.insertions}`; + + const fileDelEl = document.createElement('span'); + fileDelEl.style.cssText = 'color: var(--danger); margin-left: 8px;'; + fileDelEl.textContent = `-${file.deletions}`; + + fileStatsEl.appendChild(fileAddEl); + fileStatsEl.appendChild(fileDelEl); + fileItem.appendChild(fileNameEl); + fileItem.appendChild(fileStatsEl); fileList.appendChild(fileItem); }); @@ -6365,13 +7191,20 @@ async function loadCommitDetailsContent(commit) { const errorMsg = error.message || String(error); const isLocalGit = selectedFolder ? 'Local Git' : 'Gitea'; - container.innerHTML = `
-

❌ Error loading commit details

-

- Source: ${isLocalGit}
- Error: ${escapeHtml(errorMsg)} -

-
`; + container.innerHTML = ''; + const errorWrap = document.createElement('div'); + errorWrap.style.cssText = 'text-align: center; padding: 40px; color: var(--danger);'; + + const errorTitle = document.createElement('p'); + errorTitle.textContent = '❌ Error loading commit details'; + + const errorDetails = document.createElement('p'); + errorDetails.style.cssText = 'font-size: 12px; color: var(--text-muted); margin-top: 12px;'; + errorDetails.textContent = `Source: ${isLocalGit} | Error: ${errorMsg}`; + + errorWrap.appendChild(errorTitle); + errorWrap.appendChild(errorDetails); + container.appendChild(errorWrap); } } @@ -6419,7 +7252,8 @@ async function handleCommitSearch(query) { owner: currentCommitView.owner, repo: currentCommitView.repo, branch: currentCommitView.branch, - query: query.trim() + query: query.trim(), + platform: currentCommitView.platform || currentState.platform }); if (res.ok) { @@ -6455,9 +7289,12 @@ function formatRelativeTime(date) { } function escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; + return String(text ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); } /* ------------------------- @@ -6612,7 +7449,7 @@ async function initUpdater() { btn.disabled = true; try { - await window.electronAPI.checkForUpdates(); + await window.electronAPI.checkForUpdates({ silent: false }); setStatus('Update-Suche abgeschlossen'); } catch (error) { setStatus('Fehler bei der Update-Prüfung'); @@ -6656,15 +7493,20 @@ if (window.electronAPI.onUpdateAvailable) { }); } +if (window.electronAPI.onUpdateNotAvailable) { + window.electronAPI.onUpdateNotAvailable(() => { + showInfo('Du nutzt bereits die aktuelle Version.'); + }); +} + // AM ENDE DER DATEI: Initialisierung beim Start document.addEventListener('DOMContentLoaded', () => { // 1. Basis-Setup (Settings-Feld füllen etc.) initUpdater(); // 2. AUTOMATISCHER UPDATE-CHECK BEIM START - // Wir warten 3 Sekunden, damit die App in Ruhe laden kann + // Etwas spaeter, damit initiales UI/Repo-Laden nicht ausgebremst wird. setTimeout(() => { - console.log("[Auto-Updater] Suche im Hintergrund nach Updates..."); - window.electronAPI.checkForUpdates(); - }, 3000); + window.electronAPI.checkForUpdates({ silent: true }); + }, 12000); }); \ No newline at end of file diff --git a/renderer/style.css b/renderer/style.css index f9595db..547567e 100644 --- a/renderer/style.css +++ b/renderer/style.css @@ -789,9 +789,11 @@ body { display: flex; flex-direction: column; gap: 6px; - max-height: calc(100vh - 270px); + flex: 1; + min-height: 0; overflow: auto; padding-right: 2px; + padding-bottom: 8px; } .fav-history-list::-webkit-scrollbar { @@ -857,10 +859,6 @@ body { width: 180px; margin: 12px 0 12px 12px; } - - .fav-history-list { - max-height: 180px; - } } /* Global Drop Zone Indicator */ @@ -1047,6 +1045,14 @@ body.compact-mode .item-card:hover { justify-content: center; } +.repo-avatar-img { + width: 52px; + height: 52px; + border-radius: 10px; + object-fit: cover; + display: block; +} + .item-card:hover .item-icon { transform: translateY(-2px) scale(1.08) rotate(-4deg); } @@ -1065,6 +1071,16 @@ body.compact-mode .item-card:hover { color: var(--accent-primary); } +.item-submeta { + margin-top: 4px; + font-size: 11px; + color: var(--text-muted); + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + /* =========================== MODALS =========================== */ @@ -1310,8 +1326,9 @@ input[type="checkbox"] { border-color: rgba(255, 255, 255, 0.3); } -#btnTestGiteaConnection { - margin-top: 10px; +#btnTestGiteaConnection, +#btnTestGithubConnection { + margin-top: 0; min-height: 40px; border-radius: var(--radius-md); padding: 0 14px; @@ -1325,18 +1342,21 @@ input[type="checkbox"] { transition: transform var(--transition-normal), box-shadow var(--transition-normal), border-color var(--transition-normal), background var(--transition-normal); } -#btnTestGiteaConnection:hover { +#btnTestGiteaConnection:hover, +#btnTestGithubConnection:hover { transform: translateY(-1px); border-color: var(--accent-primary); background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(0, 212, 255, 0.14) 100%); box-shadow: var(--shadow-md); } -#btnTestGiteaConnection:active { +#btnTestGiteaConnection:active, +#btnTestGithubConnection:active { transform: translateY(0); } -#btnTestGiteaConnection:focus-visible { +#btnTestGiteaConnection:focus-visible, +#btnTestGithubConnection:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.2); } @@ -1432,6 +1452,81 @@ input[type="checkbox"] { margin-bottom: 14px; } +.settings-header-inner { + display: flex; + align-items: center; + gap: 16px; +} + +/* Avatar-Upload-Button */ +.settings-avatar-upload-btn { + display: block; + margin-top: 6px; + padding: 4px 10px; + font-size: 11px; + border-radius: var(--radius); + border: 1px solid rgba(88, 213, 255, 0.3); + background: rgba(88, 213, 255, 0.08); + color: var(--accent-primary); + cursor: pointer; + white-space: nowrap; + transition: background 0.15s, border-color 0.15s; +} +.settings-avatar-upload-btn:hover:not(:disabled) { + background: rgba(88, 213, 255, 0.18); + border-color: rgba(88, 213, 255, 0.6); +} +.settings-avatar-upload-btn:disabled { + opacity: 0.6; + cursor: wait; +} + +/* Avatar-Picker */ +.settings-avatar-wrap { + position: relative; + width: 72px; + height: 72px; + flex-shrink: 0; + border-radius: 50%; + cursor: pointer; + overflow: hidden; + border: 2px solid rgba(88, 213, 255, 0.35); + background: rgba(88, 213, 255, 0.08); + transition: border-color 0.2s; +} +.settings-avatar-wrap:hover { + border-color: rgba(88, 213, 255, 0.7); +} +.settings-avatar-img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.settings-avatar-placeholder { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 32px; + line-height: 1; +} +.settings-avatar-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.55); + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + opacity: 0; + transition: opacity 0.2s; +} +.settings-avatar-wrap:hover .settings-avatar-overlay { + opacity: 1; +} + .settings-eyebrow { display: inline-flex; align-items: center; @@ -1530,6 +1625,66 @@ input[type="checkbox"] { margin-bottom: 10px; } +.settings-credentials-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.settings-auth-card { + display: grid; + gap: 8px; + padding: 12px; + min-height: 252px; + border-radius: var(--radius-md); + border: 1px solid rgba(255, 255, 255, 0.08); + background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01)); +} + +.settings-auth-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 4px; +} + +.settings-auth-card-header button { + min-width: 170px; +} + +.settings-auth-card-header h4 { + margin: 0; + color: #c8d8f2; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.settings-auth-input { + margin-bottom: 0; +} + +.settings-auth-card .settings-inline-hint { + min-height: 34px; + margin-top: auto; +} + +.settings-auth-spacer { + min-height: 78px; + border: 1px dashed rgba(255, 255, 255, 0.1); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-muted); + font-size: 12px; + text-align: center; + padding: 8px; + background: rgba(255, 255, 255, 0.01); +} + .settings-connection-tools { display: grid; grid-template-columns: minmax(0, 1fr) auto; @@ -1776,6 +1931,56 @@ input[type="checkbox"] { transition: border-color 140ms ease, background 140ms ease, color 140ms ease, transform 140ms ease; } +.repo-owner-tabs { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.repo-owner-tab { + appearance: none; + border: 1px solid rgba(255, 255, 255, 0.18); + background: rgba(255, 255, 255, 0.04); + color: var(--text-secondary); + border-radius: 999px; + min-height: 32px; + padding: 0 12px; + font-size: 12px; + font-weight: 700; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.repo-owner-tab span { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 20px; + height: 20px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.08); + font-size: 11px; + color: var(--text-muted); +} + +.repo-owner-tab:hover { + border-color: rgba(88, 213, 255, 0.45); + color: #d9f7ff; +} + +.repo-owner-tab.active { + border-color: rgba(88, 213, 255, 0.65); + background: linear-gradient(135deg, rgba(88, 213, 255, 0.2), rgba(92, 135, 255, 0.18)); + color: #ecf8ff; +} + +.repo-owner-tab.active span { + background: rgba(88, 213, 255, 0.22); + color: #dff6ff; +} + .repo-search-clear:hover { border-color: rgba(88, 213, 255, 0.45); background: linear-gradient(180deg, rgba(88, 213, 255, 0.18) 0%, rgba(88, 213, 255, 0.10) 100%); @@ -1959,6 +2164,97 @@ input[type="checkbox"] { background: rgba(255, 255, 255, 0.12); } +.tags-editor-selected { + display: flex; + flex-wrap: wrap; + gap: 8px; + min-height: 38px; + padding: 8px; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 10px; + background: rgba(255, 255, 255, 0.03); +} + +.tags-editor-row { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 8px; + margin-top: 8px; + align-items: center; +} + +.tags-editor-input { + width: 100%; + min-height: 38px; + padding: 0 12px; + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(10, 18, 30, 0.95); + color: var(--text-primary); +} + +.tags-editor-add-btn { + min-height: 38px; + white-space: nowrap; + padding: 0 14px; + border-radius: 10px; + border: 1px solid rgba(88, 213, 255, 0.36); + background: linear-gradient(135deg, rgba(88, 213, 255, 0.24), rgba(92, 135, 255, 0.22)); + color: #c8f5ff; + font-weight: 700; + cursor: pointer; +} + +.tags-editor-add-btn:hover { + border-color: rgba(88, 213, 255, 0.62); + background: linear-gradient(135deg, rgba(88, 213, 255, 0.34), rgba(92, 135, 255, 0.31)); +} + +.tags-editor-suggestions { + margin-top: 8px; + max-height: 170px; + overflow: auto; + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 10px; + background: rgba(10, 18, 30, 0.95); +} + +.tags-editor-suggestion-item { + display: block; + width: 100%; + text-align: left; + padding: 9px 12px; + border: 0; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + background: transparent; + color: var(--text-primary); + cursor: pointer; + font-size: 13px; +} + +.tags-editor-suggestion-item:hover { + background: rgba(255, 255, 255, 0.07); +} + +.tags-editor-suggestions::-webkit-scrollbar { + width: 10px; +} + +.tags-editor-suggestions::-webkit-scrollbar-track { + background: rgba(6, 12, 22, 0.85); + border-left: 1px solid rgba(255, 255, 255, 0.08); +} + +.tags-editor-suggestions::-webkit-scrollbar-thumb { + background: linear-gradient(180deg, rgba(88, 213, 255, 0.45), rgba(92, 135, 255, 0.45)); + border-radius: 999px; + border: 2px solid rgba(6, 12, 22, 0.9); +} + +.tags-editor-suggestions::-webkit-scrollbar-thumb:hover { + background: linear-gradient(180deg, rgba(88, 213, 255, 0.68), rgba(92, 135, 255, 0.68)); +} + .activity-heatmap-months { margin-left: calc(var(--hm-weekday-col) + var(--hm-grid-gap)); width: max-content; @@ -3165,6 +3461,14 @@ progress::-moz-progress-bar { .settings-column { gap: 12px; } + + .settings-credentials-grid { + grid-template-columns: minmax(0, 1fr); + } + + .settings-auth-card { + min-height: 0; + } } @media (max-width: 760px) { @@ -3179,6 +3483,7 @@ progress::-moz-progress-bar { } .settings-fields-grid, + .settings-credentials-grid, .settings-connection-tools, .settings-version-card, .modal-buttons { @@ -3186,7 +3491,8 @@ progress::-moz-progress-bar { } .settings-update-btn, - #btnTestGiteaConnection { + #btnTestGiteaConnection, + #btnTestGithubConnection { width: 100%; } @@ -3292,413 +3598,4 @@ body.compact-mode .file-type-badge { .fav-chip[draggable="true"] { cursor: grab; -} - -/* ═══════════════════════════════════════════════════════════ - ✅ BACKUP MANAGEMENT MODAL - STYLES - ═══════════════════════════════════════════════════════════ */ - -#backupManagementModal { - display: flex; - align-items: center; - justify-content: center; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.7); - z-index: 99999; - backdrop-filter: blur(4px); -} - -#backupManagementModal.hidden { - display: none; -} - -.backup-management-card { - background: var(--bg-secondary); - border: 1px solid rgba(88, 213, 255, 0.2); - border-radius: var(--radius-lg); - width: 90%; - max-width: 700px; - max-height: 85vh; - display: flex; - flex-direction: column; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); - animation: slideUp 0.3s ease-out; - overflow: hidden; -} - -.backup-modal-header { - padding: 20px; - border-bottom: 1px solid rgba(255, 255, 255, 0.1); - display: flex; - justify-content: space-between; - align-items: center; -} - -.backup-modal-header h2 { - font-size: 18px; - font-weight: 700; - margin: 0; - color: var(--text-primary); -} - -.backup-modal-close { - background: transparent; - border: none; - color: var(--text-muted); - font-size: 20px; - cursor: pointer; - padding: 0; - width: 32px; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 6px; - transition: all var(--transition-fast); -} - -.backup-modal-close:hover { - background: rgba(255, 255, 255, 0.1); - color: var(--accent-primary); -} - -.backup-modal-body { - flex: 1; - overflow-y: auto; - padding: 20px; - display: flex; - flex-direction: column; - gap: 16px; -} - -.backup-modal-body::-webkit-scrollbar { - width: 10px; -} - -.backup-modal-body::-webkit-scrollbar-track { - background: rgba(9, 20, 39, 0.75); - border-left: 1px solid rgba(88, 213, 255, 0.12); -} - -.backup-modal-body::-webkit-scrollbar-thumb { - background: linear-gradient(180deg, rgba(88, 213, 255, 0.55), rgba(92, 135, 255, 0.55)); - border-radius: 10px; - border: 2px solid rgba(9, 20, 39, 0.9); -} - -.backup-modal-body::-webkit-scrollbar-thumb:hover { - background: linear-gradient(180deg, rgba(88, 213, 255, 0.8), rgba(92, 135, 255, 0.8)); -} - -/* Provider Selection */ -.backup-provider-select { - display: flex; - flex-direction: column; - gap: 8px; -} - -.backup-provider-select label { - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--accent-primary); -} - -#backupProviderSelect { - padding: 10px 12px; - background: var(--bg-tertiary); - border: 1px solid rgba(88, 213, 255, 0.25); - border-radius: var(--radius-md); - color: var(--text-primary); - font-size: 13px; - font-family: inherit; - cursor: pointer; - transition: all var(--transition-fast); -} - -#backupProviderSelect:hover { - border-color: rgba(88, 213, 255, 0.4); - background: rgba(19, 33, 58, 0.8); -} - -#backupProviderSelect:focus { - outline: none; - border-color: var(--accent-primary); - box-shadow: 0 0 0 3px rgba(88, 213, 255, 0.15); - background: rgba(19, 33, 58, 0.9); -} - -#backupProviderSelect option { - background: var(--bg-secondary); - color: var(--text-primary); - padding: 8px; -} - -/* Credentials Sections */ -.backup-credentials-section { - display: none; - padding: 12px; - background: linear-gradient(135deg, rgba(88, 213, 255, 0.08) 0%, rgba(122, 81, 255, 0.05) 100%); - border: 1px solid rgba(88, 213, 255, 0.2); - border-radius: var(--radius-md); - gap: 12px; - flex-direction: column; - animation: slideDown 0.2s ease-out; -} - -@keyframes slideDown { - from { - opacity: 0; - transform: translateY(-8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.backup-credentials-section.active { - display: flex; -} - -.backup-credentials-section input, -.backup-credentials-section textarea, -.backup-credentials-section select { - padding: 9px 10px; - background: rgba(0, 0, 0, 0.3); - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 6px; - color: var(--text-primary); - font-size: 12px; - font-family: inherit; - transition: all var(--transition-fast); -} - -.backup-credentials-section input::placeholder { - color: rgba(174, 189, 216, 0.35); -} - -.backup-credentials-section input:focus, -.backup-credentials-section textarea:focus, -.backup-credentials-section select:focus { - outline: none; - border-color: var(--accent-primary); - box-shadow: 0 0 0 3px rgba(88, 213, 255, 0.12); - background: rgba(0, 0, 0, 0.4); -} - -.backup-credentials-section select option { - background: var(--bg-secondary); - color: var(--text-primary); -} - -.backup-input-group { - display: flex; - flex-direction: column; - gap: 6px; -} - -.backup-input-group label { - font-size: 11px; - font-weight: 600; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.3px; -} - -/* Buttons */ -.backup-modal-buttons { - display: flex; - gap: 8px; - flex-wrap: wrap; - justify-content: center; - padding: 16px; - border-top: 1px solid rgba(255, 255, 255, 0.1); -} - -.backup-btn { - padding: 10px 16px; - border-radius: var(--radius-md); - border: none; - font-size: 12px; - font-weight: 600; - cursor: pointer; - transition: all var(--transition-fast); - min-width: 120px; - display: flex; - align-items: center; - justify-content: center; - gap: 6px; - font-family: inherit; -} - -.backup-btn-primary { - background: var(--accent-gradient); - color: #000; - box-shadow: 0 4px 12px rgba(88, 213, 255, 0.25); -} - -.backup-btn-primary:hover { - box-shadow: 0 8px 20px rgba(88, 213, 255, 0.35); - transform: translateY(-2px); -} - -.backup-btn-primary:active { - transform: translateY(0); - box-shadow: 0 2px 8px rgba(88, 213, 255, 0.25); -} - -.backup-btn-secondary { - background: transparent; - border: 1.5px solid rgba(88, 213, 255, 0.35); - color: var(--text-primary); - transition: all var(--transition-fast); -} - -.backup-btn-secondary:hover { - background: rgba(88, 213, 255, 0.12); - border-color: var(--accent-primary); - color: var(--accent-primary); - box-shadow: 0 4px 12px rgba(88, 213, 255, 0.15); - transform: translateY(-1px); -} - -.backup-btn-secondary:active { - transform: translateY(0); -} - -.backup-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none !important; -} - -/* Backup List */ -#backupListContainer { - background: linear-gradient(135deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0.15) 100%); - border: 1px solid rgba(88, 213, 255, 0.15); - border-radius: var(--radius-md); - max-height: 250px; - overflow-y: auto; - min-height: 100px; - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; -} - -.backup-list-item { - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; - padding: 12px; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - transition: background var(--transition-fast); - width: 100%; -} - -.backup-list-info { - flex: 1 1 auto; - min-width: 0; -} - -.backup-list-name { - font-weight: 600; - margin-bottom: 4px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.backup-list-meta { - color: var(--text-muted); - font-size: 12px; -} - -.backup-list-actions { - flex: 0 0 auto; - display: flex; - gap: 8px; - align-items: center; -} - -.backup-list-action { - appearance: none; - border-radius: 8px; - padding: 7px 12px; - font-size: 12px; - font-weight: 600; - border: 1px solid rgba(88, 213, 255, 0.28); - background: linear-gradient(180deg, rgba(20, 34, 58, 0.92), rgba(12, 24, 44, 0.92)); - color: var(--text-primary); - cursor: pointer; - transition: all var(--transition-fast); - white-space: nowrap; -} - -.backup-list-action:hover { - border-color: rgba(88, 213, 255, 0.6); - background: linear-gradient(180deg, rgba(30, 52, 86, 0.94), rgba(15, 30, 56, 0.94)); -} - -.backup-list-action--delete { - color: #ff9f9f; - border-color: rgba(239, 68, 68, 0.35); -} - -.backup-list-action--delete:hover { - color: #ffd6d6; - border-color: rgba(239, 68, 68, 0.65); - background: linear-gradient(180deg, rgba(72, 24, 24, 0.88), rgba(48, 14, 14, 0.9)); -} - -@media (max-width: 760px) { - .backup-list-item { - flex-direction: column; - align-items: stretch; - } - - .backup-list-actions { - width: 100%; - justify-content: flex-end; - flex-wrap: wrap; - } - - .backup-list-action { - flex: 1 1 auto; - text-align: center; - min-width: 130px; - } -} - -.backup-list-item:hover { - background: rgba(88, 213, 255, 0.1); -} - -.backup-list-item:last-child { - border-bottom: none; -} - -/* Scrollbar Styling */ -#backupListContainer::-webkit-scrollbar { - width: 8px; -} - -#backupListContainer::-webkit-scrollbar-track { - background: transparent; -} - -#backupListContainer::-webkit-scrollbar-thumb { - background: rgba(88, 213, 255, 0.2); - border-radius: 4px; -} - -#backupListContainer::-webkit-scrollbar-thumb:hover { - background: rgba(88, 213, 255, 0.4); } \ No newline at end of file