added tutorials to mobile

This commit is contained in:
PhilReact 2024-12-19 11:47:05 +02:00
parent 863be97769
commit e3a9157b23
13 changed files with 1159 additions and 18 deletions

10
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "qortal-go",
"version": "0.3.6",
"version": "0.3.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "qortal-go",
"version": "0.3.6",
"version": "0.3.8",
"dependencies": {
"@capacitor/android": "^6.1.2",
"@capacitor/app": "^6.0.1",
@ -79,6 +79,7 @@
"slate-react": "^0.109.0",
"tippy.js": "^6.3.7",
"tiptap-extension-resize-image": "^1.1.8",
"ts-key-enum": "^2.0.12",
"vite-plugin-top-level-await": "^1.4.4",
"vite-plugin-wasm": "^3.3.0"
},
@ -13285,6 +13286,11 @@
"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": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",

View File

@ -84,7 +84,8 @@
"tippy.js": "^6.3.7",
"tiptap-extension-resize-image": "^1.1.8",
"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": {
"@testing-library/dom": "^10.3.0",

View File

@ -45,6 +45,8 @@ import CloseIcon from "@mui/icons-material/Close";
import { FilePicker } from '@capawesome/capacitor-file-picker';
import './utils/seedPhrase/RandomSentenceGenerator';
import { useFetchResources } from "./common/useFetchResources";
import HelpIcon from '@mui/icons-material/Help';
import {
createAccount,
generateRandomSentence,
@ -121,6 +123,8 @@ import { openIndexedDB, showSaveFilePicker } from "./components/Apps/useQortalM
import { fileToBase64 } from "./utils/fileReading";
import { handleGetFileFromIndexedDB } from "./utils/indexedDB";
import { Wallets } from "./Wallets";
import { useHandleTutorials } from "./components/Tutorials/useHandleTutorials";
import { Tutorials } from "./components/Tutorials/Tutorials";
type extStates =
@ -302,7 +306,12 @@ export const resumeAllQueues = () => {
});
};
const defaultValuesGlobal = {
openTutorialModal: null,
setOpenTutorialModal: ()=> {}
}
export const MyContext = createContext<MyContextInterface>(defaultValues);
export const GlobalContext = createContext<any>(defaultValuesGlobal);
export let globalApiKey: string | null = null;
@ -391,6 +400,7 @@ function App() {
const [hasSettingsChanged, setHasSettingsChanged] = useRecoilState(
hasSettingsChangedAtom
);
const {showTutorial, openTutorialModal, shownTutorialsInitiated, setOpenTutorialModal} = useHandleTutorials()
const holdRefExtState = useRef<extStates>("not-authenticated");
const isFocusedRef = useRef<boolean>(true);
const { isShow, onCancel, onOk, show, message } = useModal();
@ -458,6 +468,16 @@ 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(() => {
// Attach a global event listener for double-click
const handleDoubleClick = () => {
@ -1651,7 +1671,7 @@ function App() {
</AuthenticatedContainer>
);
};
console.log('openTutorialModal3', openTutorialModal)
return (
<AppContainer
sx={{
@ -1662,6 +1682,13 @@ function App() {
backgroundRepeat: desktopViewMode === "apps" && "no-repeat",
}}
>
<GlobalContext.Provider value={{
showTutorial,
openTutorialModal,
setOpenTutorialModal,
downloadResource
}}>
<Tutorials />
{extState === "not-authenticated" && (
<NotAuthenticated
getRootProps={getRootProps}
@ -3256,6 +3283,20 @@ await showInfo({
>
{renderProfile()}
</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>
);
}

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 { CustomButton, TextItalic, TextP, TextSpan } from "../App-styles";
import {
Box,
Button,
ButtonBase,
Checkbox,
Dialog,
DialogActions,
@ -21,6 +22,8 @@ import Info from "../assets/svgs/Info.svg";
import { CustomizedSnackbars } from "../components/Snackbar/Snackbar";
import { set } from "lodash";
import { cleanUrl, isUsingLocal } from "../background";
import HelpIcon from '@mui/icons-material/Help';
import { GlobalContext } from "../App";
export const manifestData = {
version: "0.3.8",
@ -48,6 +51,8 @@ export const NotAuthenticated = ({
const [currentNode, setCurrentNode] = React.useState({
url: "http://127.0.0.1:12391",
});
const { showTutorial } = useContext(GlobalContext);
const [importedApiKey, setImportedApiKey] = React.useState(null);
//add and edit states
const [url, setUrl] = React.useState("http://");
@ -675,6 +680,17 @@ export const NotAuthenticated = ({
</DialogActions>
</Dialog>
)}
<ButtonBase onClick={()=> {
showTutorial('create-account', true)
}} sx={{
position: 'fixed',
bottom: '25px',
right: '25px'
}}>
<HelpIcon sx={{
color: 'var(--unread)'
}} />
</ButtonBase>
</>
);
};

View File

@ -1,10 +1,11 @@
import React, { useCallback } from 'react';
import React, { useCallback, useRef } from 'react';
import { useRecoilState } from 'recoil';
import { resourceDownloadControllerAtom } from '../atoms/global';
import { getBaseApiReact } from '../App';
export const useFetchResources = () => {
const [resources, setResources] = useRecoilState(resourceDownloadControllerAtom);
const intervalId = useRef(null)
const downloadResource = useCallback(({ service, name, identifier }, build) => {
setResources((prev) => ({
@ -21,9 +22,10 @@ export const useFetchResources = () => {
let isCalling = false;
let percentLoaded = 0;
let timer = 24;
let tries = 26;
let calledFirstTime = false
const intervalId = setInterval(async () => {
const callFunction = async ()=> {
if (isCalling) return;
isCalling = true;
@ -40,6 +42,24 @@ export const useFetchResources = () => {
},
});
res = await resCall.json()
if(tries > 18 ){
if(intervalId?.current){
clearInterval(intervalId?.current)
}
setResources((prev) => ({
...prev,
[`${service}-${name}-${identifier}`]: {
...(prev[`${service}-${name}-${identifier}`] || {}),
status: {
...res,
status: 'FAILED_TO_DOWNLOAD',
},
},
}));
return
}
tries = tries + 1
}
@ -103,7 +123,10 @@ export const useFetchResources = () => {
// Check if progress is 100% and clear interval if true
if (res?.status === 'READY') {
clearInterval(intervalId);
if(intervalId.current){
clearInterval(intervalId.current);
}
// Update Recoil state for completion
setResources((prev) => ({
@ -114,7 +137,12 @@ export const useFetchResources = () => {
},
}));
}
}, !calledFirstTime ? 100 :5000);
}
callFunction()
intervalId.current = setInterval(async () => {
callFunction()
}, 5000);
} catch (error) {
console.error('Error during resource fetch:', error);
}

View File

@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import { AppsHome } from "./AppsHome";
import { Spacer } from "../../common/Spacer";
import { getBaseApiReact } from "../../App";
import { GlobalContext, getBaseApiReact } from "../../App";
import { AppInfo } from "./AppInfo";
import {
executeEvent,
@ -25,9 +25,15 @@ export const Apps = ({ mode, setMode, show , myName}) => {
const [selectedTab, setSelectedTab] = useState(null);
const [isNewTabWindow, setIsNewTabWindow] = useState(false);
const [categories, setCategories] = useState([])
const { showTutorial } = useContext(GlobalContext);
const iframeRefs = useRef({});
useEffect(()=> {
if(show){
showTutorial('qapps')
}
}, [show])
const myApp = useMemo(()=> {
return availableQapps.find((app)=> app.name === myName && app.service === 'APP')

View File

@ -1,4 +1,4 @@
import React, { useMemo, useState } from "react";
import React, { useContext, useMemo, useState } from "react";
import {
AppCircle,
AppCircleContainer,
@ -9,16 +9,20 @@ import {
} from "./Apps-styles";
import { Avatar, Box, ButtonBase, Input } from "@mui/material";
import { Add } from "@mui/icons-material";
import { getBaseApiReact, isMobile } from "../../App";
import { GlobalContext, getBaseApiReact, isMobile } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { executeEvent } from "../../utils/events";
import { SortablePinnedApps } from "./SortablePinnedApps";
import { Spacer } from "../../common/Spacer";
import ArrowOutwardIcon from '@mui/icons-material/ArrowOutward';
import { extractComponents } from "../Chat/MessageDisplay";
import HelpIcon from '@mui/icons-material/Help';
import { useHandleTutorials } from "../Tutorials/useHandleTutorials";
export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps }) => {
const [qortalUrl, setQortalUrl] = useState('')
const { showTutorial } = useContext(GlobalContext);
const openQortalUrl = ()=> {
try {
@ -40,8 +44,24 @@ export const AppsHome = ({ setMode, myApp, myWebsite, availableQapps }) => {
sx={{
justifyContent: "flex-start",
position: 'relative'
}}
>
<ButtonBase sx={{
position: 'absolute',
top: '0px',
right: '0px'
}} onClick={()=> {
showTutorial('qapps', true)
}} >
<HelpIcon sx={{
color: 'var(--unread)',
fontSize: '18px'
}} />
</ButtonBase>
<AppLibrarySubTitle
>

View File

@ -297,7 +297,7 @@ export const AttachmentCard = ({
</>
)}
{resourceDetails && resourceDetails?.status?.status !== 'READY' && (
{resourceDetails && resourceDetails?.status?.status !== 'READY' && resourceDetails?.status?.status !== 'FAILED_TO_DOWNLOAD' && (
<>
<CircularProgress sx={{
color: 'white'

View File

@ -0,0 +1,723 @@
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;
}
}
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="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
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

@ -1,5 +1,5 @@
import { Box, Button, Typography } from "@mui/material";
import React from "react";
import { Box, Button, ButtonBase, Typography } from "@mui/material";
import React, { useContext } from "react";
import { Spacer } from "../../common/Spacer";
import { ListOfThreadPostsWatched } from "./ListOfThreadPostsWatched";
import { ThingsToDoInitial } from "./ThingsToDoInitial";
@ -7,6 +7,9 @@ import { GroupJoinRequests } from "./GroupJoinRequests";
import { GroupInvites } from "./GroupInvites";
import RefreshIcon from "@mui/icons-material/Refresh";
import { ListOfGroupPromotions } from "./ListOfGroupPromotions";
import HelpIcon from '@mui/icons-material/Help';
import { useHandleTutorials } from "../Tutorials/useHandleTutorials";
import { GlobalContext } from "../../App";
export const Home = ({
refreshHomeDataFunc,
@ -22,6 +25,8 @@ export const Home = ({
setOpenAddGroup,
setMobileViewMode,
}) => {
const { showTutorial } = useContext(GlobalContext);
return (
<Box
sx={{
@ -31,8 +36,27 @@ export const Home = ({
height: "100%",
overflow: "auto",
alignItems: "center",
position: 'relative'
}}
>
<ButtonBase sx={{
position: 'absolute',
top: '5px',
right: '5px'
}} onClick={()=> {
showTutorial('getting-started', true)
}} >
<HelpIcon sx={{
color: 'var(--unread)',
fontSize: '18px'
}} />
</ButtonBase>
<Spacer height="20px" />
<Typography
sx={{

View File

@ -0,0 +1,95 @@
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={false}
fullScreen
sx={{
margin: 0,
padding: 0
}}
>
<Tabs variant='scrollable' 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",
},
fontSize:'0.75rem'
}} label={item?.title} value={index} />
)
})}
</Tabs>
<DialogTitle sx={{ m: 0, p: 2, fontSize: '14px' }} >
{selectedTutorial?.title} {` Tutorial`}
</DialogTitle>
<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={false}
fullScreen
sx={{
margin: 0,
padding: 0
}}
>
<DialogTitle sx={{ m: 0, p: 2, fontSize: '14px' }} >
{openTutorialModal?.title} {` Tutorial`}
</DialogTitle>
<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,180 @@
import React, { useCallback, useEffect, useState } from "react";
import { saveToLocalStorage } from "../Apps/AppsNavBar";
import { getData, storeData } from "../../utils/chromeStorage";
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(()=> {
const getSavedData = async()=> {
try {
const storedData = await getData<any>("shown-tutorials").catch(() => {});
if (storedData) {
setShowTutorials(storedData);
} else {
setShowTutorials({})
}
} catch (error) {
//error
}
}
getSavedData()
}, [])
const saveShowTutorial = useCallback(async (type)=> {
try {
setShowTutorials((prev)=> {
return {
...(prev || {}),
[type]: true
}
})
const storedData = await getData<any>("shown-tutorials").catch(() => {});
const newData = {
...storedData,
[type]: true
}
await storeData("shown-tutorials", newData)
} 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-go",
},
});
}
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-go",
},
});
}
break;
case "getting-started":
{
if((shownTutorials || {})['getting-started'] && !isForce) return
saveShowTutorial('getting-started')
setOpenTutorialModal({
multi: [
{
title: "1. Getting Started",
resource: {
name: "a-test",
service: "VIDEO",
identifier: "getting-started-go",
},
},
{
title: "2. Overview",
resource: {
name: "a-test",
service: "VIDEO",
identifier: "overview-go",
},
},
{
title: "3. Qortal Groups",
resource: {
name: "a-test",
service: "VIDEO",
identifier: "groups-go",
},
},
],
});
}
break;
case "qapps":
{
if((shownTutorials || {})['qapps'] && !isForce) return
saveShowTutorial('qapps')
setOpenTutorialModal({
multi: [
{
title: "1. Apps Dashboard",
resource: {
name: "a-test",
service: "VIDEO",
identifier: "apps-dashboard-go",
},
},
{
title: "2. Apps Navigation",
resource: {
name: "a-test",
service: "VIDEO",
identifier: "apps-navigation-go",
},
}
],
});
}
break;
default:
break;
}
} catch (error) {
//error
}
}, [shownTutorials]);
return {
showTutorial,
openTutorialModal,
setOpenTutorialModal,
shownTutorialsInitiated: !!shownTutorials
};
};

View File

@ -121,4 +121,5 @@ html, body {
.swiper {
width: 100%;
}
}