added tutorials

This commit is contained in:
PhilReact 2024-12-18 10:44:21 +02:00
parent ea3e102bba
commit 98785e0571
8 changed files with 1100 additions and 6 deletions

6
package-lock.json generated
View File

@ -83,6 +83,7 @@
"slate-react": "^0.109.0", "slate-react": "^0.109.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tiptap-extension-resize-image": "^1.1.8", "tiptap-extension-resize-image": "^1.1.8",
"ts-key-enum": "^2.0.12",
"vite-plugin-top-level-await": "^1.4.4", "vite-plugin-top-level-await": "^1.4.4",
"vite-plugin-wasm": "^3.3.0" "vite-plugin-wasm": "^3.3.0"
}, },
@ -16224,6 +16225,11 @@
"typescript": ">=4.2.0" "typescript": ">=4.2.0"
} }
}, },
"node_modules/ts-key-enum": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/ts-key-enum/-/ts-key-enum-2.0.13.tgz",
"integrity": "sha512-zixs6j8+NhzazLUQ1SiFrlo1EFWG/DbqLuUGcWWZ5zhwjRT7kbi1hBlofxdqel+h28zrby2It5TrOyKp04kvqw=="
},
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.6.2", "version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",

View File

@ -88,7 +88,8 @@
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tiptap-extension-resize-image": "^1.1.8", "tiptap-extension-resize-image": "^1.1.8",
"vite-plugin-top-level-await": "^1.4.4", "vite-plugin-top-level-await": "^1.4.4",
"vite-plugin-wasm": "^3.3.0" "vite-plugin-wasm": "^3.3.0",
"ts-key-enum": "^2.0.12"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/dom": "^10.3.0", "@testing-library/dom": "^10.3.0",

View File

