mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
feat(glass): unify login and app optical presentation (#591)
This commit is contained in:
@@ -39,6 +39,7 @@ const draftFlowStrength = ref(settings.value.glassFlowStrength)
|
||||
const draftPreset = ref<GlassOpticalPreset>(settings.value.glassPreset)
|
||||
const draftQuality = ref<ThemeCustomizerGlassQuality>(settings.value.glassQuality)
|
||||
const draftReflectionStrength = ref(settings.value.glassReflectionStrength)
|
||||
const draftTransmissionStrength = ref(settings.value.glassTransmissionStrength)
|
||||
const draftTranslationStrength = ref(settings.value.glassTranslationStrength)
|
||||
const draftTransparencyStrength = ref(settings.value.glassTransparencyStrength)
|
||||
const isSaving = ref(false)
|
||||
@@ -69,6 +70,7 @@ watch(
|
||||
draftPreset.value = settings.value.glassPreset
|
||||
draftQuality.value = settings.value.glassQuality
|
||||
draftReflectionStrength.value = settings.value.glassReflectionStrength
|
||||
draftTransmissionStrength.value = settings.value.glassTransmissionStrength
|
||||
draftTranslationStrength.value = settings.value.glassTranslationStrength
|
||||
draftTransparencyStrength.value = settings.value.glassTransparencyStrength
|
||||
} else if (previous) {
|
||||
@@ -122,13 +124,14 @@ function updateQuality(value: unknown) {
|
||||
previewGlassSettings({ glassQuality: option.value })
|
||||
}
|
||||
|
||||
/** 将五个具体参数作为一个预览事务同步,预置只负责生成这些值。 */
|
||||
/** 将六个具体参数作为一个预览事务同步,预置只负责生成这些值。 */
|
||||
function previewDraftParameters() {
|
||||
previewGlassSettings({
|
||||
glassDeformationStrength: draftDeformationStrength.value,
|
||||
glassFlowStrength: draftFlowStrength.value,
|
||||
glassPreset: draftPreset.value,
|
||||
glassReflectionStrength: draftReflectionStrength.value,
|
||||
glassTransmissionStrength: draftTransmissionStrength.value,
|
||||
glassTranslationStrength: draftTranslationStrength.value,
|
||||
glassTransparencyStrength: draftTransparencyStrength.value,
|
||||
})
|
||||
@@ -144,6 +147,7 @@ function applyPreset(value: unknown) {
|
||||
draftDeformationStrength.value = parameters.deformation
|
||||
draftFlowStrength.value = parameters.flow
|
||||
draftReflectionStrength.value = parameters.reflection
|
||||
draftTransmissionStrength.value = parameters.transmission
|
||||
draftTranslationStrength.value = parameters.translation
|
||||
draftTransparencyStrength.value = parameters.transparency
|
||||
previewDraftParameters()
|
||||
@@ -173,6 +177,12 @@ function updateReflectionStrength(value: unknown) {
|
||||
previewGlassSettings({ glassReflectionStrength: draftReflectionStrength.value })
|
||||
}
|
||||
|
||||
/** 将透射亮度限制为稳定范围并即时调整卡片内部壁纸的明暗。 */
|
||||
function updateTransmissionStrength(value: unknown) {
|
||||
draftTransmissionStrength.value = normalizeGlassOpticalStrength(Array.isArray(value) ? value[0] : value)
|
||||
previewGlassSettings({ glassTransmissionStrength: draftTransmissionStrength.value })
|
||||
}
|
||||
|
||||
/** 将通透度限制为稳定范围并即时调整材质与真实壁纸的占比。 */
|
||||
function updateTransparencyStrength(value: unknown) {
|
||||
draftTransparencyStrength.value = normalizeGlassOpticalStrength(Array.isArray(value) ? value[0] : value)
|
||||
@@ -198,6 +208,7 @@ async function saveSettings() {
|
||||
glassPreset: draftPreset.value,
|
||||
glassQuality: draftQuality.value,
|
||||
glassReflectionStrength: draftReflectionStrength.value,
|
||||
glassTransmissionStrength: draftTransmissionStrength.value,
|
||||
glassTranslationStrength: draftTranslationStrength.value,
|
||||
glassTransparencyStrength: draftTransparencyStrength.value,
|
||||
})
|
||||
@@ -292,6 +303,7 @@ onScopeDispose(cancelGlassPreview)
|
||||
</section>
|
||||
|
||||
<section class="glass-settings-dialog__tuning">
|
||||
<h3 class="glass-settings-dialog__group-label">{{ t('theme.glassMaterialTuning') }}</h3>
|
||||
<div class="glass-settings-dialog__slider-header">
|
||||
<h3 class="glass-settings-dialog__label">{{ t('theme.glassTransparencyStrength') }}</h3>
|
||||
<output>{{ draftTransparencyStrength }}%</output>
|
||||
@@ -309,6 +321,23 @@ onScopeDispose(cancelGlassPreview)
|
||||
@update:model-value="updateTransparencyStrength"
|
||||
/>
|
||||
|
||||
<div class="glass-settings-dialog__slider-header glass-settings-dialog__slider-header--spaced">
|
||||
<h3 class="glass-settings-dialog__label">{{ t('theme.glassTransmissionStrength') }}</h3>
|
||||
<output>{{ draftTransmissionStrength }}%</output>
|
||||
</div>
|
||||
<VSlider
|
||||
:model-value="draftTransmissionStrength"
|
||||
:aria-label="t('theme.glassTransmissionStrength')"
|
||||
:min="GLASS_OPTICAL_STRENGTH_MIN"
|
||||
:max="GLASS_OPTICAL_STRENGTH_MAX"
|
||||
:step="1"
|
||||
color="primary"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
thumb-label
|
||||
@update:model-value="updateTransmissionStrength"
|
||||
/>
|
||||
|
||||
<div class="glass-settings-dialog__slider-header glass-settings-dialog__slider-header--spaced">
|
||||
<h3 class="glass-settings-dialog__label">{{ t('theme.glassReflectionStrength') }}</h3>
|
||||
<output>{{ draftReflectionStrength }}%</output>
|
||||
@@ -327,6 +356,7 @@ onScopeDispose(cancelGlassPreview)
|
||||
/>
|
||||
|
||||
<div v-if="showsDynamicTuning" class="glass-settings-dialog__live-controls">
|
||||
<h3 class="glass-settings-dialog__group-label">{{ t('theme.glassDynamicTuning') }}</h3>
|
||||
<div class="glass-settings-dialog__slider-header">
|
||||
<h3 class="glass-settings-dialog__label">{{ t('theme.glassTranslationStrength') }}</h3>
|
||||
<output>{{ draftTranslationStrength }}%</output>
|
||||
@@ -421,6 +451,14 @@ onScopeDispose(cancelGlassPreview)
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.glass-settings-dialog__group-label {
|
||||
margin: 0 0 14px;
|
||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.glass-settings-dialog__slider-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -444,7 +482,9 @@ onScopeDispose(cancelGlassPreview)
|
||||
}
|
||||
|
||||
.glass-settings-dialog__live-controls {
|
||||
margin-block-start: 18px;
|
||||
border-block-start: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||
margin-block-start: 24px;
|
||||
padding-block-start: 20px;
|
||||
|
||||
:deep(.v-slider) {
|
||||
margin-block-start: 4px;
|
||||
|
||||
@@ -27,6 +27,7 @@ const mocks = vi.hoisted(() => ({
|
||||
glassPreset: 'natural',
|
||||
glassQuality: 'css',
|
||||
glassReflectionStrength: 50,
|
||||
glassTransmissionStrength: 50,
|
||||
glassTranslationStrength: 50,
|
||||
glassTransparencyStrength: 50,
|
||||
},
|
||||
@@ -57,6 +58,7 @@ describe('GlassSettingsDialog', () => {
|
||||
mocks.settings.value.glassPreset = 'natural'
|
||||
mocks.settings.value.glassQuality = 'css'
|
||||
mocks.settings.value.glassReflectionStrength = 50
|
||||
mocks.settings.value.glassTransmissionStrength = 50
|
||||
mocks.settings.value.glassTranslationStrength = 50
|
||||
mocks.settings.value.glassTransparencyStrength = 50
|
||||
})
|
||||
@@ -76,7 +78,7 @@ describe('GlassSettingsDialog', () => {
|
||||
})
|
||||
const sliders = wrapper.findAll('.slider-stub')
|
||||
|
||||
expect(sliders).toHaveLength(2)
|
||||
expect(sliders).toHaveLength(3)
|
||||
expect(sliders[0].attributes('data-disabled')).toBe('undefined')
|
||||
expect(sliders[1].attributes('data-disabled')).toBe('undefined')
|
||||
await wrapper.setProps({ modelValue: false })
|
||||
@@ -91,6 +93,7 @@ describe('GlassSettingsDialog', () => {
|
||||
mocks.settings.value.glassDeformationStrength = 65
|
||||
mocks.settings.value.glassFlowStrength = 61
|
||||
mocks.settings.value.glassReflectionStrength = 58
|
||||
mocks.settings.value.glassTransmissionStrength = 57
|
||||
mocks.settings.value.glassTranslationStrength = 57
|
||||
mocks.settings.value.glassTransparencyStrength = 55
|
||||
const wrapper = shallowMount(GlassSettingsDialog, {
|
||||
@@ -119,7 +122,8 @@ describe('GlassSettingsDialog', () => {
|
||||
glassDeformationStrength: 82,
|
||||
glassFlowStrength: 80,
|
||||
glassPreset: 'liquid',
|
||||
glassReflectionStrength: 44,
|
||||
glassReflectionStrength: 31,
|
||||
glassTransmissionStrength: 58,
|
||||
glassTranslationStrength: 54,
|
||||
glassTransparencyStrength: 46,
|
||||
})
|
||||
@@ -141,9 +145,9 @@ describe('GlassSettingsDialog', () => {
|
||||
})
|
||||
const sliders = wrapper.findAll('.slider-stub')
|
||||
|
||||
expect(sliders).toHaveLength(2)
|
||||
expect(sliders[1].attributes('data-disabled')).toBe('undefined')
|
||||
await sliders[1].setValue('86')
|
||||
expect(sliders).toHaveLength(3)
|
||||
expect(sliders[2].attributes('data-disabled')).toBe('undefined')
|
||||
await sliders[2].setValue('86')
|
||||
|
||||
expect(mocks.previewGlassSettings).toHaveBeenCalledWith({ glassReflectionStrength: 86 })
|
||||
})
|
||||
@@ -197,7 +201,7 @@ describe('GlassSettingsDialog', () => {
|
||||
expect(preset.attributes('data-model-value')).toBe('glide')
|
||||
})
|
||||
|
||||
it('normalizes and previews all five independent slider values', async () => {
|
||||
it('normalizes and previews all six independent slider values', async () => {
|
||||
mocks.settings.value.glassQuality = 'balanced'
|
||||
const wrapper = shallowMount(GlassSettingsDialog, {
|
||||
global: {
|
||||
@@ -213,20 +217,22 @@ describe('GlassSettingsDialog', () => {
|
||||
})
|
||||
const sliders = wrapper.findAll('.slider-stub')
|
||||
|
||||
expect(sliders).toHaveLength(5)
|
||||
expect(sliders).toHaveLength(6)
|
||||
expect(sliders.every(slider => slider.attributes('data-disabled') !== 'true')).toBe(true)
|
||||
|
||||
await sliders[0].setValue('91.4')
|
||||
await sliders[1].setValue('73.6')
|
||||
await sliders[2].setValue('84.2')
|
||||
await sliders[3].setValue('68.7')
|
||||
await sliders[4].setValue('62.2')
|
||||
await sliders[1].setValue('77.5')
|
||||
await sliders[2].setValue('73.6')
|
||||
await sliders[3].setValue('84.2')
|
||||
await sliders[4].setValue('68.7')
|
||||
await sliders[5].setValue('62.2')
|
||||
|
||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(1, { glassTransparencyStrength: 91 })
|
||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(2, { glassReflectionStrength: 74 })
|
||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(3, { glassTranslationStrength: 84 })
|
||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(4, { glassDeformationStrength: 69 })
|
||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(5, { glassFlowStrength: 62 })
|
||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(2, { glassTransmissionStrength: 78 })
|
||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(3, { glassReflectionStrength: 74 })
|
||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(4, { glassTranslationStrength: 84 })
|
||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(5, { glassDeformationStrength: 69 })
|
||||
expect(mocks.previewGlassSettings).toHaveBeenNthCalledWith(6, { glassFlowStrength: 62 })
|
||||
expect(mocks.commitGlassPreview).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -246,9 +252,10 @@ describe('GlassSettingsDialog', () => {
|
||||
})
|
||||
const sliders = wrapper.findAll('.slider-stub')
|
||||
|
||||
expect(sliders).toHaveLength(5)
|
||||
expect(sliders).toHaveLength(6)
|
||||
expect(sliders.map(slider => slider.attributes('aria-label'))).toEqual([
|
||||
'theme.glassTransparencyStrength',
|
||||
'theme.glassTransmissionStrength',
|
||||
'theme.glassReflectionStrength',
|
||||
'theme.glassTranslationStrength',
|
||||
'theme.glassDeformationStrength',
|
||||
|
||||
@@ -20,6 +20,8 @@ const props = defineProps<{
|
||||
reflectionStrength: number
|
||||
/** 用户选择的真实壁纸可见度。 */
|
||||
transparencyStrength: number
|
||||
/** 玻璃内部壁纸采样的透射亮度;不会改变外层壁纸的曝光合同。 */
|
||||
transmissionStrength: number
|
||||
/** 用户选择的共享壁纸采样平移强度。 */
|
||||
translationStrength: number
|
||||
/** 路由变化标识,用于在页面内容稳定后重新发现高价值表面。 */
|
||||
@@ -34,6 +36,12 @@ const props = defineProps<{
|
||||
wallpaperUrl: string
|
||||
/** 切换期保留的上一张壁纸;空值表示当前没有交叉淡化。 */
|
||||
previousWallpaperUrl: string
|
||||
/** 下一张同源壁纸;两个 context 均完成上传后才允许外层提交切换。 */
|
||||
pendingWallpaperUrl?: string
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
/** fixed 与 scroll renderer 均已准备同一张待切换纹理。 */
|
||||
wallpaperPrepared: [url: string]
|
||||
}>()
|
||||
|
||||
const fixedCanvas = ref<HTMLCanvasElement | null>(null)
|
||||
@@ -49,6 +57,7 @@ const fixedRenderer = useGlassOpticalRenderer({
|
||||
quality: () => props.quality,
|
||||
reflectionStrength: () => props.reflectionStrength,
|
||||
transparencyStrength: () => props.transparencyStrength,
|
||||
transmissionStrength: () => props.transmissionStrength,
|
||||
translationStrength: () => props.translationStrength,
|
||||
routeKey: () => props.routeKey,
|
||||
tintColor: () => props.tintColor,
|
||||
@@ -56,6 +65,7 @@ const fixedRenderer = useGlassOpticalRenderer({
|
||||
transitionStartedAt: () => props.transitionStartedAt,
|
||||
wallpaperUrl: () => props.wallpaperUrl,
|
||||
previousWallpaperUrl: () => props.previousWallpaperUrl,
|
||||
pendingWallpaperUrl: () => props.pendingWallpaperUrl ?? '',
|
||||
surfaceSpace: 'fixed',
|
||||
syncDocumentState: false,
|
||||
})
|
||||
@@ -69,6 +79,7 @@ const scrollRenderer = useGlassOpticalRenderer({
|
||||
quality: () => props.quality,
|
||||
reflectionStrength: () => props.reflectionStrength,
|
||||
transparencyStrength: () => props.transparencyStrength,
|
||||
transmissionStrength: () => props.transmissionStrength,
|
||||
translationStrength: () => props.translationStrength,
|
||||
routeKey: () => props.routeKey,
|
||||
tintColor: () => props.tintColor,
|
||||
@@ -76,6 +87,7 @@ const scrollRenderer = useGlassOpticalRenderer({
|
||||
transitionStartedAt: () => props.transitionStartedAt,
|
||||
wallpaperUrl: () => props.wallpaperUrl,
|
||||
previousWallpaperUrl: () => props.previousWallpaperUrl,
|
||||
pendingWallpaperUrl: () => props.pendingWallpaperUrl ?? '',
|
||||
surfaceSpace: 'scroll',
|
||||
syncDocumentState: false,
|
||||
})
|
||||
@@ -94,6 +106,13 @@ watchEffect(() => {
|
||||
setGlassRendererState(rendererState, state)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
const url = props.pendingWallpaperUrl
|
||||
if (url && fixedRenderer.preparedWallpaperUrl.value === url && scrollRenderer.preparedWallpaperUrl.value === url) {
|
||||
emit('wallpaperPrepared', url)
|
||||
}
|
||||
})
|
||||
|
||||
onScopeDispose(() => {
|
||||
setGlassRendererState(rendererState, 'fallback')
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { usePWA } from '@/composables/usePWA'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useTheme } from 'vuetify'
|
||||
import { GLASS_OPTICAL_STRENGTH_DEFAULT } from '@/utils/glassOptics'
|
||||
import { GLASS_OPTICAL_STRENGTH_DEFAULT, getGlassOpticalPresetParameters } from '@/utils/glassOptics'
|
||||
|
||||
const emit = defineEmits<{
|
||||
'close': []
|
||||
@@ -38,6 +38,7 @@ const { appMode } = usePWA()
|
||||
const { t } = useI18n()
|
||||
const { global: globalTheme } = useTheme()
|
||||
const defaultPrimaryColor = themeCustomizerPrimaryColors[0].value
|
||||
const defaultGlassTransmissionStrength = getGlassOpticalPresetParameters('clear', 'balanced', 'natural').transmission
|
||||
|
||||
// 将主题定制器打开状态同步到根节点,供全局悬浮按钮避让右侧面板。
|
||||
function syncThemeCustomizerOpenState(isOpen: boolean) {
|
||||
@@ -145,6 +146,7 @@ const hasAppModeCustomization = computed(() => {
|
||||
settings.value.glassFlowStrength !== GLASS_OPTICAL_STRENGTH_DEFAULT ||
|
||||
settings.value.glassQuality !== 'css' ||
|
||||
settings.value.glassReflectionStrength !== GLASS_OPTICAL_STRENGTH_DEFAULT ||
|
||||
settings.value.glassTransmissionStrength !== defaultGlassTransmissionStrength ||
|
||||
settings.value.glassTranslationStrength !== GLASS_OPTICAL_STRENGTH_DEFAULT ||
|
||||
settings.value.glassTransparencyStrength !== GLASS_OPTICAL_STRENGTH_DEFAULT ||
|
||||
settings.value.radius !== 'default' ||
|
||||
|
||||
@@ -40,6 +40,7 @@ describe('GlassOpticalLayer', () => {
|
||||
tintColor: '#8D51F9',
|
||||
transitionDuration: 1500,
|
||||
transitionStartedAt: 0,
|
||||
transmissionStrength: 84,
|
||||
translationStrength: 50,
|
||||
transparencyStrength: 50,
|
||||
wallpaperUrl: '/wallpaper.jpg',
|
||||
|
||||
Reference in New Issue
Block a user