Fix bugs (#1274)
* handle client error ref #1255 * use bugsnag & remove unhandled modal * fix upload storage * fix assessment play origin * upgrade deps * try to fix JavaScript heap out of memory when package * update github action * fix workflow * try to fix CI in linux
This commit is contained in:
13
.github/workflows/test-enjoy-app.yml
vendored
13
.github/workflows/test-enjoy-app.yml
vendored
@@ -43,7 +43,18 @@ jobs:
|
||||
brew install sdl2
|
||||
|
||||
- name: Package App
|
||||
run: yarn enjoy:package
|
||||
shell: bash
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||
run: |
|
||||
set -e
|
||||
yarn enjoy:package
|
||||
|
||||
- name: Configure Chrome sandbox for Linux
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
run: |
|
||||
sudo chown root:root enjoy/out/Enjoy-linux-x64/chrome-sandbox
|
||||
sudo chmod 4755 enjoy/out/Enjoy-linux-x64/chrome-sandbox
|
||||
|
||||
- name: Run main tests with xvfb-run on Ubuntu
|
||||
# continue-on-error: true
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"markdown-it-sub": "^2.0.0",
|
||||
"markdown-it-sup": "^2.0.0",
|
||||
"mermaid": "^11.4.1",
|
||||
"sass": "^1.83.1",
|
||||
"sass": "^1.83.4",
|
||||
"vitepress": "^1.5.0",
|
||||
"vitepress-plugin-mermaid": "^2.0.17",
|
||||
"vue": "^3.5.13"
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxtjs/seo": "^2.0.2",
|
||||
"@nuxtjs/seo": "^2.0.3",
|
||||
"nuxt": "^3.15.1",
|
||||
"nuxt-og-image": "^4.0.2",
|
||||
"nuxt-og-image": "^4.0.3",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"sass": "^1.83.1",
|
||||
"postcss": "^8.5.1",
|
||||
"sass": "^1.83.4",
|
||||
"tailwindcss": "^3.4.17"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,16 +11,17 @@
|
||||
"predev": "yarn run download",
|
||||
"dev": "rimraf .vite && yarn run download && WEB_API_URL=http://localhost:3000 WS_URL=ws://localhost:3000 SETTINGS_PATH=${PWD}/enjoy/tmp LIBRARY_PATH=${PWD}/enjoy/tmp electron-forge start",
|
||||
"start": "rimraf .vite && yarn run download && electron-forge start",
|
||||
"package": "rimraf .vite && yarn run download && electron-forge package",
|
||||
"make": "rimraf .vite && yarn run download && electron-forge make",
|
||||
"publish": "rimraf .vite && yarn run download && electron-forge publish",
|
||||
"package": "NODE_OPTIONS='--max-old-space-size=8192' rimraf .vite && yarn run download && electron-forge package",
|
||||
"make": "NODE_OPTIONS='--max-old-space-size=8192' rimraf .vite && yarn run download && electron-forge make",
|
||||
"publish": "NODE_OPTIONS='--max-old-space-size=8192' rimraf .vite && yarn run download && electron-forge publish",
|
||||
"lint": "eslint --ext .ts,.tsx .",
|
||||
"test": "yarn run package && yarn run playwright test",
|
||||
"test:main": "yarn run playwright test e2e/main.spec.ts",
|
||||
"test:renderer": "yarn run playwright test e2e/renderer.spec.ts",
|
||||
"create-migration": "zx ./src/main/db/create-migration.mjs",
|
||||
"download-dictionaries": "zx ./scripts/download-dictionaries.mjs",
|
||||
"download": "yarn run download-dictionaries"
|
||||
"download": "yarn run download-dictionaries",
|
||||
"sourcemap": "bugsnag-source-maps upload-node --directory .vite/build --app-version 0.7.6"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": {
|
||||
@@ -29,6 +30,7 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@bugsnag/source-maps": "^2.3.3",
|
||||
"@divisey/js-mdict": "^5.0.0",
|
||||
"@electron-forge/cli": "^7.6.0",
|
||||
"@electron-forge/maker-deb": "^7.6.0",
|
||||
@@ -44,7 +46,7 @@
|
||||
"@electron/fuses": "^1.8.0",
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
"@langchain/community": "^0.3.24",
|
||||
"@langchain/core": "^0.3.29",
|
||||
"@langchain/core": "^0.3.30",
|
||||
"@langchain/ollama": "^0.1.4",
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"@playwright/test": "^1.49.1",
|
||||
@@ -85,7 +87,7 @@
|
||||
"@types/mark.js": "^8.11.12",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/mustache": "^4.2.5",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.6",
|
||||
"@types/prop-types": "^15.7.14",
|
||||
"@types/rails__actioncable": "^6.1.11",
|
||||
"@types/react": "^18.3.18",
|
||||
@@ -95,8 +97,8 @@
|
||||
"@types/unzipper": "^0.10.10",
|
||||
"@types/validator": "^13.12.2",
|
||||
"@types/wavesurfer.js": "^6.0.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.19.1",
|
||||
"@typescript-eslint/parser": "^8.19.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
||||
"@typescript-eslint/parser": "^8.20.0",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vidstack/react": "^1.12.12",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
@@ -119,14 +121,13 @@
|
||||
"decamelize": "^6.0.0",
|
||||
"decamelize-keys": "^2.0.1",
|
||||
"dependencies-tree": "^2.0.0",
|
||||
"electron": "^33.3.1",
|
||||
"electron": "^34.0.0",
|
||||
"electron-context-menu": "^4.0.4",
|
||||
"electron-devtools-installer": "^3.2.1",
|
||||
"electron-devtools-installer": "^4.0.0",
|
||||
"electron-forge-plugin-dependencies": "^1.0.0",
|
||||
"electron-log": "^5.2.4",
|
||||
"electron-playwright-helpers": "^1.7.1",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"electron-unhandled": "^5.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
@@ -141,23 +142,22 @@
|
||||
"langchain": "^0.3.11",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^11.0.2",
|
||||
"lucide-react": "^0.471.0",
|
||||
"lucide-react": "^0.471.1",
|
||||
"mark.js": "^8.11.1",
|
||||
"media-captions": "^0.0.18",
|
||||
"microsoft-cognitiveservices-speech-sdk": "^1.42.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"mustache": "^4.2.0",
|
||||
"new-github-issue-url": "^1.0.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"octokit": "^4.1.0",
|
||||
"openai": "^4.78.1",
|
||||
"pitchfinder": "^2.3.2",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss": "^8.5.1",
|
||||
"progress": "^2.0.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"react": "^18.3.1",
|
||||
"react-activity-calendar": "^2.7.6",
|
||||
"react-activity-calendar": "^2.7.7",
|
||||
"react-audio-visualize": "^1.2.0",
|
||||
"react-audio-voice-recorder": "^2.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -176,7 +176,7 @@
|
||||
"sonner": "^1.7.1",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwind-scrollbar-hide": "^1.3.1",
|
||||
"tailwind-scrollbar-hide": "^2.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"ts-node": "^10.9.2",
|
||||
@@ -185,13 +185,15 @@
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.0.7",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"wavesurfer.js": "^7.8.15",
|
||||
"wavesurfer.js": "^7.8.16",
|
||||
"zod": "^3.24.1",
|
||||
"zod-to-json-schema": "^3.24.1",
|
||||
"zx": "^8.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@andrkrn/ffprobe-static": "^5.2.0",
|
||||
"@bugsnag/electron": "^8.1.2",
|
||||
"@bugsnag/plugin-react": "^8.1.1",
|
||||
"echogarden": "^1.8.7",
|
||||
"electron-settings": "^4.0.4",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
|
||||
@@ -118,3 +118,15 @@ export const AGENT_FIXTURE_ANDREW = {
|
||||
ttsVoice: "en-US-AndrewNeural",
|
||||
},
|
||||
};
|
||||
|
||||
export const BUGSNAG_API_KEY = "828ee1de10c079a250be7fd05177662f";
|
||||
|
||||
export const MIME_TYPES: Record<string, string> = {
|
||||
".mp3": "audio/mpeg",
|
||||
".wav": "audio/wav",
|
||||
".ogg": "audio/ogg",
|
||||
".m4a": "audio/mp4",
|
||||
".aac": "audio/aac",
|
||||
".flac": "audio/flac",
|
||||
".wma": "audio/x-ms-wma",
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { app, BrowserWindow, protocol, net, shell } from "electron";
|
||||
import { app, BrowserWindow, protocol, net } from "electron";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
import settings from "@main/settings";
|
||||
@@ -6,15 +6,17 @@ import log from "@main/logger";
|
||||
import mainWindow from "@main/window";
|
||||
import ElectronSquirrelStartup from "electron-squirrel-startup";
|
||||
import contextMenu from "electron-context-menu";
|
||||
import Bugsnag from "@bugsnag/electron";
|
||||
import { t } from "i18next";
|
||||
import unhandled from "electron-unhandled";
|
||||
import newGithubIssueUrl from "new-github-issue-url";
|
||||
import { BUGSNAG_API_KEY } from "./constants";
|
||||
|
||||
const logger = log.scope("main");
|
||||
|
||||
app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer");
|
||||
|
||||
if (!app.isPackaged) {
|
||||
if (app.isPackaged) {
|
||||
Bugsnag.start({ apiKey: BUGSNAG_API_KEY });
|
||||
} else {
|
||||
app.disableHardwareAcceleration();
|
||||
app.commandLine.appendSwitch("disable-software-rasterizer");
|
||||
}
|
||||
@@ -121,43 +123,6 @@ app.on("ready", async () => {
|
||||
});
|
||||
|
||||
mainWindow.init();
|
||||
|
||||
unhandled({
|
||||
showDialog: true,
|
||||
logger: logger.error,
|
||||
reportButton: (error) => {
|
||||
const url = newGithubIssueUrl({
|
||||
user: "ZuodaoTech",
|
||||
repo: "everyone-can-use-english",
|
||||
title: "Unhandled Error",
|
||||
body: `
|
||||
## Node.js error stack
|
||||
|
||||
**Message:**
|
||||
|
||||
\`\`\`
|
||||
${error.message}
|
||||
\`\`\`
|
||||
|
||||
**Stack:**
|
||||
|
||||
\`\`\`
|
||||
${error.stack}
|
||||
\`\`\`
|
||||
|
||||
**Environment:**
|
||||
|
||||
\`\`\`
|
||||
${app.name} ${app.getVersion()}
|
||||
Electron ${process.versions.electron}
|
||||
${process.platform} ${process.arch}
|
||||
Locale: ${app.getLocale()}
|
||||
\`\`\``,
|
||||
});
|
||||
|
||||
shell.openExternal(url);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
Video,
|
||||
} from "@main/db/models";
|
||||
import settings from "@main/settings";
|
||||
import { AudioFormats, VideoFormats } from "@/constants";
|
||||
import { AudioFormats, MIME_TYPES, VideoFormats } from "@/constants";
|
||||
import { hashFile } from "@main/utils";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
@@ -159,6 +159,14 @@ export class Audio extends Model<Audio> {
|
||||
return this.getDataValue("md5") + this.extname;
|
||||
}
|
||||
|
||||
get mimeType(): string {
|
||||
if (this.metadata?.mimeType) {
|
||||
return this.metadata.mimeType;
|
||||
}
|
||||
|
||||
return MIME_TYPES[this.extname.toLowerCase()] || "audio/mpeg";
|
||||
}
|
||||
|
||||
get extname(): string {
|
||||
return (
|
||||
this.getDataValue("metadata")?.extname ||
|
||||
@@ -204,7 +212,7 @@ export class Audio extends Model<Audio> {
|
||||
if (this.isUploaded && !force) return;
|
||||
|
||||
return storage
|
||||
.put(this.md5, this.filePath)
|
||||
.put(this.md5, this.filePath, this.mimeType)
|
||||
.then((result) => {
|
||||
logger.debug("upload result:", result.data);
|
||||
if (result.data.success) {
|
||||
|
||||
@@ -146,7 +146,7 @@ export class Document extends Model<Document> {
|
||||
if (this.isUploaded && !force) return;
|
||||
|
||||
return storage
|
||||
.put(this.md5, this.filePath)
|
||||
.put(this.md5, this.filePath, this.metadata.mimeType)
|
||||
.then((result) => {
|
||||
logger.debug("upload result:", result.data);
|
||||
if (result.data.success) {
|
||||
|
||||
@@ -97,7 +97,9 @@ export class Note extends Model<Note> {
|
||||
|
||||
@AfterCreate
|
||||
static syncAndUploadAfterCreate(note: Note) {
|
||||
note.sync();
|
||||
note.sync().catch((err) => {
|
||||
logger.error("sync note error", note.id, err);
|
||||
});
|
||||
}
|
||||
|
||||
@AfterCreate
|
||||
@@ -113,7 +115,7 @@ export class Note extends Model<Note> {
|
||||
@AfterUpdate
|
||||
static syncAfterUpdate(note: Note) {
|
||||
note.sync().catch((err) => {
|
||||
logger.error("sync error", err);
|
||||
logger.error("sync note error", note.id, err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import { t } from "i18next";
|
||||
import { Attributes, Op, Transaction } from "sequelize";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import FfmpegWrapper from "@main/ffmpeg";
|
||||
import { MIME_TYPES } from "@/constants";
|
||||
|
||||
const logger = log.scope("db/models/recording");
|
||||
|
||||
@@ -141,6 +142,15 @@ export class Recording extends Model<Recording> {
|
||||
)}`;
|
||||
}
|
||||
|
||||
@Column(DataType.VIRTUAL)
|
||||
get mimeType(): string {
|
||||
return MIME_TYPES[this.extname.toLowerCase()] || "audio/mpeg";
|
||||
}
|
||||
|
||||
get extname(): string {
|
||||
return path.extname(this.filePath);
|
||||
}
|
||||
|
||||
get filePath(): string {
|
||||
const file = path.join(
|
||||
settings.userDataPath(),
|
||||
@@ -169,7 +179,7 @@ export class Recording extends Model<Recording> {
|
||||
}
|
||||
|
||||
return storage
|
||||
.put(this.md5, this.filePath)
|
||||
.put(this.md5, this.filePath, this.mimeType)
|
||||
.then((result) => {
|
||||
logger.debug("upload result:", result.data);
|
||||
if (result.data.success) {
|
||||
|
||||
@@ -25,6 +25,7 @@ import FfmpegWrapper from "@/main/ffmpeg";
|
||||
import { hashFile } from "@/main/utils";
|
||||
import fs from "fs-extra";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import { MIME_TYPES } from "@/constants";
|
||||
|
||||
const logger = log.scope("db/models/segment");
|
||||
const OUTPUT_FORMAT = "mp3";
|
||||
@@ -105,6 +106,14 @@ export class Segment extends Model<Segment> {
|
||||
);
|
||||
}
|
||||
|
||||
get extname(): string {
|
||||
return path.extname(this.filePath);
|
||||
}
|
||||
|
||||
get mimeType(): string {
|
||||
return MIME_TYPES[this.extname.toLowerCase()] || "audio/mpeg";
|
||||
}
|
||||
|
||||
async sync() {
|
||||
if (this.isSynced) return;
|
||||
|
||||
@@ -123,7 +132,7 @@ export class Segment extends Model<Segment> {
|
||||
if (this.isUploaded) return;
|
||||
|
||||
return storage
|
||||
.put(this.md5, this.filePath)
|
||||
.put(this.md5, this.filePath, this.mimeType)
|
||||
.then((result) => {
|
||||
logger.debug("upload result:", result.data);
|
||||
if (result.data.success) {
|
||||
@@ -207,7 +216,7 @@ export class Segment extends Model<Segment> {
|
||||
|
||||
unsyncedSegments.forEach((segment) => {
|
||||
segment.sync().catch((err) => {
|
||||
logger.error("sync error", err);
|
||||
logger.error("sync segment error", segment.id, err);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -230,8 +239,12 @@ export class Segment extends Model<Segment> {
|
||||
|
||||
@AfterCreate
|
||||
static syncAndUploadAfterCreate(segment: Segment) {
|
||||
segment.sync();
|
||||
segment.upload();
|
||||
segment.sync().catch((err) => {
|
||||
logger.error("sync segment error", segment.id, err);
|
||||
});
|
||||
segment.upload().catch((err) => {
|
||||
logger.error("upload segment error", segment.id, err);
|
||||
});
|
||||
}
|
||||
|
||||
@AfterUpdate
|
||||
@@ -242,7 +255,7 @@ export class Segment extends Model<Segment> {
|
||||
@AfterUpdate
|
||||
static syncAfterUpdate(segment: Segment) {
|
||||
segment.sync().catch((err) => {
|
||||
logger.error("sync error", err);
|
||||
logger.error("sync segment error", segment.id, err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ export class Transcription extends Model<Transcription> {
|
||||
@AfterUpdate
|
||||
static syncAfterUpdate(transcription: Transcription) {
|
||||
transcription.sync().catch((err) => {
|
||||
logger.error("sync error", err);
|
||||
logger.error("sync transcription error", transcription.id, err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,12 @@ import {
|
||||
UserSetting,
|
||||
} from "@main/db/models";
|
||||
import settings from "@main/settings";
|
||||
import { AudioFormats, VideoFormats, WEB_API_URL } from "@/constants";
|
||||
import {
|
||||
AudioFormats,
|
||||
MIME_TYPES,
|
||||
VideoFormats,
|
||||
WEB_API_URL,
|
||||
} from "@/constants";
|
||||
import { hashFile } from "@main/utils";
|
||||
import path from "path";
|
||||
import fs from "fs-extra";
|
||||
@@ -159,6 +164,10 @@ export class Video extends Model<Video> {
|
||||
return this.getDataValue("md5") + this.extname;
|
||||
}
|
||||
|
||||
get mimeType(): string {
|
||||
return MIME_TYPES[this.extname.toLowerCase()] || "video/mp4";
|
||||
}
|
||||
|
||||
get extname(): string {
|
||||
return (
|
||||
this.getDataValue("metadata").extname ||
|
||||
@@ -212,7 +221,7 @@ export class Video extends Model<Video> {
|
||||
const finalFile = path.join(settings.cachePath(), `${hash}.png`);
|
||||
fs.renameSync(coverFile, finalFile);
|
||||
|
||||
storage.put(hash, finalFile).then((result) => {
|
||||
storage.put(hash, finalFile, "image/png").then((result) => {
|
||||
logger.debug("cover upload result:", result.data);
|
||||
if (result.data.success) {
|
||||
this.update({ coverUrl: storage.getUrl(hash) });
|
||||
@@ -224,7 +233,7 @@ export class Video extends Model<Video> {
|
||||
if (this.isUploaded && !force) return;
|
||||
|
||||
return storage
|
||||
.put(this.md5, this.filePath)
|
||||
.put(this.md5, this.filePath, this.mimeType)
|
||||
.then((result) => {
|
||||
logger.debug("upload result:", result.data);
|
||||
if (result.data.success) {
|
||||
@@ -273,9 +282,12 @@ export class Video extends Model<Video> {
|
||||
|
||||
@AfterCreate
|
||||
static autoSync(video: Video) {
|
||||
// auto sync should not block the main thread
|
||||
video.sync().catch(() => {});
|
||||
video.generateCover().catch(() => {});
|
||||
video.sync().catch((err) => {
|
||||
logger.error("sync video error", video.id, err);
|
||||
});
|
||||
video.generateCover().catch((err) => {
|
||||
logger.error("generate cover error", video.id, err);
|
||||
});
|
||||
}
|
||||
|
||||
@AfterCreate
|
||||
@@ -286,7 +298,9 @@ export class Video extends Model<Video> {
|
||||
@AfterUpdate
|
||||
static notifyForUpdate(video: Video) {
|
||||
this.notify(video, "update");
|
||||
video.sync().catch(() => {});
|
||||
video.sync().catch((err) => {
|
||||
logger.error("sync video error", video.id, err);
|
||||
});
|
||||
}
|
||||
|
||||
@AfterDestroy
|
||||
|
||||
@@ -29,9 +29,11 @@ class Storage {
|
||||
return this.api.get(`/${key}`);
|
||||
}
|
||||
|
||||
put(key: string, filePath: string) {
|
||||
put(key: string, filePath: string, contentType?: string) {
|
||||
const data = fs.readFileSync(filePath);
|
||||
return this.api.postForm(`/${key}`, data);
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([data], { type: contentType }), key);
|
||||
return this.api.postForm(`/${key}`, form);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,20 @@ import { RouterProvider } from "react-router-dom";
|
||||
import { Toaster, toast } from "@renderer/components/ui";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import { LookupWidget, TranslateWidget } from "./components";
|
||||
import Bugsnag from "@bugsnag/electron";
|
||||
import BugsnagPluginReact from "@bugsnag/plugin-react";
|
||||
import { BUGSNAG_API_KEY } from "@/constants";
|
||||
|
||||
function App() {
|
||||
window.__ENJOY_APP__.app.isPackaged().then((isPackaged) => {
|
||||
if (isPackaged) {
|
||||
Bugsnag.start({
|
||||
apiKey: BUGSNAG_API_KEY,
|
||||
plugins: [new BugsnagPluginReact()],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.__ENJOY_APP__.onNotification((_event, notification) => {
|
||||
switch (notification.type) {
|
||||
case "success":
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { useEffect, useContext, useRef, useState, useMemo } from "react";
|
||||
import {
|
||||
useEffect,
|
||||
useContext,
|
||||
useRef,
|
||||
useState,
|
||||
useMemo,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import {
|
||||
AppSettingsProviderContext,
|
||||
HotKeysSettingsProviderContext,
|
||||
@@ -261,22 +268,25 @@ export const MediaCurrentRecording = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const playWord = (word: string, index: number) => {
|
||||
const candidates = caption.timeline.filter(
|
||||
(w: TimelineEntry) => w.text.toLowerCase() === word.toLowerCase()
|
||||
);
|
||||
const target = candidates[index];
|
||||
if (!target) return;
|
||||
const playWord = useCallback(
|
||||
(word: string, index: number) => {
|
||||
const candidates = caption.timeline.filter(
|
||||
(w: TimelineEntry) => w.text.toLowerCase() === word.toLowerCase()
|
||||
);
|
||||
const target = candidates[index];
|
||||
if (!target) return;
|
||||
|
||||
const wordIndex = caption.timeline.findIndex(
|
||||
(w) => w.startTime === target.startTime
|
||||
);
|
||||
const wordIndex = caption.timeline.findIndex(
|
||||
(w) => w.startTime === target.startTime
|
||||
);
|
||||
|
||||
toggleRegion([wordIndex]);
|
||||
setTimeout(() => {
|
||||
wavesurfer?.playPause();
|
||||
}, 250);
|
||||
};
|
||||
toggleRegion([wordIndex]);
|
||||
setTimeout(() => {
|
||||
wavesurfer?.playPause();
|
||||
}, 250);
|
||||
},
|
||||
[caption?.timeline, toggleRegion, wavesurfer]
|
||||
);
|
||||
|
||||
const calContainerSize = () => {
|
||||
const size = ref?.current
|
||||
@@ -709,9 +719,7 @@ export const MediaCurrentRecording = () => {
|
||||
|
||||
<RecordingDetail
|
||||
recording={currentRecording}
|
||||
onPlayOrigin={(word: string, index: number = 0) =>
|
||||
playWord(word, index)
|
||||
}
|
||||
onPlayOrigin={playWord}
|
||||
/>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { t } from "i18next";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { PronunciationAssessmentWordResult } from "@renderer/components";
|
||||
import { Switch, ScrollArea } from "@renderer/components/ui";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
@@ -30,6 +30,11 @@ export const PronunciationAssessmentFulltextResult = (props: {
|
||||
monotone: true,
|
||||
});
|
||||
|
||||
const handlePlayOrigin = useCallback((word: string, index: number) => {
|
||||
if (!onPlayOrigin) return;
|
||||
onPlayOrigin(word, index);
|
||||
}, []);
|
||||
|
||||
const calErrorStats = () => {
|
||||
return {
|
||||
mispronunciation: words.filter(
|
||||
@@ -69,14 +74,14 @@ export const PronunciationAssessmentFulltextResult = (props: {
|
||||
currentTime={currentTime}
|
||||
src={src}
|
||||
onPlayOrigin={() => {
|
||||
if (!onPlayOrigin) return;
|
||||
// if (!onPlayOrigin) return;
|
||||
|
||||
const word = words[index];
|
||||
const candidates = words.filter((w) => w.word === word.word);
|
||||
const wordIndex = candidates.findIndex(
|
||||
(w) => w.offset === word.offset
|
||||
);
|
||||
onPlayOrigin(word.word, wordIndex);
|
||||
handlePlayOrigin(word.word, wordIndex);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ScrollBar,
|
||||
} from "@renderer/components/ui";
|
||||
import { Volume2Icon } from "lucide-react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { memo, useEffect, useRef } from "react";
|
||||
|
||||
export const PronunciationAssessmentWordResult = (props: {
|
||||
src?: string;
|
||||
@@ -219,29 +219,28 @@ const scoreColor = (score: number) => {
|
||||
return "font-bold text-red-600";
|
||||
};
|
||||
|
||||
export const PronunciationAssessmentPhonemeResult = (props: {
|
||||
result: PronunciationAssessmentWordResultType;
|
||||
}) => {
|
||||
const { result } = props;
|
||||
console.log(result);
|
||||
export const PronunciationAssessmentPhonemeResult = memo(
|
||||
(props: { result: PronunciationAssessmentWordResultType }) => {
|
||||
const { result } = props;
|
||||
|
||||
return (
|
||||
<ScrollArea className="w-full">
|
||||
<div className="w-full flex items-center gap-2">
|
||||
{result.phonemes.map((phoneme, index) => (
|
||||
<div key={index} className="text-sm text-center">
|
||||
<div className="font-bold font-code">{phoneme.phoneme}</div>
|
||||
<div
|
||||
className={`text-xs font-serif ${scoreColor(
|
||||
phoneme.pronunciationAssessment.accuracyScore
|
||||
)}`}
|
||||
>
|
||||
{phoneme.pronunciationAssessment.accuracyScore}
|
||||
return (
|
||||
<ScrollArea className="w-full">
|
||||
<div className="w-full flex items-center gap-2">
|
||||
{result.phonemes.map((phoneme, index) => (
|
||||
<div key={index} className="text-sm text-center">
|
||||
<div className="font-bold font-code">{phoneme.phoneme}</div>
|
||||
<div
|
||||
className={`text-xs font-serif ${scoreColor(
|
||||
phoneme.pronunciationAssessment.accuracyScore
|
||||
)}`}
|
||||
>
|
||||
{phoneme.pronunciationAssessment.accuracyScore}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
))}
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ export const RecordingDetail = (props: {
|
||||
recording: RecordingType;
|
||||
pronunciationAssessment?: PronunciationAssessmentType;
|
||||
onAssess?: (assessment: PronunciationAssessmentType) => void;
|
||||
onPlayOrigin?: (word: string) => void;
|
||||
onPlayOrigin?: (word: string, index: number) => void;
|
||||
}) => {
|
||||
const { recording, onAssess, onPlayOrigin } = props;
|
||||
if (!recording) return;
|
||||
|
||||
@@ -46,6 +46,7 @@ export default defineConfig((env) => {
|
||||
}
|
||||
const config: UserConfig = {
|
||||
build: {
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: forgeConfigSelf.entry!,
|
||||
fileName: () => "[name].js",
|
||||
|
||||
@@ -16,6 +16,7 @@ export default defineConfig((env) => {
|
||||
mode,
|
||||
base: "./",
|
||||
build: {
|
||||
sourcemap: true,
|
||||
outDir: `.vite/renderer/${name}`,
|
||||
target: "esnext",
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"enjoy:publish": "yarn workspace enjoy publish",
|
||||
"enjoy:lint": "yarn workspace enjoy eslint --ext .ts,.tsx .",
|
||||
"enjoy:create-migration": "yarn workspace enjoy create-migration",
|
||||
"enjoy:sourcemap": "yarn workspace enjoy sourcemap",
|
||||
"docs:dev": "yarn workspace 1000-hours dev",
|
||||
"docs:build": "yarn workspace 1000-hours build",
|
||||
"docs:preview": "yarn workspace 1000-hours preview",
|
||||
|
||||
Reference in New Issue
Block a user