Files
Aether/backend/internal/admin/manifest_handler.go
T
HoneyandClaude Opus 5 31a027c93a 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>
2026-08-24 09:25:36 +02:00

96 lines
2.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
}