mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 09:18:31 +02:00
feat: support selfh.st icons, support homepage config overriding
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
31
internal/homepage/homepage_test.go
Normal file
31
internal/homepage/homepage_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package homepage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/yusing/go-proxy/internal/utils/testing"
|
||||
)
|
||||
|
||||
func TestOverrideItem(t *testing.T) {
|
||||
a := &Item{
|
||||
Show: false,
|
||||
Alias: "foo",
|
||||
Name: "Foo",
|
||||
Icon: &IconURL{
|
||||
Value: "/favicon.ico",
|
||||
IconSource: IconSourceRelative,
|
||||
},
|
||||
Category: "App",
|
||||
}
|
||||
overrides := GetJSONConfig()
|
||||
ExpectNoError(t, overrides.SetShowItemOverride(a.Alias, true))
|
||||
ExpectNoError(t, overrides.SetDisplayNameOverride(a.Alias, "Bar"))
|
||||
ExpectNoError(t, overrides.SetDisplayCategoryOverride(a.Alias, "Test"))
|
||||
ExpectNoError(t, overrides.SetIconOverride(a.Alias, "png/example.png"))
|
||||
|
||||
overridden := a.GetOverriddenItem()
|
||||
ExpectTrue(t, overridden.Show)
|
||||
ExpectEqual(t, overridden.Name, "Bar")
|
||||
ExpectEqual(t, overridden.Category, "Test")
|
||||
ExpectEqual(t, overridden.Icon.String(), "png/example.png")
|
||||
}
|
||||
@@ -3,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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
147
internal/homepage/json_config.go
Normal file
147
internal/homepage/json_config.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package homepage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
)
|
||||
|
||||
type JSONConfig struct {
|
||||
DisplayNameOverride map[string]string `json:"display_name_override"`
|
||||
DisplayCategoryOverride map[string]string `json:"display_category_override"`
|
||||
DisplayOrder map[string]int `json:"display_order"` // TODO: implement this
|
||||
CategoryNameOverride map[string]string `json:"category_name_override"`
|
||||
CategoryOrder map[string]int `json:"category_order"` // TODO: implement this
|
||||
IconOverride map[string]*IconURL `json:"icon_override"`
|
||||
ShowItemOverride map[string]bool `json:"show_item_override"`
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var jsonConfigInstance *JSONConfig
|
||||
|
||||
func InitOverridesConfig() {
|
||||
jsonConfigInstance = &JSONConfig{
|
||||
DisplayNameOverride: make(map[string]string),
|
||||
DisplayCategoryOverride: make(map[string]string),
|
||||
DisplayOrder: make(map[string]int),
|
||||
CategoryNameOverride: make(map[string]string),
|
||||
CategoryOrder: make(map[string]int),
|
||||
IconOverride: make(map[string]*IconURL),
|
||||
ShowItemOverride: make(map[string]bool),
|
||||
}
|
||||
err := utils.LoadJSON(common.HomepageJSONConfigPath, jsonConfigInstance)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
logging.Fatal().Err(err).Msg("failed to load homepage overrides config")
|
||||
}
|
||||
}
|
||||
|
||||
func GetJSONConfig() *JSONConfig {
|
||||
return jsonConfigInstance
|
||||
}
|
||||
|
||||
func (c *JSONConfig) save() error {
|
||||
if common.IsTest {
|
||||
return nil
|
||||
}
|
||||
return utils.SaveJSON(common.HomepageJSONConfigPath, c, 0o644)
|
||||
}
|
||||
|
||||
func (c *JSONConfig) GetDisplayName(item *Item) string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if override, ok := c.DisplayNameOverride[item.Alias]; ok {
|
||||
return override
|
||||
}
|
||||
return item.Name
|
||||
}
|
||||
|
||||
func (c *JSONConfig) SetDisplayNameOverride(key, value string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.DisplayNameOverride[key] = value
|
||||
return c.save()
|
||||
}
|
||||
|
||||
func (c *JSONConfig) GetCategory(item *Item) string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
category := item.Category
|
||||
if override, ok := c.DisplayCategoryOverride[item.Alias]; ok {
|
||||
category = override
|
||||
}
|
||||
if override, ok := c.CategoryNameOverride[category]; ok {
|
||||
return override
|
||||
}
|
||||
return category
|
||||
}
|
||||
|
||||
func (c *JSONConfig) SetDisplayCategoryOverride(key, value string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.DisplayCategoryOverride[key] = value
|
||||
return c.save()
|
||||
}
|
||||
|
||||
func (c *JSONConfig) SetDisplayOrder(key string, value int) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.DisplayOrder[key] = value
|
||||
return c.save()
|
||||
}
|
||||
|
||||
func (c *JSONConfig) SetCategoryNameOverride(key, value string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.CategoryNameOverride[key] = value
|
||||
return c.save()
|
||||
}
|
||||
|
||||
func (c *JSONConfig) SetCategoryOrder(key string, value int) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.CategoryOrder[key] = value
|
||||
return c.save()
|
||||
}
|
||||
|
||||
func (c *JSONConfig) GetDisplayIcon(item *Item) *IconURL {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if override, ok := c.IconOverride[item.Alias]; ok {
|
||||
return override
|
||||
}
|
||||
return item.Icon
|
||||
}
|
||||
|
||||
func (c *JSONConfig) SetIconOverride(key, value string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
var url IconURL
|
||||
if err := url.Parse(value); err != nil {
|
||||
return err
|
||||
}
|
||||
if !url.HasIcon() {
|
||||
return errors.New("no such icon")
|
||||
}
|
||||
c.IconOverride[key] = &url
|
||||
return c.save()
|
||||
}
|
||||
|
||||
func (c *JSONConfig) GetShowItem(item *Item) bool {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if override, ok := c.ShowItemOverride[item.Alias]; ok {
|
||||
return override
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *JSONConfig) SetShowItemOverride(key string, value bool) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.ShowItemOverride[key] = value
|
||||
return c.save()
|
||||
}
|
||||
Reference in New Issue
Block a user