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>
96 lines
2.9 KiB
Go
96 lines
2.9 KiB
Go
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"
|
||
}
|