feat: support selfh.st icons, support homepage config overriding

This commit is contained in:
yusing
2025-01-20 17:42:17 +08:00
parent 68771ce399
commit 64e85c3076
13 changed files with 591 additions and 44 deletions

View File

@@ -30,6 +30,16 @@ func (item *Item) IsEmpty() bool {
len(item.WidgetConfig) == 0)
}
func (item *Item) GetOverriddenItem() *Item {
overrides := GetJSONConfig()
clone := *item
clone.Name = overrides.GetDisplayName(item)
clone.Icon = overrides.GetDisplayIcon(item)
clone.Category = overrides.GetCategory(item)
clone.Show = overrides.GetShowItem(item)
return &clone
}
func NewHomePageConfig() Config {
return Config(make(map[string]Category))
}

View File

@@ -0,0 +1,31 @@
package homepage
import (
"testing"
. "github.com/yusing/go-proxy/internal/utils/testing"
)
func TestOverrideItem(t *testing.T) {
a := &Item{
Show: false,
Alias: "foo",
Name: "Foo",
Icon: &IconURL{
Value: "/favicon.ico",
IconSource: IconSourceRelative,
},
Category: "App",
}
overrides := GetJSONConfig()
ExpectNoError(t, overrides.SetShowItemOverride(a.Alias, true))
ExpectNoError(t, overrides.SetDisplayNameOverride(a.Alias, "Bar"))
ExpectNoError(t, overrides.SetDisplayCategoryOverride(a.Alias, "Test"))
ExpectNoError(t, overrides.SetIconOverride(a.Alias, "png/example.png"))
overridden := a.GetOverriddenItem()
ExpectTrue(t, overridden.Show)
ExpectEqual(t, overridden.Name, "Bar")
ExpectEqual(t, overridden.Category, "Test")
ExpectEqual(t, overridden.Icon.String(), "png/example.png")
}

View File

