mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
feat(ui): add BlurHashCanvas placeholder component
This commit is contained in:
parent
9f2b78ab2f
commit
6ae855256c
7
ui/package-lock.json
generated
7
ui/package-lock.json
generated
@ -15,6 +15,7 @@
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@material-ui/styles": "^4.11.5",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"blurhash": "^2.0.5",
|
||||
"clsx": "^2.1.1",
|
||||
"connected-react-router": "^6.9.3",
|
||||
"deepmerge": "^4.3.1",
|
||||
@ -4431,6 +4432,12 @@
|
||||
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/blurhash": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
|
||||
"integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/boxen": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz",
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
||||
"@material-ui/styles": "^4.11.5",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"blurhash": "^2.0.5",
|
||||
"clsx": "^2.1.1",
|
||||
"connected-react-router": "^6.9.3",
|
||||
"deepmerge": "^4.3.1",
|
||||
|
||||
46
ui/src/common/BlurHashCanvas.jsx
Normal file
46
ui/src/common/BlurHashCanvas.jsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { decode } from 'blurhash'
|
||||
|
||||
// A blurhash carries no detail beyond a few dozen pixels; CSS upscales the canvas.
|
||||
const DECODE_SIZE = 32
|
||||
|
||||
export const BlurHashCanvas = ({ hash, className }) => {
|
||||
const canvasRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!hash || !canvasRef.current) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const pixels = decode(hash, DECODE_SIZE, DECODE_SIZE)
|
||||
const ctx = canvasRef.current.getContext('2d')
|
||||
if (!ctx) {
|
||||
return
|
||||
}
|
||||
const imageData = ctx.createImageData(DECODE_SIZE, DECODE_SIZE)
|
||||
imageData.data.set(pixels)
|
||||
ctx.putImageData(imageData, 0, 0)
|
||||
} catch {
|
||||
// A malformed hash simply leaves the canvas transparent.
|
||||
}
|
||||
}, [hash])
|
||||
|
||||
if (!hash) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={DECODE_SIZE}
|
||||
height={DECODE_SIZE}
|
||||
className={className}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
BlurHashCanvas.propTypes = {
|
||||
hash: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
}
|
||||
38
ui/src/common/BlurHashCanvas.test.jsx
Normal file
38
ui/src/common/BlurHashCanvas.test.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { render } from '@testing-library/react'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { BlurHashCanvas } from './BlurHashCanvas'
|
||||
|
||||
describe('BlurHashCanvas', () => {
|
||||
// jsdom has no real 2D context; stub it so getContext doesn't hit its noisy "not implemented" path.
|
||||
let getContextSpy
|
||||
beforeEach(() => {
|
||||
getContextSpy = vi
|
||||
.spyOn(HTMLCanvasElement.prototype, 'getContext')
|
||||
.mockReturnValue({
|
||||
createImageData: (w, h) => ({ data: new Uint8ClampedArray(w * h * 4) }),
|
||||
putImageData: () => {},
|
||||
})
|
||||
})
|
||||
afterEach(() => {
|
||||
getContextSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('renders nothing without a hash', () => {
|
||||
const { container } = render(<BlurHashCanvas hash="" />)
|
||||
expect(container.querySelector('canvas')).toBeNull()
|
||||
})
|
||||
|
||||
it('renders a canvas for a valid hash', () => {
|
||||
const { container } = render(
|
||||
<BlurHashCanvas hash="LEHV6nWB2yk8pyo0adR*.7kCMdnj" />,
|
||||
)
|
||||
expect(container.querySelector('canvas')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders a canvas without throwing on a malformed hash', () => {
|
||||
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const { container } = render(<BlurHashCanvas hash="!!!not-a-blurhash!!!" />)
|
||||
expect(container.querySelector('canvas')).not.toBeNull()
|
||||
spy.mockRestore()
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user