feat: align with upstream 1.5.9.9
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import { computed, onUnmounted, ref, shallowRef } from 'vue'
|
||||
import type {
|
||||
AbstractBaseRenderer,
|
||||
BackgroundRender,
|
||||
MeshGradientRenderer
|
||||
} from '@applemusic-like-lyrics/core'
|
||||
|
||||
const isClient = typeof window !== 'undefined'
|
||||
let CoreModule: typeof import('@applemusic-like-lyrics/core') | null = null
|
||||
|
||||
const CORS_PROXY_PATH = '/api/proxy/image'
|
||||
|
||||
const needsCorsProxy = (url: string): boolean => {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
const h = u.hostname.toLowerCase()
|
||||
return (
|
||||
h.endsWith('.y.qq.com') ||
|
||||
h === 'y.qq.com' ||
|
||||
h.endsWith('.y.gtimg.cn') ||
|
||||
h === 'y.gtimg.cn' ||
|
||||
h.endsWith('.music.126.net') ||
|
||||
h === 'music.126.net' ||
|
||||
h.endsWith('.musicapp.migu.cn') ||
|
||||
h === 'd.musicapp.migu.cn' ||
|
||||
h.endsWith('.migu.cn')
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const toProxiedUrl = (url: string): string => {
|
||||
if (!needsCorsProxy(url)) return url
|
||||
return `${CORS_PROXY_PATH}?url=${encodeURIComponent(url)}`
|
||||
}
|
||||
|
||||
const ensureCoreModule = async () => {
|
||||
if (!isClient) return null
|
||||
|
||||
if (!CoreModule) {
|
||||
CoreModule = await import('@applemusic-like-lyrics/core')
|
||||
await import('@applemusic-like-lyrics/core/style.css')
|
||||
}
|
||||
|
||||
return CoreModule
|
||||
}
|
||||
|
||||
export interface BackgroundConfig {
|
||||
type: 'gradient' | 'cover'
|
||||
dynamic: boolean
|
||||
flowSpeed: number
|
||||
fps?: number
|
||||
renderScale?: number
|
||||
hasLyric?: boolean
|
||||
}
|
||||
|
||||
export const useBackgroundRenderer = () => {
|
||||
const backgroundRenderer = shallowRef<
|
||||
BackgroundRender<MeshGradientRenderer> | AbstractBaseRenderer | null
|
||||
>(null)
|
||||
const containerElement = ref<HTMLElement | null>(null)
|
||||
const coverBlurElement = ref<HTMLElement | null>(null)
|
||||
const isInitialized = ref(false)
|
||||
const isRendering = ref(false)
|
||||
const isMotionPaused = ref(false)
|
||||
const hasRenderError = ref(false)
|
||||
const currentCoverUrl = ref('')
|
||||
const loadedCoverUrl = ref('')
|
||||
|
||||
const config = ref<BackgroundConfig>({
|
||||
type: 'gradient',
|
||||
dynamic: true,
|
||||
flowSpeed: 2,
|
||||
fps: 30,
|
||||
renderScale: 0.5,
|
||||
hasLyric: true
|
||||
})
|
||||
|
||||
const applyCanvasStyle = (canvas: HTMLElement) => {
|
||||
canvas.style.position = 'absolute'
|
||||
canvas.style.inset = '0'
|
||||
canvas.style.width = '100%'
|
||||
canvas.style.height = '100%'
|
||||
canvas.style.display = 'block'
|
||||
canvas.style.zIndex = '0'
|
||||
canvas.style.pointerEvents = 'none'
|
||||
}
|
||||
|
||||
const syncRendererMotion = () => {
|
||||
const renderer = backgroundRenderer.value
|
||||
if (!renderer) return
|
||||
|
||||
if (!isRendering.value) {
|
||||
renderer.pause()
|
||||
return
|
||||
}
|
||||
|
||||
renderer.setStaticMode(false)
|
||||
|
||||
if (!config.value.dynamic) {
|
||||
renderer.setFlowSpeed(0)
|
||||
renderer.pause()
|
||||
return
|
||||
}
|
||||
|
||||
renderer.setFlowSpeed(isMotionPaused.value ? 0 : config.value.flowSpeed)
|
||||
renderer.resume()
|
||||
}
|
||||
|
||||
const applyRendererState = async () => {
|
||||
const renderer = backgroundRenderer.value
|
||||
if (!renderer) return
|
||||
|
||||
renderer.setFPS(config.value.fps ?? 30)
|
||||
renderer.setRenderScale(config.value.renderScale ?? 0.5)
|
||||
renderer.setHasLyric(config.value.hasLyric ?? true)
|
||||
|
||||
const cover = currentCoverUrl.value
|
||||
if (cover !== loadedCoverUrl.value) {
|
||||
try {
|
||||
await renderer.setAlbum(cover ? toProxiedUrl(cover) : '', false)
|
||||
if (currentCoverUrl.value === cover) {
|
||||
loadedCoverUrl.value = cover
|
||||
hasRenderError.value = false
|
||||
}
|
||||
} catch (error) {
|
||||
if (currentCoverUrl.value === cover) {
|
||||
hasRenderError.value = true
|
||||
}
|
||||
console.error('应用封面到背景失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
syncRendererMotion()
|
||||
}
|
||||
|
||||
const initializeBackground = async (container: HTMLElement) => {
|
||||
if (!container) return
|
||||
if (isInitialized.value && containerElement.value === container) {
|
||||
await applyRendererState()
|
||||
return
|
||||
}
|
||||
|
||||
dispose()
|
||||
containerElement.value = container
|
||||
hasRenderError.value = false
|
||||
|
||||
try {
|
||||
const Core = await ensureCoreModule()
|
||||
if (!Core) return
|
||||
|
||||
const renderer = Core.BackgroundRender.new(Core.MeshGradientRenderer)
|
||||
backgroundRenderer.value = renderer
|
||||
|
||||
const canvas = renderer.getElement()
|
||||
applyCanvasStyle(canvas)
|
||||
container.appendChild(canvas)
|
||||
|
||||
isInitialized.value = true
|
||||
await applyRendererState()
|
||||
} catch (error) {
|
||||
hasRenderError.value = true
|
||||
console.error('背景渲染器初始化失败:', error)
|
||||
dispose()
|
||||
}
|
||||
}
|
||||
|
||||
const updateConfig = async (newConfig: Partial<BackgroundConfig>) => {
|
||||
config.value = { ...config.value, ...newConfig }
|
||||
await applyRendererState()
|
||||
}
|
||||
|
||||
const setCoverBackground = async (coverUrl: string) => {
|
||||
currentCoverUrl.value = coverUrl || ''
|
||||
|
||||
if (coverBlurElement.value) {
|
||||
coverBlurElement.value.style.backgroundImage = coverUrl ? `url(${coverUrl})` : ''
|
||||
}
|
||||
|
||||
await applyRendererState()
|
||||
}
|
||||
|
||||
const setCoverBlurElement = (element: HTMLElement | null) => {
|
||||
coverBlurElement.value = element
|
||||
}
|
||||
|
||||
const setGradientFromCover = async (coverUrl: string) => {
|
||||
await setCoverBackground(coverUrl)
|
||||
}
|
||||
|
||||
const startRender = () => {
|
||||
isRendering.value = true
|
||||
isMotionPaused.value = false
|
||||
syncRendererMotion()
|
||||
}
|
||||
|
||||
const stopRender = () => {
|
||||
const renderer = backgroundRenderer.value
|
||||
if (!renderer) return
|
||||
|
||||
isRendering.value = false
|
||||
isMotionPaused.value = false
|
||||
renderer.setFlowSpeed(0)
|
||||
renderer.pause()
|
||||
}
|
||||
|
||||
const pauseRender = () => {
|
||||
isMotionPaused.value = true
|
||||
if (!backgroundRenderer.value) return
|
||||
syncRendererMotion()
|
||||
}
|
||||
|
||||
const resumeRender = () => {
|
||||
isMotionPaused.value = false
|
||||
if (!backgroundRenderer.value) return
|
||||
syncRendererMotion()
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
const renderer = backgroundRenderer.value
|
||||
|
||||
if (renderer) {
|
||||
try {
|
||||
renderer.pause()
|
||||
renderer.dispose()
|
||||
} catch (error) {
|
||||
console.error('清理背景渲染器失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
backgroundRenderer.value = null
|
||||
isInitialized.value = false
|
||||
isRendering.value = false
|
||||
isMotionPaused.value = false
|
||||
containerElement.value = null
|
||||
coverBlurElement.value = null
|
||||
currentCoverUrl.value = ''
|
||||
loadedCoverUrl.value = ''
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
dispose()
|
||||
})
|
||||
|
||||
return {
|
||||
backgroundRenderer: computed(() => backgroundRenderer.value),
|
||||
isInitialized: computed(() => isInitialized.value),
|
||||
isRendering: computed(() => isRendering.value),
|
||||
hasRenderError: computed(() => hasRenderError.value),
|
||||
config: computed(() => config.value),
|
||||
initializeBackground,
|
||||
updateConfig,
|
||||
setCoverBackground,
|
||||
setCoverBlurElement,
|
||||
setGradientFromCover,
|
||||
startRender,
|
||||
stopRender,
|
||||
pauseRender,
|
||||
resumeRender,
|
||||
dispose,
|
||||
applyConfig: applyRendererState
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user