| update audio player & other fixes
This commit is contained in:
parent
b03533dea6
commit
c5c36cb078
|
|
@ -22,4 +22,61 @@
|
||||||
a {
|
a {
|
||||||
@apply transition-all active:opacity-60;
|
@apply transition-all active:opacity-60;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*Input Range*/
|
||||||
|
/* Custom styles for the range input */
|
||||||
|
input[type="range"] {
|
||||||
|
-webkit-appearance: none; /* Remove default appearance */
|
||||||
|
width: 100%; /* Full width */
|
||||||
|
height: 2px; /* Track height */
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
white 0%, /* Start color of passed track */
|
||||||
|
white var(--value-percentage, 0%), /* End color of passed track */
|
||||||
|
gray var(--value-percentage, 0%), /* Start color of remaining track */
|
||||||
|
gray 100% /* End color of remaining track */
|
||||||
|
);
|
||||||
|
border-radius: 9999px; /* Rounded-full track */
|
||||||
|
outline: none; /* Remove outline */
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the thumb (toggle) */
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 9px; /* Thumb width */
|
||||||
|
height: 9px; /* Thumb height */
|
||||||
|
background: #fff; /* Thumb color (blue-500) */
|
||||||
|
border-radius: 9999px; /* Rounded-full thumb */
|
||||||
|
cursor: pointer; /* Pointer cursor on hover */
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For Firefox */
|
||||||
|
input[type="range"]::-moz-range-thumb {
|
||||||
|
appearance: none;
|
||||||
|
width: 9px; /* Thumb width */
|
||||||
|
height: 9px; /* Thumb height */
|
||||||
|
background: #fff; /* Thumb color (blue-500) */
|
||||||
|
border-radius: 9999px; /* Rounded-full thumb */
|
||||||
|
cursor: pointer; /* Pointer cursor on hover */
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-track {
|
||||||
|
-webkit-appearance: none; /* Remove default appearance */
|
||||||
|
width: 100%; /* Full width */
|
||||||
|
height: 2px; /* Track height */
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
white 0%, /* Start color of passed track */
|
||||||
|
white var(--value-percentage, 0%), /* End color of passed track */
|
||||||
|
gray var(--value-percentage, 0%), /* Start color of remaining track */
|
||||||
|
gray 100% /* End color of remaining track */
|
||||||
|
);
|
||||||
|
border-radius: 9999px; /* Rounded-full track */
|
||||||
|
outline: none; /* Remove outline */
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { CoverButton } from "~/pages/root/steps/data-step/components/cover-butto
|
||||||
import { HiddenFileInput } from "~/shared/ui/hidden-file-input";
|
import { HiddenFileInput } from "~/shared/ui/hidden-file-input";
|
||||||
import { useRootStore } from "~/shared/stores/root";
|
import { useRootStore } from "~/shared/stores/root";
|
||||||
import { Checkbox } from "~/shared/ui/checkbox";
|
import { Checkbox } from "~/shared/ui/checkbox";
|
||||||
|
import { AudioPlayer } from "~/shared/ui/audio-player";
|
||||||
|
|
||||||
type DataStepProps = {
|
type DataStepProps = {
|
||||||
nextStep(): void;
|
nextStep(): void;
|
||||||
|
|
@ -47,6 +48,7 @@ export const DataStep = ({ nextStep }: DataStepProps) => {
|
||||||
if (values.author) {
|
if (values.author) {
|
||||||
rootStore.setAuthor(values.author);
|
rootStore.setAuthor(values.author);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextStep();
|
nextStep();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error:", error);
|
console.error("Error:", error);
|
||||||
|
|
@ -88,6 +90,7 @@ export const DataStep = ({ nextStep }: DataStepProps) => {
|
||||||
onChange={(file) => {
|
onChange={(file) => {
|
||||||
rootStore.setFile(file);
|
rootStore.setFile(file);
|
||||||
rootStore.setFileSrc(URL.createObjectURL(file));
|
rootStore.setFileSrc(URL.createObjectURL(file));
|
||||||
|
rootStore.setFileType(file.type); // Save the file type for conditional rendering
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -99,6 +102,9 @@ export const DataStep = ({ nextStep }: DataStepProps) => {
|
||||||
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm"
|
"w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{rootStore.fileType?.startsWith("audio") ? (
|
||||||
|
<AudioPlayer src={rootStore.fileSrc} />
|
||||||
|
) : (
|
||||||
<ReactPlayer
|
<ReactPlayer
|
||||||
playsinline={true}
|
playsinline={true}
|
||||||
controls={true}
|
controls={true}
|
||||||
|
|
@ -106,9 +112,7 @@ export const DataStep = ({ nextStep }: DataStepProps) => {
|
||||||
config={{ file: { attributes: { playsInline: true } } }}
|
config={{ file: { attributes: { playsInline: true } } }}
|
||||||
url={rootStore.fileSrc}
|
url={rootStore.fileSrc}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ import { useUploadFile } from "~/shared/services/file";
|
||||||
import { Progress } from "~/shared/ui/progress";
|
import { Progress } from "~/shared/ui/progress";
|
||||||
import { useCreateNewContent } from "~/shared/services/content";
|
import { useCreateNewContent } from "~/shared/services/content";
|
||||||
import { BackButton } from "~/shared/ui/back-button";
|
import { BackButton } from "~/shared/ui/back-button";
|
||||||
|
import { useTonConnectUI } from "@tonconnect/ui-react";
|
||||||
|
|
||||||
|
|
||||||
type PresubmitStepProps = {
|
type PresubmitStepProps = {
|
||||||
prevStep(): void;
|
prevStep(): void;
|
||||||
|
|
@ -20,6 +22,8 @@ type PresubmitStepProps = {
|
||||||
export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
|
export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
|
||||||
const WebApp = useWebApp();
|
const WebApp = useWebApp();
|
||||||
|
|
||||||
|
const [tonConnectUI] = useTonConnectUI();
|
||||||
|
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
const [impactOccurred] = useHapticFeedback();
|
const [impactOccurred] = useHapticFeedback();
|
||||||
|
|
||||||
|
|
@ -44,7 +48,7 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await createContent.mutateAsync({
|
const createContentResponse = await createContent.mutateAsync({
|
||||||
title: rootStore.name,
|
title: rootStore.name,
|
||||||
|
|
||||||
// Это для одного автора
|
// Это для одного автора
|
||||||
|
|
@ -78,9 +82,32 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
|
||||||
: { allowResale: false, resaleLicensePrice: "0" }),
|
: { allowResale: false, resaleLicensePrice: "0" }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (createContentResponse.data) {
|
||||||
|
const transactionResponse = await tonConnectUI.sendTransaction({
|
||||||
|
validUntil: Math.floor(Date.now() / 1000) + 120,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
amount: createContentResponse.data.amount,
|
||||||
|
address: createContentResponse.data.address,
|
||||||
|
payload: createContentResponse.data.payload,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (transactionResponse.boc) {
|
||||||
|
WebApp.close();
|
||||||
|
|
||||||
|
await tonConnectUI.disconnect();
|
||||||
|
} else {
|
||||||
|
console.error("Transaction failed:", transactionResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WebApp.close();
|
WebApp.close();
|
||||||
// @ts-expect-error Type issues
|
// @ts-expect-error Type issues
|
||||||
} catch (error: never) {
|
} catch (error: never) {
|
||||||
|
await tonConnectUI.disconnect();
|
||||||
|
|
||||||
console.error("An error occurred during the submission process:", error);
|
console.error("An error occurred during the submission process:", error);
|
||||||
|
|
||||||
if (error?.status === 400) {
|
if (error?.status === 400) {
|
||||||
|
|
@ -161,8 +188,6 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
|
||||||
config={{ file: { attributes: { playsInline: true } } }}
|
config={{ file: { attributes: { playsInline: true } } }}
|
||||||
url={rootStore.fileSrc}
|
url={rootStore.fileSrc}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={"flex flex-col gap-[20px]"}>
|
<div className={"flex flex-col gap-[20px]"}>
|
||||||
<FormLabel label={"Цена TON"}>
|
<FormLabel label={"Цена продажи TON"}>
|
||||||
<div className={"my-2 flex flex-col gap-1.5"}>
|
<div className={"my-2 flex flex-col gap-1.5"}>
|
||||||
<p className={"text-xs"}>Минимальная стоимость {MIN_PRICE} TON.</p>
|
<p className={"text-xs"}>Минимальная стоимость {MIN_PRICE} TON.</p>
|
||||||
<p className={"text-xs"}>
|
<p className={"text-xs"}>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import ReactPlayer from "react-player/lazy";
|
import ReactPlayer from "react-player/lazy";
|
||||||
import { useTonConnectUI } from "@tonconnect/ui-react";
|
import { useTonConnectUI } from "@tonconnect/ui-react";
|
||||||
import { useMemo } from "react";
|
|
||||||
import { useWebApp } from "@vkruglikov/react-telegram-web-app";
|
import { useWebApp } from "@vkruglikov/react-telegram-web-app";
|
||||||
|
|
||||||
import { Button } from "~/shared/ui/button";
|
import { Button } from "~/shared/ui/button";
|
||||||
import { usePurchaseContent, useViewContent } from "~/shared/services/content";
|
import { usePurchaseContent, useViewContent } from "~/shared/services/content";
|
||||||
import { fromNanoTON } from "~/shared/utils";
|
import { fromNanoTON } from "~/shared/utils";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { AudioPlayer } from "~/shared/ui/audio-player";
|
||||||
|
|
||||||
export const ViewContentPage = () => {
|
export const ViewContentPage = () => {
|
||||||
const WebApp = useWebApp();
|
const WebApp = useWebApp();
|
||||||
|
|
@ -16,43 +17,41 @@ export const ViewContentPage = () => {
|
||||||
|
|
||||||
const [tonConnectUI] = useTonConnectUI();
|
const [tonConnectUI] = useTonConnectUI();
|
||||||
|
|
||||||
const transaction = useMemo(() => {
|
const handleBuyContent = useCallback(async () => {
|
||||||
const address = content?.data?.encrypted?.cid;
|
|
||||||
|
|
||||||
return {
|
|
||||||
validUntil: Math.floor(Date.now() / 1000) + 120,
|
|
||||||
messages: [
|
|
||||||
{ amount: content?.data?.encrypted?.license?.listen?.price, address },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
}, [content?.data]);
|
|
||||||
|
|
||||||
const handleBuyContent = async () => {
|
|
||||||
try {
|
try {
|
||||||
if (!tonConnectUI.connected) {
|
if (!tonConnectUI.connected) {
|
||||||
await tonConnectUI.openModal();
|
await tonConnectUI.openModal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await tonConnectUI.sendTransaction(transaction);
|
const contentResponse = await purchaseContent({
|
||||||
|
|
||||||
if (res.boc) {
|
|
||||||
await purchaseContent({
|
|
||||||
content_address: content?.data?.encrypted?.cid,
|
content_address: content?.data?.encrypted?.cid,
|
||||||
license_type: "listen",
|
license_type: "listen",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const transactionResponse = await tonConnectUI.sendTransaction({
|
||||||
|
validUntil: Math.floor(Date.now() / 1000) + 120,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
amount: contentResponse.data.amount,
|
||||||
|
address: contentResponse.data.address,
|
||||||
|
payload: contentResponse.data.payload,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (transactionResponse.boc) {
|
||||||
WebApp.close();
|
WebApp.close();
|
||||||
|
|
||||||
await tonConnectUI.disconnect();
|
await tonConnectUI.disconnect();
|
||||||
} else {
|
} else {
|
||||||
console.error("Transaction failed:", res);
|
console.error("Transaction failed:", transactionResponse);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await tonConnectUI.disconnect();
|
await tonConnectUI.disconnect();
|
||||||
console.error("Error handling Ton Connect subscription:", error);
|
console.error("Error handling Ton Connect subscription:", error);
|
||||||
}
|
}
|
||||||
};
|
}, [content]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={"flex w-full flex-col gap-[50px] px-4"}>
|
<main className={"flex w-full flex-col gap-[50px] px-4"}>
|
||||||
|
|
@ -66,6 +65,9 @@ export const ViewContentPage = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{content?.data?.content_type.startsWith("audio") ? (
|
||||||
|
<AudioPlayer src={content?.data?.display_options?.content_url} />
|
||||||
|
) : (
|
||||||
<ReactPlayer
|
<ReactPlayer
|
||||||
playsinline={true}
|
playsinline={true}
|
||||||
controls={true}
|
controls={true}
|
||||||
|
|
@ -73,6 +75,7 @@ export const ViewContentPage = () => {
|
||||||
config={{ file: { attributes: { playsInline: true } } }}
|
config={{ file: { attributes: { playsInline: true } } }}
|
||||||
url={content?.data?.display_options?.content_url}
|
url={content?.data?.display_options?.content_url}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<section className={"flex flex-col"}>
|
<section className={"flex flex-col"}>
|
||||||
<h1 className={"text-[20px] font-bold"}>
|
<h1 className={"text-[20px] font-bold"}>
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,9 @@ export const useCreateNewContent = () => {
|
||||||
["create-new-content"],
|
["create-new-content"],
|
||||||
(payload: UseCreateNewContentPayload) => {
|
(payload: UseCreateNewContentPayload) => {
|
||||||
return request.post<{
|
return request.post<{
|
||||||
message: string;
|
address: string;
|
||||||
|
amount: string;
|
||||||
|
payload: string;
|
||||||
}>("/blockchain.sendNewContentMessage", payload);
|
}>("/blockchain.sendNewContentMessage", payload);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -53,7 +55,11 @@ export const usePurchaseContent = () => {
|
||||||
content_address: string;
|
content_address: string;
|
||||||
license_type: "listen" | "resale";
|
license_type: "listen" | "resale";
|
||||||
}) => {
|
}) => {
|
||||||
return request.post("/blockchain.sendPurchaseContentMessage", {
|
return request.post<{
|
||||||
|
address: string;
|
||||||
|
amount: string;
|
||||||
|
payload: string;
|
||||||
|
}>("/blockchain.sendPurchaseContentMessage", {
|
||||||
content_address,
|
content_address,
|
||||||
license_type,
|
license_type,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ type RootStore = {
|
||||||
file: File | null;
|
file: File | null;
|
||||||
setFile: (file: File) => void;
|
setFile: (file: File) => void;
|
||||||
|
|
||||||
|
fileType: string;
|
||||||
|
setFileType: (type: string) => void;
|
||||||
|
|
||||||
fileSrc: string;
|
fileSrc: string;
|
||||||
setFileSrc: (fileSrc: string) => void;
|
setFileSrc: (fileSrc: string) => void;
|
||||||
|
|
||||||
|
|
@ -53,6 +56,9 @@ export const useRootStore = create<RootStore>((set) => ({
|
||||||
file: null,
|
file: null,
|
||||||
setFile: (file) => set({ file }),
|
setFile: (file) => set({ file }),
|
||||||
|
|
||||||
|
fileType: "",
|
||||||
|
setFileType: (fileType) => set({ fileType }),
|
||||||
|
|
||||||
fileSrc: "",
|
fileSrc: "",
|
||||||
setFileSrc: (fileSrc) => set({ fileSrc }),
|
setFileSrc: (fileSrc) => set({ fileSrc }),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { FC, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
interface AudioPlayerProps {
|
||||||
|
src: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AudioPlayer: FC<AudioPlayerProps> = ({ src }) => {
|
||||||
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||||
|
const [currentTime, setCurrentTime] = useState<number>(0);
|
||||||
|
const [duration, setDuration] = useState<number>(0);
|
||||||
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
const rangeRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
|
||||||
|
if (!audio) return;
|
||||||
|
|
||||||
|
const handleLoadedMetadata = () => {
|
||||||
|
setDuration(audio.duration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
setCurrentTime(audio.currentTime);
|
||||||
|
updateRangeBackground(audio.currentTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
audio.addEventListener("loadedmetadata", handleLoadedMetadata);
|
||||||
|
audio.addEventListener("timeupdate", handleTimeUpdate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
audio.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
||||||
|
audio.removeEventListener("timeupdate", handleTimeUpdate);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const togglePlayPause = () => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio) return;
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
audio.pause();
|
||||||
|
} else {
|
||||||
|
audio.play();
|
||||||
|
}
|
||||||
|
setIsPlaying(!isPlaying);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSliderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio) return;
|
||||||
|
|
||||||
|
const newTime = parseFloat(e.target.value);
|
||||||
|
audio.currentTime = newTime;
|
||||||
|
setCurrentTime(newTime);
|
||||||
|
updateRangeBackground(newTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (time: number): string => {
|
||||||
|
const minutes = Math.floor(time / 60);
|
||||||
|
const seconds = Math.floor(time % 60);
|
||||||
|
return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRangeBackground = (currentTime: number) => {
|
||||||
|
if (rangeRef.current) {
|
||||||
|
const percentage = (currentTime / duration) * 100;
|
||||||
|
rangeRef.current.style.setProperty(
|
||||||
|
"--value-percentage",
|
||||||
|
`${percentage}%`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<audio ref={audioRef} src={src} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={togglePlayPause}
|
||||||
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white text-gray focus:outline-none"
|
||||||
|
>
|
||||||
|
{isPlaying ? (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M6.75 5.25a.75.75 0 0 1 .75-.75H9a.75.75 0 0 1 .75.75v13.5a.75.75 0 0 1-.75.75H7.5a.75.75 0 0 1-.75-.75V5.25Zm7.5 0A.75.75 0 0 1 15 4.5h1.5a.75.75 0 0 1 .75.75v13.5a.75.75 0 0 1-.75.75H15a.75.75 0 0 1-.75-.75V5.25Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="size-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M4.5 5.653c0-1.427 1.529-2.33 2.779-1.643l11.54 6.347c1.295.712 1.295 2.573 0 3.286L7.28 19.99c-1.25.687-2.779-.217-2.779-1.643V5.653Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className={"flex w-full flex-col gap-2"}>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
ref={rangeRef}
|
||||||
|
min="0"
|
||||||
|
max={duration}
|
||||||
|
value={currentTime}
|
||||||
|
onChange={handleSliderChange}
|
||||||
|
className="flex-grow"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span>{formatTime(currentTime)}</span>
|
||||||
|
<span>{formatTime(duration)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue