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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user