diff --git a/internal/api/themes.go b/internal/api/themes.go index cd36b47..0325e4b 100644 --- a/internal/api/themes.go +++ b/internal/api/themes.go @@ -1,6 +1,7 @@ package api import ( + _ "embed" "encoding/json" "errors" "fmt" @@ -19,6 +20,29 @@ type themeManifest struct { Version string `json:"version"` } +//go:embed themes/white.css +var whiteThemeCSS string + +//go:embed themes/black.css +var blackThemeCSS string + +var builtinThemes = []struct { + themeManifest + css string +}{ + {themeManifest{"white", "白色", "1"}, whiteThemeCSS}, + {themeManifest{"black", "黑色", "1"}, blackThemeCSS}, +} + +func builtinTheme(id string) (themeManifest, string, bool) { + for _, theme := range builtinThemes { + if theme.ID == id { + return theme.themeManifest, theme.css, true + } + } + return themeManifest{}, "", false +} + func readThemeFile(root *os.Root, name string, limit int64) ([]byte, error) { f, err := root.Open(name) if err != nil { @@ -43,6 +67,9 @@ func readThemeFile(root *os.Root, name string, limit int64) ([]byte, error) { } func (s *Server) readTheme(id string) (themeManifest, string, error) { + if manifest, css, ok := builtinTheme(id); ok { + return manifest, css, nil + } var manifest themeManifest if !themeIDPattern.MatchString(id) || id == "default" || s.ThemesDir == "" { return manifest, "", fs.ErrNotExist @@ -73,6 +100,9 @@ func (s *Server) readTheme(id string) (themeManifest, string, error) { func (s *Server) listThemes(w http.ResponseWriter, r *http.Request) { themes := []themeManifest{{ID: "default", Name: "默认", Version: "1"}} + for _, theme := range builtinThemes { + themes = append(themes, theme.themeManifest) + } if s.ThemesDir != "" { entries, err := os.ReadDir(s.ThemesDir) if err != nil && !errors.Is(err, fs.ErrNotExist) { @@ -80,6 +110,9 @@ func (s *Server) listThemes(w http.ResponseWriter, r *http.Request) { return } for _, entry := range entries { + if _, _, ok := builtinTheme(entry.Name()); ok { + continue + } if !entry.IsDir() || !themeIDPattern.MatchString(entry.Name()) || entry.Name() == "default" { continue } diff --git a/internal/api/themes/black.css b/internal/api/themes/black.css new file mode 100644 index 0000000..4936f54 --- /dev/null +++ b/internal/api/themes/black.css @@ -0,0 +1,30 @@ +:root { + color-scheme: dark; + --rm-bg: #0b0b0c; + --rm-sidebar-bg: #111113; + --rm-text: #e6e8eb; + --rm-text-muted: #9b9fa8; + --rm-divider: #2b2d33; + --rm-accent: #8eafff; + --rm-accent-strong: #b3c9ff; + --rm-selection: rgba(142, 175, 255, 0.25); + --rm-hover: rgba(230, 232, 235, 0.065); + --rm-active: rgba(142, 175, 255, 0.14); + --rm-danger: #ff8291; + --rm-warn-bg: #2c2413; + --rm-warn-text: #e8c477; + --rm-shadow: 0 12px 36px rgba(0, 0, 0, 0.6); + --rm-inline-code-bg: #202127; + --rm-inline-code-text: #c3d5f5; + --rm-code-bg: #18191e; + --rm-code-text: #e1e4e8; + --rm-highlight-bg: rgba(232, 196, 119, 0.25); +} + +/* 浅蓝按钮使用深色文字,保持选中状态与操作按钮的对比度。 */ +.rm-btn-primary, +.rm-btn-primary:hover, +.rm-segmented button.on, +.rm-share-password button { + color: #0b0b0c; +} diff --git a/internal/api/themes/white.css b/internal/api/themes/white.css new file mode 100644 index 0000000..764d712 --- /dev/null +++ b/internal/api/themes/white.css @@ -0,0 +1,22 @@ +:root { + color-scheme: light; + --rm-bg: #ffffff; + --rm-sidebar-bg: #f6f7f9; + --rm-text: #202124; + --rm-text-muted: #656b75; + --rm-divider: #e3e6eb; + --rm-accent: #2563eb; + --rm-accent-strong: #1d4ed8; + --rm-selection: rgba(37, 99, 235, 0.18); + --rm-hover: rgba(32, 33, 36, 0.045); + --rm-active: rgba(37, 99, 235, 0.10); + --rm-danger: #c42b3a; + --rm-warn-bg: #fff8e6; + --rm-warn-text: #855600; + --rm-shadow: 0 12px 36px rgba(24, 31, 44, 0.12); + --rm-inline-code-bg: #f0f2f5; + --rm-inline-code-text: #40536d; + --rm-code-bg: #292d3e; + --rm-code-text: #e1e4e8; + --rm-highlight-bg: #fff0a8; +} diff --git a/internal/api/themes_test.go b/internal/api/themes_test.go index 4851d0c..d06d60c 100644 --- a/internal/api/themes_test.go +++ b/internal/api/themes_test.go @@ -29,9 +29,15 @@ func TestThemeAndShareAppearanceBoundaries(t *testing.T) { t.Fatalf("匿名用户不应读取主题,得到 %d", code) } themes := decode[struct{ Themes []themeManifest }](t, f.do(f.admin, "GET", "/api/v1/themes", "", nil)) - if len(themes.Themes) != 2 || themes.Themes[1].ID != "paper" { + if len(themes.Themes) != 4 || themes.Themes[0].ID != "default" || themes.Themes[1].ID != "white" || themes.Themes[2].ID != "black" || themes.Themes[3].ID != "paper" { t.Fatalf("主题列表错误:%+v", themes.Themes) } + for _, id := range []string{"white", "black"} { + theme := decode[map[string]string](t, f.do(f.admin, "GET", "/api/v1/themes/"+id, "", nil)) + if theme["id"] != id || !strings.Contains(theme["css"], "--rm-bg:") { + t.Fatalf("内置主题未提供配色:%s", id) + } + } theme := decode[map[string]string](t, f.do(f.admin, "GET", "/api/v1/themes/paper", "", nil)) if !strings.Contains(theme["css"], "#faf6ed") { t.Fatal("未读取到主题 CSS") diff --git a/web/src/app/AppearanceSettings.vue b/web/src/app/AppearanceSettings.vue index 2babe22..26af0a7 100644 --- a/web/src/app/AppearanceSettings.vue +++ b/web/src/app/AppearanceSettings.vue @@ -1,7 +1,7 @@