mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-25 17:58:40 +02:00
feat: light/dark variant icons and selfh.st tag as default category
- code refactor - reduce memory usage
This commit is contained in:
373
internal/homepage/list_icons.go
Normal file
373
internal/homepage/list_icons.go
Normal file
@@ -0,0 +1,373 @@
|
||||
package homepage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
"github.com/yusing/go-proxy/internal/logging"
|
||||
"github.com/yusing/go-proxy/internal/task"
|
||||
"github.com/yusing/go-proxy/internal/utils"
|
||||
"github.com/yusing/go-proxy/internal/utils/strutils"
|
||||
)
|
||||
|
||||
type (
|
||||
IconKey string
|
||||
IconMap map[IconKey]*IconMeta
|
||||
IconList []string
|
||||
IconMeta struct {
|
||||
SVG, PNG, WebP bool
|
||||
Light, Dark bool
|
||||
DisplayName string
|
||||
Tag string
|
||||
}
|
||||
IconMetaSearch struct {
|
||||
Source IconSource
|
||||
Ref string
|
||||
SVG bool
|
||||
PNG bool
|
||||
WebP bool
|
||||
Light bool
|
||||
Dark bool
|
||||
}
|
||||
Cache struct {
|
||||
Icons IconMap
|
||||
LastUpdate time.Time
|
||||
sync.RWMutex `json:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
func (icon *IconMeta) Filenames(ref string) []string {
|
||||
filenames := make([]string, 0)
|
||||
if icon.SVG {
|
||||
filenames = append(filenames, fmt.Sprintf("%s.svg", ref))
|
||||
if icon.Light {
|
||||
filenames = append(filenames, fmt.Sprintf("%s-light.svg", ref))
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, fmt.Sprintf("%s-dark.svg", ref))
|
||||
}
|
||||
}
|
||||
if icon.PNG {
|
||||
filenames = append(filenames, fmt.Sprintf("%s.png", ref))
|
||||
if icon.Light {
|
||||
filenames = append(filenames, fmt.Sprintf("%s-light.png", ref))
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, fmt.Sprintf("%s-dark.png", ref))
|
||||
}
|
||||
}
|
||||
if icon.WebP {
|
||||
filenames = append(filenames, fmt.Sprintf("%s.webp", ref))
|
||||
if icon.Light {
|
||||
filenames = append(filenames, fmt.Sprintf("%s-light.webp", ref))
|
||||
}
|
||||
if icon.Dark {
|
||||
filenames = append(filenames, fmt.Sprintf("%s-dark.webp", ref))
|
||||
}
|
||||
}
|
||||
return filenames
|
||||
}
|
||||
|
||||
const updateInterval = 2 * time.Hour
|
||||
|
||||
var iconsCache = &Cache{
|
||||
Icons: make(IconMap),
|
||||
}
|
||||
|
||||
const (
|
||||
walkxcodeIcons = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/tree.json"
|
||||
selfhstIcons = "https://cdn.selfh.st/directory/icons.json"
|
||||
)
|
||||
|
||||
func NewIconKey(source IconSource, reference string) IconKey {
|
||||
return IconKey(fmt.Sprintf("%s/%s", source, reference))
|
||||
}
|
||||
|
||||
func (k IconKey) SourceRef() (IconSource, string) {
|
||||
parts := strings.Split(string(k), "/")
|
||||
return IconSource(parts[0]), parts[1]
|
||||
}
|
||||
|
||||
func InitIconListCache() {
|
||||
iconsCache.Lock()
|
||||
defer iconsCache.Unlock()
|
||||
|
||||
err := utils.LoadJSONIfExist(common.IconListCachePath, iconsCache)
|
||||
if err != nil {
|
||||
logging.Error().Err(err).Msg("failed to load icons")
|
||||
} else if len(iconsCache.Icons) > 0 {
|
||||
logging.Info().
|
||||
Int("icons", len(iconsCache.Icons)).
|
||||
Msg("icons loaded")
|
||||
}
|
||||
|
||||
if err = updateIcons(); err != nil {
|
||||
logging.Error().Err(err).Msg("failed to update icons")
|
||||
}
|
||||
|
||||
task.OnProgramExit("save_icons_cache", func() {
|
||||
utils.SaveJSON(common.IconListCachePath, iconsCache, 0o644)
|
||||
})
|
||||
}
|
||||
|
||||
func ListAvailableIcons() (*Cache, error) {
|
||||
if common.IsTest {
|
||||
return iconsCache, nil
|
||||
}
|
||||
|
||||
iconsCache.RLock()
|
||||
if time.Since(iconsCache.LastUpdate) < updateInterval {
|
||||
if len(iconsCache.Icons) == 0 {
|
||||
iconsCache.RUnlock()
|
||||
return iconsCache, nil
|
||||
}
|
||||
}
|
||||
iconsCache.RUnlock()
|
||||
|
||||
iconsCache.Lock()
|
||||
defer iconsCache.Unlock()
|
||||
|
||||
logging.Info().Msg("updating icon data")
|
||||
if err := updateIcons(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logging.Info().Int("icons", len(iconsCache.Icons)).Msg("icons list updated")
|
||||
|
||||
iconsCache.LastUpdate = time.Now()
|
||||
|
||||
err := utils.SaveJSON(common.IconListCachePath, iconsCache, 0o644)
|
||||
if err != nil {
|
||||
logging.Warn().Err(err).Msg("failed to save icons")
|
||||
}
|
||||
return iconsCache, nil
|
||||
}
|
||||
|
||||
func SearchIcons(keyword string, limit int) ([]IconMetaSearch, error) {
|
||||
if keyword == "" {
|
||||
return make([]IconMetaSearch, 0), nil
|
||||
}
|
||||
iconsCache.RLock()
|
||||
defer iconsCache.RUnlock()
|
||||
result := make([]IconMetaSearch, 0)
|
||||
for k, icon := range iconsCache.Icons {
|
||||
if fuzzy.MatchFold(keyword, string(k)) {
|
||||
source, ref := k.SourceRef()
|
||||
result = append(result, IconMetaSearch{
|
||||
Source: source,
|
||||
Ref: ref,
|
||||
SVG: icon.SVG,
|
||||
PNG: icon.PNG,
|
||||
WebP: icon.WebP,
|
||||
Light: icon.Light,
|
||||
Dark: icon.Dark,
|
||||
})
|
||||
}
|
||||
if len(result) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func HasIcon(icon *IconURL) bool {
|
||||
if icon.Extra == nil {
|
||||
return false
|
||||
}
|
||||
if common.IsTest {
|
||||
return true
|
||||
}
|
||||
iconsCache.RLock()
|
||||
defer iconsCache.RUnlock()
|
||||
key := NewIconKey(icon.IconSource, icon.Extra.Ref)
|
||||
meta, ok := iconsCache.Icons[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
switch icon.Extra.FileType {
|
||||
case "png":
|
||||
return meta.PNG && (!icon.Extra.IsLight || meta.Light) && (!icon.Extra.IsDark || meta.Dark)
|
||||
case "svg":
|
||||
return meta.SVG && (!icon.Extra.IsLight || meta.Light) && (!icon.Extra.IsDark || meta.Dark)
|
||||
case "webp":
|
||||
return meta.WebP && (!icon.Extra.IsLight || meta.Light) && (!icon.Extra.IsDark || meta.Dark)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type HomepageMeta struct {
|
||||
DisplayName string
|
||||
Tag string
|
||||
}
|
||||
|
||||
func GetHomepageMeta(ref string) (HomepageMeta, bool) {
|
||||
iconsCache.RLock()
|
||||
defer iconsCache.RUnlock()
|
||||
meta, ok := iconsCache.Icons[NewIconKey(IconSourceSelfhSt, ref)]
|
||||
if !ok {
|
||||
return HomepageMeta{}, false
|
||||
}
|
||||
return HomepageMeta{
|
||||
DisplayName: meta.DisplayName,
|
||||
Tag: meta.Tag,
|
||||
}, true
|
||||
}
|
||||
|
||||
func updateIcons() error {
|
||||
clear(iconsCache.Icons)
|
||||
if err := UpdateWalkxCodeIcons(); err != nil {
|
||||
return err
|
||||
}
|
||||
return UpdateSelfhstIcons()
|
||||
}
|
||||
|
||||
var httpGet = httpGetImpl
|
||||
|
||||
func MockHttpGet(body []byte) {
|
||||
httpGet = func(_ string) ([]byte, error) {
|
||||
return body, nil
|
||||
}
|
||||
}
|
||||
|
||||
func httpGetImpl(url string) ([]byte, error) {
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
/*
|
||||
format:
|
||||
|
||||
{
|
||||
"png": [
|
||||
"*.png",
|
||||
],
|
||||
"svg": [
|
||||
"*.svg",
|
||||
],
|
||||
"webp": [
|
||||
"*.webp",
|
||||
]
|
||||
}
|
||||
*/
|
||||
func UpdateWalkxCodeIcons() error {
|
||||
body, err := httpGet(walkxcodeIcons)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := make(map[string][]string)
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for fileType, files := range data {
|
||||
var setExt func(icon *IconMeta)
|
||||
switch fileType {
|
||||
case "png":
|
||||
setExt = func(icon *IconMeta) { icon.PNG = true }
|
||||
case "svg":
|
||||
setExt = func(icon *IconMeta) { icon.SVG = true }
|
||||
case "webp":
|
||||
setExt = func(icon *IconMeta) { icon.WebP = true }
|
||||
}
|
||||
for _, f := range files {
|
||||
f = strings.TrimSuffix(f, "."+fileType)
|
||||
isLight := strings.HasSuffix(f, "-light")
|
||||
if isLight {
|
||||
f = strings.TrimSuffix(f, "-light")
|
||||
}
|
||||
key := NewIconKey(IconSourceWalkXCode, f)
|
||||
icon, ok := iconsCache.Icons[key]
|
||||
if !ok {
|
||||
icon = new(IconMeta)
|
||||
iconsCache.Icons[key] = icon
|
||||
}
|
||||
setExt(icon)
|
||||
if isLight {
|
||||
icon.Light = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
format:
|
||||
|
||||
{
|
||||
"Name": "2FAuth",
|
||||
"Reference": "2fauth",
|
||||
"SVG": "Yes",
|
||||
"PNG": "Yes",
|
||||
"WebP": "Yes",
|
||||
"Light": "Yes",
|
||||
"Dark": "Yes",
|
||||
"Tag": "",
|
||||
"Category": "Self-Hosted",
|
||||
"CreatedAt": "2024-08-16 00:27:23+00:00"
|
||||
}
|
||||
*/
|
||||
|
||||
func UpdateSelfhstIcons() error {
|
||||
type SelfhStIcon struct {
|
||||
Name string
|
||||
Reference string
|
||||
SVG string
|
||||
PNG string
|
||||
WebP string
|
||||
Light string
|
||||
Dark string
|
||||
Tags string
|
||||
}
|
||||
|
||||
body, err := httpGet(selfhstIcons)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := make([]SelfhStIcon, 0)
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, item := range data {
|
||||
var tag string
|
||||
if item.Tags != "" {
|
||||
tag = strutils.CommaSeperatedList(item.Tags)[0]
|
||||
}
|
||||
icon := &IconMeta{
|
||||
DisplayName: item.Name,
|
||||
Tag: tag,
|
||||
SVG: item.SVG == "Yes",
|
||||
PNG: item.PNG == "Yes",
|
||||
WebP: item.WebP == "Yes",
|
||||
Light: item.Light == "Yes",
|
||||
Dark: item.Dark == "Yes",
|
||||
}
|
||||
key := NewIconKey(IconSourceSelfhSt, item.Reference)
|
||||
iconsCache.Icons[key] = icon
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user