mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-21 18:06:30 +02:00
feat: support furigana and romaji generation on web (#2232)
* feat: support furigana and romaji generation on web
This commit is contained in:
+16
-5
@@ -1,15 +1,16 @@
|
||||
import { externalizeDepsPlugin, UserConfig } from 'electron-vite';
|
||||
import { defineConfig, externalizeDepsPlugin, UserConfig } from 'electron-vite';
|
||||
import { resolve } from 'path';
|
||||
import conditionalImportPlugin from 'vite-plugin-conditional-import';
|
||||
import dynamicImportPlugin from 'vite-plugin-dynamic-import';
|
||||
import { ViteEjsPlugin } from 'vite-plugin-ejs';
|
||||
|
||||
import { kuromojiDictionaryPlugin } from './vite.kuromoji-plugin';
|
||||
import { createReactPlugin } from './vite.react-plugin';
|
||||
|
||||
const currentOSEnv = process.platform;
|
||||
const electronRendererTarget = 'chrome87';
|
||||
|
||||
const config: UserConfig = {
|
||||
const createConfig = (isDevelopment: boolean): UserConfig => ({
|
||||
main: {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
@@ -65,16 +66,26 @@ const config: UserConfig = {
|
||||
localsConvention: 'camelCase',
|
||||
},
|
||||
},
|
||||
plugins: [createReactPlugin(), ViteEjsPlugin({ web: false })],
|
||||
plugins: [
|
||||
createReactPlugin(),
|
||||
...(isDevelopment ? [kuromojiDictionaryPlugin({ emitDictionary: false })] : []),
|
||||
ViteEjsPlugin({ web: false }),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'/@/i18n': resolve('src/i18n'),
|
||||
'/@/lyrics-conversion-api': resolve(
|
||||
isDevelopment
|
||||
? 'src/renderer/features/lyrics/api/development-lyrics-conversion-api.ts'
|
||||
: 'src/renderer/features/lyrics/api/electron-lyrics-conversion-api.ts',
|
||||
),
|
||||
'/@/remote': resolve('src/remote'),
|
||||
'/@/renderer': resolve('src/renderer'),
|
||||
'/@/shared': resolve('src/shared'),
|
||||
...(isDevelopment ? { path: resolve('src/renderer/shims/path.ts') } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default config;
|
||||
export default defineConfig(({ command }) => createConfig(command === 'serve'));
|
||||
|
||||
@@ -17,6 +17,14 @@ export type RomajiToken = LyricTextToken & {
|
||||
let kuroshiroInstance: any = null;
|
||||
let initPromise: null | Promise<void> = null;
|
||||
|
||||
const getDictionaryPath = (): string | undefined => {
|
||||
if (typeof document === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new URL('./assets/kuromoji/', document.baseURI).href;
|
||||
};
|
||||
|
||||
const getKuroshiro = async () => {
|
||||
if (initPromise) {
|
||||
await initPromise;
|
||||
@@ -26,8 +34,13 @@ const getKuroshiro = async () => {
|
||||
if (kuroshiroInstance) return kuroshiroInstance;
|
||||
|
||||
const KuroshiroClass = (Kuroshiro as any).default || Kuroshiro;
|
||||
const dictionaryPath = getDictionaryPath();
|
||||
const analyzer = dictionaryPath
|
||||
? new KuromojiAnalyzer({ dictPath: dictionaryPath })
|
||||
: new KuromojiAnalyzer();
|
||||
|
||||
kuroshiroInstance = new KuroshiroClass();
|
||||
initPromise = kuroshiroInstance.init(new KuromojiAnalyzer());
|
||||
initPromise = kuroshiroInstance.init(analyzer);
|
||||
await initPromise;
|
||||
|
||||
initPromise = null;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import isElectron from 'is-electron';
|
||||
|
||||
import * as browserLyricsApi from '../../../../main/features/core/lyrics/furigana';
|
||||
|
||||
const lyricsApi = isElectron() ? window.api.lyrics : browserLyricsApi;
|
||||
|
||||
export const {
|
||||
convertFurigana,
|
||||
convertFuriganaFragment,
|
||||
convertRomaji,
|
||||
convertRomajiTokens,
|
||||
parseLyricsTextTokens,
|
||||
} = lyricsApi;
|
||||
@@ -0,0 +1,7 @@
|
||||
export const {
|
||||
convertFurigana,
|
||||
convertFuriganaFragment,
|
||||
convertRomaji,
|
||||
convertRomajiTokens,
|
||||
parseLyricsTextTokens,
|
||||
} = window.api.lyrics;
|
||||
@@ -372,7 +372,6 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
|
||||
description: t('setting.enableFurigana', {
|
||||
context: 'description',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: t('setting.enableFurigana'),
|
||||
},
|
||||
{
|
||||
@@ -386,7 +385,6 @@ export const LyricsSettingsForm = ({ settingsKey }: LyricsSettingsFormProps) =>
|
||||
description: t('setting.enableRomaji', {
|
||||
context: 'description',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: t('setting.enableRomaji'),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import isElectron from 'is-electron';
|
||||
|
||||
import * as lyricsApi from '/@/lyrics-conversion-api';
|
||||
import {
|
||||
alignFuriganaToWordCues,
|
||||
alignRomajiTokensToWordCues,
|
||||
@@ -10,15 +10,9 @@ import {
|
||||
import { normalizeLyrics } from '/@/renderer/features/lyrics/api/lyrics-utils';
|
||||
import { LyricsResponse, SyncedCueLine, SynchronizedLyrics } from '/@/shared/types/domain-types';
|
||||
|
||||
const lyricsApi = isElectron() ? window.api.lyrics : null;
|
||||
|
||||
const convertSyncedLyricsFurigana = async (
|
||||
lyrics: SynchronizedLyrics,
|
||||
): Promise<SynchronizedLyrics> => {
|
||||
if (!lyricsApi) {
|
||||
return lyrics;
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
normalizeLyrics(lyrics).map(async (line) => ({
|
||||
...line,
|
||||
@@ -76,9 +70,9 @@ const convertSyncedLyricsRomaji = async (
|
||||
|
||||
export const useFuriganaLyrics = (lyrics: LyricsResponse | null | undefined, enabled: boolean) => {
|
||||
return useQuery({
|
||||
enabled: enabled && !!lyrics && !!lyricsApi,
|
||||
enabled: enabled && !!lyrics,
|
||||
queryFn: async () => {
|
||||
if (!lyrics || !lyricsApi || !enabled) return lyrics;
|
||||
if (!lyrics || !enabled) return lyrics;
|
||||
|
||||
if (typeof lyrics === 'string') {
|
||||
return await lyricsApi.convertFurigana(lyrics);
|
||||
@@ -97,9 +91,9 @@ export const useFuriganaLyrics = (lyrics: LyricsResponse | null | undefined, ena
|
||||
|
||||
export const useRomajiLyrics = (lyrics: LyricsResponse | null | undefined, enabled: boolean) => {
|
||||
return useQuery({
|
||||
enabled: enabled && !!lyrics && !!lyricsApi,
|
||||
enabled: enabled && !!lyrics,
|
||||
queryFn: async () => {
|
||||
if (!lyrics || !lyricsApi || !enabled) return lyrics;
|
||||
if (!lyrics || !enabled) return lyrics;
|
||||
|
||||
if (typeof lyrics === 'string') {
|
||||
return await lyricsApi.convertRomaji(lyrics);
|
||||
@@ -129,10 +123,6 @@ const buildSyncedRomajiLine = async (
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lyricsApi) {
|
||||
return cueLines.map(() => null);
|
||||
}
|
||||
|
||||
const tokens = (await lyricsApi.convertRomajiTokens(cueLine.value)) as RomajiToken[];
|
||||
if (!tokens.length) {
|
||||
romajiCueLines.push(null);
|
||||
@@ -160,9 +150,9 @@ export const useSyncedRomajiLyrics = (
|
||||
enabled: boolean,
|
||||
) => {
|
||||
return useQuery({
|
||||
enabled: enabled && !!lyrics && !!lyricsApi,
|
||||
enabled: enabled && !!lyrics,
|
||||
queryFn: async (): Promise<null | SyncedRomajiLyrics> => {
|
||||
if (!lyrics || !lyricsApi || !enabled) {
|
||||
if (!lyrics || !enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,6 @@ export const LyricSettings = memo(() => {
|
||||
description: t('setting.enableFurigana', {
|
||||
context: 'description',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: t('setting.enableFurigana'),
|
||||
},
|
||||
{
|
||||
@@ -118,7 +117,6 @@ export const LyricSettings = memo(() => {
|
||||
description: t('setting.enableRomaji', {
|
||||
context: 'description',
|
||||
}),
|
||||
isHidden: !isElectron(),
|
||||
title: t('setting.enableRomaji'),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const join = (...parts: string[]): string =>
|
||||
parts
|
||||
.filter(Boolean)
|
||||
.map((part, index) =>
|
||||
index === 0 ? part.replace(/\/+$/u, '') : part.replace(/^\/+|\/+$/gu, ''),
|
||||
)
|
||||
.join('/');
|
||||
|
||||
export default { join };
|
||||
@@ -2,6 +2,7 @@
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||
"include": [
|
||||
"electron.vite.config.*",
|
||||
"vite.kuromoji-plugin.ts",
|
||||
"vite.react-plugin.ts",
|
||||
"src/main/**/*",
|
||||
"src/preload/**/*",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"src/renderer/**/*.tsx",
|
||||
"src/preload/*.d.ts",
|
||||
"src/i18n/**/*",
|
||||
"src/main/features/core/lyrics/furigana.ts",
|
||||
"src/shared/**/*",
|
||||
"src/remote/**/*",
|
||||
"package.json"
|
||||
@@ -18,6 +19,9 @@
|
||||
"/@/renderer/*": [
|
||||
"src/renderer/*"
|
||||
],
|
||||
"/@/lyrics-conversion-api": [
|
||||
"src/main/features/core/lyrics/furigana.ts"
|
||||
],
|
||||
"/@/shared/*": [
|
||||
"src/shared/*"
|
||||
],
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { Connect, Plugin } from 'vite';
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import path from 'node:path';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const analyzerRequire = createRequire(require.resolve('kuroshiro-analyzer-kuromoji/package.json'));
|
||||
const dictionaryDirectory = path.join(
|
||||
path.dirname(analyzerRequire.resolve('kuromoji/package.json')),
|
||||
'dict',
|
||||
);
|
||||
const dictionaryFiles = fs
|
||||
.readdirSync(dictionaryDirectory)
|
||||
.filter((fileName) => fileName.endsWith('.dat.gz'));
|
||||
|
||||
const serveDictionary: Connect.NextHandleFunction = (request, response, next) => {
|
||||
const fileName = path.basename(new URL(request.url ?? '/', 'http://localhost').pathname);
|
||||
|
||||
if (!dictionaryFiles.includes(fileName)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
response.setHeader('Content-Type', 'application/gzip');
|
||||
fs.createReadStream(path.join(dictionaryDirectory, fileName)).pipe(response);
|
||||
};
|
||||
|
||||
type KuromojiDictionaryPluginOptions = {
|
||||
emitDictionary?: boolean;
|
||||
};
|
||||
|
||||
export const kuromojiDictionaryPlugin = ({
|
||||
emitDictionary = true,
|
||||
}: KuromojiDictionaryPluginOptions = {}): Plugin => ({
|
||||
configurePreviewServer(server) {
|
||||
server.middlewares.use('/assets/kuromoji', serveDictionary);
|
||||
},
|
||||
configureServer(server) {
|
||||
server.middlewares.use('/assets/kuromoji', serveDictionary);
|
||||
},
|
||||
generateBundle() {
|
||||
if (!emitDictionary) return;
|
||||
|
||||
for (const fileName of dictionaryFiles) {
|
||||
this.emitFile({
|
||||
fileName: `assets/kuromoji/${fileName}`,
|
||||
source: fs.readFileSync(path.join(dictionaryDirectory, fileName)),
|
||||
type: 'asset',
|
||||
});
|
||||
}
|
||||
},
|
||||
name: 'kuromoji-dictionary',
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { defineConfig, normalizePath } from 'vite';
|
||||
import { ViteEjsPlugin } from 'vite-plugin-ejs';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
import { kuromojiDictionaryPlugin } from './vite.kuromoji-plugin';
|
||||
import { createReactPlugin } from './vite.react-plugin';
|
||||
|
||||
export default defineConfig({
|
||||
@@ -65,6 +66,7 @@ export default defineConfig({
|
||||
},
|
||||
plugins: [
|
||||
createReactPlugin(),
|
||||
kuromojiDictionaryPlugin(),
|
||||
ViteEjsPlugin({
|
||||
root: normalizePath(path.resolve(__dirname, './src/renderer')),
|
||||
web: true,
|
||||
@@ -134,6 +136,7 @@ export default defineConfig({
|
||||
workbox: {
|
||||
cleanupOutdatedCaches: true,
|
||||
clientsClaim: true,
|
||||
globIgnores: ['**/kuromoji/**'],
|
||||
maximumFileSizeToCacheInBytes: 1000000 * 5, // 5 MB
|
||||
skipWaiting: true,
|
||||
},
|
||||
@@ -142,9 +145,14 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'/@/i18n': path.resolve(__dirname, './src/i18n'),
|
||||
'/@/lyrics-conversion-api': path.resolve(
|
||||
__dirname,
|
||||
'./src/main/features/core/lyrics/furigana.ts',
|
||||
),
|
||||
'/@/remote': path.resolve(__dirname, './src/remote'),
|
||||
'/@/renderer': path.resolve(__dirname, './src/renderer'),
|
||||
'/@/shared': path.resolve(__dirname, './src/shared'),
|
||||
path: path.resolve(__dirname, './src/renderer/shims/path.ts'),
|
||||
},
|
||||
},
|
||||
root: path.resolve(__dirname, './src/renderer'),
|
||||
|
||||
Reference in New Issue
Block a user