mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 21:10:30 +01:00
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:
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/config"
|
||||
"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/memlogger"
|
||||
"github.com/yusing/godoxy/internal/metrics/systeminfo"
|
||||
@@ -39,7 +39,7 @@ func main() {
|
||||
log.Trace().Msg("trace enabled")
|
||||
parallel(
|
||||
dnsproviders.InitProviders,
|
||||
homepage.InitIconListCache,
|
||||
iconlist.InitCache,
|
||||
systeminfo.Poller.Start,
|
||||
middleware.LoadComposeFiles,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
|
||||
@@ -15,7 +16,7 @@ import (
|
||||
type GetFavIconRequest struct {
|
||||
URL string `form:"url" binding:"required_without=Alias"`
|
||||
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
|
||||
|
||||
// @x-id "favicon"
|
||||
@@ -42,18 +43,18 @@ func FavIcon(c *gin.Context) {
|
||||
|
||||
// try with url
|
||||
if request.URL != "" {
|
||||
var iconURL homepage.IconURL
|
||||
var iconURL icons.URL
|
||||
if err := iconURL.Parse(request.URL); err != nil {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid url", err))
|
||||
return
|
||||
}
|
||||
icon := &iconURL
|
||||
if request.Variant != homepage.IconVariantNone {
|
||||
if request.Variant != icons.VariantNone {
|
||||
icon = icon.WithVariant(request.Variant)
|
||||
}
|
||||
fetchResult, err := homepage.FetchFavIconFromURL(c.Request.Context(), icon)
|
||||
fetchResult, err := iconfetch.FetchFavIconFromURL(c.Request.Context(), icon)
|
||||
if err != nil {
|
||||
homepage.GinFetchError(c, fetchResult.StatusCode, err)
|
||||
iconfetch.GinError(c, fetchResult.StatusCode, err)
|
||||
return
|
||||
}
|
||||
c.Data(fetchResult.StatusCode, fetchResult.ContentType(), fetchResult.Icon)
|
||||
@@ -63,40 +64,40 @@ func FavIcon(c *gin.Context) {
|
||||
// try with alias
|
||||
result, err := GetFavIconFromAlias(c.Request.Context(), request.Alias, request.Variant)
|
||||
if err != nil {
|
||||
homepage.GinFetchError(c, result.StatusCode, err)
|
||||
iconfetch.GinError(c, result.StatusCode, err)
|
||||
return
|
||||
}
|
||||
c.Data(result.StatusCode, result.ContentType(), result.Icon)
|
||||
}
|
||||
|
||||
//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
|
||||
r, ok := routes.HTTP.Get(alias)
|
||||
if !ok {
|
||||
return homepage.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||
return iconfetch.FetchResultWithErrorf(http.StatusNotFound, "route not found")
|
||||
}
|
||||
|
||||
var (
|
||||
result homepage.FetchResult
|
||||
result iconfetch.Result
|
||||
err error
|
||||
)
|
||||
hp := r.HomepageItem()
|
||||
if hp.Icon != nil {
|
||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, variant)
|
||||
} else if variant != homepage.IconVariantNone {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant))
|
||||
if hp.Icon.Source == icons.SourceRelative {
|
||||
result, err = iconfetch.FindIcon(ctx, r, *hp.Icon.FullURL, variant)
|
||||
} else if variant != icons.VariantNone {
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon.WithVariant(variant))
|
||||
if err != nil {
|
||||
// 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 {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
}
|
||||
} else {
|
||||
// try extract from "link[rel=icon]"
|
||||
result, err = homepage.FindIcon(ctx, r, "/", variant)
|
||||
result, err = iconfetch.FindIcon(ctx, r, "/", variant)
|
||||
}
|
||||
if result.StatusCode == 0 {
|
||||
result.StatusCode = http.StatusOK
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -32,6 +32,6 @@ func Icons(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
|
||||
return
|
||||
}
|
||||
icons := homepage.SearchIcons(request.Keyword, request.Limit)
|
||||
icons := iconlist.SearchIcons(request.Keyword, request.Limit)
|
||||
c.JSON(http.StatusOK, icons)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/yusing/ds/ordered"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
"github.com/yusing/godoxy/internal/homepage/widgets"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
@@ -24,7 +25,7 @@ type (
|
||||
ItemConfig struct {
|
||||
Show bool `json:"show"`
|
||||
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"`
|
||||
Description string `json:"description" aliases:"desc"`
|
||||
URL string `json:"url,omitempty"`
|
||||
|
||||
@@ -4,19 +4,24 @@ import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/godoxy/internal/homepage"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func TestOverrideItem(t *testing.T) {
|
||||
a := &Item{
|
||||
Alias: "foo",
|
||||
ItemConfig: ItemConfig{
|
||||
Show: false,
|
||||
Name: "Foo",
|
||||
Icon: &IconURL{
|
||||
Icon: &icons.URL{
|
||||
FullURL: strPtr("/favicon.ico"),
|
||||
IconSource: IconSourceRelative,
|
||||
Source: icons.SourceRelative,
|
||||
},
|
||||
Category: "App",
|
||||
},
|
||||
@@ -25,9 +30,9 @@ func TestOverrideItem(t *testing.T) {
|
||||
Show: true,
|
||||
Name: "Bar",
|
||||
Category: "Test",
|
||||
Icon: &IconURL{
|
||||
Icon: &icons.URL{
|
||||
FullURL: strPtr("@walkxcode/example.png"),
|
||||
IconSource: IconSourceWalkXCode,
|
||||
Source: icons.SourceWalkXCode,
|
||||
},
|
||||
}
|
||||
overrides := GetOverrideConfig()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package homepage
|
||||
package iconfetch
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -1,4 +1,4 @@
|
||||
package homepage
|
||||
package iconfetch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/vincent-petithory/dataurl"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
gphttp "github.com/yusing/godoxy/internal/net/gphttp"
|
||||
apitypes "github.com/yusing/goutils/apitypes"
|
||||
"github.com/yusing/goutils/cache"
|
||||
@@ -22,22 +23,22 @@ import (
|
||||
strutils "github.com/yusing/goutils/strings"
|
||||
)
|
||||
|
||||
type FetchResult struct {
|
||||
type Result struct {
|
||||
Icon []byte
|
||||
StatusCode int
|
||||
|
||||
contentType string
|
||||
}
|
||||
|
||||
func FetchResultWithErrorf(statusCode int, msgFmt string, args ...any) (FetchResult, error) {
|
||||
return FetchResult{StatusCode: statusCode}, fmt.Errorf(msgFmt, args...)
|
||||
func FetchResultWithErrorf(statusCode int, msgFmt string, args ...any) (Result, error) {
|
||||
return Result{StatusCode: statusCode}, fmt.Errorf(msgFmt, args...)
|
||||
}
|
||||
|
||||
func FetchResultOK(icon []byte, contentType string) (FetchResult, error) {
|
||||
return FetchResult{Icon: icon, contentType: contentType}, nil
|
||||
func FetchResultOK(icon []byte, contentType string) (Result, error) {
|
||||
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 {
|
||||
statusCode = http.StatusInternalServerError
|
||||
}
|
||||
@@ -50,7 +51,7 @@ func GinFetchError(c *gin.Context, statusCode int, err error) {
|
||||
|
||||
const faviconFetchTimeout = 3 * time.Second
|
||||
|
||||
func (res *FetchResult) ContentType() string {
|
||||
func (res *Result) ContentType() string {
|
||||
if res.contentType == "" {
|
||||
if bytes.HasPrefix(res.Icon, []byte("<svg")) || bytes.HasPrefix(res.Icon, []byte("<?xml")) {
|
||||
return "image/svg+xml"
|
||||
@@ -62,19 +63,19 @@ func (res *FetchResult) ContentType() string {
|
||||
|
||||
const maxRedirectDepth = 5
|
||||
|
||||
func FetchFavIconFromURL(ctx context.Context, iconURL *IconURL) (FetchResult, error) {
|
||||
switch iconURL.IconSource {
|
||||
case IconSourceAbsolute:
|
||||
func FetchFavIconFromURL(ctx context.Context, iconURL *icons.URL) (Result, error) {
|
||||
switch iconURL.Source {
|
||||
case icons.SourceAbsolute:
|
||||
return FetchIconAbsolute(ctx, iconURL.URL())
|
||||
case IconSourceRelative:
|
||||
case icons.SourceRelative:
|
||||
return FetchResultWithErrorf(http.StatusBadRequest, "unexpected relative icon")
|
||||
case IconSourceWalkXCode, IconSourceSelfhSt:
|
||||
case icons.SourceWalkXCode, icons.SourceSelfhSt:
|
||||
return fetchKnownIcon(ctx, iconURL)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
res := FetchResult{Icon: icon}
|
||||
res := Result{Icon: icon}
|
||||
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
|
||||
res.contentType = contentType
|
||||
}
|
||||
@@ -122,22 +123,22 @@ func sanitizeName(name string) string {
|
||||
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 !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())
|
||||
}
|
||||
|
||||
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"} {
|
||||
result, err := fetchKnownIcon(ctx, NewSelfhStIconURL(filename, fileType))
|
||||
result, err := fetchKnownIcon(ctx, icons.NewURL(icons.SourceSelfhSt, filename, fileType))
|
||||
if err == nil {
|
||||
return result, err
|
||||
}
|
||||
result, err = fetchKnownIcon(ctx, NewWalkXCodeIconURL(filename, fileType))
|
||||
result, err = fetchKnownIcon(ctx, icons.NewURL(icons.SourceWalkXCode, filename, fileType))
|
||||
if err == nil {
|
||||
return result, err
|
||||
}
|
||||
@@ -150,10 +151,10 @@ type contextValue struct {
|
||||
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() {
|
||||
ref = sanitizeName(ref)
|
||||
if variant != IconVariantNone {
|
||||
if variant != icons.VariantNone {
|
||||
ref += "-" + string(variant)
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
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)
|
||||
return findIconSlow(ctx, v.r, v.uri, nil)
|
||||
}).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 {
|
||||
case <-ctx.Done():
|
||||
return FetchResultWithErrorf(http.StatusBadGateway, "request timeout")
|
||||
@@ -1,4 +1,4 @@
|
||||
package homepage
|
||||
package iconfetch
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
17
internal/homepage/icons/key.go
Normal file
17
internal/homepage/icons/key.go
Normal 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
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package homepage
|
||||
package iconlist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/yusing/godoxy/internal/common"
|
||||
"github.com/yusing/godoxy/internal/homepage/icons"
|
||||
"github.com/yusing/godoxy/internal/serialization"
|
||||
httputils "github.com/yusing/goutils/http"
|
||||
"github.com/yusing/goutils/intern"
|
||||
@@ -21,60 +21,19 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
IconKey string
|
||||
IconMap map[IconKey]*IconMeta
|
||||
IconMap map[icons.Key]*icons.Meta
|
||||
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 {
|
||||
*icons.Meta
|
||||
|
||||
Source icons.Source `json:"Source"`
|
||||
Ref string `json:"Ref"`
|
||||
|
||||
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
|
||||
|
||||
var iconsCache synk.Value[IconMap]
|
||||
@@ -84,16 +43,7 @@ const (
|
||||
selfhstIcons = "https://raw.githubusercontent.com/selfhst/icons/refs/heads/main/index.json"
|
||||
)
|
||||
|
||||
func NewIconKey(source IconSource, reference string) IconKey {
|
||||
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() {
|
||||
func InitCache() {
|
||||
m := make(IconMap)
|
||||
err := serialization.LoadJSONIfExist(common.IconListCachePath, &m)
|
||||
if err != nil {
|
||||
@@ -198,7 +148,7 @@ func SearchIcons(keyword string, limit int) []*IconMetaSearch {
|
||||
ranked := &IconMetaSearch{
|
||||
Source: source,
|
||||
Ref: ref,
|
||||
IconMeta: icon,
|
||||
Meta: icon,
|
||||
rank: rank,
|
||||
}
|
||||
// Sorted insert based on rank (lower rank = better match)
|
||||
@@ -213,7 +163,7 @@ func SearchIcons(keyword string, limit int) []*IconMetaSearch {
|
||||
return results[:min(len(results), limit)]
|
||||
}
|
||||
|
||||
func HasIcon(icon *IconURL) bool {
|
||||
func HasIcon(icon *icons.URL) bool {
|
||||
if icon.Extra == nil {
|
||||
return false
|
||||
}
|
||||
@@ -241,11 +191,11 @@ type HomepageMeta struct {
|
||||
Tag string
|
||||
}
|
||||
|
||||
func GetHomepageMeta(ref string) (HomepageMeta, bool) {
|
||||
meta, ok := ListAvailableIcons()[NewIconKey(IconSourceSelfhSt, ref)]
|
||||
func GetMetadata(ref string) (HomepageMeta, bool) {
|
||||
meta, ok := ListAvailableIcons()[icons.NewKey(icons.SourceSelfhSt, ref)]
|
||||
// these info is not available in walkxcode
|
||||
// if !ok {
|
||||
// meta, ok = iconsCache.Icons[NewIconKey(IconSourceWalkXCode, ref)]
|
||||
// meta, ok = iconsCache.Icons[icons.NewIconKey(icons.IconSourceWalkXCode, ref)]
|
||||
// }
|
||||
if !ok {
|
||||
return HomepageMeta{}, false
|
||||
@@ -317,14 +267,14 @@ func UpdateWalkxCodeIcons(m IconMap) error {
|
||||
}
|
||||
|
||||
for fileType, files := range data {
|
||||
var setExt func(icon *IconMeta)
|
||||
var setExt func(icon *icons.Meta)
|
||||
switch fileType {
|
||||
case "png":
|
||||
setExt = func(icon *IconMeta) { icon.PNG = true }
|
||||
setExt = func(icon *icons.Meta) { icon.PNG = true }
|
||||
case "svg":
|
||||
setExt = func(icon *IconMeta) { icon.SVG = true }
|
||||
setExt = func(icon *icons.Meta) { icon.SVG = true }
|
||||
case "webp":
|
||||
setExt = func(icon *IconMeta) { icon.WebP = true }
|
||||
setExt = func(icon *icons.Meta) { icon.WebP = true }
|
||||
}
|
||||
for _, f := range files {
|
||||
f = strings.TrimSuffix(f, "."+fileType)
|
||||
@@ -336,10 +286,10 @@ func UpdateWalkxCodeIcons(m IconMap) error {
|
||||
if isDark {
|
||||
f = strings.TrimSuffix(f, "-dark")
|
||||
}
|
||||
key := NewIconKey(IconSourceWalkXCode, f)
|
||||
key := icons.NewKey(icons.SourceWalkXCode, f)
|
||||
icon, ok := m[key]
|
||||
if !ok {
|
||||
icon = new(IconMeta)
|
||||
icon = new(icons.Meta)
|
||||
m[key] = icon
|
||||
}
|
||||
setExt(icon)
|
||||
@@ -401,7 +351,7 @@ func UpdateSelfhstIcons(m IconMap) error {
|
||||
tag, _, _ = strings.Cut(item.Tags, ",")
|
||||
tag = strings.TrimSpace(tag)
|
||||
}
|
||||
icon := &IconMeta{
|
||||
icon := &icons.Meta{
|
||||
DisplayName: item.Name,
|
||||
Tag: intern.Make(tag).Value(),
|
||||
SVG: item.SVG == "Yes",
|
||||
@@ -410,7 +360,7 @@ func UpdateSelfhstIcons(m IconMap) error {
|
||||
Light: item.Light == "Yes",
|
||||
Dark: item.Dark == "Yes",
|
||||
}
|
||||
key := NewIconKey(IconSourceSelfhSt, item.Reference)
|
||||
key := icons.NewKey(icons.SourceSelfhSt, item.Reference)
|
||||
m[key] = icon
|
||||
}
|
||||
return nil
|
||||
@@ -1,9 +1,10 @@
|
||||
package homepage_test
|
||||
package iconlist_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/godoxy/internal/homepage"
|
||||
. "github.com/yusing/godoxy/internal/homepage/icons"
|
||||
. "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
)
|
||||
|
||||
const walkxcodeIcons = `{
|
||||
@@ -69,8 +70,8 @@ const selfhstIcons = `[
|
||||
]`
|
||||
|
||||
type testCases struct {
|
||||
Key IconKey
|
||||
IconMeta
|
||||
Key Key
|
||||
Meta
|
||||
}
|
||||
|
||||
func runTests(t *testing.T, iconsCache IconMap, test []testCases) {
|
||||
@@ -109,8 +110,8 @@ func TestListWalkxCodeIcons(t *testing.T) {
|
||||
}
|
||||
test := []testCases{
|
||||
{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "app1"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceWalkXCode, "app1"),
|
||||
Meta: Meta{
|
||||
SVG: true,
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
@@ -118,15 +119,15 @@ func TestListWalkxCodeIcons(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "app2"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceWalkXCode, "app2"),
|
||||
Meta: Meta{
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "karakeep"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceWalkXCode, "karakeep"),
|
||||
Meta: Meta{
|
||||
SVG: true,
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
@@ -149,8 +150,8 @@ func TestListSelfhstIcons(t *testing.T) {
|
||||
}
|
||||
test := []testCases{
|
||||
{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "2fauth"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceSelfhSt, "2fauth"),
|
||||
Meta: Meta{
|
||||
SVG: true,
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
@@ -160,16 +161,16 @@ func TestListSelfhstIcons(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "dittofeed"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceSelfhSt, "dittofeed"),
|
||||
Meta: Meta{
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
DisplayName: "Dittofeed",
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "ars-technica"),
|
||||
IconMeta: IconMeta{
|
||||
Key: NewKey(SourceSelfhSt, "ars-technica"),
|
||||
Meta: Meta{
|
||||
SVG: true,
|
||||
PNG: true,
|
||||
WebP: true,
|
||||
43
internal/homepage/icons/metadata.go
Normal file
43
internal/homepage/icons/metadata.go
Normal 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
|
||||
}
|
||||
21
internal/homepage/icons/provider.go
Normal file
21
internal/homepage/icons/provider.go
Normal 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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package homepage
|
||||
package icons
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -8,43 +8,43 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
IconURL struct {
|
||||
IconSource `json:"source"`
|
||||
URL struct {
|
||||
Source `json:"source"`
|
||||
|
||||
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 {
|
||||
Key IconKey `json:"key"`
|
||||
Extra struct {
|
||||
Key Key `json:"key"`
|
||||
Ref string `json:"ref"`
|
||||
FileType string `json:"file_type"`
|
||||
IsLight bool `json:"is_light"`
|
||||
IsDark bool `json:"is_dark"`
|
||||
}
|
||||
|
||||
IconSource string
|
||||
IconVariant string
|
||||
Source string
|
||||
Variant string
|
||||
)
|
||||
|
||||
const (
|
||||
IconSourceAbsolute IconSource = "https://"
|
||||
IconSourceRelative IconSource = "@target"
|
||||
IconSourceWalkXCode IconSource = "@walkxcode"
|
||||
IconSourceSelfhSt IconSource = "@selfhst"
|
||||
SourceAbsolute Source = "https://"
|
||||
SourceRelative Source = "@target"
|
||||
SourceWalkXCode Source = "@walkxcode"
|
||||
SourceSelfhSt Source = "@selfhst"
|
||||
)
|
||||
|
||||
const (
|
||||
IconVariantNone IconVariant = ""
|
||||
IconVariantLight IconVariant = "light"
|
||||
IconVariantDark IconVariant = "dark"
|
||||
VariantNone Variant = ""
|
||||
VariantLight Variant = "light"
|
||||
VariantDark Variant = "dark"
|
||||
)
|
||||
|
||||
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 {
|
||||
case IconSourceWalkXCode, IconSourceSelfhSt:
|
||||
case SourceWalkXCode, SourceSelfhSt:
|
||||
default:
|
||||
panic("invalid icon source")
|
||||
}
|
||||
@@ -56,10 +56,10 @@ func NewIconURL(source IconSource, refOrName, format string) *IconURL {
|
||||
isDark = true
|
||||
refOrName = strings.TrimSuffix(refOrName, "-dark")
|
||||
}
|
||||
return &IconURL{
|
||||
IconSource: source,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(source, refOrName),
|
||||
return &URL{
|
||||
Source: source,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(source, refOrName),
|
||||
FileType: format,
|
||||
Ref: refOrName,
|
||||
IsLight: isLight,
|
||||
@@ -68,53 +68,42 @@ func NewIconURL(source IconSource, refOrName, format string) *IconURL {
|
||||
}
|
||||
}
|
||||
|
||||
func NewSelfhStIconURL(refOrName, format string) *IconURL {
|
||||
return NewIconURL(IconSourceSelfhSt, refOrName, format)
|
||||
func (u *URL) HasIcon() bool {
|
||||
return hasIcon(u)
|
||||
}
|
||||
|
||||
func NewWalkXCodeIconURL(name, format string) *IconURL {
|
||||
return NewIconURL(IconSourceWalkXCode, name, format)
|
||||
}
|
||||
|
||||
// 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:
|
||||
func (u *URL) WithVariant(variant Variant) *URL {
|
||||
switch u.Source {
|
||||
case SourceWalkXCode, SourceSelfhSt:
|
||||
default:
|
||||
return u // no variant for absolute/relative icons
|
||||
}
|
||||
|
||||
var extra *IconExtra
|
||||
var extra *Extra
|
||||
if u.Extra != nil {
|
||||
extra = &IconExtra{
|
||||
extra = &Extra{
|
||||
Key: u.Extra.Key,
|
||||
Ref: u.Extra.Ref,
|
||||
FileType: u.Extra.FileType,
|
||||
IsLight: variant == IconVariantLight,
|
||||
IsDark: variant == IconVariantDark,
|
||||
IsLight: variant == VariantLight,
|
||||
IsDark: variant == VariantDark,
|
||||
}
|
||||
extra.Ref = strings.TrimSuffix(extra.Ref, "-light")
|
||||
extra.Ref = strings.TrimSuffix(extra.Ref, "-dark")
|
||||
}
|
||||
return &IconURL{
|
||||
IconSource: u.IconSource,
|
||||
return &URL{
|
||||
Source: u.Source,
|
||||
FullURL: u.FullURL,
|
||||
Extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse implements strutils.Parser.
|
||||
func (u *IconURL) Parse(v string) error {
|
||||
func (u *URL) Parse(v string) error {
|
||||
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 == "" {
|
||||
return ErrInvalidIconURL
|
||||
}
|
||||
@@ -126,19 +115,19 @@ func (u *IconURL) parse(v string, checkExists bool) error {
|
||||
switch beforeSlash {
|
||||
case "http:", "https:":
|
||||
u.FullURL = &v
|
||||
u.IconSource = IconSourceAbsolute
|
||||
u.Source = SourceAbsolute
|
||||
case "@target", "": // @target/favicon.ico, /favicon.ico
|
||||
url := v[slashIndex:]
|
||||
if url == "/" {
|
||||
return ErrInvalidIconURL.Withf("%s", "empty path")
|
||||
}
|
||||
u.FullURL = &url
|
||||
u.IconSource = IconSourceRelative
|
||||
u.Source = SourceRelative
|
||||
case "@selfhst", "@walkxcode": // selfh.st / walkxcode Icons, @selfhst/<reference>.<format>
|
||||
if beforeSlash == "@selfhst" {
|
||||
u.IconSource = IconSourceSelfhSt
|
||||
u.Source = SourceSelfhSt
|
||||
} else {
|
||||
u.IconSource = IconSourceWalkXCode
|
||||
u.Source = SourceWalkXCode
|
||||
}
|
||||
parts := strings.Split(v[slashIndex+1:], ".")
|
||||
if len(parts) != 2 {
|
||||
@@ -161,15 +150,15 @@ func (u *IconURL) parse(v string, checkExists bool) error {
|
||||
isDark = true
|
||||
reference = strings.TrimSuffix(reference, "-dark")
|
||||
}
|
||||
u.Extra = &IconExtra{
|
||||
Key: NewIconKey(u.IconSource, reference),
|
||||
u.Extra = &Extra{
|
||||
Key: NewKey(u.Source, reference),
|
||||
FileType: format,
|
||||
Ref: reference,
|
||||
IsLight: isLight,
|
||||
IsDark: isDark,
|
||||
}
|
||||
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:
|
||||
return ErrInvalidIconURL.Subject(v)
|
||||
@@ -178,7 +167,7 @@ func (u *IconURL) parse(v string, checkExists bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *IconURL) URL() string {
|
||||
func (u *URL) URL() string {
|
||||
if u.FullURL != nil {
|
||||
return *u.FullURL
|
||||
}
|
||||
@@ -191,16 +180,16 @@ func (u *IconURL) URL() string {
|
||||
} else if u.Extra.IsDark {
|
||||
filename += "-dark"
|
||||
}
|
||||
switch u.IconSource {
|
||||
case IconSourceWalkXCode:
|
||||
switch u.Source {
|
||||
case SourceWalkXCode:
|
||||
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 ""
|
||||
}
|
||||
|
||||
func (u *IconURL) String() string {
|
||||
func (u *URL) String() string {
|
||||
if u.FullURL != nil {
|
||||
return *u.FullURL
|
||||
}
|
||||
@@ -213,14 +202,14 @@ func (u *IconURL) String() string {
|
||||
} else if u.Extra.IsDark {
|
||||
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
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
func (u *IconURL) UnmarshalText(data []byte) error {
|
||||
func (u *URL) UnmarshalText(data []byte) error {
|
||||
return u.parse(string(data), false)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package homepage_test
|
||||
package icons_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/godoxy/internal/homepage"
|
||||
. "github.com/yusing/godoxy/internal/homepage/icons"
|
||||
expect "github.com/yusing/goutils/testing"
|
||||
)
|
||||
|
||||
@@ -15,31 +15,31 @@ func TestIconURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantValue *IconURL
|
||||
wantValue *URL
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "absolute",
|
||||
input: "http://example.com/icon.png",
|
||||
wantValue: &IconURL{
|
||||
wantValue: &URL{
|
||||
FullURL: strPtr("http://example.com/icon.png"),
|
||||
IconSource: IconSourceAbsolute,
|
||||
Source: SourceAbsolute,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "relative",
|
||||
input: "@target/icon.png",
|
||||
wantValue: &IconURL{
|
||||
wantValue: &URL{
|
||||
FullURL: strPtr("/icon.png"),
|
||||
IconSource: IconSourceRelative,
|
||||
Source: SourceRelative,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "relative2",
|
||||
input: "/icon.png",
|
||||
wantValue: &IconURL{
|
||||
wantValue: &URL{
|
||||
FullURL: strPtr("/icon.png"),
|
||||
IconSource: IconSourceRelative,
|
||||
Source: SourceRelative,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -55,10 +55,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "walkxcode",
|
||||
input: "@walkxcode/adguard-home.png",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceWalkXCode,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "adguard-home"),
|
||||
wantValue: &URL{
|
||||
Source: SourceWalkXCode,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceWalkXCode, "adguard-home"),
|
||||
FileType: "png",
|
||||
Ref: "adguard-home",
|
||||
},
|
||||
@@ -67,10 +67,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "walkxcode_light",
|
||||
input: "@walkxcode/pfsense-light.png",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceWalkXCode,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceWalkXCode, "pfsense"),
|
||||
wantValue: &URL{
|
||||
Source: SourceWalkXCode,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceWalkXCode, "pfsense"),
|
||||
FileType: "png",
|
||||
Ref: "pfsense",
|
||||
IsLight: true,
|
||||
@@ -85,10 +85,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "selfh.st_valid",
|
||||
input: "@selfhst/adguard-home.webp",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceSelfhSt,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"),
|
||||
wantValue: &URL{
|
||||
Source: SourceSelfhSt,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceSelfhSt, "adguard-home"),
|
||||
FileType: "webp",
|
||||
Ref: "adguard-home",
|
||||
},
|
||||
@@ -97,10 +97,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "selfh.st_light",
|
||||
input: "@selfhst/adguard-home-light.png",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceSelfhSt,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"),
|
||||
wantValue: &URL{
|
||||
Source: SourceSelfhSt,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceSelfhSt, "adguard-home"),
|
||||
FileType: "png",
|
||||
Ref: "adguard-home",
|
||||
IsLight: true,
|
||||
@@ -110,10 +110,10 @@ func TestIconURL(t *testing.T) {
|
||||
{
|
||||
name: "selfh.st_dark",
|
||||
input: "@selfhst/adguard-home-dark.svg",
|
||||
wantValue: &IconURL{
|
||||
IconSource: IconSourceSelfhSt,
|
||||
Extra: &IconExtra{
|
||||
Key: NewIconKey(IconSourceSelfhSt, "adguard-home"),
|
||||
wantValue: &URL{
|
||||
Source: SourceSelfhSt,
|
||||
Extra: &Extra{
|
||||
Key: NewKey(SourceSelfhSt, "adguard-home"),
|
||||
FileType: "svg",
|
||||
Ref: "adguard-home",
|
||||
IsDark: true,
|
||||
@@ -143,7 +143,7 @@ func TestIconURL(t *testing.T) {
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
u := &IconURL{}
|
||||
u := &URL{}
|
||||
err := u.Parse(tc.input)
|
||||
if tc.wantErr {
|
||||
expect.ErrorIs(t, ErrInvalidIconURL, err)
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"net/http"
|
||||
"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"
|
||||
gperr "github.com/yusing/goutils/errs"
|
||||
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
|
||||
hp := r.HomepageItem()
|
||||
if hp.Icon != nil {
|
||||
if hp.Icon.IconSource == homepage.IconSourceRelative {
|
||||
result, err = homepage.FindIcon(ctx, r, *hp.Icon.FullURL, homepage.IconVariantNone)
|
||||
if hp.Icon.Source == icons.SourceRelative {
|
||||
result, err = iconfetch.FindIcon(ctx, r, *hp.Icon.FullURL, icons.VariantNone)
|
||||
} else {
|
||||
result, err = homepage.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
result, err = iconfetch.FetchFavIconFromURL(ctx, hp.Icon)
|
||||
}
|
||||
} else {
|
||||
// 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 {
|
||||
result.StatusCode = http.StatusOK
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/yusing/godoxy/internal/docker"
|
||||
"github.com/yusing/godoxy/internal/health/monitor"
|
||||
"github.com/yusing/godoxy/internal/homepage"
|
||||
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
|
||||
homepagecfg "github.com/yusing/godoxy/internal/homepage/types"
|
||||
netutils "github.com/yusing/godoxy/internal/net"
|
||||
nettypes "github.com/yusing/godoxy/internal/net/types"
|
||||
@@ -849,7 +850,7 @@ func (r *Route) FinalizeHomepageConfig() {
|
||||
hp := r.Homepage
|
||||
refs := r.References()
|
||||
for _, ref := range refs {
|
||||
meta, ok := homepage.GetHomepageMeta(ref)
|
||||
meta, ok := iconlist.GetMetadata(ref)
|
||||
if ok {
|
||||
if hp.Name == "" {
|
||||
hp.Name = meta.DisplayName
|
||||
|
||||
Reference in New Issue
Block a user