Dieser Commit ist enthalten in:
HG
2025-12-30 19:17:07 +00:00
committet von Server Deploy
Ursprung c8707d6cf4
Commit 15627cce99
14 geänderte Dateien mit 2456 neuen und 4 gelöschten Zeilen

Datei anzeigen

@ -1,6 +1,54 @@
TASKMATE - CHANGELOG TASKMATE - CHANGELOG
==================== ====================
================================================================================
30.12.2025 - Browser-Upload: Lokale Verzeichnisse ins Gitea pushen
================================================================================
FEATURE: VERZEICHNIS-UPLOAD VOM BROWSER
--------------------------------------------------------------------------------
- Lokale Verzeichnisse direkt vom Computer ins Gitea pushen
- Verwendet File System Access API (Chrome/Edge/Opera)
- Drag & Drop als Fallback für andere Browser
- Automatische Filterung von .git, node_modules, etc.
ABLAUF
--------------------------------------------------------------------------------
1. Ziel-Repository aus Gitea-Liste auswählen
2. Ziel-Branch eingeben (Standard: main)
3. "Verzeichnis auswählen" klicken oder Ordner per Drag & Drop
4. Datei-Vorschau prüfen
5. Commit-Nachricht eingeben
6. "Commit & Push" ausführen
TECHNISCHE ÄNDERUNGEN
--------------------------------------------------------------------------------
- backend/routes/git.js:
* POST /api/git/browser-upload - Empfängt Dateien und pusht ins Gitea
* POST /api/git/browser-upload-prepare - Bereitet Upload-Session vor
* DELETE /api/git/browser-upload/:sessionId - Bricht Upload ab
* Multer-Konfiguration für Git-Uploads (50MB/Datei, 500 Dateien max)
- frontend/js/api.js:
* prepareBrowserUpload() - Session vorbereiten
* browserUploadAndPush() - Dateien hochladen und pushen
* cancelBrowserUpload() - Session abbrechen
- frontend/js/gitea.js:
* Browser-Upload Properties und Ignore-Patterns
* bindBrowserUploadEvents() - Event-Handler
* handleSelectDirectory() - File System Access API
* readDirectoryRecursive() - Verzeichnis rekursiv lesen
* handleDroppedFiles() - Drag & Drop Handler
* renderUploadPreview() - Datei-Vorschau
* executeBrowserUpload() - Upload durchführen
- frontend/index.html: Neues Browser-Upload UI mit Schritten
- frontend/css/gitea.css: Styles für Upload-Schritte, Drop-Zone, Progress
- frontend/sw.js: Cache-Version auf 132 erhöht
IGNORIERTE DATEIEN/ORDNER
--------------------------------------------------------------------------------
.git, node_modules, __pycache__, .env, .env.local, .env.production,
.DS_Store, Thumbs.db, .idea, .vscode, dist, build, .cache, coverage
================================================================================ ================================================================================
30.12.2025 - Gitea-Integration: Server-Modus 30.12.2025 - Gitea-Integration: Server-Modus
================================================================================ ================================================================================

Datei anzeigen

