From e7092254abe2bce9474f2b4baf9b788183b1e030 Mon Sep 17 00:00:00 2001 From: Victor_Jay Date: Tue, 30 Jun 2026 17:43:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(theme):=20=E7=AE=A1=E7=90=86=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E6=96=B0=E5=A2=9E=E6=A8=A1=E6=9D=BF=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=99=A8=EF=BC=8C=E6=94=AF=E6=8C=81=E8=BF=90=E8=A1=8C=E6=97=B6?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E5=88=87=E6=8D=A2=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增功能: - 主题发现模块:扫描 templates/ 目录下含 theme.json 的目录 - 站点设置新增 site.theme 键,DB 持久化 + 内存缓存 - 运行时切换主题:保存后即时重载 Gin 模板 + 静态资源哈希 - 管理后台 '外观主题' 页面:列表展示、选择、一键切换 - 动态静态资源服务:根据当前主题目录提供 CSS/JS 修复: - account.html 残留孤儿 {{end}} 模板语法错误 - 动态静态资源 handler 误判 Gin *filepath 路径前缀为绝对路径 变更文件(12 modified + 3 new): - cmd/server/main.go: 动态主题加载 + 运行时重载 + 动态静态服务 - internal/theme/discovery.go: 新增主题扫描 - internal/config/site_settings.go: ThemeName() - internal/service/site_setting_service.go: ThemeReloader + UpdateTheme - internal/controller/admin/: ISP 扩展 + GetThemes/UpdateTheme/ThemePage - internal/controller/auth_controller.go: 准则路径动态化 - internal/router/: InjectThemeReloader + 新路由 - templates/: 外观主题页面 + JS + CSS + 子导航 --- cmd/server/main.go | 93 ++++++++++++--- internal/config/site_settings.go | 5 + internal/controller/admin/interfaces.go | 3 +- .../admin/site_setting_controller.go | 74 ++++++++++++ internal/controller/auth_controller.go | 2 +- internal/router/admin.go | 4 + internal/router/router.go | 15 +++ internal/service/site_setting_service.go | 14 +++ internal/theme/discovery.go | 52 +++++++++ .../MetaLab-2026/html/settings/account.html | 1 - templates/admin/html/site-settings/_menu.html | 1 + templates/admin/html/site-settings/theme.html | 44 +++++++ templates/admin/static/css/site-settings.css | 27 +++++ .../admin/static/js/site-settings-theme.js | 109 ++++++++++++++++++ 14 files changed, 426 insertions(+), 18 deletions(-) create mode 100644 internal/theme/discovery.go create mode 100644 templates/admin/html/site-settings/theme.html create mode 100644 templates/admin/static/js/site-settings-theme.js diff --git a/cmd/server/main.go b/cmd/server/main.go index 07a3e16..82a127d 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,7 +1,12 @@ package main import ( + "html/template" "log" + "net/http" + "os" + "path/filepath" + "strings" "time" "metazone.cc/mce/internal/common" @@ -69,26 +74,49 @@ func main() { r := gin.Default() - // 计算静态资源哈希(用于缓存破坏,文件不变哈希不变) - common.ComputeAssetHashes("templates/MetaLab-2026") + // 站点设置管理器(DB 持久化 + 内存缓存 — 必须在模板加载前初始化) + siteSettings, err := config.NewSiteSettings(db) + if err != nil { + log.Fatalf("初始化站点设置失败: %v", err) + } + + // 读取当前主题名称 + themeName := siteSettings.ThemeName() + log.Printf("当前主题: %s", themeName) + + // 计算静态资源哈希 + common.ComputeAssetHashes("templates/" + themeName) // 模板加载(前端主题 + 管理后台) - tmpl, err := theme.LoadTemplates( - theme.TemplateRoot{Dir: "templates/MetaLab-2026/html", Prefix: ""}, - theme.TemplateRoot{Dir: "templates/admin/html", Prefix: "admin/"}, - ) + tmpl, err := loadThemeTemplates(themeName) if err != nil { - panic("加载模板失败: " + err.Error()) + log.Fatalf("加载模板失败: %v", err) } r.SetHTMLTemplate(tmpl) - // 静态资源 - r.Static("/static", "./templates/MetaLab-2026/static") + // 静态资源 — 前台使用动态主题目录 + r.GET("/static/*filepath", func(c *gin.Context) { + serveDynamicStatic(c, "templates", siteSettings, c.Param("filepath")) + }) r.Static("/admin/static", "./templates/admin/static") r.Static("/shared/static", "./templates/shared/static") r.Static("/uploads/avatars", "./storage/uploads/avatars") r.Static("/uploads/posts", "./storage/uploads/posts") + // 主题重载函数(切换主题后实时生效) + themeReloader := func(newTheme string) error { + log.Printf("切换主题: %s", newTheme) + // 更新静态资源哈希 + common.ComputeAssetHashes("templates/" + newTheme) + // 重新加载模板 + newTmpl, err := loadThemeTemplates(newTheme) + if err != nil { + return err + } + r.SetHTMLTemplate(newTmpl) + return nil + } + // Redis 客户端(可选,不可达时 FallbackStore 自动降级,恢复后自动切回) var redisClient *redis.Client if cfg.Redis.Enabled { @@ -103,18 +131,15 @@ func main() { log.Println("Redis 未启用,使用内存存储") } - // 站点设置管理器(DB 持久化 + 内存缓存) - siteSettings, err := config.NewSiteSettings(db) - if err != nil { - log.Fatalf("初始化站点设置失败: %v", err) - } - // 启动数据库健康检测(每 5s ping,失败自动降级) middleware.StartDBHealthCheck(db) // 路由注册 router.Setup(r, db, cfg, siteSettings, redisClient) + // 注入主题重载回调到站点设置 Controller + router.InjectThemeReloader(themeReloader) + // 定时发布调度器 scheduler.StartPublishScheduler(repository.NewPostRepo(db), 1*time.Minute) @@ -124,3 +149,41 @@ func main() { log.Fatalf("服务启动失败: %v", err) } } + +// loadThemeTemplates 加载指定主题的模板(前台 + 管理后台) +func loadThemeTemplates(themeName string) (*template.Template, error) { + return theme.LoadTemplates( + theme.TemplateRoot{Dir: "templates/" + themeName + "/html", Prefix: ""}, + theme.TemplateRoot{Dir: "templates/admin/html", Prefix: "admin/"}, + ) +} + +// serveDynamicStatic 根据当前站点主题动态提供静态资源 +func serveDynamicStatic(c *gin.Context, baseDir string, ss *config.SiteSettings, file string) { + themeName := ss.ThemeName() + root := filepath.Join(baseDir, themeName, "static") + + // 清理路径并拼接 + cleanFile := filepath.Clean(file) + fullPath := filepath.Join(root, cleanFile) + + // 安全检查:防止路径遍历逃逸出主题目录 + absRoot, err := filepath.Abs(root) + if err != nil { + c.AbortWithStatus(http.StatusNotFound) + return + } + absPath, err := filepath.Abs(fullPath) + if err != nil || !strings.HasPrefix(absPath, absRoot+string(os.PathSeparator)) && absPath != absRoot { + c.AbortWithStatus(http.StatusNotFound) + return + } + + info, err := os.Stat(fullPath) + if err != nil || info.IsDir() { + c.AbortWithStatus(http.StatusNotFound) + return + } + + http.ServeFile(c.Writer, c.Request, fullPath) +} diff --git a/internal/config/site_settings.go b/internal/config/site_settings.go index 44ca1c3..0e7e513 100644 --- a/internal/config/site_settings.go +++ b/internal/config/site_settings.go @@ -190,3 +190,8 @@ func (s *SiteSettings) GuidelinesTimer() int { func (s *SiteSettings) IsMaintenanceEnabled() bool { return s.GetBool("maintenance.enabled", false) } + +// ThemeName 当前选用的主题目录名,默认 "MetaLab-2026" +func (s *SiteSettings) ThemeName() string { + return s.Get("site.theme", "MetaLab-2026") +} diff --git a/internal/controller/admin/interfaces.go b/internal/controller/admin/interfaces.go index f610c6d..c92b3c8 100644 --- a/internal/controller/admin/interfaces.go +++ b/internal/controller/admin/interfaces.go @@ -23,12 +23,13 @@ type auditUseCase interface { Reject(reviewerID uint, submissionID uint, reason string) error } -// siteSettingUseCase SiteSettingController 对 SiteSettingService 的最小依赖(ISP:4 个方法) +// siteSettingUseCase SiteSettingController 对 SiteSettingService 的最小依赖(ISP:5 个方法) type siteSettingUseCase interface { GetSettings() map[string]string UpdateSetting(key, value string) error UpdateBoolSetting(key string, value bool) error GetAuditSettings(defaults *service.AuditDefaults) *service.AuditSettings + UpdateTheme(themeName string, reload service.ThemeReloader) error } // auditStatusProvider 审核控制器所需的用户信息查询(ISP) diff --git a/internal/controller/admin/site_setting_controller.go b/internal/controller/admin/site_setting_controller.go index db64b30..dadb181 100644 --- a/internal/controller/admin/site_setting_controller.go +++ b/internal/controller/admin/site_setting_controller.go @@ -6,6 +6,7 @@ import ( "metazone.cc/mce/internal/common" "metazone.cc/mce/internal/service" + "metazone.cc/mce/internal/theme" "github.com/gin-gonic/gin" ) @@ -14,6 +15,7 @@ import ( type SiteSettingController struct { settingService siteSettingUseCase defaults *service.AuditDefaults + themeReload service.ThemeReloader // 主题重载回调(切换后实时生效) } // NewSiteSettingController 构造函数 @@ -21,6 +23,11 @@ func NewSiteSettingController(settingService siteSettingUseCase, defaults *servi return &SiteSettingController{settingService: settingService, defaults: defaults} } +// SetThemeReloader 注入主题重载回调(main.go 中设置) +func (sc *SiteSettingController) SetThemeReloader(reload service.ThemeReloader) { + sc.themeReload = reload +} + // SiteSettingsPage 旧站点设置页 — 301 重定向到品牌页 func (sc *SiteSettingController) SiteSettingsPage(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "/admin/settings/brand") @@ -66,6 +73,16 @@ func (sc *SiteSettingController) ContentPage(c *gin.Context) { })) } +// ThemePage 外观主题设置页 +func (sc *SiteSettingController) ThemePage(c *gin.Context) { + c.HTML(http.StatusOK, "admin/site-settings/theme.html", common.BuildAdminPageData(c, gin.H{ + "Title": "站点设置 - 外观主题", + "ExtraCSS": "/admin/static/css/site-settings.css", + "ExtraJS": "/admin/static/js/site-settings-theme.js", + "ActiveSetting": "theme", + })) +} + // GetSettings 获取所有站点设置 func (sc *SiteSettingController) GetSettings(c *gin.Context) { all := sc.settingService.GetSettings() @@ -146,3 +163,60 @@ func (sc *SiteSettingController) GetBoolSetting(c *gin.Context) { common.Ok(c, gin.H{"value": d}) } } + +// GetThemes 获取所有可用主题列表 +func (sc *SiteSettingController) GetThemes(c *gin.Context) { + themes, err := theme.DiscoverThemes("templates") + if err != nil { + common.Error(c, http.StatusInternalServerError, "读取主题列表失败") + return + } + if themes == nil { + themes = []theme.ThemeInfo{} + } + // 读取当前主题 + all := sc.settingService.GetSettings() + currentTheme := "MetaLab-2026" + if t, ok := all["site.theme"]; ok && t != "" { + currentTheme = t + } + common.Ok(c, gin.H{ + "themes": themes, + "current": currentTheme, + }) +} + +// UpdateTheme 切换主题 +func (sc *SiteSettingController) UpdateTheme(c *gin.Context) { + var req struct { + Theme string `json:"theme" binding:"required"` + } + if err := c.ShouldBindJSON(&req); err != nil { + common.Error(c, http.StatusBadRequest, "参数错误") + return + } + + // 验证主题是否存在 + themes, err := theme.DiscoverThemes("templates") + if err != nil { + common.Error(c, http.StatusInternalServerError, "读取主题列表失败") + return + } + found := false + for _, t := range themes { + if t.Dir == req.Theme { + found = true + break + } + } + if !found { + common.Error(c, http.StatusBadRequest, "主题不存在") + return + } + + if err := sc.settingService.UpdateTheme(req.Theme, sc.themeReload); err != nil { + common.Error(c, http.StatusInternalServerError, "主题切换失败") + return + } + common.OkMessage(c, "主题已切换") +} diff --git a/internal/controller/auth_controller.go b/internal/controller/auth_controller.go index 484e277..4d3b6c4 100644 --- a/internal/controller/auth_controller.go +++ b/internal/controller/auth_controller.go @@ -39,7 +39,7 @@ func (ac *AuthController) RegisterPage(c *gin.Context) { if guidelinesHTML != "" { guidelines = template.HTML(guidelinesHTML) //nolint:gosec // 准则内容由管理员配置,属信任输入 } else { - content, err := theme.LoadContent("templates/MetaLab-2026/guidelines.html") + content, err := theme.LoadContent("templates/" + ac.siteSettings.ThemeName() + "/guidelines.html") if err != nil { c.String(http.StatusInternalServerError, "加载准则失败") return diff --git a/internal/router/admin.go b/internal/router/admin.go index 2526eaa..a97799b 100644 --- a/internal/router/admin.go +++ b/internal/router/admin.go @@ -36,6 +36,8 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { middleware.RequirePageRole(model.RoleOwner)) adminPages.GET("/settings/content", d.siteSettingCtrl.ContentPage, middleware.RequirePageRole(model.RoleOwner)) + adminPages.GET("/settings/theme", d.siteSettingCtrl.ThemePage, + middleware.RequirePageRole(model.RoleOwner)) adminPages.GET("/energy", d.adminEnergyCtrl.EnergyPage) adminPages.GET("/energy/logs", d.adminEnergyCtrl.EnergyLogPage) adminPages.GET("/energy/fund-logs", d.adminEnergyCtrl.FundLogPage) @@ -70,6 +72,8 @@ func setupAdminRoutes(r *gin.Engine, cfg *config.Config, d *dependencies) { siteSettingsAPI.PUT("", d.siteSettingCtrl.UpdateSetting) siteSettingsAPI.PUT("/bool", d.siteSettingCtrl.UpdateBoolSetting) siteSettingsAPI.GET("/bool", d.siteSettingCtrl.GetBoolSetting) + siteSettingsAPI.GET("/themes", d.siteSettingCtrl.GetThemes) + siteSettingsAPI.PUT("/theme", d.siteSettingCtrl.UpdateTheme) } // --- 帖子管理 API(admin+) --- diff --git a/internal/router/router.go b/internal/router/router.go index dbcd515..f00d678 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -3,13 +3,25 @@ package router import ( "metazone.cc/mce/internal/config" + adminCtrl "metazone.cc/mce/internal/controller/admin" "metazone.cc/mce/internal/middleware" + "metazone.cc/mce/internal/service" "github.com/gin-gonic/gin" "github.com/redis/go-redis/v9" "gorm.io/gorm" ) +// 保存站点设置 Controller 引用,供 InjectThemeReloader 使用 +var globalSiteSettingCtrl *adminCtrl.SiteSettingController + +// InjectThemeReloader 注入主题重载回调(main.go 在路由注册后调用,使主题切换实时生效) +func InjectThemeReloader(reload service.ThemeReloader) { + if globalSiteSettingCtrl != nil { + globalSiteSettingCtrl.SetThemeReloader(reload) + } +} + // Setup 注册所有路由 func Setup(r *gin.Engine, db *gorm.DB, cfg *config.Config, siteSettings *config.SiteSettings, redisClient *redis.Client) { r.Use(middleware.SecurityHeaders()) @@ -18,6 +30,9 @@ func Setup(r *gin.Engine, db *gorm.DB, cfg *config.Config, siteSettings *config. deps := buildDeps(db, cfg, siteSettings, redisClient) + // 保存站点设置 Controller 引用 + globalSiteSettingCtrl = deps.siteSettingCtrl + // 维护模式中间件(全局,在路由匹配之前) r.Use(deps.maintenanceMdw.Handler()) diff --git a/internal/service/site_setting_service.go b/internal/service/site_setting_service.go index df5bb58..0fa3d2f 100644 --- a/internal/service/site_setting_service.go +++ b/internal/service/site_setting_service.go @@ -80,3 +80,17 @@ func (s *SiteSettingService) GetBoolSetting(key string, defaultVal bool) bool { func ParseBool(v string) (bool, error) { return strconv.ParseBool(v) } + +// ThemeReloader 主题重载回调:切换主题后实时生效 +type ThemeReloader func(themeName string) error + +// UpdateTheme 切换主题并触发重载 +func (s *SiteSettingService) UpdateTheme(themeName string, reload ThemeReloader) error { + if err := s.settings.Set("site.theme", themeName); err != nil { + return err + } + if reload != nil { + return reload(themeName) + } + return nil +} diff --git a/internal/theme/discovery.go b/internal/theme/discovery.go new file mode 100644 index 0000000..289ae3a --- /dev/null +++ b/internal/theme/discovery.go @@ -0,0 +1,52 @@ +package theme + +import ( + "encoding/json" + "os" + "path/filepath" + "sort" +) + +// ThemeInfo 描述一个已安装的主题。 +type ThemeInfo struct { + Dir string `json:"dir"` // 目录名(如 "MetaLab-2026") + Name string `json:"name"` // 主题名称 + DisplayName string `json:"display_name"` // 展示名称 + Version string `json:"version"` // 版本号 + Author string `json:"author"` // 作者 + Description string `json:"description"` // 描述 +} + +// DiscoverThemes 扫描 templates/ 目录,返回所有包含 theme.json 的主题列表。 +func DiscoverThemes(rootDir string) ([]ThemeInfo, error) { + entries, err := os.ReadDir(rootDir) + if err != nil { + return nil, err + } + + var themes []ThemeInfo + for _, entry := range entries { + if !entry.IsDir() { + continue + } + themeJSON := filepath.Join(rootDir, entry.Name(), "theme.json") + data, err := os.ReadFile(themeJSON) //nolint:gosec // 内部主题文件,非用户输入 + if err != nil { + continue // 无 theme.json 则跳过 + } + + var info ThemeInfo + if err := json.Unmarshal(data, &info); err != nil { + continue + } + info.Dir = entry.Name() + themes = append(themes, info) + } + + // 按名称排序,保证稳定展示 + sort.Slice(themes, func(i, j int) bool { + return themes[i].Name < themes[j].Name + }) + + return themes, nil +} diff --git a/templates/MetaLab-2026/html/settings/account.html b/templates/MetaLab-2026/html/settings/account.html index f333802..2b895d0 100644 --- a/templates/MetaLab-2026/html/settings/account.html +++ b/templates/MetaLab-2026/html/settings/account.html @@ -80,6 +80,5 @@ - {{end}} {{template "settings/_footer.html" .}} diff --git a/templates/admin/html/site-settings/_menu.html b/templates/admin/html/site-settings/_menu.html index a26555f..b01e2ae 100644 --- a/templates/admin/html/site-settings/_menu.html +++ b/templates/admin/html/site-settings/_menu.html @@ -3,4 +3,5 @@ 安全与维护 注册策略 审核与内容 + 外观主题 diff --git a/templates/admin/html/site-settings/theme.html b/templates/admin/html/site-settings/theme.html new file mode 100644 index 0000000..6d41742 --- /dev/null +++ b/templates/admin/html/site-settings/theme.html @@ -0,0 +1,44 @@ +{{template "admin/layout/base.html" .}} + + + {{template "admin/site-settings/_menu.html" .}} + +
+
主题选择
+
+
+
+ 当前主题 + 切换主题后立即生效,无需重启服务 +
+
+ + +
+
+
+
+ 主题信息 + 选中主题的详细描述 +
+
+ +
+
+
+
+ + +{{template "admin/layout/footer.html" .}} + diff --git a/templates/admin/static/css/site-settings.css b/templates/admin/static/css/site-settings.css index 8ea3def..fa2bdf5 100644 --- a/templates/admin/static/css/site-settings.css +++ b/templates/admin/static/css/site-settings.css @@ -123,3 +123,30 @@ .toggle-switch input:disabled + .toggle-slider { opacity: 0.4; cursor: not-allowed; } + +/* --- 下拉选择器 + 保存按钮 --- */ +.setting-select { + padding: 6px 10px; border: 1px solid var(--border); + border-radius: 4px; font-size: 13px; min-width: 220px; + outline: none; background: #fff; cursor: pointer; + transition: border-color 0.2s; +} +.setting-select:focus { border-color: var(--primary); } +.btn-save-select { min-width: 56px; text-align: center; } +.btn-save-select:disabled { opacity: 0.5; cursor: not-allowed; } + +/* --- 主题信息详情 --- */ +.setting-info-detail { + flex: 1; min-width: 0; + font-size: 13px; color: var(--text-muted); +} + +/* --- 主题状态提示 --- */ +.status-success .section-body { + background: #e8f5e9; border-left: 3px solid #4caf50; + padding: 12px 20px; +} +.status-error .section-body { + background: #fce4ec; border-left: 3px solid #f44336; + padding: 12px 20px; +} diff --git a/templates/admin/static/js/site-settings-theme.js b/templates/admin/static/js/site-settings-theme.js new file mode 100644 index 0000000..c6ca00c --- /dev/null +++ b/templates/admin/static/js/site-settings-theme.js @@ -0,0 +1,109 @@ +/* ===================================================== + MetaLab 管理面板 - 主题切换 + 立即生效,无需重启 + ===================================================== */ + +(function() { + 'use strict'; + + var selector = document.getElementById('theme-selector'); + var saveBtn = document.getElementById('theme-save-btn'); + var infoEl = document.getElementById('theme-info'); + var statusSection = document.getElementById('theme-status'); + var statusMsg = document.getElementById('theme-status-msg'); + var themes = []; + var currentTheme = ''; + + async function loadThemes() { + try { + var res = await api.get('/api/admin/site-settings/themes'); + if (!res.success) { + showToast('加载主题列表失败', 'error'); + return; + } + themes = res.data.themes || []; + currentTheme = res.data.current || 'MetaLab-2026'; + + // 填充下拉 + selector.innerHTML = ''; + themes.forEach(function(t) { + var opt = document.createElement('option'); + opt.value = t.dir; + opt.textContent = t.display_name || t.name; + if (t.dir === currentTheme) { + opt.selected = true; + updateInfo(t); + } + selector.appendChild(opt); + }); + + saveBtn.disabled = false; + saveBtn.textContent = '保存'; + + // 选择变化时更新信息 + selector.addEventListener('change', function() { + var selected = selector.value; + var found = themes.find(function(t) { return t.dir === selected; }); + if (found) { + updateInfo(found); + } + saveBtn.disabled = (selected === currentTheme); + }); + + // 保存 + saveBtn.addEventListener('click', function() { + applyTheme(selector.value); + }); + } catch (e) { + showToast('加载主题列表失败', 'error'); + } + } + + function updateInfo(theme) { + var parts = []; + if (theme.display_name) parts.push(theme.display_name); + if (theme.version) parts.push('v' + theme.version); + if (theme.author) parts.push('作者:' + theme.author); + if (theme.description) parts.push(theme.description); + infoEl.innerHTML = '' + escapeHtml(parts.join(' · ')) + ''; + } + + async function applyTheme(themeDir) { + saveBtn.disabled = true; + saveBtn.textContent = '切换中...'; + showStatus('正在切换主题...', ''); + + try { + var res = await api.put('/api/admin/site-settings/theme', { theme: themeDir }); + if (res.success) { + currentTheme = themeDir; + saveBtn.textContent = '已生效'; + showToast('主题已切换,即时生效', 'success'); + showStatus('✅ 主题已切换为:' + themeDir + ',所有访客可见新主题', 'success'); + } else { + saveBtn.disabled = false; + saveBtn.textContent = '保存'; + showToast(res.message || '切换失败', 'error'); + showStatus('❌ 切换失败:' + (res.message || '未知错误'), 'error'); + } + } catch (e) { + saveBtn.disabled = false; + saveBtn.textContent = '保存'; + showToast('切换失败', 'error'); + showStatus('❌ 切换失败,请重试', 'error'); + } + } + + function showStatus(msg, type) { + statusSection.style.display = 'block'; + statusSection.className = 'settings-section'; + if (type === 'success') { + statusSection.classList.add('status-success'); + } else if (type === 'error') { + statusSection.classList.add('status-error'); + } + statusMsg.innerHTML = msg; + } + + loadThemes(); +})();