Use desktop file

This commit is contained in:
Nicola Benaglia 2025-04-20 13:53:57 +02:00
parent bf4e58bcfa
commit c4f7d150de
6 changed files with 682 additions and 598 deletions

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from 'react';
import { import {
AppCircle, AppCircle,
AppCircleContainer, AppCircleContainer,
@ -19,191 +19,211 @@ import {
AppsLibraryContainer, AppsLibraryContainer,
AppsParent, AppsParent,
AppsWidthLimiter, AppsWidthLimiter,
} from "./Apps-styles"; } from './Apps-styles';
import { Avatar, Box, ButtonBase, InputBase } from "@mui/material"; import { Avatar, Box, ButtonBase, InputBase } from '@mui/material';
import { Add } from "@mui/icons-material"; import { Add } from '@mui/icons-material';
import { getBaseApiReact, isMobile } from "../../App"; import { getBaseApiReact, isMobile } from '../../App';
import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import LogoSelected from '../../assets/svgs/LogoSelected.svg';
import { Spacer } from "../../common/Spacer"; import { Spacer } from '../../common/Spacer';
import { executeEvent } from "../../utils/events"; import { executeEvent } from '../../utils/events';
import { AppRating } from "./AppRating"; import { AppRating } from './AppRating';
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global"; import {
import { saveToLocalStorage } from "./AppsNavBar"; settingsLocalLastUpdatedAtom,
import { useRecoilState, useSetRecoilState } from "recoil"; sortablePinnedAppsAtom,
} from '../../atoms/global';
import { saveToLocalStorage } from './AppsNavBarDesktop';
import { useRecoilState, useSetRecoilState } from 'recoil';
export const AppInfo = ({ app, myName }) => { export const AppInfo = ({ app, myName }) => {
const isInstalled = app?.status?.status === "READY"; const isInstalled = app?.status?.status === 'READY';
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(
sortablePinnedAppsAtom
);
const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === app?.name && item?.service === app?.service) const isSelectedAppPinned = !!sortablePinnedApps?.find(
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); (item) => item?.name === app?.name && item?.service === app?.service
);
const setSettingsLocalLastUpdated = useSetRecoilState(
settingsLocalLastUpdatedAtom
);
return ( return (
<AppsLibraryContainer <AppsLibraryContainer
sx={{ sx={{
height: !isMobile && "100%", height: !isMobile && '100%',
justifyContent: !isMobile && "flex-start", justifyContent: !isMobile && 'flex-start',
alignItems: isMobile && 'center' alignItems: isMobile && 'center',
}} }}
> >
<Box sx={{ <Box
display: 'flex', sx={{
flexDirection: 'column', display: 'flex',
maxWidth: "500px", flexDirection: 'column',
width: '90%' maxWidth: '500px',
}}> width: '90%',
}}
>
{!isMobile && <Spacer height="30px" />} {!isMobile && <Spacer height="30px" />}
<AppsWidthLimiter> <AppsWidthLimiter>
<AppInfoSnippetContainer> <AppInfoSnippetContainer>
<AppInfoSnippetLeft <AppInfoSnippetLeft
sx={{
flexGrow: 1,
gap: "18px",
}}
>
<AppCircleContainer
sx={{ sx={{
width: "auto", flexGrow: 1,
gap: '18px',
}} }}
> >
<AppCircle <AppCircleContainer
sx={{ sx={{
border: "none", width: 'auto',
height: "100px",
width: "100px",
}} }}
> >
<Avatar <AppCircle
sx={{ sx={{
height: "43px", border: 'none',
width: "43px", height: '100px',
"& img": { width: '100px',
objectFit: "fill",
},
}} }}
alt={app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
> >
<img <Avatar
style={{ sx={{
width: "43px", height: '43px',
height: "auto", width: '43px',
'& img': {
objectFit: 'fill',
},
}} }}
src={LogoSelected} alt={app?.name}
alt="center-icon" src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
/> app?.name
</Avatar> }/qortal_avatar?async=true`}
</AppCircle> >
</AppCircleContainer> <img
<AppInfoSnippetMiddle> style={{
<AppInfoAppName> width: '43px',
{app?.metadata?.title || app?.name} height: 'auto',
</AppInfoAppName> }}
<Spacer height="6px" /> src={LogoSelected}
<AppInfoUserName>{app?.name}</AppInfoUserName> alt="center-icon"
<Spacer height="3px" /> />
</AppInfoSnippetMiddle> </Avatar>
</AppInfoSnippetLeft> </AppCircle>
<AppInfoSnippetRight></AppInfoSnippetRight> </AppCircleContainer>
</AppInfoSnippetContainer> <AppInfoSnippetMiddle>
<Spacer height="11px" /> <AppInfoAppName>
<Box sx={{ {app?.metadata?.title || app?.name}
width: '100%', </AppInfoAppName>
display: 'flex', <Spacer height="6px" />
alignItems: 'center', <AppInfoUserName>{app?.name}</AppInfoUserName>
gap: '20px' <Spacer height="3px" />
}}> </AppInfoSnippetMiddle>
<AppDownloadButton </AppInfoSnippetLeft>
onClick={() => { <AppInfoSnippetRight></AppInfoSnippetRight>
setSortablePinnedApps((prev) => { </AppInfoSnippetContainer>
let updatedApps; <Spacer height="11px" />
<Box
sx={{
width: '100%',
display: 'flex',
alignItems: 'center',
gap: '20px',
}}
>
<AppDownloadButton
onClick={() => {
setSortablePinnedApps((prev) => {
let updatedApps;
if (isSelectedAppPinned) { if (isSelectedAppPinned) {
// Remove the selected app if it is pinned // Remove the selected app if it is pinned
updatedApps = prev.filter( updatedApps = prev.filter(
(item) => !(item?.name === app?.name && item?.service === app?.service) (item) =>
!(
item?.name === app?.name &&
item?.service === app?.service
)
);
} else {
// Add the selected app if it is not pinned
updatedApps = [
...prev,
{
name: app?.name,
service: app?.service,
},
];
}
saveToLocalStorage(
'ext_saved_settings',
'sortablePinnedApps',
updatedApps
); );
} else { return updatedApps;
// Add the selected app if it is not pinned });
updatedApps = [...prev, { setSettingsLocalLastUpdated(Date.now());
name: app?.name, }}
service: app?.service, sx={{
}]; backgroundColor: '#359ff7ff',
} width: '100%',
maxWidth: '320px',
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps) height: '29px',
return updatedApps; opacity: isSelectedAppPinned ? 0.6 : 1,
}); }}
setSettingsLocalLastUpdated(Date.now()) >
}} <AppDownloadButtonText>
sx={{ {!isMobile ? (
backgroundColor: "#359ff7ff", <>
width: "100%", {isSelectedAppPinned
maxWidth: "320px", ? 'Unpin from dashboard'
height: "29px", : 'Pin to dashboard'}
opacity: isSelectedAppPinned ? 0.6 : 1 </>
}} ) : (
> <>{isSelectedAppPinned ? 'Unpin' : 'Pin'}</>
<AppDownloadButtonText> )}
{!isMobile ? ( </AppDownloadButtonText>
<> </AppDownloadButton>
{isSelectedAppPinned ? 'Unpin from dashboard' : 'Pin to dashboard'} <AppDownloadButton
</> onClick={() => {
) : ( executeEvent('addTab', {
<> data: app,
{isSelectedAppPinned ? 'Unpin' : 'Pin'} });
</> }}
)} sx={{
backgroundColor: isInstalled ? '#0091E1' : '#247C0E',
</AppDownloadButtonText> width: '100%',
</AppDownloadButton> maxWidth: '320px',
<AppDownloadButton height: '29px',
onClick={() => { }}
executeEvent("addTab", { >
data: app, <AppDownloadButtonText>
}); {isInstalled ? 'Open' : 'Download'}
}} </AppDownloadButtonText>
sx={{ </AppDownloadButton>
backgroundColor: isInstalled ? "#0091E1" : "#247C0E", </Box>
width: "100%", </AppsWidthLimiter>
maxWidth: "320px", <Spacer height="20px" />
height: "29px", <AppsWidthLimiter>
}} <AppsCategoryInfo>
> <AppRating ratingCountPosition="top" myName={myName} app={app} />
<AppDownloadButtonText> <Spacer width="16px" />
{isInstalled ? "Open" : "Download"} <Spacer height="40px" width="1px" backgroundColor="white" />
</AppDownloadButtonText> <Spacer width="16px" />
</AppDownloadButton> <AppsCategoryInfoSub>
</Box> <AppsCategoryInfoLabel>Category:</AppsCategoryInfoLabel>
<Spacer height="4px" />
</AppsWidthLimiter> <AppsCategoryInfoValue>
<Spacer height="20px" /> {app?.metadata?.categoryName || 'none'}
<AppsWidthLimiter> </AppsCategoryInfoValue>
<AppsCategoryInfo> </AppsCategoryInfoSub>
<AppRating ratingCountPosition="top" myName={myName} app={app} /> </AppsCategoryInfo>
<Spacer width="16px" /> <Spacer height="30px" />
<Spacer height="40px" width="1px" backgroundColor="white" /> <AppInfoAppName>About this Q-App</AppInfoAppName>
<Spacer width="16px" /> </AppsWidthLimiter>
<AppsCategoryInfoSub> <Spacer height="20px" />
<AppsCategoryInfoLabel>Category:</AppsCategoryInfoLabel> <AppsInfoDescription>
<Spacer height="4px" /> {app?.metadata?.description || 'No description'}
<AppsCategoryInfoValue> </AppsInfoDescription>
{app?.metadata?.categoryName || "none"}
</AppsCategoryInfoValue>
</AppsCategoryInfoSub>
</AppsCategoryInfo>
<Spacer height="30px" />
<AppInfoAppName>About this Q-App</AppInfoAppName>
</AppsWidthLimiter>
<Spacer height="20px" />
<AppsInfoDescription>
{app?.metadata?.description || "No description"}
</AppsInfoDescription>
</Box> </Box>
</AppsLibraryContainer> </AppsLibraryContainer>
); );

View File

@ -1,4 +1,4 @@
import React from "react"; import React from 'react';
import { import {
AppCircle, AppCircle,
AppCircleContainer, AppCircleContainer,
@ -10,148 +10,180 @@ import {
AppInfoSnippetMiddle, AppInfoSnippetMiddle,
AppInfoSnippetRight, AppInfoSnippetRight,
AppInfoUserName, AppInfoUserName,
} from "./Apps-styles"; } from './Apps-styles';
import { Avatar, ButtonBase } from "@mui/material"; import { Avatar, ButtonBase } from '@mui/material';
import { getBaseApiReact, isMobile } from "../../App"; import { getBaseApiReact, isMobile } from '../../App';
import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import LogoSelected from '../../assets/svgs/LogoSelected.svg';
import { Spacer } from "../../common/Spacer"; import { Spacer } from '../../common/Spacer';
import { executeEvent } from "../../utils/events"; import { executeEvent } from '../../utils/events';
import { AppRating } from "./AppRating"; import { AppRating } from './AppRating';
import { useRecoilState, useSetRecoilState } from "recoil"; import { useRecoilState, useSetRecoilState } from 'recoil';
import { settingsLocalLastUpdatedAtom, sortablePinnedAppsAtom } from "../../atoms/global"; import {
import { saveToLocalStorage } from "./AppsNavBar"; settingsLocalLastUpdatedAtom,
sortablePinnedAppsAtom,
} from '../../atoms/global';
import { saveToLocalStorage } from './AppsNavBarDesktop';
export const AppInfoSnippet = ({ app, myName, isFromCategory, parentStyles = {} }) => { export const AppInfoSnippet = ({
app,
myName,
isFromCategory,
parentStyles = {},
}) => {
const isInstalled = app?.status?.status === 'READY';
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(
sortablePinnedAppsAtom
);
const isInstalled = app?.status?.status === 'READY' const isSelectedAppPinned = !!sortablePinnedApps?.find(
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); (item) => item?.name === app?.name && item?.service === app?.service
);
const isSelectedAppPinned = !!sortablePinnedApps?.find((item)=> item?.name === app?.name && item?.service === app?.service) const setSettingsLocalLastUpdated = useSetRecoilState(
const setSettingsLocalLastUpdated = useSetRecoilState(settingsLocalLastUpdatedAtom); settingsLocalLastUpdatedAtom
);
return ( return (
<AppInfoSnippetContainer sx={{ <AppInfoSnippetContainer
...parentStyles sx={{
}}> ...parentStyles,
}}
>
<AppInfoSnippetLeft> <AppInfoSnippetLeft>
<ButtonBase <ButtonBase
sx={{ sx={{
height: "80px", height: '80px',
width: "60px", width: '60px',
}} }}
onClick={()=> { onClick={() => {
if(isFromCategory){ if (isFromCategory) {
executeEvent("selectedAppInfoCategory", { executeEvent('selectedAppInfoCategory', {
data: app,
});
return;
}
executeEvent('selectedAppInfo', {
data: app, data: app,
}); });
return }}
} >
executeEvent("selectedAppInfo", { <AppCircleContainer>
data: app, <AppCircle
}); sx={{
}} border: 'none',
> }}
<AppCircleContainer> >
<AppCircle <Avatar
sx={{ sx={{
border: "none", height: '42px',
width: '42px',
'& img': {
objectFit: 'fill',
},
}}
alt={app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: '31px',
height: 'auto',
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
</AppCircleContainer>
</ButtonBase>
<AppInfoSnippetMiddle>
<ButtonBase
onClick={() => {
if (isFromCategory) {
executeEvent('selectedAppInfoCategory', {
data: app,
});
return;
}
executeEvent('selectedAppInfo', {
data: app,
});
}} }}
> >
<Avatar <AppInfoAppName>{app?.metadata?.title || app?.name}</AppInfoAppName>
sx={{ </ButtonBase>
height: "42px", <Spacer height="6px" />
width: "42px", <AppInfoUserName>{app?.name}</AppInfoUserName>
'& img': {
objectFit: 'fill',
}
}}
alt={app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: "31px",
height: "auto",
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
</AppCircleContainer>
</ButtonBase>
<AppInfoSnippetMiddle>
<ButtonBase onClick={()=> {
if(isFromCategory){
executeEvent("selectedAppInfoCategory", {
data: app,
});
return
}
executeEvent("selectedAppInfo", {
data: app,
});
}}>
<AppInfoAppName >
{app?.metadata?.title || app?.name}
</AppInfoAppName>
</ButtonBase>
<Spacer height="6px" />
<AppInfoUserName>
{ app?.name}
</AppInfoUserName>
<Spacer height="3px" /> <Spacer height="3px" />
<AppRating app={app} myName={myName} /> <AppRating app={app} myName={myName} />
</AppInfoSnippetMiddle> </AppInfoSnippetMiddle>
</AppInfoSnippetLeft> </AppInfoSnippetLeft>
<AppInfoSnippetRight sx={{ <AppInfoSnippetRight
gap: '10px' sx={{
}}> gap: '10px',
}}
>
{!isMobile && ( {!isMobile && (
<AppDownloadButton onClick={()=> { <AppDownloadButton
onClick={() => {
setSortablePinnedApps((prev) => {
let updatedApps;
setSortablePinnedApps((prev) => { if (isSelectedAppPinned) {
let updatedApps; // Remove the selected app if it is pinned
updatedApps = prev.filter(
(item) =>
!(
item?.name === app?.name &&
item?.service === app?.service
)
);
} else {
// Add the selected app if it is not pinned
updatedApps = [
...prev,
{
name: app?.name,
service: app?.service,
},
];
}
if (isSelectedAppPinned) { saveToLocalStorage(
// Remove the selected app if it is pinned 'ext_saved_settings',
updatedApps = prev.filter( 'sortablePinnedApps',
(item) => !(item?.name === app?.name && item?.service === app?.service) updatedApps
); );
} else { return updatedApps;
// Add the selected app if it is not pinned });
updatedApps = [...prev, { setSettingsLocalLastUpdated(Date.now());
name: app?.name, }}
service: app?.service, sx={{
}]; backgroundColor: '#359ff7ff',
} opacity: isSelectedAppPinned ? 0.6 : 1,
}}
>
<AppDownloadButtonText>
{' '}
{isSelectedAppPinned ? 'Unpin' : 'Pin'}
</AppDownloadButtonText>
</AppDownloadButton>
)}
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps) <AppDownloadButton
return updatedApps; onClick={() => {
}); executeEvent('addTab', {
setSettingsLocalLastUpdated(Date.now()) data: app,
}} sx={{ });
backgroundColor: '#359ff7ff', }}
opacity: isSelectedAppPinned ? 0.6 : 1 sx={{
backgroundColor: isInstalled ? '#0091E1' : '#247C0E',
}}> }}
<AppDownloadButtonText> {isSelectedAppPinned ? 'Unpin' : 'Pin'}</AppDownloadButtonText> >
</AppDownloadButton> <AppDownloadButtonText>
)} {isInstalled ? 'Open' : 'Download'}
</AppDownloadButtonText>
<AppDownloadButton onClick={()=> {
executeEvent("addTab", {
data: app
})
}} sx={{
backgroundColor: isInstalled ? '#0091E1' : '#247C0E',
}}>
<AppDownloadButtonText>{isInstalled ? 'Open' : 'Download'}</AppDownloadButtonText>
</AppDownloadButton> </AppDownloadButton>
</AppInfoSnippetRight> </AppInfoSnippetRight>
</AppInfoSnippetContainer> </AppInfoSnippetContainer>

View File

@ -23,7 +23,7 @@ import {
sortablePinnedAppsAtom, sortablePinnedAppsAtom,
} from '../../atoms/global'; } from '../../atoms/global';
import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilState, useSetRecoilState } from 'recoil';
import { saveToLocalStorage } from './AppsNavBar'; import { saveToLocalStorage } from './AppsNavBarDesktop';
import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps'; import { ContextMenuPinnedApps } from '../ContextMenuPinnedApps';
import LockIcon from '@mui/icons-material/Lock'; import LockIcon from '@mui/icons-material/Lock';
import { useHandlePrivateApps } from './useHandlePrivateApps'; import { useHandlePrivateApps } from './useHandlePrivateApps';

View File

@ -1,152 +1,182 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { ListItemIcon, Menu, MenuItem, Typography, styled } from '@mui/material'; import {
ListItemIcon,
Menu,
MenuItem,
Typography,
styled,
} from '@mui/material';
import PushPinIcon from '@mui/icons-material/PushPin'; import PushPinIcon from '@mui/icons-material/PushPin';
import { saveToLocalStorage } from './Apps/AppsNavBar'; import { saveToLocalStorage } from './Apps/AppsNavBarDesktop';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { sortablePinnedAppsAtom } from '../atoms/global'; import { sortablePinnedAppsAtom } from '../atoms/global';
const CustomStyledMenu = styled(Menu)(({ theme }) => ({ const CustomStyledMenu = styled(Menu)(({ theme }) => ({
'& .MuiPaper-root': { '& .MuiPaper-root': {
backgroundColor: '#f9f9f9', backgroundColor: '#f9f9f9',
borderRadius: '12px', borderRadius: '12px',
padding: theme.spacing(1), padding: theme.spacing(1),
boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)', boxShadow: '0 5px 15px rgba(0, 0, 0, 0.2)',
}, },
'& .MuiMenuItem-root': { '& .MuiMenuItem-root': {
fontSize: '14px', fontSize: '14px',
color: '#444', color: '#444',
transition: '0.3s background-color', transition: '0.3s background-color',
'&:hover': { '&:hover': {
backgroundColor: '#f0f0f0', backgroundColor: '#f0f0f0',
},
}, },
},
})); }));
export const ContextMenuPinnedApps = ({ children, app, isMine }) => { export const ContextMenuPinnedApps = ({ children, app, isMine }) => {
const [menuPosition, setMenuPosition] = useState(null); const [menuPosition, setMenuPosition] = useState(null);
const longPressTimeout = useRef(null); const longPressTimeout = useRef(null);
const maxHoldTimeout = useRef(null); const maxHoldTimeout = useRef(null);
const preventClick = useRef(false); const preventClick = useRef(false);
const startTouchPosition = useRef({ x: 0, y: 0 }); // Track initial touch position const startTouchPosition = useRef({ x: 0, y: 0 }); // Track initial touch position
const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(sortablePinnedAppsAtom); const [sortablePinnedApps, setSortablePinnedApps] = useRecoilState(
sortablePinnedAppsAtom
);
const handleContextMenu = (event) => { const handleContextMenu = (event) => {
if(isMine) return if (isMine) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
preventClick.current = true; preventClick.current = true;
setMenuPosition({ setMenuPosition({
mouseX: event.clientX, mouseX: event.clientX,
mouseY: event.clientY, mouseY: event.clientY,
}); });
}; };
const handleTouchStart = (event) => { const handleTouchStart = (event) => {
if(isMine) return if (isMine) return;
const { clientX, clientY } = event.touches[0]; const { clientX, clientY } = event.touches[0];
startTouchPosition.current = { x: clientX, y: clientY }; startTouchPosition.current = { x: clientX, y: clientY };
longPressTimeout.current = setTimeout(() => { longPressTimeout.current = setTimeout(() => {
preventClick.current = true; preventClick.current = true;
event.stopPropagation(); event.stopPropagation();
setMenuPosition({ setMenuPosition({
mouseX: clientX, mouseX: clientX,
mouseY: clientY, mouseY: clientY,
});
}, 500);
// Set a maximum hold duration (e.g., 1.5 seconds)
maxHoldTimeout.current = setTimeout(() => {
clearTimeout(longPressTimeout.current);
}, 1500);
};
const handleTouchMove = (event) => {
if (isMine) return;
const { clientX, clientY } = event.touches[0];
const { x, y } = startTouchPosition.current;
// Determine if the touch has moved beyond a small threshold (e.g., 10px)
const movedEnough =
Math.abs(clientX - x) > 10 || Math.abs(clientY - y) > 10;
if (movedEnough) {
clearTimeout(longPressTimeout.current);
clearTimeout(maxHoldTimeout.current);
}
};
const handleTouchEnd = (event) => {
if (isMine) return;
clearTimeout(longPressTimeout.current);
clearTimeout(maxHoldTimeout.current);
if (preventClick.current) {
event.preventDefault();
event.stopPropagation();
preventClick.current = false;
}
};
const handleClose = (e) => {
if (isMine) return;
e.preventDefault();
e.stopPropagation();
setMenuPosition(null);
};
return (
<div
onContextMenu={handleContextMenu}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{ touchAction: 'none' }}
>
{children}
<CustomStyledMenu
disableAutoFocusItem
open={!!menuPosition}
onClose={handleClose}
anchorReference="anchorPosition"
anchorPosition={
menuPosition
? { top: menuPosition.mouseY, left: menuPosition.mouseX }
: undefined
}
onClick={(e) => {
e.stopPropagation();
}}
>
<MenuItem
onClick={(e) => {
handleClose(e);
setSortablePinnedApps((prev) => {
if (app?.isPrivate) {
const updatedApps = prev.filter(
(item) =>
!(
item?.privateAppProperties?.name ===
app?.privateAppProperties?.name &&
item?.privateAppProperties?.service ===
app?.privateAppProperties?.service &&
item?.privateAppProperties?.identifier ===
app?.privateAppProperties?.identifier
)
);
saveToLocalStorage(
'ext_saved_settings',
'sortablePinnedApps',
updatedApps
);
return updatedApps;
} else {
const updatedApps = prev.filter(
(item) =>
!(
item?.name === app?.name && item?.service === app?.service
)
);
saveToLocalStorage(
'ext_saved_settings',
'sortablePinnedApps',
updatedApps
);
return updatedApps;
}
}); });
}, 500); }}
// Set a maximum hold duration (e.g., 1.5 seconds)
maxHoldTimeout.current = setTimeout(() => {
clearTimeout(longPressTimeout.current);
}, 1500);
};
const handleTouchMove = (event) => {
if(isMine) return
const { clientX, clientY } = event.touches[0];
const { x, y } = startTouchPosition.current;
// Determine if the touch has moved beyond a small threshold (e.g., 10px)
const movedEnough = Math.abs(clientX - x) > 10 || Math.abs(clientY - y) > 10;
if (movedEnough) {
clearTimeout(longPressTimeout.current);
clearTimeout(maxHoldTimeout.current);
}
};
const handleTouchEnd = (event) => {
if(isMine) return
clearTimeout(longPressTimeout.current);
clearTimeout(maxHoldTimeout.current);
if (preventClick.current) {
event.preventDefault();
event.stopPropagation();
preventClick.current = false;
}
};
const handleClose = (e) => {
if(isMine) return
e.preventDefault();
e.stopPropagation();
setMenuPosition(null);
};
return (
<div
onContextMenu={handleContextMenu}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{ touchAction: 'none' }}
> >
{children} <ListItemIcon sx={{ minWidth: '32px' }}>
<CustomStyledMenu <PushPinIcon fontSize="small" />
disableAutoFocusItem </ListItemIcon>
open={!!menuPosition} <Typography variant="inherit" sx={{ fontSize: '14px' }}>
onClose={handleClose} Unpin app
anchorReference="anchorPosition" </Typography>
anchorPosition={ </MenuItem>
menuPosition </CustomStyledMenu>
? { top: menuPosition.mouseY, left: menuPosition.mouseX } </div>
: undefined );
}
onClick={(e) => {
e.stopPropagation();
}}
>
<MenuItem onClick={(e) => {
handleClose(e);
setSortablePinnedApps((prev) => {
if(app?.isPrivate){
const updatedApps = prev.filter(
(item) => !(item?.privateAppProperties?.name === app?.privateAppProperties?.name && item?.privateAppProperties?.service === app?.privateAppProperties?.service && item?.privateAppProperties?.identifier === app?.privateAppProperties?.identifier)
);
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps);
return updatedApps;
} else {
const updatedApps = prev.filter(
(item) => !(item?.name === app?.name && item?.service === app?.service)
);
saveToLocalStorage('ext_saved_settings', 'sortablePinnedApps', updatedApps);
return updatedApps;
}
});
}}>
<ListItemIcon sx={{ minWidth: '32px' }}>
<PushPinIcon fontSize="small" />
</ListItemIcon>
<Typography variant="inherit" sx={{ fontSize: '14px' }}>
Unpin app
</Typography>
</MenuItem>
</CustomStyledMenu>
</div>
);
}; };

View File

@ -19,7 +19,7 @@ import { SaveIcon } from '../../assets/Icons/SaveIcon';
import { IconWrapper } from '../Desktop/DesktopFooter'; import { IconWrapper } from '../Desktop/DesktopFooter';
import { Spacer } from '../../common/Spacer'; import { Spacer } from '../../common/Spacer';
import { LoadingButton } from '@mui/lab'; import { LoadingButton } from '@mui/lab';
import { saveToLocalStorage } from '../Apps/AppsNavBar'; import { saveToLocalStorage } from '../Apps/AppsNavBarDesktop';
import { decryptData, encryptData } from '../../qortalRequests/get'; import { decryptData, encryptData } from '../../qortalRequests/get';
import { saveFileToDiskGeneric } from '../../utils/generateWallet/generateWallet'; import { saveFileToDiskGeneric } from '../../utils/generateWallet/generateWallet';
import { import {

View File

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