Merge pull request #87 from an-lee/main

Fix bugs in Apple Mac M1/M2 chip
This commit is contained in:
an-lee
2024-01-10 20:03:38 +08:00
committed by GitHub
23 changed files with 174 additions and 94 deletions

View File

@@ -1,3 +1,4 @@
nodeLinker: node-modules
nmHoistingLimits: workspaces
yarnPath: .yarn/releases/yarn-4.0.2.cjs

View File

@@ -124,6 +124,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-i18next": "^14.0.0",
"react-markdown": "^9.0.1",
"react-router-dom": "^6.21.1",
@@ -134,6 +135,7 @@
"sequelize-typescript": "^2.1.6",
"sqlite3": "^5.1.7",
"tailwind-scrollbar-hide": "^1.1.7",
"ts-key-enum": "^2.0.12",
"umzug": "^3.5.0",
"wavesurfer.js": "^7.6.1",
"zod": "^3.22.4"

View File

@@ -153,6 +153,7 @@
"autoScroll": "auto scroll",
"detail": "detail",
"remove": "remove",
"share": "share",
"loadMore": "Load more",
"databaseError": "Failed to connect to database {{url}}",
"somethingWentWrong": "Something went wrong",

View File

@@ -153,6 +153,7 @@
"autoScroll": "自动滚动",
"detail": "详情",
"remove": "删除",
"share": "分享",
"loadMore": "加载更多",
"databaseError": "数据库错误 {{url}}",
"somethingWentWrong": "出错了",

View File

@@ -1,4 +1,4 @@
import { app, BrowserWindow, protocol, net } from "electron";
import { app, BrowserWindow, globalShortcut, protocol, net } from "electron";
import path from "path";
import settings from "@main/settings";
import "@main/i18n";
@@ -50,6 +50,10 @@ app.on("ready", async () => {
});
mainWindow.init();
globalShortcut.register("CommandOrControl+Shift+I", () => {
mainWindow.win.webContents.toggleDevTools();
});
});
// Quit when all windows are closed, except on macOS. There, it's common

View File

