Add TypeScript migration, image resizing, media upload UX, and multimedia support
All checks were successful
Deploy to NAS / deploy (push) Successful in 2m20s

- Migrate static JS to TypeScript (static-ts/ → compiled to internal/api/static/)
- Add image resizing on upload: JPEG/PNG/WebP scaled to max 1920px at quality 80
- Extract shared upload logic into upload.go (saveUpload, saveResizedImage, saveResizedWebP)
- Add POST /media endpoint for drag-drop/paste media uploads with markdown ref return
- Add background music player with video/audio coordination (autoplay.ts)
- Add global nav, public feed, hashtags, visibility, Markdown rendering for entries
- Add Dockerfile stage for TypeScript compilation (static-ts-builder)
- Add goldmark, disintegration/imaging, golang.org/x/image dependencies

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Christoph K.
2026-04-09 23:03:04 +02:00
parent 8eef933573
commit 17186e7b64
24 changed files with 890 additions and 179 deletions

View File

@@ -0,0 +1,76 @@
package api
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log/slog"
"net/http"
"strings"
)
type MediaHandler struct {
uploadDir string
}
func NewMediaHandler(uploadDir string) *MediaHandler {
return &MediaHandler{uploadDir: uploadDir}
}
// HandleUpload handles POST /media — uploads a single file and returns its markdown reference.
func (h *MediaHandler) HandleUpload(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(maxUploadSize); err != nil {
http.Error(w, "too large", http.StatusRequestEntityTooLarge)
return
}
fh, _, err := r.FormFile("file")
if err != nil {
http.Error(w, "missing file", http.StatusBadRequest)
return
}
defer fh.Close()
buf := make([]byte, 512)
n, _ := fh.Read(buf)
mime := http.DetectContentType(buf[:n])
if _, ok := allowedMIME[mime]; !ok {
http.Error(w, "unsupported type", http.StatusUnsupportedMediaType)
return
}
filename, err := saveUpload(h.uploadDir, randomID(), mime, buf[:n], fh)
if err != nil {
http.Error(w, "storage error", http.StatusInternalServerError)
return
}
ref := markdownRef(mime, filename)
slog.Info("media uploaded", "filename", filename, "mime", mime)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"filename": filename,
"mime": mime,
"ref": ref,
})
}
func markdownRef(mime, filename string) string {
url := "/uploads/" + filename
switch {
case strings.HasPrefix(mime, "image/"):
return "![" + filename + "](" + url + ")"
case strings.HasPrefix(mime, "video/"):
return "![" + filename + "](" + url + ")"
case strings.HasPrefix(mime, "audio/"):
return "[" + filename + "](" + url + ")"
default:
return "[" + filename + "](" + url + ")"
}
}
func randomID() string {
b := make([]byte, 12)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}