diff --git a/enjoy/src/i18n/en.json b/enjoy/src/i18n/en.json index 41b1755f..a7fa58d1 100644 --- a/enjoy/src/i18n/en.json +++ b/enjoy/src/i18n/en.json @@ -193,6 +193,8 @@ "checkIfFfmpegIsInstalled": "Check if FFmpeg is installed", "ffmpegInstalled": "FFmpeg is installed", "ffmpegNotInstalled": "FFmpeg is not installed.", + "usingInstalledFFmpeg": "Using installed FFmpeg", + "usingDownloadedFFmpeg": "Using downloaded FFmpeg", "downloadFfmpeg": "Download FFmpeg", "youAreReadyToGo": "You are ready to go", "welcomeBack": "Welcome back! {{name}}", @@ -265,8 +267,9 @@ "recordingActivity": "recording activity", "recordingDetail": "Recording detail", "noRecordingActivities": "no recording activities", - "basicSettings": "basic", - "advancedSettings": "advanced", + "basicSettings": "Basic settings", + "advancedSettings": "Advanced settings", + "language": "Language", "sttAiModel": "STT AI model", "relaunchIsNeededAfterChanged": "Relaunch is needed after changed", "openaiKeySaved": "OpenAI key saved", @@ -301,7 +304,7 @@ "score": "score", "inputUrlToStartReading": "Input url to start reading", "read": "read", - "add_story": "add story", + "addStory": "add story", "context": "context", "keyVocabulary": "key vocabulary", "addedStories": "added stories", diff --git a/enjoy/src/i18n/zh-CN.json b/enjoy/src/i18n/zh-CN.json index a9e2836e..55821d46 100644 --- a/enjoy/src/i18n/zh-CN.json +++ b/enjoy/src/i18n/zh-CN.json @@ -193,6 +193,8 @@ "checkIfFfmpegIsInstalled": "检查 FFmpeg 是否已正确安装", "ffmpegInstalled": "FFmpeg 已经安装", "ffmpegNotInstalled": "FFmpeg 未安装,软件部分功能依赖于 FFmpeg。", + "usingInstalledFFmpeg": "使用已安装的 FFmpeg", + "usingDownloadedFFmpeg": "使用下载的 FFmpeg", "downloadFfmpeg": "下载 FFmpeg", "youAreReadyToGo": "您已准备就绪", "welcomeBack": "欢迎回来, {{name}}", @@ -267,6 +269,7 @@ "noRecordingActivities": "没有练习活动", "basicSettings": "基本设置", "advancedSettings": "高级设置", + "language": "语言", "sttAiModel": "语音转文本 AI 模型", "relaunchIsNeededAfterChanged": "更改后需要重新启动", "openaiKeySaved": "OpenAI 密钥已保存", @@ -301,7 +304,7 @@ "score": "得分", "inputUrlToStartReading": "输入 URL 开始阅读", "read": "阅读", - "add_story": "添加文章", + "addStory": "添加文章", "context": "原文", "keyVocabulary": "关键词汇", "addedStories": "添加的文章", diff --git a/enjoy/src/main/ffmpeg.ts b/enjoy/src/main/ffmpeg.ts index 2b803434..e6959807 100644 --- a/enjoy/src/main/ffmpeg.ts +++ b/enjoy/src/main/ffmpeg.ts @@ -11,22 +11,37 @@ import storage from "@main/storage"; const logger = log.scope("ffmepg"); export default class FfmpegWrapper { public ffmpeg: Ffmpeg.FfmpegCommand; + public config: any; constructor() { - const config = settings.ffmpegConfig(); + this.config = settings.ffmpegConfig(); - if (config.commandExists) { + if (this.config.commandExists) { logger.info("Using system ffmpeg"); this.ffmpeg = Ffmpeg(); } else { logger.info("Using downloaded ffmpeg"); const ff = Ffmpeg(); - ff.setFfmpegPath(config.ffmpegPath); - ff.setFfprobePath(config.ffprobePath); + ff.setFfmpegPath(this.config.ffmpegPath); + ff.setFfprobePath(this.config.ffprobePath); this.ffmpeg = ff; } } + checkCommand() { + return new Promise((resolve, _reject) => { + this.ffmpeg.getAvailableFormats((err, formats) => { + if (err) { + logger.error("Command not valid:", err); + resolve(false); + } else { + logger.info("Command valid, available formats:", formats); + resolve(true); + } + }); + }); + } + generateMetadata(input: string): Promise { return new Promise((resolve, reject) => { this.ffmpeg @@ -292,9 +307,29 @@ export class FfmpegDownloader { logger.error(err); event.sender.send("on-notification", { type: "error", - title: `FFmpeg download failed: ${err.message}`, + message: `FFmpeg download failed: ${err.message}`, }); } }); + + ipcMain.handle("ffmpeg-check-command", async (event) => { + const ffmpeg = new FfmpegWrapper(); + const valid = await ffmpeg.checkCommand(); + + if (valid) { + event.sender.send("on-notification", { + type: "success", + message: `FFmpeg command valid, you're ready to go.`, + }); + } else { + logger.error("FFmpeg command not valid", ffmpeg.config); + event.sender.send("on-notification", { + type: "warning", + message: `FFmpeg command not valid, please check the log for detail.`, + }); + } + + return valid; + }); } } diff --git a/enjoy/src/main/i18n.ts b/enjoy/src/main/i18n.ts index 815d9f54..64d5d655 100644 --- a/enjoy/src/main/i18n.ts +++ b/enjoy/src/main/i18n.ts @@ -1,6 +1,7 @@ import * as i18n from "i18next"; import en from "@/i18n/en.json"; import zh_CN from "@/i18n/zh-CN.json"; +import settings from "@main/settings"; const resources = { en: { @@ -13,7 +14,9 @@ const resources = { i18n.init({ resources, - lng: "zh-CN", + lng: settings.language(), + supportedLngs: ["en", "zh-CN"], + fallbackLng: "en", interpolation: { escapeValue: false, // react already safes from xss }, diff --git a/enjoy/src/main/settings.ts b/enjoy/src/main/settings.ts index 8c025f41..eceff075 100644 --- a/enjoy/src/main/settings.ts +++ b/enjoy/src/main/settings.ts @@ -6,9 +6,25 @@ import fs from "fs-extra"; import os from "os"; import commandExists from "command-exists"; import log from "electron-log"; +import * as i18n from "i18next"; const logger = log.scope("settings"); +const language = () => { + const _language = settings.getSync("language"); + + if (!_language || typeof _language !== "string") { + settings.setSync("language", "en"); + } + + return settings.getSync("language") as string; +}; + +const switchLanguage = (language: string) => { + settings.setSync("language", language); + i18n.changeLanguage(language); +}; + const libraryPath = () => { const _library = settings.getSync("library"); @@ -178,6 +194,14 @@ export default { settings.setSync("ffmpeg.ffmpegPath", config.ffmpegPath); settings.setSync("ffmpeg.ffprobePath", config.ffrobePath); }); + + ipcMain.handle("settings-get-language", (_event) => { + return language(); + }); + + ipcMain.handle("settings-switch-language", (_event, language) => { + switchLanguage(language); + }); }, cachePath, libraryPath, @@ -188,5 +212,7 @@ export default { userDataPath, dbPath, ffmpegConfig, + language, + switchLanguage, ...settings, }; diff --git a/enjoy/src/preload.ts b/enjoy/src/preload.ts index 82af7a4c..5dea150f 100644 --- a/enjoy/src/preload.ts +++ b/enjoy/src/preload.ts @@ -143,6 +143,12 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", { getFfmpegConfig: () => { return ipcRenderer.invoke("settings-get-ffmpeg-config"); }, + getLanguage: (language: string) => { + return ipcRenderer.invoke("settings-get-language", language); + }, + switchLanguage: (language: string) => { + return ipcRenderer.invoke("settings-switch-language", language); + } }, path: { join: (...paths: string[]) => { @@ -338,6 +344,9 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", { download: () => { return ipcRenderer.invoke("ffmpeg-download"); }, + check: () => { + return ipcRenderer.invoke("ffmpeg-check-command"); + }, }, download: { onState: ( diff --git a/enjoy/src/renderer/components/preferences/basic-settings.tsx b/enjoy/src/renderer/components/preferences/basic-settings.tsx index 4cbb56f0..fe2d4230 100644 --- a/enjoy/src/renderer/components/preferences/basic-settings.tsx +++ b/enjoy/src/renderer/components/preferences/basic-settings.tsx @@ -23,6 +23,11 @@ import { Label, Separator, toast, + Select, + SelectTrigger, + SelectItem, + SelectValue, + SelectContent, } from "@renderer/components/ui"; import { WhisperModelOptions } from "@renderer/components"; import { @@ -39,8 +44,12 @@ export const BasicSettings = () => {
{t("basicSettings")}
+ + + + @@ -104,6 +113,46 @@ const UserSettings = () => { ); }; +const LanguageSettings = () => { + const { language, switchLanguage } = useContext(AppSettingsProviderContext); + + return ( +
+
+
{t("language")}
+
+ {language === "en" ? "English" : "简体中文"} +
+
+ +
+
+ +
+
+
+ ); +}; + const LibraryPathSettings = () => { const { libraryPath, EnjoyApp } = useContext(AppSettingsProviderContext); @@ -152,6 +201,53 @@ const LibraryPathSettings = () => { ); }; +const FfmpegSettings = () => { + const { libraryPath, EnjoyApp } = useContext(AppSettingsProviderContext); + const [config, setConfig] = useState(); + + useEffect(() => { + EnjoyApp.settings.getFfmpegConfig().then((_config) => { + setConfig(_config); + }); + }, []); + + return ( +
+
+
FFmpeg
+
+ {config?.commandExists && t("usingInstalledFFmpeg")} + {!config?.commandExists && + `${t("usingDownloadedFFmpeg")}: ${config?.ffmpegPath}`} +
+
+
+
+ {config?.ffmpegPath && ( + + )} + +
+
+
+ ); +}; + const WhisperSettings = () => { const { whisperModel, whisperModelsPath } = useContext( AppSettingsProviderContext diff --git a/enjoy/src/renderer/context/app-settings-provider.tsx b/enjoy/src/renderer/context/app-settings-provider.tsx index c57521fb..97a914d7 100644 --- a/enjoy/src/renderer/context/app-settings-provider.tsx +++ b/enjoy/src/renderer/context/app-settings-provider.tsx @@ -1,6 +1,7 @@ import { createContext, useEffect, useState } from "react"; import { WEB_API_URL } from "@/constants"; import { Client } from "@/api"; +import i18n from "@renderer/i18n"; type AppSettingsProviderState = { webApi: Client; @@ -17,6 +18,8 @@ type AppSettingsProviderState = { ffmpegConfig?: FfmpegConfigType; setFfmegConfig?: (config: FfmpegConfigType) => void; EnjoyApp?: EnjoyAppType; + language?: "en" | "zh-CN"; + switchLanguage?: (language: "en" | "zh-CN") => void; }; const initialState: AppSettingsProviderState = { @@ -42,6 +45,7 @@ export const AppSettingsProvider = ({ const [whisperModelsPath, setWhisperModelsPath] = useState(""); const [whisperModel, setWhisperModel] = useState(null); const [ffmpegConfig, setFfmegConfig] = useState(null); + const [language, setLanguage] = useState<"en" | "zh-CN">(); const EnjoyApp = window.__ENJOY_APP__; useEffect(() => { @@ -50,6 +54,7 @@ export const AppSettingsProvider = ({ fetchLibraryPath(); fetchModel(); fetchFfmpegConfig(); + fetchLanguage(); }, []); useEffect(() => { @@ -71,6 +76,20 @@ export const AppSettingsProvider = ({ ); }, [user, apiUrl]); + const fetchLanguage = async () => { + const language = await EnjoyApp.settings.getLanguage(); + console.log(language); + setLanguage(language as "en" | "zh-CN"); + i18n.changeLanguage(language); + }; + + const switchLanguage = (language: "en" | "zh-CN") => { + EnjoyApp.settings.switchLanguage(language).then(() => { + i18n.changeLanguage(language); + setLanguage(language); + }); + }; + const fetchFfmpegConfig = async () => { const config = await EnjoyApp.settings.getFfmpegConfig(); setFfmegConfig(config); @@ -150,6 +169,8 @@ export const AppSettingsProvider = ({ return ( Promise; getFfmpegConfig: () => Promise; setFfmpegConfig: () => Promise; + getLanguage: () => Promise; + switchLanguage: (language: string) => Promise; }; fs: { ensureDir: (path: string) => Promise; @@ -177,6 +179,7 @@ type EnjoyAppType = { }; ffmpeg: { download: () => Promise; + check: () => Promise; }; download: { onState: (callback: (event, state) => void) => void;