mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 07:10:41 +00:00
Add useAsync hook
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
export * from './useActiveTimout'
|
||||
export * from './useAsync'
|
||||
export * from './useAsyncFn'
|
||||
export * from './useCanvas'
|
||||
export * from './useFocus'
|
||||
export * from './useHash'
|
||||
|
||||
17
src/app/hooks/useAsync.ts
Normal file
17
src/app/hooks/useAsync.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Inputs } from 'preact/hooks'
|
||||
import { useEffect } from 'preact/hooks'
|
||||
import type { AsyncState } from './useAsyncFn'
|
||||
import { useAsyncFn } from './useAsyncFn'
|
||||
|
||||
export function useAsync<T>(
|
||||
fn: () => Promise<T>,
|
||||
inputs: Inputs = [],
|
||||
): AsyncState<T> {
|
||||
const [state, callback] = useAsyncFn<T, () => Promise<T>>(fn, inputs, { loading: true })
|
||||
|
||||
useEffect(() => {
|
||||
callback()
|
||||
}, [callback])
|
||||
|
||||
return state
|
||||
}
|
||||
59
src/app/hooks/useAsyncFn.ts
Normal file
59
src/app/hooks/useAsyncFn.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { Inputs } from 'preact/hooks'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
|
||||
|
||||
export type AsyncState<T> = {
|
||||
loading: boolean,
|
||||
error?: undefined,
|
||||
value?: undefined,
|
||||
} | {
|
||||
loading: true,
|
||||
error?: Error | undefined,
|
||||
value?: T,
|
||||
} | {
|
||||
loading: false,
|
||||
error: Error,
|
||||
value?: undefined,
|
||||
} | {
|
||||
loading: false,
|
||||
error?: undefined,
|
||||
value: T,
|
||||
}
|
||||
|
||||
export function useAsyncFn<R, T extends (...args: any[]) => Promise<R>>(
|
||||
fn: T,
|
||||
inputs: Inputs = [],
|
||||
initialState: AsyncState<R> = { loading: false },
|
||||
): [AsyncState<R>, (...args: Parameters<T>) => Promise<R | undefined>] {
|
||||
const [state, setState] = useState<AsyncState<R>>(initialState)
|
||||
const isMounted = useRef<boolean>(false)
|
||||
const lastCallId = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
isMounted.current = true
|
||||
return () => isMounted.current = false
|
||||
}, [])
|
||||
|
||||
const callback = useCallback((...args: Parameters<T>): Promise<R | undefined> => {
|
||||
const callId = ++lastCallId.current
|
||||
if (!state.loading) {
|
||||
setState(prev => ({ ...prev, loading: true }))
|
||||
}
|
||||
|
||||
return fn(...args).then(
|
||||
value => {
|
||||
if (isMounted.current && callId === lastCallId.current) {
|
||||
setState({ value, loading: false })
|
||||
}
|
||||
return value
|
||||
},
|
||||
error => {
|
||||
if (isMounted.current && callId === lastCallId.current) {
|
||||
setState({ error, loading: false })
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
}, inputs)
|
||||
|
||||
return [state, callback]
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { Ad, ChangelogList, ErrorPanel } from '../components'
|
||||
import { useLocale, useTitle } from '../contexts'
|
||||
import type { Change } from '../services'
|
||||
import { useAsync } from '../hooks'
|
||||
import { getChangelogs } from '../services'
|
||||
|
||||
interface Props {
|
||||
@@ -9,20 +8,13 @@ interface Props {
|
||||
}
|
||||
export function Changelog({}: Props) {
|
||||
const { locale } = useLocale()
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
useTitle(locale('title.changelog'))
|
||||
|
||||
const [changelogs, setChangelogs] = useState<Change[]>([])
|
||||
useEffect(() => {
|
||||
getChangelogs()
|
||||
.then(changelogs => setChangelogs(changelogs))
|
||||
.catch(e => { console.error(e); setError(e) })
|
||||
}, [])
|
||||
|
||||
const { value: changelogs, error } = useAsync(getChangelogs, [])
|
||||
|
||||
return <main>
|
||||
<Ad type="text" id="changelog" />
|
||||
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
|
||||
{error && <ErrorPanel error={error} />}
|
||||
<div class="changelog">
|
||||
<ChangelogList changes={changelogs} defaultOrder="desc" />
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'
|
||||
import config from '../../config.json'
|
||||
import { Ad, Btn, BtnMenu, ChangelogTag, Giscus, Octicon } from '../components'
|
||||
import { useLocale, useTitle, useVersion } from '../contexts'
|
||||
import { useActiveTimeout, useHash } from '../hooks'
|
||||
import { useActiveTimeout, useAsync, useHash } from '../hooks'
|
||||
import type { VersionId } from '../services'
|
||||
import { parseFrontMatter, versionContent } from '../Utils'
|
||||
|
||||
@@ -30,7 +30,10 @@ export function Guide({ id }: Props) {
|
||||
const { version, changeVersion } = useVersion()
|
||||
const { changeTitle } = useTitle()
|
||||
|
||||
const [content, setContent] = useState<string | undefined>(undefined)
|
||||
const { value: content } = useAsync(async () => {
|
||||
const res = await fetch(`../../guides/${id}.md`)
|
||||
return await res.text()
|
||||
}, [id])
|
||||
|
||||
const frontMatter = useMemo(() => {
|
||||
if (!content) return undefined
|
||||
@@ -51,14 +54,8 @@ export function Guide({ id }: Props) {
|
||||
return allowedVersions[0]
|
||||
}, [version, frontMatter?.versions])
|
||||
|
||||
const versionedContent = useMemo(() => {
|
||||
if (!content) return undefined
|
||||
const guide = content.substring(content.indexOf('---', 3) + 3)
|
||||
return versionContent(guide, guideVersion)
|
||||
}, [guideVersion, content])
|
||||
|
||||
const html = useMemo(() => {
|
||||
if (!versionedContent) return undefined
|
||||
if (!content) return undefined
|
||||
marked.use({ renderer: {
|
||||
link(href, title, text) {
|
||||
if (href === null) return text
|
||||
@@ -72,8 +69,10 @@ export function Guide({ id }: Props) {
|
||||
return `<h${level}>${link}${text}</h${level}>`
|
||||
},
|
||||
}})
|
||||
const guide = content.substring(content.indexOf('---', 3) + 3)
|
||||
const versionedContent = versionContent(guide, guideVersion)
|
||||
return marked(versionedContent, { version: '1.19' } as any)
|
||||
}, [versionedContent])
|
||||
}, [guideVersion, content])
|
||||
|
||||
const [hash, setHash] = useHash()
|
||||
|
||||
@@ -100,14 +99,6 @@ export function Guide({ id }: Props) {
|
||||
}
|
||||
}, [scrollToHeading, hash, version])
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const res = await fetch(`../../guides/${id}.md`)
|
||||
const text = await res.text()
|
||||
setContent(text)
|
||||
})()
|
||||
}, [id])
|
||||
|
||||
const [shareActive, shareSuccess] = useActiveTimeout()
|
||||
|
||||
const onShare = useCallback(() => {
|
||||
@@ -116,9 +107,9 @@ export function Guide({ id }: Props) {
|
||||
shareSuccess()
|
||||
}, [id, version])
|
||||
|
||||
const onClickTag = (tag: string) => {
|
||||
const onClickTag = useCallback((tag: string) => {
|
||||
route(`/guides/?tags=${tag}`)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const [largeWidth] = useState(window.innerWidth > 600)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from 'preact/hooks'
|
||||
import { useCallback, useMemo } from 'preact/hooks'
|
||||
import { Ad, TreeView } from '../components'
|
||||
import { getFilePath, useLocale, useProject, useTitle } from '../contexts'
|
||||
|
||||
@@ -11,10 +11,10 @@ export function Project({}: Props) {
|
||||
useTitle(locale('title.project', project.name))
|
||||
const entries = useMemo(() => project.files.map(getFilePath), project.files)
|
||||
|
||||
const selectFile = (entry: string) => {
|
||||
const selectFile = useCallback((entry: string) => {
|
||||
const [, namespace, type, ...id] = entry.split('/')
|
||||
openFile(type, `${namespace}:${id}`)
|
||||
}
|
||||
}, [openFile])
|
||||
|
||||
return <main>
|
||||
<Ad id="data-pack-project" type="text" />
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Howl, HowlOptions } from 'howler'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import config from '../../config.json'
|
||||
import { Btn, BtnMenu, ErrorPanel, SoundConfig, TextInput } from '../components'
|
||||
import { useLocale, useTitle, useVersion } from '../contexts'
|
||||
import type { SoundEvents, VersionId } from '../services'
|
||||
import { useAsync } from '../hooks'
|
||||
import type { VersionId } from '../services'
|
||||
import { fetchSounds } from '../services'
|
||||
import { hexId } from '../Utils'
|
||||
|
||||
@@ -13,7 +14,6 @@ interface Props {
|
||||
export function Sounds({}: Props) {
|
||||
const { locale } = useLocale()
|
||||
const { version, changeVersion } = useVersion()
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
useTitle(locale('title.sounds'))
|
||||
|
||||
const [howler, setHowler] = useState<undefined | ((options: HowlOptions) => Howl)>(undefined)
|
||||
@@ -24,13 +24,10 @@ export function Sounds({}: Props) {
|
||||
})()
|
||||
}, [])
|
||||
|
||||
const [sounds, setSounds] = useState<SoundEvents>({})
|
||||
const soundKeys = Object.keys(sounds ?? {})
|
||||
useEffect(() => {
|
||||
fetchSounds(version)
|
||||
.then(setSounds)
|
||||
.catch(e => { console.error(e); setError(e) })
|
||||
const { value: sounds, error } = useAsync(async () => {
|
||||
return await fetchSounds(version)
|
||||
}, [version])
|
||||
const soundKeys = useMemo(() => Object.keys(sounds ?? {}), [sounds])
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const [configs, setConfigs] = useState<SoundConfig[]>([])
|
||||
@@ -63,7 +60,7 @@ export function Sounds({}: Props) {
|
||||
}
|
||||
|
||||
return <main>
|
||||
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
|
||||
{error && <ErrorPanel error={error} />}
|
||||
{soundKeys.length > 0 && <>
|
||||
<div class="controls sounds-controls">
|
||||
<div class="sound-search-group">
|
||||
@@ -73,7 +70,7 @@ export function Sounds({}: Props) {
|
||||
</div>
|
||||
{configs.length > 1 && <Btn icon="play" label={ locale('sounds.play_all')} class="play-all-sounds" onClick={playAll} />}
|
||||
<div class="spacer"></div>
|
||||
<Btn icon="download" label={locale('download')} tooltip={locale('sounds.download_function')} class="download-sounds" onClick={downloadFunction} />
|
||||
<Btn icon="download" label={locale('download')} tooltip={locale('sounds.download_function')} tooltipLoc="se" class="download-sounds" onClick={downloadFunction} />
|
||||
<BtnMenu icon="tag" label={version} tooltip={locale('switch_version')}>
|
||||
{config.versions.slice().reverse().map(v =>
|
||||
<Btn label={v.id} active={v.id === version} onClick={() => changeVersion(v.id as VersionId)} />
|
||||
@@ -81,14 +78,14 @@ export function Sounds({}: Props) {
|
||||
</BtnMenu>
|
||||
</div>
|
||||
<div class="sounds">
|
||||
{howler && configs.map(c =>
|
||||
{sounds && howler && configs.map(c =>
|
||||
<SoundConfig key={c.id} {...c} {...{ howler, sounds, delayedPlay }} onEdit={editConfig(c.id)} onDelete={deleteConfig(c.id)} />
|
||||
)}
|
||||
</div>
|
||||
<a ref={download} style="display: none;"></a>
|
||||
<datalist id="sound-list">
|
||||
{soundKeys.map(s => <option key={s} value={s} />)}
|
||||
</datalist>
|
||||
</>}
|
||||
<datalist id="sound-list">
|
||||
{soundKeys.map(s => <option key={s} value={s} />)}
|
||||
</datalist>
|
||||
</main>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { Ad, ErrorPanel, Octicon, VersionDetail, VersionList } from '../components'
|
||||
import { useLocale, useTitle } from '../contexts'
|
||||
import { useSearchParam } from '../hooks'
|
||||
import { useAsync, useSearchParam } from '../hooks'
|
||||
import type { VersionMeta } from '../services'
|
||||
import { fetchVersions } from '../services'
|
||||
|
||||
@@ -10,27 +9,21 @@ interface Props {
|
||||
}
|
||||
export function Versions({}: Props) {
|
||||
const { locale } = useLocale()
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
useTitle(locale('title.versions'))
|
||||
|
||||
const [versions, setVersions] = useState<VersionMeta[]>([])
|
||||
useEffect(() => {
|
||||
fetchVersions()
|
||||
.then(versions => setVersions(versions))
|
||||
.catch(e => { console.error(e); setError(e) })
|
||||
}, [])
|
||||
const { value: versions, error } = useAsync(fetchVersions, [])
|
||||
|
||||
const [selectedId] = useSearchParam('id')
|
||||
const selected = versions.find(v => v.id === selectedId)
|
||||
const selected = (versions ?? []).find(v => v.id === selectedId)
|
||||
|
||||
useTitle(selected ? selected.name : 'Versions Explorer', selected ? [] : undefined)
|
||||
|
||||
const nextVersion = selected && getOffsetVersion(versions, selected, -1)
|
||||
const previousVersion = selected && getOffsetVersion(versions, selected, 1)
|
||||
const nextVersion = selected && getOffsetVersion(versions ?? [], selected, -1)
|
||||
const previousVersion = selected && getOffsetVersion(versions ?? [], selected, 1)
|
||||
|
||||
return <main>
|
||||
<Ad type="text" id="versions" />
|
||||
{error && <ErrorPanel error={error} onDismiss={() => setError(null)} />}
|
||||
{error && <ErrorPanel error={error} />}
|
||||
<div class="versions">
|
||||
{selectedId ? <>
|
||||
<div class="navigation">
|
||||
@@ -54,7 +47,7 @@ export function Versions({}: Props) {
|
||||
<p>This version does not exist. Only versions since 1.14 are tracked, or it may be too recent.</p>
|
||||
</div>
|
||||
</div>}
|
||||
</> : <VersionList versions={versions} link={id => `/versions/?id=${id}`} />}
|
||||
</> : <VersionList versions={versions ?? []} link={id => `/versions/?id=${id}`} />}
|
||||
</div>
|
||||
</main>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user