|
| 1 | +import axios from 'axios'; |
| 2 | +import { logger } from '@/Core/util/logger'; |
| 3 | +import { WebGAL } from '@/Core/WebGAL'; |
| 4 | +import { TemplateFontDescriptor, WebgalTemplate } from '@/types/template'; |
| 5 | +import { buildFontOptionsFromTemplate } from '@/Core/util/fonts/fontOptions'; |
| 6 | +import { webgalStore } from '@/store/store'; |
| 7 | +import { setFontOptions } from '@/store/GUIReducer'; |
| 8 | +import { setOptionData } from '@/store/userDataReducer'; |
| 9 | + |
| 10 | +const TEMPLATE_PATH = './game/template/template.json'; |
| 11 | +const TEMPLATE_FONT_STYLE_SELECTOR = 'style[data-webgal-template-fonts]'; |
| 12 | + |
| 13 | +export async function loadTemplate(): Promise<WebgalTemplate | null> { |
| 14 | + try { |
| 15 | + const { data } = await axios.get<WebgalTemplate>(TEMPLATE_PATH); |
| 16 | + WebGAL.template = data; |
| 17 | + const fonts = data.fonts ?? []; |
| 18 | + injectTemplateFonts(fonts); |
| 19 | + updateFontOptions(fonts); |
| 20 | + return data; |
| 21 | + } catch (error) { |
| 22 | + logger.warn('加载模板文件失败', error); |
| 23 | + updateFontOptions([]); |
| 24 | + return null; |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +function updateFontOptions(fonts: TemplateFontDescriptor[]): void { |
| 29 | + const options = buildFontOptionsFromTemplate(fonts); |
| 30 | + webgalStore.dispatch(setFontOptions(options)); |
| 31 | + const currentIndex = webgalStore.getState().userData.optionData.textboxFont ?? 0; |
| 32 | + if (options.length === 0) return; |
| 33 | + if (currentIndex >= options.length) { |
| 34 | + webgalStore.dispatch(setOptionData({ key: 'textboxFont', value: 0 })); |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +function injectTemplateFonts(fonts: TemplateFontDescriptor[]): void { |
| 39 | + if (!fonts.length) return; |
| 40 | + const rules = fonts.map((font) => generateFontFaceRule(font)).filter((rule): rule is string => Boolean(rule)); |
| 41 | + |
| 42 | + if (!rules.length) return; |
| 43 | + |
| 44 | + const styleElement = document.createElement('style'); |
| 45 | + styleElement.setAttribute('data-webgal-template-fonts', 'true'); |
| 46 | + styleElement.appendChild(document.createTextNode(rules.join('\n'))); |
| 47 | + |
| 48 | + const head = document.head; |
| 49 | + if (!head) return; |
| 50 | + |
| 51 | + const existing = head.querySelector<HTMLStyleElement>(TEMPLATE_FONT_STYLE_SELECTOR); |
| 52 | + existing?.remove(); |
| 53 | + |
| 54 | + head.appendChild(styleElement); |
| 55 | +} |
| 56 | + |
| 57 | +function generateFontFaceRule(font: TemplateFontDescriptor): string | null { |
| 58 | + const fontFamily = font['font-family']; |
| 59 | + if (!fontFamily || !font.url || !font.type) { |
| 60 | + logger.warn('忽略无效的模板字体配置', font); |
| 61 | + return null; |
| 62 | + } |
| 63 | + |
| 64 | + const src = resolveTemplateAssetPath(font.url); |
| 65 | + const weight = font.weight !== undefined ? `font-weight: ${font.weight};` : ''; |
| 66 | + const style = font.style ? `font-style: ${font.style};` : ''; |
| 67 | + const display = `font-display: ${font.display ?? 'swap'};`; |
| 68 | + |
| 69 | + return `@font-face { font-family: '${fontFamily}'; src: url('${src}') format('${font.type}'); ${weight} ${style} ${display} }`; |
| 70 | +} |
| 71 | + |
| 72 | +function resolveTemplateAssetPath(path: string): string { |
| 73 | + if (/^(https?:)?\/\//i.test(path) || path.startsWith('data:')) { |
| 74 | + return path; |
| 75 | + } |
| 76 | + const normalized = path.replace(/^[./]+/, ''); |
| 77 | + return `./game/template/${normalized}`; |
| 78 | +} |
0 commit comments