✨ Core installer & downloader
This commit is contained in:
parent
3e9a784ab8
commit
3c191d3052
@ -29,7 +29,9 @@
|
|||||||
"@mui/icons-material": "^6.3.1",
|
"@mui/icons-material": "^6.3.1",
|
||||||
"@mui/material": "^6.3.1",
|
"@mui/material": "^6.3.1",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"axios": "^1.7.9",
|
||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
|
"glob": "^11.0.1",
|
||||||
"react-router": "^7.1.1",
|
"react-router": "^7.1.1",
|
||||||
"rehype-sanitize": "^6.0.0",
|
"rehype-sanitize": "^6.0.0",
|
||||||
"rehype-stringify": "^10.0.1",
|
"rehype-stringify": "^10.0.1",
|
||||||
@ -37,7 +39,8 @@
|
|||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.1",
|
"remark-rehype": "^11.1.1",
|
||||||
"solar-js-sdk": "^0.1.2",
|
"solar-js-sdk": "^0.1.2",
|
||||||
"unified": "^11.0.5"
|
"unified": "^11.0.5",
|
||||||
|
"unzipper": "^0.12.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||||
@ -46,6 +49,7 @@
|
|||||||
"@types/node": "^20.17.12",
|
"@types/node": "^20.17.12",
|
||||||
"@types/react": "^18.3.18",
|
"@types/react": "^18.3.18",
|
||||||
"@types/react-dom": "^18.3.5",
|
"@types/react-dom": "^18.3.5",
|
||||||
|
"@types/unzipper": "^0.10.10",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"electron": "^31.7.6",
|
"electron": "^31.7.6",
|
||||||
|
78
src/main/downloader.ts
Normal file
78
src/main/downloader.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
import axios from 'axios'
|
||||||
|
import unzipper from 'unzipper'
|
||||||
|
|
||||||
|
import { InstallProgressPeriod, InstallTask, updateInstallTask } from './tasks'
|
||||||
|
import { randomUUID } from 'crypto'
|
||||||
|
|
||||||
|
function downloadFile(task: InstallTask, url: string, output: string): Promise<void> {
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const writer = fs.createWriteStream(output)
|
||||||
|
|
||||||
|
const res = await axios.get(url as string, {
|
||||||
|
responseType: 'stream',
|
||||||
|
})
|
||||||
|
|
||||||
|
const len = parseInt(res.headers['content-length'], 10)
|
||||||
|
let downloaded = 0
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
res.data.pipe(writer)
|
||||||
|
|
||||||
|
writer.on('finish', resolve)
|
||||||
|
writer.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertAssetUri(uri: string): string {
|
||||||
|
if (uri.startsWith('attachments://')) {
|
||||||
|
return `https://api.sn.solsynth.dev/cgi/uc/attachments/${uri.replace('attachments://', '')}`
|
||||||
|
}
|
||||||
|
if (uri.startsWith('http')) {
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
throw new Error('Unable handle assets uri')
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadAssets(task: InstallTask): Promise<void> {
|
||||||
|
const platform = process.platform
|
||||||
|
const asset = task.release.assets[platform]
|
||||||
|
if (!asset) return
|
||||||
|
|
||||||
|
const output = path.join(task.basePath, randomUUID())
|
||||||
|
|
||||||
|
await downloadFile(task, convertAssetUri(asset.uri), output)
|
||||||
|
|
||||||
|
const requiresExtractTypes = ['application/zip']
|
||||||
|
|
||||||
|
if (requiresExtractTypes.includes(asset.contentType)) {
|
||||||
|
if (task.progress) {
|
||||||
|
task.progress.period = InstallProgressPeriod.Extracting
|
||||||
|
task.progress.details = null
|
||||||
|
task.progress.value = null
|
||||||
|
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) }))
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.error(new Error('Failed to decompress, unsupported type'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,10 @@
|
|||||||
import { app, shell, BrowserWindow, ipcMain } from 'electron'
|
import { app, shell, BrowserWindow, ipcMain, dialog } from 'electron'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
|
|
||||||
|
import { initInstaller } from './installer'
|
||||||
|
import { initTasks } from './tasks'
|
||||||
|
import { initLibrary } from './library'
|
||||||
import icon from '../../resources/icon.png?asset'
|
import icon from '../../resources/icon.png?asset'
|
||||||
|
|
||||||
function createWindow(): void {
|
function createWindow(): void {
|
||||||
@ -53,8 +57,15 @@ app.whenReady().then(() => {
|
|||||||
optimizer.watchWindowShortcuts(window)
|
optimizer.watchWindowShortcuts(window)
|
||||||
})
|
})
|
||||||
|
|
||||||
// IPC test
|
ipcMain.handle('show-path-select', async () => {
|
||||||
ipcMain.on('ping', () => console.log('pong'))
|
const result = await dialog.showOpenDialog({ properties: ['openDirectory', 'createDirectory'] })
|
||||||
|
if (result.canceled) return null
|
||||||
|
return result.filePaths[0]
|
||||||
|
})
|
||||||
|
|
||||||
|
initTasks()
|
||||||
|
initLibrary()
|
||||||
|
initInstaller()
|
||||||
|
|
||||||
createWindow()
|
createWindow()
|
||||||
|
|
||||||
|
103
src/main/installer.ts
Normal file
103
src/main/installer.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { randomUUID } from 'crypto'
|
||||||
|
import { exec } from 'child_process'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { ipcMain, WebContents } from 'electron'
|
||||||
|
import { glob } from 'glob'
|
||||||
|
import { unlinkSync } from 'fs'
|
||||||
|
|
||||||
|
import { installTasks, InstallTask, updateInstallTask, InstallProgressPeriod } from './tasks'
|
||||||
|
import { downloadAssets } from './downloader'
|
||||||
|
import { setAppRecord } from './library'
|
||||||
|
|
||||||
|
export function initInstaller(): void {
|
||||||
|
ipcMain.handle('install-product-release', (evt, task: InstallTask) => submitInstallTask(task, evt.sender))
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitInstallTask(task: InstallTask, contents: WebContents): void {
|
||||||
|
task.id = randomUUID()
|
||||||
|
task.progress = { value: 0, period: InstallProgressPeriod.Initializing, done: false, error: null }
|
||||||
|
task.emitter = contents
|
||||||
|
|
||||||
|
installTasks[task.id] = task
|
||||||
|
updateInstallTask(task)
|
||||||
|
|
||||||
|
downloadAssets(task)
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
await runInstaller(task)
|
||||||
|
|
||||||
|
if (task.progress) {
|
||||||
|
task.progress.period = InstallProgressPeriod.Indexing
|
||||||
|
task.progress.details = null
|
||||||
|
task.progress.value = null
|
||||||
|
updateInstallTask(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppRecord({
|
||||||
|
id: task.id ?? randomUUID(),
|
||||||
|
product: task.product,
|
||||||
|
release: task.release,
|
||||||
|
basePath: task.basePath,
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
if (task.progress) {
|
||||||
|
task.progress.error = err.toString()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (task.progress) {
|
||||||
|
task.progress.period = InstallProgressPeriod.Completed
|
||||||
|
task.progress.details = null
|
||||||
|
task.progress.value = 1
|
||||||
|
task.progress.done = true
|
||||||
|
updateInstallTask(task)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
if (task.progress) {
|
||||||
|
task.progress.error = err.toString()
|
||||||
|
task.progress.done = true
|
||||||
|
updateInstallTask(task)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runInstaller(task: InstallTask): Promise<void> {
|
||||||
|
const platform = process.platform
|
||||||
|
const installer = task.release.installers[platform]
|
||||||
|
if (!installer) return
|
||||||
|
|
||||||
|
if (task.progress) {
|
||||||
|
task.progress.period = InstallProgressPeriod.Installing
|
||||||
|
task.progress.details = null
|
||||||
|
task.progress.value = null
|
||||||
|
updateInstallTask(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installer.script) {
|
||||||
|
if (task.progress) {
|
||||||
|
task.progress.details = 'Running installer scripts...'
|
||||||
|
updateInstallTask(task)
|
||||||
|
}
|
||||||
|
exec(installer.script, {
|
||||||
|
cwd: installer.workdir ? join(task.basePath, installer.workdir) : task.basePath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installer.patches) {
|
||||||
|
if (task.progress) {
|
||||||
|
task.progress.details = 'Applying patches...'
|
||||||
|
updateInstallTask(task)
|
||||||
|
}
|
||||||
|
for (const patch of installer.patches) {
|
||||||
|
switch (patch.action) {
|
||||||
|
case 'rm':
|
||||||
|
for (const file of await glob(join(task.basePath, patch.glob), {
|
||||||
|
signal: AbortSignal.timeout(1000 * 3),
|
||||||
|
})) {
|
||||||
|
unlinkSync(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
62
src/main/library.ts
Normal file
62
src/main/library.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { ipcMain, app } from 'electron'
|
||||||
|
import { MaProduct, MaRelease } from 'solar-js-sdk'
|
||||||
|
import { join } from 'path'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
|
||||||
|
export interface AppLibrary {
|
||||||
|
apps: LocalAppRecord[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export let library: AppLibrary
|
||||||
|
|
||||||
|
function readLocalLibrary(): AppLibrary {
|
||||||
|
const basePath = app.getPath('userData')
|
||||||
|
const libraryPath = join(basePath, 'apps_library.json')
|
||||||
|
if (fs.existsSync(libraryPath)) {
|
||||||
|
const data = fs.readFileSync(libraryPath, 'utf-8')
|
||||||
|
library = JSON.parse(data)
|
||||||
|
} else {
|
||||||
|
library = { apps: [] }
|
||||||
|
saveLocalLibrary()
|
||||||
|
}
|
||||||
|
return library
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveLocalLibrary() {
|
||||||
|
const basePath = app.getPath('userData')
|
||||||
|
const libraryPath = join(basePath, 'apps_library.json')
|
||||||
|
fs.writeFileSync(libraryPath, JSON.stringify(library), 'utf-8')
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocalAppRecord {
|
||||||
|
id: string
|
||||||
|
product: MaProduct
|
||||||
|
release: MaRelease
|
||||||
|
basePath: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initLibrary(): void {
|
||||||
|
readLocalLibrary()
|
||||||
|
ipcMain.handle('get-app-library', () => getAppLibrary())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAppLibrary(): LocalAppRecord[] {
|
||||||
|
return library.apps
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAppRecord(record: LocalAppRecord) {
|
||||||
|
let current = getAppLibrary()
|
||||||
|
let hit = false
|
||||||
|
|
||||||
|
current = current.map((rec) => {
|
||||||
|
if (rec.id === record.id) hit = true
|
||||||
|
return rec.id === record.id ? record : rec
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!hit) {
|
||||||
|
current.push(record)
|
||||||
|
}
|
||||||
|
|
||||||
|
library.apps = current
|
||||||
|
saveLocalLibrary()
|
||||||
|
}
|
40
src/main/tasks.ts
Normal file
40
src/main/tasks.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { ipcMain, WebContents } from 'electron'
|
||||||
|
import { MaProduct, MaRelease } from 'solar-js-sdk'
|
||||||
|
|
||||||
|
export interface InstallTask {
|
||||||
|
id?: string
|
||||||
|
product: MaProduct
|
||||||
|
release: MaRelease
|
||||||
|
basePath: string
|
||||||
|
progress?: InstallProgress
|
||||||
|
emitter?: WebContents
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum InstallProgressPeriod {
|
||||||
|
Initializing = 0,
|
||||||
|
Downloading = 1,
|
||||||
|
Extracting = 2,
|
||||||
|
Installing = 3,
|
||||||
|
Indexing = 4,
|
||||||
|
Completed = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstallProgress {
|
||||||
|
value: number | null
|
||||||
|
period: number
|
||||||
|
details?: string | null
|
||||||
|
error: string | null
|
||||||
|
done: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const installTasks: Record<string, InstallTask> = {}
|
||||||
|
|
||||||
|
export function initTasks() {
|
||||||
|
ipcMain.handle('get-install-tasks', () => 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)
|
||||||
|
}
|
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@ -4,5 +4,6 @@ declare global {
|
|||||||
interface Window {
|
interface Window {
|
||||||
electron: ElectronAPI
|
electron: ElectronAPI
|
||||||
api: unknown
|
api: unknown
|
||||||
|
platform: NodeJS.Platform
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ if (process.contextIsolated) {
|
|||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||||
contextBridge.exposeInMainWorld('api', api)
|
contextBridge.exposeInMainWorld('api', api)
|
||||||
|
contextBridge.exposeInMainWorld('platform', process.platform)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,6 @@ import { useEffect } from 'react'
|
|||||||
import ProductDetails from './pages/products/Details'
|
import ProductDetails from './pages/products/Details'
|
||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
// const ipcHandle = (): void => window.electron.ipcRenderer.send('ping')
|
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
114
src/renderer/src/components/MaInstallDialog.tsx
Normal file
114
src/renderer/src/components/MaInstallDialog.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
Typography,
|
||||||
|
Card,
|
||||||
|
Box,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
TextField,
|
||||||
|
} from '@mui/material'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { 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'
|
||||||
|
|
||||||
|
export function MaInstallDialog({
|
||||||
|
release,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
release?: MaRelease
|
||||||
|
open: boolean
|
||||||
|
onClose: () => void
|
||||||
|
}): JSX.Element {
|
||||||
|
const [content, setContent] = useState<string>()
|
||||||
|
|
||||||
|
const [installPath, setInstallPath] = useState<string>()
|
||||||
|
|
||||||
|
async function parseContent(content: string): Promise<string> {
|
||||||
|
content = content.replace(
|
||||||
|
/!\[.*?\]\(solink:\/\/attachments\/([\w-]+)\)/g,
|
||||||
|
'',
|
||||||
|
)
|
||||||
|
|
||||||
|
const out = await unified()
|
||||||
|
.use(remarkParse)
|
||||||
|
.use(remarkRehype)
|
||||||
|
.use(remarkGfm)
|
||||||
|
.use(rehypeSanitize)
|
||||||
|
.use(rehypeStringify)
|
||||||
|
.process(content)
|
||||||
|
|
||||||
|
return String(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (release == null) return
|
||||||
|
parseContent(release.meta.content).then((out) => setContent(out))
|
||||||
|
}, [release])
|
||||||
|
|
||||||
|
function checkPlatformAvailable(platform: string): boolean {
|
||||||
|
return Object.keys(release?.assets ?? {}).includes(platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectInstallPath() {
|
||||||
|
window.electron.ipcRenderer.invoke('show-path-select').then((out: string) => setInstallPath(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||||
|
<DialogTitle>Install {release?.version}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold" component="h2">
|
||||||
|
{release?.meta.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" component="p" gutterBottom>
|
||||||
|
{release?.meta.description}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Card variant="outlined" sx={{ my: 2, maxHeight: '280px', overflowY: 'auto' }}>
|
||||||
|
<Box
|
||||||
|
component="article"
|
||||||
|
className="prose prose-md dark:prose-invert"
|
||||||
|
sx={{ p: 2 }}
|
||||||
|
dangerouslySetInnerHTML={{ __html: content ?? '' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Typography variant="subtitle1" fontWeight="bold" component="h2">
|
||||||
|
Supported Platforms
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body1" component="p" gutterBottom fontFamily="monospace" fontSize={13}>
|
||||||
|
{Object.keys(release?.assets ?? {}).join(', ')}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
placeholder="Install Path"
|
||||||
|
size="small"
|
||||||
|
value={installPath}
|
||||||
|
onClick={selectInstallPath}
|
||||||
|
slotProps={{ htmlInput: { readOnly: true } }}
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
{!checkPlatformAvailable(window.platform) && (
|
||||||
|
<Typography variant="caption" component="p" sx={{ flexGrow: 1, px: 2, opacity: 0.75 }}>
|
||||||
|
This release is not supported on your current platform.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<Button onClick={onClose}>Cancel</Button>
|
||||||
|
<Button autoFocus disabled={!checkPlatformAvailable(window.platform) || !installPath}>
|
||||||
|
Install
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
@ -5,9 +5,6 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
Container,
|
Container,
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogTitle,
|
|
||||||
FormControl,
|
FormControl,
|
||||||
Grid2 as Grid,
|
Grid2 as Grid,
|
||||||
InputLabel,
|
InputLabel,
|
||||||
@ -17,7 +14,8 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate, useParams } from 'react-router'
|
import { useNavigate, useParams } from 'react-router'
|
||||||
import { MaProduct, MaRelease, sni } from 'solar-js-sdk'
|
import { MaInstallDialog } from '@renderer/components/MaInstallDialog'
|
||||||
|
import { getAttachmentUrl, MaProduct, MaRelease, sni } 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'
|
||||||
@ -59,7 +57,6 @@ export default function ProductDetails(): JSX.Element {
|
|||||||
|
|
||||||
const [releases, setReleases] = useState<MaRelease[]>()
|
const [releases, setReleases] = useState<MaRelease[]>()
|
||||||
const [selectedRelease, setSelectedRelease] = useState<MaRelease>()
|
const [selectedRelease, setSelectedRelease] = useState<MaRelease>()
|
||||||
const [selectedReleaseContent, setSelectedReleaseContent] = useState<string>()
|
|
||||||
|
|
||||||
const [showInstaller, setShowInstaller] = useState(false)
|
const [showInstaller, setShowInstaller] = useState(false)
|
||||||
|
|
||||||
@ -69,6 +66,7 @@ export default function ProductDetails(): JSX.Element {
|
|||||||
take: 10,
|
take: 10,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
setReleases(resp.data)
|
setReleases(resp.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,11 +79,6 @@ export default function ProductDetails(): JSX.Element {
|
|||||||
parseContent(product.meta.introduction).then((out) => setContent(out))
|
parseContent(product.meta.introduction).then((out) => setContent(out))
|
||||||
}, [product?.meta?.introduction])
|
}, [product?.meta?.introduction])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedRelease == null) return
|
|
||||||
parseContent(selectedRelease.meta.content).then((out) => setSelectedReleaseContent(out))
|
|
||||||
}, [selectedRelease])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{product?.previews && (
|
{product?.previews && (
|
||||||
@ -109,7 +102,7 @@ export default function ProductDetails(): JSX.Element {
|
|||||||
{product?.icon && (
|
{product?.icon && (
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
src={product.icon}
|
src={getAttachmentUrl(product.icon)}
|
||||||
sx={{ width: 64, height: 64, border: 1, borderColor: 'divider', borderRadius: 4, mb: 1, mx: '-4px' }}
|
sx={{ width: 64, height: 64, border: 1, borderColor: 'divider', borderRadius: 4, mb: 1, mx: '-4px' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -160,6 +153,7 @@ export default function ProductDetails(): JSX.Element {
|
|||||||
fullWidth
|
fullWidth
|
||||||
sx={{ mt: 2 }}
|
sx={{ mt: 2 }}
|
||||||
startIcon={<DownloadIcon />}
|
startIcon={<DownloadIcon />}
|
||||||
|
disabled={selectedRelease == null}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectedRelease == null) return
|
if (selectedRelease == null) return
|
||||||
setShowInstaller(true)
|
setShowInstaller(true)
|
||||||
@ -173,26 +167,7 @@ export default function ProductDetails(): JSX.Element {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
<Dialog open={showInstaller} onClose={() => setShowInstaller(false)} maxWidth="sm" fullWidth>
|
<MaInstallDialog release={selectedRelease} open={showInstaller} onClose={() => setShowInstaller(false)} />
|
||||||
<DialogTitle>Install v{selectedRelease?.version}</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<Typography variant="subtitle1" fontWeight="bold" component="h2">
|
|
||||||
{selectedRelease?.meta.title}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body1" component="p" gutterBottom>
|
|
||||||
{selectedRelease?.meta.description}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Card variant="outlined" sx={{ my: 2, maxHeight: '360px', overflowY: 'auto' }}>
|
|
||||||
<Box
|
|
||||||
component="article"
|
|
||||||
className="prose prose-md dark:prose-invert"
|
|
||||||
sx={{ p: 2 }}
|
|
||||||
dangerouslySetInnerHTML={{ __html: selectedReleaseContent ?? '' }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user