-
-
Notifications
You must be signed in to change notification settings - Fork 48
/
FileThumbnail.tsx
134 lines (124 loc) · 3.35 KB
/
FileThumbnail.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import { queryClient, useSDK } from '@stump/client'
import { cn } from '@stump/components'
import { Api } from '@stump/sdk'
import { Media } from '@stump/sdk'
import { Book, Folder } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
type Props = {
path: string
isDirectory: boolean
size?: 'sm' | 'md'
containerClassName?: string
}
export default function FileThumbnail({
path,
isDirectory,
size = 'sm',
containerClassName,
}: Props) {
const { sdk } = useSDK()
/**
* A boolean state to keep track of whether or not we should show the fallback icon. This
* will be set to true if the image fails to load
*/
const [showFallback, setShowFallback] = useState(false)
/**
* The book associated with the file, if any exists
*/
const [book, setBook] = useState<Media | null>(null)
/**
* A naive ref to keep track of whether or not we have fetched the book
*/
const didFetchRef = useRef(false)
/**
* An effect that attempts to fetch the book associated with the file, if any exists.
* This will only run once, and only if the file is not a directory
*/
useEffect(() => {
if (!book && !didFetchRef.current && !isDirectory) {
didFetchRef.current = true
getBook(path, sdk).then(setBook)
}
}, [book, path, isDirectory, sdk])
/**
* A function that attempts to load the image associated with the book,
* returning a promise that resolves with the image if it loads successfully
*/
const loadImage = useCallback(() => {
if (book) {
const image = new Image()
return new Promise((resolve, reject) => {
image.src = sdk.media.thumbnailURL(book.id)
image.onload = () => resolve(image)
image.onerror = (e) => {
console.error('Image failed to load:', e)
reject(new Error('Could not load image'))
}
})
} else {
return Promise.reject('No book found')
}
}, [book, sdk.media])
/**
* A function that attempts to reload the image
*/
const attemptReload = async () => {
try {
await loadImage()
setShowFallback(false)
} catch (e) {
setShowFallback(true)
}
}
const sizeClasses = cn('h-14', { 'h-20': size === 'md' })
const className = cn(
'flex aspect-[2/3] w-auto items-center justify-center rounded-sm border-[0.5px] border-edge bg-sidebar shadow-sm',
sizeClasses,
containerClassName,
)
const iconSizes = cn('h-7 w-7', { 'h-8 w-8': size === 'md' })
if (isDirectory) {
return (
<div className={className}>
<Folder className={cn('text-foreground-muted', iconSizes)} />
</div>
)
}
if (showFallback || !book) {
return (
<div className={className} onClick={attemptReload}>
<Book className={cn('text-foreground-muted', iconSizes)} />
</div>
)
}
return (
<img
className={cn('aspect-[2/3] w-auto rounded-sm object-cover', sizeClasses)}
src={sdk.media.thumbnailURL(book.id)}
onError={() => setShowFallback(true)}
/>
)
}
/**
* A function that attempts to fetch the book associated with the file, if any exists.
* The queryClient is used in order to properly cache the result.
*/
export const getBook = async (path: string, sdk: Api) => {
try {
const response = await queryClient.fetchQuery(
[sdk.media.keys.get, { path }],
() =>
sdk.media.get({
path: [path],
}),
{
// 15 minutes
cacheTime: 1000 * 60 * 15,
},
)
return response.data?.at(0) ?? null
} catch (error) {
console.error(error)
return null
}
}