🐛 Bug fixes
This commit is contained in:
parent
3c191d3052
commit
65c3249e23
@ -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
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
@ -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/*"
|
||||||
]
|
]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user