mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-23 20:37:42 +02:00
feat: add Japanese Furigana support to lyrics display (#2161)
This commit is contained in:
@@ -114,6 +114,8 @@
|
|||||||
"idb-keyval": "^6.2.5",
|
"idb-keyval": "^6.2.5",
|
||||||
"immer": "^10.2.0",
|
"immer": "^10.2.0",
|
||||||
"is-electron": "^2.2.2",
|
"is-electron": "^2.2.2",
|
||||||
|
"kuroshiro": "^1.2.0",
|
||||||
|
"kuroshiro-analyzer-kuromoji": "^1.1.0",
|
||||||
"lodash": "^4.18.1",
|
"lodash": "^4.18.1",
|
||||||
"md5": "^2.3.0",
|
"md5": "^2.3.0",
|
||||||
"motion": "^12.40.0",
|
"motion": "^12.40.0",
|
||||||
|
|||||||
Generated
+47
@@ -138,6 +138,12 @@ importers:
|
|||||||
is-electron:
|
is-electron:
|
||||||
specifier: ^2.2.2
|
specifier: ^2.2.2
|
||||||
version: 2.2.2
|
version: 2.2.2
|
||||||
|
kuroshiro:
|
||||||
|
specifier: ^1.2.0
|
||||||
|
version: 1.2.0
|
||||||
|
kuroshiro-analyzer-kuromoji:
|
||||||
|
specifier: ^1.1.0
|
||||||
|
version: 1.1.0
|
||||||
lodash:
|
lodash:
|
||||||
specifier: ^4.18.1
|
specifier: ^4.18.1
|
||||||
version: 4.18.1
|
version: 4.18.1
|
||||||
@@ -2392,6 +2398,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
async@2.6.4:
|
||||||
|
resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==}
|
||||||
|
|
||||||
async@3.2.6:
|
async@3.2.6:
|
||||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||||
|
|
||||||
@@ -2926,6 +2935,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
doublearray@0.0.2:
|
||||||
|
resolution: {integrity: sha512-aw55FtZzT6AmiamEj2kvmR6BuFqvYgKZUkfQ7teqVRNqD5UE0rw8IeW/3gieHNKQ5sPuDKlljWEn4bzv5+1bHw==}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3959,6 +3971,16 @@ packages:
|
|||||||
known-css-properties@0.37.0:
|
known-css-properties@0.37.0:
|
||||||
resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==}
|
resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==}
|
||||||
|
|
||||||
|
kuromoji@0.1.2:
|
||||||
|
resolution: {integrity: sha512-V0dUf+C2LpcPEXhoHLMAop/bOht16Dyr+mDiIE39yX3vqau7p80De/koFqpiTcL1zzdZlc3xuHZ8u5gjYRfFaQ==}
|
||||||
|
|
||||||
|
kuroshiro-analyzer-kuromoji@1.1.0:
|
||||||
|
resolution: {integrity: sha512-BSJFhpsQdPwfFLfjKxfLA9iL+/PC6LCR9vgwgb5Jc7jZwk9ilX8SAV6CwhAQZY611tiuhbB52ONYKDO8hgY1bA==}
|
||||||
|
|
||||||
|
kuroshiro@1.2.0:
|
||||||
|
resolution: {integrity: sha512-yBGCK9oDOY3LGZ/KXaN9m7ADcAuSczOR2FoMRYwHLUlis3/o/uxdMVROAjENFO0NQJgALhIdWxI/vIBVrMCk9w==}
|
||||||
|
engines: {node: '>=6.5.0'}
|
||||||
|
|
||||||
lazy-val@1.0.5:
|
lazy-val@1.0.5:
|
||||||
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
|
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
|
||||||
|
|
||||||
@@ -5756,6 +5778,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
zlibjs@0.3.1:
|
||||||
|
resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==}
|
||||||
|
|
||||||
zod-validation-error@4.0.2:
|
zod-validation-error@4.0.2:
|
||||||
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -7948,6 +7973,10 @@ snapshots:
|
|||||||
|
|
||||||
async-function@1.0.0: {}
|
async-function@1.0.0: {}
|
||||||
|
|
||||||
|
async@2.6.4:
|
||||||
|
dependencies:
|
||||||
|
lodash: 4.18.1
|
||||||
|
|
||||||
async@3.2.6: {}
|
async@3.2.6: {}
|
||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
@@ -8544,6 +8573,8 @@ snapshots:
|
|||||||
|
|
||||||
dotenv@16.6.1: {}
|
dotenv@16.6.1: {}
|
||||||
|
|
||||||
|
doublearray@0.0.2: {}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
@@ -9829,6 +9860,20 @@ snapshots:
|
|||||||
|
|
||||||
known-css-properties@0.37.0: {}
|
known-css-properties@0.37.0: {}
|
||||||
|
|
||||||
|
kuromoji@0.1.2:
|
||||||
|
dependencies:
|
||||||
|
async: 2.6.4
|
||||||
|
doublearray: 0.0.2
|
||||||
|
zlibjs: 0.3.1
|
||||||
|
|
||||||
|
kuroshiro-analyzer-kuromoji@1.1.0:
|
||||||
|
dependencies:
|
||||||
|
kuromoji: 0.1.2
|
||||||
|
|
||||||
|
kuroshiro@1.2.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.29.7
|
||||||
|
|
||||||
lazy-val@1.0.5: {}
|
lazy-val@1.0.5: {}
|
||||||
|
|
||||||
lead@4.0.0: {}
|
lead@4.0.0: {}
|
||||||
@@ -11818,6 +11863,8 @@ snapshots:
|
|||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
zlibjs@0.3.1: {}
|
||||||
|
|
||||||
zod-validation-error@4.0.2(zod@3.25.76):
|
zod-validation-error@4.0.2(zod@3.25.76):
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.76
|
zod: 3.25.76
|
||||||
|
|||||||
@@ -843,6 +843,8 @@
|
|||||||
"discordUpdateInterval_description": "The time in seconds between each update (minimum 15 seconds)",
|
"discordUpdateInterval_description": "The time in seconds between each update (minimum 15 seconds)",
|
||||||
"enableAutoTranslation_description": "Enable translation automatically when lyrics are loaded",
|
"enableAutoTranslation_description": "Enable translation automatically when lyrics are loaded",
|
||||||
"enableAutoTranslation": "Enable auto translation",
|
"enableAutoTranslation": "Enable auto translation",
|
||||||
|
"enableFurigana_description": "Display pronunciation guides (furigana) over Japanese kanji lyrics.",
|
||||||
|
"enableFurigana": "Enable furigana generation",
|
||||||
"enableRemote_description": "Enables the remote control server to allow other devices to control the application",
|
"enableRemote_description": "Enables the remote control server to allow other devices to control the application",
|
||||||
"enableRemote": "Enable remote control server",
|
"enableRemote": "Enable remote control server",
|
||||||
"exitToTray_description": "Exit the application to the system tray",
|
"exitToTray_description": "Exit the application to the system tray",
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import Kuroshiro from 'kuroshiro';
|
||||||
|
import KuromojiAnalyzer from 'kuroshiro-analyzer-kuromoji';
|
||||||
|
|
||||||
|
// doc: https://kuroshiro.org
|
||||||
|
|
||||||
|
let kuroshiroInstance: any = null;
|
||||||
|
let initPromise: null | Promise<void> = null;
|
||||||
|
|
||||||
|
const getKuroshiro = async () => {
|
||||||
|
if (kuroshiroInstance) return kuroshiroInstance;
|
||||||
|
if (initPromise) {
|
||||||
|
await initPromise;
|
||||||
|
return kuroshiroInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
|
||||||
|
kuroshiroInstance = new KuroshiroClass();
|
||||||
|
initPromise = kuroshiroInstance.init(new KuromojiAnalyzer());
|
||||||
|
await initPromise;
|
||||||
|
return kuroshiroInstance;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertFurigana = async (text: string): Promise<string> => {
|
||||||
|
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
|
||||||
|
|
||||||
|
// check if the text contains any Japanese kana (to distinguish Japanese from Chinese text, which shares Kanji)
|
||||||
|
// If no Japanese kana is detected, skip processing
|
||||||
|
if (!KuroshiroClass.Util.hasKana(text)) return text;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const kuroshiro = await getKuroshiro();
|
||||||
|
return await kuroshiro.convert(text, { mode: 'furigana', to: 'hiragana' });
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Furigana conversion error: ', e);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
|
|
||||||
import { store } from '../settings';
|
import { store } from '../settings';
|
||||||
|
import { convertFurigana } from './furigana';
|
||||||
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
|
import { getLyricsBySongId as getGenius, getSearchResults as searchGenius } from './genius';
|
||||||
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
|
import { getLyricsBySongId as getLrcLib, getSearchResults as searchLrcLib } from './lrclib';
|
||||||
import { getLyricsBySongId as getNetease, getSearchResults as searchNetease } from './netease';
|
import { getLyricsBySongId as getNetease, getSearchResults as searchNetease } from './netease';
|
||||||
@@ -231,3 +232,7 @@ ipcMain.handle('lyric-by-remote-id', async (_event, params: LyricGetQuery) => {
|
|||||||
const lyricResults = await getRemoteLyricsById(params);
|
const lyricResults = await getRemoteLyricsById(params);
|
||||||
return lyricResults;
|
return lyricResults;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('lyric-convert-furigana', async (_event, text: string) => {
|
||||||
|
return await convertFurigana(text);
|
||||||
|
});
|
||||||
|
|||||||
@@ -26,7 +26,12 @@ const getRemoteLyricsByRemoteId = (id: LyricGetQuery) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const convertFurigana = (text: string): Promise<string> => {
|
||||||
|
return ipcRenderer.invoke('lyric-convert-furigana', text);
|
||||||
|
};
|
||||||
|
|
||||||
export const lyrics = {
|
export const lyrics = {
|
||||||
|
convertFurigana,
|
||||||
getRemoteLyricsByRemoteId,
|
getRemoteLyricsByRemoteId,
|
||||||
getRemoteLyricsBySong,
|
getRemoteLyricsBySong,
|
||||||
searchRemoteLyrics,
|
searchRemoteLyrics,
|
||||||
|
|||||||
@@ -291,6 +291,22 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
|
|||||||
isHidden: !isElectron(),
|
isHidden: !isElectron(),
|
||||||
title: t('setting.lyricFetchProvider'),
|
title: t('setting.lyricFetchProvider'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
aria-label="Enable furigana"
|
||||||
|
defaultChecked={lyricsSettings.enableFurigana}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateLyricsSetting({ enableFurigana: e.currentTarget.checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.enableFurigana', {
|
||||||
|
context: 'description',
|
||||||
|
}),
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: t('setting.enableFurigana'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import isElectron from 'is-electron';
|
||||||
|
|
||||||
|
import { LyricsResponse, SynchronizedLyricsArray } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
const lyricsApi = isElectron() ? window.api.lyrics : null;
|
||||||
|
|
||||||
|
export const useFuriganaLyrics = (lyrics: LyricsResponse | null | undefined, enabled: boolean) => {
|
||||||
|
return useQuery({
|
||||||
|
enabled: enabled && !!lyrics && !!lyricsApi,
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!lyrics || !lyricsApi || !enabled) return lyrics;
|
||||||
|
|
||||||
|
if (typeof lyrics === 'string') {
|
||||||
|
return await lyricsApi.convertFurigana(lyrics);
|
||||||
|
} else if (Array.isArray(lyrics)) {
|
||||||
|
const text = lyrics.map(([, line]) => line).join('\n');
|
||||||
|
const converted = await lyricsApi.convertFurigana(text);
|
||||||
|
const convertedLines = converted.split('\n');
|
||||||
|
return lyrics.map(([time], i) => [
|
||||||
|
time,
|
||||||
|
convertedLines[i] ?? lyrics[i][1],
|
||||||
|
]) as SynchronizedLyricsArray;
|
||||||
|
}
|
||||||
|
return lyrics;
|
||||||
|
},
|
||||||
|
queryKey: ['furigana', lyrics],
|
||||||
|
staleTime: Infinity,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import { ComponentPropsWithoutRef, memo, useMemo } from 'react';
|
|||||||
|
|
||||||
import styles from './lyric-line.module.css';
|
import styles from './lyric-line.module.css';
|
||||||
|
|
||||||
|
import { sanitize } from '/@/renderer/utils/sanitize';
|
||||||
import { Box } from '/@/shared/components/box/box';
|
import { Box } from '/@/shared/components/box/box';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ export const LyricLine = memo(
|
|||||||
<Box className={clsx(styles.lyricLine, className)} style={style} {...props}>
|
<Box className={clsx(styles.lyricLine, className)} style={style} {...props}>
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
{lines.map((line, index) => (
|
{lines.map((line, index) => (
|
||||||
<span key={index}>{line}</span>
|
<span dangerouslySetInnerHTML={{ __html: sanitize(line) }} key={index} />
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
type LyricsQueryResult,
|
type LyricsQueryResult,
|
||||||
} from '/@/renderer/features/lyrics/api/lyrics-api';
|
} from '/@/renderer/features/lyrics/api/lyrics-api';
|
||||||
import { openLyricsExportModal } from '/@/renderer/features/lyrics/components/lyrics-export-form';
|
import { openLyricsExportModal } from '/@/renderer/features/lyrics/components/lyrics-export-form';
|
||||||
|
import { useFuriganaLyrics } from '/@/renderer/features/lyrics/hooks/use-furigana-lyrics';
|
||||||
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
|
import { LyricsActions } from '/@/renderer/features/lyrics/lyrics-actions';
|
||||||
import {
|
import {
|
||||||
SynchronizedLyrics,
|
SynchronizedLyrics,
|
||||||
@@ -49,6 +50,7 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
enableAutoTranslation,
|
enableAutoTranslation,
|
||||||
|
enableFurigana,
|
||||||
preferLocalLyrics,
|
preferLocalLyrics,
|
||||||
translationApiKey,
|
translationApiKey,
|
||||||
translationApiProvider,
|
translationApiProvider,
|
||||||
@@ -116,7 +118,15 @@ export const Lyrics = ({ fadeOutNoLyricsMessage = true, settingsKey = 'default'
|
|||||||
return computeSelectedFromResult(data, preferLocalLyrics, indexToUse);
|
return computeSelectedFromResult(data, preferLocalLyrics, indexToUse);
|
||||||
}, [data, indexToUse, preferLocalLyrics]);
|
}, [data, indexToUse, preferLocalLyrics]);
|
||||||
|
|
||||||
const displayLyrics = isLyricsDisabled ? null : lyrics;
|
const { data: furiganaConvertedLyrics } = useFuriganaLyrics(lyrics?.lyrics, !!enableFurigana);
|
||||||
|
|
||||||
|
const displayLyrics = useMemo(() => {
|
||||||
|
if (isLyricsDisabled || !lyrics) return null;
|
||||||
|
if (enableFurigana && furiganaConvertedLyrics) {
|
||||||
|
return { ...lyrics, lyrics: furiganaConvertedLyrics };
|
||||||
|
}
|
||||||
|
return lyrics;
|
||||||
|
}, [enableFurigana, isLyricsDisabled, lyrics, furiganaConvertedLyrics]);
|
||||||
|
|
||||||
const currentOffsetMs = useMemo(() => {
|
const currentOffsetMs = useMemo(() => {
|
||||||
if (!data) return 0;
|
if (!data) return 0;
|
||||||
|
|||||||
@@ -93,6 +93,20 @@ export const LyricSettings = memo(() => {
|
|||||||
isHidden: !isElectron(),
|
isHidden: !isElectron(),
|
||||||
title: t('setting.lyricFetchProvider'),
|
title: t('setting.lyricFetchProvider'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
aria-label="Enable furigana generation"
|
||||||
|
defaultChecked={settings.enableFurigana}
|
||||||
|
onChange={(e) => updateSetting({ enableFurigana: e.currentTarget.checked })}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.enableFurigana', {
|
||||||
|
context: 'description',
|
||||||
|
}),
|
||||||
|
isHidden: !isElectron(),
|
||||||
|
title: t('setting.enableFurigana'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
control: (
|
control: (
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -576,6 +576,7 @@ const LyricsSettingsSchema = z.object({
|
|||||||
alignment: z.enum(['center', 'left', 'right']),
|
alignment: z.enum(['center', 'left', 'right']),
|
||||||
delayMs: z.number(),
|
delayMs: z.number(),
|
||||||
enableAutoTranslation: z.boolean(),
|
enableAutoTranslation: z.boolean(),
|
||||||
|
enableFurigana: z.boolean().optional(),
|
||||||
enableNeteaseTranslation: z.boolean(),
|
enableNeteaseTranslation: z.boolean(),
|
||||||
fetch: z.boolean(),
|
fetch: z.boolean(),
|
||||||
follow: z.boolean(),
|
follow: z.boolean(),
|
||||||
@@ -1845,6 +1846,7 @@ const initialState: SettingsState = {
|
|||||||
alignment: 'center',
|
alignment: 'center',
|
||||||
delayMs: 0,
|
delayMs: 0,
|
||||||
enableAutoTranslation: false,
|
enableAutoTranslation: false,
|
||||||
|
enableFurigana: false,
|
||||||
enableNeteaseTranslation: false,
|
enableNeteaseTranslation: false,
|
||||||
fetch: true,
|
fetch: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import DomPurify, { Config } from 'dompurify';
|
|||||||
|
|
||||||
const SANITIZE_OPTIONS: Config = {
|
const SANITIZE_OPTIONS: Config = {
|
||||||
ALLOWED_ATTR: ['href'],
|
ALLOWED_ATTR: ['href'],
|
||||||
ALLOWED_TAGS: ['a', 'b', 'div', 'em', 'i', 'p', 'span', 'strong'],
|
ALLOWED_TAGS: ['a', 'b', 'div', 'em', 'i', 'p', 'span', 'strong', 'ruby', 'rt', 'rp'],
|
||||||
// allow http://, https://, and // (mapped to https)
|
// allow http://, https://, and // (mapped to https)
|
||||||
ALLOWED_URI_REGEXP: /^(http(s?):)?\/\/.+/i,
|
ALLOWED_URI_REGEXP: /^(http(s?):)?\/\/.+/i,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user