started app browser

This commit is contained in:
PhilReact 2024-10-18 02:50:19 +03:00
parent bd170d8481
commit 28c2dfbb7a
16 changed files with 795 additions and 172 deletions

11
package-lock.json generated
View File

@ -45,6 +45,7 @@
"react-countdown-circle-timer": "^3.2.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-frame-component": "^5.2.7",
"react-infinite-scroller": "^1.2.6",
"react-intersection-observer": "^9.13.0",
"react-qr-code": "^2.0.15",
@ -9256,6 +9257,16 @@
"react": ">= 16.8 || 18.0.0"
}
},
"node_modules/react-frame-component": {
"version": "5.2.7",
"resolved": "https://registry.npmjs.org/react-frame-component/-/react-frame-component-5.2.7.tgz",
"integrity": "sha512-ROjHtSLoSVYUBfTieazj/nL8jIX9rZFmHC0yXEU+dx6Y82OcBEGgU9o7VyHMrBFUN9FuQ849MtIPNNLsb4krbg==",
"peerDependencies": {
"prop-types": "^15.5.9",
"react": ">= 16.3",
"react-dom": ">= 16.3"
}
},
"node_modules/react-infinite-scroller": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.2.6.tgz",

View File

@ -49,6 +49,7 @@
"react-countdown-circle-timer": "^3.2.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-frame-component": "^5.2.7",
"react-infinite-scroller": "^1.2.6",
"react-intersection-observer": "^9.13.0",
"react-qr-code": "^2.0.15",

View File

@ -0,0 +1,4 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="20" cy="19.9999" r="18" fill="#434343"/>
<path d="M30 21.6666H21.6666V30C21.6666 30.9166 20.9166 31.6666 20 31.6666C19.0833 31.6666 18.3333 30.9166 18.3333 30V21.6666H9.99998C9.08331 21.6666 8.33331 20.9166 8.33331 20C8.33331 19.0833 9.08331 18.3333 9.99998 18.3333H18.3333V9.99995C18.3333 9.08328 19.0833 8.33328 20 8.33328C20.9166 8.33328 21.6666 9.08328 21.6666 9.99995V18.3333H30C30.9166 18.3333 31.6666 19.0833 31.6666 20C31.6666 20.9166 30.9166 21.6666 30 21.6666Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 602 B

View File

@ -0,0 +1,10 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_400_537)">
<path d="M28.3334 15.5833H11.0925L19.0117 7.6641L17 5.6666L5.66669 16.9999L17 28.3333L18.9975 26.3358L11.0925 18.4166H28.3334V15.5833Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_400_537">
<rect width="34" height="34" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@ -0,0 +1,6 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8.5" cy="8.5" r="8.5" fill="white"/>
<circle cx="8.5" cy="8.50003" r="6.61111" fill="#434343"/>
<path d="M5.66675 5.66669L11.3334 11.3334" stroke="white" stroke-width="2"/>
<path d="M11.3333 5.66675L5.66658 11.3334" stroke="white" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@ -0,0 +1,10 @@
<svg width="34" height="34" viewBox="0 0 34 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_400_556)">
<path d="M8.49996 14.1666C6.94163 14.1666 5.66663 15.4416 5.66663 16.9999C5.66663 18.5583 6.94163 19.8333 8.49996 19.8333C10.0583 19.8333 11.3333 18.5583 11.3333 16.9999C11.3333 15.4416 10.0583 14.1666 8.49996 14.1666ZM25.5 14.1666C23.9416 14.1666 22.6666 15.4416 22.6666 16.9999C22.6666 18.5583 23.9416 19.8333 25.5 19.8333C27.0583 19.8333 28.3333 18.5583 28.3333 16.9999C28.3333 15.4416 27.0583 14.1666 25.5 14.1666ZM17 14.1666C15.4416 14.1666 14.1666 15.4416 14.1666 16.9999C14.1666 18.5583 15.4416 19.8333 17 19.8333C18.5583 19.8333 19.8333 18.5583 19.8333 16.9999C19.8333 15.4416 18.5583 14.1666 17 14.1666Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_400_556">
<rect width="34" height="34" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 874 B

View File

