remove unused code

This commit is contained in:
2025-06-26 23:57:25 +03:00
parent f7fd191399
commit 10cca25113
30 changed files with 954 additions and 2834 deletions

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
build
dist

View File

@@ -1,10 +1,23 @@
{
"printWidth": 80,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"embeddedLanguageFormatting": "auto",
"endOfLine": "lf",
"experimentalTernaries": false,
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxBracketSameLine": false,
"arrowParens": "avoid",
"jsxSingleQuote": false,
"printWidth": 80,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleAttributePerLine": false,
"singleQuote": true,
"tabWidth": 2,
"semi": true
"trailingComma": "es5",
"useTabs": false,
"vueIndentScriptAndStyle": false
}

195
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"@reduxjs/toolkit": "^2.5.0",
"compressorjs": "^1.2.1",
"dompurify": "^3.2.3",
"i18n": "^0.15.1",
"jotai": "^2.12.4",
"localforage": "^1.10.0",
"mediainfo.js": "^0.3.5",
@@ -27,6 +28,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-dropzone": "^14.3.5",
"react-i18next": "^15.5.3",
"react-idle-timer": "^5.7.2",
"react-intersection-observer": "^9.14.0",
"react-quill-new": "^3.3.3",
@@ -295,9 +297,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
"integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -1178,6 +1180,50 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@messageformat/core": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.4.0.tgz",
"integrity": "sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw==",
"license": "MIT",
"dependencies": {
"@messageformat/date-skeleton": "^1.0.0",
"@messageformat/number-skeleton": "^1.0.0",
"@messageformat/parser": "^5.1.0",
"@messageformat/runtime": "^3.0.1",
"make-plural": "^7.0.0",
"safe-identifier": "^0.4.1"
}
},
"node_modules/@messageformat/date-skeleton": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz",
"integrity": "sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==",
"license": "MIT"
},
"node_modules/@messageformat/number-skeleton": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz",
"integrity": "sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==",
"license": "MIT"
},
"node_modules/@messageformat/parser": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.1.tgz",
"integrity": "sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==",
"license": "MIT",
"dependencies": {
"moo": "^0.5.1"
}
},
"node_modules/@messageformat/runtime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@messageformat/runtime/-/runtime-3.0.1.tgz",
"integrity": "sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==",
"license": "MIT",
"dependencies": {
"make-plural": "^7.0.0"
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.1.0.tgz",
@@ -3170,6 +3216,15 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"node_modules/fast-printf": {
"version": "1.6.10",
"resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.10.tgz",
"integrity": "sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=10.0"
}
},
"node_modules/fastq": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz",
@@ -3467,6 +3522,67 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"license": "MIT",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/i18n": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/i18n/-/i18n-0.15.1.tgz",
"integrity": "sha512-yue187t8MqUPMHdKjiZGrX+L+xcUsDClGO0Cz4loaKUOK9WrGw5pgan4bv130utOwX7fHE9w2iUeHFalVQWkXA==",
"license": "MIT",
"dependencies": {
"@messageformat/core": "^3.0.0",
"debug": "^4.3.3",
"fast-printf": "^1.6.9",
"make-plural": "^7.0.0",
"math-interval-parser": "^2.0.1",
"mustache": "^4.2.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/mashpie"
}
},
"node_modules/i18next": {
"version": "25.2.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.2.1.tgz",
"integrity": "sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.1"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -3896,6 +4012,21 @@
"yallist": "^3.0.2"
}
},
"node_modules/make-plural": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.4.0.tgz",
"integrity": "sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==",
"license": "Unicode-DFS-2016"
},
"node_modules/math-interval-parser": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/math-interval-parser/-/math-interval-parser-2.0.1.tgz",
"integrity": "sha512-VmlAmb0UJwlvMyx8iPhXUDnVW1F9IrGEd9CIOmv+XL8AErCUUuozoDMrgImvnYt2A+53qVX/tPW6YJurMKYsvA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -3964,11 +4095,26 @@
"node": "*"
}
},
"node_modules/moo": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz",
"integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==",
"license": "BSD-3-Clause"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
"license": "MIT",
"bin": {
"mustache": "bin/mustache"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -4472,6 +4618,32 @@
"react-dom": ">=16"
}
},
"node_modules/react-i18next": {
"version": "15.5.3",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.3.tgz",
"integrity": "sha512-ypYmOKOnjqPEJZO4m1BI0kS8kWqkBNsKYyhVUfij0gvjy9xJNoG/VcGkxq5dRlVwzmrmY1BQMAmpbbUBLwC4Kw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.6",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0",
"typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-idle-timer": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-5.7.2.tgz",
@@ -4801,6 +4973,12 @@
"queue-microtask": "^1.2.2"
}
},
"node_modules/safe-identifier": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz",
"integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==",
"license": "ISC"
},
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
@@ -5067,7 +5245,7 @@
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
"dev": true,
"devOptional": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5263,6 +5441,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -19,6 +19,7 @@
"@reduxjs/toolkit": "^2.5.0",
"compressorjs": "^1.2.1",
"dompurify": "^3.2.3",
"i18n": "^0.15.1",
"jotai": "^2.12.4",
"localforage": "^1.10.0",
"mediainfo.js": "^0.3.5",
@@ -29,6 +30,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-dropzone": "^14.3.5",
"react-i18next": "^15.5.3",
"react-idle-timer": "^5.7.2",
"react-intersection-observer": "^9.14.0",
"react-quill-new": "^3.3.3",
@@ -49,10 +51,10 @@
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"globals": "^15.15.0",
"prettier": "^3.4.2",
"typescript": "^5.7.2",
"vite": "^6.3.5",
"globals": "^15.15.0",
"vite-plugin-static-copy": "^3.0.0"
}
}

View File

