mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +08:00
fix(glass): port material accents to v3 (#660)
This commit is contained in:
@@ -1,11 +1,73 @@
|
||||
import { vi } from 'vitest'
|
||||
|
||||
vi.mock('colorthief', () => ({
|
||||
default: class ColorThief {},
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getColor: vi.fn(),
|
||||
}))
|
||||
|
||||
import { preloadCorsImage } from '@/@core/utils/image'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
vi.mock('colorthief', () => ({
|
||||
default: class ColorThief {
|
||||
getColor(image: HTMLImageElement, quality?: number) {
|
||||
return mocks.getColor(image, quality)
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
import { extractDominantColor, getDominantColor, preloadCorsImage } from '@/@core/utils/image'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
function createImage(cacheKey: string) {
|
||||
const image = document.createElement('img')
|
||||
Object.defineProperty(image, 'currentSrc', { configurable: true, value: `https://image.example/${cacheKey}.png` })
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
describe('dominant color extraction', () => {
|
||||
beforeEach(() => {
|
||||
mocks.getColor.mockReset()
|
||||
})
|
||||
|
||||
it('shares a pending extraction and reuses only the successful result', async () => {
|
||||
const image = createImage('shared-success')
|
||||
mocks.getColor.mockReturnValue([18, 52, 86])
|
||||
|
||||
await expect(Promise.all([extractDominantColor(image), extractDominantColor(image)])).resolves.toEqual([
|
||||
'#123456',
|
||||
'#123456',
|
||||
])
|
||||
await expect(extractDominantColor(image)).resolves.toBe('#123456')
|
||||
expect(mocks.getColor).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not cache failures or let one caller fallback pollute another', async () => {
|
||||
const image = createImage('retry-after-failure')
|
||||
mocks.getColor.mockImplementation(() => {
|
||||
throw new Error('tainted canvas')
|
||||
})
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
|
||||
await expect(getDominantColor(image, { fallback: '#111111' })).resolves.toBe('#111111')
|
||||
await expect(getDominantColor(image, { fallback: '#222222' })).resolves.toBe('#222222')
|
||||
expect(mocks.getColor).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('keeps the existing default fallback contract for callers such as QuickAccess', async () => {
|
||||
await expect(getDominantColor(null)).resolves.toBe('#28A9E1')
|
||||
expect(mocks.getColor).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('retains the bounded FIFO success cache', async () => {
|
||||
mocks.getColor.mockReturnValue([1, 2, 3])
|
||||
|
||||
for (let index = 0; index <= 100; index += 1) await extractDominantColor(createImage(`fifo-${index}`))
|
||||
|
||||
expect(mocks.getColor).toHaveBeenCalledTimes(101)
|
||||
await extractDominantColor(createImage('fifo-50'))
|
||||
expect(mocks.getColor).toHaveBeenCalledTimes(101)
|
||||
await extractDominantColor(createImage('fifo-0'))
|
||||
expect(mocks.getColor).toHaveBeenCalledTimes(102)
|
||||
})
|
||||
})
|
||||
|
||||
describe('preloadCorsImage', () => {
|
||||
afterEach(() => {
|
||||
|
||||
+41
-22
@@ -4,7 +4,8 @@ export { preloadCorsImage } from './corsImage'
|
||||
const DEFAULT_DOMINANT_COLOR = '#28A9E1'
|
||||
const DOMINANT_COLOR_CACHE_LIMIT = 100
|
||||
const colorThief = new ColorThief()
|
||||
const dominantColorCache = new Map<string, Promise<string>>()
|
||||
const dominantColorCache = new Map<string, string>()
|
||||
const pendingDominantColorRequests = new Map<string, Promise<string | undefined>>()
|
||||
|
||||
interface DominantColorOptions {
|
||||
fallback?: string
|
||||
@@ -29,42 +30,60 @@ function getImageCacheKey(image: HTMLImageElement) {
|
||||
return image.currentSrc || image.src || ''
|
||||
}
|
||||
|
||||
function rememberDominantColor(key: string, colorPromise: Promise<string>) {
|
||||
if (!key) return colorPromise
|
||||
function rememberDominantColor(key: string, color: string) {
|
||||
if (!key) return
|
||||
|
||||
if (dominantColorCache.size >= DOMINANT_COLOR_CACHE_LIMIT) {
|
||||
const firstKey = dominantColorCache.keys().next().value
|
||||
if (firstKey) dominantColorCache.delete(firstKey)
|
||||
}
|
||||
|
||||
dominantColorCache.set(key, colorPromise)
|
||||
dominantColorCache.set(key, color)
|
||||
}
|
||||
|
||||
/** 提取真实主色;失败不写入成功缓存,允许后续请求重试。 */
|
||||
export async function extractDominantColor(
|
||||
image: HTMLImageElement | undefined | null,
|
||||
options: Pick<DominantColorOptions, 'quality'> = {},
|
||||
): Promise<string | undefined> {
|
||||
if (!image) return undefined
|
||||
|
||||
const cacheKey = getImageCacheKey(image)
|
||||
const cachedColor = cacheKey ? dominantColorCache.get(cacheKey) : undefined
|
||||
if (cachedColor) return cachedColor
|
||||
|
||||
const pendingRequest = cacheKey ? pendingDominantColorRequests.get(cacheKey) : undefined
|
||||
if (pendingRequest) return pendingRequest
|
||||
|
||||
const colorPromise = Promise.resolve()
|
||||
.then(() => {
|
||||
const dominantColor = colorThief.getColor(image, options.quality ?? 20)
|
||||
const color = rgbStringToHex(dominantColor)
|
||||
rememberDominantColor(cacheKey, color)
|
||||
|
||||
return color
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn('Failed to extract dominant color:', error)
|
||||
return undefined
|
||||
})
|
||||
.finally(() => {
|
||||
if (cacheKey) pendingDominantColorRequests.delete(cacheKey)
|
||||
})
|
||||
|
||||
if (cacheKey) pendingDominantColorRequests.set(cacheKey, colorPromise)
|
||||
|
||||
return colorPromise
|
||||
}
|
||||
|
||||
// 提取主要颜色
|
||||
/** 提取主色并在失败时解析调用方 fallback,保持既有调用合同。 */
|
||||
export async function getDominantColor(
|
||||
image: HTMLImageElement | undefined | null,
|
||||
options: DominantColorOptions = {},
|
||||
): Promise<string> {
|
||||
const fallback = options.fallback ?? DEFAULT_DOMINANT_COLOR
|
||||
|
||||
if (!image) return fallback
|
||||
|
||||
const cacheKey = getImageCacheKey(image)
|
||||
const cachedColor = cacheKey ? dominantColorCache.get(cacheKey) : undefined
|
||||
if (cachedColor) return cachedColor
|
||||
|
||||
const colorPromise = Promise.resolve()
|
||||
.then(() => {
|
||||
const dominantColor = colorThief.getColor(image, options.quality ?? 20)
|
||||
return rgbStringToHex(dominantColor)
|
||||
})
|
||||
.catch(error => {
|
||||
console.warn('Failed to extract dominant color:', error)
|
||||
return fallback
|
||||
})
|
||||
|
||||
return rememberDominantColor(cacheKey, colorPromise)
|
||||
return (await extractDominantColor(image, options)) ?? fallback
|
||||
}
|
||||
|
||||
// 预加载图片
|
||||
|
||||
Reference in New Issue
Block a user