From b01232b39fa2a511112d18c2d75ab6a3f694ba72 Mon Sep 17 00:00:00 2001 From: PhilReact Date: Sun, 22 Jun 2025 17:51:40 +0300 Subject: [PATCH] fixed global player for mobile --- src/components/VideoPlayer/MobileControls.tsx | 202 ++++++ .../VideoPlayer/SubtitleManager.tsx | 104 +-- src/components/VideoPlayer/VideoControls.tsx | 21 +- .../VideoPlayer/VideoControlsBar.tsx | 8 +- .../VideoPlayer/VideoPlayer-styles.ts | 8 +- src/components/VideoPlayer/VideoPlayer.tsx | 201 +++++- src/context/GlobalProvider.tsx | 6 +- src/global.ts | 4 +- src/hooks/useGlobalPipPlayer.tsx | 623 ++++++++++-------- src/types/qortalRequests/interfaces.ts | 17 + 10 files changed, 810 insertions(+), 384 deletions(-) create mode 100644 src/components/VideoPlayer/MobileControls.tsx diff --git a/src/components/VideoPlayer/MobileControls.tsx b/src/components/VideoPlayer/MobileControls.tsx new file mode 100644 index 0000000..ddd9236 --- /dev/null +++ b/src/components/VideoPlayer/MobileControls.tsx @@ -0,0 +1,202 @@ +import { alpha, Box, IconButton } from "@mui/material"; +import React from "react"; +import { ProgressSlider } from "./VideoControls"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import PauseIcon from "@mui/icons-material/Pause"; +import SubtitlesIcon from "@mui/icons-material/Subtitles"; +import SlowMotionVideoIcon from "@mui/icons-material/SlowMotionVideo"; +import Fullscreen from "@mui/icons-material/Fullscreen"; +import Forward10Icon from "@mui/icons-material/Forward10"; +import Replay10Icon from "@mui/icons-material/Replay10"; + +interface MobileControlsProps { + showControlsMobile: boolean; + progress: number; + duration: number; + playerRef: any; + setShowControlsMobile: (val: boolean) => void; + isPlaying: boolean; + togglePlay: () => void; + openSubtitleManager: () => void; + openPlaybackMenu: () => void; + toggleFullscreen: () => void; + setProgressRelative: (val: number) => void; +} +export const MobileControls = ({ + showControlsMobile, + togglePlay, + isPlaying, + setShowControlsMobile, + playerRef, + progress, + duration, + openSubtitleManager, + openPlaybackMenu, + toggleFullscreen, + setProgressRelative, +}: MobileControlsProps) => { + return ( + setShowControlsMobile(false)} + sx={{ + position: "absolute", + display: showControlsMobile ? "block" : "none", + top: 0, + bottom: 0, + right: 0, + left: 0, + zIndex: 1, + background: "rgba(0,0,0,.5)", + opacity: 1, + }} + > + + { + e.stopPropagation(); + openSubtitleManager(); + }} + > + + + { + e.stopPropagation(); + openPlaybackMenu(); + }} + > + + + + + { + e.stopPropagation(); + setProgressRelative(-10); + }} + > + + + {isPlaying && ( + { + e.stopPropagation(); + togglePlay(); + }} + > + + + )} + {!isPlaying && ( + { + e.stopPropagation(); + togglePlay(); + }} + > + + + )} + { + e.stopPropagation(); + setProgressRelative(10); + }} + > + + + + + + { + e.stopPropagation(); + toggleFullscreen(); + }} + > + + + + + + + + ); +}; diff --git a/src/components/VideoPlayer/SubtitleManager.tsx b/src/components/VideoPlayer/SubtitleManager.tsx index 0f973ac..d4643d2 100644 --- a/src/components/VideoPlayer/SubtitleManager.tsx +++ b/src/components/VideoPlayer/SubtitleManager.tsx @@ -72,6 +72,8 @@ export interface SubtitleManagerProps { onSelect: (subtitle: SubtitlePublishedData) => void; subtitleBtnRef: any; currentSubTrack: null | string + setDrawerOpenSubtitles: (val: boolean)=> void + isFromDrawer: boolean } export interface Subtitle { language: string | null; @@ -108,6 +110,8 @@ const SubtitleManagerComponent = ({ onSelect, subtitleBtnRef, currentSubTrack, + setDrawerOpenSubtitles, + isFromDrawer = false }: SubtitleManagerProps) => { const [mode, setMode] = useState(1); const [isOpenPublish, setIsOpenPublish] = useState(false); @@ -177,8 +181,11 @@ const SubtitleManagerComponent = ({ } }, [open]) + console.log('isFromDrawer', ) + const handleBlur = (e: React.FocusEvent) => { - if (!e.currentTarget.contains(e.relatedTarget) && !isOpenPublish) { + if (!e.currentTarget.contains(e.relatedTarget) && !isOpenPublish && !isFromDrawer && open) { + console.log('hello close') close(); setIsOpenPublish(false) } @@ -262,13 +269,13 @@ const SubtitleManagerComponent = ({ sx={ { - position: 'absolute', - bottom: 60, - right: 5, + position: isFromDrawer ? 'relative' : 'absolute', + bottom: isFromDrawer ? 'unset' : 60, + right: isFromDrawer ? 'unset' : 5, color: "white", opacity: 0.9, borderRadius: 2, - boxShadow: 5, + boxShadow: isFromDrawer ? 'unset' : 5, p: 1, minWidth: 225, height: 300, @@ -387,30 +394,7 @@ const SubtitleManagerComponent = ({ Load community subs - {/* - {[ - '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 && ( - // - // )} */} - // ); }; + interface PublisherSubtitlesProps { publisherName: string; subtitles: any[]; diff --git a/src/components/VideoPlayer/VideoControls.tsx b/src/components/VideoPlayer/VideoControls.tsx index 2f0c732..febf84d 100644 --- a/src/components/VideoPlayer/VideoControls.tsx +++ b/src/components/VideoPlayer/VideoControls.tsx @@ -71,7 +71,7 @@ export const ProgressSlider = ({ progress, duration, playerRef }: any) => { const [hoverX, setHoverX] = useState(null); const [thumbnailUrl, setThumbnailUrl] = useState(null); const [showDuration, setShowDuration] = useState(0); - const onProgressChange = (_: any, value: number | number[]) => { + const onProgressChange = (e: any, value: number | number[]) => { if (!playerRef.current) return; playerRef.current?.currentTime(value as number); @@ -128,6 +128,11 @@ export const ProgressSlider = ({ progress, duration, playerRef }: any) => { console.log("thumbnailUrl", thumbnailUrl, hoverX); } + const handleClickCapture = (e: React.MouseEvent) => { + + e.stopPropagation(); + + }; return ( { ref={sliderRef} onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave} + onClickCapture={handleClickCapture} value={progress} onChange={onProgressChange} min={0} @@ -439,8 +445,9 @@ interface PlayBackMenuProps { isOpen: boolean onSelect: (speed: number)=> void; playbackRate: number + isFromDrawer: boolean } -export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate}: PlayBackMenuProps)=> { +export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate, isFromDrawer}: PlayBackMenuProps)=> { const theme = useTheme() const ref = useRef(null) @@ -451,7 +458,7 @@ export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate}: PlayBackMe }, [isOpen]) const handleBlur = (e: React.FocusEvent) => { - if (!e.currentTarget.contains(e.relatedTarget)) { + if (!e.currentTarget.contains(e.relatedTarget) && !isFromDrawer) { close(); } }; @@ -466,13 +473,13 @@ export const PlayBackMenu = ({close, onSelect, isOpen, playbackRate}: PlayBackMe sx={ { - position: 'absolute', - bottom: 60, - right: 5, + position: isFromDrawer ? 'relative' : 'absolute', + bottom: isFromDrawer ? 'relative' : 60, + right:isFromDrawer ? 'relative' : 5, color: "white", opacity: 0.9, borderRadius: 2, - boxShadow: 5, + boxShadow: isFromDrawer ? 'relative' : 5, p: 1, minWidth: 225, height: 300, diff --git a/src/components/VideoPlayer/VideoControlsBar.tsx b/src/components/VideoPlayer/VideoControlsBar.tsx index 8a3810c..86ab7cb 100644 --- a/src/components/VideoPlayer/VideoControlsBar.tsx +++ b/src/components/VideoPlayer/VideoControlsBar.tsx @@ -42,9 +42,10 @@ interface VideoControlsBarProps { toggleMute: ()=> void openPlaybackMenu: ()=> void togglePictureInPicture: ()=> void + isVideoPlayerSmall: boolean } -export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager, onSelectPlaybackRate, isMuted, toggleMute, openPlaybackMenu, togglePictureInPicture}: 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, onSelectPlaybackRate, isMuted, toggleMute, openPlaybackMenu, togglePictureInPicture, isVideoPlayerSmall}: VideoControlsBarProps) => { const showMobileControls = isScreenSmall && canPlay; @@ -87,7 +88,8 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in }}> - @@ -117,6 +119,8 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in + )} + ) : null} diff --git a/src/components/VideoPlayer/VideoPlayer-styles.ts b/src/components/VideoPlayer/VideoPlayer-styles.ts index 69c74f2..53d3f3f 100644 --- a/src/components/VideoPlayer/VideoPlayer-styles.ts +++ b/src/components/VideoPlayer/VideoPlayer-styles.ts @@ -1,7 +1,9 @@ -import { styled } from "@mui/system"; +import { styled, Theme } from "@mui/system"; import { Box } from "@mui/material"; -export const VideoContainer = styled(Box)(({ theme }) => ({ +export const VideoContainer = styled(Box, { + shouldForwardProp: (prop) => prop !== 'isVideoPlayerSmall', +})<{ isVideoPlayerSmall?: boolean }>(({ theme, isVideoPlayerSmall }) => ({ position: "relative", display: "flex", flexDirection: "column", @@ -11,7 +13,7 @@ export const VideoContainer = styled(Box)(({ theme }) => ({ height: "100%", margin: 0, padding: 0, - borderRadius: '12px', + borderRadius: isVideoPlayerSmall ? '0px' : '12px', overflow: 'hidden', "&:focus": { outline: "none" }, })); diff --git a/src/components/VideoPlayer/VideoPlayer.tsx b/src/components/VideoPlayer/VideoPlayer.tsx index c1ab055..fb7c1bf 100644 --- a/src/components/VideoPlayer/VideoPlayer.tsx +++ b/src/components/VideoPlayer/VideoPlayer.tsx @@ -33,6 +33,8 @@ import { TimelineActionsComponent } from "./TimelineActionsComponent"; import { PlayBackMenu } from "./VideoControls"; import { useGlobalPlayerStore } from "../../state/pip"; import { LocationContext } from "../../context/GlobalProvider"; +import { alpha, Box, Drawer, List, ListItem } from "@mui/material"; +import { MobileControls } from "./MobileControls"; export async function srtBase64ToVttBlobUrl( base64Srt: string @@ -107,6 +109,8 @@ async function getVideoMimeTypeFromUrl( return null; } } +export const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; + export const VideoPlayer = ({ videoRef, @@ -117,9 +121,19 @@ export const VideoPlayer = ({ onEnded, timelineActions }: VideoPlayerProps) => { - const containerRef = useRef | null>(null); + const containerRef = useRef(null); const [videoObjectFit] = useState("contain"); const [isPlaying, setIsPlaying] = useState(false); + + const [width, setWidth] = useState(0); +console.log('width',width) +useEffect(() => { + const observer = new ResizeObserver(([entry]) => { + setWidth(entry.contentRect.width); + }); + if (containerRef.current) observer.observe(containerRef.current); + return () => observer.disconnect(); +}, []); const { volume, setVolume, setPlaybackRate, playbackRate } = useVideoStore( (state) => ({ volume: state.playbackSettings.volume, @@ -129,6 +143,9 @@ export const VideoPlayer = ({ }) ); const playerRef = useRef(null); + const [drawerOpenSubtitles, setDrawerOpenSubtitles] = useState(false) + const [drawerOpenPlayback, setDrawerOpenPlayback] = useState(false) + const [showControlsMobile, setShowControlsMobile] = useState(false) const [isPlayerInitialized, setIsPlayerInitialized] = useState(false); const [videoCodec, setVideoCodec] = useState(null); const [isMuted, setIsMuted] = useState(false); @@ -144,6 +161,7 @@ export const VideoPlayer = ({ const locationRef = useRef(null) const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false) + const isVideoPlayerSmall = width < 600 const { reloadVideo, togglePlay, @@ -188,18 +206,71 @@ export const VideoPlayer = ({ const { getProgress } = useProgressStore(); - const enterFullscreen = useCallback(() => { - const ref = containerRef?.current as any; - if (!ref) return; +const enterFullscreen = useCallback(async () => { + const ref = containerRef?.current as HTMLElement | null; + if (!ref || document.fullscreenElement) return; - if (ref.requestFullscreen && !isFullscreen) { - ref.requestFullscreen(); + try { + // Wait for fullscreen to activate + if (ref.requestFullscreen) { + await ref.requestFullscreen(); + } else if ((ref as any).webkitRequestFullscreen) { + await (ref as any).webkitRequestFullscreen(); // Safari fallback } - }, []); - const exitFullscreen = useCallback(() => { - document?.exitFullscreen(); - }, [isFullscreen]); + + if ( + typeof screen.orientation !== 'undefined' && + 'lock' in screen.orientation && + typeof screen.orientation.lock === 'function' + ) { + try { + await (screen.orientation as any).lock('landscape'); + } catch (err) { + console.warn('Orientation lock failed:', err); + } + } + await qortalRequest({ + action: 'SCREEN_ORIENTATION', + mode: 'landscape' + }) + } catch (err) { + console.error('Failed to enter fullscreen or lock orientation:', err); + } +}, []); + + + // const exitFullscreen = useCallback(() => { + // document?.exitFullscreen(); + // }, [isFullscreen]); + + const exitFullscreen = useCallback(async () => { + try { + if (document.fullscreenElement) { + await document.exitFullscreen(); + } + + + if ( + typeof screen.orientation !== 'undefined' && + 'lock' in screen.orientation && + typeof screen.orientation.lock === 'function' +) { + try { + // Attempt to reset by locking to 'portrait' or 'any' (if supported) + await screen.orientation.lock('portrait'); // or 'any' if supported + } catch (err) { + console.warn('Orientation lock failed:', err); + } +} + await qortalRequest({ + action: 'SCREEN_ORIENTATION', + mode: 'portrait' + }) + } catch (err) { + console.warn('Error exiting fullscreen or unlocking orientation:', err); + } +}, [isFullscreen]); const toggleFullscreen = useCallback(() => { isFullscreen ? exitFullscreen() : enterFullscreen(); @@ -239,10 +310,14 @@ export const VideoPlayer = ({ const closeSubtitleManager = useCallback(() => { setIsOpenSubtitleManage(false); + setDrawerOpenSubtitles(false) }, []); const openSubtitleManager = useCallback(() => { + if(isVideoPlayerSmall){ + setDrawerOpenSubtitles(true) + } setIsOpenSubtitleManage(true); - }, []); + }, [isVideoPlayerSmall]); const videoLocation = useMemo(() => { if (!qortalVideoResource) return null; @@ -388,6 +463,7 @@ const videoLocationRef = useRef< null | string>(null) const hideTimeout = useRef(null); const resetHideTimer = () => { + if(isTouchDevice) return setShowControls(true); if (hideTimeout.current) clearTimeout(hideTimeout.current); hideTimeout.current = setTimeout(() => { @@ -396,17 +472,24 @@ const videoLocationRef = useRef< null | string>(null) }; const handleMouseMove = () => { + if(isTouchDevice) return resetHideTimer(); }; const closePlaybackMenu = useCallback(()=> { setIsOpenPlaybackmenu(false) +setDrawerOpenPlayback(false) }, []) const openPlaybackMenu = useCallback(()=> { + if(isVideoPlayerSmall){ + setDrawerOpenPlayback(true) + return + } setIsOpenPlaybackmenu(true) - }, []) + }, [isVideoPlayerSmall]) useEffect(() => { + if(isTouchDevice) return resetHideTimer(); // initial show return () => { if (hideTimeout.current) clearTimeout(hideTimeout.current); @@ -717,6 +800,33 @@ savedVideoRef.current = video.current; player.off("ratechange", handleRateChange); }; }, [isPlayerInitialized]); + const hideTimeoutRef = useRef(null); + + + const resetHideTimeout = () => { + setShowControlsMobile(true); + if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current); + hideTimeoutRef.current = setTimeout(() => { + setShowControlsMobile(false); + }, 3000); + }; + + useEffect(() => { + const handleInteraction = () => resetHideTimeout(); + + const container = containerRef.current; + if (!container) return; + + container.addEventListener('touchstart', handleInteraction); + // container.addEventListener('mousemove', handleInteraction); + + return () => { + container.removeEventListener('touchstart', handleInteraction); + // container.removeEventListener('mousemove', handleInteraction); + }; + }, []); + + console.log('showControlsMobile', showControlsMobile) return ( <> @@ -728,6 +838,7 @@ savedVideoRef.current = video.current; onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave} ref={containerRef} + isVideoPlayerSmall={isVideoPlayerSmall} > - - {/* */} + - {isReady && ( + {isReady && !showControlsMobile && ( )} - + )} + + + {!isVideoPlayerSmall && ( + + )} + + setDrawerOpenSubtitles(false)} sx={{ + + }} slotProps={{ + paper: { + sx: { + backgroundColor: alpha("#181818", 0.98), + borderRadius: 2, + width: '90%', + margin: '0 auto', + p: 1, + backgroundImage: 'none', + mb: 1 + }, + } + }}> + + + + + setDrawerOpenPlayback(false)} sx={{ + + }} slotProps={{ + paper: { + sx: { + backgroundColor: alpha("#181818", 0.98), + borderRadius: 2, + width: '90%', + margin: '0 auto', + p: 1, + backgroundImage: 'none', + mb: 1 + }, + } + }}> + + + + ); }; diff --git a/src/context/GlobalProvider.tsx b/src/context/GlobalProvider.tsx index 5f5a50b..d130677 100644 --- a/src/context/GlobalProvider.tsx +++ b/src/context/GlobalProvider.tsx @@ -18,6 +18,7 @@ import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer"; import { Location, NavigateFunction } from "react-router-dom"; import { MultiPublishDialog } from "../components/MultiPublish/MultiPublishDialog"; import { useMultiplePublishStore } from "../state/multiplePublish"; +import { useGlobalPlayerStore } from "../state/pip"; // ✅ Define Global Context Type interface GlobalContextType { @@ -62,7 +63,7 @@ export const GlobalProvider = ({ // ✅ Call hooks and pass in options dynamically const auth = useAuth(config?.auth || {}); const isPublishing = useMultiplePublishStore((s)=> s.isPublishing); - + const videoSrc = useGlobalPlayerStore((s)=> s.videoSrc); const appInfo = useAppInfo(config.appName, config?.publicSalt); const lists = useResources(); const identifierOperations = useIdentifiers( @@ -97,7 +98,8 @@ export const GlobalProvider = ({ - + + {isPublishing && ( )} diff --git a/src/global.ts b/src/global.ts index 2abc946..7383277 100644 --- a/src/global.ts +++ b/src/global.ts @@ -1,4 +1,4 @@ -import { AddForeignServerQortalRequest, AddListItemsQortalRequest, BuyNameQortalRequest, CancelSellNameQortalRequest, CancelTradeSellOrderQortalRequest, CreatePollQortalRequest, CreateTradeBuyOrderQortalRequest, CreateTradeSellOrderQortalRequest, DecryptDataQortalRequest, DecryptDataWithSharingKeyQortalRequest, DecryptQortalGroupDataQortalRequest, DeleteHostedDataQortalRequest, DeleteListItemQortalRequest, EncryptDataQortalRequest, EncryptDataWithSharingKeyQortalRequest, EncryptQortalGroupDataQortalRequest, FetchQdnResourceQortalRequest, GetAccountDataQortalRequest, GetAccountNamesQortalRequest, GetBalanceQortalRequest, GetCrosschainServerInfoQortalRequest, GetDaySummaryQortalRequest, GetForeignFeeQortalRequest, GetHostedDataQortalRequest, GetListItemsQortalRequest, GetNameDataQortalRequest, GetPriceQortalRequest, GetQdnResourceMetadataQortalRequest, GetQdnResourcePropertiesQortalRequest, GetQdnResourceStatusQortalRequest, GetQdnResourceUrlQortalRequest, GetServerConnectionHistoryQortalRequest, GetTxActivitySummaryQortalRequest, GetUserAccountQortalRequest, GetUserWalletInfoQortalRequest, GetUserWalletQortalRequest, GetWalletBalanceQortalRequest, LinkToQdnResourceQortalRequest, ListQdnResourcesQortalRequest, PublishMultipleQdnResourcesQortalRequest, PublishQdnResourceQortalRequest, RegisterNameQortalRequest, RemoveForeignServerQortalRequest, SearchNamesQortalRequest, SearchQdnResourcesQortalRequest, SellNameQortalRequest, SendCoinQortalRequest, SetCurrentForeignServerQortalRequest, UpdateForeignFeeQortalRequest, UpdateNameQortalRequest, VoteOnPollQortalRequest, SendChatMessageQortalRequest, SearchChatMessagesQortalRequest, JoinGroupQortalRequest, AddGroupAdminQortalRequest, UpdateGroupQortalRequest, ListGroupsQortalRequest, CreateGroupQortalRequest, RemoveGroupAdminQortalRequest, BanFromGroupQortalRequest, CancelGroupBanQortalRequest, KickFromGroupQortalRequest, InviteToGroupQortalRequest, CancelGroupInviteQortalRequest, LeaveGroupQortalRequest, DeployAtQortalRequest, GetAtQortalRequest, GetAtDataQortalRequest, ListAtsQortalRequest, FetchBlockQortalRequest, FetchBlockRangeQortalRequest, SearchTransactionsQortalRequest, IsUsingPublicNodeQortalRequest, AdminActionQortalRequest, OpenNewTabQortalRequest, ShowActionsQortalRequest, SignTransactionQortalRequest, CreateAndCopyEmbedLinkQortalRequest, TransferAssetQortalRequest, ShowPdfReaderQortalRequest, SaveFileQortalRequest, GetPrimaryNameQortalRequest, } from "./types/qortalRequests/interfaces" +import { AddForeignServerQortalRequest, AddListItemsQortalRequest, BuyNameQortalRequest, CancelSellNameQortalRequest, CancelTradeSellOrderQortalRequest, CreatePollQortalRequest, CreateTradeBuyOrderQortalRequest, CreateTradeSellOrderQortalRequest, DecryptDataQortalRequest, DecryptDataWithSharingKeyQortalRequest, DecryptQortalGroupDataQortalRequest, DeleteHostedDataQortalRequest, DeleteListItemQortalRequest, EncryptDataQortalRequest, EncryptDataWithSharingKeyQortalRequest, EncryptQortalGroupDataQortalRequest, FetchQdnResourceQortalRequest, GetAccountDataQortalRequest, GetAccountNamesQortalRequest, GetBalanceQortalRequest, GetCrosschainServerInfoQortalRequest, GetDaySummaryQortalRequest, GetForeignFeeQortalRequest, GetHostedDataQortalRequest, GetListItemsQortalRequest, GetNameDataQortalRequest, GetPriceQortalRequest, GetQdnResourceMetadataQortalRequest, GetQdnResourcePropertiesQortalRequest, GetQdnResourceStatusQortalRequest, GetQdnResourceUrlQortalRequest, GetServerConnectionHistoryQortalRequest, GetTxActivitySummaryQortalRequest, GetUserAccountQortalRequest, GetUserWalletInfoQortalRequest, GetUserWalletQortalRequest, GetWalletBalanceQortalRequest, LinkToQdnResourceQortalRequest, ListQdnResourcesQortalRequest, PublishMultipleQdnResourcesQortalRequest, PublishQdnResourceQortalRequest, RegisterNameQortalRequest, RemoveForeignServerQortalRequest, SearchNamesQortalRequest, SearchQdnResourcesQortalRequest, SellNameQortalRequest, SendCoinQortalRequest, SetCurrentForeignServerQortalRequest, UpdateForeignFeeQortalRequest, UpdateNameQortalRequest, VoteOnPollQortalRequest, SendChatMessageQortalRequest, SearchChatMessagesQortalRequest, JoinGroupQortalRequest, AddGroupAdminQortalRequest, UpdateGroupQortalRequest, ListGroupsQortalRequest, CreateGroupQortalRequest, RemoveGroupAdminQortalRequest, BanFromGroupQortalRequest, CancelGroupBanQortalRequest, KickFromGroupQortalRequest, InviteToGroupQortalRequest, CancelGroupInviteQortalRequest, LeaveGroupQortalRequest, DeployAtQortalRequest, GetAtQortalRequest, GetAtDataQortalRequest, ListAtsQortalRequest, FetchBlockQortalRequest, FetchBlockRangeQortalRequest, SearchTransactionsQortalRequest, IsUsingPublicNodeQortalRequest, AdminActionQortalRequest, OpenNewTabQortalRequest, ShowActionsQortalRequest, SignTransactionQortalRequest, CreateAndCopyEmbedLinkQortalRequest, TransferAssetQortalRequest, ShowPdfReaderQortalRequest, SaveFileQortalRequest, GetPrimaryNameQortalRequest, ScreenOrientation, GetNodeStatusQortalRequest, GetNodeInfoQortalRequest, } from "./types/qortalRequests/interfaces" declare global { @@ -84,7 +84,7 @@ declare global { CreateAndCopyEmbedLinkQortalRequest | TransferAssetQortalRequest | ShowPdfReaderQortalRequest | - SaveFileQortalRequest | GetPrimaryNameQortalRequest + SaveFileQortalRequest | GetPrimaryNameQortalRequest | ScreenOrientation | GetNodeStatusQortalRequest | GetNodeInfoQortalRequest; function qortalRequest(options: QortalRequestOptions): Promise diff --git a/src/hooks/useGlobalPipPlayer.tsx b/src/hooks/useGlobalPipPlayer.tsx index 7b86190..aeeba17 100644 --- a/src/hooks/useGlobalPipPlayer.tsx +++ b/src/hooks/useGlobalPipPlayer.tsx @@ -1,46 +1,65 @@ // GlobalVideoPlayer.tsx -import videojs from 'video.js'; -import { useGlobalPlayerStore } from '../state/pip'; -import { useCallback, useContext, useEffect, useRef, useState } from 'react'; -import { Box, IconButton } from '@mui/material'; -import { VideoContainer } from '../components/VideoPlayer/VideoPlayer-styles'; +import videojs from "video.js"; +import { useGlobalPlayerStore } from "../state/pip"; +import { useCallback, useContext, useEffect, useRef, useState } from "react"; +import { Box, IconButton } from "@mui/material"; +import { VideoContainer } from "../components/VideoPlayer/VideoPlayer-styles"; import { Rnd } from "react-rnd"; -import { useProgressStore } from '../state/video'; -import CloseIcon from '@mui/icons-material/Close'; -import PlayArrowIcon from '@mui/icons-material/PlayArrow'; -import PauseIcon from '@mui/icons-material/Pause'; -import OpenInFullIcon from '@mui/icons-material/OpenInFull'; -import { GlobalContext } from '../context/GlobalProvider'; +import { useProgressStore } from "../state/video"; +import CloseIcon from "@mui/icons-material/Close"; +import PlayArrowIcon from "@mui/icons-material/PlayArrow"; +import PauseIcon from "@mui/icons-material/Pause"; +import OpenInFullIcon from "@mui/icons-material/OpenInFull"; +import { GlobalContext } from "../context/GlobalProvider"; +import { isTouchDevice } from "../components/VideoPlayer/VideoPlayer"; export const GlobalPipPlayer = () => { - const { videoSrc, reset, isPlaying, location, type, currentTime, mode, videoId } = useGlobalPlayerStore(); - const [playing , setPlaying] = useState(false) - const [hasStarted, setHasStarted] = useState(false) + const { + videoSrc, + reset, + isPlaying, + location, + type, + currentTime, + mode, + videoId, + } = useGlobalPlayerStore(); + const [playing, setPlaying] = useState(false); + const [hasStarted, setHasStarted] = useState(false); const playerRef = useRef(null); - const context = useContext(GlobalContext) - const navigate = context?.navigate - const videoNode = useRef(null); - const { setProgress } = useProgressStore(); + const containerRef = useRef(null); - const updateProgress = useCallback(() => { - const player = playerRef?.current; - if (!player || typeof player?.currentTime !== "function") return; - - const currentTime = player.currentTime(); - if (typeof currentTime === "number" && videoId && currentTime > 0.1) { - setProgress(videoId, currentTime); - } - }, [videoId]); - - const rndRef = useRef(null) + const context = useContext(GlobalContext); + const navigate = context?.navigate; + const videoNode = useRef(null); + const hideTimeoutRef = useRef(null); + const { setProgress } = useProgressStore(); + + const updateProgress = useCallback(() => { + const player = playerRef?.current; + if (!player || typeof player?.currentTime !== "function") return; + + const currentTime = player.currentTime(); + if (typeof currentTime === "number" && videoId && currentTime > 0.1) { + setProgress(videoId, currentTime); + } + }, [videoId]); + + const rndRef = useRef(null); useEffect(() => { if (!playerRef.current && videoNode.current) { - playerRef.current = videojs(videoNode.current, { autoplay: true, controls: false, - responsive: true, fluid: true }); + playerRef.current = videojs(videoNode.current, { + autoplay: true, + controls: false, + responsive: true, + fluid: true, + }); + playerRef.current?.on("error", () => { + // Optional: display user-friendly message + }); // Resume playback if needed - playerRef.current.on('ready', () => { + playerRef.current.on("ready", () => { if (videoSrc) { - playerRef.current.src(videoSrc); playerRef.current.currentTime(currentTime); if (isPlaying) playerRef.current.play(); @@ -53,181 +72,216 @@ export const GlobalPipPlayer = () => { }; }, []); -useEffect(()=> { -if(!videoSrc){ - setHasStarted(false) -} -}, [videoSrc]) - -useEffect(() => { - const player = playerRef.current; - - if (!player) return; - -if (!videoSrc && player.src) { - // Only pause the player and unload the source without re-triggering playback - player.pause(); - - // Remove the video source safely - const tech = player.tech({ IWillNotUseThisInPlugins: true }); - if (tech && tech.el_) { - tech.setAttribute('src', ''); - setPlaying(false) - setHasStarted(false) - } - - // Optionally clear the poster and currentTime - player.poster(''); - player.currentTime(0); - - return; -} - - -if(videoSrc){ - // Set source and resume if needed - player.src({ src: videoSrc, type: type }); - player.currentTime(currentTime); - - if (isPlaying) { - const playPromise = player.play(); - - - if (playPromise?.catch) { - playPromise.catch((err: any) => { - console.warn('Unable to autoplay:', err); - }); - } - } else { - player.pause(); - } -} -}, [videoSrc, type, isPlaying, currentTime]); - - -// const onDragStart = () => { -// timer = Date.now(); -// isDragging.current = true; -// }; - -// const handleStopDrag = async () => { -// const time = Date.now(); -// if (timer && time - timer < 300) { -// isDragging.current = false; -// } else { -// isDragging.current = true; -// } -// }; -// const onDragStop = () => { -// handleStopDrag(); -// }; - -// const checkIfDrag = useCallback(() => { -// return isDragging.current; -// }, []); -const margin = 50; - - - const [height, setHeight] = useState(300) - const [width, setWidth] = useState(400) - useEffect(() => { - - rndRef.current.updatePosition({ - x: window.innerWidth - (width || 400) - margin, - y: window.innerHeight - (height || 300) - margin, - width: width || 400, - height: height || 300 - }); + if (!videoSrc) { + setHasStarted(false); + } }, [videoSrc]); - const [showControls, setShowControls] = useState(false) + useEffect(() => { + const player = playerRef.current; - const handleMouseMove = () => { - setShowControls(true) + if (!player) return; + + if (!videoSrc && player.src) { + // Only pause the player and unload the source without re-triggering playback + player.pause(); + + + // player.src({ src: '', type: '' }); // ⬅️ this is the safe way to clear it + + + setPlaying(false); + setHasStarted(false); + + // Optionally clear the poster and currentTime + player.poster(""); + player.currentTime(0); + + return; + } + + if (videoSrc) { + // Set source and resume if needed + player.src({ src: videoSrc, type: type }); + player.currentTime(currentTime); + + if (isPlaying) { + const playPromise = player.play(); + + if (playPromise?.catch) { + playPromise.catch((err: any) => { + console.warn("Unable to autoplay:", err); + }); + } + } else { + player.pause(); + } + } + }, [videoSrc, type, isPlaying, currentTime]); + + // const onDragStart = () => { + // timer = Date.now(); + // isDragging.current = true; + // }; + + // const handleStopDrag = async () => { + // const time = Date.now(); + // if (timer && time - timer < 300) { + // isDragging.current = false; + // } else { + // isDragging.current = true; + // } + // }; + // const onDragStop = () => { + // handleStopDrag(); + // }; + + // const checkIfDrag = useCallback(() => { + // return isDragging.current; + // }, []); + const margin = 50; + + const [height, setHeight] = useState(300); + const [width, setWidth] = useState(400); + +useEffect(() => { + if (!videoSrc) return; + + const screenWidth = window.innerWidth; + const screenHeight = window.innerHeight; + const aspectRatio = 0.75; // 300 / 400 = 3:4 + + const maxWidthByScreen = screenWidth * 0.75; + const maxWidthByHeight = (screenHeight * 0.2) / aspectRatio; + + const maxWidth = Math.min(maxWidthByScreen, maxWidthByHeight); + const maxHeight = maxWidth * aspectRatio; + + setWidth(maxWidth); + setHeight(maxHeight); + + rndRef.current.updatePosition({ + x: screenWidth - maxWidth - margin, + y: screenHeight - maxHeight - margin, + width: maxWidth, + height: maxHeight, + }); +}, [videoSrc]); + + const [showControls, setShowControls] = useState(false); + + const handleMouseMove = () => { + if (isTouchDevice) return; + setShowControls(true); }; const handleMouseLeave = () => { + if (isTouchDevice) return; setShowControls(false); }; const startPlay = useCallback(() => { - try { - - const player = playerRef.current; - if (!player) return; + try { + const player = playerRef.current; + if (!player) return; - try { - player.play(); + player.play(); } catch (err) { - console.warn('Play failed:', err); + console.warn("Play failed:", err); } - - } catch (error) { - console.error('togglePlay', error) - } + } catch (error) { + console.error("togglePlay", error); + } }, []); const stopPlay = useCallback(() => { const player = playerRef.current; - if (!player) return; - - try { - player.pause(); - } catch (err) { - console.warn('Play failed:', err); - } - + if (!player) return; + try { + player.pause(); + } catch (err) { + console.warn("Play failed:", err); + } }, []); const onPlayHandlerStart = useCallback(() => { - setPlaying(true) - setHasStarted(true) + setPlaying(true); + setHasStarted(true); }, [setPlaying]); - const onPlayHandlerStop = useCallback(() => { - setPlaying(false) + const onPlayHandlerStop = useCallback(() => { + setPlaying(false); }, [setPlaying]); - + + const resetHideTimeout = () => { + setShowControls(true); + if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current); + hideTimeoutRef.current = setTimeout(() => { + setShowControls(false); + }, 3000); + }; + + useEffect(() => { + const container = containerRef.current; + if (!videoSrc || !container) return; + + const handleInteraction = () => { + console.log("Touchstart detected!"); + resetHideTimeout(); + }; + + container.addEventListener("touchstart", handleInteraction, { + passive: true, + capture: true, + }); + + return () => { + container.removeEventListener("touchstart", handleInteraction, { + capture: true, + }); + }; + }, [videoSrc]); + + console.log("showControls", showControls); + return ( { - setWidth(ref.offsetWidth); - setHeight(ref.offsetHeight); - }} - -// default={{ -// x: 500, -// y: 500, -// width: 350, -// height: "auto", -// }} - // eslint-disable-next-line @typescript-eslint/no-empty-function - onDrag={() => {}} -> - {/*
{ + setWidth(ref.offsetWidth); + setHeight(ref.offsetHeight); + }} + // default={{ + // x: 500, + // y: 500, + // width: 350, + // height: "auto", + // }} + // eslint-disable-next-line @typescript-eslint/no-empty-function + onDrag={() => {}} + > + {/*
*/} - - {/* {backgroundColor: showControls ? 'rgba(0,0,0,.5)' : 'unset'} */} - {showControls && ( - - - - {location && ( - { - if(navigate){ - navigate(location) - } - }}> - )} - {playing && ( - - )} - {!playing && ( - - )} - - - - - + + {/* {backgroundColor: showControls ? 'rgba(0,0,0,.5)' : 'unset'} */} + {showControls && ( + + + + + + {location && ( + { + e.stopPropagation() + if (navigate) { + navigate(location); + } + }} + onTouchStart={(e) => { + e.stopPropagation() + if (navigate) { + navigate(location); + } + }} + > + + )} + {playing && ( + + + + )} + {!playing && ( + + + + )} + + + + )} - + - {/*
*/} + {/*
*/}
); }; diff --git a/src/types/qortalRequests/interfaces.ts b/src/types/qortalRequests/interfaces.ts index 8998c55..1143292 100644 --- a/src/types/qortalRequests/interfaces.ts +++ b/src/types/qortalRequests/interfaces.ts @@ -615,12 +615,29 @@ export interface ShowActionsQortalRequest extends BaseRequest { action: 'SHOW_ACTIONS' } +export interface ScreenOrientation extends BaseRequest { + action: 'SCREEN_ORIENTATION', + mode: | "portrait" + | "landscape" + | "portrait-primary" + | "portrait-secondary" + | "landscape-primary" + | "landscape-secondary" | "unlock"; +} + export interface SignTransactionQortalRequest extends BaseRequest { action: 'SIGN_TRANSACTION' unsignedBytes: string process?: boolean } +export interface GetNodeStatusQortalRequest extends BaseRequest { + action: 'GET_NODE_STATUS' +} +export interface GetNodeInfoQortalRequest extends BaseRequest { + action: 'GET_NODE_INFO' +} + export interface CreateAndCopyEmbedLinkQortalRequest extends BaseRequest { action: 'CREATE_AND_COPY_EMBED_LINK' type: string