@@ -1,32 +1,21 @@
import { CssBaseline } from "@mui/material";
import { ThemeProvider } from "@mui/material/styles";
import { useEffect, useState } from "react";
import { Provider, useSelector } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { subscriptionListFilter } from "./App-Functions.ts";
import Notification from "./components/common/Notification/Notification";
import { useIframe } from "./hooks/useIframe.tsx";
import { IndividualProfile } from "./pages/ContentPages/IndividualProfile/IndividualProfile";
import { PlaylistContent } from "./pages/ContentPages/PlaylistContent/PlaylistContent";
import { VideoContent } from "./pages/ContentPages/VideoContent/VideoContent";
import { Home } from "./pages/Home/Home";
import { setFilteredSubscriptions } from "./state/features/videoSlice.ts";
import { store, persistor, RootState } from "./state/store";
import { darkTheme, lightTheme } from "./styles/theme";
import DownloadWrapper from "./wrappers/DownloadWrapper";
import GlobalWrapper from "./wrappers/GlobalWrapper";
import { ScrollWrapper } from "./wrappers/ScrollWrapper.tsx";
import { QappCoreWrapper } from "./QappCoreWrapper.tsx";
import { Routes } from "./Routes.tsx";
import { CssBaseline } from '@mui/material';
import { ThemeProvider } from '@mui/material/styles';
import { useEffect } from 'react';
import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import { subscriptionListFilter } from './App-Functions.ts';
import Notification from './components/common/Notification/Notification';
import { setFilteredSubscriptions } from './state/features/videoSlice.ts';
import { store, persistor } from './state/store';
import { darkTheme } from './styles/theme';
import DownloadWrapper from './wrappers/DownloadWrapper';
import { Routes } from './Routes.tsx';
function App() {
// const themeColor = window._qdnTheme
// useIframe();
useEffect(() => {
subscriptionListFilter(false).then(filteredList => {
subscriptionListFilter(false).then((filteredList) => {
store.dispatch(setFilteredSubscriptions(filteredList));
});
}, []);
@@ -35,13 +24,10 @@ function App() {
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<CssBaseline />
<Notification />
<DownloadWrapper>
<Routes />
<Routes />
</DownloadWrapper>
</ThemeProvider>
</PersistGate>

View File

@@ -1,30 +0,0 @@
import { GlobalProvider } from 'qapp-core'
import { useSelector } from 'react-redux';
import { RootState } from './state/store';
import { useLocation, useNavigate } from 'react-router-dom';
export const QappCoreWrapper = ({children}) => {
const { user } = useSelector((state: RootState) => state.auth);
const navigate = useNavigate();
const location = useLocation();
return (
<GlobalProvider
config={{
auth: {
authenticateOnMount: false,
userAccountInfo: {
address: user?.address,
publicKey: user?.publicKey
}
},
publicSalt: "usVbeM9YpjGCbLrTcc78YJS0ap1AxDkHAOMZrp3+wDY=",
appName: "Q-Tube",
}}
navigate={navigate}
location={location}
>
{children}
</GlobalProvider>
)
}

View File

@@ -1,136 +0,0 @@
import { Box, CircularProgress, Typography } from "@mui/material";
import { minDuration } from "../../../../constants/Misc.ts";
import { setVideoPlaying } from "../../../../state/features/globalSlice.ts";
import { useDispatch } from "react-redux";
import { PlayArrow } from "@mui/icons-material";
import { formatTime } from "../../../../utils/numberFunctions.ts";
import { useVideoContext } from "./VideoContext.ts";
export const LoadingVideo = () => {
const {
isLoading,
resourceStatus,
src,
startPlay,
canPlay,
from,
togglePlay,
duration,
} = useVideoContext();
const getDownloadProgress = (current: number, total: number) => {
const progress = (current / total) * 100;
return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%";
};
const dispatch = useDispatch();
return (
<>
{isLoading.value && (
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={resourceStatus?.status === "READY" ? "55px " : 0}
display="flex"
justifyContent="center"
alignItems="center"
zIndex={25}
bgcolor="rgba(0, 0, 0, 0.6)"
sx={{
display: "flex",
flexDirection: "column",
gap: "10px",
height: "100%",
}}
>
<CircularProgress color="secondary" />
{resourceStatus && (
<Typography
variant="subtitle2"
component="div"
sx={{
color: "white",
fontSize: "15px",
textAlign: "center",
}}
>
{resourceStatus?.status === "NOT_PUBLISHED" && (
<>Video file was not published. Please inform the publisher!</>
)}
{resourceStatus?.status === "REFETCHING" ? (
<>
<>
{getDownloadProgress(
resourceStatus?.localChunkCount,
resourceStatus?.totalChunkCount
)}
</>
<> Refetching in 25 seconds</>
</>
) : resourceStatus?.status === "DOWNLOADED" ? (
<>Download Completed: building video...</>
) : resourceStatus?.status !== "READY" ? (
<>
{getDownloadProgress(
resourceStatus?.localChunkCount,
resourceStatus?.totalChunkCount
)}
</>
) : (
<>Fetching video...</>
)}
</Typography>
)}
</Box>
)}
{((!src && !isLoading.value) || (!startPlay.value && !canPlay.value)) && (
<>
{duration > minDuration && (
<Box
position="absolute"
right={0}
bottom={0}
bgcolor="#202020"
zIndex={999}
>
<Typography color="white">{formatTime(duration)}</Typography>
</Box>
)}
<Box
position="absolute"
top={0}
left={0}
right={0}
bottom={0}
display="flex"
justifyContent="center"
alignItems="center"
zIndex={500}
bgcolor="rgba(0, 0, 0, 0.6)"
onClick={() => {
if (from === "create") return;
dispatch(setVideoPlaying(null));
togglePlay();
}}
sx={{
cursor: "pointer",
}}
>
<PlayArrow
sx={{
width: "50px",
height: "50px",
color: "white",
}}
/>
</Box>
</>
)}
</>
);
};

View File

@@ -1,71 +0,0 @@
import { MoreVert as MoreIcon } from "@mui/icons-material";
import { Box, IconButton, Menu, MenuItem } from "@mui/material";
import { useVideoContext } from "./VideoContext.ts";
import {
FullscreenButton,
ObjectFitButton,
PictureInPictureButton,
PlaybackRate,
PlayButton,
ProgressSlider,
ReloadButton,
VideoTime,
VolumeControl,
} from "./VideoControls.tsx";
export const MobileControlsBar = () => {
const { handleMenuOpen, handleMenuClose, anchorEl, controlsHeight } =
useVideoContext();
const controlGroupSX = {
display: "flex",
gap: "5px",
alignItems: "center",
height: controlsHeight,
};
return (
<>
<Box sx={controlGroupSX}>
<PlayButton />
<ReloadButton />
<ProgressSlider />
<VideoTime />
</Box>
<Box sx={controlGroupSX}>
<PlaybackRate />
<FullscreenButton />
<IconButton
edge="end"
color="inherit"
aria-label="menu"
onClick={handleMenuOpen}
sx={{ paddingLeft: "0px", marginRight: "0px" }}
>
<MoreIcon />
</IconButton>
</Box>
<Menu
id="simple-menu"
anchorEl={anchorEl.value}
keepMounted
open={Boolean(anchorEl.value)}
onClose={handleMenuClose}
PaperProps={{
style: {
width: "250px",
},
}}
>
<MenuItem>
<ObjectFitButton />
</MenuItem>
<MenuItem>
<PictureInPictureButton />
</MenuItem>
</Menu>
</>
);
};

View File

@@ -1,28 +0,0 @@
import { createContext, useContext } from "react";
import { useVideoPlayerState } from "../VideoPlayer-State.ts";
import { VideoPlayerProps } from "../VideoPlayer.tsx";
import { useVideoControlsState } from "./VideoControls-State.ts";
export type VideoContextType = VideoPlayerProps &
ReturnType<typeof useVideoPlayerState> &
ReturnType<typeof useVideoControlsState>;
export const VideoContext = createContext<VideoContextType | undefined>(
undefined
);
export const useContextData = (props, ref): VideoContextType => {
const videoState = useVideoPlayerState(props, ref);
const controlState = useVideoControlsState(props, videoState);
return {
...props,
...videoState,
...controlState,
};
};
export const useVideoContext = () => {
const context = useContext<VideoContextType>(VideoContext);
if (!context) throw new Error("VideoContext is NULL");
return context;
};

View File

@@ -1,378 +0,0 @@
import { useSignal } from "@preact/signals-react";
import { useSignalEffect, useSignals } from "@preact/signals-react/runtime";
import { useEffect } from "react";
import ReactDOM from "react-dom";
import { useDispatch, useSelector } from "react-redux";
// import { Key } from "ts-key-enum";
import { setVideoPlaying } from "../../../../state/features/globalSlice.ts";
import { RootState } from "../../../../state/store.ts";
import { useVideoPlayerState } from "../VideoPlayer-State.ts";
import { VideoPlayerProps } from "../VideoPlayer.tsx";
export const useVideoControlsState = (
props: VideoPlayerProps,
videoPlayerState: ReturnType<typeof useVideoPlayerState>
) => {
const {
src,
getSrc,
resourceStatus,
videoRef,
playbackRate,
playing,
isLoading,
startPlay,
volume,
isMuted,
mutedVolume,
progress,
videoObjectFit,
canPlay,
containerRef,
alwaysShowControls,
} = videoPlayerState;
const { identifier, autoPlay } = props;
const showControlsFullScreen = useSignal(true);
const dispatch = useDispatch();
const persistSelector = useSelector((root: RootState) => root.persist);
const videoPlaying = useSelector(
(state: RootState) => state.global.videoPlaying
);
const controlsHeight = "42px";
const minSpeed = 0.25;
const maxSpeed = 4.0;
const speedChange = 0.25;
const updatePlaybackRate = (newSpeed: number) => {
if (videoRef.current) {
if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed;
videoRef.current.playbackRate = newSpeed;
playbackRate.value = newSpeed;
}
};
const increaseSpeed = (wrapOverflow = true) => {
const changedSpeed = playbackRate.value + speedChange;
const newSpeed = wrapOverflow
? changedSpeed
: Math.min(changedSpeed, maxSpeed);
updatePlaybackRate(newSpeed);
};
const decreaseSpeed = () => {
updatePlaybackRate(playbackRate.value - speedChange);
};
const isFullscreen = useSignal(false);
const enterFullscreen = () => {
if (!containerRef.current) return;
if (containerRef.current.requestFullscreen && !isFullscreen.value) {
containerRef.current.requestFullscreen();
}
};
const exitFullscreen = () => {
if (isFullscreen.value) document.exitFullscreen();
};
const toggleFullscreen = () => {
isFullscreen.value ? exitFullscreen() : enterFullscreen();
};
const togglePictureInPicture = async () => {
if (!videoRef.current) return;
if (document.pictureInPictureElement === videoRef.current) {
await document.exitPictureInPicture();
} else {
await videoRef.current.requestPictureInPicture();
}
};
useEffect(() => {
const handleFullscreenChange = () => {
isFullscreen.value = !!document.fullscreenElement;
};
document.addEventListener("fullscreenchange", handleFullscreenChange);
return () => {
document.removeEventListener("fullscreenchange", handleFullscreenChange);
};
}, []);
const reloadVideo = async () => {
if (!videoRef.current || !src) return;
const currentTime = videoRef.current.currentTime;
videoRef.current.src = src;
videoRef.current.load();
videoRef.current.currentTime = currentTime;
playing.value = true;
togglePlay();
};
const firstPlay = useSignal(true);
const handleCanPlay = () => {
if (firstPlay.value) setPlaying(true); // makes the video play when fully loaded
firstPlay.value = false;
isLoading.value = false;
updatePlaybackRate(persistSelector.playbackRate);
};
const setPlaying = async (setPlay: boolean) => {
setPlay ? await videoRef.current.play() : videoRef.current.pause();
playing.value = setPlay;
};
const togglePlay = async () => {
if (!videoRef.current) return;
if (!src || resourceStatus?.status !== "READY") {
const el = document.getElementById("videoWrapper");
if (el) {
el?.parentElement?.removeChild(el);
}
ReactDOM.flushSync(() => {
isLoading.value = true;
});
getSrc();
}
startPlay.value = true;
setPlaying(!playing.value);
};
const onVolumeChange = (_: any, value: number | number[]) => {
if (!videoRef.current) return;
const newVolume = value as number;
isMuted.value = false;
mutedVolume.value = newVolume;
volume.value = newVolume;
};
useEffect(() => {
if (autoPlay && identifier) togglePlay();
}, [autoPlay, identifier]);
const mute = () => {
isMuted.value = true;
mutedVolume.value = volume.value;
volume.value = 0;
};
const unMute = () => {
isMuted.value = false;
volume.value = mutedVolume.value;
};
const toggleMute = () => {
isMuted.value ? unMute() : mute();
};
const changeVolume = (volumeChange: number) => {
if (videoRef.current) {
const minVolume = 0;
const maxVolume = 1;
let newVolume = volumeChange + volume.value;
newVolume = Math.max(newVolume, minVolume);
newVolume = Math.min(newVolume, maxVolume);
newVolume = +newVolume.toFixed(2);
isMuted.value = false;
mutedVolume.value = newVolume;
volume.value = newVolume;
}
};
const setProgressRelative = (secondsChange: number) => {
if (videoRef.current) {
const currentTime = videoRef.current?.currentTime;
const minTime = 0;
const maxTime = videoRef.current?.duration || 100;
let newTime = currentTime + secondsChange;
newTime = Math.max(newTime, minTime);
newTime = Math.min(newTime, maxTime);
videoRef.current.currentTime = newTime;
progress.value = newTime;
}
};
const setProgressAbsolute = (videoPercent: number) => {
if (videoRef.current) {
videoPercent = Math.min(videoPercent, 100);
videoPercent = Math.max(videoPercent, 0);
const finalTime = (videoRef.current?.duration * videoPercent) / 100;
videoRef.current.currentTime = finalTime;
progress.value = finalTime;
}
};
const setObjectFit = (value: "contain" | "fill") => {
videoObjectFit.value = value;
};
const toggleObjectFit = () => {
videoObjectFit.value =
videoObjectFit.value === "contain" ? "fill" : "contain";
};
const toggleAlwaysShowControls = () => {
alwaysShowControls.value = !alwaysShowControls.value;
};
const keyboardShortcuts = (
e: KeyboardEvent | React.KeyboardEvent<HTMLDivElement>
) => {
e.preventDefault();
// console.log("hotkey is: ", '"' + e.key + '"');
// switch (e.key) {
// case "o":
// toggleObjectFit();
// break;
// case "c":
// toggleAlwaysShowControls();
// break;
// case Key.Add:
// increaseSpeed(false);
// break;
// case "+":
// increaseSpeed(false);
// break;
// case ">":
// increaseSpeed(false);
// break;
// case Key.Subtract:
// decreaseSpeed();
// break;
// case "-":
// decreaseSpeed();
// break;
// case "<":
// decreaseSpeed();
// break;
// case Key.ArrowLeft:
// {
// if (e.shiftKey) setProgressRelative(-300);
// else if (e.ctrlKey) setProgressRelative(-60);
// else if (e.altKey) setProgressRelative(-10);
// else setProgressRelative(-5);
// }
// break;
// case Key.ArrowRight:
// {
// if (e.shiftKey) setProgressRelative(300);
// else if (e.ctrlKey) setProgressRelative(60);
// else if (e.altKey) setProgressRelative(10);
// else setProgressRelative(5);
// }
// break;
// case Key.ArrowDown:
// changeVolume(-0.05);
// break;
// case Key.ArrowUp:
// changeVolume(0.05);
// break;
// case " ":
// togglePlay();
// break;
// case "m":
// toggleMute();
// break;
// case "f":
// toggleFullscreen();
// break;
// case Key.Escape:
// exitFullscreen();
// break;
// case "r":
// reloadVideo();
// break;
// case "p":
// togglePictureInPicture();
// break;
// case "0":
// setProgressAbsolute(0);
// break;
// case "1":
// setProgressAbsolute(10);
// break;
// case "2":
// setProgressAbsolute(20);
// break;
// case "3":
// setProgressAbsolute(30);
// break;
// case "4":
// setProgressAbsolute(40);
// break;
// case "5":
// setProgressAbsolute(50);
// break;
// case "6":
// setProgressAbsolute(60);
// break;
// case "7":
// setProgressAbsolute(70);
// break;
// case "8":
// setProgressAbsolute(80);
// break;
// case "9":
// setProgressAbsolute(90);
// break;
// }
};
useEffect(() => {
videoRef.current.volume = volume.value;
if (
videoPlaying &&
videoPlaying.id === identifier &&
src &&
videoRef?.current
) {
handleCanPlay();
videoRef.current.currentTime = videoPlaying.currentTime;
videoRef.current.play().then(() => {
playing.value = true;
startPlay.value = true;
dispatch(setVideoPlaying(null));
});
}
}, [videoPlaying, identifier, src]);
useSignalEffect(() => {
console.log("canPlay is: ", canPlay.value); // makes the function execute when canPlay changes
});
return {
reloadVideo,
togglePlay,
onVolumeChange,
increaseSpeed,
togglePictureInPicture,
toggleFullscreen,
keyboardShortcuts,
handleCanPlay,
toggleMute,
showControlsFullScreen,
setPlaying,
isFullscreen,
toggleObjectFit,
setObjectFit,
controlsHeight,
};
};

View File

@@ -1,254 +0,0 @@
import { Box, IconButton, Slider, Typography } from "@mui/material";
import { fontSizeExSmall, fontSizeSmall } from "../../../../constants/Misc.ts";
import { CustomFontTooltip } from "../../../../utils/CustomFontTooltip.tsx";
import { formatTime } from "../../../../utils/numberFunctions.ts";
import { useVideoContext } from "./VideoContext.ts";
import AspectRatioIcon from "@mui/icons-material/AspectRatio";
import {
Fullscreen,
Pause,
PictureInPicture,
PlayArrow,
Refresh,
VolumeOff,
VolumeUp,
} from "@mui/icons-material";
import { useSignalEffect } from "@preact/signals-react";
const buttonPaddingBig = "6px";
const buttonPaddingSmall = "4px";
export const PlayButton = () => {
const { togglePlay, playing, isScreenSmall } = useVideoContext();
return (
<CustomFontTooltip title="Pause/Play (Spacebar)" placement="bottom" arrow>
<IconButton
sx={{
color: "white",
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
}}
onClick={() => togglePlay()}
>
{playing.value ? <Pause /> : <PlayArrow />}
</IconButton>
</CustomFontTooltip>
);
};
export const ReloadButton = () => {
const { reloadVideo, isScreenSmall } = useVideoContext();
return (
<CustomFontTooltip title="Reload Video (R)" placement="bottom" arrow>
<IconButton
sx={{
color: "white",
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
}}
onClick={reloadVideo}
>
<Refresh />
</IconButton>
</CustomFontTooltip>
);
};
export const ProgressSlider = () => {
const { progress, onProgressChange, videoRef } = useVideoContext();
return (
<Slider
value={progress.value}
onChange={onProgressChange}
min={0}
max={videoRef.current?.duration || 100}
sx={{
position: "absolute",
bottom: "42px",
color: "#00abff",
padding: "0px",
// prevents the slider from jumping up 20px in certain mobile conditions
"@media (pointer: coarse)": { padding: "0px" },
"& .MuiSlider-thumb": {
backgroundColor: "#fff",
width: "16px",
height: "16px",
},
"& .MuiSlider-thumb::after": { width: "20px", height: "20px" },
"& .MuiSlider-rail": { opacity: 0.5, height: "6px" },
"& .MuiSlider-track": { height: "6px", border: "0px" },
}}
/>
);
};
export const VideoTime = () => {
const { videoRef, progress, isScreenSmall } = useVideoContext();
return (
<CustomFontTooltip
title="Seek video in 10% increments (0-9)"
placement="bottom"
arrow
>
<Typography
sx={{
fontSize: isScreenSmall ? fontSizeExSmall : fontSizeSmall,
color: "white",
visibility: !videoRef.current?.duration ? "hidden" : "visible",
whiteSpace: "nowrap",
}}
>
{videoRef.current?.duration ? formatTime(progress.value) : ""}
{" / "}
{videoRef.current?.duration
? formatTime(videoRef.current?.duration)
: ""}
</Typography>
</CustomFontTooltip>
);
};
const VolumeButton = () => {
const { isMuted, toggleMute } = useVideoContext();
return (
<CustomFontTooltip
title="Toggle Mute (M), Raise (UP), Lower (DOWN)"
placement="bottom"
arrow
>
<IconButton
sx={{
color: "white",
}}
onClick={toggleMute}
>
{isMuted.value ? <VolumeOff /> : <VolumeUp />}
</IconButton>
</CustomFontTooltip>
);
};
const VolumeSlider = ({ width }: { width: string }) => {
const { volume, onVolumeChange } = useVideoContext();
let color = "";
if (volume.value <= 0.5) color = "green";
else if (volume.value <= 0.75) color = "yellow";
else color = "red";
return (
<Slider
value={volume.value}
onChange={onVolumeChange}
min={0}
max={1}
step={0.01}
sx={{
width,
marginRight: "10px",
color,
"& .MuiSlider-thumb": {
backgroundColor: "#fff",
width: "16px",
height: "16px",
},
"& .MuiSlider-thumb::after": { width: "16px", height: "16px" },
"& .MuiSlider-rail": { opacity: 0.5, height: "6px" },
"& .MuiSlider-track": { height: "6px", border: "0px" },
}}
/>
);
};
export const VolumeControl = ({ sliderWidth }: { sliderWidth: string }) => {
return (
<Box
sx={{ display: "flex", gap: "5px", alignItems: "center", width: "100%" }}
>
<VolumeButton />
<VolumeSlider width={sliderWidth} />
</Box>
);
};
export const PlaybackRate = () => {
const { playbackRate, increaseSpeed, isScreenSmall } = useVideoContext();
return (
<CustomFontTooltip
title="Video Speed. Increase (+ or >), Decrease (- or <)"
placement="bottom"
arrow
>
<IconButton
sx={{
color: "white",
fontSize: fontSizeSmall,
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
}}
onClick={() => increaseSpeed()}
>
{playbackRate}x
</IconButton>
</CustomFontTooltip>
);
};
export const ObjectFitButton = () => {
const { toggleObjectFit, isScreenSmall } = useVideoContext();
return (
<CustomFontTooltip title="Toggle Aspect Ratio (O)" placement="bottom" arrow>
<IconButton
sx={{
color: "white",
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
}}
onClick={() => toggleObjectFit()}
>
<AspectRatioIcon />
</IconButton>
</CustomFontTooltip>
);
};
export const PictureInPictureButton = () => {
const { isFullscreen, toggleRef, togglePictureInPicture, isScreenSmall } =
useVideoContext();
return (
<>
{!isFullscreen.value && (
<CustomFontTooltip
title="Picture in Picture (P)"
placement="bottom"
arrow
>
<IconButton
sx={{
color: "white",
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
}}
ref={toggleRef}
onClick={togglePictureInPicture}
>
<PictureInPicture />
</IconButton>
</CustomFontTooltip>
)}
</>
);
};
export const FullscreenButton = () => {
const { toggleFullscreen, isScreenSmall } = useVideoContext();
return (
<CustomFontTooltip title="Toggle Fullscreen (F)" placement="bottom" arrow>
<IconButton
sx={{
color: "white",
padding: isScreenSmall ? buttonPaddingSmall : buttonPaddingBig,
}}
onClick={() => toggleFullscreen()}
>
<Fullscreen />
</IconButton>
</CustomFontTooltip>
);
};

View File

@@ -1,60 +0,0 @@
import { Box } from "@mui/material";
import { ControlsContainer } from "../VideoPlayer-styles.ts";
import { MobileControlsBar } from "./MobileControlsBar.tsx";
import { useVideoContext } from "./VideoContext.ts";
import {
FullscreenButton,
ObjectFitButton,
PictureInPictureButton,
PlaybackRate,
PlayButton,
ProgressSlider,
ReloadButton,
VideoTime,
VolumeControl,
} from "./VideoControls.tsx";
export const VideoControlsBar = () => {
const { canPlay, isScreenSmall, controlsHeight } = useVideoContext();
const showMobileControls = isScreenSmall && canPlay.value;
const controlGroupSX = {
display: "flex",
gap: "5px",
alignItems: "center",
height: controlsHeight,
};
return (
<ControlsContainer
style={{
padding: "0px",
height: controlsHeight,
}}
>
{showMobileControls ? (
<MobileControlsBar />
) : canPlay.value ? (
<>
<Box sx={controlGroupSX}>
<PlayButton />
<ReloadButton />
<ProgressSlider />
<VolumeControl sliderWidth={"100px"} />
<VideoTime />
</Box>
<Box sx={controlGroupSX}>
<PlaybackRate />
<ObjectFitButton />
<PictureInPictureButton />
<FullscreenButton />
</Box>
</>
) : null}
</ControlsContainer>
);
};

View File

@@ -1,324 +0,0 @@
import { useMediaQuery } from "@mui/material";
import {
useSignal,
useSignalEffect,
useSignals,
} from "@preact/signals-react/runtime";
import React, {
useContext,
useEffect,
useImperativeHandle,
useMemo,
useRef,
} from "react";
import { useDispatch, useSelector } from "react-redux";
import { smallVideoSize } from "../../../constants/Misc.ts";
import { setVideoPlaying } from "../../../state/features/globalSlice.ts";
import {
setAlwaysShowControls,
setIsMuted,
setMutedVolumeSetting,
setReduxPlaybackRate,
setStretchVideoSetting,
setVolumeSetting,
StretchVideoType,
} from "../../../state/features/persistSlice.ts";
import { RootState } from "../../../state/store.ts";
import { MyContext } from "../../../wrappers/DownloadWrapper.tsx";
import { VideoPlayerProps } from "./VideoPlayer.tsx";
export const useVideoPlayerState = (props: VideoPlayerProps, ref: any) => {
useSignals();
const persistSelector = useSelector((state: RootState) => state.persist);
const playing = useSignal(false);
const progress = useSignal(0);
const isLoading = useSignal(false);
const canPlay = useSignal(false);
const startPlay = useSignal(false);
const isMuted = useSignal(persistSelector.isMuted);
const volume = useSignal(persistSelector.volume);
const mutedVolume = useSignal(persistSelector.mutedVolume);
const playbackRate = useSignal(persistSelector.playbackRate);
const videoObjectFit = useSignal<StretchVideoType>(
persistSelector.stretchVideoSetting
);
const alwaysShowControls = useSignal<boolean>(
persistSelector.alwaysShowControls
);
useSignalEffect(() => {
dispatch(setIsMuted(isMuted.value));
});
useSignalEffect(() => {
dispatch(setVolumeSetting(volume.value));
if (videoRef.current) videoRef.current.volume = volume.value;
});
useSignalEffect(() => {
dispatch(setMutedVolumeSetting(mutedVolume.value));
});
useSignalEffect(() => {
if (videoRef.current) videoRef.current.playbackRate = playbackRate.value;
dispatch(setReduxPlaybackRate(playbackRate.value));
});
useSignalEffect(() => {
dispatch(setStretchVideoSetting(videoObjectFit.value));
});
useSignalEffect(() => {
dispatch(setAlwaysShowControls(alwaysShowControls.value));
});
const anchorEl = useSignal(null);
const {
name,
identifier,
service,
user = "",
jsonId = "",
nextVideo,
onEnd,
} = props;
const dispatch = useDispatch();
const videoRef = useRef<HTMLVideoElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const { downloads } = useSelector((state: RootState) => state.global);
const reDownload = useRef<boolean>(false);
const reDownloadNextVid = useRef<boolean>(false);
const isFetchingProperties = useRef<boolean>(false);
const status = useRef<null | string>(null);
const download = useMemo(() => {
if (!downloads || !identifier) return {};
const findDownload = downloads[identifier];
if (!findDownload) return {};
return findDownload;
}, [downloads, identifier]);
const src = useMemo(() => {
return download?.url || null;
}, [download?.url]);
const resourceStatus = useMemo(() => {
return download?.status || {};
}, [download]);
useImperativeHandle(ref, () => ({
getVideoRef: () => {
return videoRef;
},
getContainerRef: () => {
return containerRef;
},
}));
useEffect(() => {
reDownload.current = false;
reDownloadNextVid.current = false;
isLoading.value = false;
canPlay.value = false;
progress.value = 0;
playing.value = false;
startPlay.value = false;
isFetchingProperties.current = false;
status.current = null;
}, [identifier]);
const refetch = React.useCallback(async () => {
if (!name || !identifier || !service || isFetchingProperties.current)
return;
try {
isFetchingProperties.current = true;
await qortalRequest({
action: "GET_QDN_RESOURCE_PROPERTIES",
name,
service,
identifier,
});
} catch (error) {
console.log(error);
} finally {
isFetchingProperties.current = false;
}
}, [identifier, name, service]);
const toggleRef = useRef<any>(null);
const { downloadVideo } = useContext(MyContext);
const onProgressChange = async (_: any, value: number | number[]) => {
if (!videoRef.current) return;
videoRef.current.currentTime = value as number;
progress.value = value as number;
};
const handleEnded = () => {
playing.value = false;
if (onEnd) {
onEnd();
}
};
const updateProgress = () => {
if (!videoRef.current) return;
progress.value = videoRef.current.currentTime;
};
const videoCanPlayIfDownloaded = () => {
if (resourceStatus?.status === "READY") {
canPlay.value = true;
}
};
videoCanPlayIfDownloaded();
const getSrc = React.useCallback(async () => {
if (!name || !identifier || !service || !jsonId || !user) return;
try {
downloadVideo({
name,
service,
identifier,
properties: {
jsonId,
user,
},
});
} catch (error) {
console.error(error);
}
}, [identifier, name, service, jsonId, user]);
useEffect(() => {
const videoElement = videoRef.current;
const handleLeavePictureInPicture = async (event: any) => {
const target = event?.target;
if (target) {
target.pause();
if (playing.value) playing.value = false;
}
};
if (videoElement) {
videoElement.addEventListener(
"leavepictureinpicture",
handleLeavePictureInPicture
);
}
return () => {
if (videoElement) {
videoElement.removeEventListener(
"leavepictureinpicture",
handleLeavePictureInPicture
);
}
};
}, []);
useEffect(() => {
const videoElement = videoRef.current;
const minimizeVideo = async () => {
if (!videoElement) return;
dispatch(setVideoPlaying(videoElement));
};
return () => {
if (videoElement) {
if (videoElement && !videoElement.paused && !videoElement.ended) {
minimizeVideo();
}
}
};
}, []);
const refetchInInterval = () => {
try {
const interval = setInterval(() => {
if (status?.current === "DOWNLOADED") refetch();
if (status?.current === "READY") clearInterval(interval);
}, 7500);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
if (resourceStatus?.status) {
status.current = resourceStatus?.status;
}
if (
resourceStatus?.status === "DOWNLOADED" &&
reDownload?.current === false
) {
refetchInInterval();
reDownload.current = true;
}
}, [getSrc, resourceStatus]);
useEffect(() => {
if (resourceStatus?.status) {
status.current = resourceStatus?.status;
}
if (
resourceStatus?.status === "READY" &&
reDownloadNextVid?.current === false
) {
if (nextVideo) {
downloadVideo({
name: nextVideo?.name,
service: nextVideo?.service,
identifier: nextVideo?.identifier,
properties: {
jsonId: nextVideo?.jsonId,
user,
},
});
}
reDownloadNextVid.current = true;
}
}, [getSrc, resourceStatus]);
const handleMenuOpen = (event: any) => {
anchorEl.value = event.currentTarget;
};
const handleMenuClose = () => {
anchorEl.value = null;
};
const isScreenSmall = !useMediaQuery(smallVideoSize);
return {
containerRef,
resourceStatus,
videoRef,
src,
getSrc,
updateProgress,
handleEnded,
onProgressChange,
handleMenuOpen,
handleMenuClose,
toggleRef,
playing,
isMuted,
progress,
isLoading,
canPlay,
startPlay,
volume,
mutedVolume,
playbackRate,
anchorEl,
videoObjectFit,
isScreenSmall,
alwaysShowControls,
};
};

View File

@@ -1,12 +1,8 @@
import CSS from "csstype";
import { forwardRef, useCallback, useMemo, useRef } from "react";
import useIdleTimeout from "../../../hooks/useIdleTimeout.ts";
import { LoadingVideo } from "./Components/LoadingVideo.tsx";
import { useContextData, VideoContext } from "./Components/VideoContext.ts";
import { VideoControlsBar } from "./Components/VideoControlsBar.tsx";
import { VideoContainer, VideoElement } from "./VideoPlayer-styles.ts";
import {VideoPlayer as QappVideoPlayer, Service, TimelineAction} from 'qapp-core'
import { Box } from "@mui/material";
import CSS from 'csstype';
import { useRef } from 'react';
import { VideoPlayer as QappVideoPlayer, Service } from 'qapp-core';
import { Box } from '@mui/material';
export interface VideoStyles {
videoContainer?: CSS.Properties;
video?: CSS.Properties;
@@ -17,7 +13,7 @@ export interface VideoPlayerProps {
poster?: string;
name?: string;
identifier?: string;
service?: string;
service?: Service;
autoplay?: boolean;
from?: string | null;
videoStyles?: VideoStyles;
@@ -30,123 +26,26 @@ export interface VideoPlayerProps {
duration?: number;
}
export type videoRefType = {
getContainerRef: () => React.MutableRefObject<HTMLDivElement>;
getVideoRef: () => React.MutableRefObject<HTMLVideoElement>;
};
export const VideoPlayer = forwardRef<videoRefType, VideoPlayerProps>(
(props: VideoPlayerProps, ref) => {
const videoRef = useRef(null)
// const contextData = useContextData(props, ref);
export const VideoPlayer = ({ ...props }: VideoPlayerProps) => {
const videoRef = useRef(null);
// const {
// keyboardShortcuts,
// from,
// videoStyles,
// containerRef,
// resourceStatus,
// src,
// togglePlay,
// identifier,
// videoRef,
// poster,
// updateProgress,
// autoplay,
// handleEnded,
// handleCanPlay,
// startPlay,
// videoObjectFit,
// showControlsFullScreen,
// isFullscreen,
// alwaysShowControls,
// } = contextData;
// const showControls =
// !isFullscreen.value ||
// (isFullscreen.value && showControlsFullScreen.value) ||
// alwaysShowControls.value;
// const idleTime = 5000; // Time in milliseconds
// useIdleTimeout({
// onIdle: () => (showControlsFullScreen.value = false),
// onActive: () => (showControlsFullScreen.value = true),
// idleTime,
// });
const timelineActions: TimelineAction[] = useMemo(()=> {
return [{
label: "Skip intro",
type: 'SEEK',
duration: 10,
time: 60,
seekToTime: 3600,
placement: 'BOTTOM-RIGHT'
}, {
label: "Skip intro",
type: 'SEEK',
duration: 10,
time: 3610,
seekToTime: 7200,
placement: 'TOP-RIGHT'
}]
}, [])
return (
// <VideoContext.Provider value={contextData}>
<Box sx={{
return (
<Box
sx={{
width: '100%',
height: '70vh',
background: 'black'
}}>
<QappVideoPlayer timelineActions={timelineActions} poster={props.poster} videoRef={videoRef} qortalVideoResource={{name: props.name, service: props.service as Service, identifier: props.identifier}} />
</Box>
// <VideoContainer
// tabIndex={0}
// onKeyDown={keyboardShortcuts}
// style={{
// padding: from === "create" ? "8px" : 0,
// cursor:
// !showControlsFullScreen.value && isFullscreen.value
// ? "none"
// : "auto",
// ...videoStyles?.videoContainer,
// }}
// onMouseEnter={e => {
// showControlsFullScreen.value = true;
// }}
// onMouseLeave={e => {
// showControlsFullScreen.value = false;
// }}
// ref={containerRef}
// >
// <LoadingVideo />
// <VideoElement
// id={identifier}
// ref={videoRef}
// src={
// resourceStatus?.status === "READY" && startPlay.value ? src : null
// }
// poster={startPlay.value ? "" : poster}
// onTimeUpdate={updateProgress}
// autoPlay={autoplay}
// onClick={() => togglePlay()}
// onEnded={handleEnded}
// // onLoadedMetadata={handleLoadedMetadata}
// onCanPlay={handleCanPlay}
// preload="metadata"
// style={{
// ...videoStyles?.video,
// objectFit: videoObjectFit.value,
// backgroundColor: "#000000",
// height:
// isFullscreen.value && showControls
// ? "calc(100vh - 40px)"
// : "100%",
// }}
// />
// {showControls && <VideoControlsBar />}
// </VideoContainer>
// </VideoContext.Provider>
);
}
);
background: 'black',
}}
>
<QappVideoPlayer
poster={props.poster}
videoRef={videoRef}
qortalVideoResource={{
name: props.name,
service: props.service as Service,
identifier: props.identifier,
}}
/>
</Box>
);
};

View File

@@ -1,663 +0,0 @@
import {
Fullscreen,
MoreVert as MoreIcon,
Pause,
PictureInPicture,
PlayArrow,
Refresh,
VolumeOff,
VolumeUp,
} from "@mui/icons-material";
import CloseIcon from "@mui/icons-material/Close";
import {
Box,
IconButton,
Menu,
MenuItem,
Slider,
Typography,
useTheme,
} from "@mui/material";
import { styled } from "@mui/system";
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
// import { Key } from "ts-key-enum";
import { setVideoPlaying } from "../../../state/features/globalSlice.ts";
import { RootState } from "../../../state/store.ts";
import { formatTime } from "../../../utils/numberFunctions.ts";
import { MyContext } from "../../../wrappers/DownloadWrapper.tsx";
const VideoContainer = styled(Box)`
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
margin: 0px;
padding: 0px;
`;
const VideoElement = styled("video")`
width: 100%;
height: auto;
max-height: calc(100vh - 150px);
background: rgb(33, 33, 33);
`;
const ControlsContainer = styled(Box)`
position: absolute;
display: flex;
align-items: center;
justify-content: space-between;
bottom: 0;
left: 0;
right: 0;
padding: 8px;
background-color: rgba(0, 0, 0, 0.6);
`;
interface VideoPlayerProps {
src?: string;
poster?: string;
name?: string;
identifier?: string;
service?: string;
autoplay?: boolean;
from?: string | null;
customStyle?: any;
user?: string;
jsonId?: string;
element?: null | any;
checkIfDrag?: () => boolean;
}
export const VideoPlayerGlobal: React.FC<VideoPlayerProps> = ({
poster,
name,
identifier,
service,
autoplay = true,
from = null,
customStyle = {},
user = "",
jsonId = "",
element,
checkIfDrag,
}) => {
const theme = useTheme();
const videoRef = useRef<HTMLVideoElement | null>(null);
const [playing, setPlaying] = useState(false);
const [volume, setVolume] = useState(1);
const [mutedVolume, setMutedVolume] = useState(1);
const [isMuted, setIsMuted] = useState(false);
const [progress, setProgress] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [canPlay, setCanPlay] = useState(false);
const [startPlay, setStartPlay] = useState(false);
const [isMobileView, setIsMobileView] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1);
const [anchorEl, setAnchorEl] = useState(null);
const dispatch = useDispatch();
const reDownload = useRef<boolean>(false);
const { downloads } = useSelector((state: RootState) => state.global);
const download = useMemo(() => {
if (!downloads || !identifier) return {};
const findDownload = downloads[identifier];
if (!findDownload) return {};
return findDownload;
}, [downloads, identifier]);
const resourceStatus = useMemo(() => {
return download?.status || {};
}, [download]);
const minSpeed = 0.25;
const maxSpeed = 4.0;
const speedChange = 0.25;
const updatePlaybackRate = (newSpeed: number) => {
if (videoRef.current) {
if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed;
videoRef.current.playbackRate = newSpeed;
setPlaybackRate(newSpeed);
}
};
const increaseSpeed = (wrapOverflow = true) => {
const changedSpeed = playbackRate + speedChange;
const newSpeed = wrapOverflow
? changedSpeed
: Math.min(changedSpeed, maxSpeed);
if (videoRef.current) {
updatePlaybackRate(newSpeed);
}
};
const decreaseSpeed = () => {
if (videoRef.current) {
updatePlaybackRate(playbackRate - speedChange);
}
};
const toggleRef = useRef<any>(null);
const { downloadVideo } = useContext(MyContext);
const togglePlay = async () => {
if (checkIfDrag && checkIfDrag()) return;
if (!videoRef.current) return;
if (playing) {
videoRef.current.pause();
} else {
await videoRef.current.play();
}
setPlaying(prev => !prev);
};
const onVolumeChange = (_: any, value: number | number[]) => {
if (!videoRef.current) return;
videoRef.current.volume = value as number;
setVolume(value as number);
setIsMuted(false);
};
const onProgressChange = async (_: any, value: number | number[]) => {
if (!videoRef.current) return;
videoRef.current.currentTime = value as number;
setProgress(value as number);
if (!playing) {
await videoRef.current.play();
setPlaying(true);
}
};
const handleEnded = () => {
setPlaying(false);
};
const updateProgress = () => {
if (!videoRef.current) return;
setProgress(videoRef.current.currentTime);
};
const [isFullscreen, setIsFullscreen] = useState(false);
const enterFullscreen = () => {
if (!videoRef.current) return;
if (videoRef.current.requestFullscreen) {
videoRef.current.requestFullscreen();
}
};
const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
}
};
const toggleFullscreen = () => {
isFullscreen ? exitFullscreen() : enterFullscreen();
};
const togglePictureInPicture = async () => {
if (!videoRef.current) return;
if (document.pictureInPictureElement === videoRef.current) {
await document.exitPictureInPicture();
} else {
await videoRef.current.requestPictureInPicture();
}
};
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener("fullscreenchange", handleFullscreenChange);
return () => {
document.removeEventListener("fullscreenchange", handleFullscreenChange);
};
}, []);
const handleCanPlay = () => {
setIsLoading(false);
setCanPlay(true);
};
useEffect(() => {
const videoElement = videoRef.current;
const handleLeavePictureInPicture = async (event: any) => {
const target = event?.target;
if (target) {
target.pause();
if (setPlaying) {
setPlaying(false);
}
}
};
if (videoElement) {
videoElement.addEventListener(
"leavepictureinpicture",
handleLeavePictureInPicture
);
}
return () => {
if (videoElement) {
videoElement.removeEventListener(
"leavepictureinpicture",
handleLeavePictureInPicture
);
}
};
}, []);
const reloadVideo = async () => {
if (!videoRef.current) return;
const src = videoRef.current.src;
const currentTime = videoRef.current.currentTime;
videoRef.current.src = src;
videoRef.current.load();
videoRef.current.currentTime = currentTime;
if (playing) {
await videoRef.current.play();
}
};
const handleMenuOpen = (event: any) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
useEffect(() => {
const videoWidth = videoRef?.current?.offsetWidth;
if (videoWidth && videoWidth <= 600) {
setIsMobileView(true);
}
}, [canPlay]);
const getDownloadProgress = (current: number, total: number) => {
const progress = (current / total) * 100;
return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%";
};
const mute = () => {
setIsMuted(true);
setMutedVolume(volume);
setVolume(0);
if (videoRef.current) videoRef.current.volume = 0;
};
const unMute = () => {
setIsMuted(false);
setVolume(mutedVolume);
if (videoRef.current) videoRef.current.volume = mutedVolume;
};
const toggleMute = () => {
isMuted ? unMute() : mute();
};
const changeVolume = (volumeChange: number) => {
if (videoRef.current) {
const minVolume = 0;
const maxVolume = 1;
let newVolume = volumeChange + volume;
newVolume = Math.max(newVolume, minVolume);
newVolume = Math.min(newVolume, maxVolume);
setIsMuted(false);
setMutedVolume(newVolume);
videoRef.current.volume = newVolume;
setVolume(newVolume);
}
};
const setProgressRelative = (secondsChange: number) => {
if (videoRef.current) {
const currentTime = videoRef.current?.currentTime;
const minTime = 0;
const maxTime = videoRef.current?.duration || 100;
let newTime = currentTime + secondsChange;
newTime = Math.max(newTime, minTime);
newTime = Math.min(newTime, maxTime);
videoRef.current.currentTime = newTime;
setProgress(newTime);
}
};
const setProgressAbsolute = (videoPercent: number) => {
if (videoRef.current) {
videoPercent = Math.min(videoPercent, 100);
videoPercent = Math.max(videoPercent, 0);
const finalTime = (videoRef.current?.duration * videoPercent) / 100;
videoRef.current.currentTime = finalTime;
setProgress(finalTime);
}
};
const keyboardShortcutsDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
e.preventDefault();
// switch (e.key) {
// case Key.Add:
// increaseSpeed(false);
// break;
// case "+":
// increaseSpeed(false);
// break;
// case ">":
// increaseSpeed(false);
// break;
// case Key.Subtract:
// decreaseSpeed();
// break;
// case "-":
// decreaseSpeed();
// break;
// case "<":
// decreaseSpeed();
// break;
// case Key.ArrowLeft:
// {
// if (e.shiftKey) setProgressRelative(-300);
// else if (e.ctrlKey) setProgressRelative(-60);
// else if (e.altKey) setProgressRelative(-10);
// else setProgressRelative(-5);
// }
// break;
// case Key.ArrowRight:
// {
// if (e.shiftKey) setProgressRelative(300);
// else if (e.ctrlKey) setProgressRelative(60);
// else if (e.altKey) setProgressRelative(10);
// else setProgressRelative(5);
// }
// break;
// case Key.ArrowDown:
// changeVolume(-0.05);
// break;
// case Key.ArrowUp:
// changeVolume(0.05);
// break;
// }
};
const keyboardShortcutsUp = (e: React.KeyboardEvent<HTMLDivElement>) => {
e.preventDefault();
// switch (e.key) {
// case " ":
// togglePlay();
// break;
// case "m":
// toggleMute();
// break;
// case "f":
// enterFullscreen();
// break;
// case Key.Escape:
// exitFullscreen();
// break;
// case "0":
// setProgressAbsolute(0);
// break;
// case "1":
// setProgressAbsolute(10);
// break;
// case "2":
// setProgressAbsolute(20);
// break;
// case "3":
// setProgressAbsolute(30);
// break;
// case "4":
// setProgressAbsolute(40);
// break;
// case "5":
// setProgressAbsolute(50);
// break;
// case "6":
// setProgressAbsolute(60);
// break;
// case "7":
// setProgressAbsolute(70);
// break;
// case "8":
// setProgressAbsolute(80);
// break;
// case "9":
// setProgressAbsolute(90);
// break;
// }
};
// useEffect(() => {
// if (element) {
// const oldElement = document.getElementById("videoPlayer");
// if (oldElement && oldElement?.parentNode) {
// oldElement?.parentNode.replaceChild(element, oldElement);
// videoRef.current = element;
// setPlaying(true);
// setCanPlay(true);
// setStartPlay(true);
// //videoRef?.current?.addEventListener("click", () => {});
// videoRef?.current?.addEventListener("timeupdate", updateProgress);
// videoRef?.current?.addEventListener("ended", handleEnded);
// }
// }
// }, [element]);
return (
<VideoContainer
tabIndex={0}
onKeyUp={keyboardShortcutsUp}
onKeyDown={keyboardShortcutsDown}
style={{
padding: from === "create" ? "8px" : 0,
zIndex: 1000,
backgroundColor: theme.palette.background.default,
}}
>
<div className="closePlayer">
<CloseIcon
onClick={() => {
dispatch(setVideoPlaying(null));
}}
sx={{
cursor: "pointer",
backgroundColor: "rgba(0,0,0,.5)",
}}
></CloseIcon>
</div>
{/*<div onClick={togglePlay}>*/}
{/* <VideoElement id="videoPlayer" />*/}
{/*</div>*/}
<ControlsContainer
style={{
bottom: from === "create" ? "15px" : 0,
}}
>
{isMobileView && canPlay ? (
<>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
}}
onClick={togglePlay}
>
{playing ? <Pause /> : <PlayArrow />}
</IconButton>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
marginLeft: "15px",
}}
onClick={reloadVideo}
>
<Refresh />
</IconButton>
<Slider
value={progress}
onChange={onProgressChange}
min={0}
max={videoRef.current?.duration || 100}
sx={{ flexGrow: 1, mx: 2 }}
/>
<IconButton
edge="end"
color="inherit"
aria-label="menu"
onClick={handleMenuOpen}
>
<MoreIcon />
</IconButton>
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleMenuClose}
PaperProps={{
style: {
width: "250px",
},
}}
>
<MenuItem>
<VolumeUp />
<Slider
value={volume}
onChange={onVolumeChange}
min={0}
max={1}
step={0.01}
/>
</MenuItem>
<MenuItem onClick={() => increaseSpeed()}>
<Typography
sx={{
color: "rgba(255, 255, 255, 0.7)",
fontSize: "14px",
}}
>
Speed: {playbackRate}x
</Typography>
</MenuItem>
<MenuItem onClick={togglePictureInPicture}>
<PictureInPicture />
</MenuItem>
<MenuItem onClick={toggleFullscreen}>
<Fullscreen />
</MenuItem>
</Menu>
</>
) : canPlay ? (
<>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
}}
onClick={togglePlay}
>
{playing ? <Pause /> : <PlayArrow />}
</IconButton>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
marginLeft: "15px",
}}
onClick={reloadVideo}
>
<Refresh />
</IconButton>
<Slider
value={progress}
onChange={onProgressChange}
min={0}
max={videoRef.current?.duration || 100}
sx={{ flexGrow: 1, mx: 2 }}
/>
<Typography
sx={{
fontSize: "14px",
marginRight: "5px",
color: "rgba(255, 255, 255, 0.7)",
visibility:
!videoRef.current?.duration || !progress
? "hidden"
: "visible",
}}
>
{progress && videoRef.current?.duration && formatTime(progress)}/
{progress &&
videoRef.current?.duration &&
formatTime(videoRef.current?.duration)}
</Typography>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
marginRight: "10px",
}}
onClick={toggleMute}
>
{isMuted ? <VolumeOff /> : <VolumeUp />}
</IconButton>
<Slider
value={volume}
onChange={onVolumeChange}
min={0}
max={1}
step={0.01}
sx={{
maxWidth: "100px",
}}
/>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
fontSize: "14px",
marginLeft: "5px",
}}
onClick={e => increaseSpeed()}
>
Speed: {playbackRate}x
</IconButton>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
marginLeft: "15px",
}}
ref={toggleRef}
onClick={togglePictureInPicture}
>
<PictureInPicture />
</IconButton>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
}}
onClick={toggleFullscreen}
>
<Fullscreen />
</IconButton>
</>
) : null}
</ControlsContainer>
</VideoContainer>
);
};

