✨ Launch app
This commit is contained in:
		| @@ -38,7 +38,7 @@ | ||||
|     "remark-gfm": "^4.0.0", | ||||
|     "remark-parse": "^11.0.0", | ||||
|     "remark-rehype": "^11.1.1", | ||||
|     "solar-js-sdk": "^0.1.2", | ||||
|     "solar-js-sdk": "^0.1.3", | ||||
|     "unified": "^11.0.5", | ||||
|     "unzipper": "^0.12.3" | ||||
|   }, | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { ipcMain, app } from 'electron' | ||||
| import { MaProduct, MaRelease } from 'solar-js-sdk' | ||||
| import { join } from 'path' | ||||
| import * as fs from 'fs' | ||||
| import { spawn } from 'child_process' | ||||
|  | ||||
| export interface AppLibrary { | ||||
|   apps: LocalAppRecord[] | ||||
| @@ -38,7 +39,12 @@ export interface LocalAppRecord { | ||||
|  | ||||
| export function initLibrary(): void { | ||||
|   readLocalLibrary() | ||||
|   ipcMain.handle('get-app-library', () => getAppLibrary()) | ||||
|   ipcMain.handle('get-app-library', () => JSON.stringify(getAppLibrary())) | ||||
|   ipcMain.handle('get-app-library-one', (_, id: string) => | ||||
|     JSON.stringify(getAppLibrary().filter((ele) => ele.id === id)[0]), | ||||
|   ) | ||||
|  | ||||
|   ipcMain.handle('launch-app', (_, id: string) => launchApp(id)) | ||||
| } | ||||
|  | ||||
| export function getAppLibrary(): LocalAppRecord[] { | ||||
| @@ -61,3 +67,22 @@ export function setAppRecord(record: LocalAppRecord) { | ||||
|   library.apps = current | ||||
|   saveLocalLibrary() | ||||
| } | ||||
|  | ||||
| export function launchApp(id: string): void { | ||||
|   const app = getAppLibrary().filter((ele) => ele.id === id)[0] | ||||
|   if (!app) return | ||||
|  | ||||
|   const platform = process.platform | ||||
|   const runner = app.release.runners[platform] | ||||
|  | ||||
|   const segments = runner.script.split(' ') | ||||
|  | ||||
|   try { | ||||
|     spawn(segments[0], segments.slice(1), { | ||||
|       detached: true, | ||||
|       cwd: runner.workdir ? join(app.basePath, runner.workdir) : app.basePath, | ||||
|     }) | ||||
|   } catch (err: any) { | ||||
|     console.error(err) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -8,6 +8,8 @@ import { useEffect } from 'react' | ||||
|  | ||||
| import ProductDetails from './pages/products/Details' | ||||
| import Tasks from './pages/Tasks' | ||||
| import Library from './pages/Library' | ||||
| import LibraryDetails from './pages/library/Details' | ||||
|  | ||||
| function App(): JSX.Element { | ||||
|   const userStore = useUserStore() | ||||
| @@ -42,7 +44,9 @@ function App(): JSX.Element { | ||||
|         <Routes> | ||||
|           <Route path="/" element={<Landing />} /> | ||||
|           <Route path="/tasks" element={<Tasks />} /> | ||||
|           <Route path="/library" element={<Library />} /> | ||||
|           <Route path="/products/:id" element={<ProductDetails />} /> | ||||
|           <Route path="/library/:id" element={<LibraryDetails />} /> | ||||
|         </Routes> | ||||
|       </BrowserRouter> | ||||
|     </ThemeProvider> | ||||
|   | ||||
| @@ -11,14 +11,9 @@ import { | ||||
| } from '@mui/material' | ||||
| import { useEffect, useState } from 'react' | ||||
| import { MaProduct, MaRelease } from 'solar-js-sdk' | ||||
| import { unified } from 'unified' | ||||
| import rehypeSanitize from 'rehype-sanitize' | ||||
| import rehypeStringify from 'rehype-stringify' | ||||
| import remarkGfm from 'remark-gfm' | ||||
| import remarkParse from 'remark-parse' | ||||
| import remarkRehype from 'remark-rehype' | ||||
| import { InstallTask } from '@main/tasks' | ||||
| import { useNavigate } from 'react-router' | ||||
| import { parseContent } from '@renderer/services/parser' | ||||
|  | ||||
| export function MaInstallDialog({ | ||||
|   release, | ||||
| @@ -35,23 +30,6 @@ export function MaInstallDialog({ | ||||
|  | ||||
|   const [installPath, setInstallPath] = useState<string>() | ||||
|  | ||||
|   async function parseContent(content: string): Promise<string> { | ||||
|     content = content.replace( | ||||
|       /!\[.*?\]\(solink:\/\/attachments\/([\w-]+)\)/g, | ||||
|       '', | ||||
|     ) | ||||
|  | ||||
|     const out = await unified() | ||||
|       .use(remarkParse) | ||||
|       .use(remarkRehype) | ||||
|       .use(remarkGfm) | ||||
|       .use(rehypeSanitize) | ||||
|       .use(rehypeStringify) | ||||
|       .process(content) | ||||
|  | ||||
|     return String(out) | ||||
|   } | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (release == null) return | ||||
|     parseContent(release.meta.content).then((out) => setContent(out)) | ||||
|   | ||||
| @@ -32,7 +32,7 @@ export default function Landing(): JSX.Element { | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <Container sx={{ py: 8, display: 'flex', flexDirection: 'column', gap: 2 }}> | ||||
|     <Container sx={{ py: 4, display: 'flex', flexDirection: 'column', gap: 2 }}> | ||||
|       <Box> | ||||
|         <Typography variant="h4" component="div" sx={{ mb: 2 }}> | ||||
|           Matrix Marketplace | ||||
|   | ||||
							
								
								
									
										84
									
								
								src/renderer/src/pages/Library.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/renderer/src/pages/Library.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| import { LocalAppRecord } from '@main/library' | ||||
| import { | ||||
|   Avatar, | ||||
|   Card, | ||||
|   CardActionArea, | ||||
|   CardContent, | ||||
|   CardMedia, | ||||
|   Container, | ||||
|   Grid2 as Grid, | ||||
|   Typography, | ||||
| } from '@mui/material' | ||||
| import { useEffect, useState } from 'react' | ||||
| import { useNavigate } from 'react-router' | ||||
| import { getAttachmentUrl } from 'solar-js-sdk' | ||||
|  | ||||
| export default function Library(): JSX.Element { | ||||
|   const navigate = useNavigate() | ||||
|  | ||||
|   const [apps, setApps] = useState<LocalAppRecord[]>([]) | ||||
|  | ||||
|   function fetchApps() { | ||||
|     window.electron.ipcRenderer.invoke('get-app-library').then((res) => setApps(JSON.parse(res))) | ||||
|   } | ||||
|  | ||||
|   useEffect(() => { | ||||
|     fetchApps() | ||||
|   }, []) | ||||
|  | ||||
|   return ( | ||||
|     <Container sx={{ py: 4, display: 'flex', flexDirection: 'column', gap: 2 }}> | ||||
|       <Typography variant="h5" component="h1"> | ||||
|         Library | ||||
|       </Typography> | ||||
|  | ||||
|       <Grid container columns={{ xs: 1, sm: 2, md: 3 }} spacing={2}> | ||||
|         {apps.map((p) => ( | ||||
|           <Grid size={1} key={p.id}> | ||||
|             <Card> | ||||
|               <CardActionArea onClick={() => navigate('/library/' + p.id)}> | ||||
|                 {p.product.previews && ( | ||||
|                   <CardMedia | ||||
|                     sx={{ aspectRatio: 16 / 5 }} | ||||
|                     image={getAttachmentUrl(p.product.previews[0])} | ||||
|                     title="green iguana" | ||||
|                   /> | ||||
|                 )} | ||||
|                 <CardContent> | ||||
|                   {p.product.icon && ( | ||||
|                     <Avatar | ||||
|                       variant="rounded" | ||||
|                       src={getAttachmentUrl(p.product.icon)} | ||||
|                       sx={{ | ||||
|                         width: 48, | ||||
|                         height: 48, | ||||
|                         border: 1, | ||||
|                         borderColor: 'divider', | ||||
|                         borderRadius: 4, | ||||
|                         mb: 0.75, | ||||
|                         mx: '-4px', | ||||
|                       }} | ||||
|                     /> | ||||
|                   )} | ||||
|                   <Typography variant="h5" component="div"> | ||||
|                     {p.product.name} | ||||
|                   </Typography> | ||||
|                   <Typography variant="body2" component="div" gutterBottom> | ||||
|                     {p.product.description} | ||||
|                   </Typography> | ||||
|  | ||||
|                   <Typography fontSize={13} fontWeight="bold"> | ||||
|                     Installed | ||||
|                   </Typography> | ||||
|                   <Typography variant="subtitle1" fontSize={14} lineHeight={1}> | ||||
|                     {p.release.meta.title} {p.release.version} | ||||
|                   </Typography> | ||||
|                 </CardContent> | ||||
|               </CardActionArea> | ||||
|             </Card> | ||||
|           </Grid> | ||||
|         ))} | ||||
|       </Grid> | ||||
|     </Container> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										109
									
								
								src/renderer/src/pages/library/Details.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/renderer/src/pages/library/Details.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| import { LocalAppRecord } from '@main/library' | ||||
| import { Avatar, Box, Button, Container, Grid2 as Grid, IconButton, Stack, Typography } from '@mui/material' | ||||
| import { useEffect, useMemo, useState } from 'react' | ||||
| import { useNavigate, useParams } from 'react-router' | ||||
| import { getAttachmentUrl } from 'solar-js-sdk' | ||||
| import { parseContent } from '@renderer/services/parser' | ||||
|  | ||||
| import PlayIcon from '@mui/icons-material/PlayArrow' | ||||
| import ArrowBackwardIcon from '@mui/icons-material/ArrowBack' | ||||
|  | ||||
| export default function LibraryDetails(): JSX.Element { | ||||
|   const { id } = useParams() | ||||
|   const navigate = useNavigate() | ||||
|  | ||||
|   const [app, setApp] = useState<LocalAppRecord>() | ||||
|   const [appProductContent, setAppProductContent] = useState<string>() | ||||
|   const [appReleaseContent, setAppReleaseContent] = useState<string>() | ||||
|  | ||||
|   const product = useMemo(() => app?.product, [app]) | ||||
|  | ||||
|   function fetchApp() { | ||||
|     window.electron.ipcRenderer.invoke('get-app-library-one', id).then((res) => { | ||||
|       const app = JSON.parse(res) | ||||
|       if (app == null) { | ||||
|         navigate('/library') | ||||
|         return | ||||
|       } | ||||
|       return setApp(app) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   useEffect(() => { | ||||
|     fetchApp() | ||||
|   }, []) | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (app) { | ||||
|       parseContent(app.product.meta.introduction).then((out) => setAppProductContent(out)) | ||||
|       parseContent(app.release.meta.content).then((out) => setAppReleaseContent(out)) | ||||
|     } | ||||
|   }, [app]) | ||||
|  | ||||
|   function launchApp() { | ||||
|     window.electron.ipcRenderer.invoke('launch-app', app?.id) | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       {product?.previews && ( | ||||
|         <img | ||||
|           src={product.previews[0]} | ||||
|           style={{ aspectRatio: 16 / 5, width: '100%', objectFit: 'cover' }} | ||||
|           className="border-b border-1 pb" | ||||
|         /> | ||||
|       )} | ||||
|  | ||||
|       <Container sx={{ py: 4 }}> | ||||
|         <Grid container spacing={2}> | ||||
|           <Grid size={8} sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> | ||||
|             <Box> | ||||
|               <Button | ||||
|                 size="small" | ||||
|                 startIcon={<ArrowBackwardIcon />} | ||||
|                 onClick={() => navigate('/library')} | ||||
|                 sx={{ mb: 1 }} | ||||
|               > | ||||
|                 Back | ||||
|               </Button> | ||||
|             </Box> | ||||
|  | ||||
|             <Box> | ||||
|               {product?.icon && ( | ||||
|                 <Avatar | ||||
|                   variant="rounded" | ||||
|                   src={getAttachmentUrl(product.icon)} | ||||
|                   sx={{ width: 64, height: 64, border: 1, borderColor: 'divider', borderRadius: 4, mb: 1, mx: '-4px' }} | ||||
|                 /> | ||||
|               )} | ||||
|               <Typography variant="h5" component="h1" maxWidth="sm"> | ||||
|                 {product?.name} | ||||
|               </Typography> | ||||
|               <Typography variant="body1" component="p" maxWidth="sm"> | ||||
|                 {product?.description} | ||||
|               </Typography> | ||||
|             </Box> | ||||
|  | ||||
|             {appProductContent && ( | ||||
|               <Box | ||||
|                 component="article" | ||||
|                 className="prose prose-lg dark:prose-invert" | ||||
|                 dangerouslySetInnerHTML={{ __html: appProductContent ?? '' }} | ||||
|               /> | ||||
|             )} | ||||
|           </Grid> | ||||
|  | ||||
|           <Grid size={4}> | ||||
|             <Stack spacing={2} alignItems="end"> | ||||
|               <Box> | ||||
|                 <IconButton color="success" onClick={launchApp}> | ||||
|                   <PlayIcon /> | ||||
|                 </IconButton> | ||||
|               </Box> | ||||
|             </Stack> | ||||
|           </Grid> | ||||
|         </Grid> | ||||
|       </Container> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
| @@ -16,15 +16,10 @@ import { useEffect, useState } from 'react' | ||||
| import { useNavigate, useParams } from 'react-router' | ||||
| import { MaInstallDialog } from '@renderer/components/MaInstallDialog' | ||||
| import { getAttachmentUrl, MaProduct, MaRelease, sni } from 'solar-js-sdk' | ||||
| import { unified } from 'unified' | ||||
| import rehypeSanitize from 'rehype-sanitize' | ||||
| import rehypeStringify from 'rehype-stringify' | ||||
| import remarkGfm from 'remark-gfm' | ||||
| import remarkParse from 'remark-parse' | ||||
| import remarkRehype from 'remark-rehype' | ||||
|  | ||||
| import DownloadIcon from '@mui/icons-material/Download' | ||||
| import ArrowBackwardIcon from '@mui/icons-material/ArrowBack' | ||||
| import { parseContent } from '@renderer/services/parser' | ||||
|  | ||||
| export default function ProductDetails(): JSX.Element { | ||||
|   const { id } = useParams() | ||||
| @@ -38,23 +33,6 @@ export default function ProductDetails(): JSX.Element { | ||||
|     setProduct(data) | ||||
|   } | ||||
|  | ||||
|   async function parseContent(content: string): Promise<string> { | ||||
|     content = content.replace( | ||||
|       /!\[.*?\]\(solink:\/\/attachments\/([\w-]+)\)/g, | ||||
|       '', | ||||
|     ) | ||||
|  | ||||
|     const out = await unified() | ||||
|       .use(remarkParse) | ||||
|       .use(remarkRehype) | ||||
|       .use(remarkGfm) | ||||
|       .use(rehypeSanitize) | ||||
|       .use(rehypeStringify) | ||||
|       .process(content) | ||||
|  | ||||
|     return String(out) | ||||
|   } | ||||
|  | ||||
|   const [releases, setReleases] = useState<MaRelease[]>() | ||||
|   const [selectedRelease, setSelectedRelease] = useState<MaRelease>() | ||||
|  | ||||
|   | ||||
							
								
								
									
										23
									
								
								src/renderer/src/services/parser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/renderer/src/services/parser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import { unified } from 'unified' | ||||
| import rehypeSanitize from 'rehype-sanitize' | ||||
| import rehypeStringify from 'rehype-stringify' | ||||
| import remarkGfm from 'remark-gfm' | ||||
| import remarkParse from 'remark-parse' | ||||
| import remarkRehype from 'remark-rehype' | ||||
|  | ||||
| export async function parseContent(content: string): Promise<string> { | ||||
|   content = content.replace( | ||||
|     /!\[.*?\]\(solink:\/\/attachments\/([\w-]+)\)/g, | ||||
|     '', | ||||
|   ) | ||||
|  | ||||
|   const out = await unified() | ||||
|     .use(remarkParse) | ||||
|     .use(remarkRehype) | ||||
|     .use(remarkGfm) | ||||
|     .use(rehypeSanitize) | ||||
|     .use(rehypeStringify) | ||||
|     .process(content) | ||||
|  | ||||
|   return String(out) | ||||
| } | ||||
		Reference in New Issue
	
	Block a user