@ -1,22 +1,102 @@
import React, { useEffect, useMemo, useState } from "react";
import {
AppCircle,
AppCircleContainer,
AppCircleLabel,
AppDownloadButton,
AppDownloadButtonText,
AppInfoAppName,
AppInfoSnippetContainer,
AppInfoSnippetLeft,
AppInfoSnippetMiddle,
AppInfoSnippetRight,
AppInfoUserName,
AppsLibraryContainer,
AppsParent,
} from "./Apps-styles";
import { Avatar, Box, ButtonBase, InputBase } from "@mui/material";
import { Add } from "@mui/icons-material";
import { getBaseApiReact } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { Spacer } from "../../common/Spacer";
import { executeEvent } from "../../utils/events";
export const AppInfo = ({ app }) => {
const isInstalled = app?.status?.status === 'READY'
return (
<AppsLibraryContainer>
<AppInfoSnippetContainer>
<AppInfoSnippetLeft sx={{
flexGrow: 1,
gap: '18px'
}}>
<AppCircleContainer sx={{
width: 'auto'
}}>
<AppCircle
sx={{
border: "none",
height: '100px',
width: '100px'
}}
>
<Avatar
sx={{
height: "43px",
width: "43px",
'& img': {
objectFit: 'fill',
}
}}
alt={app?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
app?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: "43px",
height: "auto",
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
</AppCircleContainer>
<AppInfoSnippetMiddle>
</AppsLibraryContainer>
<AppInfoAppName >
{app?.metadata?.title || app?.name}
</AppInfoAppName>
<Spacer height="6px" />
<AppInfoUserName>
{ app?.name}
</AppInfoUserName>
<Spacer height="3px" />
</AppInfoSnippetMiddle>
</AppInfoSnippetLeft>
<AppInfoSnippetRight>
</AppInfoSnippetRight>
</AppInfoSnippetContainer>
<Spacer height="11px" />
<AppDownloadButton sx={{
backgroundColor: isInstalled ? '#0091E1' : '#247C0E',
width: '100%',
maxWidth: '320px',
height: '29px'
}}>
<AppDownloadButtonText>{isInstalled ? 'Open' : 'Download'}</AppDownloadButtonText>
</AppDownloadButton>
</AppsLibraryContainer>
);
};

View File

@ -0,0 +1,100 @@
import React, { useEffect, useMemo, useState } from "react";
import {
AppCircle,
AppCircleContainer,
AppCircleLabel,
AppDownloadButton,
AppDownloadButtonText,
AppInfoAppName,
AppInfoSnippetContainer,
AppInfoSnippetLeft,
AppInfoSnippetMiddle,
AppInfoSnippetRight,
AppInfoUserName,
AppsLibraryContainer,
} from "./Apps-styles";
import { Avatar, Box, ButtonBase, InputBase } from "@mui/material";
import { Add } from "@mui/icons-material";
import { getBaseApiReact } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { Spacer } from "../../common/Spacer";
import { executeEvent } from "../../utils/events";
export const AppInfoSnippet = ({ app }) => {
const isInstalled = app?.status?.status === 'READY'
return (
<AppInfoSnippetContainer>
<AppInfoSnippetLeft>
<ButtonBase
sx={{
height: "80px",
width: "60px",
}}
onClick={()=> {
executeEvent("selectedAppInfo", {
data: app,
});
}}
>
<AppCircleContainer>
<AppCircle
sx={{
border: "none",
}}
>
<Avatar
sx={{
height: "31px",
width: "31px",
'& 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={()=> {
executeEvent("selectedAppInfo", {
data: app,
});
}}>
<AppInfoAppName >
{app?.metadata?.title || app?.name}
</AppInfoAppName>
</ButtonBase>
<Spacer height="6px" />
<AppInfoUserName>
{ app?.name}
</AppInfoUserName>
<Spacer height="3px" />
</AppInfoSnippetMiddle>
</AppInfoSnippetLeft>
<AppInfoSnippetRight>
<AppDownloadButton sx={{
backgroundColor: isInstalled ? '#0091E1' : '#247C0E',
}}>
<AppDownloadButtonText>{isInstalled ? 'Open' : 'Download'}</AppDownloadButtonText>
</AppDownloadButton>
</AppInfoSnippetRight>
</AppInfoSnippetContainer>
);
};

View File

@ -0,0 +1,46 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import {
AppCircle,
AppCircleContainer,
AppCircleLabel,
AppDownloadButton,
AppDownloadButtonText,
AppInfoAppName,
AppInfoSnippetContainer,
AppInfoSnippetLeft,
AppInfoSnippetMiddle,
AppInfoSnippetRight,
AppInfoUserName,
AppsLibraryContainer,
AppsParent,
} from "./Apps-styles";
import { Avatar, Box, ButtonBase, InputBase } from "@mui/material";
import { Add } from "@mui/icons-material";
import { MyContext, getBaseApiReact } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { Spacer } from "../../common/Spacer";
import { executeEvent } from "../../utils/events";
export const AppViewer = ({ app }) => {
const { rootHeight } = useContext(MyContext);
const iframeRef = useRef(null);
const url = useMemo(()=> {
return `${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? app?.path : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}`
}, [app?.service, app?.name, app?.identifier, app?.path])
return (
<iframe ref={iframeRef} style={{
height: `calc(${rootHeight} - 60px - 45px)`,
border: 'none',
width: '100%'
}} id="browser-iframe" src={url} sandbox="allow-scripts allow-same-origin allow-forms allow-downloads allow-modals" allow="fullscreen">
</iframe>
);
};

View File

@ -0,0 +1,40 @@
import React, { useContext, useRef } from 'react'
import { AppViewer } from './AppViewer'
import Frame from 'react-frame-component';
import { MyContext } from '../../App';
const AppViewerContainer = ({app, isSelected}) => {
const { rootHeight } = useContext(MyContext);
const frameRef = useRef(null);
return (
<Frame id={`browser-iframe-${app?.tabId}` } ref={frameRef} head={
<>
{/* Inject styles directly into the iframe */}
<style>
{`
body {
margin: 0;
padding: 0;
}
/* Hide scrollbars for all elements */
* {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
*::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
`}
</style>
</>
} style={{
height: `calc(${rootHeight} - 60px - 45px)`,
border: 'none',
width: '100%',
display: !isSelected && 'none'
}} ><AppViewer app={app} /></Frame>
)
}
export default AppViewerContainer

View File

@ -6,6 +6,7 @@ import {
Box,
TextField,
InputLabel,
ButtonBase,
} from "@mui/material";
import { styled } from "@mui/system";
@ -15,7 +16,7 @@ import {
flexDirection: "column",
height: "100%",
alignItems: "center",
overflow: 'auto',
overflow: 'auto',
// For WebKit-based browsers (Chrome, Safari, etc.)
"::-webkit-scrollbar": {
width: "0px", // Set the width to 0 to hide the scrollbar
@ -35,6 +36,7 @@ import {
gap: '24px',
flexWrap: 'wrap',
alignItems: 'flex-start',
alignSelf: 'center'
}));
export const AppsLibraryContainer = styled(Box)(({ theme }) => ({
@ -59,13 +61,16 @@ import {
width: "90%",
justifyContent: 'flex-start',
alignItems: 'center',
gap: '10px'
gap: '10px',
flexGrow: 1,
flexShrink: 0
}));
export const AppsSearchRight = styled(Box)(({ theme }) => ({
display: "flex",
width: "90%",
justifyContent: 'flex-end',
alignItems: 'center',
flexShrink: 1
}));
export const AppCircleContainer = styled(Box)(({ theme }) => ({
display: "flex",
@ -105,4 +110,85 @@ import {
borderRadius: '50%',
backgroundColor: "var(--apps-circle)",
border: '1px solid #FFFFFF'
}));
export const AppInfoSnippetContainer = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'space-between',
alignItems: 'center',
width: '100%'
}));
export const AppInfoSnippetLeft = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'flex-start',
alignItems: 'center',
gap: '12px'
}));
export const AppInfoSnippetRight = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'flex-end',
alignItems: 'center',
}));
export const AppDownloadButton = styled(ButtonBase)(({ theme }) => ({
backgroundColor: "#247C0E",
width: '101px',
height: '29px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '25px'
}));
export const AppDownloadButtonText = styled(Typography)(({ theme }) => ({
fontSize: '14px',
fontWeight: 500,
lineHeight: 1.2,
}));
export const AppInfoSnippetMiddle = styled(Box)(({ theme }) => ({
display: "flex",
flexDirection: "column",
justifyContent: 'center',
alignItems: 'flex-start',
}));
export const AppInfoAppName = styled(Typography)(({ theme }) => ({
fontSize: '16px',
fontWeight: 500,
lineHeight: 1.2,
}));
export const AppInfoUserName = styled(Typography)(({ theme }) => ({
fontSize: '13px',
fontWeight: 400,
lineHeight: 1.2,
color: '#8D8F93'
}));
export const AppsNavBarParent = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
height: '60px',
backgroundColor: '#1F2023',
padding: '0px 10px',
position: "fixed",
bottom: 0,
zIndex: 1,
}));
export const AppsNavBarLeft = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'flex-start',
alignItems: 'center',
}));
export const AppsNavBarRight = styled(Box)(({ theme }) => ({
display: "flex",
justifyContent: 'flex-end',
alignItems: 'center',
}));

