From cb97f70e69f91aff5d597f999392e60381f030ff Mon Sep 17 00:00:00 2001 From: Angelos Bouklis <53124886+ArjixWasTaken@users.noreply.github.com> Date: Sat, 28 Dec 2024 02:41:04 +0200 Subject: [PATCH 01/10] feat(synced-lyrics): init romanization! --- electron.vite.config.mts | 105 ++++++++++-------- package.json | 3 + pnpm-lock.yaml | 50 +++++++++ src/plugins/synced-lyrics/backend.ts | 28 +++++ src/plugins/synced-lyrics/index.ts | 26 +++-- .../renderer/components/SyncedLine.tsx | 99 +++++++++++++---- src/plugins/synced-lyrics/renderer/index.ts | 44 ++++---- .../synced-lyrics/renderer/renderer.tsx | 15 ++- src/plugins/synced-lyrics/style.css | 34 +++--- .../kuroshiro-analyzer-kuromoji.d.ts | 12 ++ src/ts-declarations/kuroshiro.d.ts | 40 +++++++ 11 files changed, 337 insertions(+), 119 deletions(-) create mode 100644 src/plugins/synced-lyrics/backend.ts create mode 100644 src/ts-declarations/kuroshiro-analyzer-kuromoji.d.ts create mode 100644 src/ts-declarations/kuroshiro.d.ts diff --git a/electron.vite.config.mts b/electron.vite.config.mts index 4d5b549d89..26b1f8f84c 100644 --- a/electron.vite.config.mts +++ b/electron.vite.config.mts @@ -1,48 +1,55 @@ -import { resolve, dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; -import { UserConfig } from 'vite'; -import { defineConfig, defineViteConfig } from 'electron-vite'; -import builtinModules from 'builtin-modules'; -import viteResolve from 'vite-plugin-resolve'; -import Inspect from 'vite-plugin-inspect'; -import solidPlugin from 'vite-plugin-solid'; +import { UserConfig } from "vite"; +import { defineConfig, defineViteConfig } from "electron-vite"; +import builtinModules from "builtin-modules"; +import viteResolve from "vite-plugin-resolve"; +import Inspect from "vite-plugin-inspect"; +import solidPlugin from "vite-plugin-solid"; -import { pluginVirtualModuleGenerator } from './vite-plugins/plugin-importer.mjs'; -import pluginLoader from './vite-plugins/plugin-loader.mjs'; +import { pluginVirtualModuleGenerator } from "./vite-plugins/plugin-importer.mjs"; +import pluginLoader from "./vite-plugins/plugin-loader.mjs"; -import { i18nImporter } from './vite-plugins/i18n-importer.mjs'; +import { i18nImporter } from "./vite-plugins/i18n-importer.mjs"; const __dirname = dirname(fileURLToPath(import.meta.url)); const resolveAlias = { - '@': resolve(__dirname, './src'), - '@assets': resolve(__dirname, './assets'), + "@": resolve(__dirname, "./src"), + "@assets": resolve(__dirname, "./assets"), }; export default defineConfig({ main: defineViteConfig(({ mode }) => { const commonConfig: UserConfig = { plugins: [ - pluginLoader('backend'), + pluginLoader("backend"), viteResolve({ - 'virtual:i18n': i18nImporter(), - 'virtual:plugins': pluginVirtualModuleGenerator('main'), + "virtual:i18n": i18nImporter(), + "virtual:plugins": pluginVirtualModuleGenerator("main"), }), ], - publicDir: 'assets', + publicDir: "assets", build: { lib: { - entry: 'src/index.ts', - formats: ['cjs'], + entry: "src/index.ts", + formats: ["cjs"], }, - outDir: 'dist/main', + outDir: "dist/main", commonjsOptions: { ignoreDynamicRequires: true, }, rollupOptions: { - external: ['electron', 'custom-electron-prompt', ...builtinModules], - input: './src/index.ts', + external: [ + "electron", + "custom-electron-prompt", + "kuromoji", + "kuroshiro", + "kuroshiro-analyzer-kuromoji", + ...builtinModules, + ], + input: "./src/index.ts", }, }, resolve: { @@ -50,12 +57,12 @@ export default defineConfig({ }, }; - if (mode === 'development') { - commonConfig.build!.sourcemap = 'inline'; + if (mode === "development") { + commonConfig.build!.sourcemap = "inline"; commonConfig.plugins?.push( Inspect({ build: true, - outputDir: join(__dirname, '.vite-inspect/backend'), + outputDir: join(__dirname, ".vite-inspect/backend"), }), ); return commonConfig; @@ -73,24 +80,24 @@ export default defineConfig({ preload: defineViteConfig(({ mode }) => { const commonConfig: UserConfig = { plugins: [ - pluginLoader('preload'), + pluginLoader("preload"), viteResolve({ - 'virtual:i18n': i18nImporter(), - 'virtual:plugins': pluginVirtualModuleGenerator('preload'), + "virtual:i18n": i18nImporter(), + "virtual:plugins": pluginVirtualModuleGenerator("preload"), }), ], build: { lib: { - entry: 'src/preload.ts', - formats: ['cjs'], + entry: "src/preload.ts", + formats: ["cjs"], }, - outDir: 'dist/preload', + outDir: "dist/preload", commonjsOptions: { ignoreDynamicRequires: true, }, rollupOptions: { - external: ['electron', 'custom-electron-prompt', ...builtinModules], - input: './src/preload.ts', + external: ["electron", "custom-electron-prompt", ...builtinModules], + input: "./src/preload.ts", }, }, resolve: { @@ -98,12 +105,12 @@ export default defineConfig({ }, }; - if (mode === 'development') { - commonConfig.build!.sourcemap = 'inline'; + if (mode === "development") { + commonConfig.build!.sourcemap = "inline"; commonConfig.plugins?.push( Inspect({ build: true, - outputDir: join(__dirname, '.vite-inspect/preload'), + outputDir: join(__dirname, ".vite-inspect/preload"), }), ); return commonConfig; @@ -121,27 +128,27 @@ export default defineConfig({ renderer: defineViteConfig(({ mode }) => { const commonConfig: UserConfig = { plugins: [ - pluginLoader('renderer'), + pluginLoader("renderer"), viteResolve({ - 'virtual:i18n': i18nImporter(), - 'virtual:plugins': pluginVirtualModuleGenerator('renderer'), + "virtual:i18n": i18nImporter(), + "virtual:plugins": pluginVirtualModuleGenerator("renderer"), }), solidPlugin(), ], - root: './src/', + root: "./src/", build: { lib: { - entry: 'src/index.html', - formats: ['iife'], - name: 'renderer', + entry: "src/index.html", + formats: ["iife"], + name: "renderer", }, - outDir: 'dist/renderer', + outDir: "dist/renderer", commonjsOptions: { ignoreDynamicRequires: true, }, rollupOptions: { - external: ['electron', ...builtinModules], - input: './src/index.html', + external: ["electron", ...builtinModules], + input: "./src/index.html", }, }, resolve: { @@ -149,12 +156,12 @@ export default defineConfig({ }, }; - if (mode === 'development') { - commonConfig.build!.sourcemap = 'inline'; + if (mode === "development") { + commonConfig.build!.sourcemap = "inline"; commonConfig.plugins?.push( Inspect({ build: true, - outputDir: join(__dirname, '.vite-inspect/renderer'), + outputDir: join(__dirname, ".vite-inspect/renderer"), }), ); return commonConfig; diff --git a/package.json b/package.json index caba574176..e1265ea157 100644 --- a/package.json +++ b/package.json @@ -275,6 +275,9 @@ "jimp": "1.6.0", "keyboardevent-from-electron-accelerator": "2.0.0", "keyboardevents-areequal": "0.2.2", + "kuromoji": "^0.1.2", + "kuroshiro": "^1.2.0", + "kuroshiro-analyzer-kuromoji": "^1.1.0", "node-html-parser": "7.0.1", "node-id3": "0.2.6", "peerjs": "1.5.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 39ec1b1bcb..3cc9ae1df1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,6 +150,15 @@ importers: keyboardevents-areequal: specifier: 0.2.2 version: 0.2.2 + kuromoji: + specifier: ^0.1.2 + version: 0.1.2 + kuroshiro: + specifier: ^1.2.0 + version: 1.2.0 + kuroshiro-analyzer-kuromoji: + specifier: ^1.1.0 + version: 1.1.0 node-html-parser: specifier: 7.0.1 version: 7.0.1 @@ -1572,6 +1581,9 @@ packages: async-mutex@0.5.0: resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -2033,6 +2045,9 @@ packages: resolution: {integrity: sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==} engines: {node: '>=10'} + doublearray@0.0.2: + resolution: {integrity: sha512-aw55FtZzT6AmiamEj2kvmR6BuFqvYgKZUkfQ7teqVRNqD5UE0rw8IeW/3gieHNKQ5sPuDKlljWEn4bzv5+1bHw==} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -3003,6 +3018,16 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + 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: resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} @@ -4354,6 +4379,9 @@ packages: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} + zlibjs@0.3.1: + resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==} + zod@3.24.1: resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} @@ -5699,6 +5727,10 @@ snapshots: dependencies: tslib: 2.8.1 + async@2.6.4: + dependencies: + lodash: 4.17.21 + async@3.2.6: {} asynckit@0.4.0: {} @@ -6254,6 +6286,8 @@ snapshots: dotenv@9.0.2: {} + doublearray@0.0.2: {} + duplexer@0.1.2: {} eastasianwidth@0.2.0: {} @@ -7439,6 +7473,20 @@ snapshots: dependencies: json-buffer: 3.0.1 + 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.26.0 + lazy-val@1.0.5: {} lazystream@1.0.1: @@ -8801,4 +8849,6 @@ snapshots: compress-commons: 4.1.2 readable-stream: 3.6.2 + zlibjs@0.3.1: {} + zod@3.24.1: {} diff --git a/src/plugins/synced-lyrics/backend.ts b/src/plugins/synced-lyrics/backend.ts new file mode 100644 index 0000000000..117830859f --- /dev/null +++ b/src/plugins/synced-lyrics/backend.ts @@ -0,0 +1,28 @@ +import { createBackend } from "@/utils"; + +import Kuroshiro from "kuroshiro"; +import KuromojiAnalyzer from "kuroshiro-analyzer-kuromoji"; + +export const backend = createBackend({ + async start({ ipc }) { + console.log(Kuroshiro); + + const kuroshiro = new Kuroshiro.default(); + { + await kuroshiro.init(new KuromojiAnalyzer()); + } + + ipc.handle("synced-lyrics:romanize-line", async (line: string) => { + let out = line; + + if (Kuroshiro.default.Util.hasJapanese(line)) { + out = await kuroshiro.convert(line, { + to: "romaji", + mode: "spaced", + }); + } + + return out; + }); + }, +}); diff --git a/src/plugins/synced-lyrics/index.ts b/src/plugins/synced-lyrics/index.ts index f1e99952a5..0eec91698f 100644 --- a/src/plugins/synced-lyrics/index.ts +++ b/src/plugins/synced-lyrics/index.ts @@ -1,27 +1,29 @@ -import style from './style.css?inline'; -import { createPlugin } from '@/utils'; -import { t } from '@/i18n'; +import style from "./style.css?inline"; +import { createPlugin } from "@/utils"; +import { t } from "@/i18n"; -import { menu } from './menu'; -import { renderer } from './renderer'; +import { menu } from "./menu"; +import { renderer } from "./renderer"; -import type { SyncedLyricsPluginConfig } from './types'; +import type { SyncedLyricsPluginConfig } from "./types"; +import { backend } from "./backend"; export default createPlugin({ - name: () => t('plugins.synced-lyrics.name'), - description: () => t('plugins.synced-lyrics.description'), - authors: ['Non0reo', 'ArjixWasTaken', 'KimJammer'], + name: () => t("plugins.synced-lyrics.name"), + description: () => t("plugins.synced-lyrics.description"), + authors: ["Non0reo", "ArjixWasTaken", "KimJammer"], restartNeeded: true, - addedVersion: '3.5.X', + addedVersion: "3.5.X", config: { enabled: false, preciseTiming: true, showLyricsEvenIfInexact: true, showTimeCodes: false, - defaultTextString: '♪', - lineEffect: 'fancy', + defaultTextString: "♪", + lineEffect: "fancy", } satisfies SyncedLyricsPluginConfig, + backend, menu, renderer, stylesheets: [style], diff --git a/src/plugins/synced-lyrics/renderer/components/SyncedLine.tsx b/src/plugins/synced-lyrics/renderer/components/SyncedLine.tsx index 6f3e868322..fb1e4bffe4 100644 --- a/src/plugins/synced-lyrics/renderer/components/SyncedLine.tsx +++ b/src/plugins/synced-lyrics/renderer/components/SyncedLine.tsx @@ -1,9 +1,9 @@ -import { createEffect, createMemo, For } from 'solid-js'; +import { createEffect, createMemo, createResource, For, Show } from 'solid-js'; import { currentTime } from './LyricsContainer'; import { config } from '../renderer'; -import { _ytAPI } from '..'; +import { _ytAPI, syncedLyricsIPC } from '..'; import type { LineLyrics } from '../../types'; @@ -28,10 +28,21 @@ export const SyncedLine = ({ line }: SyncedLineProps) => { }); const text = createMemo(() => { - if (line.text.trim()) return line.text; - return config()?.defaultTextString ?? ''; + if (!line.text.trim()) { + return config()?.defaultTextString ?? ''; + } + + return line.text; }); + const [romaji] = createResource( + () => 1, + async () => { + // prettier-ignore + return await syncedLyricsIPC()?.invoke('synced-lyrics:romanize-line', text()); + }, + ); + if (!text()) { return ( { _ytAPI?.seekTo(line.timeInMs / 1000); }} > -
+
- - {(word, index) => { - return ( - - - +
{ + div.style.setProperty( + '--lyrics-duration', + `${line.duration / 1000}s`, + 'important', ); + + console.log(div, div.style.getPropertyValue('--lyrics-duration')); }} - + style={{ display: 'flex', 'flex-direction': 'column' }} + > + + + {(word, index) => { + return ( + + + + ); + }} + + + + + + + {(word, index) => { + return ( + + + + ); + }} + + + +
); diff --git a/src/plugins/synced-lyrics/renderer/index.ts b/src/plugins/synced-lyrics/renderer/index.ts index 06762b4914..9e741eae7c 100644 --- a/src/plugins/synced-lyrics/renderer/index.ts +++ b/src/plugins/synced-lyrics/renderer/index.ts @@ -1,19 +1,24 @@ -import { createRenderer } from '@/utils'; -import { waitForElement } from '@/utils/wait-for-element'; +import { createRenderer } from "@/utils"; +import { waitForElement } from "@/utils/wait-for-element"; -import { selectors, tabStates } from './utils'; -import { setConfig } from './renderer'; -import { setCurrentTime } from './components/LyricsContainer'; +import { selectors, tabStates } from "./utils"; +import { setConfig } from "./renderer"; +import { setCurrentTime } from "./components/LyricsContainer"; -import { fetchLyrics } from '../providers'; +import { fetchLyrics } from "../providers"; -import type { RendererContext } from '@/types/contexts'; -import type { YoutubePlayer } from '@/types/youtube-player'; -import type { SongInfo } from '@/providers/song-info'; -import type { SyncedLyricsPluginConfig } from '../types'; +import type { RendererContext } from "@/types/contexts"; +import type { YoutubePlayer } from "@/types/youtube-player"; +import type { SongInfo } from "@/providers/song-info"; +import type { SyncedLyricsPluginConfig } from "../types"; +import { createSignal } from "solid-js"; export let _ytAPI: YoutubePlayer | null = null; +export const [syncedLyricsIPC, setSyncedLyricsIPC] = createSignal< + RendererContext["ipc"] +>(); + export const renderer = createRenderer< { observerCallback: MutationCallback; @@ -32,11 +37,11 @@ export const renderer = createRenderer< const header = mutation.target as HTMLElement; switch (mutation.attributeName) { - case 'disabled': - header.removeAttribute('disabled'); + case "disabled": + header.removeAttribute("disabled"); break; - case 'aria-selected': - tabStates[header.ariaSelected ?? 'false'](); + case "aria-selected": + tabStates[header.ariaSelected ?? "false"](); break; } } @@ -45,7 +50,7 @@ export const renderer = createRenderer< async onPlayerApiReady(api: YoutubePlayer) { _ytAPI = api; - api.addEventListener('videodatachange', this.videoDataChange); + api.addEventListener("videodatachange", this.videoDataChange); await this.videoDataChange(); }, @@ -64,18 +69,19 @@ export const renderer = createRenderer< // Force the lyrics tab to be enabled at all times. const header = await waitForElement(selectors.head); { - header.removeAttribute('disabled'); - tabStates[header.ariaSelected ?? 'false'](); + header.removeAttribute("disabled"); + tabStates[header.ariaSelected ?? "false"](); } this.observer.observe(header, { attributes: true }); - header.removeAttribute('disabled'); + header.removeAttribute("disabled"); }, async start(ctx: RendererContext) { setConfig(await ctx.getConfig()); + setSyncedLyricsIPC(ctx.ipc); - ctx.ipc.on('ytmd:update-song-info', (info: SongInfo) => { + ctx.ipc.on("ytmd:update-song-info", (info: SongInfo) => { fetchLyrics(info); }); }, diff --git a/src/plugins/synced-lyrics/renderer/renderer.tsx b/src/plugins/synced-lyrics/renderer/renderer.tsx index cfbc3824d5..7c90506590 100644 --- a/src/plugins/synced-lyrics/renderer/renderer.tsx +++ b/src/plugins/synced-lyrics/renderer/renderer.tsx @@ -38,7 +38,10 @@ createEffect(() => { root.style.setProperty('--lyrics-active-offset', '0'); break; case 'scale': - root.style.setProperty('--lyrics-font-size', '1.4rem'); + root.style.setProperty( + '--lyrics-font-size', + 'clamp(1.4rem, 1.1vmax, 3rem)', + ); root.style.setProperty( '--lyrics-line-height', 'var(--ytmusic-body-line-height)', @@ -58,7 +61,10 @@ createEffect(() => { root.style.setProperty('--lyrics-active-offset', '0'); break; case 'offset': - root.style.setProperty('--lyrics-font-size', '1.4rem'); + root.style.setProperty( + '--lyrics-font-size', + 'clamp(1.4rem, 1.1vmax, 3rem)', + ); root.style.setProperty( '--lyrics-line-height', 'var(--ytmusic-body-line-height)', @@ -78,7 +84,10 @@ createEffect(() => { root.style.setProperty('--lyrics-active-offset', '5%'); break; case 'focus': - root.style.setProperty('--lyrics-font-size', '1.4rem'); + root.style.setProperty( + '--lyrics-font-size', + 'clamp(1.4rem, 1.1vmax, 3rem)', + ); root.style.setProperty( '--lyrics-line-height', 'var(--ytmusic-body-line-height)', diff --git a/src/plugins/synced-lyrics/style.css b/src/plugins/synced-lyrics/style.css index 0f8c2bb4f3..84648b0b82 100644 --- a/src/plugins/synced-lyrics/style.css +++ b/src/plugins/synced-lyrics/style.css @@ -8,6 +8,12 @@ display: block !important; } +@property --lyrics-duration { + syntax: '