refactor: custom fonts

This commit is contained in:
pa
2026-03-13 22:24:41 +09:00
parent 9ac18ac79e
commit 82122a4fab
12 changed files with 389 additions and 110 deletions
+7 -1
View File
@@ -703,7 +703,7 @@
"bio_language": "Target Language", "bio_language": "Target Language",
"theme_mode": "Theme", "theme_mode": "Theme",
"font_family": "Font", "font_family": "Font",
"font_family_tooltip": "Only the system font affects CJK characters", "cjk_font_pack": "CJK Font",
"font_family_inter": "Inter", "font_family_inter": "Inter",
"font_family_noto_sans": "Noto Sans", "font_family_noto_sans": "Noto Sans",
"font_family_source_sans_3": "Source Sans 3", "font_family_source_sans_3": "Source Sans 3",
@@ -713,6 +713,12 @@
"font_family_roboto": "Roboto", "font_family_roboto": "Roboto",
"font_family_fantasque_sans_mono": "Fantasque Sans Mono", "font_family_fantasque_sans_mono": "Fantasque Sans Mono",
"font_family_system_ui": "System Font", "font_family_system_ui": "System Font",
"font_family_custom": "Custom",
"font_family_custom_dialog_title": "Custom Font Family",
"font_family_custom_dialog_description": "Enter a valid CSS font-family value",
"font_family_custom_invalid": "Invalid font-family. Use comma-separated font names, e.g. 'My Font', Arial, sans-serif",
"cjk_font_pack_noto": "Noto Sans CJK",
"cjk_font_pack_pht": "PuHuiTi CJK",
"theme_mode_system": "System", "theme_mode_system": "System",
"theme_mode_light": "Light", "theme_mode_light": "Light",
"theme_mode_dark": "Dark", "theme_mode_dark": "Dark",
-1
View File
@@ -564,7 +564,6 @@
"bio_language": "翻訳先の言語", "bio_language": "翻訳先の言語",
"theme_mode": "テーマ", "theme_mode": "テーマ",
"font_family": "フォント", "font_family": "フォント",
"font_family_tooltip": "システムフォントのみがCJK文字に影響します",
"font_family_inter": "Inter", "font_family_inter": "Inter",
"font_family_noto_sans": "Noto Sans", "font_family_noto_sans": "Noto Sans",
"font_family_source_sans_3": "Source Sans 3", "font_family_source_sans_3": "Source Sans 3",
-1
View File
@@ -563,7 +563,6 @@
"bio_language": "目标语言", "bio_language": "目标语言",
"theme_mode": "主题", "theme_mode": "主题",
"font_family": "字体", "font_family": "字体",
"font_family_tooltip": "只有系统字体会影响 CJK (也就是中/日/韩文)字符。此外,由于部分字体使用了 fonts.gstatic.com 源,所以在网络受限地区可能需要魔法",
"font_family_inter": "Inter", "font_family_inter": "Inter",
"font_family_noto_sans": "Noto Sans", "font_family_noto_sans": "Noto Sans",
"font_family_source_sans_3": "Source Sans 3", "font_family_source_sans_3": "Source Sans 3",
-1
View File
@@ -559,7 +559,6 @@
"bio_language": "目標語言", "bio_language": "目標語言",
"theme_mode": "主題", "theme_mode": "主題",
"font_family": "字型", "font_family": "字型",
"font_family_tooltip": "只有系統字型會影響 CJK(中日韓)字元顯示",
"font_family_inter": "Inter", "font_family_inter": "Inter",
"font_family_noto_sans": "Noto Sans", "font_family_noto_sans": "Noto Sans",
"font_family_source_sans_3": "Source Sans 3", "font_family_source_sans_3": "Source Sans 3",
+3 -1
View File
@@ -1,7 +1,7 @@
import { initDayjs } from './dayjs'; import { initDayjs } from './dayjs';
import { initInteropApi } from './interopApi'; import { initInteropApi } from './interopApi';
import { initNoty } from './noty'; import { initNoty } from './noty';
import { initUi } from './ui'; import { initUi, initUiForVrOverlay } from './ui';
/** /**
* @param {boolean} isVrOverlay * @param {boolean} isVrOverlay
@@ -11,6 +11,8 @@ export async function initPlugins(isVrOverlay = false) {
await initInteropApi(isVrOverlay); await initInteropApi(isVrOverlay);
if (!isVrOverlay) { if (!isVrOverlay) {
await initUi(); await initUi();
} else {
await initUiForVrOverlay();
} }
initDayjs(); initDayjs();
if (isVrOverlay) { if (isVrOverlay) {
+34 -5
View File
@@ -1,12 +1,16 @@
import { import {
// changeAppDarkStyle, applyAppCjkFontPack,
applyAppFontFamily,
changeAppThemeStyle, changeAppThemeStyle,
changeHtmlLangAttribute, changeHtmlLangAttribute,
getThemeMode, getThemeMode,
initThemeColor, initThemeColor,
refreshCustomCss refreshCustomCss
// setLoginContainerStyle
} from '../shared/utils/base/ui'; } from '../shared/utils/base/ui';
import {
APP_CJK_FONT_PACK_DEFAULT_KEY,
APP_FONT_DEFAULT_KEY
} from '../shared/constants';
import { i18n, loadLocalizedStrings } from './i18n'; import { i18n, loadLocalizedStrings } from './i18n';
import configRepository from '../services/config'; import configRepository from '../services/config';
@@ -22,9 +26,7 @@ export async function initUi() {
await loadLocalizedStrings(language); await loadLocalizedStrings(language);
changeHtmlLangAttribute(language); changeHtmlLangAttribute(language);
const { initThemeMode, isDarkMode } = const { initThemeMode } = await getThemeMode(configRepository);
await getThemeMode(configRepository);
// setLoginContainerStyle(isDarkMode);
changeAppThemeStyle(initThemeMode); changeAppThemeStyle(initThemeMode);
await initThemeColor(); await initThemeColor();
} catch (error) { } catch (error) {
@@ -33,3 +35,30 @@ export async function initUi() {
refreshCustomCss(); refreshCustomCss();
} }
export async function initUiForVrOverlay() {
try {
const [language, fontFamily, customFontFamily, cjkFontPack] =
await Promise.all([
configRepository.getString('VRCX_appLanguage', 'en'),
configRepository.getString(
'VRCX_fontFamily',
APP_FONT_DEFAULT_KEY
),
configRepository.getString('VRCX_customFontFamily', ''),
configRepository.getString(
'VRCX_cjkFontPack',
APP_CJK_FONT_PACK_DEFAULT_KEY
)
]);
// @ts-ignore
i18n.locale = language;
await loadLocalizedStrings(language);
changeHtmlLangAttribute(language);
applyAppFontFamily(fontFamily, customFontFamily);
applyAppCjkFontPack(cjkFontPack);
} catch (error) {
console.error('Error initializing VR locale and fonts:', error);
}
}
+65 -4
View File
@@ -1,4 +1,5 @@
const APP_FONT_DEFAULT_KEY = 'inter'; const APP_FONT_DEFAULT_KEY = 'inter';
const APP_CJK_FONT_PACK_DEFAULT_KEY = 'noto';
const APP_FONT_CONFIG = Object.freeze({ const APP_FONT_CONFIG = Object.freeze({
inter: { inter: {
@@ -8,7 +9,7 @@ const APP_FONT_CONFIG = Object.freeze({
noto_sans: { noto_sans: {
cssName: "'Noto Sans'", cssName: "'Noto Sans'",
cssImport: cssImport:
"@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=Noto+Sans:ital,wght@0,100..900;1,100..900&family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap');" "@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');"
}, },
source_sans_3: { source_sans_3: {
cssName: "'Source Sans 3'", cssName: "'Source Sans 3'",
@@ -18,7 +19,7 @@ const APP_FONT_CONFIG = Object.freeze({
ibm_plex_sans: { ibm_plex_sans: {
cssName: "'IBM Plex Sans'", cssName: "'IBM Plex Sans'",
cssImport: cssImport:
"@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap');" "@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&display=swap');"
}, },
harmonyos_sans: { harmonyos_sans: {
cssName: "'HarmonyOS Sans'", cssName: "'HarmonyOS Sans'",
@@ -33,7 +34,7 @@ const APP_FONT_CONFIG = Object.freeze({
roboto: { roboto: {
cssName: "'Roboto'", cssName: "'Roboto'",
cssImport: cssImport:
"@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Roboto:ital,wght@0,100..900;1,100..900&display=swap');" "@import url('https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100..900;1,100..900&display=swap');"
}, },
fantasque_sans_mono: { fantasque_sans_mono: {
cssName: "'Fantasque Sans Mono'", cssName: "'Fantasque Sans Mono'",
@@ -43,9 +44,69 @@ const APP_FONT_CONFIG = Object.freeze({
system_ui: { system_ui: {
cssName: 'system-ui', cssName: 'system-ui',
link: null link: null
},
custom: {
cssName: '',
link: null
} }
}); });
const APP_FONT_FAMILIES = Object.freeze(Object.keys(APP_FONT_CONFIG)); const APP_FONT_FAMILIES = Object.freeze(Object.keys(APP_FONT_CONFIG));
export { APP_FONT_CONFIG, APP_FONT_DEFAULT_KEY, APP_FONT_FAMILIES }; const APP_CJK_FONT_PACK_CONFIG = Object.freeze({
noto: {
cssName: Object.freeze({
jp: "'Noto Sans JP Variable'",
kr: "'Noto Sans KR Variable'",
sc: "'Noto Sans SC Variable'",
tc: "'Noto Sans TC Variable'"
}),
link: null
},
pht: {
cssName: Object.freeze({
jp: "'PHT Sans JP'",
kr: "'PHT Sans KR'",
sc: "'PHT Sans SC'",
tc: "'PHT Sans TC'"
}),
cssImport: [
'/* Simplified Chinese */',
"@font-face { font-family: 'PHT Sans SC'; src: url('https://cdn.jsdelivr.net/gh/map1en/pht@1.0.0/sc/phtsansSC-Regular.woff2') format('woff2'); font-weight: 400; font-display: swap; }",
"@font-face { font-family: 'PHT Sans SC'; src: url('https://cdn.jsdelivr.net/gh/map1en/pht@1.0.0/sc/phtsansSC-Medium.woff2') format('woff2'); font-weight: 500; font-display: swap; }",
"@font-face { font-family: 'PHT Sans SC'; src: url('https://cdn.jsdelivr.net/gh/map1en/pht@1.0.0/sc/phtsansSC-SemiBold.woff2') format('woff2'); font-weight: 600; font-display: swap; }",
"@font-face { font-family: 'PHT Sans SC'; src: url('https://cdn.jsdelivr.net/gh/map1en/pht@1.0.0/sc/phtsansSC-Bold.woff2') format('woff2'); font-weight: 700; font-display: swap; }",
'/* Traditional Chinese */',
"@font-face { font-family: 'PHT Sans TC'; src: url('https://cdn.jsdelivr.net/gh/map1en/pht@1.0.0/tc/phtsansTC-55.woff2') format('woff2'); font-weight: 400; font-display: swap; }",
"@font-face { font-family: 'PHT Sans TC'; src: url('https://cdn.jsdelivr.net/gh/map1en/pht@1.0.0/tc/phtsansTC-75.woff2') format('woff2'); font-weight: 600; font-display: swap; }",
'/* Japanese */',
"@font-face { font-family: 'PHT Sans JP'; src: url('https://cdn.jsdelivr.net/gh/map1en/pht@1.0.0/jp/phtsansJP-Regular.woff2') format('woff2'); font-weight: 400; font-display: swap; }",
"@font-face { font-family: 'PHT Sans JP'; src: url('https://cdn.jsdelivr.net/gh/map1en/pht@1.0.0/jp/phtsansJP-Medium.woff2') format('woff2'); font-weight: 500; font-display: swap; }",
"@font-face { font-family: 'PHT Sans JP'; src: url('https://cdn.jsdelivr.net/gh/map1en/pht@1.0.0/jp/phtsansJP-Bold.woff2') format('woff2'); font-weight: 700; font-display: swap; }",
'/* Korean */',
"@font-face { font-family: 'PHT Sans KR'; src: url('https://cdn.jsdelivr.net/gh/map1en/pht@1.0.0/kr/phtsansKR-Regular.woff2') format('woff2'); font-weight: 400; font-display: swap; }",
"@font-face { font-family: 'PHT Sans KR'; src: url('https://cdn.jsdelivr.net/gh/map1en/pht@1.0.0/kr/phtsansKR-Medium.woff2') format('woff2'); font-weight: 500; font-display: swap; }",
"@font-face { font-family: 'PHT Sans KR'; src: url('https://cdn.jsdelivr.net/gh/map1en/pht@1.0.0/kr/phtsansKR-Bold.woff2') format('woff2'); font-weight: 700; font-display: swap; }"
].join('\n')
},
system: {
cssName: Object.freeze({
jp: 'system-ui',
kr: 'system-ui',
sc: 'system-ui',
tc: 'system-ui'
}),
link: null
}
});
const APP_CJK_FONT_PACKS = Object.freeze(Object.keys(APP_CJK_FONT_PACK_CONFIG));
export {
APP_FONT_CONFIG,
APP_FONT_DEFAULT_KEY,
APP_FONT_FAMILIES,
APP_CJK_FONT_PACK_CONFIG,
APP_CJK_FONT_PACK_DEFAULT_KEY,
APP_CJK_FONT_PACKS
};
+66 -17
View File
@@ -2,6 +2,8 @@ import { ref } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { import {
APP_CJK_FONT_PACK_CONFIG,
APP_CJK_FONT_PACK_DEFAULT_KEY,
APP_FONT_CONFIG, APP_FONT_CONFIG,
APP_FONT_DEFAULT_KEY, APP_FONT_DEFAULT_KEY,
THEME_COLORS, THEME_COLORS,
@@ -19,6 +21,7 @@ const THEME_MODE_STYLE_ID = 'app-theme-mode-style';
const DEFAULT_THEME_COLOR_KEY = 'default'; const DEFAULT_THEME_COLOR_KEY = 'default';
const APP_FONT_LINK_ATTR = 'data-app-font'; const APP_FONT_LINK_ATTR = 'data-app-font';
const APP_CJK_FONT_PACK_LINK_ATTR = 'data-app-cjk-font-pack';
const themeColors = THEME_COLORS.map((theme) => ({ const themeColors = THEME_COLORS.map((theme) => ({
...theme, ...theme,
@@ -166,44 +169,89 @@ function resolveAppFontFamily(fontKey) {
}; };
} }
function ensureAppFontLinks(fontKey) { function ensureDynamicFontStyle(attrName, styleKey, cssImport) {
const head = document.head; const head = document.head;
if (!head) { if (!head) {
return; return;
} }
document document.querySelectorAll(`style[${attrName}]`).forEach((styleEl) => {
.querySelectorAll(`style[${APP_FONT_LINK_ATTR}]`) if (styleEl.getAttribute(attrName) !== styleKey) {
.forEach((styleEl) => { styleEl.remove();
if (styleEl.getAttribute(APP_FONT_LINK_ATTR) !== fontKey) { }
styleEl.remove(); });
}
});
const config = APP_FONT_CONFIG[fontKey]; if (!cssImport) {
if (!config?.cssImport) {
return; return;
} }
const existing = document.querySelector( const existing = document.querySelector(`style[${attrName}="${styleKey}"]`);
`style[${APP_FONT_LINK_ATTR}="${fontKey}"]`
);
if (existing) { if (existing) {
return; return;
} }
const styleEl = document.createElement('style'); const styleEl = document.createElement('style');
styleEl.setAttribute(APP_FONT_LINK_ATTR, fontKey); styleEl.setAttribute(attrName, styleKey);
styleEl.textContent = config.cssImport; styleEl.textContent = cssImport;
head.appendChild(styleEl); head.appendChild(styleEl);
} }
function applyAppFontFamily(fontKey) { function resolveAppCjkFontPack(packKey) {
const normalized = String(packKey || '')
.trim()
.toLowerCase();
if (APP_CJK_FONT_PACK_CONFIG[normalized]) {
return { key: normalized, ...APP_CJK_FONT_PACK_CONFIG[normalized] };
}
return {
key: APP_CJK_FONT_PACK_DEFAULT_KEY,
...APP_CJK_FONT_PACK_CONFIG[APP_CJK_FONT_PACK_DEFAULT_KEY]
};
}
function ensureAppCjkFontPackLinks(packKey) {
const config = APP_CJK_FONT_PACK_CONFIG[packKey];
ensureDynamicFontStyle(
APP_CJK_FONT_PACK_LINK_ATTR,
packKey,
config?.cssImport
);
}
function applyAppFontFamily(fontKey, customCssName) {
if (fontKey === 'custom') {
const cssName = String(customCssName || '').trim() || 'system-ui';
const root = document.documentElement;
root.style.setProperty('--font-western-primary', cssName);
ensureDynamicFontStyle(APP_FONT_LINK_ATTR, 'custom', null);
return {
key: 'custom',
...APP_FONT_CONFIG.custom,
cssName
};
}
const resolved = resolveAppFontFamily(fontKey); const resolved = resolveAppFontFamily(fontKey);
const root = document.documentElement; const root = document.documentElement;
root.style.setProperty('--font-western-primary', resolved.cssName); root.style.setProperty('--font-western-primary', resolved.cssName);
ensureDynamicFontStyle(
APP_FONT_LINK_ATTR,
resolved.key,
resolved.cssImport
);
ensureAppFontLinks(resolved.key); return resolved;
}
function applyAppCjkFontPack(packKey) {
const resolved = resolveAppCjkFontPack(packKey);
const root = document.documentElement;
root.style.setProperty('--font-cjk-jp-primary', resolved.cssName.jp);
root.style.setProperty('--font-cjk-sc-primary', resolved.cssName.sc);
root.style.setProperty('--font-cjk-kr-primary', resolved.cssName.kr);
root.style.setProperty('--font-cjk-tc-primary', resolved.cssName.tc);
ensureAppCjkFontPackLinks(resolved.key);
return resolved; return resolved;
} }
@@ -461,6 +509,7 @@ export {
refreshCustomCss, refreshCustomCss,
refreshCustomScript, refreshCustomScript,
applyAppFontFamily, applyAppFontFamily,
applyAppCjkFontPack,
HueToHex, HueToHex,
HSVtoRGB, HSVtoRGB,
formatJsonVars, formatJsonVars,
+52 -2
View File
@@ -4,6 +4,8 @@ import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { import {
APP_CJK_FONT_PACK_DEFAULT_KEY,
APP_CJK_FONT_PACKS,
APP_FONT_DEFAULT_KEY, APP_FONT_DEFAULT_KEY,
APP_FONT_FAMILIES, APP_FONT_FAMILIES,
SEARCH_LIMIT_MAX, SEARCH_LIMIT_MAX,
@@ -13,6 +15,7 @@ import {
THEME_CONFIG THEME_CONFIG
} from '../../shared/constants'; } from '../../shared/constants';
import { import {
applyAppCjkFontPack,
HueToHex, HueToHex,
applyAppFontFamily, applyAppFontFamily,
changeAppThemeStyle, changeAppThemeStyle,
@@ -55,6 +58,8 @@ export const useAppearanceSettingsStore = defineStore(
const isDarkMode = ref(false); const isDarkMode = ref(false);
const lastDarkTheme = ref('dark'); const lastDarkTheme = ref('dark');
const appFontFamily = ref('inter'); const appFontFamily = ref('inter');
const customFontFamily = ref('');
const appCjkFontPack = ref(APP_CJK_FONT_PACK_DEFAULT_KEY);
const displayVRCPlusIconsAsAvatar = ref(false); const displayVRCPlusIconsAsAvatar = ref(false);
const hideNicknames = ref(false); const hideNicknames = ref(false);
const showInstanceIdInLocation = ref(false); const showInstanceIdInLocation = ref(false);
@@ -168,6 +173,8 @@ export const useAppearanceSettingsStore = defineStore(
dataTableStripedConfig, dataTableStripedConfig,
showPointerOnHoverConfig, showPointerOnHoverConfig,
appFontFamilyConfig, appFontFamilyConfig,
customFontFamilyConfig,
appCjkFontPackConfig,
lastDarkThemeConfig lastDarkThemeConfig
] = await Promise.all([ ] = await Promise.all([
configRepository.getString('VRCX_appLanguage'), configRepository.getString('VRCX_appLanguage'),
@@ -234,6 +241,11 @@ export const useAppearanceSettingsStore = defineStore(
'VRCX_fontFamily', 'VRCX_fontFamily',
APP_FONT_DEFAULT_KEY APP_FONT_DEFAULT_KEY
), ),
configRepository.getString('VRCX_customFontFamily', ''),
configRepository.getString(
'VRCX_cjkFontPack',
APP_CJK_FONT_PACK_DEFAULT_KEY
),
configRepository.getString( configRepository.getString(
'VRCX_lastDarkTheme', 'VRCX_lastDarkTheme',
fallbackDarkTheme fallbackDarkTheme
@@ -262,7 +274,11 @@ export const useAppearanceSettingsStore = defineStore(
fallbackDarkTheme fallbackDarkTheme
); );
appFontFamily.value = normalizeAppFontFamily(appFontFamilyConfig); appFontFamily.value = normalizeAppFontFamily(appFontFamilyConfig);
applyAppFontFamily(appFontFamily.value); customFontFamily.value = customFontFamilyConfig || '';
appCjkFontPack.value =
normalizeAppCjkFontPack(appCjkFontPackConfig);
applyAppFontFamily(appFontFamily.value, customFontFamily.value);
applyAppCjkFontPack(appCjkFontPack.value);
displayVRCPlusIconsAsAvatar.value = displayVRCPlusIconsAsAvatar.value =
displayVRCPlusIconsAsAvatarConfig; displayVRCPlusIconsAsAvatarConfig;
@@ -508,11 +524,22 @@ export const useAppearanceSettingsStore = defineStore(
* @param value * @param value
*/ */
function normalizeAppFontFamily(value) { function normalizeAppFontFamily(value) {
if (value === 'custom') return 'custom';
return APP_FONT_FAMILIES.includes(value) return APP_FONT_FAMILIES.includes(value)
? value ? value
: APP_FONT_DEFAULT_KEY; : APP_FONT_DEFAULT_KEY;
} }
/**
*
* @param value
*/
function normalizeAppCjkFontPack(value) {
return APP_CJK_FONT_PACKS.includes(value)
? value
: APP_CJK_FONT_PACK_DEFAULT_KEY;
}
/** /**
* *
* @param value * @param value
@@ -521,7 +548,26 @@ export const useAppearanceSettingsStore = defineStore(
const normalized = normalizeAppFontFamily(value); const normalized = normalizeAppFontFamily(value);
appFontFamily.value = normalized; appFontFamily.value = normalized;
configRepository.setString('VRCX_fontFamily', normalized); configRepository.setString('VRCX_fontFamily', normalized);
applyAppFontFamily(normalized); applyAppFontFamily(normalized, customFontFamily.value);
}
function setCustomFontFamily(value) {
customFontFamily.value = value;
configRepository.setString('VRCX_customFontFamily', value);
if (appFontFamily.value === 'custom') {
applyAppFontFamily('custom', value);
}
}
/**
*
* @param value
*/
function setAppCjkFontPack(value) {
const normalized = normalizeAppCjkFontPack(value);
appCjkFontPack.value = normalized;
configRepository.setString('VRCX_cjkFontPack', normalized);
applyAppCjkFontPack(normalized);
} }
/** /**
@@ -1062,6 +1108,7 @@ export const useAppearanceSettingsStore = defineStore(
themeMode, themeMode,
isDarkMode, isDarkMode,
appFontFamily, appFontFamily,
appCjkFontPack,
displayVRCPlusIconsAsAvatar, displayVRCPlusIconsAsAvatar,
hideNicknames, hideNicknames,
showInstanceIdInLocation, showInstanceIdInLocation,
@@ -1142,6 +1189,9 @@ export const useAppearanceSettingsStore = defineStore(
setNavCollapsed, setNavCollapsed,
toggleNavCollapsed, toggleNavCollapsed,
setAppFontFamily, setAppFontFamily,
customFontFamily,
setCustomFontFamily,
setAppCjkFontPack,
setThemeMode, setThemeMode,
toggleThemeMode toggleThemeMode
}; };
+16 -11
View File
@@ -12,37 +12,42 @@
} }
:root { :root {
/* Keep these bootstrap defaults aligned with src/shared/constants/fonts.js */
--font-western-primary: 'Inter Variable'; --font-western-primary: 'Inter Variable';
--font-cjk-jp-primary: 'Noto Sans JP Variable';
--font-cjk-sc-primary: 'Noto Sans SC Variable';
--font-cjk-kr-primary: 'Noto Sans KR Variable';
--font-cjk-tc-primary: 'Noto Sans TC Variable';
--font-western: --font-western:
'ellipsis-font', -apple-system, var(--font-western-primary), 'Segoe UI', 'ellipsis-font', -apple-system, var(--font-western-primary), 'Segoe UI',
'Roboto', 'Ubuntu', 'Cantarell', 'DejaVu Sans', sans-serif; 'Roboto', 'Ubuntu', 'Cantarell', 'DejaVu Sans';
--font-symbol: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; --font-symbol: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--font-fallback-cjk: sans-serif; --font-fallback-cjk: sans-serif;
--font-primary-cjk: --font-primary-cjk:
'Noto Sans JP Variable', 'Noto Sans SC Variable', var(--font-cjk-jp-primary), var(--font-cjk-sc-primary),
'Noto Sans KR Variable', 'Noto Sans TC Variable'; var(--font-cjk-kr-primary), var(--font-cjk-tc-primary);
} }
:root[lang='zh-CN'] { :root[lang='zh-CN'] {
--font-primary-cjk: --font-primary-cjk:
'Noto Sans SC Variable', 'Noto Sans JP Variable', var(--font-cjk-sc-primary), var(--font-cjk-jp-primary),
'Noto Sans KR Variable', 'Noto Sans TC Variable'; var(--font-cjk-kr-primary), var(--font-cjk-tc-primary);
} }
:root[lang='ja'] { :root[lang='ja'] {
--font-primary-cjk: --font-primary-cjk:
'Noto Sans JP Variable', 'Noto Sans KR Variable', var(--font-cjk-jp-primary), var(--font-cjk-kr-primary),
'Noto Sans TC Variable', 'Noto Sans SC Variable'; var(--font-cjk-tc-primary), var(--font-cjk-sc-primary);
} }
:root[lang='ko'] { :root[lang='ko'] {
--font-primary-cjk: --font-primary-cjk:
'Noto Sans KR Variable', 'Noto Sans JP Variable', var(--font-cjk-kr-primary), var(--font-cjk-jp-primary),
'Noto Sans TC Variable', 'Noto Sans SC Variable'; var(--font-cjk-tc-primary), var(--font-cjk-sc-primary);
} }
:root[lang='zh-TW'] { :root[lang='zh-TW'] {
--font-primary-cjk: --font-primary-cjk:
'Noto Sans TC Variable', 'Noto Sans JP Variable', var(--font-cjk-tc-primary), var(--font-cjk-jp-primary),
'Noto Sans KR Variable', 'Noto Sans SC Variable'; var(--font-cjk-kr-primary), var(--font-cjk-sc-primary);
} }
@@ -19,32 +19,61 @@
</div> </div>
<div class="options-container-item"> <div class="options-container-item">
<span class="name flex! items-center!"> <span class="name">
{{ t('view.settings.appearance.appearance.font_family') }} {{ t('view.settings.appearance.appearance.font_family') }}
<TooltipWrapper
class="ml-1.5"
side="top"
:content="t('view.settings.appearance.appearance.font_family_tooltip')">
<Info />
</TooltipWrapper>
</span> </span>
<Select :model-value="appFontFamily" @update:modelValue="setAppFontFamily"> <DropdownMenu>
<SelectTrigger size="sm"> <DropdownMenuTrigger as-child>
<SelectValue <Button variant="outline" size="sm" class="min-w-[180px] justify-between font-normal">
:placeholder="t(`view.settings.appearance.appearance.font_family_${appFontFamily}`)" /> <span class="truncate">{{ fontDropdownDisplayText }}</span>
</SelectTrigger> <ChevronDown class="ml-2 size-4 shrink-0 opacity-50" />
<SelectContent> </Button>
<SelectGroup> </DropdownMenuTrigger>
<template v-for="option in appFontFamilyOptions" :key="option.key"> <DropdownMenuContent align="end">
<SelectSeparator v-if="option.type === 'separator'" /> <DropdownMenuCheckboxItem
<SelectItem v-else :value="option.key"> v-for="option in westernFontItems"
{{ t(`view.settings.appearance.appearance.font_family_${option.key}`) }} :key="option.key"
</SelectItem> :model-value="appFontFamily === option.key"
</template> @select="handleSelectWesternFont(option.key)">
</SelectGroup> {{ option.label }}
</SelectContent> </DropdownMenuCheckboxItem>
</Select> <DropdownMenuSeparator />
<DropdownMenuCheckboxItem
v-for="option in cjkFontItems"
:key="option.key"
:model-value="appCjkFontPack === option.key && appFontFamily !== 'custom'"
@select="handleSelectCjkFont(option.key)">
{{ option.label }}
</DropdownMenuCheckboxItem>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
:model-value="appFontFamily === 'custom'"
@select="handleSelectCustomFont">
{{ t('view.settings.appearance.appearance.font_family_custom') }}
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
<Dialog v-model:open="customFontDialogOpen">
<DialogContent class="sm:max-w-md">
<DialogHeader>
<DialogTitle>{{
t('view.settings.appearance.appearance.font_family_custom_dialog_title')
}}</DialogTitle>
<DialogDescription>{{
t('view.settings.appearance.appearance.font_family_custom_dialog_description')
}}</DialogDescription>
</DialogHeader>
<Input v-model="customFontInput" placeholder="'My Font', Arial, sans-serif" />
<DialogFooter>
<Button variant="outline" @click="customFontDialogOpen = false">
{{ t('dialog.alertdialog.cancel') }}
</Button>
<Button @click="saveCustomFont">
{{ t('dialog.alertdialog.ok') }}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div> </div>
<div v-if="!isLinux" class="options-container-item"> <div v-if="!isLinux" class="options-container-item">
<span class="name">{{ t('view.settings.appearance.appearance.zoom') }}</span> <span class="name">{{ t('view.settings.appearance.appearance.zoom') }}</span>
@@ -332,6 +361,13 @@
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from '@/components/ui/select'; } from '@/components/ui/select';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { ListboxContent, ListboxFilter, ListboxItem, ListboxItemIndicator, ListboxRoot, useFilter } from 'reka-ui'; import { ListboxContent, ListboxFilter, ListboxItem, ListboxItemIndicator, ListboxRoot, useFilter } from 'reka-ui';
import { import {
NumberField, NumberField,
@@ -347,13 +383,22 @@
TagsInputItemDelete, TagsInputItemDelete,
TagsInputItemText TagsInputItemText
} from '@/components/ui/tags-input'; } from '@/components/ui/tags-input';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { computed, onBeforeUnmount, ref, watch } from 'vue'; import { computed, onBeforeUnmount, ref, watch } from 'vue';
import { CheckIcon, ChevronDown, Info } from 'lucide-vue-next'; import { CheckIcon, ChevronDown } from 'lucide-vue-next';
import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '@/stores'; import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '@/stores';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { getLanguageName, languageCodes } from '@/localization'; import { getLanguageName, languageCodes } from '@/localization';
import { APP_FONT_FAMILIES } from '@/shared/constants'; import { APP_CJK_FONT_PACKS, APP_FONT_CONFIG, APP_FONT_DEFAULT_KEY, APP_FONT_FAMILIES } from '@/shared/constants';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
@@ -374,6 +419,8 @@
appLanguage, appLanguage,
displayVRCPlusIconsAsAvatar, displayVRCPlusIconsAsAvatar,
appFontFamily, appFontFamily,
customFontFamily,
appCjkFontPack,
hideNicknames, hideNicknames,
showInstanceIdInLocation, showInstanceIdInLocation,
isAgeGatedInstancesVisible, isAgeGatedInstancesVisible,
@@ -412,18 +459,80 @@
setTablePageSizes, setTablePageSizes,
toggleStripedDataTable, toggleStripedDataTable,
togglePointerOnHover, togglePointerOnHover,
setAppFontFamily setAppFontFamily,
setCustomFontFamily,
setAppCjkFontPack
} = appearanceSettingsStore; } = appearanceSettingsStore;
const appFontFamilyOptions = computed(() => { const fontDropdownDisplayText = computed(() => {
const fontKeys = APP_FONT_FAMILIES.filter((key) => key !== 'system_ui'); if (appFontFamily.value === 'custom') {
return [ return t('view.settings.appearance.appearance.font_family_custom');
...fontKeys.map((key) => ({ type: 'item', key })), }
{ type: 'separator', key: 'separator-system-ui' }, const western = t(`view.settings.appearance.appearance.font_family_${appFontFamily.value}`);
{ type: 'item', key: 'system_ui' } const cjk =
]; appCjkFontPack.value === 'system'
? t('view.settings.appearance.appearance.font_family_system_ui')
: t(`view.settings.appearance.appearance.cjk_font_pack_${appCjkFontPack.value}`);
return `${western} / ${cjk}`;
}); });
const westernFontItems = computed(() => {
return APP_FONT_FAMILIES.filter((key) => key !== 'custom' && key !== 'system_ui').map((key) => ({
key,
label: t(`view.settings.appearance.appearance.font_family_${key}`)
}));
});
const cjkFontItems = computed(() => {
return APP_CJK_FONT_PACKS.map((key) => ({
key,
label:
key === 'system'
? t('view.settings.appearance.appearance.font_family_system_ui')
: t(`view.settings.appearance.appearance.cjk_font_pack_${key}`)
}));
});
const FONT_FAMILY_REGEX =
/^\s*(([-_\p{L}][\p{L}\p{N}_\s-]*)|'[^']+'|"[^"]+")\s*(,\s*(([-_\p{L}][\p{L}\p{N}_\s-]*)|'[^']+'|"[^"]+")\s*)*$/u;
const customFontDialogOpen = ref(false);
const customFontInput = ref('');
function handleSelectWesternFont(key) {
setAppFontFamily(key);
}
function handleSelectCjkFont(key) {
if (appFontFamily.value === 'custom') {
setAppFontFamily(APP_FONT_DEFAULT_KEY);
}
setAppCjkFontPack(key);
}
function handleSelectCustomFont() {
const cssVarValue = getComputedStyle(document.documentElement)
.getPropertyValue('--font-western-primary')
.trim();
const currentKey = String(appFontFamily.value || APP_FONT_DEFAULT_KEY)
.trim()
.toLowerCase();
const fallbackFont = APP_FONT_CONFIG[currentKey]?.cssName || APP_FONT_CONFIG[APP_FONT_DEFAULT_KEY].cssName;
customFontInput.value = customFontFamily.value?.trim() || cssVarValue || fallbackFont;
customFontDialogOpen.value = true;
}
function saveCustomFont() {
const trimmed = customFontInput.value.trim();
if (!trimmed || !FONT_FAMILY_REGEX.test(trimmed)) {
toast.error(t('view.settings.appearance.appearance.font_family_custom_invalid'));
return;
}
setCustomFontFamily(trimmed);
setAppFontFamily('custom');
customFontDialogOpen.value = false;
}
const zoomLevel = ref(100); const zoomLevel = ref(100);
const isLinux = computed(() => LINUX); const isLinux = computed(() => LINUX);
let cleanupWheel = null; let cleanupWheel = null;
+3 -32
View File
@@ -22,16 +22,8 @@ body {
margin: 0; margin: 0;
} }
/* Font variables are shared from ../styles/fonts.css */
:root { :root {
--font-western:
'ellipsis-font', -apple-system, 'Inter Variable', 'Segoe UI', 'Roboto',
'Ubuntu', 'Cantarell', 'DejaVu Sans', sans-serif;
--font-symbol: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
--font-fallback-cjk: sans-serif;
--font-primary-cjk:
'Noto Sans JP Variable', 'Noto Sans SC Variable',
'Noto Sans KR Variable', 'Noto Sans TC Variable';
/* VRChat Status Colors (duplicated from globals.css for VR panel independence) */ /* VRChat Status Colors (duplicated from globals.css for VR panel independence) */
--status-online: #67c23a; --status-online: #67c23a;
--status-joinme: #00b8ff; --status-joinme: #00b8ff;
@@ -46,26 +38,6 @@ body {
--platform-quest: #3ddc84; --platform-quest: #3ddc84;
--platform-ios: #8e8e93; --platform-ios: #8e8e93;
} }
:root[lang='zh-CN'] {
--font-primary-cjk:
'Noto Sans SC Variable', 'Noto Sans JP Variable',
'Noto Sans KR Variable', 'Noto Sans TC Variable';
}
:root[lang='ja'] {
--font-primary-cjk:
'Noto Sans JP Variable', 'Noto Sans KR Variable',
'Noto Sans TC Variable', 'Noto Sans SC Variable';
}
:root[lang='ko'] {
--font-primary-cjk:
'Noto Sans KR Variable', 'Noto Sans JP Variable',
'Noto Sans TC Variable', 'Noto Sans SC Variable';
}
:root[lang='zh-TW'] {
--font-primary-cjk:
'Noto Sans TC Variable', 'Noto Sans JP Variable',
'Noto Sans KR Variable', 'Noto Sans SC Variable';
}
body { body {
font-family: font-family:
var(--font-western), var(--font-symbol), var(--font-primary-cjk), var(--font-western), var(--font-symbol), var(--font-primary-cjk),
@@ -230,9 +202,8 @@ textarea,
select, select,
button { button {
font-family: font-family:
'ellipsis-font', 'Noto Sans JP Variable', 'Noto Sans KR Variable', var(--font-western), var(--font-symbol), var(--font-primary-cjk),
'Noto Sans TC Variable', 'Noto Sans SC Variable', 'Meiryo UI', var(--font-fallback-cjk);
'Malgun Gothic', 'Segoe UI', sans-serif;
line-height: normal; line-height: normal;
text-shadow: text-shadow:
#000 0px 0px 3px, #000 0px 0px 3px,