diff --git a/src/db/settings.rs b/src/db/settings.rs
index c7d21cb..3f93038 100644
--- a/src/db/settings.rs
+++ b/src/db/settings.rs
@@ -188,6 +188,9 @@ impl Default for BrandSettings {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct ActionSettings {
+ /// 明暗切换图标
+ #[serde(default = "default_true")]
+ pub theme: bool,
/// 刷新图标(重扫本地项目并重拉内容库)
pub refresh: bool,
/// 站点设置图标(仅登录后出现)
@@ -199,6 +202,7 @@ pub struct ActionSettings {
impl Default for ActionSettings {
fn default() -> Self {
Self {
+ theme: true,
refresh: true,
settings: true,
session: true,
diff --git a/stash-web/src/AppNext.tsx b/stash-web/src/AppNext.tsx
index 0d11853..2d5e95e 100644
--- a/stash-web/src/AppNext.tsx
+++ b/stash-web/src/AppNext.tsx
@@ -6,6 +6,7 @@ import {
SearchField,
Space,
Toaster,
+ toast,
type MenuItem,
} from '@stash/ui'
import { useEffect, useMemo, useRef, useState } from 'react'
@@ -26,6 +27,7 @@ import { useProjects } from './useProjects'
import { fetchSession, logout, type SessionState } from './auth'
import {
fetchSettings,
+ saveSettings,
type SettingsDocument,
type SiteSettings,
} from './settings'
@@ -287,7 +289,7 @@ export default function AppNext() {
favicon: '',
theme: 'auto',
brand: { name: 'STASH', mark: 'S', logo: '' },
- actions: { refresh: true, settings: true, session: true },
+ actions: { theme: true, refresh: true, settings: true, session: true },
},
}
@@ -298,6 +300,27 @@ export default function AppNext() {
document.documentElement.dataset.scheme = site.site.theme
}, [site.site.theme])
+ /** 当前实际是深色吗(auto 时按系统偏好算) */
+ const isDark =
+ site.site.theme === 'dark' ||
+ (site.site.theme === 'auto' &&
+ window.matchMedia('(prefers-color-scheme: dark)').matches)
+
+ /** 页头的明暗切换:写回站点设置,前后台一起变;只有登录后才会显示 */
+ const toggleTheme = async () => {
+ if (settings === null) return
+ const next = isDark ? 'light' : 'dark'
+ const result = await saveSettings(settings.version, {
+ ...settings.settings,
+ site: { ...settings.settings.site, theme: next },
+ })
+ if (result.ok) {
+ setSettings(result.document)
+ return
+ }
+ toast.error(result.error)
+ }
+
// 站点级设置:浏览器标签标题与 favicon(空值回落到内置)
useEffect(() => {
const title = site.site.title.trim()
@@ -648,6 +671,8 @@ export default function AppNext() {
authenticated={session?.authenticated === true}
username={session?.username ?? undefined}
refreshing={refreshing}
+ dark={isDark}
+ onToggleTheme={() => void toggleTheme()}
onRefresh={() => {
refresh()
load()
diff --git a/stash-web/src/components/admin/AdminNext.tsx b/stash-web/src/components/admin/AdminNext.tsx
index 69ea500..8699f15 100644
--- a/stash-web/src/components/admin/AdminNext.tsx
+++ b/stash-web/src/components/admin/AdminNext.tsx
@@ -1935,6 +1935,16 @@ function HeaderPanel({