feat: Client-Download-System + Auto-Upload nach Build
Backend: - GET /api/clients - Verfuegbare Clients auflisten (oeffentlich) - GET /api/clients/<platform>/download - Client herunterladen (oeffentlich) - POST /api/clients/<platform>/upload - Build hochladen (BUILD_UPLOAD_TOKEN) - Alte Version wird automatisch bei neuem Upload ersetzt - Plattformen: linux, windows, mac, android, ios Frontend: - /clients - Download-Seite mit Grid aller verfuegbaren Clients - Login-Seite zeigt "Desktop & Mobile Clients herunterladen" Link wenn mindestens ein Client verfuegbar ist build.sh: - Nach jedem Build wird der Client automatisch auf CLOUD_URL hochgeladen (wenn CLOUD_URL + BUILD_UPLOAD_TOKEN in .env gesetzt) - Bestes Format pro Plattform: AppImage > .deb > Binary (Linux), .msi > .exe (Windows), .dmg (Mac), .apk (Android), .ipa (iOS) .env.example: - CLOUD_URL: Oeffentliche URL der Cloud-Instanz - BUILD_UPLOAD_TOKEN: Auth-Token fuer Build-Upload Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="clients-container">
|
||||
<div class="clients-card">
|
||||
<div class="clients-header">
|
||||
<i class="pi pi-cloud" style="font-size: 2rem; color: var(--p-primary-color)"></i>
|
||||
<h1>Mini-Cloud Clients</h1>
|
||||
<p>Lade den Sync-Client fuer dein Geraet herunter</p>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">
|
||||
<i class="pi pi-spin pi-spinner"></i> Laden...
|
||||
</div>
|
||||
|
||||
<div v-else-if="!clients.length" class="empty">
|
||||
<p>Noch keine Clients verfuegbar.</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="clients-grid">
|
||||
<div v-for="client in clients" :key="client.platform" class="client-card">
|
||||
<div class="client-icon">
|
||||
<i :class="'pi ' + platformIcon(client.platform)"></i>
|
||||
</div>
|
||||
<h3>{{ client.name }}</h3>
|
||||
<p class="client-meta">{{ client.filename }} ({{ formatSize(client.size) }})</p>
|
||||
<Button :label="'Download ' + client.name" icon="pi pi-download"
|
||||
@click="downloadClient(client)" fluid />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clients-footer">
|
||||
<router-link to="/login">Zurueck zur Anmeldung</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const clients = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
const platformIcons = {
|
||||
linux: 'pi-desktop',
|
||||
windows: 'pi-desktop',
|
||||
mac: 'pi-desktop',
|
||||
android: 'pi-mobile',
|
||||
ios: 'pi-mobile',
|
||||
}
|
||||
|
||||
function platformIcon(platform) {
|
||||
return platformIcons[platform] || 'pi-download'
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (!bytes) return ''
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let i = 0; let size = bytes
|
||||
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++ }
|
||||
return `${size.toFixed(i > 0 ? 1 : 0)} ${units[i]}`
|
||||
}
|
||||
|
||||
function downloadClient(client) {
|
||||
window.location.href = `/api/clients/${client.platform}/download`
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/clients')
|
||||
clients.value = res.data.clients
|
||||
} catch { clients.value = [] }
|
||||
loading.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.clients-container {
|
||||
min-height: 100vh; display: flex; align-items: center; justify-content: center;
|
||||
background: var(--p-surface-50); padding: 1rem;
|
||||
}
|
||||
.clients-card {
|
||||
background: var(--p-surface-0); border-radius: 12px; padding: 2.5rem;
|
||||
max-width: 700px; width: 100%; box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
.clients-header { text-align: center; margin-bottom: 2rem; }
|
||||
.clients-header h1 { font-size: 1.5rem; margin: 0.5rem 0 0.25rem; }
|
||||
.clients-header p { color: var(--p-text-muted-color); margin: 0; }
|
||||
.clients-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; }
|
||||
.client-card {
|
||||
border: 1px solid var(--p-surface-200); border-radius: 8px; padding: 1.25rem; text-align: center;
|
||||
}
|
||||
.client-icon { font-size: 2rem; color: var(--p-primary-color); margin-bottom: 0.5rem; }
|
||||
.client-card h3 { margin: 0 0 0.25rem; font-size: 1rem; }
|
||||
.client-meta { font-size: 0.8rem; color: var(--p-text-muted-color); margin: 0 0 1rem; }
|
||||
.loading, .empty { text-align: center; padding: 2rem; color: var(--p-text-muted-color); }
|
||||
.clients-footer { text-align: center; margin-top: 1.5rem; font-size: 0.875rem; }
|
||||
.clients-footer a { color: var(--p-primary-color); text-decoration: none; }
|
||||
</style>
|
||||
@@ -46,6 +46,9 @@
|
||||
<div v-if="registrationAllowed" class="auth-footer">
|
||||
<router-link to="/register">Noch kein Konto? Registrieren</router-link>
|
||||
</div>
|
||||
<div v-if="hasClients" class="auth-footer">
|
||||
<router-link to="/clients"><i class="pi pi-download"></i> Desktop & Mobile Clients herunterladen</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -68,12 +71,17 @@ const password = ref('')
|
||||
const error = ref('')
|
||||
const loading = ref(false)
|
||||
const registrationAllowed = ref(false)
|
||||
const hasClients = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/auth/registration-status')
|
||||
registrationAllowed.value = res.data.allowed
|
||||
} catch { registrationAllowed.value = false }
|
||||
try {
|
||||
const res = await axios.get('/api/clients')
|
||||
hasClients.value = res.data.has_clients
|
||||
} catch { hasClients.value = false }
|
||||
})
|
||||
|
||||
async function handleLogin() {
|
||||
|
||||
Reference in New Issue
Block a user