refactor(homepage): reorganize icons into dedicated package structure

Split the monolithic `internal/homepage` icons functionality into a structured package hierarchy:

- `internal/homepage/icons/` - Core types (URL, Key, Meta, Provider, Source, Variant)
- `internal/homepage/icons/fetch/` - Icon fetching logic (content.go, fetch.go, route.go)
- `internal/homepage/icons/list/` - Icon listing and search (list_icons.go, list_icons_test.go)

Moved icon-related code from `internal/homepage/`:
- `icon_url.go` → `icons/url.go` (+ url_test.go)
- `content.go` → `icons/fetch/content.go`
- `route.go` → `icons/fetch/route.go`
- `list_icons.go` → `icons/list/list_icons.go` (+ list_icons_test.go)

Updated all consumers to use the new package structure:
- `cmd/main.go`
- `internal/api/v1/favicon.go`
- `internal/api/v1/icons.go`
- `internal/idlewatcher/handle_http.go`
- `internal/route/route.go`
This commit is contained in:
yusing
2026-01-09 12:06:54 +08:00
parent dc1b70d2d7
commit 74f97a6621
17 changed files with 291 additions and 260 deletions

View File

@@ -10,7 +10,7 @@ import (
"github.com/yusing/godoxy/internal/common" "github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/config" "github.com/yusing/godoxy/internal/config"
"github.com/yusing/godoxy/internal/dnsproviders" "github.com/yusing/godoxy/internal/dnsproviders"
"github.com/yusing/godoxy/internal/homepage" iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
"github.com/yusing/godoxy/internal/logging" "github.com/yusing/godoxy/internal/logging"
"github.com/yusing/godoxy/internal/logging/memlogger" "github.com/yusing/godoxy/internal/logging/memlogger"
"github.com/yusing/godoxy/internal/metrics/systeminfo" "github.com/yusing/godoxy/internal/metrics/systeminfo"
@@ -39,7 +39,7 @@ func main() {
log.Trace().Msg("trace enabled") log.Trace().Msg("trace enabled")
parallel( parallel(
dnsproviders.InitProviders, dnsproviders.InitProviders,
homepage.InitIconListCache, iconlist.InitCache,
systeminfo.Poller.Start, systeminfo.Poller.Start,
middleware.LoadComposeFiles, middleware.LoadComposeFiles,
) )

View File

@@ -5,7 +5,8 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/homepage" "github.com/yusing/godoxy/internal/homepage/icons"
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
"github.com/yusing/godoxy/internal/route/routes" "github.com/yusing/godoxy/internal/route/routes"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
@@ -13,9 +14,9 @@ import (
) )
type GetFavIconRequest struct { type GetFavIconRequest struct {
URL string `form:"url" binding:"required_without=Alias"` URL string `form:"url" binding:"required_without=Alias"`
Alias string `form:"alias" binding:"required_without=URL"` Alias string `form:"alias" binding:"required_without=URL"`
Variant homepage.IconVariant `form:"variant" binding:"omitempty,oneof=light dark"` Variant icons.Variant `form:"variant" binding:"omitempty,oneof=light dark"`
} // @name GetFavIconRequest } // @name GetFavIconRequest
// @x-id "favicon" // @x-id "favicon"
@@ -42,18 +43,18 @@ func FavIcon(c *gin.Context) {
// try with url // try with url
if request.URL != "" { if request.URL != "" {
var iconURL homepage.IconURL var iconURL icons.URL
if err := iconURL.Parse(request.URL); err != nil { if err := iconURL.Parse(request.URL); err != nil {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid url", err)) c.JSON(http.StatusBadRequest, apitypes.Error("invalid url", err))
return return
} }
icon := &iconURL icon := &iconURL
if request.Variant != homepage.IconVariantNone { if request.Variant != icons.VariantNone {
icon = icon.WithVariant(request.Variant) icon = icon.WithVariant(request.Variant)
} }
fetchResult, err := homepage.FetchFavIconFromURL(c.Request.Context(), icon) fetchResult, err := iconfetch.FetchFavIconFromURL(c.Request.Context(), icon)
if err != nil { if err != nil {
homepage.GinFetchError(c, fetchResult.StatusCode, err) iconfetch.GinError(c, fetchResult.StatusCode, err)
return return
} }
c.Data(fetchResult.StatusCode, fetchResult.ContentType(), fetchResult.Icon) c.Data(fetchResult.StatusCode, fetchResult.ContentType(), fetchResult.Icon)
@@ -63,40 +64,40 @@ func FavIcon(c *gin.Context) {
// try with alias // try with alias
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias, request.Variant) result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias, request.Variant)
if err != nil { if err != nil {
homepage.GinFetchError(c, result.StatusCode, err) iconfetch.GinError(c, result.StatusCode, err)
return return
} }
c.Data(result.StatusCode, result.ContentType(), result.Icon) c.Data(result.StatusCode, result.ContentType(), result.Icon)
} }
//go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias //go:linkname GetFavIconFromAlias v1.GetFavIconFromAlias
func GetFavIconFromAlias(ctx context.Context, alias string, variant homepage.IconVariant) (homepage.FetchResult, error) { func GetFavIconFromAlias(ctx context.Context, alias string, variant icons.Variant) (iconfetch.Result, error) {
// try with route.Icon // try with route.Icon
r, ok := routes.HTTP.Get(alias) r, ok := routes.HTTP.Get(alias)
if !ok { if !ok {
return homepage.FetchResultWithErrorf(http.StatusNotFound, "route not found") return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
} }
var ( var (
result homepage.FetchResult result iconfetch.Result
err error err error
) )
hp := r.HomepageItem() hp := r.HomepageItem()
if hp.Icon != nil { if hp.Icon != nil {
if hp.Icon.IconSource == homepage.IconSourceRelative { if hp.Icon.Source == icons.SourceRelative {
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, variant) result, err = iconfetch.FindIcon(ctx, r, *hp.Icon.FullURL, variant)
} else if variant != homepage.IconVariantNone { } else if variant != icons.VariantNone {
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant)) result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant))
if err != nil { if err != nil {
// fallback to no variant // fallback to no variant
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(homepage.IconVariantNone)) result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(icons.VariantNone))
} }
} else { } else {
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon) result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon)
} }
} else { } else {
// try extract from "link[rel=icon]" // try extract from "link[rel=icon]"
result, err = homepage.FindIcon(ctx, r, "/", variant) result, err = iconfetch.FindIcon(ctx, r, "/", variant)
} }
if result.StatusCode == 0 { if result.StatusCode == 0 {
result.StatusCode = http.StatusOK result.StatusCode = http.StatusOK

View File

@@ -4,7 +4,7 @@ import (
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/homepage" iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
) )
@@ -32,6 +32,6 @@ func Icons(c *gin.Context) {
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err)) c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return return
} }
icons := homepage.SearchIcons(request.Keyword, request.Limit) icons := iconlist.SearchIcons(request.Keyword, request.Limit)
c.JSON(http.StatusOK, icons) c.JSON(http.StatusOK, icons)
} }

