new file: file_manager.py modified: requirements.txt new file: sessions_db.json modified: templates/base.html new file: templates/file_manager.html new file: templates/file_manager_new.html new file: templates/login.html new file: templates/profile.html new file: templates/users_management.html new file: user_management.py new file: users_db.json
2146 lines
62 KiB
HTML
2146 lines
62 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}File Manager - {{ super() }}{% endblock %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
/* File Manager Specific Styles */
|
|
.file-manager-container {
|
|
background: #fff;
|
|
border-radius: 10px;
|
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
|
overflow: hidden;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.file-manager-header {
|
|
background: linear-gradient(45deg, #667eea, #764ba2);
|
|
color: white;
|
|
padding: 1rem;
|
|
border-bottom: 1px solid #e9ecef;
|
|
}
|
|
|
|
.breadcrumb-nav {
|
|
background: #f8f9fa;
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid #e9ecef;
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 0.9rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.breadcrumb-item {
|
|
cursor: pointer;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 4px;
|
|
transition: background-color 0.2s;
|
|
}
|
|
|
|
.breadcrumb-item:hover {
|
|
background: #e9ecef;
|
|
}
|
|
|
|
.breadcrumb-separator {
|
|
color: #6c757d;
|
|
}
|
|
|
|
.file-manager-toolbar {
|
|
background: #f8f9fa;
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid #e9ecef;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.file-list {
|
|
max-height: 60vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.file-item {
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid #f8f9fa;
|
|
transition: all 0.2s ease;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.file-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.file-item.selected {
|
|
background: #e3f2fd;
|
|
border-left: 4px solid #2196f3;
|
|
}
|
|
|
|
.file-icon {
|
|
width: 24px;
|
|
text-align: center;
|
|
}
|
|
|
|
.file-icon.directory {
|
|
color: #ffc107;
|
|
}
|
|
|
|
.file-icon.text {
|
|
color: #17a2b8;
|
|
}
|
|
|
|
.file-icon.image {
|
|
color: #28a745;
|
|
}
|
|
|
|
.file-icon.archive {
|
|
color: #6f42c1;
|
|
}
|
|
|
|
.file-icon.executable {
|
|
color: #dc3545;
|
|
}
|
|
|
|
.file-details {
|
|
flex: 1;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.file-info {
|
|
display: flex;
|
|
gap: 1rem;
|
|
color: #6c757d;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.empty-folder {
|
|
text-align: center;
|
|
padding: 3rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.editor-container {
|
|
display: none;
|
|
border-top: 1px solid #e9ecef;
|
|
}
|
|
|
|
.editor-header {
|
|
background: #f8f9fa;
|
|
padding: 0.75rem 1rem;
|
|
border-bottom: 1px solid #e9ecef;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.editor-content textarea {
|
|
width: 100%;
|
|
height: 50vh;
|
|
border: none;
|
|
resize: vertical;
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 0.875rem;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.context-menu {
|
|
position: absolute;
|
|
background: white;
|
|
border: 1px solid #e9ecef;
|
|
border-radius: 6px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
z-index: 1000;
|
|
display: none;
|
|
min-width: 180px;
|
|
}
|
|
|
|
.context-menu-item {
|
|
padding: 0.5rem 1rem;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s;
|
|
border-bottom: 1px solid #f8f9fa;
|
|
}
|
|
|
|
.context-menu-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.context-menu-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.context-menu-item.danger:hover {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
|
|
.upload-zone {
|
|
border: 2px dashed #dee2e6;
|
|
border-radius: 8px;
|
|
padding: 2rem;
|
|
text-align: center;
|
|
margin: 1rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.upload-zone.dragover {
|
|
border-color: #007bff;
|
|
background: #f8f9fa;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="file-manager-container">
|
|
<!-- Header -->
|
|
<div class="file-manager-header">
|
|
<h4 class="mb-0">
|
|
<i class="fas fa-folder-open me-2"></i>
|
|
File Manager
|
|
</h4>
|
|
<p class="mb-0 mt-1 opacity-75">Verwalte deine Projektdateien</p>
|
|
</div>
|
|
|
|
<!-- Breadcrumb Navigation -->
|
|
<div class="breadcrumb-nav" id="breadcrumbNav">
|
|
<i class="fas fa-home"></i>
|
|
<span class="breadcrumb-item" data-path="">Root</span>
|
|
</div>
|
|
|
|
<!-- Toolbar -->
|
|
<div class="file-manager-toolbar">
|
|
<button class="btn btn-sm btn-outline-primary" onclick="createFolder()">
|
|
<i class="fas fa-folder-plus me-1"></i> Neuer Ordner
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-success" onclick="createFile()">
|
|
<i class="fas fa-file-plus me-1"></i> Neue Datei
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-info" onclick="uploadFile()">
|
|
<i class="fas fa-upload me-1"></i> Hochladen
|
|
</button>
|
|
<div class="ms-auto">
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="refreshFileList()">
|
|
<i class="fas fa-sync-alt me-1"></i> Aktualisieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File List -->
|
|
<div class="file-list" id="fileList">
|
|
<div class="loading">
|
|
<i class="fas fa-spinner fa-spin me-2"></i>
|
|
Lade Dateien...
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File Editor -->
|
|
<div class="editor-container" id="editorContainer">
|
|
<div class="editor-header">
|
|
<span id="editorFileName">Datei.txt</span>
|
|
<div>
|
|
<button class="btn btn-sm btn-success me-2" onclick="saveFile()">
|
|
<i class="fas fa-save me-1"></i> Speichern
|
|
</button>
|
|
<button class="btn btn-sm btn-secondary" onclick="closeEditor()">
|
|
<i class="fas fa-times me-1"></i> Schließen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="editor-content">
|
|
<textarea id="fileEditor" placeholder="Dateiinhalt..."></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload Zone (Hidden) -->
|
|
<div class="upload-zone" id="uploadZone" style="display: none;">
|
|
<i class="fas fa-cloud-upload-alt fa-3x mb-3 text-muted"></i>
|
|
<p>Dateien hier ablegen zum Hochladen</p>
|
|
<input type="file" id="fileInput" multiple style="display: none;">
|
|
</div>
|
|
|
|
<!-- Context Menu -->
|
|
<div class="context-menu" id="contextMenu">
|
|
<div class="context-menu-item" onclick="openFile()">
|
|
<i class="fas fa-eye me-2"></i> Öffnen
|
|
</div>
|
|
<div class="context-menu-item" onclick="editFile()">
|
|
<i class="fas fa-edit me-2"></i> Bearbeiten
|
|
</div>
|
|
<div class="context-menu-item" onclick="downloadFile()">
|
|
<i class="fas fa-download me-2"></i> Herunterladen
|
|
</div>
|
|
<div class="context-menu-item" onclick="renameItem()">
|
|
<i class="fas fa-i-cursor me-2"></i> Umbenennen
|
|
</div>
|
|
<div class="context-menu-item" onclick="copyItem()">
|
|
<i class="fas fa-copy me-2"></i> Kopieren
|
|
</div>
|
|
<div class="context-menu-item danger" onclick="deleteItem()">
|
|
<i class="fas fa-trash me-2"></i> Löschen
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modals -->
|
|
<div class="modal fade" id="createFolderModal" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Neuer Ordner</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="text" class="form-control" id="folderName" placeholder="Ordnername">
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
<button type="button" class="btn btn-primary" onclick="createFolderConfirm()">Erstellen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="createFileModal" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Neue Datei</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="text" class="form-control" id="fileName" placeholder="Dateiname.txt">
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
<button type="button" class="btn btn-primary" onclick="createFileConfirm()">Erstellen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="renameModal" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Umbenennen</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="text" class="form-control" id="newName" placeholder="Neuer Name">
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
<button type="button" class="btn btn-primary" onclick="renameConfirm()">Umbenennen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Global Variables
|
|
let currentPath = '';
|
|
let selectedItem = null;
|
|
let currentEditFile = null;
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadFileList();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function setupEventListeners() {
|
|
// Context Menu
|
|
document.addEventListener('click', hideContextMenu);
|
|
document.addEventListener('contextmenu', function(e) {
|
|
if (!e.target.closest('.file-item')) {
|
|
hideContextMenu();
|
|
}
|
|
});
|
|
|
|
// File Upload
|
|
const fileInput = document.getElementById('fileInput');
|
|
fileInput.addEventListener('change', handleFileUpload);
|
|
}
|
|
|
|
// API Functions
|
|
function loadFileList() {
|
|
showLoading();
|
|
|
|
fetch(`/api/files/list?path=${encodeURIComponent(currentPath)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Laden: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
renderFileList(data.items);
|
|
updateBreadcrumb(data.breadcrumbs);
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
function renderFileList(items) {
|
|
const fileList = document.getElementById('fileList');
|
|
|
|
if (items.length === 0) {
|
|
fileList.innerHTML = `
|
|
<div class="empty-folder">
|
|
<i class="fas fa-folder-open fa-3x mb-3"></i>
|
|
<p>Dieser Ordner ist leer</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
|
|
items.forEach(item => {
|
|
const icon = getFileIcon(item);
|
|
const itemClass = item.type === 'directory' ? 'directory' : item.file_type || 'unknown';
|
|
|
|
html += `
|
|
<div class="file-item"
|
|
data-path="${item.path}"
|
|
data-type="${item.type}"
|
|
data-name="${item.name}"
|
|
onclick="selectItem(this)"
|
|
ondblclick="openItem('${item.path}', '${item.type}')"
|
|
oncontextmenu="showContextMenu(event, this)">
|
|
<div class="file-icon ${itemClass}">
|
|
${icon}
|
|
</div>
|
|
<div class="file-details">
|
|
<div>
|
|
<strong>${item.name}</strong>
|
|
${item.is_parent ? '<small class="text-muted">(Zurück)</small>' : ''}
|
|
</div>
|
|
<div class="file-info">
|
|
${item.size ? `<span>${item.size}</span>` : ''}
|
|
${item.modified ? `<span>${item.modified}</span>` : ''}
|
|
<span class="text-muted">${item.permissions || ''}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
fileList.innerHTML = html;
|
|
}
|
|
|
|
function getFileIcon(item) {
|
|
if (item.is_parent) {
|
|
return '<i class="fas fa-arrow-left"></i>';
|
|
}
|
|
|
|
if (item.type === 'directory') {
|
|
return '<i class="fas fa-folder"></i>';
|
|
}
|
|
|
|
switch (item.file_type) {
|
|
case 'text':
|
|
return '<i class="fas fa-file-alt"></i>';
|
|
case 'image':
|
|
return '<i class="fas fa-file-image"></i>';
|
|
case 'archive':
|
|
return '<i class="fas fa-file-archive"></i>';
|
|
case 'executable':
|
|
return '<i class="fas fa-cog"></i>';
|
|
default:
|
|
return '<i class="fas fa-file"></i>';
|
|
}
|
|
}
|
|
|
|
function updateBreadcrumb(breadcrumbs) {
|
|
const nav = document.getElementById('breadcrumbNav');
|
|
|
|
let html = '<i class="fas fa-home"></i>';
|
|
|
|
breadcrumbs.forEach((crumb, index) => {
|
|
if (index > 0) {
|
|
html += '<span class="breadcrumb-separator">/</span>';
|
|
}
|
|
|
|
html += `<span class="breadcrumb-item" data-path="${crumb.path}" onclick="navigateTo('${crumb.path}')">${crumb.name}</span>`;
|
|
});
|
|
|
|
nav.innerHTML = html;
|
|
}
|
|
|
|
// Navigation Functions
|
|
function navigateTo(path) {
|
|
currentPath = path;
|
|
loadFileList();
|
|
}
|
|
|
|
function openItem(path, type) {
|
|
if (type === 'directory') {
|
|
navigateTo(path);
|
|
} else {
|
|
editFile(path);
|
|
}
|
|
}
|
|
|
|
function selectItem(element) {
|
|
// Remove previous selection
|
|
document.querySelectorAll('.file-item.selected').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
|
|
// Select current item
|
|
element.classList.add('selected');
|
|
selectedItem = {
|
|
path: element.dataset.path,
|
|
type: element.dataset.type,
|
|
name: element.dataset.name
|
|
};
|
|
}
|
|
|
|
// Context Menu Functions
|
|
function showContextMenu(event, element) {
|
|
event.preventDefault();
|
|
selectItem(element);
|
|
|
|
const contextMenu = document.getElementById('contextMenu');
|
|
contextMenu.style.display = 'block';
|
|
contextMenu.style.left = event.pageX + 'px';
|
|
contextMenu.style.top = event.pageY + 'px';
|
|
}
|
|
|
|
function hideContextMenu() {
|
|
document.getElementById('contextMenu').style.display = 'none';
|
|
}
|
|
|
|
// File Operations
|
|
function openFile() {
|
|
if (!selectedItem) return;
|
|
|
|
if (selectedItem.type === 'directory') {
|
|
navigateTo(selectedItem.path);
|
|
} else {
|
|
window.open(`/api/files/download?path=${encodeURIComponent(selectedItem.path)}`, '_blank');
|
|
}
|
|
hideContextMenu();
|
|
}
|
|
|
|
function editFile(path = null) {
|
|
const filePath = path || (selectedItem ? selectedItem.path : null);
|
|
|
|
if (!filePath || (selectedItem && selectedItem.type === 'directory')) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/files/read?path=${encodeURIComponent(filePath)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Laden der Datei: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
if (data.is_binary) {
|
|
showError('Binäre Dateien können nicht bearbeitet werden.');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('editorFileName').textContent = filePath.split('/').pop();
|
|
document.getElementById('fileEditor').value = data.content;
|
|
document.getElementById('editorContainer').style.display = 'block';
|
|
currentEditFile = filePath;
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
|
|
hideContextMenu();
|
|
}
|
|
|
|
function saveFile() {
|
|
if (!currentEditFile) return;
|
|
|
|
const content = document.getElementById('fileEditor').value;
|
|
|
|
fetch('/api/files/write', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
path: currentEditFile,
|
|
content: content
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Speichern: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
showSuccess('Datei gespeichert!');
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
function closeEditor() {
|
|
document.getElementById('editorContainer').style.display = 'none';
|
|
currentEditFile = null;
|
|
}
|
|
|
|
function downloadFile() {
|
|
if (!selectedItem || selectedItem.type === 'directory') return;
|
|
|
|
window.open(`/api/files/download?path=${encodeURIComponent(selectedItem.path)}`, '_blank');
|
|
hideContextMenu();
|
|
}
|
|
|
|
function deleteItem() {
|
|
if (!selectedItem) return;
|
|
|
|
if (confirm(`Möchten Sie "${selectedItem.name}" wirklich löschen?`)) {
|
|
fetch('/api/files/delete', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
path: selectedItem.path
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Löschen: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
showSuccess('Element gelöscht!');
|
|
loadFileList();
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
hideContextMenu();
|
|
}
|
|
|
|
// Create Operations
|
|
function createFolder() {
|
|
document.getElementById('folderName').value = '';
|
|
new bootstrap.Modal(document.getElementById('createFolderModal')).show();
|
|
}
|
|
|
|
function createFolderConfirm() {
|
|
const name = document.getElementById('folderName').value.trim();
|
|
if (!name) return;
|
|
|
|
const path = currentPath ? `${currentPath}/${name}` : name;
|
|
|
|
fetch('/api/files/create_directory', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
path: path
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Erstellen: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
showSuccess('Ordner erstellt!');
|
|
loadFileList();
|
|
bootstrap.Modal.getInstance(document.getElementById('createFolderModal')).hide();
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
function createFile() {
|
|
document.getElementById('fileName').value = '';
|
|
new bootstrap.Modal(document.getElementById('createFileModal')).show();
|
|
}
|
|
|
|
function createFileConfirm() {
|
|
const name = document.getElementById('fileName').value.trim();
|
|
if (!name) return;
|
|
|
|
const path = currentPath ? `${currentPath}/${name}` : name;
|
|
|
|
fetch('/api/files/write', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
path: path,
|
|
content: ''
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Erstellen: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
showSuccess('Datei erstellt!');
|
|
loadFileList();
|
|
bootstrap.Modal.getInstance(document.getElementById('createFileModal')).hide();
|
|
|
|
// Öffne die neue Datei zum Bearbeiten
|
|
setTimeout(() => editFile(path), 500);
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
// Rename Operation
|
|
function renameItem() {
|
|
if (!selectedItem) return;
|
|
|
|
document.getElementById('newName').value = selectedItem.name;
|
|
new bootstrap.Modal(document.getElementById('renameModal')).show();
|
|
hideContextMenu();
|
|
}
|
|
|
|
function renameConfirm() {
|
|
const newName = document.getElementById('newName').value.trim();
|
|
if (!newName || !selectedItem) return;
|
|
|
|
fetch('/api/files/rename', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
path: selectedItem.path,
|
|
new_name: newName
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Umbenennen: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
showSuccess('Element umbenannt!');
|
|
loadFileList();
|
|
bootstrap.Modal.getInstance(document.getElementById('renameModal')).hide();
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
// Upload Operations
|
|
function uploadFile() {
|
|
document.getElementById('fileInput').click();
|
|
}
|
|
|
|
function handleFileUpload(event) {
|
|
const files = event.target.files;
|
|
if (!files.length) return;
|
|
|
|
const formData = new FormData();
|
|
for (let file of files) {
|
|
formData.append('file', file);
|
|
}
|
|
formData.append('path', currentPath);
|
|
|
|
fetch('/api/files/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Hochladen: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
showSuccess('Datei(en) hochgeladen!');
|
|
loadFileList();
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
// Utility Functions
|
|
function refreshFileList() {
|
|
loadFileList();
|
|
}
|
|
|
|
function showLoading() {
|
|
document.getElementById('fileList').innerHTML = `
|
|
<div class="loading">
|
|
<i class="fas fa-spinner fa-spin me-2"></i>
|
|
Lade Dateien...
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function showError(message) {
|
|
// Use existing flash message system
|
|
console.error(message);
|
|
alert('Fehler: ' + message);
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
// Use existing flash message system
|
|
console.log(message);
|
|
// You could also show a toast notification here
|
|
}
|
|
|
|
function copyItem() {
|
|
// Placeholder for copy functionality
|
|
showError('Kopieren ist noch nicht implementiert.');
|
|
hideContextMenu();
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="file-manager-container">
|
|
<!-- Header -->
|
|
<div class="file-manager-header">
|
|
<h4 class="mb-0">
|
|
<i class="fas fa-folder-open me-2"></i>
|
|
File Manager
|
|
</h4>
|
|
<p class="mb-0 mt-1 opacity-75">Verwalte deine Projektdateien</p>
|
|
</div>
|
|
|
|
<!-- Breadcrumb Navigation -->
|
|
<div class="breadcrumb-nav" id="breadcrumbNav">
|
|
<i class="fas fa-home"></i>
|
|
<span class="breadcrumb-item" data-path="">Root</span>
|
|
</div>
|
|
|
|
<!-- Toolbar -->
|
|
<div class="file-manager-toolbar">
|
|
<button class="btn btn-sm btn-outline-primary" onclick="createFolder()">
|
|
<i class="fas fa-folder-plus me-1"></i> Neuer Ordner
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-success" onclick="createFile()">
|
|
<i class="fas fa-file-plus me-1"></i> Neue Datei
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-info" onclick="uploadFile()">
|
|
<i class="fas fa-upload me-1"></i> Hochladen
|
|
</button>
|
|
<div class="ms-auto">
|
|
<button class="btn btn-sm btn-outline-secondary" onclick="refreshFileList()">
|
|
<i class="fas fa-sync-alt me-1"></i> Aktualisieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File List -->
|
|
<div class="file-list" id="fileList">
|
|
<div class="loading">
|
|
<i class="fas fa-spinner fa-spin me-2"></i>
|
|
Lade Dateien...
|
|
</div>
|
|
</div>
|
|
|
|
<!-- File Editor -->
|
|
<div class="editor-container" id="editorContainer">
|
|
<div class="editor-header">
|
|
<span id="editorFileName">Datei.txt</span>
|
|
<div>
|
|
<button class="btn btn-sm btn-success me-2" onclick="saveFile()">
|
|
<i class="fas fa-save me-1"></i> Speichern
|
|
</button>
|
|
<button class="btn btn-sm btn-secondary" onclick="closeEditor()">
|
|
<i class="fas fa-times me-1"></i> Schließen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="editor-content">
|
|
<textarea id="fileEditor" placeholder="Dateiinhalt..."></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload Zone (Hidden) -->
|
|
<div class="upload-zone" id="uploadZone" style="display: none;">
|
|
<i class="fas fa-cloud-upload-alt fa-3x mb-3 text-muted"></i>
|
|
<p>Dateien hier ablegen zum Hochladen</p>
|
|
<input type="file" id="fileInput" multiple style="display: none;">
|
|
</div>
|
|
|
|
<!-- Context Menu -->
|
|
<div class="context-menu" id="contextMenu">
|
|
<div class="context-menu-item" onclick="openFile()">
|
|
<i class="fas fa-eye me-2"></i> Öffnen
|
|
</div>
|
|
<div class="context-menu-item" onclick="editFile()">
|
|
<i class="fas fa-edit me-2"></i> Bearbeiten
|
|
</div>
|
|
<div class="context-menu-item" onclick="downloadFile()">
|
|
<i class="fas fa-download me-2"></i> Herunterladen
|
|
</div>
|
|
<div class="context-menu-item" onclick="renameItem()">
|
|
<i class="fas fa-i-cursor me-2"></i> Umbenennen
|
|
</div>
|
|
<div class="context-menu-item" onclick="copyItem()">
|
|
<i class="fas fa-copy me-2"></i> Kopieren
|
|
</div>
|
|
<div class="context-menu-item danger" onclick="deleteItem()">
|
|
<i class="fas fa-trash me-2"></i> Löschen
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modals -->
|
|
<div class="modal fade" id="createFolderModal" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Neuer Ordner</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="text" class="form-control" id="folderName" placeholder="Ordnername">
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
<button type="button" class="btn btn-primary" onclick="createFolderConfirm()">Erstellen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="createFileModal" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Neue Datei</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="text" class="form-control" id="fileName" placeholder="Dateiname.txt">
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
<button type="button" class="btn btn-primary" onclick="createFileConfirm()">Erstellen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="renameModal" tabindex="-1">
|
|
<div class="modal-dialog modal-sm">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Umbenennen</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="text" class="form-control" id="newName" placeholder="Neuer Name">
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
|
|
<button type="button" class="btn btn-primary" onclick="renameConfirm()">Umbenennen</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Global Variables
|
|
let currentPath = '';
|
|
let selectedItem = null;
|
|
let currentEditFile = null;
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadFileList();
|
|
setupEventListeners();
|
|
});
|
|
|
|
function setupEventListeners() {
|
|
// Context Menu
|
|
document.addEventListener('click', hideContextMenu);
|
|
document.addEventListener('contextmenu', function(e) {
|
|
if (!e.target.closest('.file-item')) {
|
|
hideContextMenu();
|
|
}
|
|
});
|
|
|
|
// File Upload
|
|
const fileInput = document.getElementById('fileInput');
|
|
fileInput.addEventListener('change', handleFileUpload);
|
|
}
|
|
|
|
// API Functions
|
|
function loadFileList() {
|
|
showLoading();
|
|
|
|
fetch(`/api/files/list?path=${encodeURIComponent(currentPath)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Laden: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
renderFileList(data.items);
|
|
updateBreadcrumb(data.breadcrumbs);
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
function renderFileList(items) {
|
|
const fileList = document.getElementById('fileList');
|
|
|
|
if (items.length === 0) {
|
|
fileList.innerHTML = `
|
|
<div class="empty-folder">
|
|
<i class="fas fa-folder-open fa-3x mb-3"></i>
|
|
<p>Dieser Ordner ist leer</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
|
|
items.forEach(item => {
|
|
const icon = getFileIcon(item);
|
|
const itemClass = item.type === 'directory' ? 'directory' : item.file_type || 'unknown';
|
|
|
|
html += `
|
|
<div class="file-item"
|
|
data-path="${item.path}"
|
|
data-type="${item.type}"
|
|
data-name="${item.name}"
|
|
onclick="selectItem(this)"
|
|
ondblclick="openItem('${item.path}', '${item.type}')"
|
|
oncontextmenu="showContextMenu(event, this)">
|
|
<div class="file-icon ${itemClass}">
|
|
${icon}
|
|
</div>
|
|
<div class="file-details">
|
|
<div>
|
|
<strong>${item.name}</strong>
|
|
${item.is_parent ? '<small class="text-muted">(Zurück)</small>' : ''}
|
|
</div>
|
|
<div class="file-info">
|
|
${item.size ? `<span>${item.size}</span>` : ''}
|
|
${item.modified ? `<span>${item.modified}</span>` : ''}
|
|
<span class="text-muted">${item.permissions || ''}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
fileList.innerHTML = html;
|
|
}
|
|
|
|
function getFileIcon(item) {
|
|
if (item.is_parent) {
|
|
return '<i class="fas fa-arrow-left"></i>';
|
|
}
|
|
|
|
if (item.type === 'directory') {
|
|
return '<i class="fas fa-folder"></i>';
|
|
}
|
|
|
|
switch (item.file_type) {
|
|
case 'text':
|
|
return '<i class="fas fa-file-alt"></i>';
|
|
case 'image':
|
|
return '<i class="fas fa-file-image"></i>';
|
|
case 'archive':
|
|
return '<i class="fas fa-file-archive"></i>';
|
|
case 'executable':
|
|
return '<i class="fas fa-cog"></i>';
|
|
default:
|
|
return '<i class="fas fa-file"></i>';
|
|
}
|
|
}
|
|
|
|
function updateBreadcrumb(breadcrumbs) {
|
|
const nav = document.getElementById('breadcrumbNav');
|
|
|
|
let html = '<i class="fas fa-home"></i>';
|
|
|
|
breadcrumbs.forEach((crumb, index) => {
|
|
if (index > 0) {
|
|
html += '<span class="breadcrumb-separator">/</span>';
|
|
}
|
|
|
|
html += `<span class="breadcrumb-item" data-path="${crumb.path}" onclick="navigateTo('${crumb.path}')">${crumb.name}</span>`;
|
|
});
|
|
|
|
nav.innerHTML = html;
|
|
}
|
|
|
|
// Navigation Functions
|
|
function navigateTo(path) {
|
|
currentPath = path;
|
|
loadFileList();
|
|
}
|
|
|
|
function openItem(path, type) {
|
|
if (type === 'directory') {
|
|
navigateTo(path);
|
|
} else {
|
|
editFile(path);
|
|
}
|
|
}
|
|
|
|
function selectItem(element) {
|
|
// Remove previous selection
|
|
document.querySelectorAll('.file-item.selected').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
|
|
// Select current item
|
|
element.classList.add('selected');
|
|
selectedItem = {
|
|
path: element.dataset.path,
|
|
type: element.dataset.type,
|
|
name: element.dataset.name
|
|
};
|
|
}
|
|
|
|
// Context Menu Functions
|
|
function showContextMenu(event, element) {
|
|
event.preventDefault();
|
|
selectItem(element);
|
|
|
|
const contextMenu = document.getElementById('contextMenu');
|
|
contextMenu.style.display = 'block';
|
|
contextMenu.style.left = event.pageX + 'px';
|
|
contextMenu.style.top = event.pageY + 'px';
|
|
}
|
|
|
|
function hideContextMenu() {
|
|
document.getElementById('contextMenu').style.display = 'none';
|
|
}
|
|
|
|
// File Operations
|
|
function openFile() {
|
|
if (!selectedItem) return;
|
|
|
|
if (selectedItem.type === 'directory') {
|
|
navigateTo(selectedItem.path);
|
|
} else {
|
|
window.open(`/api/files/download?path=${encodeURIComponent(selectedItem.path)}`, '_blank');
|
|
}
|
|
hideContextMenu();
|
|
}
|
|
|
|
function editFile(path = null) {
|
|
const filePath = path || (selectedItem ? selectedItem.path : null);
|
|
|
|
if (!filePath || (selectedItem && selectedItem.type === 'directory')) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/api/files/read?path=${encodeURIComponent(filePath)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Laden der Datei: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
if (data.is_binary) {
|
|
showError('Binäre Dateien können nicht bearbeitet werden.');
|
|
return;
|
|
}
|
|
|
|
document.getElementById('editorFileName').textContent = filePath.split('/').pop();
|
|
document.getElementById('fileEditor').value = data.content;
|
|
document.getElementById('editorContainer').style.display = 'block';
|
|
currentEditFile = filePath;
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
|
|
hideContextMenu();
|
|
}
|
|
|
|
function saveFile() {
|
|
if (!currentEditFile) return;
|
|
|
|
const content = document.getElementById('fileEditor').value;
|
|
|
|
fetch('/api/files/write', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
path: currentEditFile,
|
|
content: content
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Speichern: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
showSuccess('Datei gespeichert!');
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
function closeEditor() {
|
|
document.getElementById('editorContainer').style.display = 'none';
|
|
currentEditFile = null;
|
|
}
|
|
|
|
function downloadFile() {
|
|
if (!selectedItem || selectedItem.type === 'directory') return;
|
|
|
|
window.open(`/api/files/download?path=${encodeURIComponent(selectedItem.path)}`, '_blank');
|
|
hideContextMenu();
|
|
}
|
|
|
|
function deleteItem() {
|
|
if (!selectedItem) return;
|
|
|
|
if (confirm(`Möchten Sie "${selectedItem.name}" wirklich löschen?`)) {
|
|
fetch('/api/files/delete', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
path: selectedItem.path
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Löschen: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
showSuccess('Element gelöscht!');
|
|
loadFileList();
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
hideContextMenu();
|
|
}
|
|
|
|
// Create Operations
|
|
function createFolder() {
|
|
document.getElementById('folderName').value = '';
|
|
new bootstrap.Modal(document.getElementById('createFolderModal')).show();
|
|
}
|
|
|
|
function createFolderConfirm() {
|
|
const name = document.getElementById('folderName').value.trim();
|
|
if (!name) return;
|
|
|
|
const path = currentPath ? `${currentPath}/${name}` : name;
|
|
|
|
fetch('/api/files/create_directory', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
path: path
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Erstellen: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
showSuccess('Ordner erstellt!');
|
|
loadFileList();
|
|
bootstrap.Modal.getInstance(document.getElementById('createFolderModal')).hide();
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
function createFile() {
|
|
document.getElementById('fileName').value = '';
|
|
new bootstrap.Modal(document.getElementById('createFileModal')).show();
|
|
}
|
|
|
|
function createFileConfirm() {
|
|
const name = document.getElementById('fileName').value.trim();
|
|
if (!name) return;
|
|
|
|
const path = currentPath ? `${currentPath}/${name}` : name;
|
|
|
|
fetch('/api/files/write', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
path: path,
|
|
content: ''
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Erstellen: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
showSuccess('Datei erstellt!');
|
|
loadFileList();
|
|
bootstrap.Modal.getInstance(document.getElementById('createFileModal')).hide();
|
|
|
|
// Öffne die neue Datei zum Bearbeiten
|
|
setTimeout(() => editFile(path), 500);
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
// Rename Operation
|
|
function renameItem() {
|
|
if (!selectedItem) return;
|
|
|
|
document.getElementById('newName').value = selectedItem.name;
|
|
new bootstrap.Modal(document.getElementById('renameModal')).show();
|
|
hideContextMenu();
|
|
}
|
|
|
|
function renameConfirm() {
|
|
const newName = document.getElementById('newName').value.trim();
|
|
if (!newName || !selectedItem) return;
|
|
|
|
fetch('/api/files/rename', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
path: selectedItem.path,
|
|
new_name: newName
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Umbenennen: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
showSuccess('Element umbenannt!');
|
|
loadFileList();
|
|
bootstrap.Modal.getInstance(document.getElementById('renameModal')).hide();
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
// Upload Operations
|
|
function uploadFile() {
|
|
document.getElementById('fileInput').click();
|
|
}
|
|
|
|
function handleFileUpload(event) {
|
|
const files = event.target.files;
|
|
if (!files.length) return;
|
|
|
|
const formData = new FormData();
|
|
for (let file of files) {
|
|
formData.append('file', file);
|
|
}
|
|
formData.append('path', currentPath);
|
|
|
|
fetch('/api/files/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError('Fehler beim Hochladen: ' + data.error);
|
|
return;
|
|
}
|
|
|
|
showSuccess('Datei(en) hochgeladen!');
|
|
loadFileList();
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
// Utility Functions
|
|
function refreshFileList() {
|
|
loadFileList();
|
|
}
|
|
|
|
function showLoading() {
|
|
document.getElementById('fileList').innerHTML = `
|
|
<div class="loading">
|
|
<i class="fas fa-spinner fa-spin me-2"></i>
|
|
Lade Dateien...
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function showError(message) {
|
|
// Use existing flash message system
|
|
console.error(message);
|
|
alert('Fehler: ' + message);
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
// Use existing flash message system
|
|
console.log(message);
|
|
// You could also show a toast notification here
|
|
}
|
|
|
|
function copyItem() {
|
|
// Placeholder for copy functionality
|
|
showError('Kopieren ist noch nicht implementiert.');
|
|
hideContextMenu();
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|
|
.file-icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
margin-right: 10px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.file-icon.folder { color: #ffc107; }
|
|
.file-icon.code { color: #28a745; }
|
|
.file-icon.text { color: #6c757d; }
|
|
.file-icon.image { color: #17a2b8; }
|
|
.file-icon.archive { color: #fd7e14; }
|
|
.file-icon.config { color: #dc3545; }
|
|
|
|
.file-details {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.file-name {
|
|
font-weight: 500;
|
|
flex-grow: 1;
|
|
}
|
|
|
|
.file-meta {
|
|
font-size: 0.8rem;
|
|
color: #6c757d;
|
|
text-align: right;
|
|
}
|
|
|
|
.action-toolbar {
|
|
background: #f8f9fa;
|
|
padding: 0.75rem 1rem;
|
|
border-top: 1px solid #e9ecef;
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.editor-container {
|
|
display: none;
|
|
border-top: 1px solid #e9ecef;
|
|
}
|
|
|
|
.code-editor {
|
|
width: 100%;
|
|
height: 500px;
|
|
border: none;
|
|
padding: 1rem;
|
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
background: #2d3748;
|
|
color: #e2e8f0;
|
|
resize: vertical;
|
|
}
|
|
|
|
.upload-zone {
|
|
border: 2px dashed #ced4da;
|
|
border-radius: 8px;
|
|
padding: 2rem;
|
|
text-align: center;
|
|
margin: 1rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.upload-zone:hover, .upload-zone.dragover {
|
|
border-color: #007bff;
|
|
background: #f8f9ff;
|
|
}
|
|
|
|
.permission-badge {
|
|
font-size: 0.7rem;
|
|
padding: 0.2rem 0.4rem;
|
|
border-radius: 3px;
|
|
margin-left: 0.5rem;
|
|
}
|
|
|
|
.permission-r { background: #d4edda; color: #155724; }
|
|
.permission-w { background: #fff3cd; color: #856404; }
|
|
.permission-x { background: #f8d7da; color: #721c24; }
|
|
|
|
/* Context Menu */
|
|
.context-menu {
|
|
position: absolute;
|
|
background: white;
|
|
border: 1px solid #ced4da;
|
|
border-radius: 6px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
z-index: 1000;
|
|
min-width: 180px;
|
|
display: none;
|
|
}
|
|
|
|
.context-menu-item {
|
|
padding: 0.5rem 1rem;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid #f8f9fa;
|
|
transition: background 0.2s ease;
|
|
}
|
|
|
|
.context-menu-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.context-menu-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.context-menu-item.disabled {
|
|
color: #6c757d;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.context-menu-item.disabled:hover {
|
|
background: transparent;
|
|
}
|
|
|
|
/* Progress Bar für Upload */
|
|
.upload-progress {
|
|
display: none;
|
|
margin: 1rem;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.file-details {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.file-meta {
|
|
text-align: left;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.action-toolbar {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.action-toolbar .btn {
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h2>
|
|
<i class="fas fa-folder-open me-2 text-primary"></i>
|
|
File Manager - {{ project_name }}
|
|
</h2>
|
|
<p class="text-muted mb-0">Verwalten Sie Projektdateien direkt im Browser</p>
|
|
</div>
|
|
<a href="{{ url_for('project_details', project_name=project_name) }}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-arrow-left"></i> Zurück zum Projekt
|
|
</a>
|
|
</div>
|
|
|
|
<div class="file-manager-container">
|
|
<!-- Header mit Pfad-Navigation -->
|
|
<div class="file-manager-header">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h5 class="mb-1">
|
|
<i class="fas fa-server me-2"></i>{{ project_name }}
|
|
</h5>
|
|
<small class="opacity-75">Aktueller Pfad: {{ current_path or '/' }}</small>
|
|
</div>
|
|
<div>
|
|
<button class="btn btn-light btn-sm" onclick="refreshFileList()">
|
|
<i class="fas fa-sync-alt"></i> Aktualisieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Breadcrumb Navigation -->
|
|
<div class="breadcrumb-nav">
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb mb-0">
|
|
<li class="breadcrumb-item">
|
|
<a href="#" onclick="navigateToPath('/')" class="text-decoration-none">
|
|
<i class="fas fa-home"></i> Root
|
|
</a>
|
|
</li>
|
|
{% if current_path and current_path != '/' %}
|
|
{% set path_parts = current_path.strip('/').split('/') %}
|
|
{% for part in path_parts %}
|
|
{% if not loop.last %}
|
|
<li class="breadcrumb-item">
|
|
<a href="#" onclick="navigateToPath('/{{ path_parts[:loop.index]|join('/') }}')" class="text-decoration-none">
|
|
{{ part }}
|
|
</a>
|
|
</li>
|
|
{% else %}
|
|
<li class="breadcrumb-item active" aria-current="page">{{ part }}</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% endif %}
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- File List -->
|
|
<div class="file-list" id="fileList">
|
|
<!-- Wird dynamisch geladen -->
|
|
<div class="text-center p-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Lade Dateien...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Lade Dateiliste...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Toolbar -->
|
|
<div class="action-toolbar">
|
|
<button class="btn btn-primary btn-sm" onclick="showUploadModal()">
|
|
<i class="fas fa-upload"></i> Datei hochladen
|
|
</button>
|
|
<button class="btn btn-success btn-sm" onclick="createNewFile()">
|
|
<i class="fas fa-file-plus"></i> Neue Datei
|
|
</button>
|
|
<button class="btn btn-info btn-sm" onclick="createNewFolder()">
|
|
<i class="fas fa-folder-plus"></i> Neuer Ordner
|
|
</button>
|
|
<button class="btn btn-warning btn-sm" onclick="downloadSelected()" id="btnDownload" disabled>
|
|
<i class="fas fa-download"></i> Download
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm" onclick="editSelected()" id="btnEdit" disabled>
|
|
<i class="fas fa-edit"></i> Bearbeiten
|
|
</button>
|
|
<button class="btn btn-danger btn-sm" onclick="deleteSelected()" id="btnDelete" disabled>
|
|
<i class="fas fa-trash"></i> Löschen
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Inline Editor -->
|
|
<div class="editor-container" id="editorContainer">
|
|
<div class="p-3 bg-light border-bottom">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0">
|
|
<i class="fas fa-edit me-2"></i>
|
|
Editor: <span id="editingFileName"></span>
|
|
</h6>
|
|
<div>
|
|
<button class="btn btn-success btn-sm" onclick="saveFile()">
|
|
<i class="fas fa-save"></i> Speichern
|
|
</button>
|
|
<button class="btn btn-secondary btn-sm" onclick="closeEditor()">
|
|
<i class="fas fa-times"></i> Schließen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<textarea class="code-editor" id="codeEditor" placeholder="Dateiinhalt wird geladen..."></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload Modal -->
|
|
<div class="modal fade" id="uploadModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-upload me-2"></i>Dateien hochladen
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="upload-zone" id="uploadZone">
|
|
<i class="fas fa-cloud-upload-alt fa-3x text-muted mb-3"></i>
|
|
<h5>Dateien hier ablegen oder klicken zum Auswählen</h5>
|
|
<p class="text-muted">Unterstützte Formate: Alle Dateitypen (Max. 50MB pro Datei)</p>
|
|
<input type="file" class="d-none" id="fileInput" multiple>
|
|
</div>
|
|
<div class="upload-progress">
|
|
<div class="progress">
|
|
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
|
|
</div>
|
|
<small class="text-muted mt-1">Upload läuft...</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Context Menu -->
|
|
<div class="context-menu" id="contextMenu">
|
|
<div class="context-menu-item" onclick="openFile()">
|
|
<i class="fas fa-eye me-2"></i>Öffnen
|
|
</div>
|
|
<div class="context-menu-item" onclick="editFile()">
|
|
<i class="fas fa-edit me-2"></i>Bearbeiten
|
|
</div>
|
|
<div class="context-menu-item" onclick="downloadFile()">
|
|
<i class="fas fa-download me-2"></i>Download
|
|
</div>
|
|
<hr class="my-1">
|
|
<div class="context-menu-item" onclick="renameFile()">
|
|
<i class="fas fa-i-cursor me-2"></i>Umbenennen
|
|
</div>
|
|
<div class="context-menu-item" onclick="copyFile()">
|
|
<i class="fas fa-copy me-2"></i>Kopieren
|
|
</div>
|
|
<div class="context-menu-item" onclick="moveFile()">
|
|
<i class="fas fa-cut me-2"></i>Verschieben
|
|
</div>
|
|
<hr class="my-1">
|
|
<div class="context-menu-item text-danger" onclick="deleteFile()">
|
|
<i class="fas fa-trash me-2"></i>Löschen
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
let currentPath = '{{ current_path or "/" }}';
|
|
let selectedFiles = [];
|
|
let contextMenuTarget = null;
|
|
|
|
// Globale Variablen
|
|
const PROJECT_NAME = '{{ project_name }}';
|
|
|
|
// Page Load
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadFileList();
|
|
initializeEventListeners();
|
|
});
|
|
|
|
// Event Listeners initialisieren
|
|
function initializeEventListeners() {
|
|
// Upload Zone Events
|
|
const uploadZone = document.getElementById('uploadZone');
|
|
const fileInput = document.getElementById('fileInput');
|
|
|
|
uploadZone.addEventListener('click', () => fileInput.click());
|
|
uploadZone.addEventListener('dragover', handleDragOver);
|
|
uploadZone.addEventListener('drop', handleDrop);
|
|
uploadZone.addEventListener('dragleave', handleDragLeave);
|
|
|
|
fileInput.addEventListener('change', handleFileSelect);
|
|
|
|
// Context Menu Events
|
|
document.addEventListener('click', hideContextMenu);
|
|
document.addEventListener('contextmenu', function(e) {
|
|
if (!e.target.closest('.file-item')) {
|
|
hideContextMenu();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Dateiliste laden
|
|
function loadFileList() {
|
|
fetch(`/api/file_manager/${PROJECT_NAME}/list?path=${encodeURIComponent(currentPath)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
renderFileList(data.files);
|
|
updateBreadcrumb(data.current_path);
|
|
} else {
|
|
showError('Fehler beim Laden der Dateiliste: ' + data.error);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showError('Netzwerkfehler: ' + error.message);
|
|
});
|
|
}
|
|
|
|
// Dateiliste rendern
|
|
function renderFileList(files) {
|
|
const fileList = document.getElementById('fileList');
|
|
|
|
if (files.length === 0) {
|
|
fileList.innerHTML = `
|
|
<div class="text-center p-4 text-muted">
|
|
<i class="fas fa-folder-open fa-3x mb-3"></i>
|
|
<p>Dieser Ordner ist leer</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
|
|
// Zurück-Button wenn nicht im Root
|
|
if (currentPath !== '/') {
|
|
html += `
|
|
<div class="file-item" onclick="navigateUp()">
|
|
<div class="file-details">
|
|
<div class="d-flex align-items-center">
|
|
<div class="file-icon">
|
|
<i class="fas fa-arrow-left text-secondary"></i>
|
|
</div>
|
|
<div class="file-name text-secondary">.. (Zurück)</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Ordner zuerst, dann Dateien
|
|
const folders = files.filter(f => f.type === 'directory');
|
|
const regularFiles = files.filter(f => f.type === 'file');
|
|
|
|
[...folders, ...regularFiles].forEach(file => {
|
|
const icon = getFileIcon(file);
|
|
const size = file.type === 'directory' ? '-' : formatFileSize(file.size);
|
|
const permissions = getPermissionBadges(file.permissions);
|
|
|
|
html += `
|
|
<div class="file-item"
|
|
data-path="${file.path}"
|
|
data-type="${file.type}"
|
|
onclick="selectFile(this, '${file.path}', '${file.type}')"
|
|
ondblclick="handleDoubleClick('${file.path}', '${file.type}')"
|
|
oncontextmenu="showContextMenu(event, this, '${file.path}', '${file.type}')">
|
|
<div class="file-details">
|
|
<div class="d-flex align-items-center">
|
|
<div class="file-icon ${icon.class}">
|
|
<i class="${icon.icon}"></i>
|
|
</div>
|
|
<div class="file-name">${file.name}</div>
|
|
${permissions}
|
|
</div>
|
|
<div class="file-meta">
|
|
<div>${size}</div>
|
|
<div class="text-muted small">${formatDate(file.modified)}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
fileList.innerHTML = html;
|
|
}
|
|
|
|
// Datei-Icon bestimmen
|
|
function getFileIcon(file) {
|
|
if (file.type === 'directory') {
|
|
return { icon: 'fas fa-folder', class: 'folder' };
|
|
}
|
|
|
|
const ext = file.name.split('.').pop().toLowerCase();
|
|
|
|
// Code-Dateien
|
|
if (['js', 'ts', 'py', 'php', 'java', 'cpp', 'c', 'h', 'css', 'html', 'xml'].includes(ext)) {
|
|
return { icon: 'fas fa-file-code', class: 'code' };
|
|
}
|
|
|
|
// Config-Dateien
|
|
if (['json', 'yml', 'yaml', 'conf', 'cfg', 'ini', 'env', 'dockerfile'].includes(ext)) {
|
|
return { icon: 'fas fa-file-alt', class: 'config' };
|
|
}
|
|
|
|
// Bilder
|
|
if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'webp'].includes(ext)) {
|
|
return { icon: 'fas fa-file-image', class: 'image' };
|
|
}
|
|
|
|
// Archive
|
|
if (['zip', 'tar', 'gz', 'rar', '7z'].includes(ext)) {
|
|
return { icon: 'fas fa-file-archive', class: 'archive' };
|
|
}
|
|
|
|
// Text-Dateien
|
|
if (['txt', 'md', 'log'].includes(ext)) {
|
|
return { icon: 'fas fa-file-alt', class: 'text' };
|
|
}
|
|
|
|
return { icon: 'fas fa-file', class: 'text' };
|
|
}
|
|
|
|
// Dateigröße formatieren
|
|
function formatFileSize(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];
|
|
}
|
|
|
|
// Datum formatieren
|
|
function formatDate(dateString) {
|
|
return new Date(dateString).toLocaleString('de-DE');
|
|
}
|
|
|
|
// Berechtigungen als Badges
|
|
function getPermissionBadges(permissions) {
|
|
if (!permissions) return '';
|
|
|
|
let badges = '';
|
|
if (permissions.includes('r')) badges += '<span class="permission-badge permission-r">R</span>';
|
|
if (permissions.includes('w')) badges += '<span class="permission-badge permission-w">W</span>';
|
|
if (permissions.includes('x')) badges += '<span class="permission-badge permission-x">X</span>';
|
|
|
|
return badges;
|
|
}
|
|
|
|
// Navigation
|
|
function navigateToPath(path) {
|
|
currentPath = path;
|
|
selectedFiles = [];
|
|
updateActionButtons();
|
|
loadFileList();
|
|
}
|
|
|
|
function navigateUp() {
|
|
const pathParts = currentPath.split('/').filter(p => p);
|
|
pathParts.pop();
|
|
navigateToPath('/' + pathParts.join('/'));
|
|
}
|
|
|
|
// Datei/Ordner-Auswahl
|
|
function selectFile(element, path, type) {
|
|
// Clear andere Auswahl
|
|
document.querySelectorAll('.file-item.selected').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
|
|
element.classList.add('selected');
|
|
selectedFiles = [{ path, type }];
|
|
updateActionButtons();
|
|
}
|
|
|
|
function handleDoubleClick(path, type) {
|
|
if (type === 'directory') {
|
|
navigateToPath(path);
|
|
} else {
|
|
openFile(path);
|
|
}
|
|
}
|
|
|
|
// Action Buttons Update
|
|
function updateActionButtons() {
|
|
const hasSelection = selectedFiles.length > 0;
|
|
const isFile = hasSelection && selectedFiles[0].type === 'file';
|
|
|
|
document.getElementById('btnDownload').disabled = !hasSelection;
|
|
document.getElementById('btnEdit').disabled = !isFile;
|
|
document.getElementById('btnDelete').disabled = !hasSelection;
|
|
}
|
|
|
|
// File Operations
|
|
function openFile(path = null) {
|
|
const filePath = path || selectedFiles[0]?.path;
|
|
if (!filePath) return;
|
|
|
|
// Check if file is editable
|
|
const ext = filePath.split('.').pop().toLowerCase();
|
|
const editableExtensions = ['txt', 'md', 'json', 'yml', 'yaml', 'js', 'py', 'php', 'html', 'css', 'env', 'conf', 'cfg', 'ini'];
|
|
|
|
if (editableExtensions.includes(ext)) {
|
|
editFile(filePath);
|
|
} else {
|
|
downloadFile(filePath);
|
|
}
|
|
}
|
|
|
|
// Upload Functions
|
|
function showUploadModal() {
|
|
new bootstrap.Modal(document.getElementById('uploadModal')).show();
|
|
}
|
|
|
|
function handleDragOver(e) {
|
|
e.preventDefault();
|
|
e.currentTarget.classList.add('dragover');
|
|
}
|
|
|
|
function handleDrop(e) {
|
|
e.preventDefault();
|
|
e.currentTarget.classList.remove('dragover');
|
|
const files = e.dataTransfer.files;
|
|
uploadFiles(files);
|
|
}
|
|
|
|
function handleDragLeave(e) {
|
|
e.currentTarget.classList.remove('dragover');
|
|
}
|
|
|
|
function handleFileSelect(e) {
|
|
uploadFiles(e.target.files);
|
|
}
|
|
|
|
// Context Menu
|
|
function showContextMenu(e, element, path, type) {
|
|
e.preventDefault();
|
|
contextMenuTarget = { element, path, type };
|
|
|
|
const contextMenu = document.getElementById('contextMenu');
|
|
contextMenu.style.display = 'block';
|
|
contextMenu.style.left = e.pageX + 'px';
|
|
contextMenu.style.top = e.pageY + 'px';
|
|
|
|
// Select the item
|
|
selectFile(element, path, type);
|
|
}
|
|
|
|
function hideContextMenu() {
|
|
document.getElementById('contextMenu').style.display = 'none';
|
|
contextMenuTarget = null;
|
|
}
|
|
|
|
// Utility Functions
|
|
function refreshFileList() {
|
|
loadFileList();
|
|
}
|
|
|
|
function updateBreadcrumb(path) {
|
|
currentPath = path;
|
|
// Breadcrumb wird vom Server-Side-Template gehandhabt
|
|
}
|
|
|
|
function showError(message) {
|
|
alert('Fehler: ' + message);
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
// Implementierung für Success-Nachrichten
|
|
console.log('Success: ' + message);
|
|
}
|
|
|
|
// Placeholder Functions (werden in der nächsten Phase implementiert)
|
|
function editFile(path = null) {
|
|
alert('Editor wird in der nächsten Phase implementiert');
|
|
}
|
|
|
|
function downloadFile(path = null) {
|
|
alert('Download wird in der nächsten Phase implementiert');
|
|
}
|
|
|
|
function uploadFiles(files) {
|
|
alert('Upload wird in der nächsten Phase implementiert');
|
|
}
|
|
|
|
function createNewFile() {
|
|
alert('Neue Datei wird in der nächsten Phase implementiert');
|
|
}
|
|
|
|
function createNewFolder() {
|
|
alert('Neuer Ordner wird in der nächsten Phase implementiert');
|
|
}
|
|
|
|
function deleteSelected() {
|
|
alert('Löschen wird in der nächsten Phase implementiert');
|
|
}
|
|
|
|
function editSelected() {
|
|
editFile();
|
|
}
|
|
|
|
function downloadSelected() {
|
|
downloadFile();
|
|
}
|
|
|
|
function saveFile() {
|
|
alert('Speichern wird in der nächsten Phase implementiert');
|
|
}
|
|
|
|
function closeEditor() {
|
|
document.getElementById('editorContainer').style.display = 'none';
|
|
}
|
|
|
|
function renameFile() {
|
|
alert('Umbenennen wird in der nächsten Phase implementiert');
|
|
}
|
|
|
|
function copyFile() {
|
|
alert('Kopieren wird in der nächsten Phase implementiert');
|
|
}
|
|
|
|
function moveFile() {
|
|
alert('Verschieben wird in der nächsten Phase implementiert');
|
|
}
|
|
</script>
|
|
{% endblock %}
|