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