mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 22:30:47 +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/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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package homepage
|
package iconfetch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@@ -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")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package homepage
|
package iconfetch
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"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 (
|
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
|
||||||
@@ -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,
|
||||||
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 (
|
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)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user