new file: app.py new file: config.example.json new file: config.json new file: docker_diagnose.py new file: requirements.txt new file: start.bat new file: templates/available_projects.html new file: templates/base.html new file: templates/config.html new file: templates/docker_status.html new file: templates/index.html new file: templates/project_details.html new file: templates/project_details_fixed.html new file: templates/project_details_new.html new file: templates/project_details_old.html .gitignore
311 lines
12 KiB
HTML
311 lines
12 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>
|
|
<a href="{{ url_for('refresh_projects') }}" class="btn btn-primary">
|
|
<i class="fas fa-sync-alt"></i> Liste aktualisieren
|
|
</a>
|
|
</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">
|
|
{% for project in projects %}
|
|
<div class="col-lg-6 col-xl-4 mb-4">
|
|
<div class="card h-100">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-git-alt me-2"></i>{{ project.name or 'Unbekannt' }}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body d-flex flex-column">
|
|
<div class="mb-3">
|
|
<small class="text-muted">Repository URL:</small>
|
|
<p class="card-text small font-monospace">{{ project.url }}</p>
|
|
</div>
|
|
|
|
{% if project.description %}
|
|
<div class="mb-3">
|
|
<small class="text-muted">Beschreibung:</small>
|
|
<p class="card-text">{{ project.description }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if project.tags %}
|
|
<div class="mb-3">
|
|
{% for tag in project.tags %}
|
|
<span class="badge bg-secondary me-1">{{ tag }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="row mb-3">
|
|
<div class="col-6">
|
|
<small class="text-muted">Sprache:</small><br>
|
|
<span class="badge bg-info">{{ project.language or 'Unbekannt' }}</span>
|
|
</div>
|
|
<div class="col-6">
|
|
<small class="text-muted">Größe:</small><br>
|
|
<span class="text-muted">{{ project.size or 'Unbekannt' }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{% if project.last_updated %}
|
|
<div class="mb-3">
|
|
<small class="text-muted">Letztes Update: {{ project.last_updated }}</small>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="mt-auto">
|
|
<div class="d-grid gap-2">
|
|
<button class="btn btn-success" onclick="installProject('{{ project.url }}', '{{ project.name }}')">
|
|
<i class="fas fa-download"></i> Installieren
|
|
</button>
|
|
<a href="{{ project.url }}" target="_blank" class="btn btn-outline-info btn-sm">
|
|
<i class="fas fa-external-link-alt"></i> Repository anzeigen
|
|
</a>
|
|
</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 text-primary">{{ projects|length }}</span>
|
|
<span class="text-muted">Verfügbare Apps</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stat-item">
|
|
<span class="stat-number text-success">{{ projects|selectattr('language', 'defined')|list|length }}</span>
|
|
<span class="text-muted">Mit Sprache</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stat-item">
|
|
<span class="stat-number text-info">{{ projects|selectattr('description', 'defined')|list|length }}</span>
|
|
<span class="text-muted">Mit Beschreibung</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stat-item">
|
|
<span class="stat-number text-warning">{{ projects|selectattr('tags', 'defined')|list|length }}</span>
|
|
<span class="text-muted">Mit Tags</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 }}">
|
|
<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>
|
|
|
|
<!-- Filter und Sortierung -->
|
|
<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-filter me-2"></i>Filter & Sortierung
|
|
</h5>
|
|
</div>
|
|
<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="App-Name oder Beschreibung...">
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="languageFilter" class="form-label">Programmiersprache:</label>
|
|
<select class="form-select" id="languageFilter">
|
|
<option value="">Alle Sprachen</option>
|
|
{% set languages = projects|map(attribute='language')|select('defined')|unique|list %}
|
|
{% for lang in languages %}
|
|
<option value="{{ lang }}">{{ lang }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<label for="sortBy" class="form-label">Sortieren nach:</label>
|
|
<select class="form-select" id="sortBy">
|
|
<option value="name">Name</option>
|
|
<option value="language">Sprache</option>
|
|
<option value="updated">Letztes Update</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label"> </label>
|
|
<div class="d-grid">
|
|
<button class="btn btn-outline-primary" onclick="showBulkInstall()">
|
|
<i class="fas fa-layer-group"></i> Masseninstallation
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// Projektfilterung
|
|
function filterProjects() {
|
|
const searchTerm = document.getElementById('searchFilter').value.toLowerCase();
|
|
const languageFilter = document.getElementById('languageFilter').value;
|
|
const sortBy = document.getElementById('sortBy').value;
|
|
|
|
const projectCards = document.querySelectorAll('.col-lg-6.col-xl-4');
|
|
|
|
projectCards.forEach(card => {
|
|
const projectName = card.querySelector('.card-title').textContent.toLowerCase();
|
|
const projectDesc = card.querySelector('.card-text')?.textContent.toLowerCase() || '';
|
|
const projectLang = card.querySelector('.badge.bg-info')?.textContent || '';
|
|
|
|
let show = true;
|
|
|
|
// Textsuche
|
|
if (searchTerm && !projectName.includes(searchTerm) && !projectDesc.includes(searchTerm)) {
|
|
show = false;
|
|
}
|
|
|
|
// Sprachfilter
|
|
if (languageFilter && projectLang !== languageFilter) {
|
|
show = false;
|
|
}
|
|
|
|
card.style.display = show ? 'block' : 'none';
|
|
});
|
|
}
|
|
|
|
// Event Listener für Filter
|
|
document.getElementById('searchFilter').addEventListener('input', filterProjects);
|
|
document.getElementById('languageFilter').addEventListener('change', filterProjects);
|
|
document.getElementById('sortBy').addEventListener('change', filterProjects);
|
|
|
|
// 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.nextElementSibling.textContent.trim()
|
|
}));
|
|
|
|
if (selectedApps.length === 0) {
|
|
alert('Bitte wählen Sie mindestens eine App aus.');
|
|
return;
|
|
}
|
|
|
|
if (confirm(`${selectedApps.length} Apps werden installiert. Fortfahren?`)) {
|
|
// Schließe Modal
|
|
bootstrap.Modal.getInstance(document.getElementById('bulkInstallModal')).hide();
|
|
|
|
// Installiere Apps nacheinander
|
|
installAppsSequentially(selectedApps, 0);
|
|
}
|
|
}
|
|
|
|
function installAppsSequentially(apps, index) {
|
|
if (index >= apps.length) {
|
|
alert('Alle Apps wurden installiert!');
|
|
location.reload();
|
|
return;
|
|
}
|
|
|
|
const app = apps[index];
|
|
const progressText = `Installiere ${index + 1}/${apps.length}: ${app.name}`;
|
|
|
|
// Zeige Fortschritt (könnte eine bessere Progress-Bar sein)
|
|
console.log(progressText);
|
|
|
|
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}`);
|
|
// Installiere nächste App
|
|
setTimeout(() => installAppsSequentially(apps, index + 1), 1000);
|
|
})
|
|
.catch(error => {
|
|
console.error(`Fehler bei ${app.name}:`, error);
|
|
// Installiere trotzdem weiter
|
|
setTimeout(() => installAppsSequentially(apps, index + 1), 1000);
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|