Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a9f0728232 | |||
| edeb05f088 | |||
| 4b4d1520b9 | |||
| b8644b248f | |||
| 71ed9b7c67 | |||
| 78a94f2263 | |||
| b153431543 | |||
| 93ebe5aea9 | |||
| 1ca5856fe3 | |||
| e2dbacc77c | |||
| 925c214a1f | |||
| aafd5c3e66 | |||
| 68002a31df | |||
| ec72cd2a11 | |||
| cd739363ae | |||
| f064ffc8a2 | |||
| 6965503bb3 | |||
| 2a9812575c | |||
| 89272fb899 | |||
| 14d15dc355 | |||
| 2b2d6a8303 | |||
| b613e3bd73 | |||
| 62e4f9b85f | |||
| 7b68e92a95 | |||
| 83782d547f | |||
| f1acea14fa | |||
| 5513bdcef4 | |||
| f1f896ba65 | |||
| ca55bbdcae | |||
| e97e714826 | |||
| cb185d7714 | |||
| 4215429114 | |||
| 1195dd4c0e | |||
| a6585a856b | |||
| 5988d98d9a | |||
| 89dcd9f311 | |||
| 0366253134 | |||
| 853a93e142 | |||
| 24dd12c360 | |||
| f52b99f192 | |||
| 6ba774cb26 | |||
| f5542a6114 | |||
| 3c0c1e78dc | |||
| f041b3bc32 | |||
| 3ecb3125d3 | |||
| 272bd00c80 | |||
| 0e9c14b144 | |||
| a9c0c97287 | |||
| cdd7dc430f | |||
| 433c70d389 | |||
| d61c2b39e3 | |||
| 96c3a063cc | |||
| 9dd3696a09 | |||
| 4bef46201b | |||
| fd80283328 | |||
| 2dba14ca58 | |||
| 1391f092d8 | |||
| 88e2535e62 | |||
| de581d878f | |||
| a1f7c66f67 | |||
| 12dbdbb28e | |||
| 687a80924a | |||
| b22ea34032 | |||
| c7fb8ce0bf | |||
| b5f744db12 | |||
| 553333c623 | |||
| 13ee1b0339 | |||
| 3755796ef6 | |||
| e3dbc6c663 | |||
| 8613720a0b | |||
| 0f758616cf | |||
| b2ba2a09e2 | |||
| 2ef1e0df61 | |||
| fe0e7b7794 | |||
| c8e06bf576 | |||
| 7b480acd10 | |||
| 658b29368b | |||
| c64d40fbda | |||
| f6598cfb19 | |||
| 5787c4ca22 |
@@ -56,6 +56,6 @@ Bei Problemen oder Fragen kannst du ein Issue auf GitHub erstellen.
|
||||
|
||||
---
|
||||
|
||||
**Copyright © 2026 - M_Viper - Alle Rechte vorbehalten**
|
||||
## Lizenz
|
||||
|
||||
Die unbefugte Vervielfältigung, Verbreitung oder Weitergabe dieses Plugins ist strafbar und wird rechtlich verfolgt.
|
||||
Dieses Projekt ist Open-Source unter der [GPL Lizenz](LICENSE).
|
||||
|
||||
686
package-lock.json
generated
686
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -1,27 +1,56 @@
|
||||
{
|
||||
"name": "git-manager-gui",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.6",
|
||||
"description": "Git Manager GUI - Verwaltung von Git Repositories",
|
||||
"author": "M_Viper",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "electron-builder"
|
||||
"build": "electron-builder",
|
||||
"test": "node --test __tests__/*.test.js",
|
||||
"pdf": "electron generate-pdf.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.4",
|
||||
"axios": "^1.13.5",
|
||||
"form-data": "^4.0.5",
|
||||
"simple-git": "^3.19.1"
|
||||
"simple-git": "^3.32.3",
|
||||
"archiver": "^6.0.0",
|
||||
"unzipper": "^0.11.0"
|
||||
},
|
||||
"overrides": {
|
||||
"minimatch": "^3.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^26.2.0",
|
||||
"electron": "^41.0.4",
|
||||
"electron-builder": "^26.8.1"
|
||||
},
|
||||
"build": {
|
||||
"asar": true,
|
||||
"appId": "com.viper.gitmanager",
|
||||
"productName": "Git Manager GUI",
|
||||
"files": [
|
||||
"**/*",
|
||||
"!node_modules/.cache",
|
||||
"!**/*.map"
|
||||
"!**/*.map",
|
||||
"!**/.git",
|
||||
"!**/.git/**",
|
||||
"!repos/**",
|
||||
"!data/**",
|
||||
"!backup/**",
|
||||
"!*.zip",
|
||||
"!*.rar",
|
||||
"!*.log"
|
||||
],
|
||||
"extraResources": [
|
||||
"repos/",
|
||||
{
|
||||
"from": "data",
|
||||
"to": "data",
|
||||
"filter": [
|
||||
"**/*",
|
||||
"!credentials.json"
|
||||
]
|
||||
}
|
||||
],
|
||||
"directories": {
|
||||
"buildResources": "assets"
|
||||
|
||||
71
preload.js
71
preload.js
@@ -1,5 +1,5 @@
|
||||
// preload.js — expose IPC to renderer
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
const { contextBridge, ipcRenderer, webUtils } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Lokale Datei-Operationen
|
||||
@@ -20,6 +20,13 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
|
||||
// Gitea Datei-Operationen
|
||||
listGiteaRepos: (data) => ipcRenderer.invoke('list-gitea-repos', data),
|
||||
getGiteaCurrentUser: () => ipcRenderer.invoke('get-gitea-current-user'),
|
||||
getGiteaUserHeatmap: (data) => ipcRenderer.invoke('get-gitea-user-heatmap', data),
|
||||
|
||||
// GitHub Datei-Operationen
|
||||
listGithubRepos: (data) => ipcRenderer.invoke('list-github-repos', data),
|
||||
getGithubCurrentUser: () => ipcRenderer.invoke('get-github-current-user'),
|
||||
getGithubUserHeatmap: (data) => ipcRenderer.invoke('get-github-user-heatmap', data),
|
||||
getGiteaRepoContents: (data) => ipcRenderer.invoke('get-gitea-repo-contents', data),
|
||||
getGiteaFileContent: (data) => ipcRenderer.invoke('get-gitea-file-content', data),
|
||||
readGiteaFile: (data) => ipcRenderer.invoke('read-gitea-file', data),
|
||||
@@ -32,16 +39,42 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
// Repository & Git Management
|
||||
saveCredentials: (data) => ipcRenderer.invoke('save-credentials', data),
|
||||
loadCredentials: () => ipcRenderer.invoke('load-credentials'),
|
||||
getCredentialsStatus: () => ipcRenderer.invoke('get-credentials-status'),
|
||||
testGiteaConnection: (data) => ipcRenderer.invoke('test-gitea-connection', data),
|
||||
testGithubConnection: (data) => ipcRenderer.invoke('test-github-connection', data),
|
||||
updateGiteaAvatar: (data) => ipcRenderer.invoke('update-gitea-avatar', data),
|
||||
updateGiteaRepoAvatar: (data) => ipcRenderer.invoke('update-gitea-repo-avatar', data),
|
||||
updateGiteaRepoVisibility: (data) => ipcRenderer.invoke('update-gitea-repo-visibility', data),
|
||||
updateGiteaRepoTopics: (data) => ipcRenderer.invoke('update-gitea-repo-topics', data),
|
||||
getGiteaTopicsCatalog: () => ipcRenderer.invoke('get-gitea-topics-catalog'),
|
||||
migrateRepoToGitea: (data) => ipcRenderer.invoke('migrate-repo-to-gitea', data),
|
||||
createRepo: (data) => ipcRenderer.invoke('create-repo', data),
|
||||
pushProject: (data) => ipcRenderer.invoke('push-project', data),
|
||||
getBranches: (data) => ipcRenderer.invoke('getBranches', data),
|
||||
getCommitLogs: (data) => ipcRenderer.invoke('getCommitLogs', data),
|
||||
uploadAndPush: (data) => ipcRenderer.invoke('upload-and-push', data),
|
||||
deleteGiteaRepo: (data) => ipcRenderer.invoke('delete-gitea-repo', data),
|
||||
syncRepoToGitHub: (data) => ipcRenderer.invoke('sync-repo-to-github', data),
|
||||
runBatchRepoAction: (data) => ipcRenderer.invoke('run-batch-repo-action', data),
|
||||
validateRepoName: (data) => ipcRenderer.invoke('validate-repo-name', data),
|
||||
checkCloneTargetCollisions: (data) => ipcRenderer.invoke('check-clone-target-collisions', data),
|
||||
|
||||
// Offline/Retry Queue
|
||||
getRetryQueue: () => ipcRenderer.invoke('get-retry-queue'),
|
||||
processRetryQueueNow: () => ipcRenderer.invoke('process-retry-queue-now'),
|
||||
removeRetryQueueItem: (data) => ipcRenderer.invoke('remove-retry-queue-item', data),
|
||||
|
||||
// Drag & Drop
|
||||
prepareDownloadDrag: (data) => ipcRenderer.invoke('prepare-download-drag', data),
|
||||
startNativeDrag: (filePath) => ipcRenderer.send('ondragstart', filePath),
|
||||
getPathType: (filePath) => ipcRenderer.invoke('get-path-type', filePath),
|
||||
getPathForFile: (file) => {
|
||||
try {
|
||||
return webUtils.getPathForFile(file) || '';
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
// Release Management
|
||||
listReleases: (data) => ipcRenderer.invoke('list-releases', data),
|
||||
@@ -74,7 +107,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
saveRecent: (data) => ipcRenderer.invoke('save-recent', data),
|
||||
|
||||
// === UPDATER APIs ===
|
||||
checkForUpdates: () => ipcRenderer.invoke('check-for-updates'),
|
||||
checkForUpdates: (options) => ipcRenderer.invoke('check-for-updates', options || {}),
|
||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||
|
||||
// Triggert den tatsächlichen Download des Assets
|
||||
@@ -88,6 +121,12 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
return () => ipcRenderer.removeListener('update-available', listener);
|
||||
},
|
||||
|
||||
onUpdateNotAvailable: (cb) => {
|
||||
const listener = (event, info) => cb(info);
|
||||
ipcRenderer.on('update-not-available', listener);
|
||||
return () => ipcRenderer.removeListener('update-not-available', listener);
|
||||
},
|
||||
|
||||
onUpdateProgress: (cb) => {
|
||||
const listener = (event, percent) => cb(percent);
|
||||
ipcRenderer.on('update-progress', listener);
|
||||
@@ -110,5 +149,31 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||
ipcRenderer.on('folder-download-progress', listener);
|
||||
return () => ipcRenderer.removeListener('folder-download-progress', listener);
|
||||
}
|
||||
},
|
||||
|
||||
onRetryQueueUpdated: (cb) => {
|
||||
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||
ipcRenderer.on('retry-queue-updated', listener);
|
||||
return () => ipcRenderer.removeListener('retry-queue-updated', listener);
|
||||
},
|
||||
|
||||
onBatchActionProgress: (cb) => {
|
||||
const listener = (event, payload) => { try { cb(payload); } catch (_) {} };
|
||||
ipcRenderer.on('batch-action-progress', listener);
|
||||
return () => ipcRenderer.removeListener('batch-action-progress', listener);
|
||||
},
|
||||
|
||||
// Window Controls
|
||||
windowMinimize: () => ipcRenderer.send('window-minimize'),
|
||||
windowMaximize: () => ipcRenderer.send('window-maximize'),
|
||||
windowClose: () => ipcRenderer.send('window-close'),
|
||||
|
||||
// Autostart
|
||||
setAutostart: (enable) => ipcRenderer.invoke('set-autostart', enable),
|
||||
getAutostart: () => ipcRenderer.invoke('get-autostart'),
|
||||
|
||||
// Utility
|
||||
copyToClipboard: (text) => ipcRenderer.invoke('copy-to-clipboard', text),
|
||||
openExternalUrl: (url) => ipcRenderer.invoke('open-external-url', url),
|
||||
debugToMain: (level, message, payload) => ipcRenderer.send('renderer-debug-log', { level, message, payload })
|
||||
});
|
||||
238
renderer/App.jsx
238
renderer/App.jsx
@@ -4,39 +4,42 @@ import Settings from './Settings.jsx';
|
||||
export default function App() {
|
||||
const [folder, setFolder] = useState('');
|
||||
const [repoName, setRepoName] = useState('');
|
||||
const [platform, setPlatform] = useState('github');
|
||||
const [status, setStatus] = useState('');
|
||||
const [platform, setPlatform] = useState('gitea');
|
||||
const [status, setStatus] = useState('Bereit');
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [branches, setBranches] = useState([]);
|
||||
const [selectedBranch, setSelectedBranch] = useState('master');
|
||||
const [selectedBranch, setSelectedBranch] = useState('main');
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
async function selectFolder() {
|
||||
const selected = await window.electronAPI.selectFolder();
|
||||
if (selected) setFolder(selected);
|
||||
// Branches laden
|
||||
if (selected) {
|
||||
const branchList = await window.electronAPI.getBranches({ folder: selected });
|
||||
setBranches(branchList);
|
||||
if (branchList.includes('master')) setSelectedBranch('master');
|
||||
}
|
||||
if (!selected) return;
|
||||
setFolder(selected);
|
||||
const branchList = await window.electronAPI.getBranches({ folder: selected });
|
||||
setBranches(branchList);
|
||||
if (branchList.includes('main')) setSelectedBranch('main');
|
||||
else if (branchList.includes('master')) setSelectedBranch('master');
|
||||
else if (branchList.length > 0) setSelectedBranch(branchList[0]);
|
||||
}
|
||||
|
||||
async function createRepoHandler() {
|
||||
if (!repoName) return alert('Repo Name required!');
|
||||
setStatus('Creating repository...');
|
||||
if (!repoName) return alert('Repo-Name erforderlich!');
|
||||
setStatus('Repository wird erstellt…');
|
||||
const result = await window.electronAPI.createRepo({ name: repoName, platform });
|
||||
setStatus(result ? 'Repository created!' : 'Failed to create repository.');
|
||||
setStatus(result ? 'Repository erstellt!' : 'Fehler beim Erstellen des Repositories.');
|
||||
}
|
||||
|
||||
async function pushProjectHandler() {
|
||||
if (!folder) return alert('Select a project folder first!');
|
||||
setStatus('Pushing project...');
|
||||
if (!folder) return alert('Bitte zuerst einen Projektordner auswählen!');
|
||||
setStatus('Projekt wird gepusht…');
|
||||
setProgress(0);
|
||||
const onProgress = (p) => setProgress(p); // Callback für Fortschritt
|
||||
const result = await window.electronAPI.pushProject({ folder, branch: selectedBranch, onProgress });
|
||||
setStatus(result ? 'Project pushed!' : 'Failed to push project.');
|
||||
const result = await window.electronAPI.pushProject({
|
||||
folder,
|
||||
branch: selectedBranch,
|
||||
onProgress: (p) => setProgress(p),
|
||||
});
|
||||
setStatus(result ? 'Projekt gepusht!' : 'Push fehlgeschlagen.');
|
||||
if (result) {
|
||||
const logList = await window.electronAPI.getCommitLogs({ folder });
|
||||
setLogs(logList);
|
||||
@@ -44,61 +47,168 @@ export default function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<h1>Git Manager GUI - High-End</h1>
|
||||
<button onClick={() => setShowSettings(!showSettings)}>Settings</button>
|
||||
{showSettings && <Settings />}
|
||||
<div id="app" style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<label>Platform:</label>
|
||||
<select value={platform} onChange={e => setPlatform(e.target.value)}>
|
||||
<option value="github">GitHub</option>
|
||||
<option value="gitea">Gitea</option>
|
||||
</select>
|
||||
{/* ── Toolbar ── */}
|
||||
<div id="toolbar">
|
||||
<div className="toolbar-row toolbar-row--top">
|
||||
<div className="toolbar-brand">
|
||||
<div className="toolbar-brand-mark">
|
||||
<img src="./icon.png" alt="Git Manager Logo" className="toolbar-brand-logo" />
|
||||
</div>
|
||||
<div className="toolbar-brand-copy">
|
||||
<span className="toolbar-kicker">Workspace Control</span>
|
||||
<strong>Git Manager Explorer Pro</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="toolbar-top-actions">
|
||||
<div className="tool-group tool-group--quick-actions">
|
||||
<button onClick={createRepoHandler} title="Neues Repository erstellen">🚀 New Repo</button>
|
||||
<button onClick={pushProjectHandler} title="Projekt pushen">⬆️ Push</button>
|
||||
</div>
|
||||
|
||||
<div className="tool-group tool-group--utility">
|
||||
<button onClick={() => setShowSettings(true)} title="Einstellungen">⚙️ Settings</button>
|
||||
</div>
|
||||
|
||||
<div className="toolbar-status-wrap">
|
||||
<span className="status-dot" aria-hidden="true" />
|
||||
<span className="status">{status}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="toolbar-row toolbar-row--bottom">
|
||||
<div className="tool-group tool-group--workspace">
|
||||
<button className="accent-btn" onClick={selectFolder} title="Lokalen Ordner öffnen">
|
||||
📂 Open Local
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tool-group tool-group--repo">
|
||||
<div className="platform-switch" role="tablist" aria-label="Plattform auswählen">
|
||||
{['gitea', 'github'].map(p => (
|
||||
<button
|
||||
key={p}
|
||||
type="button"
|
||||
className={`platform-option${platform === p ? ' active' : ''}`}
|
||||
data-platform={p}
|
||||
aria-pressed={platform === p}
|
||||
onClick={() => setPlatform(p)}
|
||||
>
|
||||
{p === 'gitea' ? 'Gitea' : 'GitHub'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{branches.length > 0 && (
|
||||
<select
|
||||
value={selectedBranch}
|
||||
onChange={e => setSelectedBranch(e.target.value)}
|
||||
title="Branch auswählen"
|
||||
>
|
||||
{branches.map(b => <option key={b} value={b}>{b}</option>)}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<button onClick={selectFolder}>Select Project Folder</button>
|
||||
<span style={{ marginLeft: 10 }}>{folder}</span>
|
||||
</div>
|
||||
{/* ── Main ── */}
|
||||
<main id="main">
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<label>Branch:</label>
|
||||
<select value={selectedBranch} onChange={e => setSelectedBranch(e.target.value)}>
|
||||
{branches.map(b => <option key={b} value={b}>{b}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
{/* Fortschrittsbalken */}
|
||||
{progress > 0 && progress < 100 && (
|
||||
<div className="input-group" style={{ marginBottom: 20 }}>
|
||||
<label>Fortschritt</label>
|
||||
<progress value={progress} max="100" style={{ width: '100%', height: 8, borderRadius: 4 }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Repository Name"
|
||||
value={repoName}
|
||||
onChange={e => setRepoName(e.target.value)} />
|
||||
<button onClick={createRepoHandler} style={{ marginLeft: 10 }}>Create Repo</button>
|
||||
</div>
|
||||
{/* Repo erstellen / Ordner */}
|
||||
<div className="card" style={{ marginBottom: 20 }}>
|
||||
<h2>📁 Projekt & Repository</h2>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<button onClick={pushProjectHandler}>Push / Update Project</button>
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<label>Lokaler Projektordner</label>
|
||||
<div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
|
||||
<input
|
||||
type="text"
|
||||
readOnly
|
||||
value={folder}
|
||||
placeholder="Noch kein Ordner ausgewählt…"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<button onClick={selectFolder} style={{ flex: '0 0 auto' }}>📂 Auswählen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<label>Progress:</label>
|
||||
<progress value={progress} max="100" style={{ width: '100%' }} />
|
||||
</div>
|
||||
<div className="input-group">
|
||||
<label>Repository Name</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="mein-projekt"
|
||||
value={repoName}
|
||||
onChange={e => setRepoName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<strong>Status: </strong>{status}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10, marginTop: 8 }}>
|
||||
<button className="accent-btn" onClick={createRepoHandler}>🚀 Repo erstellen</button>
|
||||
<button onClick={pushProjectHandler}>⬆️ Push / Aktualisieren</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 20 }}>
|
||||
<h3>Commit Logs:</h3>
|
||||
<ul>
|
||||
{logs.map((log, i) => (
|
||||
<li key={i}>{log}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
{/* Commit-Logs */}
|
||||
{logs.length > 0 && (
|
||||
<div className="card">
|
||||
<h2>📊 Commit-Verlauf</h2>
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{logs.map((log, i) => (
|
||||
<li
|
||||
key={i}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
border: '1px solid rgba(140,173,255,0.08)',
|
||||
fontSize: 13,
|
||||
color: 'var(--text-secondary)',
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
>
|
||||
{log}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{logs.length === 0 && !folder && (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingTop: 60,
|
||||
gap: 16,
|
||||
opacity: 0.55,
|
||||
}}>
|
||||
<div style={{ fontSize: 64, filter: 'drop-shadow(0 8px 16px rgba(88,213,255,0.2))' }}>📂</div>
|
||||
<p style={{ color: 'var(--text-muted)', fontSize: 15, textAlign: 'center' }}>
|
||||
Öffne einen lokalen Ordner oder lade Repos über die Toolbar.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* ── Settings-Modal ── */}
|
||||
{showSettings && (
|
||||
<div id="settingsModal" className="modal" onClick={e => { if (e.target === e.currentTarget) setShowSettings(false); }}>
|
||||
<Settings onClose={() => setShowSettings(false)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
export default function Settings() {
|
||||
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();
|
||||
if (!value) return { ok: true, value: '' };
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = new URL(value);
|
||||
} catch (_) {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'Ungültige Gitea-URL. Beispiel für IPv6: http://[2001:db8::1]:3000',
|
||||
};
|
||||
}
|
||||
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'Die Gitea-URL muss mit http:// oder https:// beginnen.',
|
||||
};
|
||||
}
|
||||
|
||||
return { ok: true, value: value.replace(/\/$/, '') };
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.electronAPI.loadCredentials().then(data => {
|
||||
@@ -11,31 +39,145 @@ export default function Settings() {
|
||||
setGithubToken(data.githubToken || '');
|
||||
setGiteaToken(data.giteaToken || '');
|
||||
setGiteaURL(data.giteaURL || '');
|
||||
setAvatarB64(data.avatarB64 || null);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const save = () => {
|
||||
window.electronAPI.saveCredentials({ githubToken, giteaToken, giteaURL });
|
||||
alert('Settings saved securely!');
|
||||
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,
|
||||
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);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<h2>Settings</h2>
|
||||
<div>
|
||||
<label>GitHub Token:</label>
|
||||
<input type="password" value={githubToken} onChange={e => setGithubToken(e.target.value)} />
|
||||
<div className="modalContent card settings-modal-content">
|
||||
|
||||
<div className="settings-header">
|
||||
<div className="settings-header-inner">
|
||||
<div className="settings-avatar-wrap" onClick={() => fileInputRef.current?.click()} title="Profilbild ändern">
|
||||
{avatarB64
|
||||
? <img src={avatarB64} alt="Avatar" className="settings-avatar-img" />
|
||||
: <div className="settings-avatar-placeholder">👤</div>
|
||||
}
|
||||
<div className="settings-avatar-overlay">✏️</div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleAvatarFileChange}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="settings-eyebrow">Konfiguration</div>
|
||||
<h2>⚙️ Einstellungen</h2>
|
||||
<p className="settings-subtitle">
|
||||
Zugangsdaten für GitHub und Gitea hinterlegen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>Gitea Token:</label>
|
||||
<input type="password" value={giteaToken} onChange={e => setGiteaToken(e.target.value)} />
|
||||
|
||||
<div className="settings-layout">
|
||||
<div className="settings-column settings-column--left">
|
||||
<section className="settings-panel settings-panel--credentials">
|
||||
<div className="settings-panel-header">
|
||||
<div>
|
||||
<h3>Zugangsdaten</h3>
|
||||
<p>API-Zugriffe für GitHub und Gitea konfigurieren.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-fields-grid">
|
||||
<div className="input-group">
|
||||
<label htmlFor="react-githubToken">GitHub Token</label>
|
||||
<input
|
||||
id="react-githubToken"
|
||||
type="password"
|
||||
placeholder="ghp_…"
|
||||
value={githubToken}
|
||||
onChange={e => setGithubToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="input-group">
|
||||
<label htmlFor="react-giteaToken">Gitea Token</label>
|
||||
<input
|
||||
id="react-giteaToken"
|
||||
type="password"
|
||||
placeholder="Token hier einfügen"
|
||||
value={giteaToken}
|
||||
onChange={e => setGiteaToken(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="input-group input-group--wide">
|
||||
<label htmlFor="react-giteaURL">Gitea URL</label>
|
||||
<input
|
||||
id="react-giteaURL"
|
||||
type="text"
|
||||
placeholder="https://gitea.example.com"
|
||||
value={giteaURL}
|
||||
onChange={e => setGiteaURL(e.target.value)}
|
||||
/>
|
||||
<div className="settings-connection-tools">
|
||||
<div className="settings-inline-hint">
|
||||
Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>Gitea URL:</label>
|
||||
<input type="text" value={giteaURL} onChange={e => setGiteaURL(e.target.value)} />
|
||||
|
||||
<div className="modal-buttons settings-modal-actions">
|
||||
<button
|
||||
className="accent-btn"
|
||||
onClick={save}
|
||||
disabled={avatarUploading}
|
||||
style={savedOk ? { background: 'var(--success)', borderColor: 'var(--success)' } : {}}
|
||||
>
|
||||
{avatarUploading ? '⏳ Bild wird hochgeladen…' : savedOk ? '✅ Gespeichert' : 'Speichern'}
|
||||
</button>
|
||||
{onClose && (
|
||||
<button className="secondary" onClick={onClose}>Abbrechen</button>
|
||||
)}
|
||||
</div>
|
||||
<button onClick={save} style={{ marginTop: 10 }}>Save</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
renderer/icon.png
Normal file
BIN
renderer/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
@@ -4,97 +4,285 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>Git Manager Explorer Pro</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https: http:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';">
|
||||
<link rel="icon" type="image/png" href="./icon.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="toolbar">
|
||||
<div class="tool-group">
|
||||
<button id="btnSettings" title="Einstellungen">⚙️ Settings</button>
|
||||
<button id="btnBack" class="secondary hidden" title="Zurück">⬅️ Zurück</button>
|
||||
<button id="btnSelectFolder" class="accent-btn" title="Lokalen Ordner öffnen">📂 Open Local</button>
|
||||
<button id="btnLoadGiteaRepos" class="accent-btn" title="Gitea Repositories laden">🌐 Load Gitea</button>
|
||||
<!-- Titelbalken-Streifen -->
|
||||
<div id="titlebar-strip">
|
||||
<div class="titlebar-strip-brand">
|
||||
<img src="./icon.png" alt="" class="titlebar-strip-icon">
|
||||
<span class="titlebar-strip-title">Git Manager Explorer Pro</span>
|
||||
</div>
|
||||
|
||||
<div class="tool-group">
|
||||
<select id="platform" title="Plattform auswählen">
|
||||
<option value="gitea" selected>Gitea</option>
|
||||
<option value="github">GitHub</option>
|
||||
</select>
|
||||
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
|
||||
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
|
||||
<button id="btnCommits" class="hidden" title="Commit History anzeigen">📊 Commits</button>
|
||||
<button id="btnReleases" class="hidden" title="Releases anzeigen">📦 Releases</button>
|
||||
<div class="win-controls" aria-label="Fenster-Steuerung">
|
||||
<button class="win-btn win-btn--minimize" id="btnWinMinimize" title="Minimieren">
|
||||
<svg width="10" height="1" viewBox="0 0 10 1"><rect width="10" height="1" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<button class="win-btn win-btn--maximize" id="btnWinMaximize" title="Maximieren">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><rect x="0.5" y="0.5" width="9" height="9" rx="1" fill="none" stroke="currentColor"/></svg>
|
||||
</button>
|
||||
<button class="win-btn win-btn--close" id="btnWinClose" title="Schließen">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10"><line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span id="status" class="status">Bereit</span>
|
||||
</div>
|
||||
|
||||
<main id="main">
|
||||
<div id="explorerGrid" class="explorer-grid">
|
||||
<div id="toolbar">
|
||||
<div class="toolbar-row toolbar-row--top">
|
||||
<div class="toolbar-brand" aria-label="App Kopfbereich">
|
||||
<div class="toolbar-brand-mark">
|
||||
<img src="./icon.png" alt="Git Manager Logo" class="toolbar-brand-logo">
|
||||
</div>
|
||||
<div class="toolbar-brand-copy">
|
||||
<span class="toolbar-kicker">Workspace Control</span>
|
||||
<strong>Git Manager Explorer Pro</strong>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="toolbar-top-actions">
|
||||
<div class="tool-group tool-group--quick-actions">
|
||||
<button id="btnOpenRepoActions" title="Neues Repository erstellen">🚀 New Repo</button>
|
||||
<button id="btnOpenMigration" title="Repository von GitHub/GitLab zu Gitea migrieren">📥 Migrieren</button>
|
||||
<button id="btnPush" title="Projekt pushen">⬆️ Push</button>
|
||||
</div>
|
||||
|
||||
<div class="tool-group tool-group--utility">
|
||||
<span class="tool-group-title">Steuerung</span>
|
||||
<button id="btnSettings" title="Einstellungen">⚙️ Settings</button>
|
||||
<button id="btnBatchActions" title="Batch-Aktionen">🧩 Batch</button>
|
||||
<button id="btnOpenActivityLog" title="Aktivitätsprotokoll">📝 Activity</button>
|
||||
<button id="btnRetryQueueNow" class="secondary" title="Retry-Queue jetzt verarbeiten">🔁 Queue (0)</button>
|
||||
<button id="btnBack" class="secondary hidden" title="Zurück">⬅️ Zurück</button>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-status-wrap">
|
||||
<span class="status-dot" aria-hidden="true"></span>
|
||||
<span id="status" class="status">Bereit</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-row toolbar-row--bottom">
|
||||
<div class="tool-group tool-group--workspace">
|
||||
<span class="tool-group-title">Quelle</span>
|
||||
<button id="btnSelectFolder" class="accent-btn" title="Lokalen Ordner öffnen">📂 Open Local</button>
|
||||
<button id="btnLoadGiteaRepos" class="accent-btn" title="Projekte der gewählten Plattform laden">🌐 Load Projekte</button>
|
||||
</div>
|
||||
|
||||
<div class="tool-group tool-group--repo">
|
||||
<span class="tool-group-title">Repository</span>
|
||||
<input id="platform" type="hidden" value="gitea">
|
||||
<div class="platform-switch" role="tablist" aria-label="Plattform auswählen">
|
||||
<button type="button" class="platform-option active" data-platform="gitea" aria-pressed="true">Gitea</button>
|
||||
<button type="button" class="platform-option" data-platform="github" aria-pressed="false">GitHub</button>
|
||||
</div>
|
||||
<button id="btnCommits" class="hidden" title="Commit History anzeigen">📊 Commits</button>
|
||||
<button id="btnReleases" class="hidden" title="Releases anzeigen">📦 Releases</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="contentArea" class="content-area">
|
||||
<aside id="favHistorySidebar" class="fav-history-sidebar" aria-label="Favoriten und Verlauf"></aside>
|
||||
<main id="main">
|
||||
<div id="explorerGrid" class="explorer-grid"></div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div id="settingsModal" class="modal hidden">
|
||||
<div class="modalContent card">
|
||||
<h2>⚙️ Einstellungen</h2>
|
||||
|
||||
<div class="input-group">
|
||||
<label>GitHub Token</label>
|
||||
<input id="githubToken" type="password" placeholder="ghp_...">
|
||||
<div class="modalContent card settings-modal-content">
|
||||
<button id="btnSettingsWatermark" class="settings-watermark-btn" title="Projektinformationen anzeigen" aria-label="Projektinformationen anzeigen">ⓘ</button>
|
||||
<div id="settingsWatermarkCard" class="settings-watermark-card hidden" role="dialog" aria-label="Projektinformationen">
|
||||
<h4>Projektinformationen</h4>
|
||||
<div class="settings-watermark-row"><span>Ersteller:</span><strong>M_Viper</strong></div>
|
||||
<div class="settings-watermark-row"><span>Webseite:</span><a href="https://m-viper.de" target="_blank" rel="noopener noreferrer">https://m-viper.de</a></div>
|
||||
<div class="settings-watermark-row"><span>Discord:</span><a id="watermarkDiscord" href="https://discord.com/invite/FdRs4BRd8D" target="_blank" rel="noopener noreferrer">discord.com/invite/FdRs4BRd8D</a></div>
|
||||
<div class="settings-watermark-row"><span>E-Mail:</span><a id="watermarkMail" href="mailto:admin@m-viper.de">admin@m-viper.de</a></div>
|
||||
<div class="settings-watermark-row"><span>Version:</span><strong id="watermarkVersion">-</strong></div>
|
||||
<div class="settings-watermark-row"><span>Copyright:</span><strong id="watermarkCopyright">-</strong></div>
|
||||
<div class="settings-watermark-row"><span>Projekt:</span><strong>Git Manager Explorer Pro</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Gitea Token</label>
|
||||
<input id="giteaToken" type="password" placeholder="Token hier einfügen">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Gitea URL</label>
|
||||
<input id="giteaURL" type="text" placeholder="https://gitea.example.com">
|
||||
</div>
|
||||
|
||||
<div class="input-group" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
<label style="margin-bottom: 12px;">Übersicht</label>
|
||||
<div style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; text-transform: none; letter-spacing: normal; font-weight: normal;">
|
||||
<input type="checkbox" id="settingFavorites" checked>
|
||||
<span>⭐ Favoriten-Bereich anzeigen</span>
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; text-transform: none; letter-spacing: normal; font-weight: normal;">
|
||||
<input type="checkbox" id="settingRecent" checked>
|
||||
<span>🕐 Zuletzt geöffnet anzeigen</span>
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; text-transform: none; letter-spacing: normal; font-weight: normal;">
|
||||
<input type="checkbox" id="settingCompact">
|
||||
<span>⊞ Kompakt-Modus (kleinere Karten)</span>
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer; text-transform: none; letter-spacing: normal; font-weight: normal;">
|
||||
<input type="checkbox" id="settingColoredIcons" checked>
|
||||
<span>🎨 Farbige Datei-Icons</span>
|
||||
</label>
|
||||
|
||||
<div class="settings-header">
|
||||
<div class="settings-header-inner">
|
||||
<div class="settings-avatar-wrap" id="settingsAvatarWrap" title="Profilbild ändern">
|
||||
<img id="settingsAvatarImg" class="settings-avatar-img" src="" alt="Avatar" style="display:none;">
|
||||
<div id="settingsAvatarPlaceholder" class="settings-avatar-placeholder">👤</div>
|
||||
<div class="settings-avatar-overlay">✏️</div>
|
||||
<input id="settingsAvatarInput" type="file" accept="image/*" style="display:none;">
|
||||
</div>
|
||||
<button id="btnUploadAvatar" class="settings-avatar-upload-btn" title="Gespeichertes Bild jetzt auf Gitea hochladen">📤 Auf Gitea aktualisieren</button>
|
||||
<div>
|
||||
<div class="settings-eyebrow">Konfiguration</div>
|
||||
<h2>⚙️ Einstellungen</h2>
|
||||
<p class="settings-subtitle">Alle wichtigen Optionen auf einer Seite: Zugangsdaten, Verbindungscheck, Darstellung und Updates.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group" style="margin-top: 30px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1);">
|
||||
<label>App Version</label>
|
||||
<div style="display: flex; gap: 12px; align-items: center;">
|
||||
<input id="appVersion" type="text" readonly style="flex: 1; background: rgba(255,255,255,0.05); cursor: not-allowed;">
|
||||
<button id="btnCheckUpdates" style="
|
||||
background: linear-gradient(135deg, #00d4ff, #8b5cf6);
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
">🔄 Nach Updates suchen</button>
|
||||
<div class="settings-layout">
|
||||
<div class="settings-column settings-column--left">
|
||||
<section class="settings-panel settings-panel--credentials">
|
||||
<div class="settings-panel-header">
|
||||
<div>
|
||||
<h3>Zugangsdaten</h3>
|
||||
<p>API-Zugriffe für GitHub und Gitea konfigurieren.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-credentials-grid">
|
||||
<article class="settings-auth-card settings-auth-card--github">
|
||||
<div class="settings-auth-card-header">
|
||||
<h4>GitHub</h4>
|
||||
<button id="btnTestGithubConnection" class="secondary" type="button">🔌 Verbindung testen</button>
|
||||
</div>
|
||||
<div class="input-group settings-auth-input">
|
||||
<label for="githubToken">GitHub Token</label>
|
||||
<input id="githubToken" type="password" placeholder="ghp_...">
|
||||
</div>
|
||||
<div class="settings-auth-spacer" aria-hidden="true">GitHub benötigt keine Server-URL</div>
|
||||
<div id="githubTokenHint" class="settings-inline-hint">Hinweis: Der Token wird direkt über api.github.com geprüft.</div>
|
||||
</article>
|
||||
|
||||
<article class="settings-auth-card settings-auth-card--gitea">
|
||||
<div class="settings-auth-card-header">
|
||||
<h4>Gitea</h4>
|
||||
<button id="btnTestGiteaConnection" class="secondary" type="button">🔌 Verbindung testen</button>
|
||||
</div>
|
||||
<div class="input-group settings-auth-input">
|
||||
<label for="giteaToken">Gitea Token</label>
|
||||
<input id="giteaToken" type="password" placeholder="Token hier einfügen">
|
||||
</div>
|
||||
<div class="input-group settings-auth-input">
|
||||
<label for="giteaURL">Gitea URL</label>
|
||||
<input id="giteaURL" type="text" placeholder="https://gitea.example.com">
|
||||
</div>
|
||||
<div id="giteaUrlHint" class="settings-inline-hint">Hinweis: IPv6 mit Klammern eingeben, z.B. http://[2001:db8::1]:3000</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-panel settings-panel--display">
|
||||
<div class="settings-panel-header">
|
||||
<div>
|
||||
<h3>Darstellung</h3>
|
||||
<p>Übersicht und Explorer an deinen Arbeitsstil anpassen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-toggle-list">
|
||||
<label class="settings-toggle-row" for="settingFavorites">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">⭐ Favoriten-Bereich anzeigen</span>
|
||||
<span class="settings-toggle-desc">Pinnt wichtige Repositories und Ordner sichtbar im Kopfbereich.</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="settingFavorites" checked>
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="settings-toggle-row" for="settingRecent">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">🕐 Zuletzt geöffnet anzeigen</span>
|
||||
<span class="settings-toggle-desc">Zeigt deine letzten Projekte direkt in der Übersicht an.</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="settingRecent" checked>
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="settings-toggle-row" for="settingCompact">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">⊞ Kompakt-Modus</span>
|
||||
<span class="settings-toggle-desc">Verdichtet Karten und Abstände für kleinere Fenster.</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="settingCompact">
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="settings-toggle-row" for="settingColoredIcons">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">🎨 Farbige Datei-Icons</span>
|
||||
<span class="settings-toggle-desc">Setzt stärkere Dateityp-Farben für schnellere Orientierung.</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="settingColoredIcons" checked>
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="settings-column settings-column--right">
|
||||
<section class="settings-panel settings-panel--health">
|
||||
<div class="settings-panel-header">
|
||||
<div>
|
||||
<h3>Verbindungsstatus</h3>
|
||||
<p>Direkt sehen, ob URL, API und Auth sauber antworten.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-health-box">
|
||||
<div class="settings-health-row"><span>URL</span><strong id="healthUrl">Unbekannt</strong></div>
|
||||
<div class="settings-health-row"><span>API</span><strong id="healthApi">Unbekannt</strong></div>
|
||||
<div class="settings-health-row"><span>Auth</span><strong id="healthAuth">Unbekannt</strong></div>
|
||||
<div class="settings-health-row"><span>Latenz</span><strong id="healthLatency">-</strong></div>
|
||||
<div class="settings-health-row"><span>Server</span><strong id="healthVersion">-</strong></div>
|
||||
<div class="settings-health-row"><span>Letzter Fehler</span><strong id="healthLastError">-</strong></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-panel settings-panel--system">
|
||||
<div class="settings-panel-header">
|
||||
<div>
|
||||
<h3>System</h3>
|
||||
<p>Verhalten beim Windows-Start steuern.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-toggle-list">
|
||||
<label class="settings-toggle-row" for="settingAutostart">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">🚀 Mit Windows starten</span>
|
||||
<span class="settings-toggle-desc">Startet die App automatisch beim Anmelden, minimiert im System-Tray.</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="settingAutostart">
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-panel settings-panel--app">
|
||||
<div class="settings-panel-header">
|
||||
<div>
|
||||
<h3>App & Updates</h3>
|
||||
<p>Version prüfen und neue Releases direkt anstoßen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-version-card">
|
||||
<div class="input-group input-group--wide settings-version-field">
|
||||
<label for="appVersion">App Version</label>
|
||||
<input id="appVersion" class="settings-readonly-input" type="text" readonly>
|
||||
</div>
|
||||
<button id="btnCheckUpdates" class="settings-update-btn">🔄 Nach Updates suchen</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button id="btnSaveSettings">Speichern</button>
|
||||
|
||||
<div class="modal-buttons settings-modal-actions">
|
||||
<button id="btnSaveSettings" class="accent-btn">Speichern</button>
|
||||
<button id="btnCloseSettings" class="secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,6 +295,7 @@
|
||||
<div class="input-group">
|
||||
<label>Repository Name</label>
|
||||
<input id="repoName" type="text" placeholder="mein-projekt">
|
||||
<div id="repoNameValidationHint" class="settings-inline-hint">Name prüfen: Duplikate, ähnliche Namen und ungültige Zeichen werden erkannt.</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
@@ -147,6 +336,134 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Migration Modal -->
|
||||
<div id="migrationModal" class="modal hidden">
|
||||
<div class="modalContent card migration-modal-content">
|
||||
<h2>📥 Repository migrieren</h2>
|
||||
<p class="settings-subtitle">Ein Repository von GitHub, GitLab oder einer anderen Git-Quelle auf deine Gitea-Instanz kopieren.</p>
|
||||
|
||||
<div class="migration-fields">
|
||||
<div class="input-group">
|
||||
<label for="migrateCloneUrl">Quell-URL (Clone-URL)</label>
|
||||
<input id="migrateCloneUrl" type="text" placeholder="https://github.com/benutzer/repo.git">
|
||||
<div class="settings-inline-hint">Beispiel: https://github.com/M_Viper/NexTrade.git</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="migrateRepoName">Neuer Repository-Name auf Gitea</label>
|
||||
<input id="migrateRepoName" type="text" placeholder="NexTrade">
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label for="migrateDescription">Beschreibung (optional)</label>
|
||||
<input id="migrateDescription" type="text" placeholder="">
|
||||
</div>
|
||||
|
||||
<div class="migration-row-split">
|
||||
<div class="input-group">
|
||||
<label for="migrateAuthUsername">Auth-Benutzername (bei privaten Repos)</label>
|
||||
<input id="migrateAuthUsername" type="text" placeholder="GitHub-Benutzername">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="migrateAuthToken">Auth-Token (bei privaten Repos)</label>
|
||||
<input id="migrateAuthToken" type="password" placeholder="ghp_…">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="settings-toggle-row" style="margin-top:4px;" for="migratePrivate">
|
||||
<span class="settings-toggle-info">
|
||||
<span class="settings-toggle-title">🔒 Privates Repository</span>
|
||||
</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" id="migratePrivate">
|
||||
<span class="toggle-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="migrationStatus" class="migration-status hidden"></div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button id="btnStartMigration" class="accent-btn">📥 Migration starten</button>
|
||||
<button id="btnCloseMigration" class="secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="batchActionModal" class="modal hidden">
|
||||
<div class="modalContent card">
|
||||
<h2>🧩 Batch-Aktionen</h2>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Aktion</label>
|
||||
<select id="batchActionType">
|
||||
<option value="refresh">Repos aktualisieren</option>
|
||||
<option value="clone">Repos klonen</option>
|
||||
<option value="create-tag">Tag erstellen</option>
|
||||
<option value="create-release">Release erstellen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label>Repositories (pro Zeile: owner/repo)</label>
|
||||
<textarea id="batchRepoList" class="batch-textarea" placeholder="M_Viper/ProjektA M_Viper/ProjektB"></textarea>
|
||||
</div>
|
||||
|
||||
<div id="batchCloneGroup" class="input-group hidden">
|
||||
<label>Zielordner für Clone</label>
|
||||
<div class="batch-inline-row">
|
||||
<input id="batchCloneTarget" type="text" readonly placeholder="Bitte Zielordner auswählen">
|
||||
<button id="btnSelectBatchCloneTarget" class="secondary">📁 Wählen</button>
|
||||
</div>
|
||||
<div id="batchCloneValidationHint" class="settings-inline-hint">Kollisionsprüfung aktiv: vorhandene Zielordner und Namenskonflikte werden angezeigt.</div>
|
||||
</div>
|
||||
|
||||
<div id="batchTagGroup" class="input-group hidden">
|
||||
<label>Tag</label>
|
||||
<input id="batchTagName" type="text" placeholder="v1.0.0">
|
||||
</div>
|
||||
|
||||
<div id="batchReleaseNameGroup" class="input-group hidden">
|
||||
<label>Release-Name</label>
|
||||
<input id="batchReleaseName" type="text" placeholder="Release v1.0.0">
|
||||
</div>
|
||||
|
||||
<div id="batchReleaseBodyGroup" class="input-group hidden">
|
||||
<label>Release-Text</label>
|
||||
<textarea id="batchReleaseBody" class="batch-textarea" placeholder="Changelog..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button id="btnRunBatchAction" class="accent-btn">Ausführen</button>
|
||||
<button id="btnCloseBatchAction" class="secondary">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="activityLogModal" class="modal hidden">
|
||||
<div class="modalContent card">
|
||||
<h2>📝 Aktivitätsprotokoll</h2>
|
||||
|
||||
<div class="activity-toolbar">
|
||||
<select id="activityFilterLevel">
|
||||
<option value="all">Alle</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="warning">Warn</option>
|
||||
<option value="error">Error</option>
|
||||
</select>
|
||||
<button id="btnRetryQueueRefresh" class="secondary">🔁 Queue jetzt retry</button>
|
||||
<button id="btnClearActivityLog" class="secondary">🧹 Log leeren</button>
|
||||
</div>
|
||||
|
||||
<div id="activityQueueInfo" class="activity-queue-info">Retry-Queue: 0</div>
|
||||
<div id="activityLogList" class="activity-log-list"></div>
|
||||
|
||||
<div class="modal-buttons">
|
||||
<button id="btnCloseActivityLog" class="secondary">Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="fileEditorModal" class="modal hidden">
|
||||
<div class="file-editor-card">
|
||||
<div class="file-editor-header">
|
||||
@@ -197,7 +514,7 @@
|
||||
<div style="display: flex; align-items: center; gap: 20px; margin-bottom: 20px;">
|
||||
<div style="font-size: 3rem; filter: drop-shadow(0 0 10px var(--accent-primary));">🚀</div>
|
||||
<div>
|
||||
<h2 style="margin: 0; background: var(--accent-gradient); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Update verfügbar!</h2>
|
||||
<h2 style="margin: 0; background: var(--accent-gradient); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Update verfügbar!</h2>
|
||||
<p id="updateVersionInfo" style="color: var(--text-secondary); margin: 5px 0 0 0; font-family: monospace;"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
3995
renderer/renderer.js
3995
renderer/renderer.js
File diff suppressed because it is too large
Load Diff
2118
renderer/style.css
2118
renderer/style.css
File diff suppressed because it is too large
Load Diff
183
src/backup/BackupManager.js
Normal file
183
src/backup/BackupManager.js
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* BackupManager — Orchestrates backup operations
|
||||
*/
|
||||
|
||||
const archiver = require('archiver')
|
||||
const { createReadStream, createWriteStream } = require('fs')
|
||||
const { mkdir } = require('fs').promises
|
||||
const path = require('path')
|
||||
const { Transform } = require('stream')
|
||||
|
||||
class BackupManager {
|
||||
constructor(provider) {
|
||||
this.provider = provider
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a backup from a project folder
|
||||
* @param {string} projectPath - Path to project folder
|
||||
* @param {string} repoName - Repository name (used for filenames)
|
||||
* @returns {Promise<{filename, size, timestamp}>}
|
||||
*/
|
||||
async createBackup(projectPath, repoName) {
|
||||
try {
|
||||
// Create ZIP buffer
|
||||
const buffer = await this._createZip(projectPath)
|
||||
const timestamp = this._getTimestamp()
|
||||
const filename = `${repoName}-backup-${timestamp}.zip`
|
||||
|
||||
// Upload to provider
|
||||
const result = await this.provider.uploadBackup(buffer, filename)
|
||||
|
||||
// Cleanup old backups
|
||||
await this._cleanupOldBackups(repoName)
|
||||
|
||||
return {
|
||||
filename,
|
||||
size: result.size || buffer.length,
|
||||
timestamp
|
||||
}
|
||||
} catch (err) {
|
||||
throw new Error(`Backup creation failed: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List all backups for a repository
|
||||
* @param {string} repoName - Repository name
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async listBackups(repoName) {
|
||||
try {
|
||||
const backups = await this.provider.listBackups()
|
||||
return backups
|
||||
.filter(b => b.name.startsWith(repoName))
|
||||
.sort((a, b) => new Date(b.date || b.name) - new Date(a.date || a.name))
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to list backups: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a backup to a target folder
|
||||
* @param {string} repoName - Repository name
|
||||
* @param {string} filename - Backup filename
|
||||
* @param {string} targetPath - Target folder path
|
||||
*/
|
||||
async restoreBackup(repoName, filename, targetPath) {
|
||||
try {
|
||||
// Download backup
|
||||
const buffer = await this.provider.downloadBackup(filename)
|
||||
|
||||
// Extract ZIP
|
||||
await this._extractZip(buffer, targetPath)
|
||||
|
||||
return { ok: true, restored: filename }
|
||||
} catch (err) {
|
||||
throw new Error(`Restore failed: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a backup
|
||||
* @param {string} filename - Backup filename
|
||||
*/
|
||||
async deleteBackup(filename) {
|
||||
try {
|
||||
await this.provider.deleteBackup(filename)
|
||||
return { ok: true }
|
||||
} catch (err) {
|
||||
throw new Error(`Delete failed: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PRIVATE METHODS ====================
|
||||
|
||||
/**
|
||||
* Create ZIP buffer from project folder
|
||||
* Excludes: .git, node_modules, dist, build, .env
|
||||
*/
|
||||
async _createZip(projectPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const output = []
|
||||
const archive = archiver('zip', { zlib: { level: 5 } })
|
||||
|
||||
archive.on('data', chunk => output.push(chunk))
|
||||
archive.on('end', () => resolve(Buffer.concat(output)))
|
||||
archive.on('error', reject)
|
||||
|
||||
// Add files with exclusions
|
||||
archive.glob('**/*', {
|
||||
cwd: projectPath,
|
||||
ignore: [
|
||||
'.git/**',
|
||||
'.git',
|
||||
'node_modules/**',
|
||||
'node_modules',
|
||||
'dist/**',
|
||||
'dist',
|
||||
'build/**',
|
||||
'build',
|
||||
'.env',
|
||||
'.env.local',
|
||||
'.env.*.local',
|
||||
'*.log',
|
||||
'data/backups/**'
|
||||
],
|
||||
dot: true
|
||||
})
|
||||
|
||||
archive.finalize()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ZIP buffer to target folder
|
||||
*/
|
||||
async _extractZip(buffer, targetPath) {
|
||||
const unzipper = require('unzipper')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const { Readable } = require('stream')
|
||||
const stream = Readable.from(buffer)
|
||||
|
||||
stream
|
||||
.pipe(unzipper.Extract({ path: targetPath }))
|
||||
.on('close', resolve)
|
||||
.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO timestamp (YYYYMMDD-HHMMSS format)
|
||||
*/
|
||||
_getTimestamp() {
|
||||
const now = new Date()
|
||||
const year = now.getFullYear()
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(now.getDate()).padStart(2, '0')
|
||||
const hours = String(now.getHours()).padStart(2, '0')
|
||||
const minutes = String(now.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(now.getSeconds()).padStart(2, '0')
|
||||
|
||||
return `${year}${month}${day}-${hours}${minutes}${seconds}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup old backups (keep only last N versions)
|
||||
*/
|
||||
async _cleanupOldBackups(repoName, maxVersions = 5) {
|
||||
try {
|
||||
const backups = await this.listBackups(repoName)
|
||||
|
||||
for (let i = maxVersions; i < backups.length; i++) {
|
||||
await this.provider.deleteBackup(backups[i].name)
|
||||
}
|
||||
} catch (err) {
|
||||
// Silently ignore cleanup errors
|
||||
console.warn(`Cleanup warning: ${err.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BackupManager
|
||||
59
src/backup/BackupProvider.js
Normal file
59
src/backup/BackupProvider.js
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* BackupProvider — Abstract base class for backup providers
|
||||
* All providers must implement these methods
|
||||
*/
|
||||
|
||||
class BackupProvider {
|
||||
/**
|
||||
* Authenticate with the backup service
|
||||
* @param {Object} credentials - Provider-specific credentials
|
||||
*/
|
||||
async authenticate(credentials) {
|
||||
throw new Error('authenticate() not implemented in ' + this.constructor.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a backup file
|
||||
* @param {Buffer} buffer - File content
|
||||
* @param {string} filename - Filename (e.g., 'repo-backup-2025-03-24.zip')
|
||||
* @returns {Promise<{size: number}>}
|
||||
*/
|
||||
async uploadBackup(buffer, filename) {
|
||||
throw new Error('uploadBackup() not implemented in ' + this.constructor.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* List all backups for a repository
|
||||
* @returns {Promise<Array>} Array of {name, size, date}
|
||||
*/
|
||||
async listBackups() {
|
||||
throw new Error('listBackups() not implemented in ' + this.constructor.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a specific backup
|
||||
* @param {string} filename - Filename to download
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
async downloadBackup(filename) {
|
||||
throw new Error('downloadBackup() not implemented in ' + this.constructor.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a backup file
|
||||
* @param {string} filename - Filename to delete
|
||||
*/
|
||||
async deleteBackup(filename) {
|
||||
throw new Error('deleteBackup() not implemented in ' + this.constructor.name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test connection to backup service
|
||||
* @returns {Promise<{ok: boolean, error?: string}>}
|
||||
*/
|
||||
async testConnection() {
|
||||
throw new Error('testConnection() not implemented in ' + this.constructor.name)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BackupProvider
|
||||
84
src/backup/LocalProvider.js
Normal file
84
src/backup/LocalProvider.js
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* LocalProvider — Backup provider for local folders
|
||||
*/
|
||||
|
||||
const path = require('path')
|
||||
const fs = require('fs').promises
|
||||
const BackupProvider = require('./BackupProvider')
|
||||
|
||||
class LocalProvider extends BackupProvider {
|
||||
constructor() {
|
||||
super()
|
||||
this.basePath = null
|
||||
}
|
||||
|
||||
async authenticate(credentials) {
|
||||
const basePath = String(credentials && credentials.basePath ? credentials.basePath : '').trim()
|
||||
if (!basePath) {
|
||||
throw new Error('Lokaler Backup-Ordner fehlt')
|
||||
}
|
||||
|
||||
this.basePath = path.resolve(basePath)
|
||||
await fs.mkdir(this.basePath, { recursive: true })
|
||||
}
|
||||
|
||||
async testConnection() {
|
||||
if (!this.basePath) return { ok: false, error: 'Not authenticated' }
|
||||
|
||||
try {
|
||||
const stat = await fs.stat(this.basePath)
|
||||
if (!stat.isDirectory()) {
|
||||
return { ok: false, error: 'Pfad ist kein Ordner' }
|
||||
}
|
||||
return { ok: true }
|
||||
} catch (err) {
|
||||
return { ok: false, error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
async uploadBackup(buffer, filename) {
|
||||
if (!this.basePath) throw new Error('Not authenticated')
|
||||
|
||||
const target = path.join(this.basePath, filename)
|
||||
await fs.mkdir(path.dirname(target), { recursive: true })
|
||||
await fs.writeFile(target, buffer)
|
||||
return { size: buffer.length }
|
||||
}
|
||||
|
||||
async listBackups() {
|
||||
if (!this.basePath) throw new Error('Not authenticated')
|
||||
|
||||
const entries = await fs.readdir(this.basePath, { withFileTypes: true })
|
||||
const files = entries.filter(e => e.isFile() && e.name.endsWith('.zip'))
|
||||
|
||||
const backups = []
|
||||
for (const f of files) {
|
||||
const full = path.join(this.basePath, f.name)
|
||||
const stat = await fs.stat(full)
|
||||
backups.push({
|
||||
name: f.name,
|
||||
size: stat.size,
|
||||
date: stat.mtime.toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
backups.sort((a, b) => new Date(b.date) - new Date(a.date))
|
||||
return backups
|
||||
}
|
||||
|
||||
async downloadBackup(filename) {
|
||||
if (!this.basePath) throw new Error('Not authenticated')
|
||||
|
||||
const file = path.join(this.basePath, filename)
|
||||
return fs.readFile(file)
|
||||
}
|
||||
|
||||
async deleteBackup(filename) {
|
||||
if (!this.basePath) throw new Error('Not authenticated')
|
||||
|
||||
const file = path.join(this.basePath, filename)
|
||||
await fs.unlink(file)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LocalProvider
|
||||
File diff suppressed because it is too large
Load Diff
181
updater.js
181
updater.js
@@ -3,9 +3,11 @@ const { app, shell } = require('electron');
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
const GITEA_API_URL = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/Git-Manager-Gui/releases';
|
||||
const TRUSTED_UPDATE_HOST = 'git.viper.ipv64.net';
|
||||
|
||||
class Updater {
|
||||
constructor(mainWindow) {
|
||||
@@ -31,15 +33,28 @@ class Updater {
|
||||
console.log(`[Updater] Version-Check: Server(${serverVer}) vs Lokal(${localVer})`);
|
||||
|
||||
if (this.compareVersions(serverVer, localVer) > 0) {
|
||||
const asset = this.findAsset(latestRelease.assets);
|
||||
const checksumAsset = this.findChecksumAsset(latestRelease.assets, asset);
|
||||
const expectedSha256 = this.extractChecksumFromReleaseBody(latestRelease.body, asset?.name);
|
||||
|
||||
console.log("[Updater] Update verfügbar. Sende Daten an Renderer...");
|
||||
this.mainWindow.webContents.send('update-available', {
|
||||
version: serverVer,
|
||||
body: latestRelease.body,
|
||||
url: latestRelease.html_url,
|
||||
asset: this.findAsset(latestRelease.assets)
|
||||
asset: asset ? {
|
||||
...asset,
|
||||
checksumUrl: checksumAsset ? checksumAsset.browser_download_url : null,
|
||||
expectedSha256
|
||||
} : null
|
||||
});
|
||||
} else {
|
||||
console.log("[Updater] Anwendung ist auf dem neuesten Stand.");
|
||||
if (!silent) {
|
||||
this.mainWindow.webContents.send('update-not-available', {
|
||||
version: localVer
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Updater] Fehler beim Update-Check:', error);
|
||||
@@ -77,7 +92,117 @@ class Updater {
|
||||
findAsset(assets) {
|
||||
if (!assets) return null;
|
||||
const ext = process.platform === 'win32' ? '.exe' : '.AppImage';
|
||||
return assets.find(a => a.name.toLowerCase().endsWith(ext));
|
||||
return assets.find(a => {
|
||||
const name = String(a?.name || '').toLowerCase();
|
||||
const validName = /^[a-z0-9._-]+$/.test(name);
|
||||
return validName && name.endsWith(ext);
|
||||
});
|
||||
}
|
||||
|
||||
findChecksumAsset(assets, targetAsset) {
|
||||
if (!Array.isArray(assets) || !targetAsset?.name) return null;
|
||||
const targetLower = String(targetAsset.name).toLowerCase();
|
||||
|
||||
const exactCandidates = [
|
||||
`${targetLower}.sha256`,
|
||||
`${targetLower}.sha256sum`,
|
||||
`${targetLower}.sha512`,
|
||||
`${targetLower}.sha512sum`
|
||||
];
|
||||
|
||||
const exact = assets.find(a => exactCandidates.includes(String(a?.name || '').toLowerCase()));
|
||||
if (exact) return exact;
|
||||
|
||||
return assets.find(a => {
|
||||
const name = String(a?.name || '').toLowerCase();
|
||||
return name.includes('checksum') || name.includes('checksums') || name.endsWith('.sha256') || name.endsWith('.sha256sum');
|
||||
}) || null;
|
||||
}
|
||||
|
||||
extractChecksumFromReleaseBody(body, fileName) {
|
||||
const text = String(body || '');
|
||||
const target = String(fileName || '').trim();
|
||||
if (!text || !target) return null;
|
||||
|
||||
const lines = text.split(/\r?\n/);
|
||||
const targetLower = target.toLowerCase();
|
||||
for (const line of lines) {
|
||||
const normalized = String(line || '').trim();
|
||||
if (!normalized) continue;
|
||||
const match = normalized.match(/\b([a-fA-F0-9]{64})\b/);
|
||||
if (!match) continue;
|
||||
if (normalized.toLowerCase().includes(targetLower)) {
|
||||
return match[1].toLowerCase();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
downloadText(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.isTrustedDownloadUrl(url)) {
|
||||
reject(new Error('Unsichere Checksum-URL blockiert.'));
|
||||
return;
|
||||
}
|
||||
|
||||
https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => {
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
return resolve(this.downloadText(res.headers.location || ''));
|
||||
}
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`Checksum-Download fehlgeschlagen: HTTP ${res.statusCode}`));
|
||||
return;
|
||||
}
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk.toString('utf8'));
|
||||
res.on('end', () => resolve(data));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
extractChecksumFromText(text, fileName) {
|
||||
const lines = String(text || '').split(/\r?\n/);
|
||||
const targetLower = String(fileName || '').toLowerCase();
|
||||
for (const line of lines) {
|
||||
const normalized = String(line || '').trim();
|
||||
if (!normalized) continue;
|
||||
const hashMatch = normalized.match(/\b([a-fA-F0-9]{64})\b/);
|
||||
if (!hashMatch) continue;
|
||||
if (!targetLower || normalized.toLowerCase().includes(targetLower)) {
|
||||
return hashMatch[1].toLowerCase();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
computeFileSha256(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash('sha256');
|
||||
const stream = fs.createReadStream(filePath);
|
||||
stream.on('error', reject);
|
||||
stream.on('data', chunk => hash.update(chunk));
|
||||
stream.on('end', () => resolve(hash.digest('hex').toLowerCase()));
|
||||
});
|
||||
}
|
||||
|
||||
async resolveExpectedSha256(asset) {
|
||||
const expectedFromAsset = String(asset?.expectedSha256 || '').trim().toLowerCase();
|
||||
if (/^[a-f0-9]{64}$/.test(expectedFromAsset)) return expectedFromAsset;
|
||||
|
||||
const checksumUrl = String(asset?.checksumUrl || '').trim();
|
||||
if (!checksumUrl) return null;
|
||||
|
||||
const checksumText = await this.downloadText(checksumUrl);
|
||||
return this.extractChecksumFromText(checksumText, asset?.name);
|
||||
}
|
||||
|
||||
isTrustedDownloadUrl(rawUrl) {
|
||||
try {
|
||||
const parsed = new URL(String(rawUrl || ''));
|
||||
return parsed.protocol === 'https:' && parsed.hostname === TRUSTED_UPDATE_HOST;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,16 +214,40 @@ class Updater {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isTrustedDownloadUrl(asset.browser_download_url)) {
|
||||
console.error('[Updater] Unsichere Download-URL blockiert.');
|
||||
return;
|
||||
}
|
||||
|
||||
const tempPath = path.join(app.getPath('temp'), asset.name);
|
||||
console.log(`[Updater] Download gestartet: ${asset.name} -> ${tempPath}`);
|
||||
|
||||
let expectedSha256 = null;
|
||||
try {
|
||||
expectedSha256 = await this.resolveExpectedSha256(asset);
|
||||
} catch (e) {
|
||||
console.error('[Updater] Konnte erwartete Checksumme nicht laden:', e?.message || e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!expectedSha256) {
|
||||
console.error('[Updater] Kein SHA-256-Checksum-Wert gefunden. Update wurde aus Sicherheitsgruenden blockiert.');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fs.createWriteStream(tempPath);
|
||||
|
||||
const download = (url) => {
|
||||
if (!this.isTrustedDownloadUrl(url)) {
|
||||
console.error('[Updater] Unsicherer Redirect/Download blockiert.');
|
||||
fs.unlink(tempPath, () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
https.get(url, { headers: { 'User-Agent': 'Electron-Updater' } }, (res) => {
|
||||
// Handle Redirects
|
||||
if (res.statusCode === 301 || res.statusCode === 302) {
|
||||
return download(res.headers.location);
|
||||
return download(res.headers.location || '');
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
@@ -108,9 +257,22 @@ class Updater {
|
||||
|
||||
res.pipe(file);
|
||||
|
||||
file.on('finish', () => {
|
||||
file.on('finish', async () => {
|
||||
file.close();
|
||||
console.log("[Updater] Download abgeschlossen. Initialisiere entkoppelten Installer...");
|
||||
try {
|
||||
const actualSha256 = await this.computeFileSha256(tempPath);
|
||||
if (actualSha256 !== expectedSha256) {
|
||||
console.error('[Updater] Checksum-Validierung fehlgeschlagen. Installation wurde blockiert.');
|
||||
fs.unlink(tempPath, () => {});
|
||||
return;
|
||||
}
|
||||
} catch (verifyErr) {
|
||||
console.error('[Updater] Checksum-Validierung konnte nicht ausgeführt werden:', verifyErr?.message || verifyErr);
|
||||
fs.unlink(tempPath, () => {});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[Updater] Download und Checksum-Validierung abgeschlossen. Initialisiere entkoppelten Installer...");
|
||||
this.installAndQuit(tempPath);
|
||||
});
|
||||
}).on('error', (err) => {
|
||||
@@ -129,15 +291,14 @@ class Updater {
|
||||
console.log(`[Updater] Bereite Installation vor: ${filePath}`);
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Wir nutzen spawn mit detached: true, damit der Installer weiterläuft,
|
||||
// wenn der Hauptprozess (Electron) beendet wird.
|
||||
try {
|
||||
const child = spawn('cmd.exe', ['/c', 'start', '""', filePath], {
|
||||
const child = spawn(filePath, [], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
stdio: 'ignore',
|
||||
shell: false
|
||||
});
|
||||
|
||||
child.unref(); // Trennt die Referenz zum Installer
|
||||
child.unref();
|
||||
|
||||
console.log("[Updater] Installer-Prozess entkoppelt gestartet. App schließt in 2 Sek...");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user