package web import ( "encoding/json" "net/http" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/log" "gitgud.io/mike/mpv-manager/pkg/modernzconf" "gitgud.io/mike/mpv-manager/pkg/uiconfig" "gitgud.io/mike/mpv-manager/pkg/uoscconf" ) // handleUIConfigAPI returns all known options with their effective values and // whether each is explicitly set, for the installed mpv UI (ModernZ or uOSC). // GET /api/ui-config func (s *Server) handleUIConfigAPI(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed") return } switch ui := detectInstalledUI(); ui { case constants.UITypeModernZ: s.sendModernZUIConfig(w) case constants.UITypeUOSC: s.sendUOSCUIConfig(w) case constants.UITypeAmbiguous: status, err := uiconfig.Inspect() if err != nil { sendJSONError(w, http.StatusInternalServerError, "Failed to inspect UI configuration") return } w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(map[string]interface{}{ "ui": constants.UITypeAmbiguous, "options": []interface{}{}, "values": map[string]string{}, "is_set": map[string]bool{}, "warnings": status.Warnings, "migration": nil, }) default: w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(map[string]interface{}{ "ui": constants.UITypeNone, "options": []interface{}{}, "values": map[string]string{}, "is_set": map[string]bool{}, }) } } // sendModernZUIConfig writes the ui-config payload for a ModernZ install. func (s *Server) sendModernZUIConfig(w http.ResponseWriter) { confPath, err := modernzconf.GetModernZConfPath() if err != nil { log.Error(constants.LogPrefixAPI + "Failed to determine modernz.conf path: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, "Failed to determine modernz.conf path: "+err.Error()) return } conf, err := modernzconf.ParseModernZConf(confPath) if err != nil { log.Error(constants.LogPrefixAPI + "Failed to parse modernz.conf: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, "Failed to parse modernz.conf: "+err.Error()) return } options := modernzconf.KnownOptions() values := make(map[string]string, len(options)) isSet := make(map[string]bool, len(options)) for _, opt := range options { values[opt.Key] = conf.GetValue(opt.Key) isSet[opt.Key] = conf.IsSet(opt.Key) } w.Header().Set("Content-Type", constants.ContentTypeJSON) payload := map[string]interface{}{ "ui": constants.UITypeModernZ, "options": options, "values": values, "is_set": isSet, "path": confPath, } addUIConfigStatus(payload) json.NewEncoder(w).Encode(payload) } // sendUOSCUIConfig writes the ui-config payload for a uOSC install. func (s *Server) sendUOSCUIConfig(w http.ResponseWriter) { confPath, err := uoscconf.GetUOSCConfPath() if err != nil { log.Error(constants.LogPrefixAPI + "Failed to determine uosc.conf path: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, "Failed to determine uosc.conf path: "+err.Error()) return } conf, err := uoscconf.ParseUOSCConf(confPath) if err != nil { log.Error(constants.LogPrefixAPI + "Failed to parse uosc.conf: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, "Failed to parse uosc.conf: "+err.Error()) return } options := uoscconf.KnownOptions() values := make(map[string]string, len(options)) isSet := make(map[string]bool, len(options)) for _, opt := range options { values[opt.Key] = conf.GetValue(opt.Key) isSet[opt.Key] = conf.IsSet(opt.Key) } w.Header().Set("Content-Type", constants.ContentTypeJSON) payload := map[string]interface{}{ "ui": constants.UITypeUOSC, "options": options, "values": values, "is_set": isSet, "path": confPath, } addUIConfigStatus(payload) json.NewEncoder(w).Encode(payload) } func addUIConfigStatus(payload map[string]interface{}) { status, err := uiconfig.Inspect() if err != nil { log.Warn(constants.LogPrefixAPI + "Failed to inspect UI config compatibility: " + err.Error()) payload["warnings"] = []uiconfig.Warning{} payload["migration"] = nil return } payload["warnings"] = status.Warnings payload["migration"] = status.Migration } // handleUIConfigMigrationAPI returns or resolves persistent UI configuration // migrations. GET is used by both the settings page and the global notice; // POST accepts {id, action} where action is apply or keep. func (s *Server) handleUIConfigMigrationAPI(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: status, err := uiconfig.Inspect() if err != nil { sendJSONError(w, http.StatusInternalServerError, "Failed to inspect UI configuration: "+err.Error()) return } w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(status) case http.MethodPost: var req struct { ID string `json:"id"` Action string `json:"action"` ExpectedValue string `json:"expected_value"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { sendJSONError(w, http.StatusBadRequest, "Invalid request body") return } if err := uiconfig.ResolveMigration(req.ID, req.Action, req.ExpectedValue); err != nil { sendJSONError(w, http.StatusBadRequest, err.Error()) return } status, err := uiconfig.Inspect() if err != nil { sendJSONError(w, http.StatusInternalServerError, "Migration saved but status refresh failed: "+err.Error()) return } w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, "status": status, }) default: sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed") } } // handleUIConfigSetAPI validates and sets a single option value for the // installed mpv UI (ModernZ or uOSC). // POST /api/ui-config/set func (s *Server) handleUIConfigSetAPI(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed") return } var req struct { Key string `json:"key"` Value string `json:"value"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { sendJSONError(w, http.StatusBadRequest, "Invalid request body") return } switch ui := detectInstalledUI(); ui { case constants.UITypeModernZ: s.setModernZUIConfigValue(w, req.Key, req.Value) case constants.UITypeUOSC: s.setUOSCUIConfigValue(w, req.Key, req.Value) case constants.UITypeAmbiguous: sendJSONError(w, http.StatusConflict, "Both ModernZ and uOSC are installed; choose one UI before editing its settings") default: sendJSONError(w, http.StatusBadRequest, "No supported mpv UI (ModernZ or uOSC) is installed") } } // setModernZUIConfigValue validates, sets, and persists a ModernZ option. func (s *Server) setModernZUIConfigValue(w http.ResponseWriter, key, value string) { if err := modernzconf.ValidateValue(key, value); err != nil { sendJSONError(w, http.StatusBadRequest, err.Error()) return } confPath, err := modernzconf.GetModernZConfPath() if err != nil { log.Error(constants.LogPrefixAPI + "Failed to determine modernz.conf path: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, "Failed to determine modernz.conf path: "+err.Error()) return } if err := modernzconf.SetValueFile(confPath, key, value); err != nil { log.Error(constants.LogPrefixAPI + "Failed to write modernz.conf: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, "Failed to write modernz.conf: "+err.Error()) return } log.Info(constants.LogPrefixAPI + "Set ModernZ option: " + key) w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, }) } // setUOSCUIConfigValue validates, sets, and persists a uOSC option. func (s *Server) setUOSCUIConfigValue(w http.ResponseWriter, key, value string) { if err := uoscconf.ValidateValue(key, value); err != nil { sendJSONError(w, http.StatusBadRequest, err.Error()) return } confPath, err := uoscconf.GetUOSCConfPath() if err != nil { log.Error(constants.LogPrefixAPI + "Failed to determine uosc.conf path: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, "Failed to determine uosc.conf path: "+err.Error()) return } if err := uoscconf.SetValueFile(confPath, key, value); err != nil { log.Error(constants.LogPrefixAPI + "Failed to write uosc.conf: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, "Failed to write uosc.conf: "+err.Error()) return } log.Info(constants.LogPrefixAPI + "Set uOSC option: " + key) w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, }) } // handleUIConfigResetAPI resets a single option to its default value for the // installed mpv UI (ModernZ or uOSC). // POST /api/ui-config/reset func (s *Server) handleUIConfigResetAPI(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { sendJSONError(w, http.StatusMethodNotAllowed, "Method not allowed") return } var req struct { Key string `json:"key"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { sendJSONError(w, http.StatusBadRequest, "Invalid request body") return } switch ui := detectInstalledUI(); ui { case constants.UITypeModernZ: s.resetModernZUIConfigValue(w, req.Key) case constants.UITypeUOSC: s.resetUOSCUIConfigValue(w, req.Key) case constants.UITypeAmbiguous: sendJSONError(w, http.StatusConflict, "Both ModernZ and uOSC are installed; choose one UI before editing its settings") default: sendJSONError(w, http.StatusBadRequest, "No supported mpv UI (ModernZ or uOSC) is installed") } } // resetModernZUIConfigValue resets a ModernZ option to its default. func (s *Server) resetModernZUIConfigValue(w http.ResponseWriter, key string) { if !isKnownModernZOption(key) { sendJSONError(w, http.StatusBadRequest, "Unknown ModernZ option: "+key) return } confPath, err := modernzconf.GetModernZConfPath() if err != nil { log.Error(constants.LogPrefixAPI + "Failed to determine modernz.conf path: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, "Failed to determine modernz.conf path: "+err.Error()) return } if err := modernzconf.ResetValueFile(confPath, key); err != nil { log.Error(constants.LogPrefixAPI + "Failed to write modernz.conf: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, "Failed to write modernz.conf: "+err.Error()) return } log.Info(constants.LogPrefixAPI + "Reset ModernZ option: " + key) w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, }) } // resetUOSCUIConfigValue resets a uOSC option to its default. func (s *Server) resetUOSCUIConfigValue(w http.ResponseWriter, key string) { if !isKnownUOSCOption(key) { sendJSONError(w, http.StatusBadRequest, "Unknown uOSC option: "+key) return } confPath, err := uoscconf.GetUOSCConfPath() if err != nil { log.Error(constants.LogPrefixAPI + "Failed to determine uosc.conf path: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, "Failed to determine uosc.conf path: "+err.Error()) return } if err := uoscconf.ResetValueFile(confPath, key); err != nil { log.Error(constants.LogPrefixAPI + "Failed to write uosc.conf: " + err.Error()) sendJSONError(w, http.StatusInternalServerError, "Failed to write uosc.conf: "+err.Error()) return } log.Info(constants.LogPrefixAPI + "Reset uOSC option: " + key) w.Header().Set("Content-Type", constants.ContentTypeJSON) json.NewEncoder(w).Encode(map[string]interface{}{ "success": true, }) } // isKnownUOSCOption reports whether key is a known uOSC option. func isKnownUOSCOption(key string) bool { for _, opt := range uoscconf.KnownOptions() { if opt.Key == key { return true } } return false }