+
无法加载插件全页组件。多入口时请暴露 AppPage 或 AppPage{Pascal}(见文档),并确认插件已启用。
diff --git a/src/utils/__tests__/backgroundRotation.spec.ts b/src/utils/__tests__/backgroundRotation.spec.ts
index c3e53ac8..10d41fa6 100644
--- a/src/utils/__tests__/backgroundRotation.spec.ts
+++ b/src/utils/__tests__/backgroundRotation.spec.ts
@@ -1,5 +1,6 @@
import {
BACKGROUND_ROTATION_GRACE_MS,
+ createBackgroundCandidateOrderResolver,
findFirstAvailableBackground,
preloadBackgroundRotationImages,
shouldAllowBackgroundRotation,
@@ -27,6 +28,27 @@ describe('background rotation lifecycle', () => {
})
})
+describe('createBackgroundCandidateOrderResolver', () => {
+ it('shuffles a source list without mutating the backend response', () => {
+ const urls = ['one.jpg', 'two.jpg', 'three.jpg']
+ const resolveOrder = createBackgroundCandidateOrderResolver(() => 0)
+
+ expect(resolveOrder(urls)).toEqual(['two.jpg', 'three.jpg', 'one.jpg'])
+ expect(urls).toEqual(['one.jpg', 'two.jpg', 'three.jpg'])
+ })
+
+ it('keeps the same order across retries and reshuffles only after the source list changes', () => {
+ const random = vi.fn().mockReturnValueOnce(0).mockReturnValueOnce(0).mockReturnValueOnce(0.5)
+ const resolveOrder = createBackgroundCandidateOrderResolver(random)
+ const firstOrder = resolveOrder(['one.jpg', 'two.jpg', 'three.jpg'])
+
+ expect(resolveOrder(['one.jpg', 'two.jpg', 'three.jpg'])).toEqual(firstOrder)
+ expect(random).toHaveBeenCalledTimes(2)
+ expect(resolveOrder(['one.jpg', 'two.jpg'])).toEqual(['one.jpg', 'two.jpg'])
+ expect(random).toHaveBeenCalledTimes(3)
+ })
+})
+
describe('preloadBackgroundRotationImages', () => {
it('does not let an unused optical texture block the visible wallpaper', async () => {
const preload = vi.fn(async (url: string) => url === 'display.jpg')
diff --git a/src/utils/backgroundRotation.ts b/src/utils/backgroundRotation.ts
index ba16f9ca..2ea71a09 100644
--- a/src/utils/backgroundRotation.ts
+++ b/src/utils/backgroundRotation.ts
@@ -3,11 +3,36 @@ import type { AppActivityState } from '@/utils/appActivityLifecycle'
/** 壁纸在窗口失焦后继续轮换的最长时间,交互 renderer 仍由应用生命周期独立暂停。 */
export const BACKGROUND_ROTATION_GRACE_MS = 60_000
+type RandomSource = () => number
+
/** 壁纸轮换只在前台活动或失焦宽限期内运行,系统减少动态效果时始终停止。 */
export function shouldAllowBackgroundRotation(state: AppActivityState, graceActive: boolean, reducedMotion: boolean) {
return !reducedMotion && (state === 'active' || graceActive)
}
+/**
+ * 为每个前端页面生命周期生成稳定的随机候选顺序;相同来源列表在重试和状态恢复时不会再次洗牌。
+ */
+export function createBackgroundCandidateOrderResolver(random: RandomSource = Math.random) {
+ let sourceUrls: string[] | null = null
+ let orderedUrls: string[] = []
+
+ return (urls: string[]) => {
+ const sourceChanged =
+ !sourceUrls || sourceUrls.length !== urls.length || sourceUrls.some((url, index) => url !== urls[index])
+ if (!sourceChanged) return [...orderedUrls]
+
+ sourceUrls = [...urls]
+ orderedUrls = [...urls]
+ for (let index = orderedUrls.length - 1; index > 0; index -= 1) {
+ const swapIndex = Math.floor(random() * (index + 1))
+ ;[orderedUrls[index], orderedUrls[swapIndex]] = [orderedUrls[swapIndex], orderedUrls[index]]
+ }
+
+ return [...orderedUrls]
+ }
+}
+
interface BackgroundRotationImagePreloadOptions {
/** 外层背景实际显示的壁纸地址。 */
displayUrl: string
@@ -18,7 +43,7 @@ interface BackgroundRotationImagePreloadOptions {
}
interface FirstAvailableBackgroundOptions {
- /** 保持后端返回顺序的候选壁纸。 */
+ /** 当前页面生命周期已确定顺序的候选壁纸。 */
urls: string[]
/** 当前加载批次仍可提交时返回 true。 */
canContinue: () => boolean
@@ -26,7 +51,7 @@ interface FirstAvailableBackgroundOptions {
preload: (url: string) => Promise
}
-/** 按来源顺序寻找首张可用壁纸,单项失败或过期批次不会提交可见状态。 */
+/** 按当前候选顺序寻找首张可用壁纸,单项失败或过期批次不会提交可见状态。 */
export async function findFirstAvailableBackground(options: FirstAvailableBackgroundOptions) {
for (let index = 0; index < options.urls.length; index += 1) {
if (!options.canContinue()) return null
diff --git a/src/utils/glassOptics.ts b/src/utils/glassOptics.ts
index a5c64d65..a926be64 100644
--- a/src/utils/glassOptics.ts
+++ b/src/utils/glassOptics.ts
@@ -72,9 +72,14 @@ export interface GlassOpticalSpringState {
velocity: number
}
+/** 光学表面在共享 renderer 中采用的动态响应合同。 */
+export type GlassOpticalSurfaceMode = 'dynamic' | 'static-material'
+
export interface GlassOpticalSurfaceCandidate {
/** renderer 生命周期内稳定的表面身份。 */
key: TKey
+ /** 表面使用完整动态光学,或只保留稳定材质能量。 */
+ mode?: GlassOpticalSurfaceMode
/** 表面的当前视口几何。 */
rect: GlassOpticalRect
}