-
-
Notifications
You must be signed in to change notification settings - Fork 311
Custom template font #789
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom template font #789
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,78 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import axios from 'axios'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { logger } from '@/Core/util/logger'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { WebGAL } from '@/Core/WebGAL'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { TemplateFontDescriptor, WebgalTemplate } from '@/types/template'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { buildFontOptionsFromTemplate } from '@/Core/util/fonts/fontOptions'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { webgalStore } from '@/store/store'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { setFontOptions } from '@/store/GUIReducer'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { setOptionData } from '@/store/userDataReducer'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const TEMPLATE_PATH = './game/template/template.json'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const TEMPLATE_FONT_STYLE_SELECTOR = 'style[data-webgal-template-fonts]'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function loadTemplate(): Promise<WebgalTemplate | null> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data } = await axios.get<WebgalTemplate>(TEMPLATE_PATH); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WebGAL.template = data; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fonts = data.fonts ?? []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| injectTemplateFonts(fonts); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updateFontOptions(fonts); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return data; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warn('加载模板文件失败', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| updateFontOptions([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function updateFontOptions(fonts: TemplateFontDescriptor[]): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const options = buildFontOptionsFromTemplate(fonts); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| webgalStore.dispatch(setFontOptions(options)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const currentIndex = webgalStore.getState().userData.optionData.textboxFont ?? 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (options.length === 0) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (currentIndex >= options.length) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| webgalStore.dispatch(setOptionData({ key: 'textboxFont', value: 0 })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function injectTemplateFonts(fonts: TemplateFontDescriptor[]): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!fonts.length) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rules = fonts.map((font) => generateFontFaceRule(font)).filter((rule): rule is string => Boolean(rule)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!rules.length) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const styleElement = document.createElement('style'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| styleElement.setAttribute('data-webgal-template-fonts', 'true'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| styleElement.appendChild(document.createTextNode(rules.join('\n'))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const head = document.head; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!head) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const existing = head.querySelector<HTMLStyleElement>(TEMPLATE_FONT_STYLE_SELECTOR); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| existing?.remove(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| head.appendChild(styleElement); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function generateFontFaceRule(font: TemplateFontDescriptor): string | null { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fontFamily = font['font-family']; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!fontFamily || !font.url || !font.type) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.warn('忽略无效的模板字体配置', font); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const src = resolveTemplateAssetPath(font.url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const weight = font.weight !== undefined ? `font-weight: ${font.weight};` : ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const style = font.style ? `font-style: ${font.style};` : ''; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const display = `font-display: ${font.display ?? 'swap'};`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `@font-face { font-family: '${fontFamily}'; src: url('${src}') format('${font.type}'); ${weight} ${style} ${display} }`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+70
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function resolveTemplateAssetPath(path: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (/^(https?:)?\/\//i.test(path) || path.startsWith('data:')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return path; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalized = path.replace(/^[./]+/, ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `./game/template/${normalized}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+72
to
+78
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,46 @@ | ||||||||||||||||||||||||||
| import { FontOption } from '@/store/guiInterface'; | ||||||||||||||||||||||||||
| import { TemplateFontDescriptor } from '@/types/template'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export const DEFAULT_FONT_OPTIONS: FontOption[] = [ | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| family: `'思源宋体', serif`, | ||||||||||||||||||||||||||
| source: 'default', | ||||||||||||||||||||||||||
| labelKey: 'textFont.options.siYuanSimSun', | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| family: `'WebgalUI', serif`, | ||||||||||||||||||||||||||
| source: 'default', | ||||||||||||||||||||||||||
| labelKey: 'textFont.options.SimHei', | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| family: `'LXGW', serif`, | ||||||||||||||||||||||||||
| source: 'default', | ||||||||||||||||||||||||||
| labelKey: 'textFont.options.lxgw', | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export const FALLBACK_FONT_FAMILY = DEFAULT_FONT_OPTIONS[1].family; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export function buildFontOptionsFromTemplate(fonts: TemplateFontDescriptor[]): FontOption[] { | ||||||||||||||||||||||||||
| const templateOptions: FontOption[] = fonts.map((font) => ({ | ||||||||||||||||||||||||||
| family: formatFontFamily(font['font-family']), | ||||||||||||||||||||||||||
| source: 'template', | ||||||||||||||||||||||||||
| label: font['font-family'], | ||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const combined = [...templateOptions, ...DEFAULT_FONT_OPTIONS]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const seen = new Set<string>(); | ||||||||||||||||||||||||||
| return combined.filter((option) => { | ||||||||||||||||||||||||||
| if (seen.has(option.family)) return false; | ||||||||||||||||||||||||||
| seen.add(option.family); | ||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| export function formatFontFamily(fontFamily: string): string { | ||||||||||||||||||||||||||
| const trimmed = fontFamily.trim(); | ||||||||||||||||||||||||||
| const needsQuote = /\s/.test(trimmed); | ||||||||||||||||||||||||||
| const normalized = needsQuote ? `'${trimmed}'` : trimmed; | ||||||||||||||||||||||||||
| return `${normalized}, serif`; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+41
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,23 @@ | ||
| import { useSelector } from 'react-redux'; | ||
| import { RootState } from '@/store/store'; | ||
| import { textFont } from '@/store/userDataInterface'; | ||
| import { match } from '@/Core/util/match'; | ||
| import { RootState, webgalStore } from '@/store/store'; | ||
| import { FALLBACK_FONT_FAMILY } from '@/Core/util/fonts/fontOptions'; | ||
|
|
||
| export function useFontFamily() { | ||
| const fontFamily = useSelector((state: RootState) => state.userData.optionData.textboxFont); | ||
| export function useFontFamily(): string { | ||
| return useSelector(selectFontFamily); | ||
| } | ||
|
|
||
| function getFont() { | ||
| return match(fontFamily) | ||
| .with(textFont.song, () => '"思源宋体", serif') | ||
| .with(textFont.lxgw, () => '"LXGW", serif') | ||
| .with(textFont.hei, () => '"WebgalUI", serif') | ||
| .default(() => '"WebgalUI", serif'); | ||
| } | ||
| export function getCurrentFontFamily(): string { | ||
| return selectFontFamily(webgalStore.getState()); | ||
| } | ||
|
|
||
| return getFont(); | ||
| export function selectFontFamily(state: RootState): string { | ||
| const index = state.userData.optionData.textboxFont ?? 0; | ||
| const fonts = state.GUI.fontOptions; | ||
| if (fonts[index]) { | ||
| return fonts[index].family; | ||
| } | ||
| if (fonts.length > 0) { | ||
| return fonts[0].family; | ||
| } | ||
| return FALLBACK_FONT_FAMILY; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
loadTemplate是一个异步函数,但在这里以“即发即忘”的方式调用,没有等待其完成。这会导致initializeScript中的后续代码在模板和字体加载完成前就执行,可能引发竞态条件(race condition)。例如,UI 可能在自定义字体注入前就渲染,导致字体闪烁或初期显示不正确。为了确保初始化按预期顺序完成,initializeScript函数应该被声明为async,并且在这里await loadTemplate()。