mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-13 01:49:41 +08:00
✨ feat(font): 新增系统字体枚举与全局字体配置能力
- 新增 Go 侧已安装字体扫描接口,支持前端读取系统真实字体列表 - 接入 Wails 字体查询导出,补齐 App.d.ts 与 App.js 调用声明 - 新增字体选项构建与匹配工具,区分 UI 字体与等宽字体候选 - 外观设置支持按平台加载字体列表,并支持搜索匹配与默认字体回退 - Store 增加自定义 UI 字体与代码字体配置,持久化全局字体选择
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
:root {
|
||||
--gn-font-sans: "Inter", "PingFang SC", -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Segoe UI", sans-serif;
|
||||
--gn-font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
@@ -10,6 +15,21 @@ body, #root {
|
||||
border-radius: var(--gonavi-border-radius); /* Slightly rounded app window corners */
|
||||
}
|
||||
|
||||
body,
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font-family: var(--gn-font-sans);
|
||||
}
|
||||
|
||||
code,
|
||||
pre,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--gn-font-mono);
|
||||
}
|
||||
|
||||
/* 侧边栏 Tree 样式优化 */
|
||||
.ant-tree .ant-tree-treenode {
|
||||
width: 100%;
|
||||
|
||||
@@ -179,8 +179,16 @@ describe('global appearance tokens', () => {
|
||||
expect(appSource).toContain("setProperty('--gn-sidebar-tree-font-size'");
|
||||
expect(appSource).toContain("setProperty('--gn-control-height'");
|
||||
expect(appSource).toContain("setProperty('--gn-control-height-sm'");
|
||||
expect(appSource).toContain('fontFamily: resolvedUiFontFamily');
|
||||
expect(appSource).toContain('fontFamilyCode: resolvedMonoFontFamily');
|
||||
expect(appSource).toContain('数据表字体大小');
|
||||
expect(appSource).toContain('左侧库表字体大小');
|
||||
expect(appSource).toContain('buildFontFamilyOptions(runtimePlatform, \'ui\', installedFontFamilies)');
|
||||
expect(appSource).toContain('buildFontFamilyOptions(runtimePlatform, \'mono\', installedFontFamilies)');
|
||||
expect(appSource).toContain('ListInstalledFontFamilies()');
|
||||
expect(appSource).toContain('const [installedFontFamilies, setInstalledFontFamilies] = useState<InstalledFontFamily[]>(EMPTY_INSTALLED_FONT_FAMILIES);');
|
||||
expect(appSource).toContain('matchFontFamilyOption');
|
||||
expect(appSource).toContain('showSearch');
|
||||
expect(appSource).toContain('const dataTableFontSizeFollowsGlobal = appearance.dataTableFontSizeFollowGlobal !== false;');
|
||||
expect(appSource).toContain('const sidebarTreeFontSizeFollowsGlobal = appearance.sidebarTreeFontSizeFollowGlobal !== false;');
|
||||
expect(appSource).toContain('disabled={dataTableFontSizeFollowsGlobal}');
|
||||
|
||||
@@ -19,6 +19,7 @@ import SecurityUpdateSettingsModal from './components/SecurityUpdateSettingsModa
|
||||
import { DEFAULT_APPEARANCE, useStore } from './store';
|
||||
import { SavedConnection, SecurityUpdateIssue, SecurityUpdateStatus } from './types';
|
||||
import { blurToFilter, isMacLikePlatform, normalizeBlurForPlatform, normalizeOpacityForPlatform, isWindowsPlatform, resolveAppearanceValues } from './utils/appearance';
|
||||
import { buildFontFamilyOptions, DEFAULT_MONO_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, matchFontFamilyOption, resolveMonoFontFamily, resolveUIFontFamily, sanitizeFontFamilyInput, type FontFamilyOption, type InstalledFontFamily } from './utils/fontFamilies';
|
||||
import {
|
||||
DENSITY_OPTIONS,
|
||||
sanitizeDataTableDensity,
|
||||
@@ -91,7 +92,7 @@ import {
|
||||
} from './utils/aiEntryLayout';
|
||||
import { DEFAULT_AI_PANEL_WIDTH, resolveOverlayAIPanelWidth, shouldOverlayAIPanel } from './utils/aiPanelLayout';
|
||||
import { safeWindowRuntimeCall } from './utils/wailsRuntime';
|
||||
import { ApplyDataRootDirectory, GetDataRootDirectoryInfo, GetSavedConnections, OpenDataRootDirectory, SelectDataRootDirectory, SetMacNativeWindowControls, SetWindowTranslucency } from '../wailsjs/go/app/App';
|
||||
import { ApplyDataRootDirectory, GetDataRootDirectoryInfo, GetSavedConnections, ListInstalledFontFamilies, OpenDataRootDirectory, SelectDataRootDirectory, SetMacNativeWindowControls, SetWindowTranslucency } from '../wailsjs/go/app/App';
|
||||
import './App.css';
|
||||
import './v2-theme.css';
|
||||
|
||||
@@ -270,6 +271,8 @@ function App() {
|
||||
const effectiveSidebarTreeFontSize = sidebarTreeFontSizeFollowsGlobal
|
||||
? effectiveFontSize
|
||||
: (sanitizeSidebarTreeFontSize(appearance.sidebarTreeFontSize) ?? effectiveFontSize);
|
||||
const resolvedUiFontFamily = resolveUIFontFamily(appearance.customUIFontFamily);
|
||||
const resolvedMonoFontFamily = resolveMonoFontFamily(appearance.customMonoFontFamily);
|
||||
const appComponentSize: 'small' | 'middle' | 'large' = effectiveUiScale <= 0.92 ? 'small' : (effectiveUiScale >= 1.12 ? 'large' : 'middle');
|
||||
const titleBarHeight = Math.max(28, Math.round(32 * effectiveUiScale));
|
||||
const titleBarButtonWidth = Math.max(40, Math.round(46 * effectiveUiScale));
|
||||
@@ -281,6 +284,18 @@ function App() {
|
||||
const [runtimePlatform, setRuntimePlatform] = useState('');
|
||||
const [runtimeBuildType, setRuntimeBuildType] = useState('');
|
||||
const [isLinuxRuntime, setIsLinuxRuntime] = useState(false);
|
||||
const [installedFontFamilies, setInstalledFontFamilies] = useState<InstalledFontFamily[]>(EMPTY_INSTALLED_FONT_FAMILIES);
|
||||
const [isFontFamiliesLoading, setIsFontFamiliesLoading] = useState(false);
|
||||
const [fontFamiliesLoadError, setFontFamiliesLoadError] = useState<string | null>(null);
|
||||
const hasLoadedInstalledFontsRef = useRef(false);
|
||||
const uiFontOptions = useMemo(
|
||||
() => buildFontFamilyOptions(runtimePlatform, 'ui', installedFontFamilies),
|
||||
[installedFontFamilies, runtimePlatform],
|
||||
);
|
||||
const monoFontOptions = useMemo(
|
||||
() => buildFontFamilyOptions(runtimePlatform, 'mono', installedFontFamilies),
|
||||
[installedFontFamilies, runtimePlatform],
|
||||
);
|
||||
const [isStoreHydrated, setIsStoreHydrated] = useState(() => useStore.persist.hasHydrated());
|
||||
const [hasLoadedSecureConfig, setHasLoadedSecureConfig] = useState(false);
|
||||
const [viewportWidth, setViewportWidth] = useState(() => (typeof window === 'undefined' ? 1280 : window.innerWidth || 1280));
|
||||
@@ -2644,6 +2659,9 @@ function App() {
|
||||
darkMode,
|
||||
effectiveDataTableFontSize,
|
||||
effectiveFontSize,
|
||||
resolvedMonoFontFamily,
|
||||
resolvedUiFontFamily,
|
||||
runtimePlatform,
|
||||
effectiveSidebarTreeFontSize,
|
||||
effectiveUiScale,
|
||||
tokenControlHeight,
|
||||
@@ -2923,6 +2941,8 @@ function App() {
|
||||
fontSize: tokenFontSize,
|
||||
fontSizeSM: tokenFontSizeSM,
|
||||
fontSizeLG: tokenFontSizeLG,
|
||||
fontFamily: resolvedUiFontFamily,
|
||||
fontFamilyCode: resolvedMonoFontFamily,
|
||||
controlHeight: tokenControlHeight,
|
||||
controlHeightSM: tokenControlHeightSM,
|
||||
controlHeightLG: tokenControlHeightLG,
|
||||
@@ -2973,13 +2993,30 @@ function App() {
|
||||
}), [
|
||||
darkMode,
|
||||
effectiveOpacity,
|
||||
isV2Ui,
|
||||
tokenControlHeight,
|
||||
tokenControlHeightLG,
|
||||
tokenControlHeightSM,
|
||||
tokenFontSize,
|
||||
tokenFontSizeLG,
|
||||
tokenFontSizeSM,
|
||||
resolvedMonoFontFamily,
|
||||
resolvedUiFontFamily,
|
||||
]);
|
||||
const filterFontOption = useCallback((input: string, option?: { value?: string; label?: React.ReactNode }) => (
|
||||
matchFontFamilyOption(input, {
|
||||
value: String(option?.value || ''),
|
||||
label: String(option?.label || ''),
|
||||
})
|
||||
), []);
|
||||
const renderFontOptionLabel = useCallback((option: FontFamilyOption) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, lineHeight: 1.35 }}>
|
||||
<span>{option.label}</span>
|
||||
<span style={{ fontSize: 11, color: darkMode ? 'rgba(255,255,255,0.45)' : 'rgba(16,24,40,0.45)' }}>
|
||||
{option.value}
|
||||
</span>
|
||||
</div>
|
||||
), [darkMode]);
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
@@ -3998,6 +4035,75 @@ function App() {
|
||||
<span style={{ width: 56 }}>{effectiveFontSize}px</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style={utilityPanelStyle}>
|
||||
<div style={{ marginBottom: 10, fontWeight: 500 }}>字体族</div>
|
||||
<div style={{ display: 'grid', gap: 14 }}>
|
||||
<div>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>界面字体 (UI Font Family)</div>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
loading={isFontFamiliesLoading}
|
||||
placeholder={DEFAULT_UI_FONT_FAMILY}
|
||||
value={appearance.customUIFontFamily ?? undefined}
|
||||
onChange={(value) => setAppearance({
|
||||
customUIFontFamily: sanitizeFontFamilyInput(value),
|
||||
})}
|
||||
onClear={() => setAppearance({ customUIFontFamily: null })}
|
||||
options={uiFontOptions.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
}))}
|
||||
filterOption={filterFontOption}
|
||||
popupMatchSelectWidth
|
||||
style={{ width: '100%' }}
|
||||
optionRender={(option) => renderFontOptionLabel({
|
||||
value: String(option.data.value),
|
||||
label: String(option.data.label),
|
||||
})}
|
||||
/>
|
||||
<div style={{ ...utilityMutedTextStyle, marginTop: 6 }}>
|
||||
{fontFamiliesLoadError
|
||||
? `系统字体加载失败,当前回退常见字体预置:${fontFamiliesLoadError}`
|
||||
: (installedFontFamilies.length > 0
|
||||
? `已读取当前系统 ${installedFontFamilies.length} 个字体族,支持输入搜索匹配。清空后回退默认 UI 字体。`
|
||||
: '按当前系统实时加载已安装字体,支持输入搜索匹配。清空后回退默认 UI 字体。')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ marginBottom: 8, fontWeight: 500 }}>代码字体 (Mono Font Family)</div>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
loading={isFontFamiliesLoading}
|
||||
placeholder={DEFAULT_MONO_FONT_FAMILY}
|
||||
value={appearance.customMonoFontFamily ?? undefined}
|
||||
onChange={(value) => setAppearance({
|
||||
customMonoFontFamily: sanitizeFontFamilyInput(value),
|
||||
})}
|
||||
onClear={() => setAppearance({ customMonoFontFamily: null })}
|
||||
options={monoFontOptions.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.label,
|
||||
}))}
|
||||
filterOption={filterFontOption}
|
||||
popupMatchSelectWidth
|
||||
style={{ width: '100%' }}
|
||||
optionRender={(option) => renderFontOptionLabel({
|
||||
value: String(option.data.value),
|
||||
label: String(option.data.label),
|
||||
})}
|
||||
/>
|
||||
<div style={{ ...utilityMutedTextStyle, marginTop: 6 }}>
|
||||
{fontFamiliesLoadError
|
||||
? '当前已回退常见代码字体预置。作用于 SQL 编辑器、AI 代码块、日志、DDL 与数据表等宽内容。'
|
||||
: '优先展示当前系统已安装字体,名称接近 Mono/Code/Console 的字体会靠前。作用于 SQL 编辑器、AI 代码块、日志、DDL 与数据表等宽内容。'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={utilityPanelStyle}>
|
||||
<div style={{ marginBottom: 10, fontWeight: 500 }}>透明与模糊效果</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginBottom: 12 }}>
|
||||
@@ -4277,7 +4383,7 @@ function App() {
|
||||
<Input
|
||||
readOnly
|
||||
value={isCapturing ? '请按下快捷键...' : getShortcutDisplayLabel(binding.combo, activeShortcutPlatform)}
|
||||
style={{ width: 180, fontFamily: 'Consolas, Menlo, Monaco, monospace' }}
|
||||
style={{ width: 180, fontFamily: resolvedMonoFontFamily }}
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
|
||||
@@ -99,6 +99,7 @@ if (typeof window !== 'undefined' && !(window as any).go) {
|
||||
CheckUpdate: async () => ({ success: false }),
|
||||
DownloadUpdate: async () => ({ success: false }),
|
||||
GetSavedConnections: async () => cloneBrowserMockValue(mockConnections),
|
||||
ListInstalledFontFamilies: async () => ({ success: true, data: [] }),
|
||||
SaveConnection: async (input: any) => saveMockConnection(input),
|
||||
DeleteConnection: async (id: string) => {
|
||||
const index = mockConnections.findIndex((item) => item.id === id);
|
||||
@@ -185,4 +186,3 @@ ReactDOM.createRoot(rootNode).render(
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -75,6 +75,8 @@ describe('store appearance persistence', () => {
|
||||
expect(appearance.dataTableFontSizeFollowGlobal).toBe(true);
|
||||
expect(appearance.sidebarTreeFontSize).toBeNull();
|
||||
expect(appearance.sidebarTreeFontSizeFollowGlobal).toBe(true);
|
||||
expect(appearance.customUIFontFamily).toBeNull();
|
||||
expect(appearance.customMonoFontFamily).toBeNull();
|
||||
});
|
||||
|
||||
it('persists DataGrid appearance settings and restores them after reload', async () => {
|
||||
@@ -97,6 +99,26 @@ describe('store appearance persistence', () => {
|
||||
expect(appearance.dataTableDensity).toBe('compact');
|
||||
});
|
||||
|
||||
it('persists custom font families and sanitizes blank values', async () => {
|
||||
const { useStore } = await importStore();
|
||||
|
||||
useStore.getState().setAppearance({
|
||||
customUIFontFamily: ' IBM Plex Sans, PingFang SC ',
|
||||
customMonoFontFamily: ' ',
|
||||
});
|
||||
|
||||
let appearance = useStore.getState().appearance;
|
||||
expect(appearance.customUIFontFamily).toBe('IBM Plex Sans, PingFang SC');
|
||||
expect(appearance.customMonoFontFamily).toBeNull();
|
||||
|
||||
vi.resetModules();
|
||||
const reloaded = await importStore();
|
||||
appearance = reloaded.useStore.getState().appearance;
|
||||
|
||||
expect(appearance.customUIFontFamily).toBe('IBM Plex Sans, PingFang SC');
|
||||
expect(appearance.customMonoFontFamily).toBeNull();
|
||||
});
|
||||
|
||||
it('does not clear persisted legacy connections during hydration migration', async () => {
|
||||
storage.setItem('lite-db-storage', JSON.stringify({
|
||||
state: {
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
resolveOceanBaseProtocolFromConfig,
|
||||
resolveOceanBaseProtocolFromQueryText,
|
||||
} from "./utils/oceanBaseProtocol";
|
||||
import { sanitizeFontFamilyInput } from "./utils/fontFamilies";
|
||||
|
||||
export interface AppearanceSettings extends DataGridDisplaySettings {
|
||||
uiVersion: "legacy" | "v2";
|
||||
@@ -53,6 +54,8 @@ export interface AppearanceSettings extends DataGridDisplaySettings {
|
||||
opacity: number;
|
||||
blur: number;
|
||||
useNativeMacWindowControls: boolean;
|
||||
customUIFontFamily: string | null;
|
||||
customMonoFontFamily: string | null;
|
||||
}
|
||||
|
||||
export const DEFAULT_APPEARANCE: AppearanceSettings = {
|
||||
@@ -61,6 +64,8 @@ export const DEFAULT_APPEARANCE: AppearanceSettings = {
|
||||
opacity: 1.0,
|
||||
blur: 0,
|
||||
useNativeMacWindowControls: false,
|
||||
customUIFontFamily: null,
|
||||
customMonoFontFamily: null,
|
||||
...DEFAULT_DATA_GRID_DISPLAY_SETTINGS,
|
||||
};
|
||||
const DEFAULT_UI_SCALE = 1.0;
|
||||
@@ -79,7 +84,7 @@ const DEFAULT_TIMEOUT_SECONDS = 30;
|
||||
const MAX_TIMEOUT_SECONDS = 3600;
|
||||
const DEFAULT_DIAGNOSTIC_TIMEOUT_SECONDS = 15;
|
||||
const MAX_DIAGNOSTIC_TIMEOUT_SECONDS = 300;
|
||||
const PERSIST_VERSION = 9;
|
||||
const PERSIST_VERSION = 10;
|
||||
const PERSIST_STORAGE_KEY = "lite-db-storage";
|
||||
const PERSIST_WRITE_DEBOUNCE_MS = 160;
|
||||
const MAX_PERSISTED_QUERY_TABS = 20;
|
||||
@@ -1593,6 +1598,8 @@ const sanitizeAppearance = (
|
||||
typeof appearance.useNativeMacWindowControls === "boolean"
|
||||
? appearance.useNativeMacWindowControls
|
||||
: DEFAULT_APPEARANCE.useNativeMacWindowControls,
|
||||
customUIFontFamily: sanitizeFontFamilyInput(appearance.customUIFontFamily),
|
||||
customMonoFontFamily: sanitizeFontFamilyInput(appearance.customMonoFontFamily),
|
||||
showDataTableVerticalBorders:
|
||||
dataGridDisplaySettings.showDataTableVerticalBorders,
|
||||
dataTableDensity: dataGridDisplaySettings.dataTableDensity,
|
||||
@@ -2493,7 +2500,10 @@ export const useStore = create<AppState>()(
|
||||
setTheme: (theme) => set({ theme }),
|
||||
setAppearance: (appearance) =>
|
||||
set((state) => ({
|
||||
appearance: { ...state.appearance, ...appearance },
|
||||
appearance: sanitizeAppearance(
|
||||
{ ...state.appearance, ...appearance },
|
||||
PERSIST_VERSION,
|
||||
),
|
||||
})),
|
||||
setUiScale: (scale) => set({ uiScale: sanitizeUiScale(scale) }),
|
||||
setFontSize: (size) => set({ fontSize: sanitizeFontSize(size) }),
|
||||
@@ -2971,16 +2981,28 @@ export const useStore = create<AppState>()(
|
||||
persistedState,
|
||||
) as Partial<AppState>;
|
||||
const safeTabs = sanitizeQueryTabs(state.tabs);
|
||||
const persistedConnections =
|
||||
state.connections === undefined
|
||||
? currentState.connections
|
||||
: sanitizeConnections(state.connections);
|
||||
const persistedConnectionTags =
|
||||
state.connectionTags === undefined
|
||||
? currentState.connectionTags
|
||||
: sanitizeConnectionTags(state.connectionTags);
|
||||
const persistedSidebarRootOrder =
|
||||
state.sidebarRootOrder === undefined
|
||||
? currentState.sidebarRootOrder
|
||||
: resolveSidebarRootOrderTokens(
|
||||
state.sidebarRootOrder,
|
||||
persistedConnectionTags,
|
||||
persistedConnections,
|
||||
);
|
||||
return {
|
||||
...currentState,
|
||||
...state,
|
||||
connections: sanitizeConnections(state.connections),
|
||||
connectionTags: sanitizeConnectionTags(state.connectionTags),
|
||||
sidebarRootOrder: resolveSidebarRootOrderTokens(
|
||||
state.sidebarRootOrder,
|
||||
sanitizeConnectionTags(state.connectionTags),
|
||||
sanitizeConnections(state.connections),
|
||||
),
|
||||
connections: persistedConnections,
|
||||
connectionTags: persistedConnectionTags,
|
||||
sidebarRootOrder: persistedSidebarRootOrder,
|
||||
tabs: safeTabs,
|
||||
activeTabId: sanitizeActiveTabId(state.activeTabId, safeTabs),
|
||||
savedQueries: sanitizeSavedQueries(state.savedQueries),
|
||||
|
||||
316
frontend/src/utils/fontFamilies.ts
Normal file
316
frontend/src/utils/fontFamilies.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
export const DEFAULT_UI_FONT_FAMILY =
|
||||
'"Inter", "PingFang SC", -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Segoe UI", sans-serif';
|
||||
export const DEFAULT_MONO_FONT_FAMILY =
|
||||
'"JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace';
|
||||
|
||||
const MAX_FONT_FAMILY_LENGTH = 512;
|
||||
|
||||
export type FontFamilyOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
keywords?: string[];
|
||||
};
|
||||
|
||||
export type InstalledFontFamily = {
|
||||
family: string;
|
||||
path?: string;
|
||||
};
|
||||
|
||||
const UI_FONT_FALLBACK_STACK =
|
||||
'-apple-system, BlinkMacSystemFont, "Helvetica Neue", "Segoe UI", "PingFang SC", sans-serif';
|
||||
const MONO_FONT_FALLBACK_STACK =
|
||||
'ui-monospace, "SF Mono", Menlo, Consolas, monospace';
|
||||
|
||||
const MONO_FONT_PRIORITY_HINTS = [
|
||||
'mono',
|
||||
'code',
|
||||
'console',
|
||||
'terminal',
|
||||
'jetbrains',
|
||||
'cascadia',
|
||||
'consolas',
|
||||
'courier',
|
||||
'fira',
|
||||
'hack',
|
||||
'iosevka',
|
||||
'menlo',
|
||||
'monaco',
|
||||
'operator',
|
||||
'sarasa',
|
||||
'sf mono',
|
||||
'source code',
|
||||
'ubuntu mono',
|
||||
];
|
||||
|
||||
const normalizeFontSearchToken = (value: string): string => String(value || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fff]+/gi, '');
|
||||
|
||||
const insertFontNameWordBreaks = (value: string): string => value
|
||||
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
||||
.replace(/([a-z\d])([A-Z])/g, '$1 $2');
|
||||
|
||||
const WINDOWS_UI_FONTS: FontFamilyOption[] = [
|
||||
{ value: '"Segoe UI", "Microsoft YaHei", sans-serif', label: 'Segoe UI', keywords: ['windows', 'microsoft yahei', '雅黑'] },
|
||||
{ value: '"Microsoft YaHei", "Segoe UI", sans-serif', label: 'Microsoft YaHei', keywords: ['windows', '雅黑'] },
|
||||
{ value: '"Microsoft JhengHei", "Segoe UI", sans-serif', label: 'Microsoft JhengHei', keywords: ['windows', '繁中'] },
|
||||
{ value: '"SimSun", serif', label: 'SimSun', keywords: ['windows', '宋体'] },
|
||||
{ value: '"SimHei", sans-serif', label: 'SimHei', keywords: ['windows', '黑体'] },
|
||||
{ value: '"Arial", "Segoe UI", sans-serif', label: 'Arial', keywords: ['windows'] },
|
||||
{ value: '"Tahoma", "Segoe UI", sans-serif', label: 'Tahoma', keywords: ['windows'] },
|
||||
];
|
||||
|
||||
const WINDOWS_MONO_FONTS: FontFamilyOption[] = [
|
||||
{ value: '"JetBrains Mono", Consolas, monospace', label: 'JetBrains Mono', keywords: ['windows', 'code'] },
|
||||
{ value: '"Cascadia Code", Consolas, monospace', label: 'Cascadia Code', keywords: ['windows', 'terminal'] },
|
||||
{ value: '"Consolas", "Cascadia Mono", monospace', label: 'Consolas', keywords: ['windows'] },
|
||||
{ value: '"Courier New", monospace', label: 'Courier New', keywords: ['windows'] },
|
||||
];
|
||||
|
||||
const MAC_UI_FONTS: FontFamilyOption[] = [
|
||||
{ value: '"Inter", "PingFang SC", -apple-system, sans-serif', label: 'Inter', keywords: ['mac', 'default'] },
|
||||
{ value: '"PingFang SC", -apple-system, sans-serif', label: 'PingFang SC', keywords: ['mac', '苹方'] },
|
||||
{ value: '"Helvetica Neue", -apple-system, sans-serif', label: 'Helvetica Neue', keywords: ['mac'] },
|
||||
{ value: '"Hiragino Sans GB", -apple-system, sans-serif', label: 'Hiragino Sans GB', keywords: ['mac', '冬青黑体'] },
|
||||
{ value: '"Songti SC", serif', label: 'Songti SC', keywords: ['mac', '宋体'] },
|
||||
];
|
||||
|
||||
const MAC_MONO_FONTS: FontFamilyOption[] = [
|
||||
{ value: '"JetBrains Mono", "SF Mono", Menlo, monospace', label: 'JetBrains Mono', keywords: ['mac', 'code'] },
|
||||
{ value: '"SF Mono", Menlo, monospace', label: 'SF Mono', keywords: ['mac', 'system mono'] },
|
||||
{ value: '"Menlo", "SF Mono", monospace', label: 'Menlo', keywords: ['mac'] },
|
||||
{ value: '"Monaco", "SF Mono", monospace', label: 'Monaco', keywords: ['mac'] },
|
||||
];
|
||||
|
||||
const LINUX_UI_FONTS: FontFamilyOption[] = [
|
||||
{ value: '"Noto Sans", "Noto Sans CJK SC", sans-serif', label: 'Noto Sans', keywords: ['linux', 'default'] },
|
||||
{ value: '"Ubuntu", "Noto Sans", sans-serif', label: 'Ubuntu', keywords: ['linux', 'ubuntu'] },
|
||||
{ value: '"DejaVu Sans", "Noto Sans", sans-serif', label: 'DejaVu Sans', keywords: ['linux'] },
|
||||
{ value: '"Liberation Sans", "Noto Sans", sans-serif', label: 'Liberation Sans', keywords: ['linux'] },
|
||||
{ value: '"WenQuanYi Micro Hei", "Noto Sans CJK SC", sans-serif', label: 'WenQuanYi Micro Hei', keywords: ['linux', '文泉驿'] },
|
||||
];
|
||||
|
||||
const LINUX_MONO_FONTS: FontFamilyOption[] = [
|
||||
{ value: '"JetBrains Mono", "DejaVu Sans Mono", monospace', label: 'JetBrains Mono', keywords: ['linux', 'code'] },
|
||||
{ value: '"Ubuntu Mono", "DejaVu Sans Mono", monospace', label: 'Ubuntu Mono', keywords: ['linux', 'ubuntu'] },
|
||||
{ value: '"DejaVu Sans Mono", monospace', label: 'DejaVu Sans Mono', keywords: ['linux'] },
|
||||
{ value: '"Liberation Mono", monospace', label: 'Liberation Mono', keywords: ['linux'] },
|
||||
];
|
||||
|
||||
const SHARED_UI_FONTS: FontFamilyOption[] = [
|
||||
{ value: DEFAULT_UI_FONT_FAMILY, label: '默认 UI 字体', keywords: ['default', 'system'] },
|
||||
{ value: '"Inter", sans-serif', label: 'Inter', keywords: ['shared'] },
|
||||
{ value: '"PingFang SC", sans-serif', label: 'PingFang SC', keywords: ['shared', '苹方'] },
|
||||
{ value: '"Microsoft YaHei", sans-serif', label: 'Microsoft YaHei', keywords: ['shared', '雅黑'] },
|
||||
{ value: '"Noto Sans CJK SC", sans-serif', label: 'Noto Sans CJK SC', keywords: ['shared', 'noto'] },
|
||||
];
|
||||
|
||||
const SHARED_MONO_FONTS: FontFamilyOption[] = [
|
||||
{ value: DEFAULT_MONO_FONT_FAMILY, label: '默认代码字体', keywords: ['default', 'system', 'mono'] },
|
||||
{ value: '"JetBrains Mono", monospace', label: 'JetBrains Mono', keywords: ['shared'] },
|
||||
{ value: '"Cascadia Code", monospace', label: 'Cascadia Code', keywords: ['shared'] },
|
||||
{ value: '"Fira Code", monospace', label: 'Fira Code', keywords: ['shared'] },
|
||||
{ value: '"Source Code Pro", monospace', label: 'Source Code Pro', keywords: ['shared'] },
|
||||
];
|
||||
|
||||
const normalizeFontOptionLabel = (label: string): string => sanitizeFontFamilyInput(label)?.toLowerCase() || '';
|
||||
|
||||
const dedupeFontOptions = (options: FontFamilyOption[]): FontFamilyOption[] => {
|
||||
const seenLabels = new Set<string>();
|
||||
const seenValues = new Set<string>();
|
||||
const result: FontFamilyOption[] = [];
|
||||
options.forEach((option) => {
|
||||
const normalizedValue = sanitizeFontFamilyInput(option.value);
|
||||
const normalizedLabel = normalizeFontOptionLabel(option.label);
|
||||
if (!normalizedValue) {
|
||||
return;
|
||||
}
|
||||
if ((normalizedLabel && seenLabels.has(normalizedLabel)) || seenValues.has(normalizedValue)) {
|
||||
return;
|
||||
}
|
||||
if (normalizedLabel) {
|
||||
seenLabels.add(normalizedLabel);
|
||||
}
|
||||
seenValues.add(normalizedValue);
|
||||
result.push({
|
||||
value: normalizedValue,
|
||||
label: option.label,
|
||||
keywords: option.keywords,
|
||||
});
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const sortFontOptions = (options: FontFamilyOption[]): FontFamilyOption[] => {
|
||||
const defaultOptions: FontFamilyOption[] = [];
|
||||
const regularOptions: FontFamilyOption[] = [];
|
||||
options.forEach((option) => {
|
||||
if (option.label.startsWith('默认')) {
|
||||
defaultOptions.push(option);
|
||||
return;
|
||||
}
|
||||
regularOptions.push(option);
|
||||
});
|
||||
regularOptions.sort((left, right) => left.label.localeCompare(right.label, undefined, {
|
||||
sensitivity: 'base',
|
||||
numeric: true,
|
||||
}));
|
||||
return [...defaultOptions, ...regularOptions];
|
||||
};
|
||||
|
||||
const escapeFontFamilyName = (value: string): string => value
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"');
|
||||
|
||||
const formatInstalledFontLabel = (family: string): string => sanitizeFontFamilyInput(
|
||||
insertFontNameWordBreaks(family),
|
||||
) || family;
|
||||
|
||||
const isInstalledMonoCandidate = (family: string): boolean => scoreInstalledMonoFamily(family) > 0;
|
||||
|
||||
const normalizeInstalledFontFamilyName = (entry: string | InstalledFontFamily): string | null => {
|
||||
const family = typeof entry === 'string' ? entry : entry.family;
|
||||
return sanitizeFontFamilyInput(family);
|
||||
};
|
||||
|
||||
const buildInstalledFontKeywords = (family: string): string[] => {
|
||||
const label = formatInstalledFontLabel(family);
|
||||
const tokens = family
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9\u4e00-\u9fff]+/i)
|
||||
.filter(Boolean);
|
||||
const labelTokens = label
|
||||
.toLowerCase()
|
||||
.split(/[^a-z0-9\u4e00-\u9fff]+/i)
|
||||
.filter(Boolean);
|
||||
return Array.from(new Set([
|
||||
...tokens,
|
||||
...labelTokens,
|
||||
family.toLowerCase(),
|
||||
label.toLowerCase(),
|
||||
normalizeFontSearchToken(family),
|
||||
normalizeFontSearchToken(label),
|
||||
].filter(Boolean)));
|
||||
};
|
||||
|
||||
const buildInstalledFontValue = (family: string, kind: 'ui' | 'mono'): string => {
|
||||
const fallback = kind === 'ui' ? UI_FONT_FALLBACK_STACK : MONO_FONT_FALLBACK_STACK;
|
||||
return `"${escapeFontFamilyName(family)}", ${fallback}`;
|
||||
};
|
||||
|
||||
const scoreInstalledMonoFamily = (family: string): number => {
|
||||
const normalized = family.toLowerCase();
|
||||
return MONO_FONT_PRIORITY_HINTS.reduce((score, hint) => (
|
||||
normalized.includes(hint) ? score + 10 : score
|
||||
), 0);
|
||||
};
|
||||
|
||||
const buildInstalledFontOptions = (
|
||||
installedFamilies: Array<string | InstalledFontFamily>,
|
||||
kind: 'ui' | 'mono',
|
||||
): FontFamilyOption[] => {
|
||||
const familyNames: string[] = [];
|
||||
const seenFamilies = new Set<string>();
|
||||
|
||||
installedFamilies.forEach((entry) => {
|
||||
const family = normalizeInstalledFontFamilyName(entry);
|
||||
if (!family) {
|
||||
return;
|
||||
}
|
||||
if (kind === 'mono' && !isInstalledMonoCandidate(family)) {
|
||||
return;
|
||||
}
|
||||
const dedupeKey = family.toLowerCase();
|
||||
if (seenFamilies.has(dedupeKey)) {
|
||||
return;
|
||||
}
|
||||
seenFamilies.add(dedupeKey);
|
||||
familyNames.push(family);
|
||||
});
|
||||
|
||||
familyNames.sort((left, right) => {
|
||||
if (kind === 'mono') {
|
||||
const scoreDiff = scoreInstalledMonoFamily(right) - scoreInstalledMonoFamily(left);
|
||||
if (scoreDiff !== 0) {
|
||||
return scoreDiff;
|
||||
}
|
||||
}
|
||||
return left.localeCompare(right, undefined, { sensitivity: 'base' });
|
||||
});
|
||||
|
||||
return familyNames.map((family) => ({
|
||||
value: buildInstalledFontValue(family, kind),
|
||||
label: formatInstalledFontLabel(family),
|
||||
keywords: ['installed', ...buildInstalledFontKeywords(family)],
|
||||
}));
|
||||
};
|
||||
|
||||
export const sanitizeFontFamilyInput = (value: unknown): string | null => {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const normalized = value.replace(/\s+/g, " ").trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
return normalized.slice(0, MAX_FONT_FAMILY_LENGTH);
|
||||
};
|
||||
|
||||
export const resolveUIFontFamily = (customValue: unknown): string => {
|
||||
return sanitizeFontFamilyInput(customValue) ?? DEFAULT_UI_FONT_FAMILY;
|
||||
};
|
||||
|
||||
export const resolveMonoFontFamily = (customValue: unknown): string => {
|
||||
return sanitizeFontFamilyInput(customValue) ?? DEFAULT_MONO_FONT_FAMILY;
|
||||
};
|
||||
|
||||
export const getPlatformFontFamilyOptions = (
|
||||
platform: string,
|
||||
kind: "ui" | "mono",
|
||||
): FontFamilyOption[] => {
|
||||
const normalizedPlatform = String(platform || "").toLowerCase();
|
||||
const platformOptions =
|
||||
normalizedPlatform === "windows"
|
||||
? (kind === "ui" ? WINDOWS_UI_FONTS : WINDOWS_MONO_FONTS)
|
||||
: normalizedPlatform === "darwin"
|
||||
? (kind === "ui" ? MAC_UI_FONTS : MAC_MONO_FONTS)
|
||||
: normalizedPlatform === "linux"
|
||||
? (kind === "ui" ? LINUX_UI_FONTS : LINUX_MONO_FONTS)
|
||||
: [];
|
||||
return sortFontOptions(dedupeFontOptions([
|
||||
...platformOptions,
|
||||
...(kind === "ui" ? SHARED_UI_FONTS : SHARED_MONO_FONTS),
|
||||
]));
|
||||
};
|
||||
|
||||
export const buildFontFamilyOptions = (
|
||||
platform: string,
|
||||
kind: 'ui' | 'mono',
|
||||
installedFamilies: Array<string | InstalledFontFamily>,
|
||||
): FontFamilyOption[] => {
|
||||
return sortFontOptions(dedupeFontOptions([
|
||||
...buildInstalledFontOptions(installedFamilies, kind),
|
||||
...getPlatformFontFamilyOptions(platform, kind),
|
||||
]));
|
||||
};
|
||||
|
||||
export const matchFontFamilyOption = (
|
||||
input: string,
|
||||
option?: FontFamilyOption,
|
||||
): boolean => {
|
||||
const normalizedInput = String(input || "").trim().toLowerCase();
|
||||
const compactInput = normalizeFontSearchToken(normalizedInput);
|
||||
if (!normalizedInput) {
|
||||
return true;
|
||||
}
|
||||
if (!option) {
|
||||
return false;
|
||||
}
|
||||
return [option.label, option.value, ...(option.keywords || [])].some((entry) => {
|
||||
const text = String(entry || '').toLowerCase();
|
||||
if (text.includes(normalizedInput)) {
|
||||
return true;
|
||||
}
|
||||
return compactInput ? normalizeFontSearchToken(text).includes(compactInput) : false;
|
||||
});
|
||||
};
|
||||
2
frontend/wailsjs/go/app/App.d.ts
vendored
2
frontend/wailsjs/go/app/App.d.ts
vendored
@@ -158,6 +158,8 @@ export function JVMStartMonitoring(arg1:connection.ConnectionConfig):Promise<con
|
||||
|
||||
export function JVMStopMonitoring(arg1:connection.ConnectionConfig,arg2:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function ListInstalledFontFamilies():Promise<connection.QueryResult>;
|
||||
|
||||
export function ListSQLDirectory(arg1:string):Promise<connection.QueryResult>;
|
||||
|
||||
export function LogWindowDiagnostic(arg1:string,arg2:string):Promise<void>;
|
||||
|
||||
@@ -306,6 +306,10 @@ export function JVMStopMonitoring(arg1, arg2) {
|
||||
return window['go']['app']['App']['JVMStopMonitoring'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function ListInstalledFontFamilies() {
|
||||
return window['go']['app']['App']['ListInstalledFontFamilies']();
|
||||
}
|
||||
|
||||
export function ListSQLDirectory(arg1) {
|
||||
return window['go']['app']['App']['ListSQLDirectory'](arg1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user