Font related improvements
This commit is contained in:
@@ -56,6 +56,13 @@ build 把同一个 build ID 注入浏览器 bundle 与 worker,发现更新后
|
|||||||
metadata 的唯一文案来源。Vite 在构建时解析并注入它,Rust 则嵌入同一文件来验证允许保存的 locale。增加文案键时必须为每个 locale 提供值;增加语言时同时增加完整
|
metadata 的唯一文案来源。Vite 在构建时解析并注入它,Rust 则嵌入同一文件来验证允许保存的 locale。增加文案键时必须为每个 locale 提供值;增加语言时同时增加完整
|
||||||
`[locales."<code>".messages]` 段。业务命令如“点歌”是 Bilibili 输入协议,不属于 UI 翻译。
|
`[locales."<code>".messages]` 段。业务命令如“点歌”是 Bilibili 输入协议,不属于 UI 翻译。
|
||||||
|
|
||||||
|
## 字体资源
|
||||||
|
|
||||||
|
Noto Serif SC、ZCOOL XiaoWei 与 LXGW
|
||||||
|
WenKai 由锁定的 Fontsource 依赖提供;漓雨手书则以固定版本的 WOFF2 文件保存在
|
||||||
|
`public/fonts/`。浏览器从 Rust 静态服务同域加载这些字体,不依赖 OBS 设备的系统字体。每套字体的 OFL-1.1 许可证均输出到
|
||||||
|
`/fonts/licenses/`。
|
||||||
|
|
||||||
## 格式化与构建
|
## 格式化与构建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
Generated
+30
@@ -8,6 +8,9 @@
|
|||||||
"name": "lxc-stream-overlay",
|
"name": "lxc-stream-overlay",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/lxgw-wenkai": "5.3.0",
|
||||||
|
"@fontsource/noto-serif-sc": "5.3.0",
|
||||||
|
"@fontsource/zcool-xiaowei": "5.3.0",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1"
|
"react-dom": "19.1.1"
|
||||||
},
|
},
|
||||||
@@ -745,6 +748,33 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource/lxgw-wenkai": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/lxgw-wenkai/-/lxgw-wenkai-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-+U5AAOXeaB7o+3G0Qm7I7Djz2Jd1hqNY+kClOZH8bTunyyGa2WxY12LZftt2AqUsxlfxwdQpd75Js1HQIt4H4g==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/noto-serif-sc": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/noto-serif-sc/-/noto-serif-sc-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-0/zaEFkidiWldE62rTeD74x8ygUsQvejiSNtO0LQxQk3qpaHnlMZ3w4C7yH80B4KTIg8VKeFP4oSgwWMchY9+g==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/zcool-xiaowei": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/zcool-xiaowei/-/zcool-xiaowei-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-iRuUPl9ONFbYzi6JcEshpE4VedINyzQ4hv0Z5XEt0cB0pMPiQeqc2Qvv1NwJCNzkPwG02VCdcO+cF2K8ylYn2A==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
|
|||||||
@@ -3,13 +3,16 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build && node scripts/check-font-assets.mjs",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"docs:check": "node scripts/check-doc-links.mjs ../..",
|
"docs:check": "node scripts/check-doc-links.mjs ../..",
|
||||||
"format": "prettier --ignore-path ../../.prettierignore --write . ../server-rust/README.md ../../README.md ../../docs ../../compose.yaml",
|
"format": "prettier --ignore-path ../../.prettierignore --write . ../server-rust/README.md ../../README.md ../../docs ../../compose.yaml",
|
||||||
"format:check": "prettier --ignore-path ../../.prettierignore --check . ../server-rust/README.md ../../README.md ../../docs ../../compose.yaml"
|
"format:check": "prettier --ignore-path ../../.prettierignore --check . ../server-rust/README.md ../../README.md ../../docs ../../compose.yaml"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/lxgw-wenkai": "5.3.0",
|
||||||
|
"@fontsource/noto-serif-sc": "5.3.0",
|
||||||
|
"@fontsource/zcool-xiaowei": "5.3.0",
|
||||||
"react": "19.1.1",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.1"
|
"react-dom": "19.1.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Bundled font notice
|
||||||
|
|
||||||
|
## Liyu Shoushu
|
||||||
|
|
||||||
|
`liyu-shoushu-v0.107.woff2` is a WOFF2 conversion of `LiyuShoushu.ttf` from the official
|
||||||
|
[Liyu Shoushu v0.107 release](https://github.com/InkPantherType/LiyuShoushu/releases/tag/v0.107).
|
||||||
|
|
||||||
|
- Release tag commit: `219a0f42cb7b3f0d00be6e5bf9f8b5298a1f3060`
|
||||||
|
- Original TTF SHA-256: `16554439b7e95da1fc354ed7b2c1551e83e942c3c5afb2a2e883761c6037dc22`
|
||||||
|
- Bundled WOFF2 SHA-256: `b17f8c4fc6612a539d7a46e0eb5f57fdc732cff1d17804563b00a3f0a7eb9ab3`
|
||||||
|
- Conversion: FontTools 4.63.0 WOFF2 compression with Brotli 1.2.0; no glyph subsetting
|
||||||
|
- Coverage retained: 18,828 glyphs and 18,221 Unicode mappings
|
||||||
|
- License: SIL Open Font License 1.1; see
|
||||||
|
[`licenses/liyu-shoushu-OFL.txt`](licenses/liyu-shoushu-OFL.txt)
|
||||||
|
|
||||||
|
The original font credits Yuji Kataoka and the Yuji Project Authors for the base brush glyphs, LXGW
|
||||||
|
and the LXGW ZhenKai Project Authors for punctuation and symbols, and Yuchen Tian's
|
||||||
|
[zi2zi-JiT](https://github.com/kaonashi-tyc/zi2zi-JiT) for generated Chinese glyphs. This notice
|
||||||
|
preserves the zi2zi-JiT attribution required by the upstream font documentation.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
Copyright 2026 Alexander Tseng.
|
||||||
|
Portions Copyright 2021 The Yuji Project Authors (https://github.com/Kinutafontfactory/Yuji).
|
||||||
|
Portions Copyright LXGW and LXGWZhenKai Project Authors (https://github.com/lxgw/LxgwZhenKai).
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
@@ -66,6 +66,7 @@ function isControlShell(pathname) {
|
|||||||
function isStaticAsset(pathname) {
|
function isStaticAsset(pathname) {
|
||||||
return (
|
return (
|
||||||
pathname.startsWith('/assets/') ||
|
pathname.startsWith('/assets/') ||
|
||||||
|
pathname.startsWith('/fonts/') ||
|
||||||
pathname.startsWith('/pwa/') ||
|
pathname.startsWith('/pwa/') ||
|
||||||
pathname === '/control/manifest.webmanifest'
|
pathname === '/control/manifest.webmanifest'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import { readdir, readFile } from 'node:fs/promises'
|
||||||
|
|
||||||
|
const dist = new URL('../dist/', import.meta.url)
|
||||||
|
|
||||||
|
async function filesBelow(directory, prefix = '') {
|
||||||
|
const entries = await readdir(directory, { withFileTypes: true })
|
||||||
|
const files = []
|
||||||
|
for (const entry of entries) {
|
||||||
|
const relative = `${prefix}${entry.name}`
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
files.push(...(await filesBelow(new URL(`${entry.name}/`, directory), `${relative}/`)))
|
||||||
|
} else {
|
||||||
|
files.push(relative)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await filesBelow(dist)
|
||||||
|
const expectedFonts = ['noto-serif-sc', 'zcool-xiaowei', 'lxgw-wenkai']
|
||||||
|
const liyuFont = 'fonts/liyu-shoushu-v0.107.woff2'
|
||||||
|
|
||||||
|
for (const font of expectedFonts) {
|
||||||
|
if (!files.some(file => file.startsWith(`assets/${font}-`) && file.endsWith('.woff2'))) {
|
||||||
|
throw new Error(`Missing bundled WOFF2 assets for ${font}`)
|
||||||
|
}
|
||||||
|
if (!files.includes(`fonts/licenses/${font}-OFL.txt`)) {
|
||||||
|
throw new Error(`Missing bundled license for ${font}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!files.includes(liyuFont)) throw new Error('Missing bundled WOFF2 asset for liyu-shoushu')
|
||||||
|
if (!files.includes('fonts/licenses/liyu-shoushu-OFL.txt')) {
|
||||||
|
throw new Error('Missing bundled license for liyu-shoushu')
|
||||||
|
}
|
||||||
|
const liyuDigest = createHash('sha256')
|
||||||
|
.update(await readFile(new URL(liyuFont, dist)))
|
||||||
|
.digest('hex')
|
||||||
|
if (liyuDigest !== 'b17f8c4fc6612a539d7a46e0eb5f57fdc732cff1d17804563b00a3f0a7eb9ab3') {
|
||||||
|
throw new Error(`Unexpected liyu-shoushu WOFF2 digest: ${liyuDigest}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyWoff = files.find(file => file.endsWith('.woff'))
|
||||||
|
if (legacyWoff) throw new Error(`Unexpected legacy WOFF asset: ${legacyWoff}`)
|
||||||
|
|
||||||
|
const css = (
|
||||||
|
await Promise.all(
|
||||||
|
files.filter(file => file.endsWith('.css')).map(file => readFile(new URL(file, dist), 'utf8')),
|
||||||
|
)
|
||||||
|
).join('\n')
|
||||||
|
|
||||||
|
for (const family of ['Noto Serif SC', 'ZCOOL XiaoWei', 'LXGW WenKai', 'Liyu Shoushu']) {
|
||||||
|
const declarations = [
|
||||||
|
`font-family:${family}`,
|
||||||
|
`font-family:${JSON.stringify(family)}`,
|
||||||
|
`font-family:'${family}'`,
|
||||||
|
]
|
||||||
|
if (!declarations.some(declaration => css.includes(declaration))) {
|
||||||
|
throw new Error(`Missing @font-face declaration for ${family}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!css.includes('font-display:block') || css.includes('font-display:swap')) {
|
||||||
|
throw new Error('Bundled fonts must block local fallback while loading')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Verified same-origin WOFF2 component fonts and licenses.')
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Liyu Shoushu';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: block;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url('/fonts/liyu-shoushu-v0.107.woff2') format('woff2');
|
||||||
|
}
|
||||||
@@ -25,6 +25,10 @@ import { SongRequestOverlay } from './songOverlay'
|
|||||||
import { useComponentStream } from './stream'
|
import { useComponentStream } from './stream'
|
||||||
import { I18nProvider, translate, useI18n } from './i18n'
|
import { I18nProvider, translate, useI18n } from './i18n'
|
||||||
import type { Session } from './types'
|
import type { Session } from './types'
|
||||||
|
import '@fontsource/noto-serif-sc/400.css'
|
||||||
|
import '@fontsource/zcool-xiaowei/400.css'
|
||||||
|
import '@fontsource/lxgw-wenkai/500.css'
|
||||||
|
import './fonts.css'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
import './control.css'
|
import './control.css'
|
||||||
import './giftEffect.css'
|
import './giftEffect.css'
|
||||||
|
|||||||
@@ -105,6 +105,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gift-menu-overlay.gift-menu-theme-moonlit-water {
|
.gift-menu-overlay.gift-menu-theme-moonlit-water {
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
padding-block: calc(clamp(8px, 1.3vmin, 16px) + var(--moonlit-edge-space));
|
padding-block: calc(clamp(8px, 1.3vmin, 16px) + var(--moonlit-edge-space));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { CSSProperties } from 'react'
|
import type { CSSProperties } from 'react'
|
||||||
|
|
||||||
/** Stable IDs persisted by the backend; each maps to a fixed, injection-safe stack. */
|
/** Stable IDs persisted by the backend; each maps to a bundled, same-origin webfont. */
|
||||||
export const fontFamilyIds = ['song', 'fang-song', 'kai'] as const
|
export const fontFamilyIds = ['song', 'fang-song', 'kai', 'liyu-shoushu'] as const
|
||||||
|
|
||||||
export type FontFamilyId = (typeof fontFamilyIds)[number]
|
export type FontFamilyId = (typeof fontFamilyIds)[number]
|
||||||
|
|
||||||
@@ -9,12 +9,14 @@ export const fontFamilies: ReadonlyArray<{ id: FontFamilyId; nameKey: string }>
|
|||||||
{ id: 'song', nameKey: 'settings.font_family.song' },
|
{ id: 'song', nameKey: 'settings.font_family.song' },
|
||||||
{ id: 'fang-song', nameKey: 'settings.font_family.fang_song' },
|
{ id: 'fang-song', nameKey: 'settings.font_family.fang_song' },
|
||||||
{ id: 'kai', nameKey: 'settings.font_family.kai' },
|
{ id: 'kai', nameKey: 'settings.font_family.kai' },
|
||||||
|
{ id: 'liyu-shoushu', nameKey: 'settings.font_family.liyu_shoushu' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const fontStacks: Record<FontFamilyId, string> = {
|
const fontStacks: Record<FontFamilyId, string> = {
|
||||||
song: '"Source Han Serif SC", "Noto Serif CJK SC", "Noto Serif SC", "Songti SC", STSong, SimSun, serif',
|
song: '"Noto Serif SC", serif',
|
||||||
'fang-song': 'FangSong, STFangsong, "FZShuSong-Z01", "Noto Serif SC", "Songti SC", serif',
|
'fang-song': '"ZCOOL XiaoWei", serif',
|
||||||
kai: '"Kaiti SC", STKaiti, KaiTi, "LXGW WenKai", "Noto Serif SC", serif',
|
kai: '"LXGW WenKai", serif',
|
||||||
|
'liyu-shoushu': '"Liyu Shoushu", serif',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeFontFamilyId(value: unknown): FontFamilyId {
|
export function normalizeFontFamilyId(value: unknown): FontFamilyId {
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ import { parse } from 'smol-toml'
|
|||||||
// The waiting worker is still activated explicitly by the user in the console.
|
// The waiting worker is still activated explicitly by the user in the console.
|
||||||
const pwaBuildId = Date.now().toString(36)
|
const pwaBuildId = Date.now().toString(36)
|
||||||
const serviceWorkerSource = readFileSync(new URL('./pwa/control-sw.js', import.meta.url), 'utf8')
|
const serviceWorkerSource = readFileSync(new URL('./pwa/control-sw.js', import.meta.url), 'utf8')
|
||||||
|
const bundledFontLicenses = ['noto-serif-sc', 'zcool-xiaowei', 'lxgw-wenkai'].map(font => ({
|
||||||
|
fileName: `fonts/licenses/${font}-OFL.txt`,
|
||||||
|
source: readFileSync(
|
||||||
|
new URL(`./node_modules/@fontsource/${font}/LICENSE`, import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
),
|
||||||
|
}))
|
||||||
const i18nResource = parse(
|
const i18nResource = parse(
|
||||||
readFileSync(new URL('../../resources/i18n.toml', import.meta.url), 'utf8'),
|
readFileSync(new URL('../../resources/i18n.toml', import.meta.url), 'utf8'),
|
||||||
) as unknown as {
|
) as unknown as {
|
||||||
@@ -51,6 +58,16 @@ const manifest = {
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
|
{
|
||||||
|
name: 'same-origin-woff2-fonts',
|
||||||
|
enforce: 'pre',
|
||||||
|
transform(source, id) {
|
||||||
|
if (!id.includes('/node_modules/@fontsource/') || !id.endsWith('.css')) return
|
||||||
|
return source
|
||||||
|
.replaceAll('font-display: swap', 'font-display: block')
|
||||||
|
.replace(/,\s*url\([^)]*\.woff\) format\(['"]woff['"]\)/g, '')
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'control-pwa-assets',
|
name: 'control-pwa-assets',
|
||||||
transformIndexHtml(html) {
|
transformIndexHtml(html) {
|
||||||
@@ -61,6 +78,9 @@ export default defineConfig({
|
|||||||
.replaceAll('__APP_DESCRIPTION__', defaultMessages['pwa.manifest.description'])
|
.replaceAll('__APP_DESCRIPTION__', defaultMessages['pwa.manifest.description'])
|
||||||
},
|
},
|
||||||
generateBundle() {
|
generateBundle() {
|
||||||
|
for (const license of bundledFontLicenses) {
|
||||||
|
this.emitFile({ type: 'asset', ...license })
|
||||||
|
}
|
||||||
this.emitFile({
|
this.emitFile({
|
||||||
type: 'asset',
|
type: 'asset',
|
||||||
fileName: 'control/sw.js',
|
fileName: 'control/sw.js',
|
||||||
|
|||||||
Generated
+1
@@ -956,6 +956,7 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "libilibili"
|
name = "libilibili"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
source = "git+http://gitea.home.arpa/felis/libilibili.git#3cb2a902d4d9387c5581a1827cc19248b6000ae2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"brotli",
|
"brotli",
|
||||||
|
|||||||
@@ -7,20 +7,30 @@ edition = "2024"
|
|||||||
axum = { version = "0.8", features = ["ws", "json"] }
|
axum = { version = "0.8", features = ["ws", "json"] }
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock", "serde"] }
|
chrono = { version = "0.4", default-features = false, features = [
|
||||||
|
"clock",
|
||||||
|
"serde",
|
||||||
|
] }
|
||||||
chacha20poly1305 = "0.10"
|
chacha20poly1305 = "0.10"
|
||||||
deadpool-postgres = "0.14"
|
deadpool-postgres = "0.14"
|
||||||
# Kept as a sibling checkout during development. Docker Compose supplies the
|
# Kept as a sibling checkout during development. Docker Compose supplies the
|
||||||
# same directory as a named BuildKit context at `/libilibili`.
|
# same directory as a named BuildKit context at `/libilibili`.
|
||||||
libilibili = { path = "../../../libilibili" }
|
libilibili = { git = "http://gitea.home.arpa/felis/libilibili.git" }
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = [
|
||||||
|
"json",
|
||||||
|
"rustls-tls",
|
||||||
|
] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
subtle = "2"
|
subtle = "2"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tokio-postgres = { version = "0.7", features = ["with-serde_json-1", "with-uuid-1", "with-chrono-0_4"] }
|
tokio-postgres = { version = "0.7", features = [
|
||||||
|
"with-serde_json-1",
|
||||||
|
"with-uuid-1",
|
||||||
|
"with-chrono-0_4",
|
||||||
|
] }
|
||||||
tokio-util = "0.7"
|
tokio-util = "0.7"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
totp-rs = { version = "5.7", features = ["gen_secret", "qr", "zeroize"] }
|
totp-rs = { version = "5.7", features = ["gen_secret", "qr", "zeroize"] }
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//! Shared, validated typography settings for all built-in OBS components.
|
//! Shared, validated typography settings for all built-in OBS components.
|
||||||
//!
|
//!
|
||||||
//! Font IDs map to fixed frontend font stacks rather than accepting arbitrary
|
//! Font IDs map to fixed, same-origin WOFF2 assets in the frontend rather than
|
||||||
//! CSS. This keeps settings portable between OBS hosts and prevents untrusted
|
//! accepting arbitrary CSS. This keeps settings portable between OBS hosts and
|
||||||
//! component settings from becoming a CSS injection boundary.
|
//! prevents untrusted component settings from becoming a CSS injection boundary.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ pub enum FontFamilyId {
|
|||||||
#[default]
|
#[default]
|
||||||
FangSong,
|
FangSong,
|
||||||
Kai,
|
Kai,
|
||||||
|
LiyuShoushu,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn default_font_brightness() -> u16 {
|
pub const fn default_font_brightness() -> u16 {
|
||||||
@@ -35,5 +36,9 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(sanitize_font_brightness(1), 70);
|
assert_eq!(sanitize_font_brightness(1), 70);
|
||||||
assert_eq!(sanitize_font_brightness(u16::MAX), 180);
|
assert_eq!(sanitize_font_brightness(u16::MAX), 180);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::to_string(&FontFamilyId::LiyuShoushu).unwrap(),
|
||||||
|
"\"liyu-shoushu\""
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,28 +12,30 @@
|
|||||||
|
|
||||||
## 设置
|
## 设置
|
||||||
|
|
||||||
| 字段 | 范围/单位 | 行为 |
|
| 字段 | 范围/单位 | 行为 |
|
||||||
| ------------------------ | ----------- | -------------------------- |
|
| ------------------------ | ----------- | ------------------------------------------ |
|
||||||
| `themeId` | 主题 ID | 选择已安装的完整视觉主题 |
|
| `themeId` | 主题 ID | 选择已安装的完整视觉主题 |
|
||||||
| `fontFamily` | 字体 ID | `song`、`fang-song`、`kai` |
|
| `fontFamily` | 字体 ID | `song`、`fang-song`、`kai`、`liyu-shoushu` |
|
||||||
| `fontBrightness` | 70–180% | 只调整文字亮度 |
|
| `fontBrightness` | 70–180% | 只调整文字亮度 |
|
||||||
| `viewerColor` | `#RRGGBB` | 可选的用户昵称颜色覆盖 |
|
| `viewerColor` | `#RRGGBB` | 可选的用户昵称颜色覆盖 |
|
||||||
| `danmakuColor` | `#RRGGBB` | 可选的弹幕正文颜色覆盖 |
|
| `danmakuColor` | `#RRGGBB` | 可选的弹幕正文颜色覆盖 |
|
||||||
| `fontScale` | 50–300% | 展开与收缩字号的统一比例 |
|
| `fontScale` | 50–300% | 展开与收缩字号的统一比例 |
|
||||||
| `decorationLineWeight` | 50–300% | 边框、分隔线与古风边缘粗细 |
|
| `decorationLineWeight` | 50–300% | 边框、分隔线与古风边缘粗细 |
|
||||||
| `maxVisible` | 1–12 | 同时保留的消息卡数量 |
|
| `maxVisible` | 1–12 | 同时保留的消息卡数量 |
|
||||||
| `collapseAfterSeconds` | 2–120 秒 | 最新卡从展开态切换到紧凑态 |
|
| `collapseAfterSeconds` | 2–120 秒 | 最新卡从展开态切换到紧凑态 |
|
||||||
| `unfoldDurationMs` | 200–5000 ms | 横向卷轴展开动画时间 |
|
| `unfoldDurationMs` | 200–5000 ms | 横向卷轴展开动画时间 |
|
||||||
| `motionIntensity` | 0–100% | 卡片、流光与焦点动画强度 |
|
| `motionIntensity` | 0–100% | 卡片、流光与焦点动画强度 |
|
||||||
| `particleCount` | 0–12 | 每卡星花粒子数量 |
|
| `particleCount` | 0–12 | 每卡星花粒子数量 |
|
||||||
| `particleSpeed` | 25–300% | 粒子动画速度 |
|
| `particleSpeed` | 25–300% | 粒子动画速度 |
|
||||||
| `lowPerformanceMode` | boolean | 关闭高成本动态效果 |
|
| `lowPerformanceMode` | boolean | 关闭高成本动态效果 |
|
||||||
| `highValueThreshold` | 千分之一元 | 高价值礼物起点 |
|
| `highValueThreshold` | 千分之一元 | 高价值礼物起点 |
|
||||||
| `featuredValueThreshold` | 千分之一元 | 焦点礼物起点 |
|
| `featuredValueThreshold` | 千分之一元 | 焦点礼物起点 |
|
||||||
| `show*` | boolean | 控制各事件类别订阅 |
|
| `show*` | boolean | 控制各事件类别订阅 |
|
||||||
|
|
||||||
后端的 `OverlaySettings::sanitize` 是最终边界。控制台 range
|
后端的 `OverlaySettings::sanitize` 是最终边界。控制台 range
|
||||||
input 只改善交互,不能取代服务端校验。字体 ID 只映射到前端预先注册的安全字体栈,不接受任意 CSS;OBS 设备缺少首选字体时会依次回退到可用衬线字体。颜色覆盖同样只接受六位十六进制颜色;留空时使用主题色,切换主题后会自动采用新主题的配色。
|
input 只改善交互,不能取代服务端校验。字体 ID 只映射到前端预先注册并由服务同域提供的 WOFF2 字体,不接受任意 CSS,也不依赖 OBS 设备安装的字体;`song`、`fang-song`、`kai`
|
||||||
|
与 `liyu-shoushu` 分别使用 Noto Serif SC、ZCOOL XiaoWei、LXGW
|
||||||
|
WenKai 与漓雨手书。颜色覆盖同样只接受六位十六进制颜色;留空时使用主题色,切换主题后会自动采用新主题的配色。
|
||||||
|
|
||||||
## 主题
|
## 主题
|
||||||
|
|
||||||
|
|||||||
+10
-8
@@ -142,10 +142,11 @@ name = "简体中文"
|
|||||||
"nav.logout" = "退出"
|
"nav.logout" = "退出"
|
||||||
"settings.theme" = "主题"
|
"settings.theme" = "主题"
|
||||||
"settings.font_family" = "中文字体"
|
"settings.font_family" = "中文字体"
|
||||||
"settings.font_family_description" = "按 OBS 所在设备已安装字体依次匹配;缺失时自动回退到衬线字体。"
|
"settings.font_family_description" = "字体随服务提供并由 OBS 同域加载,不依赖设备已安装字体。"
|
||||||
"settings.font_family.song" = "宋体 · 端正"
|
"settings.font_family.song" = "思源宋体 · 端正"
|
||||||
"settings.font_family.fang_song" = "仿宋 · 清雅"
|
"settings.font_family.fang_song" = "小薇体 · 清雅"
|
||||||
"settings.font_family.kai" = "楷体 · 书卷"
|
"settings.font_family.kai" = "霞鹜文楷 · 书卷"
|
||||||
|
"settings.font_family.liyu_shoushu" = "漓雨手书 · 毛笔手写"
|
||||||
"settings.font_brightness" = "文字亮度"
|
"settings.font_brightness" = "文字亮度"
|
||||||
"settings.font_brightness_description" = "只提亮文字,不改变透明背景、礼物图片或直播画面。"
|
"settings.font_brightness_description" = "只提亮文字,不改变透明背景、礼物图片或直播画面。"
|
||||||
"settings.viewer_color" = "用户昵称颜色"
|
"settings.viewer_color" = "用户昵称颜色"
|
||||||
@@ -642,10 +643,11 @@ name = "English"
|
|||||||
"nav.logout" = "Log out"
|
"nav.logout" = "Log out"
|
||||||
"settings.theme" = "Theme"
|
"settings.theme" = "Theme"
|
||||||
"settings.font_family" = "Chinese typeface"
|
"settings.font_family" = "Chinese typeface"
|
||||||
"settings.font_family_description" = "Uses the first installed font on the OBS device and falls back safely to a serif face."
|
"settings.font_family_description" = "Fonts are served by this service and loaded from the same origin instead of relying on fonts installed on the OBS device."
|
||||||
"settings.font_family.song" = "Song · Formal"
|
"settings.font_family.song" = "Noto Serif SC · Formal"
|
||||||
"settings.font_family.fang_song" = "FangSong · Refined"
|
"settings.font_family.fang_song" = "ZCOOL XiaoWei · Refined"
|
||||||
"settings.font_family.kai" = "Kai · Calligraphic"
|
"settings.font_family.kai" = "LXGW WenKai · Calligraphic"
|
||||||
|
"settings.font_family.liyu_shoushu" = "Liyu Shoushu · Brush handwriting"
|
||||||
"settings.font_brightness" = "Text brightness"
|
"settings.font_brightness" = "Text brightness"
|
||||||
"settings.font_brightness_description" = "Brightens text only; transparency, gift artwork, and the underlying scene are unchanged."
|
"settings.font_brightness_description" = "Brightens text only; transparency, gift artwork, and the underlying scene are unchanged."
|
||||||
"settings.viewer_color" = "Viewer name color"
|
"settings.viewer_color" = "Viewer name color"
|
||||||
|
|||||||
Reference in New Issue
Block a user