feat: 前台明暗切换改为本机偏好,不再要求登录
- 新增 src/theme.ts:localStorage 的 stash-theme 作为本机偏好,优先于站点默认 - 前台页头的月亮/太阳只写本机偏好,未登录也能用,刷新后保持 - 后台「页头 → 配色」与后台页头按钮改的是站点默认值;后台按钮同时清掉本机偏好, 保证点完看到的就是新默认 - 两个壳都按「本机偏好 ?? 站点默认」决定 data-scheme
This commit is contained in:
+21
-22
@@ -6,7 +6,6 @@ import {
|
|||||||
SearchField,
|
SearchField,
|
||||||
Space,
|
Space,
|
||||||
Toaster,
|
Toaster,
|
||||||
toast,
|
|
||||||
type MenuItem,
|
type MenuItem,
|
||||||
} from '@stash/ui'
|
} from '@stash/ui'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
@@ -27,10 +26,15 @@ import { useProjects } from './useProjects'
|
|||||||
import { fetchSession, logout, type SessionState } from './auth'
|
import { fetchSession, logout, type SessionState } from './auth'
|
||||||
import {
|
import {
|
||||||
fetchSettings,
|
fetchSettings,
|
||||||
saveSettings,
|
|
||||||
type SettingsDocument,
|
type SettingsDocument,
|
||||||
type SiteSettings,
|
type SiteSettings,
|
||||||
} from './settings'
|
} from './settings'
|
||||||
|
import {
|
||||||
|
isDarkScheme,
|
||||||
|
readThemeOverride,
|
||||||
|
writeThemeOverride,
|
||||||
|
type ThemeChoice,
|
||||||
|
} from './theme'
|
||||||
import {
|
import {
|
||||||
appDetailModel,
|
appDetailModel,
|
||||||
posterDetailModel,
|
posterDetailModel,
|
||||||
@@ -295,30 +299,25 @@ export default function AppNext() {
|
|||||||
|
|
||||||
const keywordLower = keyword.trim().toLowerCase()
|
const keywordLower = keyword.trim().toLowerCase()
|
||||||
|
|
||||||
// 配色:写在 <html data-scheme> 上,深色由组件库的令牌块接管
|
// 配色:本机偏好优先于站点默认,写在 <html data-scheme> 上,
|
||||||
|
// 深色由组件库的令牌块接管,页面与组件都不用改
|
||||||
|
const [themeOverride, setThemeOverride] = useState<ThemeChoice | null>(() =>
|
||||||
|
readThemeOverride(),
|
||||||
|
)
|
||||||
|
const effectiveTheme = themeOverride ?? site.site.theme
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.documentElement.dataset.scheme = site.site.theme
|
document.documentElement.dataset.scheme = effectiveTheme
|
||||||
}, [site.site.theme])
|
}, [effectiveTheme])
|
||||||
|
|
||||||
/** 当前实际是深色吗(auto 时按系统偏好算) */
|
/** 当前实际是深色吗(auto 时按系统偏好算) */
|
||||||
const isDark =
|
const isDark = isDarkScheme(effectiveTheme)
|
||||||
site.site.theme === 'dark' ||
|
|
||||||
(site.site.theme === 'auto' &&
|
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
||||||
|
|
||||||
/** 页头的明暗切换:写回站点设置,前后台一起变;只有登录后才会显示 */
|
/** 页头的明暗切换:只写本机偏好,不需要登录;站点默认值在后台改 */
|
||||||
const toggleTheme = async () => {
|
const toggleTheme = () => {
|
||||||
if (settings === null) return
|
const next: ThemeChoice = isDark ? 'light' : 'dark'
|
||||||
const next = isDark ? 'light' : 'dark'
|
setThemeOverride(next)
|
||||||
const result = await saveSettings(settings.version, {
|
writeThemeOverride(next)
|
||||||
...settings.settings,
|
|
||||||
site: { ...settings.settings.site, theme: next },
|
|
||||||
})
|
|
||||||
if (result.ok) {
|
|
||||||
setSettings(result.document)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toast.error(result.error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 站点级设置:浏览器标签标题与 favicon(空值回落到内置)
|
// 站点级设置:浏览器标签标题与 favicon(空值回落到内置)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import { changePassword, fetchSession, logout, type SessionState } from '../../auth'
|
import { changePassword, fetchSession, logout, type SessionState } from '../../auth'
|
||||||
import { LoginDialog } from '../auth/LoginDialog'
|
import { LoginDialog } from '../auth/LoginDialog'
|
||||||
import { GroupMark as GroupGlyph } from '../common/GroupMark'
|
import { GroupMark as GroupGlyph } from '../common/GroupMark'
|
||||||
|
import { isDarkScheme, readThemeOverride, writeThemeOverride } from '../../theme'
|
||||||
import {
|
import {
|
||||||
fetchSettings,
|
fetchSettings,
|
||||||
saveSettings,
|
saveSettings,
|
||||||
@@ -135,7 +136,8 @@ export function AdminNext() {
|
|||||||
if (meta === undefined) return
|
if (meta === undefined) return
|
||||||
const title = meta.title.trim()
|
const title = meta.title.trim()
|
||||||
if (title !== '') window.document.title = title
|
if (title !== '') window.document.title = title
|
||||||
window.document.documentElement.dataset.scheme = meta.theme
|
// 本机偏好优先于站点默认,跟前台一致
|
||||||
|
window.document.documentElement.dataset.scheme = readThemeOverride() ?? meta.theme
|
||||||
const href = meta.favicon.trim() === '' ? '/favicon.svg' : meta.favicon.trim()
|
const href = meta.favicon.trim() === '' ? '/favicon.svg' : meta.favicon.trim()
|
||||||
let link = window.document.head.querySelector<HTMLLinkElement>('link[rel="icon"]')
|
let link = window.document.head.querySelector<HTMLLinkElement>('link[rel="icon"]')
|
||||||
if (link === null) {
|
if (link === null) {
|
||||||
@@ -178,15 +180,15 @@ export function AdminNext() {
|
|||||||
toast.success('侧边栏已保存')
|
toast.success('侧边栏已保存')
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 页头的明暗切换:写回 site.theme,前后台一起变 */
|
/** 页头的明暗切换:改的是站点默认值,并清掉本机偏好,保证点完看到的就是新默认 */
|
||||||
const themeSetting = draft?.site.theme ?? document?.settings.site.theme ?? 'auto'
|
const themeSetting = draft?.site.theme ?? document?.settings.site.theme ?? 'auto'
|
||||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
const isDark = isDarkScheme(readThemeOverride() ?? themeSetting)
|
||||||
const isDark = themeSetting === 'dark' || (themeSetting === 'auto' && prefersDark)
|
|
||||||
|
|
||||||
const toggleTheme = async () => {
|
const toggleTheme = async () => {
|
||||||
if (document === null || draft === null) return
|
if (document === null || draft === null) return
|
||||||
const next = isDark ? 'light' : 'dark'
|
const next = isDark ? 'light' : 'dark'
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
writeThemeOverride(null)
|
||||||
const result = await saveSettings(document.version, {
|
const result = await saveSettings(document.version, {
|
||||||
...draft,
|
...draft,
|
||||||
site: { ...draft.site, theme: next },
|
site: { ...draft.site, theme: next },
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* 明暗主题的「本机偏好」。
|
||||||
|
*
|
||||||
|
* 站点设置里的 `site.theme` 是**所有人**的默认值;这里的 localStorage 是**这台浏览器**
|
||||||
|
* 的选择,读到时优先于默认值。前台页头的切换按钮只改本机偏好,不需要登录;
|
||||||
|
* 后台「页头 → 配色」改的是站点默认值。
|
||||||
|
*/
|
||||||
|
export type ThemeChoice = 'light' | 'dark'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'stash-theme'
|
||||||
|
|
||||||
|
export function readThemeOverride(): ThemeChoice | null {
|
||||||
|
try {
|
||||||
|
const value = window.localStorage.getItem(STORAGE_KEY)
|
||||||
|
return value === 'light' || value === 'dark' ? value : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeThemeOverride(value: ThemeChoice | null): void {
|
||||||
|
try {
|
||||||
|
if (value === null) window.localStorage.removeItem(STORAGE_KEY)
|
||||||
|
else window.localStorage.setItem(STORAGE_KEY, value)
|
||||||
|
} catch {
|
||||||
|
// 隐私模式下 localStorage 可能不可写,忽略即可
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `light` / `dark` / `auto` 里,当前实际是不是深色 */
|
||||||
|
export function isDarkScheme(scheme: string): boolean {
|
||||||
|
if (scheme === 'dark') return true
|
||||||
|
if (scheme === 'light') return false
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user