modified: config.json modified: templates/available_projects.html modified: templates/base.html new file: templates/custom_install.html
1398 lines
60 KiB
HTML
1398 lines
60 KiB
HTML
{% 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>
|
|
<button id="refreshProjectsBtn" class="btn btn-primary" onclick="refreshProjectsList()">
|
|
<i class="fas fa-sync-alt"></i> Liste aktualisieren
|
|
</button>
|
|
</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">
|
|
<!-- Installation Buttons mit Dropdown -->
|
|
{% if project.installation and project.installation.methods %}
|
|
{% set preferred_method = project.installation.preferred or 'clone' %}
|
|
{% set methods = project.installation.methods %}
|
|
{% set preferred = methods[preferred_method] if preferred_method in methods else methods.values()|list|first %}
|
|
{% set available_methods = [] %}
|
|
{% for method_type, method_info in methods.items() %}
|
|
{% if method_info.get('available', True) %}
|
|
{% set _ = available_methods.append((method_type, method_info)) %}
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if available_methods|length > 1 %}
|
|
<!-- Split-Button: Links = Preferred Install, Rechts = Custom Install -->
|
|
<div class="btn-group w-100" role="group">
|
|
<!-- Linke Hälfte: Preferred Installation -->
|
|
<button class="btn btn-success install-btn"
|
|
style="flex: 1 1 70%"
|
|
data-url="{{ preferred.url }}"
|
|
data-name="{{ project.name }}"
|
|
data-method="{{ preferred_method }}"
|
|
onclick="installProject('{{ preferred.url }}', '{{ project.name }}', '{{ preferred_method }}')">
|
|
<i class="fas fa-download me-1"></i>
|
|
{% if preferred_method in ['image', 'docker_registry', 'docker_url', 'docker_file'] %}
|
|
<i class="fab fa-docker me-1"></i>Docker
|
|
{% elif preferred_method in ['docker_build', 'dockerfile'] %}
|
|
<i class="fas fa-cog me-1"></i>Build
|
|
{% else %}
|
|
<i class="fab fa-git-alt me-1"></i>Code
|
|
{% endif %}
|
|
</button>
|
|
<!-- Rechte Hälfte: Custom Install -->
|
|
<button type="button" class="btn btn-outline-success"
|
|
style="flex: 0 0 30%"
|
|
onclick="openCustomInstall('{{ project.name }}')">
|
|
<i class="fas fa-cog"></i> Custom
|
|
</button>
|
|
</div>
|
|
{% else %}
|
|
<!-- Split-Button auch bei einer Methode für Konsistenz -->
|
|
<div class="btn-group w-100" role="group">
|
|
<!-- Linke Hälfte: Einzige verfügbare Installation -->
|
|
<button class="btn btn-success install-btn"
|
|
style="flex: 1 1 70%"
|
|
data-url="{{ preferred.url }}"
|
|
data-name="{{ project.name }}"
|
|
data-method="{{ preferred_method }}"
|
|
onclick="installProject('{{ preferred.url }}', '{{ project.name }}', '{{ preferred_method }}')">
|
|
<i class="fas fa-download me-1"></i>
|
|
{% if preferred_method == 'image' %}
|
|
<i class="fab fa-docker me-1"></i>Docker installieren
|
|
{% else %}
|
|
<i class="fab fa-git-alt me-1"></i>Code installieren
|
|
{% endif %}
|
|
</button>
|
|
<!-- Rechte Hälfte: Details anzeigen -->
|
|
<button type="button" class="btn btn-outline-success"
|
|
style="flex: 0 0 30%"
|
|
onclick="openCustomInstall('{{ project.name }}')">
|
|
<i class="fas fa-info"></i> Info
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% else %}
|
|
<!-- Fallback für alte Projekte ohne neue Struktur -->
|
|
<button class="btn btn-success install-btn w-100"
|
|
data-url="{{ project.url }}"
|
|
data-name="{{ project.name }}"
|
|
data-method="clone"
|
|
onclick="installProject('{{ project.url }}', '{{ project.name }}', 'clone')">
|
|
<i class="fas fa-download me-1"></i><i class="fab fa-git-alt me-1"></i>Installieren
|
|
</button>
|
|
{% endif %}
|
|
|
|
<!-- Zusätzliche Action-Buttons -->
|
|
<div class="btn-group btn-group-sm w-100" role="group">
|
|
{% if project.demo and project.demo.available %}
|
|
<a href="{{ project.demo.url }}" target="_blank" class="btn btn-outline-primary">
|
|
<i class="fas fa-play me-1"></i>Demo
|
|
</a>
|
|
{% endif %}
|
|
{% if project.metadata and project.metadata.documentation and project.metadata.documentation.available %}
|
|
<a href="{{ project.metadata.documentation.url }}" target="_blank" class="btn btn-outline-secondary">
|
|
<i class="fas fa-book me-1"></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 me-1"></i>Home
|
|
</a>
|
|
{% endif %}
|
|
<button class="btn btn-outline-info" onclick="showProjectDetails('{{ project.name }}')">
|
|
<i class="fas fa-info-circle me-1"></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-lg-2 col-md-3 col-6 mb-3">
|
|
<div class="stat-item">
|
|
<span class="stat-number text-primary">{{ projects|length }}</span>
|
|
<span class="text-muted small">Verfügbare Apps</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-2 col-md-3 col-6 mb-3">
|
|
<div class="stat-item">
|
|
<span class="stat-number text-success" id="installedCount">0</span>
|
|
<span class="text-muted small">Installierte Apps</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-2 col-md-3 col-6 mb-3">
|
|
<div class="stat-item">
|
|
<span class="stat-number text-info">{{ projects|selectattr('category', 'equalto', 'Web Application')|list|length }}</span>
|
|
<span class="text-muted small">Web Applications</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-2 col-md-3 col-6 mb-3">
|
|
<div class="stat-item">
|
|
<span class="stat-number text-warning">{{ projects|selectattr('category', 'equalto', 'Web Server')|list|length }}</span>
|
|
<span class="text-muted small">Web Server</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-2 col-md-3 col-6 mb-3">
|
|
<div class="stat-item">
|
|
<span class="stat-number text-secondary">{{ projects|selectattr('category', 'equalto', 'Productivity')|list|length }}</span>
|
|
<span class="text-muted small">Productivity</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-2 col-md-3 col-6 mb-3">
|
|
<div class="stat-item">
|
|
<span class="stat-number text-danger">{{ projects|selectattr('category', 'equalto', 'Media & Entertainment')|list|length }}</span>
|
|
<span class="text-muted small">Media & Entertainment</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Zusätzliche Statistiken Zeile -->
|
|
<div class="row text-center mt-3 pt-3 border-top">
|
|
<div class="col-lg-3 col-md-6 col-12 mb-2">
|
|
<div class="stat-item-small">
|
|
<i class="fab fa-docker text-primary me-1"></i>
|
|
<span class="stat-number-small">{{ projects|selectattr('requirements.docker', 'equalto', true)|list|length }}</span>
|
|
<span class="text-muted small">Docker-fähig</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-md-6 col-12 mb-2">
|
|
<div class="stat-item-small">
|
|
<i class="fab fa-js-square text-warning me-1"></i>
|
|
<span class="stat-number-small">{{ projects|selectattr('language', 'equalto', 'JavaScript')|list|length + projects|selectattr('language', 'equalto', 'Node.js')|list|length }}</span>
|
|
<span class="text-muted small">JavaScript/Node.js</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-md-6 col-12 mb-2">
|
|
<div class="stat-item-small">
|
|
<i class="fab fa-python text-success me-1"></i>
|
|
<span class="stat-number-small">{{ projects|selectattr('language', 'equalto', 'Python')|list|length }}</span>
|
|
<span class="text-muted small">Python</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-3 col-md-6 col-12 mb-2">
|
|
<div class="stat-item-small">
|
|
<i class="fas fa-code text-info me-1"></i>
|
|
<span class="stat-number-small">{{ (projects|length) - (projects|selectattr('language', 'equalto', 'JavaScript')|list|length) - (projects|selectattr('language', 'equalto', 'Node.js')|list|length) - (projects|selectattr('language', 'equalto', 'Python')|list|length) }}</span>
|
|
<span class="text-muted small">Andere Sprachen</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Bulk Installation Modal -->
|
|
<div class="modal fade" id="bulkInstallModal" tabindex="-1" role="dialog" aria-labelledby="bulkInstallTitle" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg" role="document">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="bulkInstallTitle">
|
|
<i class="fas fa-download me-2"></i>Masseninstallation
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen" onclick="closeModalSafely('bulkInstallModal')"></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" onclick="closeModalSafely('bulkInstallModal')">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" role="dialog" aria-labelledby="projectDetailsTitle" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg" role="document">
|
|
<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" aria-label="Schließen" onclick="closeModalSimple('projectDetailsModal')"></button>
|
|
</div>
|
|
<div class="modal-body" id="projectDetailsContent">
|
|
<!-- Content wird per JavaScript gefüllt -->
|
|
<div class="text-center">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Laden...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="closeModalSimple('projectDetailsModal')">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 %}
|
|
<style>
|
|
.stat-item {
|
|
text-align: center;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.stat-item .stat-number {
|
|
display: block;
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
line-height: 1;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.stat-item span:last-child {
|
|
display: block;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.stat-item-small {
|
|
text-align: center;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.stat-item-small .stat-number-small {
|
|
font-size: 1.25rem;
|
|
font-weight: bold;
|
|
margin: 0 0.25rem;
|
|
}
|
|
|
|
.stat-item-small i {
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.stat-item-small span:last-child {
|
|
font-size: 0.8rem;
|
|
margin-left: 0.25rem;
|
|
}
|
|
|
|
/* Responsive Anpassungen für Statistiken */
|
|
@media (max-width: 768px) {
|
|
.stat-item .stat-number {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.stat-item-small .stat-number-small {
|
|
font-size: 1rem;
|
|
}
|
|
}
|
|
|
|
/* Split-Button Styling für bessere UX */
|
|
.btn-group .btn:first-child {
|
|
border-top-right-radius: 0;
|
|
border-bottom-right-radius: 0;
|
|
}
|
|
|
|
.btn-group .btn:last-child {
|
|
border-top-left-radius: 0;
|
|
border-bottom-left-radius: 0;
|
|
border-left: 1px solid rgba(255,255,255,0.2);
|
|
}
|
|
|
|
.btn-group .btn:last-child:hover {
|
|
border-left: 1px solid rgba(255,255,255,0.4);
|
|
}
|
|
</style>
|
|
<script>
|
|
let installedProjects = [];
|
|
|
|
// Lade installierte Projekte beim Seitenaufruf
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
console.log('🚀 Seite geladen, initialisiere...');
|
|
|
|
// SOFORTIGE Bereinigung beim Seitenaufruf
|
|
forceCleanupModals();
|
|
|
|
loadInstalledProjects();
|
|
setupFilters();
|
|
|
|
// Cleanup bei Browser-Navigation
|
|
window.addEventListener('beforeunload', forceCleanupModals);
|
|
|
|
// Cleanup bei ESC-Taste
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Escape') {
|
|
console.log('🔑 ESC gedrückt, bereinige Modals');
|
|
forceCleanupModals();
|
|
}
|
|
});
|
|
|
|
// Extra-Sicherheit: Periodische Backdrop-Bereinigung
|
|
setInterval(() => {
|
|
const backdrops = document.querySelectorAll('.modal-backdrop');
|
|
if (backdrops.length > 0) {
|
|
console.log(`⚠️ Gefunden ${backdrops.length} verwaiste Backdrops, entferne sie`);
|
|
backdrops.forEach(backdrop => backdrop.remove());
|
|
}
|
|
}, 2000); // Prüfe alle 2 Sekunden
|
|
});
|
|
|
|
// Lade installierte Projekte mit Versionsinfo
|
|
function loadInstalledProjects() {
|
|
fetch('/api/installed_projects')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
installedProjects = data.projects.map(p => ({
|
|
name: p.name,
|
|
version: p.version || '1.0.0',
|
|
status: p.status || 'unknown'
|
|
}));
|
|
updateProjectStatus();
|
|
updateInstalledCount();
|
|
} else {
|
|
console.error('Fehler beim Laden der installierten Projekte:', data.error);
|
|
// Fallback: Lade über Homepage
|
|
loadInstalledProjectsFallback();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Laden der installierten Projekte:', error);
|
|
loadInstalledProjectsFallback();
|
|
});
|
|
}
|
|
|
|
// Fallback-Methode über Homepage
|
|
function loadInstalledProjectsFallback() {
|
|
fetch('/')
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
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 {
|
|
name: projectName,
|
|
version: '1.0.0', // Default version
|
|
status: 'running'
|
|
};
|
|
});
|
|
|
|
updateProjectStatus();
|
|
updateInstalledCount();
|
|
})
|
|
.catch(error => {
|
|
console.error('Fallback-Fehler beim Laden der installierten Projekte:', error);
|
|
});
|
|
}
|
|
|
|
// Version vergleichen (semantic versioning)
|
|
function compareVersions(version1, version2) {
|
|
const v1parts = version1.split('.').map(n => parseInt(n, 10));
|
|
const v2parts = version2.split('.').map(n => parseInt(n, 10));
|
|
|
|
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
|
|
const v1part = v1parts[i] || 0;
|
|
const v2part = v2parts[i] || 0;
|
|
|
|
if (v1part > v2part) return 1;
|
|
if (v1part < v2part) return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Update Projekt-Status Badges
|
|
function updateProjectStatus() {
|
|
document.querySelectorAll('.project-status').forEach(statusEl => {
|
|
const projectName = statusEl.dataset.projectName;
|
|
const installedProject = installedProjects.find(p => p.name === projectName);
|
|
|
|
// Finde Projektdaten aus der verfügbaren Liste
|
|
const projectData = {{ projects|tojson }};
|
|
const availableProject = projectData.find(p => p.name === projectName);
|
|
const availableVersion = availableProject?.metadata?.version || '1.0.0';
|
|
|
|
if (installedProject) {
|
|
const installedVersion = installedProject.version;
|
|
const versionComparison = compareVersions(availableVersion, installedVersion);
|
|
|
|
if (versionComparison > 0) {
|
|
// Update verfügbar
|
|
statusEl.innerHTML = `
|
|
<span class="badge bg-warning">Update verfügbar</span>
|
|
<small class="text-muted d-block">${installedVersion} → ${availableVersion}</small>
|
|
`;
|
|
|
|
// Update Button aktivieren
|
|
const installBtn = statusEl.closest('.card').querySelector('.install-btn');
|
|
if (installBtn) {
|
|
installBtn.innerHTML = `<i class="fas fa-arrow-up"></i> Update (${installedVersion} → ${availableVersion})`;
|
|
installBtn.className = 'btn btn-warning';
|
|
installBtn.disabled = false;
|
|
installBtn.onclick = () => updateProject(installBtn.dataset.url, installBtn.dataset.name);
|
|
}
|
|
} else {
|
|
// Bereits aktuell
|
|
statusEl.innerHTML = `
|
|
<span class="badge bg-success">Aktuell (${installedVersion})</span>
|
|
`;
|
|
|
|
// Update Button deaktivieren
|
|
const installBtn = statusEl.closest('.card').querySelector('.install-btn');
|
|
if (installBtn) {
|
|
installBtn.innerHTML = `<i class="fas fa-check"></i> Aktuell`;
|
|
installBtn.className = 'btn btn-success';
|
|
installBtn.disabled = true;
|
|
installBtn.style.opacity = '0.6';
|
|
}
|
|
|
|
// Dropdown-Toggle auch deaktivieren
|
|
const dropdownToggle = statusEl.closest('.card').querySelector('.dropdown-toggle-split');
|
|
if (dropdownToggle) {
|
|
dropdownToggle.disabled = true;
|
|
dropdownToggle.style.opacity = '0.6';
|
|
dropdownToggle.classList.add('disabled');
|
|
// Entferne Bootstrap Dropdown-Funktionalität
|
|
dropdownToggle.removeAttribute('data-bs-toggle');
|
|
dropdownToggle.title = 'Projekt ist bereits auf dem neuesten Stand';
|
|
}
|
|
|
|
// Dropdown-Menü-Items deaktivieren
|
|
const dropdownItems = statusEl.closest('.card').querySelectorAll('.dropdown-item');
|
|
dropdownItems.forEach(item => {
|
|
if (!item.textContent.includes('Projektdetails')) {
|
|
item.style.opacity = '0.5';
|
|
item.style.pointerEvents = 'none';
|
|
item.classList.add('disabled');
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
// Nicht installiert
|
|
statusEl.innerHTML = '<span class="badge bg-secondary">Verfügbar</span>';
|
|
|
|
const installBtn = statusEl.closest('.card').querySelector('.install-btn');
|
|
if (installBtn) {
|
|
installBtn.innerHTML = '<i class="fas fa-download"></i> Installieren';
|
|
installBtn.className = 'btn btn-success';
|
|
installBtn.disabled = false;
|
|
installBtn.style.opacity = '1';
|
|
installBtn.onclick = () => installProject(installBtn.dataset.url, installBtn.dataset.name);
|
|
}
|
|
|
|
// Dropdown-Toggle wieder aktivieren
|
|
const dropdownToggle = statusEl.closest('.card').querySelector('.dropdown-toggle-split');
|
|
if (dropdownToggle) {
|
|
dropdownToggle.disabled = false;
|
|
dropdownToggle.style.opacity = '1';
|
|
dropdownToggle.classList.remove('disabled');
|
|
dropdownToggle.setAttribute('data-bs-toggle', 'dropdown');
|
|
dropdownToggle.removeAttribute('title');
|
|
}
|
|
|
|
// Dropdown-Menü-Items wieder aktivieren
|
|
const dropdownItems = statusEl.closest('.card').querySelectorAll('.dropdown-item');
|
|
dropdownItems.forEach(item => {
|
|
item.style.opacity = '1';
|
|
item.style.pointerEvents = 'auto';
|
|
item.classList.remove('disabled');
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Update Anzahl installierter Apps
|
|
function updateInstalledCount() {
|
|
const installedCount = document.getElementById('installedCount');
|
|
if (installedCount) {
|
|
installedCount.textContent = installedProjects.length;
|
|
}
|
|
}
|
|
|
|
// Projekt installieren mit Installationsmethode
|
|
function installProject(url, name, method = 'clone') {
|
|
const button = event.target;
|
|
const originalText = button.innerHTML;
|
|
|
|
// Zeige spezifischen Loading-Text basierend auf Methode
|
|
if (method === 'image') {
|
|
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Docker lädt...';
|
|
} else {
|
|
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Klont Code...';
|
|
}
|
|
button.disabled = true;
|
|
|
|
console.log(`🚀 Installiere ${name} via ${method} von ${url}`);
|
|
|
|
fetch('/install_project', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
},
|
|
body: `project_url=${encodeURIComponent(url)}&project_name=${encodeURIComponent(name)}&installation_method=${encodeURIComponent(method)}`
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Erfolgsmeldung mit Methode und Version
|
|
const version = data.version || '1.0.0';
|
|
const methodIcon = method === 'image' ? '🐳' : '📦';
|
|
button.innerHTML = `<i class="fas fa-check"></i> ${methodIcon} Installiert (v${version})`;
|
|
button.className = 'btn btn-success';
|
|
|
|
// Update installierte Projekte Liste
|
|
const existingProject = installedProjects.find(p => p.name === name);
|
|
if (existingProject) {
|
|
existingProject.version = version;
|
|
existingProject.method = method;
|
|
} else {
|
|
installedProjects.push({
|
|
name: name,
|
|
version: version,
|
|
method: method,
|
|
status: 'running'
|
|
});
|
|
}
|
|
|
|
updateProjectStatus();
|
|
updateInstalledCount();
|
|
|
|
// Erfolgsmeldung anzeigen
|
|
showAlert('success', `${name} erfolgreich installiert! Seite wird in 3 Sekunden neu geladen...`);
|
|
|
|
// Nach 3 Sekunden Seite neuladen um aktuellen Status zu zeigen
|
|
setTimeout(() => {
|
|
console.log('🔄 Lade Seite nach erfolgreicher Installation neu...');
|
|
window.location.reload();
|
|
}, 3000);
|
|
} else {
|
|
button.innerHTML = originalText;
|
|
button.disabled = false;
|
|
|
|
// Zeige detaillierten Fehler
|
|
showAlert('danger', `Installationsfehler für ${name}: ${data.message}`);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
button.innerHTML = originalText;
|
|
button.disabled = false;
|
|
console.error('Installation error:', error);
|
|
showAlert('danger', `Netzwerkfehler bei Installation von ${name}: ${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';
|
|
|
|
// Erfolgsmeldung anzeigen
|
|
showAlert('success', `${name} erfolgreich aktualisiert! Seite wird in 3 Sekunden neu geladen...`);
|
|
|
|
// Nach 3 Sekunden Seite neuladen um aktuellen Status zu zeigen
|
|
setTimeout(() => {
|
|
console.log('🔄 Lade Seite nach erfolgreichem Update neu...');
|
|
window.location.reload();
|
|
}, 3000);
|
|
} else {
|
|
button.innerHTML = originalText;
|
|
button.disabled = false;
|
|
showAlert('danger', `Update-Fehler für ${name}: ${data.message}`);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
button.innerHTML = originalText;
|
|
button.disabled = false;
|
|
console.error('Update error:', error);
|
|
showAlert('danger', `Netzwerkfehler beim Update von ${name}: ${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() {
|
|
showModalSafely('bulkInstallModal');
|
|
}
|
|
|
|
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?`)) {
|
|
closeModalSafely('bulkInstallModal');
|
|
|
|
// Kurz warten bis Modal geschlossen ist
|
|
setTimeout(() => {
|
|
installAppsSequentially(selectedApps, 0);
|
|
}, 300);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
console.log('🔍 Zeige Projektdetails für:', projectName);
|
|
|
|
// SOFORTIGE und AGGRESSIVE Backdrop-Bereinigung
|
|
forceCleanupModals();
|
|
|
|
// Warte bis alles sauber ist, dann öffne Modal
|
|
setTimeout(() => {
|
|
// 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 && project.screenshots.available ? `
|
|
<h6>Screenshots</h6>
|
|
<div class="row">
|
|
${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;"
|
|
onclick="window.open('${img}', '_blank')">
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
` : ''}
|
|
|
|
${project.installation_notes ? `
|
|
<h6 class="mt-3">Installationshinweise</h6>
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-info-circle me-2"></i>${project.installation_notes}
|
|
</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>
|
|
${project.install_time ? `<tr><td>Installationszeit:</td><td>${project.install_time}</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>
|
|
` : ''}
|
|
|
|
${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 && 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>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('projectDetailsContent').innerHTML = content;
|
|
|
|
// Install Button Setup mit verbesserter Logik
|
|
const installBtn = document.getElementById('installFromDetails');
|
|
const installedProject = installedProjects.find(p => p.name === projectName);
|
|
|
|
if (installedProject) {
|
|
const availableVersion = project.metadata?.version || '1.0.0';
|
|
const installedVersion = installedProject.version;
|
|
const versionComparison = compareVersions(availableVersion, installedVersion);
|
|
|
|
if (versionComparison > 0) {
|
|
installBtn.innerHTML = `<i class="fas fa-arrow-up"></i> Update (${installedVersion} → ${availableVersion})`;
|
|
installBtn.className = 'btn btn-warning';
|
|
installBtn.disabled = false;
|
|
installBtn.onclick = () => {
|
|
closeModalSimple('projectDetailsModal');
|
|
updateProject(project.url, project.name);
|
|
};
|
|
} else {
|
|
installBtn.innerHTML = `<i class="fas fa-check"></i> Bereits installiert (${installedVersion})`;
|
|
installBtn.className = 'btn btn-success';
|
|
installBtn.disabled = true;
|
|
}
|
|
} else {
|
|
installBtn.innerHTML = '<i class="fas fa-download"></i> Installieren';
|
|
installBtn.className = 'btn btn-success';
|
|
installBtn.disabled = false;
|
|
installBtn.onclick = () => {
|
|
closeModalSimple('projectDetailsModal');
|
|
installProject(project.url, project.name);
|
|
};
|
|
}
|
|
|
|
// Öffne Modal mit EINFACHER API
|
|
openModalSimple('projectDetailsModal');
|
|
|
|
}, 200); // Längere Wartezeit für absolute Sicherheit
|
|
}
|
|
|
|
// EINFACHE Modal-Funktionen ohne komplexe Bootstrap-Logik
|
|
function openModalSimple(modalId) {
|
|
console.log('🎯 Öffne Modal einfach:', modalId);
|
|
|
|
// SOFORT alle Backdrops entfernen
|
|
document.querySelectorAll('.modal-backdrop').forEach(backdrop => backdrop.remove());
|
|
|
|
// Body-Klassen bereinigen
|
|
document.body.classList.remove('modal-open');
|
|
document.body.style.removeProperty('overflow');
|
|
document.body.style.removeProperty('padding-right');
|
|
|
|
const modalElement = document.getElementById(modalId);
|
|
if (!modalElement) {
|
|
console.error('❌ Modal nicht gefunden:', modalId);
|
|
return;
|
|
}
|
|
|
|
// Modal direkt zeigen OHNE Bootstrap-Instanz
|
|
modalElement.style.display = 'block';
|
|
modalElement.classList.add('show');
|
|
modalElement.setAttribute('aria-modal', 'true');
|
|
modalElement.removeAttribute('aria-hidden');
|
|
|
|
// Body-Klasse für Modal hinzufügen
|
|
document.body.classList.add('modal-open');
|
|
|
|
console.log('✅ Modal einfach geöffnet:', modalId);
|
|
}
|
|
|
|
function closeModalSimple(modalId) {
|
|
console.log('🎯 Schließe Modal einfach:', modalId);
|
|
|
|
const modalElement = document.getElementById(modalId);
|
|
if (modalElement) {
|
|
modalElement.style.display = 'none';
|
|
modalElement.classList.remove('show');
|
|
modalElement.setAttribute('aria-hidden', 'true');
|
|
modalElement.removeAttribute('aria-modal');
|
|
}
|
|
|
|
// Body bereinigen
|
|
document.body.classList.remove('modal-open');
|
|
document.body.style.removeProperty('overflow');
|
|
document.body.style.removeProperty('padding-right');
|
|
|
|
// Alle Backdrops entfernen
|
|
document.querySelectorAll('.modal-backdrop').forEach(backdrop => backdrop.remove());
|
|
|
|
console.log('✅ Modal einfach geschlossen:', modalId);
|
|
}
|
|
|
|
// Sichere Modal-Funktionen
|
|
function showModalSafely(modalId) {
|
|
console.log('🚀 Öffne Modal sicher:', modalId);
|
|
|
|
const modalElement = document.getElementById(modalId);
|
|
if (!modalElement) {
|
|
console.error('❌ Modal Element nicht gefunden:', modalId);
|
|
return;
|
|
}
|
|
|
|
// Sofortige aggressive Bereinigung
|
|
forceCleanupModals();
|
|
|
|
// Längere Wartezeit für gründliche Bereinigung
|
|
setTimeout(() => {
|
|
try {
|
|
console.log('📋 Initialisiere Modal:', modalId);
|
|
|
|
// Erstelle Modal mit sicheren Einstellungen
|
|
const modal = new bootstrap.Modal(modalElement, {
|
|
backdrop: false, // KEIN Backdrop! Verhindert das Problem
|
|
keyboard: true,
|
|
focus: true
|
|
});
|
|
|
|
// Cleanup-Handler für Schließen
|
|
modalElement.addEventListener('hidden.bs.modal', function handleModalClose(e) {
|
|
console.log('🔒 Modal geschlossen:', modalId);
|
|
modalElement.removeEventListener('hidden.bs.modal', handleModalClose);
|
|
setTimeout(() => forceCleanupModals(), 100);
|
|
});
|
|
|
|
// Zeige Modal
|
|
modal.show();
|
|
console.log('✅ Modal erfolgreich geöffnet:', modalId);
|
|
|
|
} catch (error) {
|
|
console.error('❌ Fehler beim Öffnen des Modals:', error);
|
|
forceCleanupModals();
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
function closeModalSafely(modalId) {
|
|
console.log('🔒 Schließe Modal sicher:', modalId);
|
|
|
|
const modalElement = document.getElementById(modalId);
|
|
if (!modalElement) {
|
|
console.log('⚠️ Modal Element nicht gefunden:', modalId);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const modal = bootstrap.Modal.getInstance(modalElement);
|
|
if (modal) {
|
|
console.log('📋 Bootstrap Modal gefunden, verstecke...');
|
|
modal.hide();
|
|
} else {
|
|
console.log('⚠️ Keine Bootstrap Modal Instanz, manuelles Verstecken...');
|
|
modalElement.classList.remove('show');
|
|
modalElement.style.display = 'none';
|
|
modalElement.setAttribute('aria-hidden', 'true');
|
|
modalElement.removeAttribute('aria-modal');
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Fehler beim Schließen:', error);
|
|
}
|
|
|
|
// Sofortige Bereinigung
|
|
setTimeout(() => {
|
|
forceCleanupModals();
|
|
}, 150);
|
|
}
|
|
|
|
function forceCleanupModals() {
|
|
console.log('🧹 Aggressive Modal-Bereinigung...');
|
|
|
|
// 1. Entferne ALLE Modal-Backdrops sofort
|
|
const backdrops = document.querySelectorAll('.modal-backdrop');
|
|
console.log(`Gefundene Backdrops: ${backdrops.length}`);
|
|
backdrops.forEach((backdrop, index) => {
|
|
console.log(`Entferne Backdrop ${index + 1}/${backdrops.length}`);
|
|
backdrop.remove();
|
|
});
|
|
|
|
// 2. Bereinige Body komplett
|
|
document.body.classList.remove('modal-open');
|
|
document.body.style.removeProperty('overflow');
|
|
document.body.style.removeProperty('padding-right');
|
|
document.body.style.removeProperty('margin-right');
|
|
|
|
// 3. Verstecke und bereinige alle Modals
|
|
document.querySelectorAll('.modal').forEach(modal => {
|
|
modal.classList.remove('show', 'fade', 'd-block');
|
|
modal.style.display = 'none';
|
|
modal.style.removeProperty('padding-right');
|
|
modal.setAttribute('aria-hidden', 'true');
|
|
modal.removeAttribute('aria-modal');
|
|
modal.removeAttribute('role');
|
|
|
|
// Zerstöre Bootstrap Modal Instanz
|
|
const bsModal = bootstrap.Modal.getInstance(modal);
|
|
if (bsModal) {
|
|
try {
|
|
bsModal.dispose();
|
|
console.log('Modal-Instanz disposed:', modal.id);
|
|
} catch (e) {
|
|
console.warn('Fehler beim Disposal:', e);
|
|
}
|
|
}
|
|
});
|
|
|
|
// 4. Extra-Sicherheit: Entferne alle versteckten Bootstrap-Elemente
|
|
setTimeout(() => {
|
|
document.querySelectorAll('.modal-backdrop, .fade.show').forEach(el => el.remove());
|
|
console.log('✅ Modal-Bereinigung abgeschlossen');
|
|
}, 50);
|
|
}
|
|
|
|
// AJAX Funktionen für Projektliste-Update
|
|
function refreshProjectsList() {
|
|
const refreshBtn = document.getElementById('refreshProjectsBtn');
|
|
const originalText = refreshBtn.innerHTML;
|
|
|
|
// Zeige Loading-Status
|
|
refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Aktualisiere...';
|
|
refreshBtn.disabled = true;
|
|
|
|
fetch('/api/refresh_projects', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Zeige Erfolg
|
|
showAlert('success', data.message);
|
|
|
|
// Lade Seite neu um aktualisierte Projekte anzuzeigen
|
|
setTimeout(() => {
|
|
window.location.reload();
|
|
}, 1000);
|
|
} else {
|
|
showAlert('danger', data.error || 'Fehler beim Aktualisieren der Projektliste');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Fehler beim Aktualisieren:', error);
|
|
showAlert('danger', 'Netzwerkfehler beim Aktualisieren der Projektliste');
|
|
})
|
|
.finally(() => {
|
|
// Button zurücksetzen
|
|
refreshBtn.innerHTML = originalText;
|
|
refreshBtn.disabled = false;
|
|
});
|
|
}
|
|
|
|
function showAlert(type, message) {
|
|
// Erstelle Alert-Element
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
|
alertDiv.innerHTML = `
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
// Füge am Anfang des Containers hinzu
|
|
const container = document.querySelector('.container-fluid') || document.querySelector('.container');
|
|
container.insertBefore(alertDiv, container.firstChild);
|
|
|
|
// Automatisch nach 5 Sekunden ausblenden
|
|
setTimeout(() => {
|
|
alertDiv.remove();
|
|
}, 5000);
|
|
}
|
|
|
|
// Custom Install Page öffnen
|
|
function openCustomInstall(projectName) {
|
|
window.location.href = `/custom_install/${encodeURIComponent(projectName)}`;
|
|
}
|
|
</script>
|
|
{% endblock %}
|