Feat: more preferences (#106)

* add ffmpeg command check

* may switch language

* tweak
This commit is contained in:
an-lee
2024-01-14 16:54:15 +08:00
committed by GitHub
parent afb818f215
commit d2510d00cb
10 changed files with 212 additions and 14 deletions

View File

@@ -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",

View File

@@ -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": "添加的文章",

View File

@@ -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;
});
}
}

View File

@@ -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
},

View File

@@ -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,
};

View File

@@ -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: (

View File

@@ -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

View File

@@ -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,

View File

@@ -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
},

View File

@@ -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;