mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-07 00:36:41 +08:00
feat(theme): refine glass and transparency settings (#578)
This commit is contained in:
+1
-1
@@ -28,7 +28,7 @@ import { useGlobalOfflineStatus, type ConnectionFailureReason } from '@/composab
|
|||||||
|
|
||||||
const LOGIN_WALLPAPER_ROUTE = '/login'
|
const LOGIN_WALLPAPER_ROUTE = '/login'
|
||||||
const BACKGROUND_CROSSFADE_DURATION_MS = 1500
|
const BACKGROUND_CROSSFADE_DURATION_MS = 1500
|
||||||
const WINDOW_BLUR_RENDER_THROTTLE_DELAY_MS = 180_000
|
const WINDOW_BLUR_RENDER_THROTTLE_DELAY_MS = 60_000
|
||||||
const MEDIA_DENSE_OPTICAL_DEFER_MS = 1_600
|
const MEDIA_DENSE_OPTICAL_DEFER_MS = 1_600
|
||||||
|
|
||||||
// 生效主题
|
// 生效主题
|
||||||
|
|||||||
@@ -88,6 +88,16 @@ function updateQuality(value: unknown) {
|
|||||||
previewGlassSettings({ glassQuality: option.value })
|
previewGlassSettings({ glassQuality: option.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将当前草稿恢复为玻璃主题默认值并立即预览。 */
|
||||||
|
function resetSettings() {
|
||||||
|
draftAppearance.value = 'clear'
|
||||||
|
draftQuality.value = 'css'
|
||||||
|
previewGlassSettings({
|
||||||
|
glassAppearance: draftAppearance.value,
|
||||||
|
glassQuality: draftQuality.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** 一次提交当前预览,持久化后关闭不会发生视觉回跳。 */
|
/** 一次提交当前预览,持久化后关闭不会发生视觉回跳。 */
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
if (isSaving.value) return
|
if (isSaving.value) return
|
||||||
@@ -111,19 +121,16 @@ onScopeDispose(cancelGlassPreview)
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VDialog v-if="visible" v-model="visible" max-width="28rem" scrollable :fullscreen="display.xs.value">
|
<VDialog v-if="visible" v-model="visible" max-width="30rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||||
<VCard class="glass-settings-dialog">
|
<VCard>
|
||||||
<VCardItem class="glass-settings-dialog__header py-3">
|
<VCardItem>
|
||||||
<template #prepend>
|
|
||||||
<VAvatar color="primary" variant="tonal" rounded size="40" class="me-2">
|
|
||||||
<VIcon icon="mdi-blur-radial" size="22" />
|
|
||||||
</VAvatar>
|
|
||||||
</template>
|
|
||||||
<VCardTitle>
|
<VCardTitle>
|
||||||
|
<VIcon icon="mdi-blur-radial" class="me-2" />
|
||||||
{{ t('theme.glassSettings') }}
|
{{ t('theme.glassSettings') }}
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
<VDialogCloseBtn v-model="visible" />
|
<VDialogCloseBtn v-model="visible" />
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
|
||||||
<VCardText class="glass-settings-dialog__body">
|
<VCardText class="glass-settings-dialog__body">
|
||||||
<section>
|
<section>
|
||||||
@@ -169,33 +176,20 @@ onScopeDispose(cancelGlassPreview)
|
|||||||
</section>
|
</section>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
|
||||||
<VCardActions class="app-dialog-actions glass-settings-dialog__actions">
|
<VDivider />
|
||||||
<VSpacer />
|
<VCardText class="text-center">
|
||||||
<VBtn
|
<VBtn variant="outlined" prepend-icon="mdi-refresh" class="me-2" @click="resetSettings">
|
||||||
color="primary"
|
{{ t('common.reset') }}
|
||||||
variant="flat"
|
</VBtn>
|
||||||
prepend-icon="mdi-content-save"
|
<VBtn color="primary" prepend-icon="mdi-content-save" :loading="isSaving" @click="saveSettings">
|
||||||
class="px-5"
|
|
||||||
:loading="isSaving"
|
|
||||||
@click="saveSettings"
|
|
||||||
>
|
|
||||||
{{ t('common.save') }}
|
{{ t('common.save') }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</VCardActions>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.glass-settings-dialog {
|
|
||||||
overflow: hidden;
|
|
||||||
max-block-size: calc(100dvh - 2rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass-settings-dialog__header {
|
|
||||||
border-block-end: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass-settings-dialog__body {
|
.glass-settings-dialog__body {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
@@ -260,25 +254,4 @@ onScopeDispose(cancelGlassPreview)
|
|||||||
background-color: rgba(var(--v-theme-primary), 0.14) !important;
|
background-color: rgba(var(--v-theme-primary), 0.14) !important;
|
||||||
box-shadow: inset 0 0 0 1px rgba(var(--v-theme-primary), 0.38) !important;
|
box-shadow: inset 0 0 0 1px rgba(var(--v-theme-primary), 0.38) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-settings-dialog__actions {
|
|
||||||
border-block-start: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
|
||||||
padding-block: 0.875rem;
|
|
||||||
padding-inline: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (width <= 600px) {
|
|
||||||
.glass-settings-dialog {
|
|
||||||
block-size: 100dvh;
|
|
||||||
max-block-size: 100dvh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass-settings-dialog__body {
|
|
||||||
padding-inline: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.glass-settings-dialog__actions {
|
|
||||||
padding-block-end: max(0.875rem, calc(env(safe-area-inset-bottom) + 0.75rem));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const emit = defineEmits<{
|
|||||||
const visible = computed({
|
const visible = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: value => {
|
set: value => {
|
||||||
|
if (!value) cancelTransparencySettings()
|
||||||
emit('update:modelValue', value)
|
emit('update:modelValue', value)
|
||||||
if (!value) emit('close')
|
if (!value) emit('close')
|
||||||
},
|
},
|
||||||
@@ -38,6 +39,7 @@ const {
|
|||||||
adjustTransparency,
|
adjustTransparency,
|
||||||
backgroundBlur,
|
backgroundBlur,
|
||||||
backgroundPosterOpacity,
|
backgroundPosterOpacity,
|
||||||
|
cancelTransparencySettings,
|
||||||
currentPresetLevel,
|
currentPresetLevel,
|
||||||
onBackgroundBlurChange,
|
onBackgroundBlurChange,
|
||||||
onBackgroundPosterOpacityChange,
|
onBackgroundPosterOpacityChange,
|
||||||
@@ -45,10 +47,28 @@ const {
|
|||||||
onGlassQualityChange,
|
onGlassQualityChange,
|
||||||
onOpacityChange,
|
onOpacityChange,
|
||||||
resetTransparencySettings,
|
resetTransparencySettings,
|
||||||
|
saveTransparencySettings,
|
||||||
transparencyBlur,
|
transparencyBlur,
|
||||||
transparencyGlassQuality,
|
transparencyGlassQuality,
|
||||||
transparencyOpacity,
|
transparencyOpacity,
|
||||||
} = useTransparencySettings()
|
} = useTransparencySettings()
|
||||||
|
|
||||||
|
/** 父级控制弹窗生命周期时,未保存关闭仍需恢复持久化设置。 */
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(value, previous) => {
|
||||||
|
if (!value && previous) cancelTransparencySettings()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 保存当前透明度预览,随后关闭面板。 */
|
||||||
|
function saveSettings() {
|
||||||
|
saveTransparencySettings()
|
||||||
|
visible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗的任意未保存销毁路径都应恢复持久化快照。
|
||||||
|
onScopeDispose(cancelTransparencySettings)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -185,14 +205,11 @@ const {
|
|||||||
</VCardText>
|
</VCardText>
|
||||||
<VDivider />
|
<VDivider />
|
||||||
<VCardText class="text-center">
|
<VCardText class="text-center">
|
||||||
<VBtn @click="resetTransparencySettings" variant="outlined" class="me-2">
|
<VBtn variant="outlined" prepend-icon="mdi-refresh" class="me-2" @click="resetTransparencySettings">
|
||||||
<template #prepend>
|
{{ t('common.reset') }}
|
||||||
<VIcon icon="mdi-refresh" />
|
|
||||||
</template>
|
|
||||||
{{ t('theme.transparencyReset') }}
|
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn @click="visible = false" color="primary">
|
<VBtn color="primary" prepend-icon="mdi-content-save" @click="saveSettings">
|
||||||
{{ t('common.confirm') }}
|
{{ t('common.save') }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</VCard>
|
</VCard>
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ import { shallowMount } from '@vue/test-utils'
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import GlassSettingsDialog from '@/components/dialog/GlassSettingsDialog.vue'
|
import GlassSettingsDialog from '@/components/dialog/GlassSettingsDialog.vue'
|
||||||
|
|
||||||
|
const slotStub = { template: '<div><slot /></div>' }
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
const mocks = vi.hoisted(() => ({
|
||||||
cancelGlassPreview: vi.fn(),
|
cancelGlassPreview: vi.fn(),
|
||||||
|
commitGlassPreview: vi.fn(),
|
||||||
|
previewGlassSettings: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/composables/useThemeCustomizer', () => ({
|
vi.mock('@/composables/useThemeCustomizer', () => ({
|
||||||
cancelGlassPreview: mocks.cancelGlassPreview,
|
cancelGlassPreview: mocks.cancelGlassPreview,
|
||||||
commitGlassPreview: vi.fn(),
|
commitGlassPreview: mocks.commitGlassPreview,
|
||||||
previewGlassSettings: vi.fn(),
|
previewGlassSettings: mocks.previewGlassSettings,
|
||||||
useThemeCustomizer: () => ({
|
useThemeCustomizer: () => ({
|
||||||
settings: {
|
settings: {
|
||||||
value: {
|
value: {
|
||||||
@@ -25,12 +29,14 @@ vi.mock('vue-i18n', () => ({
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('vuetify', () => ({
|
vi.mock('vuetify', () => ({
|
||||||
useDisplay: () => ({ xs: { value: false } }),
|
useDisplay: () => ({ mdAndUp: { value: true } }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
describe('GlassSettingsDialog', () => {
|
describe('GlassSettingsDialog', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.cancelGlassPreview.mockClear()
|
mocks.cancelGlassPreview.mockClear()
|
||||||
|
mocks.commitGlassPreview.mockClear()
|
||||||
|
mocks.previewGlassSettings.mockClear()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('cancels an active preview when the parent closes the dialog', async () => {
|
it('cancels an active preview when the parent closes the dialog', async () => {
|
||||||
@@ -45,4 +51,33 @@ describe('GlassSettingsDialog', () => {
|
|||||||
|
|
||||||
expect(mocks.cancelGlassPreview).toHaveBeenCalledOnce()
|
expect(mocks.cancelGlassPreview).toHaveBeenCalledOnce()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('resets the draft to the default glass settings without committing', async () => {
|
||||||
|
const wrapper = shallowMount(GlassSettingsDialog, {
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
VCard: slotStub,
|
||||||
|
VCardText: slotStub,
|
||||||
|
VBtn: {
|
||||||
|
emits: ['click'],
|
||||||
|
props: ['prependIcon'],
|
||||||
|
template: '<button :data-icon="prependIcon" @click="$emit(\'click\')"><slot /></button>',
|
||||||
|
},
|
||||||
|
VDialog: slotStub,
|
||||||
|
VDialogCloseBtn: true,
|
||||||
|
VDivider: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: { modelValue: true },
|
||||||
|
})
|
||||||
|
const resetButton = wrapper.find('[data-icon="mdi-refresh"]')
|
||||||
|
|
||||||
|
await resetButton.trigger('click')
|
||||||
|
|
||||||
|
expect(mocks.previewGlassSettings).toHaveBeenCalledWith({
|
||||||
|
glassAppearance: 'clear',
|
||||||
|
glassQuality: 'css',
|
||||||
|
})
|
||||||
|
expect(mocks.commitGlassPreview).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { shallowMount } from '@vue/test-utils'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import TransparencySettingsDialog from '@/components/dialog/TransparencySettingsDialog.vue'
|
||||||
|
|
||||||
|
const slotStub = { template: '<div><slot /></div>' }
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
cancelTransparencySettings: vi.fn(),
|
||||||
|
saveTransparencySettings: vi.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/composables/useTransparencySettings', () => ({
|
||||||
|
useTransparencySettings: () => ({
|
||||||
|
adjustTransparency: vi.fn(),
|
||||||
|
backgroundBlur: ref(16),
|
||||||
|
backgroundPosterOpacity: ref(0),
|
||||||
|
cancelTransparencySettings: mocks.cancelTransparencySettings,
|
||||||
|
currentPresetLevel: ref('medium'),
|
||||||
|
onBackgroundBlurChange: vi.fn(),
|
||||||
|
onBackgroundPosterOpacityChange: vi.fn(),
|
||||||
|
onBlurChange: vi.fn(),
|
||||||
|
onGlassQualityChange: vi.fn(),
|
||||||
|
onOpacityChange: vi.fn(),
|
||||||
|
resetTransparencySettings: vi.fn(),
|
||||||
|
saveTransparencySettings: mocks.saveTransparencySettings,
|
||||||
|
transparencyBlur: ref(10),
|
||||||
|
transparencyGlassQuality: ref('lightweight'),
|
||||||
|
transparencyOpacity: ref(0.3),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('vue-i18n', () => ({
|
||||||
|
useI18n: () => ({ t: (key: string) => key }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('vuetify', () => ({
|
||||||
|
useDisplay: () => ({ mdAndUp: { value: true } }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('TransparencySettingsDialog', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.cancelTransparencySettings.mockClear()
|
||||||
|
mocks.saveTransparencySettings.mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancels an active preview when the parent closes the dialog', async () => {
|
||||||
|
const wrapper = shallowMount(TransparencySettingsDialog, {
|
||||||
|
global: {
|
||||||
|
stubs: { VDialogCloseBtn: true, VSlider: true },
|
||||||
|
},
|
||||||
|
props: { modelValue: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.setProps({ modelValue: false })
|
||||||
|
|
||||||
|
expect(mocks.cancelTransparencySettings).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('persists the preview only from the save action', async () => {
|
||||||
|
const wrapper = shallowMount(TransparencySettingsDialog, {
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
VCard: slotStub,
|
||||||
|
VCardText: slotStub,
|
||||||
|
VBtn: {
|
||||||
|
emits: ['click'],
|
||||||
|
props: ['prependIcon'],
|
||||||
|
template: '<button :data-icon="prependIcon" @click="$emit(\'click\')"><slot /></button>',
|
||||||
|
},
|
||||||
|
VDialog: slotStub,
|
||||||
|
VDialogCloseBtn: true,
|
||||||
|
VDivider: true,
|
||||||
|
VSlider: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
props: { modelValue: true },
|
||||||
|
})
|
||||||
|
const saveButton = wrapper.find('[data-icon="mdi-content-save"]')
|
||||||
|
|
||||||
|
expect(mocks.saveTransparencySettings).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
await saveButton.trigger('click')
|
||||||
|
|
||||||
|
expect(mocks.saveTransparencySettings).toHaveBeenCalledOnce()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import {
|
||||||
|
applyTransparencySettings,
|
||||||
|
cancelTransparencyPreview,
|
||||||
|
commitTransparencyPreview,
|
||||||
|
previewTransparencySettings,
|
||||||
|
readTransparencySettings,
|
||||||
|
useTransparencySettings,
|
||||||
|
type TransparencySettings,
|
||||||
|
} from '@/composables/useTransparencySettings'
|
||||||
|
import { beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
const storedSettings: TransparencySettings = {
|
||||||
|
backgroundBlur: 16,
|
||||||
|
backgroundPosterOpacity: 0.2,
|
||||||
|
blur: 10,
|
||||||
|
glassQuality: 'lightweight',
|
||||||
|
level: 'medium',
|
||||||
|
opacity: 0.3,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useTransparencySettings preview transaction', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cancelTransparencyPreview()
|
||||||
|
localStorage.clear()
|
||||||
|
applyTransparencySettings(storedSettings)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('previews changes without writing them to local storage', () => {
|
||||||
|
previewTransparencySettings({
|
||||||
|
backgroundBlur: 24,
|
||||||
|
glassQuality: 'realtime',
|
||||||
|
opacity: 0.7,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(document.documentElement.style.getPropertyValue('--transparent-opacity')).toBe('0.7')
|
||||||
|
expect(document.documentElement.style.getPropertyValue('--transparent-background-blur')).toBe('24px')
|
||||||
|
expect(document.documentElement.classList.contains('transparent-glass-realtime')).toBe(true)
|
||||||
|
expect(readTransparencySettings()).toEqual(storedSettings)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('commits the latest preview as one persisted state', () => {
|
||||||
|
previewTransparencySettings({ opacity: 0.7 })
|
||||||
|
previewTransparencySettings({ backgroundBlur: 24, glassQuality: 'realtime' })
|
||||||
|
|
||||||
|
commitTransparencyPreview()
|
||||||
|
|
||||||
|
expect(readTransparencySettings()).toEqual({
|
||||||
|
...storedSettings,
|
||||||
|
backgroundBlur: 24,
|
||||||
|
glassQuality: 'realtime',
|
||||||
|
opacity: 0.7,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('restores the persisted appearance when the preview is cancelled', () => {
|
||||||
|
previewTransparencySettings({
|
||||||
|
backgroundBlur: 24,
|
||||||
|
glassQuality: 'realtime',
|
||||||
|
opacity: 0.7,
|
||||||
|
})
|
||||||
|
|
||||||
|
cancelTransparencyPreview()
|
||||||
|
|
||||||
|
expect(document.documentElement.style.getPropertyValue('--transparent-opacity')).toBe('0.3')
|
||||||
|
expect(document.documentElement.style.getPropertyValue('--transparent-background-blur')).toBe('16px')
|
||||||
|
expect(document.documentElement.classList.contains('transparent-glass-lightweight')).toBe(true)
|
||||||
|
expect(readTransparencySettings()).toEqual(storedSettings)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps reset as a preview until the dialog saves it', () => {
|
||||||
|
applyTransparencySettings({
|
||||||
|
backgroundBlur: 25,
|
||||||
|
backgroundPosterOpacity: 0.6,
|
||||||
|
blur: 20,
|
||||||
|
glassQuality: 'realtime',
|
||||||
|
level: '',
|
||||||
|
opacity: 0.8,
|
||||||
|
})
|
||||||
|
const settings = useTransparencySettings()
|
||||||
|
|
||||||
|
settings.resetTransparencySettings()
|
||||||
|
|
||||||
|
expect(readTransparencySettings()).toMatchObject({
|
||||||
|
backgroundBlur: 25,
|
||||||
|
backgroundPosterOpacity: 0.6,
|
||||||
|
blur: 20,
|
||||||
|
glassQuality: 'realtime',
|
||||||
|
opacity: 0.8,
|
||||||
|
})
|
||||||
|
|
||||||
|
settings.saveTransparencySettings()
|
||||||
|
|
||||||
|
expect(readTransparencySettings()).toEqual({
|
||||||
|
...storedSettings,
|
||||||
|
backgroundPosterOpacity: 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -19,6 +19,9 @@ export const transparencyPresets = {
|
|||||||
|
|
||||||
export const TRANSPARENCY_SETTINGS_CHANGED_EVENT = 'transparency-settings-changed'
|
export const TRANSPARENCY_SETTINGS_CHANGED_EVENT = 'transparency-settings-changed'
|
||||||
|
|
||||||
|
let transparencyPreviewSnapshot: TransparencySettings | null = null
|
||||||
|
let transparencyPreviewState: TransparencySettings | null = null
|
||||||
|
|
||||||
/** 将数值限制在指定范围内。 */
|
/** 将数值限制在指定范围内。 */
|
||||||
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))
|
||||||
@@ -43,9 +46,9 @@ export function readTransparencySettings(): TransparencySettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 应用透明主题设置并写入本地存储。 */
|
/** 校验透明主题设置并限制数值范围。 */
|
||||||
export function applyTransparencySettings(settings: TransparencySettings) {
|
function normalizeTransparencySettings(settings: TransparencySettings): TransparencySettings {
|
||||||
const normalized: TransparencySettings = {
|
return {
|
||||||
opacity: Number.isFinite(settings.opacity) ? clamp(settings.opacity, 0, 1) : 0.3,
|
opacity: Number.isFinite(settings.opacity) ? clamp(settings.opacity, 0, 1) : 0.3,
|
||||||
blur: Number.isFinite(settings.blur) ? clamp(settings.blur, 0, 30) : 10,
|
blur: Number.isFinite(settings.blur) ? clamp(settings.blur, 0, 30) : 10,
|
||||||
backgroundPosterOpacity: Number.isFinite(settings.backgroundPosterOpacity)
|
backgroundPosterOpacity: Number.isFinite(settings.backgroundPosterOpacity)
|
||||||
@@ -55,6 +58,11 @@ export function applyTransparencySettings(settings: TransparencySettings) {
|
|||||||
glassQuality: settings.glassQuality === 'realtime' ? 'realtime' : 'lightweight',
|
glassQuality: settings.glassQuality === 'realtime' ? 'realtime' : 'lightweight',
|
||||||
level: settings.level,
|
level: settings.level,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将透明主题设置应用到当前页面,不改变持久化状态。 */
|
||||||
|
function applyTransparencyAppearance(settings: TransparencySettings) {
|
||||||
|
const normalized = normalizeTransparencySettings(settings)
|
||||||
|
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
root.style.setProperty('--transparent-opacity', normalized.opacity.toString())
|
root.style.setProperty('--transparent-opacity', normalized.opacity.toString())
|
||||||
@@ -70,6 +78,17 @@ export function applyTransparencySettings(settings: TransparencySettings) {
|
|||||||
root.classList.toggle('transparent-glass-lightweight', normalized.glassQuality === 'lightweight')
|
root.classList.toggle('transparent-glass-lightweight', normalized.glassQuality === 'lightweight')
|
||||||
root.classList.toggle('transparent-glass-realtime', normalized.glassQuality === 'realtime')
|
root.classList.toggle('transparent-glass-realtime', normalized.glassQuality === 'realtime')
|
||||||
|
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent<TransparencySettings>(TRANSPARENCY_SETTINGS_CHANGED_EVENT, { detail: normalized }),
|
||||||
|
)
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将透明主题设置写入本地存储。 */
|
||||||
|
function persistTransparencySettings(settings: TransparencySettings) {
|
||||||
|
const normalized = normalizeTransparencySettings(settings)
|
||||||
|
|
||||||
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())
|
||||||
@@ -77,19 +96,64 @@ export function applyTransparencySettings(settings: TransparencySettings) {
|
|||||||
localStorage.setItem('transparency-glass-quality', normalized.glassQuality)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 应用透明主题设置并写入本地存储。 */
|
||||||
|
export function applyTransparencySettings(settings: TransparencySettings) {
|
||||||
|
const normalized = persistTransparencySettings(settings)
|
||||||
|
|
||||||
|
return applyTransparencyAppearance(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
/** 按本地存储中的最新值应用透明主题设置。 */
|
/** 按本地存储中的最新值应用透明主题设置。 */
|
||||||
export function applyStoredTransparencySettings() {
|
export function applyStoredTransparencySettings() {
|
||||||
|
transparencyPreviewSnapshot = null
|
||||||
|
transparencyPreviewState = null
|
||||||
|
|
||||||
return applyTransparencySettings(readTransparencySettings())
|
return applyTransparencySettings(readTransparencySettings())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 临时预览透明主题设置,关闭设置面板时可恢复已保存快照。 */
|
||||||
|
export function previewTransparencySettings(patch: Partial<TransparencySettings>) {
|
||||||
|
if (!transparencyPreviewSnapshot) {
|
||||||
|
transparencyPreviewSnapshot = normalizeTransparencySettings(readTransparencySettings())
|
||||||
|
}
|
||||||
|
|
||||||
|
transparencyPreviewState = applyTransparencyAppearance(
|
||||||
|
normalizeTransparencySettings({
|
||||||
|
...transparencyPreviewSnapshot,
|
||||||
|
...transparencyPreviewState,
|
||||||
|
...patch,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return transparencyPreviewState
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将当前透明主题预览作为一次设置事务持久化。 */
|
||||||
|
export function commitTransparencyPreview() {
|
||||||
|
const settings = transparencyPreviewState
|
||||||
|
|
||||||
|
transparencyPreviewSnapshot = null
|
||||||
|
transparencyPreviewState = null
|
||||||
|
|
||||||
|
return settings ? applyTransparencySettings(settings) : normalizeTransparencySettings(readTransparencySettings())
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 丢弃透明主题预览并恢复打开设置面板前的持久化快照。 */
|
||||||
|
export function cancelTransparencyPreview() {
|
||||||
|
const snapshot = transparencyPreviewSnapshot
|
||||||
|
|
||||||
|
transparencyPreviewSnapshot = null
|
||||||
|
transparencyPreviewState = null
|
||||||
|
|
||||||
|
return snapshot ? applyTransparencyAppearance(snapshot) : normalizeTransparencySettings(readTransparencySettings())
|
||||||
|
}
|
||||||
|
|
||||||
/** 提供透明主题设置的响应式状态和操作方法。 */
|
/** 提供透明主题设置的响应式状态和操作方法。 */
|
||||||
export function useTransparencySettings() {
|
export function useTransparencySettings() {
|
||||||
const storedSettings = readTransparencySettings()
|
const storedSettings = normalizeTransparencySettings(readTransparencySettings())
|
||||||
const transparencyOpacity = ref(storedSettings.opacity)
|
const transparencyOpacity = ref(storedSettings.opacity)
|
||||||
const transparencyBlur = ref(storedSettings.blur)
|
const transparencyBlur = ref(storedSettings.blur)
|
||||||
const backgroundPosterOpacity = ref(storedSettings.backgroundPosterOpacity)
|
const backgroundPosterOpacity = ref(storedSettings.backgroundPosterOpacity)
|
||||||
@@ -110,9 +174,9 @@ export function useTransparencySettings() {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 同步当前响应式状态到 CSS 变量和本地存储。 */
|
/** 将当前响应式状态同步为未持久化预览。 */
|
||||||
function syncTransparencySettings() {
|
function syncTransparencySettings() {
|
||||||
const normalized = applyTransparencySettings({
|
const normalized = previewTransparencySettings({
|
||||||
opacity: transparencyOpacity.value,
|
opacity: transparencyOpacity.value,
|
||||||
blur: transparencyBlur.value,
|
blur: transparencyBlur.value,
|
||||||
backgroundPosterOpacity: backgroundPosterOpacity.value,
|
backgroundPosterOpacity: backgroundPosterOpacity.value,
|
||||||
@@ -129,6 +193,30 @@ export function useTransparencySettings() {
|
|||||||
transparencyLevel.value = normalized.level
|
transparencyLevel.value = normalized.level
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将当前预览写入本地存储。 */
|
||||||
|
function saveTransparencySettings() {
|
||||||
|
const normalized = commitTransparencyPreview()
|
||||||
|
|
||||||
|
syncReactiveState(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 丢弃当前预览并恢复持久化设置。 */
|
||||||
|
function cancelTransparencySettings() {
|
||||||
|
const normalized = cancelTransparencyPreview()
|
||||||
|
|
||||||
|
syncReactiveState(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 使用指定设置刷新面板草稿。 */
|
||||||
|
function syncReactiveState(settings: TransparencySettings) {
|
||||||
|
transparencyOpacity.value = settings.opacity
|
||||||
|
transparencyBlur.value = settings.blur
|
||||||
|
backgroundPosterOpacity.value = settings.backgroundPosterOpacity
|
||||||
|
backgroundBlur.value = settings.backgroundBlur
|
||||||
|
transparencyGlassQuality.value = settings.glassQuality
|
||||||
|
transparencyLevel.value = settings.level
|
||||||
|
}
|
||||||
|
|
||||||
/** 按预设级别调整透明度和模糊度。 */
|
/** 按预设级别调整透明度和模糊度。 */
|
||||||
function adjustTransparency(level: string) {
|
function adjustTransparency(level: string) {
|
||||||
transparencyLevel.value = level
|
transparencyLevel.value = level
|
||||||
@@ -193,6 +281,7 @@ export function useTransparencySettings() {
|
|||||||
adjustTransparency,
|
adjustTransparency,
|
||||||
backgroundBlur,
|
backgroundBlur,
|
||||||
backgroundPosterOpacity,
|
backgroundPosterOpacity,
|
||||||
|
cancelTransparencySettings,
|
||||||
currentPresetLevel,
|
currentPresetLevel,
|
||||||
onBackgroundBlurChange,
|
onBackgroundBlurChange,
|
||||||
onBackgroundPosterOpacityChange,
|
onBackgroundPosterOpacityChange,
|
||||||
@@ -200,6 +289,7 @@ export function useTransparencySettings() {
|
|||||||
onGlassQualityChange,
|
onGlassQualityChange,
|
||||||
onOpacityChange,
|
onOpacityChange,
|
||||||
resetTransparencySettings,
|
resetTransparencySettings,
|
||||||
|
saveTransparencySettings,
|
||||||
syncTransparencySettings,
|
syncTransparencySettings,
|
||||||
transparencyBlur,
|
transparencyBlur,
|
||||||
transparencyGlassQuality,
|
transparencyGlassQuality,
|
||||||
|
|||||||
@@ -170,10 +170,6 @@ html[data-theme='glass'] {
|
|||||||
--glass-control-shortcut-color: rgba(242, 245, 250, 76%);
|
--glass-control-shortcut-color: rgba(242, 245, 250, 76%);
|
||||||
--glass-button-surface: rgba(255, 255, 255, 10%);
|
--glass-button-surface: rgba(255, 255, 255, 10%);
|
||||||
--glass-button-surface-hover: rgba(255, 255, 255, 14%);
|
--glass-button-surface-hover: rgba(255, 255, 255, 14%);
|
||||||
--glass-nav-active-background: linear-gradient(270deg, rgb(var(--v-theme-primary)) 0%, rgb(255, 255, 255) 300%);
|
|
||||||
--glass-nav-active-border: 0 solid transparent;
|
|
||||||
--glass-nav-active-shadow:
|
|
||||||
0 4px 14px -4px rgba(3, 7, 18, 34%), 0 4px 8px -4px rgba(3, 7, 18, 24%), 0 4px 8px -4px rgba(3, 7, 18, 16%);
|
|
||||||
--glass-dashboard-backdrop-filter: blur(var(--glass-blur-surface)) saturate(var(--glass-saturate));
|
--glass-dashboard-backdrop-filter: blur(var(--glass-blur-surface)) saturate(var(--glass-saturate));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user