| update audio player & other fixes
This commit is contained in:
parent
b03533dea6
commit
c5c36cb078
|
|
@ -22,4 +22,61 @@
|
|||
a {
|
||||
@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 { useRootStore } from "~/shared/stores/root";
|
||||
import { Checkbox } from "~/shared/ui/checkbox";
|
||||
import { AudioPlayer } from "~/shared/ui/audio-player";
|
||||
|
||||
type DataStepProps = {
|
||||
nextStep(): void;
|
||||
|
|
@ -47,6 +48,7 @@ export const DataStep = ({ nextStep }: DataStepProps) => {
|
|||
if (values.author) {
|
||||
rootStore.setAuthor(values.author);
|
||||
}
|
||||
|
||||
nextStep();
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
|
|
@ -88,6 +90,7 @@ export const DataStep = ({ nextStep }: DataStepProps) => {
|
|||
onChange={(file) => {
|
||||
rootStore.setFile(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"
|
||||
}
|
||||
>
|
||||
{rootStore.fileType?.startsWith("audio") ? (
|
||||
<AudioPlayer src={rootStore.fileSrc} />
|
||||
) : (
|
||||
<ReactPlayer
|
||||
playsinline={true}
|
||||
controls={true}
|
||||
|
|
@ -106,9 +112,7 @@ export const DataStep = ({ nextStep }: DataStepProps) => {
|
|||
config={{ file: { attributes: { playsInline: true } } }}
|
||||
url={rootStore.fileSrc}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</FormLabel>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import { useUploadFile } from "~/shared/services/file";
|
|||
import { Progress } from "~/shared/ui/progress";
|
||||
import { useCreateNewContent } from "~/shared/services/content";
|
||||
import { BackButton } from "~/shared/ui/back-button";
|
||||
import { useTonConnectUI } from "@tonconnect/ui-react";
|
||||
|
||||
|
||||
type PresubmitStepProps = {
|
||||
prevStep(): void;
|
||||
|
|
@ -20,6 +22,8 @@ type PresubmitStepProps = {
|
|||
export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
|
||||
const WebApp = useWebApp();
|
||||
|
||||
const [tonConnectUI] = useTonConnectUI();
|
||||
|
||||
const rootStore = useRootStore();
|
||||
const [impactOccurred] = useHapticFeedback();
|
||||
|
||||
|
|
@ -44,7 +48,7 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
|
|||
);
|
||||
}
|
||||
|
||||
await createContent.mutateAsync({
|
||||
const createContentResponse = await createContent.mutateAsync({
|
||||
title: rootStore.name,
|
||||
|
||||
// Это для одного автора
|
||||
|
|
@ -78,9 +82,32 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
|
|||
: { 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();
|
||||
// @ts-expect-error Type issues
|
||||
} catch (error: never) {
|
||||
await tonConnectUI.disconnect();
|
||||
|
||||
console.error("An error occurred during the submission process:", error);
|
||||
|
||||
if (error?.status === 400) {
|
||||
|
|
@ -161,8 +188,6 @@ export const PresubmitStep = ({ prevStep }: PresubmitStepProps) => {
|
|||
config={{ file: { attributes: { playsInline: true } } }}
|
||||
url={rootStore.fileSrc}
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export const PriceStep = ({ nextStep, prevStep }: PriceStepProps) => {
|
|||
</div>
|
||||
|
||||
<div className={"flex flex-col gap-[20px]"}>
|
||||
<FormLabel label={"Цена TON"}>
|
||||
<FormLabel label={"Цена продажи TON"}>
|
||||
<div className={"my-2 flex flex-col gap-1.5"}>
|
||||
<p className={"text-xs"}>Минимальная стоимость {MIN_PRICE} TON.</p>
|
||||
<p className={"text-xs"}>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import ReactPlayer from "react-player/lazy";
|
||||
import { useTonConnectUI } from "@tonconnect/ui-react";
|
||||
import { useMemo } from "react";
|
||||
import { useWebApp } from "@vkruglikov/react-telegram-web-app";
|
||||
|
||||
import { Button } from "~/shared/ui/button";
|
||||
import { usePurchaseContent, useViewContent } from "~/shared/services/content";
|
||||
import { fromNanoTON } from "~/shared/utils";
|
||||
import { useCallback } from "react";
|
||||
import { AudioPlayer } from "~/shared/ui/audio-player";
|
||||
|
||||
export const ViewContentPage = () => {
|
||||
const WebApp = useWebApp();
|
||||
|
|
@ -16,43 +17,41 @@ export const ViewContentPage = () => {
|
|||
|
||||
const [tonConnectUI] = useTonConnectUI();
|
||||
|
||||
const transaction = useMemo(() => {
|
||||
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 () => {
|
||||
const handleBuyContent = useCallback(async () => {
|
||||
try {
|
||||
if (!tonConnectUI.connected) {
|
||||
await tonConnectUI.openModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await tonConnectUI.sendTransaction(transaction);
|
||||
|
||||
if (res.boc) {
|
||||
await purchaseContent({
|
||||
const contentResponse = await purchaseContent({
|
||||
content_address: content?.data?.encrypted?.cid,
|
||||
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();
|
||||
|
||||
await tonConnectUI.disconnect();
|
||||
} else {
|
||||
console.error("Transaction failed:", res);
|
||||
console.error("Transaction failed:", transactionResponse);
|
||||
}
|
||||
} catch (error) {
|
||||
await tonConnectUI.disconnect();
|
||||
console.error("Error handling Ton Connect subscription:", error);
|
||||
}
|
||||
};
|
||||
}, [content]);
|
||||
|
||||
return (
|
||||
<main className={"flex w-full flex-col gap-[50px] px-4"}>
|
||||
|
|
@ -66,6 +65,9 @@ export const ViewContentPage = () => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{content?.data?.content_type.startsWith("audio") ? (
|
||||
<AudioPlayer src={content?.data?.display_options?.content_url} />
|
||||
) : (
|
||||
<ReactPlayer
|
||||
playsinline={true}
|
||||
controls={true}
|
||||
|
|
@ -73,6 +75,7 @@ export const ViewContentPage = () => {
|
|||
config={{ file: { attributes: { playsInline: true } } }}
|
||||
url={content?.data?.display_options?.content_url}
|
||||
/>
|
||||
)}
|
||||
|
||||
<section className={"flex flex-col"}>
|
||||
<h1 className={"text-[20px] font-bold"}>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ export const useCreateNewContent = () => {
|
|||
["create-new-content"],
|
||||
(payload: UseCreateNewContentPayload) => {
|
||||
return request.post<{
|
||||
message: string;
|
||||
address: string;
|
||||
amount: string;
|
||||
payload: string;
|
||||
}>("/blockchain.sendNewContentMessage", payload);
|
||||
},
|
||||
);
|
||||
|
|
@ -53,7 +55,11 @@ export const usePurchaseContent = () => {
|
|||
content_address: string;
|
||||
license_type: "listen" | "resale";
|
||||
}) => {
|
||||
return request.post("/blockchain.sendPurchaseContentMessage", {
|
||||
return request.post<{
|
||||
address: string;
|
||||
amount: string;
|
||||
payload: string;
|
||||
}>("/blockchain.sendPurchaseContentMessage", {
|
||||
content_address,
|
||||
license_type,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ type RootStore = {
|
|||
file: File | null;
|
||||
setFile: (file: File) => void;
|
||||
|
||||
fileType: string;
|
||||
setFileType: (type: string) => void;
|
||||
|
||||
fileSrc: string;
|
||||
setFileSrc: (fileSrc: string) => void;
|
||||
|
||||
|
|
@ -53,6 +56,9 @@ export const useRootStore = create<RootStore>((set) => ({
|
|||
file: null,
|
||||
setFile: (file) => set({ file }),
|
||||
|
||||
fileType: "",
|
||||
setFileType: (fileType) => set({ fileType }),
|
||||
|
||||
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