From e02cacdf2a76c261afd0aa7686a26d25450694df Mon Sep 17 00:00:00 2001 From: yusing Date: Sun, 20 Jul 2025 12:59:25 +0800 Subject: [PATCH] feat(middleware): add `themed` middleware with customizable themes and styles - Introduced a new themed middleware that allows for dynamic theme application. - Added support for multiple themes: dark, dark-grey, solarized-dark, and custom CSS. - Included CSS files for each theme and a font CSS template for font customization. - Updated middleware registry to include the new themed middleware. --- internal/net/gphttp/middleware/middlewares.go | 1 + internal/net/gphttp/middleware/themed.go | 113 +++++++++++++ .../gphttp/middleware/themes/dark-grey.css | 116 ++++++++++++++ .../net/gphttp/middleware/themes/dark.css | 116 ++++++++++++++ .../net/gphttp/middleware/themes/font.css | 4 + .../middleware/themes/solarized-dark.css | 149 ++++++++++++++++++ 6 files changed, 499 insertions(+) create mode 100644 internal/net/gphttp/middleware/themed.go create mode 100644 internal/net/gphttp/middleware/themes/dark-grey.css create mode 100644 internal/net/gphttp/middleware/themes/dark.css create mode 100644 internal/net/gphttp/middleware/themes/font.css create mode 100644 internal/net/gphttp/middleware/themes/solarized-dark.css diff --git a/internal/net/gphttp/middleware/middlewares.go b/internal/net/gphttp/middleware/middlewares.go index 0e98bc4..b9295f3 100644 --- a/internal/net/gphttp/middleware/middlewares.go +++ b/internal/net/gphttp/middleware/middlewares.go @@ -25,6 +25,7 @@ var allMiddlewares = map[string]*Middleware{ "hidexforwarded": HideXForwarded, "modifyhtml": ModifyHTML, + "themed": Themed, "errorpage": CustomErrorPage, "customerrorpage": CustomErrorPage, diff --git a/internal/net/gphttp/middleware/themed.go b/internal/net/gphttp/middleware/themed.go new file mode 100644 index 0000000..175c851 --- /dev/null +++ b/internal/net/gphttp/middleware/themed.go @@ -0,0 +1,113 @@ +package middleware + +import ( + "bytes" + "fmt" + "net/http" + "os" + "strings" + "text/template" + + _ "embed" + + "github.com/yusing/go-proxy/internal/gperr" +) + +type themed struct { + FontURL string `json:"font_url"` + FontFamily string `json:"font_family"` + Theme Theme `json:"theme"` // predefined themes + CSS string `json:"css"` // css url or content + + m modifyHTML +} + +var Themed = NewMiddleware[themed]() + +type Theme string + +const ( + DarkTheme Theme = "dark" + DarkGreyTheme Theme = "dark-grey" + SolarizedDarkTheme Theme = "solarized-dark" +) + +var ( + //go:embed themes/dark.css + darkModeCSS string + //go:embed themes/dark-grey.css + darkGreyModeCSS string + //go:embed themes/solarized-dark.css + solarizedDarkModeCSS string + //go:embed themes/font.css + fontCSS string +) + +var fontCSSTemplate = template.Must(template.New("fontCSS").Parse(fontCSS)) + +const overAllocate = 256 + +func (m *themed) before(w http.ResponseWriter, req *http.Request) bool { + return m.m.before(w, req) +} + +func (m *themed) modifyResponse(resp *http.Response) error { + return m.m.modifyResponse(resp) +} + +func (m *themed) finalize() error { + m.m.Target = "body" + if m.FontURL != "" && m.FontFamily != "" { + buf := bytes.NewBuffer(bytePool.GetSized(len(fontCSS) + overAllocate)) + buf.WriteString(`") + m.m.HTML += buf.String() + } + if m.CSS != "" && m.Theme != "" { + return gperr.New("css and theme are mutually exclusive") + } + // credit: https://hackcss.egoist.dev + if m.Theme != "" { + switch m.Theme { + case DarkTheme: + m.m.HTML += wrapStyleTag(darkModeCSS) + case DarkGreyTheme: + m.m.HTML += wrapStyleTag(darkGreyModeCSS) + case SolarizedDarkTheme: + m.m.HTML += wrapStyleTag(solarizedDarkModeCSS) + default: + return gperr.New("invalid theme").Subject(string(m.Theme)) + } + } + if m.CSS != "" { + switch { + case strings.HasPrefix(m.CSS, "https://"), + strings.HasPrefix(m.CSS, "http://"), + strings.HasPrefix(m.CSS, "/"): + m.m.HTML += wrapStylesheetLinkTag(m.CSS) + case strings.HasPrefix(m.CSS, "file://"): + css, err := os.ReadFile(strings.TrimPrefix(m.CSS, "file://")) + if err != nil { + return err + } + m.m.HTML += wrapStyleTag(string(css)) + default: + // inline css + m.m.HTML += wrapStyleTag(m.CSS) + } + } + return nil +} + +func wrapStyleTag(css string) string { + return fmt.Sprintf(``, css) +} + +func wrapStylesheetLinkTag(href string) string { + return fmt.Sprintf(``, href) +} diff --git a/internal/net/gphttp/middleware/themes/dark-grey.css b/internal/net/gphttp/middleware/themes/dark-grey.css new file mode 100644 index 0000000..74a8df4 --- /dev/null +++ b/internal/net/gphttp/middleware/themes/dark-grey.css @@ -0,0 +1,116 @@ +:not(img, svg) { + background-color: #181818 !important; + color: #ccc !important; +} +pre { + background-color: #181818 !important; + padding: 0 !important; + border: none !important; +} +pre code { + color: #00bcd4 !important; +} +h1 a, +h2 a, +h3 a, +h4 a, +h5 a { + color: #ccc !important; +} +code, +strong { + color: #fff !important; +} +code { + font-weight: 100; +} +table { + color: #ccc !important; +} +table td, +table th { + border-color: #444 !important; +} +table tbody td:first-child { + color: #fff !important; +} +.form-group label { + color: #ccc !important; + border-color: rgba(95, 95, 95, 0.78) !important; +} +.form-group.form-textarea label:after { + background-color: #181818 !important; +} +.form-control { + color: #ccc !important; + border-color: rgba(95, 95, 95, 0.78) !important; +} +.form-control:focus { + border-color: #ccc !important; + color: #ccc !important; +} +textarea.form-control { + color: #ccc !important; +} +.card { + border-color: rgba(95, 95, 95, 0.78) !important; +} +.card .card-header { + background-color: transparent !important; + color: #ccc !important; + border-bottom: 1px solid rgba(95, 95, 95, 0.78) !important; +} +.btn.btn-ghost.btn-default { + border-color: #ababab !important; + color: #ababab !important; +} +.btn.btn-ghost.btn-default:focus, +.btn.btn-ghost.btn-default:hover { + border-color: #9c9c9c !important; + color: #9c9c9c !important; + z-index: 1; +} +.btn.btn-ghost.btn-default:focus, +.btn.btn-ghost.btn-default:hover { + border-color: #e0e0e0 !important; + color: #e0e0e0 !important; +} +.btn.btn-ghost.btn-primary:focus, +.btn.btn-ghost.btn-primary:hover { + border-color: #64b5f6 !important; + color: #64b5f6 !important; +} +.btn.btn-ghost.btn-success:focus, +.btn.btn-ghost.btn-success:hover { + border-color: #81c784 !important; + color: #81c784 !important; +} +.btn.btn-ghost.btn-info:focus, +.btn.btn-ghost.btn-info:hover { + border-color: #4dd0e1 !important; + color: #4dd0e1 !important; +} +.btn.btn-ghost.btn-error:focus, +.btn.btn-ghost.btn-error:hover { + border-color: #e57373 !important; + color: #e57373 !important; +} +.btn.btn-ghost.btn-warning:focus, +.btn.btn-ghost.btn-warning:hover { + border-color: #ffb74d !important; + color: #ffb74d !important; +} +.avatarholder, +.placeholder { + background-color: transparent !important; + border-color: #333 !important; +} +.menu .menu-item { + color: #ccc !important; + border-color: rgba(95, 95, 95, 0.78) !important; +} +.menu .menu-item.active, +.menu .menu-item:hover { + color: #fff !important; + border-color: #ccc !important; +} diff --git a/internal/net/gphttp/middleware/themes/dark.css b/internal/net/gphttp/middleware/themes/dark.css new file mode 100644 index 0000000..789350d --- /dev/null +++ b/internal/net/gphttp/middleware/themes/dark.css @@ -0,0 +1,116 @@ +:not(img, svg) { + background-color: #000 !important; + color: #ccc !important; +} +pre { + background-color: #000 !important; + padding: 0; + border: none; +} +pre code { + color: #00bcd4 !important; +} +h1 a, +h2 a, +h3 a, +h4 a, +h5 a { + color: #ccc !important; +} +code, +strong { + color: #fff !important; +} +code { + font-weight: 100; +} +table { + color: #ccc !important; +} +table td, +table th { + border-color: #444 !important; +} +table tbody td:first-child { + color: #fff !important; +} +.form-group label { + color: #ccc !important; + border-color: rgba(95, 95, 95, 0.78) !important; +} +.form-group.form-textarea label:after { + background-color: #000 !important; +} +.form-control { + color: #ccc !important; + border-color: rgba(95, 95, 95, 0.78) !important; +} +.form-control:focus { + border-color: #ccc !important; + color: #ccc !important; +} +textarea.form-control { + color: #ccc !important; +} +.card { + border-color: rgba(95, 95, 95, 0.78) !important; +} +.card .card-header { + background-color: transparent !important; + color: #ccc !important; + border-bottom: 1px solid rgba(95, 95, 95, 0.78) !important; +} +.btn.btn-ghost.btn-default { + border-color: #ababab !important; + color: #ababab !important; +} +.btn.btn-ghost.btn-default:focus, +.btn.btn-ghost.btn-default:hover { + border-color: #9c9c9c !important; + color: #9c9c9c !important; + z-index: 1; +} +.btn.btn-ghost.btn-default:focus, +.btn.btn-ghost.btn-default:hover { + border-color: #e0e0e0 !important; + color: #e0e0e0 !important; +} +.btn.btn-ghost.btn-primary:focus, +.btn.btn-ghost.btn-primary:hover { + border-color: #64b5f6 !important; + color: #64b5f6 !important; +} +.btn.btn-ghost.btn-success:focus, +.btn.btn-ghost.btn-success:hover { + border-color: #81c784 !important; + color: #81c784 !important; +} +.btn.btn-ghost.btn-info:focus, +.btn.btn-ghost.btn-info:hover { + border-color: #4dd0e1 !important; + color: #4dd0e1 !important; +} +.btn.btn-ghost.btn-error:focus, +.btn.btn-ghost.btn-error:hover { + border-color: #e57373 !important; + color: #e57373 !important; +} +.btn.btn-ghost.btn-warning:focus, +.btn.btn-ghost.btn-warning:hover { + border-color: #ffb74d !important; + color: #ffb74d !important; +} +.avatarholder, +.placeholder { + background-color: transparent !important; + border-color: #333 !important; +} +.menu .menu-item { + color: #ccc !important; + border-color: rgba(95, 95, 95, 0.78) !important; +} +.menu .menu-item.active, +.menu .menu-item:hover { + color: #fff !important; + border-color: #ccc !important; +} diff --git a/internal/net/gphttp/middleware/themes/font.css b/internal/net/gphttp/middleware/themes/font.css new file mode 100644 index 0000000..31006b9 --- /dev/null +++ b/internal/net/gphttp/middleware/themes/font.css @@ -0,0 +1,4 @@ +@import url("{{.FontURL}}"); +body { + font-family: "{{.FontFamily}}", "arial", "roboto" !important; +} diff --git a/internal/net/gphttp/middleware/themes/solarized-dark.css b/internal/net/gphttp/middleware/themes/solarized-dark.css new file mode 100644 index 0000000..904d236 --- /dev/null +++ b/internal/net/gphttp/middleware/themes/solarized-dark.css @@ -0,0 +1,149 @@ +:not(img, svg) { + background-color: #073642 !important; + color: #78909c !important; +} +pre { + background-color: #073642 !important; + padding: 0; + border: none; +} +pre code { + color: #009688 !important; +} +h1, +h2, +h3, +h4, +h5, +h6 { + color: #1e88e5 !important; +} +h1 a, +h2 a, +h3 a, +h4 a, +h5 a, +h6 a { + color: #1e88e5 !important; + border-bottom-color: #1e88e5 !important; +} +h1 a:hover, +h2 a:hover, +h3 a:hover, +h4 a:hover, +h5 a:hover, +h6 a:hover { + background-color: #1e88e5 !important; + color: #fff !important; +} +h1 a, +h2 a, +h3 a, +h4 a, +h5 a { + color: #78909c !important; +} +code, +strong { + color: #90a4ae !important; +} +code { + font-weight: 100; +} +.progress-bar-filled { + background-color: #558b2f !important; +} +.progress-bar-filled:after, +.progress-bar-filled:before { + color: #90a4ae !important; +} +table { + color: #78909c !important; +} +table td, +table th { + border-color: #b0bec5 !important; +} +table tbody td:first-child { + color: #b0bec5 !important; +} +.form-group label { + color: #78909c !important; + border-color: #90a4ae !important; +} +.form-group.form-textarea label:after { + background-color: #073642 !important; +} +.form-control { + color: #78909c !important; + border-color: #90a4ae !important; +} +.form-control:focus { + border-color: #cfd8dc !important; + color: #cfd8dc !important; +} +textarea.form-control { + color: #78909c !important; +} +.card { + border-color: #90a4ae !important; +} +.card .card-header { + background-color: transparent !important; + color: #78909c !important; + border-bottom: 1px solid #90a4ae !important; +} +.btn.btn-ghost.btn-default { + border-color: #607d8b !important; + color: #607d8b !important; +} +.btn.btn-ghost.btn-default:focus, +.btn.btn-ghost.btn-default:hover { + border-color: #90a4ae !important; + color: #90a4ae !important; + z-index: 1; +} +.btn.btn-ghost.btn-default:focus, +.btn.btn-ghost.btn-default:hover { + border-color: #e0e0e0 !important; + color: #e0e0e0 !important; +} +.btn.btn-ghost.btn-primary:focus, +.btn.btn-ghost.btn-primary:hover { + border-color: #64b5f6 !important; + color: #64b5f6 !important; +} +.btn.btn-ghost.btn-success:focus, +.btn.btn-ghost.btn-success:hover { + border-color: #81c784 !important; + color: #81c784 !important; +} +.btn.btn-ghost.btn-info:focus, +.btn.btn-ghost.btn-info:hover { + border-color: #4dd0e1 !important; + color: #4dd0e1 !important; +} +.btn.btn-ghost.btn-error:focus, +.btn.btn-ghost.btn-error:hover { + border-color: #e57373 !important; + color: #e57373 !important; +} +.btn.btn-ghost.btn-warning:focus, +.btn.btn-ghost.btn-warning:hover { + border-color: #ffb74d !important; + color: #ffb74d !important; +} +.avatarholder, +.placeholder { + background-color: transparent !important; + border-color: #90a4ae !important; +} +.menu .menu-item { + color: #78909c !important; + border-color: #90a4ae !important; +} +.menu .menu-item.active, +.menu .menu-item:hover { + color: #fff !important; + border-color: #78909c !important; +}