@ -80,6 +80,8 @@ import { LoadingButton } from "@mui/lab";
import { Label } from "./components/Group/AddGroup"; import { Label } from "./components/Group/AddGroup";
import { CustomizedSnackbars } from "./components/Snackbar/Snackbar"; import { CustomizedSnackbars } from "./components/Snackbar/Snackbar";
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
import HelpIcon from '@mui/icons-material/Help';
import { import {
cleanUrl, cleanUrl,
getFee, getFee,
@ -128,6 +130,8 @@ import { CoreSyncStatus } from "./components/CoreSyncStatus";
import { Wallets } from "./Wallets"; import { Wallets } from "./Wallets";
import { RandomSentenceGenerator } from "./utils/seedPhrase/RandomSentenceGenerator"; import { RandomSentenceGenerator } from "./utils/seedPhrase/RandomSentenceGenerator";
import { useFetchResources } from "./common/useFetchResources"; import { useFetchResources } from "./common/useFetchResources";
import { Tutorials } from "./components/Tutorials/Tutorials";
import { useHandleTutorials } from "./components/Tutorials/useHandleTutorials";
type extStates = type extStates =
| "not-authenticated" | "not-authenticated"
@ -247,8 +251,12 @@ export const resumeAllQueues = () => {
}; };
const defaultValuesGlobal = {
openTutorialModal: null,
setOpenTutorialModal: ()=> {}
}
export const MyContext = createContext<MyContextInterface>(defaultValues); export const MyContext = createContext<MyContextInterface>(defaultValues);
export const GlobalContext = createContext<MyContextInterface>(defaultValuesGlobal);
export let globalApiKey: string | null = null; export let globalApiKey: string | null = null;
@ -339,6 +347,7 @@ function App() {
const {downloadResource} = useFetchResources() const {downloadResource} = useFetchResources()
const holdRefExtState = useRef<extStates>("not-authenticated"); const holdRefExtState = useRef<extStates>("not-authenticated");
const isFocusedRef = useRef<boolean>(true); const isFocusedRef = useRef<boolean>(true);
const {showTutorial, openTutorialModal, shownTutorialsInitiated, setOpenTutorialModal} = useHandleTutorials()
const { isShow, onCancel, onOk, show, message } = useModal(); const { isShow, onCancel, onOk, show, message } = useModal();
const { const {
isShow: isShowUnsavedChanges, isShow: isShowUnsavedChanges,
@ -412,6 +421,17 @@ function App() {
} }
}, []); }, []);
useEffect(()=> {
if(!shownTutorialsInitiated) return
if(extState === 'not-authenticated'){
showTutorial('create-account')
} else if(extState === "create-wallet" && walletToBeDownloaded){
showTutorial('important-information')
} else if(extState === "authenticated"){
showTutorial('getting-started')
}
}, [extState, walletToBeDownloaded, shownTutorialsInitiated])
useEffect(() => { useEffect(() => {
// Attach a global event listener for double-click // Attach a global event listener for double-click
const handleDoubleClick = () => { const handleDoubleClick = () => {
@ -974,7 +994,7 @@ function App() {
message: message:
"Your settings have changed. If you logout you will lose your changes. Click on the save button in the header to keep your changed settings.", "Your settings have changed. If you logout you will lose your changes. Click on the save button in the header to keep your changed settings.",
}); });
} else { } else if(extState === 'authenticated') {
await showUnsavedChanges({ await showUnsavedChanges({
message: message:
"Are you sure you would like to logout?", "Are you sure you would like to logout?",
@ -1541,6 +1561,23 @@ function App() {
alignItems: 'center' alignItems: 'center'
}} }}
> >
{(desktopViewMode === "apps" || desktopViewMode === "home") && (
<ButtonBase onClick={()=> {
if(desktopViewMode === "apps"){
showTutorial('qapps', true)
} else {
showTutorial('create-account', true)
}
}} >
<HelpIcon sx={{
color: 'var(--unread)'
}} />
</ButtonBase>
)}
<Spacer height="20px" />
<img <img
onClick={() => { onClick={() => {
setExtstate("download-wallet"); setExtstate("download-wallet");
@ -1569,6 +1606,13 @@ function App() {
// backgroundRepeat: desktopViewMode === "apps" && "no-repeat", // backgroundRepeat: desktopViewMode === "apps" && "no-repeat",
}} }}
> >
<GlobalContext.Provider value={{
showTutorial,
openTutorialModal,
setOpenTutorialModal,
downloadResource
}}>
<Tutorials />
{extState === "not-authenticated" && ( {extState === "not-authenticated" && (
<NotAuthenticated <NotAuthenticated
getRootProps={getRootProps} getRootProps={getRootProps}
@ -1602,7 +1646,8 @@ function App() {
setOpenSnackGlobal: setOpenSnack, setOpenSnackGlobal: setOpenSnack,
infoSnackCustom: infoSnack, infoSnackCustom: infoSnack,
setInfoSnackCustom: setInfoSnack, setInfoSnackCustom: setInfoSnack,
downloadResource downloadResource,
}} }}
> >
<Box <Box
@ -3174,6 +3219,21 @@ function App() {
> >
{renderProfile()} {renderProfile()}
</DrawerComponent> </DrawerComponent>
</GlobalContext.Provider>
{extState === "create-wallet" && walletToBeDownloaded && (
<ButtonBase onClick={()=> {
showTutorial('important-information', true)
}} sx={{
position: 'fixed',
bottom: '25px',
right: '25px'
}}>
<HelpIcon sx={{
color: 'var(--unread)'
}} />
</ButtonBase>
)}
</AppContainer> </AppContainer>
); );
} }

View File

@ -1,9 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useContext, useEffect, useRef, useState } from "react";
import { Spacer } from "../common/Spacer"; import { Spacer } from "../common/Spacer";
import { CustomButton, TextItalic, TextP, TextSpan } from "../App-styles"; import { CustomButton, TextItalic, TextP, TextSpan } from "../App-styles";
import { import {
Box, Box,
Button, Button,
ButtonBase,
Checkbox, Checkbox,
Dialog, Dialog,
DialogActions, DialogActions,
@ -18,9 +19,11 @@ import {
import Logo1 from "../assets/svgs/Logo1.svg"; import Logo1 from "../assets/svgs/Logo1.svg";
import Logo1Dark from "../assets/svgs/Logo1Dark.svg"; import Logo1Dark from "../assets/svgs/Logo1Dark.svg";
import Info from "../assets/svgs/Info.svg"; import Info from "../assets/svgs/Info.svg";
import HelpIcon from '@mui/icons-material/Help';
import { CustomizedSnackbars } from "../components/Snackbar/Snackbar"; import { CustomizedSnackbars } from "../components/Snackbar/Snackbar";
import { set } from "lodash"; import { set } from "lodash";
import { cleanUrl, isUsingLocal } from "../background"; import { cleanUrl, isUsingLocal } from "../background";
import { GlobalContext } from "../App";
const manifestData = { const manifestData = {
version: "0.3.8", version: "0.3.8",
@ -53,6 +56,8 @@ export const NotAuthenticated = ({
const [customApikey, setCustomApiKey] = React.useState(""); const [customApikey, setCustomApiKey] = React.useState("");
const [customNodeToSaveIndex, setCustomNodeToSaveIndex] = const [customNodeToSaveIndex, setCustomNodeToSaveIndex] =
React.useState(null); React.useState(null);
const { showTutorial } = useContext(GlobalContext);
const importedApiKeyRef = useRef(null); const importedApiKeyRef = useRef(null);
const currentNodeRef = useRef(null); const currentNodeRef = useRef(null);
const hasLocalNodeRef = useRef(null); const hasLocalNodeRef = useRef(null);
@ -291,6 +296,7 @@ export const NotAuthenticated = ({
WELCOME TO <TextItalic>YOUR</TextItalic> <br></br> WELCOME TO <TextItalic>YOUR</TextItalic> <br></br>
<TextSpan> QORTAL WALLET</TextSpan> <TextSpan> QORTAL WALLET</TextSpan>
</TextP> </TextP>
<Spacer height="30px" /> <Spacer height="30px" />
<Box <Box
sx={{ sx={{
@ -686,6 +692,17 @@ export const NotAuthenticated = ({
</DialogActions> </DialogActions>
</Dialog> </Dialog>
)} )}
<ButtonBase onClick={()=> {
showTutorial('getting-started', true)
}} sx={{
position: 'fixed',
bottom: '25px',
right: '25px'
}}>
<HelpIcon sx={{
color: 'var(--unread)'
}} />
</ButtonBase>
</> </>
); );
}; };

View File

@ -1,7 +1,7 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { AppsHomeDesktop } from "./AppsHomeDesktop"; import { AppsHomeDesktop } from "./AppsHomeDesktop";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { MyContext, getBaseApiReact } from "../../App"; import { GlobalContext, MyContext, getBaseApiReact } from "../../App";
import { AppInfo } from "./AppInfo"; import { AppInfo } from "./AppInfo";
import { import {
executeEvent, executeEvent,
@ -39,6 +39,8 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
const [categories, setCategories] = useState([]) const [categories, setCategories] = useState([])
const iframeRefs = useRef({}); const iframeRefs = useRef({});
const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom) const [isEnabledDevMode, setIsEnabledDevMode] = useRecoilState(enabledDevModeAtom)
const { showTutorial } = useContext(GlobalContext);
const myApp = useMemo(()=> { const myApp = useMemo(()=> {
return availableQapps.find((app)=> app.name === myName && app.service === 'APP') return availableQapps.find((app)=> app.name === myName && app.service === 'APP')
@ -48,6 +50,13 @@ export const AppsDesktop = ({ mode, setMode, show , myName, goToHome, setDesktop
return availableQapps.find((app)=> app.name === myName && app.service === 'WEBSITE') return availableQapps.find((app)=> app.name === myName && app.service === 'WEBSITE')
}, [myName, availableQapps]) }, [myName, availableQapps])
useEffect(()=> {
if(show){
showTutorial('qapps')
}
}, [show])
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
executeEvent("setTabsToNav", { executeEvent("setTabsToNav", {

View File

@ -0,0 +1,724 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
import ReactDOM from 'react-dom'
import { Box, IconButton, Slider } from '@mui/material'
import { CircularProgress, Typography } from '@mui/material'
import { Key } from 'ts-key-enum'
import {
PlayArrow,
Pause,
VolumeUp,
Fullscreen,
PictureInPicture, VolumeOff, Calculate
} from '@mui/icons-material'
import { styled } from '@mui/system'
import { Refresh } from '@mui/icons-material'
import { Menu, MenuItem } from '@mui/material'
import { MoreVert as MoreIcon } from '@mui/icons-material'
import { GlobalContext, getBaseApiReact } from '../../App'
import { resourceKeySelector } from '../../atoms/global'
import { useRecoilValue } from 'recoil'
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
}
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
poster,
name,
identifier,
service,
autoplay = true,
from = null,
customStyle = {},
node
}) => {
const keyIdentifier = useMemo(()=> {
if(name && identifier && service){
return `${service}-${name}-${identifier}`
} else {
return undefined
}
}, [service, name, identifier])
const download = useRecoilValue(resourceKeySelector(keyIdentifier));
const { downloadResource } = useContext(GlobalContext);
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 reDownload = useRef<boolean>(false)
const resetVideoState = () => {
// Reset all states to their initial values
setPlaying(false);
setVolume(1);
setMutedVolume(1);
setIsMuted(false);
setProgress(0);
setIsLoading(false);
setCanPlay(false);
setStartPlay(false);
setIsMobileView(false);
setPlaybackRate(1);
setAnchorEl(null);
// Reset refs to their initial values
if (videoRef.current) {
videoRef.current.pause(); // Ensure the video is paused
videoRef.current.currentTime = 0; // Reset video progress
}
reDownload.current = false;
};
const src = useMemo(() => {
if(name && identifier && service){
return `${node || getBaseApiReact()}/arbitrary/${service}/${name}/${identifier}`
}
return ''
}, [service, name, identifier])
useEffect(()=> {
resetVideoState()
}, [keyIdentifier])
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
let newSpeed = wrapOverflow ? changedSpeed : Math.min(changedSpeed, maxSpeed)
if (videoRef.current) {
updatePlaybackRate(newSpeed);
}
}
const decreaseSpeed = () => {
if (videoRef.current) {
updatePlaybackRate(playbackRate - speedChange);
}
}
const togglePlay = async () => {
if (!videoRef.current) return
setStartPlay(true)
if (!src || resourceStatus?.status !== 'READY') {
ReactDOM.flushSync(() => {
setIsLoading(true)
})
getSrc()
}
if (playing) {
videoRef.current.pause()
} else {
videoRef.current.play()
}
setPlaying(!playing)
}
const onVolumeChange = (_: any, value: number | number[]) => {
if (!videoRef.current) return
videoRef.current.volume = value as number
setVolume(value as number)
setIsMuted(false)
}
const onProgressChange = (_: any, value: number | number[]) => {
if (!videoRef.current) return
videoRef.current.currentTime = value as number
setProgress(value as number)
if (!playing) {
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()
}
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement)
}
document.addEventListener('fullscreenchange', handleFullscreenChange)
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange)
}
}, [])
const handleCanPlay = () => {
setIsLoading(false)
setCanPlay(true)
}
const getSrc = React.useCallback(async () => {
if (!name || !identifier || !service) return
try {
downloadResource({
name,
service,
identifier
})
} catch (error) {
console.error(error)
}
}, [identifier, name, service])
function formatTime(seconds: number): string {
seconds = Math.floor(seconds)
let minutes: number | string = Math.floor(seconds / 60)
let hours: number | string = Math.floor(minutes / 60)
let remainingSeconds: number | string = seconds % 60
let remainingMinutes: number | string = minutes % 60
if (remainingSeconds < 10) {
remainingSeconds = '0' + remainingSeconds
}
if (remainingMinutes < 10) {
remainingMinutes = '0' + remainingMinutes
}
if (hours === 0) {
hours = ''
}
else {
hours = hours + ':'
}
return hours + remainingMinutes + ':' + remainingSeconds
}
const reloadVideo = () => {
if (!videoRef.current) return
const currentTime = videoRef.current.currentTime
videoRef.current.src = src
videoRef.current.load()
videoRef.current.currentTime = currentTime
if (playing) {
videoRef.current.play()
}
}
useEffect(() => {
if (
resourceStatus?.status === 'DOWNLOADED' &&
reDownload?.current === false
) {
getSrc()
reDownload.current = true
}
}, [getSrc, resourceStatus])
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;
}
}
console.log('!src && !isLoading) || !startPlay', startPlay, resourceStatus?.status === 'READY')
return (
<VideoContainer
tabIndex={0}
onKeyUp={keyboardShortcutsUp}
onKeyDown={keyboardShortcutsDown}
style={{
padding: from === 'create' ? '8px' : 0,
width: '100%',
height: '100%',
}}
>
{isLoading && (
<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'
}}
>
<CircularProgress color="secondary" />
{resourceStatus && (
<Typography
variant="subtitle2"
component="div"
sx={{
color: 'white',
fontSize: '15px',
textAlign: 'center'
}}
>
{resourceStatus?.status === 'REFETCHING' ? (
<>
<>
{getDownloadProgress(resourceStatus?.localChunkCount, resourceStatus?.totalChunkCount)}
</>
<> Refetching data in 25 seconds</>
</>
) : resourceStatus?.status === 'DOWNLOADED' ? (
<>Download Completed: building tutorial video...</>
) : resourceStatus?.status !== 'READY' ? (
<>
{getDownloadProgress(resourceStatus?.localChunkCount, resourceStatus?.totalChunkCount)}
</>
) : (
<>Fetching tutorial from the Qortal Network...</>
)}
</Typography>
)}
</Box>
)}
{((!src && !isLoading) || !startPlay) && (
<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={() => {
togglePlay()
}}
sx={{
cursor: 'pointer'
}}
>
<PlayArrow
sx={{
width: '50px',
height: '50px',
color: 'white'
}}
/>
</Box>
)}
<Box sx={{
display: 'flex',
flexGrow: 1,
width: '100%',
height: 'calc(100% - 60px)',
}}>
<VideoElement
id={identifier}
ref={videoRef}
src={!startPlay ? '' : resourceStatus?.status === 'READY' ? src : ''}
poster={!startPlay ? poster : ""}
onTimeUpdate={updateProgress}
autoPlay={autoplay}
onClick={togglePlay}
onEnded={handleEnded}
// onLoadedMetadata={handleLoadedMetadata}
onCanPlay={handleCanPlay}
preload="metadata"
style={{
width: '100%',
height: '100%',
...customStyle
}}
/>
</Box>
<ControlsContainer
sx={{
position: 'relative',
background: 'var(--bg-primary)',
width: '100%',
flexShrink: 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={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, color: 'var(--Mail-Background)' }}
/>
<Typography
sx={{
fontSize: '14px',
marginRight: '5px',
color: 'rgba(255, 255, 255, 0.7)',
visibility:
!videoRef.current?.duration || !progress
? 'hidden'
: 'visible',
flexShrink: 0
}}
>
{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',
color: 'var(--Mail-Background)'
}}
/>
<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)'
}}
onClick={toggleFullscreen}
>
<Fullscreen />
</IconButton>
</>
) : null}
</ControlsContainer>
</VideoContainer>
)
}

