1 Commits

Author SHA1 Message Date
dependabot[bot]
e17f99d30b Bump react-hotkeys-hook from 4.4.3 to 4.4.4
Bumps [react-hotkeys-hook](https://github.com/JohannesKlauss/react-keymap-hook) from 4.4.3 to 4.4.4.
- [Release notes](https://github.com/JohannesKlauss/react-keymap-hook/releases)
- [Changelog](https://github.com/JohannesKlauss/react-hotkeys-hook/blob/main/CHANGELOG.md)
- [Commits](https://github.com/JohannesKlauss/react-keymap-hook/compare/v4.4.3...v4.4.4)

---
updated-dependencies:
- dependency-name: react-hotkeys-hook
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-15 05:52:06 +00:00
43 changed files with 185 additions and 606 deletions

View File

@@ -18,7 +18,3 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
run: yarn publish:enjoy
- if: matrix.os == 'macos-latest'
env:
GITHUB_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
run: yarn run publish --arch=arm64

View File

@@ -7,7 +7,7 @@
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": false
"cssVariables": true
},
"aliases": {
"components": "src/renderer/components",

View File

@@ -2,7 +2,6 @@ import type { ForgeConfig } from "@electron-forge/shared-types";
import { MakerSquirrel } from "@electron-forge/maker-squirrel";
import { MakerZIP } from "@electron-forge/maker-zip";
import { MakerDeb } from "@electron-forge/maker-deb";
import { MakerRpm } from "@electron-forge/maker-rpm";
import { VitePlugin } from "@electron-forge/plugin-vite";
import { dirname } from "node:path";
import { Walker, DepType, type Module } from "flora-colossus";
@@ -45,14 +44,6 @@ const config: ForgeConfig = {
mimeType: ["x-scheme-handler/enjoy"],
},
}),
new MakerRpm({
options: {
name: "enjoy",
productName: "Enjoy",
icon: "./assets/icon.png",
mimeType: ["x-scheme-handler/enjoy"],
},
}),
],
publishers: [
{

View File

@@ -2,7 +2,7 @@
"private": true,
"name": "enjoy",
"productName": "Enjoy",
"version": "0.1.0-alpha.3",
"version": "0.1.0-alpha.2",
"description": "Enjoy desktop app",
"main": ".vite/build/main.js",
"types": "./src/types.d.ts",
@@ -125,7 +125,7 @@
"react-activity-calendar": "^2.2.1",
"react-dom": "^18.2.0",
"react-hook-form": "^7.49.2",
"react-hotkeys-hook": "^4.4.3",
"react-hotkeys-hook": "^4.4.4",
"react-i18next": "^14.0.0",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.21.1",

View File

@@ -32,9 +32,9 @@ export const WHISPER_MODELS_OPTIONS = [
},
{
type: "large",
name: "ggml-large-v3.bin",
name: "ggml-large.bin",
size: "3.09 GB",
url: "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3.bin",
url: "https://huggingface.co/ggerganov/whisper.cpp/blob/main/ggml-large-v3.bin",
},
];
@@ -46,3 +46,35 @@ export const PROCESS_TIMEOUT = 1000 * 60 * 15;
export const AI_GATEWAY_ENDPOINT =
"https://gateway.ai.cloudflare.com/v1/11d43ab275eb7e1b271ba4089ecc3864/enjoy";
export const CONVERSATION_PRESET_SCENARIOS: {
scenario: string;
autoSpeech: boolean;
prompt: string;
}[] = [
{
scenario: "translation",
autoSpeech: false,
prompt: `Act as a translation machine that converts any language input I provide into fluent, idiomatic American English. If the input is already in English, refine it to sound like native American English.
Suggestions:
Ensure that the translation maintains the original meaning and tone of the input as much as possible.
In case of English inputs, focus on enhancing clarity, grammar, and style to match American English standards.
Return the translation only, no other words needed.
`,
},
{
scenario: "vocal_coach",
autoSpeech: true,
prompt: `As an AI English vocal coach with an American accent, engage in a conversation with me to help improve my spoken English skills. Use the appropriate tone and expressions that a native American English speaker would use, keeping in mind that your responses will be converted to audio.
Suggestions:
Use common American idioms and phrases to give a more authentic experience of American English.
Provide corrections and suggestions for improvement in a supportive and encouraging manner.
Use a variety of sentence structures and vocabulary to expose me to different aspects of the language.`,
},
];

View File

@@ -190,20 +190,9 @@
"AIModel": "AI Model",
"chooseAIModelToDownload": "Choose AI Model to download",
"ffmpegCheck": "FFmpeg Check",
"check": "Check",
"ffmpegCommandIsWorking": "FFmpeg command is working",
"ffmpegCommandIsNotWorking": "FFmpeg command is not working",
"scan": "Scan",
"checkIfFfmpegIsInstalled": "Check if FFmpeg is installed",
"ffmpegFoundAt": "FFmpeg found at {{path}}",
"ffmpegNotFound": "FFmpeg not found",
"ffmpegInstallSteps": "FFmpeg Install Steps",
"Install": "Install",
"runTheFollowingCommandInTerminal": "Run the following command in terminal",
"click": "Click",
"willAutomaticallyFindFFmpeg": "Enjoy will automatically find FFmpeg",
"tryingToFindValidFFmepgInTheseDirectories": "Trying to find valid FFmpeg in these directories: {{dirs}}",
"invalidFfmpegPath": "Invalid FFmpeg path",
"ffmpegInstalled": "FFmpeg is installed",
"ffmpegNotInstalled": "FFmpeg is not installed.",
"usingInstalledFFmpeg": "Using installed FFmpeg",
"usingDownloadedFFmpeg": "Using downloaded FFmpeg",
"downloadFfmpeg": "Download FFmpeg",
@@ -219,10 +208,7 @@
"reset": "Reset",
"resetAll": "Reset All",
"resetAllConfirmation": "It will remove all of your personal data, are you sure?",
"resetSettings": "Reset Settings",
"resetSettingsConfirmation": "It will reset all of your settings, are you sure? The library will not be affected.",
"logoutAndRemoveAllPersonalData": "Logout and remove all personal data",
"logoutAndRemoveAllPersonalSettings": "Logout and remove all personal settings",
"hotkeys": "Hotkeys",
"quitApp": "Quit APP",
"openPreferences": "Open preferences",
@@ -281,11 +267,8 @@
"recordingActivity": "recording activity",
"recordingDetail": "Recording detail",
"noRecordingActivities": "no recording activities",
"basicSettingsShort": "Basic",
"basicSettings": "Basic settings",
"advancedSettingsShort": "Advanced",
"advancedSettings": "Advanced settings",
"advanced": "Advanced",
"language": "Language",
"sttAiModel": "STT AI model",
"relaunchIsNeededAfterChanged": "Relaunch is needed after changed",

View File

@@ -190,20 +190,9 @@
"AIModel": "AI 模型",
"chooseAIModelToDownload": "选择 AI 模型下载",
"ffmpegCheck": "FFmpeg 检查",
"check": "检查",
"ffmpegCommandIsWorking": "FFmpeg 命令正常工作",
"ffmpegCommandIsNotWorking": "FFmpeg 命令无法正常工作",
"scan": "查找",
"checkIfFfmpegIsInstalled": "检查 FFmpeg 是否已正确安装",
"ffmpegFoundAt": "检测到 FFmpeg 命令: {{path}}",
"ffmpegNotFound": "未检测到可用的 FFmpeg 命令",
"ffmpegInstallSteps": "FFmpeg 安装步骤",
"Install": "安装",
"runTheFollowingCommandInTerminal": "在终端中运行以下命令",
"click": "点击",
"willAutomaticallyFindFFmpeg": "Enjoy 将自动检测 FFmpeg 命令",
"tryingToFindValidFFmepgInTheseDirectories": "正在尝试在以下目录中查找有效的 FFmpeg 命令: {{dirs}}",
"invalidFfmpegPath": "无效的 FFmpeg 路径",
"ffmpegInstalled": "FFmpeg 已经安装",
"ffmpegNotInstalled": "FFmpeg 未安装,软件部分功能依赖于 FFmpeg",
"usingInstalledFFmpeg": "使用已安装的 FFmpeg",
"usingDownloadedFFmpeg": "使用下载的 FFmpeg",
"downloadFfmpeg": "下载 FFmpeg",
@@ -219,10 +208,7 @@
"reset": "重置",
"resetAll": "重置所有",
"resetAllConfirmation": "这将删除您的所有个人数据, 您确定要重置吗?",
"resetSettings": "重置设置选项",
"resetSettingsConfirmation": "您确定要重置个人设置选项吗?资料库不会受影响。",
"logoutAndRemoveAllPersonalData": "退出登录并删除所有个人数据",
"logoutAndRemoveAllPersonalSettings": "退出登录并删除所有个人设置选项",
"hotkeys": "快捷键",
"quitApp": "退出应用",
"openPreferences": "打开设置",
@@ -281,9 +267,7 @@
"recordingActivity": "练习活动",
"recordingDetail": "录音详情",
"noRecordingActivities": "没有练习活动",
"basicSettingsShort": "基本设置",
"basicSettings": "基本设置",
"advancedSettingsShort": "高级设置",
"advancedSettings": "高级设置",
"language": "语言",
"sttAiModel": "语音转文本 AI 模型",

View File

@@ -51,6 +51,9 @@ db.connect = async () => {
db.connection = sequelize;
// vacuum the database
await sequelize.query("VACUUM");
const umzug = new Umzug({
migrations: { glob: __dirname + "/migrations/*.js" },
context: sequelize.getQueryInterface(),
@@ -65,23 +68,6 @@ db.connect = async () => {
await sequelize.sync();
await sequelize.authenticate();
// TODO:
// clear the large waveform data in DB.
// Remove this in next release
const caches = await CacheObject.findAll({
attributes: ["id", "key"],
});
const cacheIds: string[] = [];
caches.forEach((cache) => {
if (cache.key.startsWith("waveform")) {
cacheIds.push(cache.id);
}
});
await CacheObject.destroy({ where: { id: cacheIds } });
// vacuum the database
await sequelize.query("VACUUM");
// register handlers
audiosHandler.register();
cacheObjectsHandler.register();

View File

@@ -7,20 +7,14 @@ import fs from "fs-extra";
import AdmZip from "adm-zip";
import downloader from "@main/downloader";
import storage from "@main/storage";
import readdirp from "readdirp";
import { t } from "i18next";
const logger = log.scope("ffmpeg");
const logger = log.scope("ffmepg");
export default class FfmpegWrapper {
public ffmpeg: Ffmpeg.FfmpegCommand;
public config: any;
constructor(config?: {
ffmpegPath: string;
ffprobePath: string;
commandExists?: boolean;
}) {
this.config = config || settings.ffmpegConfig();
constructor() {
this.config = settings.ffmpegConfig();
if (this.config.commandExists) {
logger.info("Using system ffmpeg");
@@ -34,7 +28,7 @@ export default class FfmpegWrapper {
}
}
checkCommand(): Promise<boolean> {
checkCommand() {
return new Promise((resolve, _reject) => {
this.ffmpeg.getAvailableFormats((err, formats) => {
if (err) {
@@ -325,106 +319,17 @@ export class FfmpegDownloader {
if (valid) {
event.sender.send("on-notification", {
type: "success",
message: t("ffmpegCommandIsWorking"),
message: `FFmpeg command valid, you're ready to go.`,
});
} else {
logger.error("FFmpeg command not valid", ffmpeg.config);
event.sender.send("on-notification", {
type: "warning",
message: t("ffmpegCommandIsNotWorking"),
message: `FFmpeg command not valid, please check the log for detail.`,
});
}
return valid;
});
ipcMain.handle("ffmpeg-discover-command", async (event) => {
try {
return await discoverFfmpeg();
} catch (err) {
logger.error(err);
event.sender.send("on-notification", {
type: "error",
message: `FFmpeg discover failed: ${err.message}`,
});
}
});
}
}
export const discoverFfmpeg = async () => {
const platform = process.platform;
let ffmpegPath: string;
let ffprobePath: string;
const libraryFfmpegPath = path.join(settings.libraryPath(), "ffmpeg");
const scanDirs = [...COMMAND_SCAN_DIR[platform], libraryFfmpegPath];
await Promise.all(
scanDirs.map(async (dir: string) => {
if (!fs.existsSync(dir)) return;
dir = path.resolve(dir);
log.info("FFmpeg scanning: " + dir);
const fileStream = readdirp(dir, {
depth: 3,
});
for await (const entry of fileStream) {
const appName = entry.basename
.replace(".app", "")
.replace(".exe", "")
.toLowerCase();
if (appName === "ffmpeg") {
logger.info("Found ffmpeg: ", entry.fullPath);
ffmpegPath = entry.fullPath;
}
if (appName === "ffprobe") {
logger.info("Found ffprobe: ", entry.fullPath);
ffprobePath = entry.fullPath;
}
if (ffmpegPath && ffprobePath) break;
}
})
);
let valid = false;
if (ffmpegPath && ffprobePath) {
const ffmepg = new FfmpegWrapper({ ffmpegPath, ffprobePath });
valid = await ffmepg.checkCommand();
}
if (valid) {
settings.setSync("ffmpeg", {
ffmpegPath,
ffprobePath,
});
} else {
ffmpegPath = undefined;
ffprobePath = undefined;
settings.setSync("ffmpeg", null);
}
return {
ffmpegPath,
ffprobePath,
scanDirs,
};
};
export const COMMAND_SCAN_DIR: { [key: string]: string[] } = {
darwin: [
"/Applications",
process.env.HOME + "/Applications",
"/opt/homebrew/bin",
],
linux: ["/usr/bin", "/usr/local/bin", "/snap/bin"],
win32: [
process.env.SystemDrive + "\\Program Files\\",
process.env.SystemDrive + "\\Program Files (x86)\\",
process.env.LOCALAPPDATA + "\\Apps\\2.0\\",
],
};

View File

@@ -96,19 +96,32 @@ const userDataPath = () => {
};
const ffmpegConfig = () => {
const ffmpegPath = settings.getSync("ffmpeg.ffmpegPath");
const ffprobePath = settings.getSync("ffmpeg.ffprobePath");
const _ffmpegPath = path.join(
libraryPath(),
"ffmpeg",
os.platform() === "win32" ? "ffmpeg.exe" : "ffmpeg"
);
const _ffprobePath = path.join(
libraryPath(),
"ffmpeg",
os.platform() === "win32" ? "ffprobe.exe" : "ffprobe"
);
const ffmpegPath = fs.existsSync(_ffmpegPath) ? _ffmpegPath : "";
const ffprobePath = fs.existsSync(_ffprobePath) ? _ffprobePath : "";
const _commandExists =
commandExists.sync("ffmpeg") && commandExists.sync("ffprobe");
const ready = Boolean(_commandExists || (ffmpegPath && ffprobePath));
const config = {
os: os.platform(),
arch: os.arch(),
commandExists: _commandExists,
ffmpegPath,
ffprobePath,
ready: Boolean(_commandExists || (ffmpegPath && ffprobePath)),
ready,
};
logger.info("ffmpeg config", config);

View File

@@ -1,38 +0,0 @@
import { ipcMain } from "electron";
import settings from "@main/settings";
import path from "path";
import fs from "fs-extra";
export class Waveform {
public dir = path.join(settings.libraryPath(), "waveforms");
constructor() {
fs.ensureDirSync(this.dir);
}
find(id: string) {
const file = path.join(this.dir, id + ".waveform.json");
if (fs.existsSync(file)) {
return fs.readJsonSync(file);
} else {
return null;
}
}
save(id: string, data: WaveFormDataType) {
const file = path.join(this.dir, id + ".waveform.json");
fs.writeJsonSync(file, data);
}
registerIpcHandlers() {
ipcMain.handle("waveforms-find", async (_event, id) => {
return this.find(id);
});
ipcMain.handle("waveforms-save", (_event, id, data) => {
return this.save(id, data);
});
}
}

View File

@@ -18,7 +18,6 @@ import log from "electron-log/main";
import { WEB_API_URL } from "@/constants";
import { AudibleProvider, TedProvider } from "@main/providers";
import { FfmpegDownloader } from "@main/ffmpeg";
import { Waveform } from "./waveform";
log.initialize({ preload: true });
const logger = log.scope("window");
@@ -26,7 +25,6 @@ const logger = log.scope("window");
const audibleProvider = new AudibleProvider();
const tedProvider = new TedProvider();
const ffmpegDownloader = new FfmpegDownloader();
const waveform = new Waveform();
const main = {
win: null as BrowserWindow | null,
@@ -48,9 +46,6 @@ main.init = () => {
// Whisper
whisper.registerIpcHandlers();
// Waveform
waveform.registerIpcHandlers();
// Downloader
downloader.registerIpcHandlers();
@@ -221,14 +216,6 @@ main.init = () => {
// App options
ipcMain.handle("app-reset", () => {
fs.removeSync(settings.userDataPath());
fs.removeSync(settings.file());
app.relaunch();
app.exit();
});
ipcMain.handle("app-reset-settings", () => {
fs.removeSync(settings.file());
app.relaunch();
app.exit();

View File

@@ -8,9 +8,6 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
reset: () => {
ipcRenderer.invoke("app-reset");
},
resetSettings: () => {
ipcRenderer.invoke("app-reset-settings");
},
relaunch: () => {
ipcRenderer.invoke("app-relaunch");
},
@@ -146,15 +143,12 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
getFfmpegConfig: () => {
return ipcRenderer.invoke("settings-get-ffmpeg-config");
},
setFfmpegConfig: (config: FfmpegConfigType) => {
return ipcRenderer.invoke("settings-set-ffmpeg-config", config);
},
getLanguage: (language: string) => {
return ipcRenderer.invoke("settings-get-language", language);
},
switchLanguage: (language: string) => {
return ipcRenderer.invoke("settings-switch-language", language);
},
}
},
path: {
join: (...paths: string[]) => {
@@ -350,9 +344,6 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
download: () => {
return ipcRenderer.invoke("ffmpeg-download");
},
discover: () => {
return ipcRenderer.invoke("ffmpeg-discover-command");
},
check: () => {
return ipcRenderer.invoke("ffmpeg-check-command");
},
@@ -399,12 +390,4 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
return ipcRenderer.invoke("transcriptions-update", id, params);
},
},
waveforms: {
find: (id: string) => {
return ipcRenderer.invoke("waveforms-find", id);
},
save: (id: string, data: WaveFormDataType) => {
return ipcRenderer.invoke("waveforms-save", id, data);
},
}
});

View File

@@ -53,7 +53,7 @@ function App() {
<AISettingsProvider>
<DbProvider>
<RouterProvider router={router} />
<Toaster richColors closeButton position="top-center" />
<Toaster richColors />
<Tooltip id="global-tooltip" />
</DbProvider>
</AISettingsProvider>

View File

@@ -132,7 +132,7 @@ export const AudioDetail = (props: { id?: string; md5?: string }) => {
mediaId={audio.id}
mediaType="Audio"
mediaUrl={audio.src}
mediaMd5={audio.md5}
waveformCacheKey={`waveform-audio-${audio.md5}`}
transcription={transcription}
currentTime={currentTime}
setCurrentTime={setCurrentTime}
@@ -207,7 +207,7 @@ export const AudioDetail = (props: { id?: string; md5?: string }) => {
</AlertDialog>
{!initialized && (
<div className="top-0 w-full h-full absolute z-30 bg-background/10 flex items-center justify-center">
<div className="top-0 w-full h-full absolute z-30 bg-white/10 flex items-center justify-center">
<LoaderIcon className="text-muted-foreground animate-spin w-8 h-8" />
</div>
)}

View File

@@ -48,7 +48,7 @@ export const ConversationsShortcut = (props: {
<div
key={conversation.id}
onClick={() => ask(conversation)}
className="bg-background text-primary rounded-full w-full mb-2 py-2 px-4 hover:bg-primary hover:text-white cursor-pointer flex items-center border"
className="bg-white text-primary rounded-full w-full mb-2 py-2 px-4 hover:bg-primary hover:text-white cursor-pointer flex items-center border"
style={{
borderLeftColor: `#${conversation.id
.replaceAll("-", "")

View File

@@ -89,7 +89,7 @@ export const SpeechPlayer = (props: {
</div>
<div
ref={ref}
className="bg-background rounded-lg grid grid-cols-9 items-center relative pl-2 h-[100px]"
className="bg-white rounded-lg grid grid-cols-9 items-center relative pl-2 h-[100px]"
>
{!initialized && (
<div className="col-span-9 flex flex-col justify-around h-[80px]">

View File

@@ -1,19 +1,13 @@
import { t } from "i18next";
import { useContext, useEffect, useState } from "react";
import { Button, Progress, toast } from "@renderer/components/ui";
import { Button, Progress } from "@renderer/components/ui";
import { AppSettingsProviderContext } from "@renderer/context";
import { CheckCircle2Icon, XCircleIcon, LoaderIcon } from "lucide-react";
import Markdown from "react-markdown";
export const FfmpegCheck = () => {
const { ffmpegConfig, setFfmegConfig, EnjoyApp } = useContext(
AppSettingsProviderContext
);
const [scanResult, setScanResult] = useState<{
ffmpegPath: string;
ffprobePath: string;
scanDirs: string[];
}>();
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState(0);
@@ -23,25 +17,15 @@ export const FfmpegCheck = () => {
});
};
const discoverFfmpeg = () => {
EnjoyApp.ffmpeg.discover().then((config) => {
setScanResult(config);
if (config.ffmpegPath && config.ffprobePath) {
toast.success(t("ffmpegFound"));
refreshFfmpegConfig();
} else {
toast.error(t("ffmpegNotFound"));
}
});
};
const downloadFfmpeg = () => {
listenToDownloadState();
setDownloading(true);
EnjoyApp.ffmpeg
.download()
.then(() => {
refreshFfmpegConfig();
.then((config) => {
if (config) {
setFfmegConfig(config);
}
})
.finally(() => {
setDownloading(false);
@@ -60,11 +44,11 @@ export const FfmpegCheck = () => {
}, [ffmpegConfig?.ready]);
useEffect(() => {
discoverFfmpeg();
refreshFfmpegConfig();
}, []);
return (
<div className="w-full max-w-screen-md mx-auto px-6">
<div className="w-full max-w-sm px-6">
{ffmpegConfig?.ready ? (
<>
<div className="flex justify-center items-center mb-8">
@@ -74,7 +58,7 @@ export const FfmpegCheck = () => {
<CheckCircle2Icon className="text-green-500 w-10 h-10 mb-4" />
</div>
<div className="text-center text-sm opacity-70">
{t("ffmpegFoundAt", { path: ffmpegConfig.ffmpegPath })}
{t("ffmpegInstalled")}
</div>
</>
) : (
@@ -85,87 +69,24 @@ export const FfmpegCheck = () => {
<div className="flex justify-center mb-4">
<XCircleIcon className="text-red-500 w-10 h-10" />
</div>
<div className="mb-4">
<div className="text-center text-sm mb-2">
{t("ffmpegNotFound")}
</div>
{scanResult && (
<div className="text-center text-xs text-muted-foreground mb-2">
{t("tryingToFindValidFFmepgInTheseDirectories", {
dirs: scanResult.scanDirs.join(", "),
})}
</div>
)}
<div className="text-center text-sm opacity-70 mb-4">
{t("ffmpegNotInstalled")}
</div>
<div className="flex items-center justify-center space-x-4 mb-4">
<Button onClick={discoverFfmpeg} variant="default">
{t("scan")}
<div className="flex items-center justify-center mb-4">
<Button
disabled={downloading}
className=""
onClick={downloadFfmpeg}
>
{downloading && <LoaderIcon className="animate-spin mr-2" />}
{t("downloadFfmpeg")}
</Button>
{ffmpegConfig.os === "win32" && (
<Button
variant="secondary"
disabled={downloading}
onClick={downloadFfmpeg}
>
{downloading && <LoaderIcon className="animate-spin mr-2" />}
{t("download")}
</Button>
)}
</div>
{downloading && (
<div className="w-full">
<Progress value={progress} />
</div>
)}
{ffmpegConfig.os === "darwin" && (
<div className="my-6 select-text prose mx-auto border rounded-lg p-4">
<h3 className="text-center">{t("ffmpegInstallSteps")}</h3>
<h4>
1. {t("install")}{" "}
<a
className="cursor-pointer text-blue-500 hover:underline"
onClick={() => {
EnjoyApp.shell.openExternal("https://brew.sh/");
}}
>
Homebrew
</a>
</h4>
<p>{t("runTheFollowingCommandInTerminal")} </p>
<pre>
<code>
/bin/bash -c "$(curl -fsSL
https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
</code>
</pre>
<h4>2. {t("install")} FFmpeg</h4>
<p>{t("runTheFollowingCommandInTerminal")} </p>
<pre>
<code>brew install ffmpeg</code>
</pre>
<h4>3. {t("scan")} FFmpeg</h4>
<p>
{t("click")}
<Button
onClick={discoverFfmpeg}
variant="default"
size="sm"
className="mx-2"
>
{t("scan")}
</Button>
, {t("willAutomaticallyFindFFmpeg")}
</p>
</div>
)}
</>
)}
</div>

View File

@@ -1,13 +1,10 @@
import { Button, toast, Separator } from "@renderer/components/ui";
import { Button, toast } from "@renderer/components/ui";
import { useContext, useEffect } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
import { t } from "i18next";
import { UserSettings, LanguageSettings } from "@renderer/components";
export const LoginForm = () => {
const { EnjoyApp, login, webApi, user } = useContext(
AppSettingsProviderContext
);
const { EnjoyApp, login, webApi } = useContext(AppSettingsProviderContext);
const handleMixinLogin = () => {
const url = `${webApi.baseUrl}/sessions/new?provider=mixin`;
@@ -62,16 +59,6 @@ export const LoginForm = () => {
};
}, [webApi]);
if (user) {
return (
<div className="px-4 py-2 border rounded-lg w-full max-w-sm">
<UserSettings />
<Separator />
<LanguageSettings />
</div>
);
}
return (
<div className="w-full max-w-sm px-6 flex flex-col space-y-4">
<Button

View File

@@ -34,7 +34,7 @@ export const MediaPlayer = (props: {
mediaId: string;
mediaType: "Audio" | "Video";
mediaUrl: string;
mediaMd5?: string;
waveformCacheKey: string;
transcription: TranscriptionType;
// player controls
currentTime: number;
@@ -67,7 +67,7 @@ export const MediaPlayer = (props: {
mediaId,
mediaType,
mediaUrl,
mediaMd5,
waveformCacheKey,
transcription,
height = 200,
currentTime,
@@ -94,7 +94,12 @@ export const MediaPlayer = (props: {
if (!mediaUrl) return;
const [wavesurfer, setWavesurfer] = useState(null);
const [waveform, setWaveForm] = useState<WaveFormDataType>(null);
const [waveform, setWaveForm] = useState<{
peaks: number[];
duration: number;
frequencies: number[];
sampleRate: number;
}>(null);
const containerRef = useRef<HTMLDivElement>();
const [mediaProvider, setMediaProvider] = useState<
HTMLAudioElement | HTMLVideoElement
@@ -176,7 +181,7 @@ export const MediaPlayer = (props: {
const renderPitchContour = (region: RegionType) => {
if (!region) return;
if (!waveform?.frequencies?.length) return;
if (!waveform.frequencies.length) return;
if (!wavesurfer) return;
const duration = wavesurfer.getDuration();
@@ -275,6 +280,7 @@ export const MediaPlayer = (props: {
const ws = WaveSurfer.create({
container: containerRef.current,
height,
url: mediaUrl,
waveColor: "#ddd",
progressColor: "rgba(0, 0, 0, 0.25)",
cursorColor: "#dc143c",
@@ -318,7 +324,6 @@ export const MediaPlayer = (props: {
const subscriptions = [
wavesurfer.on("play", () => setIsPlaying(true)),
wavesurfer.on("pause", () => setIsPlaying(false)),
wavesurfer.on("loading", (percent: number) => console.log(percent)),
wavesurfer.on("timeupdate", (time: number) => setCurrentTime(time)),
wavesurfer.on("decode", () => {
if (waveform?.frequencies) return;
@@ -335,7 +340,7 @@ export const MediaPlayer = (props: {
sampleRate,
frequencies: _frequencies,
};
EnjoyApp.waveforms.save(mediaMd5, _waveform);
EnjoyApp.cacheObjects.set(waveformCacheKey, _waveform);
setWaveForm(_waveform);
}),
wavesurfer.on("ready", () => {
@@ -474,8 +479,10 @@ export const MediaPlayer = (props: {
}, [wavesurfer, isPlaying]);
useEffect(() => {
EnjoyApp.waveforms.find(mediaMd5).then((waveform) => {
setWaveForm(waveform);
EnjoyApp.cacheObjects.get(waveformCacheKey).then((cached) => {
if (!cached) return;
setWaveForm(cached);
});
}, []);

View File

@@ -90,11 +90,11 @@ export const AssistantMessageComponent = (props: {
id={`message-${message.id}`}
className="flex items-end space-x-2 pr-10"
>
<Avatar className="w-8 h-8 bg-background avatar">
<Avatar className="w-8 h-8 bg-white avatar">
<AvatarImage></AvatarImage>
<AvatarFallback className="bg-background">AI</AvatarFallback>
<AvatarFallback className="bg-white">AI</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-2 px-4 py-2 bg-background border rounded-lg shadow-sm w-full">
<div className="flex flex-col gap-2 px-4 py-2 bg-white border rounded-lg shadow-sm w-full">
{configuration?.autoSpeech && speeching ? (
<div className="p-4">
<LoaderIcon className="w-8 h-8 animate-spin" />

View File

@@ -32,7 +32,6 @@ import {
} from "lucide-react";
import { useCopyToClipboard } from "@uidotdev/usehooks";
import { t } from "i18next";
import { useNavigate } from "react-router-dom";
import Markdown from "react-markdown";
export const UserMessageComponent = (props: {
@@ -46,7 +45,6 @@ export const UserMessageComponent = (props: {
const { user, webApi } = useContext(AppSettingsProviderContext);
const [_, copyToClipboard] = useCopyToClipboard();
const [copied, setCopied] = useState<boolean>(false);
const navigate = useNavigate();
const handleShare = async () => {
if (message.role === "user") {
@@ -59,18 +57,7 @@ export const UserMessageComponent = (props: {
},
})
.then(() => {
toast.success(t("sharedSuccessfully"), {
description: t("sharedPrompt"),
action: {
label: t("view"),
onClick: () => {
navigate("/community");
},
},
actionButtonStyle: {
backgroundColor: "var(--primary)",
},
});
toast(t("sharedSuccessfully"), { description: t("sharedPrompt") });
})
.catch((err) => {
toast.error(t("shareFailed"), { description: err.message });
@@ -168,7 +155,7 @@ export const UserMessageComponent = (props: {
</DropdownMenuContent>
</DropdownMenu>
<Avatar className="w-8 h-8 bg-background">
<Avatar className="w-8 h-8 bg-white">
<AvatarImage src={user.avatarUrl} />
<AvatarFallback className="bg-primary text-white capitalize">
{user.name[0]}

View File

@@ -165,7 +165,7 @@ const WavesurferPlayer = (props: {
<div
ref={ref}
className="bg-background rounded-lg grid grid-cols-9 items-center relative h-[80px]"
className="bg-white rounded-lg grid grid-cols-9 items-center relative h-[80px]"
>
{!initialized && (
<div className="col-span-9 flex flex-col justify-around h-[80px]">

View File

@@ -20,7 +20,7 @@ export const PostCard = (props: {
const { user } = useContext(AppSettingsProviderContext);
return (
<div className="rounded p-4 bg-background space-y-3">
<div className="rounded p-4 bg-white space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Avatar>

View File

@@ -10,35 +10,6 @@ export const AdvancedSettings = () => {
{t("advancedSettings")}
</div>
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">{t("resetSettings")}</div>
<div className="text-sm text-muted-foreground mb-2">
{t("logoutAndRemoveAllPersonalSettings")}
</div>
</div>
<div className="">
<div className="mb-2 flex justify-end">
<ResetAllButton>
<Button
variant="secondary"
className="text-destructive"
size="sm"
>
{t("resetSettings")}
</Button>
</ResetAllButton>
</div>
<div className="text-xs text-muted-foreground">
<InfoIcon className="mr-1 w-3 h-3 inline" />
<span>{t("relaunchIsNeededAfterChanged")}</span>
</div>
</div>
</div>
<Separator />
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">{t("resetAll")}</div>
@@ -55,7 +26,7 @@ export const AdvancedSettings = () => {
className="text-destructive"
size="sm"
>
{t("resetAll")}
{t("reset")}
</Button>
</ResetAllButton>
</div>

View File

@@ -36,7 +36,7 @@ import {
} from "@renderer/context";
import { useContext, useState, useRef, useEffect } from "react";
import { redirect } from "react-router-dom";
import { InfoIcon, EditIcon } from "lucide-react";
import { InfoIcon } from "lucide-react";
export const BasicSettings = () => {
return (
@@ -60,7 +60,7 @@ export const BasicSettings = () => {
);
};
export const UserSettings = () => {
const UserSettings = () => {
const { user, logout } = useContext(AppSettingsProviderContext);
if (!user) return null;
@@ -113,7 +113,7 @@ export const UserSettings = () => {
);
};
export const LanguageSettings = () => {
const LanguageSettings = () => {
const { language, switchLanguage } = useContext(AppSettingsProviderContext);
return (
@@ -188,11 +188,7 @@ const LibraryPathSettings = () => {
<Button variant="secondary" size="sm" onClick={openLibraryPath}>
{t("open")}
</Button>
<Button
variant="secondary"
size="sm"
onClick={handleChooseLibraryPath}
>
<Button variant="default" size="sm" onClick={handleChooseLibraryPath}>
{t("edit")}
</Button>
</div>
@@ -206,103 +202,49 @@ const LibraryPathSettings = () => {
};
const FfmpegSettings = () => {
const { EnjoyApp, setFfmegConfig, ffmpegConfig } = useContext(
AppSettingsProviderContext
);
const [editing, setEditing] = useState(false);
const { libraryPath, EnjoyApp } = useContext(AppSettingsProviderContext);
const [config, setConfig] = useState<FfmpegConfigType>();
const refreshFfmpegConfig = async () => {
EnjoyApp.settings.getFfmpegConfig().then((config) => {
setFfmegConfig(config);
useEffect(() => {
EnjoyApp.settings.getFfmpegConfig().then((_config) => {
setConfig(_config);
});
};
const handleChooseFfmpeg = async () => {
const filePaths = await EnjoyApp.dialog.showOpenDialog({
properties: ["openFile"],
});
const path = filePaths?.[0];
if (!path) return;
if (path.includes("ffmpeg")) {
EnjoyApp.settings.setFfmpegConfig({
...ffmpegConfig,
ffmpegPath: path,
});
refreshFfmpegConfig();
} else if (path.includes("ffprobe")) {
EnjoyApp.settings.setFfmpegConfig({
...ffmpegConfig,
ffprobePath: path,
});
refreshFfmpegConfig();
} else {
toast.error(t("invalidFfmpegPath"));
}
};
}, []);
return (
<>
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">FFmpeg</div>
<div className="flex items-center space-x-4">
<span className=" text-sm text-muted-foreground">
<b>ffmpeg</b>: {ffmpegConfig?.ffmpegPath || ""}
</span>
{editing && (
<Button onClick={handleChooseFfmpeg} variant="ghost" size="icon">
<EditIcon className="w-4 h-4 text-muted-foreground" />
</Button>
)}
</div>
<div className="flex items-center space-x-4">
<span className=" text-sm text-muted-foreground">
<b>ffprobe</b>: {ffmpegConfig?.ffprobePath || ""}
</span>
{editing && (
<Button onClick={handleChooseFfmpeg} variant="ghost" size="icon">
<EditIcon className="w-4 h-4 text-muted-foreground" />
</Button>
)}
</div>
<div className="flex items-start justify-between py-4">
<div className="">
<div className="mb-2">FFmpeg</div>
<div className="text-sm text-muted-foreground mb-2">
{config?.commandExists && t("usingInstalledFFmpeg")}
{!config?.commandExists &&
`${t("usingDownloadedFFmpeg")}: ${config?.ffmpegPath}`}
</div>
<div className="">
<div className="flex items-center justify-end space-x-2 mb-2">
</div>
<div className="">
<div className="flex items-center justify-end space-x-2 mb-2">
{config?.ffmpegPath && (
<Button
variant="secondary"
size="sm"
onClick={() => {
EnjoyApp.ffmpeg
.discover()
.then(({ ffmpegPath, ffprobePath }) => {
if (ffmpegPath && ffprobePath) {
toast.success(
t("ffmpegFoundAt", {
path: ffmpegPath + ", " + ffprobePath,
})
);
} else {
toast.warning(t("ffmpegNotFound"));
}
refreshFfmpegConfig();
});
EnjoyApp.shell.openPath(libraryPath);
}}
>
{t("scan")}
{t("open")}
</Button>
<Button
variant={editing ? "outline" : "secondary"}
size="sm"
onClick={() => setEditing(!editing)}
>
{editing ? t("cancel") : t("edit")}
</Button>
</div>
)}
<Button
size="sm"
onClick={() => {
EnjoyApp.ffmpeg.check();
}}
>
{t("check")}
</Button>
</div>
</div>
</>
</div>
);
};
@@ -320,9 +262,7 @@ const WhisperSettings = () => {
<Dialog>
<DialogTrigger asChild>
<Button variant="secondary" size="sm">
{t("edit")}
</Button>
<Button size="sm">{t("edit")}</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>{t("sttAiModel")}</DialogHeader>
@@ -399,7 +339,7 @@ const OpenaiSettings = () => {
</div>
<div className="">
<Button
variant={editing ? "outline" : "secondary"}
variant={editing ? "secondary" : "default"}
size="sm"
onClick={() => setEditing(!editing)}
>
@@ -463,7 +403,7 @@ const GoogleGenerativeAiSettings = () => {
</div>
<div className="">
<Button
variant={editing ? "outline" : "secondary"}
variant={editing ? "secondary" : "default"}
size="sm"
onClick={() => setEditing(!editing)}
>

View File

@@ -1,23 +1,18 @@
import { t } from "i18next";
import { Button, ScrollArea } from "@renderer/components/ui";
import {
BasicSettings,
AdvancedSettings,
About,
Hotkeys,
} from "@renderer/components";
import { BasicSettings, AdvancedSettings, About, Hotkeys } from "@renderer/components";
import { useState } from "react";
export const Preferences = () => {
const TABS = [
{
value: "basic",
label: t("basicSettingsShort"),
label: t("basicSettings"),
component: () => <BasicSettings />,
},
{
value: "advanced",
label: t("advancedSettingsShort"),
label: t("advancedSettings"),
component: () => <AdvancedSettings />,
},
{
@@ -35,8 +30,8 @@ export const Preferences = () => {
const [activeTab, setActiveTab] = useState<string>("basic");
return (
<div className="grid grid-cols-5 overflow-hidden h-full">
<ScrollArea className="h-full col-span-1 bg-muted/50 p-4">
<div className="grid grid-cols-5">
<ScrollArea className="col-span-1 h-full bg-muted/50 p-4">
<div className="py-2 text-muted-foreground mb-4">
{t("sidebar.preferences")}
</div>
@@ -55,7 +50,7 @@ export const Preferences = () => {
</Button>
))}
</ScrollArea>
<ScrollArea className="h-full col-span-4 py-6 px-10">
<ScrollArea className="col-span-4 p-6">
{TABS.find((tab) => tab.value === activeTab)?.component()}
</ScrollArea>
</div>

View File

@@ -96,7 +96,7 @@ export const PronunciationAssessmentScoreResult = (props: {
</div>
{!pronunciationScore && (
<div className="w-full h-full absolute z-30 bg-background/10 flex items-center justify-center">
<div className="w-full h-full absolute z-30 bg-white/10 flex items-center justify-center">
<Button size="lg" disabled={assessing} onClick={onAssess}>
{assessing && (
<LoaderIcon className="w-4 h-4 animate-spin inline mr-2" />

View File

@@ -70,7 +70,7 @@ export const RecordingCard = (props: {
return (
<div id={id} className="flex items-center justify-end px-4 transition-all">
<div className="w-full">
<div className="bg-background rounded-lg py-2 px-4 relative mb-1">
<div className="bg-white rounded-lg py-2 px-4 relative mb-1">
<div className="flex items-center justify-end space-x-2">
<span className="text-xs text-muted-foreground">
{secondsToTimestamp(recording.duration / 1000)}

View File

@@ -44,35 +44,3 @@ export const ResetAllButton = (props: { children: React.ReactNode }) => {
</AlertDialog>
);
};
export const ResetSettingsButton = (props: { children: React.ReactNode }) => {
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const reset = () => {
EnjoyApp.app.resetSettings();
};
return (
<AlertDialog>
<AlertDialogTrigger asChild>{props.children}</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("resetSettings")}</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
{t("resetSettingsConfirmation")}
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive hover:bg-destructive-hover"
onClick={reset}
>
{t("resetSettings")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -13,7 +13,7 @@ import { debounce , uniq } from "lodash";
import Mark from "mark.js";
export const StoryViewer = (props: {
story: Partial<StoryType> & Partial<CreateStoryParamsType>;
story: StoryType & Partial<CreateStoryParamsType>;
marked?: boolean;
meanings?: MeaningType[];
setMeanings: (meanings: MeaningType[]) => void;
@@ -96,7 +96,7 @@ export const StoryViewer = (props: {
return (
<>
<div className="w-full max-w-2xl xl:max-w-3xl mx-auto sticky bg-background top-0 z-30 px-4 py-2 border-b">
<div className="w-full max-w-2xl xl:max-w-3xl mx-auto sticky bg-white top-0 z-30 px-4 py-2 border-b">
<div className="w-full flex items-center space-x-4">
<Button
variant="ghost"
@@ -130,10 +130,10 @@ export const StoryViewer = (props: {
</div>
</div>
</div>
<div className="bg-background py-6 px-8 max-w-2xl xl:max-w-3xl mx-auto relative shadow-lg">
<div className="bg-white py-6 px-8 max-w-2xl xl:max-w-3xl mx-auto relative shadow-lg">
<article
ref={ref}
className="relative select-text prose dark:prose-invert prose-lg xl:prose-xl font-serif text-lg"
className="relative select-text prose prose-lg xl:prose-xl font-serif text-lg"
>
<h2>
{story.title.split(" ").map((word, i) => (

View File

@@ -41,8 +41,8 @@ export const ToolbarButton = (props: {
className={cn(
`rounded-full p-3 h-12 w-12 ${
toggled
? "bg-primary dark:bg-background text-background dark:text-foreground"
: "bg-background dark:bg-muted text-muted-foreground hover:text-background "
? "bg-primary text-white"
: "bg-white text-muted-foreground hover:text-white "
}`,
className
)}

View File

@@ -6,7 +6,7 @@ import { Toaster as Sonner, toast } from "sonner";
type ToasterProps = React.ComponentProps<typeof Sonner>;
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "light" } = useTheme();
const { theme = "system" } = useTheme();
return (
<Sonner

View File

@@ -81,9 +81,7 @@ export const VideoDetail = (props: { id?: string; md5?: string }) => {
targetId: video.id,
})
.then(() => {
toast.success(t("sharedSuccessfully"), {
description: t("sharedVideo"),
});
toast.success(t("sharedSuccessfully"), { description: t("sharedVideo") });
})
.catch((err) => {
toast.error(t("shareFailed"), { description: err.message });
@@ -138,7 +136,7 @@ export const VideoDetail = (props: { id?: string; md5?: string }) => {
mediaId={video.id}
mediaType="Video"
mediaUrl={video.src}
mediaMd5={video.md5}
waveformCacheKey={`waveform-video-${video.md5}`}
transcription={transcription}
currentTime={currentTime}
setCurrentTime={setCurrentTime}
@@ -218,7 +216,7 @@ export const VideoDetail = (props: { id?: string; md5?: string }) => {
</AlertDialog>
{!initialized && (
<div className="top-0 w-full h-full absolute z-30 bg-background/10 flex items-center justify-center">
<div className="top-0 w-full h-full absolute z-30 bg-white/10 flex items-center justify-center">
<LoaderIcon className="text-muted-foreground animate-spin w-8 h-8" />
</div>
)}

View File

@@ -78,6 +78,7 @@ export const AppSettingsProvider = ({
const fetchLanguage = async () => {
const language = await EnjoyApp.settings.getLanguage();
console.log(language);
setLanguage(language as "en" | "zh-CN");
i18n.changeLanguage(language);
};

View File

@@ -271,7 +271,7 @@ export default () => {
</ScrollArea>
<div className="px-4 absolute w-full bottom-0 left-0 h-14 bg-muted z-50">
<div className="focus-within:bg-background px-4 py-2 flex items-center space-x-4 rounded-lg border">
<div className="focus-within:bg-white px-4 py-2 flex items-center space-x-4 rounded-lg border">
<Textarea
rows={1}
ref={inputRef}
@@ -279,7 +279,7 @@ export default () => {
value={content}
onChange={(e) => setConent(e.target.value)}
placeholder={t("pressEnterToSend")}
className="px-0 py-0 shadow-none border-none focus-visible:outline-0 focus-visible:ring-0 border-none bg-muted focus:bg-background min-h-[1.25rem] max-h-[3.5rem] !overflow-x-hidden"
className="px-0 py-0 shadow-none border-none focus-visible:outline-0 focus-visible:ring-0 border-none bg-muted focus:bg-white min-h-[1.25rem] max-h-[3.5rem] !overflow-x-hidden"
/>
<Button
type="submit"

View File

@@ -86,7 +86,7 @@ export default () => {
{conversations.map((conversation) => (
<Link key={conversation.id} to={`/conversations/${conversation.id}`}>
<div
className="bg-background text-muted-foreground rounded-full w-full mb-2 p-4 hover:bg-primary hover:text-muted cursor-pointer flex items-center"
className="bg-white text-primary rounded-full w-full mb-2 p-4 hover:bg-primary hover:text-white cursor-pointer flex items-center"
style={{
borderLeftColor: `#${conversation.id
.replaceAll("-", "")

View File

@@ -6,6 +6,7 @@ import {
LoginForm,
ChooseLibraryPathInput,
WhisperModelOptionsPanel,
UserCard,
FfmpegCheck,
} from "@renderer/components";
import { AppSettingsProviderContext } from "@renderer/context";
@@ -92,7 +93,7 @@ export default () => {
</div>
</div>
<div className="flex-1 flex justify-center items-center">
{currentStep == 1 && <LoginForm />}
{currentStep == 1 && (user ? <UserCard user={user} /> : <LoginForm />)}
{currentStep == 2 && <ChooseLibraryPathInput />}
{currentStep == 3 && <WhisperModelOptionsPanel />}
{currentStep == 4 && <FfmpegCheck />}

View File

@@ -38,8 +38,7 @@ export default () => {
}
return (
<div className="h-[100vh] bg-muted">
<div className="max-w-screen-md mx-auto px-4 py-6">
<div className="h-[100vh] max-w-screen-md mx-auto px-4 py-6">
<div className="flex space-x-1 items-center mb-4">
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
<ChevronLeftIcon className="w-5 h-5" />
@@ -63,7 +62,7 @@ export default () => {
>
<ChevronLeftIcon className="w-5 h-5" />
</Button>
<div className="bg-background flex-1 h-5/6 border p-6 rounded-xl shadow-lg">
<div className="flex-1 h-5/6 border p-6 rounded-xl shadow-lg">
<MeaningMemorizingCard meaning={meanings[currentIndex]} />
</div>
<Button
@@ -84,6 +83,5 @@ export default () => {
</div>
)}
</div>
</div>
);
};

View File

@@ -75,13 +75,11 @@ type TransactionStateType = {
record?: AudioType | UserType | RecordingType;
};
type FfmpegConfigType = {
os: string;
arch: string;
commandExists: boolean;
ffmpegPath?: string;
ffprobePath?: string;
scanDirs: string[];
ready: boolean;
};
@@ -112,6 +110,7 @@ type PagyResponseType = {
next: number | null;
};
type AudibleBookType = {
title: string;
subtitle: string;

View File

@@ -1,7 +1,6 @@
type EnjoyAppType = {
app: {
reset: () => Promise<void>;
resetSettings: () => Promise<void>;
relaunch: () => Promise<void>;
reload: () => Promise<void>;
isPackaged: () => Promise<boolean>;
@@ -76,7 +75,7 @@ type EnjoyAppType = {
LlmProviderType
) => Promise<void>;
getFfmpegConfig: () => Promise<FfmpegConfigType>;
setFfmpegConfig: (config: FfmpegConfigType) => Promise<void>;
setFfmpegConfig: () => Promise<void>;
getLanguage: () => Promise<string>;
switchLanguage: (language: string) => Promise<void>;
};
@@ -181,11 +180,6 @@ type EnjoyAppType = {
ffmpeg: {
download: () => Promise<FfmpegConfigType>;
check: () => Promise<boolean>;
discover: () => Promise<{
ffmpegPath: string;
ffprobePath: string;
scanDirs: string[];
}>;
};
download: {
onState: (callback: (event, state) => void) => void;
@@ -205,8 +199,4 @@ type EnjoyAppType = {
process: (params: any) => Promise<void>;
update: (id: string, params: any) => Promise<void>;
};
waveforms: {
find: (id: string) => Promise<WaveFormDataType>;
save: (id: string, data: WaveFormDataType) => Promise<void>;
};
};

View File

@@ -1,6 +0,0 @@
type WaveFormDataType = {
peaks: number[];
sampleRate: number;
duration: number;
frequencies: number[];
};

View File

@@ -5827,7 +5827,7 @@ __metadata:
react-activity-calendar: "npm:^2.2.1"
react-dom: "npm:^18.2.0"
react-hook-form: "npm:^7.49.2"
react-hotkeys-hook: "npm:^4.4.3"
react-hotkeys-hook: "npm:^4.4.4"
react-i18next: "npm:^14.0.0"
react-markdown: "npm:^9.0.1"
react-router-dom: "npm:^6.21.1"
@@ -10684,13 +10684,13 @@ __metadata:
languageName: node
linkType: hard
"react-hotkeys-hook@npm:^4.4.3":
version: 4.4.3
resolution: "react-hotkeys-hook@npm:4.4.3"
"react-hotkeys-hook@npm:^4.4.4":
version: 4.4.4
resolution: "react-hotkeys-hook@npm:4.4.4"
peerDependencies:
react: ">=16.8.1"
react-dom: ">=16.8.1"
checksum: ef79e279129f6e55d81c8762b1da214d9c6ee4617b9597dbc3f93057cba8d166831508967b8e3f763a0c4ce0af3b59e6888c6fc94d152deac9335020b2ba80df
checksum: afe7418c8bd0ecd3a3f315648b84d9978d06a02e55f93df23aaa00e56613fca839f9ff4d10387e8336454702b4aa03cef97f3e67bfe3e121e42f16d84685f55a
languageName: node
linkType: hard