View File

@@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/yusing/ds/ordered" "github.com/yusing/ds/ordered"
"github.com/yusing/godoxy/internal/homepage/icons"
"github.com/yusing/godoxy/internal/homepage/widgets" "github.com/yusing/godoxy/internal/homepage/widgets"
"github.com/yusing/godoxy/internal/serialization" "github.com/yusing/godoxy/internal/serialization"
strutils "github.com/yusing/goutils/strings" strutils "github.com/yusing/goutils/strings"
@@ -22,13 +23,13 @@ type (
} // @name HomepageCategory } // @name HomepageCategory
ItemConfig struct { ItemConfig struct {
Show bool `json:"show"` Show bool `json:"show"`
Name string `json:"name"` // display name Name string `json:"name"` // display name
Icon *IconURL `json:"icon" swaggertype:"string"` Icon *icons.URL `json:"icon" swaggertype:"string"`
Category string `json:"category" validate:"omitempty"` Category string `json:"category" validate:"omitempty"`
Description string `json:"description" aliases:"desc"` Description string `json:"description" aliases:"desc"`
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
Favorite bool `json:"favorite"` Favorite bool `json:"favorite"`
WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget" extensions:"x-nullable"` WidgetConfig *widgets.Config `json:"widget_config,omitempty" aliases:"widget" extensions:"x-nullable"`
} // @name HomepageItemConfig } // @name HomepageItemConfig

View File

@@ -4,19 +4,24 @@ import (
"testing" "testing"
. "github.com/yusing/godoxy/internal/homepage" . "github.com/yusing/godoxy/internal/homepage"
"github.com/yusing/godoxy/internal/homepage/icons"
expect "github.com/yusing/goutils/testing" expect "github.com/yusing/goutils/testing"
) )
func strPtr(s string) *string {
return &s
}
func TestOverrideItem(t *testing.T) { func TestOverrideItem(t *testing.T) {
a := &Item{ a := &Item{
Alias: "foo", Alias: "foo",
ItemConfig: ItemConfig{ ItemConfig: ItemConfig{
Show: false, Show: false,
Name: "Foo", Name: "Foo",
Icon: &IconURL{ Icon: &icons.URL{
FullURL: strPtr("/favicon.ico"), FullURL: strPtr("/favicon.ico"),
IconSource: IconSourceRelative, Source: icons.SourceRelative,
}, },
Category: "App", Category: "App",
}, },
@@ -25,9 +30,9 @@ func TestOverrideItem(t *testing.T) {
Show: true, Show: true,
Name: "Bar", Name: "Bar",
Category: "Test", Category: "Test",
Icon: &IconURL{ Icon: &icons.URL{
FullURL: strPtr("@walkxcode/example.png"), FullURL: strPtr("@walkxcode/example.png"),
IconSource: IconSourceWalkXCode, Source: icons.SourceWalkXCode,
}, },
} }
overrides := GetOverrideConfig() overrides := GetOverrideConfig()

View File

@@ -1,4 +1,4 @@
package homepage package iconfetch
import ( import (
"bufio" "bufio"

View File

@@ -1,4 +1,4 @@
package homepage package iconfetch
import ( import (
"bytes" "bytes"
@@ -15,6 +15,7 @@ import (
"github.com/PuerkitoBio/goquery" "github.com/PuerkitoBio/goquery"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/vincent-petithory/dataurl" "github.com/vincent-petithory/dataurl"
"github.com/yusing/godoxy/internal/homepage/icons"
gphttp "github.com/yusing/godoxy/internal/net/gphttp" gphttp "github.com/yusing/godoxy/internal/net/gphttp"
apitypes "github.com/yusing/goutils/apitypes" apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/cache" "github.com/yusing/goutils/cache"
@@ -22,22 +23,22 @@ import (
strutils "github.com/yusing/goutils/strings" strutils "github.com/yusing/goutils/strings"
) )
type FetchResult struct { type Result struct {
Icon []byte Icon []byte
StatusCode int StatusCode int
contentType string contentType string
} }
func FetchResultWithErrorf(statusCode int, msgFmt string, args ...any) (FetchResult, error) { func FetchResultWithErrorf(statusCode int, msgFmt string, args ...any) (Result, error) {
return FetchResult{StatusCode: statusCode}, fmt.Errorf(msgFmt, args...) return Result{StatusCode: statusCode}, fmt.Errorf(msgFmt, args...)
} }
func FetchResultOK(icon []byte, contentType string) (FetchResult, error) { func FetchResultOK(icon []byte, contentType string) (Result, error) {
return FetchResult{Icon: icon, contentType: contentType}, nil return Result{Icon: icon, contentType: contentType}, nil
} }
func GinFetchError(c *gin.Context, statusCode int, err error) { func GinError(c *gin.Context, statusCode int, err error) {
if statusCode == 0 { if statusCode == 0 {
statusCode = http.StatusInternalServerError statusCode = http.StatusInternalServerError
} }
@@ -50,7 +51,7 @@ func GinFetchError(c *gin.Context, statusCode int, err error) {
const faviconFetchTimeout = 3 * time.Second const faviconFetchTimeout = 3 * time.Second
func (res *FetchResult) ContentType() string { func (res *Result) ContentType() string {
if res.contentType == "" { if res.contentType == "" {
if bytes.HasPrefix(res.Icon, []byte("<svg")) || bytes.HasPrefix(res.Icon, []byte("<?xml")) { if bytes.HasPrefix(res.Icon, []byte("<svg")) || bytes.HasPrefix(res.Icon, []byte("<?xml")) {
return "image/svg+xml" return "image/svg+xml"
@@ -62,19 +63,19 @@ func (res *FetchResult) ContentType() string {
const maxRedirectDepth = 5 const maxRedirectDepth = 5
func FetchFavIconFromURL(ctx context.Context, iconURL *IconURL) (FetchResult, error) { func FetchFavIconFromURL(ctx context.Context, iconURL *icons.URL) (Result, error) {
switch iconURL.IconSource { switch iconURL.Source {
case IconSourceAbsolute: case icons.SourceAbsolute:
return FetchIconAbsolute(ctx, iconURL.URL()) return FetchIconAbsolute(ctx, iconURL.URL())
case IconSourceRelative: case icons.SourceRelative:
return FetchResultWithErrorf(http.StatusBadRequest, "unexpected relative icon") return FetchResultWithErrorf(http.StatusBadRequest, "unexpected relative icon")
case IconSourceWalkXCode, IconSourceSelfhSt: case icons.SourceWalkXCode, icons.SourceSelfhSt:
return fetchKnownIcon(ctx, iconURL) return fetchKnownIcon(ctx, iconURL)
} }
return FetchResultWithErrorf(http.StatusBadRequest, "invalid icon source") return FetchResultWithErrorf(http.StatusBadRequest, "invalid icon source")
} }
var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (FetchResult, error) { var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (Result, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { if err != nil {
return FetchResultWithErrorf(http.StatusInternalServerError, "cannot create request: %w", err) return FetchResultWithErrorf(http.StatusInternalServerError, "cannot create request: %w", err)
@@ -103,7 +104,7 @@ var FetchIconAbsolute = cache.NewKeyFunc(func(ctx context.Context, url string) (
return FetchResultWithErrorf(http.StatusNotFound, "empty icon") return FetchResultWithErrorf(http.StatusNotFound, "empty icon")
} }
res := FetchResult{Icon: icon} res := Result{Icon: icon}
if contentType := resp.Header.Get("Content-Type"); contentType != "" { if contentType := resp.Header.Get("Content-Type"); contentType != "" {
res.contentType = contentType res.contentType = contentType
} }
@@ -122,22 +123,22 @@ func sanitizeName(name string) string {
return strings.ToLower(nameSanitizer.Replace(name)) return strings.ToLower(nameSanitizer.Replace(name))
} }
func fetchKnownIcon(ctx context.Context, url *IconURL) (FetchResult, error) { func fetchKnownIcon(ctx context.Context, url *icons.URL) (Result, error) {
// if icon isn't in the list, no need to fetch // if icon isn't in the list, no need to fetch
if !url.HasIcon() { if !url.HasIcon() {
return FetchResult{StatusCode: http.StatusNotFound}, errors.New("no such icon") return Result{StatusCode: http.StatusNotFound}, errors.New("no such icon")
} }
return FetchIconAbsolute(ctx, url.URL()) return FetchIconAbsolute(ctx, url.URL())
} }
func fetchIcon(ctx context.Context, filename string) (FetchResult, error) { func fetchIcon(ctx context.Context, filename string) (Result, error) {
for _, fileType := range []string{"svg", "webp", "png"} { for _, fileType := range []string{"svg", "webp", "png"} {
result, err := fetchKnownIcon(ctx, NewSelfhStIconURL(filename, fileType)) result, err := fetchKnownIcon(ctx, icons.NewURL(icons.SourceSelfhSt, filename, fileType))
if err == nil { if err == nil {
return result, err return result, err
} }
result, err = fetchKnownIcon(ctx, NewWalkXCodeIconURL(filename, fileType)) result, err = fetchKnownIcon(ctx, icons.NewURL(icons.SourceWalkXCode, filename, fileType))
if err == nil { if err == nil {
return result, err return result, err
} }
@@ -150,10 +151,10 @@ type contextValue struct {
uri string uri string
} }
func FindIcon(ctx context.Context, r route, uri string, variant IconVariant) (FetchResult, error) { func FindIcon(ctx context.Context, r route, uri string, variant icons.Variant) (Result, error) {
for _, ref := range r.References() { for _, ref := range r.References() {
ref = sanitizeName(ref) ref = sanitizeName(ref)
if variant != IconVariantNone { if variant != icons.VariantNone {
ref += "-" + string(variant) ref += "-" + string(variant)
} }
result, err := fetchIcon(ctx, ref) result, err := fetchIcon(ctx, ref)
@@ -168,12 +169,12 @@ func FindIcon(ctx context.Context, r route, uri string, variant IconVariant) (Fe
return FetchResultWithErrorf(http.StatusNotFound, "no icon found") return FetchResultWithErrorf(http.StatusNotFound, "no icon found")
} }
var findIconSlowCached = cache.NewKeyFunc(func(ctx context.Context, key string) (FetchResult, error) { var findIconSlowCached = cache.NewKeyFunc(func(ctx context.Context, key string) (Result, error) {
v := ctx.Value("route").(contextValue) v := ctx.Value("route").(contextValue)
return findIconSlow(ctx, v.r, v.uri, nil) return findIconSlow(ctx, v.r, v.uri, nil)
}).WithMaxEntries(200).Build() // no retries, no ttl }).WithMaxEntries(200).Build() // no retries, no ttl
func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string) (FetchResult, error) { func findIconSlow(ctx context.Context, r httpRoute, uri string, stack []string) (Result, error) {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return FetchResultWithErrorf(http.StatusBadGateway, "request timeout") return FetchResultWithErrorf(http.StatusBadGateway, "request timeout")

View File

@@ -1,4 +1,4 @@
package homepage package iconfetch
import ( import (
"net/http" "net/http"

View File

@@ -0,0 +1,17 @@
package icons
import (
"fmt"
"strings"
)
type Key string
func NewKey(source Source, reference string) Key {
return Key(fmt.Sprintf("%s/%s", source, reference))
}
func (k Key) SourceRef() (Source, string) {
source, ref, _ := strings.Cut(string(k), "/")
return Source(source), ref
}

View File

@@ -1,8 +1,7 @@
package homepage package iconlist
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"slices" "slices"
"strings" "strings"
@@ -12,6 +11,7 @@ import (
"github.com/lithammer/fuzzysearch/fuzzy" "github.com/lithammer/fuzzysearch/fuzzy"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/yusing/godoxy/internal/common" "github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/homepage/icons"
"github.com/yusing/godoxy/internal/serialization" "github.com/yusing/godoxy/internal/serialization"
httputils "github.com/yusing/goutils/http" httputils "github.com/yusing/goutils/http"
"github.com/yusing/goutils/intern" "github.com/yusing/goutils/intern"
@@ -21,60 +21,19 @@ import (
) )
type ( type (
IconKey string IconMap map[icons.Key]*icons.Meta
IconMap map[IconKey]*IconMeta
IconList []string IconList []string
IconMeta struct {
SVG bool `json:"SVG"`
PNG bool `json:"PNG"`
WebP bool `json:"WebP"`
Light bool `json:"Light"`
Dark bool `json:"Dark"`
DisplayName string `json:"-"`
Tag string `json:"-"`
}
IconMetaSearch struct {
*IconMeta
Source IconSource `json:"Source"` IconMetaSearch struct {
Ref string `json:"Ref"` *icons.Meta
Source icons.Source `json:"Source"`
Ref string `json:"Ref"`
rank int rank int
} }
) )
func (icon *IconMeta) Filenames(ref string) []string {
filenames := make([]string, 0)
if icon.SVG {
filenames = append(filenames, ref+".svg")
if icon.Light {
filenames = append(filenames, ref+"-light.svg")
}
if icon.Dark {
filenames = append(filenames, ref+"-dark.svg")
}
}
if icon.PNG {
filenames = append(filenames, ref+".png")
if icon.Light {
filenames = append(filenames, ref+"-light.png")
}
if icon.Dark {
filenames = append(filenames, ref+"-dark.png")
}
}
if icon.WebP {
filenames = append(filenames, ref+".webp")
if icon.Light {
filenames = append(filenames, ref+"-light.webp")
}
if icon.Dark {
filenames = append(filenames, ref+"-dark.webp")
}
}
return filenames
}
const updateInterval = 2 * time.Hour const updateInterval = 2 * time.Hour
var iconsCache synk.Value[IconMap] var iconsCache synk.Value[IconMap]
@@ -84,16 +43,7 @@ const (
selfhstIcons = "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/index.json" selfhstIcons = "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/index.json"
) )
func NewIconKey(source IconSource, reference string) IconKey { func InitCache() {
return IconKey(fmt.Sprintf("%s/%s", source, reference))
}
func (k IconKey) SourceRef() (IconSource, string) {
source, ref, _ := strings.Cut(string(k), "/")
return IconSource(source), ref
}
func InitIconListCache() {
m := make(IconMap) m := make(IconMap)
err := serialization.LoadJSONIfExist(common.IconListCachePath, &m) err := serialization.LoadJSONIfExist(common.IconListCachePath, &m)
if err != nil { if err != nil {
@@ -196,10 +146,10 @@ func SearchIcons(keyword string, limit int) []*IconMetaSearch {
source, ref := k.SourceRef() source, ref := k.SourceRef()
ranked := &IconMetaSearch{ ranked := &IconMetaSearch{
Source: source, Source: source,
Ref: ref, Ref: ref,
IconMeta: icon, Meta: icon,
rank: rank, rank: rank,
} }
// Sorted insert based on rank (lower rank = better match) // Sorted insert based on rank (lower rank = better match)
insertPos, _ := slices.BinarySearchFunc(results, ranked, sortByRank) insertPos, _ := slices.BinarySearchFunc(results, ranked, sortByRank)
@@ -213,7 +163,7 @@ func SearchIcons(keyword string, limit int) []*IconMetaSearch {
return results[:min(len(results), limit)] return results[:min(len(results), limit)]
} }
func HasIcon(icon *IconURL) bool { func HasIcon(icon *icons.URL) bool {
if icon.Extra == nil { if icon.Extra == nil {
return false return false
} }
@@ -241,11 +191,11 @@ type HomepageMeta struct {
Tag string Tag string
} }
func GetHomepageMeta(ref string) (HomepageMeta, bool) { func GetMetadata(ref string) (HomepageMeta, bool) {
meta, ok := ListAvailableIcons()[NewIconKey(IconSourceSelfhSt, ref)] meta, ok := ListAvailableIcons()[icons.NewKey(icons.SourceSelfhSt, ref)]
// these info is not available in walkxcode // these info is not available in walkxcode
// if !ok { // if !ok {
// meta, ok = iconsCache.Icons[NewIconKey(IconSourceWalkXCode, ref)] // meta, ok = iconsCache.Icons[icons.NewIconKey(icons.IconSourceWalkXCode, ref)]
// } // }
if !ok { if !ok {
return HomepageMeta{}, false return HomepageMeta{}, false
@@ -317,14 +267,14 @@ func UpdateWalkxCodeIcons(m IconMap) error {
} }
for fileType, files := range data { for fileType, files := range data {
var setExt func(icon *IconMeta) var setExt func(icon *icons.Meta)
switch fileType { switch fileType {
case "png": case "png":
setExt = func(icon *IconMeta) { icon.PNG = true } setExt = func(icon *icons.Meta) { icon.PNG = true }
case "svg": case "svg":
setExt = func(icon *IconMeta) { icon.SVG = true } setExt = func(icon *icons.Meta) { icon.SVG = true }
case "webp": case "webp":
setExt = func(icon *IconMeta) { icon.WebP = true } setExt = func(icon *icons.Meta) { icon.WebP = true }
} }
for _, f := range files { for _, f := range files {
f = strings.TrimSuffix(f, "."+fileType) f = strings.TrimSuffix(f, "."+fileType)
@@ -336,10 +286,10 @@ func UpdateWalkxCodeIcons(m IconMap) error {
if isDark { if isDark {
f = strings.TrimSuffix(f, "-dark") f = strings.TrimSuffix(f, "-dark")
} }
key := NewIconKey(IconSourceWalkXCode, f) key := icons.NewKey(icons.SourceWalkXCode, f)
icon, ok := m[key] icon, ok := m[key]
if !ok { if !ok {
icon = new(IconMeta) icon = new(icons.Meta)
m[key] = icon m[key] = icon
} }
setExt(icon) setExt(icon)
@@ -401,7 +351,7 @@ func UpdateSelfhstIcons(m IconMap) error {
tag, _, _ = strings.Cut(item.Tags, ",") tag, _, _ = strings.Cut(item.Tags, ",")
tag = strings.TrimSpace(tag) tag = strings.TrimSpace(tag)
} }
icon := &IconMeta{ icon := &icons.Meta{
DisplayName: item.Name, DisplayName: item.Name,
Tag: intern.Make(tag).Value(), Tag: intern.Make(tag).Value(),
SVG: item.SVG == "Yes", SVG: item.SVG == "Yes",
@@ -410,7 +360,7 @@ func UpdateSelfhstIcons(m IconMap) error {
Light: item.Light == "Yes", Light: item.Light == "Yes",
Dark: item.Dark == "Yes", Dark: item.Dark == "Yes",
} }
key := NewIconKey(IconSourceSelfhSt, item.Reference) key := icons.NewKey(icons.SourceSelfhSt, item.Reference)
m[key] = icon m[key] = icon
} }
return nil return nil

View File

@@ -1,9 +1,10 @@
package homepage_test package iconlist_test
import ( import (
"testing" "testing"
. "github.com/yusing/godoxy/internal/homepage" . "github.com/yusing/godoxy/internal/homepage/icons"
. "github.com/yusing/godoxy/internal/homepage/icons/list"
) )
const walkxcodeIcons = `{ const walkxcodeIcons = `{
@@ -69,8 +70,8 @@ const selfhstIcons = `[
]` ]`
type testCases struct { type testCases struct {
Key IconKey Key Key
IconMeta Meta
} }
func runTests(t *testing.T, iconsCache IconMap, test []testCases) { func runTests(t *testing.T, iconsCache IconMap, test []testCases) {
@@ -109,8 +110,8 @@ func TestListWalkxCodeIcons(t *testing.T) {
} }
test := []testCases{ test := []testCases{
{ {
Key: NewIconKey(IconSourceWalkXCode, "app1"), Key: NewKey(SourceWalkXCode, "app1"),
IconMeta: IconMeta{ Meta: Meta{
SVG: true, SVG: true,
PNG: true, PNG: true,
WebP: true, WebP: true,
@@ -118,15 +119,15 @@ func TestListWalkxCodeIcons(t *testing.T) {
}, },
}, },
{ {
Key: NewIconKey(IconSourceWalkXCode, "app2"), Key: NewKey(SourceWalkXCode, "app2"),
IconMeta: IconMeta{ Meta: Meta{
PNG: true, PNG: true,
WebP: true, WebP: true,
}, },
}, },
{ {
Key: NewIconKey(IconSourceWalkXCode, "karakeep"), Key: NewKey(SourceWalkXCode, "karakeep"),
IconMeta: IconMeta{ Meta: Meta{
SVG: true, SVG: true,
PNG: true, PNG: true,
WebP: true, WebP: true,
@@ -149,8 +150,8 @@ func TestListSelfhstIcons(t *testing.T) {
} }
test := []testCases{ test := []testCases{
{ {
Key: NewIconKey(IconSourceSelfhSt, "2fauth"), Key: NewKey(SourceSelfhSt, "2fauth"),
IconMeta: IconMeta{ Meta: Meta{
SVG: true, SVG: true,
PNG: true, PNG: true,
WebP: true, WebP: true,
@@ -160,16 +161,16 @@ func TestListSelfhstIcons(t *testing.T) {
}, },
}, },
{ {
Key: NewIconKey(IconSourceSelfhSt, "dittofeed"), Key: NewKey(SourceSelfhSt, "dittofeed"),
IconMeta: IconMeta{ Meta: Meta{
PNG: true, PNG: true,
WebP: true, WebP: true,
DisplayName: "Dittofeed", DisplayName: "Dittofeed",
}, },
}, },
{ {
Key: NewIconKey(IconSourceSelfhSt, "ars-technica"), Key: NewKey(SourceSelfhSt, "ars-technica"),
IconMeta: IconMeta{ Meta: Meta{
SVG: true, SVG: true,
PNG: true, PNG: true,
WebP: true, WebP: true,

View File

@@ -0,0 +1,43 @@
package icons
type Meta struct {
SVG bool `json:"SVG"`
PNG bool `json:"PNG"`
WebP bool `json:"WebP"`
Light bool `json:"Light"`
Dark bool `json:"Dark"`
DisplayName string `json:"-"`
Tag string `json:"-"`
}
func (icon *Meta) Filenames(ref string) []string {
filenames := make([]string, 0)
if icon.SVG {
filenames = append(filenames, ref+".svg")
if icon.Light {
filenames = append(filenames, ref+"-light.svg")
}
if icon.Dark {
filenames = append(filenames, ref+"-dark.svg")
}
}
if icon.PNG {
filenames = append(filenames, ref+".png")
if icon.Light {
filenames = append(filenames, ref+"-light.png")
}
if icon.Dark {
filenames = append(filenames, ref+"-dark.png")
}
}
if icon.WebP {
filenames = append(filenames, ref+".webp")
if icon.Light {
filenames = append(filenames, ref+"-light.webp")
}
if icon.Dark {
filenames = append(filenames, ref+"-dark.webp")
}
}
return filenames
}

View File

@@ -0,0 +1,21 @@
package icons
import "sync/atomic"
type Provider interface {
HasIcon(u *URL) bool
}
var provider atomic.Value
func SetProvider(p Provider) {
provider.Store(p)
}
func hasIcon(u *URL) bool {
v := provider.Load()
if v == nil {
return false
}
return v.(Provider).HasIcon(u)
}

View File

@@ -1,4 +1,4 @@
package homepage package icons
import ( import (
"fmt" "fmt"
@@ -8,43 +8,43 @@ import (
) )
type ( type (
IconURL struct { URL struct {
IconSource `json:"source"` Source `json:"source"`
FullURL *string `json:"value,omitempty"` // only for absolute/relative icons FullURL *string `json:"value,omitempty"` // only for absolute/relative icons
Extra *IconExtra `json:"extra,omitempty"` // only for walkxcode/selfhst icons Extra *Extra `json:"extra,omitempty"` // only for walkxcode/selfhst icons
} }
IconExtra struct { Extra struct {
Key IconKey `json:"key"` Key Key `json:"key"`
Ref string `json:"ref"` Ref string `json:"ref"`
FileType string `json:"file_type"` FileType string `json:"file_type"`
IsLight bool `json:"is_light"` IsLight bool `json:"is_light"`
IsDark bool `json:"is_dark"` IsDark bool `json:"is_dark"`
} }
IconSource string Source string
IconVariant string Variant string
) )
const ( const (
IconSourceAbsolute IconSource = "https://" SourceAbsolute Source = "https://"
IconSourceRelative IconSource = "@target" SourceRelative Source = "@target"
IconSourceWalkXCode IconSource = "@walkxcode" SourceWalkXCode Source = "@walkxcode"
IconSourceSelfhSt IconSource = "@selfhst" SourceSelfhSt Source = "@selfhst"
) )
const ( const (
IconVariantNone IconVariant = "" VariantNone Variant = ""
IconVariantLight IconVariant = "light" VariantLight Variant = "light"
IconVariantDark IconVariant = "dark" VariantDark Variant = "dark"
) )
var ErrInvalidIconURL = gperr.New("invalid icon url") var ErrInvalidIconURL = gperr.New("invalid icon url")
func NewIconURL(source IconSource, refOrName, format string) *IconURL { func NewURL(source Source, refOrName, format string) *URL {
switch source { switch source {
case IconSourceWalkXCode, IconSourceSelfhSt: case SourceWalkXCode, SourceSelfhSt:
default: default:
panic("invalid icon source") panic("invalid icon source")
} }
@@ -56,10 +56,10 @@ func NewIconURL(source IconSource, refOrName, format string) *IconURL {
isDark = true isDark = true
refOrName = strings.TrimSuffix(refOrName, "-dark") refOrName = strings.TrimSuffix(refOrName, "-dark")
} }
return &IconURL{ return &URL{
IconSource: source, Source: source,
Extra: &IconExtra{ Extra: &Extra{
Key: NewIconKey(source, refOrName), Key: NewKey(source, refOrName),
FileType: format, FileType: format,
Ref: refOrName, Ref: refOrName,
IsLight: isLight, IsLight: isLight,
@@ -68,53 +68,42 @@ func NewIconURL(source IconSource, refOrName, format string) *IconURL {
} }
} }
func NewSelfhStIconURL(refOrName, format string) *IconURL { func (u *URL) HasIcon() bool {
return NewIconURL(IconSourceSelfhSt, refOrName, format) return hasIcon(u)
} }
func NewWalkXCodeIconURL(name, format string) *IconURL { func (u *URL) WithVariant(variant Variant) *URL {
return NewIconURL(IconSourceWalkXCode, name, format) switch u.Source {
} case SourceWalkXCode, SourceSelfhSt:
// HasIcon checks if the icon referenced by the IconURL exists in the cache based on its source.
// Returns false if the icon does not exist for IconSourceSelfhSt or IconSourceWalkXCode,
// otherwise returns true.
func (u *IconURL) HasIcon() bool {
return HasIcon(u)
}
func (u *IconURL) WithVariant(variant IconVariant) *IconURL {
switch u.IconSource {
case IconSourceWalkXCode, IconSourceSelfhSt:
default: default:
return u // no variant for absolute/relative icons return u // no variant for absolute/relative icons
} }
var extra *IconExtra var extra *Extra
if u.Extra != nil { if u.Extra != nil {
extra = &IconExtra{ extra = &Extra{
Key: u.Extra.Key, Key: u.Extra.Key,
Ref: u.Extra.Ref, Ref: u.Extra.Ref,
FileType: u.Extra.FileType, FileType: u.Extra.FileType,
IsLight: variant == IconVariantLight, IsLight: variant == VariantLight,
IsDark: variant == IconVariantDark, IsDark: variant == VariantDark,
} }
extra.Ref = strings.TrimSuffix(extra.Ref, "-light") extra.Ref = strings.TrimSuffix(extra.Ref, "-light")
extra.Ref = strings.TrimSuffix(extra.Ref, "-dark") extra.Ref = strings.TrimSuffix(extra.Ref, "-dark")
} }
return &IconURL{ return &URL{
IconSource: u.IconSource, Source: u.Source,
FullURL: u.FullURL, FullURL: u.FullURL,
Extra: extra, Extra: extra,
} }
} }
// Parse implements strutils.Parser. // Parse implements strutils.Parser.
func (u *IconURL) Parse(v string) error { func (u *URL) Parse(v string) error {
return u.parse(v, true) return u.parse(v, true)
} }
func (u *IconURL) parse(v string, checkExists bool) error { func (u *URL) parse(v string, checkExists bool) error {
if v == "" { if v == "" {
return ErrInvalidIconURL return ErrInvalidIconURL
} }
@@ -126,19 +115,19 @@ func (u *IconURL) parse(v string, checkExists bool) error {
switch beforeSlash { switch beforeSlash {
case "http:", "https:": case "http:", "https:":
u.FullURL = &v u.FullURL = &v
u.IconSource = IconSourceAbsolute u.Source = SourceAbsolute
case "@target", "": // @target/favicon.ico, /favicon.ico case "@target", "": // @target/favicon.ico, /favicon.ico
url := v[slashIndex:] url := v[slashIndex:]
if url == "/" { if url == "/" {
return ErrInvalidIconURL.Withf("%s", "empty path") return ErrInvalidIconURL.Withf("%s", "empty path")
} }
u.FullURL = &url u.FullURL = &url
u.IconSource = IconSourceRelative u.Source = SourceRelative
case "@selfhst", "@walkxcode": // selfh.st / walkxcode Icons, @selfhst/<reference>.<format> case "@selfhst", "@walkxcode": // selfh.st / walkxcode Icons, @selfhst/<reference>.<format>
if beforeSlash == "@selfhst" { if beforeSlash == "@selfhst" {
u.IconSource = IconSourceSelfhSt u.Source = SourceSelfhSt
} else { } else {
u.IconSource = IconSourceWalkXCode u.Source = SourceWalkXCode
} }
parts := strings.Split(v[slashIndex+1:], ".") parts := strings.Split(v[slashIndex+1:], ".")
if len(parts) != 2 { if len(parts) != 2 {
@@ -161,15 +150,15 @@ func (u *IconURL) parse(v string, checkExists bool) error {
isDark = true isDark = true
reference = strings.TrimSuffix(reference, "-dark") reference = strings.TrimSuffix(reference, "-dark")
} }
u.Extra = &IconExtra{ u.Extra = &Extra{
Key: NewIconKey(u.IconSource, reference), Key: NewKey(u.Source, reference),
FileType: format, FileType: format,
Ref: reference, Ref: reference,
IsLight: isLight, IsLight: isLight,
IsDark: isDark, IsDark: isDark,
} }
if checkExists && !u.HasIcon() { if checkExists && !u.HasIcon() {
return ErrInvalidIconURL.Withf("no such icon %s.%s from %s", reference, format, u.IconSource) return ErrInvalidIconURL.Withf("no such icon %s.%s from %s", reference, format, u.Source)
} }
default: default:
return ErrInvalidIconURL.Subject(v) return ErrInvalidIconURL.Subject(v)
@@ -178,7 +167,7 @@ func (u *IconURL) parse(v string, checkExists bool) error {
return nil return nil
} }
func (u *IconURL) URL() string { func (u *URL) URL() string {
if u.FullURL != nil { if u.FullURL != nil {
return *u.FullURL return *u.FullURL
} }
@@ -191,16 +180,16 @@ func (u *IconURL) URL() string {
} else if u.Extra.IsDark { } else if u.Extra.IsDark {
filename += "-dark" filename += "-dark"
} }
switch u.IconSource { switch u.Source {
case IconSourceWalkXCode: case SourceWalkXCode:
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/%s/%s.%s", u.Extra.FileType, filename, u.Extra.FileType) return fmt.Sprintf("https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/%s/%s.%s", u.Extra.FileType, filename, u.Extra.FileType)
case IconSourceSelfhSt: case SourceSelfhSt:
return fmt.Sprintf("https://cdn.jsdelivr.net/gh/selfhst/icons/%s/%s.%s", u.Extra.FileType, filename, u.Extra.FileType) return fmt.Sprintf("https://cdn.jsdelivr.net/gh/selfhst/icons/%s/%s.%s", u.Extra.FileType, filename, u.Extra.FileType)
} }
return "" return ""
} }
func (u *IconURL) String() string { func (u *URL) String() string {
if u.FullURL != nil { if u.FullURL != nil {
return *u.FullURL return *u.FullURL
} }
@@ -213,14 +202,14 @@ func (u *IconURL) String() string {
} else if u.Extra.IsDark { } else if u.Extra.IsDark {
suffix = "-dark" suffix = "-dark"
} }
return fmt.Sprintf("%s/%s%s.%s", u.IconSource, u.Extra.Ref, suffix, u.Extra.FileType) return fmt.Sprintf("%s/%s%s.%s", u.Source, u.Extra.Ref, suffix, u.Extra.FileType)
} }
func (u *IconURL) MarshalText() ([]byte, error) { func (u *URL) MarshalText() ([]byte, error) {
return []byte(u.String()), nil return []byte(u.String()), nil
} }
// UnmarshalText implements encoding.TextUnmarshaler. // UnmarshalText implements encoding.TextUnmarshaler.
func (u *IconURL) UnmarshalText(data []byte) error { func (u *URL) UnmarshalText(data []byte) error {
return u.parse(string(data), false) return u.parse(string(data), false)
} }

View File

@@ -1,9 +1,9 @@
package homepage_test package icons_test
import ( import (
"testing" "testing"
. "github.com/yusing/godoxy/internal/homepage" . "github.com/yusing/godoxy/internal/homepage/icons"
expect "github.com/yusing/goutils/testing" expect "github.com/yusing/goutils/testing"
) )
@@ -15,31 +15,31 @@ func TestIconURL(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
input string input string
wantValue *IconURL wantValue *URL
wantErr bool wantErr bool
}{ }{
{ {
name: "absolute", name: "absolute",
input: "http://example.com/icon.png", input: "http://example.com/icon.png",
wantValue: &IconURL{ wantValue: &URL{
FullURL: strPtr("http://example.com/icon.png"), FullURL: strPtr("http://example.com/icon.png"),
IconSource: IconSourceAbsolute, Source: SourceAbsolute,
}, },
}, },
{ {
name: "relative", name: "relative",
input: "@target/icon.png", input: "@target/icon.png",
wantValue: &IconURL{ wantValue: &URL{
FullURL: strPtr("/icon.png"), FullURL: strPtr("/icon.png"),
IconSource: IconSourceRelative, Source: SourceRelative,
}, },
}, },
{ {
name: "relative2", name: "relative2",
input: "/icon.png", input: "/icon.png",
wantValue: &IconURL{ wantValue: &URL{
FullURL: strPtr("/icon.png"), FullURL: strPtr("/icon.png"),
IconSource: IconSourceRelative, Source: SourceRelative,
}, },
}, },
{ {
@@ -55,10 +55,10 @@ func TestIconURL(t *testing.T) {
{ {
name: "walkxcode", name: "walkxcode",
input: "@walkxcode/adguard-home.png", input: "@walkxcode/adguard-home.png",
wantValue: &IconURL{ wantValue: &URL{
IconSource: IconSourceWalkXCode, Source: SourceWalkXCode,
Extra: &IconExtra{ Extra: &Extra{
Key: NewIconKey(IconSourceWalkXCode, "adguard-home"), Key: NewKey(SourceWalkXCode, "adguard-home"),
FileType: "png", FileType: "png",
Ref: "adguard-home", Ref: "adguard-home",
}, },
@@ -67,10 +67,10 @@ func TestIconURL(t *testing.T) {
{ {
name: "walkxcode_light", name: "walkxcode_light",
input: "@walkxcode/pfsense-light.png", input: "@walkxcode/pfsense-light.png",
wantValue: &IconURL{ wantValue: &URL{
IconSource: IconSourceWalkXCode, Source: SourceWalkXCode,
Extra: &IconExtra{ Extra: &Extra{
Key: NewIconKey(IconSourceWalkXCode, "pfsense"), Key: NewKey(SourceWalkXCode, "pfsense"),
FileType: "png", FileType: "png",
Ref: "pfsense", Ref: "pfsense",
IsLight: true, IsLight: true,
@@ -85,10 +85,10 @@ func TestIconURL(t *testing.T) {
{ {
name: "selfh.st_valid", name: "selfh.st_valid",
input: "@selfhst/adguard-home.webp", input: "@selfhst/adguard-home.webp",
wantValue: &IconURL{ wantValue: &URL{
IconSource: IconSourceSelfhSt, Source: SourceSelfhSt,
Extra: &IconExtra{ Extra: &Extra{
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"), Key: NewKey(SourceSelfhSt, "adguard-home"),
FileType: "webp", FileType: "webp",
Ref: "adguard-home", Ref: "adguard-home",
}, },
@@ -97,10 +97,10 @@ func TestIconURL(t *testing.T) {
{ {
name: "selfh.st_light", name: "selfh.st_light",
input: "@selfhst/adguard-home-light.png", input: "@selfhst/adguard-home-light.png",
wantValue: &IconURL{ wantValue: &URL{
IconSource: IconSourceSelfhSt, Source: SourceSelfhSt,
Extra: &IconExtra{ Extra: &Extra{
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"), Key: NewKey(SourceSelfhSt, "adguard-home"),
FileType: "png", FileType: "png",
Ref: "adguard-home", Ref: "adguard-home",
IsLight: true, IsLight: true,
@@ -110,10 +110,10 @@ func TestIconURL(t *testing.T) {
{ {
name: "selfh.st_dark", name: "selfh.st_dark",
input: "@selfhst/adguard-home-dark.svg", input: "@selfhst/adguard-home-dark.svg",
wantValue: &IconURL{ wantValue: &URL{
IconSource: IconSourceSelfhSt, Source: SourceSelfhSt,
Extra: &IconExtra{ Extra: &Extra{
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"), Key: NewKey(SourceSelfhSt, "adguard-home"),
FileType: "svg", FileType: "svg",
Ref: "adguard-home", Ref: "adguard-home",
IsDark: true, IsDark: true,
@@ -143,7 +143,7 @@ func TestIconURL(t *testing.T) {
} }
for _, tc := range tests { for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
u := &IconURL{} u := &URL{}
err := u.Parse(tc.input) err := u.Parse(tc.input)
if tc.wantErr { if tc.wantErr {
expect.ErrorIs(t, ErrInvalidIconURL, err) expect.ErrorIs(t, ErrInvalidIconURL, err)

View File

@@ -7,7 +7,8 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"github.com/yusing/godoxy/internal/homepage" "github.com/yusing/godoxy/internal/homepage/icons"
iconfetch "github.com/yusing/godoxy/internal/homepage/icons/fetch"
idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types" idlewatcher "github.com/yusing/godoxy/internal/idlewatcher/types"
gperr "github.com/yusing/goutils/errs" gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http" httputils "github.com/yusing/goutils/http"
@@ -99,18 +100,18 @@ func (w *Watcher) handleWakeEventsSSE(rw http.ResponseWriter, r *http.Request) {
} }
} }
func (w *Watcher) getFavIcon(ctx context.Context) (result homepage.FetchResult, err error) { func (w *Watcher) getFavIcon(ctx context.Context) (result iconfetch.Result, err error) {
r := w.route r := w.route
hp := r.HomepageItem() hp := r.HomepageItem()
if hp.Icon != nil { if hp.Icon != nil {
if hp.Icon.IconSource == homepage.IconSourceRelative { if hp.Icon.Source == icons.SourceRelative {
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, homepage.IconVariantNone) result, err = iconfetch.FindIcon(ctx, r, *hp.Icon.FullURL, icons.VariantNone)
} else { } else {
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon) result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon)
} }
} else { } else {
// try extract from "link[rel=icon]" // try extract from "link[rel=icon]"
result, err = homepage.FindIcon(ctx, r, "/", homepage.IconVariantNone) result, err = iconfetch.FindIcon(ctx, r, "/", icons.VariantNone)
} }
if result.StatusCode == 0 { if result.StatusCode == 0 {
result.StatusCode = http.StatusOK result.StatusCode = http.StatusOK

View File

@@ -20,6 +20,7 @@ import (
"github.com/yusing/godoxy/internal/docker" "github.com/yusing/godoxy/internal/docker"
"github.com/yusing/godoxy/internal/health/monitor" "github.com/yusing/godoxy/internal/health/monitor"
"github.com/yusing/godoxy/internal/homepage" "github.com/yusing/godoxy/internal/homepage"
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
homepagecfg "github.com/yusing/godoxy/internal/homepage/types" homepagecfg "github.com/yusing/godoxy/internal/homepage/types"
netutils "github.com/yusing/godoxy/internal/net" netutils "github.com/yusing/godoxy/internal/net"
nettypes "github.com/yusing/godoxy/internal/net/types" nettypes "github.com/yusing/godoxy/internal/net/types"
@@ -849,7 +850,7 @@ func (r *Route) FinalizeHomepageConfig() {
hp := r.Homepage hp := r.Homepage
refs := r.References() refs := r.References()
for _, ref := range refs { for _, ref := range refs {
meta, ok := homepage.GetHomepageMeta(ref) meta, ok := iconlist.GetMetadata(ref)
if ok { if ok {
if hp.Name == "" { if hp.Name == "" {
hp.Name = meta.DisplayName hp.Name = meta.DisplayName