🐛 Bug fixes

This commit is contained in:
LittleSheep 2025-01-11 20:59:14 +08:00
parent 3c191d3052
commit 65c3249e23
11 changed files with 166 additions and 24 deletions

View File

@ -13,6 +13,7 @@ export default defineConfig({
resolve: {
alias: {
'@renderer': resolve('src/renderer/src'),
'@main': resolve('src/main'),
},
},
plugins: [react()],

View File

@ -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
task.progress.value = percent
task.progress.period = InstallProgressPeriod.Downloading
task.progress.details = `${downloaded}/${len}`
updateInstallTask(task)
// 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
}

View File

@ -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 {

View File

@ -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)

View File

@ -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))
}

View File

@ -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>

View File

@ -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>
))}

View File

@ -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>

View 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>
)
}

View File

@ -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)}
/>
</>
)
}

View File

@ -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/*"
]