View File

@ -0,0 +1,108 @@
import React, { useContext, useState } from 'react'
import { GlobalContext, MyContext } from '../../App';
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Tab, Tabs, Typography } from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { VideoPlayer } from '../Embeds/VideoPlayer';
export const Tutorials = () => {
const { openTutorialModal, setOpenTutorialModal } = useContext(GlobalContext);
const [multiNumber, setMultiNumber] = useState(0)
const handleClose = ()=> {
setOpenTutorialModal(null)
setMultiNumber(0)
}
if(!openTutorialModal) return null
if(openTutorialModal?.multi){
const selectedTutorial = openTutorialModal?.multi[multiNumber]
return (
<Dialog
onClose={handleClose}
aria-labelledby="customized-dialog-title"
open={!!openTutorialModal}
fullWidth={true}
maxWidth="xl"
>
<Tabs sx={{
"& .MuiTabs-indicator": {
backgroundColor: "white",
},
}} value={multiNumber} onChange={(e, value)=> setMultiNumber(value)} aria-label="basic tabs example">
{openTutorialModal?.multi?.map((item, index)=> {
return (
<Tab sx={{
"&.Mui-selected": {
color: "white",
},
}} label={item?.title} value={index} />
)
})}
</Tabs>
<DialogTitle sx={{ m: 0, p: 2 }} >
{selectedTutorial?.title} {` Tutorial`}
</DialogTitle>
<IconButton
aria-label="close"
onClick={handleClose}
sx={(theme) => ({
position: 'absolute',
right: 8,
top: 8,
color: theme.palette.grey[500],
})}
>
<CloseIcon />
</IconButton>
<DialogContent dividers sx={{
height: '85vh'
}}>
<VideoPlayer node="https://ext-node.qortal.link" {...selectedTutorial?.resource || {}} />
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={handleClose}>
Close
</Button>
</DialogActions>
</Dialog>
)
}
return (
<>
<Dialog
onClose={handleClose}
aria-labelledby="customized-dialog-title"
open={!!openTutorialModal}
fullWidth={true}
maxWidth="xl"
>
<DialogTitle sx={{ m: 0, p: 2 }} >
{openTutorialModal?.title} {` Tutorial`}
</DialogTitle>
<IconButton
aria-label="close"
onClick={handleClose}
sx={(theme) => ({
position: 'absolute',
right: 8,
top: 8,
color: theme.palette.grey[500],
})}
>
<CloseIcon />
</IconButton>
<DialogContent dividers sx={{
height: '85vh'
}}>
<VideoPlayer node="https://ext-node.qortal.link" {...openTutorialModal?.resource || {}} />
</DialogContent>
<DialogActions>
<Button variant="contained" onClick={handleClose}>
Close
</Button>
</DialogActions>
</Dialog>
</>
)
}

