mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 00:36:41 +08:00
feat: add transparent glass quality options (#507)
* feat(theme): add lightweight transparent glass mode * perf(theme): reduce realtime glass backdrop filters * fix(theme): align transparency presets
This commit is contained in:
+47
-15
@@ -12,6 +12,12 @@ import { addBackgroundTimer, removeBackgroundTimer } from '@/utils/backgroundMan
|
|||||||
import PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue'
|
import PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue'
|
||||||
import SharedDialogHost from '@/components/dialog/SharedDialogHost.vue'
|
import SharedDialogHost from '@/components/dialog/SharedDialogHost.vue'
|
||||||
import { applyStoredThemeCustomizerAppearance } from '@/composables/useThemeCustomizer'
|
import { applyStoredThemeCustomizerAppearance } from '@/composables/useThemeCustomizer'
|
||||||
|
import {
|
||||||
|
applyStoredTransparencySettings,
|
||||||
|
TRANSPARENCY_SETTINGS_CHANGED_EVENT,
|
||||||
|
type TransparencyGlassQuality,
|
||||||
|
type TransparencySettings,
|
||||||
|
} from '@/composables/useTransparencySettings'
|
||||||
import { completeLaunchLoading } from '@/composables/useLaunchLoading'
|
import { completeLaunchLoading } from '@/composables/useLaunchLoading'
|
||||||
import { usePWA } from '@/composables/usePWA'
|
import { usePWA } from '@/composables/usePWA'
|
||||||
import { themeManager } from '@/utils/themeManager'
|
import { themeManager } from '@/utils/themeManager'
|
||||||
@@ -60,29 +66,38 @@ const backgroundImages = ref<string[]>([])
|
|||||||
const activeImageIndex = ref(0)
|
const activeImageIndex = ref(0)
|
||||||
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
const isTransparentTheme = computed(() => globalTheme.name.value === 'transparent')
|
||||||
const isLoginWallpaperRoute = computed(() => !isLogin.value && route.path === LOGIN_WALLPAPER_ROUTE)
|
const isLoginWallpaperRoute = computed(() => !isLogin.value && route.path === LOGIN_WALLPAPER_ROUTE)
|
||||||
|
const shouldUseTransparentBackgroundTreatment = computed(
|
||||||
|
() => (Boolean(isLogin.value) && isTransparentTheme.value) || isLoginWallpaperRoute.value,
|
||||||
|
)
|
||||||
const shouldLoadBackgroundImages = computed(
|
const shouldLoadBackgroundImages = computed(
|
||||||
() => isLoginWallpaperRoute.value || (Boolean(isLogin.value) && isTransparentTheme.value),
|
() => isLoginWallpaperRoute.value || (Boolean(isLogin.value) && isTransparentTheme.value),
|
||||||
)
|
)
|
||||||
|
const transparentBackgroundBlur = ref(16)
|
||||||
|
const transparencyGlassQuality = ref<TransparencyGlassQuality>(
|
||||||
|
localStorage.getItem('transparency-glass-quality') === 'realtime' ? 'realtime' : 'lightweight',
|
||||||
|
)
|
||||||
|
const shouldRenderGlobalBlurLayer = computed(
|
||||||
|
() =>
|
||||||
|
shouldUseTransparentBackgroundTreatment.value &&
|
||||||
|
transparentBackgroundBlur.value > 0 &&
|
||||||
|
transparencyGlassQuality.value === 'realtime',
|
||||||
|
)
|
||||||
let backgroundRetryTimer: number | null = null
|
let backgroundRetryTimer: number | null = null
|
||||||
let backgroundRequestController: AbortController | null = null
|
let backgroundRequestController: AbortController | null = null
|
||||||
let authenticatedStateTimer: number | null = null
|
let authenticatedStateTimer: number | null = null
|
||||||
|
|
||||||
function getStoredNumber(key: string, fallback: number, min: number, max: number) {
|
function applyTransparentBackgroundSettings() {
|
||||||
const parsed = Number.parseFloat(localStorage.getItem(key) || '')
|
const settings = applyStoredTransparencySettings()
|
||||||
if (!Number.isFinite(parsed)) return fallback
|
|
||||||
|
|
||||||
return Math.min(max, Math.max(min, parsed))
|
transparentBackgroundBlur.value = settings.backgroundBlur
|
||||||
|
transparencyGlassQuality.value = settings.glassQuality
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTransparentBackgroundSettings() {
|
function handleTransparencySettingsChanged(event: Event) {
|
||||||
document.documentElement.style.setProperty(
|
const { backgroundBlur, glassQuality } = (event as CustomEvent<TransparencySettings>).detail
|
||||||
'--transparent-background-poster-opacity',
|
|
||||||
(1 - getStoredNumber('transparency-background-poster-opacity', 0, 0, 1)).toString(),
|
transparentBackgroundBlur.value = backgroundBlur
|
||||||
)
|
transparencyGlassQuality.value = glassQuality
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
'--transparent-background-blur',
|
|
||||||
`${getStoredNumber('transparency-background-blur', 16, 0, 30)}px`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyTransparentBackgroundSettings()
|
applyTransparentBackgroundSettings()
|
||||||
@@ -360,6 +375,7 @@ onMounted(async () => {
|
|||||||
document.addEventListener('visibilitychange', handleVisibilityThemeSync)
|
document.addEventListener('visibilitychange', handleVisibilityThemeSync)
|
||||||
window.addEventListener('pageshow', handlePageShowThemeSync)
|
window.addEventListener('pageshow', handlePageShowThemeSync)
|
||||||
window.addEventListener('focus', handlePageShowThemeSync)
|
window.addEventListener('focus', handlePageShowThemeSync)
|
||||||
|
window.addEventListener(TRANSPARENCY_SETTINGS_CHANGED_EVENT, handleTransparencySettingsChanged)
|
||||||
|
|
||||||
// 登录页壁纸仅在未登录登录页需要,避免其他首屏额外发起图片列表请求。
|
// 登录页壁纸仅在未登录登录页需要,避免其他首屏额外发起图片列表请求。
|
||||||
watch(
|
watch(
|
||||||
@@ -413,6 +429,7 @@ onUnmounted(() => {
|
|||||||
document.removeEventListener('visibilitychange', handleVisibilityThemeSync)
|
document.removeEventListener('visibilitychange', handleVisibilityThemeSync)
|
||||||
window.removeEventListener('pageshow', handlePageShowThemeSync)
|
window.removeEventListener('pageshow', handlePageShowThemeSync)
|
||||||
window.removeEventListener('focus', handlePageShowThemeSync)
|
window.removeEventListener('focus', handlePageShowThemeSync)
|
||||||
|
window.removeEventListener(TRANSPARENCY_SETTINGS_CHANGED_EVENT, handleTransparencySettingsChanged)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -422,7 +439,11 @@ onUnmounted(() => {
|
|||||||
<div
|
<div
|
||||||
v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)"
|
v-if="backgroundImages.length > 0 && (isTransparentTheme || !isLogin)"
|
||||||
class="background-container"
|
class="background-container"
|
||||||
:class="{ 'is-transparent-theme': isTransparentTheme && isLogin }"
|
:class="{
|
||||||
|
'is-transparent-theme': shouldUseTransparentBackgroundTreatment,
|
||||||
|
'is-transparent-glass-lightweight':
|
||||||
|
shouldUseTransparentBackgroundTreatment && transparencyGlassQuality === 'lightweight',
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(imageUrl, index) in backgroundImages"
|
v-for="(imageUrl, index) in backgroundImages"
|
||||||
@@ -432,7 +453,7 @@ onUnmounted(() => {
|
|||||||
:style="{ 'backgroundImage': `url(${imageUrl})` }"
|
:style="{ 'backgroundImage': `url(${imageUrl})` }"
|
||||||
/>
|
/>
|
||||||
<!-- 全局磨砂层 -->
|
<!-- 全局磨砂层 -->
|
||||||
<div v-if="isLogin && isTransparentTheme" class="global-blur-layer"></div>
|
<div v-if="shouldRenderGlobalBlurLayer" class="global-blur-layer"></div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 页面内容 -->
|
<!-- 页面内容 -->
|
||||||
<VApp :class="{ 'app-shell--login-wallpaper': isLoginWallpaperRoute }">
|
<VApp :class="{ 'app-shell--login-wallpaper': isLoginWallpaperRoute }">
|
||||||
@@ -494,6 +515,17 @@ onUnmounted(() => {
|
|||||||
opacity: var(--transparent-background-poster-opacity, 1);
|
opacity: var(--transparent-background-poster-opacity, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.background-container.is-transparent-glass-lightweight .background-image.active {
|
||||||
|
filter: blur(var(--transparent-background-blur, 16px));
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-container.is-transparent-glass-lightweight .background-image.active::after {
|
||||||
|
background:
|
||||||
|
linear-gradient(rgba(0, 0, 0, 30%) 0%, rgba(0, 0, 0, 60%) 100%),
|
||||||
|
rgba(128, 128, 128, 30%);
|
||||||
|
}
|
||||||
|
|
||||||
/* 全局磨砂层 */
|
/* 全局磨砂层 */
|
||||||
.global-blur-layer {
|
.global-blur-layer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -38,9 +38,11 @@ const {
|
|||||||
onBackgroundBlurChange,
|
onBackgroundBlurChange,
|
||||||
onBackgroundPosterOpacityChange,
|
onBackgroundPosterOpacityChange,
|
||||||
onBlurChange,
|
onBlurChange,
|
||||||
|
onGlassQualityChange,
|
||||||
onOpacityChange,
|
onOpacityChange,
|
||||||
resetTransparencySettings,
|
resetTransparencySettings,
|
||||||
transparencyBlur,
|
transparencyBlur,
|
||||||
|
transparencyGlassQuality,
|
||||||
transparencyOpacity,
|
transparencyOpacity,
|
||||||
} = useTransparencySettings()
|
} = useTransparencySettings()
|
||||||
</script>
|
</script>
|
||||||
@@ -118,6 +120,34 @@ const {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span class="text-body-2 d-block mb-2">{{ t('theme.transparencyGlassQuality') }}</span>
|
||||||
|
<VBtnToggle
|
||||||
|
v-model="transparencyGlassQuality"
|
||||||
|
mandatory
|
||||||
|
divided
|
||||||
|
density="comfortable"
|
||||||
|
variant="outlined"
|
||||||
|
color="primary"
|
||||||
|
class="w-full"
|
||||||
|
@update:model-value="onGlassQualityChange"
|
||||||
|
>
|
||||||
|
<VBtn value="lightweight" class="flex-1">
|
||||||
|
{{ t('theme.transparencyGlassQualityLightweight') }}
|
||||||
|
</VBtn>
|
||||||
|
<VBtn value="realtime" class="flex-1">
|
||||||
|
{{ t('theme.transparencyGlassQualityRealtime') }}
|
||||||
|
</VBtn>
|
||||||
|
</VBtnToggle>
|
||||||
|
<p class="text-caption text-medium-emphasis mt-2 mb-0">
|
||||||
|
{{
|
||||||
|
transparencyGlassQuality === 'realtime'
|
||||||
|
? t('theme.transparencyGlassQualityRealtimeHint')
|
||||||
|
: t('theme.transparencyGlassQualityLightweightHint')
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span class="text-body-2 d-block mb-2">{{ t('common.preset') }}</span>
|
<span class="text-body-2 d-block mb-2">{{ t('common.preset') }}</span>
|
||||||
<VBtnGroup density="compact" variant="outlined" class="w-full">
|
<VBtnGroup density="compact" variant="outlined" class="w-full">
|
||||||
|
|||||||
@@ -1,24 +1,36 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
export type TransparencyGlassQuality = 'lightweight' | 'realtime'
|
||||||
|
|
||||||
export interface TransparencySettings {
|
export interface TransparencySettings {
|
||||||
backgroundBlur: number
|
backgroundBlur: number
|
||||||
backgroundPosterOpacity: number
|
backgroundPosterOpacity: number
|
||||||
blur: number
|
blur: number
|
||||||
|
glassQuality: TransparencyGlassQuality
|
||||||
level: string
|
level: string
|
||||||
opacity: number
|
opacity: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const transparencyPresets = {
|
export const transparencyPresets = {
|
||||||
low: { opacity: 0.1, blur: 5 },
|
low: { opacity: 0.6, blur: 5 },
|
||||||
medium: { opacity: 0.3, blur: 10 },
|
medium: { opacity: 0.3, blur: 10 },
|
||||||
high: { opacity: 0.6, blur: 15 },
|
high: { opacity: 0.1, blur: 15 },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const TRANSPARENCY_SETTINGS_CHANGED_EVENT = 'transparency-settings-changed'
|
||||||
|
|
||||||
/** 将数值限制在指定范围内。 */
|
/** 将数值限制在指定范围内。 */
|
||||||
function clamp(value: number, min: number, max: number) {
|
function clamp(value: number, min: number, max: number) {
|
||||||
return Math.min(max, Math.max(min, value))
|
return Math.min(max, Math.max(min, value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 读取玻璃质量档位,未知值回落到默认的轻量玻璃路径。 */
|
||||||
|
function readGlassQuality(): TransparencyGlassQuality {
|
||||||
|
const storedQuality = localStorage.getItem('transparency-glass-quality')
|
||||||
|
|
||||||
|
return storedQuality === 'realtime' ? 'realtime' : 'lightweight'
|
||||||
|
}
|
||||||
|
|
||||||
/** 从本地存储读取透明主题设置。 */
|
/** 从本地存储读取透明主题设置。 */
|
||||||
export function readTransparencySettings(): TransparencySettings {
|
export function readTransparencySettings(): TransparencySettings {
|
||||||
return {
|
return {
|
||||||
@@ -26,6 +38,7 @@ export function readTransparencySettings(): TransparencySettings {
|
|||||||
blur: parseFloat(localStorage.getItem('transparency-blur') || '10'),
|
blur: parseFloat(localStorage.getItem('transparency-blur') || '10'),
|
||||||
backgroundPosterOpacity: parseFloat(localStorage.getItem('transparency-background-poster-opacity') || '0'),
|
backgroundPosterOpacity: parseFloat(localStorage.getItem('transparency-background-poster-opacity') || '0'),
|
||||||
backgroundBlur: parseFloat(localStorage.getItem('transparency-background-blur') || '16'),
|
backgroundBlur: parseFloat(localStorage.getItem('transparency-background-blur') || '16'),
|
||||||
|
glassQuality: readGlassQuality(),
|
||||||
level: localStorage.getItem('transparency-level') || 'medium',
|
level: localStorage.getItem('transparency-level') || 'medium',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,6 +52,7 @@ export function applyTransparencySettings(settings: TransparencySettings) {
|
|||||||
? clamp(settings.backgroundPosterOpacity, 0, 1)
|
? clamp(settings.backgroundPosterOpacity, 0, 1)
|
||||||
: 0,
|
: 0,
|
||||||
backgroundBlur: Number.isFinite(settings.backgroundBlur) ? clamp(settings.backgroundBlur, 0, 30) : 16,
|
backgroundBlur: Number.isFinite(settings.backgroundBlur) ? clamp(settings.backgroundBlur, 0, 30) : 16,
|
||||||
|
glassQuality: settings.glassQuality === 'realtime' ? 'realtime' : 'lightweight',
|
||||||
level: settings.level,
|
level: settings.level,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,13 +65,20 @@ export function applyTransparencySettings(settings: TransparencySettings) {
|
|||||||
root.style.setProperty('--transparent-blur-heavy', `${normalized.blur * 1.6}px`)
|
root.style.setProperty('--transparent-blur-heavy', `${normalized.blur * 1.6}px`)
|
||||||
root.style.setProperty('--transparent-background-poster-opacity', (1 - normalized.backgroundPosterOpacity).toString())
|
root.style.setProperty('--transparent-background-poster-opacity', (1 - normalized.backgroundPosterOpacity).toString())
|
||||||
root.style.setProperty('--transparent-background-blur', `${normalized.backgroundBlur}px`)
|
root.style.setProperty('--transparent-background-blur', `${normalized.backgroundBlur}px`)
|
||||||
|
root.classList.toggle('transparent-blur-disabled', normalized.blur <= 0)
|
||||||
|
root.classList.toggle('transparent-background-blur-disabled', normalized.backgroundBlur <= 0)
|
||||||
|
root.classList.toggle('transparent-glass-lightweight', normalized.glassQuality === 'lightweight')
|
||||||
|
root.classList.toggle('transparent-glass-realtime', normalized.glassQuality === 'realtime')
|
||||||
|
|
||||||
localStorage.setItem('transparency-opacity', normalized.opacity.toString())
|
localStorage.setItem('transparency-opacity', normalized.opacity.toString())
|
||||||
localStorage.setItem('transparency-blur', normalized.blur.toString())
|
localStorage.setItem('transparency-blur', normalized.blur.toString())
|
||||||
localStorage.setItem('transparency-background-poster-opacity', normalized.backgroundPosterOpacity.toString())
|
localStorage.setItem('transparency-background-poster-opacity', normalized.backgroundPosterOpacity.toString())
|
||||||
localStorage.setItem('transparency-background-blur', normalized.backgroundBlur.toString())
|
localStorage.setItem('transparency-background-blur', normalized.backgroundBlur.toString())
|
||||||
|
localStorage.setItem('transparency-glass-quality', normalized.glassQuality)
|
||||||
localStorage.setItem('transparency-level', normalized.level)
|
localStorage.setItem('transparency-level', normalized.level)
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent<TransparencySettings>(TRANSPARENCY_SETTINGS_CHANGED_EVENT, { detail: normalized }))
|
||||||
|
|
||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +94,7 @@ export function useTransparencySettings() {
|
|||||||
const transparencyBlur = ref(storedSettings.blur)
|
const transparencyBlur = ref(storedSettings.blur)
|
||||||
const backgroundPosterOpacity = ref(storedSettings.backgroundPosterOpacity)
|
const backgroundPosterOpacity = ref(storedSettings.backgroundPosterOpacity)
|
||||||
const backgroundBlur = ref(storedSettings.backgroundBlur)
|
const backgroundBlur = ref(storedSettings.backgroundBlur)
|
||||||
|
const transparencyGlassQuality = ref<TransparencyGlassQuality>(storedSettings.glassQuality)
|
||||||
const transparencyLevel = ref(storedSettings.level)
|
const transparencyLevel = ref(storedSettings.level)
|
||||||
|
|
||||||
const currentPresetLevel = computed(() => {
|
const currentPresetLevel = computed(() => {
|
||||||
@@ -95,6 +117,7 @@ export function useTransparencySettings() {
|
|||||||
blur: transparencyBlur.value,
|
blur: transparencyBlur.value,
|
||||||
backgroundPosterOpacity: backgroundPosterOpacity.value,
|
backgroundPosterOpacity: backgroundPosterOpacity.value,
|
||||||
backgroundBlur: backgroundBlur.value,
|
backgroundBlur: backgroundBlur.value,
|
||||||
|
glassQuality: transparencyGlassQuality.value,
|
||||||
level: transparencyLevel.value,
|
level: transparencyLevel.value,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -102,6 +125,7 @@ export function useTransparencySettings() {
|
|||||||
transparencyBlur.value = normalized.blur
|
transparencyBlur.value = normalized.blur
|
||||||
backgroundPosterOpacity.value = normalized.backgroundPosterOpacity
|
backgroundPosterOpacity.value = normalized.backgroundPosterOpacity
|
||||||
backgroundBlur.value = normalized.backgroundBlur
|
backgroundBlur.value = normalized.backgroundBlur
|
||||||
|
transparencyGlassQuality.value = normalized.glassQuality
|
||||||
transparencyLevel.value = normalized.level
|
transparencyLevel.value = normalized.level
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,12 +173,18 @@ export function useTransparencySettings() {
|
|||||||
syncTransparencySettings()
|
syncTransparencySettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 处理玻璃质量档位变化。 */
|
||||||
|
function onGlassQualityChange() {
|
||||||
|
syncTransparencySettings()
|
||||||
|
}
|
||||||
|
|
||||||
/** 重置透明主题设置为默认值。 */
|
/** 重置透明主题设置为默认值。 */
|
||||||
function resetTransparencySettings() {
|
function resetTransparencySettings() {
|
||||||
transparencyOpacity.value = transparencyPresets.medium.opacity
|
transparencyOpacity.value = transparencyPresets.medium.opacity
|
||||||
transparencyBlur.value = transparencyPresets.medium.blur
|
transparencyBlur.value = transparencyPresets.medium.blur
|
||||||
backgroundPosterOpacity.value = 0
|
backgroundPosterOpacity.value = 0
|
||||||
backgroundBlur.value = 16
|
backgroundBlur.value = 16
|
||||||
|
transparencyGlassQuality.value = 'lightweight'
|
||||||
transparencyLevel.value = 'medium'
|
transparencyLevel.value = 'medium'
|
||||||
syncTransparencySettings()
|
syncTransparencySettings()
|
||||||
}
|
}
|
||||||
@@ -167,10 +197,12 @@ export function useTransparencySettings() {
|
|||||||
onBackgroundBlurChange,
|
onBackgroundBlurChange,
|
||||||
onBackgroundPosterOpacityChange,
|
onBackgroundPosterOpacityChange,
|
||||||
onBlurChange,
|
onBlurChange,
|
||||||
|
onGlassQualityChange,
|
||||||
onOpacityChange,
|
onOpacityChange,
|
||||||
resetTransparencySettings,
|
resetTransparencySettings,
|
||||||
syncTransparencySettings,
|
syncTransparencySettings,
|
||||||
transparencyBlur,
|
transparencyBlur,
|
||||||
|
transparencyGlassQuality,
|
||||||
transparencyOpacity,
|
transparencyOpacity,
|
||||||
transparencyLevel,
|
transparencyLevel,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,13 @@ export default {
|
|||||||
transparencyBlur: 'Blur',
|
transparencyBlur: 'Blur',
|
||||||
backgroundPosterOpacity: 'Background Opacity',
|
backgroundPosterOpacity: 'Background Opacity',
|
||||||
backgroundBlur: 'Background Frosted Blur',
|
backgroundBlur: 'Background Frosted Blur',
|
||||||
|
transparencyGlassQuality: 'Glass Effect Quality',
|
||||||
|
transparencyGlassQualityLightweight: 'Lightweight',
|
||||||
|
transparencyGlassQualityRealtime: 'Realtime',
|
||||||
|
transparencyGlassQualityLightweightHint:
|
||||||
|
'Keeps a similar translucent glass look while reducing live background sampling on dense pages.',
|
||||||
|
transparencyGlassQualityRealtimeHint:
|
||||||
|
'Uses the full live glass rendering path for higher visual quality, but may increase GPU usage.',
|
||||||
transparencyReset: 'Reset',
|
transparencyReset: 'Reset',
|
||||||
transparencyLow: 'Low Transparency',
|
transparencyLow: 'Low Transparency',
|
||||||
transparencyMedium: 'Medium Transparency',
|
transparencyMedium: 'Medium Transparency',
|
||||||
|
|||||||
@@ -152,6 +152,11 @@ export default {
|
|||||||
transparencyBlur: '模糊度',
|
transparencyBlur: '模糊度',
|
||||||
backgroundPosterOpacity: '背景透明度',
|
backgroundPosterOpacity: '背景透明度',
|
||||||
backgroundBlur: '背景磨砂效果',
|
backgroundBlur: '背景磨砂效果',
|
||||||
|
transparencyGlassQuality: '玻璃效果质量',
|
||||||
|
transparencyGlassQualityLightweight: '轻量',
|
||||||
|
transparencyGlassQualityRealtime: '实时',
|
||||||
|
transparencyGlassQualityLightweightHint: '保持接近实时毛玻璃的透明观感,减少密集页面的背景实时采样。',
|
||||||
|
transparencyGlassQualityRealtimeHint: '使用完整实时毛玻璃路径,视觉质量更高,但可能增加 GPU 占用。',
|
||||||
transparencyReset: '重置',
|
transparencyReset: '重置',
|
||||||
transparencyLow: '低透明度',
|
transparencyLow: '低透明度',
|
||||||
transparencyMedium: '中等透明度',
|
transparencyMedium: '中等透明度',
|
||||||
|
|||||||
@@ -152,6 +152,11 @@ export default {
|
|||||||
transparencyBlur: '模糊度',
|
transparencyBlur: '模糊度',
|
||||||
backgroundPosterOpacity: '背景透明度',
|
backgroundPosterOpacity: '背景透明度',
|
||||||
backgroundBlur: '背景磨砂效果',
|
backgroundBlur: '背景磨砂效果',
|
||||||
|
transparencyGlassQuality: '玻璃效果品質',
|
||||||
|
transparencyGlassQualityLightweight: '輕量',
|
||||||
|
transparencyGlassQualityRealtime: '即時',
|
||||||
|
transparencyGlassQualityLightweightHint: '保持接近即時毛玻璃的透明觀感,減少密集頁面的背景即時取樣。',
|
||||||
|
transparencyGlassQualityRealtimeHint: '使用完整即時毛玻璃路徑,視覺品質更高,但可能增加 GPU 佔用。',
|
||||||
transparencyReset: '重置',
|
transparencyReset: '重置',
|
||||||
transparencyLow: '低透明度',
|
transparencyLow: '低透明度',
|
||||||
transparencyMedium: '中等透明度',
|
transparencyMedium: '中等透明度',
|
||||||
|
|||||||
@@ -348,6 +348,11 @@ onActivated(async () => {
|
|||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-group :deep(.slider-content-wrapper) {
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-block-size: 16rem;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-category {
|
.empty-category {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -707,6 +707,13 @@ html[data-theme="transparent"] .app-card-colorful,
|
|||||||
--app-surface-border: 0 solid transparent;
|
--app-surface-border: 0 solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-theme="transparent"].transparent-glass-lightweight .app-card-colorful,
|
||||||
|
html[data-theme="transparent"].transparent-glass-lightweight .v-theme--transparent .app-card-colorful,
|
||||||
|
html[data-theme="transparent"].transparent-glass-realtime .app-card-colorful,
|
||||||
|
html[data-theme="transparent"].transparent-glass-realtime .v-theme--transparent .app-card-colorful {
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
// 保证卡片右上角的浮动操作区始终高于可点击的卡片内容层,避免误触发详情打开。
|
// 保证卡片右上角的浮动操作区始终高于可点击的卡片内容层,避免误触发详情打开。
|
||||||
.app-card-top-action {
|
.app-card-top-action {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
@@ -165,3 +165,122 @@ html[data-theme="transparent"] {
|
|||||||
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy));
|
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html[data-theme="transparent"].transparent-background-blur-disabled {
|
||||||
|
.global-blur-layer,
|
||||||
|
.background-image.active {
|
||||||
|
backdrop-filter: none;
|
||||||
|
filter: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="transparent"].transparent-glass-lightweight {
|
||||||
|
.global-blur-layer {
|
||||||
|
display: none !important;
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-vertical-nav,
|
||||||
|
.v-toolbar,
|
||||||
|
.v-footer,
|
||||||
|
.theme-customizer-panel-host,
|
||||||
|
.theme-customizer-panel,
|
||||||
|
.agent-assistant-panel,
|
||||||
|
.agent-assistant-fab__bubble,
|
||||||
|
.nav-button,
|
||||||
|
.layout-navbar {
|
||||||
|
backdrop-filter: blur(var(--transparent-blur));
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-vertical-nav,
|
||||||
|
.theme-customizer-panel-host,
|
||||||
|
.agent-assistant-panel {
|
||||||
|
backdrop-filter: blur(var(--transparent-blur-heavy));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list,
|
||||||
|
.v-card:not(.no-blur, .bg-primary),
|
||||||
|
.v-sheet,
|
||||||
|
.v-expansion-panel {
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card:not(.no-blur, .bg-primary),
|
||||||
|
.v-sheet,
|
||||||
|
.v-expansion-panel,
|
||||||
|
.v-list {
|
||||||
|
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-overlay__content,
|
||||||
|
.v-overlay__content .v-card:not(.bg-primary),
|
||||||
|
.v-overlay__content .v-list {
|
||||||
|
backdrop-filter: blur(var(--transparent-blur));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-overlay__content .v-card:not(.bg-primary),
|
||||||
|
.v-overlay__content .v-list {
|
||||||
|
background-color: rgba(var(--v-theme-surface), var(--transparent-opacity-heavy));
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-card-colorful {
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="transparent"].transparent-glass-realtime {
|
||||||
|
.background-image.active {
|
||||||
|
filter: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list,
|
||||||
|
.v-card:not(.no-blur, .bg-primary),
|
||||||
|
.v-sheet,
|
||||||
|
.v-expansion-panel {
|
||||||
|
backdrop-filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-vertical-nav,
|
||||||
|
.v-toolbar,
|
||||||
|
.v-footer,
|
||||||
|
.theme-customizer-panel-host,
|
||||||
|
.theme-customizer-panel,
|
||||||
|
.agent-assistant-panel,
|
||||||
|
.agent-assistant-fab__bubble,
|
||||||
|
.nav-button,
|
||||||
|
.layout-navbar,
|
||||||
|
.v-overlay__content,
|
||||||
|
.v-overlay__content .v-card:not(.bg-primary),
|
||||||
|
.v-overlay__content .v-list {
|
||||||
|
backdrop-filter: blur(var(--transparent-blur));
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-vertical-nav,
|
||||||
|
.theme-customizer-panel-host,
|
||||||
|
.agent-assistant-panel {
|
||||||
|
backdrop-filter: blur(var(--transparent-blur-heavy));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html[data-theme="transparent"].transparent-blur-disabled {
|
||||||
|
.layout-vertical-nav,
|
||||||
|
.v-list,
|
||||||
|
.v-card:not(.no-blur),
|
||||||
|
.v-toolbar,
|
||||||
|
.v-footer,
|
||||||
|
.v-sheet,
|
||||||
|
.v-expansion-panel,
|
||||||
|
.v-overlay__content,
|
||||||
|
.v-overlay__content .v-card:not(.bg-primary),
|
||||||
|
.v-overlay__content .v-list,
|
||||||
|
.theme-customizer-panel-host,
|
||||||
|
.theme-customizer-panel,
|
||||||
|
.agent-assistant-panel,
|
||||||
|
.agent-assistant-fab__bubble,
|
||||||
|
.nav-button,
|
||||||
|
.layout-navbar {
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ provide('rankingPropsKey', reactive({ ...props }))
|
|||||||
const componentLoaded = ref(false)
|
const componentLoaded = ref(false)
|
||||||
// 是否已尝试加载
|
// 是否已尝试加载
|
||||||
const hasTriedLoading = ref(false)
|
const hasTriedLoading = ref(false)
|
||||||
|
const loadingStarted = ref(false)
|
||||||
|
|
||||||
// 使用 shallowRef 避免横向卡片区的大数组深层代理
|
// 使用 shallowRef 避免横向卡片区的大数组深层代理
|
||||||
const dataList = shallowRef<MediaInfo[]>([])
|
const dataList = shallowRef<MediaInfo[]>([])
|
||||||
@@ -36,6 +37,8 @@ const containerRef = ref<HTMLElement | null>(null)
|
|||||||
// 获取订阅列表数据
|
// 获取订阅列表数据
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
try {
|
try {
|
||||||
|
if (loadingStarted.value) return
|
||||||
|
loadingStarted.value = true
|
||||||
if (!props.apipath) return
|
if (!props.apipath) return
|
||||||
dataList.value = await api.get(props.apipath)
|
dataList.value = await api.get(props.apipath)
|
||||||
if (dataList.value.length > 0) {
|
if (dataList.value.length > 0) {
|
||||||
@@ -47,10 +50,29 @@ async function fetchData() {
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
componentLoaded.value = true
|
componentLoaded.value = true
|
||||||
} finally {
|
} finally {
|
||||||
|
loadingStarted.value = false
|
||||||
hasTriedLoading.value = true
|
hasTriedLoading.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isNearViewport(element: HTMLElement, rootMargin = 300) {
|
||||||
|
const rect = element.getBoundingClientRect()
|
||||||
|
const viewportHeight = window.innerHeight || document.documentElement.clientHeight
|
||||||
|
|
||||||
|
return rect.bottom >= -rootMargin && rect.top <= viewportHeight + rootMargin
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IntersectionObserver 未及时回调时,首屏附近内容仍需主动发起数据请求。 */
|
||||||
|
function loadIfNearViewport() {
|
||||||
|
const element = containerRef.value
|
||||||
|
if (!element || loadingStarted.value || hasTriedLoading.value) return
|
||||||
|
|
||||||
|
if (isNearViewport(element)) {
|
||||||
|
fetchData()
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 使用 IntersectionObserver 实现懒加载
|
// 使用 IntersectionObserver 实现懒加载
|
||||||
const { stop } = useIntersectionObserver(
|
const { stop } = useIntersectionObserver(
|
||||||
containerRef,
|
containerRef,
|
||||||
@@ -65,7 +87,13 @@ const { stop } = useIntersectionObserver(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
requestAnimationFrame(loadIfNearViewport)
|
||||||
|
window.setTimeout(loadIfNearViewport, 600)
|
||||||
|
})
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
|
loadIfNearViewport()
|
||||||
if (dataList.value.length == 0 && hasTriedLoading.value) {
|
if (dataList.value.length == 0 && hasTriedLoading.value) {
|
||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user