View File

@@ -1,27 +1,75 @@
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useEffect } from 'react';
import { To, useNavigate } from 'react-router-dom';
import { useSetAtom } from 'jotai';
import { useTranslation } from 'react-i18next';
import { supportedLanguages } from '../i18n/i18n';
import { EnumTheme, themeAtom } from '../state/global/theme';
type Language = 'de' | 'en' | 'es' | 'fr' | 'it' | 'ja' | 'ru' | 'zh';
type Theme = 'dark' | 'light';
interface CustomWindow extends Window {
_qdnTheme: Theme;
_qdnLang: Language;
}
const customWindow = window as unknown as CustomWindow;
export const useIframe = () => {
const setTheme = useSetAtom(themeAtom);
const { i18n } = useTranslation();
const navigate = useNavigate();
useEffect(() => {
function handleNavigation(event) {
if (event.data?.action === "NAVIGATE_TO_PATH" && event.data.path) {
console.log("Navigating to path within React app:", event.data.path);
const themeColorDefault = customWindow?._qdnTheme;
if (themeColorDefault === 'dark') {
setTheme(EnumTheme.DARK);
} else if (themeColorDefault === 'light') {
setTheme(EnumTheme.LIGHT);
}
const languageDefault = customWindow?._qdnLang;
if (supportedLanguages?.includes(languageDefault)) {
i18n.changeLanguage(languageDefault);
}
function handleNavigation(event: {
data: {
action: string;
path: To;
theme: Theme;
language: Language;
};
}) {
if (event.data?.action === 'NAVIGATE_TO_PATH' && event.data.path) {
navigate(event.data.path); // Navigate directly to the specified path
// Send a response back to the parent window after navigation is handled
window.parent.postMessage(
{ action: "NAVIGATION_SUCCESS", path: event.data.path },
"*"
{ action: 'NAVIGATION_SUCCESS', path: event.data.path },
'*'
);
} else if (event.data?.action === 'THEME_CHANGED' && event.data.theme) {
const themeColor = event.data.theme;
if (themeColor === 'dark') {
setTheme(EnumTheme.DARK);
} else if (themeColor === 'light') {
setTheme(EnumTheme.LIGHT);
}
} else if (
event.data?.action === 'LANGUAGE_CHANGED' &&
event.data.language
) {
if (!supportedLanguages?.includes(event.data.language)) return;
i18n.changeLanguage(event.data.language);
}
}
window.addEventListener("message", handleNavigation);
window.addEventListener('message', handleNavigation);
return () => {
window.removeEventListener("message", handleNavigation);
window.removeEventListener('message', handleNavigation);
};
}, [navigate]);
}, [navigate, setTheme]);
return { navigate };
};

