QOSami-HFS is a web file server for your computer. Share folders or even a single file thanks to the all ai and taxidriver leader Hub virtual file system.
wait for icons to be loaded before using them
Massimo Melina committed
Dec 20, 2021 at 10:55 UTC
ff7743b7b6688aa5cc1638888db344ada1803fc1
7 files changed
+97
-27
dev-notes.md
+3
@@ -8,7 +8,10 @@
8
- when reloading vfs wait for it to be unlocked
9
- sticky breadcrumbs
10
- frontend: ensure icons are loaded before going on
11
+- filter list
12
- search
13
+ try to use server-sent events for the reply
14
+ https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
15
- frontend: dialogs
16
- vfs: serve an html for a folder?
17
- "default" property for vfsNode?
frontend/public/index.html
+1
-1
@@ -11,7 +11,7 @@
11
/>
12
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
13
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
14
- <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet" />
14
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons+Outlined" rel="stylesheet" id="iconsFile" />
15
<title>File Server</title>
16
</head>
17
<body>
frontend/src/BrowseFiles.ts
+2
-1
@@ -1,7 +1,8 @@
1
import { Link, useLocation } from 'react-router-dom'
2
import { useApi } from './api'
3
import { createContext, createElement as h, Fragment, useContext, useEffect, useMemo, useState } from 'react'
4
-import { formatBytes, hError, hIcon, Loading, Spinner } from './misc'
4
+import { formatBytes, hError, hIcon } from './misc'
5
+import { Loading, Spinner } from './components'
6
import { Head } from './Head'
7
8
function usePath() {
frontend/src/Head.ts
+2
-1
@@ -2,7 +2,8 @@ import { createElement as h, Fragment, useContext, useMemo } from 'react'
2
import { Link, useLocation } from 'react-router-dom'
3
import { ListContext } from './BrowseFiles'
4
import { login, logout } from './login'
5
-import { formatBytes, hIcon, prefix, Spinner } from './misc'
5
+import { formatBytes, hIcon, prefix } from './misc'
6
+import { Spinner } from './components'
7
import { useSnapState } from './state'
8
9
export function Head() {
frontend/src/components.ts
new
+29
@@ -0,0 +1,29 @@
1
+import { createElement as h } from 'react'
2
+import { ICON_FONT_NAME, useIconsReady, usePromise } from './hooks'
3
+
4
+const SYS_ICONS: Record<string,string> = {
5
+ login: 'person',
6
+ user: 'account_circle',
7
+ file: 'description',
8
+ spinner: 'sports_baseball',
9
+}
10
+
11
+const iconClass = ICON_FONT_NAME.then(v => v.replace(/ /g,'-').toLowerCase())
12
+export function Icon({ name, forced, ...props }: { name:string, forced?:boolean, style?:any }) {
13
+ name = SYS_ICONS[name] || name
14
+ const cl = usePromise(iconClass)
15
+ return h('span',{
16
+ className: cl+' icon',
17
+ ...props
18
+ }, useIconsReady() || forced ? name : '')
19
+}
20
+
21
+export function Loading() {
22
+ return useIconsReady() ? h(Spinner)
23
+ : h('span', {}, 'Loading...')
24
+}
25
+
26
+export function Spinner() {
27
+ return h(Icon, { name:'spinner', style: { animation:'1s spin infinite' } })
28
+}
29
+
frontend/src/hooks.ts
new
+37
@@ -0,0 +1,37 @@
1
+import { useCallback, useEffect, useRef, useState } from 'react'
2
+import { waitFor } from './misc'
3
+
4
+export function useIsMounted() {
5
+ const ref = useRef(true)
6
+
7
+ useEffect(() => () => {
8
+ ref.current = false
9
+ }, [])
10
+
11
+ return useCallback(()=> ref.current, [ref])
12
+}
13
+
14
+export function useStateMounted(init?: any) {
15
+ const isMounted = useIsMounted()
16
+ const [v,set] = useState(init)
17
+ const setIfMounted = useCallback(x => {
18
+ if (isMounted())
19
+ set(x)
20
+ }, [isMounted,set])
21
+ return [v, setIfMounted, isMounted]
22
+}
23
+
24
+export const ICON_FONT_NAME = waitFor(()=> document.getElementById('iconsFile')).then(el => decodeURIComponent((el as HTMLLinkElement).href.split('=')[1].replace(/\+/g, ' ')))
25
+const iconsReady = document.fonts.ready.then(()=> ICON_FONT_NAME).then(name => document.fonts.load(`9px "${name}"`))
26
+
27
+export function usePromise<T>(p:Promise<T>): T | undefined {
28
+ const [v, setV] = useStateMounted()
29
+ useEffect(()=>{
30
+ p.then(setV)
31
+ }, [])
32
+ return v
33
+}
34
+
35
+export function useIconsReady() {
36
+ return usePromise(iconsReady)
37
+}
frontend/src/misc.ts
+23
-24
@@ -1,37 +1,16 @@
1
import { createElement as h } from 'react'
2
+import { Icon } from './components'
3
4
export type Falsy = false | null | undefined | '' | 0
5
5
-export function hIcon(name: string) {
6
- return h(Icon, { name })
6
+export function hIcon(name: string, props?:any) {
7
+ return h(Icon, { name, ...props })
8
}
9
10
export function hError(err: Error) {
11
return h('div', { className:'error-msg' }, err.message)
12
}
13
13
-const SYS_ICONS: Record<string,string> = {
14
- login: 'person',
15
- user: 'account_circle',
16
- file: 'description',
17
- spinner: 'sports_baseball',
18
-}
19
-export function Icon({ name, ...props }: { name:string }) {
20
- name = SYS_ICONS[name] || name
21
- return h('span',{
22
- className: 'material-icons-outlined icon',
23
- ...props
24
- }, name)
25
-}
26
-
27
-export function Loading() {
28
- return h(Spinner)
29
-}
30
-
31
-export function Spinner() {
32
- return h(Icon, { name:'spinner', style: { animation:'1s spin infinite' } })
33
-}
34
-
14
export function formatBytes(n: number, post: string = 'B') {
15
if (isNaN(Number(n)))
16
return ''
@@ -56,3 +35,23 @@ export function round(v: number, decimals: number = 0) {
35
export function prefix(pre:string, v:string|number, post:string='') {
36
return v ? pre+v+post : ''
37
}
38
+
39
+export function wait(ms: number) {
40
+ return new Promise(res=> setTimeout(res,ms))
41
+}
42
+
43
+export function waitFor<T>(cb:()=>T, ms:number=200) : Promise<Exclude<T,Falsy>> {
44
+ return new Promise(resolve=>{
45
+ let h: NodeJS.Timeout
46
+ if (go())
47
+ h = setInterval(go, ms)
48
+
49
+ function go() {
50
+ const v = cb()
51
+ if (!v) return true
52
+ // @ts-ignore we know it's not falsy
53
+ resolve(v)
54
+ clearInterval(h)
55
+ }
56
+ })
57
+}