Improve: use database to save user settings (#1041)
* add user settings * fix user setting * migrate dict settings * migrate hotkeys * fix hotkey setting * update library settings * migrate gpt Engine * use user setting key enum * migrate openai * migrate more settings * migrate whisper config * migrate whisper * refactor * clean up * migrate profile * migrate recorder config * refactor * refactor * fix e2e * add api status * fix e2e * fix app init * fetch apiUrl before fetch user * update stt engine enums * update enums * update enums * refactor login flow * Fix warning * Login from remembered users * fix e2e * refactor * add unauthorized alert * feat: 🎸 dict import update (#1040) * rectified. according to Issues. * issue #1025 * feat: add Vietnamese language to support (#1043) * feat: add vietnamese language to support * fix: update Vietnamese language name to native form --------- Co-authored-by: Ryan <trongdv@coccoc.com> * upgrade deps * update locales --------- Co-authored-by: divisey <18656007202@163.com> Co-authored-by: xiaolai <lixiaolai@gmail.com> Co-authored-by: ryan <69750456+ryangwn@users.noreply.github.com> Co-authored-by: Ryan <trongdv@coccoc.com>
This commit is contained in:
@@ -7,11 +7,11 @@
|
||||
"markdown-it-mathjax3": "^4.3.2",
|
||||
"markdown-it-sub": "^2.0.0",
|
||||
"markdown-it-sup": "^2.0.0",
|
||||
"mermaid": "^11.0.2",
|
||||
"sass": "^1.77.8",
|
||||
"mermaid": "^11.1.0",
|
||||
"sass": "^1.78.0",
|
||||
"vitepress": "^1.3.4",
|
||||
"vitepress-plugin-mermaid": "^2.0.16",
|
||||
"vue": "^3.4.38"
|
||||
"vue": "^3.5.3"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vitepress dev",
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/seo": "^2.0.0-rc.19",
|
||||
"nuxt": "^3.13.0",
|
||||
"nuxt-og-image": "^3.0.0-rc.65",
|
||||
"vue": "^3.4.38",
|
||||
"@nuxtjs/seo": "^2.0.0-rc.21",
|
||||
"nuxt": "^3.13.1",
|
||||
"nuxt-og-image": "^3.0.0-rc.66",
|
||||
"vue": "^3.5.3",
|
||||
"vue-router": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.43",
|
||||
"sass": "^1.77.8",
|
||||
"postcss": "^8.4.45",
|
||||
"sass": "^1.78.0",
|
||||
"tailwindcss": "^3.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,9 +66,6 @@ test("validate whisper command", async () => {
|
||||
});
|
||||
console.info(res.log);
|
||||
expect(res.success).toBeTruthy();
|
||||
|
||||
const settings = fs.readJsonSync(path.join(resultDir, "settings.json"));
|
||||
expect(settings.whisper.service).toBe("azure");
|
||||
});
|
||||
|
||||
test("valid ffmpeg command", async () => {
|
||||
@@ -83,9 +80,6 @@ test("validate echogarden align command", async () => {
|
||||
return window.__ENJOY_APP__.echogarden.check();
|
||||
});
|
||||
expect(res).toBeTruthy();
|
||||
|
||||
const settings = fs.readJsonSync(path.join(resultDir, "settings.json"));
|
||||
expect(settings.whisper.service).toBe("azure");
|
||||
});
|
||||
|
||||
test("should setup default library path", async () => {
|
||||
|
||||
@@ -61,13 +61,13 @@ test.describe("with login", async () => {
|
||||
settings.user = user;
|
||||
fs.writeJsonSync(path.join(resultDir, "settings.json"), settings);
|
||||
|
||||
page.route("**/api/me", (route) => {
|
||||
await page.route("**/api/me", (route) => {
|
||||
route.fulfill({
|
||||
json: user,
|
||||
});
|
||||
});
|
||||
|
||||
page.route("**/api/stories", (route) => {
|
||||
await page.route("**/api/stories", (route) => {
|
||||
route.fulfill({
|
||||
json: {
|
||||
stories: [],
|
||||
@@ -199,6 +199,7 @@ test.describe("with login", async () => {
|
||||
|
||||
// add to library
|
||||
await page.getByTestId("message-start-shadow").click();
|
||||
await page.getByTestId("transcribe-continue-button").click();
|
||||
await page.getByTestId("audio-player").waitFor();
|
||||
await page
|
||||
.getByTestId("media-player-container")
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"@electron-forge/plugin-vite": "^7.4.0",
|
||||
"@electron-forge/publisher-github": "^7.4.0",
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@playwright/test": "^1.47.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/ahoy.js": "^0.4.2",
|
||||
"@types/autosize": "^4.0.3",
|
||||
@@ -53,7 +53,7 @@
|
||||
"@types/lodash": "^4.17.7",
|
||||
"@types/mark.js": "^8.11.12",
|
||||
"@types/mustache": "^4.2.5",
|
||||
"@types/node": "^22.5.2",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/prop-types": "^15.7.12",
|
||||
"@types/rails__actioncable": "^6.1.11",
|
||||
"@types/react": "^18.3.5",
|
||||
@@ -61,16 +61,16 @@
|
||||
"@types/unzipper": "^0.10.10",
|
||||
"@types/validator": "^13.12.1",
|
||||
"@types/wavesurfer.js": "^6.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.3.0",
|
||||
"@typescript-eslint/parser": "^8.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.4.0",
|
||||
"@typescript-eslint/parser": "^8.4.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"electron": "^32.0.1",
|
||||
"electron": "^32.0.2",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-playwright-helpers": "^1.7.1",
|
||||
"eslint": "^9.9.1",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-import": "^2.30.0",
|
||||
"flora-colossus": "^2.0.0",
|
||||
"octokit": "^4.0.2",
|
||||
"progress": "^2.0.3",
|
||||
@@ -82,7 +82,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.2",
|
||||
"vite": "^5.4.3",
|
||||
"vite-plugin-static-copy": "^1.0.6",
|
||||
"zx": "^8.1.5"
|
||||
},
|
||||
@@ -90,7 +90,7 @@
|
||||
"@andrkrn/ffprobe-static": "^5.2.0",
|
||||
"@electron-forge/publisher-s3": "^7.4.0",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@langchain/community": "^0.2.31",
|
||||
"@langchain/community": "^0.2.32",
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
@@ -141,7 +141,7 @@
|
||||
"decamelize-keys": "^2.0.1",
|
||||
"echogarden": "^1.5.0",
|
||||
"electron-context-menu": "^4.0.4",
|
||||
"electron-log": "^5.1.7",
|
||||
"electron-log": "^5.2.0",
|
||||
"electron-settings": "^4.0.4",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
@@ -150,19 +150,19 @@
|
||||
"html-to-text": "^9.0.5",
|
||||
"https-proxy-agent": "^7.0.5",
|
||||
"i18next": "^23.14.0",
|
||||
"intl-tel-input": "^24.3.6",
|
||||
"intl-tel-input": "^24.4.0",
|
||||
"js-md5": "^0.8.3",
|
||||
"langchain": "^0.2.17",
|
||||
"langchain": "^0.2.18",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^11.0.0",
|
||||
"lucide-react": "^0.438.0",
|
||||
"lru-cache": "^11.0.1",
|
||||
"lucide-react": "^0.439.0",
|
||||
"mark.js": "^8.11.1",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.40.0",
|
||||
"mustache": "^4.2.0",
|
||||
"next-themes": "^0.3.0",
|
||||
"openai": "^4.57.0",
|
||||
"openai": "^4.58.0",
|
||||
"pitchfinder": "^2.3.2",
|
||||
"postcss": "^8.4.43",
|
||||
"postcss": "^8.4.45",
|
||||
"proxy-agent": "^6.4.0",
|
||||
"react": "^18.3.1",
|
||||
"react-activity-calendar": "^2.5.1",
|
||||
@@ -171,7 +171,7 @@
|
||||
"react-dom": "^18.3.1",
|
||||
"react-frame-component": "^5.2.7",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-hotkeys-hook": "^4.5.0",
|
||||
"react-hotkeys-hook": "^4.5.1",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^2.1.2",
|
||||
|
||||
@@ -14,8 +14,17 @@ export class Client {
|
||||
accessToken?: string;
|
||||
logger?: any;
|
||||
locale?: "en" | "zh-CN";
|
||||
onError?: (err: any) => void;
|
||||
onSuccess?: (res: any) => void;
|
||||
}) {
|
||||
const { baseUrl, accessToken, logger, locale = "en" } = options;
|
||||
const {
|
||||
baseUrl,
|
||||
accessToken,
|
||||
logger,
|
||||
locale = "en",
|
||||
onError,
|
||||
onSuccess,
|
||||
} = options;
|
||||
this.baseUrl = baseUrl;
|
||||
this.logger = logger || console;
|
||||
|
||||
@@ -40,6 +49,10 @@ export class Client {
|
||||
});
|
||||
this.api.interceptors.response.use(
|
||||
(response) => {
|
||||
if (onSuccess) {
|
||||
onSuccess(response);
|
||||
}
|
||||
|
||||
this.logger.debug(
|
||||
response.status,
|
||||
response.config.method.toUpperCase(),
|
||||
@@ -48,14 +61,22 @@ export class Client {
|
||||
return camelcaseKeys(response.data, { deep: true });
|
||||
},
|
||||
(err) => {
|
||||
if (onError) {
|
||||
onError(err);
|
||||
}
|
||||
|
||||
if (err.response) {
|
||||
this.logger.error(
|
||||
err.response.status,
|
||||
err.response.config.method.toUpperCase(),
|
||||
err.response.config.baseURL + err.response.config.url
|
||||
err.response.config.baseURL + err.response.config.url,
|
||||
err.response.data
|
||||
);
|
||||
this.logger.error(err.response.data);
|
||||
return Promise.reject(new Error(err.response.data));
|
||||
|
||||
if (err.response.data) {
|
||||
err.message = err.response.data;
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
this.logger.error(err.message);
|
||||
|
||||
@@ -401,10 +401,10 @@
|
||||
"sttAiService": "STT AI service",
|
||||
"local": "Local",
|
||||
"localSpeechToTextDescription": "Use local whisper model to transcribe. It is free.",
|
||||
"azureAi": "Azure AI",
|
||||
"azureSpeechToTextDescription": "Use Azure AI Speech to transcribe. It is a paid service.",
|
||||
"cloudflareAi": "Cloudflare AI",
|
||||
"cloudflareSpeechToTextDescription": "Use Cloudflare AI Worker to transcribe. It is in beta and free for now.",
|
||||
"enjoyAzure": "Azure AI (by Enjoy)",
|
||||
"enjoyAzureSpeechToTextDescription": "Use Azure AI Speech to transcribe. It is a paid service from Enjoy.",
|
||||
"enjoyCloudflare": "Cloudflare AI (by Enjoy)",
|
||||
"enjoyCloudflareSpeechToTextDescription": "Use Cloudflare AI Worker to transcribe. It is in beta and free for now.",
|
||||
"openaiSpeechToTextDescription": "Use openAI to transcribe using your own key.",
|
||||
"uploadSpeechToTextDescription": "Upload transcript file or input transcript text to align.",
|
||||
"checkingWhisper": "Checking whisper status",
|
||||
@@ -781,5 +781,7 @@
|
||||
"selectDir": "Select Folder",
|
||||
"selectFile": "Select File",
|
||||
"importMdictFile": "Import the original dictionary file",
|
||||
"mdictFileTip": "Directly import .mdx .mdd format files (.mdx files are required, .mdd files are optional and can have multiple), but there may be problems with style and usability."
|
||||
"mdictFileTip": "Directly import .mdx .mdd format files (.mdx files are required, .mdd files are optional and can have multiple), but there may be problems with style and usability.",
|
||||
"authorizationExpired": "Your authorization has expired. Please login again.",
|
||||
"selectUser": "Select user"
|
||||
}
|
||||
|
||||
@@ -401,11 +401,11 @@
|
||||
"sttAiService": "语音转文本服务",
|
||||
"local": "本地",
|
||||
"localSpeechToTextDescription": "使用本地 whisper 模型进行语音转文本,不会产生费用",
|
||||
"azureAi": "Azure AI",
|
||||
"azureSpeechToTextDescription": "使用 Azure AI Speech 进行语音转文本,收费服务",
|
||||
"cloudflareAi": "Cloudflare AI",
|
||||
"cloudflareSpeechToTextDescription": "使用 Cloudflare AI 进行语音转文本,目前免费",
|
||||
"openaiSpeechToTextDescription": "使用 OpenAI 进行语音转文本(需要 API 密钥)",
|
||||
"enjoyAzure": "Azure (Enjoy 提供)",
|
||||
"enjoyAzureSpeechToTextDescription": "使用 Azure AI Speech 进行语音转文本,Enjoy 提供的收费服务",
|
||||
"enjoyCloudflare": "Cloudflare AI (Enjoy 提供)",
|
||||
"enjoyCloudflareSpeechToTextDescription": "使用 Cloudflare AI Worker 进行语音转文本,Enjoy 提供的免费服务,有一定的使用限额",
|
||||
"openaiSpeechToTextDescription": "使用 OpenAI 进行语音转文本(需要自备 API 密钥)",
|
||||
"uploadSpeechToTextDescription": "上传字幕文件或者输入文本进行字幕对齐",
|
||||
"checkingWhisper": "正在检查 Whisper",
|
||||
"pleaseDownloadWhisperModelFirst": "请先下载 Whisper 模型",
|
||||
@@ -781,5 +781,7 @@
|
||||
"selectDir": "选择文件夹",
|
||||
"selectFile": "选择文件",
|
||||
"importMdictFile": "导入原词典文件",
|
||||
"mdictFileTip": "直接导入 .mdx .mdd 格式的文件 (.mdx 文件是必须的,.mdd 文件是可选的且可以有多个),不过样式和可用性可能存在问题。"
|
||||
"mdictFileTip": "直接导入 .mdx .mdd 格式的文件 (.mdx 文件是必须的,.mdd 文件是可选的且可以有多个),不过样式和可用性可能存在问题。",
|
||||
"authorizationExpired": "您的登录授权已过期,请重新登录。",
|
||||
"selectUser": "选择用户"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { app, BrowserWindow, protocol, net } from "electron";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import settings from "@main/settings";
|
||||
import "@main/i18n";
|
||||
import log from "@main/logger";
|
||||
import mainWindow from "@main/window";
|
||||
import ElectronSquirrelStartup from "electron-squirrel-startup";
|
||||
|
||||
@@ -188,6 +188,17 @@ class AudiosHandler {
|
||||
ipcMain.handle("audios-crop", this.crop);
|
||||
ipcMain.handle("audios-clean-up", this.cleanUp);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("audios-find-all");
|
||||
ipcMain.removeHandler("audios-find-one");
|
||||
ipcMain.removeHandler("audios-create");
|
||||
ipcMain.removeHandler("audios-update");
|
||||
ipcMain.removeHandler("audios-destroy");
|
||||
ipcMain.removeHandler("audios-upload");
|
||||
ipcMain.removeHandler("audios-crop");
|
||||
ipcMain.removeHandler("audios-clean-up");
|
||||
}
|
||||
}
|
||||
|
||||
export const audiosHandler = new AudiosHandler();
|
||||
|
||||
@@ -82,6 +82,14 @@ class CacheObjectsHandler {
|
||||
ipcMain.handle("cache-objects-clear", this.clear);
|
||||
ipcMain.handle("cache-objects-write-file", this.writeFile);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("cache-objects-get");
|
||||
ipcMain.removeHandler("cache-objects-set");
|
||||
ipcMain.removeHandler("cache-objects-delete");
|
||||
ipcMain.removeHandler("cache-objects-clear");
|
||||
ipcMain.removeHandler("cache-objects-write-file");
|
||||
}
|
||||
}
|
||||
|
||||
export const cacheObjectsHandler = new CacheObjectsHandler();
|
||||
|
||||
@@ -81,6 +81,14 @@ class ChatAgentsHandler {
|
||||
ipcMain.handle("chat-agents-update", this.update);
|
||||
ipcMain.handle("chat-agents-destroy", this.destroy);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("chat-agents-find-all");
|
||||
ipcMain.removeHandler("chat-agents-find-one");
|
||||
ipcMain.removeHandler("chat-agents-create");
|
||||
ipcMain.removeHandler("chat-agents-update");
|
||||
ipcMain.removeHandler("chat-agents-destroy");
|
||||
}
|
||||
}
|
||||
|
||||
export const chatAgentsHandler = new ChatAgentsHandler();
|
||||
|
||||
@@ -13,12 +13,15 @@ class ChatMembersHandler {
|
||||
private async findAll(
|
||||
_event: IpcMainEvent,
|
||||
options: FindOptions<Attributes<ChatMember>> & { query?: string }
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
register() {
|
||||
ipcMain.handle("chat-members-find-all", this.findAll);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("chat-members-find-all");
|
||||
}
|
||||
}
|
||||
|
||||
export const chatMembersHandler = new ChatMembersHandler();
|
||||
export const chatMembersHandler = new ChatMembersHandler();
|
||||
|
||||
@@ -141,6 +141,14 @@ class ChatMessagesHandler {
|
||||
ipcMain.handle("chat-messages-update", this.update);
|
||||
ipcMain.handle("chat-messages-destroy", this.destroy);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("chat-messages-find-all");
|
||||
ipcMain.removeHandler("chat-messages-find-one");
|
||||
ipcMain.removeHandler("chat-messages-create");
|
||||
ipcMain.removeHandler("chat-messages-update");
|
||||
ipcMain.removeHandler("chat-messages-destroy");
|
||||
}
|
||||
}
|
||||
|
||||
export const chatMessagesHandler = new ChatMessagesHandler();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ipcMain, IpcMainEvent } from "electron";
|
||||
import { Chat, ChatAgent, ChatMember } from "@main/db/models";
|
||||
import { Chat, ChatAgent, ChatMember, UserSetting } from "@main/db/models";
|
||||
import { FindOptions, WhereOptions, Attributes, Op } from "sequelize";
|
||||
import log from "@main/logger";
|
||||
import { t } from "i18next";
|
||||
import db from "@main/db";
|
||||
import settings from "@/main/settings";
|
||||
import { UserSettingKeyEnum } from "@/types/enums";
|
||||
|
||||
const logger = log.scope("db/handlers/chats-handler");
|
||||
|
||||
@@ -73,7 +73,9 @@ class ChatsHandler {
|
||||
|
||||
const transaction = await db.connection.transaction();
|
||||
if (!chatData.config?.sttEngine) {
|
||||
chatData.config.sttEngine = settings.whisperConfig().service;
|
||||
chatData.config.sttEngine = (await UserSetting.get(
|
||||
UserSettingKeyEnum.STT_ENGINE
|
||||
)) as string;
|
||||
}
|
||||
const chat = await Chat.create(chatData, {
|
||||
transaction,
|
||||
@@ -195,6 +197,14 @@ class ChatsHandler {
|
||||
ipcMain.handle("chats-update", this.update);
|
||||
ipcMain.handle("chats-destroy", this.destroy);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("chats-find-all");
|
||||
ipcMain.removeHandler("chats-find-one");
|
||||
ipcMain.removeHandler("chats-create");
|
||||
ipcMain.removeHandler("chats-update");
|
||||
ipcMain.removeHandler("chats-destroy");
|
||||
}
|
||||
}
|
||||
|
||||
export const chatsHandler = new ChatsHandler();
|
||||
|
||||
@@ -120,6 +120,14 @@ class ConversationsHandler {
|
||||
ipcMain.handle("conversations-update", this.update);
|
||||
ipcMain.handle("conversations-destroy", this.destroy);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("conversations-find-all");
|
||||
ipcMain.removeHandler("conversations-find-one");
|
||||
ipcMain.removeHandler("conversations-create");
|
||||
ipcMain.removeHandler("conversations-update");
|
||||
ipcMain.removeHandler("conversations-destroy");
|
||||
}
|
||||
}
|
||||
|
||||
export const conversationsHandler = new ConversationsHandler();
|
||||
|
||||
@@ -12,4 +12,5 @@ export * from "./recordings-handler";
|
||||
export * from "./speeches-handler";
|
||||
export * from "./segments-handler";
|
||||
export * from "./transcriptions-handler";
|
||||
export * from "./user-settings-handler";
|
||||
export * from "./videos-handler";
|
||||
|
||||
@@ -177,6 +177,16 @@ class MessagesHandler {
|
||||
ipcMain.handle("messages-destroy", this.destroy);
|
||||
ipcMain.handle("messages-create-speech", this.createSpeech);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("messages-find-all");
|
||||
ipcMain.removeHandler("messages-find-one");
|
||||
ipcMain.removeHandler("messages-create");
|
||||
ipcMain.removeHandler("messages-create-in-batch");
|
||||
ipcMain.removeHandler("messages-update");
|
||||
ipcMain.removeHandler("messages-destroy");
|
||||
ipcMain.removeHandler("messages-create-speech");
|
||||
}
|
||||
}
|
||||
|
||||
export const messagesHandler = new MessagesHandler();
|
||||
|
||||
@@ -165,6 +165,17 @@ class NotesHandler {
|
||||
ipcMain.handle("notes-create", this.create);
|
||||
ipcMain.handle("notes-sync", this.sync);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("notes-group-by-target");
|
||||
ipcMain.removeHandler("notes-group-by-segment");
|
||||
ipcMain.removeHandler("notes-find-all");
|
||||
ipcMain.removeHandler("notes-find");
|
||||
ipcMain.removeHandler("notes-update");
|
||||
ipcMain.removeHandler("notes-delete");
|
||||
ipcMain.removeHandler("notes-create");
|
||||
ipcMain.removeHandler("notes-sync");
|
||||
}
|
||||
}
|
||||
|
||||
export const notesHandler = new NotesHandler();
|
||||
|
||||
@@ -102,6 +102,14 @@ class PronunciationAssessmentsHandler {
|
||||
ipcMain.handle("pronunciation-assessments-update", this.update);
|
||||
ipcMain.handle("pronunciation-assessments-destroy", this.destroy);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("pronunciation-assessments-find-all");
|
||||
ipcMain.removeHandler("pronunciation-assessments-find-one");
|
||||
ipcMain.removeHandler("pronunciation-assessments-create");
|
||||
ipcMain.removeHandler("pronunciation-assessments-update");
|
||||
ipcMain.removeHandler("pronunciation-assessments-destroy");
|
||||
}
|
||||
}
|
||||
|
||||
export const pronunciationAssessmentsHandler =
|
||||
|
||||
@@ -342,6 +342,20 @@ class RecordingsHandler {
|
||||
ipcMain.handle("recordings-group-by-target", this.groupByTarget);
|
||||
ipcMain.handle("recordings-group-by-segment", this.groupBySegment);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("recordings-find-all");
|
||||
ipcMain.removeHandler("recordings-find-one");
|
||||
ipcMain.removeHandler("recordings-sync");
|
||||
ipcMain.removeHandler("recordings-sync-all");
|
||||
ipcMain.removeHandler("recordings-create");
|
||||
ipcMain.removeHandler("recordings-destroy");
|
||||
ipcMain.removeHandler("recordings-upload");
|
||||
ipcMain.removeHandler("recordings-stats");
|
||||
ipcMain.removeHandler("recordings-group-by-date");
|
||||
ipcMain.removeHandler("recordings-group-by-target");
|
||||
ipcMain.removeHandler("recordings-group-by-segment");
|
||||
}
|
||||
}
|
||||
|
||||
export const recordingsHandler = new RecordingsHandler();
|
||||
|
||||
@@ -57,6 +57,13 @@ class SegmentsHandler {
|
||||
ipcMain.handle("segments-find-all", this.findAll);
|
||||
ipcMain.handle("segments-sync", this.sync);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("segments-create");
|
||||
ipcMain.removeHandler("segments-find");
|
||||
ipcMain.removeHandler("segments-find-all");
|
||||
ipcMain.removeHandler("segments-sync");
|
||||
}
|
||||
}
|
||||
|
||||
export const segmentsHandler = new SegmentsHandler();
|
||||
|
||||
@@ -59,6 +59,11 @@ class SpeechesHandler {
|
||||
ipcMain.handle("speeches-find-one", this.findOne);
|
||||
ipcMain.handle("speeches-create", this.create);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("speeches-find-one");
|
||||
ipcMain.removeHandler("speeches-create");
|
||||
}
|
||||
}
|
||||
|
||||
export const speechesHandler = new SpeechesHandler();
|
||||
|
||||
@@ -60,6 +60,11 @@ class TranscriptionsHandler {
|
||||
ipcMain.handle("transcriptions-find-or-create", this.findOrCreate);
|
||||
ipcMain.handle("transcriptions-update", this.update);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("transcriptions-find-or-create");
|
||||
ipcMain.removeHandler("transcriptions-update");
|
||||
}
|
||||
}
|
||||
|
||||
export const transcriptionsHandler = new TranscriptionsHandler();
|
||||
|
||||
79
enjoy/src/main/db/handlers/user-settings-handler.ts
Normal file
79
enjoy/src/main/db/handlers/user-settings-handler.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { ipcMain, IpcMainEvent } from "electron";
|
||||
import { UserSetting } from "@main/db/models";
|
||||
import db from "@main/db";
|
||||
import { UserSettingKeyEnum } from "@/types/enums";
|
||||
|
||||
class UserSettingsHandler {
|
||||
private async get(event: IpcMainEvent, key: UserSettingKeyEnum) {
|
||||
return UserSetting.get(key)
|
||||
.then((value) => {
|
||||
return value;
|
||||
})
|
||||
.catch((err) => {
|
||||
event.sender.send("on-notification", {
|
||||
type: "error",
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async set(
|
||||
event: IpcMainEvent,
|
||||
key: UserSettingKeyEnum,
|
||||
value: string | object
|
||||
) {
|
||||
return UserSetting.set(key, value)
|
||||
.then(() => {
|
||||
return;
|
||||
})
|
||||
.catch((err) => {
|
||||
event.sender.send("on-notification", {
|
||||
type: "error",
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async delete(event: IpcMainEvent, key: UserSettingKeyEnum) {
|
||||
return UserSetting.destroy({ where: { key } })
|
||||
.then(() => {
|
||||
return;
|
||||
})
|
||||
.catch((err) => {
|
||||
event.sender.send("on-notification", {
|
||||
type: "error",
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async clear(event: IpcMainEvent) {
|
||||
return UserSetting.destroy({ where: {} })
|
||||
.then(() => {
|
||||
db.connection.query("VACUUM");
|
||||
return;
|
||||
})
|
||||
.catch((err) => {
|
||||
event.sender.send("on-notification", {
|
||||
type: "error",
|
||||
message: err.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
register() {
|
||||
ipcMain.handle("user-settings-get", this.get);
|
||||
ipcMain.handle("user-settings-set", this.set);
|
||||
ipcMain.handle("user-settings-delete", this.delete);
|
||||
ipcMain.handle("user-settings-clear", this.clear);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("user-settings-get");
|
||||
ipcMain.removeHandler("user-settings-set");
|
||||
ipcMain.removeHandler("user-settings-delete");
|
||||
ipcMain.removeHandler("user-settings-clear");
|
||||
}
|
||||
}
|
||||
|
||||
export const userSettingsHandler = new UserSettingsHandler();
|
||||
@@ -178,6 +178,17 @@ class VideosHandler {
|
||||
ipcMain.handle("videos-crop", this.crop);
|
||||
ipcMain.handle("videos-clean-up", this.cleanUp);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
ipcMain.removeHandler("videos-find-all");
|
||||
ipcMain.removeHandler("videos-find-one");
|
||||
ipcMain.removeHandler("videos-create");
|
||||
ipcMain.removeHandler("videos-update");
|
||||
ipcMain.removeHandler("videos-destroy");
|
||||
ipcMain.removeHandler("videos-upload");
|
||||
ipcMain.removeHandler("videos-crop");
|
||||
ipcMain.removeHandler("videos-clean-up");
|
||||
}
|
||||
}
|
||||
|
||||
export const videosHandler = new VideosHandler();
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
Audio,
|
||||
Recording,
|
||||
CacheObject,
|
||||
Chat,
|
||||
ChatAgent,
|
||||
ChatMember,
|
||||
ChatMessage,
|
||||
Conversation,
|
||||
Message,
|
||||
Note,
|
||||
@@ -14,14 +18,15 @@ import {
|
||||
Speech,
|
||||
Transcription,
|
||||
Video,
|
||||
Chat,
|
||||
ChatAgent,
|
||||
ChatMember,
|
||||
ChatMessage,
|
||||
UserSetting,
|
||||
} from "./models";
|
||||
import {
|
||||
audiosHandler,
|
||||
cacheObjectsHandler,
|
||||
chatAgentsHandler,
|
||||
chatMembersHandler,
|
||||
chatMessagesHandler,
|
||||
chatsHandler,
|
||||
conversationsHandler,
|
||||
messagesHandler,
|
||||
notesHandler,
|
||||
@@ -31,14 +36,13 @@ import {
|
||||
speechesHandler,
|
||||
transcriptionsHandler,
|
||||
videosHandler,
|
||||
chatAgentsHandler,
|
||||
chatMembersHandler,
|
||||
chatMessagesHandler,
|
||||
chatsHandler,
|
||||
userSettingsHandler,
|
||||
} from "./handlers";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import url from "url";
|
||||
import { i18n } from "@main/i18n";
|
||||
import { UserSettingKeyEnum } from "@/types/enums";
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -46,6 +50,7 @@ const __dirname = path.dirname(__filename);
|
||||
const db = {
|
||||
connection: null as Sequelize | null,
|
||||
connect: async () => {},
|
||||
disconnect: async () => {},
|
||||
registerIpcHandlers: () => {},
|
||||
};
|
||||
|
||||
@@ -53,12 +58,21 @@ db.connect = async () => {
|
||||
if (db.connection) {
|
||||
return;
|
||||
}
|
||||
const dbPath = settings.dbPath();
|
||||
if (!dbPath) {
|
||||
throw new Error("Db path is not ready");
|
||||
}
|
||||
|
||||
const sequelize = new Sequelize({
|
||||
dialect: "sqlite",
|
||||
storage: settings.dbPath(),
|
||||
storage: dbPath,
|
||||
models: [
|
||||
Audio,
|
||||
CacheObject,
|
||||
Chat,
|
||||
ChatAgent,
|
||||
ChatMember,
|
||||
ChatMessage,
|
||||
Conversation,
|
||||
Message,
|
||||
Note,
|
||||
@@ -67,11 +81,8 @@ db.connect = async () => {
|
||||
Segment,
|
||||
Speech,
|
||||
Transcription,
|
||||
UserSetting,
|
||||
Video,
|
||||
Chat,
|
||||
ChatAgent,
|
||||
ChatMember,
|
||||
ChatMessage,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -143,12 +154,25 @@ db.connect = async () => {
|
||||
});
|
||||
});
|
||||
|
||||
// migrate settings
|
||||
await UserSetting.migrateFromSettings();
|
||||
|
||||
// initialize i18n
|
||||
const language = (await UserSetting.get(
|
||||
UserSettingKeyEnum.LANGUAGE
|
||||
)) as string;
|
||||
i18n(language);
|
||||
|
||||
// vacuum the database
|
||||
await sequelize.query("VACUUM");
|
||||
|
||||
// register handlers
|
||||
audiosHandler.register();
|
||||
cacheObjectsHandler.register();
|
||||
chatAgentsHandler.register();
|
||||
chatMembersHandler.register();
|
||||
chatMessagesHandler.register();
|
||||
chatsHandler.register();
|
||||
conversationsHandler.register();
|
||||
messagesHandler.register();
|
||||
notesHandler.register();
|
||||
@@ -157,28 +181,53 @@ db.connect = async () => {
|
||||
segmentsHandler.register();
|
||||
speechesHandler.register();
|
||||
transcriptionsHandler.register();
|
||||
userSettingsHandler.register();
|
||||
videosHandler.register();
|
||||
chatAgentsHandler.register();
|
||||
chatMembersHandler.register();
|
||||
chatMessagesHandler.register();
|
||||
chatsHandler.register();
|
||||
};
|
||||
|
||||
db.disconnect = async () => {
|
||||
// unregister handlers
|
||||
audiosHandler.unregister();
|
||||
cacheObjectsHandler.unregister();
|
||||
chatAgentsHandler.unregister();
|
||||
chatMembersHandler.unregister();
|
||||
chatMessagesHandler.unregister();
|
||||
chatsHandler.unregister();
|
||||
conversationsHandler.unregister();
|
||||
messagesHandler.unregister();
|
||||
notesHandler.unregister();
|
||||
pronunciationAssessmentsHandler.unregister();
|
||||
recordingsHandler.unregister();
|
||||
segmentsHandler.unregister();
|
||||
speechesHandler.unregister();
|
||||
transcriptionsHandler.unregister();
|
||||
userSettingsHandler.unregister();
|
||||
videosHandler.unregister();
|
||||
|
||||
await db.connection?.close();
|
||||
db.connection = null;
|
||||
};
|
||||
|
||||
db.registerIpcHandlers = () => {
|
||||
ipcMain.handle("db-init", async () => {
|
||||
return db
|
||||
.connect()
|
||||
.then(() => {
|
||||
return {
|
||||
state: "connected",
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
return {
|
||||
state: "error",
|
||||
error: err.message,
|
||||
};
|
||||
});
|
||||
ipcMain.handle("db-connect", async () => {
|
||||
try {
|
||||
await db.connect();
|
||||
return {
|
||||
state: "connected",
|
||||
path: settings.dbPath(),
|
||||
error: null,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
state: "error",
|
||||
error: err.message,
|
||||
path: settings.dbPath(),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("db-disconnect", async () => {
|
||||
db.disconnect();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { DataTypes } from "sequelize";
|
||||
|
||||
async function up({ context: queryInterface }) {
|
||||
queryInterface.createTable(
|
||||
"user_settings",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
},
|
||||
key: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false,
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updated_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ["key"],
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function down({ context: queryInterface }) {
|
||||
queryInterface.dropTable("user_settings");
|
||||
}
|
||||
|
||||
export { up, down };
|
||||
@@ -11,5 +11,6 @@ export * from "./pronunciation-assessment";
|
||||
export * from "./recording";
|
||||
export * from "./segment";
|
||||
export * from "./speech";
|
||||
export * from "./user-setting";
|
||||
export * from "./transcription";
|
||||
export * from "./video";
|
||||
|
||||
176
enjoy/src/main/db/models/user-setting.ts
Normal file
176
enjoy/src/main/db/models/user-setting.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import {
|
||||
Table,
|
||||
Column,
|
||||
Default,
|
||||
IsUUID,
|
||||
Model,
|
||||
DataType,
|
||||
AllowNull,
|
||||
} from "sequelize-typescript";
|
||||
import log from "@main/logger";
|
||||
import settings from "@main/settings";
|
||||
import * as i18n from "i18next";
|
||||
import { SttEngineOptionEnum, UserSettingKeyEnum } from "@/types/enums";
|
||||
|
||||
const logger = log.scope("db/userSetting");
|
||||
|
||||
@Table({
|
||||
modelName: "UserSetting",
|
||||
tableName: "user_settings",
|
||||
underscored: true,
|
||||
timestamps: true,
|
||||
})
|
||||
export class UserSetting extends Model<UserSetting> {
|
||||
@IsUUID(4)
|
||||
@Default(DataType.UUIDV4)
|
||||
@Column({ primaryKey: true, type: DataType.UUID })
|
||||
id: string;
|
||||
|
||||
@Column(DataType.STRING)
|
||||
key: string;
|
||||
|
||||
@AllowNull(false)
|
||||
@Column(DataType.TEXT)
|
||||
value: string;
|
||||
|
||||
static async get(
|
||||
key: UserSettingKeyEnum
|
||||
): Promise<string | Object | number | null> {
|
||||
const setting = await UserSetting.findOne({ where: { key } });
|
||||
if (!setting) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(setting.value);
|
||||
} catch {
|
||||
return setting.value;
|
||||
}
|
||||
}
|
||||
|
||||
static async set(
|
||||
key: UserSettingKeyEnum,
|
||||
value: object | string
|
||||
): Promise<void> {
|
||||
const setting = await UserSetting.findOne({ where: { key } });
|
||||
|
||||
if (typeof value === "object") {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
|
||||
if (setting) {
|
||||
await setting.update({ value });
|
||||
} else {
|
||||
await UserSetting.create({ key, value });
|
||||
}
|
||||
|
||||
// update i18n
|
||||
if (key === UserSettingKeyEnum.LANGUAGE) {
|
||||
i18n.changeLanguage(value);
|
||||
}
|
||||
}
|
||||
|
||||
static async migrateFromSettings(): Promise<void> {
|
||||
// hotkeys
|
||||
const hotkeys = await UserSetting.get(UserSettingKeyEnum.HOTKEYS);
|
||||
const prevHotkeys = await settings.get("defaultHotkeys");
|
||||
if (prevHotkeys && !hotkeys) {
|
||||
UserSetting.set(UserSettingKeyEnum.HOTKEYS, prevHotkeys as object);
|
||||
}
|
||||
|
||||
// GPT Engine
|
||||
const gptEngine = await UserSetting.get(UserSettingKeyEnum.GPT_ENGINE);
|
||||
const prevGptEngine = await settings.get("engine.gpt");
|
||||
if (prevGptEngine && !gptEngine) {
|
||||
UserSetting.set(UserSettingKeyEnum.GPT_ENGINE, prevGptEngine as object);
|
||||
}
|
||||
|
||||
// OpenAI API Key
|
||||
const openai = await UserSetting.get(UserSettingKeyEnum.OPENAI);
|
||||
const prevOpenai = await settings.get("openai");
|
||||
if (prevOpenai && !openai) {
|
||||
UserSetting.set(UserSettingKeyEnum.OPENAI, prevOpenai as object);
|
||||
}
|
||||
|
||||
// Language
|
||||
const language = await UserSetting.get(UserSettingKeyEnum.LANGUAGE);
|
||||
const prevLanguage = await settings.get("language");
|
||||
if (prevLanguage && !language) {
|
||||
UserSetting.set(UserSettingKeyEnum.LANGUAGE, prevLanguage as string);
|
||||
}
|
||||
|
||||
// Native Language
|
||||
const nativeLanguage = await UserSetting.get(
|
||||
UserSettingKeyEnum.NATIVE_LANGUAGE
|
||||
);
|
||||
const prevNativeLanguage = await settings.get("nativeLanguage");
|
||||
if (prevNativeLanguage && !nativeLanguage) {
|
||||
UserSetting.set(
|
||||
UserSettingKeyEnum.NATIVE_LANGUAGE,
|
||||
prevNativeLanguage as string
|
||||
);
|
||||
}
|
||||
|
||||
// Learning Language
|
||||
const learningLanguage = await UserSetting.get(
|
||||
UserSettingKeyEnum.LEARNING_LANGUAGE
|
||||
);
|
||||
const prevLearningLanguage = await settings.get("learningLanguage");
|
||||
if (prevLearningLanguage && !learningLanguage) {
|
||||
UserSetting.set(
|
||||
UserSettingKeyEnum.LEARNING_LANGUAGE,
|
||||
prevLearningLanguage as string
|
||||
);
|
||||
}
|
||||
|
||||
// STT Engine
|
||||
const sttEngine = await UserSetting.get(UserSettingKeyEnum.STT_ENGINE);
|
||||
const prevSttEngine = await settings.get("whisper.service");
|
||||
if (prevSttEngine && !sttEngine) {
|
||||
switch (prevSttEngine) {
|
||||
case "azure":
|
||||
UserSetting.set(
|
||||
UserSettingKeyEnum.STT_ENGINE,
|
||||
SttEngineOptionEnum.ENJOY_AZURE
|
||||
);
|
||||
break;
|
||||
case "cloudflare":
|
||||
UserSetting.set(
|
||||
UserSettingKeyEnum.STT_ENGINE,
|
||||
SttEngineOptionEnum.ENJOY_CLOUDFLARE
|
||||
);
|
||||
break;
|
||||
case "openai":
|
||||
UserSetting.set(
|
||||
UserSettingKeyEnum.STT_ENGINE,
|
||||
SttEngineOptionEnum.OPENAI
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Whisper
|
||||
const whisper = await UserSetting.get(UserSettingKeyEnum.WHISPER);
|
||||
const prevWhisper = await settings.get("whisper.model");
|
||||
if (prevWhisper && !whisper) {
|
||||
UserSetting.set(UserSettingKeyEnum.WHISPER, prevWhisper as string);
|
||||
}
|
||||
|
||||
// Profile
|
||||
const profile = await UserSetting.get(UserSettingKeyEnum.PROFILE);
|
||||
const prevProfile = (await settings.get("user")) as UserType;
|
||||
if (prevProfile && !profile) {
|
||||
UserSetting.set(UserSettingKeyEnum.PROFILE, prevProfile as UserType);
|
||||
}
|
||||
|
||||
// Recorder Config
|
||||
const recorderConfig = await UserSetting.get(UserSettingKeyEnum.RECORDER);
|
||||
const prevRecorderConfig = await settings.get("recorderConfig");
|
||||
if (prevRecorderConfig && !recorderConfig) {
|
||||
UserSetting.set(
|
||||
UserSettingKeyEnum.RECORDER,
|
||||
prevRecorderConfig as string
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as i18n from "i18next";
|
||||
import * as i18next from "i18next";
|
||||
import en from "@/i18n/en.json";
|
||||
import zh_CN from "@/i18n/zh-CN.json";
|
||||
import settings from "@main/settings";
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
@@ -12,14 +11,14 @@ const resources = {
|
||||
},
|
||||
};
|
||||
|
||||
i18n.init({
|
||||
resources,
|
||||
lng: settings.language(),
|
||||
supportedLngs: ["en", "zh-CN"],
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false, // react already safes from xss
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
export const i18n = (language: string) => {
|
||||
i18next.init({
|
||||
resources,
|
||||
lng: language,
|
||||
supportedLngs: ["en", "zh-CN"],
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false, // react already safes from xss
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { LIBRARY_PATH_SUFFIX, DATABASE_NAME, WEB_API_URL } from "@/constants";
|
||||
import { ipcMain, app } from "electron";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import * as i18n from "i18next";
|
||||
import { AppSettingsKeyEnum } from "@/types/enums";
|
||||
|
||||
if (process.env.SETTINGS_PATH) {
|
||||
settings.configure({
|
||||
@@ -12,35 +12,23 @@ if (process.env.SETTINGS_PATH) {
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
if (!_library || typeof _library !== "string") {
|
||||
settings.setSync(
|
||||
"library",
|
||||
AppSettingsKeyEnum.LIBRARY,
|
||||
process.env.LIBRARY_PATH ||
|
||||
path.join(app.getPath("documents"), LIBRARY_PATH_SUFFIX)
|
||||
);
|
||||
} else if (path.parse(_library).base !== LIBRARY_PATH_SUFFIX) {
|
||||
settings.setSync("library", path.join(_library, LIBRARY_PATH_SUFFIX));
|
||||
settings.setSync(
|
||||
AppSettingsKeyEnum.LIBRARY,
|
||||
path.join(_library, LIBRARY_PATH_SUFFIX)
|
||||
);
|
||||
}
|
||||
|
||||
const library = settings.getSync("library") as string;
|
||||
const library = settings.getSync(AppSettingsKeyEnum.LIBRARY) as string;
|
||||
fs.ensureDirSync(library);
|
||||
|
||||
return library;
|
||||
@@ -54,165 +42,84 @@ const cachePath = () => {
|
||||
};
|
||||
|
||||
const dbPath = () => {
|
||||
if (!userDataPath()) return null;
|
||||
|
||||
const dbName = app.isPackaged
|
||||
? `${DATABASE_NAME}.sqlite`
|
||||
: `${DATABASE_NAME}_dev.sqlite`;
|
||||
return path.join(userDataPath(), dbName);
|
||||
};
|
||||
|
||||
const whisperConfig = (): WhisperConfigType => {
|
||||
const model = settings.getSync("whisper.model") as string;
|
||||
|
||||
let service = settings.getSync(
|
||||
"whisper.service"
|
||||
) as WhisperConfigType["service"];
|
||||
|
||||
if (!service) {
|
||||
settings.setSync("whisper.service", "azure");
|
||||
service = "azure";
|
||||
}
|
||||
|
||||
return {
|
||||
service,
|
||||
availableModels: settings.getSync(
|
||||
"whisper.availableModels"
|
||||
) as WhisperConfigType["availableModels"],
|
||||
modelsPath: settings.getSync("whisper.modelsPath") as string,
|
||||
model,
|
||||
};
|
||||
};
|
||||
|
||||
const userDataPath = () => {
|
||||
const userData = path.join(
|
||||
libraryPath(),
|
||||
settings.getSync("user.id").toString()
|
||||
);
|
||||
const userId = settings.getSync("user.id");
|
||||
if (!userId) return null;
|
||||
|
||||
const userData = path.join(libraryPath(), userId.toString());
|
||||
fs.ensureDirSync(userData);
|
||||
|
||||
return userData;
|
||||
};
|
||||
|
||||
const apiUrl = () => {
|
||||
const url: string = settings.getSync("apiUrl") as string;
|
||||
const url: string = settings.getSync(AppSettingsKeyEnum.API_URL) as string;
|
||||
return process.env.WEB_API_URL || url || WEB_API_URL;
|
||||
};
|
||||
|
||||
// scan library directory and get all user data directories
|
||||
// the name of user data directory is the user id, and they are all numbers and 8 digits
|
||||
const sessions = () => {
|
||||
const library = libraryPath();
|
||||
const sessions = fs.readdirSync(library).filter((dir) => {
|
||||
return dir.match(/^\d{8}$/);
|
||||
});
|
||||
return sessions.map((id) => ({ id: parseInt(id), name: id }));
|
||||
};
|
||||
|
||||
export default {
|
||||
registerIpcHandlers: () => {
|
||||
ipcMain.handle("settings-get", (_event, key) => {
|
||||
return settings.getSync(key);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-set", (_event, key, value) => {
|
||||
settings.setSync(key, value);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-get-library", (_event) => {
|
||||
ipcMain.handle("app-settings-get-library", (_event) => {
|
||||
libraryPath();
|
||||
return settings.getSync("library");
|
||||
return settings.getSync(AppSettingsKeyEnum.LIBRARY);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-set-library", (_event, library) => {
|
||||
ipcMain.handle("app-settings-set-library", (_event, library) => {
|
||||
if (path.parse(library).base === LIBRARY_PATH_SUFFIX) {
|
||||
settings.setSync("library", library);
|
||||
settings.setSync(AppSettingsKeyEnum.LIBRARY, library);
|
||||
} else {
|
||||
const dir = path.join(library, LIBRARY_PATH_SUFFIX);
|
||||
fs.ensureDirSync(dir);
|
||||
settings.setSync("library", dir);
|
||||
settings.setSync(AppSettingsKeyEnum.LIBRARY, dir);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-get-user", (_event) => {
|
||||
return settings.getSync("user");
|
||||
ipcMain.handle("app-settings-get-user", (_event) => {
|
||||
return settings.getSync(AppSettingsKeyEnum.USER);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-set-user", (_event, user) => {
|
||||
settings.setSync("user", user);
|
||||
ipcMain.handle("app-settings-set-user", (_event, user) => {
|
||||
settings.setSync(AppSettingsKeyEnum.USER, user);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-get-whisper-model", (_event) => {
|
||||
return settings.getSync("whisper.model");
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-set-whisper-model", (_event, model) => {
|
||||
settings.setSync("whisper.model", model);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-get-user-data-path", (_event) => {
|
||||
ipcMain.handle("app-settings-get-user-data-path", (_event) => {
|
||||
return userDataPath();
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-get-llm", (_event, provider) => {
|
||||
return settings.getSync(provider);
|
||||
ipcMain.handle("app-settings-get-api-url", (_event) => {
|
||||
return settings.getSync(AppSettingsKeyEnum.API_URL);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-set-llm", (_event, provider, config) => {
|
||||
return settings.setSync(provider, config);
|
||||
ipcMain.handle("app-settings-set-api-url", (_event, url) => {
|
||||
settings.setSync(AppSettingsKeyEnum.API_URL, url);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-get-language", (_event) => {
|
||||
return language();
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-switch-language", (_event, language) => {
|
||||
switchLanguage(language);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-get-default-engine", (_event) => {
|
||||
return settings.getSync("defaultEngine");
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-set-default-engine", (_event, engine) => {
|
||||
return settings.setSync("defaultEngine", engine);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-get-gpt-engine", (_event) => {
|
||||
return settings.getSync("engine.gpt");
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-set-gpt-engine", (_event, engine) => {
|
||||
return settings.setSync("engine.gpt", engine);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-get-default-hotkeys", (_event) => {
|
||||
return settings.getSync("defaultHotkeys");
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-set-default-hotkeys", (_event, records) => {
|
||||
return settings.setSync("defaultHotkeys", records);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-get-dict", (_event) => {
|
||||
return settings.getSync("dicts");
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-set-dicts", (_event, dict) => {
|
||||
return settings.setSync("dicts", dict);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-get-api-url", (_event) => {
|
||||
return settings.getSync("apiUrl");
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-set-api-url", (_event, url) => {
|
||||
return settings.setSync("apiUrl", url);
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-get-vocabulary-config", (_event) => {
|
||||
return settings.getSync("vocabularyConfig");
|
||||
});
|
||||
|
||||
ipcMain.handle("settings-set-vocabulary-config", (_event, records) => {
|
||||
return settings.setSync("vocabularyConfig", records);
|
||||
ipcMain.handle("app-settings-get-sessions", (_event) => {
|
||||
return sessions();
|
||||
});
|
||||
},
|
||||
cachePath,
|
||||
libraryPath,
|
||||
userDataPath,
|
||||
dbPath,
|
||||
whisperConfig,
|
||||
language,
|
||||
switchLanguage,
|
||||
apiUrl,
|
||||
...settings,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,9 @@ import log from "@main/logger";
|
||||
import url from "url";
|
||||
import { enjoyUrlToPath } from "./utils";
|
||||
import { t } from "i18next";
|
||||
import { UserSetting } from "@main/db/models";
|
||||
import db from "@main/db";
|
||||
import { UserSettingKeyEnum } from "@/types/enums";
|
||||
|
||||
const __filename = url.fileURLToPath(import.meta.url);
|
||||
/*
|
||||
@@ -40,7 +43,7 @@ class Whipser {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
async initialize() {
|
||||
const models = [];
|
||||
|
||||
const bundledModels = fs.readdirSync(this.bundledModelsDir);
|
||||
@@ -66,9 +69,23 @@ class Whipser {
|
||||
savePath: path.join(dir, file),
|
||||
});
|
||||
}
|
||||
settings.setSync("whisper.availableModels", models);
|
||||
settings.setSync("whisper.modelsPath", dir);
|
||||
this.config = settings.whisperConfig();
|
||||
|
||||
if (db.connection) {
|
||||
const whisperConfig = (await UserSetting.get(
|
||||
UserSettingKeyEnum.WHISPER
|
||||
)) as string;
|
||||
this.config = {
|
||||
model: whisperConfig || models[0].name,
|
||||
availableModels: models,
|
||||
modelsPath: dir,
|
||||
};
|
||||
} else {
|
||||
this.config = {
|
||||
model: models[0].name,
|
||||
availableModels: models,
|
||||
modelsPath: dir,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
currentModel() {
|
||||
@@ -82,9 +99,10 @@ class Whipser {
|
||||
}
|
||||
if (!model) {
|
||||
model = this.config.availableModels[0];
|
||||
this.config = Object.assign({}, this.config, { model: model.name });
|
||||
UserSetting.set(UserSettingKeyEnum.WHISPER, model.name);
|
||||
}
|
||||
|
||||
settings.setSync("whisper.model", model.name);
|
||||
return model;
|
||||
}
|
||||
|
||||
@@ -258,13 +276,13 @@ class Whipser {
|
||||
|
||||
registerIpcHandlers() {
|
||||
ipcMain.handle("whisper-config", async () => {
|
||||
await this.initialize();
|
||||
return this.config;
|
||||
});
|
||||
|
||||
ipcMain.handle("whisper-set-model", async (_event, model) => {
|
||||
const originalModel = settings.getSync("whisper.model");
|
||||
settings.setSync("whisper.model", model);
|
||||
this.config = settings.whisperConfig();
|
||||
const originalModel = this.config.model;
|
||||
this.config.model = model;
|
||||
|
||||
return this.check()
|
||||
.then(({ success, log }) => {
|
||||
@@ -275,26 +293,14 @@ class Whipser {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
settings.setSync("whisper.model", originalModel);
|
||||
this.config.model = originalModel;
|
||||
throw err;
|
||||
})
|
||||
.finally(() => {
|
||||
UserSetting.set(UserSettingKeyEnum.WHISPER, this.config.model);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle("whisper-set-service", async (_event, service) => {
|
||||
if (service === "local") {
|
||||
await this.check();
|
||||
settings.setSync("whisper.service", service);
|
||||
this.config.service = service;
|
||||
return this.config;
|
||||
} else if (["cloudflare", "azure", "openai"].includes(service)) {
|
||||
settings.setSync("whisper.service", service);
|
||||
this.config.service = service;
|
||||
return this.config;
|
||||
} else {
|
||||
throw new Error("Unknown service");
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("whisper-check", async (_event) => {
|
||||
return await this.check();
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@ import settings from "@main/settings";
|
||||
import downloader from "@main/downloader";
|
||||
import whisper from "@main/whisper";
|
||||
import fs from "fs-extra";
|
||||
import "@main/i18n";
|
||||
import log from "@main/logger";
|
||||
import { REPO_URL, WS_URL } from "@/constants";
|
||||
import { AudibleProvider, TedProvider, YoutubeProvider } from "@main/providers";
|
||||
@@ -42,7 +41,7 @@ const main = {
|
||||
init: () => {},
|
||||
};
|
||||
|
||||
main.init = () => {
|
||||
main.init = async () => {
|
||||
if (main.win) {
|
||||
main.win.show();
|
||||
return;
|
||||
|
||||
@@ -182,75 +182,44 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
||||
showErrorBox: (title: string, content: string) =>
|
||||
ipcRenderer.invoke("dialog-show-error-box", title, content),
|
||||
},
|
||||
settings: {
|
||||
appSettings: {
|
||||
get: (key: string) => {
|
||||
return ipcRenderer.invoke("settings-get", key);
|
||||
return ipcRenderer.invoke("app-settings-get", key);
|
||||
},
|
||||
set: (key: string, value: any) => {
|
||||
return ipcRenderer.invoke("settings-set", key, value);
|
||||
return ipcRenderer.invoke("app-settings-set", key, value);
|
||||
},
|
||||
getLibrary: () => {
|
||||
return ipcRenderer.invoke("settings-get-library");
|
||||
return ipcRenderer.invoke("app-settings-get-library");
|
||||
},
|
||||
setLibrary: (library: string) => {
|
||||
return ipcRenderer.invoke("settings-set-library", library);
|
||||
return ipcRenderer.invoke("app-settings-set-library", library);
|
||||
},
|
||||
getSessions: () => {
|
||||
return ipcRenderer.invoke("app-settings-get-sessions");
|
||||
},
|
||||
getUser: () => {
|
||||
return ipcRenderer.invoke("settings-get-user");
|
||||
return ipcRenderer.invoke("app-settings-get-user");
|
||||
},
|
||||
setUser: (user: UserType) => {
|
||||
return ipcRenderer.invoke("settings-set-user", user);
|
||||
return ipcRenderer.invoke("app-settings-set-user", user);
|
||||
},
|
||||
getUserDataPath: () => {
|
||||
return ipcRenderer.invoke("settings-get-user-data-path");
|
||||
},
|
||||
getDefaultEngine: () => {
|
||||
return ipcRenderer.invoke("settings-get-default-engine");
|
||||
},
|
||||
setDefaultEngine: (engine: "enjoyai" | "openai") => {
|
||||
return ipcRenderer.invoke("settings-set-default-engine", engine);
|
||||
},
|
||||
getGptEngine: () => {
|
||||
return ipcRenderer.invoke("settings-get-gpt-engine");
|
||||
},
|
||||
setGptEngine: (engine: GptEngineSettingType) => {
|
||||
return ipcRenderer.invoke("settings-set-gpt-engine", engine);
|
||||
},
|
||||
getLlm: (provider: string) => {
|
||||
return ipcRenderer.invoke("settings-get-llm", provider);
|
||||
},
|
||||
setLlm: (provider: string, config: LlmProviderType) => {
|
||||
return ipcRenderer.invoke("settings-set-llm", provider, config);
|
||||
},
|
||||
getLanguage: (language: string) => {
|
||||
return ipcRenderer.invoke("settings-get-language", language);
|
||||
},
|
||||
switchLanguage: (language: string) => {
|
||||
return ipcRenderer.invoke("settings-switch-language", language);
|
||||
},
|
||||
getDefaultHotkeys: () => {
|
||||
return ipcRenderer.invoke("settings-get-default-hotkeys");
|
||||
},
|
||||
setDefaultHotkeys: (records: Record<string, string>) => {
|
||||
return ipcRenderer.invoke("settings-set-default-hotkeys", records);
|
||||
},
|
||||
getDictSettings: () => {
|
||||
return ipcRenderer.invoke("settings-get-dict");
|
||||
},
|
||||
setDictSettings: (dict: DictSettingType) => {
|
||||
return ipcRenderer.invoke("settings-set-dicts", dict);
|
||||
return ipcRenderer.invoke("app-settings-get-user-data-path");
|
||||
},
|
||||
getApiUrl: () => {
|
||||
return ipcRenderer.invoke("settings-get-api-url");
|
||||
return ipcRenderer.invoke("app-settings-get-api-url");
|
||||
},
|
||||
setApiUrl: (url: string) => {
|
||||
return ipcRenderer.invoke("settings-set-api-url", url);
|
||||
return ipcRenderer.invoke("app-settings-set-api-url", url);
|
||||
},
|
||||
getVocabularyConfig: () => {
|
||||
return ipcRenderer.invoke("settings-get-vocabulary-config");
|
||||
},
|
||||
userSettings: {
|
||||
get: (key: string) => {
|
||||
return ipcRenderer.invoke("user-settings-get", key);
|
||||
},
|
||||
setVocabularyConfig: (records: Record<string, string>) => {
|
||||
return ipcRenderer.invoke("settings-set-vocabulary-config", records);
|
||||
set: (key: string, value: any) => {
|
||||
return ipcRenderer.invoke("user-settings-set", key, value);
|
||||
},
|
||||
},
|
||||
path: {
|
||||
@@ -259,7 +228,8 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
||||
},
|
||||
},
|
||||
db: {
|
||||
init: () => ipcRenderer.invoke("db-init"),
|
||||
connect: () => ipcRenderer.invoke("db-connect"),
|
||||
disconnect: () => ipcRenderer.invoke("db-disconnect"),
|
||||
onTransaction: (
|
||||
callback: (
|
||||
event: IpcRendererEvent,
|
||||
@@ -516,9 +486,6 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
||||
setModel: (model: string) => {
|
||||
return ipcRenderer.invoke("whisper-set-model", model);
|
||||
},
|
||||
setService: (service: string) => {
|
||||
return ipcRenderer.invoke("whisper-set-service", service);
|
||||
},
|
||||
check: () => {
|
||||
return ipcRenderer.invoke("whisper-check");
|
||||
},
|
||||
|
||||
@@ -35,21 +35,21 @@ function App() {
|
||||
|
||||
return (
|
||||
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
|
||||
<AppSettingsProvider>
|
||||
<HotKeysSettingsProvider>
|
||||
<AISettingsProvider>
|
||||
<DictProvider>
|
||||
<DbProvider>
|
||||
<DbProvider>
|
||||
<AppSettingsProvider>
|
||||
<HotKeysSettingsProvider>
|
||||
<AISettingsProvider>
|
||||
<DictProvider>
|
||||
<RouterProvider router={router} />
|
||||
<Toaster richColors closeButton position="top-center" />
|
||||
<Tooltip id="global-tooltip" />
|
||||
<TranslateWidget />
|
||||
<LookupWidget />
|
||||
</DbProvider>
|
||||
</DictProvider>
|
||||
</AISettingsProvider>
|
||||
</HotKeysSettingsProvider>
|
||||
</AppSettingsProvider>
|
||||
</DictProvider>
|
||||
</AISettingsProvider>
|
||||
</HotKeysSettingsProvider>
|
||||
</AppSettingsProvider>
|
||||
</DbProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,4 +46,8 @@ export class NoticiationsChannel {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
unsubscribe() {
|
||||
this.consumer.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,9 +192,9 @@ export const ChatForm = (props: {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="local">{t("local")}</SelectItem>
|
||||
<SelectItem value="azure">{t("azureAi")}</SelectItem>
|
||||
<SelectItem value="azure">{t("enjoyAzure")}</SelectItem>
|
||||
<SelectItem value="cloudflare">
|
||||
{t("cloudflareAi")}
|
||||
{t("enjoyCloudflare")}
|
||||
</SelectItem>
|
||||
<SelectItem value="openai">OpenAI</SelectItem>
|
||||
</SelectContent>
|
||||
@@ -203,9 +203,9 @@ export const ChatForm = (props: {
|
||||
{form.watch("config.sttEngine") === "local" &&
|
||||
t("localSpeechToTextDescription")}
|
||||
{form.watch("config.sttEngine") === "azure" &&
|
||||
t("azureSpeechToTextDescription")}
|
||||
t("enjoyAzureSpeechToTextDescription")}
|
||||
{form.watch("config.sttEngine") === "cloudflare" &&
|
||||
t("cloudflareSpeechToTextDescription")}
|
||||
t("enjoyCloudflareSpeechToTextDescription")}
|
||||
{form.watch("config.sttEngine") === "openai" &&
|
||||
t("openaiSpeechToTextDescription")}
|
||||
</FormDescription>
|
||||
|
||||
@@ -14,14 +14,11 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@renderer/components/ui";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CircleAlertIcon,
|
||||
LoaderIcon,
|
||||
} from "lucide-react";
|
||||
import { CheckCircleIcon, CircleAlertIcon, LoaderIcon } from "lucide-react";
|
||||
import { t } from "i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { TranscriptionCreateForm, TranscriptionsList } from "../transcriptions";
|
||||
import { SttEngineOptionEnum } from "@/types/enums";
|
||||
|
||||
export const MediaLoadingModal = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -68,7 +65,7 @@ export const MediaLoadingModal = () => {
|
||||
generateTranscription({
|
||||
originalText: data.text,
|
||||
language: data.language,
|
||||
service: data.service as WhisperConfigType["service"],
|
||||
service: data.service as SttEngineOptionEnum | "upload",
|
||||
isolate: data.isolate,
|
||||
});
|
||||
}}
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@renderer/components/ui";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
@@ -38,6 +40,9 @@ export const BanduLoginButton = () => {
|
||||
className="h-screen"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<SheetHeader>
|
||||
<SheetTitle className="sr-only">学升登录</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="w-full h-full flex">
|
||||
<div className="m-auto">{open && <BanduLoginForm />}</div>
|
||||
</div>
|
||||
|
||||
@@ -8,12 +8,6 @@ import { t } from "i18next";
|
||||
export const DbState = () => {
|
||||
const db = useContext(DbProviderContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (db.state === "disconnected") {
|
||||
db.connect();
|
||||
}
|
||||
}, [db.state]);
|
||||
|
||||
if (db.state === "connecting") {
|
||||
return (
|
||||
<div className="flex justify-center">
|
||||
|
||||
@@ -25,7 +25,7 @@ export const Layout = () => {
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<Link to="/landing" replace>
|
||||
<Link data-testid="landing-button" to="/landing" replace>
|
||||
<Button size="lg">{t("startToUse")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,10 @@ import {
|
||||
Separator,
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Avatar,
|
||||
AvatarImage,
|
||||
AvatarFallback,
|
||||
Button,
|
||||
toast,
|
||||
Tabs,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
@@ -28,40 +25,16 @@ import {
|
||||
NetworkState,
|
||||
} from "@renderer/components";
|
||||
import { EmailLoginForm } from "./email-login-form";
|
||||
import { Client } from "@/api";
|
||||
|
||||
export const LoginForm = () => {
|
||||
const { user, EnjoyApp, login, apiUrl } = useContext(
|
||||
AppSettingsProviderContext
|
||||
);
|
||||
const [rememberedUser, setRememberedUser] = useState(null);
|
||||
|
||||
const loginWithRememberedUser = async () => {
|
||||
if (!rememberedUser) return;
|
||||
const client = new Client({
|
||||
baseUrl: apiUrl,
|
||||
accessToken: rememberedUser.accessToken,
|
||||
});
|
||||
|
||||
client
|
||||
.me()
|
||||
.then((user) => {
|
||||
if (user?.id) {
|
||||
login(Object.assign({}, rememberedUser, user));
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
toast.error(error.message);
|
||||
});
|
||||
};
|
||||
const { user, EnjoyApp, login } = useContext(AppSettingsProviderContext);
|
||||
const [rememberedUsers, setRememberedUsers] = useState<{ id: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) return;
|
||||
|
||||
EnjoyApp.settings.getUser().then((user) => {
|
||||
setRememberedUser(user);
|
||||
EnjoyApp.appSettings.getSessions().then((sessions) => {
|
||||
setRememberedUsers(sessions);
|
||||
});
|
||||
}, [user]);
|
||||
}, []);
|
||||
|
||||
if (user) {
|
||||
return (
|
||||
@@ -73,77 +46,67 @@ export const LoginForm = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (rememberedUser) {
|
||||
return (
|
||||
<Tabs className="w-full max-w-md" defaultValue="login">
|
||||
<TabsList className="w-full grid grid-cols-2">
|
||||
<TabsTrigger value="login">{t("login")}</TabsTrigger>
|
||||
<TabsTrigger value="advanced">{t("advanced")}</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="login">
|
||||
<div className="px-4 py-2 border rounded-lg w-full max-w-md">
|
||||
<div className="flex items-start justify-between py-4">
|
||||
<div className="">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
crossOrigin="anonymous"
|
||||
src={rememberedUser.avatarUrl}
|
||||
/>
|
||||
<AvatarFallback className="text-xl">
|
||||
{rememberedUser.name[0].toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="">
|
||||
<div className="text-sm font-semibold">
|
||||
{rememberedUser.name}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{rememberedUser.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setRememberedUser(null)}
|
||||
>
|
||||
{t("reLogin")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={loginWithRememberedUser}
|
||||
>
|
||||
{t("login")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="advanced">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardContent className="mt-6">
|
||||
<ApiUrlSettings />
|
||||
<Separator />
|
||||
<ProxySettings />
|
||||
<Separator />
|
||||
<NetworkState />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs className="w-full max-w-md" defaultValue="login">
|
||||
<TabsList className="w-full grid grid-cols-2">
|
||||
<Tabs
|
||||
className="w-full max-w-md"
|
||||
defaultValue={rememberedUsers.length > 0 ? "selectUser" : "login"}
|
||||
>
|
||||
<TabsList
|
||||
className={`w-full grid grid-cols-${
|
||||
rememberedUsers.length > 0 ? 3 : 2
|
||||
}`}
|
||||
>
|
||||
{rememberedUsers.length > 0 && (
|
||||
<TabsTrigger value="selectUser">{t("selectUser")}</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="login">{t("login")}</TabsTrigger>
|
||||
<TabsTrigger value="advanced">{t("advanced")}</TabsTrigger>
|
||||
</TabsList>
|
||||
{rememberedUsers.length > 0 && (
|
||||
<TabsContent value="selectUser">
|
||||
<div className="grid gap-4 border rounded-lg">
|
||||
{rememberedUsers.map((rememberedUser) => (
|
||||
<div
|
||||
key={rememberedUser.id}
|
||||
className="px-4 py-2 border-b last:border-b-0 w-full max-w-md"
|
||||
>
|
||||
<div className="flex items-center justify-between py-4">
|
||||
<div className="">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
crossOrigin="anonymous"
|
||||
src={`https://api.dicebear.com/9.x/thumbs/svg?seed=${rememberedUser.id}`}
|
||||
/>
|
||||
<AvatarFallback className="text-xl"></AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="">
|
||||
<div className="text-sm font-semibold"></div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{rememberedUser.id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
<Button
|
||||
variant="default"
|
||||
data-testid="login-with-remembered-user-button"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
await EnjoyApp.appSettings.setUser(rememberedUser);
|
||||
login(rememberedUser);
|
||||
}}
|
||||
>
|
||||
{t("select")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
<TabsContent value="login">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardContent className="mt-6">
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@renderer/components/ui";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
@@ -37,6 +39,9 @@ export const MixinLoginButton = () => {
|
||||
className="h-screen"
|
||||
aria-describedby={undefined}
|
||||
>
|
||||
<SheetHeader>
|
||||
<SheetTitle className="sr-only">Mixin Messenger Login</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="w-full h-full flex">
|
||||
<div className="m-auto">{open && <MixinLoginForm />}</div>
|
||||
</div>
|
||||
@@ -141,7 +146,7 @@ export const MixinLoginForm = () => {
|
||||
setCountdown(120);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
toast.error(err.response?.data || err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
@@ -164,7 +169,7 @@ export const MixinLoginForm = () => {
|
||||
if (user?.id && user?.accessToken) login(user);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
toast.error(err.response?.data || err.message);
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -47,9 +47,15 @@ export const Sidebar = () => {
|
||||
const { EnjoyApp, cable } = useContext(AppSettingsProviderContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!cable) return;
|
||||
|
||||
const channel = new NoticiationsChannel(cable);
|
||||
channel.subscribe();
|
||||
}, []);
|
||||
|
||||
return () => {
|
||||
channel.unsubscribe();
|
||||
};
|
||||
}, [cable]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -190,7 +196,10 @@ export const Sidebar = () => {
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-w-screen-md xl:max-w-screen-lg h-5/6 p-0">
|
||||
<DialogContent
|
||||
aria-describedby={undefined}
|
||||
className="max-w-screen-md xl:max-w-screen-lg h-5/6 p-0"
|
||||
>
|
||||
<DialogTitle className="hidden">
|
||||
{t("sidebar.preferences")}
|
||||
</DialogTitle>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const ApiUrlSettings = () => {
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="flex items-start justify-between py-4">
|
||||
<div className="flex items-start justify-between space-x-2 py-4">
|
||||
<div className="">
|
||||
<div className="mb-2">{t("apiSettings")}</div>
|
||||
<div className="text-sm text-muted-foreground mb-2 ml-1">
|
||||
@@ -100,7 +100,7 @@ export const ApiUrlSettings = () => {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
<div className="text-xs text-muted-foreground text-right">
|
||||
<InfoIcon className="mr-1 w-3 h-3 inline" />
|
||||
<span>{t("reloadIsNeededAfterChanged")}</span>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@ export * from "./learning-language-settings";
|
||||
export * from "./default-engine-settings";
|
||||
export * from "./openai-settings";
|
||||
export * from "./library-settings";
|
||||
export * from "./whisper-settings";
|
||||
export * from "./stt-settings";
|
||||
|
||||
export * from "./user-settings";
|
||||
export * from "./email-settings";
|
||||
|
||||
@@ -25,8 +25,8 @@ export const LibrarySettings = () => {
|
||||
});
|
||||
|
||||
if (filePaths) {
|
||||
EnjoyApp.settings.setLibrary(filePaths[0]);
|
||||
const _library = await EnjoyApp.settings.getLibrary();
|
||||
EnjoyApp.appSettings.setLibrary(filePaths[0]);
|
||||
const _library = await EnjoyApp.appSettings.getLibrary();
|
||||
if (_library !== libraryPath) {
|
||||
EnjoyApp.app.relaunch();
|
||||
}
|
||||
@@ -83,9 +83,13 @@ const DiskUsage = () => {
|
||||
const [usage, setUsage] = useState<DiskUsageType>([]);
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
|
||||
const openPath = async (path: string) => {
|
||||
if (path) {
|
||||
await EnjoyApp.shell.openPath(path);
|
||||
const openPath = async (filePath: string) => {
|
||||
console.log(filePath);
|
||||
|
||||
if (filePath?.match(/.+\.json$/)) {
|
||||
await EnjoyApp.shell.openPath(filePath.split("/").slice(0, -1).join("/"));
|
||||
} else if (filePath) {
|
||||
await EnjoyApp.shell.openPath(filePath);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -21,11 +21,11 @@ import {
|
||||
} from "@renderer/context";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { InfoIcon, AlertCircleIcon } from "lucide-react";
|
||||
import { SttEngineOptionEnum } from "@/types/enums";
|
||||
|
||||
export const WhisperSettings = () => {
|
||||
const { whisperConfig, refreshWhisperConfig, setWhisperService } = useContext(
|
||||
AISettingsProviderContext
|
||||
);
|
||||
const { sttEngine, whisperConfig, refreshWhisperConfig, setSttEngine } =
|
||||
useContext(AISettingsProviderContext);
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const [stderr, setStderr] = useState("");
|
||||
|
||||
@@ -58,39 +58,47 @@ export const WhisperSettings = () => {
|
||||
<div className="">
|
||||
<div className="flex items-center mb-2">
|
||||
<span>{t("sttAiService")}</span>
|
||||
{stderr && <AlertCircleIcon className="ml-2 w-4 h-4 text-yellow-500" />}
|
||||
{stderr && (
|
||||
<AlertCircleIcon className="ml-2 w-4 h-4 text-yellow-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{whisperConfig?.service === "local" &&
|
||||
{sttEngine === SttEngineOptionEnum.LOCAL &&
|
||||
t("localSpeechToTextDescription")}
|
||||
{whisperConfig?.service === "azure" &&
|
||||
t("azureSpeechToTextDescription")}
|
||||
{whisperConfig?.service === "cloudflare" &&
|
||||
t("cloudflareSpeechToTextDescription")}
|
||||
{whisperConfig?.service === "openai" &&
|
||||
{sttEngine === SttEngineOptionEnum.ENJOY_AZURE &&
|
||||
t("enjoyAzureSpeechToTextDescription")}
|
||||
{sttEngine === SttEngineOptionEnum.ENJOY_CLOUDFLARE &&
|
||||
t("enjoyCloudflareSpeechToTextDescription")}
|
||||
{sttEngine === SttEngineOptionEnum.OPENAI &&
|
||||
t("openaiSpeechToTextDescription")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Select
|
||||
value={whisperConfig.service}
|
||||
value={sttEngine}
|
||||
onValueChange={(value) => {
|
||||
setWhisperService(value);
|
||||
setSttEngine(value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="min-w-fit">
|
||||
<SelectValue placeholder="service"></SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="local">{t("local")}</SelectItem>
|
||||
<SelectItem value="azure">{t("azureAi")}</SelectItem>
|
||||
<SelectItem value="cloudflare">{t("cloudflareAi")}</SelectItem>
|
||||
<SelectItem value="openai">OpenAI</SelectItem>
|
||||
<SelectItem value={SttEngineOptionEnum.LOCAL}>
|
||||
{t("local")}
|
||||
</SelectItem>
|
||||
<SelectItem value={SttEngineOptionEnum.ENJOY_AZURE}>
|
||||
{t("enjoyAzure")}
|
||||
</SelectItem>
|
||||
<SelectItem value={SttEngineOptionEnum.ENJOY_CLOUDFLARE}>
|
||||
{t("enjoyCloudflare")}
|
||||
</SelectItem>
|
||||
<SelectItem value={SttEngineOptionEnum.OPENAI}>OpenAI</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{whisperConfig.service === "local" && (
|
||||
{sttEngine === "local" && (
|
||||
<>
|
||||
<Button onClick={handleCheck} variant="secondary" size="sm">
|
||||
{t("check")}
|
||||
@@ -119,7 +127,7 @@ export const WhisperSettings = () => {
|
||||
</span>
|
||||
<Button
|
||||
onClick={() => {
|
||||
EnjoyApp.shell.openPath(whisperConfig.modelsPath);
|
||||
EnjoyApp.shell.openPath(whisperConfig?.modelsPath);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -127,7 +127,7 @@ export const UserSettings = () => {
|
||||
className="bg-destructive hover:bg-destructive-hover"
|
||||
onClick={() => {
|
||||
logout();
|
||||
redirect("/");
|
||||
redirect("/landing");
|
||||
}}
|
||||
>
|
||||
{t("logout")}
|
||||
|
||||
@@ -59,13 +59,13 @@ export const TranscriptionCreateForm = (props: {
|
||||
originalText,
|
||||
} = props;
|
||||
const { learningLanguage } = useContext(AppSettingsProviderContext);
|
||||
const { whisperConfig } = useContext(AISettingsProviderContext);
|
||||
const { sttEngine } = useContext(AISettingsProviderContext);
|
||||
|
||||
const form = useForm<z.infer<typeof transcriptionSchema>>({
|
||||
resolver: zodResolver(transcriptionSchema),
|
||||
values: {
|
||||
language: learningLanguage,
|
||||
service: originalText ? "upload" : whisperConfig.service,
|
||||
service: originalText ? "upload" : sttEngine,
|
||||
text: originalText,
|
||||
isolate: false,
|
||||
},
|
||||
@@ -166,9 +166,9 @@ export const TranscriptionCreateForm = (props: {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="local">{t("local")}</SelectItem>
|
||||
<SelectItem value="azure">{t("azureAi")}</SelectItem>
|
||||
<SelectItem value="azure">{t("enjoyAzure")}</SelectItem>
|
||||
<SelectItem value="cloudflare">
|
||||
{t("cloudflareAi")}
|
||||
{t("enjoyCloudflare")}
|
||||
</SelectItem>
|
||||
<SelectItem value="openai">OpenAI</SelectItem>
|
||||
<SelectItem value="upload">{t("upload")}</SelectItem>
|
||||
@@ -178,9 +178,9 @@ export const TranscriptionCreateForm = (props: {
|
||||
{form.watch("service") === "local" &&
|
||||
t("localSpeechToTextDescription")}
|
||||
{form.watch("service") === "azure" &&
|
||||
t("azureSpeechToTextDescription")}
|
||||
t("enjoyAzureSpeechToTextDescription")}
|
||||
{form.watch("service") === "cloudflare" &&
|
||||
t("cloudflareSpeechToTextDescription")}
|
||||
t("enjoyCloudflareSpeechToTextDescription")}
|
||||
{form.watch("service") === "openai" &&
|
||||
t("openaiSpeechToTextDescription")}
|
||||
{form.watch("service") === "upload" &&
|
||||
@@ -324,7 +324,12 @@ export const TranscriptionCreateForm = (props: {
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
)}
|
||||
<Button disabled={transcribing} type="submit" variant="default">
|
||||
<Button
|
||||
data-testid="transcribe-continue-button"
|
||||
disabled={transcribing}
|
||||
type="submit"
|
||||
variant="default"
|
||||
>
|
||||
{transcribing && <LoaderIcon className="animate-spin w-4 mr-2" />}
|
||||
{t("continue")}
|
||||
</Button>
|
||||
|
||||
@@ -7,10 +7,10 @@ export const Sentence = ({ sentence }: { sentence: string }) => {
|
||||
<span className="break-all align-middle">
|
||||
{words.map((word, index) => {
|
||||
return (
|
||||
<>
|
||||
<span key={index}>
|
||||
<Vocabulary key={index} word={word} context={sentence} />
|
||||
{index === words.length - 1 ? " " : " "}
|
||||
</>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { createContext, useEffect, useState, useContext } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import {
|
||||
AppSettingsProviderContext,
|
||||
DbProviderContext,
|
||||
} from "@renderer/context";
|
||||
import { SttEngineOptionEnum, UserSettingKeyEnum } from "@/types/enums";
|
||||
|
||||
type AISettingsProviderState = {
|
||||
setWhisperModel?: (name: string) => Promise<void>;
|
||||
setWhisperService?: (name: string) => Promise<void>;
|
||||
sttEngine?: SttEngineOptionEnum;
|
||||
setSttEngine?: (name: string) => Promise<void>;
|
||||
whisperConfig?: WhisperConfigType;
|
||||
refreshWhisperConfig?: () => void;
|
||||
openai?: LlmProviderType;
|
||||
@@ -30,19 +35,26 @@ export const AISettingsProvider = ({
|
||||
});
|
||||
const [openai, setOpenai] = useState<LlmProviderType>(null);
|
||||
const [whisperConfig, setWhisperConfig] = useState<WhisperConfigType>(null);
|
||||
const [sttEngine, setSttEngine] = useState<SttEngineOptionEnum>(
|
||||
SttEngineOptionEnum.ENJOY_AZURE
|
||||
);
|
||||
const { EnjoyApp, libraryPath, user, apiUrl } = useContext(
|
||||
AppSettingsProviderContext
|
||||
);
|
||||
const db = useContext(DbProviderContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (db.state !== "connected") return;
|
||||
|
||||
fetchSettings();
|
||||
}, []);
|
||||
}, [db.state]);
|
||||
|
||||
useEffect(() => {
|
||||
if (db.state !== "connected") return;
|
||||
if (!libraryPath) return;
|
||||
|
||||
refreshWhisperConfig();
|
||||
}, [libraryPath]);
|
||||
}, [db.state, libraryPath]);
|
||||
|
||||
const refreshWhisperConfig = async () => {
|
||||
const config = await EnjoyApp.whisper.config();
|
||||
@@ -56,34 +68,29 @@ export const AISettingsProvider = ({
|
||||
});
|
||||
};
|
||||
|
||||
const setWhisperService = async (name: WhisperConfigType["service"]) => {
|
||||
return EnjoyApp.whisper.setService(name).then((config) => {
|
||||
if (!config) return;
|
||||
setWhisperConfig(config);
|
||||
});
|
||||
const handleSetSttEngine = async (name: SttEngineOptionEnum) => {
|
||||
setSttEngine(name);
|
||||
return EnjoyApp.userSettings.set(UserSettingKeyEnum.STT_ENGINE, name);
|
||||
};
|
||||
|
||||
const fetchSettings = async () => {
|
||||
const _openai = await EnjoyApp.settings.getLlm("openai");
|
||||
const _sttEngine = await EnjoyApp.userSettings.get(
|
||||
UserSettingKeyEnum.STT_ENGINE
|
||||
);
|
||||
if (_sttEngine) {
|
||||
setSttEngine(_sttEngine);
|
||||
}
|
||||
|
||||
const _openai = await EnjoyApp.userSettings.get(UserSettingKeyEnum.OPENAI);
|
||||
if (_openai) {
|
||||
setOpenai(Object.assign({ name: "openai" }, _openai));
|
||||
}
|
||||
|
||||
const _defaultEngine = await EnjoyApp.settings.getDefaultEngine();
|
||||
const _gptEngine = await EnjoyApp.settings.getGptEngine();
|
||||
const _gptEngine = await EnjoyApp.userSettings.get(
|
||||
UserSettingKeyEnum.GPT_ENGINE
|
||||
);
|
||||
if (_gptEngine) {
|
||||
setGptEngine(_gptEngine);
|
||||
} else if (_defaultEngine) {
|
||||
// Migrate default engine to gpt engine
|
||||
const engine = {
|
||||
name: _defaultEngine,
|
||||
models: {
|
||||
default: "gpt-4o",
|
||||
},
|
||||
};
|
||||
EnjoyApp.settings.setGptEngine(engine).then(() => {
|
||||
setGptEngine(engine);
|
||||
});
|
||||
} else if (_openai?.key) {
|
||||
const engine = {
|
||||
name: "openai",
|
||||
@@ -91,9 +98,11 @@ export const AISettingsProvider = ({
|
||||
default: "gpt-4o",
|
||||
},
|
||||
};
|
||||
EnjoyApp.settings.setGptEngine(engine).then(() => {
|
||||
setGptEngine(engine);
|
||||
});
|
||||
EnjoyApp.userSettings
|
||||
.set(UserSettingKeyEnum.GPT_ENGINE, engine)
|
||||
.then(() => {
|
||||
setGptEngine(engine);
|
||||
});
|
||||
} else {
|
||||
const engine = {
|
||||
name: "enjoyai",
|
||||
@@ -101,35 +110,28 @@ export const AISettingsProvider = ({
|
||||
default: "gpt-4o",
|
||||
},
|
||||
};
|
||||
EnjoyApp.settings.setGptEngine(engine).then(() => {
|
||||
setGptEngine(engine);
|
||||
});
|
||||
EnjoyApp.userSettings
|
||||
.set(UserSettingKeyEnum.GPT_ENGINE, engine)
|
||||
.then(() => {
|
||||
setGptEngine(engine);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetLlm = async (
|
||||
name: SupportedLlmProviderType,
|
||||
config: LlmProviderType
|
||||
) => {
|
||||
await EnjoyApp.settings.setLlm(name, config);
|
||||
const _config = await EnjoyApp.settings.getLlm(name);
|
||||
|
||||
switch (name) {
|
||||
case "openai":
|
||||
setOpenai(Object.assign({ name: "openai" }, _config));
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unsupported LLM provider");
|
||||
}
|
||||
const handleSetOpenai = async (config: LlmProviderType) => {
|
||||
await EnjoyApp.userSettings.set(UserSettingKeyEnum.OPENAI, config);
|
||||
setOpenai(Object.assign({ name: "openai" }, config));
|
||||
};
|
||||
|
||||
return (
|
||||
<AISettingsProviderContext.Provider
|
||||
value={{
|
||||
setGptEngine: (engine: GptEngineSettingType) => {
|
||||
EnjoyApp.settings.setGptEngine(engine).then(() => {
|
||||
setGptEngine(engine);
|
||||
});
|
||||
EnjoyApp.userSettings
|
||||
.set(UserSettingKeyEnum.GPT_ENGINE, engine)
|
||||
.then(() => {
|
||||
setGptEngine(engine);
|
||||
});
|
||||
},
|
||||
currentEngine:
|
||||
gptEngine.name === "openai"
|
||||
@@ -142,11 +144,12 @@ export const AISettingsProvider = ({
|
||||
baseUrl: `${apiUrl}/api/ai`,
|
||||
}),
|
||||
openai,
|
||||
setOpenai: (config: LlmProviderType) => handleSetLlm("openai", config),
|
||||
setOpenai: (config: LlmProviderType) => handleSetOpenai(config),
|
||||
whisperConfig,
|
||||
refreshWhisperConfig,
|
||||
setWhisperModel,
|
||||
setWhisperService,
|
||||
sttEngine,
|
||||
setSttEngine: (name: SttEngineOptionEnum) => handleSetSttEngine(name),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { WEB_API_URL, LANGUAGES, IPA_MAPPINGS } from "@/constants";
|
||||
import { Client } from "@/api";
|
||||
import i18n from "@renderer/i18n";
|
||||
@@ -6,6 +6,8 @@ import ahoy from "ahoy.js";
|
||||
import { type Consumer, createConsumer } from "@rails/actioncable";
|
||||
import * as Sentry from "@sentry/electron/renderer";
|
||||
import { SENTRY_DSN } from "@/constants";
|
||||
import { DbProviderContext } from "@renderer/context";
|
||||
import { UserSettingKeyEnum } from "@/types/enums";
|
||||
|
||||
type AppSettingsProviderState = {
|
||||
webApi: Client;
|
||||
@@ -68,6 +70,7 @@ export const AppSettingsProvider = ({
|
||||
const [ipaMappings, setIpaMappings] = useState<{ [key: string]: string }>(
|
||||
IPA_MAPPINGS
|
||||
);
|
||||
const db = useContext(DbProviderContext);
|
||||
|
||||
const initSentry = () => {
|
||||
EnjoyApp.app.isPackaged().then((isPackaged) => {
|
||||
@@ -80,24 +83,30 @@ export const AppSettingsProvider = ({
|
||||
};
|
||||
|
||||
const fetchLanguages = async () => {
|
||||
const language = await EnjoyApp.settings.getLanguage();
|
||||
setLanguage(language as "en" | "zh-CN");
|
||||
const language = await EnjoyApp.userSettings.get(
|
||||
UserSettingKeyEnum.LANGUAGE
|
||||
);
|
||||
setLanguage((language as "en" | "zh-CN") || "en");
|
||||
i18n.changeLanguage(language);
|
||||
|
||||
const _nativeLanguage =
|
||||
(await EnjoyApp.settings.get("nativeLanguage")) || "zh-CN";
|
||||
(await EnjoyApp.userSettings.get(UserSettingKeyEnum.NATIVE_LANGUAGE)) ||
|
||||
"zh-CN";
|
||||
setNativeLanguage(_nativeLanguage);
|
||||
|
||||
const _learningLanguage =
|
||||
(await EnjoyApp.settings.get("learningLanguage")) || "en-US";
|
||||
(await EnjoyApp.userSettings.get(UserSettingKeyEnum.LEARNING_LANGUAGE)) ||
|
||||
"en-US";
|
||||
setLearningLanguage(_learningLanguage);
|
||||
};
|
||||
|
||||
const switchLanguage = (language: "en" | "zh-CN") => {
|
||||
EnjoyApp.settings.switchLanguage(language).then(() => {
|
||||
i18n.changeLanguage(language);
|
||||
setLanguage(language);
|
||||
});
|
||||
EnjoyApp.userSettings
|
||||
.set(UserSettingKeyEnum.LANGUAGE, language)
|
||||
.then(() => {
|
||||
i18n.changeLanguage(language);
|
||||
setLanguage(language);
|
||||
});
|
||||
};
|
||||
|
||||
const switchNativeLanguage = (lang: string) => {
|
||||
@@ -105,14 +114,14 @@ export const AppSettingsProvider = ({
|
||||
if (lang == learningLanguage) return;
|
||||
|
||||
setNativeLanguage(lang);
|
||||
EnjoyApp.settings.set("nativeLanguage", lang);
|
||||
EnjoyApp.userSettings.set(UserSettingKeyEnum.NATIVE_LANGUAGE, lang);
|
||||
};
|
||||
|
||||
const switchLearningLanguage = (lang: string) => {
|
||||
if (LANGUAGES.findIndex((l) => l.code == lang) < 0) return;
|
||||
if (lang == nativeLanguage) return;
|
||||
|
||||
EnjoyApp.settings.set("learningLanguage", lang);
|
||||
EnjoyApp.userSettings.set(UserSettingKeyEnum.LEARNING_LANGUAGE, lang);
|
||||
setLearningLanguage(lang);
|
||||
};
|
||||
|
||||
@@ -121,43 +130,40 @@ export const AppSettingsProvider = ({
|
||||
setVersion(version);
|
||||
};
|
||||
|
||||
const fetchUser = async () => {
|
||||
const fetchApiUrl = async () => {
|
||||
const apiUrl = await EnjoyApp.app.apiUrl();
|
||||
setApiUrl(apiUrl);
|
||||
|
||||
const currentUser = await EnjoyApp.settings.getUser();
|
||||
if (!currentUser) return;
|
||||
|
||||
const client = new Client({
|
||||
baseUrl: apiUrl,
|
||||
accessToken: currentUser.accessToken,
|
||||
});
|
||||
|
||||
client.me().then((user) => {
|
||||
if (user?.id) {
|
||||
login(Object.assign({}, currentUser, user));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const login = (user: UserType) => {
|
||||
const autoLogin = async () => {
|
||||
const currentUser = await EnjoyApp.appSettings.getUser();
|
||||
if (!currentUser) return;
|
||||
|
||||
setUser(currentUser);
|
||||
};
|
||||
|
||||
const login = async (user: UserType) => {
|
||||
if (!user?.id) return;
|
||||
|
||||
setUser(user);
|
||||
EnjoyApp.settings.setUser(user);
|
||||
createCable(user.accessToken);
|
||||
if (user.accessToken) {
|
||||
// Set current user to App settings
|
||||
EnjoyApp.appSettings.setUser({ id: user.id, name: user.name });
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
EnjoyApp.settings.setUser(null);
|
||||
EnjoyApp.appSettings.setUser(null);
|
||||
};
|
||||
|
||||
const fetchLibraryPath = async () => {
|
||||
const dir = await EnjoyApp.settings.getLibrary();
|
||||
const dir = await EnjoyApp.appSettings.getLibrary();
|
||||
setLibraryPath(dir);
|
||||
};
|
||||
|
||||
const setLibraryPathHandler = async (dir: string) => {
|
||||
await EnjoyApp.settings.setLibrary(dir);
|
||||
await EnjoyApp.appSettings.setLibrary(dir);
|
||||
setLibraryPath(dir);
|
||||
};
|
||||
|
||||
@@ -173,19 +179,21 @@ export const AppSettingsProvider = ({
|
||||
};
|
||||
|
||||
const setApiUrlHandler = async (url: string) => {
|
||||
EnjoyApp.settings.setApiUrl(url).then(() => {
|
||||
EnjoyApp.appSettings.setApiUrl(url).then(() => {
|
||||
EnjoyApp.app.reload();
|
||||
});
|
||||
};
|
||||
|
||||
const createCable = async (token: string) => {
|
||||
if (!token) return;
|
||||
|
||||
const wsUrl = await EnjoyApp.app.wsUrl();
|
||||
const consumer = createConsumer(wsUrl + "/cable?token=" + token);
|
||||
setCable(consumer);
|
||||
};
|
||||
|
||||
const fetchRecorderConfig = async () => {
|
||||
const config = await EnjoyApp.settings.get("recorderConfig");
|
||||
const config = await EnjoyApp.userSettings.get(UserSettingKeyEnum.RECORDER);
|
||||
if (config) {
|
||||
setRecorderConfig(config);
|
||||
} else {
|
||||
@@ -201,30 +209,45 @@ export const AppSettingsProvider = ({
|
||||
};
|
||||
|
||||
const setRecorderConfigHandler = async (config: RecorderConfigType) => {
|
||||
return EnjoyApp.settings.set("recorderConfig", config).then(() => {
|
||||
setRecorderConfig(config);
|
||||
});
|
||||
return EnjoyApp.userSettings
|
||||
.set(UserSettingKeyEnum.RECORDER, config)
|
||||
.then(() => {
|
||||
setRecorderConfig(config);
|
||||
});
|
||||
};
|
||||
|
||||
const fetchVocabularyConfig = async () => {
|
||||
const config = await EnjoyApp.settings.getVocabularyConfig();
|
||||
setVocabularyConfig(config || { lookupOnMouseOver: false });
|
||||
EnjoyApp.userSettings
|
||||
.get(UserSettingKeyEnum.VOCABULARY)
|
||||
.then((config) => {
|
||||
setVocabularyConfig(config || { lookupOnMouseOver: true });
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
setVocabularyConfig({ lookupOnMouseOver: true });
|
||||
});
|
||||
};
|
||||
|
||||
const setVocabularyConfigHandler = async (config: VocabularyConfigType) => {
|
||||
await EnjoyApp.settings.setVocabularyConfig(config);
|
||||
await EnjoyApp.userSettings.set(UserSettingKeyEnum.VOCABULARY, config);
|
||||
setVocabularyConfig(config);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchVersion();
|
||||
fetchUser();
|
||||
fetchLibraryPath();
|
||||
if (db.state !== "connected") return;
|
||||
|
||||
fetchLanguages();
|
||||
fetchProxyConfig();
|
||||
fetchVocabularyConfig();
|
||||
initSentry();
|
||||
fetchRecorderConfig();
|
||||
}, [db.state]);
|
||||
|
||||
useEffect(() => {
|
||||
autoLogin();
|
||||
fetchVersion();
|
||||
fetchLibraryPath();
|
||||
fetchProxyConfig();
|
||||
fetchApiUrl();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -235,6 +258,11 @@ export const AppSettingsProvider = ({
|
||||
baseUrl: apiUrl,
|
||||
accessToken: user?.accessToken,
|
||||
locale: language,
|
||||
onError: (err) => {
|
||||
if (user.accessToken && err.status == 401) {
|
||||
setUser({ ...user, accessToken: null });
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [user, apiUrl, language]);
|
||||
@@ -255,6 +283,27 @@ export const AppSettingsProvider = ({
|
||||
});
|
||||
}, [webApi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
|
||||
db.connect().then(async () => {
|
||||
// Login via API, update profile to DB
|
||||
if (user.accessToken) {
|
||||
EnjoyApp.userSettings.set(UserSettingKeyEnum.PROFILE, user);
|
||||
} else {
|
||||
// Auto login from local settings, get full profile from DB
|
||||
const profile = await EnjoyApp.userSettings.get(
|
||||
UserSettingKeyEnum.PROFILE
|
||||
);
|
||||
setUser(profile);
|
||||
EnjoyApp.appSettings.setUser({ id: profile.id, name: profile.name });
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
db.disconnect();
|
||||
};
|
||||
}, [user?.id]);
|
||||
|
||||
return (
|
||||
<AppSettingsProviderContext.Provider
|
||||
value={{
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { createContext, useState, useEffect, useContext } from "react";
|
||||
import { AppSettingsProviderContext } from "./app-settings-provider";
|
||||
import log from "electron-log/renderer";
|
||||
|
||||
type DbStateEnum = "connected" | "connecting" | "error" | "disconnected";
|
||||
type DbState = {
|
||||
type DbProviderState = {
|
||||
state: DbStateEnum;
|
||||
path?: string;
|
||||
error?: string;
|
||||
connect?: () => void;
|
||||
connect?: () => Promise<void>;
|
||||
disconnect?: () => Promise<void>;
|
||||
addDblistener?: (callback: (event: CustomEvent) => void) => void;
|
||||
removeDbListener?: (callback: (event: CustomEvent) => void) => void;
|
||||
};
|
||||
type DbProviderState = DbState & {
|
||||
connect?: () => void;
|
||||
};
|
||||
|
||||
const initialState: DbProviderState = {
|
||||
state: "disconnected",
|
||||
@@ -25,20 +22,44 @@ export const DbProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [state, setState] = useState<DbStateEnum>("disconnected");
|
||||
const [path, setPath] = useState();
|
||||
const [error, setError] = useState();
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const EnjoyApp = window.__ENJOY_APP__;
|
||||
|
||||
const connect = async () => {
|
||||
if (["connected", "connecting"].includes(state)) return;
|
||||
|
||||
console.info("--- connecting db ---");
|
||||
setState("connecting");
|
||||
|
||||
const _db = await EnjoyApp.db.init();
|
||||
|
||||
setState(_db.state);
|
||||
setPath(_db.path);
|
||||
setError(_db.error);
|
||||
return EnjoyApp.db
|
||||
.connect()
|
||||
.then((_db) => {
|
||||
setState(_db.state);
|
||||
setPath(_db.path);
|
||||
setError(_db.error);
|
||||
})
|
||||
.catch((err) => {
|
||||
setState("error");
|
||||
setError(err.message);
|
||||
});
|
||||
};
|
||||
|
||||
const disconnect = () => {
|
||||
console.info("--- disconnecting db ---");
|
||||
return EnjoyApp.db.disconnect().then(() => {
|
||||
setState("disconnected");
|
||||
setPath(undefined);
|
||||
setError(undefined);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.info(
|
||||
"--- db state changed ---\n",
|
||||
`state: ${state};\n`,
|
||||
`path: ${path};\n`,
|
||||
`error: ${error};\n`
|
||||
);
|
||||
}, [state]);
|
||||
|
||||
const addDblistener = (callback: (event: CustomEvent) => void) => {
|
||||
document.addEventListener("db-on-transaction", callback);
|
||||
};
|
||||
@@ -48,14 +69,14 @@ export const DbProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (state !== "connected") return;
|
||||
if (state === "connected") {
|
||||
EnjoyApp.db.onTransaction((_event, state) => {
|
||||
log.debug("db-on-transaction", state);
|
||||
|
||||
EnjoyApp.db.onTransaction((_event, state) => {
|
||||
log.debug("db-on-transaction", state);
|
||||
|
||||
const event = new CustomEvent("db-on-transaction", { detail: state });
|
||||
document.dispatchEvent(event);
|
||||
});
|
||||
const event = new CustomEvent("db-on-transaction", { detail: state });
|
||||
document.dispatchEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
EnjoyApp.db.removeListeners();
|
||||
@@ -69,6 +90,7 @@ export const DbProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
path,
|
||||
error,
|
||||
connect,
|
||||
disconnect,
|
||||
addDblistener,
|
||||
removeDbListener,
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { createContext, useState, useEffect, useContext, useMemo } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import {
|
||||
AppSettingsProviderContext,
|
||||
DbProviderContext,
|
||||
} from "@renderer/context";
|
||||
import { t } from "i18next";
|
||||
import { UserSettingKeyEnum } from "@/types/enums";
|
||||
|
||||
type DictProviderState = {
|
||||
settings: DictSettingType;
|
||||
@@ -48,6 +52,7 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
});
|
||||
const [currentDictValue, setCurrentDictValue] = useState<string>("");
|
||||
const [currentDict, setCurrentDict] = useState<Dict | null>();
|
||||
const { state: dbState } = useContext(DbProviderContext);
|
||||
|
||||
const availableDicts = useMemo(
|
||||
() =>
|
||||
@@ -93,12 +98,14 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
}, [availableDicts, settings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dbState !== "connected") return;
|
||||
|
||||
fetchSettings();
|
||||
fetchDicts();
|
||||
}, []);
|
||||
}, [dbState]);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
return EnjoyApp.settings.getDictSettings().then((res) => {
|
||||
return EnjoyApp.userSettings.get(UserSettingKeyEnum.DICTS).then((res) => {
|
||||
res && setSettings(res);
|
||||
});
|
||||
};
|
||||
@@ -119,8 +126,8 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const setDefault = async (dict: Dict | null) => {
|
||||
const _settings = { ...settings, default: dict?.name ?? "" };
|
||||
|
||||
EnjoyApp.settings
|
||||
.setDictSettings(_settings)
|
||||
EnjoyApp.userSettings
|
||||
.set(UserSettingKeyEnum.DICTS, _settings)
|
||||
.then(() => setSettings(_settings));
|
||||
};
|
||||
|
||||
@@ -129,8 +136,8 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const removing = [...(settings.removing ?? []), dict.name];
|
||||
const _settings = { ...settings, removing };
|
||||
|
||||
EnjoyApp.settings
|
||||
.setDictSettings(_settings)
|
||||
EnjoyApp.userSettings
|
||||
.set(UserSettingKeyEnum.DICTS, _settings)
|
||||
.then(() => setSettings(_settings));
|
||||
}
|
||||
};
|
||||
@@ -140,8 +147,8 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
settings.removing?.filter((name) => name !== dict.name) ?? [];
|
||||
const _settings = { ...settings, removing };
|
||||
|
||||
EnjoyApp.settings
|
||||
.setDictSettings(_settings)
|
||||
EnjoyApp.userSettings
|
||||
.set(UserSettingKeyEnum.DICTS, _settings)
|
||||
.then(() => setSettings(_settings));
|
||||
};
|
||||
|
||||
|
||||
@@ -6,8 +6,12 @@ import {
|
||||
useState,
|
||||
} from "react";
|
||||
import { useHotkeys, useRecordHotkeys } from "react-hotkeys-hook";
|
||||
import { AppSettingsProviderContext } from "./app-settings-provider";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
AppSettingsProviderContext,
|
||||
DbProviderContext,
|
||||
} from "@renderer/context";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { UserSettingKeyEnum } from "@/types/enums";
|
||||
|
||||
function isShortcutValid(shortcut: string) {
|
||||
const modifiers = ["ctrl", "alt", "shift", "meta"];
|
||||
@@ -150,18 +154,25 @@ export const HotKeysSettingsProvider = ({
|
||||
const [keys, { start, stop, resetKeys, isRecording }] = useRecordHotkeys();
|
||||
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const { state: dbState } = useContext(DbProviderContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (dbState !== "connected") return;
|
||||
|
||||
fetchSettings();
|
||||
}, []);
|
||||
}, [dbState]);
|
||||
|
||||
const fetchSettings = async () => {
|
||||
const _hotkeys = await EnjoyApp.settings.getDefaultHotkeys();
|
||||
const _hotkeys = await EnjoyApp.userSettings.get(
|
||||
UserSettingKeyEnum.HOTKEYS
|
||||
);
|
||||
// During version iterations, there may be added or removed keys.
|
||||
const merged = mergeWithPreference(_hotkeys ?? {}, defaultKeyMap);
|
||||
await EnjoyApp.settings.setDefaultHotkeys(merged).then(() => {
|
||||
setCurrentHotkeys(merged);
|
||||
});
|
||||
await EnjoyApp.userSettings
|
||||
.set(UserSettingKeyEnum.HOTKEYS, merged)
|
||||
.then(() => {
|
||||
setCurrentHotkeys(merged);
|
||||
});
|
||||
};
|
||||
|
||||
const changeHotkey = useCallback(
|
||||
@@ -200,9 +211,11 @@ export const HotKeysSettingsProvider = ({
|
||||
};
|
||||
}
|
||||
|
||||
await EnjoyApp.settings.setDefaultHotkeys(newMap).then(() => {
|
||||
setCurrentHotkeys(newMap);
|
||||
});
|
||||
await EnjoyApp.userSettings
|
||||
.set(UserSettingKeyEnum.HOTKEYS, newMap)
|
||||
.then(() => {
|
||||
setCurrentHotkeys(newMap);
|
||||
});
|
||||
resetKeys();
|
||||
},
|
||||
[currentHotkeys]
|
||||
@@ -230,7 +243,9 @@ export const HotKeysSettingsProvider = ({
|
||||
changeHotkey,
|
||||
}}
|
||||
>
|
||||
{_.isEmpty(currentHotkeys) ? null : (
|
||||
{isEmpty(currentHotkeys) ? (
|
||||
children
|
||||
) : (
|
||||
<HotKeysSettingsSystemSettings
|
||||
{...{
|
||||
currentHotkeys,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Tooltip } from "react-tooltip";
|
||||
import { debounce } from "lodash";
|
||||
import { useAudioRecorder } from "react-audio-voice-recorder";
|
||||
import { t } from "i18next";
|
||||
import { SttEngineOptionEnum } from "@/types/enums";
|
||||
|
||||
const ONE_MINUTE = 60;
|
||||
const TEN_MINUTES = 10 * ONE_MINUTE;
|
||||
@@ -73,7 +74,7 @@ type MediaPlayerContextType = {
|
||||
generateTranscription: (params?: {
|
||||
originalText?: string;
|
||||
language?: string;
|
||||
service?: WhisperConfigType["service"] | "upload";
|
||||
service?: SttEngineOptionEnum | "upload";
|
||||
isolate?: boolean;
|
||||
}) => Promise<void>;
|
||||
transcribing: boolean;
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import take from "lodash/take";
|
||||
import sortedUniqBy from "lodash/sortedUniqBy";
|
||||
import { parseText } from "media-captions";
|
||||
import { SttEngineOptionEnum } from "@/types/enums";
|
||||
|
||||
// test a text string has any punctuations or not
|
||||
// some transcribed text may not have any punctuations
|
||||
@@ -48,7 +49,7 @@ export const useTranscribe = () => {
|
||||
targetType?: string;
|
||||
originalText?: string;
|
||||
language: string;
|
||||
service: WhisperConfigType["service"] | "upload";
|
||||
service: SttEngineOptionEnum | "upload";
|
||||
isolate?: boolean;
|
||||
align?: boolean;
|
||||
}
|
||||
@@ -100,15 +101,15 @@ export const useTranscribe = () => {
|
||||
text: originalText,
|
||||
};
|
||||
}
|
||||
} else if (service === "local") {
|
||||
} else if (service === SttEngineOptionEnum.LOCAL) {
|
||||
result = await transcribeByLocal(url, language);
|
||||
} else if (service === "cloudflare") {
|
||||
} else if (service === SttEngineOptionEnum.ENJOY_CLOUDFLARE) {
|
||||
result = await transcribeByCloudflareAi(blob);
|
||||
} else if (service === "openai") {
|
||||
} else if (service === SttEngineOptionEnum.OPENAI) {
|
||||
result = await transcribeByOpenAi(
|
||||
new File([blob], "audio.mp3", { type: "audio/mp3" })
|
||||
);
|
||||
} else if (service === "azure") {
|
||||
} else if (service === SttEngineOptionEnum.ENJOY_AZURE) {
|
||||
result = await transcribeByAzureAi(
|
||||
new File([blob], "audio.wav", { type: "audio/wav" }),
|
||||
language,
|
||||
|
||||
@@ -8,21 +8,18 @@ import {
|
||||
import { toast } from "@renderer/components/ui";
|
||||
import { TimelineEntry } from "echogarden/dist/utilities/Timeline.d.js";
|
||||
import { MAGIC_TOKEN_REGEX, END_OF_SENTENCE_REGEX } from "@/constants";
|
||||
import { SttEngineOptionEnum } from "@/types/enums";
|
||||
|
||||
export const useTranscriptions = (media: AudioType | VideoType) => {
|
||||
const { whisperConfig } = useContext(AISettingsProviderContext);
|
||||
const { EnjoyApp, learningLanguage } = useContext(
|
||||
AppSettingsProviderContext
|
||||
);
|
||||
const { sttEngine } = useContext(AISettingsProviderContext);
|
||||
const { EnjoyApp, learningLanguage } = useContext(AppSettingsProviderContext);
|
||||
const { addDblistener, removeDbListener } = useContext(DbProviderContext);
|
||||
const [transcription, setTranscription] = useState<TranscriptionType>(null);
|
||||
const { transcribe, output } = useTranscribe();
|
||||
const [transcribingProgress, setTranscribingProgress] = useState<number>(0);
|
||||
const [transcribing, setTranscribing] = useState<boolean>(false);
|
||||
const [transcribingOutput, setTranscribingOutput] = useState<string>("");
|
||||
const [service, setService] = useState<
|
||||
WhisperConfigType["service"] | "upload"
|
||||
>(whisperConfig.service);
|
||||
const [service, setService] = useState<SttEngineOptionEnum | "upload">(sttEngine);
|
||||
|
||||
const onTransactionUpdate = (event: CustomEvent) => {
|
||||
if (!transcription) return;
|
||||
@@ -63,13 +60,13 @@ export const useTranscriptions = (media: AudioType | VideoType) => {
|
||||
const generateTranscription = async (params?: {
|
||||
originalText?: string;
|
||||
language?: string;
|
||||
service?: WhisperConfigType["service"] | "upload";
|
||||
service?: SttEngineOptionEnum | "upload";
|
||||
isolate?: boolean;
|
||||
}) => {
|
||||
let {
|
||||
originalText,
|
||||
language = learningLanguage,
|
||||
service = whisperConfig.service,
|
||||
service = sttEngine,
|
||||
isolate = false,
|
||||
} = params || {};
|
||||
setService(service);
|
||||
|
||||
@@ -5,10 +5,11 @@ import {
|
||||
VideosSegment,
|
||||
YoutubeVideosSegment,
|
||||
EnrollmentSegment,
|
||||
Vocabulary,
|
||||
} from "@renderer/components";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { Button } from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
|
||||
export default () => {
|
||||
const [channels, setChannels] = useState<string[]>([
|
||||
@@ -28,17 +29,37 @@ export default () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto px-4 py-6 lg:px-8">
|
||||
<div className="space-y-4">
|
||||
<EnrollmentSegment />
|
||||
<AudiosSegment />
|
||||
<VideosSegment />
|
||||
<StoriesSegment />
|
||||
<AudibleBooksSegment />
|
||||
{channels.map((channel) => (
|
||||
<YoutubeVideosSegment key={channel} channel={channel} />
|
||||
))}
|
||||
<div className="relative">
|
||||
<AuthorizationStatusBar />
|
||||
<div className="max-w-5xl mx-auto px-4 py-6 lg:px-8">
|
||||
<div className="space-y-4">
|
||||
<EnrollmentSegment />
|
||||
<AudiosSegment />
|
||||
<VideosSegment />
|
||||
<StoriesSegment />
|
||||
<AudibleBooksSegment />
|
||||
{channels.map((channel) => (
|
||||
<YoutubeVideosSegment key={channel} channel={channel} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AuthorizationStatusBar = () => {
|
||||
const { user, logout } = useContext(AppSettingsProviderContext);
|
||||
|
||||
if (user.accessToken === null) {
|
||||
return (
|
||||
<div className="bg-red-500 text-white py-2 px-4 h-12 flex items-center sticky top-0">
|
||||
<span className="text-sm">{t("authorizationExpired")}</span>
|
||||
<Button variant="outline" size="sm" className="ml-2" onClick={logout}>
|
||||
{t("reLogin")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -20,7 +20,7 @@ export default () => {
|
||||
<div className="mt-auto">
|
||||
<div className="flex mb-4 justify-end space-x-4">
|
||||
{initialized && (
|
||||
<Link to="/" replace>
|
||||
<Link data-testid="start-to-use-button" to="/" replace>
|
||||
<Button className="w-24">{t("startToUse")}</Button>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
30
enjoy/src/types/enjoy-app.d.ts
vendored
30
enjoy/src/types/enjoy-app.d.ts
vendored
@@ -115,7 +115,7 @@ type EnjoyAppType = {
|
||||
) => Promise<Electron.MessageBoxReturnValue>;
|
||||
showErrorBox: (title: string, content: string) => Promise<void>;
|
||||
};
|
||||
settings: {
|
||||
appSettings: {
|
||||
get: (key: string) => Promise<any>;
|
||||
set: (key: string, value: any) => Promise<void>;
|
||||
getLibrary: () => Promise<string>;
|
||||
@@ -123,25 +123,13 @@ type EnjoyAppType = {
|
||||
getUser: () => Promise<UserType>;
|
||||
setUser: (user: UserType) => Promise<void>;
|
||||
getUserDataPath: () => Promise<string>;
|
||||
getDefaultEngine: () => Promise<string>;
|
||||
setDefaultEngine: (string) => Promise<string>;
|
||||
getGptEngine: () => Promise<GptEngineSettingType>;
|
||||
setGptEngine: (GptEngineSettingType) => Promise<GptEngineSettingType>;
|
||||
getLlm: (provider: SupportedLlmProviderType) => Promise<LlmProviderType>;
|
||||
setLlm: (
|
||||
provider: SupportedLlmProviderType,
|
||||
LlmProviderType
|
||||
) => Promise<void>;
|
||||
getLanguage: () => Promise<string>;
|
||||
switchLanguage: (language: string) => Promise<void>;
|
||||
getDefaultHotkeys: () => Promise<Record<string, string> | undefined>;
|
||||
setDefaultHotkeys: (records: Record<string, string>) => Promise<void>;
|
||||
getDictSettings: () => Promise<DictSettingType>;
|
||||
setDictSettings: (dict: DictSettingType) => Promise<void>;
|
||||
getApiUrl: () => Promise<string>;
|
||||
setApiUrl: (url: string) => Promise<void>;
|
||||
getVocabularyConfig: () => Promise<VocabularyConfigType | undefined>;
|
||||
setVocabularyConfig: (records: VocabularyConfigType) => Promise<void>;
|
||||
getSessions: () => Promise<{ id: string }[]>;
|
||||
};
|
||||
userSettings: {
|
||||
get: (key: UserSettingKeyEnum) => Promise<any>;
|
||||
set: (key: UserSettingKeyEnum, value: any) => Promise<void>;
|
||||
};
|
||||
fs: {
|
||||
ensureDir: (path: string) => Promise<boolean>;
|
||||
@@ -150,7 +138,8 @@ type EnjoyAppType = {
|
||||
join: (...paths: string[]) => Promise<string>;
|
||||
};
|
||||
db: {
|
||||
init: () => Promise<DbState>;
|
||||
connect: () => Promise<DbState>;
|
||||
disconnect: () => Promise<void>;
|
||||
onTransaction: (
|
||||
callback: (event, state: TransactionStateType) => void
|
||||
) => Promise<void>;
|
||||
@@ -290,9 +279,6 @@ type EnjoyAppType = {
|
||||
config: () => Promise<WhisperConfigType>;
|
||||
check: () => Promise<{ success: boolean; log: string }>;
|
||||
setModel: (model: string) => Promise<WhisperConfigType>;
|
||||
setService: (
|
||||
service: WhisperConfigType["service"]
|
||||
) => Promise<WhisperConfigType>;
|
||||
transcribe: (
|
||||
params: {
|
||||
file?: string;
|
||||
|
||||
27
enjoy/src/types/enums.ts
Normal file
27
enjoy/src/types/enums.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export enum UserSettingKeyEnum {
|
||||
PROFILE = "profile",
|
||||
LANGUAGE = "language",
|
||||
NATIVE_LANGUAGE = "native_language",
|
||||
LEARNING_LANGUAGE = "learning_language",
|
||||
WHISPER = "whisper",
|
||||
OPENAI = "openai",
|
||||
HOTKEYS = "hotkeys",
|
||||
GPT_ENGINE = "gpt_engine",
|
||||
STT_ENGINE = "stt_engine",
|
||||
VOCABULARY = "vocabulary",
|
||||
DICTS = "dicts",
|
||||
RECORDER = "recorder",
|
||||
}
|
||||
|
||||
export enum SttEngineOptionEnum {
|
||||
LOCAL = "local",
|
||||
ENJOY_AZURE = "enjoy_azure",
|
||||
ENJOY_CLOUDFLARE = "enjoy_cloudflare",
|
||||
OPENAI = "openai",
|
||||
}
|
||||
|
||||
export enum AppSettingsKeyEnum {
|
||||
LIBRARY = "library",
|
||||
USER = "user",
|
||||
API_URL = "api_url",
|
||||
}
|
||||
2
enjoy/src/types/index.d.ts
vendored
2
enjoy/src/types/index.d.ts
vendored
@@ -40,7 +40,7 @@ type NotificationType = {
|
||||
};
|
||||
|
||||
type WhisperConfigType = {
|
||||
service: "local" | "azure" | "cloudflare" | "openai";
|
||||
// service: "local" | "azure" | "cloudflare" | "openai";
|
||||
availableModels: {
|
||||
type: string;
|
||||
name: string;
|
||||
|
||||
2
enjoy/src/types/user.d.ts
vendored
2
enjoy/src/types/user.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
type UserType = {
|
||||
id: string;
|
||||
name: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
balance?: number;
|
||||
avatarUrl?: string;
|
||||
|
||||
Reference in New Issue
Block a user