🐛 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: { resolve: {
alias: { alias: {
'@renderer': resolve('src/renderer/src'), '@renderer': resolve('src/renderer/src'),
'@main': resolve('src/main'),
}, },
}, },
plugins: [react()], 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) 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
// 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.value = percent
task.progress.period = InstallProgressPeriod.Downloading task.progress.period = InstallProgressPeriod.Downloading
task.progress.details = `${downloaded}/${len}` task.progress.details = `${downloaded}/${len}`
updateInstallTask(task) 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
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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> </Grid>
</Container> </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", "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/*"
] ]