share audio/video & display post
This commit is contained in:
@@ -94,7 +94,11 @@ export class Client {
|
||||
return this.api.get(`/api/posts/${id}`);
|
||||
}
|
||||
|
||||
createPost(params: { content: string }): Promise<PostType> {
|
||||
createPost(params: {
|
||||
content?: string;
|
||||
targetType?: string;
|
||||
targetId?: string;
|
||||
}): Promise<PostType> {
|
||||
return this.api.post("/api/posts", decamelizeKeys(params));
|
||||
}
|
||||
|
||||
|
||||
@@ -329,5 +329,11 @@
|
||||
"allRankings": "All time rankings",
|
||||
"noOneHasRecordedYet": "No one has recorded yet",
|
||||
"activities": "Activities",
|
||||
"noOneSharedYet": "No one shared yet"
|
||||
"noOneSharedYet": "No one shared yet",
|
||||
"sharedSuccessfully": "Shared successfully",
|
||||
"shareFailed": "Share failed",
|
||||
"shareAudio": "Share audio",
|
||||
"areYouSureToShareThisAudioToCommunity": "Are you sure to share this audio to community?",
|
||||
"shareVideo": "Share video",
|
||||
"areYouSureToShareThisVideoToCommunity": "Are you sure to share this video to community?"
|
||||
}
|
||||
|
||||
@@ -329,5 +329,11 @@
|
||||
"allRankings": "总排行榜",
|
||||
"noOneHasRecordedYet": "还没有人练习",
|
||||
"activities": "动态",
|
||||
"noOneSharedYet": "还没有人分享"
|
||||
"noOneSharedYet": "还没有人分享",
|
||||
"sharedSuccessfully": "分享成功",
|
||||
"sharedFailed": "分享失败",
|
||||
"shareAudio": "分享音频",
|
||||
"areYouSureToShareThisAudioToCommunity": "您确定要分享此音频到社区吗?",
|
||||
"shareVideo": "分享视频",
|
||||
"areYouSureToShareThisVideoToCommunity": "您确定要分享此视频到社区吗?"
|
||||
}
|
||||
|
||||
@@ -90,27 +90,29 @@ class AudiosHandler {
|
||||
|
||||
private async create(
|
||||
event: IpcMainEvent,
|
||||
source: string,
|
||||
uri: string,
|
||||
params: {
|
||||
name?: string;
|
||||
coverUrl?: string;
|
||||
} = {}
|
||||
) {
|
||||
let file = source;
|
||||
if (source.startsWith("http")) {
|
||||
let file = uri;
|
||||
let source;
|
||||
if (uri.startsWith("http")) {
|
||||
try {
|
||||
if (youtubedr.validateYtURL(source)) {
|
||||
file = await youtubedr.autoDownload(source);
|
||||
if (youtubedr.validateYtURL(uri)) {
|
||||
file = await youtubedr.autoDownload(uri);
|
||||
} else {
|
||||
file = await downloader.download(source, {
|
||||
file = await downloader.download(uri, {
|
||||
webContents: event.sender,
|
||||
});
|
||||
}
|
||||
if (!file) throw new Error("Failed to download file");
|
||||
source = uri;
|
||||
} catch (err) {
|
||||
return event.sender.send("on-notification", {
|
||||
type: "error",
|
||||
message: t("models.audio.failedToDownloadFile", { file: source }),
|
||||
message: t("models.audio.failedToDownloadFile", { file: uri }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,27 +90,29 @@ class VideosHandler {
|
||||
|
||||
private async create(
|
||||
event: IpcMainEvent,
|
||||
source: string,
|
||||
uri: string,
|
||||
params: {
|
||||
name?: string;
|
||||
coverUrl?: string;
|
||||
} = {}
|
||||
) {
|
||||
let file = source;
|
||||
if (source.startsWith("http")) {
|
||||
let file = uri;
|
||||
let source;
|
||||
if (uri.startsWith("http")) {
|
||||
try {
|
||||
if (youtubedr.validateYtURL(source)) {
|
||||
file = await youtubedr.autoDownload(source);
|
||||
if (youtubedr.validateYtURL(uri)) {
|
||||
file = await youtubedr.autoDownload(uri);
|
||||
} else {
|
||||
file = await downloader.download(source, {
|
||||
file = await downloader.download(uri, {
|
||||
webContents: event.sender,
|
||||
});
|
||||
}
|
||||
if (!file) throw new Error("Failed to download file");
|
||||
source = uri;
|
||||
} catch (err) {
|
||||
return event.sender.send("on-notification", {
|
||||
type: "error",
|
||||
message: t("models.video.failedToDownloadFile", { file: source }),
|
||||
message: t("models.video.failedToDownloadFile", { file: uri }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ class Youtubedr {
|
||||
this.getYtVideoId(url);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logger.warn(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -175,8 +175,8 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
||||
findOne: (params: object) => {
|
||||
return ipcRenderer.invoke("audios-find-one", params);
|
||||
},
|
||||
create: (source: string, params?: object) => {
|
||||
return ipcRenderer.invoke("audios-create", source, params);
|
||||
create: (uri: string, params?: object) => {
|
||||
return ipcRenderer.invoke("audios-create", uri, params);
|
||||
},
|
||||
update: (id: string, params: object) => {
|
||||
return ipcRenderer.invoke("audios-update", id, params);
|
||||
@@ -201,8 +201,8 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
||||
findOne: (params: object) => {
|
||||
return ipcRenderer.invoke("videos-find-one", params);
|
||||
},
|
||||
create: (source: string, params?: object) => {
|
||||
return ipcRenderer.invoke("videos-create", source, params);
|
||||
create: (uri: string, params?: object) => {
|
||||
return ipcRenderer.invoke("videos-create", uri, params);
|
||||
},
|
||||
update: (id: string, params: object) => {
|
||||
return ipcRenderer.invoke("videos-update", id, params);
|
||||
|
||||
@@ -11,16 +11,30 @@ import {
|
||||
MediaTranscription,
|
||||
} from "@renderer/components";
|
||||
import { LoaderIcon } from "lucide-react";
|
||||
import { ScrollArea } from "@renderer/components/ui";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogHeader,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
AlertDialogContent,
|
||||
AlertDialogFooter,
|
||||
AlertDialogCancel,
|
||||
Button,
|
||||
ScrollArea,
|
||||
useToast,
|
||||
} from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
|
||||
export const AudioDetail = (props: { id?: string; md5?: string }) => {
|
||||
const { id, md5 } = props;
|
||||
const { toast } = useToast();
|
||||
const { addDblistener, removeDbListener } = useContext(DbProviderContext);
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const { EnjoyApp, webApi } = useContext(AppSettingsProviderContext);
|
||||
|
||||
const [audio, setAudio] = useState<AudioType | null>(null);
|
||||
const [transcription, setTranscription] = useState<TranscriptionType>(null);
|
||||
const [initialized, setInitialized] = useState<boolean>(false);
|
||||
const [sharing, setSharing] = useState<boolean>(false);
|
||||
|
||||
// Player controls
|
||||
const [currentTime, setCurrentTime] = useState<number>(0);
|
||||
@@ -43,6 +57,38 @@ export const AudioDetail = (props: { id?: string; md5?: string }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
if (!audio.source && !audio.isUploaded) {
|
||||
try {
|
||||
await EnjoyApp.audios.upload(audio.id);
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: t("shareFailed"),
|
||||
description: err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
webApi
|
||||
.createPost({
|
||||
targetType: "Audio",
|
||||
targetId: audio.id,
|
||||
})
|
||||
.then(() => {
|
||||
toast({
|
||||
title: t("shared"),
|
||||
description: t("sharedSuccessfully"),
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
toast({
|
||||
title: t("shareFailed"),
|
||||
description: err.message,
|
||||
});
|
||||
});
|
||||
setSharing(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const where = id ? { id } : { md5 };
|
||||
EnjoyApp.audios.findOne(where).then((audio) => {
|
||||
@@ -110,6 +156,7 @@ export const AudioDetail = (props: { id?: string; md5?: string }) => {
|
||||
setPlaybackRate={setPlaybackRate}
|
||||
displayInlineCaption={displayInlineCaption}
|
||||
setDisplayInlineCaption={setDisplayInlineCaption}
|
||||
onShare={() => setSharing(true)}
|
||||
/>
|
||||
|
||||
<ScrollArea className={`flex-1 relative bg-muted`}>
|
||||
@@ -146,6 +193,23 @@ export const AudioDetail = (props: { id?: string; md5?: string }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={sharing} onOpenChange={(value) => setSharing(value)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("shareAudio")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("areYouSureToShareThisAudioToCommunity")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<Button variant="default" onClick={handleShare}>
|
||||
{t("share")}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{!initialized && (
|
||||
<div className="top-0 w-full h-full absolute z-30 bg-white/10 flex items-center justify-center">
|
||||
<LoaderIcon className="text-muted-foreground animate-spin w-8 h-8" />
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
MinimizeIcon,
|
||||
GalleryHorizontalIcon,
|
||||
SpellCheckIcon,
|
||||
Share2Icon,
|
||||
} from "lucide-react";
|
||||
import { t } from "i18next";
|
||||
import { type WaveSurferOptions } from "wavesurfer.js";
|
||||
@@ -24,7 +25,6 @@ import { Tooltip } from "react-tooltip";
|
||||
const PLAYBACK_RATE_OPTIONS = [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75];
|
||||
const MIN_ZOOM_RATIO = 0.25;
|
||||
const MAX_ZOOM_RATIO = 5.0;
|
||||
const ZOOM_RATIO_STEP = 0.25;
|
||||
|
||||
export const MediaPlayerControls = (props: {
|
||||
isPlaying: boolean;
|
||||
@@ -47,6 +47,7 @@ export const MediaPlayerControls = (props: {
|
||||
setWavesurferOptions?: (options: Partial<WaveSurferOptions>) => void;
|
||||
displayInlineCaption?: boolean;
|
||||
setDisplayInlineCaption?: (display: boolean) => void;
|
||||
onShare?: () => void;
|
||||
}) => {
|
||||
const {
|
||||
isPlaying,
|
||||
@@ -67,6 +68,7 @@ export const MediaPlayerControls = (props: {
|
||||
setWavesurferOptions,
|
||||
displayInlineCaption,
|
||||
setDisplayInlineCaption,
|
||||
onShare,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -244,20 +246,32 @@ export const MediaPlayerControls = (props: {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{transcriptionDirty && (
|
||||
<div className="absolute right-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className=""
|
||||
onClick={resetTranscription}
|
||||
>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
<Button onClick={saveTranscription}>{t("save")}</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
data-tooltip-id="media-player-controls-tooltip"
|
||||
data-tooltip-content={t("share")}
|
||||
className="relative aspect-square p-0 h-10"
|
||||
onClick={onShare}
|
||||
>
|
||||
<Share2Icon className="w-6 h-6" />
|
||||
</Button>
|
||||
|
||||
<div className="absolute right-4">
|
||||
<div className="flex items-center space-x-4">
|
||||
{transcriptionDirty && (
|
||||
<>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className=""
|
||||
onClick={resetTranscription}
|
||||
>
|
||||
{t("reset")}
|
||||
</Button>
|
||||
<Button onClick={saveTranscription}>{t("save")}</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Tooltip id="media-player-controls-tooltip" />
|
||||
</div>
|
||||
|
||||
@@ -60,6 +60,7 @@ export const MediaPlayer = (props: {
|
||||
setPlaybackRate: (value: number) => void;
|
||||
displayInlineCaption?: boolean;
|
||||
setDisplayInlineCaption?: (value: boolean) => void;
|
||||
onShare?: () => void;
|
||||
}) => {
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const {
|
||||
@@ -88,6 +89,7 @@ export const MediaPlayer = (props: {
|
||||
setPlaybackRate,
|
||||
displayInlineCaption,
|
||||
setDisplayInlineCaption,
|
||||
onShare,
|
||||
} = props;
|
||||
if (!mediaUrl) return;
|
||||
|
||||
@@ -536,6 +538,7 @@ export const MediaPlayer = (props: {
|
||||
setWavesurferOptions={(options) => wavesurfer?.setOptions(options)}
|
||||
displayInlineCaption={displayInlineCaption}
|
||||
setDisplayInlineCaption={setDisplayInlineCaption}
|
||||
onShare={onShare}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './posts';
|
||||
export * from "./posts";
|
||||
export * from "./post-audio-player";
|
||||
|
||||
121
enjoy/src/renderer/components/posts/post-audio-player.tsx
Normal file
121
enjoy/src/renderer/components/posts/post-audio-player.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { PitchContour } from "@renderer/components";
|
||||
import WaveSurfer from "wavesurfer.js";
|
||||
import { Button, Skeleton } from "@renderer/components/ui";
|
||||
import { PlayIcon, PauseIcon } from "lucide-react";
|
||||
import { useIntersectionObserver } from "@uidotdev/usehooks";
|
||||
import { secondsToTimestamp } from "@renderer/lib/utils";
|
||||
|
||||
export const PostAudioPlayer = (props: { src: string; height?: number }) => {
|
||||
const { src, height = 80 } = props;
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [wavesurfer, setWavesurfer] = useState(null);
|
||||
const containerRef = useRef();
|
||||
const [ref, entry] = useIntersectionObserver({
|
||||
threshold: 1,
|
||||
});
|
||||
const [duration, setDuration] = useState<number>(0);
|
||||
|
||||
const onPlayClick = useCallback(() => {
|
||||
wavesurfer.isPlaying() ? wavesurfer.pause() : wavesurfer.play();
|
||||
}, [wavesurfer]);
|
||||
|
||||
useEffect(() => {
|
||||
// use the intersection observer to only create the wavesurfer instance
|
||||
// when the player is visible
|
||||
if (!entry?.isIntersecting) return;
|
||||
if (!src) return;
|
||||
if (wavesurfer) return;
|
||||
|
||||
const ws = WaveSurfer.create({
|
||||
container: containerRef.current,
|
||||
url: src,
|
||||
height,
|
||||
barWidth: 1,
|
||||
cursorWidth: 0,
|
||||
autoCenter: true,
|
||||
autoScroll: true,
|
||||
dragToSeek: true,
|
||||
hideScrollbar: true,
|
||||
minPxPerSec: 100,
|
||||
waveColor: "#ddd",
|
||||
progressColor: "rgba(0, 0, 0, 0.25)",
|
||||
normalize: true,
|
||||
});
|
||||
|
||||
setWavesurfer(ws);
|
||||
}, [src, entry]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wavesurfer) return;
|
||||
|
||||
const subscriptions = [
|
||||
wavesurfer.on("play", () => {
|
||||
setIsPlaying(true);
|
||||
}),
|
||||
wavesurfer.on("pause", () => {
|
||||
setIsPlaying(false);
|
||||
}),
|
||||
wavesurfer.on("decode", () => {
|
||||
setDuration(wavesurfer.getDuration());
|
||||
const peaks = wavesurfer.getDecodedData().getChannelData(0);
|
||||
const sampleRate = wavesurfer.options.sampleRate;
|
||||
wavesurfer.renderer.getWrapper().appendChild(
|
||||
PitchContour({
|
||||
peaks,
|
||||
sampleRate,
|
||||
height,
|
||||
})
|
||||
);
|
||||
setInitialized(true);
|
||||
}),
|
||||
];
|
||||
|
||||
return () => {
|
||||
subscriptions.forEach((unsub) => unsub());
|
||||
wavesurfer?.destroy();
|
||||
};
|
||||
}, [wavesurfer]);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="flex justify-end">
|
||||
<span className="text-xs text-muted-foreground mb-1">
|
||||
{secondsToTimestamp(duration)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={ref}
|
||||
className="bg-white rounded-lg grid grid-cols-9 items-center relative h-[80px]"
|
||||
>
|
||||
{!initialized && (
|
||||
<div className="col-span-9 flex flex-col justify-around h-[80px]">
|
||||
<Skeleton className="h-3 w-full rounded-full" />
|
||||
<Skeleton className="h-3 w-full rounded-full" />
|
||||
<Skeleton className="h-3 w-full rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`flex justify-center ${initialized ? "" : "hidden"}`}>
|
||||
<Button
|
||||
onClick={onPlayClick}
|
||||
className="aspect-square rounded-full p-2 w-12 h-12 bg-blue-600 hover:bg-blue-500"
|
||||
>
|
||||
{isPlaying ? (
|
||||
<PauseIcon className="w-6 h-6 text-white" />
|
||||
) : (
|
||||
<PlayIcon className="w-6 h-6 text-white" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`col-span-8 ${initialized ? "" : "hidden"}`}
|
||||
ref={containerRef}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { AppSettingsProviderContext } from "@renderer/context";
|
||||
import { PostAudioPlayer } from "@renderer/components";
|
||||
import { Avatar, AvatarImage, AvatarFallback } from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
import { MediaPlayer, MediaProvider } from "@vidstack/react";
|
||||
import {
|
||||
DefaultVideoLayout,
|
||||
defaultLayoutIcons,
|
||||
} from "@vidstack/react/player/layouts/default";
|
||||
|
||||
export const Posts = () => {
|
||||
const { webApi } = useContext(AppSettingsProviderContext);
|
||||
@@ -9,6 +16,7 @@ export const Posts = () => {
|
||||
const fetchPosts = async () => {
|
||||
webApi.posts().then(
|
||||
(res) => {
|
||||
console.log(res);
|
||||
setPosts(res.posts);
|
||||
},
|
||||
(err) => {
|
||||
@@ -22,10 +30,74 @@ export const Posts = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="max-w-screen-sm mx-auto">
|
||||
{posts.length === 0 && (
|
||||
<div className="text-center text-gray-500">{t("noOneSharedYet")}</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{posts.map((post) => (
|
||||
<PostCard key={post.id} post={post} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PostCard = (props: { post: PostType }) => {
|
||||
const { post } = props;
|
||||
|
||||
return (
|
||||
<div className="rounded p-4 bg-white">
|
||||
<div className="flex items-center mb-4 justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar>
|
||||
<AvatarImage src={post.user.avatarUrl} />
|
||||
<AvatarFallback className="text-xl">
|
||||
{post.user.name[0].toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="">{post.user.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
{post.content && <div className="mb-4">{post.content}</div>}
|
||||
{post.targetType == "Medium" && <PostMedium medium={post.target} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PostMedium = (props: { medium: MediumType }) => {
|
||||
const { medium } = props;
|
||||
if (!medium.sourceUrl) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
{medium.mediumType == "Video" && (
|
||||
<MediaPlayer
|
||||
poster={medium.coverUrl}
|
||||
src={{
|
||||
type: `${medium.mediumType.toLowerCase()}/${
|
||||
medium.extname.replace(".", "") || "mp4"
|
||||
}`,
|
||||
src: medium.sourceUrl,
|
||||
}}
|
||||
>
|
||||
<MediaProvider />
|
||||
<DefaultVideoLayout icons={defaultLayoutIcons} />
|
||||
</MediaPlayer>
|
||||
)}
|
||||
|
||||
{medium.mediumType == "Audio" && (
|
||||
<PostAudioPlayer src={medium.sourceUrl} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{medium.coverUrl && medium.mediumType == "Audio" && (
|
||||
<div className="">
|
||||
<img src={medium.coverUrl} className="w-full rounded" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -58,11 +58,11 @@ const RankingsCard = (props: {
|
||||
)}
|
||||
|
||||
{rankings.map((user, index) => (
|
||||
<div key={user.id} className="flex items-center space-x-4 px-4 py-2">
|
||||
<div key={user.id} className="flex items-center space-x-4 p-2">
|
||||
<div className="font-mono text-sm">#{index + 1}</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Avatar>
|
||||
<Avatar className="w-8 h-8">
|
||||
<AvatarImage src={user.avatarUrl} />
|
||||
<AvatarFallback className="text-xl">
|
||||
{user.name[0].toUpperCase()}
|
||||
|
||||
@@ -11,16 +11,30 @@ import {
|
||||
MediaTranscription,
|
||||
} from "@renderer/components";
|
||||
import { LoaderIcon } from "lucide-react";
|
||||
import { ScrollArea } from "@renderer/components/ui";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogHeader,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
AlertDialogContent,
|
||||
AlertDialogFooter,
|
||||
AlertDialogCancel,
|
||||
Button,
|
||||
ScrollArea,
|
||||
useToast,
|
||||
} from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
|
||||
export const VideoDetail = (props: { id?: string; md5?: string }) => {
|
||||
const { id, md5 } = props;
|
||||
const { toast } = useToast();
|
||||
const { addDblistener, removeDbListener } = useContext(DbProviderContext);
|
||||
const { EnjoyApp } = useContext(AppSettingsProviderContext);
|
||||
const { EnjoyApp, webApi } = useContext(AppSettingsProviderContext);
|
||||
|
||||
const [video, setVideo] = useState<VideoType | null>(null);
|
||||
const [transcription, setTranscription] = useState<TranscriptionType>(null);
|
||||
const [initialized, setInitialized] = useState<boolean>(false);
|
||||
const [sharing, setSharing] = useState<boolean>(false);
|
||||
|
||||
// Player controls
|
||||
const [currentTime, setCurrentTime] = useState<number>(0);
|
||||
@@ -45,6 +59,38 @@ export const VideoDetail = (props: { id?: string; md5?: string }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare = async () => {
|
||||
if (!video.source && !video.isUploaded) {
|
||||
try {
|
||||
await EnjoyApp.videos.upload(video.id);
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: t("shareFailed"),
|
||||
description: err.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
webApi
|
||||
.createPost({
|
||||
targetType: "Video",
|
||||
targetId: video.id,
|
||||
})
|
||||
.then(() => {
|
||||
toast({
|
||||
description: t("sharedSuccessfully"),
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
toast({
|
||||
title: t("shareFailed"),
|
||||
description: err.message,
|
||||
});
|
||||
});
|
||||
setSharing(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const where = id ? { id } : { md5 };
|
||||
EnjoyApp.videos.findOne(where).then((video) => {
|
||||
@@ -113,6 +159,7 @@ export const VideoDetail = (props: { id?: string; md5?: string }) => {
|
||||
setPlaybackRate={setPlaybackRate}
|
||||
displayInlineCaption={displayInlineCaption}
|
||||
setDisplayInlineCaption={setDisplayInlineCaption}
|
||||
onShare={() => setSharing(true)}
|
||||
/>
|
||||
|
||||
<ScrollArea
|
||||
@@ -153,6 +200,23 @@ export const VideoDetail = (props: { id?: string; md5?: string }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AlertDialog open={sharing} onOpenChange={(value) => setSharing(value)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t("shareAudio")}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("areYouSureToShareThisAudioToCommunity")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t("cancel")}</AlertDialogCancel>
|
||||
<Button variant="default" onClick={handleShare}>
|
||||
{t("share")}
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{!initialized && (
|
||||
<div className="top-0 w-full h-full absolute z-30 bg-white/10 flex items-center justify-center">
|
||||
<LoaderIcon className="text-muted-foreground animate-spin w-8 h-8" />
|
||||
|
||||
@@ -14,28 +14,30 @@ export default () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="h-full max-w-5xl mx-auto px-4 py-6">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.community")}</span>
|
||||
<div className="bg-muted h-full px-4 lg:px-8 py-6">
|
||||
<div className="max-w-screen-md mx-auto">
|
||||
<div className="flex space-x-1 items-center mb-4">
|
||||
<Button variant="ghost" size="icon" onClick={() => navigate(-1)}>
|
||||
<ChevronLeftIcon className="w-5 h-5" />
|
||||
</Button>
|
||||
<span>{t("sidebar.community")}</span>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="activities">
|
||||
<TabsList className="mb-6">
|
||||
<TabsTrigger value="activities">{t("activities")}</TabsTrigger>
|
||||
<TabsTrigger value="rankings">{t("rankings")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="activities">
|
||||
<Posts />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="rankings">
|
||||
<UsersRankings />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="activities">
|
||||
<TabsList className="mb-6">
|
||||
<TabsTrigger value="activities">{t("activities")}</TabsTrigger>
|
||||
<TabsTrigger value="rankings">{t("rankings")}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="activities">
|
||||
<Posts />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="rankings">
|
||||
<UsersRankings />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
1
enjoy/src/types/audio.d.ts
vendored
1
enjoy/src/types/audio.d.ts
vendored
@@ -11,6 +11,7 @@ type AudioType = {
|
||||
transcribing?: boolean;
|
||||
recordingsCount?: number;
|
||||
recordingsDuration?: number;
|
||||
isUploaded?: boolean;
|
||||
uploadedAt?: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
4
enjoy/src/types/enjoy-app.d.ts
vendored
4
enjoy/src/types/enjoy-app.d.ts
vendored
@@ -93,7 +93,7 @@ type EnjoyAppType = {
|
||||
audios: {
|
||||
findAll: (params: object) => Promise<AudioType[]>;
|
||||
findOne: (params: object) => Promise<AudioType>;
|
||||
create: (source: string, params?: object) => Promise<AudioType>;
|
||||
create: (uri: string, params?: object) => Promise<AudioType>;
|
||||
update: (id: string, params: object) => Promise<AudioType | undefined>;
|
||||
destroy: (id: string) => Promise<undefined>;
|
||||
transcribe: (id: string) => Promise<void>;
|
||||
@@ -102,7 +102,7 @@ type EnjoyAppType = {
|
||||
videos: {
|
||||
findAll: (params: object) => Promise<VideoType[]>;
|
||||
findOne: (params: object) => Promise<VideoType>;
|
||||
create: (source: string, params?: object) => Promise<VideoType>;
|
||||
create: (uri: string, params?: object) => Promise<VideoType>;
|
||||
update: (id: string, params: object) => Promise<VideoType | undefined>;
|
||||
destroy: (id: string) => Promise<undefined>;
|
||||
transcribe: (id: string) => Promise<void>;
|
||||
|
||||
9
enjoy/src/types/medium.d.ts
vendored
Normal file
9
enjoy/src/types/medium.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
type MediumType = {
|
||||
id: string;
|
||||
mediumType: string;
|
||||
coverUrl?: string;
|
||||
sourceUrl?: string;
|
||||
extname?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
2
enjoy/src/types/post.d.ts
vendored
2
enjoy/src/types/post.d.ts
vendored
@@ -2,6 +2,8 @@ type PostType = {
|
||||
id: string;
|
||||
content?: string;
|
||||
user: UserType;
|
||||
targetType: string;
|
||||
target?: MediumType;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
1
enjoy/src/types/video.d.ts
vendored
1
enjoy/src/types/video.d.ts
vendored
@@ -12,6 +12,7 @@ type VideoType = {
|
||||
transcribing: boolean;
|
||||
recordingsCount?: number;
|
||||
recordingsDuration?: number;
|
||||
isUploaded?: boolean;
|
||||
uploadedAt?: Date;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
Reference in New Issue
Block a user