main
ts 149 lines 6.23 KB
Raw
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 import { apiCall, useApiList } from './api'
4 import { Fragment, createElement as h, useState } from 'react'
5 import { DataTable } from './DataTable'
6 import { err2msg, HFS_REPO, HTTP_FAILED_DEPENDENCY, newDialog, wantArray, xlate } from './misc'
7 import { ArrowBack, ArrowForward, Download, RemoveRedEye, Search, Warning } from '@mui/icons-material'
8 import { StringField } from '@hfs/mui-grid-form'
9 import { useDebounce } from 'usehooks-ts'
10 import { descriptionField, renderName, startPlugin, themeField } from './InstalledPlugins'
11 import { state, useSnapState } from './state'
12 import { alertDialog, confirmDialog, toast } from './dialog'
13 import _ from 'lodash'
14 import { PLUGIN_ERRORS } from './PluginsPage'
15 import { Flex, IconBtn } from './mui'
16 import { Box } from '@mui/material'
17
18 const HFS_GITHUB_ACCOUNT = HFS_REPO.replace(/\/.+/, `/`)
19
20 export default function OnlinePlugins() {
21 const [search, setSearch] = useState('')
22 const debouncedSearch = useDebounce(search, 1000)
23 const { list, error, initializing } = useApiList('get_online_plugins', { text: debouncedSearch })
24 const snap = useSnapState()
25 return h(Fragment, {},
26 h(StringField, {
27 value: search,
28 onChange: setSearch as any,
29 start: h(Search),
30 typing: true,
31 label: "Search text"
32 }),
33 h(DataTable, {
34 error: error && err2msg(xlate(error, PLUGIN_ERRORS)),
35 rows: list.length ? list : [], // workaround for DataGrid bug causing 'no rows' message to be not displayed after 'loading' was also used
36 noRows: "No compatible plugins have been found",
37 fillFlex: true,
38 initializing,
39 columnVisibilityModel: snap.onlinePluginsColumns,
40 onColumnVisibilityModelChange: newModel => Object.assign(state.onlinePluginsColumns, newModel),
41 columns: [
42 {
43 field: 'id',
44 headerName: "name",
45 flex: 1,
46 renderCell: renderName,
47 mergeRender: { description: { sx: { fontSize: 'x-small' } } },
48 },
49 {
50 field: 'version',
51 width: 70,
52 },
53 {
54 field: 'pushed_at',
55 headerName: "last update",
56 valueGetter: (value) => new Date(value).toLocaleDateString(),
57 },
58 {
59 field: 'license',
60 width: 80,
61 },
62 themeField,
63 {
64 ...descriptionField,
65 flex: 3,
66 hideUnder: 'sm',
67 },
68 {
69 field: 'stargazers_count',
70 width: 50,
71 headerName: "stars",
72 align: 'center',
73 hideUnder: 'sm',
74 },
75 ],
76 actions: ({ row, id }) => [
77 h(IconBtn, {
78 icon: Download,
79 title: "Install",
80 progress: row.downloading,
81 disabled: row.installed && "Already installed",
82 tooltipProps: { placement:'bottom-end' }, // workaround problem with horizontal scrolling by moving the tooltip leftward
83 onClick: () => installPluginFromResult(row)
84 }),
85 h(IconBtn, {
86 icon: RemoveRedEye,
87 disabled: !row.preview,
88 onClick: () => newDialog({
89 title: id,
90 Content: () => h(ShowImages, { imgs: wantArray(row.preview) })
91 })
92 }),
93 ]
94 })
95 )
96 }
97
98 function ShowImages({ imgs }: { imgs: string[] }) {
99 const [cur, setCur] = useState(0)
100 return h(Flex, { vert: true, flex: 1 },
101 h(Flex, { vert: true, center: true, height: 0, flex: 'auto', minHeight: '50vh', minWidth: '50vw' },
102 h('img', { src: imgs[cur], style: { margin: 'auto', /*center*/ maxWidth: '100%', maxHeight: '100%' /*limit*/ } }),
103 ),
104 imgs.length > 1 && h(Flex, { center: true },
105 h(IconBtn, { icon: ArrowBack, disabled: !cur, onClick: () => setCur(cur - 1) }),
106 h(IconBtn, { icon: ArrowForward, disabled: cur >= imgs.length - 1, onClick: () => setCur(cur + 1) }),
107 ),
108 )
109 }
110
111 export async function installPluginFromResult(row: any) {
112 if (!row.id.startsWith(HFS_GITHUB_ACCOUNT))
113 if (!await confirmDialog(
114 h(Flex, { vert: true, alignItems: 'center' },
115 h(Warning, { color: 'warning', fontSize: 'large' }),
116 "Proceed only if you trust this plugin",
117 h(Box, { sx: { fontSize: '60%' } }, "A plugin has the same power of any other software"),
118 ))) return
119 if (row.missing && !await confirmDialog("This will also install: " + _.map(row.missing, 'repo').join(', '))) return
120 const branch = row.branch || row.default_branch
121 return installPlugin(row.id, branch).catch((e: any) => {
122 if (e.code !== HTTP_FAILED_DEPENDENCY)
123 return alertDialog(e)
124 const msg = h(Fragment, {}, "This plugin has some dependencies unmet:",
125 e.data.map((x: any) => h('li', { key: x.repo }, x.repo + ': ' + x.error)) )
126 return alertDialog(msg, 'error')
127 })
128 }
129
130 async function installPlugin(id: string, branch?: string): Promise<any> {
131 try {
132 const res = await apiCall('download_plugin', { id, branch, stop: true }, { timeout: false })
133 if (await confirmDialog(`Plugin ${id} downloaded`, { trueText: "Start" }))
134 await startPlugin(res.id)
135 }
136 catch(e:any) {
137 let done = false
138 if (e.code === HTTP_FAILED_DEPENDENCY) // try to install automatically
139 for (const x of e.cause)
140 if (x.error === 'missing') {
141 toast("Installing dependency: " + x.repo)
142 await installPlugin(x.repo)
143 done = true
144 }
145 if (done) // try again
146 return installPlugin(id, branch)
147 throw e
148 }
149 }