Improve audio video page (#975)
* remember tab value * fix locale * tips for no source file resources * add alert for no source file * improve code * clean up resources
This commit is contained in:
@@ -667,7 +667,7 @@
|
||||
"scoreDesc": "Score desc",
|
||||
"scoreAsc": "Score asc",
|
||||
"recordingsDurationDesc": "Recordings duration",
|
||||
"recordingsCountDesc": "Recordings duration",
|
||||
"recordingsCountDesc": "Recordings count",
|
||||
"all": "All",
|
||||
"allLanguages": "All languages",
|
||||
"search": "Search",
|
||||
@@ -719,5 +719,9 @@
|
||||
"textInput": "Text input",
|
||||
"increasePlaybackRate": "Increase playback rate",
|
||||
"descreasePlaybackRate": "Descrease playback rate",
|
||||
"usage": "Usage"
|
||||
"usage": "Usage",
|
||||
"cannotFindSourceFile": "Cannot find source file",
|
||||
"cleanUp": "Clean up",
|
||||
"cleanUpConfirmation": "Are you sure to remove resources without source file?",
|
||||
"cleanedUpSuccessfully": "Cleaned up successfully"
|
||||
}
|
||||
|
||||
@@ -719,5 +719,9 @@
|
||||
"textInput": "文字输入",
|
||||
"increasePlaybackRate": "加快播放速度",
|
||||
"decreasePlaybackRate": "减慢播放速度",
|
||||
"usage": "使用情况"
|
||||
"usage": "使用情况",
|
||||
"cannotFindSourceFile": "无法找到源文件,可能已经被删除",
|
||||
"cleanUp": "清理",
|
||||
"cleanUpConfirmation": "您确定要移除所有找不到源文件的资源吗?",
|
||||
"cleanedUpSuccessfully": "清理成功"
|
||||
}
|
||||
|
||||
@@ -168,6 +168,16 @@ class AudiosHandler {
|
||||
return pathToEnjoyUrl(output);
|
||||
}
|
||||
|
||||
private async cleanUp() {
|
||||
const audios = await Audio.findAll();
|
||||
|
||||
for (const audio of audios) {
|
||||
if (!audio.src) {
|
||||
audio.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
register() {
|
||||
ipcMain.handle("audios-find-all", this.findAll);
|
||||
ipcMain.handle("audios-find-one", this.findOne);
|
||||
@@ -176,6 +186,7 @@ class AudiosHandler {
|
||||
ipcMain.handle("audios-destroy", this.destroy);
|
||||
ipcMain.handle("audios-upload", this.upload);
|
||||
ipcMain.handle("audios-crop", this.crop);
|
||||
ipcMain.handle("audios-clean-up", this.cleanUp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,16 @@ class VideosHandler {
|
||||
return pathToEnjoyUrl(output);
|
||||
}
|
||||
|
||||
private async cleanUp() {
|
||||
const videos = await Video.findAll();
|
||||
|
||||
for (const video of videos) {
|
||||
if (!video.src) {
|
||||
video.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
register() {
|
||||
ipcMain.handle("videos-find-all", this.findAll);
|
||||
ipcMain.handle("videos-find-one", this.findOne);
|
||||
@@ -166,6 +176,7 @@ class VideosHandler {
|
||||
ipcMain.handle("videos-destroy", this.destroy);
|
||||
ipcMain.handle("videos-upload", this.upload);
|
||||
ipcMain.handle("videos-crop", this.crop);
|
||||
ipcMain.handle("videos-clean-up", this.cleanUp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,11 +121,15 @@ export class Audio extends Model<Audio> {
|
||||
|
||||
@Column(DataType.VIRTUAL)
|
||||
get src(): string {
|
||||
return `enjoy://${path.posix.join(
|
||||
"library",
|
||||
"audios",
|
||||
this.getDataValue("md5") + this.extname
|
||||
)}`;
|
||||
if (this.filePath) {
|
||||
return `enjoy://${path.posix.join(
|
||||
"library",
|
||||
"audios",
|
||||
this.getDataValue("md5") + this.extname
|
||||
)}`;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Column(DataType.VIRTUAL)
|
||||
@@ -152,11 +156,17 @@ export class Audio extends Model<Audio> {
|
||||
}
|
||||
|
||||
get filePath(): string {
|
||||
return path.join(
|
||||
const file = path.join(
|
||||
settings.userDataPath(),
|
||||
"audios",
|
||||
this.getDataValue("md5") + this.extname
|
||||
);
|
||||
|
||||
if (fs.existsSync(file)) {
|
||||
return file;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async upload(force: boolean = false) {
|
||||
@@ -248,7 +258,9 @@ export class Audio extends Model<Audio> {
|
||||
|
||||
@AfterDestroy
|
||||
static cleanupFile(audio: Audio) {
|
||||
fs.remove(audio.filePath);
|
||||
if (audio.filePath) {
|
||||
fs.remove(audio.filePath);
|
||||
}
|
||||
Recording.destroy({
|
||||
where: {
|
||||
targetId: audio.id,
|
||||
|
||||
@@ -121,11 +121,15 @@ export class Video extends Model<Video> {
|
||||
|
||||
@Column(DataType.VIRTUAL)
|
||||
get src(): string {
|
||||
return `enjoy://${path.posix.join(
|
||||
"library",
|
||||
"videos",
|
||||
this.getDataValue("md5") + this.extname
|
||||
)}`;
|
||||
if (this.filePath) {
|
||||
return `enjoy://${path.posix.join(
|
||||
"library",
|
||||
"videos",
|
||||
this.getDataValue("md5") + this.extname
|
||||
)}`;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Column(DataType.VIRTUAL)
|
||||
@@ -152,11 +156,17 @@ export class Video extends Model<Video> {
|
||||
}
|
||||
|
||||
get filePath(): string {
|
||||
return path.join(
|
||||
const file = path.join(
|
||||
settings.userDataPath(),
|
||||
"videos",
|
||||
this.getDataValue("md5") + this.extname
|
||||
);
|
||||
|
||||
if (fs.existsSync(file)) {
|
||||
return file;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// generate cover and upload
|
||||
@@ -270,7 +280,9 @@ export class Video extends Model<Video> {
|
||||
|
||||
@AfterDestroy
|
||||
static cleanupFile(video: Video) {
|
||||
fs.remove(video.filePath);
|
||||
if (video.filePath) {
|
||||
fs.remove(video.filePath);
|
||||
}
|
||||
Recording.destroy({
|
||||
where: {
|
||||
targetId: video.id,
|
||||
|
||||
@@ -280,6 +280,9 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
||||
crop: (id: string, params: { startTime: number; endTime: number }) => {
|
||||
return ipcRenderer.invoke("audios-crop", id, params);
|
||||
},
|
||||
cleanUp: () => {
|
||||
return ipcRenderer.invoke("audios-clean-up");
|
||||
},
|
||||
},
|
||||
videos: {
|
||||
findAll: (params: {
|
||||
@@ -306,6 +309,9 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
||||
crop: (id: string, params: { startTime: number; endTime: number }) => {
|
||||
return ipcRenderer.invoke("videos-crop", id, params);
|
||||
},
|
||||
cleanUp: () => {
|
||||
return ipcRenderer.invoke("videos-clean-up");
|
||||
},
|
||||
},
|
||||
recordings: {
|
||||
findAll: (params?: {
|
||||
@@ -655,7 +661,11 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
||||
},
|
||||
},
|
||||
chatMessages: {
|
||||
findAll: (params: { chatSessionId: string; offset?: number; limit?: number }) => {
|
||||
findAll: (params: {
|
||||
chatSessionId: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}) => {
|
||||
return ipcRenderer.invoke("chat-messages-find-all", params);
|
||||
},
|
||||
findOne: (params: any) => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { cn } from "@renderer/lib/utils";
|
||||
import { AudioLinesIcon } from "lucide-react";
|
||||
import { AudioLinesIcon, CircleAlertIcon } from "lucide-react";
|
||||
import { Badge } from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
|
||||
export const AudioCard = (props: {
|
||||
audio: Partial<AudioType>;
|
||||
@@ -32,6 +33,15 @@ export const AudioCard = (props: {
|
||||
{audio.language && (
|
||||
<Badge className="absolute left-2 top-2">{audio.language}</Badge>
|
||||
)}
|
||||
{!audio.src && (
|
||||
<div
|
||||
data-tooltip-content={t("cannotFindSourceFile")}
|
||||
data-tooltip-id="global-tooltip"
|
||||
className="absolute right-2 top-2"
|
||||
>
|
||||
<CircleAlertIcon className="text-destructive w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<div className="text-sm font-semibold mt-2 max-w-full line-clamp-2 h-10">
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
DialogDescription,
|
||||
AlertDialogTrigger,
|
||||
} from "@renderer/components/ui";
|
||||
import {
|
||||
DbProviderContext,
|
||||
@@ -58,6 +60,8 @@ export const AudiosComponent = () => {
|
||||
const [deleting, setDeleting] = useState<Partial<AudioType> | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [tab, setTab] = useState("grid");
|
||||
|
||||
useEffect(() => {
|
||||
addDblistener(onAudiosUpdate);
|
||||
|
||||
@@ -66,6 +70,18 @@ export const AudiosComponent = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
EnjoyApp.cacheObjects.get("audios-page-tab").then((value) => {
|
||||
if (value) {
|
||||
setTab(value);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
EnjoyApp.cacheObjects.set("audios-page-tab", tab);
|
||||
}, [tab]);
|
||||
|
||||
const fetchAudios = async (options?: { offset: number }) => {
|
||||
if (loading) return;
|
||||
const { offset = audios.length } = options || {};
|
||||
@@ -154,7 +170,7 @@ export const AudiosComponent = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="">
|
||||
<Tabs defaultValue="grid">
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<div className="flex flex-wrap items-center gap-4 mb-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="grid">
|
||||
@@ -213,6 +229,29 @@ export const AudiosComponent = () => {
|
||||
/>
|
||||
|
||||
<AddMediaButton type="Audio" />
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="secondary">{t("cleanUp")}</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>{t("cleanUp")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("cleanUpConfirmation")}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() =>
|
||||
EnjoyApp.audios
|
||||
.cleanUp()
|
||||
.then(() => toast.success(t("cleanedUpSuccessfully")))
|
||||
}
|
||||
>
|
||||
{t("confirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
|
||||
{audios.length === 0 ? (
|
||||
@@ -259,6 +298,9 @@ export const AudiosComponent = () => {
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("editResource")}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
edit audio
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<AudioEditForm
|
||||
@@ -270,7 +312,7 @@ export const AudiosComponent = () => {
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog
|
||||
open={!!deleting}
|
||||
open={Boolean(deleting)}
|
||||
onOpenChange={(value) => {
|
||||
if (value) return;
|
||||
setDeleting(null);
|
||||
@@ -280,21 +322,23 @@ export const AudiosComponent = () => {
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("deleteResource")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<p className="break-all">
|
||||
<span className="break-all">
|
||||
{t("deleteResourceConfirmation", {
|
||||
name: deleting?.name || "",
|
||||
})}
|
||||
</p>
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive"
|
||||
onClick={async () => {
|
||||
onClick={() => {
|
||||
if (!deleting) return;
|
||||
await EnjoyApp.audios.destroy(deleting.id);
|
||||
setDeleting(null);
|
||||
EnjoyApp.audios
|
||||
.destroy(deleting.id)
|
||||
.catch((err) => toast.error(err.message))
|
||||
.finally(() => setDeleting(null));
|
||||
}}
|
||||
>
|
||||
{t("delete")}
|
||||
|
||||
@@ -12,9 +12,13 @@ import {
|
||||
TooltipTrigger,
|
||||
Button,
|
||||
PingPoint,
|
||||
Badge,
|
||||
} from "@renderer/components/ui";
|
||||
import { EditIcon, TrashIcon, CheckCircleIcon } from "lucide-react";
|
||||
import {
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
CheckCircleIcon,
|
||||
CircleAlertIcon,
|
||||
} from "lucide-react";
|
||||
import dayjs from "@renderer/lib/dayjs";
|
||||
import { secondsToTimestamp } from "@renderer/lib/utils";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -58,8 +62,17 @@ export const AudiosTable = (props: {
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Link to={`/audios/${audio.id}`}>
|
||||
<div className="cursor-pointer truncate max-w-[12rem]">
|
||||
{audio.name}
|
||||
<div className="flex items-center space-x-2">
|
||||
{!audio.src && (
|
||||
<CircleAlertIcon
|
||||
data-tooltip-content={t("cannotFindSourceFile")}
|
||||
data-tooltip-id="global-tooltip"
|
||||
className="text-destructive w-4 h-4"
|
||||
/>
|
||||
)}
|
||||
<div className="cursor-pointer truncate max-w-[12rem]">
|
||||
{audio.name}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -14,7 +14,11 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@renderer/components/ui";
|
||||
import { CheckCircleIcon, LoaderIcon, XCircleIcon } from "lucide-react";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
CircleAlertIcon,
|
||||
LoaderIcon,
|
||||
} from "lucide-react";
|
||||
import { t } from "i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { TranscriptionCreateForm, TranscriptionsList } from "../transcriptions";
|
||||
@@ -53,7 +57,9 @@ export const MediaLoadingModal = () => {
|
||||
<Tabs defaultValue="transcribe">
|
||||
<TabsList className="w-full grid grid-cols-2 mb-4">
|
||||
<TabsTrigger value="transcribe">{t("transcribe")}</TabsTrigger>
|
||||
<TabsTrigger value="download">{t("downloadTranscript")}</TabsTrigger>
|
||||
<TabsTrigger value="download">
|
||||
{t("downloadTranscript")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="transcribe">
|
||||
<TranscriptionCreateForm
|
||||
@@ -85,7 +91,7 @@ export const MediaLoadingModal = () => {
|
||||
{decodeError ? (
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<div className="w-4 h-4">
|
||||
<XCircleIcon className="w-4 h-4 text-destructive" />
|
||||
<CircleAlertIcon className="text-destructive w-4 h-4" />
|
||||
</div>
|
||||
<div className="select-text">
|
||||
<div className="mb-2">{decodeError}</div>
|
||||
@@ -97,8 +103,17 @@ export const MediaLoadingModal = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-4 flex items-center space-x-4">
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
<span>{t("decodingWaveform")}</span>
|
||||
{media?.src ? (
|
||||
<>
|
||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
||||
<span>{t("decodingWaveform")}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CircleAlertIcon className="text-destructive w-4 h-4" />
|
||||
<span>{t("cannotFindSourceFile")}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<AlertDialogFooter>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { cn } from "@renderer/lib/utils";
|
||||
import { VideoIcon } from "lucide-react";
|
||||
import { CircleAlertIcon, VideoIcon } from "lucide-react";
|
||||
import { Badge } from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
|
||||
export const VideoCard = (props: {
|
||||
video: Partial<VideoType>;
|
||||
@@ -12,7 +14,7 @@ export const VideoCard = (props: {
|
||||
<div className={cn("w-full", className)}>
|
||||
<Link to={`/videos/${video.id}`}>
|
||||
<div
|
||||
className="aspect-[4/3] border rounded-lg overflow-hidden"
|
||||
className="aspect-[4/3] border rounded-lg overflow-hidden relative"
|
||||
style={{
|
||||
borderBottomColor: `#${video.md5.substr(0, 6)}`,
|
||||
borderBottomWidth: 3,
|
||||
@@ -26,6 +28,18 @@ export const VideoCard = (props: {
|
||||
className="absolute top-0 left-0 hover:scale-105 object-cover w-full h-full bg-cover bg-center"
|
||||
/>
|
||||
</div>
|
||||
{video.language && (
|
||||
<Badge className="absolute left-2 top-2">{video.language}</Badge>
|
||||
)}
|
||||
{!video.src && (
|
||||
<div
|
||||
data-tooltip-content={t("cannotFindSourceFile")}
|
||||
data-tooltip-id="global-tooltip"
|
||||
className="absolute right-2 top-2"
|
||||
>
|
||||
<CircleAlertIcon className="text-destructive w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
<div className="text-sm font-semibold mt-2 max-w-full truncate">
|
||||
|
||||
@@ -32,6 +32,8 @@ import {
|
||||
SelectItem,
|
||||
toast,
|
||||
Input,
|
||||
DialogDescription,
|
||||
AlertDialogTrigger,
|
||||
} from "@renderer/components/ui";
|
||||
import {
|
||||
DbProviderContext,
|
||||
@@ -56,9 +58,10 @@ export const VideosComponent = () => {
|
||||
|
||||
const [editing, setEditing] = useState<Partial<VideoType> | null>(null);
|
||||
const [deleting, setDeleting] = useState<Partial<VideoType> | null>(null);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [tab, setTab] = useState("grid");
|
||||
|
||||
useEffect(() => {
|
||||
addDblistener(onVideosUpdate);
|
||||
|
||||
@@ -67,6 +70,18 @@ export const VideosComponent = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
EnjoyApp.cacheObjects.get("videos-page-tab").then((value) => {
|
||||
if (value) {
|
||||
setTab(value);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
EnjoyApp.cacheObjects.set("videos-page-tab", tab);
|
||||
}, [tab]);
|
||||
|
||||
const fetchVideos = async (options?: { offset: number }) => {
|
||||
if (loading) return;
|
||||
const { offset = videos.length } = options || {};
|
||||
@@ -154,7 +169,7 @@ export const VideosComponent = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="">
|
||||
<Tabs defaultValue="grid">
|
||||
<Tabs value={tab} onValueChange={setTab}>
|
||||
<div className="flex flex-wrap items-center gap-4 mb-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="grid">
|
||||
@@ -211,6 +226,29 @@ export const VideosComponent = () => {
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
/>
|
||||
<AddMediaButton type="Video" />
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="secondary">{t("cleanUp")}</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogTitle>{t("cleanUp")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("cleanUpConfirmation")}
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() =>
|
||||
EnjoyApp.videos
|
||||
.cleanUp()
|
||||
.then(() => toast.success(t("cleanedUpSuccessfully")))
|
||||
}
|
||||
>
|
||||
{t("confirm")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
{videos.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-48 border border-dashed rounded-lg">
|
||||
@@ -255,6 +293,9 @@ export const VideosComponent = () => {
|
||||
<DialogContent aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("editResource")}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
edit video
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<VideoEditForm
|
||||
@@ -276,21 +317,23 @@ export const VideosComponent = () => {
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("deleteResource")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<p className="break-all">
|
||||
<span className="break-all">
|
||||
{t("deleteResourceConfirmation", {
|
||||
name: deleting?.name || "",
|
||||
})}
|
||||
</p>
|
||||
</span>
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className="bg-destructive"
|
||||
onClick={async () => {
|
||||
onClick={() => {
|
||||
if (!deleting) return;
|
||||
await EnjoyApp.videos.destroy(deleting.id);
|
||||
setDeleting(null);
|
||||
EnjoyApp.videos
|
||||
.destroy(deleting.id)
|
||||
.catch((err) => toast.error(err.message))
|
||||
.finally(() => setDeleting(null));
|
||||
}}
|
||||
>
|
||||
{t("delete")}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
CheckCircleIcon,
|
||||
AudioWaveformIcon,
|
||||
CircleAlertIcon,
|
||||
} from "lucide-react";
|
||||
import dayjs from "@renderer/lib/dayjs";
|
||||
import { secondsToTimestamp } from "@renderer/lib/utils";
|
||||
@@ -62,8 +62,17 @@ export const VideosTable = (props: {
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Link to={`/videos/${video.id}`}>
|
||||
<div className="cursor-pointer truncate max-w-[12rem]">
|
||||
{video.name}
|
||||
<div className="flex items-center space-x-2">
|
||||
{!video.src && (
|
||||
<CircleAlertIcon
|
||||
data-tooltip-content={t("cannotFindSourceFile")}
|
||||
data-tooltip-id="global-tooltip"
|
||||
className="text-destructive w-4 h-4"
|
||||
/>
|
||||
)}
|
||||
<div className="cursor-pointer truncate max-w-[12rem]">
|
||||
{video.name}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
|
||||
2
enjoy/src/types/enjoy-app.d.ts
vendored
2
enjoy/src/types/enjoy-app.d.ts
vendored
@@ -159,6 +159,7 @@ type EnjoyAppType = {
|
||||
id: string,
|
||||
params: { startTime: number; endTime: number }
|
||||
) => Promise<string>;
|
||||
cleanUp: () => Promise<void>;
|
||||
};
|
||||
videos: {
|
||||
findAll: (params: any) => Promise<VideoType[]>;
|
||||
@@ -171,6 +172,7 @@ type EnjoyAppType = {
|
||||
id: string,
|
||||
params: { startTime: number; endTime: number }
|
||||
) => Promise<string>;
|
||||
cleanUp: () => Promise<void>;
|
||||
};
|
||||
recordings: {
|
||||
findAll: (where: any) => Promise<RecordingType[]>;
|
||||
|
||||
Reference in New Issue
Block a user