mirror of
https://github.com/yusing/godoxy.git
synced 2025-05-20 12:42:34 +02:00
feat: support selfh.st icons, support homepage config overriding
This commit is contained in:
parent
68771ce399
commit
64e85c3076
13 changed files with 591 additions and 44 deletions
|
@ -34,6 +34,7 @@ func NewHandler(cfg config.ConfigInstance) http.Handler {
|
||||||
mux.HandleFunc("GET", "/v1/health/ws", auth.RequireAuth(useCfg(cfg, v1.HealthWS)))
|
mux.HandleFunc("GET", "/v1/health/ws", auth.RequireAuth(useCfg(cfg, v1.HealthWS)))
|
||||||
mux.HandleFunc("GET", "/v1/logs/ws", auth.RequireAuth(useCfg(cfg, v1.LogsWS())))
|
mux.HandleFunc("GET", "/v1/logs/ws", auth.RequireAuth(useCfg(cfg, v1.LogsWS())))
|
||||||
mux.HandleFunc("GET", "/v1/favicon/{alias}", auth.RequireAuth(favicon.GetFavIcon))
|
mux.HandleFunc("GET", "/v1/favicon/{alias}", auth.RequireAuth(favicon.GetFavIcon))
|
||||||
|
mux.HandleFunc("POST", "/v1/homepage/set", auth.RequireAuth(v1.SetHomePageOverrides))
|
||||||
|
|
||||||
defaultAuth := auth.GetDefaultAuth()
|
defaultAuth := auth.GetDefaultAuth()
|
||||||
if defaultAuth != nil {
|
if defaultAuth != nil {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -88,6 +89,8 @@ func GetFavIcon(w http.ResponseWriter, req *http.Request) {
|
||||||
icon, status, errMsg = findIcon(r, req, hp.Icon.Value)
|
icon, status, errMsg = findIcon(r, req, hp.Icon.Value)
|
||||||
case homepage.IconSourceWalkXCode:
|
case homepage.IconSourceWalkXCode:
|
||||||
icon, status, errMsg = fetchWalkxcodeIcon(hp.Icon.Extra.FileType, hp.Icon.Extra.Name)
|
icon, status, errMsg = fetchWalkxcodeIcon(hp.Icon.Extra.FileType, hp.Icon.Extra.Name)
|
||||||
|
case homepage.IconSourceSelfhSt:
|
||||||
|
icon, status, errMsg = fetchSelfhStIcon(hp.Icon.Extra.FileType, hp.Icon.Extra.Name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// try extract from "link[rel=icon]"
|
// try extract from "link[rel=icon]"
|
||||||
|
@ -117,8 +120,8 @@ func ResetIconCache(route route.HTTPRoute) {
|
||||||
|
|
||||||
func loadIconCache(key string) (icon []byte, ok bool) {
|
func loadIconCache(key string) (icon []byte, ok bool) {
|
||||||
iconCacheMu.RLock()
|
iconCacheMu.RLock()
|
||||||
|
defer iconCacheMu.RUnlock()
|
||||||
icon, ok = iconCache[key]
|
icon, ok = iconCache[key]
|
||||||
iconCacheMu.RUnlock()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,9 +172,9 @@ func sanitizeName(name string) string {
|
||||||
return strings.ToLower(nameSanitizer.Replace(name))
|
return strings.ToLower(nameSanitizer.Replace(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchWalkxcodeIcon(filetype string, name string) ([]byte, int, string) {
|
func fetchWalkxcodeIcon(filetype, name string) ([]byte, int, string) {
|
||||||
// if icon isn't in the list, no need to fetch
|
// if icon isn't in the list, no need to fetch
|
||||||
if !internal.HasIcon(name, filetype) {
|
if !internal.HasWalkxCodeIcon(name, filetype) {
|
||||||
logging.Debug().
|
logging.Debug().
|
||||||
Str("filetype", filetype).
|
Str("filetype", filetype).
|
||||||
Str("name", name).
|
Str("name", name).
|
||||||
|
@ -184,10 +187,39 @@ func fetchWalkxcodeIcon(filetype string, name string) ([]byte, int, string) {
|
||||||
return icon, http.StatusOK, ""
|
return icon, http.StatusOK, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
url := homepage.DashboardIconBaseURL + filetype + "/" + name + "." + filetype
|
// url := homepage.DashboardIconBaseURL + filetype + "/" + name + "." + filetype
|
||||||
|
url := fmt.Sprintf("https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/%s/%s.%s", filetype, name, filetype)
|
||||||
return fetchIconAbsolute(url)
|
return fetchIconAbsolute(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fetchSelfhStIcon(filetype, reference string) ([]byte, int, string) {
|
||||||
|
// if icon isn't in the list, no need to fetch
|
||||||
|
if !internal.HasSelfhstIcon(reference, filetype) {
|
||||||
|
logging.Debug().
|
||||||
|
Str("filetype", filetype).
|
||||||
|
Str("reference", reference).
|
||||||
|
Msg("icon not found")
|
||||||
|
return nil, http.StatusNotFound, "icon not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
icon, ok := loadIconCache("selfh.st/" + filetype + "/" + reference)
|
||||||
|
if ok {
|
||||||
|
return icon, http.StatusOK, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://cdn.jsdelivr.net/gh/selfhst/icons/%s/%s.%s", filetype, reference, filetype)
|
||||||
|
return fetchIconAbsolute(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchIcon(filetype, filename string) (icon []byte, status int, errMsg string) {
|
||||||
|
icon, status, errMsg = fetchSelfhStIcon(filetype, filename)
|
||||||
|
if icon != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
icon, status, errMsg = fetchWalkxcodeIcon(filetype, filename)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func findIcon(r route.HTTPRoute, req *http.Request, uri string) (icon []byte, status int, errMsg string) {
|
func findIcon(r route.HTTPRoute, req *http.Request, uri string) (icon []byte, status int, errMsg string) {
|
||||||
key := r.TargetName()
|
key := r.TargetName()
|
||||||
icon, ok := loadIconCache(key)
|
icon, ok := loadIconCache(key)
|
||||||
|
@ -198,10 +230,10 @@ func findIcon(r route.HTTPRoute, req *http.Request, uri string) (icon []byte, st
|
||||||
return icon, http.StatusOK, ""
|
return icon, http.StatusOK, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
icon, status, errMsg = fetchWalkxcodeIcon("png", sanitizeName(r.TargetName()))
|
icon, status, errMsg = fetchIcon("png", sanitizeName(r.TargetName()))
|
||||||
cont := r.RawEntry().Container
|
cont := r.RawEntry().Container
|
||||||
if icon == nil && cont != nil {
|
if icon == nil && cont != nil {
|
||||||
icon, status, errMsg = fetchWalkxcodeIcon("png", sanitizeName(cont.ImageName))
|
icon, status, errMsg = fetchIcon("png", sanitizeName(cont.ImageName))
|
||||||
}
|
}
|
||||||
if icon == nil {
|
if icon == nil {
|
||||||
// fallback to parse html
|
// fallback to parse html
|
||||||
|
@ -213,7 +245,6 @@ func findIcon(r route.HTTPRoute, req *http.Request, uri string) (icon []byte, st
|
||||||
}
|
}
|
||||||
|
|
||||||
func findIconSlow(r route.HTTPRoute, req *http.Request, uri string) (icon []byte, status int, errMsg string) {
|
func findIconSlow(r route.HTTPRoute, req *http.Request, uri string) (icon []byte, status int, errMsg string) {
|
||||||
c := newContent()
|
|
||||||
ctx, cancel := context.WithTimeoutCause(req.Context(), 3*time.Second, errors.New("favicon request timeout"))
|
ctx, cancel := context.WithTimeoutCause(req.Context(), 3*time.Second, errors.New("favicon request timeout"))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
newReq := req.WithContext(ctx)
|
newReq := req.WithContext(ctx)
|
||||||
|
@ -233,6 +264,8 @@ func findIconSlow(r route.HTTPRoute, req *http.Request, uri string) (icon []byte
|
||||||
newReq.URL.RawPath = u.RawPath
|
newReq.URL.RawPath = u.RawPath
|
||||||
newReq.URL.RawQuery = u.RawQuery
|
newReq.URL.RawQuery = u.RawQuery
|
||||||
newReq.RequestURI = u.String()
|
newReq.RequestURI = u.String()
|
||||||
|
|
||||||
|
c := newContent()
|
||||||
r.ServeHTTP(c, newReq)
|
r.ServeHTTP(c, newReq)
|
||||||
if c.status != http.StatusOK {
|
if c.status != http.StatusOK {
|
||||||
switch c.status {
|
switch c.status {
|
||||||
|
|
58
internal/api/v1/homepage_overrides.go
Normal file
58
internal/api/v1/homepage_overrides.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HomepageOverrideDisplayname = "display_name"
|
||||||
|
HomepageOverrideDisplayOrder = "display_order"
|
||||||
|
HomepageOverrideDisplayCategory = "display_category"
|
||||||
|
HomepageOverrideCategoryOrder = "category_order"
|
||||||
|
HomepageOverrideCategoryName = "category_name"
|
||||||
|
HomepageOverrideIcon = "icon"
|
||||||
|
HomepageOverrideShow = "show"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetHomePageOverrides(w http.ResponseWriter, r *http.Request) {
|
||||||
|
what, which, value := r.FormValue("what"), r.FormValue("which"), r.FormValue("value")
|
||||||
|
if what == "" || which == "" {
|
||||||
|
http.Error(w, "missing what or which", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if value == "" {
|
||||||
|
http.Error(w, "missing value", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
overrides := homepage.GetJSONConfig()
|
||||||
|
switch what {
|
||||||
|
case HomepageOverrideDisplayname:
|
||||||
|
utils.RespondError(w, overrides.SetDisplayNameOverride(which, value))
|
||||||
|
case HomepageOverrideDisplayCategory:
|
||||||
|
utils.RespondError(w, overrides.SetDisplayCategoryOverride(which, value))
|
||||||
|
case HomepageOverrideCategoryName:
|
||||||
|
utils.RespondError(w, overrides.SetCategoryNameOverride(which, value))
|
||||||
|
case HomepageOverrideIcon:
|
||||||
|
utils.RespondError(w, overrides.SetIconOverride(which, value))
|
||||||
|
case HomepageOverrideShow:
|
||||||
|
utils.RespondError(w, overrides.SetShowItemOverride(which, strutils.ParseBool(value)))
|
||||||
|
case HomepageOverrideDisplayOrder, HomepageOverrideCategoryOrder:
|
||||||
|
v, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid integer", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if what == HomepageOverrideDisplayOrder {
|
||||||
|
utils.RespondError(w, overrides.SetDisplayOrder(which, v))
|
||||||
|
} else {
|
||||||
|
utils.RespondError(w, overrides.SetCategoryOrder(which, v))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
http.Error(w, "invalid what", http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal"
|
||||||
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
U "github.com/yusing/go-proxy/internal/api/v1/utils"
|
||||||
"github.com/yusing/go-proxy/internal/common"
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
config "github.com/yusing/go-proxy/internal/config/types"
|
config "github.com/yusing/go-proxy/internal/config/types"
|
||||||
|
@ -24,6 +25,7 @@ const (
|
||||||
ListHomepageConfig = "homepage_config"
|
ListHomepageConfig = "homepage_config"
|
||||||
ListRouteProviders = "route_providers"
|
ListRouteProviders = "route_providers"
|
||||||
ListHomepageCategories = "homepage_categories"
|
ListHomepageCategories = "homepage_categories"
|
||||||
|
ListIcons = "icons"
|
||||||
ListTasks = "tasks"
|
ListTasks = "tasks"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,6 +60,13 @@ func List(cfg config.ConfigInstance, w http.ResponseWriter, r *http.Request) {
|
||||||
U.RespondJSON(w, r, cfg.RouteProviderList())
|
U.RespondJSON(w, r, cfg.RouteProviderList())
|
||||||
case ListHomepageCategories:
|
case ListHomepageCategories:
|
||||||
U.RespondJSON(w, r, routequery.HomepageCategories())
|
U.RespondJSON(w, r, routequery.HomepageCategories())
|
||||||
|
case ListIcons:
|
||||||
|
icons, err := internal.ListAvailableIcons()
|
||||||
|
if err != nil {
|
||||||
|
U.RespondError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
U.RespondJSON(w, r, icons)
|
||||||
case ListTasks:
|
case ListTasks:
|
||||||
U.RespondJSON(w, r, task.DebugTaskList())
|
U.RespondJSON(w, r, task.DebugTaskList())
|
||||||
default:
|
default:
|
||||||
|
@ -80,7 +89,7 @@ func listRoute(which string) any {
|
||||||
}
|
}
|
||||||
|
|
||||||
func listFiles(w http.ResponseWriter, r *http.Request) {
|
func listFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
files, err := utils.ListFiles(common.ConfigBasePath, 0)
|
files, err := utils.ListFiles(common.ConfigBasePath, 0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
U.HandleErr(w, r, err)
|
U.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
|
@ -97,7 +106,7 @@ func listFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
resp[t] = append(resp[t], file)
|
resp[t] = append(resp[t], file)
|
||||||
}
|
}
|
||||||
|
|
||||||
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0)
|
mids, err := utils.ListFiles(common.MiddlewareComposeBasePath, 0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
U.HandleErr(w, r, err)
|
U.HandleErr(w, r, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -20,8 +20,8 @@ const (
|
||||||
ConfigFileName = "config.yml"
|
ConfigFileName = "config.yml"
|
||||||
ConfigExampleFileName = "config.example.yml"
|
ConfigExampleFileName = "config.example.yml"
|
||||||
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
ConfigPath = ConfigBasePath + "/" + ConfigFileName
|
||||||
|
HomepageJSONConfigPath = ConfigBasePath + "/.homepage.json"
|
||||||
JWTKeyPath = ConfigBasePath + "/jwt.key"
|
IconListCachePath = ConfigBasePath + "/.icon_list_cache.json"
|
||||||
|
|
||||||
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
|
MiddlewareComposeBasePath = ConfigBasePath + "/middlewares"
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,16 @@ func (item *Item) IsEmpty() bool {
|
||||||
len(item.WidgetConfig) == 0)
|
len(item.WidgetConfig) == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (item *Item) GetOverriddenItem() *Item {
|
||||||
|
overrides := GetJSONConfig()
|
||||||
|
clone := *item
|
||||||
|
clone.Name = overrides.GetDisplayName(item)
|
||||||
|
clone.Icon = overrides.GetDisplayIcon(item)
|
||||||
|
clone.Category = overrides.GetCategory(item)
|
||||||
|
clone.Show = overrides.GetShowItem(item)
|
||||||
|
return &clone
|
||||||
|
}
|
||||||
|
|
||||||
func NewHomePageConfig() Config {
|
func NewHomePageConfig() Config {
|
||||||
return Config(make(map[string]Category))
|
return Config(make(map[string]Category))
|
||||||
}
|
}
|
||||||
|
|
31
internal/homepage/homepage_test.go
Normal file
31
internal/homepage/homepage_test.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package homepage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOverrideItem(t *testing.T) {
|
||||||
|
a := &Item{
|
||||||
|
Show: false,
|
||||||
|
Alias: "foo",
|
||||||
|
Name: "Foo",
|
||||||
|
Icon: &IconURL{
|
||||||
|
Value: "/favicon.ico",
|
||||||
|
IconSource: IconSourceRelative,
|
||||||
|
},
|
||||||
|
Category: "App",
|
||||||
|
}
|
||||||
|
overrides := GetJSONConfig()
|
||||||
|
ExpectNoError(t, overrides.SetShowItemOverride(a.Alias, true))
|
||||||
|
ExpectNoError(t, overrides.SetDisplayNameOverride(a.Alias, "Bar"))
|
||||||
|
ExpectNoError(t, overrides.SetDisplayCategoryOverride(a.Alias, "Test"))
|
||||||
|
ExpectNoError(t, overrides.SetIconOverride(a.Alias, "png/example.png"))
|
||||||
|
|
||||||
|
overridden := a.GetOverriddenItem()
|
||||||
|
ExpectTrue(t, overridden.Show)
|
||||||
|
ExpectEqual(t, overridden.Name, "Bar")
|
||||||
|
ExpectEqual(t, overridden.Category, "Test")
|
||||||
|
ExpectEqual(t, overridden.Icon.String(), "png/example.png")
|
||||||
|
}
|
|
@ -3,13 +3,14 @@ package homepage
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal"
|
||||||
E "github.com/yusing/go-proxy/internal/error"
|
E "github.com/yusing/go-proxy/internal/error"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
IconURL struct {
|
IconURL struct {
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
IconSource
|
IconSource `json:"source"`
|
||||||
Extra *IconExtra `json:"extra"`
|
Extra *IconExtra `json:"extra"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,12 +26,23 @@ const (
|
||||||
IconSourceAbsolute IconSource = iota
|
IconSourceAbsolute IconSource = iota
|
||||||
IconSourceRelative
|
IconSourceRelative
|
||||||
IconSourceWalkXCode
|
IconSourceWalkXCode
|
||||||
|
IconSourceSelfhSt
|
||||||
)
|
)
|
||||||
|
|
||||||
const DashboardIconBaseURL = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/"
|
|
||||||
|
|
||||||
var ErrInvalidIconURL = E.New("invalid icon url")
|
var ErrInvalidIconURL = E.New("invalid icon url")
|
||||||
|
|
||||||
|
func (u *IconURL) HasIcon() bool {
|
||||||
|
if u.IconSource == IconSourceSelfhSt &&
|
||||||
|
!internal.HasSelfhstIcon(u.Extra.Name, u.Extra.FileType) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if u.IconSource == IconSourceWalkXCode &&
|
||||||
|
!internal.HasWalkxCodeIcon(u.Extra.Name, u.Extra.FileType) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Parse implements strutils.Parser.
|
// Parse implements strutils.Parser.
|
||||||
func (u *IconURL) Parse(v string) error {
|
func (u *IconURL) Parse(v string) error {
|
||||||
if v == "" {
|
if v == "" {
|
||||||
|
@ -45,11 +57,12 @@ func (u *IconURL) Parse(v string) error {
|
||||||
case "http:", "https:":
|
case "http:", "https:":
|
||||||
u.Value = v
|
u.Value = v
|
||||||
u.IconSource = IconSourceAbsolute
|
u.IconSource = IconSourceAbsolute
|
||||||
return nil
|
case "@target", "": // @target/favicon.ico, /favicon.ico
|
||||||
case "@target":
|
|
||||||
u.Value = v[slashIndex:]
|
u.Value = v[slashIndex:]
|
||||||
u.IconSource = IconSourceRelative
|
u.IconSource = IconSourceRelative
|
||||||
return nil
|
if u.Value == "/" {
|
||||||
|
return ErrInvalidIconURL.Withf("%s", "empty path")
|
||||||
|
}
|
||||||
case "png", "svg", "webp": // walkXCode Icons
|
case "png", "svg", "webp": // walkXCode Icons
|
||||||
u.Value = v
|
u.Value = v
|
||||||
u.IconSource = IconSourceWalkXCode
|
u.IconSource = IconSourceWalkXCode
|
||||||
|
@ -57,10 +70,38 @@ func (u *IconURL) Parse(v string) error {
|
||||||
FileType: beforeSlash,
|
FileType: beforeSlash,
|
||||||
Name: strings.TrimSuffix(v[slashIndex+1:], "."+beforeSlash),
|
Name: strings.TrimSuffix(v[slashIndex+1:], "."+beforeSlash),
|
||||||
}
|
}
|
||||||
return nil
|
case "@selfhst": // selfh.st Icons, @selfhst/<reference>.<format>
|
||||||
default:
|
u.Value = v[slashIndex:]
|
||||||
|
u.IconSource = IconSourceSelfhSt
|
||||||
|
parts := strings.Split(v[slashIndex+1:], ".")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return ErrInvalidIconURL.Withf("%s", "expect @selfhst/<reference>.<format>, e.g. @selfhst/adguard-home.webp")
|
||||||
|
}
|
||||||
|
reference, format := parts[0], strings.ToLower(parts[1])
|
||||||
|
if reference == "" || format == "" {
|
||||||
return ErrInvalidIconURL
|
return ErrInvalidIconURL
|
||||||
}
|
}
|
||||||
|
switch format {
|
||||||
|
case "svg", "png", "webp":
|
||||||
|
default:
|
||||||
|
return ErrInvalidIconURL.Withf("%s", "invalid format, expect svg/png/webp")
|
||||||
|
}
|
||||||
|
u.Extra = &IconExtra{
|
||||||
|
FileType: format,
|
||||||
|
Name: reference,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return ErrInvalidIconURL.Withf("%s", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Value == "" {
|
||||||
|
return ErrInvalidIconURL.Withf("%s", "empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !u.HasIcon() {
|
||||||
|
return ErrInvalidIconURL.Withf("no such icon %s", u.Value)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *IconURL) String() string {
|
func (u *IconURL) String() string {
|
||||||
|
|
|
@ -29,6 +29,24 @@ func TestIconURL(t *testing.T) {
|
||||||
IconSource: IconSourceRelative,
|
IconSource: IconSourceRelative,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "relative2",
|
||||||
|
input: "/icon.png",
|
||||||
|
wantValue: &IconURL{
|
||||||
|
Value: "/icon.png",
|
||||||
|
IconSource: IconSourceRelative,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relative_empty_path",
|
||||||
|
input: "@target/",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relative_empty_path2",
|
||||||
|
input: "/",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "walkxcode",
|
name: "walkxcode",
|
||||||
input: "png/walkxcode.png",
|
input: "png/walkxcode.png",
|
||||||
|
@ -41,6 +59,33 @@ func TestIconURL(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "walkxcode_invalid_format",
|
||||||
|
input: "foo/walkxcode.png",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selfh.st_valid",
|
||||||
|
input: "@selfhst/foo.png",
|
||||||
|
wantValue: &IconURL{
|
||||||
|
Value: "/foo.png",
|
||||||
|
IconSource: IconSourceSelfhSt,
|
||||||
|
Extra: &IconExtra{
|
||||||
|
FileType: "png",
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selfh.st_invalid",
|
||||||
|
input: "@selfhst/foo",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "selfh.st_invalid_format",
|
||||||
|
input: "@selfhst/foo.bar",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "invalid",
|
name: "invalid",
|
||||||
input: "invalid",
|
input: "invalid",
|
||||||
|
|
147
internal/homepage/json_config.go
Normal file
147
internal/homepage/json_config.go
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
package homepage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JSONConfig struct {
|
||||||
|
DisplayNameOverride map[string]string `json:"display_name_override"`
|
||||||
|
DisplayCategoryOverride map[string]string `json:"display_category_override"`
|
||||||
|
DisplayOrder map[string]int `json:"display_order"` // TODO: implement this
|
||||||
|
CategoryNameOverride map[string]string `json:"category_name_override"`
|
||||||
|
CategoryOrder map[string]int `json:"category_order"` // TODO: implement this
|
||||||
|
IconOverride map[string]*IconURL `json:"icon_override"`
|
||||||
|
ShowItemOverride map[string]bool `json:"show_item_override"`
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonConfigInstance *JSONConfig
|
||||||
|
|
||||||
|
func InitOverridesConfig() {
|
||||||
|
jsonConfigInstance = &JSONConfig{
|
||||||
|
DisplayNameOverride: make(map[string]string),
|
||||||
|
DisplayCategoryOverride: make(map[string]string),
|
||||||
|
DisplayOrder: make(map[string]int),
|
||||||
|
CategoryNameOverride: make(map[string]string),
|
||||||
|
CategoryOrder: make(map[string]int),
|
||||||
|
IconOverride: make(map[string]*IconURL),
|
||||||
|
ShowItemOverride: make(map[string]bool),
|
||||||
|
}
|
||||||
|
err := utils.LoadJSON(common.HomepageJSONConfigPath, jsonConfigInstance)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
logging.Fatal().Err(err).Msg("failed to load homepage overrides config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetJSONConfig() *JSONConfig {
|
||||||
|
return jsonConfigInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JSONConfig) save() error {
|
||||||
|
if common.IsTest {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return utils.SaveJSON(common.HomepageJSONConfigPath, c, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JSONConfig) GetDisplayName(item *Item) string {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
if override, ok := c.DisplayNameOverride[item.Alias]; ok {
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
return item.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JSONConfig) SetDisplayNameOverride(key, value string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.DisplayNameOverride[key] = value
|
||||||
|
return c.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JSONConfig) GetCategory(item *Item) string {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
category := item.Category
|
||||||
|
if override, ok := c.DisplayCategoryOverride[item.Alias]; ok {
|
||||||
|
category = override
|
||||||
|
}
|
||||||
|
if override, ok := c.CategoryNameOverride[category]; ok {
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
return category
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JSONConfig) SetDisplayCategoryOverride(key, value string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.DisplayCategoryOverride[key] = value
|
||||||
|
return c.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JSONConfig) SetDisplayOrder(key string, value int) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.DisplayOrder[key] = value
|
||||||
|
return c.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JSONConfig) SetCategoryNameOverride(key, value string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.CategoryNameOverride[key] = value
|
||||||
|
return c.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JSONConfig) SetCategoryOrder(key string, value int) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.CategoryOrder[key] = value
|
||||||
|
return c.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JSONConfig) GetDisplayIcon(item *Item) *IconURL {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
if override, ok := c.IconOverride[item.Alias]; ok {
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
return item.Icon
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JSONConfig) SetIconOverride(key, value string) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
var url IconURL
|
||||||
|
if err := url.Parse(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !url.HasIcon() {
|
||||||
|
return errors.New("no such icon")
|
||||||
|
}
|
||||||
|
c.IconOverride[key] = &url
|
||||||
|
return c.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JSONConfig) GetShowItem(item *Item) bool {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
if override, ok := c.ShowItemOverride[item.Alias]; ok {
|
||||||
|
return override
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *JSONConfig) SetShowItemOverride(key string, value bool) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.ShowItemOverride[key] = value
|
||||||
|
return c.save()
|
||||||
|
}
|
|
@ -4,10 +4,13 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal/common"
|
||||||
"github.com/yusing/go-proxy/internal/logging"
|
"github.com/yusing/go-proxy/internal/logging"
|
||||||
|
"github.com/yusing/go-proxy/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GitHubContents struct { //! keep this, may reuse in future
|
type GitHubContents struct { //! keep this, may reuse in future
|
||||||
|
@ -18,53 +21,136 @@ type GitHubContents struct { //! keep this, may reuse in future
|
||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Icons map[string]map[string]struct{}
|
type (
|
||||||
|
IconsMap map[string]map[string]struct{}
|
||||||
|
Cache struct {
|
||||||
|
WalkxCode, Selfhst IconsMap
|
||||||
|
DisplayNames ReferenceDisplayNameMap
|
||||||
|
}
|
||||||
|
ReferenceDisplayNameMap map[string]string
|
||||||
|
)
|
||||||
|
|
||||||
// no longer cache for `godoxy ls-icons`
|
func (icons *Cache) isEmpty() bool {
|
||||||
|
return len(icons.WalkxCode) == 0 && len(icons.Selfhst) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (icons *Cache) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[string]any{
|
||||||
|
"walkxcode": icons.WalkxCode,
|
||||||
|
"selfhst": icons.Selfhst,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const updateInterval = 1 * time.Hour
|
const updateInterval = 1 * time.Hour
|
||||||
|
|
||||||
var (
|
var (
|
||||||
iconsCache = make(Icons)
|
iconsCache *Cache
|
||||||
iconsCahceMu sync.Mutex
|
iconsCahceMu sync.Mutex
|
||||||
lastUpdate time.Time
|
lastUpdate time.Time
|
||||||
)
|
)
|
||||||
|
|
||||||
const walkxcodeIcons = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/tree.json"
|
const (
|
||||||
|
walkxcodeIcons = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/tree.json"
|
||||||
|
selfhstIcons = "https://cdn.selfh.st/directory/icons.json"
|
||||||
|
)
|
||||||
|
|
||||||
func ListAvailableIcons() (Icons, error) {
|
func InitIconListCache() {
|
||||||
|
iconsCache = &Cache{
|
||||||
|
WalkxCode: make(IconsMap),
|
||||||
|
Selfhst: make(IconsMap),
|
||||||
|
DisplayNames: make(ReferenceDisplayNameMap),
|
||||||
|
}
|
||||||
|
err := utils.LoadJSON(common.IconListCachePath, iconsCache)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
logging.Fatal().Err(err).Msg("failed to load icon list cache config")
|
||||||
|
} else if err == nil {
|
||||||
|
if stats, err := os.Stat(common.IconListCachePath); err != nil {
|
||||||
|
logging.Fatal().Err(err).Msg("failed to load icon list cache config")
|
||||||
|
} else {
|
||||||
|
lastUpdate = stats.ModTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListAvailableIcons() (*Cache, error) {
|
||||||
iconsCahceMu.Lock()
|
iconsCahceMu.Lock()
|
||||||
defer iconsCahceMu.Unlock()
|
defer iconsCahceMu.Unlock()
|
||||||
|
|
||||||
if time.Since(lastUpdate) < updateInterval {
|
if time.Since(lastUpdate) < updateInterval {
|
||||||
if len(iconsCache) > 0 {
|
if !iconsCache.isEmpty() {
|
||||||
return iconsCache, nil
|
return iconsCache, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
icons, err := getIcons()
|
icons, err := fetchIconData()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
iconsCache = icons
|
iconsCache = icons
|
||||||
lastUpdate = time.Now()
|
lastUpdate = time.Now()
|
||||||
|
|
||||||
|
err = utils.SaveJSON(common.IconListCachePath, iconsCache, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
logging.Warn().Err(err).Msg("failed to save icon list cache")
|
||||||
|
}
|
||||||
return icons, nil
|
return icons, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func HasIcon(name string, filetype string) bool {
|
func HasWalkxCodeIcon(name string, filetype string) bool {
|
||||||
icons, err := ListAvailableIcons()
|
icons, err := ListAvailableIcons()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logging.Error().Err(err).Msg("failed to list icons")
|
logging.Error().Err(err).Msg("failed to list icons")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if _, ok := icons[filetype]; !ok {
|
if _, ok := icons.WalkxCode[filetype]; !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
_, ok := icons[filetype][name+"."+filetype]
|
_, ok := icons.WalkxCode[filetype][name+"."+filetype]
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func HasSelfhstIcon(name string, filetype string) bool {
|
||||||
|
icons, err := ListAvailableIcons()
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).Msg("failed to list icons")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, ok := icons.Selfhst[filetype]; !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := icons.Selfhst[filetype][name+"."+filetype]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDisplayName(reference string) (string, bool) {
|
||||||
|
icons, err := ListAvailableIcons()
|
||||||
|
if err != nil {
|
||||||
|
logging.Error().Err(err).Msg("failed to list icons")
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
displayName, ok := icons.DisplayNames[reference]
|
||||||
|
return displayName, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchIconData() (*Cache, error) {
|
||||||
|
walkxCodeIcons, err := fetchWalkxCodeIcons()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
selfhstIcons, referenceToNames, err := fetchSelfhstIcons()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Cache{
|
||||||
|
WalkxCode: walkxCodeIcons,
|
||||||
|
Selfhst: selfhstIcons,
|
||||||
|
DisplayNames: referenceToNames,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
format:
|
format:
|
||||||
|
|
||||||
|
@ -74,10 +160,13 @@ format:
|
||||||
],
|
],
|
||||||
"svg": [
|
"svg": [
|
||||||
"*.svg",
|
"*.svg",
|
||||||
|
],
|
||||||
|
"webp": [
|
||||||
|
"*.webp",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
func getIcons() (Icons, error) {
|
func fetchWalkxCodeIcons() (IconsMap, error) {
|
||||||
req, err := http.NewRequest(http.MethodGet, walkxcodeIcons, nil)
|
req, err := http.NewRequest(http.MethodGet, walkxcodeIcons, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -98,7 +187,7 @@ func getIcons() (Icons, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
icons := make(Icons, len(data))
|
icons := make(IconsMap, len(data))
|
||||||
for fileType, files := range data {
|
for fileType, files := range data {
|
||||||
icons[fileType] = make(map[string]struct{}, len(files))
|
icons[fileType] = make(map[string]struct{}, len(files))
|
||||||
for _, icon := range files {
|
for _, icon := range files {
|
||||||
|
@ -107,3 +196,73 @@ func getIcons() (Icons, error) {
|
||||||
}
|
}
|
||||||
return icons, nil
|
return icons, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
format:
|
||||||
|
|
||||||
|
{
|
||||||
|
"Name": "2FAuth",
|
||||||
|
"Reference": "2fauth",
|
||||||
|
"SVG": "Yes",
|
||||||
|
"PNG": "Yes",
|
||||||
|
"WebP": "Yes",
|
||||||
|
"Light": "Yes",
|
||||||
|
"Category": "Self-Hosted",
|
||||||
|
"CreatedAt": "2024-08-16 00:27:23+00:00"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
func fetchSelfhstIcons() (IconsMap, ReferenceDisplayNameMap, error) {
|
||||||
|
type SelfhStIcon struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
Reference string `json:"Reference"`
|
||||||
|
SVG string `json:"SVG"`
|
||||||
|
PNG string `json:"PNG"`
|
||||||
|
WebP string `json:"WebP"`
|
||||||
|
// Light string
|
||||||
|
// Category string
|
||||||
|
// CreatedAt string
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, selfhstIcons, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := make([]SelfhStIcon, 0, 2000)
|
||||||
|
err = json.Unmarshal(body, &data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
icons := make(IconsMap)
|
||||||
|
icons["svg"] = make(map[string]struct{}, len(data))
|
||||||
|
icons["png"] = make(map[string]struct{}, len(data))
|
||||||
|
icons["webp"] = make(map[string]struct{}, len(data))
|
||||||
|
|
||||||
|
referenceToNames := make(ReferenceDisplayNameMap, len(data))
|
||||||
|
|
||||||
|
for _, item := range data {
|
||||||
|
if item.SVG == "Yes" {
|
||||||
|
icons["svg"][item.Reference+".svg"] = struct{}{}
|
||||||
|
}
|
||||||
|
if item.PNG == "Yes" {
|
||||||
|
icons["png"][item.Reference+".png"] = struct{}{}
|
||||||
|
}
|
||||||
|
if item.WebP == "Yes" {
|
||||||
|
icons["webp"][item.Reference+".webp"] = struct{}{}
|
||||||
|
}
|
||||||
|
referenceToNames[item.Reference] = item.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
return icons, referenceToNames, nil
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/yusing/go-proxy/internal"
|
||||||
"github.com/yusing/go-proxy/internal/homepage"
|
"github.com/yusing/go-proxy/internal/homepage"
|
||||||
"github.com/yusing/go-proxy/internal/route/entry"
|
"github.com/yusing/go-proxy/internal/route/entry"
|
||||||
provider "github.com/yusing/go-proxy/internal/route/provider/types"
|
provider "github.com/yusing/go-proxy/internal/route/provider/types"
|
||||||
|
@ -83,6 +84,14 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
|
||||||
}
|
}
|
||||||
|
|
||||||
if item.Name == "" {
|
if item.Name == "" {
|
||||||
|
reference := r.TargetName()
|
||||||
|
if r.RawEntry().Container != nil {
|
||||||
|
reference = r.RawEntry().Container.ImageName
|
||||||
|
}
|
||||||
|
name, ok := internal.GetDisplayName(reference)
|
||||||
|
if ok {
|
||||||
|
item.Name = name
|
||||||
|
} else {
|
||||||
item.Name = strutils.Title(
|
item.Name = strutils.Title(
|
||||||
strings.ReplaceAll(
|
strings.ReplaceAll(
|
||||||
strings.ReplaceAll(alias, "-", " "),
|
strings.ReplaceAll(alias, "-", " "),
|
||||||
|
@ -90,6 +99,7 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if useDefaultCategories {
|
if useDefaultCategories {
|
||||||
if en.Container != nil && item.Category == "" {
|
if en.Container != nil && item.Category == "" {
|
||||||
|
@ -128,7 +138,7 @@ func HomepageConfig(useDefaultCategories bool, categoryFilter, providerFilter st
|
||||||
}
|
}
|
||||||
|
|
||||||
item.AltURL = r.TargetURL().String()
|
item.AltURL = r.TargetURL().String()
|
||||||
hpCfg.Add(item)
|
hpCfg.Add(item.GetOverriddenItem())
|
||||||
})
|
})
|
||||||
return hpCfg
|
return hpCfg
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,9 @@ GoDoxy v0.8.2 expected changes
|
||||||
do: error 403 Forbidden
|
do: error 403 Forbidden
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **new** Brand new rewritten WebUI
|
||||||
|
- **new** Support selfh.st icons: `@selfhst/<reference>.<format>` _(e.g. `@selfhst/adguard-home.webp`)_
|
||||||
|
- also uses the display name on https://selfh.st/icons/ as default for our dashboard!
|
||||||
- **new** GoDoxy server side favicon retreiving and caching
|
- **new** GoDoxy server side favicon retreiving and caching
|
||||||
- deliver smooth dashboard experience by caching favicons
|
- deliver smooth dashboard experience by caching favicons
|
||||||
- correct icon can show without setting `homepage.icon` by parsing it from app's root path "/", selecting `link[rel=icon]` from HTML as default icon
|
- correct icon can show without setting `homepage.icon` by parsing it from app's root path "/", selecting `link[rel=icon]` from HTML as default icon
|
||||||
|
|
Loading…
Add table
Reference in a new issue