[feature]: Support using system fonts (#304)
* [feature]: Support using system fonts Uses the **experimental** queryLocalFonts API, when prompted, to get the fonts and do CSS. Resolves #270 and #288 (by proxy) Caveats/notes: - This is experimental, and is only supported by Chrome/Chromium/Edgeium (see https://caniuse.com/?search=querylocalfonts) - As far as I can tell, the only way to dynamically change the font (shown in https://wicg.github.io/local-font-access/#example-style-with-local-fonts) was by DOM manipulation; css variables did not seem to work - This shows **all** fonts, including their variants (bold/italic/etc); given that the style names could be localized, not sure of a way to parse this (on my system, for instance, I had 859 different combinations) - I made fonts a separate top-level setting because it was easier to manipulate without causing as many rerenders; feel free to put that back * add permission chec * add electron magic to support custom font * restrict content types
This commit is contained in:
parent
e6ed9229c2
commit
74cab01013
8 changed files with 282 additions and 20 deletions
|
@ -21,6 +21,8 @@ import {
|
||||||
Menu,
|
Menu,
|
||||||
nativeImage,
|
nativeImage,
|
||||||
BrowserWindowConstructorOptions,
|
BrowserWindowConstructorOptions,
|
||||||
|
protocol,
|
||||||
|
net,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import electronLocalShortcut from 'electron-localshortcut';
|
import electronLocalShortcut from 'electron-localshortcut';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
@ -43,6 +45,8 @@ export default class AppUpdater {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'feishin' }]);
|
||||||
|
|
||||||
process.on('uncaughtException', (error: any) => {
|
process.on('uncaughtException', (error: any) => {
|
||||||
console.log('Error in main process', error);
|
console.log('Error in main process', error);
|
||||||
});
|
});
|
||||||
|
@ -653,8 +657,34 @@ app.on('window-all-closed', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const FONT_HEADERS = [
|
||||||
|
'font/collection',
|
||||||
|
'font/otf',
|
||||||
|
'font/sfnt',
|
||||||
|
'font/ttf',
|
||||||
|
'font/woff',
|
||||||
|
'font/woff2',
|
||||||
|
];
|
||||||
|
|
||||||
app.whenReady()
|
app.whenReady()
|
||||||
.then(() => {
|
.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)) {
|
||||||
|
getMainWindow()?.webContents.send('custom-font-error', filePath);
|
||||||
|
|
||||||
|
return new Response(null, {
|
||||||
|
status: 403,
|
||||||
|
statusText: 'Forbidden',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
createTray();
|
createTray();
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ipcRenderer, webFrame } from 'electron';
|
import { IpcRendererEvent, ipcRenderer, webFrame } from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
|
|
||||||
const store = new Store();
|
const store = new Store();
|
||||||
|
@ -39,9 +39,14 @@ const setZoomFactor = (zoomFactor: number) => {
|
||||||
webFrame.setZoomFactor(zoomFactor / 100);
|
webFrame.setZoomFactor(zoomFactor / 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fontError = (cb: (event: IpcRendererEvent, file: string) => void) => {
|
||||||
|
ipcRenderer.on('custom-font-error', cb);
|
||||||
|
};
|
||||||
|
|
||||||
export const localSettings = {
|
export const localSettings = {
|
||||||
disableMediaKeys,
|
disableMediaKeys,
|
||||||
enableMediaKeys,
|
enableMediaKeys,
|
||||||
|
fontError,
|
||||||
get,
|
get,
|
||||||
passwordGet,
|
passwordGet,
|
||||||
passwordRemove,
|
passwordRemove,
|
||||||
|
|
|
@ -1130,3 +1130,12 @@ export enum LyricSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LyricsOverride = Omit<FullLyricsMetadata, 'lyrics'> & { id: string };
|
export type LyricsOverride = Omit<FullLyricsMetadata, 'lyrics'> & { id: string };
|
||||||
|
|
||||||
|
// This type from https://wicg.github.io/local-font-access/#fontdata
|
||||||
|
// NOTE: it is still experimental, so this should be updates as appropriate
|
||||||
|
export type FontData = {
|
||||||
|
family: string;
|
||||||
|
fullName: string;
|
||||||
|
postscriptName: string;
|
||||||
|
style: string;
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
|
import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-model';
|
||||||
import { ModuleRegistry } from '@ag-grid-community/core';
|
import { ModuleRegistry } from '@ag-grid-community/core';
|
||||||
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
|
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
|
||||||
|
@ -23,7 +23,7 @@ import { PlayQueueHandlerContext } from '/@/renderer/features/player';
|
||||||
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists';
|
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists';
|
||||||
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
||||||
import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store';
|
import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store';
|
||||||
import { PlaybackType, PlayerStatus } from '/@/renderer/types';
|
import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types';
|
||||||
import '@ag-grid-community/styles/ag-grid.css';
|
import '@ag-grid-community/styles/ag-grid.css';
|
||||||
|
|
||||||
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
|
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
|
||||||
|
@ -37,17 +37,48 @@ const remote = isElectron() ? window.electron.remote : null;
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const contentFont = useSettingsStore((state) => state.general.fontContent);
|
const { builtIn, custom, system, type } = useSettingsStore((state) => state.font);
|
||||||
const { type: playbackType } = usePlaybackSettings();
|
const { type: playbackType } = usePlaybackSettings();
|
||||||
const { bindings } = useHotkeySettings();
|
const { bindings } = useHotkeySettings();
|
||||||
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
||||||
const { clearQueue, restoreQueue } = useQueueControls();
|
const { clearQueue, restoreQueue } = useQueueControls();
|
||||||
const remoteSettings = useRemoteSettings();
|
const remoteSettings = useRemoteSettings();
|
||||||
|
const textStyleRef = useRef<HTMLStyleElement>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (type === FontType.SYSTEM && system) {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
root.style.setProperty('--content-font-family', contentFont);
|
root.style.setProperty('--content-font-family', 'dynamic-font');
|
||||||
}, [contentFont]);
|
|
||||||
|
if (!textStyleRef.current) {
|
||||||
|
textStyleRef.current = document.createElement('style');
|
||||||
|
document.body.appendChild(textStyleRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
textStyleRef.current.textContent = `
|
||||||
|
@font-face {
|
||||||
|
font-family: "dynamic-font";
|
||||||
|
src: local("${system}");
|
||||||
|
}`;
|
||||||
|
} else if (type === FontType.CUSTOM && custom) {
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.style.setProperty('--content-font-family', 'dynamic-font');
|
||||||
|
|
||||||
|
if (!textStyleRef.current) {
|
||||||
|
textStyleRef.current = document.createElement('style');
|
||||||
|
document.body.appendChild(textStyleRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
textStyleRef.current.textContent = `
|
||||||
|
@font-face {
|
||||||
|
font-family: "dynamic-font";
|
||||||
|
src: url("feishin://${custom}");
|
||||||
|
}`;
|
||||||
|
} else {
|
||||||
|
const root = document.documentElement;
|
||||||
|
root.style.setProperty('--content-font-family', builtIn);
|
||||||
|
}
|
||||||
|
}, [builtIn, custom, system, type]);
|
||||||
|
|
||||||
const providerValue = useMemo(() => {
|
const providerValue = useMemo(() => {
|
||||||
return { handlePlayQueueAdd };
|
return { handlePlayQueueAdd };
|
||||||
|
|
|
@ -1,14 +1,27 @@
|
||||||
|
import type { IpcRendererEvent } from 'electron';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { NumberInput, Select } from '/@/renderer/components';
|
import { FileInput, NumberInput, Select, toast } from '/@/renderer/components';
|
||||||
import {
|
import {
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
SettingOption,
|
SettingOption,
|
||||||
} from '/@/renderer/features/settings/components/settings-section';
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
import { useGeneralSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
import {
|
||||||
|
useFontSettings,
|
||||||
|
useGeneralSettings,
|
||||||
|
useSettingsStoreActions,
|
||||||
|
} from '/@/renderer/store/settings.store';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { FontType } from '/@/renderer/types';
|
||||||
|
|
||||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||||
|
const ipc = isElectron() ? window.electron.ipc : null;
|
||||||
|
|
||||||
const FONT_OPTIONS = [
|
type Font = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FONT_OPTIONS: Font[] = [
|
||||||
{ label: 'Archivo', value: 'Archivo' },
|
{ label: 'Archivo', value: 'Archivo' },
|
||||||
{ label: 'Fredoka', value: 'Fredoka' },
|
{ label: 'Fredoka', value: 'Fredoka' },
|
||||||
{ label: 'Inter', value: 'Inter' },
|
{ label: 'Inter', value: 'Inter' },
|
||||||
|
@ -20,9 +33,99 @@ const FONT_OPTIONS = [
|
||||||
{ label: 'Work Sans', value: 'Work Sans' },
|
{ label: 'Work Sans', value: 'Work Sans' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const FONT_TYPES: Font[] = [{ label: 'Built-in font', value: FontType.BUILT_IN }];
|
||||||
|
|
||||||
|
if (window.queryLocalFonts) {
|
||||||
|
FONT_TYPES.push({ label: 'System font', value: FontType.SYSTEM });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isElectron()) {
|
||||||
|
FONT_TYPES.push({ label: 'Custom font', value: FontType.CUSTOM });
|
||||||
|
}
|
||||||
|
|
||||||
export const ApplicationSettings = () => {
|
export const ApplicationSettings = () => {
|
||||||
const settings = useGeneralSettings();
|
const settings = useGeneralSettings();
|
||||||
|
const fontSettings = useFontSettings();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
const [localFonts, setLocalFonts] = useState<Font[]>([]);
|
||||||
|
|
||||||
|
const fontList = useMemo(() => {
|
||||||
|
if (fontSettings.custom) {
|
||||||
|
const newFile = new File([], fontSettings.custom.split(/(\\|\/)/g).pop()!);
|
||||||
|
newFile.path = fontSettings.custom;
|
||||||
|
return newFile;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [fontSettings.custom]);
|
||||||
|
|
||||||
|
const onFontError = useCallback(
|
||||||
|
(_: IpcRendererEvent, file: string) => {
|
||||||
|
toast.error({
|
||||||
|
message: `${file} is not a valid font file`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setSettings({
|
||||||
|
font: {
|
||||||
|
...fontSettings,
|
||||||
|
custom: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[fontSettings, setSettings],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (localSettings) {
|
||||||
|
localSettings.fontError(onFontError);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
ipc!.removeAllListeners('custom-font-error');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
}, [onFontError]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getFonts = async () => {
|
||||||
|
if (
|
||||||
|
fontSettings.type === FontType.SYSTEM &&
|
||||||
|
localFonts.length === 0 &&
|
||||||
|
window.queryLocalFonts
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// WARNING (Oct 17 2023): while this query is valid for chromium-based
|
||||||
|
// browsers, it is still experimental, and so Typescript will complain
|
||||||
|
// @ts-ignore
|
||||||
|
const status = await navigator.permissions.query({ name: 'local-fonts' });
|
||||||
|
|
||||||
|
if (status.state === 'denied') {
|
||||||
|
throw new Error('Access denied to local fonts');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await window.queryLocalFonts();
|
||||||
|
setLocalFonts(
|
||||||
|
data.map((font) => ({
|
||||||
|
label: font.fullName,
|
||||||
|
value: font.postscriptName,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error({
|
||||||
|
message: 'An error occurred when trying to get system fonts',
|
||||||
|
});
|
||||||
|
|
||||||
|
setSettings({
|
||||||
|
font: {
|
||||||
|
...fontSettings,
|
||||||
|
type: FontType.BUILT_IN,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getFonts();
|
||||||
|
}, [fontSettings, localFonts, setSettings]);
|
||||||
|
|
||||||
const options: SettingOption[] = [
|
const options: SettingOption[] = [
|
||||||
{
|
{
|
||||||
|
@ -39,24 +142,87 @@ export const ApplicationSettings = () => {
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Select
|
<Select
|
||||||
searchable
|
data={FONT_TYPES}
|
||||||
data={FONT_OPTIONS}
|
value={fontSettings.type}
|
||||||
defaultValue={settings.fontContent}
|
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (!e) return;
|
if (!e) return;
|
||||||
setSettings({
|
setSettings({
|
||||||
general: {
|
font: {
|
||||||
...settings,
|
...fontSettings,
|
||||||
fontContent: e,
|
type: e as FontType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description:
|
||||||
|
'What font to use. Built-in font selects one of the fonts provided by Feishin. System font allows you to select any font provided by your OS. Custom allows you to provide your own font',
|
||||||
|
isHidden: FONT_TYPES.length === 1,
|
||||||
|
title: 'Use system font',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
searchable
|
||||||
|
data={FONT_OPTIONS}
|
||||||
|
value={fontSettings.builtIn}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e) return;
|
||||||
|
setSettings({
|
||||||
|
font: {
|
||||||
|
...fontSettings,
|
||||||
|
builtIn: e,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
description: 'Sets the application content font',
|
description: 'Sets the application content font',
|
||||||
isHidden: false,
|
isHidden: localFonts && fontSettings.type !== FontType.BUILT_IN,
|
||||||
title: 'Font (Content)',
|
title: 'Font (Content)',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Select
|
||||||
|
searchable
|
||||||
|
data={localFonts}
|
||||||
|
value={fontSettings.system}
|
||||||
|
w={300}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e) return;
|
||||||
|
setSettings({
|
||||||
|
font: {
|
||||||
|
...fontSettings,
|
||||||
|
system: e,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Sets the application content font',
|
||||||
|
isHidden: !localFonts || fontSettings.type !== FontType.SYSTEM,
|
||||||
|
title: 'Font (Content)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<FileInput
|
||||||
|
accept=".ttc,.ttf,.otf,.woff,.woff2"
|
||||||
|
defaultValue={fontList}
|
||||||
|
w={300}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSettings({
|
||||||
|
font: {
|
||||||
|
...fontSettings,
|
||||||
|
custom: e?.path ?? null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: 'Path to custom font',
|
||||||
|
isHidden: fontSettings.type !== FontType.CUSTOM,
|
||||||
|
title: 'Path to custom font',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<NumberInput
|
<NumberInput
|
||||||
|
|
3
src/renderer/preload.d.ts
vendored
3
src/renderer/preload.d.ts
vendored
|
@ -1,6 +1,6 @@
|
||||||
import { IpcRendererEvent } from 'electron';
|
import { IpcRendererEvent } from 'electron';
|
||||||
import { PlayerData, PlayerState } from './store';
|
import { PlayerData, PlayerState } from './store';
|
||||||
import { InternetProviderLyricResponse, QueueSong } from '/@/renderer/api/types';
|
import { FontData, InternetProviderLyricResponse, QueueSong } from '/@/renderer/api/types';
|
||||||
import { Remote } from '/@/main/preload/remote';
|
import { Remote } from '/@/main/preload/remote';
|
||||||
import { Mpris } from '/@/main/preload/mpris';
|
import { Mpris } from '/@/main/preload/mpris';
|
||||||
import { MpvPLayer, MpvPlayerListener } from '/@/main/preload/mpv-player';
|
import { MpvPLayer, MpvPlayerListener } from '/@/main/preload/mpv-player';
|
||||||
|
@ -76,6 +76,7 @@ declare global {
|
||||||
remote?: Remote;
|
remote?: Remote;
|
||||||
utils?: Utils;
|
utils?: Utils;
|
||||||
};
|
};
|
||||||
|
queryLocalFonts?: () => Promise<FontData[]>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
PlaybackType,
|
PlaybackType,
|
||||||
TableType,
|
TableType,
|
||||||
Platform,
|
Platform,
|
||||||
|
FontType,
|
||||||
} from '/@/renderer/types';
|
} from '/@/renderer/types';
|
||||||
import { randomString } from '/@/renderer/utils';
|
import { randomString } from '/@/renderer/utils';
|
||||||
|
|
||||||
|
@ -111,10 +112,16 @@ export enum BindingActions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsState {
|
export interface SettingsState {
|
||||||
|
font: {
|
||||||
|
builtIn: string;
|
||||||
|
custom: string | null;
|
||||||
|
system: string | null;
|
||||||
|
type: FontType;
|
||||||
|
};
|
||||||
general: {
|
general: {
|
||||||
defaultFullPlaylist: boolean;
|
defaultFullPlaylist: boolean;
|
||||||
followSystemTheme: boolean;
|
followSystemTheme: boolean;
|
||||||
fontContent: string;
|
|
||||||
playButtonBehavior: Play;
|
playButtonBehavior: Play;
|
||||||
resume: boolean;
|
resume: boolean;
|
||||||
showQueueDrawerButton: boolean;
|
showQueueDrawerButton: boolean;
|
||||||
|
@ -208,10 +215,15 @@ const getPlatformDefaultWindowBarStyle = (): Platform => {
|
||||||
const platformDefaultWindowBarStyle: Platform = getPlatformDefaultWindowBarStyle();
|
const platformDefaultWindowBarStyle: Platform = getPlatformDefaultWindowBarStyle();
|
||||||
|
|
||||||
const initialState: SettingsState = {
|
const initialState: SettingsState = {
|
||||||
|
font: {
|
||||||
|
builtIn: 'Inter',
|
||||||
|
custom: null,
|
||||||
|
system: null,
|
||||||
|
type: FontType.BUILT_IN,
|
||||||
|
},
|
||||||
general: {
|
general: {
|
||||||
defaultFullPlaylist: true,
|
defaultFullPlaylist: true,
|
||||||
followSystemTheme: false,
|
followSystemTheme: false,
|
||||||
fontContent: 'Inter',
|
|
||||||
playButtonBehavior: Play.NOW,
|
playButtonBehavior: Play.NOW,
|
||||||
resume: false,
|
resume: false,
|
||||||
showQueueDrawerButton: false,
|
showQueueDrawerButton: false,
|
||||||
|
@ -542,3 +554,5 @@ export const useMpvSettings = () =>
|
||||||
export const useLyricsSettings = () => useSettingsStore((state) => state.lyrics, shallow);
|
export const useLyricsSettings = () => useSettingsStore((state) => state.lyrics, shallow);
|
||||||
|
|
||||||
export const useRemoteSettings = () => useSettingsStore((state) => state.remote, shallow);
|
export const useRemoteSettings = () => useSettingsStore((state) => state.remote, shallow);
|
||||||
|
|
||||||
|
export const useFontSettings = () => useSettingsStore((state) => state.font, shallow);
|
||||||
|
|
|
@ -203,3 +203,9 @@ export type SongUpdate = {
|
||||||
/** This volume is in range 0-100 */
|
/** This volume is in range 0-100 */
|
||||||
volume?: number;
|
volume?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum FontType {
|
||||||
|
BUILT_IN = 'builtIn',
|
||||||
|
CUSTOM = 'custom',
|
||||||
|
SYSTEM = 'system',
|
||||||
|
}
|
||||||
|
|
Reference in a new issue