mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-09 01:36:45 +08:00
fix(glass): load cross-origin wallpapers directly (#593)
This commit is contained in:
@@ -0,0 +1,53 @@
|
|||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
vi.mock('colorthief', () => ({
|
||||||
|
default: class ColorThief {},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { preloadCorsImage } from '@/@core/utils/image'
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
describe('preloadCorsImage', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses a CORS-clean cached response without reloading it', async () => {
|
||||||
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
blob: vi.fn().mockResolvedValue(new Blob(['image'])),
|
||||||
|
ok: true,
|
||||||
|
})
|
||||||
|
vi.stubGlobal('fetch', fetchMock)
|
||||||
|
|
||||||
|
await expect(preloadCorsImage('https://image.example/wallpaper.jpg')).resolves.toBe(true)
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
|
new URL('https://image.example/wallpaper.jpg'),
|
||||||
|
expect.objectContaining({ cache: 'force-cache', credentials: 'omit', mode: 'cors' }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reloads a response when an earlier non-CORS cache entry blocks the first request', async () => {
|
||||||
|
const fetchMock = vi
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValueOnce(new TypeError('Failed to fetch'))
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
blob: vi.fn().mockResolvedValue(new Blob(['image'])),
|
||||||
|
ok: true,
|
||||||
|
})
|
||||||
|
vi.stubGlobal('fetch', fetchMock)
|
||||||
|
|
||||||
|
await expect(preloadCorsImage('/wallpaper.jpg')).resolves.toBe(true)
|
||||||
|
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
new URL('/wallpaper.jpg', window.location.href),
|
||||||
|
expect.objectContaining({ cache: 'reload', credentials: 'same-origin', mode: 'cors' }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns false when the source cannot be read with CORS', async () => {
|
||||||
|
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new TypeError('Failed to fetch')))
|
||||||
|
|
||||||
|
await expect(preloadCorsImage('https://image.example/wallpaper.jpg')).resolves.toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -70,22 +70,57 @@ export async function getDominantColor(
|
|||||||
export async function preloadImage(url: string): Promise<boolean> {
|
export async function preloadImage(url: string): Promise<boolean> {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
|
let settled = false
|
||||||
|
const finish = (available: boolean) => {
|
||||||
|
if (settled) return
|
||||||
|
|
||||||
img.onload = () => resolve(true)
|
settled = true
|
||||||
img.onerror = () => resolve(false)
|
clearTimeout(timeout)
|
||||||
|
resolve(available)
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onload = () => finish(true)
|
||||||
|
img.onerror = () => finish(false)
|
||||||
|
|
||||||
// 设置超时,防止图片长时间加载
|
// 设置超时,防止图片长时间加载
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
img.src = ''
|
img.src = ''
|
||||||
resolve(false)
|
finish(false)
|
||||||
}, 5000) // 5秒超时
|
}, 5000) // 5秒超时
|
||||||
|
|
||||||
img.src = url
|
img.src = url
|
||||||
|
|
||||||
// 如果图片已经缓存,onload可能不会触发
|
// 如果图片已经缓存,onload可能不会触发
|
||||||
if (img.complete) {
|
if (img.complete) {
|
||||||
clearTimeout(timeout)
|
finish(img.naturalWidth > 0)
|
||||||
resolve(true)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 在纹理加载前建立带 CORS 响应头的缓存,避免普通图片缓存污染 WebGL 读取。 */
|
||||||
|
export async function preloadCorsImage(url: string): Promise<boolean> {
|
||||||
|
const request = async (cache: RequestCache) => {
|
||||||
|
const source = new URL(url, window.location.href)
|
||||||
|
const response = await fetch(source, {
|
||||||
|
cache,
|
||||||
|
credentials: source.origin === window.location.origin ? 'same-origin' : 'omit',
|
||||||
|
mode: 'cors',
|
||||||
|
})
|
||||||
|
if (!response.ok) return false
|
||||||
|
|
||||||
|
await response.blob()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (await request('force-cache')) return true
|
||||||
|
} catch {
|
||||||
|
// 缓存中的非 CORS 响应可能使首次读取失败,重新验证后再决定是否回退。
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await request('reload')
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+24
-19
@@ -7,7 +7,7 @@ import { useAuthStore, useGlobalSettingsStore } from '@/stores'
|
|||||||
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
|
import { getBrowserLocale, setI18nLanguage } from './plugins/i18n'
|
||||||
import { SupportedLocale } from '@/types/i18n'
|
import { SupportedLocale } from '@/types/i18n'
|
||||||
import { checkAndEmitUnreadMessages } from '@/utils/badge'
|
import { checkAndEmitUnreadMessages } from '@/utils/badge'
|
||||||
import { preloadImage } from './@core/utils/image'
|
import { preloadCorsImage, preloadImage } from './@core/utils/image'
|
||||||
import { globalLoadingStateManager } from '@/utils/loadingStateManager'
|
import { globalLoadingStateManager } from '@/utils/loadingStateManager'
|
||||||
import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager'
|
import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundManager'
|
||||||
import PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue'
|
import PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue'
|
||||||
@@ -38,10 +38,8 @@ import {
|
|||||||
createLoginBackgroundLayers,
|
createLoginBackgroundLayers,
|
||||||
getLoginGlassOpticalSettings,
|
getLoginGlassOpticalSettings,
|
||||||
getLoginVisualProfile,
|
getLoginVisualProfile,
|
||||||
getLoginWallpaperRequestMode,
|
|
||||||
prepareLoginBackgroundLayer,
|
prepareLoginBackgroundLayer,
|
||||||
settleLoginBackgroundLayers,
|
settleLoginBackgroundLayers,
|
||||||
type LoginWallpaperRequestMode,
|
|
||||||
} from '@/utils/loginPresentation'
|
} from '@/utils/loginPresentation'
|
||||||
|
|
||||||
const LOGIN_WALLPAPER_ROUTE = '/login'
|
const LOGIN_WALLPAPER_ROUTE = '/login'
|
||||||
@@ -158,7 +156,6 @@ const shouldUseGlassBackgroundTreatment = computed(
|
|||||||
const shouldLoadBackgroundImages = computed(
|
const shouldLoadBackgroundImages = computed(
|
||||||
() => isLoginWallpaperRoute.value || (Boolean(isLogin.value) && isBackdropTheme.value),
|
() => isLoginWallpaperRoute.value || (Boolean(isLogin.value) && isBackdropTheme.value),
|
||||||
)
|
)
|
||||||
const wallpaperRequestMode = computed(() => getLoginWallpaperRequestMode(loginVisualProfile.value))
|
|
||||||
const activeBackgroundImage = computed(() => backgroundImages.value[activeImageIndex.value] ?? '')
|
const activeBackgroundImage = computed(() => backgroundImages.value[activeImageIndex.value] ?? '')
|
||||||
const renderedBackgroundLayers = computed(() => backgroundLayers.value)
|
const renderedBackgroundLayers = computed(() => backgroundLayers.value)
|
||||||
const getOpticalBackgroundImage = (imageUrl: string) => getDisplayImageUrl(imageUrl, Boolean(isLogin.value))
|
const getOpticalBackgroundImage = (imageUrl: string) => getDisplayImageUrl(imageUrl, Boolean(isLogin.value))
|
||||||
@@ -480,13 +477,12 @@ function activateBackgroundImage(nextIndex: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取背景图片列表;只有选出实际可用的首图后才提交到可见状态。
|
// 获取背景图片列表;只有选出实际可用的首图后才提交到可见状态。
|
||||||
async function fetchBackgroundImages(requestMode: LoginWallpaperRequestMode) {
|
async function fetchBackgroundImages() {
|
||||||
backgroundRequestController?.abort()
|
backgroundRequestController?.abort()
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
backgroundRequestController = controller
|
backgroundRequestController = controller
|
||||||
try {
|
try {
|
||||||
return await api.get<string[], string[]>(`/login/wallpapers`, {
|
return await api.get<string[], string[]>(`/login/wallpapers`, {
|
||||||
params: requestMode === 'same-origin' ? { same_origin: true } : undefined,
|
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
@@ -498,7 +494,18 @@ async function fetchBackgroundImages(requestMode: LoginWallpaperRequestMode) {
|
|||||||
function preloadNextBackgroundImage() {
|
function preloadNextBackgroundImage() {
|
||||||
if (!allowsBackgroundRotation.value || backgroundImages.value.length <= 1) return
|
if (!allowsBackgroundRotation.value || backgroundImages.value.length <= 1) return
|
||||||
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
|
const nextIndex = (activeImageIndex.value + 1) % backgroundImages.value.length
|
||||||
void preloadImage(backgroundImages.value[nextIndex])
|
void preloadBackgroundCandidate(backgroundImages.value[nextIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 实时玻璃先建立可供 WebGL 读取的缓存,失败时仍允许 CSS 材质显示该壁纸。 */
|
||||||
|
async function preloadBackgroundCandidate(imageUrl: string) {
|
||||||
|
if (!shouldRenderGlassOpticalLayer.value) return preloadImage(imageUrl)
|
||||||
|
|
||||||
|
const opticalUrl = getOpticalBackgroundImage(imageUrl)
|
||||||
|
const opticalReady = await preloadCorsImage(opticalUrl)
|
||||||
|
if (!opticalReady) return preloadImage(imageUrl)
|
||||||
|
|
||||||
|
return opticalUrl === imageUrl ? true : preloadImage(imageUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 背景图片轮换函数
|
// 背景图片轮换函数
|
||||||
@@ -513,13 +520,13 @@ async function rotateBackgroundImage() {
|
|||||||
const nextIndex = (activeIndex + offset) % backgroundImages.value.length
|
const nextIndex = (activeIndex + offset) % backgroundImages.value.length
|
||||||
const nextImage = backgroundImages.value[nextIndex]
|
const nextImage = backgroundImages.value[nextIndex]
|
||||||
const opticalImage = shouldRenderGlassOpticalLayer.value ? getOpticalBackgroundImage(nextImage) : undefined
|
const opticalImage = shouldRenderGlassOpticalLayer.value ? getOpticalBackgroundImage(nextImage) : undefined
|
||||||
|
if (opticalImage && !(await prepareOpticalWallpaper(opticalImage))) continue
|
||||||
const imagesReady = await preloadBackgroundRotationImages({
|
const imagesReady = await preloadBackgroundRotationImages({
|
||||||
displayUrl: nextImage,
|
displayUrl: nextImage,
|
||||||
opticalUrl: opticalImage,
|
opticalUrl: opticalImage,
|
||||||
preload: preloadImage,
|
preload: preloadImage,
|
||||||
})
|
})
|
||||||
if (!imagesReady) continue
|
if (!imagesReady) continue
|
||||||
if (opticalImage && !(await prepareOpticalWallpaper(opticalImage))) continue
|
|
||||||
if (!allowsBackgroundRotation.value || requestVersion !== backgroundRotationVersion) return
|
if (!allowsBackgroundRotation.value || requestVersion !== backgroundRotationVersion) return
|
||||||
|
|
||||||
activateBackgroundImage(nextIndex)
|
activateBackgroundImage(nextIndex)
|
||||||
@@ -531,7 +538,7 @@ async function rotateBackgroundImage() {
|
|||||||
stopBackgroundRotation()
|
stopBackgroundRotation()
|
||||||
const recoveryVersion = ++backgroundLoadVersion
|
const recoveryVersion = ++backgroundLoadVersion
|
||||||
backgroundRecoveryAttemptedVersion = recoveryVersion
|
backgroundRecoveryAttemptedVersion = recoveryVersion
|
||||||
void loadBackgroundImages(wallpaperRequestMode.value, recoveryVersion)
|
void loadBackgroundImages(recoveryVersion)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -708,16 +715,16 @@ async function removeLoadingWithStateCheck() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 加载背景图片
|
// 加载背景图片
|
||||||
async function loadBackgroundImages(requestMode: LoginWallpaperRequestMode, loadVersion: number, retryCount = 0) {
|
async function loadBackgroundImages(loadVersion: number, retryCount = 0) {
|
||||||
const maxRetries = 3
|
const maxRetries = 3
|
||||||
try {
|
try {
|
||||||
const images = await fetchBackgroundImages(requestMode)
|
const images = await fetchBackgroundImages()
|
||||||
if (loadVersion !== backgroundLoadVersion) return
|
if (loadVersion !== backgroundLoadVersion) return
|
||||||
|
|
||||||
const firstAvailableIndex = await findFirstAvailableBackground({
|
const firstAvailableIndex = await findFirstAvailableBackground({
|
||||||
urls: images,
|
urls: images,
|
||||||
canContinue: () => loadVersion === backgroundLoadVersion,
|
canContinue: () => loadVersion === backgroundLoadVersion,
|
||||||
preload: preloadImage,
|
preload: preloadBackgroundCandidate,
|
||||||
})
|
})
|
||||||
if (firstAvailableIndex === null) throw new Error('没有可用的登录壁纸')
|
if (firstAvailableIndex === null) throw new Error('没有可用的登录壁纸')
|
||||||
if (loadVersion !== backgroundLoadVersion) return
|
if (loadVersion !== backgroundLoadVersion) return
|
||||||
@@ -743,7 +750,7 @@ async function loadBackgroundImages(requestMode: LoginWallpaperRequestMode, load
|
|||||||
backgroundRetryTimer = window.setTimeout(() => {
|
backgroundRetryTimer = window.setTimeout(() => {
|
||||||
backgroundRetryTimer = null
|
backgroundRetryTimer = null
|
||||||
if (loadVersion === backgroundLoadVersion) {
|
if (loadVersion === backgroundLoadVersion) {
|
||||||
void loadBackgroundImages(requestMode, loadVersion, retryCount + 1)
|
void loadBackgroundImages(loadVersion, retryCount + 1)
|
||||||
}
|
}
|
||||||
}, retryDelay)
|
}, retryDelay)
|
||||||
}
|
}
|
||||||
@@ -788,15 +795,13 @@ onMounted(async () => {
|
|||||||
window.addEventListener('focus', handlePageShowThemeSync)
|
window.addEventListener('focus', handlePageShowThemeSync)
|
||||||
window.addEventListener(TRANSPARENCY_SETTINGS_CHANGED_EVENT, handleTransparencySettingsChanged)
|
window.addEventListener(TRANSPARENCY_SETTINGS_CHANGED_EVENT, handleTransparencySettingsChanged)
|
||||||
|
|
||||||
// 背景范围或玻璃同源能力变化时重新加载;登录前后玻璃主题保持同一请求模式和活动壁纸。
|
// 登录前后复用同一壁纸列表和活动项,主题变化只改变呈现方式。
|
||||||
watch(
|
watch(
|
||||||
() => [shouldLoadBackgroundImages.value, wallpaperRequestMode.value] as const,
|
shouldLoadBackgroundImages,
|
||||||
([shouldLoad, requestMode], previous) => {
|
shouldLoad => {
|
||||||
if (previous && shouldLoad === previous[0] && requestMode === previous[1]) return
|
|
||||||
|
|
||||||
stopBackgroundLoading()
|
stopBackgroundLoading()
|
||||||
if (shouldLoad) {
|
if (shouldLoad) {
|
||||||
void loadBackgroundImages(requestMode, backgroundLoadVersion)
|
void loadBackgroundImages(backgroundLoadVersion)
|
||||||
} else if (!isBackdropTheme.value) {
|
} else if (!isBackdropTheme.value) {
|
||||||
backgroundImages.value = []
|
backgroundImages.value = []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -745,6 +745,16 @@ onUnmounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<!-- 登录页面容器 -->
|
<!-- 登录页面容器 -->
|
||||||
<div class="login-root" :data-login-visual-profile="loginVisualProfile">
|
<div class="login-root" :data-login-visual-profile="loginVisualProfile">
|
||||||
|
<svg class="login-glass-filter-defs" aria-hidden="true">
|
||||||
|
<defs>
|
||||||
|
<filter id="login-glass-static-refraction" x="-12%" y="-12%" width="124%" height="124%">
|
||||||
|
<feTurbulence type="fractalNoise" baseFrequency="0.008 0.014" numOctaves="2" seed="8" result="noise" />
|
||||||
|
<feGaussianBlur in="noise" stdDeviation="0.35" result="softNoise" />
|
||||||
|
<feDisplacementMap in="SourceGraphic" in2="softNoise" scale="11" xChannelSelector="R" yChannelSelector="G" />
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
<!-- 经典主题保留一层低频品牌环境光;透明与玻璃 profile 不挂载该装饰。 -->
|
<!-- 经典主题保留一层低频品牌环境光;透明与玻璃 profile 不挂载该装饰。 -->
|
||||||
<div v-if="loginVisualProfile === 'classic'" class="login-ambient-light" aria-hidden="true">
|
<div v-if="loginVisualProfile === 'classic'" class="login-ambient-light" aria-hidden="true">
|
||||||
<span class="login-ambient-light__wash" />
|
<span class="login-ambient-light__wash" />
|
||||||
@@ -1032,6 +1042,14 @@ onUnmounted(() => {
|
|||||||
padding-inline: 16px;
|
padding-inline: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-glass-filter-defs {
|
||||||
|
position: absolute;
|
||||||
|
overflow: hidden;
|
||||||
|
block-size: 0;
|
||||||
|
inline-size: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* ===================== 玻璃卡片 ===================== */
|
/* ===================== 玻璃卡片 ===================== */
|
||||||
.login-card {
|
.login-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1469,6 +1487,7 @@ onUnmounted(() => {
|
|||||||
.native-login-field {
|
.native-login-field {
|
||||||
backdrop-filter: none !important;
|
backdrop-filter: none !important;
|
||||||
background: rgb(var(--v-theme-surface)) !important;
|
background: rgb(var(--v-theme-surface)) !important;
|
||||||
|
filter: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1108,6 +1108,12 @@ html[data-theme='glass'] body[data-theme='glass'] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-theme='glass'][data-glass-renderer-state='fallback'] body[data-theme='glass'] {
|
||||||
|
.app-wrapper--login-glass-high .login-card__surface {
|
||||||
|
filter: url('#login-glass-static-refraction');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
html[data-theme='glass'][data-glass-appearance='tinted'] body[data-theme='glass'] {
|
html[data-theme='glass'][data-glass-appearance='tinted'] body[data-theme='glass'] {
|
||||||
.login-card__surface {
|
.login-card__surface {
|
||||||
background:
|
background:
|
||||||
|
|||||||
@@ -351,14 +351,16 @@ describe('glass optics geometry', () => {
|
|||||||
expect(getGlassOpticalSurfaceTransitionWeights(96, 96)).toEqual({ incoming: 1, outgoing: 0 })
|
expect(getGlassOpticalSurfaceTransitionWeights(96, 96)).toEqual({ incoming: 1, outgoing: 0 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('only uploads browser-readable login wallpapers to WebGL', () => {
|
it('attempts browser-supported wallpaper protocols without allowing mixed content', () => {
|
||||||
const documentUrl = 'https://moviepilot.example/login'
|
const documentUrl = 'https://moviepilot.example/login'
|
||||||
|
|
||||||
expect(canUseGlassWallpaperTexture('/api/v1/login/wallpaper/0', documentUrl)).toBe(true)
|
expect(canUseGlassWallpaperTexture('/api/v1/login/wallpaper/0', documentUrl)).toBe(true)
|
||||||
expect(canUseGlassWallpaperTexture('https://moviepilot.example/assets/login.jpg', documentUrl)).toBe(true)
|
expect(canUseGlassWallpaperTexture('https://moviepilot.example/assets/login.jpg', documentUrl)).toBe(true)
|
||||||
expect(canUseGlassWallpaperTexture('blob:https://moviepilot.example/texture', documentUrl)).toBe(true)
|
expect(canUseGlassWallpaperTexture('blob:https://moviepilot.example/texture', documentUrl)).toBe(true)
|
||||||
expect(canUseGlassWallpaperTexture('data:image/png;base64,AA==', documentUrl)).toBe(true)
|
expect(canUseGlassWallpaperTexture('data:image/png;base64,AA==', documentUrl)).toBe(true)
|
||||||
expect(canUseGlassWallpaperTexture('https://image.tmdb.org/t/p/original/poster.jpg', documentUrl)).toBe(false)
|
expect(canUseGlassWallpaperTexture('https://image.tmdb.org/t/p/original/poster.jpg', documentUrl)).toBe(true)
|
||||||
|
expect(canUseGlassWallpaperTexture('http://image.tmdb.org/t/p/original/poster.jpg', documentUrl)).toBe(false)
|
||||||
|
expect(canUseGlassWallpaperTexture('ftp://image.tmdb.org/poster.jpg', documentUrl)).toBe(false)
|
||||||
expect(canUseGlassWallpaperTexture('', documentUrl)).toBe(false)
|
expect(canUseGlassWallpaperTexture('', documentUrl)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
createLoginBackgroundLayers,
|
createLoginBackgroundLayers,
|
||||||
getLoginGlassOpticalSettings,
|
getLoginGlassOpticalSettings,
|
||||||
getLoginVisualProfile,
|
getLoginVisualProfile,
|
||||||
getLoginWallpaperRequestMode,
|
|
||||||
prepareLoginBackgroundLayer,
|
prepareLoginBackgroundLayer,
|
||||||
settleLoginBackgroundLayers,
|
settleLoginBackgroundLayers,
|
||||||
} from '@/utils/loginPresentation'
|
} from '@/utils/loginPresentation'
|
||||||
@@ -42,12 +41,6 @@ describe('login presentation', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('requests same-origin wallpapers only for glass', () => {
|
|
||||||
expect(getLoginWallpaperRequestMode('glass')).toBe('same-origin')
|
|
||||||
expect(getLoginWallpaperRequestMode('classic')).toBe('default')
|
|
||||||
expect(getLoginWallpaperRequestMode('transparent')).toBe('default')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('keeps two stable wallpaper slots while their transition roles change', () => {
|
it('keeps two stable wallpaper slots while their transition roles change', () => {
|
||||||
const initial = createLoginBackgroundLayers('one.jpg')
|
const initial = createLoginBackgroundLayers('one.jpg')
|
||||||
const prepared = prepareLoginBackgroundLayer(initial, 'two.jpg')
|
const prepared = prepareLoginBackgroundLayer(initial, 'two.jpg')
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ export function reconcileGlassOpticalSurfaceSlots<TKey>(
|
|||||||
return [...stable, ...reserved].slice(0, maxCount)
|
return [...stable, ...reserved].slice(0, maxCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 只将同源及本地对象交给 WebGL,避免跨域纹理失败污染登录页控制台。 */
|
/** 过滤浏览器不会作为纹理读取的协议;跨域读取能力由实际纹理加载结果判定。 */
|
||||||
export function canUseGlassWallpaperTexture(url: string, documentUrl: string): boolean {
|
export function canUseGlassWallpaperTexture(url: string, documentUrl: string): boolean {
|
||||||
if (!url || !documentUrl) return false
|
if (!url || !documentUrl) return false
|
||||||
|
|
||||||
@@ -500,7 +500,11 @@ export function canUseGlassWallpaperTexture(url: string, documentUrl: string): b
|
|||||||
const source = new URL(url, documentUrl)
|
const source = new URL(url, documentUrl)
|
||||||
if (source.protocol === 'blob:' || source.protocol === 'data:') return true
|
if (source.protocol === 'blob:' || source.protocol === 'data:') return true
|
||||||
|
|
||||||
return source.origin === new URL(documentUrl).origin
|
const document = new URL(documentUrl)
|
||||||
|
if (source.protocol !== 'http:' && source.protocol !== 'https:') return false
|
||||||
|
if (document.protocol === 'https:' && source.protocol === 'http:') return false
|
||||||
|
|
||||||
|
return true
|
||||||
} catch {
|
} catch {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { GlassAppearance, GlassOpticalPreset } from '@/utils/glassOptics'
|
import type { GlassAppearance, GlassOpticalPreset } from '@/utils/glassOptics'
|
||||||
|
|
||||||
export type LoginVisualProfile = 'classic' | 'glass' | 'transparent'
|
export type LoginVisualProfile = 'classic' | 'glass' | 'transparent'
|
||||||
export type LoginWallpaperRequestMode = 'default' | 'same-origin'
|
|
||||||
|
|
||||||
export interface LoginGlassPreference {
|
export interface LoginGlassPreference {
|
||||||
/** 用户选择的玻璃材质。 */
|
/** 用户选择的玻璃材质。 */
|
||||||
@@ -53,11 +52,6 @@ export function getLoginGlassOpticalSettings(preference: LoginGlassPreference) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 玻璃主题需要同源纹理,其余主题保留现有外链返回行为。 */
|
|
||||||
export function getLoginWallpaperRequestMode(profile: LoginVisualProfile): LoginWallpaperRequestMode {
|
|
||||||
return profile === 'glass' ? 'same-origin' : 'default'
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 建立两个始终存在的背景槽位,避免角色变化时复用错误的 DOM 合成层。 */
|
/** 建立两个始终存在的背景槽位,避免角色变化时复用错误的 DOM 合成层。 */
|
||||||
export function createLoginBackgroundLayers(activeUrl = ''): LoginBackgroundLayer[] {
|
export function createLoginBackgroundLayers(activeUrl = ''): LoginBackgroundLayer[] {
|
||||||
return [
|
return [
|
||||||
|
|||||||
Reference in New Issue
Block a user