@ -6,14 +6,49 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const path = require('path');
const fs = require('fs');
const os = require('os');
const { getDb } = require('../database'); const { getDb } = require('../database');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const gitService = require('../services/gitService'); const gitService = require('../services/gitService');
const giteaService = require('../services/giteaService'); const giteaService = require('../services/giteaService');
const multer = require('multer');
// Fester Pfad für Server-Modus (TaskMate Source-Code) // Fester Pfad für Server-Modus (TaskMate Source-Code)
const SERVER_SOURCE_PATH = '/app/taskmate-source'; const SERVER_SOURCE_PATH = '/app/taskmate-source';
// Temporäres Verzeichnis für Browser-Uploads
const TEMP_UPLOAD_DIR = path.join(os.tmpdir(), 'taskmate-git-uploads');
// Multer-Konfiguration für Git-Uploads (beliebige Dateitypen)
const gitUploadStorage = multer.diskStorage({
destination: (req, file, cb) => {
// Erstelle eindeutiges temporäres Verzeichnis pro Upload-Session
const sessionId = req.body.sessionId || Date.now().toString();
const sessionDir = path.join(TEMP_UPLOAD_DIR, sessionId);
// Relativen Pfad aus dem Dateinamen extrahieren (wird vom Frontend gesendet)
const relativePath = file.originalname;
const fileDir = path.join(sessionDir, path.dirname(relativePath));
fs.mkdirSync(fileDir, { recursive: true });
cb(null, fileDir);
},
filename: (req, file, cb) => {
// Nur den Dateinamen ohne Pfad
cb(null, path.basename(file.originalname));
}
});
const gitUpload = multer({
storage: gitUploadStorage,
limits: {
fileSize: 50 * 1024 * 1024, // 50MB pro Datei
files: 500 // Maximal 500 Dateien
}
});
/** /**
* Hilfsfunktion: Anwendung für Projekt abrufen * Hilfsfunktion: Anwendung für Projekt abrufen
*/ */
@ -757,4 +792,178 @@ router.get('/server/info', (req, res) => {
} }
}); });
// ============================================
// BROWSER-UPLOAD ENDPOINTS
// Für lokale Verzeichnis-Uploads vom Browser
// ============================================
/**
* Hilfsfunktion: Verzeichnis rekursiv löschen
*/
function deleteFolderRecursive(dirPath) {
if (fs.existsSync(dirPath)) {
fs.readdirSync(dirPath).forEach((file) => {
const curPath = path.join(dirPath, file);
if (fs.lstatSync(curPath).isDirectory()) {
deleteFolderRecursive(curPath);
} else {
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(dirPath);
}
}
/**
* POST /api/git/browser-upload
* Empfängt Dateien vom Browser und pusht sie ins Gitea
*
* Body (multipart/form-data):
* - files: Die hochgeladenen Dateien (originalname enthält relativen Pfad)
* - repoUrl: Die Gitea-Repository-URL
* - branch: Der Ziel-Branch (default: main)
* - commitMessage: Die Commit-Nachricht
* - sessionId: Eindeutige Session-ID für den Upload
*/
router.post('/browser-upload', gitUpload.array('files', 500), async (req, res) => {
const sessionId = req.body.sessionId || Date.now().toString();
const sessionDir = path.join(TEMP_UPLOAD_DIR, sessionId);
try {
const { repoUrl, branch = 'main', commitMessage } = req.body;
const files = req.files;
// Validierung
if (!repoUrl) {
return res.status(400).json({ error: 'Repository-URL ist erforderlich' });
}
if (!commitMessage) {
return res.status(400).json({ error: 'Commit-Nachricht ist erforderlich' });
}
if (!files || files.length === 0) {
return res.status(400).json({ error: 'Keine Dateien hochgeladen' });
}
logger.info(`[Browser-Upload] ${files.length} Dateien empfangen für ${repoUrl}`);
// Git-Repository initialisieren
const initResult = gitService.initRepository(sessionDir);
if (!initResult.success) {
throw new Error('Git-Initialisierung fehlgeschlagen: ' + initResult.error);
}
// Remote hinzufügen
const remoteResult = gitService.addRemote(sessionDir, repoUrl, 'origin');
if (!remoteResult.success) {
throw new Error('Remote hinzufügen fehlgeschlagen: ' + remoteResult.error);
}
// Autor aus eingeloggtem Benutzer
const author = req.user ? {
name: req.user.display_name || req.user.username,
email: req.user.email || `${req.user.username.toLowerCase()}@taskmate.local`
} : null;
// Alle Dateien stagen
const stageResult = gitService.stageAll(sessionDir);
if (!stageResult.success) {
throw new Error('Staging fehlgeschlagen: ' + stageResult.error);
}
// Commit erstellen
const commitResult = gitService.commit(sessionDir, commitMessage, author);
if (!commitResult.success) {
throw new Error('Commit fehlgeschlagen: ' + commitResult.error);
}
// Push mit Upstream
const pushResult = gitService.pushWithUpstream(sessionDir, branch, 'origin', false);
if (!pushResult.success) {
// Bei Fehler: Versuche Force-Push falls Branch existiert
if (pushResult.error && pushResult.error.includes('rejected')) {
logger.warn('[Browser-Upload] Normaler Push abgelehnt, versuche mit Force...');
const forcePushResult = gitService.pushWithUpstream(sessionDir, branch, 'origin', true);
if (!forcePushResult.success) {
throw new Error('Push fehlgeschlagen: ' + forcePushResult.error);
}
} else {
throw new Error('Push fehlgeschlagen: ' + pushResult.error);
}
}
logger.info(`[Browser-Upload] Erfolgreich nach ${repoUrl}/${branch} gepusht`);
// Erfolgreich - Aufräumen
deleteFolderRecursive(sessionDir);
res.json({
success: true,
message: `${files.length} Dateien erfolgreich nach ${branch} gepusht`,
filesCount: files.length,
branch: branch,
commit: commitResult.hash || null
});
} catch (error) {
logger.error('[Browser-Upload] Fehler:', error);
// Bei Fehler aufräumen
try {
deleteFolderRecursive(sessionDir);
} catch (cleanupError) {
logger.warn('[Browser-Upload] Aufräumen fehlgeschlagen:', cleanupError);
}
res.status(500).json({
success: false,
error: error.message || 'Upload fehlgeschlagen'
});
}
});
/**
* POST /api/git/browser-upload-prepare
* Bereitet einen Upload vor und gibt eine Session-ID zurück
*/
router.post('/browser-upload-prepare', (req, res) => {
const sessionId = `upload_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const sessionDir = path.join(TEMP_UPLOAD_DIR, sessionId);
try {
fs.mkdirSync(sessionDir, { recursive: true });
res.json({
success: true,
sessionId: sessionId,
maxFileSize: 50 * 1024 * 1024, // 50MB
maxFiles: 500
});
} catch (error) {
logger.error('[Browser-Upload] Prepare fehlgeschlagen:', error);
res.status(500).json({ error: 'Vorbereitung fehlgeschlagen' });
}
});
/**
* DELETE /api/git/browser-upload/:sessionId
* Löscht eine Upload-Session (für Abbruch)
*/
router.delete('/browser-upload/:sessionId', (req, res) => {
const { sessionId } = req.params;
const sessionDir = path.join(TEMP_UPLOAD_DIR, sessionId);
try {
if (fs.existsSync(sessionDir)) {
deleteFolderRecursive(sessionDir);
}
res.json({ success: true });
} catch (error) {
logger.error('[Browser-Upload] Löschen fehlgeschlagen:', error);
res.status(500).json({ error: 'Löschen fehlgeschlagen' });
}
});
module.exports = router; module.exports = router;

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Datei anzeigen

@ -695,3 +695,206 @@
justify-content: center; justify-content: center;
} }
} }
/* =============================================================================
BROWSER UPLOAD
============================================================================= */
.browser-compat-notice {
display: flex;
align-items: flex-start;
gap: var(--spacing-3);
padding: var(--spacing-3) var(--spacing-4);
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-4);
color: var(--warning);
font-size: var(--text-sm);
}
.browser-compat-notice svg {
flex-shrink: 0;
margin-top: 2px;
}
/* Upload Steps */
.upload-step {
background: var(--bg-tertiary);
border-radius: var(--radius-md);
padding: var(--spacing-4);
margin-bottom: var(--spacing-4);
}
.step-header {
display: flex;
align-items: center;
gap: var(--spacing-3);
margin-bottom: var(--spacing-4);
}
.step-number {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: var(--primary);
color: white;
border-radius: 50%;
font-weight: 600;
font-size: var(--text-sm);
flex-shrink: 0;
}
.step-title {
font-weight: 500;
color: var(--text-primary);
flex: 1;
}
.file-count {
font-size: var(--text-sm);
color: var(--text-secondary);
background: var(--bg-primary);
padding: var(--spacing-1) var(--spacing-2);
border-radius: var(--radius-sm);
}
/* Directory Picker */
.directory-picker {
display: flex;
flex-direction: column;
gap: var(--spacing-3);
align-items: center;
}
.btn-lg {
padding: var(--spacing-3) var(--spacing-6);
font-size: var(--text-base);
}
.drop-zone {
width: 100%;
padding: var(--spacing-8);
border: 2px dashed var(--border-default);
border-radius: var(--radius-lg);
text-align: center;
color: var(--text-tertiary);
transition: all 0.2s;
}
.drop-zone svg {
margin-bottom: var(--spacing-2);
color: var(--text-tertiary);
}
.drop-zone p {
margin: 0;
font-size: var(--text-sm);
}
.drop-zone.drag-over {
border-color: var(--primary);
background: rgba(59, 130, 246, 0.05);
color: var(--primary);
}
.drop-zone.drag-over svg {
color: var(--primary);
}
/* Files List */
.upload-files-list {
max-height: 250px;
overflow-y: auto;
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
background: var(--bg-primary);
}
.upload-file-item {
display: flex;
align-items: center;
gap: var(--spacing-2);
padding: var(--spacing-2) var(--spacing-3);
border-bottom: 1px solid var(--border-default);
font-size: var(--text-sm);
font-family: monospace;
}
.upload-file-item:last-child {
border-bottom: none;
}
.upload-file-item .file-icon {
color: var(--text-tertiary);
flex-shrink: 0;
}
.upload-file-item .file-path {
flex: 1;
color: var(--text-primary);
word-break: break-all;
}
.upload-file-item .file-size {
color: var(--text-tertiary);
font-size: var(--text-xs);
flex-shrink: 0;
}
.excluded-info {
margin-top: var(--spacing-2);
color: var(--text-tertiary);
}
/* Upload Actions */
.upload-actions {
display: flex;
gap: var(--spacing-3);
justify-content: flex-end;
margin-top: var(--spacing-4);
}
/* Progress Bar */
.upload-progress {
margin-top: var(--spacing-4);
display: flex;
align-items: center;
gap: var(--spacing-3);
}
.progress-bar {
flex: 1;
height: 8px;
background: var(--bg-primary);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--primary);
border-radius: var(--radius-full);
transition: width 0.3s ease;
width: 0%;
}
#upload-progress-text {
font-size: var(--text-sm);
color: var(--text-secondary);
min-width: 40px;
text-align: right;
}
/* Responsive */
@media (max-width: 480px) {
.upload-actions {
flex-direction: column;
}
.upload-actions .btn {
width: 100%;
}
}

Datei anzeigen

@ -610,7 +610,110 @@
</div> </div>
<!-- Projekt-Modus: Kein Projekt ausgewählt --> <!-- Projekt-Modus: Browser-Upload Ansicht -->
<div id="gitea-browser-upload" class="gitea-section hidden">
<!-- Header -->
<div class="gitea-config-header">
<h2>
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Lokales Verzeichnis hochladen
</h2>
<p>Wählen Sie ein Verzeichnis von Ihrem Computer und pushen Sie es direkt ins Gitea.</p>
</div>
<!-- Browser-Kompatibilität Hinweis -->
<div id="browser-upload-compat" class="browser-compat-notice hidden">
<svg viewBox="0 0 24 24" width="20" height="20"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><path d="M12 8v4M12 16h.01" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
<span>Die Verzeichnis-Auswahl funktioniert nur in Chrome, Edge oder Opera. In anderen Browsern können Sie Dateien per Drag & Drop hochladen.</span>
</div>
<!-- Schritt 1: Repository auswählen -->
<div class="upload-step">
<div class="step-header">
<span class="step-number">1</span>
<span class="step-title">Ziel-Repository auswählen</span>
</div>
<div class="form-group">
<div class="gitea-repo-select-group">
<select id="browser-upload-repo-select" class="form-control">
<option value="">-- Repository wählen --</option>
</select>
<button type="button" id="btn-refresh-upload-repos" class="btn btn-icon" title="Repositories aktualisieren">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 3v5h5" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" stroke="currentColor" stroke-width="2" fill="none"/><path d="M21 21v-5h-5" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</button>
</div>
</div>
<div class="form-group">
<label for="browser-upload-branch">Ziel-Branch</label>
<input type="text" id="browser-upload-branch" class="form-control" value="main" placeholder="main">
</div>
</div>
<!-- Schritt 2: Verzeichnis auswählen -->
<div class="upload-step">
<div class="step-header">
<span class="step-number">2</span>
<span class="step-title">Verzeichnis auswählen</span>
</div>
<div class="directory-picker">
<button type="button" id="btn-select-directory" class="btn btn-secondary btn-lg">
<svg viewBox="0 0 24 24" width="20" height="20"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Verzeichnis auswählen
</button>
<div id="drop-zone" class="drop-zone hidden">
<svg viewBox="0 0 24 24" width="48" height="48"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<p>Oder Verzeichnis hierher ziehen</p>
</div>
</div>
</div>
<!-- Schritt 3: Datei-Vorschau (erscheint nach Auswahl) -->
<div id="upload-preview-section" class="upload-step hidden">
<div class="step-header">
<span class="step-number">3</span>
<span class="step-title">Ausgewählte Dateien</span>
<span id="upload-file-count" class="file-count">0 Dateien</span>
</div>
<div id="upload-files-list" class="upload-files-list">
<!-- Dynamisch gefüllt -->
</div>
<div class="excluded-info">
<small>Automatisch ausgeschlossen: .git, node_modules, __pycache__, .env, *.log</small>
</div>
</div>
<!-- Schritt 4: Commit und Push -->
<div id="upload-commit-section" class="upload-step hidden">
<div class="step-header">
<span class="step-number">4</span>
<span class="step-title">Commit erstellen und pushen</span>
</div>
<div class="form-group">
<label for="browser-upload-commit-message">Commit-Nachricht</label>
<textarea id="browser-upload-commit-message" class="form-control" rows="2" placeholder="Beschreiben Sie Ihre Änderungen..."></textarea>
</div>
<div class="upload-actions">
<button type="button" id="btn-cancel-upload" class="btn btn-secondary">
Abbrechen
</button>
<button type="button" id="btn-execute-upload" class="btn btn-primary">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Commit & Push
</button>
</div>
<!-- Progress Bar -->
<div id="upload-progress-container" class="upload-progress hidden">
<div class="progress-bar">
<div id="upload-progress-bar" class="progress-fill"></div>
</div>
<span id="upload-progress-text">0%</span>
</div>
</div>
</div>
<!-- Projekt-Modus: Kein Projekt (altes Element, jetzt versteckt) -->
<div id="gitea-no-project" class="gitea-empty-state hidden"> <div id="gitea-no-project" class="gitea-empty-state hidden">
<svg viewBox="0 0 24 24" width="64" height="64"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg viewBox="0 0 24 24" width="64" height="64"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<h3>Kein Projekt ausgewählt</h3> <h3>Kein Projekt ausgewählt</h3>

Datei anzeigen

@ -884,6 +884,99 @@ class ApiClient {
async serverGitCheckout(branch) { async serverGitCheckout(branch) {
return this.post('/git/server/checkout', { branch }); return this.post('/git/server/checkout', { branch });
} }
// =====================
// BROWSER-UPLOAD ENDPOINTS
// =====================
async prepareBrowserUpload() {
return this.post('/git/browser-upload-prepare', {});
}
async cancelBrowserUpload(sessionId) {
return this.delete(`/git/browser-upload/${sessionId}`);
}
/**
* Lädt Dateien vom Browser hoch und pusht sie ins Gitea
* @param {Object} options - Upload-Optionen
* @param {File[]} options.files - Array von File-Objekten mit relativePath Property
* @param {string} options.repoUrl - Gitea Repository URL
* @param {string} options.branch - Ziel-Branch
* @param {string} options.commitMessage - Commit-Nachricht
* @param {string} options.sessionId - Session-ID vom prepare-Aufruf
* @param {Function} options.onProgress - Progress-Callback (optional)
*/
async browserUploadAndPush(options) {
const { files, repoUrl, branch, commitMessage, sessionId, onProgress } = options;
const url = `${this.baseUrl}/git/browser-upload`;
const formData = new FormData();
// Metadaten hinzufügen
formData.append('repoUrl', repoUrl);
formData.append('branch', branch || 'main');
formData.append('commitMessage', commitMessage);
formData.append('sessionId', sessionId);
// Dateien hinzufügen (mit relativem Pfad als Dateiname)
files.forEach(fileInfo => {
// Erstelle neues File-Objekt mit relativem Pfad als Namen
const file = new File([fileInfo.file], fileInfo.relativePath, {
type: fileInfo.file.type
});
formData.append('files', file);
});
const token = this.getToken();
const csrfToken = this.getCsrfToken();
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', url);
if (token) {
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
}
if (csrfToken) {
xhr.setRequestHeader('X-CSRF-Token', csrfToken);
}
if (onProgress) {
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentage = Math.round((e.loaded / e.total) * 100);
onProgress(percentage);
}
});
}
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText));
} catch {
resolve({ success: true });
}
} else {
try {
const error = JSON.parse(xhr.responseText);
reject(new Error(error.error || 'Upload fehlgeschlagen'));
} catch {
reject(new Error('Upload fehlgeschlagen'));
}
}
});
xhr.addEventListener('error', () => {
reject(new Error('Netzwerkfehler beim Upload'));
});
xhr.send(formData);
});
}
} }
// Custom API Error Class // Custom API Error Class

Datei anzeigen

@ -31,6 +31,29 @@ class GiteaManager {
this.serverBranches = []; this.serverBranches = [];
this.serverCommits = []; this.serverCommits = [];
this.hiddenServerCommits = new Set(); this.hiddenServerCommits = new Set();
// Browser-Upload Eigenschaften
this.browserUploadFiles = [];
this.browserUploadSessionId = null;
this.supportsDirectoryPicker = 'showDirectoryPicker' in window;
// Zu ignorierende Ordner/Dateien
this.ignorePatterns = [
'.git',
'node_modules',
'__pycache__',
'.env',
'.env.local',
'.env.production',
'.DS_Store',
'Thumbs.db',
'.idea',
'.vscode',
'dist',
'build',
'.cache',
'coverage'
];
} }
async init() { async init() {
@ -51,8 +74,12 @@ class GiteaManager {
this.mainSection = $('#gitea-main-section'); this.mainSection = $('#gitea-main-section');
this.connectionStatus = $('#gitea-connection-status'); this.connectionStatus = $('#gitea-connection-status');
// DOM Elements - Browser-Upload
this.browserUploadSection = $('#gitea-browser-upload');
this.bindEvents(); this.bindEvents();
this.bindServerEvents(); this.bindServerEvents();
this.bindBrowserUploadEvents();
this.subscribeToStore(); this.subscribeToStore();
this.initialized = true; this.initialized = true;
} }
@ -143,14 +170,19 @@ class GiteaManager {
if (this.currentMode === 'server') { if (this.currentMode === 'server') {
// Server-Modus // Server-Modus
this.serverModeSection?.classList.remove('hidden'); this.serverModeSection?.classList.remove('hidden');
this.browserUploadSection?.classList.add('hidden');
this.noProjectSection?.classList.add('hidden'); this.noProjectSection?.classList.add('hidden');
this.configSection?.classList.add('hidden'); this.configSection?.classList.add('hidden');
this.mainSection?.classList.add('hidden'); this.mainSection?.classList.add('hidden');
this.loadServerData(); this.loadServerData();
} else { } else {
// Projekt-Modus // Projekt-Modus (Browser-Upload)
this.serverModeSection?.classList.add('hidden'); this.serverModeSection?.classList.add('hidden');
this.loadApplication(); this.browserUploadSection?.classList.remove('hidden');
this.noProjectSection?.classList.add('hidden');
this.configSection?.classList.add('hidden');
this.mainSection?.classList.add('hidden');
this.initBrowserUpload();
} }
} }
@ -524,6 +556,402 @@ class GiteaManager {
} }
} }
// =====================
// BROWSER-UPLOAD METHODEN
// =====================
bindBrowserUploadEvents() {
// Verzeichnis auswählen
$('#btn-select-directory')?.addEventListener('click', () => this.handleSelectDirectory());
// Repository für Upload aktualisieren
$('#btn-refresh-upload-repos')?.addEventListener('click', () => this.loadBrowserUploadRepos());
// Upload abbrechen
$('#btn-cancel-upload')?.addEventListener('click', () => this.cancelBrowserUpload());
// Upload ausführen
$('#btn-execute-upload')?.addEventListener('click', () => this.executeBrowserUpload());
// Drop Zone Events
const dropZone = $('#drop-zone');
if (dropZone) {
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
this.handleDroppedFiles(e.dataTransfer);
});
}
}
async initBrowserUpload() {
// Browser-Kompatibilität prüfen
const compatNotice = $('#browser-upload-compat');
if (compatNotice) {
if (!this.supportsDirectoryPicker) {
compatNotice.classList.remove('hidden');
// Drop-Zone anzeigen als Alternative
$('#drop-zone')?.classList.remove('hidden');
} else {
compatNotice.classList.add('hidden');
}
}
// Upload-Zustand zurücksetzen
this.browserUploadFiles = [];
this.browserUploadSessionId = null;
this.resetBrowserUploadUI();
// Repositories laden
await this.loadBrowserUploadRepos();
}
async loadBrowserUploadRepos() {
const select = $('#browser-upload-repo-select');
if (!select) return;
try {
const result = await api.getGiteaRepositories();
// API gibt { success, repositories } zurück
this.giteaRepos = result.repositories || [];
select.innerHTML = '<option value="">-- Repository wählen --</option>';
this.giteaRepos.forEach(repo => {
const option = document.createElement('option');
// API-Felder: cloneUrl, fullName, owner, name
option.value = repo.cloneUrl;
option.textContent = repo.fullName;
option.dataset.owner = repo.owner || '';
option.dataset.name = repo.name;
select.appendChild(option);
});
if (this.giteaRepos.length === 0) {
this.showToast('Keine Repositories gefunden', 'info');
}
} catch (error) {
console.error('[Browser-Upload] Repositories laden fehlgeschlagen:', error);
this.showToast('Repositories konnten nicht geladen werden', 'error');
}
}
async handleSelectDirectory() {
if (!this.supportsDirectoryPicker) {
this.showToast('Verzeichnis-Auswahl wird in diesem Browser nicht unterstützt', 'error');
return;
}
try {
// File System Access API verwenden
const dirHandle = await window.showDirectoryPicker({
mode: 'read'
});
this.showToast('Lese Verzeichnis...', 'info');
// Dateien rekursiv lesen
const files = await this.readDirectoryRecursive(dirHandle, '');
if (files.length === 0) {
this.showToast('Keine Dateien gefunden oder alle wurden ignoriert', 'warning');
return;
}
this.browserUploadFiles = files;
this.renderUploadPreview();
this.showToast(`${files.length} Dateien ausgewählt`, 'success');
} catch (error) {
if (error.name === 'AbortError') {
// Benutzer hat abgebrochen
return;
}
console.error('[Browser-Upload] Verzeichnis lesen fehlgeschlagen:', error);
this.showToast('Verzeichnis konnte nicht gelesen werden', 'error');
}
}
async readDirectoryRecursive(dirHandle, basePath) {
const files = [];
for await (const entry of dirHandle.values()) {
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
// Ignorierte Muster prüfen
if (this.shouldIgnore(entry.name, relativePath)) {
continue;
}
if (entry.kind === 'file') {
try {
const file = await entry.getFile();
files.push({
file: file,
relativePath: relativePath,
size: file.size
});
} catch (err) {
console.warn(`Datei konnte nicht gelesen werden: ${relativePath}`, err);
}
} else if (entry.kind === 'directory') {
const subFiles = await this.readDirectoryRecursive(entry, relativePath);
files.push(...subFiles);
}
}
return files;
}
shouldIgnore(name, path) {
// Einfache Muster prüfen
for (const pattern of this.ignorePatterns) {
if (pattern.startsWith('*.')) {
// Wildcard-Muster (z.B. *.log)
const ext = pattern.substring(1);
if (name.endsWith(ext)) {
return true;
}
} else if (name === pattern || path.includes(`/${pattern}/`) || path.startsWith(`${pattern}/`)) {
return true;
}
}
return false;
}
async handleDroppedFiles(dataTransfer) {
const items = dataTransfer.items;
if (!items || items.length === 0) return;
const files = [];
for (const item of items) {
if (item.kind === 'file') {
// webkitGetAsEntry für Verzeichnis-Support
const entry = item.webkitGetAsEntry?.();
if (entry) {
const entryFiles = await this.readEntry(entry, '');
files.push(...entryFiles);
} else {
// Fallback: Einzelne Datei
const file = item.getAsFile();
if (file && !this.shouldIgnore(file.name, file.name)) {
files.push({
file: file,
relativePath: file.name,
size: file.size
});
}
}
}
}
if (files.length === 0) {
this.showToast('Keine Dateien gefunden oder alle wurden ignoriert', 'warning');
return;
}
this.browserUploadFiles = files;
this.renderUploadPreview();
this.showToast(`${files.length} Dateien ausgewählt`, 'success');
}
async readEntry(entry, basePath) {
const files = [];
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
if (this.shouldIgnore(entry.name, relativePath)) {
return files;
}
if (entry.isFile) {
return new Promise((resolve) => {
entry.file((file) => {
resolve([{
file: file,
relativePath: relativePath,
size: file.size
}]);
}, () => resolve([]));
});
} else if (entry.isDirectory) {
const reader = entry.createReader();
return new Promise((resolve) => {
reader.readEntries(async (entries) => {
for (const subEntry of entries) {
const subFiles = await this.readEntry(subEntry, relativePath);
files.push(...subFiles);
}
resolve(files);
}, () => resolve([]));
});
}
return files;
}
renderUploadPreview() {
const previewSection = $('#upload-preview-section');
const commitSection = $('#upload-commit-section');
const filesList = $('#upload-files-list');
const fileCount = $('#upload-file-count');
if (!previewSection || !filesList) return;
// Sektionen anzeigen
previewSection.classList.remove('hidden');
commitSection?.classList.remove('hidden');
// Dateianzahl
if (fileCount) {
fileCount.textContent = `${this.browserUploadFiles.length} Dateien`;
}
// Dateiliste rendern (max 100 anzeigen)
const displayFiles = this.browserUploadFiles.slice(0, 100);
filesList.innerHTML = displayFiles.map(f => `
<div class="upload-file-item">
<span class="file-icon">
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="2" fill="none"/><polyline points="14 2 14 8 20 8" stroke="currentColor" stroke-width="2" fill="none"/></svg>
</span>
<span class="file-path">${escapeHtml(f.relativePath)}</span>
<span class="file-size">${this.formatFileSizeUpload(f.size)}</span>
</div>
`).join('');
if (this.browserUploadFiles.length > 100) {
filesList.innerHTML += `
<div class="upload-file-item" style="justify-content: center; color: var(--text-tertiary);">
... und ${this.browserUploadFiles.length - 100} weitere Dateien
</div>
`;
}
}
formatFileSizeUpload(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
resetBrowserUploadUI() {
// Vorschau und Commit-Sektion verstecken
$('#upload-preview-section')?.classList.add('hidden');
$('#upload-commit-section')?.classList.add('hidden');
$('#upload-progress-container')?.classList.add('hidden');
// Felder zurücksetzen
const commitMessage = $('#browser-upload-commit-message');
if (commitMessage) commitMessage.value = '';
const filesList = $('#upload-files-list');
if (filesList) filesList.innerHTML = '';
// Progress zurücksetzen
const progressBar = $('#upload-progress-bar');
if (progressBar) progressBar.style.width = '0%';
const progressText = $('#upload-progress-text');
if (progressText) progressText.textContent = '0%';
}
cancelBrowserUpload() {
this.browserUploadFiles = [];
this.resetBrowserUploadUI();
this.showToast('Upload abgebrochen', 'info');
}
async executeBrowserUpload() {
// Validierung
const repoSelect = $('#browser-upload-repo-select');
const repoUrl = repoSelect?.value;
if (!repoUrl) {
this.showToast('Bitte wählen Sie ein Repository aus', 'error');
return;
}
if (this.browserUploadFiles.length === 0) {
this.showToast('Keine Dateien ausgewählt', 'error');
return;
}
const commitMessage = $('#browser-upload-commit-message')?.value.trim();
if (!commitMessage) {
this.showToast('Bitte geben Sie eine Commit-Nachricht ein', 'error');
return;
}
const branch = $('#browser-upload-branch')?.value || 'main';
// UI aktualisieren
const executeBtn = $('#btn-execute-upload');
const cancelBtn = $('#btn-cancel-upload');
const progressContainer = $('#upload-progress-container');
if (executeBtn) {
executeBtn.disabled = true;
executeBtn.innerHTML = '<span class="spinner"></span> Lädt hoch...';
}
if (cancelBtn) cancelBtn.disabled = true;
progressContainer?.classList.remove('hidden');
try {
// Session vorbereiten
const prepareResult = await api.prepareBrowserUpload();
this.browserUploadSessionId = prepareResult.sessionId;
// Upload durchführen
const result = await api.browserUploadAndPush({
files: this.browserUploadFiles,
repoUrl: repoUrl,
branch: branch,
commitMessage: commitMessage,
sessionId: this.browserUploadSessionId,
onProgress: (percent) => {
const progressBar = $('#upload-progress-bar');
const progressText = $('#upload-progress-text');
if (progressBar) progressBar.style.width = `${percent}%`;
if (progressText) progressText.textContent = `${percent}%`;
}
});
if (result.success) {
this.showToast(`Erfolgreich gepusht: ${result.filesCount} Dateien`, 'success');
this.cancelBrowserUpload(); // UI zurücksetzen
} else {
throw new Error(result.error || 'Upload fehlgeschlagen');
}
} catch (error) {
console.error('[Browser-Upload] Fehler:', error);
this.showToast(error.message || 'Upload fehlgeschlagen', 'error');
} finally {
// UI wiederherstellen
if (executeBtn) {
executeBtn.disabled = false;
executeBtn.innerHTML = `
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Commit & Push
`;
}
if (cancelBtn) cancelBtn.disabled = false;
progressContainer?.classList.add('hidden');
this.browserUploadSessionId = null;
}
}
// ===================== // =====================
// PROJEKT-MODUS METHODEN // PROJEKT-MODUS METHODEN
// ===================== // =====================

Datei anzeigen

@ -4,7 +4,7 @@
* Offline support and caching * Offline support and caching
*/ */
const CACHE_VERSION = '131'; const CACHE_VERSION = '133';
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION; const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION; const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION; const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;

Datei-Diff unterdrückt, da er zu groß ist Diff laden