🐛 Bug fixes
This commit is contained in:
		| @@ -13,6 +13,7 @@ export default defineConfig({ | |||||||
|     resolve: { |     resolve: { | ||||||
|       alias: { |       alias: { | ||||||
|         '@renderer': resolve('src/renderer/src'), |         '@renderer': resolve('src/renderer/src'), | ||||||
|  |         '@main': resolve('src/main'), | ||||||
|       }, |       }, | ||||||
|     }, |     }, | ||||||
|     plugins: [react()], |     plugins: [react()], | ||||||
|   | |||||||
| @@ -18,14 +18,22 @@ function downloadFile(task: InstallTask, url: string, output: string): Promise<v | |||||||
|     const len = parseInt(res.headers['content-length'], 10) |     const len = parseInt(res.headers['content-length'], 10) | ||||||
|     let downloaded = 0 |     let downloaded = 0 | ||||||
|  |  | ||||||
|  |     let lastUpdate = Date.now() | ||||||
|  |  | ||||||
|     res.data.on('data', (chunk: any) => { |     res.data.on('data', (chunk: any) => { | ||||||
|       downloaded += chunk.length |       downloaded += chunk.length | ||||||
|       const percent = downloaded / len |       const percent = downloaded / len | ||||||
|       if (!task.progress) return |  | ||||||
|       task.progress.value = percent |       // Only update if 100ms has passed since the last update | ||||||
|       task.progress.period = InstallProgressPeriod.Downloading |       if (Date.now() - lastUpdate >= 100) { | ||||||
|       task.progress.details = `${downloaded}/${len}` |         lastUpdate = Date.now() | ||||||
|       updateInstallTask(task) |         if (task.progress) { | ||||||
|  |           task.progress.value = percent | ||||||
|  |           task.progress.period = InstallProgressPeriod.Downloading | ||||||
|  |           task.progress.details = `${downloaded}/${len}` | ||||||
|  |           updateInstallTask(task) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     }) |     }) | ||||||
|  |  | ||||||
|     res.data.pipe(writer) |     res.data.pipe(writer) | ||||||
| @@ -45,7 +53,7 @@ function convertAssetUri(uri: string): string { | |||||||
|   throw new Error('Unable handle assets uri') |   throw new Error('Unable handle assets uri') | ||||||
| } | } | ||||||
|  |  | ||||||
| export async function downloadAssets(task: InstallTask): Promise<void> { | export async function downloadAssets(task: InstallTask): Promise<string | undefined> { | ||||||
|   const platform = process.platform |   const platform = process.platform | ||||||
|   const asset = task.release.assets[platform] |   const asset = task.release.assets[platform] | ||||||
|   if (!asset) return |   if (!asset) return | ||||||
| @@ -64,15 +72,16 @@ export async function downloadAssets(task: InstallTask): Promise<void> { | |||||||
|       updateInstallTask(task) |       updateInstallTask(task) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const stream = fs.createReadStream(output) |  | ||||||
|  |  | ||||||
|     switch (asset.contentType) { |     switch (asset.contentType) { | ||||||
|       case 'application/zip': |       case 'application/zip': | ||||||
|         stream.on('close', () => fs.unlinkSync(output)) |         const directory = await unzipper.Open.file(output) | ||||||
|         stream.pipe(unzipper.Extract({ path: path.dirname(output) })) |         await directory.extract({ path: task.basePath }) | ||||||
|  |         fs.unlinkSync(output) | ||||||
|         break |         break | ||||||
|       default: |       default: | ||||||
|         console.error(new Error('Failed to decompress, unsupported type')) |         throw new Error('Failed to decompress, unsupported type') | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   return output | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ import { downloadAssets } from './downloader' | |||||||
| import { setAppRecord } from './library' | import { setAppRecord } from './library' | ||||||
|  |  | ||||||
| export function initInstaller(): void { | export function initInstaller(): void { | ||||||
|   ipcMain.handle('install-product-release', (evt, task: InstallTask) => submitInstallTask(task, evt.sender)) |   ipcMain.handle('install-product-release', (evt, task: string) => submitInstallTask(JSON.parse(task), evt.sender)) | ||||||
| } | } | ||||||
|  |  | ||||||
| function submitInstallTask(task: InstallTask, contents: WebContents): void { | function submitInstallTask(task: InstallTask, contents: WebContents): void { | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ export let library: AppLibrary | |||||||
| function readLocalLibrary(): AppLibrary { | function readLocalLibrary(): AppLibrary { | ||||||
|   const basePath = app.getPath('userData') |   const basePath = app.getPath('userData') | ||||||
|   const libraryPath = join(basePath, 'apps_library.json') |   const libraryPath = join(basePath, 'apps_library.json') | ||||||
|  |   console.log(`[Library] Loading library from ${libraryPath}`) | ||||||
|   if (fs.existsSync(libraryPath)) { |   if (fs.existsSync(libraryPath)) { | ||||||
|     const data = fs.readFileSync(libraryPath, 'utf-8') |     const data = fs.readFileSync(libraryPath, 'utf-8') | ||||||
|     library = JSON.parse(data) |     library = JSON.parse(data) | ||||||
|   | |||||||
| @@ -30,11 +30,14 @@ export interface InstallProgress { | |||||||
| export const installTasks: Record<string, InstallTask> = {} | export const installTasks: Record<string, InstallTask> = {} | ||||||
|  |  | ||||||
| export function initTasks() { | export function initTasks() { | ||||||
|   ipcMain.handle('get-install-tasks', () => Object.values(installTasks)) |   ipcMain.handle('get-install-tasks', () => JSON.stringify(Object.values(installTasks))) | ||||||
| } | } | ||||||
|  |  | ||||||
| export function updateInstallTask(task: InstallTask): void { | export function updateInstallTask(task: InstallTask): void { | ||||||
|   if (!task.emitter) return |   if (!task.emitter) return | ||||||
|   if (task.id) installTasks[task.id] = task |   if (task.id) installTasks[task.id] = task | ||||||
|   task.emitter.send('update:install-task', task) |   console.log( | ||||||
|  |     `[InstallTask] Task #${task.id} updated, progress: ${task.progress?.value}, period: ${task.progress?.period}, error: ${task.progress?.error}`, | ||||||
|  |   ) | ||||||
|  |   task.emitter.send('update:install-task', JSON.stringify(task)) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import { useUserStore } from 'solar-js-sdk' | |||||||
| import { useEffect } from 'react' | import { useEffect } from 'react' | ||||||
|  |  | ||||||
| import ProductDetails from './pages/products/Details' | import ProductDetails from './pages/products/Details' | ||||||
|  | import Tasks from './pages/Tasks' | ||||||
|  |  | ||||||
| function App(): JSX.Element { | function App(): JSX.Element { | ||||||
|   const userStore = useUserStore() |   const userStore = useUserStore() | ||||||
| @@ -40,6 +41,7 @@ function App(): JSX.Element { | |||||||
|  |  | ||||||
|         <Routes> |         <Routes> | ||||||
|           <Route path="/" element={<Landing />} /> |           <Route path="/" element={<Landing />} /> | ||||||
|  |           <Route path="/tasks" element={<Tasks />} /> | ||||||
|           <Route path="/products/:id" element={<ProductDetails />} /> |           <Route path="/products/:id" element={<ProductDetails />} /> | ||||||
|         </Routes> |         </Routes> | ||||||
|       </BrowserRouter> |       </BrowserRouter> | ||||||
|   | |||||||
| @@ -13,21 +13,49 @@ import { | |||||||
| import { useState } from 'react' | import { useState } from 'react' | ||||||
|  |  | ||||||
| import GamepadIcon from '@mui/icons-material/Gamepad' | import GamepadIcon from '@mui/icons-material/Gamepad' | ||||||
|  | import ExploreIcon from '@mui/icons-material/Explore' | ||||||
|  | import AppsIcon from '@mui/icons-material/Apps' | ||||||
|  | import TaskIcon from '@mui/icons-material/Task' | ||||||
|  | import { useNavigate } from 'react-router' | ||||||
|  |  | ||||||
|  | export interface NavLink { | ||||||
|  |   title: string | ||||||
|  |   icon?: JSX.Element | ||||||
|  |   href: string | ||||||
|  | } | ||||||
|  |  | ||||||
| export function MaAppBar(): JSX.Element { | export function MaAppBar(): JSX.Element { | ||||||
|  |   const navigate = useNavigate() | ||||||
|  |  | ||||||
|   const [open, setOpen] = useState(false) |   const [open, setOpen] = useState(false) | ||||||
|  |  | ||||||
|  |   const functionLinks: NavLink[] = [ | ||||||
|  |     { | ||||||
|  |       title: 'Explore', | ||||||
|  |       icon: <ExploreIcon />, | ||||||
|  |       href: '/', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: 'Library', | ||||||
|  |       icon: <AppsIcon />, | ||||||
|  |       href: '/library', | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       title: 'Tasks', | ||||||
|  |       icon: <TaskIcon />, | ||||||
|  |       href: '/tasks', | ||||||
|  |     }, | ||||||
|  |   ] | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <Drawer open={open} onClose={() => setOpen(false)} sx={{ width: '320px' }}> |       <Drawer open={open} onClose={() => setOpen(false)} sx={{ width: '320px' }}> | ||||||
|         <List sx={{ width: '320px' }}> |         <List sx={{ width: '320px' }}> | ||||||
|           {['Inbox', 'Starred', 'Send email', 'Drafts'].map((text) => ( |           {functionLinks.map((l) => ( | ||||||
|             <ListItem key={text} disablePadding> |             <ListItem disablePadding> | ||||||
|               <ListItemButton> |               <ListItemButton selected={window.location.pathname == l.href} onClick={() => navigate(l.href)}> | ||||||
|                 <ListItemIcon> |                 <ListItemIcon>{l.icon}</ListItemIcon> | ||||||
|                   <GamepadIcon /> |                 <ListItemText primary={l.title} /> | ||||||
|                 </ListItemIcon> |  | ||||||
|                 <ListItemText primary={text} /> |  | ||||||
|               </ListItemButton> |               </ListItemButton> | ||||||
|             </ListItem> |             </ListItem> | ||||||
|           ))} |           ))} | ||||||
|   | |||||||
| @@ -10,20 +10,24 @@ import { | |||||||
|   TextField, |   TextField, | ||||||
| } from '@mui/material' | } from '@mui/material' | ||||||
| import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||||
| import { MaRelease } from 'solar-js-sdk' | import { MaProduct, MaRelease } from 'solar-js-sdk' | ||||||
| import { unified } from 'unified' | import { unified } from 'unified' | ||||||
| import rehypeSanitize from 'rehype-sanitize' | import rehypeSanitize from 'rehype-sanitize' | ||||||
| import rehypeStringify from 'rehype-stringify' | import rehypeStringify from 'rehype-stringify' | ||||||
| import remarkGfm from 'remark-gfm' | import remarkGfm from 'remark-gfm' | ||||||
| import remarkParse from 'remark-parse' | import remarkParse from 'remark-parse' | ||||||
| import remarkRehype from 'remark-rehype' | import remarkRehype from 'remark-rehype' | ||||||
|  | import { InstallTask } from '@main/tasks' | ||||||
|  | import { useNavigate } from 'react-router' | ||||||
|  |  | ||||||
| export function MaInstallDialog({ | export function MaInstallDialog({ | ||||||
|   release, |   release, | ||||||
|  |   product, | ||||||
|   open, |   open, | ||||||
|   onClose, |   onClose, | ||||||
| }: { | }: { | ||||||
|   release?: MaRelease |   release?: MaRelease | ||||||
|  |   product?: MaProduct | ||||||
|   open: boolean |   open: boolean | ||||||
|   onClose: () => void |   onClose: () => void | ||||||
| }): JSX.Element { | }): JSX.Element { | ||||||
| @@ -61,6 +65,18 @@ export function MaInstallDialog({ | |||||||
|     window.electron.ipcRenderer.invoke('show-path-select').then((out: string) => setInstallPath(out)) |     window.electron.ipcRenderer.invoke('show-path-select').then((out: string) => setInstallPath(out)) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   const navigate = useNavigate() | ||||||
|  |  | ||||||
|  |   function installApp() { | ||||||
|  |     const task: InstallTask = { | ||||||
|  |       release: release!, | ||||||
|  |       product: product!, | ||||||
|  |       basePath: installPath!, | ||||||
|  |     } | ||||||
|  |     window.electron.ipcRenderer.invoke('install-product-release', JSON.stringify(task)) | ||||||
|  |     navigate('/tasks') | ||||||
|  |   } | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth> |     <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth> | ||||||
|       <DialogTitle>Install {release?.version}</DialogTitle> |       <DialogTitle>Install {release?.version}</DialogTitle> | ||||||
| @@ -105,7 +121,7 @@ export function MaInstallDialog({ | |||||||
|           </Typography> |           </Typography> | ||||||
|         )} |         )} | ||||||
|         <Button onClick={onClose}>Cancel</Button> |         <Button onClick={onClose}>Cancel</Button> | ||||||
|         <Button autoFocus disabled={!checkPlatformAvailable(window.platform) || !installPath}> |         <Button autoFocus disabled={!checkPlatformAvailable(window.platform) || !installPath} onClick={installApp}> | ||||||
|           Install |           Install | ||||||
|         </Button> |         </Button> | ||||||
|       </DialogActions> |       </DialogActions> | ||||||
|   | |||||||
							
								
								
									
										70
									
								
								src/renderer/src/pages/Tasks.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/renderer/src/pages/Tasks.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | import { Box, Card, CardContent, Container, LinearProgress, Typography } from '@mui/material' | ||||||
|  | import { useEffect, useState } from 'react' | ||||||
|  | import { InstallTask } from '@main/tasks' | ||||||
|  |  | ||||||
|  | export default function Tasks(): JSX.Element { | ||||||
|  |   const [tasks, setTasks] = useState<InstallTask[]>([]) | ||||||
|  |  | ||||||
|  |   function initListeners() { | ||||||
|  |     window.electron.ipcRenderer.on('update:install-task', (_, raw: string) => { | ||||||
|  |       const task = JSON.parse(raw) | ||||||
|  |       setTasks((tasks) => tasks.map((t) => (t.id === task.id ? task : t))) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async function fetchTasks() { | ||||||
|  |     window.electron.ipcRenderer.invoke('get-install-tasks').then((res) => setTasks(JSON.parse(res))) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     initListeners() | ||||||
|  |     fetchTasks() | ||||||
|  |   }, []) | ||||||
|  |  | ||||||
|  |   const periodLabels = ['Initializing', 'Downloading', 'Extracting', 'Installing', 'Indexing', 'Completed'] | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Container sx={{ py: 4 }}> | ||||||
|  |       <Typography variant="h5" component="h1"> | ||||||
|  |         Tasks | ||||||
|  |       </Typography> | ||||||
|  |  | ||||||
|  |       <Box sx={{ mt: 3, display: 'flex', flexDirection: 'column', gap: 2 }}> | ||||||
|  |         {tasks.map((t) => ( | ||||||
|  |           <Card variant="outlined" key={t.id}> | ||||||
|  |             <CardContent> | ||||||
|  |               <Typography variant="h6" component="h2" lineHeight={1.2}> | ||||||
|  |                 {t.product.name} | ||||||
|  |               </Typography> | ||||||
|  |               <Typography variant="subtitle1" component="p" gutterBottom> | ||||||
|  |                 {t.release.meta.title} {t.release.version} | ||||||
|  |               </Typography> | ||||||
|  |  | ||||||
|  |               <Typography variant="body1" component="p"> | ||||||
|  |                 {periodLabels[t.progress?.period ?? 0]} · Period {(t.progress?.period ?? 0) + 1}/6 | ||||||
|  |               </Typography> | ||||||
|  |               {t.progress?.details && ( | ||||||
|  |                 <Typography variant="body2" component="p" fontFamily="monospace" fontSize={13}> | ||||||
|  |                   {t.progress.details} | ||||||
|  |                 </Typography> | ||||||
|  |               )} | ||||||
|  |  | ||||||
|  |               <Box sx={{ mt: 2 }}> | ||||||
|  |                 {t.progress?.value ? ( | ||||||
|  |                   <LinearProgress | ||||||
|  |                     variant="determinate" | ||||||
|  |                     color={t.progress?.done ? 'success' : 'primary'} | ||||||
|  |                     value={t.progress.value * 100} | ||||||
|  |                     sx={{ borderRadius: 4 }} | ||||||
|  |                   /> | ||||||
|  |                 ) : ( | ||||||
|  |                   <LinearProgress variant="indeterminate" sx={{ borderRadius: 4 }} /> | ||||||
|  |                 )} | ||||||
|  |               </Box> | ||||||
|  |             </CardContent> | ||||||
|  |           </Card> | ||||||
|  |         ))} | ||||||
|  |       </Box> | ||||||
|  |     </Container> | ||||||
|  |   ) | ||||||
|  | } | ||||||
| @@ -167,7 +167,12 @@ export default function ProductDetails(): JSX.Element { | |||||||
|         </Grid> |         </Grid> | ||||||
|       </Container> |       </Container> | ||||||
|  |  | ||||||
|       <MaInstallDialog release={selectedRelease} open={showInstaller} onClose={() => setShowInstaller(false)} /> |       <MaInstallDialog | ||||||
|  |         product={product} | ||||||
|  |         release={selectedRelease} | ||||||
|  |         open={showInstaller} | ||||||
|  |         onClose={() => setShowInstaller(false)} | ||||||
|  |       /> | ||||||
|     </> |     </> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,16 +1,23 @@ | |||||||
| { | { | ||||||
|   "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", |   "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", | ||||||
|   "include": [ |   "include": [ | ||||||
|  |     "src/main/**/*.ts", | ||||||
|     "src/renderer/src/env.d.ts", |     "src/renderer/src/env.d.ts", | ||||||
|     "src/renderer/src/**/*", |     "src/renderer/src/**/*", | ||||||
|     "src/renderer/src/**/*.tsx", |     "src/renderer/src/**/*.tsx", | ||||||
|     "src/preload/*.d.ts" |     "src/preload/*.d.ts" | ||||||
|   ], |   ], | ||||||
|  |   "exclude": [ | ||||||
|  |     "src/main/index.ts" | ||||||
|  |   ], | ||||||
|   "compilerOptions": { |   "compilerOptions": { | ||||||
|     "composite": true, |     "composite": true, | ||||||
|     "jsx": "react-jsx", |     "jsx": "react-jsx", | ||||||
|     "baseUrl": ".", |     "baseUrl": ".", | ||||||
|     "paths": { |     "paths": { | ||||||
|  |       "@main/*": [ | ||||||
|  |         "src/main/*" | ||||||
|  |       ], | ||||||
|       "@renderer/*": [ |       "@renderer/*": [ | ||||||
|         "src/renderer/src/*" |         "src/renderer/src/*" | ||||||
|       ] |       ] | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user