feat(aether): initial commit – self-hosted Cloud, Phasen 1–7
Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA. Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb, Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming, Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office (Collabora/OnlyOffice). Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort) sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
31a027c93a
225 files changed
+38042
No files matched your search
@@ -0,0 +1,95 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Das Web-App-Manifest kommt aus der Datenbank statt aus dem Build. Vorher
|
||||
// stand es fest in vite.config.ts und zeigte auf drei mitgelieferte Dateien –
|
||||
// ein hochgeladenes Symbol konnte die installierte App also nie erreichen.
|
||||
|
||||
// Die mitgelieferten Symbole aus frontend/public/, wenn keins hinterlegt ist.
|
||||
var builtinIcons = []manifestIcon{
|
||||
{Src: "/icon-192.png", Sizes: "192x192", Type: "image/png"},
|
||||
{Src: "/icon-512.png", Sizes: "512x512", Type: "image/png"},
|
||||
{Src: "/icon-maskable-512.png", Sizes: "512x512", Type: "image/png", Purpose: "maskable"},
|
||||
}
|
||||
|
||||
type manifestIcon struct {
|
||||
Src string `json:"src"`
|
||||
Sizes string `json:"sizes"`
|
||||
Type string `json:"type"`
|
||||
// Purpose bleibt bei "any" leer – das ist die Vorgabe, und ein leeres Feld
|
||||
// ist im Manifest weniger missverständlich als ein ausgeschriebenes "any".
|
||||
Purpose string `json:"purpose,omitempty"`
|
||||
}
|
||||
|
||||
// ServeManifest liefert /manifest.webmanifest. Ohne Anmeldung: Der Browser
|
||||
// holt es, bevor irgendjemand angemeldet ist.
|
||||
func (h *Handler) ServeManifest(w http.ResponseWriter, r *http.Request) {
|
||||
name := "Aether"
|
||||
themeColor := "#6366f1"
|
||||
var iconDir string
|
||||
|
||||
if s, err := h.store.GetAllSettings(r.Context()); err == nil {
|
||||
if n := s["cloud_name"]; n != "" {
|
||||
name = n
|
||||
}
|
||||
if c := s["primary_color"]; c != "" {
|
||||
themeColor = c
|
||||
}
|
||||
iconDir = s[appIconSettingKey]
|
||||
}
|
||||
|
||||
icons := builtinIcons
|
||||
if sizes := h.AvailableIconSizes(r, iconDir); len(sizes) > 0 {
|
||||
icons = nil
|
||||
for _, size := range sizes {
|
||||
// Das 64er ist das Tab-Symbol. Android will mindestens 192 und
|
||||
// wertet kleinere Einträge nur als Ballast.
|
||||
if size == "64" {
|
||||
continue
|
||||
}
|
||||
icon := manifestIcon{Src: AppIconURL(iconDir, size), Type: "image/png"}
|
||||
if size == "maskable" {
|
||||
icon.Purpose = "maskable"
|
||||
icon.Sizes = maskableSizeOf(sizes)
|
||||
} else {
|
||||
icon.Sizes = size + "x" + size
|
||||
}
|
||||
icons = append(icons, icon)
|
||||
}
|
||||
}
|
||||
|
||||
manifest := map[string]any{
|
||||
"name": name,
|
||||
"short_name": name,
|
||||
"description": "Your self-hosted cloud",
|
||||
"theme_color": themeColor,
|
||||
"background_color": "#0f172a",
|
||||
"display": "standalone",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"lang": "de",
|
||||
"icons": icons,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/manifest+json")
|
||||
// Die Symbole darin dürfen lange zwischengespeichert werden, die Liste
|
||||
// selbst nicht: Sonst bekäme der Browser einen Symbolwechsel tagelang nicht
|
||||
// mit, und genau darauf wartet die installierte App.
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
_ = json.NewEncoder(w).Encode(manifest)
|
||||
}
|
||||
|
||||
// maskableSizeOf nennt die Kantenlänge der maskable-Fassung. Sie entsteht in
|
||||
// 512, sobald die Vorlage das hergibt – sonst in 192.
|
||||
func maskableSizeOf(sizes []string) string {
|
||||
for _, s := range sizes {
|
||||
if s == "512" {
|
||||
return "512x512"
|
||||
}
|
||||
}
|
||||
return "192x192"
|
||||
}
|
||||
Reference in new issue
Block a user