Gitea-Fix
Dieser Commit ist enthalten in:
@ -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
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|||||||
@ -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.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
// =====================
|
// =====================
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
1368
logs/app.log
1368
logs/app.log
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
In neuem Issue referenzieren
Einen Benutzer sperren