Feat: more preferences (#106)
* add ffmpeg command check * may switch language * tweak
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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": "添加的文章",
|
||||
|
||||
@@ -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<Ffmpeg.FfprobeData> {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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 = () => {
|
||||
<div className="font-semibold mb-4 capitilized">{t("basicSettings")}</div>
|
||||
<UserSettings />
|
||||
<Separator />
|
||||
<LanguageSettings />
|
||||
<Separator />
|
||||
<LibraryPathSettings />
|
||||
<Separator />
|
||||
<FfmpegSettings />
|
||||
<Separator />
|
||||
<WhisperSettings />
|
||||
<Separator />
|
||||
<OpenaiSettings />
|
||||
@@ -104,6 +113,46 @@ const UserSettings = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const LanguageSettings = () => {
|
||||
const { language, switchLanguage } = useContext(AppSettingsProviderContext);
|
||||
|
||||
return (
|
||||
<div className="flex items-start justify-between py-4">
|
||||
<div className="">
|
||||
<div className="mb-2">{t("language")}</div>
|
||||
<div className="text-sm text-muted-foreground mb-2">
|
||||
{language === "en" ? "English" : "简体中文"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<div className="flex items-center justify-end space-x-2 mb-2">
|
||||
<Select
|
||||
value={language}
|
||||
onValueChange={(value: "en" | "zh-CN") => {
|
||||
switchLanguage(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="text-xs">
|
||||
<SelectValue>
|
||||
{language === "en" ? "English" : "简体中文"}
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem className="text-xs" value="en">
|
||||
English
|
||||
</SelectItem>
|
||||
<SelectItem className="text-xs" value="zh-CN">
|
||||
简体中文
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LibraryPathSettings = () => {
|
||||
const { libraryPath, EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
|
||||
@@ -152,6 +201,53 @@ const LibraryPathSettings = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const FfmpegSettings = () => {
|
||||
const { libraryPath, EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const [config, setConfig] = useState<FfmpegConfigType>();
|
||||
|
||||
useEffect(() => {
|
||||
EnjoyApp.settings.getFfmpegConfig().then((_config) => {
|
||||
setConfig(_config);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-start justify-between py-4">
|
||||
<div className="">
|
||||
<div className="mb-2">FFmpeg</div>
|
||||
<div className="text-sm text-muted-foreground mb-2">
|
||||
{config?.commandExists && t("usingInstalledFFmpeg")}
|
||||
{!config?.commandExists &&
|
||||
`${t("usingDownloadedFFmpeg")}: ${config?.ffmpegPath}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<div className="flex items-center justify-end space-x-2 mb-2">
|
||||
{config?.ffmpegPath && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
EnjoyApp.shell.openPath(libraryPath);
|
||||
}}
|
||||
>
|
||||
{t("open")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
EnjoyApp.ffmpeg.check();
|
||||
}}
|
||||
>
|
||||
{t("check")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WhisperSettings = () => {
|
||||
const { whisperModel, whisperModelsPath } = useContext(
|
||||
AppSettingsProviderContext
|
||||
|
||||
@@ -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<string>("");
|
||||
const [whisperModel, setWhisperModel] = useState<string>(null);
|
||||
const [ffmpegConfig, setFfmegConfig] = useState<FfmpegConfigType>(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 (
|
||||
<AppSettingsProviderContext.Provider
|
||||
value={{
|
||||
language,
|
||||
switchLanguage,
|
||||
EnjoyApp,
|
||||
version,
|
||||
webApi,
|
||||
|
||||
@@ -19,10 +19,9 @@ i18n
|
||||
.use(initReactI18next) // passes i18n down to react-i18next
|
||||
.init({
|
||||
resources,
|
||||
lng: "zh-CN", // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources
|
||||
// you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage
|
||||
// if you're using a language detector, do not define the lng option
|
||||
|
||||
lng: "en",
|
||||
supportedLngs: ["en", "zh-CN"],
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false, // react already safes from xss
|
||||
},
|
||||
|
||||
3
enjoy/src/types/enjoy-app.d.ts
vendored
3
enjoy/src/types/enjoy-app.d.ts
vendored
@@ -76,6 +76,8 @@ type EnjoyAppType = {
|
||||
) => Promise<void>;
|
||||
getFfmpegConfig: () => Promise<FfmpegConfigType>;
|
||||
setFfmpegConfig: () => Promise<void>;
|
||||
getLanguage: () => Promise<string>;
|
||||
switchLanguage: (language: string) => Promise<void>;
|
||||
};
|
||||
fs: {
|
||||
ensureDir: (path: string) => Promise<boolean>;
|
||||
@@ -177,6 +179,7 @@ type EnjoyAppType = {
|
||||
};
|
||||
ffmpeg: {
|
||||
download: () => Promise<FfmpegConfigType>;
|
||||
check: () => Promise<boolean>;
|
||||
};
|
||||
download: {
|
||||
onState: (callback: (event, state) => void) => void;
|
||||
|
||||
Reference in New Issue
Block a user