modified: projects_list.json

modified:   templates/available_projects.html
	deleted:    templates/available_projects_new.html
	modified:   templates/base.html
This commit is contained in:
SimolZimol
2025-07-06 13:37:42 +02:00
parent f6bcfb298a
commit f40e118fc2
4 changed files with 168 additions and 796 deletions

View File

@@ -11,8 +11,8 @@
"NODE_ENV": "production",
"PORT": "3000",
"DATABASE_URL": "sqlite:///data/quiz.db",
"JWT_SECRET": "",
"ADMIN_PASSWORD": ""
"JWT_SECRET": "your-secret-key-here",
"ADMIN_PASSWORD": "admin123"
},
"requirements": {
"docker": true,
@@ -101,20 +101,24 @@
}
},
{
"url": "https://github.com/example/simple-app",
"name": "simple-app",
"description": "Eine einfache Test-App ohne alle Features",
"url": "https://github.com/example/simple-notes",
"name": "simple-notes",
"description": "Einfache Notizen-App ohne komplexe Features",
"language": "Python",
"tags": ["test", "simple", "python"],
"category": "Utility",
"tags": ["notes", "simple", "python", "flask"],
"category": "Productivity",
"docker_port": 8080,
"environment_vars": {
"FLASK_ENV": "production",
"SECRET_KEY": "change-me"
},
"installation": {
"methods": {
"clone": {
"available": true,
"url": "https://github.com/example/simple-app",
"url": "https://github.com/example/simple-notes",
"type": "git",
"description": "Nur Git Clone verfügbar"
"description": "Quellcode klonen und bauen"
},
"image": {
"available": false,
@@ -128,24 +132,25 @@
"metadata": {
"created": "2024-06-01",
"last_updated": "2025-07-01",
"version": "1.0.0",
"author": "Test Author",
"version": "1.0.5",
"author": "Example Dev",
"license": "MIT",
"homepage": "https://github.com/example/simple-app",
"homepage": "https://github.com/example/simple-notes",
"documentation": {
"available": false,
"url": ""
},
"issues": {
"available": false,
"url": ""
"available": true,
"url": "https://github.com/example/simple-notes/issues"
},
"downloads": 10,
"stars": 2
"downloads": 58,
"stars": 12
},
"features": [
"Einfache Funktionalität",
"Leichtgewichtig"
"Einfache Notizen erstellen und bearbeiten",
"Markdown-Unterstützung",
"Leichtgewichtige Flask-App"
],
"screenshots": {
"available": false,
@@ -156,18 +161,109 @@
"url": "",
"description": ""
},
"installation_notes": "Einfache Python-App mit minimalen Anforderungen.",
"rating": {
"score": 3.0,
"reviews": 2,
"downloads": 10
"installation_notes": "Einfache Python-Flask-App mit minimalen Anforderungen.",
"config_hints": {
"env_example": ".env.example mit Secret Key",
"dockerfile": "Einfaches Dockerfile vorhanden"
},
"size_mb": 5,
"install_time": "1 Minute",
"rating": {
"score": 3.8,
"reviews": 5,
"downloads": 58
},
"size_mb": 8,
"install_time": "1-2 Minuten",
"min_resources": {
"ram_mb": 128,
"cpu_cores": 1,
"disk_mb": 100
"disk_mb": 150
}
},
{
"url": "https://github.com/example/media-server",
"name": "media-server",
"description": "Lokaler Medien-Server zum Streaming von Videos und Musik",
"language": "Node.js",
"tags": ["media", "streaming", "server", "video", "music"],
"category": "Media & Entertainment",
"docker_port": 9000,
"environment_vars": {
"NODE_ENV": "production",
"MEDIA_PATH": "/data/media",
"STREAM_QUALITY": "high"
},
"installation": {
"methods": {
"clone": {
"available": true,
"url": "https://github.com/example/media-server",
"type": "git",
"description": "Quellcode klonen für Anpassungen"
},
"image": {
"available": true,
"url": "docker.io/example/media-server:2.1.0",
"type": "docker",
"description": "Fertiges Docker-Image"
}
},
"preferred": "image"
},
"metadata": {
"created": "2023-08-20",
"last_updated": "2025-06-15",
"version": "2.1.0",
"author": "MediaTeam",
"license": "GPL-3.0",
"homepage": "https://github.com/example/media-server",
"documentation": {
"available": true,
"url": "https://github.com/example/media-server/wiki"
},
"issues": {
"available": true,
"url": "https://github.com/example/media-server/issues"
},
"downloads": 1250,
"stars": 89
},
"features": [
"Video und Audio Streaming",
"Playlists und Favoriten",
"Multi-User Unterstützung",
"Mobile App Unterstützung",
"Subtitle-Unterstützung",
"Transcoding on-the-fly"
],
"screenshots": {
"available": true,
"urls": [
"https://github.com/example/media-server/raw/main/docs/screenshot1.png",
"https://github.com/example/media-server/raw/main/docs/screenshot2.png"
]
},
"demo": {
"available": true,
"url": "https://demo.media-server.example.com",
"description": "Demo mit Beispielmedien"
},
"installation_notes": "Benötigt Docker und mindestens 2GB RAM für optimale Performance. Medienordner muss gemountet werden.",
"config_hints": {
"env_example": ".env.example mit allen Streaming-Optionen",
"dockerfile": "Multi-stage Build mit FFmpeg",
"docker_compose": "Vollständige Compose-Konfiguration mit Volumes"
},
"rating": {
"score": 4.2,
"reviews": 34,
"downloads": 1250
},
"size_mb": 180,
"install_time": "3-5 Minuten",
"min_resources": {
"ram_mb": 2048,
"cpu_cores": 2,
"disk_mb": 1000
}
}
]

View File

@@ -920,10 +920,10 @@ function showProjectDetails(projectName) {
</ul>
` : ''}
${project.screenshots ? `
${project.screenshots && project.screenshots.available ? `
<h6>Screenshots</h6>
<div class="row">
${project.screenshots.slice(0, 3).map(img => `
${project.screenshots.urls.slice(0, 3).map(img => `
<div class="col-4">
<img src="${img}" class="img-fluid rounded" alt="Screenshot"
style="max-height: 150px; object-fit: cover; cursor: pointer;"
@@ -962,12 +962,13 @@ function showProjectDetails(projectName) {
</table>
` : ''}
${project.metadata?.homepage || project.metadata?.documentation ? `
${project.metadata?.homepage || (project.metadata?.documentation && project.metadata.documentation.available) || (project.demo && project.demo.available) ? `
<h6>Links</h6>
<div class="d-grid gap-2">
${project.metadata.homepage ? `<a href="${project.metadata.homepage}" target="_blank" class="btn btn-outline-primary btn-sm"><i class="fas fa-home"></i> Homepage</a>` : ''}
${project.metadata.documentation ? `<a href="${project.metadata.documentation}" target="_blank" class="btn btn-outline-info btn-sm"><i class="fas fa-book"></i> Dokumentation</a>` : ''}
${project.demo_url ? `<a href="${project.demo_url}" target="_blank" class="btn btn-outline-success btn-sm"><i class="fas fa-play"></i> Demo</a>` : ''}
${project.metadata.documentation && project.metadata.documentation.available ? `<a href="${project.metadata.documentation.url}" target="_blank" class="btn btn-outline-info btn-sm"><i class="fas fa-book"></i> Dokumentation</a>` : ''}
${project.demo && project.demo.available ? `<a href="${project.demo.url}" target="_blank" class="btn btn-outline-success btn-sm"><i class="fas fa-play"></i> Demo</a>` : ''}
${project.metadata.issues && project.metadata.issues.available ? `<a href="${project.metadata.issues.url}" target="_blank" class="btn btn-outline-danger btn-sm"><i class="fas fa-bug"></i> Issues</a>` : ''}
</div>
` : ''}
</div>

View File

@@ -1,765 +0,0 @@
{% extends "base.html" %}
{% block title %}Verfügbare Apps - {{ super() }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2><i class="fas fa-cloud-download-alt me-2"></i>Verfügbare Apps</h2>
<div>
<button class="btn btn-outline-info me-2" onclick="showBulkInstall()">
<i class="fas fa-layer-group"></i> Masseninstallation
</button>
<a href="{{ url_for('refresh_projects') }}" class="btn btn-primary">
<i class="fas fa-sync-alt"></i> Liste aktualisieren
</a>
</div>
</div>
<!-- Filter und Suche -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<label for="searchFilter" class="form-label">Suche</label>
<input type="text" class="form-control" id="searchFilter" placeholder="Name, Beschreibung, Tags...">
</div>
<div class="col-md-2">
<label for="languageFilter" class="form-label">Sprache</label>
<select class="form-control" id="languageFilter">
<option value="">Alle</option>
{% set languages = [] %}
{% for project in projects %}
{% if project.language and project.language not in languages %}
{% set _ = languages.append(project.language) %}
{% endif %}
{% endfor %}
{% for language in languages|sort %}
<option value="{{ language }}">{{ language }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label for="categoryFilter" class="form-label">Kategorie</label>
<select class="form-control" id="categoryFilter">
<option value="">Alle</option>
{% set categories = [] %}
{% for project in projects %}
{% if project.category and project.category not in categories %}
{% set _ = categories.append(project.category) %}
{% endif %}
{% endfor %}
{% for category in categories|sort %}
<option value="{{ category }}">{{ category }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<label for="sortBy" class="form-label">Sortierung</label>
<select class="form-control" id="sortBy">
<option value="name">Name</option>
<option value="updated">Letzte Aktualisierung</option>
<option value="rating">Bewertung</option>
<option value="downloads">Downloads</option>
<option value="size">Größe</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Optionen</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="showInstalledFilter">
<label class="form-check-label" for="showInstalledFilter">
Installierte anzeigen
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% if not projects %}
<div class="text-center py-5">
<i class="fas fa-exclamation-triangle fa-3x text-warning mb-3"></i>
<h3 class="text-muted">Keine Projekte verfügbar</h3>
<p class="text-muted mb-4">
Stellen Sie sicher, dass eine gültige Projekt-URL in den
<a href="{{ url_for('config') }}">Einstellungen</a> konfiguriert ist.
</p>
<a href="{{ url_for('config') }}" class="btn btn-primary">
<i class="fas fa-cog"></i> Einstellungen öffnen
</a>
</div>
{% else %}
<div class="row" id="projectContainer">
{% for project in projects %}
<div class="col-lg-6 col-xl-4 mb-4 project-card-wrapper"
data-name="{{ project.name|lower }}"
data-language="{{ project.language|lower if project.language else '' }}"
data-category="{{ project.category|lower if project.category else '' }}"
data-tags="{{ project.tags|join(',')|lower if project.tags else '' }}"
data-rating="{{ project.rating.score if project.rating and project.rating.score else 0 }}"
data-downloads="{{ project.rating.downloads if project.rating and project.rating.downloads else 0 }}"
data-size="{{ project.size_mb if project.size_mb else 0 }}"
data-updated="{{ project.metadata.last_updated if project.metadata and project.metadata.last_updated else '' }}">
<div class="card h-100 project-card">
<div class="card-header d-flex justify-content-between align-items-start">
<div>
<h5 class="card-title mb-1">
<i class="fas fa-cube me-2"></i>{{ project.name or 'Unbekannt' }}
{% if project.metadata and project.metadata.version %}
<small class="text-muted">v{{ project.metadata.version }}</small>
{% endif %}
</h5>
{% if project.category %}
<span class="badge bg-primary">{{ project.category }}</span>
{% endif %}
</div>
<!-- Status Badge -->
<div class="project-status" data-project-name="{{ project.name }}">
<span class="badge bg-secondary">Prüfung...</span>
</div>
</div>
<div class="card-body d-flex flex-column">
<!-- Bewertung und Statistiken -->
{% if project.rating %}
<div class="mb-2 d-flex justify-content-between">
<div class="rating">
{% for i in range(5) %}
{% if i < (project.rating.score|round|int) %}
<i class="fas fa-star text-warning"></i>
{% else %}
<i class="far fa-star text-muted"></i>
{% endif %}
{% endfor %}
<small class="text-muted ms-1">({{ project.rating.reviews or 0 }})</small>
</div>
<small class="text-muted">{{ project.rating.downloads or 0 }} Downloads</small>
</div>
{% endif %}
<!-- Beschreibung -->
<div class="mb-3">
<small class="text-muted">Repository URL:</small>
<p class="card-text small font-monospace text-truncate">{{ project.url }}</p>
</div>
{% if project.description %}
<div class="mb-3">
<p class="card-text">{{ project.description }}</p>
</div>
{% endif %}
<!-- Tags -->
{% if project.tags %}
<div class="mb-3">
{% for tag in project.tags[:6] %}
<span class="badge bg-secondary me-1 mb-1">{{ tag }}</span>
{% endfor %}
{% if project.tags|length > 6 %}
<span class="badge bg-light text-dark">+{{ project.tags|length - 6 }}</span>
{% endif %}
</div>
{% endif %}
<!-- Technische Details -->
<div class="row mb-3 small">
<div class="col-6">
<strong>Sprache:</strong><br>
<span class="badge bg-info">{{ project.language or 'Unbekannt' }}</span>
</div>
<div class="col-6">
<strong>Größe:</strong><br>
<span class="text-muted">
{% if project.size_mb %}
{{ project.size_mb }} MB
{% else %}
Unbekannt
{% endif %}
</span>
</div>
</div>
{% if project.min_resources %}
<div class="row mb-3 small">
<div class="col-4">
<strong>RAM:</strong><br>
<span class="text-muted">{{ project.min_resources.ram_mb or 512 }} MB</span>
</div>
<div class="col-4">
<strong>CPU:</strong><br>
<span class="text-muted">{{ project.min_resources.cpu_cores or 1 }} Core(s)</span>
</div>
<div class="col-4">
<strong>Port:</strong><br>
<span class="text-muted">{{ project.docker_port or 8080 }}</span>
</div>
</div>
{% endif %}
<!-- Features -->
{% if project.features %}
<div class="mb-3">
<strong class="small">Features:</strong>
<ul class="list-unstyled small mt-1">
{% for feature in project.features[:3] %}
<li><i class="fas fa-check text-success me-1"></i>{{ feature }}</li>
{% endfor %}
{% if project.features|length > 3 %}
<li class="text-muted">... und {{ project.features|length - 3 }} weitere</li>
{% endif %}
</ul>
</div>
{% endif %}
<!-- Metadaten -->
{% if project.metadata %}
<div class="mb-3 small text-muted">
{% if project.metadata.last_updated %}
<div>Letzte Aktualisierung: {{ project.metadata.last_updated }}</div>
{% endif %}
{% if project.metadata.author %}
<div>Autor: {{ project.metadata.author }}</div>
{% endif %}
{% if project.install_time %}
<div>Installationszeit: {{ project.install_time }}</div>
{% endif %}
</div>
{% endif %}
<!-- Action Buttons -->
<div class="mt-auto">
<div class="d-grid gap-2">
<div class="btn-group" role="group">
<button class="btn btn-success install-btn"
data-url="{{ project.url }}"
data-name="{{ project.name }}"
onclick="installProject('{{ project.url }}', '{{ project.name }}')">
<i class="fas fa-download"></i> Installieren
</button>
{% if project.demo_url %}
<a href="{{ project.demo_url }}" target="_blank" class="btn btn-outline-info">
<i class="fas fa-external-link-alt"></i> Demo
</a>
{% endif %}
</div>
<!-- Zusätzliche Buttons -->
<div class="btn-group btn-group-sm" role="group">
{% if project.metadata and project.metadata.documentation %}
<a href="{{ project.metadata.documentation }}" target="_blank" class="btn btn-outline-secondary">
<i class="fas fa-book"></i> Docs
</a>
{% endif %}
{% if project.metadata and project.metadata.homepage %}
<a href="{{ project.metadata.homepage }}" target="_blank" class="btn btn-outline-secondary">
<i class="fas fa-home"></i> Home
</a>
{% endif %}
<button class="btn btn-outline-secondary" onclick="showProjectDetails('{{ project.name }}')">
<i class="fas fa-info-circle"></i> Details
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Installationsstatistiken -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-chart-bar me-2"></i>Installationsstatistiken
</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-md-3">
<div class="stat-item">
<span class="stat-number">{{ projects|length }}</span>
<span>Verfügbare Apps</span>
</div>
</div>
<div class="col-md-3">
<div class="stat-item">
<span class="stat-number" id="installedCount">0</span>
<span>Installierte Apps</span>
</div>
</div>
<div class="col-md-3">
<div class="stat-item">
<span class="stat-number">{{ projects|selectattr('language', 'equalto', 'JavaScript')|list|length }}</span>
<span>JavaScript Apps</span>
</div>
</div>
<div class="col-md-3">
<div class="stat-item">
<span class="stat-number">{{ projects|selectattr('language', 'equalto', 'Python')|list|length }}</span>
<span>Python Apps</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Bulk Installation Modal -->
<div class="modal fade" id="bulkInstallModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-download me-2"></i>Masseninstallation
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Wählen Sie die Apps aus, die Sie installieren möchten:</p>
<div class="row" id="bulkInstallList">
{% for project in projects %}
<div class="col-md-6 mb-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" value="{{ project.url }}"
id="bulk_{{ loop.index }}" data-name="{{ project.name }}">
<label class="form-check-label" for="bulk_{{ loop.index }}">
{{ project.name }}
</label>
</div>
</div>
{% endfor %}
</div>
</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="startBulkInstall()">
<i class="fas fa-download"></i> Ausgewählte installieren
</button>
</div>
</div>
</div>
</div>
<!-- Project Details Modal -->
<div class="modal fade" id="projectDetailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="projectDetailsTitle">Projektdetails</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body" id="projectDetailsContent">
<!-- Content wird per JavaScript gefüllt -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
<button type="button" class="btn btn-success" id="installFromDetails">
<i class="fas fa-download"></i> Installieren
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let installedProjects = [];
// Lade installierte Projekte beim Seitenaufruf
document.addEventListener('DOMContentLoaded', function() {
loadInstalledProjects();
setupFilters();
});
// Lade installierte Projekte
function loadInstalledProjects() {
fetch('/')
.then(response => response.text())
.then(html => {
// Parse HTML um installierte Projekte zu finden
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const projectCards = doc.querySelectorAll('.project-card .card-title');
installedProjects = Array.from(projectCards).map(title => {
const projectName = title.textContent.trim().replace('🚀', '').trim();
return projectName;
});
updateProjectStatus();
updateInstalledCount();
})
.catch(error => {
console.error('Fehler beim Laden der installierten Projekte:', error);
});
}
// Update Projekt-Status Badges
function updateProjectStatus() {
document.querySelectorAll('.project-status').forEach(statusEl => {
const projectName = statusEl.dataset.projectName;
const isInstalled = installedProjects.includes(projectName);
if (isInstalled) {
statusEl.innerHTML = '<span class="badge bg-success">Installiert</span>';
// Update Button
const installBtn = statusEl.closest('.card').querySelector('.install-btn');
if (installBtn) {
installBtn.innerHTML = '<i class="fas fa-sync-alt"></i> Update';
installBtn.className = 'btn btn-warning';
installBtn.onclick = () => updateProject(installBtn.dataset.url, installBtn.dataset.name);
}
} else {
statusEl.innerHTML = '<span class="badge bg-secondary">Verfügbar</span>';
}
});
}
// Update Anzahl installierter Apps
function updateInstalledCount() {
const installedCount = document.getElementById('installedCount');
if (installedCount) {
installedCount.textContent = installedProjects.length;
}
}
// Projekt installieren
function installProject(url, name) {
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Installiere...';
button.disabled = true;
fetch('/install_project', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `project_url=${encodeURIComponent(url)}&project_name=${encodeURIComponent(name)}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
button.innerHTML = '<i class="fas fa-check"></i> Installiert';
button.className = 'btn btn-success';
// Update installierte Projekte Liste
if (!installedProjects.includes(name)) {
installedProjects.push(name);
updateProjectStatus();
updateInstalledCount();
}
setTimeout(() => {
button.innerHTML = '<i class="fas fa-sync-alt"></i> Update';
button.className = 'btn btn-warning';
button.onclick = () => updateProject(url, name);
button.disabled = false;
}, 2000);
} else {
button.innerHTML = originalText;
button.disabled = false;
alert('Fehler: ' + data.message);
}
})
.catch(error => {
button.innerHTML = originalText;
button.disabled = false;
alert('Netzwerkfehler: ' + error);
});
}
// Projekt updaten
function updateProject(url, name) {
if (!confirm(`Möchten Sie das Projekt "${name}" wirklich aktualisieren? Dies überschreibt lokale Änderungen.`)) {
return;
}
const button = event.target;
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Aktualisiere...';
button.disabled = true;
// Verwende die neue Update-API
fetch('/update_project', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `project_name=${encodeURIComponent(name)}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
button.innerHTML = '<i class="fas fa-check"></i> Aktualisiert';
button.className = 'btn btn-success';
// Aktualisiere installierte Projekte Liste
setTimeout(() => {
loadInstalledProjects();
updateProjectStatus();
}, 1000);
setTimeout(() => {
button.innerHTML = originalText;
button.className = 'btn btn-warning';
button.disabled = false;
}, 3000);
} else {
button.innerHTML = originalText;
button.disabled = false;
alert('Update-Fehler: ' + data.message);
}
})
.catch(error => {
button.innerHTML = originalText;
button.disabled = false;
alert('Netzwerkfehler beim Update: ' + error);
});
}
}
});
}, 1000);
})
.catch(error => {
button.innerHTML = originalText;
button.disabled = false;
alert('Fehler beim Update: ' + error);
});
}
// Filter Setup
function setupFilters() {
document.getElementById('searchFilter').addEventListener('input', filterProjects);
document.getElementById('languageFilter').addEventListener('change', filterProjects);
document.getElementById('categoryFilter').addEventListener('change', filterProjects);
document.getElementById('sortBy').addEventListener('change', sortProjects);
document.getElementById('showInstalledFilter').addEventListener('change', filterProjects);
}
// Projekte filtern
function filterProjects() {
const searchTerm = document.getElementById('searchFilter').value.toLowerCase();
const languageFilter = document.getElementById('languageFilter').value.toLowerCase();
const categoryFilter = document.getElementById('categoryFilter').value.toLowerCase();
const showInstalled = document.getElementById('showInstalledFilter').checked;
const projectCards = document.querySelectorAll('.project-card-wrapper');
projectCards.forEach(card => {
const name = card.dataset.name;
const language = card.dataset.language;
const category = card.dataset.category;
const tags = card.dataset.tags;
const projectName = card.querySelector('.card-title').textContent.toLowerCase();
const isInstalled = installedProjects.includes(card.querySelector('[data-name]').dataset.name);
let show = true;
// Textsuche
if (searchTerm && !name.includes(searchTerm) && !tags.includes(searchTerm) && !projectName.includes(searchTerm)) {
show = false;
}
// Sprachfilter
if (languageFilter && language !== languageFilter) {
show = false;
}
// Kategoriefilter
if (categoryFilter && category !== categoryFilter) {
show = false;
}
// Installierte anzeigen Filter
if (showInstalled && !isInstalled) {
show = false;
}
card.style.display = show ? 'block' : 'none';
});
}
// Projekte sortieren
function sortProjects() {
const sortBy = document.getElementById('sortBy').value;
const container = document.getElementById('projectContainer');
const cards = Array.from(container.querySelectorAll('.project-card-wrapper'));
cards.sort((a, b) => {
let aVal, bVal;
switch(sortBy) {
case 'name':
aVal = a.dataset.name;
bVal = b.dataset.name;
break;
case 'updated':
aVal = a.dataset.updated;
bVal = b.dataset.updated;
break;
case 'rating':
aVal = parseFloat(a.dataset.rating);
bVal = parseFloat(b.dataset.rating);
return bVal - aVal; // Absteigend
case 'downloads':
aVal = parseInt(a.dataset.downloads);
bVal = parseInt(b.dataset.downloads);
return bVal - aVal; // Absteigend
case 'size':
aVal = parseInt(a.dataset.size);
bVal = parseInt(b.dataset.size);
return aVal - bVal; // Aufsteigend
default:
return 0;
}
return aVal.localeCompare(bVal);
});
// Sortierte Cards wieder einfügen
cards.forEach(card => container.appendChild(card));
}
// Masseninstallation
function showBulkInstall() {
const modal = new bootstrap.Modal(document.getElementById('bulkInstallModal'));
modal.show();
}
function startBulkInstall() {
const checkboxes = document.querySelectorAll('#bulkInstallList input[type="checkbox"]:checked');
const selectedApps = Array.from(checkboxes).map(cb => ({
url: cb.value,
name: cb.dataset.name
}));
if (selectedApps.length === 0) {
alert('Bitte wählen Sie mindestens eine App aus.');
return;
}
if (confirm(`${selectedApps.length} Apps werden installiert. Fortfahren?`)) {
bootstrap.Modal.getInstance(document.getElementById('bulkInstallModal')).hide();
installAppsSequentially(selectedApps, 0);
}
}
function installAppsSequentially(apps, index) {
if (index >= apps.length) {
alert('Alle Apps wurden installiert!');
loadInstalledProjects(); // Refresh
return;
}
const app = apps[index];
console.log(`Installiere ${index + 1}/${apps.length}: ${app.name}`);
fetch('/install_project', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `project_url=${encodeURIComponent(app.url)}&project_name=${encodeURIComponent(app.name)}`
})
.then(response => response.json())
.then(data => {
console.log(`${app.name}: ${data.success ? 'Erfolgreich' : 'Fehler'} - ${data.message}`);
setTimeout(() => installAppsSequentially(apps, index + 1), 1000);
})
.catch(error => {
console.error(`Fehler bei ${app.name}:`, error);
setTimeout(() => installAppsSequentially(apps, index + 1), 1000);
});
}
// Projektdetails anzeigen
function showProjectDetails(projectName) {
// Finde Projekt in der Liste
const projectData = {{ projects|tojson }};
const project = projectData.find(p => p.name === projectName);
if (!project) {
alert('Projektdaten nicht gefunden');
return;
}
// Fülle Modal mit Daten
document.getElementById('projectDetailsTitle').textContent = project.name;
let content = `
<div class="row">
<div class="col-md-8">
<h6>Beschreibung</h6>
<p>${project.description || 'Keine Beschreibung verfügbar'}</p>
${project.features ? `
<h6>Features</h6>
<ul>
${project.features.map(f => `<li>${f}</li>`).join('')}
</ul>
` : ''}
${project.screenshots ? `
<h6>Screenshots</h6>
<div class="row">
${project.screenshots.slice(0, 3).map(img => `
<div class="col-4">
<img src="${img}" class="img-fluid rounded" alt="Screenshot"
style="max-height: 150px; object-fit: cover;">
</div>
`).join('')}
</div>
` : ''}
</div>
<div class="col-md-4">
<h6>Technische Details</h6>
<table class="table table-sm">
<tr><td>Sprache:</td><td>${project.language || 'Unbekannt'}</td></tr>
<tr><td>Kategorie:</td><td>${project.category || 'Unbekannt'}</td></tr>
<tr><td>Version:</td><td>${project.metadata?.version || 'Unbekannt'}</td></tr>
<tr><td>Autor:</td><td>${project.metadata?.author || 'Unbekannt'}</td></tr>
<tr><td>Lizenz:</td><td>${project.metadata?.license || 'Unbekannt'}</td></tr>
<tr><td>Größe:</td><td>${project.size_mb ? project.size_mb + ' MB' : 'Unbekannt'}</td></tr>
<tr><td>Port:</td><td>${project.docker_port || 8080}</td></tr>
</table>
${project.min_resources ? `
<h6>Systemanforderungen</h6>
<table class="table table-sm">
<tr><td>RAM:</td><td>${project.min_resources.ram_mb} MB</td></tr>
<tr><td>CPU:</td><td>${project.min_resources.cpu_cores} Core(s)</td></tr>
<tr><td>Disk:</td><td>${project.min_resources.disk_mb} MB</td></tr>
</table>
` : ''}
</div>
</div>
`;
document.getElementById('projectDetailsContent').innerHTML = content;
// Install Button Setup
const installBtn = document.getElementById('installFromDetails');
installBtn.onclick = () => {
bootstrap.Modal.getInstance(document.getElementById('projectDetailsModal')).hide();
installProject(project.url, project.name);
};
// Modal anzeigen
const modal = new bootstrap.Modal(document.getElementById('projectDetailsModal'));
modal.show();
}
</script>
{% endblock %}

View File

@@ -116,6 +116,46 @@
flex-direction: column;
}
}
/* Dropdown-Menü Styles */
.dropdown-menu {
border-radius: 10px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
border: none;
}
.dropdown-item {
padding: 10px 15px;
border-radius: 6px;
margin: 2px 5px;
transition: background-color 0.2s ease;
}
.dropdown-item:hover {
background-color: #f8f9fa;
}
.dropdown-item i {
width: 20px;
text-align: center;
}
.dropdown-toggle-split {
border-left: 1px solid rgba(255, 255, 255, 0.2);
}
/* Installation Button Group */
.btn-group .btn-success {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.btn-group .dropdown-toggle-split {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding-left: 8px;
padding-right: 8px;
}
</style>
</head>
<body>