diff --git a/package.json b/package.json index bebfe14..0889446 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "@electron-toolkit/tsconfig": "^1.0.1", "@eslint/js": "^9.31.0", "@rushstack/eslint-patch": "^1.10.3", - "@types/howler": "^2.2.12", "@types/node": "^20.14.8", "@types/tinycolor2": "^1.4.6", @@ -84,8 +83,8 @@ "eslint-plugin-vue": "^10.3.0", "eslint-plugin-vue-scoped-css": "^2.11.0", "globals": "^16.3.0", - "lint-staged": "^15.2.10", "howler": "^2.2.4", + "lint-staged": "^15.2.10", "lodash": "^4.17.21", "marked": "^15.0.4", "naive-ui": "^2.41.0", @@ -105,6 +104,7 @@ "vite-plugin-compression": "^0.5.1", "vite-plugin-vue-devtools": "7.7.2", "vue": "^3.5.13", + "vue-eslint-parser": "^10.2.0", "vue-router": "^4.5.0", "vue-tsc": "^2.0.22" }, @@ -204,5 +204,11 @@ "shortcutName": "AlgerMusicPlayer", "include": "build/installer.nsh" } + }, + "pnpm": { + "onlyBuiltDependencies": [ + "electron", + "esbuild" + ] } } diff --git a/src/i18n/lang/en-US/settings.ts b/src/i18n/lang/en-US/settings.ts index cad2a1a..81768a0 100644 --- a/src/i18n/lang/en-US/settings.ts +++ b/src/i18n/lang/en-US/settings.ts @@ -258,6 +258,12 @@ export default { lyricLines: 'Lyric Lines', mobileUnavailable: 'This setting is only available on mobile devices' }, + // Lyric translation engine + translationEngine: 'Lyric Translation Engine', + translationEngineOptions: { + none: 'Off', + opencc: 'OpenCC Traditionalize' + }, themeColor: { title: 'Lyric Theme Color', presetColors: 'Preset Colors', diff --git a/src/i18n/lang/ja-JP/settings.ts b/src/i18n/lang/ja-JP/settings.ts index 8b93e4f..bea1c59 100644 --- a/src/i18n/lang/ja-JP/settings.ts +++ b/src/i18n/lang/ja-JP/settings.ts @@ -257,6 +257,12 @@ export default { lyricLines: '歌詞行数', mobileUnavailable: 'この設定はモバイルでのみ利用可能です' }, + // 歌詞翻訳エンジン + translationEngine: '歌詞翻訳エンジン', + translationEngineOptions: { + none: 'オフ', + opencc: 'OpenCC 繁体字化' + }, themeColor: { title: '歌詞テーマカラー', presetColors: 'プリセットカラー', diff --git a/src/i18n/lang/zh-CN/settings.ts b/src/i18n/lang/zh-CN/settings.ts index 0ed3a1a..5493beb 100644 --- a/src/i18n/lang/zh-CN/settings.ts +++ b/src/i18n/lang/zh-CN/settings.ts @@ -258,6 +258,12 @@ export default { lyricLines: '歌词行数', mobileUnavailable: '此设置仅在移动端可用' }, + // 歌词翻译引擎相关 + translationEngine: '歌詞翻譯引擎', + translationEngineOptions: { + none: '关闭', + opencc: 'OpenCC 繁化' + }, themeColor: { title: '歌词主题色', presetColors: '预设颜色', diff --git a/src/i18n/lang/zh-Hant/settings.ts b/src/i18n/lang/zh-Hant/settings.ts index 679417c..fec3156 100644 --- a/src/i18n/lang/zh-Hant/settings.ts +++ b/src/i18n/lang/zh-Hant/settings.ts @@ -255,6 +255,12 @@ export default { }, placeholder: '#1db954' }, + // 歌詞翻譯引擎相關 + translationEngine: '歌詞翻譯引擎', + translationEngineOptions: { + none: '關閉', + opencc: 'OpenCC 繁化' + }, cookie: { title: 'Cookie設定', description: '請輸入網易雲音樂的Cookie:', diff --git a/src/main/set.json b/src/main/set.json index ca146fd..93d8bd4 100644 --- a/src/main/set.json +++ b/src/main/set.json @@ -15,6 +15,7 @@ "musicApiPort": 30488, "closeAction": "ask", "musicQuality": "higher", + "lyricTranslationEngine": "none", "fontFamily": "system-ui", "fontScope": "global", "autoPlay": false, diff --git a/src/renderer/hooks/MusicHook.ts b/src/renderer/hooks/MusicHook.ts index 80c856d..c1c46a8 100644 --- a/src/renderer/hooks/MusicHook.ts +++ b/src/renderer/hooks/MusicHook.ts @@ -279,8 +279,15 @@ const setupMusicWatchers = () => { nextTick(async () => { console.log('歌曲切换,更新歌词数据'); // 更新歌词数据 - lrcArray.value = playMusic.value.lyric?.lrcArray || []; + const rawLrc = playMusic.value.lyric?.lrcArray || []; lrcTimeArray.value = playMusic.value.lyric?.lrcTimeArray || []; + try { + const { translateLyrics } = await import('@/services/lyricTranslation'); + lrcArray.value = await translateLyrics(rawLrc as any); + } catch (e) { + console.error('翻译歌词失败,使用原始歌词:', e); + lrcArray.value = rawLrc as any; + } // 当歌词数据更新时,如果歌词窗口打开,则发送数据 if (isElectron && isLyricWindowOpen.value) { diff --git a/src/renderer/services/lyricTranslation.ts b/src/renderer/services/lyricTranslation.ts new file mode 100644 index 0000000..49865ae --- /dev/null +++ b/src/renderer/services/lyricTranslation.ts @@ -0,0 +1,28 @@ +import { useSettingsStore } from '@/store/modules/settings'; +import type { ILyricText } from '@/types/music'; + +/** + * Translate lyric lines according to selected engine. + * Supports runtime-loading `opencc-rust` from CDN and caches the converter. + */ +export async function translateLyrics(lines: ILyricText[] | undefined) { + if (!lines || lines.length === 0) return lines || []; + + const settingsStore = useSettingsStore(); + const engine = settingsStore.setData?.lyricTranslationEngine || 'none'; + + switch (engine) { + case 'opencc': { + const mod: any = await import('./translation-engines/opencc'); + const engineMod = await mod.ensureOpenccConverter(); + return engineMod.translateLines(lines); + } + default: { + return lines.map((l) => ({ ...l, trText: l.trText || '' })); + } + } +} + +export default { + translateLyrics +}; diff --git a/src/renderer/services/translation-engines/index.ts b/src/renderer/services/translation-engines/index.ts new file mode 100644 index 0000000..3a39b84 --- /dev/null +++ b/src/renderer/services/translation-engines/index.ts @@ -0,0 +1,3 @@ +// Re-export available translation engines from this folder. +// Add more engines here as separate files and re-export them. +export * from './opencc'; diff --git a/src/renderer/services/translation-engines/opencc.ts b/src/renderer/services/translation-engines/opencc.ts new file mode 100644 index 0000000..a86153e --- /dev/null +++ b/src/renderer/services/translation-engines/opencc.ts @@ -0,0 +1,94 @@ +import type { ILyricText } from '@/types/music'; + +let _inited = false; +let _converter: any = null; + +export async function init(): Promise { + if (_inited) return; + const mod: any = await import('https://cdn.jsdelivr.net/npm/opencc-rust/dist/opencc-rust.mjs'); + if (!mod?.initOpenccRust || !mod?.getConverter) { + throw new Error('opencc-rust module missing expected exports'); + } + await mod.initOpenccRust(); + _converter = mod.getConverter(); + _inited = true; +} + +export async function convert(text: string): Promise { + await init(); + if (!_converter) return text; + return _converter.convert(text); +} + +export async function convertLines(lines: string[]) { + await init(); + if (!_converter) return lines.slice(); + + const cjkRe = /[\u4e00-\u9fff]/; + + const convertOne = async (s: string) => { + const src = (s || '').trim(); + if (!src || !cjkRe.test(src)) return ''; + try { + return await _converter.convert(src); + } catch (e) { + console.warn('opencc convertLines item failed:', e); + return ''; + } + }; + + const results = await Promise.all(lines.map((s) => convertOne(s))); + return results; +} + +export async function translateLines(lines: ILyricText[]) { + if (!lines || lines.length === 0) return lines.slice(); + const srcInfo = lines.map((l) => { + const hasTr = !!(l.trText && l.trText.trim() !== ''); + return { + src: hasTr ? (l.trText || '').trim() : (l.text || '').trim(), + targetIsTr: hasTr + }; + }); + + const srcLines = srcInfo.map((s) => s.src); + const converted = await convertLines(srcLines); + + return lines.map((l, i) => { + const { targetIsTr } = srcInfo[i]; + const conv = converted[i] || ''; + if (targetIsTr) { + const tr = conv || l.trText || l.text || ''; + return { ...l, trText: tr }; + } + const txt = conv || l.text || ''; + return { ...l, text: txt }; + }); +} + +export default { + init, + convert, + convertLines, + translateLines +}; + +// Ensure the engine is initialized and return public API. +let _ensurePromise: Promise | null = null; +export async function ensureOpenccConverter() { + if (_ensurePromise) return _ensurePromise; + _ensurePromise = (async () => { + try { + await init(); + } catch (e) { + console.warn('opencc ensureOpenccConverter init failed:', e); + } + return { + init, + convert, + convertLines, + translateLines + }; + })(); + return _ensurePromise; +} diff --git a/src/renderer/types/lyric.ts b/src/renderer/types/lyric.ts index 4bd7ca3..fe4d205 100644 --- a/src/renderer/types/lyric.ts +++ b/src/renderer/types/lyric.ts @@ -7,6 +7,7 @@ export interface LyricConfig { showTranslation: boolean; theme: 'default' | 'light' | 'dark'; hidePlayBar: boolean; + translationEngine?: 'none' | 'opencc'; pureModeEnabled: boolean; hideMiniPlayBar: boolean; hideLyrics: boolean; @@ -31,7 +32,9 @@ export const DEFAULT_LYRIC_CONFIG: LyricConfig = { // 移动端默认配置 mobileLayout: 'ios', mobileCoverStyle: 'full', - mobileShowLyricLines: 3 + mobileShowLyricLines: 3, + // 翻译引擎: 'none' or 'opencc' + translationEngine: 'none' }; export interface ILyric { diff --git a/src/renderer/types/opencc-rust.d.ts b/src/renderer/types/opencc-rust.d.ts new file mode 100644 index 0000000..2d3d83d --- /dev/null +++ b/src/renderer/types/opencc-rust.d.ts @@ -0,0 +1,19 @@ +declare module 'https://cdn.jsdelivr.net/npm/opencc-rust/dist/opencc-rust.mjs' { + export function initOpenccRust(): Promise; + export function getConverter(): { + convert: (text: string) => Promise; + }; +} + +// Allow wildcard import if different CDN URL is used +declare module 'opencc-rust' { + export function initOpenccRust(): Promise; + export function getConverter(): { + convert: (text: string) => Promise; + }; +} + +declare module './translation-engines/opencc' { + export function init(): Promise; + export function convert(text: string): Promise; +} diff --git a/src/renderer/views/set/index.vue b/src/renderer/views/set/index.vue index daa6492..d2636ed 100644 --- a/src/renderer/views/set/index.vue +++ b/src/renderer/views/set/index.vue @@ -59,6 +59,21 @@ +
+
+
{{ t('settings.translationEngine') }}
+
{{ t('settings.translationEngine') }}
+
+ +
+
{{ t('settings.basic.font') }}