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