forked from mxx1111/mdlook
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy paththeme.ts
More file actions
352 lines (297 loc) · 10.9 KB
/
Copy paththeme.ts
File metadata and controls
352 lines (297 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
import type { HeadingLevel, HeadingStyles, HeadingStyleType, PerThemeSettings, PerThemeSettingsMap, ThemeName } from '@md/shared/configs'
import type { DesignMdTheme } from '@md/shared/design-md'
import { applyTheme } from '@md/core'
import { defaultPerThemeSettings, defaultStyleConfig, themeOptions, widthOptions } from '@md/shared/configs'
import { useCssEditorStore } from '@/stores/cssEditor'
import { addPrefix } from '@/utils'
import { store } from '@/utils/storage'
/** Legacy localStorage keys used before per-theme settings */
const LEGACY_KEYS = [`fonts`, `size`, `color`, `codeBlockTheme`, `headingStyles`, `isMacCodeBlock`, `isShowLineNumber`]
/**
* Run legacy migration synchronously before store.reactive installs its watch.
* Returns the migrated PerThemeSettingsMap (or {} if nothing to migrate).
*/
function migrateLegacySettingsSync(currentTheme: string): PerThemeSettingsMap {
let hasAnyLegacyKey = false
try {
hasAnyLegacyKey = LEGACY_KEYS.some(key => localStorage.getItem(key) !== null)
}
catch { return {} }
if (!hasAnyLegacyKey)
return {}
const migrationKey = addPrefix(`legacy_migrated`)
try {
if (localStorage.getItem(migrationKey) !== null)
return {}
}
catch { return {} }
const existingMapRaw = localStorage.getItem(addPrefix(`themeSettings`))
const existingMap: PerThemeSettingsMap = existingMapRaw ? JSON.parse(existingMapRaw) : {}
const defaults = defaultPerThemeSettings()
const settings: PerThemeSettings = { ...existingMap[currentTheme] ?? defaults }
const legacyFont = localStorage.getItem(`fonts`)
if (legacyFont)
settings.fontFamily = legacyFont
const legacySize = localStorage.getItem(`size`)
if (legacySize)
settings.fontSize = legacySize
const legacyColor = localStorage.getItem(`color`)
if (legacyColor)
settings.primaryColor = legacyColor
const legacyCodeTheme = localStorage.getItem(`codeBlockTheme`)
if (legacyCodeTheme)
settings.codeBlockTheme = legacyCodeTheme
const legacyHeading = localStorage.getItem(`headingStyles`)
if (legacyHeading) {
try { settings.headingStyles = JSON.parse(legacyHeading) }
catch { /* ignore parse error */ }
}
const legacyMacBlock = localStorage.getItem(`isMacCodeBlock`)
if (legacyMacBlock !== null)
settings.isMacCodeBlock = legacyMacBlock === `true`
const legacyLineNum = localStorage.getItem(`isShowLineNumber`)
if (legacyLineNum !== null)
settings.isShowLineNumber = legacyLineNum === `true`
const result: PerThemeSettingsMap = { ...existingMap, [currentTheme]: settings }
// Persist the merged map so store.reactive will pick it up
try { localStorage.setItem(addPrefix(`themeSettings`), JSON.stringify(result)) }
catch { /* quota error — non-critical */ }
// Mark migration as done
try { localStorage.setItem(migrationKey, `1`) }
catch { /* ignore */ }
// Clean up legacy keys
for (const key of LEGACY_KEYS) {
try { localStorage.removeItem(key) }
catch { /* ignore */ }
}
return result
}
/**
* 主题和样式配置 Store
* 负责管理所有与主题、字体、颜色相关的配置
*
* 每个主题拥有独立的配置(primaryColor、fontFamily、fontSize、codeBlockTheme、
* headingStyles、isShowLineNumber、isMacCodeBlock),切换主题时自动加载对应配置。
*/
export const useThemeStore = defineStore(`theme`, () => {
// --- Legacy migration: run BEFORE reactive so initial value is correct ---
const initialTheme = (() => {
try { return localStorage.getItem(addPrefix(`theme`)) ?? defaultStyleConfig.theme }
catch { return defaultStyleConfig.theme }
})()
const migratedSettings = migrateLegacySettingsSync(initialTheme as ThemeName)
// 当前选中的主题
const theme = store.reactive<string>(addPrefix(`theme`), defaultStyleConfig.theme)
// DESIGN.md 导入主题(包含文章 CSS 与 App 变量 CSS)
const designThemes = store.reactive<Record<string, DesignMdTheme>>(addPrefix(`designThemes`), {})
// 每个主题的独立配置(持久化到 localStorage)
const themeSettings = store.reactive<PerThemeSettingsMap>(
addPrefix(`themeSettings`),
migratedSettings,
)
// 获取当前主题的配置(不存在时返回默认值)
const currentSettings = computed<PerThemeSettings>(() => {
return themeSettings.value[theme.value] ?? defaultPerThemeSettings()
})
const allThemeOptions = computed(() => {
const imported = Object.values(designThemes.value).map(item => ({
label: item.name.replace(/\s+inspired$/i, ``),
value: item.id,
desc: item.description,
}))
return [...themeOptions, ...imported]
})
const currentDesignTheme = computed(() => designThemes.value[theme.value])
// --- Per-theme computed properties ---
// 使用 computed({ get, set }) 保持与现有 UI 组件的 Ref API 兼容
const primaryColor = computed<string>({
get: () => currentSettings.value.primaryColor,
set: (v: string) => { setThemeField(`primaryColor`, v) },
})
const fontFamily = computed<string>({
get: () => currentSettings.value.fontFamily,
set: (v: string) => { setThemeField(`fontFamily`, v) },
})
const fontSize = computed<string>({
get: () => currentSettings.value.fontSize,
set: (v: string) => { setThemeField(`fontSize`, v) },
})
const codeBlockTheme = computed<string>({
get: () => currentSettings.value.codeBlockTheme,
set: (v: string) => { setThemeField(`codeBlockTheme`, v) },
})
const headingStyles = computed<HeadingStyles>({
get: () => currentSettings.value.headingStyles,
set: (v: HeadingStyles) => { setThemeField(`headingStyles`, v) },
})
const isShowLineNumber = computed<boolean>({
get: () => currentSettings.value.isShowLineNumber,
set: (v: boolean) => { setThemeField(`isShowLineNumber`, v) },
})
const isMacCodeBlock = computed<boolean>({
get: () => currentSettings.value.isMacCodeBlock,
set: (v: boolean) => { setThemeField(`isMacCodeBlock`, v) },
})
/** 更新当前主题的某个字段 */
function setThemeField<K extends keyof PerThemeSettings>(key: K, value: PerThemeSettings[K]) {
const t = theme.value
const existing = themeSettings.value[t] ?? defaultPerThemeSettings()
themeSettings.value = {
...themeSettings.value,
[t]: { ...existing, [key]: value },
}
}
// --- Global (non-theme) properties ---
// 是否开启微信外链接底部引用
const isCiteStatus = store.reactive(`isCiteStatus`, defaultStyleConfig.isCiteStatus)
// 是否统计字数和阅读时间
const isCountStatus = store.reactive(`isCountStatus`, defaultStyleConfig.isCountStatus)
// 是否开启段落首行缩进
const isUseIndent = store.reactive(addPrefix(`use_indent`), false)
// 是否开启两端对齐
const isUseJustify = store.reactive(addPrefix(`use_justify`), false)
// 图注格式
const legend = store.reactive(`legend`, defaultStyleConfig.legend)
// 预览宽度
const previewWidth = store.reactive(`previewWidth`, widthOptions[0].value)
// 计算属性
const fontSizeNumber = computed(() => Number(fontSize.value.replace(`px`, ``)))
// Toggle 方法
const toggleMacCodeBlock = useToggle(isMacCodeBlock)
const toggleShowLineNumber = useToggle(isShowLineNumber)
const toggleCiteStatus = useToggle(isCiteStatus)
const toggleCountStatus = useToggle(isCountStatus)
const toggleUseIndent = useToggle(isUseIndent)
const toggleUseJustify = useToggle(isUseJustify)
// 重置样式(仅重置当前主题的配置)
const resetStyle = () => {
themeSettings.value = {
...themeSettings.value,
[theme.value]: defaultPerThemeSettings(),
}
isCiteStatus.value = defaultStyleConfig.isCiteStatus
isCountStatus.value = defaultStyleConfig.isCountStatus
legend.value = defaultStyleConfig.legend
isUseIndent.value = false
isUseJustify.value = false
}
function setDesignTheme(designTheme: DesignMdTheme) {
designThemes.value = {
...designThemes.value,
[designTheme.id]: designTheme,
}
theme.value = designTheme.id
}
function removeDesignTheme(id: string) {
const next = { ...designThemes.value }
delete next[id]
designThemes.value = next
if (theme.value === id)
theme.value = defaultStyleConfig.theme
}
function applyAppThemeCss(css?: string) {
const id = `md-design-app-theme`
let el = document.querySelector<HTMLStyleElement>(`#${id}`)
if (!css) {
el?.remove()
return
}
if (!el) {
el = document.createElement(`style`)
el.id = id
document.head.appendChild(el)
}
el.textContent = css
}
// 设置标题样式
const setHeadingStyle = (level: HeadingLevel, style: HeadingStyleType) => {
const existing = headingStyles.value
headingStyles.value = {
...existing,
[level]: style === `default` ? undefined : style,
}
}
// 获取标题样式
const getHeadingStyle = (level: HeadingLevel): HeadingStyleType => {
return headingStyles.value[level] || `default`
}
// 切换 highlight.js 代码主题
const updateCodeTheme = () => {
const cssUrl = codeBlockTheme.value
const el = document.querySelector(`#hljs`)
if (el) {
el.setAttribute(`href`, cssUrl)
}
else {
const link = document.createElement(`link`)
link.setAttribute(`type`, `text/css`)
link.setAttribute(`rel`, `stylesheet`)
link.setAttribute(`href`, cssUrl)
link.setAttribute(`id`, `hljs`)
document.head.appendChild(link)
}
}
/**
* 应用当前主题配置(新主题系统)
* 使用 CSS 注入而非内联样式
*/
const applyCurrentTheme = async () => {
try {
const cssEditorStore = useCssEditorStore()
const customCSS = cssEditorStore.getCurrentTabContent()
await applyTheme({
themeName: theme.value,
customCSS,
dynamicThemeCSS: currentDesignTheme.value?.articleCss,
variables: {
primaryColor: primaryColor.value,
fontFamily: fontFamily.value,
fontSize: fontSize.value,
isUseIndent: isUseIndent.value,
isUseJustify: isUseJustify.value,
headingStyles: headingStyles.value,
},
})
applyAppThemeCss(currentDesignTheme.value?.appCss)
}
catch (error) {
console.error(`[applyCurrentTheme] 主题应用失败:`, error)
}
}
return {
// State
theme,
themeSettings,
designThemes,
allThemeOptions,
currentDesignTheme,
fontFamily,
fontSize,
fontSizeNumber,
primaryColor,
codeBlockTheme,
legend,
isMacCodeBlock,
isShowLineNumber,
isCiteStatus,
isCountStatus,
isUseIndent,
isUseJustify,
previewWidth,
headingStyles,
// Actions
toggleMacCodeBlock,
toggleShowLineNumber,
toggleCiteStatus,
toggleCountStatus,
toggleUseIndent,
toggleUseJustify,
resetStyle,
setDesignTheme,
removeDesignTheme,
updateCodeTheme,
applyCurrentTheme,
setHeadingStyle,
getHeadingStyle,
}
})