Graphical overhaul.
Remove all legacy pixeldrain pages, handlers and assets. Remove old theming engine. Make logo responsive to the accent colour. Lots of other stuff.
This commit is contained in:
@@ -1,68 +1,63 @@
|
||||
package webcontroller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"fornaxian.tech/log"
|
||||
"fornaxian.tech/nova_server/api"
|
||||
"fornaxian.tech/pixeldrain_api_client/pixelapi"
|
||||
"fornaxian.tech/util"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func (wc *WebController) serveDirectory(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
var err error
|
||||
var td = wc.newTemplateData(r)
|
||||
var path = strings.TrimPrefix(p.ByName("path"), "/")
|
||||
var pdapi = wc.api.RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent())
|
||||
|
||||
// Prevent search engines from indexing this page for privacy reasons
|
||||
w.Header().Set("X-Robots-Tag", "noindex, nofollow")
|
||||
|
||||
if path == "" {
|
||||
wc.templates.Run(w, r, "404", td)
|
||||
return
|
||||
}
|
||||
|
||||
var pdapi = wc.api.RealIP(util.RemoteAddress(r)).RealAgent(r.UserAgent())
|
||||
|
||||
// The user and the node are embedded in the page so the JS doesn't have to
|
||||
// request them again. When a request fails the JS requests it again and
|
||||
// shows the error
|
||||
if apikey, err := wc.getAPIKey(r); err == nil {
|
||||
pdapi = pdapi.Login(apikey)
|
||||
_ = pdapi.GetJSON("user", &td.User)
|
||||
} else {
|
||||
td.User = json.RawMessage("null")
|
||||
}
|
||||
|
||||
node, err := pdapi.GetFilesystemPath(path)
|
||||
if err != nil {
|
||||
if apiErr, ok := errors.AsType[pixelapi.Error](err); ok {
|
||||
// Set the proper response code for the error message
|
||||
switch apiErr.StatusCode {
|
||||
case "not_found", "path_not_found":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case "forbidden", "permission_denied":
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case "unavailable_for_legal_reasons":
|
||||
w.WriteHeader(http.StatusUnavailableForLegalReasons)
|
||||
case "authentication_required":
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
// Let the JS figure it out
|
||||
if err = wc.templates.Run(w, r, "wrap", td); err != nil {
|
||||
log.Error("Failed to run template: %s", err)
|
||||
}
|
||||
} else {
|
||||
log.Error("Failed to get path: %s", err)
|
||||
wc.templates.Run(w, r, "500", td)
|
||||
var path = strings.TrimPrefix(p.ByName("path"), "/")
|
||||
var err = pdapi.GetJSON("filesystem/"+url.PathEscape(path)+"?stat", &td.Node)
|
||||
if apiErr, ok := errors.AsType[pixelapi.Error](err); ok {
|
||||
// Set the proper response code for the error message
|
||||
switch apiErr.StatusCode {
|
||||
case "not_found", "path_not_found":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
case "forbidden", "permission_denied":
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
case "unavailable_for_legal_reasons":
|
||||
w.WriteHeader(http.StatusUnavailableForLegalReasons)
|
||||
case "authentication_required":
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error("Failed to get path: %s", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
} else {
|
||||
var node api.FilesystemPath
|
||||
if err = json.Unmarshal(td.Node, &node); err != nil {
|
||||
panic(fmt.Errorf("decode filesystem path: %w", err))
|
||||
}
|
||||
td.Title = fmt.Sprintf("%s ~ Nova", node.Path[node.BaseIndex].Name)
|
||||
td.OGData = wc.metadataFromFilesystem(r, node)
|
||||
}
|
||||
|
||||
td.Title = fmt.Sprintf("%s ~ Nova", node.Path[node.BaseIndex].Name)
|
||||
td.Other = node
|
||||
td.OGData = wc.metadataFromFilesystem(r, node)
|
||||
err = wc.templates.Run(w, r, "wrap", td)
|
||||
if err != nil && !util.IsNetError(err) {
|
||||
log.Error("Error executing template filesystem: %s", err)
|
||||
}
|
||||
wc.renderPage(w, r, td)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"fornaxian.tech/pixeldrain_api_client/pixelapi"
|
||||
"fornaxian.tech/nova_server/api"
|
||||
)
|
||||
|
||||
const defaultThemeColour = "#220735"
|
||||
const defaultThemeColour = "#49dfa0"
|
||||
const defaultHost = "https://nova.storage"
|
||||
const siteDescription = "Cost-effective cloud storage and content delivery"
|
||||
|
||||
type ogData struct {
|
||||
MetaPropRules []ogProp
|
||||
@@ -78,6 +79,22 @@ func generateOGData(name, filetype, pageurl, fileurl, thumbnailurl, themecolour
|
||||
return og
|
||||
}
|
||||
|
||||
// siteOGData describes the site itself, for link previews of pages which are
|
||||
// not a shared file
|
||||
func siteOGData(r *http.Request) (og ogData) {
|
||||
var addr = getRequestAddress(r)
|
||||
og.addProp("og:type", "website")
|
||||
og.addProp("og:site_name", "Nova.storage")
|
||||
og.addProp("og:title", "Nova.storage")
|
||||
og.addProp("og:description", siteDescription)
|
||||
og.addProp("og:url", addr+r.URL.Path)
|
||||
og.addProp("og:image", addr+"/res/img/nova_512.png")
|
||||
og.addName("description", siteDescription)
|
||||
og.addName("theme-color", defaultThemeColour)
|
||||
og.addName("twitter:card", "summary")
|
||||
return og
|
||||
}
|
||||
|
||||
func getRequestAddress(r *http.Request) (addr string) {
|
||||
if r.Host == "" {
|
||||
return defaultHost
|
||||
@@ -87,7 +104,7 @@ func getRequestAddress(r *http.Request) (addr string) {
|
||||
return "https://" + r.Host
|
||||
}
|
||||
}
|
||||
func (wc *WebController) metadataFromFilesystem(r *http.Request, f pixelapi.FilesystemPath) (og ogData) {
|
||||
func (wc *WebController) metadataFromFilesystem(r *http.Request, f api.FilesystemPath) (og ogData) {
|
||||
var addr = getRequestAddress(r)
|
||||
var base = &f.Path[f.BaseIndex]
|
||||
|
||||
@@ -102,8 +119,8 @@ func (wc *WebController) metadataFromFilesystem(r *http.Request, f pixelapi.File
|
||||
var colour = defaultThemeColour
|
||||
for _, node := range f.Path {
|
||||
if node.Properties["branding_enabled"] == "true" &&
|
||||
node.Properties["brand_highlight_color"] != "" {
|
||||
colour = node.Properties["brand_highlight_color"]
|
||||
node.Properties["brand_accent_color"] != "" {
|
||||
colour = node.Properties["brand_accent_color"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,733 +0,0 @@
|
||||
package webcontroller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
func (wc *WebController) themeHandler(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
w.Header().Set("Content-Type", "text/css")
|
||||
w.Write([]byte(userStyleFromRequest(r)))
|
||||
}
|
||||
|
||||
func userStyleFromRequest(r *http.Request) (s template.CSS) {
|
||||
// Get the chosen style from the URL
|
||||
var style = r.URL.Query().Get("style")
|
||||
var hue = -1
|
||||
|
||||
if hueStr := r.URL.Query().Get("hue"); hueStr != "" {
|
||||
hue, _ = strconv.Atoi(hueStr)
|
||||
}
|
||||
|
||||
// If the URL style was empty use the cookie value
|
||||
if style == "" {
|
||||
if cookie, err := r.Cookie("style"); err == nil {
|
||||
style = cookie.Value
|
||||
}
|
||||
}
|
||||
if hue == -1 {
|
||||
if cookie, err := r.Cookie("hue"); err == nil {
|
||||
hue, _ = strconv.Atoi(cookie.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return userStyle(style, hue)
|
||||
}
|
||||
|
||||
func userStyle(style string, hue int) template.CSS {
|
||||
var (
|
||||
def styleSheet
|
||||
light styleSheet
|
||||
hasLight bool
|
||||
)
|
||||
|
||||
switch style {
|
||||
default:
|
||||
fallthrough
|
||||
case "nova":
|
||||
def = novaStyle
|
||||
case "adwaita":
|
||||
def = adwaitaDarkStyle
|
||||
light = adwaitaLightStyle
|
||||
hasLight = true
|
||||
case "nord":
|
||||
def = nordDarkStyle
|
||||
light = nordLightStyle
|
||||
hasLight = true
|
||||
case "nord_dark":
|
||||
def = nordDarkStyle
|
||||
case "nord_light", "snowstorm":
|
||||
def = nordLightStyle
|
||||
case "solarized":
|
||||
def = solarizedDarkStyle
|
||||
light = solarizedLightStyle
|
||||
hasLight = true
|
||||
case "solarized_dark":
|
||||
def = solarizedDarkStyle
|
||||
case "solarized_light":
|
||||
def = solarizedLightStyle
|
||||
case "classic":
|
||||
def = classicStyle
|
||||
hue = -1 // Does not support custom hues
|
||||
case "purple_drain":
|
||||
def = purpleDrainStyle
|
||||
hue = -1 // Does not support custom hues
|
||||
case "maroon":
|
||||
def = maroonStyle
|
||||
case "hacker":
|
||||
def = hackerStyle
|
||||
hue = -1 // Does not support custom hues
|
||||
case "canta":
|
||||
def = cantaNovaStyle
|
||||
case "skeuos":
|
||||
def = skeuosNovaStyle
|
||||
case "sweet":
|
||||
def = sweetNovaStyle
|
||||
case "adwaita_dark":
|
||||
def = adwaitaDarkStyle
|
||||
case "adwaita_light":
|
||||
def = adwaitaLightStyle
|
||||
case "nova98":
|
||||
def = nova98Style
|
||||
case "nova98_dark":
|
||||
def = nova98StyleDark
|
||||
}
|
||||
|
||||
if hue >= 0 && hue <= 360 {
|
||||
def = def.withHue(hue)
|
||||
light = light.withHue(hue)
|
||||
}
|
||||
|
||||
if hasLight {
|
||||
return template.CSS(def.withLight(light))
|
||||
} else {
|
||||
return template.CSS(def.String())
|
||||
}
|
||||
}
|
||||
|
||||
type styleSheet struct {
|
||||
Link HSL // Based on Highlight if undefined
|
||||
Input CSS
|
||||
InputHover CSS
|
||||
InputText CSS
|
||||
InputDisabledText CSS
|
||||
HighlightBackground CSS
|
||||
Highlight HSL // Links, highlighted buttons, list navigation
|
||||
HighlightText HSL // Text on buttons
|
||||
Danger HSL
|
||||
ScrollbarForeground CSS // Based on Input if undefined
|
||||
ScrollbarHover CSS // Based on ScrollbarForeground if undefined
|
||||
|
||||
BackgroundColor HSL
|
||||
Background CSS
|
||||
BackgroundText HSL
|
||||
BackgroundPattern CSS
|
||||
Navigation CSS
|
||||
BodyColor HSL
|
||||
BodyBackground CSS
|
||||
BodyText HSL
|
||||
Separator HSL
|
||||
CardColor HSL
|
||||
CardText HSL
|
||||
|
||||
// Colors to use in graphs
|
||||
Chart1 HSL
|
||||
Chart2 HSL
|
||||
Chart3 HSL
|
||||
|
||||
StyleOverrides string
|
||||
}
|
||||
|
||||
func (s styleSheet) withDefaults() styleSheet {
|
||||
// Set default colors
|
||||
var noColor = HSL{0, 0, 0}
|
||||
var defaultHSL = func(target *HSL, def HSL) {
|
||||
if *target == noColor {
|
||||
*target = def
|
||||
}
|
||||
}
|
||||
var defaultCSS = func(target *CSS, def CSS) {
|
||||
if *target == nil {
|
||||
*target = def
|
||||
}
|
||||
}
|
||||
defaultHSL(&s.Link, s.Highlight.Add(0, 0, -.05))
|
||||
defaultCSS(&s.ScrollbarForeground, s.Input)
|
||||
defaultCSS(&s.ScrollbarHover, s.InputHover)
|
||||
defaultHSL(&s.Chart1, s.Highlight)
|
||||
defaultHSL(&s.Chart2, s.Chart1.Add(120, 0, 0))
|
||||
defaultHSL(&s.Chart3, s.Chart2.Add(120, 0, 0))
|
||||
defaultCSS(&s.HighlightBackground, s.Highlight)
|
||||
defaultCSS(&s.Background, s.BackgroundColor)
|
||||
defaultCSS(&s.BackgroundPattern, s.BackgroundColor)
|
||||
defaultCSS(&s.Navigation, RawCSS("none"))
|
||||
defaultCSS(&s.BodyBackground, s.BodyColor)
|
||||
defaultHSL(&s.BackgroundText, s.BodyText)
|
||||
defaultHSL(&s.Separator, s.BodyColor.Add(0, 0, .06))
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s styleSheet) withHue(hue int) styleSheet {
|
||||
s = s.withDefaults()
|
||||
|
||||
var setHue = func(c CSS) CSS {
|
||||
if hsl, ok := c.(HSL); ok {
|
||||
hsl.Hue = hue
|
||||
return hsl
|
||||
} else {
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
s.Input = setHue(s.Input)
|
||||
s.InputHover = setHue(s.InputHover)
|
||||
s.InputText = setHue(s.InputText)
|
||||
s.InputDisabledText = setHue(s.InputDisabledText)
|
||||
s.ScrollbarForeground = setHue(s.ScrollbarForeground)
|
||||
s.ScrollbarHover = setHue(s.ScrollbarHover)
|
||||
s.BackgroundColor.Hue = hue
|
||||
s.Background = setHue(s.Background)
|
||||
s.BackgroundText.Hue = hue
|
||||
s.BackgroundPattern = setHue(s.BackgroundPattern)
|
||||
s.Navigation = setHue(s.Navigation)
|
||||
s.BodyColor.Hue = hue
|
||||
s.BodyBackground = setHue(s.BodyBackground)
|
||||
s.BodyText.Hue = hue
|
||||
s.Separator.Hue = hue
|
||||
s.CardColor.Hue = hue
|
||||
s.CardText.Hue = hue
|
||||
return s
|
||||
}
|
||||
|
||||
func (s styleSheet) String() string {
|
||||
s = s.withDefaults()
|
||||
|
||||
return fmt.Sprintf(
|
||||
`:root {
|
||||
--link_color: %s;
|
||||
--input_background: %s;
|
||||
--input_hover_background: %s;
|
||||
--input_text: %s;
|
||||
--input_disabled_text: %s;
|
||||
--highlight_background: %s;
|
||||
--highlight_color: %s;
|
||||
--highlight_text_color: %s;
|
||||
--danger_color: %s;
|
||||
--danger_text_color: %s;
|
||||
--scrollbar_foreground_color: %s;
|
||||
--scrollbar_hover_color: %s;
|
||||
|
||||
--background_color: %s;
|
||||
--background: %s;
|
||||
--background_text_color: %s;
|
||||
--background_pattern_color: %s;
|
||||
--navigation_background: %s;
|
||||
--body_color: %s;
|
||||
--body_background: %s;
|
||||
--body_text_color: %s;
|
||||
--separator: %s;
|
||||
--shaded_background: %s;
|
||||
--card_color: %s;
|
||||
|
||||
--chart_1_color: %s;
|
||||
--chart_2_color: %s;
|
||||
--chart_3_color: %s;
|
||||
|
||||
--shadow_color: %s;
|
||||
}
|
||||
|
||||
%s
|
||||
`,
|
||||
s.Link.CSS(),
|
||||
s.Input.CSS(),
|
||||
s.InputHover.CSS(),
|
||||
s.InputText.CSS(),
|
||||
s.InputDisabledText.CSS(),
|
||||
s.HighlightBackground.CSS(),
|
||||
s.Highlight.CSS(),
|
||||
s.HighlightText.CSS(),
|
||||
s.Danger.CSS(),
|
||||
s.HighlightText.CSS(),
|
||||
s.ScrollbarForeground.CSS(),
|
||||
s.ScrollbarHover.CSS(),
|
||||
s.BackgroundColor.CSS(),
|
||||
s.Background.CSS(),
|
||||
s.BackgroundText.CSS(),
|
||||
s.BackgroundPattern.CSS(),
|
||||
s.Navigation.CSS(),
|
||||
s.BodyColor.CSS(),
|
||||
s.BodyBackground.CSS(),
|
||||
s.BodyText.CSS(),
|
||||
s.Separator.CSS(),
|
||||
s.BodyColor.WithAlpha(0.9).CSS(), // shaded_background
|
||||
s.CardColor.CSS(),
|
||||
s.Chart1.CSS(),
|
||||
s.Chart2.CSS(),
|
||||
s.Chart3.CSS(),
|
||||
s.BodyColor.Darken(0.8).CSS(),
|
||||
s.StyleOverrides,
|
||||
)
|
||||
}
|
||||
|
||||
func (dark styleSheet) withLight(light styleSheet) string {
|
||||
return fmt.Sprintf(
|
||||
`%s
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
%s
|
||||
}`,
|
||||
dark.String(),
|
||||
light.String(),
|
||||
)
|
||||
}
|
||||
|
||||
// Following are all the available styles
|
||||
|
||||
var purpleDrainStyle = styleSheet{
|
||||
Input: RGBA{77, 19, 236, .3},
|
||||
InputHover: RGBA{77, 19, 236, .4},
|
||||
InputText: HSL{0, 0, .9},
|
||||
InputDisabledText: HSL{266, .85, .4},
|
||||
HighlightBackground: NewGradient(160, HSL{150, .84, .39}, HSL{85, .85, .35}),
|
||||
Highlight: HSL{117, .63, .46},
|
||||
HighlightText: HSL{0, 0, 0},
|
||||
Danger: HSL{357, .63, .46},
|
||||
ScrollbarForeground: HSL{266, .85, .40},
|
||||
ScrollbarHover: HSL{266, .85, .50},
|
||||
|
||||
BackgroundColor: HSL{270, .84, .12},
|
||||
Background: NewGradient(120, HSL{255, .84, .16}, HSL{300, .85, .14}),
|
||||
BackgroundPattern: RawCSS("none"),
|
||||
Navigation: RawCSS("none"),
|
||||
BodyColor: HSL{274, .9, .14},
|
||||
BodyBackground: NewGradient(120, HSL{255, .84, .18}, HSL{300, .85, .16}),
|
||||
BodyText: HSL{0, 0, .8},
|
||||
CardColor: HSL{275, .8, .18},
|
||||
}
|
||||
|
||||
var classicStyle = styleSheet{
|
||||
Input: HSL{0, 0, .18},
|
||||
InputHover: HSL{0, 0, .22},
|
||||
InputText: HSL{0, 0, .9},
|
||||
InputDisabledText: HSL{0, 0, .4},
|
||||
Highlight: HSL{89, .60, .45},
|
||||
HighlightText: HSL{0, 0, 0},
|
||||
Danger: HSL{339, .65, .31},
|
||||
|
||||
BackgroundColor: HSL{0, 0, .08},
|
||||
BodyColor: HSL{0, 0, .12},
|
||||
BodyText: HSL{0, 0, .8},
|
||||
CardColor: HSL{0, 0, .16},
|
||||
}
|
||||
|
||||
var novaStyle = styleSheet{
|
||||
Input: HSL{94, 1, .1},
|
||||
InputHover: HSL{94, 1, .15},
|
||||
InputText: HSL{0, 0, .9},
|
||||
InputDisabledText: HSL{0, 0, .4},
|
||||
Highlight: HSL{94, .70, .45},
|
||||
HighlightText: HSL{0, 0, 0},
|
||||
Danger: HSL{339, .65, .31},
|
||||
|
||||
BackgroundColor: HSL{94, 1, .02},
|
||||
BodyColor: HSL{94, 1, .04},
|
||||
BodyText: HSL{94, 1, .9},
|
||||
CardColor: HSL{94, 1, .06},
|
||||
}
|
||||
|
||||
var maroonStyle = styleSheet{
|
||||
Input: HSL{0, .8, .20}, // hsl(0, 87%, 40%)
|
||||
InputHover: HSL{0, .8, .25},
|
||||
InputText: HSL{0, 0, 1},
|
||||
InputDisabledText: HSL{0, 0, .5},
|
||||
Highlight: HSL{137, 1, .37}, //hsl(137, 100%, 37%)
|
||||
HighlightText: HSL{0, 0, 0},
|
||||
Danger: HSL{9, .96, .42}, //hsl(9, 96%, 42%)
|
||||
|
||||
BackgroundColor: HSL{0, .7, .05},
|
||||
BodyColor: HSL{0, .8, .08}, // HSL{0, .8, .15},
|
||||
BodyText: HSL{0, 0, .8},
|
||||
CardColor: HSL{0, .9, .14},
|
||||
}
|
||||
|
||||
var hackerStyle = styleSheet{
|
||||
Input: HSL{0, 0, .1},
|
||||
InputHover: HSL{0, 0, .15},
|
||||
InputText: HSL{0, 0, 1},
|
||||
InputDisabledText: HSL{0, 0, .5},
|
||||
Highlight: HSL{120, .8, .5},
|
||||
HighlightText: HSL{0, 0, 0},
|
||||
Danger: HSL{0, 1, .4},
|
||||
|
||||
BackgroundColor: HSL{0, 0, 0},
|
||||
BodyColor: HSL{0, 0, .03},
|
||||
BodyText: HSL{0, 0, .8},
|
||||
CardColor: HSL{120, .4, .05},
|
||||
}
|
||||
|
||||
var cantaNovaStyle = styleSheet{
|
||||
Input: HSL{167, .06, .30}, // hsl(167, 6%, 30%)
|
||||
InputHover: HSL{167, .06, .35}, // hsl(167, 6%, 30%)
|
||||
InputText: HSL{0, 0, 1},
|
||||
InputDisabledText: HSL{0, 0, .5},
|
||||
Highlight: HSL{165, 1, .40}, // hsl(165, 100%, 40%)
|
||||
HighlightText: HSL{0, 0, 0},
|
||||
Danger: HSL{40, 1, .5}, // hsl(40, 100%, 50%)
|
||||
|
||||
BackgroundColor: HSL{180, .04, .16},
|
||||
BodyColor: HSL{168, .05, .21},
|
||||
BodyText: HSL{0, 0, .8},
|
||||
CardColor: HSL{170, .05, .26},
|
||||
}
|
||||
|
||||
var skeuosNovaStyle = styleSheet{
|
||||
Input: HSL{226, .15, .23}, //hsl(226, 15%, 23%)
|
||||
InputHover: HSL{226, .15, .28},
|
||||
InputText: HSL{60, .06, .93},
|
||||
InputDisabledText: HSL{0, 0, .5},
|
||||
Highlight: HSL{282, .65, .54}, // hsl(282, 65%, 54%)
|
||||
HighlightText: HSL{0, 0, 1},
|
||||
Danger: HSL{0, .79, .43}, // hsl(0, 79%, 43%)
|
||||
|
||||
BackgroundColor: HSL{232, .14, .11}, //hsl(232, 14%, 11%)
|
||||
BodyColor: HSL{229, .14, .16}, // hsl(229, 14%, 16%)
|
||||
BodyText: HSL{60, .06, .93}, // hsl(60, 6%, 93%)
|
||||
CardColor: HSL{225, .14, .17}, // hsl(225, 14%, 17%)
|
||||
}
|
||||
|
||||
var (
|
||||
nord0 = HSL{220, .16, .22} // Dark
|
||||
nord1 = HSL{222, .16, .28} // Dark
|
||||
nord2 = HSL{220, .17, .32} // Dark
|
||||
nord3 = HSL{220, .16, .36} // Light
|
||||
nord4 = HSL{219, .28, .88} // Light
|
||||
nord5 = HSL{218, .27, .92} // Light
|
||||
nord6 = HSL{218, .27, .94} // Light
|
||||
nord7 = HSL{179, .25, .65} // Teal
|
||||
nord8 = HSL{193, .43, .67} // Light blue
|
||||
nord11 = HSL{354, .42, .56} // Red
|
||||
nord14 = HSL{92, .28, .65} // Green
|
||||
)
|
||||
|
||||
var nordDarkStyle = styleSheet{
|
||||
Input: nord3.Add(0, 0, 0.01),
|
||||
InputHover: nord3.Add(0, 0, 0.03),
|
||||
InputText: nord5,
|
||||
InputDisabledText: nord0,
|
||||
Highlight: nord14,
|
||||
HighlightText: nord0,
|
||||
Danger: nord11,
|
||||
ScrollbarForeground: nord7,
|
||||
ScrollbarHover: nord8,
|
||||
|
||||
BackgroundColor: nord0,
|
||||
BodyColor: nord1,
|
||||
BodyText: nord4,
|
||||
CardColor: nord2,
|
||||
}
|
||||
|
||||
var nordLightStyle = styleSheet{
|
||||
Link: HSL{92, .40, .32},
|
||||
Input: nord4.Add(0, 0, -0.04),
|
||||
InputHover: nord4.Add(0, 0, -0.06),
|
||||
InputText: nord0,
|
||||
InputDisabledText: nord6,
|
||||
Highlight: nord14,
|
||||
HighlightText: nord1,
|
||||
Danger: nord11,
|
||||
ScrollbarForeground: nord7,
|
||||
ScrollbarHover: nord8,
|
||||
|
||||
BackgroundColor: nord4,
|
||||
BackgroundText: nord0,
|
||||
BodyColor: nord6,
|
||||
BodyText: nord2,
|
||||
Separator: nord4,
|
||||
BackgroundPattern: nord4,
|
||||
CardColor: nord5,
|
||||
}
|
||||
|
||||
var sweetNovaStyle = styleSheet{
|
||||
Input: HSL{229, .25, .18}, // hsl(229, 25%, 14%)
|
||||
InputHover: HSL{229, .25, .23}, // hsl(229, 25%, 14%)
|
||||
InputText: HSL{223, .13, .79},
|
||||
InputDisabledText: HSL{0, 0, .5},
|
||||
Highlight: HSL{296, .88, .44},
|
||||
HighlightText: HSL{0, 0, 0},
|
||||
Danger: HSL{356, 1, .64}, // hsl(356, 100%, 64%)
|
||||
|
||||
BackgroundColor: HSL{225, .25, .06}, // hsl(225, 25%, 6%)
|
||||
BodyColor: HSL{228, .25, .12}, // hsl(228, 25%, 12%)
|
||||
BodyText: HSL{223, .13, .79}, // hsl(223, 13%, 79%)
|
||||
CardColor: HSL{229, .25, .14}, // hsl(229, 25%, 14%)
|
||||
}
|
||||
|
||||
var adwaitaDarkStyle = styleSheet{
|
||||
Input: RGBA{255, 255, 255, .06},
|
||||
InputHover: RGBA{255, 255, 255, .11},
|
||||
InputText: HSL{0, 0, 1},
|
||||
InputDisabledText: HSL{0, 0, .5},
|
||||
Highlight: HSL{152, .62, .39}, // hsl(152, 62%, 39%)
|
||||
HighlightText: HSL{0, 0, 0},
|
||||
Danger: HSL{9, 1, .69}, // hsl(9, 100%, 69%)
|
||||
|
||||
BackgroundColor: HSL{0, 0, .12},
|
||||
BodyColor: HSL{0, 0, .14},
|
||||
BodyText: HSL{0, 0, 1},
|
||||
CardColor: HSL{0, 0, .21},
|
||||
}
|
||||
|
||||
var adwaitaLightStyle = styleSheet{
|
||||
Input: RGBA{0, 0, 0, .06},
|
||||
InputHover: RGBA{0, 0, 0, .11},
|
||||
InputText: HSL{0, 0, .2},
|
||||
InputDisabledText: HSL{0, 0, .7},
|
||||
Highlight: HSL{152, .62, .47}, // hsl(152, 62%, 47%)
|
||||
HighlightText: HSL{0, 0, 1},
|
||||
Danger: HSL{356, .75, .43}, // hsl(356, 75%, 43%)
|
||||
|
||||
BackgroundColor: HSL{0, 0, .92},
|
||||
BodyColor: HSL{0, 0, .98},
|
||||
BodyText: HSL{0, 0, .2},
|
||||
Separator: HSL{0, 0, .86},
|
||||
CardColor: HSL{0, 0, 1},
|
||||
}
|
||||
|
||||
var solarizedDarkStyle = styleSheet{
|
||||
Input: HSL{192, .81, .18}, // hsl(194, 14%, 40%)
|
||||
InputHover: HSL{192, .81, .23}, // hsl(196, 13%, 45%)
|
||||
InputText: HSL{180, .07, .80}, // hsl(44, 87%, 94%)
|
||||
InputDisabledText: HSL{194, .14, .30}, // hsl(194, 14%, 40%)
|
||||
Highlight: HSL{68, 1, .30}, // hsl(68, 100%, 30%)
|
||||
HighlightText: HSL{192, .81, .14}, // hsl(192, 100%, 11%)
|
||||
Danger: HSL{1, .71, .52}, // hsl(1, 71%, 52%)
|
||||
|
||||
BackgroundColor: HSL{192, 1, .11}, //hsl(192, 100%, 11%)
|
||||
BodyColor: HSL{192, .81, .14}, // hsl(192, 81%, 14%)
|
||||
BodyText: HSL{180, .07, .60}, // hsl(180, 7%, 60%)
|
||||
CardColor: HSL{192, .81, .16},
|
||||
}
|
||||
|
||||
var solarizedLightStyle = styleSheet{
|
||||
Input: HSL{46, .42, .84},
|
||||
InputHover: HSL{46, .42, .80},
|
||||
InputText: HSL{194, .14, .20}, // hsl(192, 81%, 14%)
|
||||
InputDisabledText: HSL{44, .87, .94},
|
||||
Highlight: HSL{68, 1, .30}, // hsl(68, 100%, 30%)
|
||||
HighlightText: HSL{44, .87, .94},
|
||||
Danger: HSL{1, .71, .52}, // hsl(1, 71%, 52%)
|
||||
|
||||
BackgroundColor: HSL{46, .42, .88}, // hsl(46, 42%, 88%)
|
||||
BackgroundText: HSL{192, .81, .14},
|
||||
BodyColor: HSL{44, .87, .94}, // hsl(44, 87%, 94%)
|
||||
BodyText: HSL{194, .14, .40}, // hsl(194, 14%, 40%)
|
||||
Separator: HSL{46, .42, .88},
|
||||
CardColor: HSL{44, .87, .92},
|
||||
}
|
||||
|
||||
const override98 = `
|
||||
button, .button,
|
||||
dialog,
|
||||
.highlight_border,
|
||||
.page_content,
|
||||
footer, .footer_content,
|
||||
.card,
|
||||
.window,
|
||||
.block,
|
||||
.sharebar,
|
||||
.directory_sorters, .directory_area,
|
||||
.container > .directory,
|
||||
.file_preview > .directory,
|
||||
.directory > .node,
|
||||
.file, .file_button,
|
||||
.upload_task,
|
||||
.add_button,
|
||||
.expandable,
|
||||
.bubble,
|
||||
.upload_widget {
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: inset -1px -1px #0a0a0a,
|
||||
inset 1px 1px #dfdfdf,
|
||||
inset -2px -2px grey,
|
||||
inset 2px 2px #fff !important;
|
||||
}
|
||||
.window, .page_content, .file_button, .expandable, pre {
|
||||
padding: 3px !important;
|
||||
}
|
||||
.window > .header,
|
||||
.headerbar,
|
||||
.upload_widget > .header {
|
||||
background: linear-gradient(90deg,navy,#1084d0) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.tab_bar > button, .tab_bar > .button {
|
||||
border-top-left-radius: 3px !important;
|
||||
border-top-right-radius: 3px !important;
|
||||
box-shadow: inset -1px 0 #0a0a0a,inset 1px 1px #dfdfdf,inset -2px 0 grey,inset 2px 2px #fff !important;
|
||||
}
|
||||
.directory > tr {
|
||||
border: none !important;
|
||||
}
|
||||
.button_toggle_navigation {
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="password"],
|
||||
input[type="text"],
|
||||
input[type="date"],
|
||||
select, textarea, pre {
|
||||
border-radius: 0 !important;
|
||||
border: none !important;
|
||||
background-color: #fff !important;
|
||||
box-shadow: inset -1px -1px #fff,
|
||||
inset 1px 1px grey,
|
||||
inset -2px -2px #dfdfdf,
|
||||
inset 2px 2px #0a0a0a !important;
|
||||
}
|
||||
code {
|
||||
border-radius: 0 !important;
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
hr,
|
||||
.toolbar > .separator,
|
||||
.toolbar > .grid > .separator,
|
||||
.highlight_shaded {
|
||||
border-radius: 0 !important;
|
||||
border: none !important;
|
||||
box-shadow: inset -1px -1px #fff,
|
||||
inset 1px 1px grey,
|
||||
inset -2px -2px #dfdfdf,
|
||||
inset 2px 2px #0a0a0a !important;
|
||||
}
|
||||
`
|
||||
|
||||
var nova98Style = styleSheet{
|
||||
Input: RGB{190, 190, 190},
|
||||
InputHover: RGB{220, 220, 220},
|
||||
InputText: RGB{0, 0, 0},
|
||||
InputDisabledText: RGB{128, 128, 128},
|
||||
Highlight: HSL{240, 1, .5}, // hsl(68, 100%, 30%)
|
||||
HighlightText: HSL{0, 1, 1},
|
||||
Danger: HSL{1, .71, .52}, // hsl(1, 71%, 52%)
|
||||
|
||||
BackgroundColor: HSL{180, 1, .25},
|
||||
BackgroundText: HSL{0, 1, 1},
|
||||
BodyColor: HSL{0, 0, .75},
|
||||
BodyText: HSL{0, 0, 0},
|
||||
Separator: HSL{0, 1, 1},
|
||||
CardColor: HSL{0, 0, .75},
|
||||
|
||||
StyleOverrides: override98,
|
||||
}
|
||||
|
||||
var nova98StyleDark = styleSheet{
|
||||
Input: HSL{0, 0, .25},
|
||||
InputHover: HSL{0, 0, .30},
|
||||
InputText: RGB{255, 255, 255},
|
||||
InputDisabledText: RGB{128, 128, 128},
|
||||
Highlight: HSL{240, 1, .7},
|
||||
HighlightText: HSL{0, 1, 1},
|
||||
Danger: HSL{1, .71, .7},
|
||||
Link: HSL{240, 1, .8},
|
||||
|
||||
BackgroundColor: HSL{180, 1, .08},
|
||||
BackgroundText: HSL{0, 1, 1},
|
||||
BodyColor: HSL{0, 0, .25},
|
||||
BodyText: HSL{0, 1, 1},
|
||||
Separator: HSL{0, 0, .33},
|
||||
CardColor: HSL{0, 0, .25},
|
||||
|
||||
StyleOverrides: `
|
||||
:root {
|
||||
--border_98: inset -1px -1px #222,
|
||||
inset 1px 1px #666,
|
||||
inset -2px -2px #333,
|
||||
inset 2px 2px #555;
|
||||
--inset_98: inset -1px -1px #666,
|
||||
inset 1px 1px #111,
|
||||
inset -2px -2px #555,
|
||||
inset 2px 2px #333;
|
||||
}
|
||||
|
||||
button, .button,
|
||||
dialog,
|
||||
fieldset,
|
||||
.highlight_border,
|
||||
.page_content,
|
||||
footer, .footer_content,
|
||||
.card,
|
||||
.window,
|
||||
.block,
|
||||
.sharebar,
|
||||
.directory_sorters, .directory_area,
|
||||
.container > .directory,
|
||||
.file_preview > .directory,
|
||||
.directory > .node,
|
||||
.file, .file_button,
|
||||
.upload_task,
|
||||
.add_button,
|
||||
.expandable,
|
||||
.bubble,
|
||||
.upload_widget,
|
||||
.highlight_shaded {
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: var(--border_98) !important;
|
||||
}
|
||||
legend {
|
||||
background-color: var(--body_color);
|
||||
border: none !important;
|
||||
}
|
||||
.window, .page_content, .file_button, .expandable, pre {
|
||||
padding: 3px !important;
|
||||
}
|
||||
.window > .header,
|
||||
.headerbar,
|
||||
.upload_widget > .header {
|
||||
background: linear-gradient(90deg, #003d66, #005c99) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.tab_bar > button, .tab_bar > .button {
|
||||
border-top-left-radius: 3px !important;
|
||||
border-top-right-radius: 3px !important;
|
||||
box-shadow: var(--border_98) !important;
|
||||
}
|
||||
.directory > tr {
|
||||
border: none !important;
|
||||
}
|
||||
.button_toggle_navigation {
|
||||
padding: 8px !important;
|
||||
}
|
||||
|
||||
input[type="email"],
|
||||
input[type="number"],
|
||||
input[type="password"],
|
||||
input[type="text"],
|
||||
input[type="date"],
|
||||
select, textarea, pre {
|
||||
border-radius: 0 !important;
|
||||
border: none !important;
|
||||
box-shadow: var(--inset_98) !important;
|
||||
}
|
||||
code {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
hr,
|
||||
.toolbar > .separator,
|
||||
.toolbar > .grid > .separator {
|
||||
border-radius: 0 !important;
|
||||
border: none !important;
|
||||
box-shadow: var(--inset_98) !important;
|
||||
}
|
||||
`,
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
package webcontroller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type Color interface {
|
||||
CSS
|
||||
HSL() HSL
|
||||
RGB() RGB
|
||||
}
|
||||
|
||||
type CSS interface {
|
||||
CSS() string
|
||||
}
|
||||
|
||||
// Raw CSS
|
||||
type RawCSS string
|
||||
|
||||
func (c RawCSS) CSS() string { return string(c) }
|
||||
|
||||
// HSL color
|
||||
type HSL struct {
|
||||
Hue int
|
||||
Saturation float64
|
||||
Lightness float64
|
||||
}
|
||||
|
||||
var _ Color = HSL{} // Confirm interface compliance
|
||||
|
||||
func (hsl HSL) CSS() string { return hsl.RGB().CSS() }
|
||||
func (hsl HSL) HSL() HSL { return hsl }
|
||||
func (hsl HSL) RGB() RGB {
|
||||
var r, g, b, q, p float64
|
||||
var h, s, l = float64(hsl.Hue) / 360, hsl.Saturation, hsl.Lightness
|
||||
|
||||
if s == 0 {
|
||||
r, g, b = l, l, l
|
||||
} else {
|
||||
var hue2rgb = func(p, q, t float64) float64 {
|
||||
if t < 0 {
|
||||
t++
|
||||
}
|
||||
if t > 1 {
|
||||
t--
|
||||
}
|
||||
|
||||
if t < 1.0/6.0 {
|
||||
return p + (q-p)*6*t
|
||||
} else if t < 1.0/2.0 {
|
||||
return q
|
||||
} else if t < 2.0/3.0 {
|
||||
return p + (q-p)*(2.0/3.0-t)*6
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
if l < 0.5 {
|
||||
q = l * (1 + s)
|
||||
} else {
|
||||
q = l + s - l*s
|
||||
}
|
||||
|
||||
p = 2*l - q
|
||||
r = hue2rgb(p, q, h+1.0/3.0)
|
||||
g = hue2rgb(p, q, h)
|
||||
b = hue2rgb(p, q, h-1.0/3.0)
|
||||
}
|
||||
return RGB{R: uint8(r * 255), G: uint8(g * 255), B: uint8(b * 255)}
|
||||
}
|
||||
|
||||
// Add returns a NEW HSL struct, it doesn't modify the current one
|
||||
func (hsl HSL) Add(hue int, saturation float64, lightness float64) HSL {
|
||||
var new = HSL{
|
||||
hsl.Hue + hue,
|
||||
hsl.Saturation + saturation,
|
||||
hsl.Lightness + lightness,
|
||||
}
|
||||
// Hue bounds correction
|
||||
if new.Hue < 0 {
|
||||
new.Hue += 360
|
||||
} else if new.Hue > 360 {
|
||||
new.Hue -= 360
|
||||
}
|
||||
// Saturation bounds check
|
||||
if new.Saturation < 0 {
|
||||
new.Saturation = 0
|
||||
} else if new.Saturation > 1 {
|
||||
new.Saturation = 1
|
||||
}
|
||||
// Lightness bounds check
|
||||
if new.Lightness < 0 {
|
||||
new.Lightness = 0
|
||||
} else if new.Lightness > 1 {
|
||||
new.Lightness = 1
|
||||
}
|
||||
|
||||
return new
|
||||
}
|
||||
|
||||
func (hsl HSL) Darken(percent float64) HSL {
|
||||
hsl.Lightness = hsl.Lightness * percent
|
||||
hsl.Saturation = hsl.Saturation * percent
|
||||
return hsl
|
||||
}
|
||||
|
||||
func (hsl HSL) WithAlpha(alpha float64) HSLA {
|
||||
return HSLA{hsl.Hue, hsl.Saturation, hsl.Lightness, alpha}
|
||||
}
|
||||
|
||||
type HSLA struct {
|
||||
Hue int
|
||||
Saturation float64
|
||||
Lightness float64
|
||||
Alpha float64
|
||||
}
|
||||
|
||||
var _ Color = HSLA{}
|
||||
|
||||
func (hsla HSLA) CSS() string {
|
||||
return fmt.Sprintf(
|
||||
"hsla(%d, %.2f%%, %.2f%%, %.2f)",
|
||||
hsla.Hue, hsla.Saturation*100, hsla.Lightness*100, hsla.Alpha,
|
||||
)
|
||||
}
|
||||
func (hsla HSLA) HSL() HSL { return HSL{hsla.Hue, hsla.Saturation, hsla.Lightness} }
|
||||
func (hsla HSLA) RGB() RGB { return hsla.HSL().RGB() }
|
||||
|
||||
type RGB struct {
|
||||
R uint8
|
||||
G uint8
|
||||
B uint8
|
||||
}
|
||||
|
||||
var _ Color = RGB{}
|
||||
|
||||
func (rgb RGB) CSS() string { return fmt.Sprintf("#%02x%02x%02x", rgb.R, rgb.G, rgb.B) }
|
||||
func (rgb RGB) HSL() HSL {
|
||||
var r, g, b = float64(rgb.R), float64(rgb.G), float64(rgb.B)
|
||||
var h, s, l float64
|
||||
|
||||
max := math.Max(math.Max(r, g), b)
|
||||
min := math.Min(math.Min(r, g), b)
|
||||
|
||||
// Luminosity is the average of the max and min rgb color intensities.
|
||||
l = (max + min) / 2
|
||||
|
||||
// saturation
|
||||
delta := max - min
|
||||
if delta == 0 {
|
||||
// it's gray
|
||||
return HSL{0, 0, l}
|
||||
}
|
||||
|
||||
// it's not gray
|
||||
if l < 0.5 {
|
||||
s = delta / (max + min)
|
||||
} else {
|
||||
s = delta / (2 - max - min)
|
||||
}
|
||||
|
||||
// hue
|
||||
r2 := (((max - r) / 6) + (delta / 2)) / delta
|
||||
g2 := (((max - g) / 6) + (delta / 2)) / delta
|
||||
b2 := (((max - b) / 6) + (delta / 2)) / delta
|
||||
switch {
|
||||
case r == max:
|
||||
h = b2 - g2
|
||||
case g == max:
|
||||
h = (1.0 / 3.0) + r2 - b2
|
||||
case b == max:
|
||||
h = (2.0 / 3.0) + g2 - r2
|
||||
}
|
||||
|
||||
if h < 0 {
|
||||
h += 1
|
||||
} else if h > 1 {
|
||||
h -= 1
|
||||
}
|
||||
|
||||
return HSL{int(h), s, l}
|
||||
}
|
||||
func (rgb RGB) RGB() RGB { return rgb }
|
||||
|
||||
type RGBA struct {
|
||||
R uint8
|
||||
G uint8
|
||||
B uint8
|
||||
A float64
|
||||
}
|
||||
|
||||
var _ Color = RGBA{}
|
||||
|
||||
func (rgba RGBA) CSS() string {
|
||||
return fmt.Sprintf("rgba(%d, %d, %d, %f)", rgba.R, rgba.G, rgba.B, rgba.A)
|
||||
}
|
||||
|
||||
func (rgba RGBA) HSL() HSL { return rgba.RGB().HSL() }
|
||||
func (rgba RGBA) RGB() RGB { return RGB{rgba.R, rgba.G, rgba.B} }
|
||||
|
||||
type Gradient struct {
|
||||
Angle int
|
||||
Colors []Color
|
||||
}
|
||||
|
||||
var _ CSS = Gradient{}
|
||||
|
||||
func NewGradient(angle int, colors ...Color) Gradient {
|
||||
return Gradient{angle, colors}
|
||||
}
|
||||
|
||||
func (g Gradient) CSS() string {
|
||||
var colors string
|
||||
for i, color := range g.Colors {
|
||||
if i != 0 {
|
||||
colors += ", "
|
||||
}
|
||||
colors += color.CSS()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("linear-gradient(%ddeg, %s)", g.Angle, colors)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package webcontroller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"fornaxian.tech/log"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
)
|
||||
|
||||
// New initializes a new WebController by registering all the request handlers
|
||||
// and parsing all templates in the resource directory
|
||||
func serveSK(r *httprouter.Router) (err error) {
|
||||
// r.ServeFiles("/_app/*filepath", http.Dir(assetsPath+"/_app"))
|
||||
// r.ServeFiles("/style/*filepath", http.Dir(assetsPath+"/style"))
|
||||
|
||||
readdir, err := os.ReadDir(assetsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range readdir {
|
||||
if entry.Type().IsRegular() {
|
||||
serveFile(r, "/"+entry.Name(), assetsPath+"/"+entry.Name())
|
||||
} else if entry.IsDir() {
|
||||
r.ServeFiles("/"+entry.Name()+"/*filepath", http.Dir(assetsPath+"/"+entry.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debug("Running fallback handler for %s", r.URL.Path)
|
||||
http.ServeFile(w, r, assetsPath+"/fallback.html")
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
const assetsPath = "/home/wim/Workspace/svelte/fnx_sk/build"
|
||||
|
||||
func serveFile(r *httprouter.Router, path, file string) {
|
||||
var handler = func(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
p httprouter.Params,
|
||||
) {
|
||||
http.ServeFile(w, r, assetsPath+"/"+file)
|
||||
}
|
||||
|
||||
r.GET(path, handler)
|
||||
r.HEAD(path, handler)
|
||||
r.OPTIONS(path, handler)
|
||||
r.POST(path, handler)
|
||||
r.PUT(path, handler)
|
||||
r.PATCH(path, handler)
|
||||
r.DELETE(path, handler)
|
||||
}
|
||||
@@ -1,314 +1,61 @@
|
||||
package webcontroller
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fornaxian.tech/log"
|
||||
"fornaxian.tech/util"
|
||||
)
|
||||
|
||||
// TemplateData is a struct that every template expects when being rendered. In
|
||||
// the field Other you can pass your own template-specific variables.
|
||||
// TemplateData is the data for the page template, res/template/wrap.html
|
||||
type TemplateData struct {
|
||||
tpm *TemplateManager
|
||||
APIEndpoint template.URL
|
||||
Hostname template.HTML
|
||||
CacheID int64
|
||||
|
||||
// Only used on file viewer page
|
||||
// Describe the site by default, or the file on a filesystem page
|
||||
Title string
|
||||
OGData ogData
|
||||
|
||||
Other any
|
||||
URLQuery url.Values
|
||||
// Embedded in the page as window.user and window.initial_node, so the JS
|
||||
// doesn't have to request them. Left empty when they were not requested
|
||||
User json.RawMessage
|
||||
Node json.RawMessage
|
||||
}
|
||||
|
||||
func (wc *WebController) newTemplateData(r *http.Request) (t *TemplateData) {
|
||||
t = &TemplateData{
|
||||
tpm: wc.templates,
|
||||
APIEndpoint: template.URL(wc.config.APIURLExternal),
|
||||
Hostname: template.HTML(wc.hostname),
|
||||
URLQuery: r.URL.Query(),
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// TemplateManager parses templates and provides utility functions to the
|
||||
// templates' scripting language
|
||||
type TemplateManager struct {
|
||||
tpl *template.Template
|
||||
|
||||
// Config
|
||||
resourceDir string
|
||||
externalAPIEndpoint string
|
||||
debugModeEnabled bool
|
||||
}
|
||||
|
||||
// NewTemplateManager creates a new template manager
|
||||
func NewTemplateManager(resourceDir, externalAPIEndpoint string, debugMode bool) *TemplateManager {
|
||||
return &TemplateManager{
|
||||
resourceDir: resourceDir,
|
||||
externalAPIEndpoint: externalAPIEndpoint,
|
||||
debugModeEnabled: debugMode,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseTemplates parses the templates in the template directory which is
|
||||
// defined in the config file.
|
||||
// If silent is false it will print an info log message for every template found
|
||||
func (tm *TemplateManager) ParseTemplates(silent bool) {
|
||||
var err error
|
||||
var templatePaths []string
|
||||
tpl := template.New("")
|
||||
|
||||
// Import template functions
|
||||
tpl.Funcs(template.FuncMap{
|
||||
"cacheID": tm.cacheID,
|
||||
"debugMode": tm.debugMode,
|
||||
"apiUrl": tm.apiURL,
|
||||
"pageNr": tm.pageNr,
|
||||
"add": tm.add,
|
||||
"sub": tm.sub,
|
||||
"mul": tm.mul,
|
||||
"div": tm.div,
|
||||
"formatData": tm.formatData,
|
||||
"formatDataBits": tm.formatDataBits,
|
||||
"formatSC": tm.formatSC,
|
||||
"noescape": tm.noEscape,
|
||||
"noescapeJS": tm.noEscapeJS,
|
||||
"slashes": tm.slashes,
|
||||
})
|
||||
|
||||
// Parse dynamic templates
|
||||
if err = filepath.Walk(tm.resourceDir+"/template", func(path string, f os.FileInfo, err error) error {
|
||||
if f == nil || f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
templatePaths = append(templatePaths, path)
|
||||
if !silent {
|
||||
log.Debug("Template found: %s", path)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Error("Failed to parse templates: %s", err)
|
||||
}
|
||||
if _, err = tpl.ParseFiles(templatePaths...); err != nil {
|
||||
log.Error("Template parsing failed: %v", err)
|
||||
}
|
||||
|
||||
// Parse static resources
|
||||
var file []byte
|
||||
if err = filepath.Walk(tm.resourceDir+"/include", func(path string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("walk err: %w", err)
|
||||
}
|
||||
if f == nil || f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if file, err = os.ReadFile(path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.HasSuffix(path, ".png") {
|
||||
file = []byte("data:image/png;base64," + base64.StdEncoding.EncodeToString(file))
|
||||
} else if strings.HasSuffix(path, ".gif") {
|
||||
file = []byte("data:image/gif;base64," + base64.StdEncoding.EncodeToString(file))
|
||||
} else if strings.HasSuffix(path, ".webp") {
|
||||
file = []byte("data:image/webp;base64," + base64.StdEncoding.EncodeToString(file))
|
||||
}
|
||||
|
||||
// Wrap the resources in a template definition
|
||||
if _, err = tpl.Parse(
|
||||
`{{define "` + f.Name() + `"}}` + string(file) + `{{end}}`,
|
||||
); err != nil {
|
||||
return fmt.Errorf("failed to parse '%s': %w", path, err)
|
||||
}
|
||||
|
||||
if !silent {
|
||||
log.Debug("Template parsed: %s", path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Error("Failed to parse templates: %s", err)
|
||||
}
|
||||
|
||||
tm.tpl = tpl
|
||||
}
|
||||
|
||||
// Run runs a template by name
|
||||
func (tm *TemplateManager) Run(w io.Writer, r *http.Request, name string, data any) (err error) {
|
||||
if tm.debugModeEnabled {
|
||||
tm.ParseTemplates(true)
|
||||
}
|
||||
if r.Method == "HEAD" {
|
||||
return nil
|
||||
}
|
||||
return tm.tpl.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
// Template functions. These can be called from within the template to execute
|
||||
// more specialized actions
|
||||
|
||||
// The cache ID is used to invalidate caches when the webserver is restarted.
|
||||
// It's rounded to a full hour to prevent cached from being emptied too often if
|
||||
// the server is regularly restarted
|
||||
var cacheID = time.Now().Unix() / 3600
|
||||
|
||||
func (tm *TemplateManager) cacheID() int64 {
|
||||
return cacheID
|
||||
}
|
||||
func (tm *TemplateManager) debugMode() bool {
|
||||
return tm.debugModeEnabled
|
||||
}
|
||||
func (tm *TemplateManager) apiURL() string {
|
||||
return tm.externalAPIEndpoint
|
||||
}
|
||||
func (tm *TemplateManager) pageNr(s string) (nr int) {
|
||||
// Atoi returns 0 on error, which is fine for page numbers
|
||||
if nr, _ = strconv.Atoi(s); nr < 0 {
|
||||
return 0
|
||||
func (wc *WebController) newTemplateData(r *http.Request) *TemplateData {
|
||||
return &TemplateData{
|
||||
APIEndpoint: template.URL(wc.config.APIURLExternal),
|
||||
Hostname: template.HTML(wc.hostname),
|
||||
CacheID: cacheID,
|
||||
Title: "Nova.storage",
|
||||
OGData: siteOGData(r),
|
||||
}
|
||||
return nr
|
||||
}
|
||||
func (tm *TemplateManager) add(a, b any) float64 { return toFloat(a) + toFloat(b) }
|
||||
func (tm *TemplateManager) sub(a, b any) float64 { return toFloat(a) - toFloat(b) }
|
||||
func (tm *TemplateManager) mul(a, b any) float64 { return toFloat(a) * toFloat(b) }
|
||||
func (tm *TemplateManager) div(a, b any) float64 { return toFloat(a) / toFloat(b) }
|
||||
|
||||
func (tm *TemplateManager) formatData(i any) string {
|
||||
return util.FormatData(detectInt(i))
|
||||
}
|
||||
func (tm *TemplateManager) formatDataBits(i any) string {
|
||||
var size = detectInt(i) * 8
|
||||
var sizef = float64(size)
|
||||
|
||||
var fmtSize = func(n float64, u string) string {
|
||||
var f string
|
||||
if n >= 100 {
|
||||
f = "%.1f"
|
||||
} else if n >= 10 {
|
||||
f = "%.2f"
|
||||
} else {
|
||||
f = "%.3f"
|
||||
}
|
||||
return fmt.Sprintf(f+" "+u, n)
|
||||
}
|
||||
|
||||
if size >= 1e18 {
|
||||
// An exabyte is the largest volume of data you can express in a signed
|
||||
// 64-bit integer
|
||||
return fmtSize(sizef/1e18, "Eb")
|
||||
} else if sizef >= 1e15 {
|
||||
return fmtSize(sizef/1e15, "Pb")
|
||||
} else if sizef >= 1e12 {
|
||||
return fmtSize(sizef/1e12, "Tb")
|
||||
} else if sizef >= 1e9 {
|
||||
return fmtSize(sizef/1e9, "Gb")
|
||||
} else if sizef >= 1e6 {
|
||||
return fmtSize(sizef/1e6, "Mb")
|
||||
} else if sizef >= 1e3 {
|
||||
return fmtSize(sizef/1e3, "kb")
|
||||
}
|
||||
return fmt.Sprintf("%.0f b", sizef)
|
||||
}
|
||||
func (tm *TemplateManager) formatSC(amt float64) string {
|
||||
var fmtSize = func(n float64, u string) string {
|
||||
var f string
|
||||
if n >= 100 {
|
||||
f = "%.1f"
|
||||
} else if n >= 10 {
|
||||
f = "%.2f"
|
||||
} else {
|
||||
f = "%.3f"
|
||||
}
|
||||
return fmt.Sprintf(f+" "+u, n)
|
||||
}
|
||||
if amt >= 1 {
|
||||
return fmtSize(amt, "SC")
|
||||
} else if amt >= 1e-3 {
|
||||
return fmtSize(amt/1e-3, "mS")
|
||||
} else if amt >= 1e-6 {
|
||||
return fmtSize(amt/1e-6, "μS")
|
||||
} else if amt >= 1e-9 {
|
||||
return fmtSize(amt/1e-9, "nS")
|
||||
} else if amt >= 1e-12 {
|
||||
return fmtSize(amt/1e-12, "pS")
|
||||
}
|
||||
return fmtSize(amt/1e-24, "H")
|
||||
}
|
||||
func (tm *TemplateManager) noEscape(t string) template.HTML { return template.HTML(t) }
|
||||
func (tm *TemplateManager) noEscapeJS(t string) template.JS { return template.JS(t) }
|
||||
func (tm *TemplateManager) slashes() template.HTML { return template.HTML("//") }
|
||||
|
||||
func detectInt(i any) int {
|
||||
switch v := i.(type) {
|
||||
case int:
|
||||
return int(v)
|
||||
case int8:
|
||||
return int(v)
|
||||
case int16:
|
||||
return int(v)
|
||||
case int32:
|
||||
return int(v)
|
||||
case int64:
|
||||
return int(v)
|
||||
case uint:
|
||||
return int(v)
|
||||
case uint8:
|
||||
return int(v)
|
||||
case uint16:
|
||||
return int(v)
|
||||
case uint32:
|
||||
return int(v)
|
||||
case uint64:
|
||||
return int(v)
|
||||
case float32:
|
||||
return int(v)
|
||||
case float64:
|
||||
return int(v)
|
||||
}
|
||||
panic(fmt.Sprintf("%v is not an int", i))
|
||||
}
|
||||
|
||||
func toFloat(i any) float64 {
|
||||
switch v := i.(type) {
|
||||
case int:
|
||||
return float64(v)
|
||||
case int8:
|
||||
return float64(v)
|
||||
case int16:
|
||||
return float64(v)
|
||||
case int32:
|
||||
return float64(v)
|
||||
case int64:
|
||||
return float64(v)
|
||||
case uint:
|
||||
return float64(v)
|
||||
case uint8:
|
||||
return float64(v)
|
||||
case uint16:
|
||||
return float64(v)
|
||||
case uint32:
|
||||
return float64(v)
|
||||
case uint64:
|
||||
return float64(v)
|
||||
case float32:
|
||||
return float64(v)
|
||||
case float64:
|
||||
return float64(v)
|
||||
}
|
||||
panic(fmt.Sprintf("%v is not a number", i))
|
||||
func (wc *WebController) parseTemplate() *template.Template {
|
||||
return template.Must(template.ParseFiles(wc.config.ResourceDir + "/template/wrap.html"))
|
||||
}
|
||||
|
||||
// renderPage writes the page which loads the Svelte app. In debug mode the
|
||||
// template is parsed on every request, so changes show up without a restart
|
||||
func (wc *WebController) renderPage(w http.ResponseWriter, r *http.Request, td *TemplateData) {
|
||||
var tpl = wc.tpl
|
||||
if wc.config.DebugMode {
|
||||
tpl = wc.parseTemplate()
|
||||
}
|
||||
if r.Method == "HEAD" {
|
||||
return
|
||||
}
|
||||
if err := tpl.Execute(w, td); err != nil && !util.IsNetError(err) {
|
||||
log.Error("Error executing page template: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package webcontroller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
@@ -10,37 +9,30 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fornaxian.tech/log"
|
||||
"fornaxian.tech/pixeldrain_api_client/pixelapi"
|
||||
"fornaxian.tech/util"
|
||||
"github.com/julienschmidt/httprouter"
|
||||
blackfriday "github.com/russross/blackfriday/v2"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
APIURLExternal string `toml:"api_url_external"`
|
||||
APIURLInternal string `toml:"api_url_internal"`
|
||||
APISocketPath string `toml:"api_socket_path"`
|
||||
SessionCookieDomain string `toml:"session_cookie_domain"`
|
||||
ResourceDir string `toml:"resource_dir"`
|
||||
DebugMode bool `toml:"debug_mode"`
|
||||
ProxyAPIRequests bool `toml:"proxy_api_requests"`
|
||||
MaintenanceMode bool `toml:"maintenance_mode"`
|
||||
APIURLExternal string `toml:"api_url_external"`
|
||||
APIURLInternal string `toml:"api_url_internal"`
|
||||
APISocketPath string `toml:"api_socket_path"`
|
||||
ResourceDir string `toml:"resource_dir"`
|
||||
DebugMode bool `toml:"debug_mode"`
|
||||
ProxyAPIRequests bool `toml:"proxy_api_requests"`
|
||||
}
|
||||
|
||||
// WebController controls how requests are handled and makes sure they have
|
||||
// proper context when running
|
||||
type WebController struct {
|
||||
templates *TemplateManager
|
||||
config Config
|
||||
tpl *template.Template
|
||||
config Config
|
||||
|
||||
// Server hostname, displayed in the footer of every web page
|
||||
hostname string
|
||||
|
||||
httpClient *http.Client
|
||||
|
||||
// API client to use for all requests. If the user is authenticated you
|
||||
// should call Login() on this object. Calling Login will create a copy and
|
||||
// not alter the original PixelAPI, but it will use the same HTTP Transport
|
||||
@@ -48,49 +40,44 @@ type WebController struct {
|
||||
}
|
||||
|
||||
// New initializes a new WebController by registering all the request handlers
|
||||
// and parsing all templates in the resource directory
|
||||
func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
|
||||
var err error
|
||||
wc = &WebController{
|
||||
config: conf,
|
||||
httpClient: &http.Client{Timeout: time.Minute * 10},
|
||||
api: pixelapi.New(conf.APIURLInternal),
|
||||
// and parsing the page template
|
||||
func New(r *httprouter.Router, conf Config) (wc *WebController) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("could not get hostname: %s", err))
|
||||
}
|
||||
|
||||
wc = &WebController{
|
||||
config: conf,
|
||||
hostname: hostname,
|
||||
api: pixelapi.New(conf.APIURLInternal),
|
||||
}
|
||||
if conf.APISocketPath != "" {
|
||||
wc.api = wc.api.UnixSocketPath(conf.APISocketPath)
|
||||
}
|
||||
|
||||
wc.templates = NewTemplateManager(conf.ResourceDir, conf.APIURLExternal, conf.DebugMode)
|
||||
wc.templates.ParseTemplates(false)
|
||||
|
||||
if wc.hostname, err = os.Hostname(); err != nil {
|
||||
panic(fmt.Errorf("could not get hostname: %s", err))
|
||||
}
|
||||
wc.tpl = wc.parseTemplate()
|
||||
|
||||
// Serve static files
|
||||
var fs = http.FileServer(http.Dir(conf.ResourceDir + "/static"))
|
||||
var resourceHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
// Cache resources for a year
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
// Cache resources for a year. Except the Svelte entrypoint, it keeps its
|
||||
// name when it changes. The chunks it imports have a hash in their name
|
||||
if p.ByName("filepath") == "/svelte/wrap.js" {
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
} else {
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
}
|
||||
r.URL.Path = p.ByName("filepath")
|
||||
fs.ServeHTTP(w, r)
|
||||
}
|
||||
r.HEAD(prefix+"/res/*filepath", resourceHandler)
|
||||
r.OPTIONS(prefix+"/res/*filepath", resourceHandler)
|
||||
r.GET(prefix+"/res/*filepath", resourceHandler)
|
||||
r.HEAD("/res/*filepath", resourceHandler)
|
||||
r.OPTIONS("/res/*filepath", resourceHandler)
|
||||
r.GET("/res/*filepath", resourceHandler)
|
||||
|
||||
// Static assets
|
||||
r.GET(prefix+"/favicon.ico" /* */, wc.serveFile("/favicon.ico"))
|
||||
r.GET(prefix+"/robots.txt" /* */, wc.serveFile("/robots.txt"))
|
||||
|
||||
if conf.MaintenanceMode {
|
||||
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
wc.templates.Run(w, r, "maintenance", wc.newTemplateData(r))
|
||||
})
|
||||
return wc
|
||||
}
|
||||
// Browsers and crawlers look for these in the root
|
||||
var rootFile = func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fs.ServeHTTP(w, r) }
|
||||
r.GET("/favicon.ico", rootFile)
|
||||
r.GET("/robots.txt", rootFile)
|
||||
|
||||
if conf.ProxyAPIRequests {
|
||||
remoteURL, err := url.Parse(strings.TrimSuffix(conf.APIURLInternal, "/api"))
|
||||
@@ -100,50 +87,30 @@ func New(r *httprouter.Router, prefix string, conf Config) (wc *WebController) {
|
||||
|
||||
log.Info("Starting API proxy to %s", remoteURL)
|
||||
var prox = httputil.NewSingleHostReverseProxy(remoteURL)
|
||||
prox.Transport = wc.httpClient.Transport
|
||||
|
||||
var proxyHandler = func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
log.Info("Proxying request to %s", r.URL)
|
||||
r.Host = remoteURL.Host
|
||||
r.Header.Set("Origin", remoteURL.String())
|
||||
prox.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
r.Handle("OPTIONS", "/api/*p", proxyHandler)
|
||||
r.Handle("POST", "/api/*p", proxyHandler)
|
||||
r.Handle("GET", "/api/*p", proxyHandler)
|
||||
r.Handle("PUT", "/api/*p", proxyHandler)
|
||||
r.Handle("PATCH", "/api/*p", proxyHandler)
|
||||
r.Handle("DELETE", "/api/*p", proxyHandler)
|
||||
}
|
||||
|
||||
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
wc.serveTemplate("wrap")(w, r, nil)
|
||||
})
|
||||
|
||||
// Request method shorthands. These help keep the array of handlers aligned
|
||||
const PST, GET = "POST", "GET"
|
||||
|
||||
// Loop over the handlers and register all of them in the router
|
||||
for _, h := range []struct {
|
||||
method string // HTTP request method this API handler uses
|
||||
path string // The URL path this API will be registered on
|
||||
handler httprouter.Handle // The function to run when this API is called
|
||||
}{
|
||||
{GET, "api" /* */, wc.serveMarkdown("api.md")},
|
||||
{GET, "d/*path" /* */, wc.serveDirectory},
|
||||
{GET, "widgets" /* */, wc.serveTemplate("widgets")},
|
||||
{GET, "status" /* */, wc.serveTemplate("status")},
|
||||
{GET, "theme.css", wc.themeHandler},
|
||||
} {
|
||||
r.Handle(h.method, prefix+"/"+h.path, middleware(h.handler))
|
||||
|
||||
// Also support HEAD requests
|
||||
if h.method == GET {
|
||||
r.HEAD(prefix+"/"+h.path, middleware(h.handler))
|
||||
for _, method := range []string{"OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE"} {
|
||||
r.Handle(method, "/api/*p", proxyHandler)
|
||||
}
|
||||
}
|
||||
|
||||
// Every other path is a page of the app, which shows its own not found
|
||||
// message for paths it doesn't know
|
||||
var page = middleware(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
wc.renderPage(w, r, wc.newTemplateData(r))
|
||||
})
|
||||
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { page(w, r, nil) })
|
||||
|
||||
// Filesystem pages embed the requested node, for link previews
|
||||
var directory = middleware(wc.serveDirectory)
|
||||
r.GET("/d/*path", directory)
|
||||
r.HEAD("/d/*path", directory)
|
||||
|
||||
return wc
|
||||
}
|
||||
|
||||
@@ -165,96 +132,6 @@ func middleware(handle httprouter.Handle) httprouter.Handle {
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *WebController) serveTemplate(tpl string) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||
// if opts.NoEmbed {
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
// }
|
||||
|
||||
err := wc.templates.Run(w, r, tpl, wc.newTemplateData(r))
|
||||
if err != nil && !util.IsNetError(err) {
|
||||
log.Error("Error executing template '%s': %s", tpl, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *WebController) serveMarkdown(tpl string) httprouter.Handle {
|
||||
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||
var err error
|
||||
// if opts.NoEmbed {
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
// }
|
||||
|
||||
var tpld = wc.newTemplateData(r)
|
||||
|
||||
// Execute the raw markdown template and save the result in a buffer
|
||||
var tplBuf bytes.Buffer
|
||||
err = wc.templates.Run(&tplBuf, r, tpl, tpld)
|
||||
if err != nil && !util.IsNetError(err) {
|
||||
log.Error("Error executing template '%s': %s", tpl, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the markdown document and save the resulting HTML in a buffer
|
||||
renderer := blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{
|
||||
Flags: blackfriday.CommonHTMLFlags | blackfriday.TOC,
|
||||
})
|
||||
|
||||
// We parse the markdown document, walk through the nodes. Extract the
|
||||
// title of the document, and the rest of the nodes are rendered like
|
||||
// normal
|
||||
var mdBuf bytes.Buffer
|
||||
|
||||
blackfriday.New(
|
||||
blackfriday.WithRenderer(renderer),
|
||||
blackfriday.WithExtensions(blackfriday.CommonExtensions|blackfriday.AutoHeadingIDs),
|
||||
).Parse(
|
||||
tplBuf.Bytes(),
|
||||
).Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus {
|
||||
// Capture the title of the document so we can put it at the top of
|
||||
// the template and in the metadata. When entering a h1 node the
|
||||
// first child will be the title of the document. Save that value
|
||||
if node.Type == blackfriday.Heading && node.HeadingData.Level == 1 {
|
||||
tpld.Title = string(node.FirstChild.Literal)
|
||||
return blackfriday.SkipChildren
|
||||
}
|
||||
|
||||
// If this text node contains solely the text "[TOC]" then we render
|
||||
// the table of contents
|
||||
if node.Type == blackfriday.Text && bytes.Equal(node.Literal, []byte("[TOC]")) {
|
||||
// Find the document node and render its TOC
|
||||
for parent := node.Parent; ; parent = parent.Parent {
|
||||
if parent.Type == blackfriday.Document {
|
||||
renderer.RenderHeader(&mdBuf, parent)
|
||||
return blackfriday.SkipChildren
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return renderer.RenderNode(&mdBuf, node, entering)
|
||||
})
|
||||
|
||||
// Pass the buffer's parsed contents to the wrapper template
|
||||
tpld.Other = template.HTML(mdBuf.Bytes())
|
||||
|
||||
// Execute the wrapper template
|
||||
err = wc.templates.Run(w, r, "markdown_wrapper", tpld)
|
||||
if err != nil && !util.IsNetError(err) {
|
||||
log.Error("Error executing template '%s': %s", tpl, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *WebController) serveFile(path string) httprouter.Handle {
|
||||
return func(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
p httprouter.Params,
|
||||
) {
|
||||
http.ServeFile(w, r, wc.config.ResourceDir+"/static"+path)
|
||||
}
|
||||
}
|
||||
|
||||
func (wc *WebController) getAPIKey(r *http.Request) (key string, err error) {
|
||||
if cookie, err := r.Cookie("nova_auth_key"); err == nil {
|
||||
if len(cookie.Value) == 36 {
|
||||
|
||||
Reference in New Issue
Block a user