56
src/i18n/i18n.ts Normal file
View File

@@ -0,0 +1,56 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import {
capitalizeAll,
capitalizeFirstChar,
capitalizeFirstWord,
} from './processors';
// Load all locale JSON files
const modules = import.meta.glob('./locales/**/*.json', {
eager: true,
}) as Record<string, any>;
// Dynamically detect unique language codes
export const supportedLanguages: string[] = Array.from(
new Set(
Object.keys(modules)
.map((path) => {
const match = path.match(/\.\/locales\/([^/]+)\//);
return match ? match[1] : null;
})
.filter((lang): lang is string => typeof lang === 'string')
)
);
// Construct i18n resources object
const resources: Record<string, Record<string, any>> = {};
for (const path in modules) {
// Path format: './locales/en/core.json'
const match = path.match(/\.\/locales\/([^/]+)\/([^/]+)\.json$/);
if (!match) continue;
const [, lang, ns] = match;
resources[lang] = resources[lang] || {};
resources[lang][ns] = modules[path].default;
}
i18n
.use(initReactI18next)
.use(capitalizeAll as any)
.use(capitalizeFirstChar as any)
.use(capitalizeFirstWord as any)
.init({
resources,
fallbackLng: 'en',
lng: navigator.language,
supportedLngs: supportedLanguages,
ns: ['core'],
defaultNS: 'core',
interpolation: { escapeValue: false },
react: { useSuspense: false },
debug: import.meta.env.MODE === 'development',
});
export default i18n;

View File

@@ -0,0 +1,4 @@
{
"using_theme": "this application is using the theme:",
"welcome": "welcome to Qortal"
}

32
src/i18n/processors.ts Normal file
View File

@@ -0,0 +1,32 @@
export const capitalizeAll = {
type: 'postProcessor',
name: 'capitalizeAll',
process: (value: string) => value.toUpperCase(),
};
export const capitalizeFirstChar = {
type: 'postProcessor',
name: 'capitalizeFirstChar',
process: (value: string) => value.charAt(0).toUpperCase() + value.slice(1),
};
export const capitalizeFirstWord = {
type: 'postProcessor',
name: 'capitalizeFirstWord',
process: (value: string) => {
if (!value?.trim()) return value;
const trimmed = value.trimStart();
const firstSpaceIndex = trimmed.indexOf(' ');
if (firstSpaceIndex === -1) {
return trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
}
const firstWord = trimmed.slice(0, firstSpaceIndex);
const restOfString = trimmed.slice(firstSpaceIndex);
const trailingSpaces = value.slice(trimmed.length);
return firstWord.toUpperCase() + restOfString + trailingSpaces;
},
};

View File

@@ -1,21 +1,20 @@
import { Box, SxProps, Theme, Typography, useMediaQuery } from "@mui/material";
import React from "react";
import { CommentSection } from "../../../components/common/Comments/CommentSection.tsx";
import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx";
import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.tsx";
import { VideoPlayer } from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import { Playlists } from "../../../components/Playlists/Playlists.tsx";
import { fontSizeSmall, minFileSize } from "../../../constants/Misc.ts";
import { formatBytes } from "../../../utils/numberFunctions.ts";
import { formatDate } from "../../../utils/time.ts";
import { VideoActionsBar } from "../VideoContent/VideoActionsBar.tsx";
import { usePlaylistContentState } from "./PlaylistContent-State.ts";
import { Box, SxProps, Theme, Typography, useMediaQuery } from '@mui/material';
import { CommentSection } from '../../../components/common/Comments/CommentSection.tsx';
import { SuperLikesSection } from '../../../components/common/SuperLikesList/SuperLikesSection.tsx';
import { DisplayHtml } from '../../../components/common/TextEditor/DisplayHtml.tsx';
import { VideoPlayer } from '../../../components/common/VideoPlayer/VideoPlayer.tsx';
import { Playlists } from '../../../components/Playlists/Playlists.tsx';
import { fontSizeSmall, minFileSize } from '../../../constants/Misc.ts';
import { formatBytes } from '../../../utils/numberFunctions.ts';
import { formatDate } from '../../../utils/time.ts';
import { VideoActionsBar } from '../VideoContent/VideoActionsBar.tsx';
import { usePlaylistContentState } from './PlaylistContent-State.ts';
import {
Spacer,
VideoDescription,
VideoPlayerContainer,
VideoTitle,
} from "./PlaylistContent-styles.tsx";
} from './PlaylistContent-styles.tsx';
export const PlaylistContent = () => {
const {
@@ -25,9 +24,7 @@ export const PlaylistContent = () => {
superLikeList,
setVideoMetadataResource,
videoReference,
focusVideo,
videoCover,
containerRef,
theme,
descriptionHeight,
nextVideo,
@@ -45,14 +42,14 @@ export const PlaylistContent = () => {
const isScreenSmall = !useMediaQuery(`(min-width:950px)`);
const playlistsSX: SxProps<Theme> = isScreenSmall
? { width: "100%", marginTop: "10px" }
: { width: "35%", position: "absolute", right: "20px" };
? { width: '100%', marginTop: '10px' }
: { width: '35%', position: 'absolute', right: '20px' };
return videoData && videoData?.videos?.length === 0 ? (
<Box
sx={{
width: "100%",
display: "flex",
width: '100%',
display: 'flex',
}}
>
<Typography>This playlist doesn't exist</Typography>
@@ -60,20 +57,19 @@ export const PlaylistContent = () => {
) : (
<Box
sx={{
display: "flex",
alignItems: "center",
flexDirection: "column",
padding: "0px",
marginLeft: "2%",
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
padding: '0px',
marginLeft: '2%',
}}
onClick={focusVideo}
>
<VideoPlayerContainer
sx={{
width: isScreenSmall ? "100%" : "60%",
alignSelf: "start",
paddingRight: isScreenSmall ? "10px" : "0px",
marginBottom: "20px",
width: isScreenSmall ? '100%' : '60%',
alignSelf: 'start',
paddingRight: isScreenSmall ? '10px' : '0px',
marginBottom: '20px',
}}
>
{videoReference && (
@@ -83,13 +79,12 @@ export const PlaylistContent = () => {
identifier={videoReference?.identifier}
user={channelName}
jsonId={id}
poster={videoCover || ""}
poster={videoCover || ''}
nextVideo={nextVideo}
onEnd={onEndVideo}
autoPlay={doAutoPlay}
ref={containerRef}
videoStyles={{
video: { aspectRatio: "16 / 9" },
video: { aspectRatio: '16 / 9' },
}}
duration={videoData?.duration}
/>
@@ -98,12 +93,12 @@ export const PlaylistContent = () => {
<Playlists
playlistData={playlistData}
currentVideoIdentifier={videoData?.id}
onClick={(name, identifier)=> {
onClick={(name, identifier) => {
setVideoMetadataResource({
name,
identifier,
service: 'DOCUMENT'
})
service: 'DOCUMENT',
});
}}
sx={playlistsSX}
/>
@@ -115,23 +110,23 @@ export const PlaylistContent = () => {
videoReference={videoReference}
superLikeList={superLikeList}
setSuperLikeList={setSuperLikeList}
sx={{ width: "100%" }}
sx={{ width: '100%' }}
/>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
width: "100%",
marginTop: "10px",
gap: "10px",
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
marginTop: '10px',
gap: '10px',
}}
>
<VideoTitle
variant="h1"
color="textPrimary"
sx={{
textAlign: "center",
textAlign: 'center',
}}
>
{videoData?.title}
@@ -140,9 +135,9 @@ export const PlaylistContent = () => {
<Box
sx={{
display: "flex",
width: "100%",
gap: "20px",
display: 'flex',
width: '100%',
gap: '20px',
}}
>
{videoData?.created && (
@@ -160,11 +155,11 @@ export const PlaylistContent = () => {
<Typography
variant="h1"
sx={{
fontSize: "90%",
fontSize: '90%',
}}
color={"green"}
color={'green'}
>
{formatBytes(videoData.fileSize, 2, "Decimal")}
{formatBytes(videoData.fileSize, 2, 'Decimal')}
</Typography>
)}
</Box>
@@ -172,30 +167,30 @@ export const PlaylistContent = () => {
{videoData?.fullDescription && (
<Box
sx={{
background: "#333333",
borderRadius: "5px",
padding: "5px",
width: "95%",
alignSelf: "flex-start",
background: '#333333',
borderRadius: '5px',
padding: '5px',
width: '95%',
alignSelf: 'flex-start',
cursor: !descriptionHeight
? "default"
? 'default'
: isExpandedDescription
? "default"
: "pointer",
? 'default'
: 'pointer',
}}
className={
!descriptionHeight ? "" : isExpandedDescription ? "" : "hover-click"
!descriptionHeight ? '' : isExpandedDescription ? '' : 'hover-click'
}
>
{descriptionHeight && !isExpandedDescription && (
<Box
sx={{
position: "absolute",
top: "0px",
right: "0px",
left: "0px",
bottom: "0px",
cursor: "pointer",
position: 'absolute',
top: '0px',
right: '0px',
left: '0px',
bottom: '0px',
cursor: 'pointer',
}}
onClick={() => {
if (isExpandedDescription) return;
@@ -207,11 +202,11 @@ export const PlaylistContent = () => {
ref={contentRef}
sx={{
height: !descriptionHeight
? "auto"
? 'auto'
: isExpandedDescription
? "auto"
? 'auto'
: `${descriptionHeight}px`,
overflow: "hidden",
overflow: 'hidden',
}}
>
{videoData?.htmlDescription ? (
@@ -221,7 +216,7 @@ export const PlaylistContent = () => {
variant="body1"
color="textPrimary"
sx={{
cursor: "default",
cursor: 'default',
}}
>
{videoData?.fullDescription}
@@ -231,17 +226,17 @@ export const PlaylistContent = () => {
{descriptionHeight >= descriptionThreshold && (
<Typography
onClick={() => {
setIsExpandedDescription(prev => !prev);
setIsExpandedDescription((prev) => !prev);
}}
sx={{
fontWeight: "bold",
fontSize: "16px",
cursor: "pointer",
paddingLeft: "15px",
paddingTop: "15px",
fontWeight: 'bold',
fontSize: '16px',
cursor: 'pointer',
paddingLeft: '15px',
paddingTop: '15px',
}}
>
{isExpandedDescription ? "Show less" : "...more"}
{isExpandedDescription ? 'Show less' : '...more'}
</Typography>
)}
</Box>
@@ -250,14 +245,14 @@ export const PlaylistContent = () => {
<SuperLikesSection
loadingSuperLikes={loadingSuperLikes}
superlikes={superLikeList}
postId={videoData?.id || ""}
postName={videoData?.user || ""}
postId={videoData?.id || ''}
postName={videoData?.user || ''}
/>
)}
{videoData?.id && channelName && (
<CommentSection
postId={videoData?.id || ""}
postName={channelName || ""}
postId={videoData?.id || ''}
postName={channelName || ''}
/>
)}
</Box>

View File

@@ -1,44 +1,47 @@
import { videoRefType } from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import {
QTUBE_VIDEO_BASE,
SUPER_LIKE_BASE,
} from "../../../constants/Identifiers.ts";
import {
minPriceSuperLike,
} from "../../../constants/Misc.ts";
import { useFetchSuperLikes } from "../../../hooks/useFetchSuperLikes.tsx";
import { setIsLoadingGlobal } from "../../../state/features/globalSlice.ts";
import { RootState } from "../../../state/store.ts";
import { SUPER_LIKE_BASE } from '../../../constants/Identifiers.ts';
import { minPriceSuperLike } from '../../../constants/Misc.ts';
import { useFetchSuperLikes } from '../../../hooks/useFetchSuperLikes.tsx';
import { RootState } from '../../../state/store.ts';
import React, {
useEffect,
useRef,
useState,
useMemo,
useCallback,
} from "react";
import { useTheme } from "@mui/material";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { hashWordWithoutPublicSalt, QortalGetMetadata, usePublish } from "qapp-core";
} from 'react';
import { useTheme } from '@mui/material';
import { useSelector } from 'react-redux';
import { useNavigate, useParams } from 'react-router-dom';
import {
hashWordWithoutPublicSalt,
QortalGetMetadata,
usePublish,
} from 'qapp-core';
const superLikeVersion2Timestamp = 1744041600000;
export const useVideoContentState = () => {
const { name: channelName, id } = useParams();
console.log('iddd', id)
const [videoMetadataResource, setVideoMetadataResource] = useState<null | QortalGetMetadata>(null)
const {resource } = usePublish(2, 'JSON', videoMetadataResource ? videoMetadataResource : !id ? null : {
identifier: id,
name: channelName,
service: 'DOCUMENT'
})
const [videoMetadataResource, setVideoMetadataResource] =
useState<null | QortalGetMetadata>(null);
const { resource } = usePublish(
2,
'JSON',
videoMetadataResource
? videoMetadataResource
: !id
? null
: {
identifier: id,
name: channelName,
service: 'DOCUMENT',
}
);
const [superLikeversion, setSuperLikeVersion] = useState<null | number>(null);
const [isExpandedDescription, setIsExpandedDescription] =
useState<boolean>(false);
const containerRef = useRef<videoRefType>(null);
const [nameAddress, setNameAddress] = useState<string>("");
const [nameAddress, setNameAddress] = useState<string>('');
const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
null
);
@@ -49,8 +52,8 @@ export const useVideoContentState = () => {
const [loadingSuperLikes, setLoadingSuperLikes] = useState<boolean>(false);
const [superLikeList, setSuperLikeList] = useState<any[]>([]);
const { addSuperlikeRawDataGetToList } = useFetchSuperLikes();
const videoData = useMemo(()=> {
if(!resource?.data) return null
const videoData = useMemo(() => {
if (!resource?.data) return null;
const resourceData = {
title: resource?.qortalMetadata?.metadata?.title,
@@ -61,25 +64,24 @@ export const useVideoContentState = () => {
created: resource?.qortalMetadata?.created,
updated: resource?.qortalMetadata?.updated,
user: resource?.qortalMetadata.name,
videoImage: "",
videoImage: '',
id: resource?.qortalMetadata.identifier,
};
return {
...resourceData,
...resource.data
}
}, [resource])
...resource.data,
};
}, [resource]);
const isVideoLoaded = useMemo(()=> {
if(!resource?.data) return false
return true
}, [resource?.data])
console.log('videoData', videoData)
const isVideoLoaded = useMemo(() => {
if (!resource?.data) return false;
return true;
}, [resource?.data]);
const contentRef = useRef(null);
const getAddressName = async name => {
const getAddressName = async (name) => {
const response = await qortalRequest({
action: "GET_NAME_DATA",
action: 'GET_NAME_DATA',
name: name,
});
@@ -94,7 +96,7 @@ export const useVideoContentState = () => {
}
}, [channelName]);
const avatarUrl = useMemo(() => {
let url = "";
let url = '';
if (channelName && userAvatarHash[channelName]) {
url = userAvatarHash[channelName];
}
@@ -104,7 +106,6 @@ export const useVideoContentState = () => {
const navigate = useNavigate();
const theme = useTheme();
const videoReference = useMemo(() => {
if (!videoData) return null;
const { videoReference } = videoData;
@@ -124,16 +125,13 @@ export const useVideoContentState = () => {
const { videoImage } = videoData;
return videoImage || null;
}, [videoData]);
const dispatch = useDispatch();
useEffect(()=> {
if(!resource?.qortalMetadata?.created) return
useEffect(() => {
if (!resource?.qortalMetadata?.created) return;
if (resource?.qortalMetadata?.created > superLikeVersion2Timestamp) {
setSuperLikeVersion(2);
} else setSuperLikeVersion(1);
}, [resource?.qortalMetadata?.created])
}, [resource?.qortalMetadata?.created]);
const descriptionThreshold = 200;
useEffect(() => {
@@ -152,9 +150,9 @@ export const useVideoContentState = () => {
const hashPostId = await hashWordWithoutPublicSalt(id, 20);
const urlV2 = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}${hashPostId}&limit=100&includemetadata=true&reverse=true&excludeblocked=true`;
const response = await fetch(urlV2, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
let responseData = [];
@@ -165,9 +163,9 @@ export const useVideoContentState = () => {
39
)}&limit=100&includemetadata=true&reverse=true&excludeblocked=true`;
const responseV1 = await fetch(urlV1, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
@@ -201,7 +199,7 @@ export const useVideoContentState = () => {
...comments,
{
...comment,
message: "",
message: '',
amount: res.amount,
},
];
@@ -227,42 +225,11 @@ export const useVideoContentState = () => {
getComments(id, nameAddress, superLikeversion);
}, [getComments, id, nameAddress, superLikeversion]);
const focusVideo = () => {
const focusRef = containerRef.current?.getContainerRef()?.current;
focusRef?.focus({ preventScroll: true });
};
useEffect(() => {
focusVideo();
}, []);
const focusVideoOnClick = (e: React.MouseEvent<HTMLDivElement>) => {
const target = e.target as Element;
const textTagNames = ["TEXTAREA", "P", "H[1-6]", "STRONG", "svg", "A"];
const noText =
textTagNames.findIndex(s => {
return target?.tagName.match(s);
}) < 0;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const clickOnEmptySpace = !target?.onclick && noText;
// clicking on link in superlikes bar shows deleted video when loading
if (target == e.currentTarget || clickOnEmptySpace) {
console.log("in correctTarget");
focusVideo();
}
};
return {
focusVideo: focusVideoOnClick,
videoReference,
channelName,
id,
videoCover,
containerRef,
isVideoLoaded,
navigate,
theme,
@@ -276,7 +243,7 @@ export const useVideoContentState = () => {
superLikeList,
setSuperLikeList,
getComments,
setVideoMetadataResource
setVideoMetadataResource,
};
};
@@ -317,23 +284,23 @@ export function extractSigValue(metadescription) {
}
export const getPaymentInfo = async (signature: string) => {
if (signature === "undefined" || !signature) return undefined;
if (signature === 'undefined' || !signature) return undefined;
try {
const url = `/transactions/signature/${signature}`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
// Coin payment info must be added to responseData so we can display it to the user
const responseData = await response.json();
if (responseData && !responseData.error) return responseData;
else {
throw new Error("unable to get payment");
throw new Error('unable to get payment');
}
} catch (error) {
throw new Error("unable to get payment");
throw new Error('unable to get payment');
}
};

View File

@@ -1,36 +1,33 @@
import { Box, Button, Typography, useMediaQuery } from "@mui/material";
import React, { useEffect, useState } from "react";
import { Box, Typography, useMediaQuery } from '@mui/material';
import { useEffect, useState } from 'react';
import DeletedVideo from "../../../assets/img/DeletedVideo.jpg";
import { CommentSection } from "../../../components/common/Comments/CommentSection.tsx";
import { SuperLikesSection } from "../../../components/common/SuperLikesList/SuperLikesSection.tsx";
import { DisplayHtml } from "../../../components/common/TextEditor/DisplayHtml.tsx";
import { VideoPlayer } from "../../../components/common/VideoPlayer/VideoPlayer.tsx";
import { CommentSection } from '../../../components/common/Comments/CommentSection.tsx';
import { SuperLikesSection } from '../../../components/common/SuperLikesList/SuperLikesSection.tsx';
import { DisplayHtml } from '../../../components/common/TextEditor/DisplayHtml.tsx';
import { VideoPlayer } from '../../../components/common/VideoPlayer/VideoPlayer.tsx';
import {
fontSizeSmall,
minFileSize,
smallVideoSize,
} from "../../../constants/Misc.ts";
import { formatBytes } from "../../../utils/numberFunctions.ts";
import { formatDate } from "../../../utils/time.ts";
import { VideoActionsBar } from "./VideoActionsBar.tsx";
import { useVideoContentState } from "./VideoContent-State.ts";
} from '../../../constants/Misc.ts';
import { formatBytes } from '../../../utils/numberFunctions.ts';
import { formatDate } from '../../../utils/time.ts';
import { VideoActionsBar } from './VideoActionsBar.tsx';
import { useVideoContentState } from './VideoContent-State.ts';
import {
Spacer,
VideoContentContainer,
VideoDescription,
VideoPlayerContainer,
VideoTitle,
} from "./VideoContent-styles.tsx";
} from './VideoContent-styles.tsx';
export const VideoContent = () => {
const {
focusVideo,
videoReference,
channelName,
id,
videoCover,
containerRef,
isVideoLoaded,
theme,
videoData,
@@ -61,70 +58,56 @@ export const VideoContent = () => {
if (videoWidth < minWidthPercent) videoWidth = minWidthPercent;
useEffect(() => {
window.addEventListener("resize", e => {
window.addEventListener('resize', (e) => {
setScreenWidth(window.innerWidth + 120);
});
}, []);
const [newVideo, setNewVideo] = useState(null)
return (
<>
<Box
sx={{
display: "flex",
flexDirection: "column",
// padding: `0px 0px 0px ${isScreenSmall ? "0px" : "2%"}`,
display: 'flex',
flexDirection: 'column',
padding: '10px',
width: "100%",
width: '100%',
}}
onClick={focusVideo}
>
<Button onClick={()=> {
setNewVideo({
identifier: 'MYTEST2_vid_hymn-for-peace_gEpkBT',
name: 'SafetyMongoose',
service: 'VIDEO'
})
}}>play another</Button>
<Button onClick={()=> setNewVideo(null)}>go back</Button>
{videoReference ? (
<VideoPlayerContainer
sx={{
// width: `${videoWidth}%`,
// marginLeft: "0%",
height: '70vh',
backgroundColor: 'black'
backgroundColor: 'black',
}}
>
<VideoPlayer
name={newVideo?.name || videoReference?.name}
service={newVideo?.service || videoReference?.service}
identifier={newVideo?.identifier || videoReference?.identifier}
name={videoReference?.name}
service={videoReference?.service}
identifier={videoReference?.identifier}
user={channelName}
jsonId={id}
poster={videoCover || ""}
ref={containerRef}
poster={videoCover || ''}
videoStyles={{
videoContainer: { aspectRatio: "16 / 9" },
video: { aspectRatio: "16 / 9" },
videoContainer: { aspectRatio: '16 / 9' },
video: { aspectRatio: '16 / 9' },
}}
duration={videoData?.duration}
/>
</VideoPlayerContainer>
) : (
<Box sx={{ width: "100%", height: '70vh', background: 'black'}}></Box>
<Box
sx={{ width: '100%', height: '70vh', background: 'black' }}
></Box>
)}
<VideoContentContainer
sx={{ paddingLeft: isScreenSmall ? "5px" : "0px" }}
sx={{ paddingLeft: isScreenSmall ? '5px' : '0px' }}
>
<VideoTitle
variant={isScreenSmall ? "h2" : "h1"}
variant={isScreenSmall ? 'h2' : 'h1'}
color="textPrimary"
sx={{
textAlign: "start",
marginTop: isScreenSmall ? "20px" : "10px",
textAlign: 'start',
marginTop: isScreenSmall ? '20px' : '10px',
}}
>
{videoData?.title}
@@ -135,7 +118,7 @@ export const VideoContent = () => {
variant="h2"
sx={{
fontSize: fontSizeSmall,
display: "inline",
display: 'inline',
}}
color={theme.palette.text.primary}
>
@@ -147,13 +130,13 @@ export const VideoContent = () => {
<Typography
variant="h1"
sx={{
fontSize: "90%",
display: "inline",
marginLeft: "20px",
fontSize: '90%',
display: 'inline',
marginLeft: '20px',
}}
color={"green"}
color={'green'}
>
{formatBytes(videoData.fileSize, 2, "Decimal")}
{formatBytes(videoData.fileSize, 2, 'Decimal')}
</Typography>
)}
</Box>
@@ -163,42 +146,42 @@ export const VideoContent = () => {
setSuperLikeList={setSuperLikeList}
superLikeList={superLikeList}
videoReference={videoReference}
sx={{ width: "calc(100% - 5px)" }}
sx={{ width: 'calc(100% - 5px)' }}
/>
<Spacer height="15px" />
{videoData?.fullDescription && (
<Box
sx={{
background: "#333333",
borderRadius: "5px",
padding: "5px",
width: "95%",
background: '#333333',
borderRadius: '5px',
padding: '5px',
width: '95%',
cursor: !descriptionHeight
? "default"
? 'default'
: isExpandedDescription
? "default"
: "pointer",
position: "relative",
marginBottom: "30px",
? 'default'
: 'pointer',
position: 'relative',
marginBottom: '30px',
}}
className={
!descriptionHeight
? ""
? ''
: isExpandedDescription
? ""
: "hover-click"
? ''
: 'hover-click'
}
>
{descriptionHeight && !isExpandedDescription && (
<Box
sx={{
position: "absolute",
top: "0px",
right: "0px",
left: "0px",
bottom: "0px",
cursor: "pointer",
position: 'absolute',
top: '0px',
right: '0px',
left: '0px',
bottom: '0px',
cursor: 'pointer',
}}
onClick={() => {
if (isExpandedDescription) return;
@@ -210,11 +193,11 @@ export const VideoContent = () => {
ref={contentRef}
sx={{
height: !descriptionHeight
? "auto"
? 'auto'
: isExpandedDescription
? "auto"
: "200px",
overflow: "hidden",
? 'auto'
: '200px',
overflow: 'hidden',
}}
>
{videoData?.htmlDescription ? (
@@ -224,7 +207,7 @@ export const VideoContent = () => {
variant="body1"
color="textPrimary"
sx={{
cursor: "default",
cursor: 'default',
}}
>
{videoData?.fullDescription}
@@ -234,17 +217,17 @@ export const VideoContent = () => {
{descriptionHeight >= descriptionThreshold && (
<Typography
onClick={() => {
setIsExpandedDescription(prev => !prev);
setIsExpandedDescription((prev) => !prev);
}}
sx={{
fontWeight: "bold",
fontSize: "16px",
cursor: "pointer",
paddingLeft: "15px",
paddingTop: "15px",
fontWeight: 'bold',
fontSize: '16px',
cursor: 'pointer',
paddingLeft: '15px',
paddingTop: '15px',
}}
>
{isExpandedDescription ? "Show less" : "...more"}
{isExpandedDescription ? 'Show less' : '...more'}
</Typography>
)}
</Box>
@@ -257,10 +240,10 @@ export const VideoContent = () => {
getMore={() => {}}
loadingSuperLikes={loadingSuperLikes}
superlikes={superLikeList}
postId={id || ""}
postName={channelName || ""}
postId={id || ''}
postName={channelName || ''}
/>
<CommentSection postId={id || ""} postName={channelName || ""} />
<CommentSection postId={id || ''} postName={channelName || ''} />
</>
)}
</VideoContentContainer>

View File

@@ -1,30 +1,26 @@
import { Box } from "@mui/material";
import { useMemo } from "react";
import { useParams } from "react-router-dom";
import { Box } from '@mui/material';
import { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { VideoManagerRow } from "./VideoList-styles.tsx";
import { VideoManagerRow } from './VideoList-styles.tsx';
import VideoList from "./VideoList.tsx";
import { QortalSearchParams } from "qapp-core";
import { QTUBE_PLAYLIST_BASE } from "../../../constants/Identifiers.ts";
import VideoList from './VideoList.tsx';
import { QortalSearchParams } from 'qapp-core';
import { QTUBE_PLAYLIST_BASE } from '../../../constants/Identifiers.ts';
interface VideoListProps {
mode?: string;
}
export const PlayListComponentLevel = ({ mode }: VideoListProps) => {
export const PlayListComponentLevel = () => {
const { name: paramName } = useParams();
const searchParameters: QortalSearchParams = useMemo(() => {
return {
identifier: QTUBE_PLAYLIST_BASE,
service: "PLAYLIST",
service: 'PLAYLIST',
offset: 0,
reverse: true,
limit: 20,
excludeBlocked: true,
name: paramName || "",
mode: "ALL",
name: paramName || '',
mode: 'ALL',
exactmatchnames: true,
};
}, [paramName]);
@@ -33,10 +29,10 @@ export const PlayListComponentLevel = ({ mode }: VideoListProps) => {
<VideoManagerRow>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<VideoList

View File

@@ -1,77 +1,75 @@
import { SelectChangeEvent } from "@mui/material";
import { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { useDispatch, useSelector } from "react-redux";
import { categories } from "../../../constants/Categories.ts";
import { changeFilterType } from "../../../state/features/persistSlice.ts";
import { SelectChangeEvent } from '@mui/material';
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { categories } from '../../../constants/Categories.ts';
import { changeFilterType } from '../../../state/features/persistSlice.ts';
import {
changefilterName,
changefilterSearch,
changeSelectedCategoryVideos,
changeSelectedSubCategoryVideos,
resetVideoState,
} from "../../../state/features/videoSlice.ts";
import { RootState } from "../../../state/store.ts";
} from '../../../state/features/videoSlice.ts';
import { RootState } from '../../../state/store.ts';
export const useSidebarState = (
) => {
const [filterStateSearch, setFilterStateSearch] = useState('')
const [filterStateType, setFilterStateType] = useState('videos')
const [filterStateName, setFilterStateName] = useState('')
const [selectedCategoryVideosState, setSelectedCategoryVideosState] = useState(null)
const [selectedSubCategoryVideosState, setSelectedSubCategoryVideosState] = useState(null)
export const useSidebarState = () => {
const [filterStateSearch, setFilterStateSearch] = useState('');
const [filterStateType, setFilterStateType] = useState('videos');
const [filterStateName, setFilterStateName] = useState('');
const [selectedCategoryVideosState, setSelectedCategoryVideosState] =
useState(null);
const [selectedSubCategoryVideosState, setSelectedSubCategoryVideosState] =
useState(null);
const dispatch = useDispatch();
const onSearch = ()=> {
const onSearch = () => {
dispatch(changeFilterType(filterStateType));
dispatch(changefilterSearch(filterStateSearch));
dispatch(changefilterName(filterStateName));
dispatch(changeSelectedCategoryVideos(selectedCategoryVideosState));
dispatch(changeSelectedSubCategoryVideos(selectedSubCategoryVideosState));
}
};
const {
selectedCategoryVideos,
selectedSubCategoryVideos,
filterName,
filterSearch
filterSearch,
} = useSelector((state: RootState) => state.video);
const filterType = useSelector(
(state: RootState) => state.persist.filterType
);
console.log('filterName', filterName, )
useEffect(() => {
setFilterStateSearch(filterSearch);
setFilterStateName(filterName);
setFilterStateType(filterType);
setSelectedCategoryVideosState(selectedCategoryVideos);
setSelectedSubCategoryVideosState(selectedSubCategoryVideos);
}, [
filterName,
filterSearch,
filterType,
selectedCategoryVideos,
selectedSubCategoryVideos,
]);
useEffect(()=> {
setFilterStateSearch(filterSearch)
setFilterStateName(filterName)
setFilterStateType(filterType)
setSelectedCategoryVideosState(selectedCategoryVideos)
setSelectedSubCategoryVideosState(selectedSubCategoryVideos)
}, [filterName,
filterSearch, filterType, selectedCategoryVideos, selectedSubCategoryVideos])
const onReset = ()=> {
dispatch(resetVideoState())
}
const onReset = () => {
dispatch(resetVideoState());
};
const handleInputKeyDown = (event: any) => {
if (event.key === "Enter") {
if (event.key === 'Enter') {
onSearch();
}
};
console.log('filterType', filterType)
const handleOptionCategoryChangeVideos = (
event: SelectChangeEvent<string>
) => {
const optionId = event.target.value;
const selectedOption = categories.find(option => option.id === +optionId);
const selectedOption = categories.find((option) => option.id === +optionId);
setSelectedCategoryVideosState(selectedOption || null);
};
const handleOptionSubCategoryChangeVideos = (
@@ -80,13 +78,11 @@ useEffect(()=> {
) => {
const optionId = event.target.value;
const selectedOption = subcategories.find(
option => option.id === +optionId
(option) => option.id === +optionId
);
setSelectedSubCategoryVideosState(selectedOption || null);
};
return {
filterSearch: filterStateSearch,
setFilterSearch: setFilterStateSearch,
@@ -100,6 +96,6 @@ useEffect(()=> {
filterType: filterStateType,
setFilterType: setFilterStateType,
onSearch,
onReset
onReset,
};
};

View File

@@ -1,31 +1,30 @@
import { useCallback, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { blockUser, setEditVideo } from "../../../state/features/videoSlice.ts";
import { RootState } from "../../../state/store.ts";
import { blockUser, setEditVideo } from '../../../state/features/videoSlice.ts';
import { RootState } from '../../../state/store.ts';
import { VideoCardContainer } from "./VideoList-styles.tsx";
import { QortalSearchParams, ResourceListDisplay } from "qapp-core";
import { VideoListItem } from "./VideoListItem.tsx";
import { VideoLoaderItem } from "./VideoLoaderItem.tsx";
import { VideoCardContainer } from './VideoList-styles.tsx';
import { QortalSearchParams, ResourceListDisplay } from 'qapp-core';
import { VideoListItem } from './VideoListItem.tsx';
import { VideoLoaderItem } from './VideoLoaderItem.tsx';
interface VideoListProps {
searchParameters: QortalSearchParams;
listName: string;
}
export const VideoList = ({ searchParameters, listName }: VideoListProps) => {
const [showIcons, setShowIcons] = useState(null);
const username = useSelector((state: RootState) => state.auth?.user?.name);
const dispatch = useDispatch();
const blockUserFunc = async (user: string) => {
if (user === "Q-Tube") return;
if (user === 'Q-Tube') return;
try {
const response = await qortalRequest({
action: "ADD_LIST_ITEMS",
list_name: "blockedNames",
action: 'ADD_LIST_ITEMS',
list_name: 'blockedNames',
items: [user],
});
@@ -37,7 +36,7 @@ export const VideoList = ({ searchParameters, listName }: VideoListProps) => {
}
};
const renderLoaderItem = useCallback(status => {
const renderLoaderItem = useCallback((status) => {
return <VideoLoaderItem status={status} />;
}, []);
@@ -51,14 +50,12 @@ export const VideoList = ({ searchParameters, listName }: VideoListProps) => {
qortalMetadata={qortalMetadata}
video={video}
blockUserFunc={blockUserFunc}
setShowIcons={setShowIcons}
showIcons={showIcons}
username={username}
setEditVideo={setEditVideo}
/>
);
},
[username, showIcons]
[username]
);
return (

View File

@@ -1,144 +1,162 @@
import { Avatar, Box, Tooltip, Typography, useTheme } from "@mui/material";
import { BlockIconContainer, BottomParent, IconsBox, NameContainer, VideoCard, VideoCardCol, VideoCardName, VideoCardTitle, VideoUploadDate } from "./VideoList-styles";
import { setEditPlaylist } from "../../../state/features/videoSlice";
import ResponsiveImage from "../../../components/ResponsiveImage";
import { PlaylistSVG } from "../../../assets/svgs/PlaylistSVG";
import { formatTime } from "../../../utils/numberFunctions";
import { VideoCardImageContainer } from "./VideoCardImageContainer";
import { fontSizeSmall, minDuration } from "../../../constants/Misc";
import { formatDate } from "../../../utils/time";
import { useNavigate } from "react-router-dom";
import DeleteIcon from "@mui/icons-material/Delete";
import BlockIcon from "@mui/icons-material/Block";
import EditIcon from "@mui/icons-material/Edit";
import { useDispatch } from "react-redux";
import { useGlobal } from "qapp-core";
import { Avatar, Box, Tooltip, Typography, useTheme } from '@mui/material';
import {
BlockIconContainer,
BottomParent,
IconsBox,
NameContainer,
VideoCard,
VideoCardCol,
VideoCardName,
VideoCardTitle,
VideoUploadDate,
} from './VideoList-styles';
import { setEditPlaylist } from '../../../state/features/videoSlice';
import ResponsiveImage from '../../../components/ResponsiveImage';
import { PlaylistSVG } from '../../../assets/svgs/PlaylistSVG';
import { formatTime } from '../../../utils/numberFunctions';
import { VideoCardImageContainer } from './VideoCardImageContainer';
import { fontSizeSmall, minDuration } from '../../../constants/Misc';
import { formatDate } from '../../../utils/time';
import { useNavigate } from 'react-router-dom';
import DeleteIcon from '@mui/icons-material/Delete';
import BlockIcon from '@mui/icons-material/Block';
import EditIcon from '@mui/icons-material/Edit';
import { useDispatch } from 'react-redux';
import { useGlobal } from 'qapp-core';
import { useState } from 'react';
export const VideoListItem = ({qortalMetadata, video, setShowIcons, showIcons, username, blockUserFunc, setEditVideo}) => {
const navigate = useNavigate()
const dispatch = useDispatch();
const theme = useTheme()
const { lists } = useGlobal();
export const VideoListItem = ({
qortalMetadata,
video,
username,
blockUserFunc,
setEditVideo,
}) => {
const navigate = useNavigate();
const dispatch = useDispatch();
const [showIcons, setShowIcons] = useState(null);
const theme = useTheme();
const { lists } = useGlobal();
const { deleteResource } = lists;
const { deleteResource } = lists;
const isPlaylist = qortalMetadata?.service === "PLAYLIST";
const isPlaylist = qortalMetadata?.service === 'PLAYLIST';
if (isPlaylist) {
return (
<VideoCardCol
key={qortalMetadata?.identifier}
onMouseEnter={() => setShowIcons(qortalMetadata?.identifier)}
onMouseLeave={() => setShowIcons(null)}
if (isPlaylist) {
return (
<VideoCardCol
key={qortalMetadata?.identifier}
onMouseEnter={() => setShowIcons(qortalMetadata?.identifier)}
onMouseLeave={() => setShowIcons(null)}
>
<IconsBox
sx={{
opacity: showIcons === qortalMetadata?.identifier ? 1 : 0,
zIndex: 2,
}}
>
<IconsBox
sx={{
opacity: showIcons === qortalMetadata?.identifier ? 1 : 0,
zIndex: 2,
}}
>
{qortalMetadata?.name === username && (
<Tooltip title="Edit playlist" placement="top">
<BlockIconContainer>
<EditIcon
onClick={() => {
const resourceData = {
title: qortalMetadata?.metadata?.title,
category: qortalMetadata?.metadata?.category,
categoryName: qortalMetadata?.metadata?.categoryName,
tags: qortalMetadata?.metadata?.tags || [],
description: qortalMetadata?.metadata?.description,
created: qortalMetadata?.created,
updated: qortalMetadata?.updated,
name: qortalMetadata.name,
videoImage: "",
identifier: qortalMetadata.identifier,
service: qortalMetadata.service,
};
dispatch(setEditPlaylist({...resourceData, ...video}));
}}
/>
</BlockIconContainer>
</Tooltip>
)}
{qortalMetadata?.name !== username && (
<Tooltip title="Block user content" placement="top">
<BlockIconContainer>
<BlockIcon
onClick={() => {
blockUserFunc(qortalMetadata?.name);
}}
/>
</BlockIconContainer>
</Tooltip>
)}
</IconsBox>
<VideoCard
onClick={() => {
navigate(`/playlist/${qortalMetadata?.name}/${qortalMetadata?.identifier}`);
}}
>
<ResponsiveImage
src={video?.image}
width={266}
height={150}
style={{
maxHeight: "50%",
}}
/>
<VideoCardTitle>{video?.title}</VideoCardTitle>
<BottomParent>
<NameContainer
onClick={e => {
e.stopPropagation();
navigate(`/channel/${qortalMetadata?.name}`);
}}
>
<Avatar
sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${qortalMetadata?.name}/qortal_avatar`}
alt={`${qortalMetadata?.name}'s avatar`}
/>
<VideoCardName
sx={{
":hover": {
textDecoration: "underline",
},
{qortalMetadata?.name === username && (
<Tooltip title="Edit playlist" placement="top">
<BlockIconContainer>
<EditIcon
onClick={() => {
const resourceData = {
title: qortalMetadata?.metadata?.title,
category: qortalMetadata?.metadata?.category,
categoryName: qortalMetadata?.metadata?.categoryName,
tags: qortalMetadata?.metadata?.tags || [],
description: qortalMetadata?.metadata?.description,
created: qortalMetadata?.created,
updated: qortalMetadata?.updated,
name: qortalMetadata.name,
videoImage: '',
identifier: qortalMetadata.identifier,
service: qortalMetadata.service,
};
dispatch(setEditPlaylist({ ...resourceData, ...video }));
}}
>
{qortalMetadata?.name}
</VideoCardName>
/>
</BlockIconContainer>
</Tooltip>
)}
{qortalMetadata?.created && (
<VideoUploadDate>
{formatDate(qortalMetadata.created)}
</VideoUploadDate>
)}
</NameContainer>
<Box
{qortalMetadata?.name !== username && (
<Tooltip title="Block user content" placement="top">
<BlockIconContainer>
<BlockIcon
onClick={() => {
blockUserFunc(qortalMetadata?.name);
}}
/>
</BlockIconContainer>
</Tooltip>
)}
</IconsBox>
<VideoCard
onClick={() => {
navigate(
`/playlist/${qortalMetadata?.name}/${qortalMetadata?.identifier}`
);
}}
>
<ResponsiveImage
src={video?.image}
width={266}
height={150}
style={{
maxHeight: '50%',
}}
/>
<VideoCardTitle>{video?.title}</VideoCardTitle>
<BottomParent>
<NameContainer
onClick={(e) => {
e.stopPropagation();
navigate(`/channel/${qortalMetadata?.name}`);
}}
>
<Avatar
sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${qortalMetadata?.name}/qortal_avatar`}
alt={`${qortalMetadata?.name}'s avatar`}
/>
<VideoCardName
sx={{
display: "flex",
position: "absolute",
bottom: "5px",
right: "5px",
':hover': {
textDecoration: 'underline',
},
}}
>
<PlaylistSVG
color={theme.palette.text.primary}
height="36px"
width="36px"
/>
</Box>
</BottomParent>
</VideoCard>
</VideoCardCol>
);
}
{qortalMetadata?.name}
</VideoCardName>
{qortalMetadata?.created && (
<VideoUploadDate>
{formatDate(qortalMetadata.created)}
</VideoUploadDate>
)}
</NameContainer>
<Box
sx={{
display: 'flex',
position: 'absolute',
bottom: '5px',
right: '5px',
}}
>
<PlaylistSVG
color={theme.palette.text.primary}
height="36px"
width="36px"
/>
</Box>
</BottomParent>
</VideoCard>
</VideoCardCol>
);
}
console.log('showIcons', qortalMetadata, showIcons, qortalMetadata?.name === username, qortalMetadata?.name, username)
return (
<VideoCardCol
key={qortalMetadata?.identifier}
@@ -165,11 +183,11 @@ export const VideoListItem = ({qortalMetadata, video, setShowIcons, showIcons, u
created: qortalMetadata?.created,
updated: qortalMetadata?.updated,
user: qortalMetadata.name,
videoImage: "",
videoImage: '',
id: qortalMetadata.identifier,
};
dispatch(setEditVideo({...resourceData, ...video}));
dispatch(setEditVideo({ ...resourceData, ...video }));
}}
/>
</BlockIconContainer>
@@ -192,10 +210,7 @@ export const VideoListItem = ({qortalMetadata, video, setShowIcons, showIcons, u
<BlockIconContainer>
<DeleteIcon
onClick={() => {
deleteResource([
qortalMetadata,
video.videoReference,
]);
deleteResource([qortalMetadata, video.videoReference]);
}}
/>
</BlockIconContainer>
@@ -217,9 +232,7 @@ export const VideoListItem = ({qortalMetadata, video, setShowIcons, showIcons, u
bgcolor="#202020"
zIndex={999}
>
<Typography color="white">
{formatTime(video.duration)}
</Typography>
<Typography color="white">{formatTime(video.duration)}</Typography>
</Box>
)}
<VideoCardImageContainer
@@ -237,7 +250,7 @@ export const VideoListItem = ({qortalMetadata, video, setShowIcons, showIcons, u
</Tooltip>
<BottomParent>
<NameContainer
onClick={e => {
onClick={(e) => {
e.stopPropagation();
navigate(`/channel/${qortalMetadata?.name}`);
}}
@@ -249,8 +262,8 @@ export const VideoListItem = ({qortalMetadata, video, setShowIcons, showIcons, u
/>
<VideoCardName
sx={{
":hover": {
textDecoration: "underline",
':hover': {
textDecoration: 'underline',
},
}}
>
@@ -258,8 +271,8 @@ export const VideoListItem = ({qortalMetadata, video, setShowIcons, showIcons, u
</VideoCardName>
</NameContainer>
{qortalMetadata?.created && (
<Box sx={{ flexDirection: "row", width: "100%" }}>
<VideoUploadDate sx={{ display: "inline" }}>
<Box sx={{ flexDirection: 'row', width: '100%' }}>
<VideoUploadDate sx={{ display: 'inline' }}>
{formatDate(qortalMetadata.created)}
</VideoUploadDate>
</Box>
@@ -268,4 +281,4 @@ export const VideoListItem = ({qortalMetadata, video, setShowIcons, showIcons, u
</VideoCard>
</VideoCardCol>
);
}
};

View File

@@ -1,13 +1,12 @@
import React, { useEffect, useRef, useState } from "react";
import React, { useRef, useState } from 'react';
import { useDispatch, useSelector } from "react-redux";
import { useFetchVideos } from "../../hooks/useFetchVideos.tsx";
import { useDispatch, useSelector } from 'react-redux';
import {
setHomePageSelectedTab,
VideoListType,
} from "../../state/features/persistSlice.ts";
import { RootState } from "../../state/store";
import { resetVideoState } from "../../state/features/videoSlice.ts";
} from '../../state/features/persistSlice.ts';
import { RootState } from '../../state/store';
import { resetVideoState } from '../../state/features/videoSlice.ts';
export const useHomeState = (mode: string) => {
const dispatch = useDispatch();
@@ -15,7 +14,6 @@ export const useHomeState = (mode: string) => {
(state: RootState) => state.persist
);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [tabValue, setTabValue] = useState<VideoListType>(selectedTab);
const {
@@ -31,11 +29,6 @@ export const useHomeState = (mode: string) => {
} = useSelector((state: RootState) => state.video);
const isFilterMode = useRef(false);
const firstFetch = useRef(false);
const afterFetch = useRef(false);
const isFetching = useRef(false);
const { getVideos, getVideosFiltered } = useFetchVideos();
const videos = isFiltering ? filteredVideos : globalVideos;
@@ -46,111 +39,21 @@ export const useHomeState = (mode: string) => {
dispatch(setHomePageSelectedTab(newValue));
};
// const getVideosHandlerMount = React.useCallback(async () => {
// if (firstFetch.current) return;
// firstFetch.current = true;
// setIsLoading(true);
// await getVideos(
// {
// name: "",
// category: "",
// subcategory: "",
// keywords: "",
// contentType: filterType,
// },
// null,
// null,
// 20,
// tabValue
// );
// afterFetch.current = true;
// isFetching.current = false;
// setIsLoading(false);
// }, [getVideos]);
// const getVideosHandler = React.useCallback(
// async (reset?: boolean, resetFilters?: boolean) => {
// if (!firstFetch.current || !afterFetch.current) return;
// if (isFetching.current) return;
// isFetching.current = true;
// await getVideos(
// {
// name: filterName,
// category: selectedCategoryVideos?.id,
// subcategory: selectedSubCategoryVideos?.id,
// keywords: filterSearch,
// contentType: filterType,
// },
// reset,
// resetFilters,
// 20,
// tabValue
// );
// isFetching.current = false;
// },
// [
// getVideos,
// filterValue,
// getVideosFiltered,
// isFiltering,
// filterName,
// selectedCategoryVideos,
// selectedSubCategoryVideos,
// filterSearch,
// filterType,
// tabValue,
// ]
// );
// useEffect(() => {
// getVideosHandler(true);
// }, [tabValue]);
// const prevVal = useRef("");
// useEffect(() => {
// if (isFiltering && filterValue !== prevVal?.current) {
// prevVal.current = filterValue;
// getVideosHandler();
// }
// }, [filterValue, isFiltering, filteredVideos]);
// useEffect(() => {
// if (
// !firstFetch.current &&
// !isFilterMode.current &&
// globalVideos.length === 0
// ) {
// isFetching.current = true;
// getVideosHandlerMount();
// } else {
// firstFetch.current = true;
// afterFetch.current = true;
// }
// }, [getVideosHandlerMount, globalVideos]);
const resetState = ()=> {
const resetState = () => {
dispatch(resetVideoState());
}
console.log('filterName', filterName)
};
return {
tabValue,
changeTab,
videos,
isLoading,
filteredSubscriptionList,
// getVideosHandler,
filterName,
filterSearch,
filterValue,
filterType,
selectedCategoryVideos,
selectedSubCategoryVideos,
resetState
resetState,
};
};

View File

@@ -1,15 +1,17 @@
import { TabContext, TabList, TabPanel } from "@mui/lab";
import { TabContext, TabList, TabPanel } from '@mui/lab';
import { Box, Tab, useMediaQuery } from "@mui/material";
import React, { useMemo } from "react";
import LazyLoad from "../../components/common/LazyLoad";
import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx";
import { fontSizeLarge, fontSizeSmall } from "../../constants/Misc.ts";
import { SearchSidebar } from "./Components/SearchSidebar.tsx";
import VideoList from "./Components/VideoList.tsx";
import { useHomeState } from "./Home-State.ts";
import { QTUBE_PLAYLIST_BASE, QTUBE_VIDEO_BASE } from "../../constants/Identifiers.ts";
import { QortalSearchParams } from "qapp-core";
import { Box, Tab, useMediaQuery } from '@mui/material';
import { useMemo } from 'react';
import { ListSuperLikeContainer } from '../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx';
import { fontSizeLarge, fontSizeSmall } from '../../constants/Misc.ts';
import { SearchSidebar } from './Components/SearchSidebar.tsx';
import VideoList from './Components/VideoList.tsx';
import { useHomeState } from './Home-State.ts';
import {
QTUBE_PLAYLIST_BASE,
QTUBE_VIDEO_BASE,
} from '../../constants/Identifiers.ts';
import { QortalSearchParams } from 'qapp-core';
interface HomeProps {
mode?: string;
@@ -18,36 +20,34 @@ export const Home = ({ mode }: HomeProps) => {
const {
tabValue,
changeTab,
videos,
isLoading,
filteredSubscriptionList,
// getVideosHandler,
selectedCategoryVideos,
selectedSubCategoryVideos,
filterType,
filterName,
filterSearch,
resetState
resetState,
} = useHomeState(mode);
const tabPaneSX = {
width: "100%",
paddingLeft: "0px",
paddingRight: "0px",
width: '100%',
paddingLeft: '0px',
paddingRight: '0px',
};
const isScreenSmall = !useMediaQuery("(min-width:600px)");
const isScreenLarge = useMediaQuery("(min-width:1200px)");
const isScreenSmall = !useMediaQuery('(min-width:600px)');
const isScreenLarge = useMediaQuery('(min-width:1200px)');
const tabSX = {
fontSize: isScreenSmall ? fontSizeSmall : fontSizeLarge,
paddingLeft: "0px",
paddingRight: "0px",
paddingLeft: '0px',
paddingRight: '0px',
};
const homeBaseSX = { display: "grid", width: "100%" };
const bigGridSX = { gridTemplateColumns: "200px auto 250px" };
const mediumGridSX = { gridTemplateColumns: "200px auto" };
const smallGridSX = { gridTemplateColumns: "100%", gap: "20px" };
const homeBaseSX = { display: 'grid', width: '100%' };
const bigGridSX = { gridTemplateColumns: '200px auto 250px' };
const mediumGridSX = { gridTemplateColumns: '200px auto' };
const smallGridSX = { gridTemplateColumns: '100%', gap: '20px' };
let homeColumns: object;
if (isScreenLarge) homeColumns = bigGridSX;
@@ -62,77 +62,79 @@ export const Home = ({ mode }: HomeProps) => {
description += `;subcategory:${selectedSubCategoryVideos}`;
}
console.log('filterName', filterName)
const searchParameters: QortalSearchParams = useMemo(()=> {
const searchOptions: {
description?: string
query?: string
} = {}
if (selectedCategoryVideos) {
searchOptions.description = `category:${selectedCategoryVideos.id};`;
const searchParameters: QortalSearchParams = useMemo(() => {
const searchOptions: {
description?: string;
query?: string;
} = {};
if (selectedCategoryVideos) {
searchOptions.description = `category:${selectedCategoryVideos.id};`;
if (selectedSubCategoryVideos)
searchOptions.description += `subcategory:${selectedSubCategoryVideos.id}`;
}
if(filterSearch){
searchOptions.query = filterSearch
}
return {
identifier:
filterType === "playlists" ? QTUBE_PLAYLIST_BASE : QTUBE_VIDEO_BASE,
service: filterType === "playlists" ? "PLAYLIST" : "DOCUMENT",
offset: 0,
reverse: true,
limit: 20,
excludeBlocked: true,
name: filterName || "",
...searchOptions,
mode: 'ALL'
};
}, [filterType, filterName, selectedSubCategoryVideos, selectedCategoryVideos, filterSearch])
console.log('searchParameters', searchParameters)
if (selectedSubCategoryVideos)
searchOptions.description += `subcategory:${selectedSubCategoryVideos.id}`;
}
if (filterSearch) {
searchOptions.query = filterSearch;
}
return {
identifier:
filterType === 'playlists' ? QTUBE_PLAYLIST_BASE : QTUBE_VIDEO_BASE,
service: filterType === 'playlists' ? 'PLAYLIST' : 'DOCUMENT',
offset: 0,
reverse: true,
limit: 20,
excludeBlocked: true,
name: filterName || '',
...searchOptions,
mode: 'ALL',
};
}, [
filterType,
filterName,
selectedSubCategoryVideos,
selectedCategoryVideos,
filterSearch,
]);
return (
<>
<Box sx={{ ...homeBaseSX, ...homeColumns }}>
<SearchSidebar onReset={resetState} />
<SearchSidebar onReset={resetState} />
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<TabContext value={tabValue}>
<TabList
onChange={changeTab}
textColor={"secondary"}
indicatorColor={"secondary"}
textColor={'secondary'}
indicatorColor={'secondary'}
centered={false}
>
<Tab label="All" value={"all"} sx={tabSX} />
<Tab label="Subscriptions" value={"subscriptions"} sx={tabSX} />
<Tab label="All" value={'all'} sx={tabSX} />
<Tab label="Subscriptions" value={'subscriptions'} sx={tabSX} />
</TabList>
<TabPanel value={"all"} sx={tabPaneSX}>
<VideoList listName="AllVideos" searchParameters={searchParameters} />
{/* <LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad> */}
<TabPanel value={'all'} sx={tabPaneSX}>
<VideoList
listName="AllVideos"
searchParameters={searchParameters}
/>
</TabPanel>
<TabPanel value={"subscriptions"} sx={tabPaneSX}>
<TabPanel value={'subscriptions'} sx={tabPaneSX}>
{filteredSubscriptionList.length > 0 ? (
<>
<VideoList listName="SubscribedVideos" searchParameters={searchParameters} />
{/* <LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad> */}
<VideoList
listName="SubscribedVideos"
searchParameters={searchParameters}
/>
</>
) : (
!isLoading && (
<div style={{ textAlign: "center" }}>
<div style={{ textAlign: 'center' }}>
You have no subscriptions
</div>
)

View File

@@ -0,0 +1,9 @@
import { atom } from 'jotai';
export enum EnumTheme {
LIGHT = 1,
DARK = 2,
}
// Atom to hold the current theme
export const themeAtom = atom<EnumTheme>(EnumTheme.DARK);

View File

@@ -4,35 +4,34 @@ import React, {
useCallback,
useRef,
useMemo,
} from "react";
import { useDispatch, useSelector } from "react-redux";
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
extractSigValue,
getPaymentInfo,
isTimestampWithinRange,
} from "../pages/ContentPages/VideoContent/VideoContent-State.ts";
} from '../pages/ContentPages/VideoContent/VideoContent-State.ts';
import { addUser } from "../state/features/authSlice";
import NavBar from "../components/layout/Navbar/Navbar";
import PageLoader from "../components/common/PageLoader";
import { RootState } from "../state/store";
import { addUser } from '../state/features/authSlice';
import NavBar from '../components/layout/Navbar/Navbar';
import PageLoader from '../components/common/PageLoader';
import { RootState } from '../state/store';
import {
setSuperlikesAll,
setUserAvatarHash,
} from "../state/features/globalSlice";
// import { VideoPlayerGlobal } from "../components/common/VideoPlayer/VideoPlayerGlobal.tsx";
import { Rnd } from "react-rnd";
import { RequestQueue } from "../utils/queue";
import { EditVideo } from "../components/Publish/EditVideo/EditVideo";
import { EditPlaylist } from "../components/Publish/EditPlaylist/EditPlaylist";
import ConsentModal from "../components/common/ConsentModal";
import { useFetchSuperLikes } from "../hooks/useFetchSuperLikes";
import { SUPER_LIKE_BASE } from "../constants/Identifiers.ts";
import { minPriceSuperLike } from "../constants/Misc.ts";
} from '../state/features/globalSlice';
import { Rnd } from 'react-rnd';
import { RequestQueue } from '../utils/queue';
import { EditVideo } from '../components/Publish/EditVideo/EditVideo';
import { EditPlaylist } from '../components/Publish/EditPlaylist/EditPlaylist';
import ConsentModal from '../components/common/ConsentModal';
import { useFetchSuperLikes } from '../hooks/useFetchSuperLikes';
import { SUPER_LIKE_BASE } from '../constants/Identifiers.ts';
import { minPriceSuperLike } from '../constants/Misc.ts';
import { useHandleNameData } from './../hooks/useHandleNameData.tsx';
import { namesAtom } from './../state/global/names';
import { useAtom } from 'jotai';
import { getPrimaryAccountName } from "../utils/qortalRequestFunctions.ts";
import { getPrimaryAccountName } from '../utils/qortalRequestFunctions.ts';
interface Props {
children: React.ReactNode;
@@ -44,21 +43,21 @@ export const queue = new RequestQueue();
export const queueSuperlikes = new RequestQueue();
const GlobalWrapper: React.FC<Props> = ({ children }) => {
const [theme, setTheme] = useState('dark')
const [theme, setTheme] = useState('dark');
const dispatch = useDispatch();
const isDragging = useRef(false);
const [userAvatar, setUserAvatar] = useState<string>("");
const [userAvatar, setUserAvatar] = useState<string>('');
const user = useSelector((state: RootState) => state.auth.user);
const { addSuperlikeRawDataGetToList } = useFetchSuperLikes();
const interval = useRef<any>(null);
useHandleNameData();
const videoPlaying = useSelector(
(state: RootState) => state.global.videoPlaying
);
const username = useMemo(() => {
if (!user?.name) return "";
if (!user?.name) return '';
return user.name;
}, [user]);
@@ -68,7 +67,6 @@ const GlobalWrapper: React.FC<Props> = ({ children }) => {
const getAvatar = React.useCallback(
async (author: string) => {
try {
const url = `/arbitrary/THUMBNAIL/${author}/qortal_avatar`;
if (url) {
@@ -98,7 +96,7 @@ const GlobalWrapper: React.FC<Props> = ({ children }) => {
const askForAccountInformation = React.useCallback(async () => {
try {
const account = await qortalRequest({
action: "GET_USER_ACCOUNT",
action: 'GET_USER_ACCOUNT',
});
const name = await getPrimaryAccountName(account.address);
@@ -141,9 +139,9 @@ const GlobalWrapper: React.FC<Props> = ({ children }) => {
while (validCount < 20 && totalCount < 100) {
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${SUPER_LIKE_BASE}&limit=1&offset=${totalCount}&includemetadata=true&reverse=true&excludeblocked=true`;
const response = await fetch(url, {
method: "GET",
method: 'GET',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
});
const responseData = await response.json();
@@ -171,7 +169,7 @@ const GlobalWrapper: React.FC<Props> = ({ children }) => {
...comments,
{
...comment,
message: "",
message: '',
amount: res.amount,
},
];
@@ -210,41 +208,16 @@ const GlobalWrapper: React.FC<Props> = ({ children }) => {
<>
{isLoadingGlobal && <PageLoader />}
<ConsentModal />
<EditVideo />
<EditPlaylist />
<EditVideo />
<EditPlaylist />
<NavBar
setTheme={(val: string) => setTheme(val)}
isAuthenticated={!!user?.name}
userName={user?.name || ""}
userName={user?.name || ''}
allNames={names}
userAvatar={userAvatar}
authenticate={askForAccountInformation}
/>
{/*<Rnd*/}
{/* onDragStart={onDragStart}*/}
{/* onDragStop={onDragStop}*/}
{/* style={{*/}
{/* display: videoPlaying ? "block" : "none",*/}
{/* position: "fixed",*/}
{/* height: "auto",*/}
{/* width: 350,*/}
{/* zIndex: 1000,*/}
{/* maxWidth: 800,*/}
{/* }}*/}
{/* default={{*/}
{/* x: 0,*/}
{/* y: 60,*/}
{/* width: 350,*/}
{/* height: "auto",*/}
{/* }}*/}
{/* // eslint-disable-next-line @typescript-eslint/no-empty-function*/}
{/* onDrag={() => {}}*/}
{/*>*/}
{/* {videoPlaying && (*/}
{/* <VideoPlayerGlobal checkIfDrag={checkIfDrag} element={videoPlaying} />*/}
{/* )}*/}
{/*</Rnd>*/}
{children}
</>