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" }