diff --git a/src/main/features/core/settings/index.ts b/src/main/features/core/settings/index.ts index c48f7a806..cf49d64c2 100644 --- a/src/main/features/core/settings/index.ts +++ b/src/main/features/core/settings/index.ts @@ -141,6 +141,14 @@ ipcMain.on('settings-set', (__event, data: { property: string; value: any }) => } }); +ipcMain.handle('settings-set-sync', (__event, data: { property: string; value: any }) => { + if (data.value === null) { + store.delete(data.property); + } else { + store.set(data.property, data.value); + } +}); + ipcMain.handle('password-get', (_event, server: string): null | string => { if (safeStorage.isEncryptionAvailable()) { const servers = store.get('server') as Record | undefined; diff --git a/src/main/index.ts b/src/main/index.ts index 4420f51b7..7533c1575 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -252,7 +252,9 @@ function createAlphaUpdaterInstance(): AppImageUpdater | MacUpdater | NsisUpdate return new NsisUpdater(ALPHA_UPDATER_CONFIG); } -protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]); +protocol.registerSchemesAsPrivileged([ + { privileges: { bypassCSP: true, corsEnabled: true }, scheme: 'feishin' }, +]); process.on('uncaughtException', (error: any) => { console.error('Error in main process', error); @@ -989,14 +991,33 @@ app.on('window-all-closed', () => { } }); -const FONT_HEADERS = [ +const FONT_HEADERS = new Set([ 'font/collection', 'font/otf', 'font/sfnt', 'font/ttf', 'font/woff', 'font/woff2', -]; +]); + +const bytesToInt = (array: Uint8Array, length: number): number => { + let value = 0; + for (let i = 0; i < length; i++) { + value = (value << 8) + array[i]; + } + + return value; +}; + +const FONT_FOUR_BYTE_MAGIC_NUMBERS = new Set([ + 0x4f54544f, // font/otf + 0x774f4632, // font/woff2 + 0x774f4646, // font/woff +]); + +const FONT_FIVE_BYTE_MAGIC_NUMBERS = new Set([ + 0x0001000000, // ttf, collection, sfnt +]); const singleInstance = isDevelopment ? true : app.requestSingleInstanceLock(); @@ -1017,12 +1038,9 @@ if (!singleInstance) { app.whenReady() .then(() => { - protocol.handle('feishin', async (request) => { - const filePath = `file:${request.url.slice('feishin:'.length)}`; - const response = await net.fetch(filePath); - const contentType = response.headers.get('content-type'); - - if (!contentType || !FONT_HEADERS.includes(contentType)) { + protocol.handle('feishin', async () => { + const filePath = store.get('local_font_path'); + if (typeof filePath !== 'string') { getMainWindow()?.webContents.send('custom-font-error', filePath); return new Response(null, { @@ -1031,7 +1049,38 @@ if (!singleInstance) { }); } - return response; + const response = await net.fetch('file:' + filePath); + const contentType = response.headers.get('content-type'); + + // On Linux, the mime type is included in the response header + // In this case, we can forward the response with no further processing + if (contentType && FONT_HEADERS.has(contentType)) { + return response; + } + + // Otherwise, let's check the magic number to see if + // the file is a font type. This is either four or five bytes + const payload = await response.arrayBuffer(); + const magicNumber = new Uint8Array(payload.slice(0, 5)); + const fiveHex = bytesToInt(magicNumber, 5); + const fourHex = bytesToInt(magicNumber, 4); + + if ( + FONT_FIVE_BYTE_MAGIC_NUMBERS.has(fiveHex) || + FONT_FOUR_BYTE_MAGIC_NUMBERS.has(fourHex) + ) { + // We have to create a new response with the payload, since it has been read now + return new Response(payload, { + headers: response.headers, + }); + } + + getMainWindow()?.webContents.send('custom-font-error', filePath); + + return new Response(null, { + status: 403, + statusText: 'Forbidden', + }); }); session.defaultSession.webRequest.onHeadersReceived((details, callback) => { diff --git a/src/preload/local-settings.ts b/src/preload/local-settings.ts index 92aaca5eb..2715ca987 100644 --- a/src/preload/local-settings.ts +++ b/src/preload/local-settings.ts @@ -9,6 +9,13 @@ const set = ( ipcRenderer.send('settings-set', { property, value }); }; +const setSync = async ( + property: string, + value: boolean | null | Record | string | string[], +) => { + return ipcRenderer.invoke('settings-set-sync', { property, value }); +}; + const get = async (property: string) => { return ipcRenderer.invoke('settings-get', { property }); }; @@ -99,6 +106,7 @@ export const localSettings = { passwordSet, restart, set, + setSync, setZoomFactor, themeSet, }; diff --git a/src/preload/utils.ts b/src/preload/utils.ts index 0a0f67ed8..dae22176e 100644 --- a/src/preload/utils.ts +++ b/src/preload/utils.ts @@ -139,6 +139,7 @@ export const utils = { rendererToggleSidebar, rendererUpdateAvailable, saveCustomCss, + separator: isWindows() ? '\\' : '/', setInputFocused, startPowerSaveBlocker, stopPowerSaveBlocker, diff --git a/src/renderer/features/settings/components/general/application-settings.tsx b/src/renderer/features/settings/components/general/application-settings.tsx index 941b3690b..1a6ecffdb 100644 --- a/src/renderer/features/settings/components/general/application-settings.tsx +++ b/src/renderer/features/settings/components/general/application-settings.tsx @@ -36,6 +36,7 @@ import { FontType } from '/@/shared/types/types'; const localSettings = isElectron() ? window.api.localSettings : null; const ipc = isElectron() ? window.api.ipc : null; +const utils = isElectron() ? window.api.utils : null; // Electron 32+ removed file.path, use this which is exposed in preload to get real path const getPathForFile = isElectron() ? window.api.getPathForFile : null; @@ -289,21 +290,29 @@ export const ApplicationSettings = memo(() => { control: ( + clearable + defaultValue={ + fontSettings.custom + ? new File([], fontSettings.custom.split(utils?.separator || '').pop()!) + : null + } + onChange={async (e) => { + const custom = e ? getPathForFile?.(e) || null : null; + await localSettings?.setSync('local_font_path', custom); setSettings({ font: { ...fontSettings, - custom: e ? getPathForFile?.(e) || null : null, + custom, }, - }) - } + }); + }} w={300} /> ), description: t('setting.customFontPath', { context: 'description', }), - isHidden: fontSettings.type !== FontType.CUSTOM, + isHidden: !isElectron() || fontSettings.type !== FontType.CUSTOM, title: t('setting.customFontPath'), }, { diff --git a/src/renderer/hooks/use-sync-settings-to-main.ts b/src/renderer/hooks/use-sync-settings-to-main.ts index f76e4b829..52e754528 100644 --- a/src/renderer/hooks/use-sync-settings-to-main.ts +++ b/src/renderer/hooks/use-sync-settings-to-main.ts @@ -32,6 +32,7 @@ export const useSyncSettingsToMain = () => { const settingsFromStore = useSettingsStore.getState(); const settings = { + font: settingsFromStore.font, general: settingsFromStore.general, hotkeys: settingsFromStore.hotkeys, lyrics: settingsFromStore.lyrics, @@ -101,6 +102,10 @@ export const useSyncSettingsToMain = () => { mainStoreKey: 'enableNeteaseTranslation', rendererValue: settings.lyrics.enableNeteaseTranslation, }, + { + mainStoreKey: 'local_font_path', + rendererValue: settings.font.custom, + }, ]; // Compare and sync each setting diff --git a/src/renderer/themes/use-app-theme.ts b/src/renderer/themes/use-app-theme.ts index 1157c610a..af7f26e45 100644 --- a/src/renderer/themes/use-app-theme.ts +++ b/src/renderer/themes/use-app-theme.ts @@ -134,6 +134,11 @@ export const useAppTheme = (overrideTheme?: AppTheme) => { document.body.appendChild(textStyleRef.current); } + // Note: we change the url to bust caches when changing the path + // The url provided here does NOT matter, validation is done + // on the main process. Any feishin:/ url will fetch the same + // item, which the renderer will check via magic number to be + // some font item textStyleRef.current.textContent = ` @font-face { font-family: "dynamic-font";