Merge pull request #87 from an-lee/main
Fix bugs in Apple Mac M1/M2 chip
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
nodeLinker: node-modules
|
||||
nmHoistingLimits: workspaces
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.0.2.cjs
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"autoScroll": "自动滚动",
|
||||
"detail": "详情",
|
||||
"remove": "删除",
|
||||
"share": "分享",
|
||||
"loadMore": "加载更多",
|
||||
"databaseError": "数据库错误 {{url}}",
|
||||
"somethingWentWrong": "出错了",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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="" />
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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);
|
||||
|
||||
19
yarn.lock
19
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user