fix: keep active page on config save, add Cyrillic preview support, fix grid sizing
- daemon.go: save/restore active page on config reload instead of always activating default_page (fixes page switch on save) - render.go: replace basicfont.Face7x13 with parseDisplayFace() for unicode/Cyrillic support in edit key preview - web.go: add SSE endpoint (GET /api/events) for real-time page_changed events and POST /api/activate-page API - app.js: persist active page via localStorage, listen for SSE page_changed events - styles.css, index.html: fix grid sizing (width 100%, minmax(0, 1fr), aspect-ratio on key buttons, max-width 525px)
This commit is contained in:
@@ -37,12 +37,20 @@ type WebServer struct {
|
||||
cfg *config.Config
|
||||
configPath string
|
||||
pm *PageManager
|
||||
extraPMs []*PageManager
|
||||
decks []*Deck
|
||||
mu sync.RWMutex
|
||||
|
||||
sseClients map[chan string]struct{}
|
||||
sseMu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewWebServer(cfg *config.Config, configPath string) *WebServer {
|
||||
return &WebServer{cfg: cfg, configPath: configPath}
|
||||
return &WebServer{
|
||||
cfg: cfg,
|
||||
configPath: configPath,
|
||||
sseClients: make(map[chan string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebServer) UpdateConfig(cfg *config.Config) {
|
||||
@@ -63,6 +71,23 @@ func (s *WebServer) SetDecks(decks []*Deck) {
|
||||
s.decks = decks
|
||||
}
|
||||
|
||||
func (s *WebServer) SetExtraPageManagers(pms []*PageManager) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.extraPMs = pms
|
||||
}
|
||||
|
||||
func (s *WebServer) BroadcastPageChange(page string) {
|
||||
s.sseMu.RLock()
|
||||
defer s.sseMu.RUnlock()
|
||||
for ch := range s.sseClients {
|
||||
select {
|
||||
case ch <- page:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebServer) Serve(ctx context.Context, addr string) error {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
@@ -86,6 +111,9 @@ func (s *WebServer) Serve(ctx context.Context, addr string) error {
|
||||
|
||||
mux.HandleFunc("GET /api/status", s.handleGetStatus)
|
||||
|
||||
mux.HandleFunc("GET /api/events", s.handleSSE)
|
||||
mux.HandleFunc("POST /api/activate-page", s.handleActivatePage)
|
||||
|
||||
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticRoot))))
|
||||
|
||||
mux.HandleFunc("/", s.handleSPA)
|
||||
@@ -127,6 +155,80 @@ func (s *WebServer) handleSPA(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func (s *WebServer) handleSSE(w http.ResponseWriter, r *http.Request) {
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming not supported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
ch := make(chan string, 8)
|
||||
|
||||
s.sseMu.Lock()
|
||||
s.sseClients[ch] = struct{}{}
|
||||
s.sseMu.Unlock()
|
||||
|
||||
notify := r.Context().Done()
|
||||
go func() {
|
||||
<-notify
|
||||
s.sseMu.Lock()
|
||||
delete(s.sseClients, ch)
|
||||
s.sseMu.Unlock()
|
||||
}()
|
||||
|
||||
flusher.Flush()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-notify:
|
||||
return
|
||||
case page := <-ch:
|
||||
data, _ := json.Marshal(map[string]string{"page": page})
|
||||
fmt.Fprintf(w, "event: page_changed\ndata: %s\n\n", data)
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebServer) handleActivatePage(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Page string `json:"page"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid JSON: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Page == "" {
|
||||
http.Error(w, "page is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
allPMs := append([]*PageManager{s.pm}, s.extraPMs...)
|
||||
s.mu.RUnlock()
|
||||
|
||||
for _, pm := range allPMs {
|
||||
if pm == nil {
|
||||
continue
|
||||
}
|
||||
if err := pm.ActivatePage(req.Page); err != nil {
|
||||
slog.Warn("activate page", "page", req.Page, "error", err)
|
||||
} else {
|
||||
pm.stopPeriodicKeys()
|
||||
pm.startPeriodicKeys()
|
||||
}
|
||||
}
|
||||
|
||||
s.BroadcastPageChange(req.Page)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "ok", "page": req.Page})
|
||||
}
|
||||
|
||||
func (s *WebServer) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
Reference in New Issue
Block a user