diff --git a/package-lock.json b/package-lock.json index 56f6ef5..d8aa078 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "dexie": "^4.0.11", "dompurify": "^3.2.4", "idb-keyval": "^6.2.2", + "iso-639-1": "^3.1.5", "react-dropzone": "^14.3.8", "react-hot-toast": "^2.5.2", "react-idle-timer": "^5.7.2", @@ -2566,6 +2567,15 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/iso-639-1": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.5.tgz", + "integrity": "sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", diff --git a/package.json b/package.json index 57dc322..95d14fe 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "dexie": "^4.0.11", "dompurify": "^3.2.4", "idb-keyval": "^6.2.2", + "iso-639-1": "^3.1.5", "react-dropzone": "^14.3.8", "react-hot-toast": "^2.5.2", "react-idle-timer": "^5.7.2", diff --git a/src/components/VideoPlayer/LanguageSelect.tsx b/src/components/VideoPlayer/LanguageSelect.tsx new file mode 100644 index 0000000..af1dbcd --- /dev/null +++ b/src/components/VideoPlayer/LanguageSelect.tsx @@ -0,0 +1,33 @@ +// components/LanguageSelector.tsx +import React from 'react'; +import { Autocomplete, TextField } from '@mui/material'; +import { languageOptions } from './SubtitleManager'; + + +export default function LanguageSelector({ + value, + onChange, +}: { + value: string | null; + onChange: (value: string | null) => void; +}) { + return ( + `${option.name} (${option.code})`} + value={languageOptions.find((opt) => opt.code === value) || null} + onChange={(event, newValue) => onChange(newValue?.code || null)} + renderInput={(params) => } + isOptionEqualToValue={(option, val) => option.code === val.code} + sx={{ width: 300 }} + slotProps={{ + popper: { + sx: { + zIndex: 999991, // Must be higher than Dialog's default zIndex (1300) + }, + }, + }} + + /> + ); +} diff --git a/src/components/VideoPlayer/SubtitleManager.tsx b/src/components/VideoPlayer/SubtitleManager.tsx index 08efddc..ca636f2 100644 --- a/src/components/VideoPlayer/SubtitleManager.tsx +++ b/src/components/VideoPlayer/SubtitleManager.tsx @@ -1,117 +1,350 @@ -import { useCallback, useEffect, useState } from "react" -import { QortalGetMetadata } from "../../types/interfaces/resources" -import { Box, ButtonBase, Dialog, DialogContent, DialogTitle, IconButton, Typography } from "@mui/material" +import React, { useCallback, useEffect, useState } from "react"; +import { QortalGetMetadata, QortalMetadata, Service } from "../../types/interfaces/resources"; +import { + alpha, + Box, + Button, + ButtonBase, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Fade, + IconButton, + Popover, + Typography, +} from "@mui/material"; +import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; +import ModeEditIcon from '@mui/icons-material/ModeEdit'; import CloseIcon from "@mui/icons-material/Close"; import { useListStore } from "../../state/lists"; -import { useResources } from "../../hooks/useResources"; +import { Resource, useResources } from "../../hooks/useResources"; import { useGlobal } from "../../context/GlobalProvider"; +import { ENTITY_SUBTITLE, SERVICE_SUBTITLE } from "./video-player-constants"; +import ISO6391, { LanguageCode } from "iso-639-1"; +import LanguageSelect from "./LanguageSelect"; +import { + useDropzone, + DropzoneRootProps, + DropzoneInputProps, +} from "react-dropzone"; +import { fileToBase64, objectToBase64 } from "../../utils/base64"; +import { ResourceToPublish } from "../../types/qortalRequests/types"; +import { useListReturn } from "../../hooks/useListData"; +import { usePublish } from "../../hooks/usePublish"; interface SubtitleManagerProps { - qortalMetadata: QortalGetMetadata - close: ()=> void - open: boolean + qortalMetadata: QortalGetMetadata; + close: () => void; + open: boolean; + onSelect: (subtitle: SubtitlePublishedData)=> void; + subtitleBtnRef: any +} +export interface Subtitle { + language: string | null; + base64: string; + type: string; + filename: string; + size: number; +} +export interface SubtitlePublishedData { + language: string | null; + subtitleData: string; + type: string; + filename: string; + size: number; } -export const SubtitleManager = ({qortalMetadata, open, close}: SubtitleManagerProps) => { - const [mode, setMode] = useState(1) - const {lists} = useGlobal() - const {fetchResources} = useResources() - // const [subtitles, setSubtitles] = useState([]) - const subtitles = useListStore( - (state) => state.lists[`${qortalMetadata?.service}- ${qortalMetadata?.name}-${qortalMetadata?.identifier}`]?.items || [] -); - const getPublishedSubtitles = useCallback(async ()=> { - try { - await fetchResources(qortalMetadata, `${qortalMetadata?.service}- ${qortalMetadata?.name}-${qortalMetadata?.identifier}`, "BASE64"); - } catch (error) { - console.error(error) - } - }, []) - - useEffect(()=> { - if(!qortalMetadata?.identifier || !qortalMetadata?.name || !qortalMetadata?.service) return +export const languageOptions = ISO6391.getAllCodes().map((code) => ({ + code, + name: ISO6391.getName(code), + nativeName: ISO6391.getNativeName(code), +})); +const SubtitleManagerComponent = ({ + qortalMetadata, + open, + close, + onSelect, + subtitleBtnRef +}: SubtitleManagerProps) => { + const [mode, setMode] = useState(1); + const { lists, identifierOperations, auth } = useGlobal(); + const { fetchResources } = useResources(); + // const [subtitles, setSubtitles] = useState([]) + const subtitles = useListReturn(`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`) + - // getPublishedSubtitles() - }, [qortalMetadata?.identifier, qortalMetadata?.service, qortalMetadata?.name, getPublishedSubtitles]) + console.log('subtitles222', subtitles) + const getPublishedSubtitles = useCallback(async () => { + try { + const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`; + console.log('videoId', videoId) + const postIdSearch = await identifierOperations.buildSearchPrefix( + ENTITY_SUBTITLE, + videoId, + ); + const searchParams = { + service: SERVICE_SUBTITLE, + identifier: postIdSearch, + limit: 0 + }; + const res = await lists.fetchResources(searchParams, `subs-${videoId}`, "BASE64"); + lists.addList(`subs-${videoId}`, res || []); + console.log('resres2', res) + } catch (error) { + console.error(error); + } + }, []); - const handleClose = () => { - close() + useEffect(() => { + if ( + !qortalMetadata?.identifier || + !qortalMetadata?.name || + !qortalMetadata?.service + ) + return; + + getPublishedSubtitles() + }, [ + qortalMetadata?.identifier, + qortalMetadata?.service, + qortalMetadata?.name, + getPublishedSubtitles, + ]); + + const handleClose = () => { + close(); setMode(1); // setTitle(""); // setDescription(""); // setHasMetadata(false); }; - const onSelect = ()=> { + const publishHandler = async (subtitles: Subtitle[]) => { + try { + const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`; + + const identifier = await identifierOperations.buildIdentifier(ENTITY_SUBTITLE, videoId); + const name = auth?.name +console.log('identifier2', identifier) + if(!name) return + const resources: ResourceToPublish[] = [] + const tempResources: {qortalMetadata: QortalMetadata, data: any}[] = [] + for(const sub of subtitles ){ + const data = { + subtitleData: sub.base64, + language: sub.language, + filename: sub.filename, + type: sub.type + } + + const base64Data = await objectToBase64(data) + const resource = { + name, + identifier, + service: SERVICE_SUBTITLE, + base64: base64Data, + filename: sub.filename, + title: sub.language || undefined + } + resources.push(resource) + tempResources.push({ + qortalMetadata: { + identifier, + service: SERVICE_SUBTITLE, + name, + size: 100, + created: Date.now() + }, + data: data, + }) + } + console.log('resources', resources) + + await qortalRequest({ + action: 'PUBLISH_MULTIPLE_QDN_RESOURCES', + resources + }) + + + lists.addNewResources(`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`, tempResources) + } catch (error) { + + } + }; + const onBack = ()=> { + if(mode === 1) close() + } + + const onSelectHandler = (sub: SubtitlePublishedData)=> { + onSelect(sub) + close() } return ( - - Subtitles - ({ - position: "absolute", - right: 8, - top: 8, - })} + - - - {mode === 1 && ( + + + + + + Subtitles + + + + + + + + {mode === 1 && ( - )} - {/* {mode === 2 && ( - )} + {/* + {[ + 'Ambient mode', + 'Annotations', + 'Subtitles/CC', + 'Sleep timer', + 'Playback speed', + 'Quality', + ].map((label) => ( + + {label} + + ))} + */} + + // + // Subtitles + // ({ + // position: "absolute", + // right: 8, + // top: 8, + // })} + // > + // + // + // + // {mode === 1 && ( + // + // )} + // {mode === 5 && } + // {/* {mode === 2 && ( + // + // )} - {mode === 4 && ( - - )} */} - - ) -} + // {mode === 4 && ( + // + // )} */} + // + ); +}; interface PublisherSubtitlesProps { - publisherName: string - subtitles: any[] - setMode: (val: number)=> void - onSelect: (subtitle: any)=> void + publisherName: string; + subtitles: any[]; + setMode: (val: number) => void; + onSelect: (subtitle: any) => void; + onBack: ()=> void; } const PublisherSubtitles = ({ @@ -119,8 +352,80 @@ const PublisherSubtitles = ({ subtitles, setMode, onSelect, + onBack }: PublisherSubtitlesProps) => { + + return ( + <> + + {subtitles?.map((sub)=> { + return + })} + + + + ); +}; + +interface PublishSubtitlesProps { + publishHandler: (subs: Subtitle[])=> void +} + + + +const PublishSubtitles = ({ publishHandler }: PublishSubtitlesProps) => { + const [language, setLanguage] = useState(null); + const [subtitles, setSubtitles] = useState([]); + const onDrop = useCallback(async (acceptedFiles: File[]) => { + const newSubtitles: Subtitle[] = []; + for (const file of acceptedFiles) { + try { + const newSubtitle = { + base64: await fileToBase64(file), + language: null, + type: file.type, + filename: file.name, + size: file.size, + }; + newSubtitles.push(newSubtitle) + } catch (error) { + console.error("Failed to parse audio file:", error); + } + } + setSubtitles((prev) => [...newSubtitles, ...prev]); + }, []); + const { + getRootProps, + getInputProps, + }: { + getRootProps: () => DropzoneRootProps; + getInputProps: () => DropzoneInputProps; + isDragActive: boolean; + } = useDropzone({ + onDrop, + accept: { + "application/x-subrip": [".srt"], // SRT subtitles + "text/vtt": [".vtt"], // WebVTT subtitles + }, + multiple: true, + maxSize: 2 * 1024 * 1024, // 2MB + }); + +const onChangeValue = (field: string, data: any, index: number) => { + const sub = subtitles[index]; + if (!sub) return; + + const copySub = { ...sub, [field]: data }; + + setSubtitles((prev) => { + const copyPrev = [...prev]; + copyPrev[index] = copySub; + return copyPrev; + }); +}; +console.log('subtitles', subtitles) + return ( <> @@ -133,27 +438,64 @@ const PublisherSubtitles = ({ alignItems: "flex-start", }} > - + + + {subtitles?.map((sub, i) => { + return ( + <> + onChangeValue('language',val, i)} + /> + + ); + })} + + + ); -}; \ No newline at end of file +}; + +interface SubProps { + sub: QortalGetMetadata + onSelect: (subtitle: Subtitle)=> void; +} +const Subtitle = ({sub, onSelect}: SubProps)=> { + const {resource, isLoading } = usePublish(2, 'JSON', sub) + console.log('resource', resource) + return onSelect(resource?.data)} + + sx={{ + px: 2, + py: 1, + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + cursor: 'pointer', + }, + }} + > + {resource?.data?.language} + +} + + export const SubtitleManager = React.memo(SubtitleManagerComponent); diff --git a/src/components/VideoPlayer/VideoControlsBar.tsx b/src/components/VideoPlayer/VideoControlsBar.tsx index ebe588e..355cbcd 100644 --- a/src/components/VideoPlayer/VideoControlsBar.tsx +++ b/src/components/VideoPlayer/VideoControlsBar.tsx @@ -35,9 +35,10 @@ interface VideoControlsBarProps { decreaseSpeed: ()=> void playbackRate: number openSubtitleManager: ()=> void + subtitleBtnRef: any } -export const VideoControlsBar = ({showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager}: VideoControlsBarProps) => { +export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager}: VideoControlsBarProps) => { const showMobileControls = isScreenSmall && canPlay; @@ -96,7 +97,7 @@ export const VideoControlsBar = ({showControls, playbackRate, increaseSpeed,decr - + sub diff --git a/src/components/VideoPlayer/VideoPlayer.tsx b/src/components/VideoPlayer/VideoPlayer.tsx index e09192d..096269f 100644 --- a/src/components/VideoPlayer/VideoPlayer.tsx +++ b/src/components/VideoPlayer/VideoPlayer.tsx @@ -10,9 +10,30 @@ import videojs from 'video.js'; import 'video.js/dist/video-js.css'; import Player from "video.js/dist/types/player"; -import { SubtitleManager } from "./SubtitleManager"; +import { Subtitle, SubtitleManager, SubtitlePublishedData } from "./SubtitleManager"; +import { base64ToBlobUrl } from "../../utils/base64"; +import convert from 'srt-webvtt'; +export async function srtBase64ToVttBlobUrl(base64Srt: string): Promise { + try { + // Step 1: Convert base64 string to a Uint8Array + const binary = atob(base64Srt); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + // Step 2: Create a Blob from the Uint8Array with correct MIME type + const srtBlob = new Blob([bytes], { type: 'application/x-subrip' }); + console.log('srtBlob', srtBlob) + // Step 3: Use convert() with the Blob + const vttBlobUrl: string = await convert(srtBlob); + return vttBlobUrl + } catch (error) { + console.error('Failed to convert SRT to VTT:', error); + return null; + } +} type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down"; @@ -142,6 +163,7 @@ export const VideoPlayer = ({ const [isLoading, setIsLoading] = useState(true); const [showControls, setShowControls] = useState(false) const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false) + const subtitleBtnRef = useRef(null) const { reloadVideo, togglePlay, @@ -219,12 +241,10 @@ const closeSubtitleManager = useCallback(()=> { useVideoPlayerHotKeys(hotkeyHandlers); const updateProgress = useCallback(() => { - console.log('currentTime2') const player = playerRef?.current; if (!player || typeof player?.currentTime !== 'function') return; const currentTime = player.currentTime(); - console.log('currentTime3', currentTime) if (typeof currentTime === 'number' && videoLocation && currentTime > 0.1) { setProgress(videoLocation, currentTime); setLocalProgress(currentTime); @@ -251,7 +271,6 @@ const closeSubtitleManager = useCallback(()=> { (e: React.SyntheticEvent) => { try { const video = e.currentTarget; - console.log('onVolumeChangeHandler') setVolume(video.volume); setIsMuted(video.muted); } catch (error) { @@ -271,7 +290,6 @@ const closeSubtitleManager = useCallback(()=> { }; }, [showControls]); - console.log('isFullscreen', isFullscreen, showControlsFullScreen) const videoStylesVideo = useMemo(() => { return { @@ -318,11 +336,9 @@ const closeSubtitleManager = useCallback(()=> { const enterFullscreen = () => { const ref = containerRef?.current as any; - console.log('refffff', ref) if (!ref) return; if (ref.requestFullscreen && !isFullscreen) { - console.log('requset ') ref.requestFullscreen(); } @@ -399,7 +415,102 @@ useEffect(() => { }; }, []); +const previousSubtitleUrlRef = useRef(null); +useEffect(() => { + return () => { + // Component unmount cleanup + if (previousSubtitleUrlRef.current) { + URL.revokeObjectURL(previousSubtitleUrlRef.current); + previousSubtitleUrlRef.current = null; + } + }; +}, []); + +const onSelectSubtitle = useCallback(async (subtitle: SubtitlePublishedData)=> { + console.log('onSelectSubtitle', subtitle) + const player = playerRef.current; + if (!player || !subtitle.subtitleData || !subtitle.type) return; + + // Cleanup: revoke previous Blob URL + if (previousSubtitleUrlRef.current) { + URL.revokeObjectURL(previousSubtitleUrlRef.current); + previousSubtitleUrlRef.current = null; + } + let blobUrl + if(subtitle?.type === "application/x-subrip"){ + blobUrl = await srtBase64ToVttBlobUrl(subtitle.subtitleData) + } else { + blobUrl = base64ToBlobUrl(subtitle.subtitleData, subtitle.type) + + } + + previousSubtitleUrlRef.current = blobUrl; + +const remoteTracksList = playerRef.current?.remoteTextTracks(); + +if (remoteTracksList) { + const toRemove: TextTrack[] = []; + + // Bypass TS restrictions safely + const list = remoteTracksList as unknown as { length: number; [index: number]: TextTrack }; + + for (let i = 0; i < list.length; i++) { + const track = list[i]; + if (track) toRemove.push(track); + } + + toRemove.forEach((track) => { + playerRef.current?.removeRemoteTextTrack(track); + }); +} + playerRef.current?.addRemoteTextTrack({ + kind: 'subtitles', + src: blobUrl, + srclang: 'en', + label: 'English', + default: true + }, true); + + // Remove all existing remote text tracks +// try { +// const remoteTracks = playerRef.current?.remoteTextTracks()?.tracks_ +// if (remoteTracks && remoteTracks?.length) { +// const toRemove: TextTrack[] = []; +// for (let i = 0; i < remoteTracks.length; i++) { +// const track = remoteTracks[i]; +// toRemove.push(track); +// } +// toRemove.forEach((track) => { +// console.log('removing track') +// playerRef.current?.removeRemoteTextTrack(track); +// }); +// } +// } catch (error) { +// console.log('error2', error) +// } + +await new Promise((res)=> { + setTimeout(() => { + res(null) + }, 1000); +}) +const tracksInfo = playerRef.current?.textTracks(); +console.log('tracksInfo', tracksInfo) +if (!tracksInfo) return; + +const tracks = Array.from({ length: (tracksInfo as any).length }, (_, i) => (tracksInfo as any)[i]); +console.log('tracks', tracks) +for (const track of tracks) { + console.log('track', track) + + if (track.kind === 'subtitles') { + track.mode = 'showing'; // force display + } +} + + +},[]) const handleMouseLeave = useCallback(() => { setShowControls(false); @@ -413,7 +524,6 @@ useEffect(() => { useEffect(() => { if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay) return; - console.log("EFFECT TRIGGERED", { isReady, resourceUrl, startPlay, poster, videoLocactionStringified }); const resource = JSON.parse(videoLocactionStringified) let canceled = false; @@ -437,7 +547,6 @@ useEffect(() => { }, ], }; - console.log('options', options) const ref = videoRef as any; if (!ref.current) return; @@ -448,13 +557,7 @@ useEffect(() => { playerRef.current?.playbackRate(playbackRate) playerRef.current?.volume(volume); - playerRef.current?.addRemoteTextTrack({ - kind: 'subtitles', - src: 'http://127.0.0.1:22393/arbitrary/DOCUMENT/a-test/test-identifier', - srclang: 'en', - label: 'English', - default: true - }, true); + playerRef.current?.play(); }); @@ -472,7 +575,6 @@ useEffect(() => { console.error('useEffect start player', error) } return () => { - console.log('canceled') canceled = true; const player = playerRef.current; @@ -490,12 +592,10 @@ useEffect(() => { useEffect(() => { if(!isPlayerInitialized) return const player = playerRef?.current; - console.log('player rate', player) if (!player) return; const handleRateChange = () => { const newRate = player?.playbackRate(); - console.log('Playback rate changed:', newRate); if(newRate){ setPlaybackRate(newRate); // or any other state/action } @@ -544,11 +644,11 @@ useEffect(() => { {isReady && ( - )} - + ); diff --git a/src/components/VideoPlayer/useVideoPlayerController.tsx b/src/components/VideoPlayer/useVideoPlayerController.tsx index dc6d627..0eca16e 100644 --- a/src/components/VideoPlayer/useVideoPlayerController.tsx +++ b/src/components/VideoPlayer/useVideoPlayerController.tsx @@ -62,13 +62,11 @@ export const useVideoPlayerController = (props: UseVideoControls) => { useEffect(() => { if (videoLocation && isPlayerInitialized) { - console.log('hellohhhh5') try { const ref = playerRef as any; if (!ref.current) return; const savedProgress = getProgress(videoLocation); - console.log('savedProgress', savedProgress) if (typeof savedProgress === "number") { playerRef.current?.currentTime(savedProgress); diff --git a/src/components/VideoPlayer/video-player-constants.ts b/src/components/VideoPlayer/video-player-constants.ts new file mode 100644 index 0000000..5196949 --- /dev/null +++ b/src/components/VideoPlayer/video-player-constants.ts @@ -0,0 +1,4 @@ +import { Service } from "../../types/interfaces/resources"; + +export const ENTITY_SUBTITLE = "ENTITY_SUBTITLE"; +export const SERVICE_SUBTITLE: Service = "FILE" \ No newline at end of file diff --git a/src/hooks/useListData.tsx b/src/hooks/useListData.tsx new file mode 100644 index 0000000..97502ec --- /dev/null +++ b/src/hooks/useListData.tsx @@ -0,0 +1,9 @@ +import React, { useMemo } from "react"; +import { useListStore } from "../state/lists"; +import { useCacheStore } from "../state/cache"; // Assuming you export getResourceCache +import { QortalGetMetadata } from "../types/interfaces/resources"; + +export function useListReturn(listName: string): QortalGetMetadata[] { + const list = useListStore((state) => state.lists[listName]?.items) || []; + return list +} diff --git a/src/hooks/useResources.tsx b/src/hooks/useResources.tsx index a6c70f8..fd25be4 100644 --- a/src/hooks/useResources.tsx +++ b/src/hooks/useResources.tsx @@ -28,6 +28,7 @@ export const useResources = (retryAttempts: number = 2) => { const addTemporaryResource = useCacheStore((s) => s.addTemporaryResource); const markResourceAsDeleted = useCacheStore((s) => s.markResourceAsDeleted); const setSearchParamsForList = useCacheStore((s) => s.setSearchParamsForList); + const addList = useListStore((s) => s.addList); const deleteList = useListStore(state => state.deleteList) const requestControllers = new Map(); @@ -204,9 +205,10 @@ export const useResources = (retryAttempts: number = 2) => { if (cancelRequests) { cancelAllRequests(); } - + console.log('listName', listName) const cacheKey = generateCacheKey(params); const searchCache = getSearchCache(listName, cacheKey); + console.log('searchCache', searchCache) if (searchCache) { const copyParams = {...params} delete copyParams.after @@ -219,9 +221,12 @@ export const useResources = (retryAttempts: number = 2) => { let responseData: QortalMetadata[] = []; let filteredResults: QortalMetadata[] = []; let lastCreated = params.before || undefined; + console.log('lastCreated', lastCreated) const targetLimit = params.limit ?? 20; // Use `params.limit` if provided, else default to 20 + const isUnlimited = params.limit === 0; - while (filteredResults.length < targetLimit) { + while (isUnlimited || filteredResults.length < targetLimit) { + console.log('beforebefore') const response = await qortalRequest({ action: "SEARCH_QDN_RESOURCES", mode: "ALL", @@ -229,27 +234,31 @@ export const useResources = (retryAttempts: number = 2) => { limit: targetLimit - filteredResults.length, // Adjust limit dynamically before: lastCreated, }); - + console.log('responseresponse', response) if (!response || response.length === 0) { break; // No more data available } responseData = response; const validResults = responseData.filter((item) => item.size !== 32); + console.log('validResults', validResults) filteredResults = [...filteredResults, ...validResults]; - if (filteredResults.length >= targetLimit) { + if (filteredResults.length >= targetLimit && !isUnlimited) { filteredResults = filteredResults.slice(0, targetLimit); break; } lastCreated = responseData[responseData.length - 1]?.created; + if (isUnlimited) break; + if (!lastCreated) break; } const copyParams = {...params} delete copyParams.after delete copyParams.before delete copyParams.offset + console.log('listName2', listName, filteredResults) setSearchCache(listName, cacheKey, filteredResults, cancelRequests ? JSON.stringify(copyParams) : null); fetchDataFromResults(filteredResults, returnType); @@ -349,7 +358,6 @@ export const useResources = (retryAttempts: number = 2) => { return true; }, []); - return useMemo(() => ({ fetchResources, @@ -357,8 +365,9 @@ export const useResources = (retryAttempts: number = 2) => { updateNewResources, deleteResource, deleteList, + addList, fetchResourcesResultsOnly - }), [fetchResources, addNewResources, updateNewResources, deleteResource, deleteList, fetchResourcesResultsOnly]); + }), [fetchResources, addNewResources, updateNewResources, deleteResource, deleteList, fetchResourcesResultsOnly, addList]); }; diff --git a/src/index.ts b/src/index.ts index 53409d5..acd86de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export { useModal } from './hooks/useModal'; export { AudioPlayerControls , OnTrackChangeMeta, AudioPlayerProps, AudioPlayerHandle} from './components/AudioPlayer/AudioPlayerControls'; export { useAudioPlayerHotkeys } from './components/AudioPlayer/useAudioPlayerHotkeys'; export { VideoPlayer } from './components/VideoPlayer/VideoPlayer'; +export { useListReturn } from './hooks/useListData'; import './index.css' export { executeEvent, subscribeToEvent, unsubscribeFromEvent } from './utils/events'; export { formatBytes, formatDuration } from './utils/numbers'; diff --git a/src/types/interfaces/resources.ts b/src/types/interfaces/resources.ts index 833f859..a7d394f 100644 --- a/src/types/interfaces/resources.ts +++ b/src/types/interfaces/resources.ts @@ -51,7 +51,7 @@ export type Service = | "VOICE_PRIVATE" | "DOCUMENT_PRIVATE" | "MAIL_PRIVATE" -| "MESSAGE_PRIVATE"; +| "MESSAGE_PRIVATE" | 'AUTO_UPDATE'; export interface QortalMetadata { diff --git a/src/types/qortalRequests/interfaces.ts b/src/types/qortalRequests/interfaces.ts index 3d1a9db..8998c55 100644 --- a/src/types/qortalRequests/interfaces.ts +++ b/src/types/qortalRequests/interfaces.ts @@ -1,3 +1,4 @@ +import { Service } from "../interfaces/resources"; import { Coin, ConfirmationStatus, @@ -8,7 +9,6 @@ import { ForeignCoin, ResourcePointer, ResourceToPublish, - Service, TxType, } from "./types"; diff --git a/src/types/qortalRequests/types.ts b/src/types/qortalRequests/types.ts index dee1a0b..4efb11b 100644 --- a/src/types/qortalRequests/types.ts +++ b/src/types/qortalRequests/types.ts @@ -1,3 +1,5 @@ +import { Service } from "../interfaces/resources" + export type ForeignCoin = | 'BTC' | 'LTC' @@ -31,61 +33,7 @@ export type ForeignCoin = qortalAtAddress: string; } - export type Service = - | 'AUTO_UPDATE' - | 'ARBITRARY_DATA' - | 'QCHAT_ATTACHMENT' - | 'QCHAT_ATTACHMENT_PRIVATE' - | 'ATTACHMENT' - | 'ATTACHMENT_PRIVATE' - | 'FILE' - | 'FILE_PRIVATE' - | 'FILES' - | 'CHAIN_DATA' - | 'WEBSITE' - | 'GIT_REPOSITORY' - | 'IMAGE' - | 'IMAGE_PRIVATE' - | 'THUMBNAIL' - | 'QCHAT_IMAGE' - | 'VIDEO' - | 'VIDEO_PRIVATE' - | 'AUDIO' - | 'AUDIO_PRIVATE' - | 'QCHAT_AUDIO' - | 'QCHAT_VOICE' - | 'VOICE' - | 'VOICE_PRIVATE' - | 'PODCAST' - | 'BLOG' - | 'BLOG_POST' - | 'BLOG_COMMENT' - | 'DOCUMENT' - | 'DOCUMENT_PRIVATE' - | 'LIST' - | 'PLAYLIST' - | 'APP' - | 'METADATA' - | 'JSON' - | 'GIF_REPOSITORY' - | 'STORE' - | 'PRODUCT' - | 'OFFER' - | 'COUPON' - | 'CODE' - | 'PLUGIN' - | 'EXTENSION' - | 'GAME' - | 'ITEM' - | 'NFT' - | 'DATABASE' - | 'SNAPSHOT' - | 'COMMENT' - | 'CHAIN_COMMENT' - | 'MAIL' - | 'MAIL_PRIVATE' - | 'MESSAGE' - | 'MESSAGE_PRIVATE' + export type ResourceToPublish = diff --git a/src/utils/base64.ts b/src/utils/base64.ts index 4e8c46c..a8e63b7 100644 --- a/src/utils/base64.ts +++ b/src/utils/base64.ts @@ -118,4 +118,24 @@ export function base64ToObject(base64: string){ const toObject = uint8ArrayToObject(toUint); return toObject -} \ No newline at end of file +} + +export const base64ToBlobUrl = (base64: string, mimeType = 'text/vtt'): string => { + console.log('base64ToBlobUrl', base64, mimeType) + const cleanedBase64 = base64.length % 4 === 0 ? base64 : base64 + '='.repeat(4 - base64.length % 4); + + try { + const binary = atob(cleanedBase64); + const len = binary.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binary.charCodeAt(i); + } + + const blob = new Blob([bytes], { type: mimeType }); + return URL.createObjectURL(blob); + } catch (err) { + console.error("Failed to decode base64:", err); + return ''; + } +};