@@ -37,7 +37,7 @@ const logger = log.scope("db/models/audio");
timestamps: true,
})
export class Audio extends Model<Audio> {
@IsUUID(4)
@IsUUID("all")
@Default(DataType.UUIDV4)
@Column({ primaryKey: true, type: DataType.UUID })
id: string;
@@ -175,11 +175,6 @@ export class Audio extends Model<Audio> {
} catch (err) {
logger.error("failed to generate metadata", err.message);
}
// Generate unique ID base on user ID and audio MD5
const userId = settings.getSync("user.id");
audio.id = uuidv5(`${userId}/${audio.md5}`, uuidv5.URL);
logger.info("generated ID:", audio.id);
}
@AfterCreate
@@ -241,6 +236,11 @@ export class Audio extends Model<Audio> {
const md5 = await hashFile(filePath, { algo: "md5" });
// Generate ID
const userId = settings.getSync("user.id");
const id = uuidv5(`${userId}/${md5}`, uuidv5.URL);
logger.debug("Generated ID:", id);
const destDir = path.join(settings.userDataPath(), "audios");
const destFile = path.join(destDir, `${md5}${extname}`);
@@ -265,6 +265,7 @@ export class Audio extends Model<Audio> {
coverUrl,
} = params || {};
const record = this.build({
id,
source,
md5,
name,

View File

@@ -31,7 +31,7 @@ import webApi from "@main/web-api";
},
}))
export class PronunciationAssessment extends Model<PronunciationAssessment> {
@IsUUID(4)
@IsUUID('all')
@Default(DataType.UUIDV4)
@Column({ primaryKey: true, type: DataType.UUID })
id: string;

View File

@@ -35,7 +35,7 @@ import camelcaseKeys from "camelcase-keys";
timestamps: true,
})
export class Recording extends Model<Recording> {
@IsUUID(4)
@IsUUID('all')
@Default(DataType.UUIDV4)
@Column({ primaryKey: true, type: DataType.UUID })
id: string;

View File

@@ -25,7 +25,7 @@ const logger = log.scope("db/models/transcription");
timestamps: true,
})
export class Transcription extends Model<Transcription> {
@IsUUID(4)
@IsUUID('all')
@Default(DataType.UUIDV4)
@Column({ primaryKey: true, type: DataType.UUID })
id: string;

View File

@@ -37,7 +37,7 @@ const logger = log.scope("db/models/video");
timestamps: true,
})
export class Video extends Model<Video> {
@IsUUID(4)
@IsUUID('all')
@Default(DataType.UUIDV4)
@Column({ primaryKey: true, type: DataType.UUID })
id: string;
@@ -196,11 +196,6 @@ export class Video extends Model<Video> {
} catch (err) {
logger.error("failed to generate metadata", err.message);
}
// Generate unique ID base on user ID and audio MD5
const userId = settings.getSync("user.id");
video.id = uuidv5(`${userId}/${video.md5}`, uuidv5.URL);
logger.info("generated ID:", video.id);
}
@AfterCreate
@@ -263,6 +258,11 @@ export class Video extends Model<Video> {
const md5 = await hashFile(filePath, { algo: "md5" });
// Generate ID
const userId = settings.getSync("user.id");
const id = uuidv5(`${userId}/${md5}`, uuidv5.URL);
logger.debug("Generated ID:", id);
const destDir = path.join(settings.userDataPath(), "videos");
const destFile = path.join(destDir, `${md5}${extname}`);
@@ -287,6 +287,7 @@ export class Video extends Model<Video> {
coverUrl,
} = params || {};
const record = this.build({
id,
source,
md5,
name,

View File

@@ -1,4 +1,4 @@
import { ipcMain, app } from "electron";
import { ipcMain } from "electron";
import Ffmpeg from "fluent-ffmpeg";
import settings from "@main/settings";
import log from "electron-log/main";
@@ -6,8 +6,9 @@ import path from "path";
import fs from "fs-extra";
import AdmZip from "adm-zip";
import downloader from "@main/downloader";
import storage from "@main/storage";
const logger = log.scope("FFMPEG");
const logger = log.scope("ffmepg");
export default class FfmpegWrapper {
public ffmpeg: Ffmpeg.FfmpegCommand;
@@ -15,8 +16,10 @@ export default class FfmpegWrapper {
const config = settings.ffmpegConfig();
if (config.commandExists) {
logger.info("Using system ffmpeg");
this.ffmpeg = Ffmpeg();
} else {
logger.info("Using downloaded ffmpeg");
const ff = Ffmpeg();
ff.setFfmpegPath(config.ffmpegPath);
ff.setFfprobePath(config.ffprobePath);
@@ -31,6 +34,10 @@ export default class FfmpegWrapper {
.on("start", (commandLine) => {
logger.info("Spawned FFmpeg with command: " + commandLine);
})
.on("error", (err) => {
logger.error(err);
reject(err);
})
.ffprobe((err, metadata) => {
if (err) {
logger.error(err);
@@ -121,6 +128,7 @@ export default class FfmpegWrapper {
resolve(output);
})
.on("error", (err: Error) => {
logger.error(err);
reject(err);
})
.save(output);
@@ -167,7 +175,41 @@ export class FfmpegDownloader {
}
}
async downloadForDarwinArm64(webContents?: Electron.WebContents) {
const DARWIN_FFMPEG_ARM64_URL = storage.getUrl(
"ffmpeg-apple-arm64-build-6.0.zip"
);
fs.ensureDirSync(path.join(settings.libraryPath(), "ffmpeg"));
const ffmpegZipPath = await downloader.download(DARWIN_FFMPEG_ARM64_URL, {
webContents,
});
const ffmepgZip = new AdmZip(ffmpegZipPath);
ffmepgZip.extractEntryTo(
"ffmpeg/ffmpeg",
path.join(settings.libraryPath(), "ffmpeg"),
false,
true
);
ffmepgZip.extractEntryTo(
"ffmpeg/ffprobe",
path.join(settings.libraryPath(), "ffmpeg"),
false,
true
);
fs.chmodSync(path.join(settings.libraryPath(), "ffmpeg", "ffmpeg"), 0o775);
fs.chmodSync(path.join(settings.libraryPath(), "ffmpeg", "ffprobe"), 0o775);
}
async downloadForDarwin(webContents?: Electron.WebContents) {
if (process.arch === "arm64") {
return this.downloadForDarwinArm64(webContents);
}
const DARWIN_FFMPEG_URL = "https://evermeet.cx/ffmpeg/getrelease/zip";
const DARWIN_FFPROBE_URL =
"https://evermeet.cx/ffmpeg/getrelease/ffprobe/zip";
@@ -185,10 +227,7 @@ export class FfmpegDownloader {
true
);
fs.chmodSync(
path.join(settings.libraryPath(), "ffmpeg", "ffmpeg"),
0o775
);
fs.chmodSync(path.join(settings.libraryPath(), "ffmpeg", "ffmpeg"), 0o775);
const ffprobeZipPath = await downloader.download(DARWIN_FFPROBE_URL, {
webContents,
@@ -200,10 +239,7 @@ export class FfmpegDownloader {
false,
true
);
fs.chmodSync(
path.join(settings.libraryPath(), "ffmpeg", "ffprobe"),
0o775
);
fs.chmodSync(path.join(settings.libraryPath(), "ffmpeg", "ffprobe"), 0o775);
return settings.ffmpegConfig();
}

View File

@@ -2,7 +2,7 @@ import log from "electron-log/main";
import $ from "cheerio";
import { BrowserView, ipcMain } from "electron";
const logger = log.scope("AUDIBLE_PROVIDER");
const logger = log.scope("providers/audible-provider");
export class AudibleProvider {
baseURL: string;
@@ -12,6 +12,7 @@ export class AudibleProvider {
}
scrape = async (path: string) => {
logger.debug(`Scraping ${this.baseURL + path}`);
return new Promise<string>((resolve, reject) => {
const view = new BrowserView();
view.webContents.loadURL(this.baseURL + path);

View File

@@ -5,6 +5,9 @@ import path from "path";
import fs from "fs-extra";
import os from "os";
import commandExists from "command-exists";
import log from "electron-log";
const logger = log.scope("settings");
const libraryPath = () => {
const _library = settings.getSync("library");
@@ -96,12 +99,18 @@ const ffmpegConfig = () => {
const ready = Boolean(_commandExists || (ffmpegPath && ffprobePath));
return {
const config = {
os: os.platform(),
arch: os.arch(),
commandExists: _commandExists,
ffmpegPath,
ffprobePath,
ready,
};
logger.info("ffmpeg config", config);
return config;
};
export default {

View File

@@ -9,7 +9,7 @@ import { exec } from "child_process";
import fs from "fs-extra";
import log from "electron-log/main";
const logger = log.scope("WHISPER");
const logger = log.scope("whisper");
const MAGIC_TOKENS = ["Mrs.", "Ms.", "Mr.", "Dr.", "Prof.", "St."];
const END_OF_WORD_REGEX = /[^\.!,\?][\.!\?]/g;
class Whipser {

View File

@@ -21,7 +21,7 @@ import { AudibleProvider, TedProvider } from "@main/providers";
import { FfmpegDownloader } from "@main/ffmpeg";
log.initialize({ preload: true });
const logger = log.scope("WINDOW");
const logger = log.scope("window");
const audibleProvider = new AudibleProvider();
const tedProvider = new TedProvider();
@@ -147,7 +147,15 @@ main.init = () => {
const view = mainWindow.getBrowserView();
if (!view) return;
view.setBounds({ x: 0, y: 0, width: 0, height: 0 });
const bounds = view.getBounds();
logger.debug("current view bounds", bounds);
view.setBounds({
x: -bounds.width,
y: -bounds.height,
width: 0,
height: 0,
});
});
ipcMain.handle(
@@ -161,8 +169,11 @@ main.init = () => {
height: number;
}
) => {
const view = mainWindow.getBrowserView();
if (!view) return;
logger.debug("view-show", bounds);
mainWindow.getBrowserView()?.setBounds(bounds);
view.setBounds(bounds);
}
);

View File

@@ -5,12 +5,18 @@ import { AppSettingsProviderContext } from "@renderer/context";
import { CheckCircle2Icon, XCircleIcon, LoaderIcon } from "lucide-react";
export const FfmpegCheck = () => {
const { ffmpegConfg, setFfmegConfig, EnjoyApp } = useContext(
const { ffmpegConfig, setFfmegConfig, EnjoyApp } = useContext(
AppSettingsProviderContext
);
const [downloading, setDownloading] = useState(false);
const [progress, setProgress] = useState(0);
const refreshFfmpegConfig = async () => {
EnjoyApp.settings.getFfmpegConfig().then((config) => {
setFfmegConfig(config);
});
};
const downloadFfmpeg = () => {
listenToDownloadState();
setDownloading(true);
@@ -35,11 +41,15 @@ export const FfmpegCheck = () => {
useEffect(() => {
return EnjoyApp.download.removeAllListeners();
}, [ffmpegConfg?.ready]);
}, [ffmpegConfig?.ready]);
useEffect(() => {
refreshFfmpegConfig();
}, []);
return (
<div className="w-full max-w-sm px-6">
{ffmpegConfg?.ready ? (
{ffmpegConfig?.ready ? (
<>
<div className="flex justify-center items-center mb-8">
<img src="./assets/ffmpeg-logo.svg" className="" />

View File

@@ -26,6 +26,8 @@ import {
DefaultVideoLayout,
defaultLayoutIcons,
} from "@vidstack/react/player/layouts/default";
import { useHotkeys } from "react-hotkeys-hook";
import { Key } from "ts-key-enum";
const minPxPerSecBase = 150;
@@ -499,6 +501,15 @@ export const MediaPlayer = (props: {
return fitZoomRatio;
};
useHotkeys(
" ",
() => {
if (!wavesurfer) return;
onPlayClick();
},
[wavesurfer]
);
return (
<>
<div className="mb-2" ref={containerRef} />

View File

@@ -5,6 +5,7 @@ import RecordPlugin from "wavesurfer.js/dist/plugins/record";
import WaveSurfer from "wavesurfer.js";
import { cn } from "@renderer/lib/utils";
import { RadialProgress, useToast } from "@renderer/components/ui";
import { useHotkeys } from "react-hotkeys-hook";
export const RecordButton = (props: {
className?: string;
@@ -17,6 +18,11 @@ export const RecordButton = (props: {
const [duration, setDuration] = useState<number>(0);
const { toast } = useToast();
useHotkeys(["command+alt+r", "control+alt+r"], () => {
if (disabled) return;
setIsRecording((isRecording) => !isRecording);
});
useEffect(() => {
if (!isRecording) return;

View File

@@ -1,9 +1,6 @@
import { useState, useContext } from "react";
import { AppSettingsProviderContext } from "@/renderer/context";
import {
RecordingPlayer,
PronunciationAssessmentScoreIcon,
} from "@renderer/components";
import { RecordingPlayer } from "@renderer/components";
import {
AlertDialog,
AlertDialogHeader,
@@ -16,12 +13,10 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@renderer/components/ui";
import { MoreHorizontalIcon, Trash2Icon, InfoIcon } from "lucide-react";
import { formatDateTime , secondsToTimestamp } from "@renderer/lib/utils";
import { useLongPress } from "@uidotdev/usehooks";
import { ChevronDownIcon, Trash2Icon, InfoIcon, Share2Icon } from "lucide-react";
import { formatDateTime, secondsToTimestamp } from "@renderer/lib/utils";
import { t } from "i18next";
export const RecordingCard = (props: {
@@ -30,45 +25,19 @@ export const RecordingCard = (props: {
onSelect?: () => void;
}) => {
const { recording, id, onSelect } = props;
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const [isPlaying, setIsPlaying] = useState(false);
const longPressAttrs = useLongPress(
() => {
setIsMenuOpen(true);
},
{
onFinish: () => {},
onCancel: () => {},
threshold: 400,
}
);
const handleDelete = () => {
EnjoyApp.recordings.destroy(recording.id);
};
return (
<div
id={id}
className={`flex items-center justify-end p-4 transition-all ${
isMenuOpen ? "bg-sky-500/20" : ""
}`}
>
<DropdownMenu
open={isMenuOpen}
onOpenChange={(value) => setIsMenuOpen(value)}
>
<div {...longPressAttrs} className="w-full">
<div className="flex items-center space-x-2 justify-start px-2 mb-1">
<DropdownMenuTrigger>
<MoreHorizontalIcon className="w-4 h-4 text-muted-foreground hidden" />
</DropdownMenuTrigger>
</div>
<div className="bg-white rounded-lg py-2 px-4 mb-2 relative">
<div id={id} className="flex items-center justify-end px-4 transition-all">
<DropdownMenu>
<div className="w-full">
<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)}
@@ -81,7 +50,7 @@ export const RecordingCard = (props: {
setIsPlaying={setIsPlaying}
/>
<div className="flex items-center justify-between space-x-2">
<div className="flex items-center justify-end space-x-2">
<Button
onClick={onSelect}
variant="ghost"
@@ -104,22 +73,19 @@ export const RecordingCard = (props: {
`}
/>
</Button>
<span className="text-xs text-muted-foreground">
{formatDateTime(recording.createdAt)}
</span>
<DropdownMenuTrigger>
<ChevronDownIcon className="w-4 h-4 text-muted-foreground" />
</DropdownMenuTrigger>
</div>
</div>
<div className="flex justify-end">
<span className="text-xs text-muted-foreground">
{formatDateTime(recording.createdAt)}
</span>
</div>
</div>
<DropdownMenuContent>
<DropdownMenuItem onClick={onSelect}>
<span className="mr-auto capitalize">{t("detail")}</span>
<InfoIcon className="w-4 h-4" />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setIsDeleteDialogOpen(true)}>
<span className="mr-auto text-destructive capitalize">
{t("delete")}

View File

@@ -156,7 +156,7 @@ export const RecordingsList = (props: {
</div>
)}
<div className="flex flex-col-reverse">
<div className="flex flex-col-reverse space-y-4">
<div className="w-full h-24"></div>
{recordings.map((recording) => (
<RecordingCard

View File

@@ -11,7 +11,7 @@ type AppSettingsProviderState = {
logout?: () => void;
setLibraryPath?: (path: string) => Promise<void>;
setWhisperModel?: (name: string) => void;
ffmpegConfg?: FfmpegConfigType;
ffmpegConfig?: FfmpegConfigType;
setFfmegConfig?: (config: FfmpegConfigType) => void;
EnjoyApp?: EnjoyAppType;
};
@@ -35,7 +35,7 @@ export const AppSettingsProvider = ({
const [libraryPath, setLibraryPath] = useState("");
const [whisperModelsPath, setWhisperModelsPath] = useState<string>("");
const [whisperModel, setWhisperModel] = useState<string>(null);
const [ffmpegConfg, setFfmegConfig] = useState<FfmpegConfigType>(null);
const [ffmpegConfig, setFfmegConfig] = useState<FfmpegConfigType>(null);
const EnjoyApp = window.__ENJOY_APP__;
useEffect(() => {
@@ -52,7 +52,7 @@ export const AppSettingsProvider = ({
useEffect(() => {
validate();
}, [user, libraryPath, whisperModel, ffmpegConfg]);
}, [user, libraryPath, whisperModel, ffmpegConfig]);
const fetchFfmpegConfig = async () => {
const config = await EnjoyApp.settings.getFfmpegConfig();
@@ -114,7 +114,7 @@ export const AppSettingsProvider = ({
const validate = async () => {
setInitialized(
!!(user && libraryPath && whisperModel && ffmpegConfg?.ready)
!!(user && libraryPath && whisperModel && ffmpegConfig?.ready)
);
};
@@ -131,7 +131,7 @@ export const AppSettingsProvider = ({
whisperModelsPath,
whisperModel,
setWhisperModel: setModelHandler,
ffmpegConfg,
ffmpegConfig,
setFfmegConfig,
initialized,
}}

View File

@@ -16,13 +16,13 @@ export default () => {
const [currentStep, setCurrentStep] = useState<number>(1);
const [currentStepValid, setCurrentStepValid] = useState<boolean>(false);
const { user, libraryPath, whisperModel, ffmpegConfg, initialized } =
const { user, libraryPath, whisperModel, ffmpegConfig, initialized } =
useContext(AppSettingsProviderContext);
const totalSteps = 5;
useEffect(() => {
validateCurrentStep();
}, [currentStep, user, whisperModel, ffmpegConfg]);
}, [currentStep, user, whisperModel, ffmpegConfig]);
const validateCurrentStep = async () => {
switch (currentStep) {
@@ -36,7 +36,7 @@ export default () => {
setCurrentStepValid(!!whisperModel);
break;
case 4:
setCurrentStepValid(ffmpegConfg?.ready);
setCurrentStepValid(ffmpegConfig?.ready);
break;
case 5:
setCurrentStepValid(initialized);

View File

@@ -5792,6 +5792,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-i18next: "npm:^14.0.0"
react-markdown: "npm:^9.0.1"
react-router-dom: "npm:^6.21.1"
@@ -5805,6 +5806,7 @@ __metadata:
tailwind-scrollbar-hide: "npm:^1.1.7"
tailwindcss: "npm:^3.4.1"
tailwindcss-animate: "npm:^1.0.7"
ts-key-enum: "npm:^2.0.12"
ts-node: "npm:^10.9.2"
tslib: "npm:^2.6.2"
typescript: "npm:^5.3.3"
@@ -10633,6 +10635,16 @@ __metadata:
languageName: node
linkType: hard
"react-hotkeys-hook@npm:^4.4.3":
version: 4.4.3
resolution: "react-hotkeys-hook@npm:4.4.3"
peerDependencies:
react: ">=16.8.1"
react-dom: ">=16.8.1"
checksum: ef79e279129f6e55d81c8762b1da214d9c6ee4617b9597dbc3f93057cba8d166831508967b8e3f763a0c4ce0af3b59e6888c6fc94d152deac9335020b2ba80df
languageName: node
linkType: hard
"react-i18next@npm:^14.0.0":
version: 14.0.0
resolution: "react-i18next@npm:14.0.0"
@@ -12114,6 +12126,13 @@ __metadata:
languageName: node
linkType: hard
"ts-key-enum@npm:^2.0.12":
version: 2.0.12
resolution: "ts-key-enum@npm:2.0.12"
checksum: 1d9cf8085785bdc324827c5c38f6359b09d9438deab81dfab7fa6d8315c618280ba7527e98d06b68c11066a5a81b06ef84eb378a48bf80ca5772ab0e4c6683d5
languageName: node
linkType: hard
"ts-node@npm:^10.9.2":
version: 10.9.2
resolution: "ts-node@npm:10.9.2"