✨ Launch app
This commit is contained in:
		| @@ -38,7 +38,7 @@ | |||||||
|     "remark-gfm": "^4.0.0", |     "remark-gfm": "^4.0.0", | ||||||
|     "remark-parse": "^11.0.0", |     "remark-parse": "^11.0.0", | ||||||
|     "remark-rehype": "^11.1.1", |     "remark-rehype": "^11.1.1", | ||||||
|     "solar-js-sdk": "^0.1.2", |     "solar-js-sdk": "^0.1.3", | ||||||
|     "unified": "^11.0.5", |     "unified": "^11.0.5", | ||||||
|     "unzipper": "^0.12.3" |     "unzipper": "^0.12.3" | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import { ipcMain, app } from 'electron' | |||||||
| import { MaProduct, MaRelease } from 'solar-js-sdk' | import { MaProduct, MaRelease } from 'solar-js-sdk' | ||||||
| import { join } from 'path' | import { join } from 'path' | ||||||
| import * as fs from 'fs' | import * as fs from 'fs' | ||||||
|  | import { spawn } from 'child_process' | ||||||
|  |  | ||||||
| export interface AppLibrary { | export interface AppLibrary { | ||||||
|   apps: LocalAppRecord[] |   apps: LocalAppRecord[] | ||||||
| @@ -38,7 +39,12 @@ export interface LocalAppRecord { | |||||||
|  |  | ||||||
| export function initLibrary(): void { | export function initLibrary(): void { | ||||||
|   readLocalLibrary() |   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[] { | export function getAppLibrary(): LocalAppRecord[] { | ||||||
| @@ -61,3 +67,22 @@ export function setAppRecord(record: LocalAppRecord) { | |||||||
|   library.apps = current |   library.apps = current | ||||||
|   saveLocalLibrary() |   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 ProductDetails from './pages/products/Details' | ||||||
| import Tasks from './pages/Tasks' | import Tasks from './pages/Tasks' | ||||||
|  | import Library from './pages/Library' | ||||||
|  | import LibraryDetails from './pages/library/Details' | ||||||
|  |  | ||||||
| function App(): JSX.Element { | function App(): JSX.Element { | ||||||
|   const userStore = useUserStore() |   const userStore = useUserStore() | ||||||
| @@ -42,7 +44,9 @@ function App(): JSX.Element { | |||||||
|         <Routes> |         <Routes> | ||||||
|           <Route path="/" element={<Landing />} /> |           <Route path="/" element={<Landing />} /> | ||||||
|           <Route path="/tasks" element={<Tasks />} /> |           <Route path="/tasks" element={<Tasks />} /> | ||||||
|  |           <Route path="/library" element={<Library />} /> | ||||||
|           <Route path="/products/:id" element={<ProductDetails />} /> |           <Route path="/products/:id" element={<ProductDetails />} /> | ||||||
|  |           <Route path="/library/:id" element={<LibraryDetails />} /> | ||||||
|         </Routes> |         </Routes> | ||||||
|       </BrowserRouter> |       </BrowserRouter> | ||||||
|     </ThemeProvider> |     </ThemeProvider> | ||||||
|   | |||||||
| @@ -11,14 +11,9 @@ import { | |||||||
| } from '@mui/material' | } from '@mui/material' | ||||||
| import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||||
| import { MaProduct, MaRelease } from 'solar-js-sdk' | 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 { InstallTask } from '@main/tasks' | ||||||
| import { useNavigate } from 'react-router' | import { useNavigate } from 'react-router' | ||||||
|  | import { parseContent } from '@renderer/services/parser' | ||||||
|  |  | ||||||
| export function MaInstallDialog({ | export function MaInstallDialog({ | ||||||
|   release, |   release, | ||||||
| @@ -35,23 +30,6 @@ export function MaInstallDialog({ | |||||||
|  |  | ||||||
|   const [installPath, setInstallPath] = useState<string>() |   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(() => { |   useEffect(() => { | ||||||
|     if (release == null) return |     if (release == null) return | ||||||
|     parseContent(release.meta.content).then((out) => setContent(out)) |     parseContent(release.meta.content).then((out) => setContent(out)) | ||||||
|   | |||||||
| @@ -32,7 +32,7 @@ export default function Landing(): JSX.Element { | |||||||
|   }, []) |   }, []) | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Container sx={{ py: 8, display: 'flex', flexDirection: 'column', gap: 2 }}> |     <Container sx={{ py: 4, display: 'flex', flexDirection: 'column', gap: 2 }}> | ||||||
|       <Box> |       <Box> | ||||||
|         <Typography variant="h4" component="div" sx={{ mb: 2 }}> |         <Typography variant="h4" component="div" sx={{ mb: 2 }}> | ||||||
|           Matrix Marketplace |           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 { useNavigate, useParams } from 'react-router' | ||||||
| import { MaInstallDialog } from '@renderer/components/MaInstallDialog' | import { MaInstallDialog } from '@renderer/components/MaInstallDialog' | ||||||
| import { getAttachmentUrl, MaProduct, MaRelease, sni } from 'solar-js-sdk' | 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 DownloadIcon from '@mui/icons-material/Download' | ||||||
| import ArrowBackwardIcon from '@mui/icons-material/ArrowBack' | import ArrowBackwardIcon from '@mui/icons-material/ArrowBack' | ||||||
|  | import { parseContent } from '@renderer/services/parser' | ||||||
|  |  | ||||||
| export default function ProductDetails(): JSX.Element { | export default function ProductDetails(): JSX.Element { | ||||||
|   const { id } = useParams() |   const { id } = useParams() | ||||||
| @@ -38,23 +33,6 @@ export default function ProductDetails(): JSX.Element { | |||||||
|     setProduct(data) |     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 [releases, setReleases] = useState<MaRelease[]>() | ||||||
|   const [selectedRelease, setSelectedRelease] = 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