Files
sub-store/internal/handler/settings.go
T

204 lines
6.0 KiB
Go

package handler
import (
"encoding/json"
"github.com/gofiber/fiber/v3"
)
// defaultSettings returns the default settings map.
func defaultSettings() map[string]any {
return map[string]any{
"defaultUserAgent": "clash.meta/v1.19.24",
"defaultFlowUserAgent": "clash.meta/v1.19.24",
"defaultTimeout": "30000",
"backendRequestConcurrency": "3",
"backendRequestConcurrencyWaitTime": "100",
"remoteCacheTtl": "300",
"remoteCacheStaleOnError": true,
"nodeInfoApiUrl": "https://ipwho.is/{ip}",
"theme": map[string]any{
"auto": true,
"name": "light",
"dark": "dark",
"light": "light",
},
"appearanceSetting": map[string]any{
"isSimpleMode": true,
"isLeftRight": false,
"isDefaultIcon": false,
"isIconColor": false,
"isShowIcon": true,
"isSimpleShowRemark": false,
"isEditorCommon": false,
"manualSubscriptionsDisplayMode": "collapsed",
"editorGroupingMode": "always",
"isSimpleReicon": false,
"isSubItemMenuFold": true,
"showFloatingRefreshButton": false,
"showFloatingAddButton": false,
"createItemPosition": "bottom",
"displayPreviewInWebPage": true,
"subProgressStyle": "hidden",
"listPageViewMode": "single-column",
"listPageViewModeInWideScreenNarrowMode": "single-column",
"useNarrowModeOnWideScreen": false,
},
"appName": "Sub-Store",
}
}
// mergeSettings merges default + stored settings.
// Per review-resolution #4: top-level keys from stored override default;
// theme and appearanceSetting are shallow-merged (stored child keys override default child keys).
func mergeSettings(base, stored map[string]any) map[string]any {
result := make(map[string]any, len(base))
for k, v := range base {
result[k] = v
}
for k, v := range stored {
if v != nil {
result[k] = v
}
}
// Shallow merge for theme and appearanceSetting
result["theme"] = shallowMergeObject(base["theme"], stored["theme"])
result["appearanceSetting"] = shallowMergeObject(base["appearanceSetting"], stored["appearanceSetting"])
return result
}
func shallowMergeObject(base, overlay any) map[string]any {
result := map[string]any{}
if b, ok := base.(map[string]any); ok {
for k, v := range b {
result[k] = v
}
}
if o, ok := overlay.(map[string]any); ok {
for k, v := range o {
if v != nil {
result[k] = v
}
}
}
return result
}
// HandleGetSettings returns merged settings.
func (d *Deps) HandleGetSettings(c fiber.Ctx) error {
stored, err := d.SettingsRepo.Get()
if err != nil {
return failed(c, "Failed to read settings", 500)
}
return success(c, mergeSettings(defaultSettings(), stored))
}
// HandleUpdateSettings patches settings.
func (d *Deps) HandleUpdateSettings(c fiber.Ctx) error {
var input map[string]any
if err := json.Unmarshal(c.Body(), &input); err != nil {
input = map[string]any{}
}
merged, err := d.SettingsRepo.Update(input)
if err != nil {
return failed(c, "Failed to update settings", 500)
}
return success(c, mergeSettings(defaultSettings(), merged))
}
// HandleExportStorage exports all data.
// Per review-resolution #31: exclude built-in templates.
func (d *Deps) HandleExportStorage(c fiber.Ctx) error {
sources, _ := d.SourceRepo.List()
collections, _ := d.CollectionRepo.List()
templates, _ := d.TemplateRepo.List()
settings, _ := d.SettingsRepo.Get()
// Filter out built-in templates
exportedTemplates := make([]any, 0, len(templates))
builtinIDs := getBuiltinTemplateIDs()
for _, t := range templates {
if !builtinIDs[t.ID] {
exportedTemplates = append(exportedTemplates, t)
}
}
// Actually filter properly — already done above
payload := fiber.Map{
"version": 1,
"exportedAt": "now",
"settings": settings,
"sources": sources,
"collections": collections,
"templates": exportedTemplates,
}
c.Set("Content-Type", "application/json; charset=utf-8")
c.Set("Content-Disposition", `attachment; filename="sub-store-backup.json"`)
return c.JSON(payload)
}
// HandleImportStorage imports data.
// Per review-resolution #30: import order = settings → sources → templates → collections.
func (d *Deps) HandleImportStorage(c fiber.Ctx) error {
input, err := parseJsonOrText(c)
if err != nil {
return failed(c, "Invalid import payload", 400)
}
// Import settings first
if settings, ok := input["settings"].(map[string]any); ok {
d.SettingsRepo.Update(settings)
}
// Import sources
if sources, ok := input["sources"].([]any); ok {
for _, s := range sources {
if record, ok := s.(map[string]any); ok {
d.SourceRepo.Upsert(mapToSourceRecord(record))
}
}
}
// Import templates (before collections, since collections reference templates)
builtinIDs := getBuiltinTemplateIDs()
if templates, ok := input["templates"].([]any); ok {
for _, t := range templates {
if record, ok := t.(map[string]any); ok {
id := getStringValue(record["id"])
if id != "" && !builtinIDs[id] {
d.TemplateRepo.Upsert(mapToTemplateRecord(record))
}
}
}
}
// Import collections last
if collections, ok := input["collections"].([]any); ok {
for _, col := range collections {
if record, ok := col.(map[string]any); ok {
d.CollectionRepo.Upsert(mapToCollectionRecord(record))
}
}
}
return success(c, fiber.Map{"restored": true})
}
// parseJsonOrText parses body as JSON, or wraps text as {content: text}.
// Per review-resolution #9: JSON first, plain text fallback.
func parseJsonOrText(c fiber.Ctx) (map[string]any, error) {
body := c.Body()
var result map[string]any
if err := json.Unmarshal(body, &result); err == nil {
return result, nil
}
// Try as string
text := string(body)
if text == "" {
return map[string]any{}, nil
}
return map[string]any{"content": text}, nil
}