mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-04 23:18:44 +08:00
Fix mobile clear glass surfaces
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
collectGlassOpticalRects,
|
||||||
containsGlassOpticalSurface,
|
containsGlassOpticalSurface,
|
||||||
setGlassRendererState,
|
setGlassRendererState,
|
||||||
useGlassOpticalRenderer,
|
useGlassOpticalRenderer,
|
||||||
@@ -35,11 +36,33 @@ vi.mock('three', async importOriginal => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 提供 renderer 单元测试所需的最小 ResizeObserver 实现。 */
|
||||||
class ResizeObserverMock {
|
class ResizeObserverMock {
|
||||||
|
/** 断开观察时无需执行额外逻辑。 */
|
||||||
disconnect() {}
|
disconnect() {}
|
||||||
|
|
||||||
|
/** 测试不需要追踪具体被观察元素。 */
|
||||||
observe() {}
|
observe() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 创建带稳定视口边界的光学表面元素。 */
|
||||||
|
function appendOpticalSurface(className: string, bounds: Pick<DOMRect, 'height' | 'width' | 'x' | 'y'>) {
|
||||||
|
const surface = document.createElement('section')
|
||||||
|
surface.className = className
|
||||||
|
surface.getBoundingClientRect = () =>
|
||||||
|
({
|
||||||
|
...bounds,
|
||||||
|
bottom: bounds.y + bounds.height,
|
||||||
|
left: bounds.x,
|
||||||
|
right: bounds.x + bounds.width,
|
||||||
|
top: bounds.y,
|
||||||
|
toJSON: () => ({}),
|
||||||
|
}) as DOMRect
|
||||||
|
document.body.append(surface)
|
||||||
|
|
||||||
|
return surface
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubGlobal('ResizeObserver', ResizeObserverMock)
|
vi.stubGlobal('ResizeObserver', ResizeObserverMock)
|
||||||
vi.stubGlobal('WebGLRenderingContext', class {})
|
vi.stubGlobal('WebGLRenderingContext', class {})
|
||||||
@@ -49,6 +72,7 @@ beforeEach(() => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks()
|
vi.restoreAllMocks()
|
||||||
vi.unstubAllGlobals()
|
vi.unstubAllGlobals()
|
||||||
|
document.body.replaceChildren()
|
||||||
delete document.documentElement.dataset.glassRendererState
|
delete document.documentElement.dataset.glassRendererState
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -89,6 +113,32 @@ describe('glass optical surface discovery', () => {
|
|||||||
expect(document.documentElement.dataset.glassRendererState).toBe('fallback')
|
expect(document.documentElement.dataset.glassRendererState).toBe('fallback')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('excludes the navbar from mobile clear optical surfaces', () => {
|
||||||
|
appendOpticalSurface('layout-navbar', { height: 64, width: 390, x: 0, y: 0 })
|
||||||
|
appendOpticalSurface('dashboard-grid-item-content', { height: 200, width: 350, x: 20, y: 120 })
|
||||||
|
|
||||||
|
const rects = collectGlassOpticalRects(390, 800, 'clear')
|
||||||
|
|
||||||
|
expect(rects).toHaveLength(1)
|
||||||
|
expect(rects[0]).toMatchObject({ height: 200, width: 350, x: 20, y: 120 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the navbar in mobile frosted optical surfaces', () => {
|
||||||
|
appendOpticalSurface('layout-navbar', { height: 64, width: 390, x: 0, y: 0 })
|
||||||
|
|
||||||
|
expect(collectGlassOpticalRects(390, 800, 'frosted')).toEqual([
|
||||||
|
expect.objectContaining({ height: 64, width: 390, x: 0, y: 0 }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the navbar in desktop clear optical surfaces', () => {
|
||||||
|
appendOpticalSurface('layout-navbar', { height: 64, width: 1000, x: 0, y: 0 })
|
||||||
|
|
||||||
|
expect(collectGlassOpticalRects(1000, 800, 'clear')).toEqual([
|
||||||
|
expect.objectContaining({ height: 64, width: 1000, x: 0, y: 0 }),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('recovers after consecutive WebGL context loss cycles', async () => {
|
it('recovers after consecutive WebGL context loss cycles', async () => {
|
||||||
const canvas = document.createElement('canvas')
|
const canvas = document.createElement('canvas')
|
||||||
const scope = effectScope()
|
const scope = effectScope()
|
||||||
|
|||||||
@@ -231,11 +231,18 @@ function isVisibleSurface(element: HTMLElement, bounds: DOMRect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 将活动界面中的高价值材质面集中转换为 renderer 矩形预算。 */
|
/** 将活动界面中的高价值材质面集中转换为 renderer 矩形预算。 */
|
||||||
export function collectGlassOpticalRects(viewportWidth: number, viewportHeight: number) {
|
export function collectGlassOpticalRects(
|
||||||
|
viewportWidth: number,
|
||||||
|
viewportHeight: number,
|
||||||
|
appearance: ThemeCustomizerGlassAppearance,
|
||||||
|
) {
|
||||||
const candidates: GlassOpticalRect[] = []
|
const candidates: GlassOpticalRect[] = []
|
||||||
const seen = new Set<HTMLElement>()
|
const seen = new Set<HTMLElement>()
|
||||||
|
|
||||||
for (const { rank, selector } of SURFACE_SELECTORS) {
|
for (const { rank, selector } of SURFACE_SELECTORS) {
|
||||||
|
// 移动端透明顶栏只使用稳定的 CSS 表面,避免滚动重扫时再次叠加壁纸折射。
|
||||||
|
if (viewportWidth <= 600 && appearance === 'clear' && selector === '.layout-navbar') continue
|
||||||
|
|
||||||
for (const element of document.querySelectorAll<HTMLElement>(selector)) {
|
for (const element of document.querySelectorAll<HTMLElement>(selector)) {
|
||||||
if (seen.has(element)) continue
|
if (seen.has(element)) continue
|
||||||
seen.add(element)
|
seen.add(element)
|
||||||
@@ -315,7 +322,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
|
|
||||||
const viewportWidth = window.innerWidth
|
const viewportWidth = window.innerWidth
|
||||||
const viewportHeight = window.innerHeight
|
const viewportHeight = window.innerHeight
|
||||||
const rects = collectGlassOpticalRects(viewportWidth, viewportHeight)
|
const rects = collectGlassOpticalRects(viewportWidth, viewportHeight, toValue(options.appearance))
|
||||||
currentRects = rects
|
currentRects = rects
|
||||||
const normalized = rects.map(rect => normalizeGlassOpticalRect(rect, viewportWidth, viewportHeight))
|
const normalized = rects.map(rect => normalizeGlassOpticalRect(rect, viewportWidth, viewportHeight))
|
||||||
const uniformRects = resources.uniforms.uRects.value
|
const uniformRects = resources.uniforms.uRects.value
|
||||||
@@ -742,7 +749,7 @@ export function useGlassOpticalRenderer(options: UseGlassOpticalRendererOptions)
|
|||||||
if (!resources) return
|
if (!resources) return
|
||||||
|
|
||||||
resources.uniforms.uAppearance.value = getGlassAppearanceUniformValue(appearance)
|
resources.uniforms.uAppearance.value = getGlassAppearanceUniformValue(appearance)
|
||||||
scheduleFrame()
|
scheduleSurfaceUpdate()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ html[data-theme='glass'] {
|
|||||||
--glass-control-backdrop-filter: none;
|
--glass-control-backdrop-filter: none;
|
||||||
--glass-control-prominent-backdrop-filter: none;
|
--glass-control-prominent-backdrop-filter: none;
|
||||||
--glass-navbar-backdrop-filter: var(--glass-raised-backdrop-filter);
|
--glass-navbar-backdrop-filter: var(--glass-raised-backdrop-filter);
|
||||||
--glass-clear-overlay-surface: rgba(11, 19, 34, 72%);
|
|
||||||
--glass-clear-mobile-nav-surface: rgba(11, 19, 34, 68%);
|
|
||||||
--glass-control-icon-color: rgba(242, 245, 250, 70%);
|
--glass-control-icon-color: rgba(242, 245, 250, 70%);
|
||||||
--glass-control-placeholder-color: rgba(242, 245, 250, 54%);
|
--glass-control-placeholder-color: rgba(242, 245, 250, 54%);
|
||||||
--glass-control-shortcut-background: rgba(255, 255, 255, 8%);
|
--glass-control-shortcut-background: rgba(255, 255, 255, 8%);
|
||||||
@@ -490,7 +488,8 @@ html[data-theme='glass'] {
|
|||||||
.v-overlay__content {
|
.v-overlay__content {
|
||||||
border-radius: var(--app-overlay-radius);
|
border-radius: var(--app-overlay-radius);
|
||||||
|
|
||||||
> :where(.v-card, .v-sheet, .v-list) {
|
> :where(.v-card, .v-sheet, .v-list),
|
||||||
|
> form > :where(.v-card, .v-sheet) {
|
||||||
-webkit-backdrop-filter: var(--glass-raised-backdrop-filter) !important;
|
-webkit-backdrop-filter: var(--glass-raised-backdrop-filter) !important;
|
||||||
backdrop-filter: var(--glass-raised-backdrop-filter) !important;
|
backdrop-filter: var(--glass-raised-backdrop-filter) !important;
|
||||||
background-color: var(--glass-surface-raised) !important;
|
background-color: var(--glass-surface-raised) !important;
|
||||||
@@ -499,7 +498,7 @@ html[data-theme='glass'] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 透明弹层沿用普通玻璃的色相,只提高 alpha,不引入独立底色。
|
// 透明弹层与顶栏共用同一静态表面,不额外增加模糊或深色覆盖。
|
||||||
&[data-glass-appearance='clear'] {
|
&[data-glass-appearance='clear'] {
|
||||||
.v-overlay__content > :where(.v-card, .v-sheet, .v-list),
|
.v-overlay__content > :where(.v-card, .v-sheet, .v-list),
|
||||||
.v-overlay__content > form > :where(.v-card, .v-sheet),
|
.v-overlay__content > form > :where(.v-card, .v-sheet),
|
||||||
@@ -507,7 +506,7 @@ html[data-theme='glass'] {
|
|||||||
.agent-assistant-panel {
|
.agent-assistant-panel {
|
||||||
-webkit-backdrop-filter: none !important;
|
-webkit-backdrop-filter: none !important;
|
||||||
backdrop-filter: none !important;
|
backdrop-filter: none !important;
|
||||||
background-color: var(--glass-clear-overlay-surface) !important;
|
background-color: var(--glass-surface-raised) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,15 +809,6 @@ html[data-theme='glass'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (width <= 960px) {
|
@media (width <= 960px) {
|
||||||
&[data-glass-appearance='clear'] {
|
|
||||||
// 底部主导航和旁侧动态按钮共用 footer-nav-card,提高 alpha 后仍保持透明材质色相。
|
|
||||||
.footer-nav-card.v-card {
|
|
||||||
-webkit-backdrop-filter: none !important;
|
|
||||||
backdrop-filter: none !important;
|
|
||||||
background-color: var(--glass-clear-mobile-nav-surface) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全屏快捷弹窗释放外层玻璃背景,让定时服务卡片直接采样全局壁纸。
|
// 全屏快捷弹窗释放外层玻璃背景,让定时服务卡片直接采样全局壁纸。
|
||||||
&[data-glass-appearance='frosted'] .scheduler-shortcut-dialog-card {
|
&[data-glass-appearance='frosted'] .scheduler-shortcut-dialog-card {
|
||||||
-webkit-backdrop-filter: none !important;
|
-webkit-backdrop-filter: none !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user