hfs 0.26.6 → 0.26.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/admin/.DS_Store +0 -0
  2. package/admin/.eslintrc +8 -0
  3. package/admin/.gitignore +23 -0
  4. package/admin/index.html +1 -3
  5. package/admin/package.json +67 -0
  6. package/admin/{logo.svg → public/logo.svg} +0 -0
  7. package/admin/src/AccountForm.ts +92 -0
  8. package/admin/src/AccountsPage.ts +143 -0
  9. package/admin/src/App.ts +83 -0
  10. package/admin/src/ArrayField.ts +84 -0
  11. package/admin/src/ConfigPage.ts +279 -0
  12. package/admin/src/FileField.ts +52 -0
  13. package/admin/src/FileForm.ts +148 -0
  14. package/admin/src/FilePicker.ts +166 -0
  15. package/admin/src/HomePage.ts +96 -0
  16. package/admin/src/InstalledPlugins.ts +158 -0
  17. package/admin/src/LoginRequired.ts +75 -0
  18. package/admin/src/LogoutPage.ts +27 -0
  19. package/admin/src/LogsPage.ts +75 -0
  20. package/admin/src/MainMenu.ts +74 -0
  21. package/admin/src/MenuButton.ts +38 -0
  22. package/admin/src/MonitorPage.ts +200 -0
  23. package/admin/src/OnlinePlugins.ts +101 -0
  24. package/admin/src/PermField.ts +80 -0
  25. package/admin/src/PluginsPage.ts +27 -0
  26. package/admin/src/VfsMenuBar.ts +58 -0
  27. package/admin/src/VfsPage.ts +124 -0
  28. package/admin/src/VfsTree.ts +95 -0
  29. package/admin/src/addFiles.ts +59 -0
  30. package/admin/src/api.ts +246 -0
  31. package/admin/src/dialog.ts +203 -0
  32. package/admin/src/index.css +21 -0
  33. package/admin/src/index.ts +10 -0
  34. package/admin/src/md.ts +31 -0
  35. package/admin/src/misc.ts +141 -0
  36. package/admin/src/react-app-env.d.ts +1 -0
  37. package/admin/src/reportWebVitals.ts +15 -0
  38. package/admin/src/setupTests.ts +5 -0
  39. package/admin/src/state.ts +40 -0
  40. package/admin/src/theme.ts +37 -0
  41. package/admin/tsconfig.json +26 -0
  42. package/admin/vite.config.ts +32 -0
  43. package/frontend/.DS_Store +0 -0
  44. package/frontend/.eslintrc +8 -0
  45. package/frontend/.gitignore +23 -0
  46. package/frontend/index.html +1 -3
  47. package/frontend/package.json +51 -0
  48. package/frontend/{fontello.css → public/fontello.css} +0 -0
  49. package/frontend/{fontello.woff2 → public/fontello.woff2} +0 -0
  50. package/frontend/src/App.ts +25 -0
  51. package/frontend/src/Breadcrumbs.ts +43 -0
  52. package/frontend/src/BrowseFiles.ts +141 -0
  53. package/frontend/src/Head.ts +45 -0
  54. package/frontend/src/UserPanel.ts +52 -0
  55. package/frontend/src/api.ts +78 -0
  56. package/frontend/src/components.ts +54 -0
  57. package/frontend/src/dialog.css +76 -0
  58. package/frontend/src/dialog.ts +105 -0
  59. package/frontend/src/icons.ts +46 -0
  60. package/frontend/src/index.scss +307 -0
  61. package/frontend/src/index.ts +10 -0
  62. package/frontend/src/login.ts +50 -0
  63. package/frontend/src/menu.ts +188 -0
  64. package/frontend/src/misc.ts +54 -0
  65. package/frontend/src/options.ts +52 -0
  66. package/frontend/src/react-app-env.d.ts +1 -0
  67. package/frontend/src/reportWebVitals.ts +15 -0
  68. package/frontend/src/setupTests.ts +5 -0
  69. package/frontend/src/state.ts +82 -0
  70. package/frontend/src/useAuthorized.ts +17 -0
  71. package/frontend/src/useFetchList.ts +144 -0
  72. package/frontend/src/useTheme.ts +23 -0
  73. package/frontend/tsconfig.json +26 -0
  74. package/frontend/vite.config.ts +21 -0
  75. package/package.json +2 -1
  76. package/plugins/vhosting/plugin.js +1 -1
  77. package/src/QuickZipStream.ts +279 -0
  78. package/src/ThrottledStream.ts +98 -0
  79. package/src/adminApis.ts +161 -0
  80. package/src/api.accounts.ts +78 -0
  81. package/src/api.auth.ts +131 -0
  82. package/src/api.file_list.ts +102 -0
  83. package/src/api.helpers.ts +30 -0
  84. package/src/api.monitor.ts +106 -0
  85. package/src/api.plugins.ts +139 -0
  86. package/src/api.vfs.ts +182 -0
  87. package/src/apiMiddleware.ts +124 -0
  88. package/src/block.ts +35 -0
  89. package/src/commands.ts +122 -0
  90. package/src/config.ts +166 -0
  91. package/src/connections.ts +60 -0
  92. package/src/const.ts +57 -0
  93. package/src/crypt.ts +16 -0
  94. package/src/debounceAsync.ts +51 -0
  95. package/src/events.ts +6 -0
  96. package/src/frontEndApis.ts +17 -0
  97. package/src/github.ts +102 -0
  98. package/src/index.ts +53 -0
  99. package/src/listen.ts +220 -0
  100. package/src/log.ts +128 -0
  101. package/src/middlewares.ts +176 -0
  102. package/src/misc.ts +149 -0
  103. package/src/pbkdf2.ts +83 -0
  104. package/src/perm.ts +194 -0
  105. package/src/plugins.ts +342 -0
  106. package/src/serveFile.ts +104 -0
  107. package/src/serveGuiFiles.ts +95 -0
  108. package/src/sse.ts +29 -0
  109. package/src/throttler.ts +106 -0
  110. package/src/update.ts +67 -0
  111. package/src/util-files.ts +137 -0
  112. package/src/util-generators.ts +29 -0
  113. package/src/util-http.ts +29 -0
  114. package/src/vfs.ts +258 -0
  115. package/src/watchLoad.ts +75 -0
  116. package/src/zip.ts +69 -0
  117. package/admin/assets/index.3129dad1.js +0 -282
  118. package/admin/assets/index.dcc78777.css +0 -1
  119. package/admin/assets/sha512.e9b1ee42.js +0 -8
  120. package/frontend/assets/index.1151988f.js +0 -85
  121. package/frontend/assets/index.93366732.css +0 -1
  122. package/frontend/assets/sha512.bb881250.js +0 -8
  123. package/src/QuickZipStream.js +0 -285
  124. package/src/ThrottledStream.js +0 -93
  125. package/src/adminApis.js +0 -169
  126. package/src/api.accounts.js +0 -59
  127. package/src/api.auth.js +0 -130
  128. package/src/api.file_list.js +0 -103
  129. package/src/api.helpers.js +0 -32
  130. package/src/api.monitor.js +0 -102
  131. package/src/api.plugins.js +0 -127
  132. package/src/api.vfs.js +0 -164
  133. package/src/apiMiddleware.js +0 -136
  134. package/src/block.js +0 -33
  135. package/src/commands.js +0 -124
  136. package/src/config.js +0 -168
  137. package/src/connections.js +0 -57
  138. package/src/const.js +0 -83
  139. package/src/crypt.js +0 -21
  140. package/src/debounceAsync.js +0 -48
  141. package/src/events.js +0 -9
  142. package/src/frontEndApis.js +0 -38
  143. package/src/github.js +0 -102
  144. package/src/index.js +0 -55
  145. package/src/listen.js +0 -235
  146. package/src/log.js +0 -137
  147. package/src/middlewares.js +0 -154
  148. package/src/misc.js +0 -160
  149. package/src/pbkdf2.js +0 -74
  150. package/src/perm.js +0 -176
  151. package/src/plugins.js +0 -343
  152. package/src/serveFile.js +0 -104
  153. package/src/serveGuiFiles.js +0 -113
  154. package/src/sse.js +0 -29
  155. package/src/throttler.js +0 -91
  156. package/src/update.js +0 -69
  157. package/src/util-files.js +0 -148
  158. package/src/util-generators.js +0 -30
  159. package/src/util-http.js +0 -30
  160. package/src/vfs.js +0 -226
  161. package/src/watchLoad.js +0 -73
  162. package/src/zip.js +0 -69
