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