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:
an-lee
2024-08-15 15:12:52 +08:00
committed by GitHub
parent b56d6a34b4
commit 7da9fb8095
15 changed files with 261 additions and 47 deletions

View File

@@ -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">

View File

@@ -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")}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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")}

View File

@@ -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>