@@ -0,0 +1,59 @@
1
+ // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+
3
+ import { alertDialog, newDialog, promptDialog } from './dialog'
4
+ import { createElement as h, Fragment } from 'react'
5
+ import { Box } from '@mui/material'
6
+ import { VfsNode, reloadVfs } from './VfsPage'
7
+ import { state } from './state'
8
+ import { apiCall } from './api'
9
+ import FilePicker from './FilePicker'
10
+ import { onlyTruthy } from './misc'
11
+
12
+ export default function addFiles() {
13
+ const close = newDialog({
14
+ title: "Add files or folders",
15
+ dialogProps: { sx:{ minWidth: 'min(90vw, 40em)', minHeight: 'calc(100vh - 9em)' } },
16
+ Content() {
17
+ const under = getUnder()
18
+ return h(Fragment, {},
19
+ h(Box, { sx:{ typography: 'body1', px: 1, py: 2 } },
20
+ "Selected elements will be added to virtual path " + (under || '(home)')),
21
+ h(FilePicker, {
22
+ async onSelect(sel) {
23
+ let failed = await Promise.all(sel.map(source =>
24
+ apiCall('add_vfs', { under, source }).then(() => '', () => source) ))
25
+ failed = onlyTruthy(failed)
26
+ if (failed.length)
27
+ await alertDialog("Some elements have been rejected: "+failed.join(', '), 'error')
28
+ reloadVfs()
29
+ close()
30
+ }
31
+ })
32
+ )
33
+ }
34
+ })
35
+ }
36
+
37
+ export async function addVirtual() {
38
+ try {
39
+ const name = await promptDialog("Enter folder name")
40
+ if (!name) return
41
+ const under = getUnder()
42
+ await apiCall('add_vfs', { under, name })
43
+ reloadVfs([ (under||'') + '/' + name ])
44
+ await alertDialog(`Folder "${name}" created`, 'success')
45
+ }
46
+ catch(e) {
47
+ await alertDialog(e as Error)
48
+ }
49
+ }
50
+
51
+ function getUnder() {
52
+ let f: VfsNode | undefined = state.selectedFiles[0]
53
+ if (!f)
54
+ return ''
55
+ if (f.type !== 'folder')
56
+ f = f.parent
57
+ const { id } = f!
58
+ return id === '/' ? '' : id
59
+ }
@@ -0,0 +1,246 @@
1
+ // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+
3
+ import { createElement as h, useCallback, useEffect, useMemo, useRef } from 'react'
4
+ import { Dict, err2msg, Falsy, getCookie, IconBtn, spinner, useStateMounted, wantArray } from './misc'
5
+ import { Alert } from '@mui/material'
6
+ import _ from 'lodash'
7
+ import { state } from './state'
8
+ import { Refresh } from '@mui/icons-material'
9
+ import produce, { Draft } from 'immer'
10
+
11
+ export function useApiEx<T=any>(...args: Parameters<typeof useApi>) {
12
+ const [data, error, reload] = useApi<T>(...args)
13
+ const cmd = args[0]
14
+ const loading = data === undefined
15
+ const element = useMemo(() =>
16
+ !cmd ? null
17
+ : error ? h(Alert, { severity: 'error' }, String(error), h(IconBtn, { icon: Refresh, onClick: reload, sx: { m:'-8px 0 -8px 16px' } }))
18
+ : loading ? spinner()
19
+ : null,
20
+ [error, cmd, loading, reload])
21
+ return { data, error, reload, loading, element }
22
+ }
23
+
24
+ const PREFIX = '/~/api/'
25
+
26
+ const timeoutByApi: Dict = {
27
+ get_status: 20 // can be lengthy on slow machines because of the find-process-on-busy-port feature
28
+ }
29
+ export function apiCall(cmd: string, params?: Dict, { timeout=undefined }={}) : Promise<any> {
30
+ const csrf = getCsrf()
31
+ if (csrf)
32
+ params = { csrf, ...params }
33
+
34
+ const controller = new AbortController()
35
+ if (timeout !== false)
36
+ setTimeout(() => controller.abort('timeout'), 1000*(timeoutByApi[cmd] ?? timeout ?? 10))
37
+ return fetch(PREFIX+cmd, {
38
+ method: 'POST',
39
+ headers: { 'content-type': 'application/json' },
40
+ signal: controller.signal,
41
+ body: params && JSON.stringify(params),
42
+ }).then(async res => {
43
+ if (res.ok)
44
+ return res.json().then(json => {
45
+ console.debug('API', cmd, params, '>>', json)
46
+ return json
47
+ })
48
+ const msg = await res.text() || 'Failed API ' + cmd
49
+ console.warn(msg + (params ? ' ' + JSON.stringify(params) : ''))
50
+ if (res.status === 401)
51
+ state.loginRequired = true
52
+ throw new ApiError(res.status, msg)
53
+ }, err => {
54
+ if (err?.message?.includes('fetch'))
55
+ throw Error("Network error")
56
+ throw err
57
+ })
58
+ }
59
+
60
+ export class ApiError extends Error {
61
+ constructor(readonly code:number, message: string) {
62
+ super(message);
63
+ }
64
+ }
65
+
66
+ export function useApi<T=any>(cmd: string | Falsy, params?: object) : [T | undefined, undefined | Error, ()=>void] {
67
+ const [ret, setRet] = useStateMounted<T | undefined>(undefined)
68
+ const [err, setErr] = useStateMounted<Error | undefined>(undefined)
69
+ const [forcer, setForcer] = useStateMounted(0)
70
+ const loadingRef = useRef(false)
71
+ useEffect(()=>{
72
+ setRet(undefined)
73
+ setErr(undefined)
74
+ if (!cmd) return
75
+ loadingRef.current = true
76
+ apiCall(cmd, params)
77
+ .then(setRet, setErr)
78
+ .finally(()=> loadingRef.current = false)
79
+ }, [cmd, JSON.stringify(params), forcer]) //eslint-disable-line -- json-ize to detect deep changes
80
+ const reload = useCallback(()=> loadingRef.current || setForcer(v => v+1), [setForcer])
81
+ return [ret, err, reload]
82
+ }
83
+
84
+ type EventHandler = (type:string, data?:any) => void
85
+
86
+ export function apiEvents(cmd: string, params: Dict, cb:EventHandler) {
87
+ console.debug('API EVENTS', cmd, params)
88
+ const csrf = getCsrf()
89
+ const processed: Record<string,string> = { csrf: csrf && JSON.stringify(csrf) }
90
+ for (const k in params) {
91
+ const v = params[k]
92
+ if (v === undefined) continue
93
+ processed[k] = JSON.stringify(v)
94
+ }
95
+ const source = new EventSource(PREFIX + cmd + '?' + new URLSearchParams(processed))
96
+ source.onopen = () => cb('connected')
97
+ source.onerror = err => cb('error', err)
98
+ source.onmessage = ({ data }) => {
99
+ if (!data) {
100
+ cb('closed')
101
+ return source.close()
102
+ }
103
+ try { data = JSON.parse(data) }
104
+ catch {
105
+ return cb('string', data)
106
+ }
107
+ console.debug('SSE msg', data)
108
+ cb('msg', data)
109
+ }
110
+ return source
111
+ }
112
+
113
+ function getCsrf() {
114
+ return getCookie('csrf')
115
+ }
116
+
117
+ export function useApiEvents(cmd: string, params: Dict={}) {
118
+ const [data, setData] = useStateMounted<any>(undefined)
119
+ const [error, setError] = useStateMounted<any>(undefined)
120
+ const [loading, setLoading] = useStateMounted(false)
121
+ useEffect(() => {
122
+ const src = apiEvents(cmd, params, (type, data) => {
123
+ switch (type) {
124
+ case 'error':
125
+ setError("Connection error")
126
+ return stop()
127
+ case 'closed':
128
+ return stop()
129
+ case 'msg':
130
+ if (src?.readyState === src?.CLOSED)
131
+ return stop()
132
+ return setData(data)
133
+ }
134
+ })
135
+ return () => {
136
+ src.close()
137
+ stop()
138
+ }
139
+
140
+ function stop() {
141
+ setLoading(false)
142
+ }
143
+ }, [cmd, JSON.stringify(params)]) //eslint-disable-line
144
+ return { data, loading, error }
145
+ }
146
+
147
+ export function useApiList<T=any>(cmd:string|Falsy, params: Dict={}, { addId=false, map=((x:any)=>x) }={}) {
148
+ const [list, setList] = useStateMounted<T[]>([])
149
+ const [error, setError] = useStateMounted<any>(undefined)
150
+ const [connecting, setConnecting] = useStateMounted(true)
151
+ const [loading, setLoading] = useStateMounted(false)
152
+ const [initializing, setInitializing] = useStateMounted(true)
153
+ const idRef = useRef(0)
154
+ useEffect(() => {
155
+ if (!cmd) return
156
+ const buffer: T[] = []
157
+ const apply = _.debounce(() => {
158
+ const chunk = buffer.splice(0, Infinity)
159
+ if (chunk.length)
160
+ setList(list => [ ...list, ...chunk ])
161
+ }, 1000, { maxWait: 1000 })
162
+ setError(undefined)
163
+ setLoading(true)
164
+ setConnecting(true)
165
+ setInitializing(true)
166
+ setList([])
167
+ const src = apiEvents(cmd, params, (type, data) => {
168
+ switch (type) {
169
+ case 'connected':
170
+ setConnecting(false)
171
+ return setTimeout(() => apply.flush()) // this trick we'll cause first entries to be rendered almost immediately, while the rest will be subject to normal debouncing
172
+ case 'error':
173
+ setError("Connection error")
174
+ return stop()
175
+ case 'closed':
176
+ return stop()
177
+ case 'msg':
178
+ wantArray(data).forEach(data => {
179
+ if (data === 'ready') {
180
+ apply.flush()
181
+ setInitializing(false)
182
+ return
183
+ }
184
+ if (data.error)
185
+ return setError(err2msg(data.error))
186
+ if (data.add) {
187
+ const rec = map(data.add)
188
+ if (addId)
189
+ rec.id = ++idRef.current
190
+ buffer.push(rec)
191
+ apply()
192
+ return
193
+ }
194
+ if (data.remove) {
195
+ const matchOnList: ReturnType<typeof _.matches>[] = []
196
+ // first remove from the buffer
197
+ for (const key of data.remove) {
198
+ const match1 = _.matches(key)
199
+ if (_.isEmpty(_.remove(buffer, match1)))
200
+ matchOnList.push(match1)
201
+ }
202
+ // then work the hooked state
203
+ if (_.isEmpty(matchOnList))
204
+ return
205
+ setList(list => {
206
+ const filtered = list.filter(rec => !matchOnList.some(match1 => match1(rec)))
207
+ return filtered.length < list.length ? filtered : list // avoid unnecessary changes
208
+ })
209
+ return
210
+ }
211
+ if (data.update) {
212
+ apply.flush() // avoid treating buffer
213
+ setList(list => {
214
+ const modified = [...list]
215
+ for (const { search, change } of data.update) {
216
+ const idx = modified.findIndex(_.matches(search))
217
+ if (idx >= 0)
218
+ modified[idx] = { ...modified[idx], ...change }
219
+ }
220
+ return modified
221
+ })
222
+ return
223
+ }
224
+ console.debug('unknown api event', type, data)
225
+ })
226
+ if (src?.readyState === src?.CLOSED)
227
+ stop()
228
+ }
229
+ })
230
+
231
+ return () => src.close()
232
+
233
+ function stop() {
234
+ setInitializing(false)
235
+ setLoading(false)
236
+ apply.flush()
237
+ }
238
+ }, [cmd, JSON.stringify(params)]) //eslint-disable-line
239
+ return { list, loading, error, initializing, connecting, setList, updateList }
240
+
241
+ function updateList(cb: (toModify: Draft<typeof list>) => void) {
242
+ setList(produce(list, x => {
243
+ cb(x)
244
+ }))
245
+ }
246
+ }
@@ -0,0 +1,203 @@
1
+ // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+
3
+ import {
4
+ Box,
5
+ Button,
6
+ CircularProgress,
7
+ Dialog as MuiDialog,
8
+ DialogContent,
9
+ DialogProps,
10
+ DialogTitle,
11
+ IconButton
12
+ } from '@mui/material'
13
+ import {
14
+ createElement as h, Fragment,
15
+ isValidElement,
16
+ ReactElement,
17
+ useEffect,
18
+ useRef,
19
+ useState
20
+ } from 'react'
21
+ import { Check, Close, Error as ErrorIcon, Forward, Info, Warning } from '@mui/icons-material'
22
+ import { newDialog, closeDialog, dialogsDefaults, DialogOptions } from '@hfs/shared'
23
+ import { Form, FormProps } from '@hfs/mui-grid-form'
24
+ import { useBreakpoint } from './misc'
25
+ import { Flex } from '@hfs/frontend/src/components'
26
+ export * from '@hfs/shared/lib/dialogs'
27
+
28
+ dialogsDefaults.Container = function Container(d:DialogOptions) {
29
+ useEffect(()=>{
30
+ ref.current?.focus()
31
+ }, [])
32
+ const ref = useRef<HTMLElement>()
33
+ d = { ...dialogsDefaults, ...d }
34
+ const { sx, root, ...rest } = d.dialogProps||{}
35
+ const p = d.padding ? 2 : 0
36
+ return h(MuiDialog, {
37
+ open: true,
38
+ maxWidth: 'lg',
39
+ fullScreen: !useBreakpoint('sm'),
40
+ ...rest,
41
+ ...root,
42
+ onClose: ()=> closeDialog(),
43
+ },
44
+ d.title && h(DialogTitle, {}, d.title),
45
+ h(DialogContent, {
46
+ ref,
47
+ sx: { ...sx, px: p, pb: p, display: 'flex', flexDirection: 'column', }
48
+ }, h(d.Content) )
49
+ )
50
+ }
51
+
52
+ type AlertType = 'error' | 'warning' | 'info' | 'success'
53
+
54
+ const type2ico = {
55
+ error: ErrorIcon,
56
+ warning: Warning,
57
+ info: Info,
58
+ success: Check,
59
+ }
60
+ export async function alertDialog(msg: ReactElement | string | Error, options?: AlertType | ({ type?:AlertType, icon?: ReactElement } & Partial<DialogOptions>)) {
61
+ return new Promise(resolve => {
62
+ const opt = typeof options === 'string' ? { type: options } : (options ?? {})
63
+ let { type='info', ...rest } = opt
64
+ if (msg instanceof Error) {
65
+ msg = msg.message || String(msg)
66
+ type = 'error'
67
+ }
68
+ const close = newDialog({
69
+ className: 'dialog-alert-' + type,
70
+ icon: '!',
71
+ onClose: resolve,
72
+ ...rest,
73
+ Content() {
74
+ return h(Box, { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 },
75
+ h(IconButton, {
76
+ onClick() {
77
+ close()
78
+ },
79
+ size: 'small',
80
+ sx: { position: 'absolute', right: 0, top: 0, opacity: .5 }
81
+ }, h(Close)),
82
+ opt.icon ?? h(type2ico[type], { color: type, fontSize: 'large' }),
83
+ isValidElement(msg) ? msg
84
+ : h(Box, { fontSize: 'large', mb: 1, lineHeight: '1.8em' }, String(msg)),
85
+ )
86
+ }
87
+ })
88
+ })
89
+ }
90
+
91
+ interface ConfirmOptions { href?: string }
92
+ export async function confirmDialog(msg: string | ReactElement, { href }: ConfirmOptions={}) : Promise<boolean> {
93
+ return new Promise(resolve => newDialog({
94
+ className: 'dialog-confirm',
95
+ icon: '?',
96
+ onClose: resolve,
97
+ Content
98
+ }) )
99
+
100
+ function Content() {
101
+ return h(Fragment, {},
102
+ h(Box, { mb: 2 }, msg),
103
+ h(Flex, {},
104
+ h('a', {
105
+ href,
106
+ onClick: () => closeDialog(true),
107
+ }, h(Button, { variant: 'contained' }, "Confirm")),
108
+ h(Button, { onClick: () => closeDialog(false) }, "Don't"),
109
+ ),
110
+ )
111
+ }
112
+ }
113
+
114
+ type FormDialog<T> = Pick<DialogProps, 'fullScreen' | 'title'>
115
+ & Pick<DialogOptions, 'dialogProps'>
116
+ & Omit<FormProps<T>, 'values' | 'save' | 'set'>
117
+ & Partial<Pick<FormProps<T>, 'values' | 'save'>>
118
+ & {
119
+ onChange?: (values:Partial<T>, extra: { setValues: React.Dispatch<React.SetStateAction<Partial<T>>> }) => void,
120
+ before?: any
121
+ }
122
+ export async function formDialog<T>({ fullScreen, title, onChange, before, ...props }: FormDialog<T>) : Promise<T> {
123
+ return new Promise(resolve => newDialog({
124
+ className: 'dialog-confirm',
125
+ icon: '?',
126
+ onClose: resolve,
127
+ title,
128
+ Content
129
+ }) )
130
+
131
+ function Content() {
132
+ const [values, setValues] = useState<Partial<T>>(props.values||{})
133
+ return h(Fragment, {},
134
+ before,
135
+ h(Form, {
136
+ ...props,
137
+ values,
138
+ set(v, k) {
139
+ const newV = { ...values, [k]: v }
140
+ setValues(newV)
141
+ onChange?.(newV, { setValues })
142
+ },
143
+ save: {
144
+ ...props.save,
145
+ onClick() {
146
+ closeDialog(values)
147
+ }
148
+ }
149
+ })
150
+ )
151
+ }
152
+ }
153
+
154
+ export async function promptDialog(msg: string, props:any={}) : Promise<string | undefined> {
155
+ return formDialog<{ text: string }>({
156
+ ...props,
157
+ fields: [
158
+ { k: 'text', label: null, autoFocus: true,
159
+ before: h(Box, { mb: 2 }, msg),
160
+ ...props.field
161
+ },
162
+ ],
163
+ save: {
164
+ children: "Continue",
165
+ startIcon: h(Forward),
166
+ ...props.save,
167
+ },
168
+ saveOnEnter: true,
169
+ barSx: { gap: 2 },
170
+ addToBar: [
171
+ h(Button, { onClick: closeDialog }, "Cancel"),
172
+ ...props.addToBar||[],
173
+ ]
174
+ }).then(values => values?.text)
175
+ }
176
+
177
+ export function waitDialog() {
178
+ return newDialog({ Content: CircularProgress, closable: false })
179
+ }
180
+
181
+ export function toast(msg: string | ReactElement, type: AlertType | ReactElement='info') {
182
+ const ms = 3000
183
+ const close = newDialog({
184
+ Content,
185
+ dialogProps: {
186
+ PaperProps: {
187
+ sx: { transition: `opacity ${ms}ms ease-in` },
188
+ ref(x: HTMLElement) { // we need to set opacity later, to trigger transition
189
+ if (x)
190
+ x.style.opacity = '0'
191
+ }
192
+ }
193
+ }
194
+ })
195
+ setTimeout(close, ms)
196
+
197
+ function Content(){
198
+ return h(Box, { display:'flex', flexDirection: 'column', alignItems: 'center', gap: 1 },
199
+ isValidElement(type) ? type : h(type2ico[type], { color:type }),
200
+ isValidElement(msg) ? msg : h('div', {}, String(msg))
201
+ )
202
+ }
203
+ }
@@ -0,0 +1,21 @@
1
+ body {
2
+ margin: 0;
3
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5
+ sans-serif;
6
+ -webkit-font-smoothing: antialiased;
7
+ -moz-osx-font-smoothing: grayscale;
8
+ height: 100vh;
9
+ }
10
+ #root { min-height: 100%; display:flex; }
11
+
12
+ code {
13
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
14
+ monospace;
15
+ }
16
+
17
+ .MuiTreeItem-content {
18
+ box-sizing: border-box; /* avoid unwanted scrolling caused by its width:100% + padding */
19
+ }
20
+
21
+ ol, ul { margin-top: .2em }
@@ -0,0 +1,10 @@
1
+ // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+
3
+ import { createElement as h, StrictMode } from 'react'
4
+ import { createRoot } from 'react-dom/client'
5
+ import './index.css'
6
+ import '@hfs/shared/src/min-crypto-polyfill'
7
+ import App from './App'
8
+
9
+ createRoot(document.getElementById('root')!)
10
+ .render( h(StrictMode, {}, h(App)) )
@@ -0,0 +1,31 @@
1
+ // This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
+
3
+ import { createElement as h, Fragment } from 'react'
4
+
5
+ // markdown inspired syntax to transform text into react elements: * for bold, / for italic, _ for underline, ` for code
6
+ export default function md(text: string | TemplateStringsArray) {
7
+ if (typeof text !== 'string')
8
+ text = text[0]
9
+ const re = /([`*/_])(.+)\1|(\n)/g
10
+ const res = []
11
+ let last = 0
12
+ let match
13
+ while (match = re.exec(text)) { //eslint-disable-line no-cond-assign
14
+ res.push( text.slice(last, match.index) )
15
+ if (match[3])
16
+ res.push(h('br'))
17
+ else {
18
+ const tag = ({
19
+ '`': 'code',
20
+ '*': 'b',
21
+ '/': 'i',
22
+ '_': 'u',
23
+ })[ match[1] ]
24
+ if (!tag)
25
+ throw Error("should never happen")
26
+ res.push( h(tag,{}, match[2]) )
27
+ }
28
+ last = match.index + match[0].length
29
+ }
30
+ return h(Fragment, {}, ...res, text.slice(last, Infinity))
31
+ }