package controller import ( "errors" "io" "net/http" "strconv" "github.com/mhsanaei/3x-ui/v3/internal/web/entity" "github.com/mhsanaei/3x-ui/v3/internal/web/service" "github.com/gin-gonic/gin" ) // Item 7 B5: theme store administration — list/install (chunked // upload or GitHub address)/update/delete/switch, the public-surface // settings block, and site icons. Endpoints ride the panel- // authenticated API group and answer in the panel's Msg envelope; // operator-facing refusals from the service layer (a bad package, a // bad repository address) are shown verbatim, machine faults are // logged and answered generically. The public contract surface in // probe_public.go is separate and stays envelope-free. type ProbeThemesController struct{} func NewProbeThemesController(g *gin.RouterGroup) *ProbeThemesController { pc := &ProbeThemesController{} g.GET("/probe/themes", pc.list) g.GET("/probe/themes/:short/preview", pc.preview) g.POST("/probe/theme-upload", pc.upload) g.POST("/probe/theme-install", pc.install) g.POST("/probe/themes/:short/update", pc.update) g.POST("/probe/themes/:short/select", pc.selectTheme) g.DELETE("/probe/themes/:short", pc.remove) g.GET("/probe/settings", pc.getSettings) g.PUT("/probe/settings", pc.putSettings) return pc } // themeFail answers a store operation: a refusal is the operator's // answer, anything else is a machine fault (jsonMsgObj logs it). func themeFail(c *gin.Context, what string, err error) { var refusal *service.ProbeThemeRefusal if errors.As(err, &refusal) { c.JSON(http.StatusOK, entity.Msg{Success: false, Msg: refusal.Msg}) return } jsonMsg(c, what, err) } type probeThemesPayload struct { Themes []service.ProbeTheme `json:"themes"` Settings service.ProbePublicSettings `json:"settings"` Favicon string `json:"favicon"` TouchIcon string `json:"touchIcon"` } func (pc *ProbeThemesController) list(c *gin.Context) { themes, err := service.ProbeThemes() if err != nil { jsonMsg(c, "load themes failed", err) return } jsonObj(c, probeThemesPayload{ Themes: themes, Settings: service.GetProbePublicSettings(), Favicon: service.ProbeIconSetting("favicon"), TouchIcon: service.ProbeIconSetting("touch_icon"), }, nil) } func (pc *ProbeThemesController) preview(c *gin.Context) { data := service.ProbeThemePreview(c.Param("short")) if data == nil { c.Status(http.StatusNotFound) return } // Not cached: reinstalling a theme under the same name also // replaces the image (hub theme_preview). c.Header("Cache-Control", "no-cache") c.Data(http.StatusOK, "image/png", data) } // upload receives one chunk of a theme.tar.gz (hub Chunk protocol: // ?offset=&total=, the raw piece as the body). A non-final piece // answers the length received so far; the final piece installs and // answers the installed theme. func (pc *ProbeThemesController) upload(c *gin.Context) { offset, oerr := strconv.ParseInt(c.Query("offset"), 10, 64) total, terr := strconv.ParseInt(c.Query("total"), 10, 64) if oerr != nil || terr != nil || offset < 0 || total < 0 { c.JSON(http.StatusOK, entity.Msg{Success: false, Msg: "分片参数不对"}) return } body := io.LimitReader(c.Request.Body, service.MaxThemeArchive+1) received, err := service.ReceiveProbeThemeChunk(offset, total, body) if err != nil { themeFail(c, "theme upload failed", err) return } if received < total { jsonObj(c, gin.H{"received": received}, nil) return } theme, err := service.FinishProbeThemeUpload() if err != nil { themeFail(c, "theme install failed", err) return } jsonObj(c, gin.H{"theme": theme}, nil) } type probeInstallForm struct { URL string `json:"url"` } func (pc *ProbeThemesController) install(c *gin.Context) { var form probeInstallForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, entity.Msg{Success: false, Msg: "请求格式不对"}) return } theme, err := service.InstallProbeThemeFromRepo(form.URL) if err != nil { themeFail(c, "theme install failed", err) return } jsonObj(c, gin.H{"theme": theme}, nil) } func (pc *ProbeThemesController) update(c *gin.Context) { updated, version, err := service.ProbeThemeUpdate(c.Param("short")) if err != nil { themeFail(c, "theme update failed", err) return } jsonObj(c, gin.H{"updated": updated, "version": version}, nil) } func (pc *ProbeThemesController) selectTheme(c *gin.Context) { if err := service.SetProbeCurrentTheme(c.Param("short")); err != nil { themeFail(c, "theme switch failed", err) return } jsonMsg(c, "", nil) } func (pc *ProbeThemesController) remove(c *gin.Context) { if err := service.RemoveProbeTheme(c.Param("short")); err != nil { themeFail(c, "theme delete failed", err) return } jsonMsg(c, "", nil) } type probeSettingsForm struct { service.ProbePublicSettings Favicon string `json:"favicon"` TouchIcon string `json:"touchIcon"` } func (pc *ProbeThemesController) getSettings(c *gin.Context) { jsonObj(c, probeSettingsForm{ ProbePublicSettings: service.GetProbePublicSettings(), Favicon: service.ProbeIconSetting("favicon"), TouchIcon: service.ProbeIconSetting("touch_icon"), }, nil) } func (pc *ProbeThemesController) putSettings(c *gin.Context) { var form probeSettingsForm if err := c.ShouldBindJSON(&form); err != nil { c.JSON(http.StatusOK, entity.Msg{Success: false, Msg: "请求格式不对"}) return } if err := service.SetProbePublicSettings(form.ProbePublicSettings); err != nil { themeFail(c, "save probe settings failed", err) return } if err := service.SetProbeIconSetting("favicon", form.Favicon); err != nil { themeFail(c, "save probe settings failed", err) return } if err := service.SetProbeIconSetting("touch_icon", form.TouchIcon); err != nil { themeFail(c, "save probe settings failed", err) return } jsonMsg(c, "", nil) }