🐛 Bug fixes
This commit is contained in:
		| @@ -13,6 +13,7 @@ export default defineConfig({ | ||||
|     resolve: { | ||||
|       alias: { | ||||
|         '@renderer': resolve('src/renderer/src'), | ||||
|         '@main': resolve('src/main'), | ||||
|       }, | ||||
|     }, | ||||
|     plugins: [react()], | ||||
|   | ||||
| @@ -18,14 +18,22 @@ function downloadFile(task: InstallTask, url: string, output: string): Promise<v | ||||
|     const len = parseInt(res.headers['content-length'], 10) | ||||
|     let downloaded = 0 | ||||
|  | ||||
|     let lastUpdate = Date.now() | ||||
|  | ||||
|     res.data.on('data', (chunk: any) => { | ||||
|       downloaded += chunk.length | ||||
|       const percent = downloaded / len | ||||
|       if (!task.progress) return | ||||
|  | ||||
|       // Only update if 100ms has passed since the last update | ||||
|       if (Date.now() - lastUpdate >= 100) { | ||||
|         lastUpdate = Date.now() | ||||
|         if (task.progress) { | ||||
|           task.progress.value = percent | ||||
|           task.progress.period = InstallProgressPeriod.Downloading | ||||
|           task.progress.details = `${downloaded}/${len}` | ||||
|           updateInstallTask(task) | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|     res.data.pipe(writer) | ||||
| @@ -45,7 +53,7 @@ function convertAssetUri(uri: string): string { | ||||
|   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 asset = task.release.assets[platform] | ||||
|   if (!asset) return | ||||
| @@ -64,15 +72,16 @@ export async function downloadAssets(task: InstallTask): Promise<void> { | ||||
|       updateInstallTask(task) | ||||
|     } | ||||
|  | ||||
|     const stream = fs.createReadStream(output) | ||||
|  | ||||
|     switch (asset.contentType) { | ||||
|       case 'application/zip': | ||||
|         stream.on('close', () => fs.unlinkSync(output)) | ||||
|         stream.pipe(unzipper.Extract({ path: path.dirname(output) })) | ||||
|         const directory = await unzipper.Open.file(output) | ||||
|         await directory.extract({ path: task.basePath }) | ||||
|         fs.unlinkSync(output) | ||||
|         break | ||||
|       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' | ||||
|  | ||||
| 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 { | ||||
|   | ||||
| @@ -12,6 +12,7 @@ export let library: AppLibrary | ||||
| function readLocalLibrary(): AppLibrary { | ||||
|   const basePath = app.getPath('userData') | ||||
|   const libraryPath = join(basePath, 'apps_library.json') | ||||
|   console.log(`[Library] Loading library from ${libraryPath}`) | ||||
|   if (fs.existsSync(libraryPath)) { | ||||
|     const data = fs.readFileSync(libraryPath, 'utf-8') | ||||
|     library = JSON.parse(data) | ||||
|   | ||||
| @@ -30,11 +30,14 @@ export interface InstallProgress { | ||||
| export const installTasks: Record<string, InstallTask> = {} | ||||
|  | ||||
| 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 { | ||||
|   if (!task.emitter) return | ||||
|   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 ProductDetails from './pages/products/Details' | ||||
| import Tasks from './pages/Tasks' | ||||
|  | ||||
| function App(): JSX.Element { | ||||
|   const userStore = useUserStore() | ||||
| @@ -40,6 +41,7 @@ function App(): JSX.Element { | ||||
|  | ||||
|         <Routes> | ||||
|           <Route path="/" element={<Landing />} /> | ||||
|           <Route path="/tasks" element={<Tasks />} /> | ||||
|           <Route path="/products/:id" element={<ProductDetails />} /> | ||||
|         </Routes> | ||||
|       </BrowserRouter> | ||||
|   | ||||
| @@ -13,21 +13,49 @@ import { | ||||
| import { useState } from 'react' | ||||
|  | ||||
| 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 { | ||||
|   const navigate = useNavigate() | ||||
|  | ||||
|   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 ( | ||||
|     <> | ||||
|       <Drawer open={open} onClose={() => setOpen(false)} sx={{ width: '320px' }}> | ||||
|         <List sx={{ width: '320px' }}> | ||||
|           {['Inbox', 'Starred', 'Send email', 'Drafts'].map((text) => ( | ||||
|             <ListItem key={text} disablePadding> | ||||
|               <ListItemButton> | ||||
|                 <ListItemIcon> | ||||
|                   <GamepadIcon /> | ||||
|                 </ListItemIcon> | ||||
|                 <ListItemText primary={text} /> | ||||
|           {functionLinks.map((l) => ( | ||||
|             <ListItem disablePadding> | ||||
|               <ListItemButton selected={window.location.pathname == l.href} onClick={() => navigate(l.href)}> | ||||
|                 <ListItemIcon>{l.icon}</ListItemIcon> | ||||
|                 <ListItemText primary={l.title} /> | ||||
|               </ListItemButton> | ||||
|             </ListItem> | ||||
|           ))} | ||||
|   | ||||
| @@ -10,20 +10,24 @@ import { | ||||
|   TextField, | ||||
| } from '@mui/material' | ||||
| import { useEffect, useState } from 'react' | ||||
| import { 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 { useNavigate } from 'react-router' | ||||
|  | ||||
| export function MaInstallDialog({ | ||||
|   release, | ||||
|   product, | ||||
|   open, | ||||
|   onClose, | ||||
| }: { | ||||
|   release?: MaRelease | ||||
|   product?: MaProduct | ||||
|   open: boolean | ||||
|   onClose: () => void | ||||
| }): JSX.Element { | ||||
| @@ -61,6 +65,18 @@ export function MaInstallDialog({ | ||||
|     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 ( | ||||
|     <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth> | ||||
|       <DialogTitle>Install {release?.version}</DialogTitle> | ||||
| @@ -105,7 +121,7 @@ export function MaInstallDialog({ | ||||
|           </Typography> | ||||
|         )} | ||||
|         <Button onClick={onClose}>Cancel</Button> | ||||
|         <Button autoFocus disabled={!checkPlatformAvailable(window.platform) || !installPath}> | ||||
|         <Button autoFocus disabled={!checkPlatformAvailable(window.platform) || !installPath} onClick={installApp}> | ||||
|           Install | ||||
|         </Button> | ||||
|       </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> | ||||
|       </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", | ||||
|   "include": [ | ||||
|     "src/main/**/*.ts", | ||||
|     "src/renderer/src/env.d.ts", | ||||
|     "src/renderer/src/**/*", | ||||
|     "src/renderer/src/**/*.tsx", | ||||
|     "src/preload/*.d.ts" | ||||
|   ], | ||||
|   "exclude": [ | ||||
|     "src/main/index.ts" | ||||
|   ], | ||||
|   "compilerOptions": { | ||||
|     "composite": true, | ||||
|     "jsx": "react-jsx", | ||||
|     "baseUrl": ".", | ||||
|     "paths": { | ||||
|       "@main/*": [ | ||||
|         "src/main/*" | ||||
|       ], | ||||
|       "@renderer/*": [ | ||||
|         "src/renderer/src/*" | ||||
|       ] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user