@@ -3,14 +3,15 @@ package homepage
import (
"strings"
"github.com/yusing/go-proxy/internal"
E "github.com/yusing/go-proxy/internal/error"
)
type (
IconURL struct {
Value string `json:"value"`
IconSource
Extra *IconExtra `json:"extra"`
Value string `json:"value"`
IconSource `json:"source"`
Extra *IconExtra `json:"extra"`
}
IconExtra struct {
@@ -25,12 +26,23 @@ const (
IconSourceAbsolute IconSource = iota
IconSourceRelative
IconSourceWalkXCode
IconSourceSelfhSt
)
const DashboardIconBaseURL = "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/"
var ErrInvalidIconURL = E.New("invalid icon url")
func (u *IconURL) HasIcon() bool {
if u.IconSource == IconSourceSelfhSt &&
!internal.HasSelfhstIcon(u.Extra.Name, u.Extra.FileType) {
return false
}
if u.IconSource == IconSourceWalkXCode &&
!internal.HasWalkxCodeIcon(u.Extra.Name, u.Extra.FileType) {
return false
}
return true
}
// Parse implements strutils.Parser.
func (u *IconURL) Parse(v string) error {
if v == "" {
@@ -45,11 +57,12 @@ func (u *IconURL) Parse(v string) error {
case "http:", "https:":
u.Value = v
u.IconSource = IconSourceAbsolute
return nil
case "@target":
case "@target", "": // @target/favicon.ico, /favicon.ico
u.Value = v[slashIndex:]
u.IconSource = IconSourceRelative
return nil
if u.Value == "/" {
return ErrInvalidIconURL.Withf("%s", "empty path")
}
case "png", "svg", "webp": // walkXCode Icons
u.Value = v
u.IconSource = IconSourceWalkXCode
@@ -57,10 +70,38 @@ func (u *IconURL) Parse(v string) error {
FileType: beforeSlash,
Name: strings.TrimSuffix(v[slashIndex+1:], "."+beforeSlash),
}
return nil
case "@selfhst": // selfh.st Icons, @selfhst/<reference>.<format>
u.Value = v[slashIndex:]
u.IconSource = IconSourceSelfhSt
parts := strings.Split(v[slashIndex+1:], ".")
if len(parts) != 2 {
return ErrInvalidIconURL.Withf("%s", "expect @selfhst/<reference>.<format>, e.g. @selfhst/adguard-home.webp")
}
reference, format := parts[0], strings.ToLower(parts[1])
if reference == "" || format == "" {
return ErrInvalidIconURL
}
switch format {
case "svg", "png", "webp":
default:
return ErrInvalidIconURL.Withf("%s", "invalid format, expect svg/png/webp")
}
u.Extra = &IconExtra{
FileType: format,
Name: reference,
}
default:
return ErrInvalidIconURL
return ErrInvalidIconURL.Withf("%s", v)
}
if u.Value == "" {
return ErrInvalidIconURL.Withf("%s", "empty")
}
if !u.HasIcon() {
return ErrInvalidIconURL.Withf("no such icon %s", u.Value)
}
return nil
}
func (u *IconURL) String() string {

View File

@@ -29,6 +29,24 @@ func TestIconURL(t *testing.T) {
IconSource: IconSourceRelative,
},
},
{
name: "relative2",
input: "/icon.png",
wantValue: &IconURL{
Value: "/icon.png",
IconSource: IconSourceRelative,
},
},
{
name: "relative_empty_path",
input: "@target/",
wantErr: true,
},
{
name: "relative_empty_path2",
input: "/",
wantErr: true,
},
{
name: "walkxcode",
input: "png/walkxcode.png",
@@ -41,6 +59,33 @@ func TestIconURL(t *testing.T) {
},
},
},
{
name: "walkxcode_invalid_format",
input: "foo/walkxcode.png",
wantErr: true,
},
{
name: "selfh.st_valid",
input: "@selfhst/foo.png",
wantValue: &IconURL{
Value: "/foo.png",
IconSource: IconSourceSelfhSt,
Extra: &IconExtra{
FileType: "png",
Name: "foo",
},
},
},
{
name: "selfh.st_invalid",
input: "@selfhst/foo",
wantErr: true,
},
{
name: "selfh.st_invalid_format",
input: "@selfhst/foo.bar",
wantErr: true,
},
{
name: "invalid",
input: "invalid",

View File

@@ -0,0 +1,147 @@
package homepage
import (
"errors"
"os"
"sync"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/utils"
)
type JSONConfig struct {
DisplayNameOverride map[string]string `json:"display_name_override"`
DisplayCategoryOverride map[string]string `json:"display_category_override"`
DisplayOrder map[string]int `json:"display_order"` // TODO: implement this
CategoryNameOverride map[string]string `json:"category_name_override"`
CategoryOrder map[string]int `json:"category_order"` // TODO: implement this
IconOverride map[string]*IconURL `json:"icon_override"`
ShowItemOverride map[string]bool `json:"show_item_override"`
mu sync.RWMutex
}
var jsonConfigInstance *JSONConfig
func InitOverridesConfig() {
jsonConfigInstance = &JSONConfig{
DisplayNameOverride: make(map[string]string),
DisplayCategoryOverride: make(map[string]string),
DisplayOrder: make(map[string]int),
CategoryNameOverride: make(map[string]string),
CategoryOrder: make(map[string]int),
IconOverride: make(map[string]*IconURL),
ShowItemOverride: make(map[string]bool),
}
err := utils.LoadJSON(common.HomepageJSONConfigPath, jsonConfigInstance)
if err != nil && !os.IsNotExist(err) {
logging.Fatal().Err(err).Msg("failed to load homepage overrides config")
}
}
func GetJSONConfig() *JSONConfig {
return jsonConfigInstance
}
func (c *JSONConfig) save() error {
if common.IsTest {
return nil
}
return utils.SaveJSON(common.HomepageJSONConfigPath, c, 0o644)
}
func (c *JSONConfig) GetDisplayName(item *Item) string {
c.mu.RLock()
defer c.mu.RUnlock()
if override, ok := c.DisplayNameOverride[item.Alias]; ok {
return override
}
return item.Name
}
func (c *JSONConfig) SetDisplayNameOverride(key, value string) error {
c.mu.Lock()
defer c.mu.Unlock()
c.DisplayNameOverride[key] = value
return c.save()
}
func (c *JSONConfig) GetCategory(item *Item) string {
c.mu.RLock()
defer c.mu.RUnlock()
category := item.Category
if override, ok := c.DisplayCategoryOverride[item.Alias]; ok {
category = override
}
if override, ok := c.CategoryNameOverride[category]; ok {
return override
}
return category
}
func (c *JSONConfig) SetDisplayCategoryOverride(key, value string) error {
c.mu.Lock()
defer c.mu.Unlock()
c.DisplayCategoryOverride[key] = value
return c.save()
}
func (c *JSONConfig) SetDisplayOrder(key string, value int) error {
c.mu.Lock()
defer c.mu.Unlock()
c.DisplayOrder[key] = value
return c.save()
}
func (c *JSONConfig) SetCategoryNameOverride(key, value string) error {
c.mu.Lock()
defer c.mu.Unlock()
c.CategoryNameOverride[key] = value
return c.save()
}
func (c *JSONConfig) SetCategoryOrder(key string, value int) error {
c.mu.Lock()
defer c.mu.Unlock()
c.CategoryOrder[key] = value
return c.save()
}
func (c *JSONConfig) GetDisplayIcon(item *Item) *IconURL {
c.mu.RLock()
defer c.mu.RUnlock()
if override, ok := c.IconOverride[item.Alias]; ok {
return override
}
return item.Icon
}
func (c *JSONConfig) SetIconOverride(key, value string) error {
c.mu.Lock()
defer c.mu.Unlock()
var url IconURL
if err := url.Parse(value); err != nil {
return err
}
if !url.HasIcon() {
return errors.New("no such icon")
}
c.IconOverride[key] = &url
return c.save()
}
func (c *JSONConfig) GetShowItem(item *Item) bool {
c.mu.RLock()
defer c.mu.RUnlock()
if override, ok := c.ShowItemOverride[item.Alias]; ok {
return override
}
return true
}
func (c *JSONConfig) SetShowItemOverride(key string, value bool) error {
c.mu.Lock()
defer c.mu.Unlock()
c.ShowItemOverride[key] = value
return c.save()
}