diff --git a/renderer/index.html b/renderer/index.html index fe12e5f..b7d3557 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -54,6 +54,28 @@ +
+ +
+ + + + +
+
+
diff --git a/renderer/renderer.js b/renderer/renderer.js index 070bd32..7dfb6c6 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -4,6 +4,293 @@ const $ = id => document.getElementById(id); let selectedFolder = null; let giteaCache = {}; +/* ================================================ + FAVORITEN & ZULETZT GEÖFFNET — State & Helpers + ================================================ */ + +let favorites = []; // [{ owner, repo, cloneUrl, addedAt }] +let recentRepos = []; // [{ owner, repo, cloneUrl, openedAt }] + +// Feature-Flags (aus Settings, persistent via Credentials) +let featureFavorites = true; +let featureRecent = true; +let compactMode = false; + +async function loadFavoritesAndRecent() { + try { + const [favRes, recRes] = await Promise.all([ + window.electronAPI.loadFavorites(), + window.electronAPI.loadRecent() + ]); + if (favRes && favRes.ok) favorites = favRes.favorites || []; + if (recRes && recRes.ok) recentRepos = recRes.recent || []; + } catch(e) { + console.error('loadFavoritesAndRecent:', e); + } +} + +function isFavorite(owner, repo) { + return favorites.some(f => f.owner === owner && f.repo === repo); +} + +async function toggleFavorite(owner, repo, cloneUrl) { + if (isFavorite(owner, repo)) { + favorites = favorites.filter(f => !(f.owner === owner && f.repo === repo)); + } else { + favorites.unshift({ owner, repo, cloneUrl, addedAt: new Date().toISOString() }); + } + await window.electronAPI.saveFavorites(favorites); +} + +async function addToRecent(owner, repo, cloneUrl) { + if (!featureRecent) return; + recentRepos = recentRepos.filter(r => !(r.owner === owner && r.repo === repo)); + recentRepos.unshift({ owner, repo, cloneUrl, openedAt: new Date().toISOString() }); + recentRepos = recentRepos.slice(0, 20); + await window.electronAPI.saveRecent(recentRepos); +} + +function formatRelDate(iso) { + if (!iso) return ''; + const diff = Date.now() - new Date(iso).getTime(); + const m = Math.floor(diff / 60000); + const h = Math.floor(diff / 3600000); + const d = Math.floor(diff / 86400000); + if (m < 1) return 'Gerade eben'; + if (m < 60) return `vor ${m} Min.`; + if (h < 24) return `vor ${h} Std.`; + if (d < 7) return `vor ${d} Tag${d > 1 ? 'en' : ''}`; + return new Date(iso).toLocaleDateString('de-DE'); +} + +/* Rendert Favoriten + Zuletzt-geöffnet-Bereich in ein beliebiges Container-Element */ +// Collapse-Zustand (wird in Credentials persistiert) +const favSectionCollapsed = { favorites: false, recent: false }; + +function makeFavSectionBlock(type, allRepos) { + const isFav = type === 'favorites'; + const icon = isFav ? '⭐' : '🕐'; + const label = isFav ? 'Favoriten' : 'Zuletzt geöffnet'; + + const sec = document.createElement('div'); + sec.style.cssText = `margin-bottom: ${isFav ? '20' : '24'}px;`; + + // ── Header (klickbar) ────────────────────────────── + const hdr = document.createElement('div'); + hdr.className = 'fav-section-header fav-section-header--toggle'; + + const iconEl = document.createElement('span'); + iconEl.className = 'fav-section-icon'; + if (isFav) iconEl.style.color = '#f59e0b'; + iconEl.textContent = icon; + + const labelEl = document.createElement('span'); + labelEl.textContent = label; + + const arrow = document.createElement('span'); + arrow.className = 'fav-collapse-arrow'; + arrow.textContent = favSectionCollapsed[type] ? '▶' : '▼'; + + hdr.appendChild(iconEl); + hdr.appendChild(labelEl); + hdr.appendChild(arrow); + sec.appendChild(hdr); + + // ── Inhalt ──────────────────────────────────────── + const row = document.createElement('div'); + row.className = 'fav-chips-row'; + row.style.cssText = favSectionCollapsed[type] + ? 'display:none;' + : 'display:flex;flex-wrap:wrap;gap:8px;'; + + const items = isFav ? favorites : recentRepos.slice(0, 8); + items.forEach(entry => row.appendChild(makeChip(entry, isFav ? 'favorite' : 'recent', allRepos))); + sec.appendChild(row); + + // ── Toggle-Logik ────────────────────────────────── + hdr.addEventListener('click', () => { + favSectionCollapsed[type] = !favSectionCollapsed[type]; + const collapsed = favSectionCollapsed[type]; + row.style.display = collapsed ? 'none' : 'flex'; + arrow.textContent = collapsed ? '▶' : '▼'; + // Zustand persistent speichern + window.electronAPI.loadCredentials().then(c => { + if (c && c.ok) { + window.electronAPI.saveCredentials({ + ...c, + favCollapsedFavorites: favSectionCollapsed.favorites, + favCollapsedRecent: favSectionCollapsed.recent + }); + } + }).catch(() => {}); + }); + + return sec; +} + +function renderFavRecentSection(container, allRepos) { + container.innerHTML = ''; + const showFav = featureFavorites && favorites.length > 0; + const showRec = featureRecent && recentRepos.length > 0; + if (!showFav && !showRec) return; + + if (showFav) container.appendChild(makeFavSectionBlock('favorites', allRepos)); + if (showRec) container.appendChild(makeFavSectionBlock('recent', allRepos)); + + // Trennlinie + const div = document.createElement('div'); + div.className = 'fav-divider'; + container.appendChild(div); +} + +function makeChip(entry, type, allRepos) { + const isFav = type === 'favorite'; + const chip = document.createElement('div'); + chip.className = `fav-chip${isFav ? ' fav-chip--star' : ''}`; + chip.title = `${entry.owner}/${entry.repo}`; + + const icon = document.createElement('span'); + icon.className = 'fav-chip-icon'; + icon.textContent = isFav ? '⭐' : '🕐'; + + const label = document.createElement('span'); + label.className = 'fav-chip-label'; + label.textContent = `${entry.owner}/${entry.repo}`; + + chip.appendChild(icon); + chip.appendChild(label); + + if (!isFav && entry.openedAt) { + const time = document.createElement('span'); + time.className = 'fav-chip-time'; + time.textContent = formatRelDate(entry.openedAt); + chip.appendChild(time); + } + + chip.onclick = () => { + addToRecent(entry.owner, entry.repo, entry.cloneUrl); + loadRepoContents(entry.owner, entry.repo, ''); + }; + + chip.oncontextmenu = (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + showChipContextMenu(ev, entry, type); + }; + + // Drag-Reorder (nur für Favoriten) + if (isFav) { + chip.draggable = true; + chip.dataset.owner = entry.owner; + 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); + chip.classList.add('fav-chip--dragging'); + }); + chip.addEventListener('dragend', () => chip.classList.remove('fav-chip--dragging')); + + chip.addEventListener('dragover', (ev) => { + ev.preventDefault(); + ev.dataTransfer.dropEffect = 'move'; + chip.classList.add('fav-chip--drop-target'); + }); + chip.addEventListener('dragleave', () => chip.classList.remove('fav-chip--drop-target')); + + chip.addEventListener('drop', async (ev) => { + 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'); + 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); + if (fromIdx < 0 || toIdx < 0) return; + + // Reorder + const [moved] = favorites.splice(fromIdx, 1); + favorites.splice(toIdx, 0, moved); + await window.electronAPI.saveFavorites(favorites); + + // Sektion neu rendern + const sec = $('favRecentSection'); + if (sec) { + // allRepos fehlt hier, daher einfach neu laden + const favBlock = sec.querySelector('.fav-chips-row'); + if (favBlock) { + const allChips = Array.from(favBlock.querySelectorAll('.fav-chip')); + const movedChip = allChips.find(c => c.dataset.owner === srcOwner && c.dataset.repo === srcRepo); + const targetChip = allChips.find(c => c.dataset.owner === entry.owner && c.dataset.repo === entry.repo); + if (movedChip && targetChip) { + favBlock.insertBefore(movedChip, fromIdx > toIdx ? targetChip : targetChip.nextSibling); + } + } + } + }); + } + + return chip; +} + +function showChipContextMenu(ev, entry, type) { + const old = $('ctxMenu'); + if (old) old.remove(); + 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'; + + const addItem = (icon, text, cb, color) => { + const el = document.createElement('div'); + el.className = 'context-item'; + el.innerHTML = `${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); + loadRepoContents(entry.owner, entry.repo, ''); + }); + + // Separator + const sep = document.createElement('div'); + sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; + menu.appendChild(sep); + + if (type === 'favorite') { + addItem('⭐', 'Aus Favoriten entfernen', async () => { + await toggleFavorite(entry.owner, entry.repo, entry.cloneUrl); + loadGiteaRepos(); + }, '#f59e0b'); + } else { + addItem('⭐', 'Zu Favoriten hinzufügen', async () => { + await toggleFavorite(entry.owner, entry.repo, entry.cloneUrl); + loadGiteaRepos(); + }); + addItem('✕', 'Aus Verlauf entfernen', async () => { + recentRepos = recentRepos.filter(r => !(r.owner === entry.owner && r.repo === entry.repo)); + await window.electronAPI.saveRecent(recentRepos); + loadGiteaRepos(); + }, '#ef4444'); + } + + document.body.appendChild(menu); + setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); +} + +// Speichert den default_branch pro Repo (owner/repo -> 'main' oder 'master') +let repoDefaultBranches = {}; + +function getDefaultBranch(owner, repo) { + return repoDefaultBranches[`${owner}/${repo}`] || 'HEAD'; +} + // Navigations-Status für die Explorer-Ansicht let currentState = { view: 'none', // 'local', 'gitea-list', 'gitea-repo' @@ -12,11 +299,171 @@ let currentState = { path: '' }; +// Clipboard für Cut & Paste +let clipboard = { + item: null, // { path, name, type, owner, repo, isGitea, isLocal, nodePath } + action: null // 'cut' +}; + +// Mehrfachauswahl +let selectedItems = new Set(); // Set von item-Pfaden +let isMultiSelectMode = false; + +// Zuletzt angeklicktes Item (für F2/Entf) +let lastSelectedItem = null; // { type:'gitea', item, owner, repo } | { type:'local', node } + +// Feature-Flag für farbige Icons +let featureColoredIcons = true; + function setStatus(txt) { const s = $('status'); if (s) s.innerText = txt || ''; } +/* ------------------------- + TOAST NOTIFICATIONS + ------------------------- */ +function showToast(message, type = 'info', duration = 4000) { + const container = (() => { + let c = $('toastContainer'); + if (!c) { + c = document.createElement('div'); + c.id = 'toastContainer'; + c.style.cssText = ` + position: fixed; + bottom: 24px; + right: 24px; + z-index: 99999; + display: flex; + flex-direction: column; + gap: 10px; + pointer-events: none; + `; + document.body.appendChild(c); + } + return c; + })(); + + const colors = { + error: { bg: 'rgba(239,68,68,0.15)', border: '#ef4444', icon: '✗' }, + success: { bg: 'rgba(34,197,94,0.15)', border: '#22c55e', icon: '✓' }, + info: { bg: 'rgba(0,212,255,0.12)', border: '#00d4ff', icon: 'ℹ' }, + warning: { bg: 'rgba(245,158,11,0.15)', border: '#f59e0b', icon: '⚠' }, + }; + const c = colors[type] || colors.info; + + const toast = document.createElement('div'); + toast.style.cssText = ` + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 16px; + background: ${c.bg}; + border: 1px solid ${c.border}; + border-left: 3px solid ${c.border}; + border-radius: 10px; + backdrop-filter: blur(12px); + box-shadow: 0 4px 24px rgba(0,0,0,0.4); + color: #fff; + font-size: 13px; + font-weight: 500; + max-width: 360px; + pointer-events: auto; + cursor: pointer; + opacity: 0; + transform: translateX(20px); + transition: opacity 220ms ease, transform 220ms ease; + line-height: 1.4; + `; + + const iconEl = document.createElement('span'); + iconEl.style.cssText = `color: ${c.border}; font-weight: 700; font-size: 15px; flex-shrink: 0; margin-top: 1px;`; + iconEl.textContent = c.icon; + + const msgEl = document.createElement('span'); + msgEl.textContent = message; + + toast.appendChild(iconEl); + toast.appendChild(msgEl); + container.appendChild(toast); + + // Einblenden + requestAnimationFrame(() => { + toast.style.opacity = '1'; + toast.style.transform = 'translateX(0)'; + }); + + const dismiss = () => { + toast.style.opacity = '0'; + toast.style.transform = 'translateX(20px)'; + setTimeout(() => toast.remove(), 220); + }; + + toast.addEventListener('click', dismiss); + setTimeout(dismiss, duration); +} + +// Kurzformen +function showError(msg) { setStatus(msg); showToast(msg, 'error'); } +function showSuccess(msg) { setStatus(msg); showToast(msg, 'success', 3000); } +function showWarning(msg) { setStatus(msg); showToast(msg, 'warning'); } + +// Löschen-Bestätigung als Toast (ersetzt confirm()) +function showDeleteConfirm(message, onConfirm) { + const container = (() => { + let c = $('toastContainer'); + if (!c) { c = document.createElement('div'); c.id = 'toastContainer'; c.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:99999;display:flex;flex-direction:column;gap:10px;pointer-events:none;'; document.body.appendChild(c); } + return c; + })(); + + const toast = document.createElement('div'); + toast.style.cssText = ` + padding: 14px 16px; + background: rgba(239,68,68,0.15); + border: 1px solid #ef4444; + border-left: 3px solid #ef4444; + border-radius: 10px; + backdrop-filter: blur(12px); + box-shadow: 0 4px 24px rgba(0,0,0,0.4); + color: #fff; + font-size: 13px; + max-width: 360px; + pointer-events: auto; + opacity: 0; + transform: translateX(20px); + transition: opacity 220ms ease, transform 220ms ease; + `; + + const msgEl = document.createElement('div'); + msgEl.style.cssText = 'font-weight:600;margin-bottom:10px;'; + msgEl.textContent = '🗑️ ' + message; + + const btns = document.createElement('div'); + btns.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;'; + + const cancelBtn = document.createElement('button'); + cancelBtn.textContent = 'Abbrechen'; + cancelBtn.style.cssText = 'padding:5px 12px;border-radius:6px;border:1px solid rgba(255,255,255,0.2);background:transparent;color:#fff;cursor:pointer;font-size:12px;'; + + const confirmBtn = document.createElement('button'); + confirmBtn.textContent = 'Löschen'; + confirmBtn.style.cssText = 'padding:5px 12px;border-radius:6px;border:none;background:#ef4444;color:#fff;cursor:pointer;font-size:12px;font-weight:600;'; + + btns.appendChild(cancelBtn); + btns.appendChild(confirmBtn); + toast.appendChild(msgEl); + toast.appendChild(btns); + container.appendChild(toast); + + requestAnimationFrame(() => { toast.style.opacity = '1'; toast.style.transform = 'translateX(0)'; }); + + const dismiss = () => { toast.style.opacity = '0'; toast.style.transform = 'translateX(20px)'; setTimeout(() => toast.remove(), 220); }; + + cancelBtn.addEventListener('click', dismiss); + confirmBtn.addEventListener('click', () => { dismiss(); onConfirm(); }); + setTimeout(dismiss, 8000); +} + /* ------------------------- PROGRESS UI ------------------------- */ @@ -286,7 +733,7 @@ async function updateEditor() { owner: tab.owner, repo: tab.repo, path: filePath, - ref: 'main' + ref: getDefaultBranch(tab.owner, tab.repo) }); if (response.ok) { @@ -566,7 +1013,7 @@ async function openGiteaFileInEditor(owner, repo, filePath, fileName) { owner, repo, path: filePath, - ref: 'main' + ref: getDefaultBranch(owner, repo) }); if (response.ok) { @@ -583,25 +1030,102 @@ async function openGiteaFileInEditor(owner, repo, filePath, fileName) { console.log('✅ Gitea file opened'); } else { alert(`Fehler: ${response.error}`); - setStatus('Fehler beim Laden der Datei'); + showError('Fehler beim Laden der Datei'); } } catch (error) { console.error('Error opening Gitea file:', error); alert('Fehler beim Öffnen der Datei'); - setStatus('Fehler'); + showError('Fehler'); } } +// Farbige Icons pro Dateityp (emoji + Farb-Overlay via CSS-Klassen) +const FILE_ICONS = { + // Web + js: { icon: '📄', color: '#f7df1e', label: 'JS' }, + jsx: { icon: '📄', color: '#61dafb', label: 'JSX' }, + ts: { icon: '📄', color: '#3178c6', label: 'TS' }, + tsx: { icon: '📄', color: '#3178c6', label: 'TSX' }, + html: { icon: '📄', color: '#e34c26', label: 'HTML' }, + css: { icon: '📄', color: '#264de4', label: 'CSS' }, + scss: { icon: '📄', color: '#cd6799', label: 'SCSS' }, + vue: { icon: '📄', color: '#42b883', label: 'VUE' }, + svelte:{ icon: '📄', color: '#ff3e00', label: 'SVE' }, + // Backend + py: { icon: '📄', color: '#3572a5', label: 'PY' }, + java: { icon: '📄', color: '#b07219', label: 'JAVA' }, + rb: { icon: '📄', color: '#701516', label: 'RB' }, + php: { icon: '📄', color: '#4f5d95', label: 'PHP' }, + go: { icon: '📄', color: '#00add8', label: 'GO' }, + rs: { icon: '📄', color: '#dea584', label: 'RS' }, + cs: { icon: '📄', color: '#178600', label: 'C#' }, + cpp: { icon: '📄', color: '#f34b7d', label: 'C++' }, + c: { icon: '📄', color: '#555555', label: 'C' }, + // Config + json: { icon: '📄', color: '#fbc02d', label: 'JSON' }, + yaml: { icon: '📄', color: '#cb171e', label: 'YAML' }, + yml: { icon: '📄', color: '#cb171e', label: 'YAML' }, + toml: { icon: '📄', color: '#9c4221', label: 'TOML' }, + env: { icon: '📄', color: '#ecd53f', label: 'ENV' }, + xml: { icon: '📄', color: '#f60', label: 'XML' }, + // Docs + md: { icon: '📄', color: '#083fa1', label: 'MD' }, + txt: { icon: '📄', color: '#888', label: 'TXT' }, + pdf: { icon: '📄', color: '#e53935', label: 'PDF' }, + // Shell + sh: { icon: '📄', color: '#89e051', label: 'SH' }, + bat: { icon: '📄', color: '#c1f12e', label: 'BAT' }, + // Images + png: { icon: '🖼️', color: '#4caf50', label: 'PNG' }, + jpg: { icon: '🖼️', color: '#4caf50', label: 'JPG' }, + jpeg: { icon: '🖼️', color: '#4caf50', label: 'JPG' }, + gif: { icon: '🖼️', color: '#4caf50', label: 'GIF' }, + svg: { icon: '🖼️', color: '#ff9800', label: 'SVG' }, + webp: { icon: '🖼️', color: '#4caf50', label: 'WEBP' }, + // Archives + zip: { icon: '📦', color: '#ff9800', label: 'ZIP' }, + tar: { icon: '📦', color: '#ff9800', label: 'TAR' }, + gz: { icon: '📦', color: '#ff9800', label: 'GZ' }, +}; + function getFileIcon(fileName) { const ext = fileName.split('.').pop()?.toLowerCase(); - const icons = { - 'js': '🟨', 'jsx': '⚛️', 'ts': '🔵', 'tsx': '⚛️', - 'py': '🐍', 'java': '☕', 'cpp': '⚙️', 'c': '⚙️', - 'html': '🌐', 'css': '🎨', 'scss': '🎨', 'json': '📋', - 'md': '📝', 'txt': '📄', 'xml': '📦', 'yaml': '⚙️', - 'yml': '⚙️', 'env': '🔑', 'sh': '💻', 'bat': '💻' - }; - return icons[ext] || '📄'; + const info = FILE_ICONS[ext]; + if (!info) return '📄'; + return info.icon; +} + +// Gibt ein DOM-Element für das Explorer-Icon zurück (mit farbigem Badge wenn aktiviert) +function makeFileIconEl(fileName, isDir = false) { + const wrapper = document.createElement('div'); + wrapper.className = 'item-icon'; + + if (isDir) { + wrapper.textContent = '📁'; + return wrapper; + } + + const ext = fileName.split('.').pop()?.toLowerCase(); + const info = featureColoredIcons ? FILE_ICONS[ext] : null; + + wrapper.textContent = info ? info.icon : '📄'; + + if (info) { + const badge = document.createElement('span'); + badge.className = 'file-type-badge'; + badge.textContent = info.label; + badge.style.background = info.color; + // Helligkeit prüfen für Textfarbe + const hex = info.color.replace('#',''); + const r = parseInt(hex.slice(0,2)||'88',16); + const g = parseInt(hex.slice(2,4)||'88',16); + const b = parseInt(hex.slice(4,6)||'88',16); + const lum = (0.299*r + 0.587*g + 0.114*b) / 255; + badge.style.color = lum > 0.55 ? '#111' : '#fff'; + wrapper.appendChild(badge); + } + + return wrapper; } /* ------------------------- @@ -696,7 +1220,7 @@ async function saveCurrentFile(isAutoSave = false) { repo: tab.repo, path: currentActiveTab.replace(`gitea://${tab.owner}/${tab.repo}/`, ''), content: content, - ref: 'main' + ref: getDefaultBranch(tab.owner, tab.repo) }); } else { // Lokale Datei @@ -843,7 +1367,7 @@ async function loadGiteaRepos() { try { const res = await window.electronAPI.listGiteaRepos(); if (!res.ok) { - setStatus('Failed to load repos: ' + (res.error || 'Unknown error')); + showError('Failed to load repos: ' + (res.error || 'Unknown error')); return; } @@ -902,16 +1426,71 @@ async function loadGiteaRepos() { }); // ----------------------------------- + // ── Favoriten & Zuletzt geöffnet ── + // Sektion IMMER ins DOM einfügen (auch wenn leer), + // damit der Stern-Klick später $('favRecentSection') findet + if (featureFavorites || featureRecent) { + const favSection = document.createElement('div'); + favSection.id = 'favRecentSection'; + favSection.style.cssText = 'grid-column: 1/-1;'; + grid.appendChild(favSection); + renderFavRecentSection(favSection, res.repos); + } + 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; + // default_branch speichern (main ODER master je nach Repo) + const defaultBranch = repo.default_branch || 'HEAD'; + repoDefaultBranches[`${owner}/${repoName}`] = defaultBranch; + const card = document.createElement('div'); card.className = 'item-card'; - card.innerHTML = `
📦
${repoName}
`; + card.style.position = 'relative'; card.dataset.cloneUrl = cloneUrl; + // Stern-Button (nur wenn Favoriten-Feature aktiv) + if (featureFavorites) { + const starBtn = document.createElement('button'); + starBtn.className = 'fav-star-btn' + (isFavorite(owner, repoName) ? ' active' : ''); + starBtn.title = isFavorite(owner, repoName) ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen'; + starBtn.textContent = isFavorite(owner, repoName) ? '⭐' : '☆'; + starBtn.addEventListener('click', async (e) => { + e.stopPropagation(); + await toggleFavorite(owner, repoName, cloneUrl); + const nowFav = isFavorite(owner, repoName); + starBtn.textContent = nowFav ? '⭐' : '☆'; + starBtn.title = nowFav ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen'; + starBtn.classList.toggle('active', nowFav); + // Favoriten-Sektion live aktualisieren + const sec = $('favRecentSection'); + if (sec) renderFavRecentSection(sec, res.repos); + }); + card.appendChild(starBtn); + } + + const iconEl = document.createElement('div'); + iconEl.className = 'item-icon'; + iconEl.textContent = '📦'; + card.appendChild(iconEl); + const nameEl = document.createElement('div'); + nameEl.className = 'item-name'; + nameEl.textContent = repoName; + card.appendChild(nameEl); + + // Repo-Größe anzeigen + if (repo.size != null) { + const sizeEl = document.createElement('div'); + sizeEl.className = 'repo-size-badge'; + const kb = repo.size; + sizeEl.textContent = kb >= 1024 + ? `${(kb / 1024).toFixed(1)} MB` + : `${kb} KB`; + card.appendChild(sizeEl); + } + // --- Nativer Drag Start (Download) --- card.draggable = true; card.addEventListener('dragstart', async (ev) => { @@ -930,11 +1509,11 @@ async function loadGiteaRepos() { window.electronAPI.startNativeDrag(resDrag.tempPath); setStatus('Ready to drag'); } else { - setStatus('Download preparation failed'); + showError('Download preparation failed'); } } catch (error) { console.error('Drag preparation error:', error); - setStatus('Error preparing download'); + showError('Error preparing download'); } finally { hideProgress(); } @@ -960,7 +1539,7 @@ async function loadGiteaRepos() { const files = ev.dataTransfer.files; if (!files || files.length === 0) { - setStatus("Keine Dateien zum Upload gefunden."); + showWarning("Keine Dateien zum Upload gefunden."); return; } @@ -978,12 +1557,12 @@ async function loadGiteaRepos() { repo: repoName, destPath: '', cloneUrl, - branch: 'main' + branch: getDefaultBranch(owner, repoName) }); if (!res.ok) { console.error("Upload Fehler:", res.error); - setStatus("Fehler: " + res.error); + showError("Fehler: " + res.error); } } catch (err) { console.error("Kritischer Upload Fehler:", err); @@ -995,7 +1574,10 @@ async function loadGiteaRepos() { setStatus('Upload abgeschlossen'); }); - card.onclick = () => loadRepoContents(owner, repoName, ''); + card.onclick = () => { + addToRecent(owner, repoName, cloneUrl); + loadRepoContents(owner, repoName, ''); + }; card.oncontextmenu = (ev) => showRepoContextMenu(ev, owner, repoName, cloneUrl, card); grid.appendChild(card); }); @@ -1003,7 +1585,7 @@ async function loadGiteaRepos() { setStatus(`Loaded ${res.repos.length} repos`); } catch (error) { console.error('Error loading repos:', error); - setStatus('Error loading repositories'); + showError('Error loading repositories'); } } @@ -1020,7 +1602,7 @@ async function loadRepoContents(owner, repo, path) { if (btnCommits) { btnCommits.classList.remove('hidden'); - btnCommits.onclick = () => loadCommitHistory(owner, repo, 'main'); + btnCommits.onclick = () => loadCommitHistory(owner, repo, getDefaultBranch(owner, repo)); } if (btnReleases) { @@ -1036,16 +1618,18 @@ async function loadRepoContents(owner, repo, path) { setStatus(`Loading: /${path || 'root'}`); + const ref = getDefaultBranch(owner, repo); + try { const res = await window.electronAPI.getGiteaRepoContents({ owner, repo, path, - ref: 'main' + ref }); if (!res.ok) { - setStatus('Error: ' + (res.error || 'Unknown error')); + showError('Error: ' + (res.error || 'Unknown error')); return; } @@ -1054,16 +1638,26 @@ async function loadRepoContents(owner, repo, path) { grid.innerHTML = ''; if (!res.items || res.items.length === 0) { - grid.innerHTML = '
Leerer Ordner
'; - setStatus('Empty folder'); + const emptyMsg = res.empty + ? '📭 Leeres Repository — noch keine Commits' + : '📂 Leerer Ordner'; + grid.innerHTML = `
${emptyMsg}
`; + setStatus(res.empty ? 'Leeres Repository' : 'Leerer Ordner'); return; } res.items.forEach(item => { - const icon = item.type === 'dir' ? '📁' : '📄'; const card = document.createElement('div'); card.className = 'item-card'; - card.innerHTML = `
${icon}
${item.name}
`; + // Farbiges Icon-Element + const iconEl = makeFileIconEl(item.name, item.type === 'dir'); + const nameEl = document.createElement('div'); + nameEl.className = 'item-name'; + nameEl.textContent = item.name; + card.appendChild(iconEl); + card.appendChild(nameEl); + // lastSelectedItem tracken + card.addEventListener('click', () => { lastSelectedItem = { type: 'gitea', item, owner, repo }; }); // Drag für Files und Folders if (item.type === 'dir') { @@ -1126,7 +1720,7 @@ async function loadRepoContents(owner, repo, path) { owner, repo, destPath: targetPath, - branch: 'main' + branch: getDefaultBranch(owner, repo) }); } catch (error) { console.error('Upload error:', error); @@ -1138,9 +1732,27 @@ async function loadRepoContents(owner, repo, path) { }); if (item.type === 'dir') { - card.onclick = () => loadRepoContents(owner, repo, item.path); + card.onclick = (e) => { + if (e.ctrlKey || e.metaKey) { + if (selectedItems.has(item.path)) { selectedItems.delete(item.path); card.classList.remove('selected'); } + else { selectedItems.add(item.path); card.classList.add('selected'); } + return; + } + selectedItems.clear(); + document.querySelectorAll('.item-card.selected').forEach(c => c.classList.remove('selected')); + loadRepoContents(owner, repo, item.path); + }; } else { - card.onclick = () => openGiteaFileInEditor(owner, repo, item.path, item.name); + card.onclick = (e) => { + if (e.ctrlKey || e.metaKey) { + if (selectedItems.has(item.path)) { selectedItems.delete(item.path); card.classList.remove('selected'); } + else { selectedItems.add(item.path); card.classList.add('selected'); } + return; + } + selectedItems.clear(); + document.querySelectorAll('.item-card.selected').forEach(c => c.classList.remove('selected')); + openGiteaFileInEditor(owner, repo, item.path, item.name); + }; } card.oncontextmenu = (ev) => showGiteaItemContextMenu(ev, item, owner, repo); @@ -1150,7 +1762,7 @@ async function loadRepoContents(owner, repo, path) { setStatus(`Loaded ${res.items.length} items`); } catch (error) { console.error('Error loading repo contents:', error); - setStatus('Error loading contents'); + showError('Error loading contents'); } } @@ -1171,7 +1783,7 @@ async function selectLocalFolder() { await loadBranches(folder); } catch (error) { console.error('Error selecting folder:', error); - setStatus('Error selecting folder'); + showError('Error selecting folder'); } } @@ -1188,7 +1800,7 @@ async function refreshLocalTree(folder) { grid.innerHTML = ''; if (!res.ok) { - setStatus('Error loading local files'); + showError('Error loading local files'); return; } @@ -1200,22 +1812,44 @@ async function refreshLocalTree(folder) { res.tree.forEach(node => { const card = document.createElement('div'); card.className = 'item-card'; - card.innerHTML = ` -
${node.isDirectory ? '📁' : '📄'}
-
${node.name}
- `; + card.dataset.path = node.path; + // Farbiges Icon + Name + const nodeIconEl = makeFileIconEl(node.name, node.isDirectory); + const nodeNameEl = document.createElement('div'); + nodeNameEl.className = 'item-name'; + nodeNameEl.textContent = node.name; + card.appendChild(nodeIconEl); + card.appendChild(nodeNameEl); + // lastSelectedItem tracken + card.addEventListener('click', () => { lastSelectedItem = { type: 'local', node }; }); - card.onclick = async () => { + card.onclick = async (e) => { + if (e.ctrlKey || e.metaKey) { + // Mehrfachauswahl + if (selectedItems.has(node.path)) { + selectedItems.delete(node.path); + card.classList.remove('selected'); + } else { + selectedItems.add(node.path); + card.classList.add('selected'); + } + return; + } + // Normale Auswahl + selectedItems.clear(); + grid.querySelectorAll('.item-card.selected').forEach(c => c.classList.remove('selected')); if (!node.isDirectory) { openFileEditor(node.path, node.name); } }; + + card.oncontextmenu = (ev) => showLocalItemContextMenu(ev, node); grid.appendChild(card); }); } catch (error) { console.error('Error refreshing tree:', error); - setStatus('Error loading file tree'); + showError('Error loading file tree'); } } @@ -1227,7 +1861,11 @@ async function pushLocalFolder() { alert('Select local folder first'); return; } - + + // Commit-Nachricht abfragen + const message = await showCommitMessageModal(); + if (message === null) return; // Abgebrochen + const branch = $('branchSelect')?.value || 'main'; const repoName = $('repoName')?.value; const platform = $('platform')?.value; @@ -1240,22 +1878,87 @@ async function pushLocalFolder() { folder: selectedFolder, branch, repoName, - platform + platform, + commitMessage: message }); if (res.ok) { setStatus('Push succeeded'); } else { - setStatus('Push failed: ' + (res.error || 'Unknown error')); + showError('Push failed: ' + (res.error || 'Unknown error')); } } catch (error) { console.error('Push error:', error); - setStatus('Push failed'); + showError('Push failed'); } finally { hideProgress(); } } +// Modal für Commit-Nachricht +function showCommitMessageModal() { + return new Promise((resolve) => { + 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('')} +
+ +
+ `; + document.body.appendChild(modal); + + const input = modal.querySelector('#commitMsgInput'); + input.focus(); + + // Quick-Buttons + modal.querySelectorAll('.commit-quick-btn').forEach(btn => { + btn.onclick = () => { + input.value = btn.textContent.trim(); + input.focus(); + }; + }); + + modal.querySelector('#btnCommitOk').onclick = () => { + const val = input.value.trim() || 'Update via Git Manager GUI'; + modal.remove(); + resolve(val); + }; + + modal.querySelector('#btnCommitCancel').onclick = () => { + modal.remove(); + resolve(null); + }; + + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') modal.querySelector('#btnCommitOk').click(); + if (e.key === 'Escape') modal.querySelector('#btnCommitCancel').click(); + }); + }); +} + async function loadBranches(folder) { try { const res = await window.electronAPI.getBranches({ folder }); @@ -1323,6 +2026,26 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { return item; }; + // ── Favorit ── + if (featureFavorites) { + const isFav = isFavorite(owner, repoName); + const favItem = createMenuItem( + isFav ? '⭐' : '☆', + isFav ? 'Aus Favoriten entfernen' : 'Zu Favoriten hinzufügen', + async () => { + menu.remove(); + await toggleFavorite(owner, repoName, cloneUrl); + loadGiteaRepos(); + }, + isFav ? '#f59e0b' : null + ); + menu.appendChild(favItem); + + const sep = document.createElement('div'); + sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.08);margin:4px 0;'; + menu.appendChild(sep); + } + const uploadItem = createMenuItem('🚀', 'Folder hier hochladen', async () => { menu.remove(); try { @@ -1335,7 +2058,7 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { repo: repoName, destPath: '', cloneUrl, - branch: 'main' + branch: getDefaultBranch(owner, repoName) }); hideProgress(); setStatus('Upload complete'); @@ -1343,7 +2066,7 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { } catch (error) { console.error('Upload error:', error); hideProgress(); - setStatus('Upload failed'); + showError('Upload failed'); } }); @@ -1354,13 +2077,13 @@ function showRepoContextMenu(ev, owner, repoName, cloneUrl, element) { const res = await window.electronAPI.deleteGiteaRepo({ owner, repo: repoName }); if (res.ok) { element.remove(); - setStatus('Repository deleted'); + showSuccess('Repository deleted'); } else { - setStatus('Delete failed: ' + res.error); + showError('Delete failed: ' + res.error); } } catch (error) { console.error('Delete error:', error); - setStatus('Delete failed'); + showError('Delete failed'); } } }, '#ef4444'); @@ -1384,38 +2107,413 @@ function showGiteaItemContextMenu(ev, item, owner, repo) { const menu = document.createElement('div'); menu.id = 'ctxMenu'; menu.className = 'context-menu'; - menu.style.left = ev.clientX + 'px'; - menu.style.top = ev.clientY + 'px'; - if (item.type === 'file') { - const downloadItem = document.createElement('div'); - downloadItem.className = 'context-item'; - downloadItem.innerHTML = '📥 Download File'; - downloadItem.onclick = async () => { - menu.remove(); - try { - const res = await window.electronAPI.downloadGiteaFile({ - owner, - repo, - path: item.path - }); - if (res.ok) { - setStatus(`Saved to ${res.savedTo}`); - } else { - setStatus('Download failed'); - } - } catch (error) { - console.error('Download error:', error); - setStatus('Download failed'); + // Menü positionieren (nicht außerhalb des Fensters) + const menuW = 220, menuH = 360; + const x = Math.min(ev.clientX, window.innerWidth - menuW); + const y = Math.min(ev.clientY, window.innerHeight - menuH); + menu.style.left = x + 'px'; + menu.style.top = y + 'px'; + + const addSep = () => { + const s = document.createElement('div'); + s.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; + menu.appendChild(s); + }; + + const addItem = (icon, text, onClick, color = null) => { + const el = document.createElement('div'); + el.className = 'context-item'; + el.innerHTML = `${icon} ${text}`; + if (color) el.style.color = color; + el.onclick = () => { menu.remove(); onClick(); }; + menu.appendChild(el); + }; + + // Mehrfachauswahl-Info + if (selectedItems.size > 1 && selectedItems.has(item.path)) { + const infoEl = document.createElement('div'); + infoEl.style.cssText = 'padding:8px 14px;font-size:11px;color:var(--accent-primary);font-weight:600;'; + infoEl.textContent = `${selectedItems.size} Elemente ausgewählt`; + menu.appendChild(infoEl); + addSep(); + + addItem('🗑️', `Alle ${selectedItems.size} löschen`, async () => { + if (!confirm(`${selectedItems.size} Elemente wirklich löschen?`)) return; + showProgress(0, 'Lösche...'); + let done = 0; + for (const p of selectedItems) { + await window.electronAPI.deleteFile({ path: p, owner, repo, isGitea: true }); + done++; + showProgress(Math.round((done / selectedItems.size) * 100), `Lösche ${done}/${selectedItems.size}`); } - }; - menu.appendChild(downloadItem); + selectedItems.clear(); + hideProgress(); + setStatus('Bulk-Delete abgeschlossen'); + loadRepoContents(owner, repo, currentState.path); + }, '#ef4444'); + + document.body.appendChild(menu); + setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); + return; } + // --- ÖFFNEN --- + addItem( + item.type === 'dir' ? '📂' : '✏️', + item.type === 'dir' ? 'Öffnen' : 'Im Editor öffnen', + () => { + if (item.type === 'dir') loadRepoContents(owner, repo, item.path); + else openGiteaFileInEditor(owner, repo, item.path, item.name); + } + ); + + addSep(); + + // --- NEU ERSTELLEN (immer sichtbar) --- + addItem('📄', 'Neue Datei erstellen', () => showNewGiteaItemModal(owner, repo, item.type === 'dir' ? item.path : currentState.path, 'file')); + addItem('📁', 'Neuen Ordner erstellen', () => showNewGiteaItemModal(owner, repo, item.type === 'dir' ? item.path : currentState.path, 'folder')); + + addSep(); + + // --- UMBENENNEN --- + addItem('✏️', 'Umbenennen', () => showGiteaRenameModal(item, owner, repo)); + + // --- CUT & PASTE --- + addItem('✂️', 'Ausschneiden (Cut)', () => { + clipboard = { item: { ...item, owner, repo, isGitea: true }, action: 'cut' }; + setStatus(`✂️ "${item.name}" ausgeschnitten — Zielordner öffnen und Einfügen wählen`); + }); + + if (clipboard.item && clipboard.item.isGitea && item.type === 'dir') { + addItem('📋', `Einfügen: "${clipboard.item.name}"`, async () => { + await pasteGiteaItem(owner, repo, item.path); + }); + } + + addSep(); + + // --- DOWNLOAD --- + if (item.type === 'file') { + addItem('📥', 'Herunterladen', async () => { + const res = await window.electronAPI.downloadGiteaFile({ owner, repo, path: item.path }); + setStatus(res.ok ? `Gespeichert: ${res.savedTo}` : 'Download fehlgeschlagen'); + }); + } else { + addItem('📥', 'Ordner herunterladen', async () => { + showProgress(0, `Lade ${item.name}...`); + const res = await window.electronAPI.downloadGiteaFolder({ owner, repo, path: item.path }); + hideProgress(); + setStatus(res.ok ? `Gespeichert: ${res.savedTo}` : 'Download fehlgeschlagen'); + }); + } + + addSep(); + + // --- LÖSCHEN --- + addItem('🗑️', 'Löschen', async () => { + if (!confirm(`"${item.name}" wirklich löschen?`)) return; + showProgress(0, `Lösche ${item.name}...`); + const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: true }); + 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')); + } + }, '#ef4444'); + document.body.appendChild(menu); - setTimeout(() => { - document.addEventListener('click', () => menu.remove(), { once: true }); - }, 10); + setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); +} + +/* ------------------------- + LOKALES KONTEXT-MENÜ + ------------------------- */ +function showLocalItemContextMenu(ev, node) { + ev.preventDefault(); + ev.stopPropagation(); + + const old = $('ctxMenu'); + if (old) old.remove(); + + const menu = document.createElement('div'); + menu.id = 'ctxMenu'; + menu.className = 'context-menu'; + + const menuW = 220, menuH = 360; + const x = Math.min(ev.clientX, window.innerWidth - menuW); + const y = Math.min(ev.clientY, window.innerHeight - menuH); + menu.style.left = x + 'px'; + menu.style.top = y + 'px'; + + const addSep = () => { + const s = document.createElement('div'); + s.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; + menu.appendChild(s); + }; + + const addItem = (icon, text, onClick, color = null) => { + const el = document.createElement('div'); + el.className = 'context-item'; + el.innerHTML = `${icon} ${text}`; + if (color) el.style.color = color; + el.onclick = () => { menu.remove(); onClick(); }; + menu.appendChild(el); + }; + + // Mehrfachauswahl-Bulk-Delete + if (selectedItems.size > 1 && selectedItems.has(node.path)) { + const infoEl = document.createElement('div'); + infoEl.style.cssText = 'padding:8px 14px;font-size:11px;color:var(--accent-primary);font-weight:600;'; + infoEl.textContent = `${selectedItems.size} Elemente ausgewählt`; + menu.appendChild(infoEl); + addSep(); + + addItem('🗑️', `Alle ${selectedItems.size} löschen`, async () => { + if (!confirm(`${selectedItems.size} Elemente wirklich löschen?`)) return; + for (const p of selectedItems) { + await window.electronAPI.deleteFile({ path: p }); + } + selectedItems.clear(); + setStatus('Bulk-Delete abgeschlossen'); + if (selectedFolder) refreshLocalTree(selectedFolder); + }, '#ef4444'); + + document.body.appendChild(menu); + setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); + return; + } + + // --- ÖFFNEN --- + if (!node.isDirectory) { + addItem('✏️', 'Im Editor öffnen', () => openFileEditor(node.path, node.name)); + addSep(); + } + + // --- NEU ERSTELLEN --- + const targetDir = node.isDirectory ? node.path : require('path').dirname(node.path); + addItem('📄', 'Neue Datei erstellen', () => showNewLocalItemModal(targetDir, 'file')); + addItem('📁', 'Neuen Ordner erstellen', () => showNewLocalItemModal(targetDir, 'folder')); + + addSep(); + + // --- UMBENENNEN --- + addItem('✏️', 'Umbenennen', () => showLocalRenameModal(node)); + + // --- CUT & PASTE --- + addItem('✂️', 'Ausschneiden (Cut)', () => { + clipboard = { item: { ...node, isLocal: true }, action: 'cut' }; + setStatus(`✂️ "${node.name}" ausgeschnitten`); + }); + + if (clipboard.item && clipboard.item.isLocal && node.isDirectory) { + addItem('📋', `Einfügen: "${clipboard.item.name}"`, async () => { + await pasteLocalItem(node.path); + }); + } + + addSep(); + + // --- DOWNLOAD / KOPIEREN --- + addItem('📥', 'Kopieren nach...', async () => { + const destFolder = await window.electronAPI.selectFolder(); + if (!destFolder) return; + const res = await window.electronAPI.copyLocalItem({ src: node.path, destDir: destFolder }); + setStatus(res?.ok ? `Kopiert nach: ${destFolder}` : 'Kopieren fehlgeschlagen: ' + (res?.error || '')); + }); + + addSep(); + + // --- LÖSCHEN --- + addItem('🗑️', 'Löschen', async () => { + if (!confirm(`"${node.name}" wirklich löschen?`)) return; + const res = await window.electronAPI.deleteFile({ path: node.path }); + if (res && res.ok) { + setStatus(`${node.name} gelöscht`); + if (selectedFolder) refreshLocalTree(selectedFolder); + } else { + showError('Löschen fehlgeschlagen: ' + (res?.error || '')); + } + }, '#ef4444'); + + document.body.appendChild(menu); + setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); +} + +/* ========================================= + MODAL HELPER FUNKTIONEN + ========================================= */ + +// Gitea: Umbenennen +function showGiteaRenameModal(item, owner, repo) { + showInputModal({ + title: `✏️ Umbenennen`, + label: 'Neuer Name', + defaultValue: item.name, + confirmText: 'Umbenennen', + onConfirm: async (newName) => { + if (!newName || newName === item.name) return; + setStatus('Umbenennen...'); + const parentPath = item.path.split('/').slice(0, -1).join('/'); + const newPath = parentPath ? `${parentPath}/${newName}` : newName; + const res = await window.electronAPI.renameGiteaItem({ + owner, repo, + oldPath: item.path, + newPath, + isDir: item.type === 'dir' + }); + if (res?.ok) { + setStatus(`Umbenannt in "${newName}"`); + loadRepoContents(owner, repo, currentState.path); + } else { + alert('Umbenennen fehlgeschlagen:\n' + (res?.error || 'Unbekannter Fehler')); + showError('Fehler beim Umbenennen'); + } + } + }); +} + +// Gitea: Neue Datei / Ordner +function showNewGiteaItemModal(owner, repo, parentPath, type) { + showInputModal({ + title: type === 'file' ? '📄 Neue Datei' : '📁 Neuer Ordner', + label: type === 'file' ? 'Dateiname (z.B. README.md)' : 'Ordnername', + defaultValue: '', + confirmText: 'Erstellen', + onConfirm: async (name) => { + if (!name) return; + const targetPath = parentPath ? `${parentPath}/${name}` : name; + const res = await window.electronAPI.createGiteaItem({ + owner, repo, + path: targetPath, + type + }); + if (res?.ok) { + setStatus(`"${name}" erstellt`); + loadRepoContents(owner, repo, currentState.path); + } else { + alert('Erstellen fehlgeschlagen:\n' + (res?.error || '')); + } + } + }); +} + +// Lokal: Umbenennen +function showLocalRenameModal(node) { + showInputModal({ + title: `✏️ Umbenennen`, + label: 'Neuer Name', + defaultValue: node.name, + confirmText: 'Umbenennen', + onConfirm: async (newName) => { + if (!newName || newName === node.name) return; + const res = await window.electronAPI.renameLocalItem({ oldPath: node.path, newName }); + if (res?.ok) { + setStatus(`Umbenannt in "${newName}"`); + if (selectedFolder) refreshLocalTree(selectedFolder); + } else { + alert('Umbenennen fehlgeschlagen:\n' + (res?.error || '')); + } + } + }); +} + +// Lokal: Neue Datei / Ordner +function showNewLocalItemModal(parentDir, type) { + showInputModal({ + title: type === 'file' ? '📄 Neue Datei' : '📁 Neuer Ordner', + label: type === 'file' ? 'Dateiname (z.B. README.md)' : 'Ordnername', + defaultValue: '', + confirmText: 'Erstellen', + onConfirm: async (name) => { + if (!name) return; + const res = await window.electronAPI.createLocalItem({ parentDir, name, type }); + if (res?.ok) { + setStatus(`"${name}" erstellt`); + if (selectedFolder) refreshLocalTree(selectedFolder); + } else { + alert('Erstellen fehlgeschlagen:\n' + (res?.error || '')); + } + } + }); +} + +// Gitea: Einfügen nach Cut +async function pasteGiteaItem(owner, repo, destFolderPath) { + if (!clipboard.item || !clipboard.item.isGitea) return; + const src = clipboard.item; + const newPath = destFolderPath ? `${destFolderPath}/${src.name}` : src.name; + setStatus(`Verschiebe "${src.name}"...`); + showProgress(0, `Verschiebe...`); + const res = await window.electronAPI.renameGiteaItem({ + owner, repo, + oldPath: src.path, + newPath, + isDir: src.type === 'dir' + }); + hideProgress(); + if (res?.ok) { + clipboard = { item: null, action: null }; + setStatus(`"${src.name}" verschoben`); + loadRepoContents(owner, repo, currentState.path); + } else { + alert('Verschieben fehlgeschlagen:\n' + (res?.error || '')); + showError('Fehler beim Verschieben'); + } +} + +// Lokal: Einfügen nach Cut +async function pasteLocalItem(destDir) { + if (!clipboard.item || !clipboard.item.isLocal) return; + const src = clipboard.item; + setStatus(`Verschiebe "${src.name}"...`); + const res = await window.electronAPI.moveLocalItem({ srcPath: src.path, destDir }); + if (res?.ok) { + clipboard = { item: null, action: null }; + setStatus(`"${src.name}" verschoben`); + if (selectedFolder) refreshLocalTree(selectedFolder); + } else { + alert('Verschieben fehlgeschlagen:\n' + (res?.error || '')); + } +} + +// Generic Input Modal +function showInputModal({ title, label, defaultValue, confirmText, onConfirm }) { + const modal = document.createElement('div'); + modal.className = 'modal'; + modal.style.zIndex = '99999'; + modal.innerHTML = ` +
+

