mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-08-12 17:14:37 +08:00
feat(theme): refine glass and transparency settings (#578)
This commit is contained in:
@@ -28,7 +28,7 @@ import { useGlobalOfflineStatus, type ConnectionFailureReason } from '@/composab
|
||||
|
||||
const LOGIN_WALLPAPER_ROUTE = '/login'
|
||||
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
|
||||
|
||||
// 生效主题
|
||||
|
||||
@@ -88,6 +88,16 @@ function updateQuality(value: unknown) {
|
||||
previewGlassSettings({ glassQuality: option.value })
|
||||
}
|
||||
|
||||
/** 将当前草稿恢复为玻璃主题默认值并立即预览。 */
|
||||
function resetSettings() {
|
||||
draftAppearance.value = 'clear'
|
||||
draftQuality.value = 'css'
|
||||
previewGlassSettings({
|
||||
glassAppearance: draftAppearance.value,
|
||||
glassQuality: draftQuality.value,
|
||||
})
|
||||
}
|
||||
|
||||
/** 一次提交当前预览,持久化后关闭不会发生视觉回跳。 */
|
||||
async function saveSettings() {
|
||||
if (isSaving.value) return
|
||||
@@ -111,19 +121,16 @@ onScopeDispose(cancelGlassPreview)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VDialog v-if="visible" v-model="visible" max-width="28rem" scrollable :fullscreen="display.xs.value">
|
||||
<VCard class="glass-settings-dialog">
|
||||
<VCardItem class="glass-settings-dialog__header py-3">
|
||||
<template #prepend>
|
||||
<VAvatar color="primary" variant="tonal" rounded size="40" class="me-2">
|
||||
<VIcon icon="mdi-blur-radial" size="22" />
|
||||
</VAvatar>
|
||||
</template>
|
||||
<VDialog v-if="visible" v-model="visible" max-width="30rem" scrollable :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>
|
||||
<VIcon icon="mdi-blur-radial" class="me-2" />
|
||||
{{ t('theme.glassSettings') }}
|
||||
</VCardTitle>
|
||||
<VDialogCloseBtn v-model="visible" />
|
||||
</VCardItem>
|
||||
<VDivider />
|
||||
|
||||
<VCardText class="glass-settings-dialog__body">
|
||||
<section>
|
||||
@@ -169,33 +176,20 @@ onScopeDispose(cancelGlassPreview)
|
||||
</section>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions class="app-dialog-actions glass-settings-dialog__actions">
|
||||
<VSpacer />
|
||||
<VBtn
|
||||
color="primary"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-content-save"
|
||||
class="px-5"
|
||||
:loading="isSaving"
|
||||
@click="saveSettings"
|
||||
>
|
||||
<VDivider />
|
||||
<VCardText class="text-center">
|
||||
<VBtn variant="outlined" prepend-icon="mdi-refresh" class="me-2" @click="resetSettings">
|
||||
{{ t('common.reset') }}
|
||||
</VBtn>
|
||||
<VBtn color="primary" prepend-icon="mdi-content-save" :loading="isSaving" @click="saveSettings">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
@@ -260,25 +254,4 @@ onScopeDispose(cancelGlassPreview)
|
||||
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;
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@@ -29,6 +29,7 @@ const emit = defineEmits<{
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: value => {
|
||||
if (!value) cancelTransparencySettings()
|
||||
emit('update:modelValue', value)
|
||||
if (!value) emit('close')
|
||||
},
|
||||
@@ -38,6 +39,7 @@ const {
|
||||
adjustTransparency,
|
||||
backgroundBlur,
|
||||
backgroundPosterOpacity,
|
||||
cancelTransparencySettings,
|
||||
currentPresetLevel,
|
||||
onBackgroundBlurChange,
|
||||
onBackgroundPosterOpacityChange,
|
||||
@@ -45,10 +47,28 @@ const {
|
||||
onGlassQualityChange,
|
||||
onOpacityChange,
|
||||
resetTransparencySettings,
|
||||
saveTransparencySettings,
|
||||
transparencyBlur,
|
||||
transparencyGlassQuality,
|
||||
transparencyOpacity,
|
||||
} = useTransparencySettings()
|
||||
|
||||
/** 父级控制弹窗生命周期时,未保存关闭仍需恢复持久化设置。 */
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value, previous) => {
|
||||
if (!value && previous) cancelTransparencySettings()
|
||||
},
|
||||
)
|
||||
|
||||
/** 保存当前透明度预览,随后关闭面板。 */
|
||||
function saveSettings() {
|
||||
saveTransparencySettings()
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 弹窗的任意未保存销毁路径都应恢复持久化快照。
|
||||
onScopeDispose(cancelTransparencySettings)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -185,14 +205,11 @@ const {
|
||||
</VCardText>
|
||||
<VDivider />
|
||||
<VCardText class="text-center">
|
||||
<VBtn @click="resetTransparencySettings" variant="outlined" class="me-2">
|
||||
<template #prepend>
|
||||
<VIcon icon="mdi-refresh" />
|
||||
</template>
|
||||
{{ t('theme.transparencyReset') }}
|
||||
<VBtn variant="outlined" prepend-icon="mdi-refresh" class="me-2" @click="resetTransparencySettings">
|
||||
{{ t('common.reset') }}
|
||||
</VBtn>
|
||||
<VBtn @click="visible = false" color="primary">
|
||||
{{ t('common.confirm') }}
|
||||
<VBtn color="primary" prepend-icon="mdi-content-save" @click="saveSettings">
|
||||
{{ t('common.save') }}
|
||||
</VBtn>
|
||||
</VCardText>
|
||||
</VCard>
|
||||
|
||||
@@ -2,14 +2,18 @@ import { shallowMount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import GlassSettingsDialog from '@/components/dialog/GlassSettingsDialog.vue'
|
||||
|
||||
const slotStub = { template: '<div><slot /></div>' }
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
cancelGlassPreview: vi.fn(),
|
||||
commitGlassPreview: vi.fn(),
|
||||
previewGlassSettings: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useThemeCustomizer', () => ({
|
||||
cancelGlassPreview: mocks.cancelGlassPreview,
|
||||
commitGlassPreview: vi.fn(),
|
||||
previewGlassSettings: vi.fn(),
|
||||
commitGlassPreview: mocks.commitGlassPreview,
|
||||
previewGlassSettings: mocks.previewGlassSettings,
|
||||
useThemeCustomizer: () => ({
|
||||
settings: {
|
||||
value: {
|
||||
@@ -25,12 +29,14 @@ vi.mock('vue-i18n', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('vuetify', () => ({
|
||||
useDisplay: () => ({ xs: { value: false } }),
|
||||
useDisplay: () => ({ mdAndUp: { value: true } }),
|
||||
}))
|
||||
|
||||
describe('GlassSettingsDialog', () => {
|
||||
beforeEach(() => {
|
||||
mocks.cancelGlassPreview.mockClear()
|
||||
mocks.commitGlassPreview.mockClear()
|
||||
mocks.previewGlassSettings.mockClear()
|
||||
})
|
||||
|
||||
it('cancels an active preview when the parent closes the dialog', async () => {
|
||||
@@ -45,4 +51,33 @@ describe('GlassSettingsDialog', () => {
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
98
src/composables/__tests__/useTransparencySettings.spec.ts
Normal file
98
src/composables/__tests__/useTransparencySettings.spec.ts
Normal file
@@ -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'
|
||||
|
||||
let transparencyPreviewSnapshot: TransparencySettings | null = null
|
||||
let transparencyPreviewState: TransparencySettings | null = null
|
||||
|
||||
/** 将数值限制在指定范围内。 */
|
||||
function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value))
|
||||
@@ -43,9 +46,9 @@ export function readTransparencySettings(): TransparencySettings {
|
||||
}
|
||||
}
|
||||
|
||||
/** 应用透明主题设置并写入本地存储。 */
|
||||
export function applyTransparencySettings(settings: TransparencySettings) {
|
||||
const normalized: TransparencySettings = {
|
||||
/** 校验透明主题设置并限制数值范围。 */
|
||||
function normalizeTransparencySettings(settings: TransparencySettings): TransparencySettings {
|
||||
return {
|
||||
opacity: Number.isFinite(settings.opacity) ? clamp(settings.opacity, 0, 1) : 0.3,
|
||||
blur: Number.isFinite(settings.blur) ? clamp(settings.blur, 0, 30) : 10,
|
||||
backgroundPosterOpacity: Number.isFinite(settings.backgroundPosterOpacity)
|
||||
@@ -55,6 +58,11 @@ export function applyTransparencySettings(settings: TransparencySettings) {
|
||||
glassQuality: settings.glassQuality === 'realtime' ? 'realtime' : 'lightweight',
|
||||
level: settings.level,
|
||||
}
|
||||
}
|
||||
|
||||
/** 将透明主题设置应用到当前页面,不改变持久化状态。 */
|
||||
function applyTransparencyAppearance(settings: TransparencySettings) {
|
||||
const normalized = normalizeTransparencySettings(settings)
|
||||
|
||||
const root = document.documentElement
|
||||
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-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-blur', normalized.blur.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-level', normalized.level)
|
||||
|
||||
window.dispatchEvent(new CustomEvent<TransparencySettings>(TRANSPARENCY_SETTINGS_CHANGED_EVENT, { detail: normalized }))
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
/** 应用透明主题设置并写入本地存储。 */
|
||||
export function applyTransparencySettings(settings: TransparencySettings) {
|
||||
const normalized = persistTransparencySettings(settings)
|
||||
|
||||
return applyTransparencyAppearance(normalized)
|
||||
}
|
||||
|
||||
/** 按本地存储中的最新值应用透明主题设置。 */
|
||||
export function applyStoredTransparencySettings() {
|
||||
transparencyPreviewSnapshot = null
|
||||
transparencyPreviewState = null
|
||||
|
||||
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() {
|
||||
const storedSettings = readTransparencySettings()
|
||||
const storedSettings = normalizeTransparencySettings(readTransparencySettings())
|
||||
const transparencyOpacity = ref(storedSettings.opacity)
|
||||
const transparencyBlur = ref(storedSettings.blur)
|
||||
const backgroundPosterOpacity = ref(storedSettings.backgroundPosterOpacity)
|
||||
@@ -110,9 +174,9 @@ export function useTransparencySettings() {
|
||||
return null
|
||||
})
|
||||
|
||||
/** 同步当前响应式状态到 CSS 变量和本地存储。 */
|
||||
/** 将当前响应式状态同步为未持久化预览。 */
|
||||
function syncTransparencySettings() {
|
||||
const normalized = applyTransparencySettings({
|
||||
const normalized = previewTransparencySettings({
|
||||
opacity: transparencyOpacity.value,
|
||||
blur: transparencyBlur.value,
|
||||
backgroundPosterOpacity: backgroundPosterOpacity.value,
|
||||
@@ -129,6 +193,30 @@ export function useTransparencySettings() {
|
||||
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) {
|
||||
transparencyLevel.value = level
|
||||
@@ -193,6 +281,7 @@ export function useTransparencySettings() {
|
||||
adjustTransparency,
|
||||
backgroundBlur,
|
||||
backgroundPosterOpacity,
|
||||
cancelTransparencySettings,
|
||||
currentPresetLevel,
|
||||
onBackgroundBlurChange,
|
||||
onBackgroundPosterOpacityChange,
|
||||
@@ -200,6 +289,7 @@ export function useTransparencySettings() {
|
||||
onGlassQualityChange,
|
||||
onOpacityChange,
|
||||
resetTransparencySettings,
|
||||
saveTransparencySettings,
|
||||
syncTransparencySettings,
|
||||
transparencyBlur,
|
||||
transparencyGlassQuality,
|
||||
|
||||
@@ -170,10 +170,6 @@ html[data-theme='glass'] {
|
||||
--glass-control-shortcut-color: rgba(242, 245, 250, 76%);
|
||||
--glass-button-surface: rgba(255, 255, 255, 10%);
|
||||
--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));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user