From d2e350bc280a014fdea8ab09c2ae249ee19b301c Mon Sep 17 00:00:00 2001 From: root Date: Thu, 16 Oct 2025 16:22:15 +0000 Subject: [PATCH] smashed content view fixes --- src/pages/root/steps/data-step/index.tsx | 93 ++++++-- src/pages/root/steps/presubmit-step/index.tsx | 36 ++- src/pages/view-content/index.tsx | 219 +++++++++--------- src/shared/services/content/index.ts | 4 +- src/shared/stores/root/index.ts | 4 + src/shared/ui/checkbox/index.tsx | 8 +- 6 files changed, 227 insertions(+), 137 deletions(-) diff --git a/src/pages/root/steps/data-step/index.tsx b/src/pages/root/steps/data-step/index.tsx index dc42ac7..aeeab3b 100644 --- a/src/pages/root/steps/data-step/index.tsx +++ b/src/pages/root/steps/data-step/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -17,12 +17,29 @@ import { HashtagInput } from "~/shared/ui/hashtag-input"; import { Replace } from "~/shared/ui/icons/replace"; import { DisclaimerModal } from "./components/disclaimer-modal"; +const formatFileSize = (bytes: number | undefined) => { + if (!bytes) { + return ""; + } + const units = ["Б", "КБ", "МБ", "ГБ"]; + let value = bytes; + let unitIndex = 0; + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex += 1; + } + return `${value.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; +}; + type DataStepProps = { nextStep(): void; }; export const DataStep = ({ nextStep }: DataStepProps) => { const rootStore = useRootStore(); + const isAudioFile = rootStore.fileType?.startsWith("audio"); + const isVideoFile = rootStore.fileType?.startsWith("video"); + const isMediaFile = isAudioFile || isVideoFile; const [disclaimerAccepted, setDisclaimerAccepted] = useState(false); @@ -61,11 +78,13 @@ export const DataStep = ({ nextStep }: DataStepProps) => { })(); }; - const handleFileReset = () => { + const handleFileReset = useCallback(() => { rootStore.setFile(null); rootStore.setFileSrc(''); rootStore.setFileType(''); - } + rootStore.setDownloadLocked(false); + rootStore.setAllowDwnld(false); + }, [rootStore]); useEffect(() => { @@ -80,6 +99,39 @@ export const DataStep = ({ nextStep }: DataStepProps) => { localStorage.setItem('disclaimerAccepted', 'true'); }; + useEffect(() => { + if (!rootStore.file) { + if (rootStore.isDownloadLocked) { + rootStore.setDownloadLocked(false); + } + return; + } + + if (!isMediaFile) { + if (!rootStore.allowDwnld) { + rootStore.setAllowDwnld(true); + } + if (!rootStore.isDownloadLocked) { + rootStore.setDownloadLocked(true); + } + } else if (rootStore.isDownloadLocked) { + rootStore.setDownloadLocked(false); + } + }, [ + isMediaFile, + rootStore, + rootStore.allowDwnld, + rootStore.file, + rootStore.isDownloadLocked, + ]); + + const handleFileChange = useCallback((file: File) => { + rootStore.setFile(file); + rootStore.setFileSrc(URL.createObjectURL(file)); + const mime = file.type || "application/octet-stream"; + rootStore.setFileType(mime); + }, [rootStore]); + return (
@@ -118,20 +170,18 @@ export const DataStep = ({ nextStep }: DataStepProps) => { - {!rootStore.fileSrc && <> + {!rootStore.fileSrc && ( + <> { - rootStore.setFile(file); - rootStore.setFileSrc(URL.createObjectURL(file)); - rootStore.setFileType(file.type); // Save the file type for conditional rendering - }} + accept={"*/*"} + onChange={handleFileChange} /> - } + + )} {rootStore.fileSrc && (
{ "w-full border border-white bg-[#2B2B2B] px-[10px] py-[8px] text-sm" } > - {rootStore.fileType?.startsWith("audio") ? ( + {isMediaFile ? ( + isAudioFile ? ( - ) : ( + ) : ( { config={{ file: { attributes: { playsInline: true } } }} url={rootStore.fileSrc} /> + ) + ) : ( +
+
+ {rootStore.file?.name} + + {formatFileSize(rootStore.file?.size)} + +
+
)}
)} - {uploadFile.isUploading && uploadFile.isLoading && ( )} diff --git a/src/pages/view-content/index.tsx b/src/pages/view-content/index.tsx index 8f4a221..f7923d5 100644 --- a/src/pages/view-content/index.tsx +++ b/src/pages/view-content/index.tsx @@ -36,37 +36,21 @@ export const ViewContentPage = () => { const [isCongratsModal, setIsCongratsModal] = useState(false); const [isErrorModal, setIsErrorModal] = useState(false); - const statusState = content?.data?.status?.state ?? "uploaded"; - const conversionState = content?.data?.conversion?.state; - const uploadState = content?.data?.upload?.state; - const statusMessage = useMemo(() => { - switch (statusState) { - case "processing": - return "Контент обрабатывается"; - case "failed": - return "Ошибка обработки"; - case "ready": - return null; - default: - return "Файл загружен"; - } - }, [statusState]); - - const mediaUrl = content?.data?.display_options?.content_url ?? null; - const isAudio = Boolean(content?.data?.content_type?.startsWith('audio')); - const isReady = Boolean(mediaUrl); - const metadataName = content?.data?.display_options?.metadata?.name; - const contentTitle = metadataName || content?.data?.encrypted?.title || 'Контент'; - const processingDetails = useMemo(() => { - if (!statusMessage) { - return null; - } - return { - conversion: conversionState, - upload: uploadState, - }; - }, [conversionState, statusMessage, uploadState]); - + const displayOptions = content?.data?.display_options ?? {}; + const mediaUrl = displayOptions?.content_url ?? null; + const downloadUrl = displayOptions?.download_url ?? null; + const metadata = displayOptions?.metadata ?? {}; + const metadataTitle = metadata?.title || metadata?.name || content?.data?.encrypted?.title || 'Контент'; + const metadataArtist = metadata?.artist || content?.data?.encrypted?.artist || null; + const displayTitle = metadata?.display_name || (metadataArtist ? `${metadataArtist} – ${metadataTitle}` : metadataTitle); + const contentKind = + displayOptions?.content_kind ?? + content?.data?.content_kind ?? + ((content?.data?.content_type ?? '').split('/')[0] || 'other'); + const isAudio = contentKind === 'audio'; + const hasMediaPlayer = Boolean(mediaUrl); + const coverUrl = metadata?.image ?? null; + const canDownload = Boolean(content?.data?.downloadable && downloadUrl); const handleBuyContentTON = useCallback(async () => { if (!contentId) { console.error('No content identifier available for purchase'); @@ -202,10 +186,10 @@ export const ViewContentPage = () => { }, [haveLicense, refetchContent]); useEffect(() => { - if (contentTitle) { - document.title = contentTitle; + if (displayTitle) { + document.title = displayTitle; } - }, [contentTitle]); + }, [displayTitle]); useEffect(() => { if (!contentId) { @@ -229,12 +213,17 @@ export const ViewContentPage = () => { const handleDwnldContent = async () => { try { - const fileUrl = content?.data?.display_options?.content_url; - const fileName = content?.data?.display_options?.metadata?.name || 'content'; - const fileFormat = content?.data?.content_ext || '.raw'; + const fileUrl = downloadUrl ?? mediaUrl; + if (!fileUrl) { + console.error('No file URL available for download'); + return; + } + const fileName = metadataTitle || 'content'; + const rawExtension = content?.data?.content_ext || metadata?.file_extension || 'raw'; + const normalizedExtension = rawExtension.startsWith('.') ? rawExtension.slice(1) : rawExtension; await WebApp.downloadFile({ url: fileUrl, - file_name: fileName + '.' + fileFormat, + file_name: `${fileName}.${normalizedExtension || 'raw'}`, }); } catch (error) { console.error('Error downloading content:', error); @@ -242,22 +231,22 @@ export const ViewContentPage = () => { }; return ( -
+
{isCongratsModal && } {isErrorModal && } - {isReady && isAudio && - content?.data?.display_options?.metadata?.image && ( -
- {'content_image'} -
- )} - {isReady ? ( + {hasMediaPlayer ? ( <> + {isAudio && coverUrl && ( +
+ {'content_image'} +
+ )} + {isAudio ? ( ) : ( @@ -270,7 +259,7 @@ export const ViewContentPage = () => { attributes: { playsInline: true, autoPlay: true, - poster: content?.data?.display_options?.metadata?.image || undefined, + poster: coverUrl || undefined, }, }, }} @@ -279,82 +268,88 @@ export const ViewContentPage = () => { )}
-

{metadataName}

-

- {content?.data?.display_options?.metadata?.description} -

+

{displayTitle}

+ {metadata?.description && ( +

+ {metadata.description} +

+ )}
- -
- {content?.data?.downloadable && ( -
- )} -
); }; diff --git a/src/shared/services/content/index.ts b/src/shared/services/content/index.ts index 23fa10f..98fbd26 100644 --- a/src/shared/services/content/index.ts +++ b/src/shared/services/content/index.ts @@ -14,6 +14,7 @@ const VIEW_CONTENT_API_PATH = "/api/v1"; type UseCreateNewContentPayload = { title: string; + artist?: string; content: string; image: string; description: string; @@ -39,6 +40,7 @@ export const useCreateNewContent = () => { royaltyCount: payload.royaltyParams.length, downloadable: payload.downloadable, allowResale: payload.allowResale, + artist: payload.artist, }); try { @@ -46,7 +48,7 @@ export const useCreateNewContent = () => { address: string; amount: string; payload: string; - }>("/blockchain.sendNewContentMessage", payload, { + }>("/blockchain.sendNewContentMessage", payload, { headers: { Authorization: localStorage.getItem('auth_v1_token') ?? "" } diff --git a/src/shared/stores/root/index.ts b/src/shared/stores/root/index.ts index 18b8486..fb6ef0c 100644 --- a/src/shared/stores/root/index.ts +++ b/src/shared/stores/root/index.ts @@ -35,6 +35,8 @@ type RootStore = { allowDwnld: boolean; setAllowDwnld: (allowDwnld: boolean) => void; + isDownloadLocked: boolean; + setDownloadLocked: (locked: boolean) => void; cover: File | null; setCover: (cover: File | null) => void; @@ -86,6 +88,8 @@ export const useRootStore = create((set) => ({ allowDwnld: false, setAllowDwnld: (allowDwnld) => set({ allowDwnld }), + isDownloadLocked: false, + setDownloadLocked: (isDownloadLocked) => set({ isDownloadLocked }), cover: null, setCover: (cover) => set({ cover }), diff --git a/src/shared/ui/checkbox/index.tsx b/src/shared/ui/checkbox/index.tsx index 1921ada..b15adcc 100644 --- a/src/shared/ui/checkbox/index.tsx +++ b/src/shared/ui/checkbox/index.tsx @@ -3,12 +3,16 @@ import { useHapticFeedback } from "@vkruglikov/react-telegram-web-app"; type CheckboxProps = { onClick(): void; checked: boolean; + disabled?: boolean; }; -export const Checkbox = ({ onClick, checked }: CheckboxProps) => { +export const Checkbox = ({ onClick, checked, disabled }: CheckboxProps) => { const [impactOccurred] = useHapticFeedback(); const handleClick = () => { + if (disabled) { + return; + } impactOccurred("light"); onClick(); }; @@ -16,7 +20,7 @@ export const Checkbox = ({ onClick, checked }: CheckboxProps) => { return (
{checked &&
}