${title}

+
+ + +
+ +
+ `; + document.body.appendChild(modal); + const input = modal.querySelector('#inputModalField'); + input.focus(); + input.select(); + + modal.querySelector('#inputModalOk').onclick = () => { + const val = input.value.trim(); + modal.remove(); + onConfirm(val); + }; + modal.querySelector('#inputModalCancel').onclick = () => modal.remove(); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') modal.querySelector('#inputModalOk').click(); + if (e.key === 'Escape') modal.querySelector('#inputModalCancel').click(); + }); + modal.onclick = (e) => { if (e.target === modal) modal.remove(); }; } /* ------------------------- @@ -1435,18 +2533,18 @@ async function previewGiteaFile(owner, repo, filePath) { owner, repo, path: filePath, - ref: 'main' + ref: getDefaultBranch(owner, repo) }); if (res.ok) { console.log("Content of", filePath, ":", res.content); setStatus(`Previewed: ${filePath}`); } else { - setStatus('Preview failed'); + showError('Preview failed'); } } catch (error) { console.error('Preview error:', error); - setStatus('Preview failed'); + showError('Preview failed'); } } @@ -1469,20 +2567,149 @@ async function createRepoHandler() { if (res.ok) { $('repoActionModal')?.classList.add('hidden'); - setStatus('Repository created'); + showSuccess('Repository created'); loadGiteaRepos(); } else { - setStatus('Create failed: ' + (res.error || 'Unknown error')); + showError('Create failed: ' + (res.error || 'Unknown error')); } } catch (error) { console.error('Create repo error:', error); - setStatus('Create failed'); + showError('Create failed'); } } /* ------------------------- GLOBALER DROP-HANDLER FÜR REPO-ANSICHT ------------------------- */ +/* ------------------------- + HINTERGRUND KONTEXT-MENÜ (Rechtsklick auf leere Fläche) + ------------------------- */ +function setupBackgroundContextMenu() { + // Listener auf #main statt explorerGrid, da Grid kleiner als sichtbarer Bereich sein kann + const mainEl = $('main'); + if (!mainEl) return; + + mainEl.addEventListener('contextmenu', (ev) => { + // Nicht auslösen wenn auf eine Karte oder interaktives Element geklickt wird + if (ev.target.closest('.item-card, .release-card, .commit-card, .fav-chip, .fav-star-btn, button, input, textarea, select, a')) return; + + // Nur in Repo- oder Lokal-Ansicht + if (currentState.view !== 'gitea-repo' && currentState.view !== 'local') return; + + ev.preventDefault(); + ev.stopPropagation(); + + const old = $('ctxMenu'); + if (old) old.remove(); + + const menu = document.createElement('div'); + menu.id = 'ctxMenu'; + menu.className = 'context-menu'; + + const menuW = 220, menuH = 160; + const x = Math.min(ev.clientX, window.innerWidth - menuW); + const y = Math.min(ev.clientY, window.innerHeight - menuH); + menu.style.left = x + 'px'; + menu.style.top = y + 'px'; + + // Aktuelle Pfad-Info als Header + const header = document.createElement('div'); + header.style.cssText = 'padding:8px 14px 4px;font-size:11px;color:var(--text-muted);letter-spacing:0.5px;'; + const currentPath = currentState.view === 'gitea-repo' + ? (currentState.path || 'Root') + : (selectedFolder ? selectedFolder.split(/[\\/]/).pop() : 'Lokal'); + header.textContent = `📂 ${currentPath}`; + menu.appendChild(header); + + const sep = document.createElement('div'); + sep.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; + menu.appendChild(sep); + + const addItem = (icon, text, onClick) => { + const el = document.createElement('div'); + el.className = 'context-item'; + el.innerHTML = `${icon} ${text}`; + el.onclick = () => { menu.remove(); onClick(); }; + menu.appendChild(el); + }; + + if (currentState.view === 'gitea-repo') { + const { owner, repo, path: currentPath } = currentState; + + addItem('📄', 'Neue Datei erstellen', () => + showNewGiteaItemModal(owner, repo, currentPath, 'file') + ); + addItem('📁', 'Neuen Ordner erstellen', () => + showNewGiteaItemModal(owner, repo, currentPath, 'folder') + ); + + // Einfügen: gleiche Quelle ODER Cross-Paste von Lokal + if (clipboard.item) { + const sep2 = document.createElement('div'); + sep2.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; + menu.appendChild(sep2); + if (clipboard.item.isGitea) { + addItem('📋', `Einfügen: "${clipboard.item.name}"`, () => + pasteGiteaItem(owner, repo, currentPath) + ); + } else if (clipboard.item.isLocal) { + addItem('📋', `⬆️ Von Lokal einfügen: "${clipboard.item.name}"`, async () => { + showProgress(0, `Lade "${clipboard.item.name}" hoch...`); + try { + await window.electronAPI.uploadAndPush({ + localFolder: clipboard.item.path, + owner, repo, + destPath: currentPath, + branch: getDefaultBranch(owner, repo) + }); + showSuccess(`"${clipboard.item.name}" nach Gitea kopiert`); + loadRepoContents(owner, repo, currentState.path); + } catch(e) { showError('Cross-Paste fehlgeschlagen'); } + finally { hideProgress(); } + }); + } + } + + } else if (currentState.view === 'local' && selectedFolder) { + addItem('📄', 'Neue Datei erstellen', () => + showNewLocalItemModal(selectedFolder, 'file') + ); + addItem('📁', 'Neuen Ordner erstellen', () => + showNewLocalItemModal(selectedFolder, 'folder') + ); + + if (clipboard.item) { + const sep2 = document.createElement('div'); + sep2.style.cssText = 'height:1px;background:rgba(255,255,255,0.1);margin:4px 0;'; + menu.appendChild(sep2); + if (clipboard.item.isLocal) { + addItem('📋', `Einfügen: "${clipboard.item.name}"`, () => + pasteLocalItem(selectedFolder) + ); + } else if (clipboard.item.isGitea) { + addItem('📋', `⬇️ Von Gitea einfügen: "${clipboard.item.name}"`, async () => { + showProgress(0, `Lade "${clipboard.item.name}" herunter...`); + try { + await window.electronAPI.downloadGiteaFolder({ + owner: clipboard.item.owner, + repo: clipboard.item.repo, + giteaPath: clipboard.item.path, + localPath: selectedFolder + }); + showSuccess(`"${clipboard.item.name}" nach Lokal kopiert`); + refreshLocalTree(selectedFolder); + } catch(e) { showError('Cross-Paste fehlgeschlagen'); } + finally { hideProgress(); } + }); + } + } + } + + document.body.appendChild(menu); + setTimeout(() => document.addEventListener('click', () => menu.remove(), { once: true }), 10); + }); +} + function setupGlobalDropZone() { const main = $('main'); if (!main) return; @@ -1525,7 +2752,7 @@ function setupGlobalDropZone() { const files = ev.dataTransfer.files; if (!files || files.length === 0) { - setStatus("Keine Dateien zum Upload gefunden."); + showWarning("Keine Dateien zum Upload gefunden."); return; } @@ -1547,18 +2774,18 @@ function setupGlobalDropZone() { owner, repo, destPath: targetPath, - branch: 'main' + branch: getDefaultBranch(owner, repo) }); if (!res.ok) { console.error("Upload error:", res.error); - setStatus("Error: " + res.error); + showError("Error: " + res.error); } else { setStatus(`Uploaded: ${baseName}`); } } catch (err) { console.error("Critical upload error:", err); - setStatus("Upload failed"); + showError("Upload failed"); } } @@ -1575,6 +2802,8 @@ function setupGlobalDropZone() { INITIALISIERUNG ------------------------- */ window.addEventListener('DOMContentLoaded', async () => { + // Favoriten & Verlauf vorladen + await loadFavoritesAndRecent(); // Prevent default drag/drop on document (except in repo view) document.addEventListener('dragover', e => { if (currentState.view !== 'gitea-repo') { @@ -1596,6 +2825,27 @@ window.addEventListener('DOMContentLoaded', async () => { if ($('githubToken')) $('githubToken').value = creds.githubToken || ''; if ($('giteaToken')) $('giteaToken').value = creds.giteaToken || ''; if ($('giteaURL')) $('giteaURL').value = creds.giteaURL || ''; + + // Feature-Flags aus gespeicherten Einstellungen + if (typeof creds.featureFavorites === 'boolean') featureFavorites = creds.featureFavorites; + if (typeof creds.featureRecent === 'boolean') featureRecent = creds.featureRecent; + if (typeof creds.compactMode === 'boolean') compactMode = creds.compactMode; + if (typeof creds.featureColoredIcons === 'boolean') featureColoredIcons = creds.featureColoredIcons; + document.body.classList.toggle('compact-mode', compactMode); + + // Collapse-Zustand wiederherstellen + if (typeof creds.favCollapsedFavorites === 'boolean') favSectionCollapsed.favorites = creds.favCollapsedFavorites; + if (typeof creds.favCollapsedRecent === 'boolean') favSectionCollapsed.recent = creds.favCollapsedRecent; + + // Settings-Checkboxen befüllen + const cbFav = $('settingFavorites'); + const cbRec = $('settingRecent'); + const cbCompact = $('settingCompact'); + if (cbFav) cbFav.checked = featureFavorites; + if (cbRec) cbRec.checked = featureRecent; + if (cbCompact) cbCompact.checked = compactMode; + const cbColorIcons = $('settingColoredIcons'); + if (cbColorIcons) cbColorIcons.checked = featureColoredIcons; // 🆕 AUTO-LOGIN: Wenn Gitea-Credentials vorhanden sind, lade sofort die Repos if (creds.giteaToken && creds.giteaURL) { @@ -1616,7 +2866,7 @@ window.addEventListener('DOMContentLoaded', async () => { } } catch (error) { console.error('Error loading credentials:', error); - setStatus('Fehler beim Laden der Einstellungen'); + showError('Fehler beim Laden der Einstellungen'); } // Rest of Event Handlers... (bleibt unverändert) @@ -1680,17 +2930,36 @@ window.addEventListener('DOMContentLoaded', async () => { if ($('btnSaveSettings')) { $('btnSaveSettings').onclick = async () => { try { + // Feature-Flags aus Checkboxen lesen + const cbFav = $('settingFavorites'); + const cbRec = $('settingRecent'); + const cbCompact = $('settingCompact'); + featureFavorites = cbFav ? cbFav.checked : true; + featureRecent = cbRec ? cbRec.checked : true; + compactMode = cbCompact ? cbCompact.checked : false; + const cbColorIcons2 = $('settingColoredIcons'); + featureColoredIcons = cbColorIcons2 ? cbColorIcons2.checked : true; + document.body.classList.toggle('compact-mode', compactMode); + const data = { githubToken: $('githubToken').value, giteaToken: $('giteaToken').value, - giteaURL: $('giteaURL').value + giteaURL: $('giteaURL').value, + featureFavorites, + featureRecent, + compactMode, + featureColoredIcons, + favCollapsedFavorites: favSectionCollapsed.favorites, + favCollapsedRecent: favSectionCollapsed.recent }; await window.electronAPI.saveCredentials(data); $('settingsModal').classList.add('hidden'); - setStatus('Settings saved'); + showSuccess('Settings saved'); + // Ansicht aktualisieren falls Feature-Flags geändert + loadGiteaRepos(); } catch (error) { console.error('Error saving settings:', error); - setStatus('Save failed'); + showError('Save failed'); } }; } @@ -1777,6 +3046,36 @@ window.addEventListener('DOMContentLoaded', async () => { $('searchBar').classList.add('hidden'); } } + + // F2 - Umbenennen + if (e.key === 'F2' && lastSelectedItem && !currentActiveTab) { + e.preventDefault(); + if (lastSelectedItem.type === 'gitea') { + showGiteaRenameModal(lastSelectedItem.item, lastSelectedItem.owner, lastSelectedItem.repo); + } else if (lastSelectedItem.type === 'local') { + showLocalRenameModal(lastSelectedItem.node); + } + } + + // Entf - Löschen mit Bestätigungs-Toast + if (e.key === 'Delete' && lastSelectedItem && !currentActiveTab) { + e.preventDefault(); + if (lastSelectedItem.type === 'gitea') { + const { item, owner, repo } = lastSelectedItem; + showDeleteConfirm(`"${item.name}" wirklich löschen?`, async () => { + const res = await window.electronAPI.deleteFile({ path: item.path, owner, repo, isGitea: true }); + if (res?.ok) { showSuccess(`"${item.name}" gelöscht`); loadRepoContents(owner, repo, currentState.path); lastSelectedItem = null; } + else showError('Löschen fehlgeschlagen: ' + (res?.error || '')); + }); + } else if (lastSelectedItem.type === 'local') { + const { node } = lastSelectedItem; + showDeleteConfirm(`"${node.name}" wirklich löschen?`, async () => { + const res = await window.electronAPI.deleteFile({ path: node.path }); + if (res?.ok) { showSuccess(`"${node.name}" gelöscht`); if (selectedFolder) refreshLocalTree(selectedFolder); lastSelectedItem = null; } + else showError('Löschen fehlgeschlagen: ' + (res?.error || '')); + }); + } + } }); // Progress listeners @@ -1790,6 +3089,7 @@ window.addEventListener('DOMContentLoaded', async () => { // Setup globalen Drop-Handler für Repo-Ansicht setupGlobalDropZone(); + setupBackgroundContextMenu(); setStatus('Ready'); initUpdater(); // Updater initialisieren @@ -1818,7 +3118,7 @@ async function loadRepoReleases(owner, repo) { const res = await window.electronAPI.listReleases({ owner, repo }); if (!res.ok) { - setStatus('Error loading releases: ' + res.error); + showError('Error loading releases: ' + res.error); return; } @@ -1877,7 +3177,7 @@ async function loadRepoReleases(owner, repo) { } catch (error) { console.error('Error loading releases:', error); - setStatus('Failed to load releases'); + showError('Failed to load releases'); } } @@ -2317,12 +3617,12 @@ function showCreateReleaseModal(owner, repo) { } else { console.error('Asset Upload fehlgeschlagen:', uploadRes.error); alert(`Release erstellt, aber Asset Upload fehlgeschlagen: ${uploadRes.error}`); - setStatus('Release erstellt (Upload Fehler)'); + showWarning('Release erstellt (Upload Fehler)'); } } catch (uploadErr) { console.error('Upload error:', uploadErr); alert('Release erstellt, aber Fehler beim Hochladen der Datei.'); - setStatus('Release erstellt (Upload Fehler)'); + showWarning('Release erstellt (Upload Fehler)'); } finally { hideProgress(); } @@ -2333,12 +3633,12 @@ function showCreateReleaseModal(owner, repo) { modal.remove(); loadRepoReleases(owner, repo); // Liste neu laden } else { - setStatus('Failed: ' + res.error); + showError('Failed: ' + res.error); alert('Fehler beim Erstellen des Releases: ' + res.error); } } catch (error) { console.error('Create release error:', error); - setStatus('Create failed'); + showError('Create failed'); alert('Ein unerwarteter Fehler ist aufgetreten.'); } }; @@ -2382,13 +3682,13 @@ async function showUploadAssetDialog(release) { // Reload releases to show new asset loadRepoReleases(currentReleaseView.owner, currentReleaseView.repo); } else { - setStatus('Upload failed: ' + uploadRes.error); + showError('Upload failed: ' + uploadRes.error); } } catch (error) { console.error('Upload asset error:', error); hideProgress(); - setStatus('Upload failed'); + showError('Upload failed'); } } @@ -2418,7 +3718,7 @@ function formatBytes(bytes) { let currentCommitView = { owner: null, repo: null, - branch: 'main', + branch: 'HEAD', commits: [], selectedCommit: null }; @@ -2442,7 +3742,7 @@ async function loadCommitHistory(owner, repo, branch = 'main') { }); if (!res.ok) { - setStatus('Error loading commits: ' + res.error); + showError('Error loading commits: ' + res.error); return; } @@ -2452,7 +3752,7 @@ async function loadCommitHistory(owner, repo, branch = 'main') { } catch (error) { console.error('Error loading commit history:', error); - setStatus('Failed to load commits'); + showError('Failed to load commits'); } } diff --git a/renderer/style.css b/renderer/style.css index 74bf42b..21e8e11 100644 --- a/renderer/style.css +++ b/renderer/style.css @@ -326,6 +326,29 @@ body { animation: fadeIn var(--transition-slow) ease-out; } +/* ── Kompakt-Modus ── */ +body.compact-mode .explorer-grid { + grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); + gap: var(--spacing-sm); +} + +body.compact-mode .item-card { + padding: 10px 8px; +} + +body.compact-mode .item-icon { + font-size: 32px; + margin-bottom: 6px; +} + +body.compact-mode .item-name { + font-size: 11px; +} + +body.compact-mode .item-card:hover { + transform: translateY(-2px) scale(1.01); +} + @keyframes fadeIn { from { opacity: 0; @@ -389,11 +412,32 @@ body { box-shadow: 0 0 20px rgba(34, 197, 94, 0.3); } +/* Mehrfachauswahl */ +.item-card.selected { + border-color: var(--accent-primary); + background: rgba(0, 212, 255, 0.12); + box-shadow: 0 0 0 2px var(--accent-primary), 0 0 20px rgba(0, 212, 255, 0.2); +} + +.item-card.selected .item-name { + color: var(--accent-primary); +} + +/* Commit Quick Buttons */ +.commit-quick-btn:hover { + border-color: var(--accent-primary) !important; + color: var(--accent-primary) !important; +} + .item-icon { font-size: 56px; margin-bottom: var(--spacing-md); filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); transition: transform var(--transition-normal); + position: relative; + display: flex; + align-items: center; + justify-content: center; } .item-card:hover .item-icon { @@ -1410,4 +1454,290 @@ progress::-moz-progress-bar { font-size: 0.9rem; color: var(--text-secondary); border: 1px solid rgba(255,255,255,0.05); +} +/* ============================================= + FAVORITEN & ZULETZT GEÖFFNET — Styles + ============================================= */ + +/* Sektions-Überschrift */ +.fav-section-header { + display: flex; + align-items: center; + gap: 7px; + margin-bottom: 10px; + color: var(--text-muted); + font-size: 11px; + font-weight: 700; + letter-spacing: 1.5px; + text-transform: uppercase; +} + +.fav-section-header--toggle { + cursor: pointer; + user-select: none; + border-radius: var(--radius-sm); + padding: 3px 6px 3px 2px; + margin-left: -2px; + transition: background var(--transition-fast), color var(--transition-fast); +} + +.fav-section-header--toggle:hover { + background: rgba(255, 255, 255, 0.06); + color: var(--text-primary); +} + +.fav-collapse-arrow { + margin-left: auto; + font-size: 9px; + opacity: 0.5; + transition: opacity var(--transition-fast); +} + +.fav-section-header--toggle:hover .fav-collapse-arrow { + opacity: 1; +} + +.fav-section-icon { + font-size: 13px; +} + +/* Reihe der Chips */ +.fav-chips-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +/* Trennlinie nach den Sektionen */ +.fav-divider { + height: 1px; + background: rgba(255, 255, 255, 0.07); + margin: 4px 0 20px 0; +} + +/* Einzelner Chip */ +.fav-chip { + display: flex; + align-items: center; + gap: 7px; + padding: 7px 13px 7px 10px; + background: var(--bg-tertiary); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 30px; + cursor: pointer; + transition: border-color var(--transition-fast), + background var(--transition-fast), + transform var(--transition-fast), + box-shadow var(--transition-fast); + max-width: 260px; + user-select: none; +} + +.fav-chip:hover { + border-color: var(--accent-primary); + background: rgba(0, 212, 255, 0.06); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 212, 255, 0.12); +} + +.fav-chip--star:hover { + border-color: #f59e0b; + background: rgba(245, 158, 11, 0.06); + box-shadow: 0 4px 12px rgba(245, 158, 11, 0.15); +} + +.fav-chip-icon { + font-size: 13px; + flex-shrink: 0; +} + +.fav-chip-label { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 160px; +} + +.fav-chip-time { + font-size: 11px; + color: var(--text-muted); + white-space: nowrap; + flex-shrink: 0; +} + +/* Stern-Button auf Repo-Karten */ +.fav-star-btn { + position: absolute; + top: 6px; + right: 6px; + background: transparent; + border: none; + font-size: 15px; + cursor: pointer; + opacity: 0; + transition: opacity var(--transition-fast), transform var(--transition-fast); + z-index: 5; + line-height: 1; + padding: 4px; + border-radius: 50%; + color: inherit; +} + +.item-card:hover .fav-star-btn { + opacity: 0.6; +} + +.fav-star-btn:hover, +.fav-star-btn.active { + opacity: 1; + transform: scale(1.2); +} + +.fav-star-btn.active { + color: #f59e0b; +} + +/* ============================================= + SETTINGS — Toggle-Switch Styles + ============================================= */ + +.settings-toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: var(--radius-md); + margin-bottom: 8px; + cursor: pointer; + transition: background var(--transition-fast), border-color var(--transition-fast); + gap: 16px; +} + +.settings-toggle-row:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.12); +} + +.settings-toggle-info { + display: flex; + flex-direction: column; + gap: 3px; +} + +.settings-toggle-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); +} + +.settings-toggle-desc { + font-size: 12px; + color: var(--text-muted); + font-weight: 400; + text-transform: none; + letter-spacing: 0; +} + +/* Toggle-Switch */ +.toggle-switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + flex-shrink: 0; + cursor: pointer; +} + +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; + position: absolute; +} + +.toggle-track { + position: absolute; + inset: 0; + background: rgba(255, 255, 255, 0.15); + border-radius: 24px; + transition: background var(--transition-normal); +} + +.toggle-track::before { + content: ''; + position: absolute; + width: 18px; + height: 18px; + left: 3px; + top: 3px; + background: #fff; + border-radius: 50%; + transition: transform var(--transition-normal); + box-shadow: 0 1px 4px rgba(0,0,0,0.3); +} + +.toggle-switch input:checked + .toggle-track { + background: var(--accent-primary); +} + +.toggle-switch input:checked + .toggle-track::before { + transform: translateX(20px); +} +/* =========================== + FARBIGE DATEI-ICONS + =========================== */ + +.file-type-badge { + position: absolute; + bottom: -4px; + right: -6px; + font-size: 9px; + font-weight: 800; + padding: 1px 4px; + border-radius: 4px; + letter-spacing: 0.3px; + line-height: 14px; + pointer-events: none; + white-space: nowrap; + box-shadow: 0 1px 4px rgba(0,0,0,0.4); +} + +body.compact-mode .file-type-badge { + font-size: 8px; + padding: 1px 3px; +} + +/* =========================== + REPO-GRÖSSE BADGE + =========================== */ +.repo-size-badge { + font-size: 10px; + color: var(--text-muted); + margin-top: 4px; + opacity: 0.7; +} + +/* =========================== + FAVORITEN DRAG-REORDER + =========================== */ +.fav-chip--dragging { + opacity: 0.4; + transform: scale(0.95); + cursor: grabbing !important; +} + +.fav-chip--drop-target { + border-color: var(--accent-primary) !important; + background: rgba(0, 212, 255, 0.12) !important; + transform: translateY(-3px); + box-shadow: 0 4px 16px rgba(0, 212, 255, 0.25); +} + +.fav-chip[draggable="true"] { + cursor: grab; } \ No newline at end of file