View File

@ -1,14 +1,26 @@
import React, { useEffect, useState } from 'react'
import React, { useContext, useEffect, useRef, useState } from 'react'
import { AppsHome } from './AppsHome'
import { Spacer } from '../../common/Spacer'
import { getBaseApiReact } from '../../App'
import { MyContext, getBaseApiReact } from '../../App'
import { AppsLibrary } from './AppsLibrary'
import { AppInfo } from './AppInfo'
import { subscribeToEvent, unsubscribeFromEvent } from '../../utils/events'
import { AppsNavBar } from './AppsNavBar'
import { AppsParent } from './Apps-styles'
import { AppViewer } from './AppViewer'
import AppViewerContainer from './AppViewerContainer'
import ShortUniqueId from "short-unique-id";
export const Apps = () => {
const [mode, setMode] = useState('home')
const uid = new ShortUniqueId({ length: 8 });
export const Apps = ({mode, setMode}) => {
const [availableQapps, setAvailableQapps] = useState([])
const [downloadedQapps, setDownloadedQapps] = useState([])
const [selectedAppInfo, setSelectedAppInfo] = useState(null)
const [tabs, setTabs] = useState([])
const [selectedTab, setSelectedTab] = useState(null)
const getQapps = React.useCallback(
async () => {
@ -52,11 +64,95 @@ export const Apps = () => {
getQapps()
}, [getQapps])
const selectedAppInfoFunc = (e) => {
const data = e.detail?.data;
setSelectedAppInfo(data)
setMode('appInfo')
};
useEffect(() => {
subscribeToEvent("selectedAppInfo", selectedAppInfoFunc);
return () => {
unsubscribeFromEvent("selectedAppInfo", selectedAppInfoFunc);
};
}, []);
const navigateBackFunc = (e) => {
if(mode === 'appInfo'){
setMode('library')
} else if(mode === 'library'){
setMode('home')
} else {
const iframe = document.getElementById('browser-iframe2');
console.log('iframe', iframe)
// Go Back in the iframe's history
if (iframe) {
console.log(iframe.contentWindow)
if (iframe && iframe.contentWindow) {
const iframeWindow = iframe.contentWindow;
if (iframeWindow && iframeWindow.history) {
iframeWindow.history.back();
}
}
}
}
};
useEffect(() => {
subscribeToEvent("navigateBack", navigateBackFunc);
return () => {
unsubscribeFromEvent("navigateBack", navigateBackFunc);
};
}, [mode]);
const addTabFunc = (e) => {
const data = e.detail?.data;
const newTab = {
...data,
tabId: uid.rnd()
}
setTabs((prev)=> [...prev, newTab])
setSelectedTab(newTab)
};
useEffect(() => {
subscribeToEvent("addTab", addTabFunc);
return () => {
unsubscribeFromEvent("addTab", addTabFunc);
};
}, [mode]);
return (
<>
<Spacer height="30px" />
<AppsParent>
{mode !== 'viewer' && (
<Spacer height="30px" />
)}
{mode === 'home' && <AppsHome downloadedQapps={downloadedQapps} setMode={setMode} />}
{mode === 'library' && <AppsLibrary downloadedQapps={downloadedQapps} availableQapps={availableQapps} />}
</>
{mode === 'appInfo' && <AppInfo app={selectedAppInfo} />}
{mode === 'viewer' && (
<>
{tabs.map((tab)=> {
return <AppViewerContainer isSelected={tab?.tabId === selectedTab?.tabId} app={tab} />
})}
</>
) }
{mode !== 'viewer' && (
<Spacer height="180px" />
)}
</AppsParent>
)
}

View File

@ -1,56 +1,81 @@
import React, { useMemo, useState } from 'react'
import { AppCircle, AppCircleContainer, AppCircleLabel, AppsContainer, AppsParent } from './Apps-styles'
import { Avatar, ButtonBase } from '@mui/material'
import { Add } from '@mui/icons-material'
import { getBaseApiReact } from '../../App'
import React, { useMemo, useState } from "react";
import {
AppCircle,
AppCircleContainer,
AppCircleLabel,
AppsContainer,
AppsParent,
} from "./Apps-styles";
import { Avatar, ButtonBase } from "@mui/material";
import { Add } from "@mui/icons-material";
import { getBaseApiReact } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { executeEvent } from "../../utils/events";
export const AppsHome = ({downloadedQapps, setMode}) => {
export const AppsHome = ({ downloadedQapps, setMode }) => {
return (
<AppsParent>
<AppsContainer>
<ButtonBase onClick={()=> {
setMode('library')
}}>
<AppCircleContainer>
<AppsContainer>
<ButtonBase
onClick={() => {
setMode("library");
}}
>
<AppCircleContainer>
<AppCircle>
<Add>+</Add>
<Add>+</Add>
</AppCircle>
<AppCircleLabel>Add</AppCircleLabel>
</AppCircleContainer>
</AppCircleContainer>
</ButtonBase>
{downloadedQapps?.map((app) => {
return (
<ButtonBase
sx={{
height: "80px",
width: "60px",
}}
onClick={()=> {
executeEvent("addTab", {
data: app
})
}}
>
<AppCircleContainer>
<AppCircle
sx={{
border: "none",
}}
>
<Avatar
sx={{
height: "31px",
width: "31px",
'& 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>
<AppCircleLabel>
{app?.name}
</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
{downloadedQapps?.map((qapp)=> {
return (
<ButtonBase sx={{
height: '80px',
width: '60px'
}}>
<AppCircleContainer>
<AppCircle sx={{
border: 'none'
}}>
<Avatar
sx={{
height: "31px",
width: "31px",
}}
alt={qapp?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${qapp?.name}/qortal_avatar?async=true`}
>
<img style={{
width: '31px',
height: 'auto'
}} src={LogoSelected} alt="center-icon" />
</Avatar>
</AppCircle>
<AppCircleLabel>{qapp?.metadata?.title || qapp?.name}</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
)
})}
</AppsContainer>
</AppsParent>
)
}
);
})}
</AppsContainer>
);
};

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import {
AppCircle,
AppCircleContainer,
@ -11,14 +11,16 @@ import {
AppsSearchLeft,
AppsSearchRight,
} from "./Apps-styles";
import { Avatar, Box, ButtonBase, InputBase } from "@mui/material";
import { Avatar, Box, ButtonBase, InputBase, styled } from "@mui/material";
import { Add } from "@mui/icons-material";
import { getBaseApiReact } from "../../App";
import { MyContext, getBaseApiReact } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import IconSearch from "../../assets/svgs/Search.svg";
import IconClearInput from "../../assets/svgs/ClearInput.svg";
import { Spacer } from "../../common/Spacer";
import { AppInfoSnippet } from "./AppInfoSnippet";
import { Virtuoso } from "react-virtuoso";
const officialAppList = [
"q-tube",
"q-blog",
@ -30,21 +32,41 @@ const officialAppList = [
"q-shop",
];
const ScrollerStyled = styled('div')({
// Hide scrollbar for WebKit browsers (Chrome, Safari)
"::-webkit-scrollbar": {
width: "0px",
height: "0px",
},
// Hide scrollbar for Firefox
scrollbarWidth: "none",
// Hide scrollbar for IE and older Edge
"-ms-overflow-style": "none",
});
export const AppsLibrary = ({ downloadedQapps, availableQapps }) => {
const [searchValue, setSearchValue] = useState('')
const [searchValue, setSearchValue] = useState("");
const virtuosoRef = useRef();
const { rootHeight } = useContext(MyContext);
const officialApps = useMemo(() => {
return availableQapps.filter((app) => app.service === 'APP' &&
officialAppList.includes(app?.name?.toLowerCase())
return availableQapps.filter(
(app) =>
app.service === "APP" &&
officialAppList.includes(app?.name?.toLowerCase())
);
}, [availableQapps]);
const [debouncedValue, setDebouncedValue] = useState(''); // Debounced value
const [debouncedValue, setDebouncedValue] = useState(""); // Debounced value
// Debounce logic
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(searchValue); // Update debounced value after delay
}, 500); // 500ms debounce time (adjustable)
setDebouncedValue(searchValue);
}, 250);
// Cleanup timeout if searchValue changes before the timeout completes
return () => {
@ -53,111 +75,147 @@ export const AppsLibrary = ({ downloadedQapps, availableQapps }) => {
}, [searchValue]); // Runs effect when searchValue changes
// Example: Perform search or other actions based on debouncedValue
const searchedList = useMemo(()=> {
if(!debouncedValue) return []
return availableQapps.filter((app)=> app.name.toLowerCase().includes(debouncedValue.toLowerCase()))
}, [debouncedValue])
console.log('officialApps', searchedList)
return (
<AppsParent>
<AppsLibraryContainer>
<Box sx={{
display: 'flex',
width: '100%',
justifyContent: 'center'
}}>
<AppsSearchContainer>
<AppsSearchLeft>
<img src={IconSearch} />
<InputBase
value={searchValue}
onChange={(e)=> setSearchValue(e.target.value)}
sx={{ ml: 1, flex: 1 }}
placeholder="Search for apps"
inputProps={{ 'aria-label': 'Search for apps', fontSize: '16px', fontWeight: 400 }}
/>
</AppsSearchLeft>
<AppsSearchRight>
{searchValue && (
<ButtonBase onClick={()=> {
setSearchValue('')
}}>
<img src={IconClearInput} />
</ButtonBase>
)}
</AppsSearchRight>
</AppsSearchContainer>
</Box>
<Spacer height="25px" />
{searchedList?.length > 0 ? (
<>
{searchedList.map((app)=> {
return (
<AppInfo app={app} />
)
})}
</>
) : (
<>
<AppLibrarySubTitle>Official Apps</AppLibrarySubTitle>
<Spacer height="18px" />
<AppsContainer>
{officialApps?.map((qapp) => {
return (
<ButtonBase
sx={{
height: "80px",
width: "60px",
const searchedList = useMemo(() => {
if (!debouncedValue) return [];
return availableQapps.filter((app) =>
app.name.toLowerCase().includes(debouncedValue.toLowerCase())
);
}, [debouncedValue]);
console.log("officialApps", searchedList);
const rowRenderer = (index) => {
let app = searchedList[index];
console.log('appi', app)
return <AppInfoSnippet key={`${app?.service}-${app?.name}`} app={app} />;
};
const StyledVirtuosoContainer = styled('div')({
position: 'relative',
height: rootHeight,
width: '100%',
display: 'flex',
flexDirection: 'column',
// Hide scrollbar for WebKit browsers (Chrome, Safari)
"::-webkit-scrollbar": {
width: "0px",
height: "0px",
},
// Hide scrollbar for Firefox
scrollbarWidth: "none",
// Hide scrollbar for IE and older Edge
"-ms-overflow-style": "none",
});
return (
<AppsLibraryContainer>
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "center",
}}
>
<AppsSearchContainer>
<AppsSearchLeft>
<img src={IconSearch} />
<InputBase
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
sx={{ ml: 1, flex: 1 }}
placeholder="Search for apps"
inputProps={{
"aria-label": "Search for apps",
fontSize: "16px",
fontWeight: 400,
}}
>
<AppCircleContainer>
<AppCircle
/>
</AppsSearchLeft>
<AppsSearchRight>
{searchValue && (
<ButtonBase
onClick={() => {
setSearchValue("");
}}
>
<img src={IconClearInput} />
</ButtonBase>
)}
</AppsSearchRight>
</AppsSearchContainer>
</Box>
<Spacer height="25px" />
{searchedList?.length > 0 ? (
<StyledVirtuosoContainer>
<Virtuoso
ref={virtuosoRef}
data={searchedList}
itemContent={rowRenderer}
atBottomThreshold={50}
followOutput="smooth"
components={{
Scroller: ScrollerStyled // Use the styled scroller component
}}
/>
</StyledVirtuosoContainer>
) : (
<>
<AppLibrarySubTitle>Official Apps</AppLibrarySubTitle>
<Spacer height="18px" />
<AppsContainer>
{officialApps?.map((qapp) => {
return (
<ButtonBase
sx={{
border: "none",
height: "80px",
width: "60px",
}}
>
<Avatar
sx={{
height: "31px",
width: "31px",
}}
alt={qapp?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
qapp?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: "31px",
height: "auto",
<AppCircleContainer>
<AppCircle
sx={{
border: "none",
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
<AppCircleLabel>
{qapp?.metadata?.title || qapp?.name}
</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
);
})}
</AppsContainer>
<Spacer height="18px" />
<AppLibrarySubTitle>Featured</AppLibrarySubTitle>
>
<Avatar
sx={{
height: "31px",
width: "31px",
}}
alt={qapp?.name}
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
qapp?.name
}/qortal_avatar?async=true`}
>
<img
style={{
width: "31px",
height: "auto",
}}
src={LogoSelected}
alt="center-icon"
/>
</Avatar>
</AppCircle>
<AppCircleLabel>
{qapp?.metadata?.title || qapp?.name}
</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
);
})}
</AppsContainer>
<Spacer height="18px" />
<AppLibrarySubTitle>Featured</AppLibrarySubTitle>
<Spacer height="18px" />
<AppLibrarySubTitle>Categories</AppLibrarySubTitle>
</>
<Spacer height="18px" />
<AppLibrarySubTitle>Categories</AppLibrarySubTitle>
</>
)}
</AppsLibraryContainer>
</AppsParent>
);
};

View File

@ -0,0 +1,43 @@
import React from "react";
import {
AppsNavBarLeft,
AppsNavBarParent,
AppsNavBarRight,
} from "./Apps-styles";
import NavBack from "../../assets/svgs/NavBack.svg";
import NavCloseTab from "../../assets/svgs/NavCloseTab.svg";
import NavAdd from "../../assets/svgs/NavAdd.svg";
import NavMoreMenu from "../../assets/svgs/NavMoreMenu.svg";
import { ButtonBase } from "@mui/material";
import { executeEvent } from "../../utils/events";
export const AppsNavBar = () => {
return (
<AppsNavBarParent>
<AppsNavBarLeft>
<ButtonBase onClick={()=> {
executeEvent("navigateBack", {
});
}}>
<img src={NavBack} />
</ButtonBase>
</AppsNavBarLeft>
<AppsNavBarRight sx={{
gap: '10px'
}}>
<ButtonBase>
<img style={{
height: '40px',
width: '40px'
}} src={NavAdd} />
</ButtonBase>
<ButtonBase>
<img style={{
height: '34px',
width: '34px'
}} src={NavMoreMenu} />
</ButtonBase>
</AppsNavBarRight>
</AppsNavBarParent>
);
};

View File

@ -89,6 +89,7 @@ import { HomeDesktop } from "./HomeDesktop";
import { DesktopFooter } from "../Desktop/DesktopFooter";
import { DesktopHeader } from "../Desktop/DesktopHeader";
import { Apps } from "../Apps/Apps";
import { AppsNavBar } from "../Apps/AppsNavBar";
// let touchStartY = 0;
// let disablePullToRefresh = false;
@ -432,6 +433,7 @@ export const Group = ({
const { clearStatesMessageQueueProvider } = useMessageQueue();
const initiatedGetMembers = useRef(false);
const [groupChatTimestamps, setGroupChatTimestamps] = React.useState({});
const [appsMode, setAppsMode] = useState('viewer')
useEffect(()=> {
timestampEnterDataRef.current = timestampEnterData
@ -2229,7 +2231,7 @@ export const Group = ({
isThin={
mobileViewMode === "groups" ||
mobileViewMode === "group" ||
mobileViewModeKeepOpen === "messaging"
mobileViewModeKeepOpen === "messaging" || (mobileViewMode === "apps" && appsMode !== 'home')
}
logoutFunc={logoutFunc}
goToHome={goToHome}
@ -2735,7 +2737,7 @@ export const Group = ({
/>
)}
{isMobile && mobileViewMode === "apps" && (
<Apps />
<Apps mode={appsMode} setMode={setAppsMode} />
)}
{
!isMobile && !selectedGroup &&
@ -2962,7 +2964,7 @@ export const Group = ({
/>
</div>
{(isMobile && mobileViewMode === "home" || isMobile && mobileViewMode === "apps") && !mobileViewModeKeepOpen && (
{(isMobile && mobileViewMode === "home" || (isMobile && mobileViewMode === "apps" && appsMode === 'home')) && !mobileViewModeKeepOpen && (
<>
<div
style={{
@ -3004,6 +3006,11 @@ export const Group = ({
)}
</>
)}
{(isMobile && isMobile && mobileViewMode === "apps" && appsMode !== 'home') && !mobileViewModeKeepOpen && (
<>
<AppsNavBar />
</>
)}
</>
);
};