Improve audio/video adding & display (#1181)
* improve audio/video adding * show window when ready * add actions for audio/video card
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"size": "size",
|
||||
"source": "source",
|
||||
"createdAt": "created at",
|
||||
"updatedAt": "updated at",
|
||||
"recordingsCount": "recordings count",
|
||||
"recordingsDuration": "recordings duration",
|
||||
"isTranscribed": "transcribed",
|
||||
@@ -40,6 +41,7 @@
|
||||
"size": "size",
|
||||
"source": "source",
|
||||
"createdAt": "created at",
|
||||
"updatedAt": "updated at",
|
||||
"recordingsCount": "recordings count",
|
||||
"recordingsDuration": "recordings duration",
|
||||
"isTranscribed": "transcribed",
|
||||
@@ -923,5 +925,7 @@
|
||||
"whisperCppEngineDescription": "C++ port of the Whisper architecture.",
|
||||
"ttsService": "Text to Speech Service",
|
||||
"openaiTtsServiceDescription": "Use OpenAI TTS service from your own key.",
|
||||
"enjoyTtsServiceDescription": "Use TTS service provided by Enjoy. OpenAI or Azure is supported."
|
||||
"enjoyTtsServiceDescription": "Use TTS service provided by Enjoy. OpenAI or Azure is supported.",
|
||||
"compressMediaBeforeAdding": "Compress media before adding",
|
||||
"keepOriginalMedia": "Keep original media"
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"size": "大小",
|
||||
"source": "来源",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间",
|
||||
"recordingsCount": "练习次数",
|
||||
"recordingsDuration": "练习时长",
|
||||
"isTranscribed": "语音文本",
|
||||
@@ -40,6 +41,7 @@
|
||||
"size": "大小",
|
||||
"source": "来源",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间",
|
||||
"recordingsCount": "练习次数",
|
||||
"recordingsDuration": "练习时长",
|
||||
"isTranscribed": "语音文本",
|
||||
@@ -923,5 +925,7 @@
|
||||
"whisperCppEngineDescription": "Whisper 的 C++ 实现。",
|
||||
"ttsService": "文字转语音服务",
|
||||
"openaiTtsServiceDescription": "使用您自己的 API key 来使用 OpenAI TTS 服务。",
|
||||
"enjoyTtsServiceDescription": "使用 Enjoy 提供的 TTS 服务,支持 OpenAI 或 Azure。"
|
||||
"enjoyTtsServiceDescription": "使用 Enjoy 提供的 TTS 服务,支持 OpenAI 或 Azure。",
|
||||
"compressMediaBeforeAdding": "添加前压缩媒体",
|
||||
"keepOriginalMedia": "保存原始媒体"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ class AudiosHandler {
|
||||
name?: string;
|
||||
coverUrl?: string;
|
||||
originalText?: string;
|
||||
compressing?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
let file = uri;
|
||||
@@ -90,6 +91,7 @@ class AudiosHandler {
|
||||
source,
|
||||
name: params.name,
|
||||
coverUrl: params.coverUrl,
|
||||
compressing: params.compressing,
|
||||
});
|
||||
|
||||
// create transcription if originalText is provided
|
||||
|
||||
@@ -68,6 +68,7 @@ class VideosHandler {
|
||||
name?: string;
|
||||
coverUrl?: string;
|
||||
md5?: string;
|
||||
compressing?: boolean;
|
||||
} = {}
|
||||
) {
|
||||
let file = uri;
|
||||
|
||||
@@ -161,8 +161,9 @@ export class Audio extends Model<Audio> {
|
||||
|
||||
get extname(): string {
|
||||
return (
|
||||
this.getDataValue("metadata").extname ||
|
||||
path.extname(this.getDataValue("source")) ||
|
||||
this.getDataValue("metadata")?.extname ||
|
||||
(this.getDataValue("source") &&
|
||||
path.extname(this.getDataValue("source"))) ||
|
||||
""
|
||||
);
|
||||
}
|
||||
@@ -308,8 +309,10 @@ export class Audio extends Model<Audio> {
|
||||
description?: string;
|
||||
source?: string;
|
||||
coverUrl?: string;
|
||||
compressing?: boolean;
|
||||
}
|
||||
): Promise<Audio | Video> {
|
||||
const { compressing = true } = params || {};
|
||||
// Check if file exists
|
||||
try {
|
||||
fs.accessSync(filePath, fs.constants.R_OK);
|
||||
@@ -334,7 +337,9 @@ export class Audio extends Model<Audio> {
|
||||
},
|
||||
});
|
||||
if (!!existing) {
|
||||
logger.warn("Audio already exists:", existing.id);
|
||||
logger.warn("Audio already exists:", existing.id, existing.name);
|
||||
existing.changed("updatedAt", true);
|
||||
existing.update({ updatedAt: new Date() });
|
||||
return existing;
|
||||
}
|
||||
|
||||
@@ -344,7 +349,10 @@ export class Audio extends Model<Audio> {
|
||||
logger.debug("Generated ID:", id);
|
||||
|
||||
const destDir = path.join(settings.userDataPath(), "audios");
|
||||
const destFile = path.join(destDir, `${md5}.compressed.mp3`);
|
||||
const destFile = path.join(
|
||||
destDir,
|
||||
compressing ? `${md5}.compressed.mp3` : `${md5}${extname}`
|
||||
);
|
||||
|
||||
let metadata = {
|
||||
extname,
|
||||
@@ -363,8 +371,13 @@ export class Audio extends Model<Audio> {
|
||||
duration: fileMetadata.format.duration,
|
||||
});
|
||||
|
||||
// Compress file
|
||||
await ffmpeg.compressAudio(filePath, destFile);
|
||||
if (compressing) {
|
||||
// Compress file
|
||||
await ffmpeg.compressAudio(filePath, destFile);
|
||||
} else {
|
||||
// Copy file
|
||||
fs.copyFileSync(filePath, destFile);
|
||||
}
|
||||
|
||||
// Check if file copied
|
||||
fs.accessSync(destFile, fs.constants.R_OK);
|
||||
|
||||
@@ -324,8 +324,11 @@ export class Video extends Model<Video> {
|
||||
description?: string;
|
||||
source?: string;
|
||||
coverUrl?: string;
|
||||
compressing?: boolean;
|
||||
}
|
||||
): Promise<Audio | Video> {
|
||||
const { compressing = true } = params || {};
|
||||
|
||||
// Check if file exists
|
||||
try {
|
||||
fs.accessSync(filePath, fs.constants.R_OK);
|
||||
@@ -350,7 +353,9 @@ export class Video extends Model<Video> {
|
||||
},
|
||||
});
|
||||
if (!!existing) {
|
||||
logger.warn("Video already exists:", existing.id);
|
||||
logger.warn("Video already exists:", existing.id, existing.name);
|
||||
existing.changed("updatedAt", true);
|
||||
existing.update({ updatedAt: new Date() });
|
||||
return existing;
|
||||
}
|
||||
|
||||
@@ -360,7 +365,10 @@ export class Video extends Model<Video> {
|
||||
logger.debug("Generated ID:", id);
|
||||
|
||||
const destDir = path.join(settings.userDataPath(), "videos");
|
||||
const destFile = path.join(destDir, `${md5}.compressed.mp4`);
|
||||
const destFile = path.join(
|
||||
destDir,
|
||||
compressing ? `${md5}.compressed.mp4` : `${md5}${extname}`
|
||||
);
|
||||
|
||||
let metadata = {
|
||||
extname,
|
||||
@@ -379,8 +387,13 @@ export class Video extends Model<Video> {
|
||||
duration: fileMetadata.format.duration,
|
||||
});
|
||||
|
||||
// Compress file to destFile
|
||||
await ffmpeg.compressVideo(filePath, destFile);
|
||||
if (compressing) {
|
||||
// Compress file to destFile
|
||||
await ffmpeg.compressVideo(filePath, destFile);
|
||||
} else {
|
||||
// Copy file
|
||||
fs.copyFileSync(filePath, destFile);
|
||||
}
|
||||
|
||||
// Check if file copied
|
||||
fs.accessSync(destFile, fs.constants.R_OK);
|
||||
|
||||
@@ -509,6 +509,7 @@ ${log}
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
show: false,
|
||||
icon:
|
||||
process.platform === "win32" ? "./assets/icon.ico" : "./assets/icon.png",
|
||||
width: 1280,
|
||||
@@ -521,6 +522,10 @@ ${log}
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.on("ready-to-show", () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
mainWindow.on("resize", () => {
|
||||
mainWindow.webContents.send("window-on-resize", mainWindow.getBounds());
|
||||
});
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { cn } from "@renderer/lib/utils";
|
||||
import { AudioLinesIcon, CircleAlertIcon } from "lucide-react";
|
||||
import { Badge } from "@renderer/components/ui";
|
||||
import {
|
||||
AudioLinesIcon,
|
||||
CircleAlertIcon,
|
||||
EditIcon,
|
||||
MoreVerticalIcon,
|
||||
TrashIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
|
||||
export const AudioCard = (props: {
|
||||
audio: Partial<AudioType>;
|
||||
className?: string;
|
||||
onDelete?: () => void;
|
||||
onEdit?: () => void;
|
||||
}) => {
|
||||
const { audio, className } = props;
|
||||
const { audio, className, onDelete, onEdit } = props;
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
<div className={cn("w-full relative", className)}>
|
||||
<Link to={`/audios/${audio.id}`}>
|
||||
<div
|
||||
className="aspect-square border rounded-lg overflow-hidden flex relative"
|
||||
@@ -47,6 +62,38 @@ export const AudioCard = (props: {
|
||||
<div className="text-sm font-semibold mt-2 max-w-full line-clamp-2 h-10">
|
||||
{audio.name}
|
||||
</div>
|
||||
{(onDelete || onEdit) && (
|
||||
<div className="absolute right-1 top-1 z-10">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hover:bg-transparent w-6 h-6"
|
||||
>
|
||||
<MoreVerticalIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent>
|
||||
{onEdit && (
|
||||
<DropdownMenuItem onClick={onEdit}>
|
||||
<EditIcon className="size-4" />
|
||||
<span className="ml-2 text-sm">{t("edit")}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{onDelete && (
|
||||
<DropdownMenuItem onClick={onDelete}>
|
||||
<TrashIcon className="size-4 text-destructive" />
|
||||
<span className="ml-2 text-destructive text-sm">
|
||||
{t("delete")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
MediaAddButton,
|
||||
AudiosTable,
|
||||
AudioEditForm,
|
||||
LoaderSpin,
|
||||
} from "@renderer/components";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
@@ -255,15 +256,24 @@ export const AudiosComponent = () => {
|
||||
</div>
|
||||
|
||||
{audios.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-48 border border-dashed rounded-lg">
|
||||
{t("noData")}
|
||||
</div>
|
||||
loading ? (
|
||||
<LoaderSpin />
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-48 border border-dashed rounded-lg">
|
||||
{t("noData")}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<TabsContent value="grid">
|
||||
<div className="grid gap-4 grid-cols-5">
|
||||
{audios.map((audio) => (
|
||||
<AudioCard audio={audio} key={audio.id} />
|
||||
<AudioCard
|
||||
audio={audio}
|
||||
key={audio.id}
|
||||
onEdit={() => setEditing(audio)}
|
||||
onDelete={() => setDeleting(audio)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -280,7 +290,7 @@ export const AudiosComponent = () => {
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{hasMore && (
|
||||
{!loading && hasMore && (
|
||||
<div className="flex items-center justify-center my-4">
|
||||
<Button variant="link" onClick={() => fetchAudios()}>
|
||||
{t("loadMore")}
|
||||
|
||||
@@ -19,8 +19,7 @@ import {
|
||||
CheckCircleIcon,
|
||||
CircleAlertIcon,
|
||||
} from "lucide-react";
|
||||
import dayjs from "@renderer/lib/dayjs";
|
||||
import { secondsToTimestamp } from "@renderer/lib/utils";
|
||||
import { formatDateTime, secondsToTimestamp } from "@renderer/lib/utils";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const AudiosTable = (props: {
|
||||
@@ -46,7 +45,7 @@ export const AudiosTable = (props: {
|
||||
{t("models.audio.recordingsDuration")}
|
||||
</TableHead>
|
||||
<TableHead className="capitalize">
|
||||
{t("models.audio.createdAt")}
|
||||
{t("models.audio.updatedAt")}
|
||||
</TableHead>
|
||||
<TableHead className="capitalize">
|
||||
{t("models.audio.isTranscribed")}
|
||||
@@ -92,9 +91,7 @@ export const AudiosTable = (props: {
|
||||
<TableCell>
|
||||
{secondsToTimestamp(audio.recordingsDuration / 1000)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{dayjs(audio.createdAt).format("YYYY-MM-DD HH:mm")}
|
||||
</TableCell>
|
||||
<TableCell>{formatDateTime(audio.updatedAt)}</TableCell>
|
||||
<TableCell>
|
||||
{audio.transcribed ? (
|
||||
<CheckCircleIcon className="text-green-500 w-4 h-4" />
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
Button,
|
||||
Progress,
|
||||
toast,
|
||||
Switch,
|
||||
Label,
|
||||
} from "@renderer/components/ui";
|
||||
import { PlusCircleIcon, LoaderIcon } from "lucide-react";
|
||||
import { t } from "i18next";
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
AppSettingsProviderContext,
|
||||
DbProviderContext,
|
||||
} from "@renderer/context";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const MediaAddButton = (props: { type?: "Audio" | "Video" }) => {
|
||||
const { type = "Audio" } = props;
|
||||
@@ -26,10 +29,13 @@ export const MediaAddButton = (props: { type?: "Audio" | "Video" }) => {
|
||||
const { addDblistener, removeDbListener } = useContext(DbProviderContext);
|
||||
const [uri, setUri] = useState("");
|
||||
const [files, setFiles] = useState<string[]>([]);
|
||||
const [compressing, setCompressing] = useState(true);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [createdCount, setCreatedCount] = useState(0);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleOpen = (value: boolean) => {
|
||||
if (submitting) {
|
||||
setOpen(true);
|
||||
@@ -48,7 +54,13 @@ export const MediaAddButton = (props: { type?: "Audio" | "Video" }) => {
|
||||
setSubmitting(true);
|
||||
|
||||
if (files.length > 1) {
|
||||
Promise.allSettled(files.map((f) => EnjoyApp.audios.create(f)))
|
||||
Promise.allSettled(
|
||||
files.map((f) =>
|
||||
EnjoyApp[type.toLowerCase() as "audios" | "videos"].create(f, {
|
||||
compressing,
|
||||
})
|
||||
)
|
||||
)
|
||||
.then((results) => {
|
||||
const fulfilled = results.filter((r) => r.status === "fulfilled");
|
||||
const rejected = results.filter((r) => r.status === "rejected");
|
||||
@@ -87,8 +99,9 @@ export const MediaAddButton = (props: { type?: "Audio" | "Video" }) => {
|
||||
} else {
|
||||
EnjoyApp.audios
|
||||
.create(uri)
|
||||
.then(() => {
|
||||
.then((media) => {
|
||||
toast.success(t("resourceAdded"));
|
||||
navigate(`/${type.toLowerCase()}s/${media.id}`);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(err.message);
|
||||
@@ -135,7 +148,7 @@ export const MediaAddButton = (props: { type?: "Audio" | "Video" }) => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex space-x-2 mb-2">
|
||||
<Input
|
||||
placeholder="https://"
|
||||
value={uri}
|
||||
@@ -168,6 +181,17 @@ export const MediaAddButton = (props: { type?: "Audio" | "Video" }) => {
|
||||
{t("localFile")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<Switch
|
||||
checked={compressing}
|
||||
onCheckedChange={(value) => setCompressing(value)}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{compressing
|
||||
? t("compressMediaBeforeAdding")
|
||||
: t("keepOriginalMedia")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="">
|
||||
|
||||
@@ -1,17 +1,32 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { cn } from "@renderer/lib/utils";
|
||||
import { CircleAlertIcon, VideoIcon } from "lucide-react";
|
||||
import { Badge } from "@renderer/components/ui";
|
||||
import {
|
||||
CircleAlertIcon,
|
||||
VideoIcon,
|
||||
MoreVerticalIcon,
|
||||
TrashIcon,
|
||||
EditIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
|
||||
export const VideoCard = (props: {
|
||||
video: Partial<VideoType>;
|
||||
className?: string;
|
||||
onDelete?: () => void;
|
||||
onEdit?: () => void;
|
||||
}) => {
|
||||
const { video, className } = props;
|
||||
const { video, className, onDelete, onEdit } = props;
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
<div className={cn("w-full relative", className)}>
|
||||
<Link to={`/videos/${video.id}`}>
|
||||
<div
|
||||
className="aspect-[4/3] border rounded-lg overflow-hidden relative"
|
||||
@@ -45,6 +60,38 @@ export const VideoCard = (props: {
|
||||
<div className="text-sm font-semibold mt-2 max-w-full truncate">
|
||||
{video.name}
|
||||
</div>
|
||||
{(onDelete || onEdit) && (
|
||||
<div className="absolute right-1 top-1 z-10">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hover:bg-transparent w-6 h-6"
|
||||
>
|
||||
<MoreVerticalIcon className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent>
|
||||
{onEdit && (
|
||||
<DropdownMenuItem onClick={onEdit}>
|
||||
<EditIcon className="size-4" />
|
||||
<span className="ml-2 text-sm">{t("edit")}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{onDelete && (
|
||||
<DropdownMenuItem onClick={onDelete}>
|
||||
<TrashIcon className="size-4 text-destructive" />
|
||||
<span className="ml-2 text-destructive text-sm">
|
||||
{t("delete")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
VideosTable,
|
||||
VideoEditForm,
|
||||
MediaAddButton,
|
||||
LoaderSpin,
|
||||
} from "@renderer/components";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
@@ -251,15 +252,23 @@ export const VideosComponent = () => {
|
||||
</AlertDialog>
|
||||
</div>
|
||||
{videos.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-48 border border-dashed rounded-lg">
|
||||
{t("noData")}
|
||||
</div>
|
||||
loading ? (
|
||||
<LoaderSpin />
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-48 border border-dashed rounded-lg">
|
||||
{t("noData")}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<TabsContent value="grid">
|
||||
<div className="grid gap-4 grid-cols-4">
|
||||
{videos.map((video) => (
|
||||
<VideoCard video={video} key={video.id} />
|
||||
<VideoCard
|
||||
video={video}
|
||||
key={video.id}
|
||||
onDelete={() => setDeleting(video)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -275,7 +284,7 @@ export const VideosComponent = () => {
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{hasMore && (
|
||||
{!loading && hasMore && (
|
||||
<div className="flex items-center justify-center my-4">
|
||||
<Button variant="link" onClick={() => fetchVideos()}>
|
||||
{t("loadMore")}
|
||||
|
||||
@@ -19,8 +19,7 @@ import {
|
||||
CheckCircleIcon,
|
||||
CircleAlertIcon,
|
||||
} from "lucide-react";
|
||||
import dayjs from "@renderer/lib/dayjs";
|
||||
import { secondsToTimestamp } from "@renderer/lib/utils";
|
||||
import { formatDateTime, secondsToTimestamp } from "@renderer/lib/utils";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const VideosTable = (props: {
|
||||
@@ -46,7 +45,7 @@ export const VideosTable = (props: {
|
||||
{t("models.video.recordingsDuration")}
|
||||
</TableHead>
|
||||
<TableHead className="capitalize">
|
||||
{t("models.video.createdAt")}
|
||||
{t("models.video.updatedAt")}
|
||||
</TableHead>
|
||||
<TableHead className="capitalize">
|
||||
{t("models.video.isTranscribed")}
|
||||
@@ -92,9 +91,7 @@ export const VideosTable = (props: {
|
||||
<TableCell>
|
||||
{secondsToTimestamp(video.recordingsDuration / 1000)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{dayjs(video.createdAt).format("YYYY-MM-DD HH:mm")}
|
||||
</TableCell>
|
||||
<TableCell>{formatDateTime(video.updatedAt)}</TableCell>
|
||||
<TableCell>
|
||||
{video.transcribed ? (
|
||||
<CheckCircleIcon className="text-green-500 w-4 h-4" />
|
||||
|
||||
Reference in New Issue
Block a user