View File

@ -0,0 +1,169 @@
import React, { useCallback, useEffect, useState } from "react";
import { saveToLocalStorage } from "../Apps/AppsNavBar";
const checkIfGatewayIsOnline = async () => {
try {
const url = `https://ext-node.qortal.link/admin/status`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
if (data?.height) {
return true
}
return false
} catch (error) {
return false
}
}
export const useHandleTutorials = () => {
const [openTutorialModal, setOpenTutorialModal] = useState<any>(null);
const [shownTutorials, setShowTutorials] = useState(null)
useEffect(()=> {
try {
const storedData = localStorage.getItem('shown-tutorials');
if (storedData) {
setShowTutorials(JSON.parse(storedData));
} else {
setShowTutorials({})
}
} catch (error) {
//error
}
}, [])
const saveShowTutorial = useCallback((type)=> {
try {
setShowTutorials((prev)=> {
return {
...(prev || {}),
[type]: true
}
})
saveToLocalStorage('shown-tutorials', type, true)
} catch (error) {
//error
}
}, [])
const showTutorial = useCallback(async (type, isForce) => {
try {
const isOnline = await checkIfGatewayIsOnline()
if(!isOnline) return
switch (type) {
case "create-account":
{
if((shownTutorials || {})['create-account'] && !isForce) return
saveShowTutorial('create-account')
setOpenTutorialModal({
title: "Account Creation",
resource: {
name: "a-test",
service: "VIDEO",
identifier: "account-creation-hub",
},
});
}
break;
case "important-information":
{
if((shownTutorials || {})['important-information'] && !isForce) return
saveShowTutorial('important-information')
setOpenTutorialModal({
title: "Important Information!",
resource: {
name: "a-test",
service: "VIDEO",
identifier: "important-information-hub",
},
});
}
break;
case "getting-started":
{
if((shownTutorials || {})['getting-started'] && !isForce) return
saveShowTutorial('getting-started')
setOpenTutorialModal({
multi: [
{
title: "Getting Started",
resource: {
name: "a-test",
service: "VIDEO",
identifier: "getting-started-hub",
},
},
{
title: "Overview",
resource: {
name: "a-test",
service: "VIDEO",
identifier: "overview-hub",
},
},
{
title: "Qortal Groups",
resource: {
name: "a-test",
service: "VIDEO",
identifier: "groups-hub",
},
},
],
});
}
break;
case "qapps":
{
if((shownTutorials || {})['qapps'] && !isForce) return
saveShowTutorial('qapps')
setOpenTutorialModal({
multi: [
{
title: "Apps Dashboard",
resource: {
name: "a-test",
service: "VIDEO",
identifier: "apps-dashboard-hub",
},
},
{
title: "Apps Navigation",
resource: {
name: "a-test",
service: "VIDEO",
identifier: "apps-navigation-hub",
},
}
],
});
}
break;
default:
break;
}
} catch (error) {
//error
}
}, [shownTutorials]);
return {
showTutorial,
openTutorialModal,
setOpenTutorialModal,
shownTutorialsInitiated: !!shownTutorials
};
};