feat(stage-web): new settings animation (#100)
* feat(stage-web): new settings animation * fix: optimize, issues * fix: allow disable icon animation * fix: typo * fix: disable icon animation when disable all animation
This commit is contained in:
@@ -46,6 +46,9 @@ settings:
|
||||
animations:
|
||||
stage-transitions:
|
||||
title: Disable Stage Transitions
|
||||
use-page-specific-transitions:
|
||||
title: Use Page Specific Transitions
|
||||
description: Some pages will have their own transitions, this will override the stage transitions
|
||||
language:
|
||||
chinese: 简体中文
|
||||
english: English
|
||||
|
||||
@@ -34,6 +34,9 @@ settings:
|
||||
animations:
|
||||
stage-transitions:
|
||||
title: 是否开启舞台动画
|
||||
use-page-specific-transitions:
|
||||
title: 是否使用页面特定过场动画
|
||||
description: 某些页面会有自己的过场动画,这将覆盖舞台过场动画
|
||||
language:
|
||||
chinese: 简体中文
|
||||
english: English
|
||||
|
||||
@@ -54,6 +54,7 @@ watch(settings.themeColorsHueDynamic, () => {
|
||||
:colors="colors"
|
||||
:z-index="100"
|
||||
:disable-transitions="settings.disableTransitions.value"
|
||||
:use-page-specific-transitions="settings.usePageSpecificTransitions.value"
|
||||
>
|
||||
<RouterView />
|
||||
</StageTransitionGroup>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
icon: string
|
||||
iconSize: number // avoid conflict with unocss size prop
|
||||
position: string
|
||||
duration: number
|
||||
started: boolean
|
||||
textColor: string
|
||||
isReverse?: boolean
|
||||
zIndex?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'animationEnded'): void
|
||||
}>()
|
||||
|
||||
const isAnimating = ref(false)
|
||||
|
||||
watch(() => props.started, (newVal) => {
|
||||
if (newVal) {
|
||||
requestAnimationFrame(() => {
|
||||
isAnimating.value = true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const clsAndProps = computed(() => {
|
||||
return {
|
||||
opacity: isAnimating.value !== props.isReverse ? 1 : 0, // this equals to opacity: isAnimating.value ? props.isReverse ? 1 : 0 : props.isReverse ? 0 : 1
|
||||
size: isAnimating.value !== props.isReverse ? 25 : props.iconSize,
|
||||
position: isAnimating.value !== props.isReverse ? `calc(50dvw - 12.5rem), calc(50dvh - 12.5rem)` : props.position,
|
||||
textColor: isAnimating.value !== props.isReverse ? 'text-white' : props.textColor,
|
||||
}
|
||||
})
|
||||
|
||||
const animationEndProps = ref<string[]>([])
|
||||
const animationEnded = ref(false)
|
||||
function handleAnimationEnded(e: TransitionEvent) {
|
||||
animationEndProps.value.push(e.propertyName)
|
||||
if (animationEndProps.value.includes('color') && animationEndProps.value.includes('width') && animationEndProps.value.includes('height') && animationEndProps.value.includes('transform')) {
|
||||
animationEnded.value = true
|
||||
emit('animationEnded')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
pointer-events-none fixed w="100dvw" h="100dvh"
|
||||
:style="{
|
||||
zIndex: animationEnded ? zIndex : undefined,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
bg-primary-500 fixed inset-0 transition-opacity ease-linear :style="{
|
||||
opacity: clsAndProps.opacity,
|
||||
transitionDuration: `${duration}ms`,
|
||||
}"
|
||||
/>
|
||||
<div
|
||||
fixed inset-0 ease-in-out :style="{
|
||||
width: `${clsAndProps.size}rem`,
|
||||
height: `${clsAndProps.size}rem`,
|
||||
transform: `translate(${clsAndProps.position})`,
|
||||
transitionDuration: `${duration}ms`,
|
||||
}" :class="[
|
||||
clsAndProps.textColor,
|
||||
props.icon,
|
||||
{ 'transition-all': isAnimating },
|
||||
]"
|
||||
@transitionend="handleAnimationEnded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,6 +3,7 @@ defineProps<{
|
||||
text: string
|
||||
iconOn: string
|
||||
iconOff: string
|
||||
description?: string
|
||||
}>()
|
||||
const model = defineModel<boolean>()
|
||||
</script>
|
||||
@@ -14,7 +15,12 @@ const model = defineModel<boolean>()
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
>
|
||||
<input v-model="model" :aria-checked="model" type="checkbox" hidden>
|
||||
{{ $t(text) }}
|
||||
<div>
|
||||
{{ $t(text) }}
|
||||
<div v-if="description" text="sm neutral-500">
|
||||
{{ $t(description) }}
|
||||
</div>
|
||||
</div>
|
||||
<Transition name="slide-away" mode="out-in">
|
||||
<div v-if="model" :class="iconOn" transition="all ease-in-out duration-250" />
|
||||
<div v-else :class="iconOff" transition="all ease-in-out duration-250" />
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
export function useIconAnimation(icon: string) {
|
||||
const iconAnimationStarted = ref(false)
|
||||
const showAnimationComponent = ref(false)
|
||||
const animationIcon = ref(icon)
|
||||
|
||||
const settingsStore = useSettings()
|
||||
const showIconAnimation = computed(() => showAnimationComponent.value && !settingsStore.disableTransitions && settingsStore.usePageSpecificTransitions)
|
||||
|
||||
onMounted(() => {
|
||||
showAnimationComponent.value = true
|
||||
requestAnimationFrame(() => {
|
||||
iconAnimationStarted.value = true
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
iconAnimationStarted.value = false
|
||||
showAnimationComponent.value = false
|
||||
})
|
||||
|
||||
return {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useDark } from '@vueuse/core'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import CheckBar from '../../../components/Settings/CheckBar.vue'
|
||||
import ColorPalette from '../../../components/Settings/ColorPalette.vue'
|
||||
import Section from '../../../components/Settings/Section.vue'
|
||||
import { useIconAnimation } from '../../../composables/useIconAnimation'
|
||||
import COLOR_PRESETS from './color-presets.json'
|
||||
|
||||
const router = useRouter()
|
||||
const settings = useSettings()
|
||||
const dark = useDark()
|
||||
const usePageSpecificTransitionsSettingChanged = ref(false)
|
||||
|
||||
const { iconAnimationStarted, showIconAnimation, animationIcon } = useIconAnimation('i-lucide:paintbrush')
|
||||
|
||||
// avoid showing the animation component when the page specific transitions are enabled
|
||||
watch(() => [settings.usePageSpecificTransitions, settings.disableTransitions], () => {
|
||||
usePageSpecificTransitionsSettingChanged.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -140,9 +150,28 @@ const dark = useDark()
|
||||
icon-off="i-solar:running-2-line-duotone"
|
||||
text="settings.animations.stage-transitions.title"
|
||||
/>
|
||||
<CheckBar
|
||||
v-model="settings.usePageSpecificTransitions"
|
||||
:disabled="settings.disableTransitions"
|
||||
icon-on="i-solar:running-2-line-duotone"
|
||||
icon-off="i-solar:people-nearby-bold-duotone"
|
||||
text="settings.animations.use-page-specific-transitions.title"
|
||||
description="settings.animations.use-page-specific-transitions.description"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation && !usePageSpecificTransitionsSettingChanged"
|
||||
:z-index="-1"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
<div v-if="!settings.usePageSpecificTransitions" text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:paintbrush />
|
||||
</div>
|
||||
</template>
|
||||
@@ -202,4 +231,5 @@ const dark = useDark()
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
|
||||
@@ -1,8 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { IconItem } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import IconAnimation from '../../components/IconAnimation.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const iconAnimationStarted = ref(false)
|
||||
const iconAnimation = ref<InstanceType<typeof IconAnimation>>()
|
||||
const resolveAnimation = ref<() => void>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const animationIcon = ref('')
|
||||
const animationPosition = ref('')
|
||||
const showAnimationComponent = ref(false)
|
||||
const settingsStore = useSettings()
|
||||
|
||||
function handleAnimationEnded() {
|
||||
resolveAnimation.value?.()
|
||||
}
|
||||
|
||||
async function handleIconItemClick(event: MouseEvent, setting: typeof settings.value[0]) {
|
||||
const target = event.currentTarget as HTMLElement
|
||||
const iconElement = target.querySelector('.menu-icon-item-icon') as HTMLElement
|
||||
if (!iconElement)
|
||||
return
|
||||
|
||||
// get the position of the icon element
|
||||
const rect = iconElement.getBoundingClientRect()
|
||||
const position = `${rect.left}px, ${rect.top}px`
|
||||
|
||||
// set the icon and position
|
||||
animationIcon.value = setting.icon
|
||||
animationPosition.value = position
|
||||
|
||||
// show the animation component
|
||||
showAnimationComponent.value = true
|
||||
|
||||
// wait for the DOM to update
|
||||
await nextTick()
|
||||
|
||||
// start the animation
|
||||
iconAnimationStarted.value = true
|
||||
}
|
||||
|
||||
const removeBeforeEach = router.beforeEach(async (_, __, next) => {
|
||||
if (!settingsStore.usePageSpecificTransitions || settingsStore.disableTransitions) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
resolveAnimation.value = resolve
|
||||
})
|
||||
removeBeforeEach()
|
||||
next()
|
||||
})
|
||||
|
||||
const settings = computed(() => [
|
||||
{
|
||||
title: t('settings.pages.modules.title'),
|
||||
description: t('settings.pages.modules.description'),
|
||||
icon: 'i-lucide:blocks',
|
||||
to: '/settings/modules',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.models.title'),
|
||||
description: t('settings.pages.models.description'),
|
||||
icon: 'i-lucide:person-standing',
|
||||
to: '/settings/models',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.memory.title'),
|
||||
description: t('settings.pages.memory.description'),
|
||||
icon: 'i-lucide:sprout',
|
||||
to: '/settings/memory',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.providers.title'),
|
||||
description: t('settings.pages.providers.description'),
|
||||
icon: 'i-lucide:brain',
|
||||
to: '/settings/providers',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.themes.title'),
|
||||
description: t('settings.pages.themes.description'),
|
||||
icon: 'i-lucide:paintbrush',
|
||||
to: '/settings/appearance',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -23,63 +111,36 @@ const router = useRouter()
|
||||
<div flex="~ col gap-4">
|
||||
<div flex="~ col gap-4">
|
||||
<IconItem
|
||||
v-for="(setting, index) in settings"
|
||||
:key="setting.to"
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:title="$t('settings.pages.modules.title')"
|
||||
:description="$t('settings.pages.modules.description')"
|
||||
icon="i-lucide:blocks"
|
||||
to="/settings/modules"
|
||||
/>
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="50"
|
||||
:title="$t('settings.pages.models.title')"
|
||||
:description="$t('settings.pages.models.description')"
|
||||
icon="i-lucide:person-standing"
|
||||
to="/settings/models"
|
||||
/>
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="100"
|
||||
:title="$t('settings.pages.memory.title')"
|
||||
:description="$t('settings.pages.memory.description')"
|
||||
icon="i-lucide:sprout"
|
||||
to="/settings/memory"
|
||||
/>
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="100"
|
||||
:title="$t('settings.pages.providers.title')"
|
||||
:description="$t('settings.pages.providers.description')"
|
||||
icon="i-lucide:brain"
|
||||
to="/settings/providers"
|
||||
/>
|
||||
<IconItem
|
||||
v-motion
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250"
|
||||
:delay="150"
|
||||
:title="$t('settings.pages.themes.title')"
|
||||
:description="$t('settings.pages.themes.description')"
|
||||
icon="i-lucide:paintbrush"
|
||||
to="/settings/appearance"
|
||||
:style="{
|
||||
transitionDelay: `${index * 50}ms`, // delay between each item, unocss doesn't support dynamic generation of classes now
|
||||
}"
|
||||
:title="setting.title"
|
||||
:description="setting.description"
|
||||
:icon="setting.icon"
|
||||
:to="setting.to"
|
||||
@click="(e) => handleIconItemClick(e, setting)"
|
||||
/>
|
||||
</div>
|
||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div v-motion text="60" i-lucide:cog />
|
||||
</div>
|
||||
<IconAnimation
|
||||
v-if="showAnimationComponent && !settingsStore.disableTransitions && settingsStore.usePageSpecificTransitions"
|
||||
ref="iconAnimation"
|
||||
:icon="animationIcon"
|
||||
:icon-size="6 * 1.2"
|
||||
:position="animationPosition"
|
||||
:duration="1000"
|
||||
text-color="text-neutral-400/50 dark:text-neutral-600/20"
|
||||
:started="iconAnimationStarted"
|
||||
@animation-ended.once="handleAnimationEnded"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import Live2DCanvas from '@proj-airi/stage-ui/components/Live2D/Canvas.vue'
|
||||
import Live2DModel from '@proj-airi/stage-ui/components/Live2D/Model.vue'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
import { Vibrant } from 'node-vibrant/browser'
|
||||
import { ref } from 'vue'
|
||||
@@ -8,12 +9,14 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import Live2DSettings from '../../../components/Widgets/Live2DSettings.vue'
|
||||
import { useIconAnimation } from '../../../composables/useIconAnimation'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const live2dContainerRef = ref<HTMLDivElement>()
|
||||
const live2dCanvasRef = ref<InstanceType<typeof Live2DCanvas>>()
|
||||
const { width, height } = useElementBounding(live2dContainerRef)
|
||||
const settingsStore = useSettings()
|
||||
|
||||
const palette = ref<string[]>([])
|
||||
|
||||
@@ -38,6 +41,12 @@ async function extractColorsFromModel() {
|
||||
URL.revokeObjectURL(frameUrl)
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-lucide:person-standing')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -68,7 +77,18 @@ async function extractColorsFromModel() {
|
||||
<Live2DSettings w="50%" h="80vh" :palette="palette" @extract-colors-from-model="extractColorsFromModel" />
|
||||
</div>
|
||||
|
||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:z-index="-1"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
<div v-if="!settingsStore.usePageSpecificTransitions" text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:person-standing />
|
||||
</div>
|
||||
</template>
|
||||
@@ -77,4 +97,5 @@ async function extractColorsFromModel() {
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { IconStatusItem } from '@proj-airi/stage-ui/components'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import IconAnimation from '../../../components/IconAnimation.vue'
|
||||
import { useIconAnimation } from '../../../composables/useIconAnimation'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const settingsStore = useSettings()
|
||||
|
||||
interface Module {
|
||||
id: string
|
||||
@@ -101,6 +106,12 @@ const modulesList = computed<Module[]>(() => [
|
||||
configured: false,
|
||||
},
|
||||
])
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-lucide:blocks')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -142,7 +153,18 @@ const modulesList = computed<Module[]>(() => [
|
||||
:configured="module.configured"
|
||||
/>
|
||||
</div>
|
||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
:z-index="-1"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
/>
|
||||
<div v-if="!settingsStore.usePageSpecificTransitions" text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:blocks />
|
||||
</div>
|
||||
</template>
|
||||
@@ -151,4 +173,5 @@ const modulesList = computed<Module[]>(() => [
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { IconStatusItem } from '@proj-airi/stage-ui/components'
|
||||
import { useProvidersStore } from '@proj-airi/stage-ui/stores'
|
||||
import { useProvidersStore, useSettings } from '@proj-airi/stage-ui/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useIconAnimation } from '../../../composables/useIconAnimation'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const providersStore = useProvidersStore()
|
||||
const { allProvidersMetadata } = storeToRefs(providersStore)
|
||||
const settingsStore = useSettings()
|
||||
|
||||
const {
|
||||
iconAnimationStarted,
|
||||
showIconAnimation,
|
||||
animationIcon,
|
||||
} = useIconAnimation('i-lucide:brain')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -50,7 +59,18 @@ const { allProvidersMetadata } = storeToRefs(providersStore)
|
||||
:configured="provider.configured"
|
||||
/>
|
||||
</div>
|
||||
<div text="neutral-200/50 dark:neutral-600/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<IconAnimation
|
||||
v-if="showIconAnimation"
|
||||
:z-index="-1"
|
||||
:icon="animationIcon"
|
||||
:icon-size="12"
|
||||
:duration="1000"
|
||||
:started="iconAnimationStarted"
|
||||
:is-reverse="true"
|
||||
position="calc(100dvw - 9.5rem), calc(100dvh - 9.5rem)"
|
||||
text-color="text-neutral-200/50 dark:text-neutral-600/20"
|
||||
/>
|
||||
<div v-if="!settingsStore.usePageSpecificTransitions" text="neutral-200/50 dark:neutral-500/20" pointer-events-none fixed bottom-0 right-0 z--1 translate-x-10 translate-y-10>
|
||||
<div text="40" i-lucide:brain />
|
||||
</div>
|
||||
</template>
|
||||
@@ -59,4 +79,5 @@ const { allProvidersMetadata } = storeToRefs(providersStore)
|
||||
meta:
|
||||
stageTransition:
|
||||
name: slide
|
||||
pageSpecificAvailable: true
|
||||
</route>
|
||||
|
||||
@@ -29,6 +29,7 @@ export const useSettings = defineStore('settings', () => {
|
||||
const live2dMotionMap = useLocalStorage<Record<string, string>>('settings/live2d/motion-map', {})
|
||||
|
||||
const disableTransitions = useLocalStorage('settings/disable-transitions', true)
|
||||
const usePageSpecificTransitions = useLocalStorage('settings/use-page-specific-transitions', true)
|
||||
|
||||
const themeColorsHue = useLocalStorage('settings/theme/colors/hue', DEFAULT_THEME_COLORS_HUE)
|
||||
const themeColorsHueDynamic = useLocalStorage('settings/theme/colors/hue-dynamic', false)
|
||||
@@ -87,6 +88,7 @@ export const useSettings = defineStore('settings', () => {
|
||||
live2dMotionMap,
|
||||
loadingLive2dModel,
|
||||
disableTransitions,
|
||||
usePageSpecificTransitions,
|
||||
language,
|
||||
stageView,
|
||||
themeColorsHue,
|
||||
|
||||
@@ -17,6 +17,7 @@ const props = defineProps<{
|
||||
colors?: string[]
|
||||
zIndex?: number
|
||||
disableTransitions?: boolean
|
||||
usePageSpecificTransitions?: boolean
|
||||
}>()
|
||||
|
||||
interface StageTransitionCommonParams {
|
||||
@@ -27,6 +28,7 @@ interface StageTransitionCommonParams {
|
||||
direction?: 'top' | 'bottom' | 'left' | 'right'
|
||||
colors?: string[]
|
||||
zIndex?: number
|
||||
pageSpecificAvailable?: boolean
|
||||
}
|
||||
|
||||
type TransitionComponent =
|
||||
@@ -247,12 +249,21 @@ function triggerTransition(params: StageTransitionCommonParams, next: Navigation
|
||||
}
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
if (props.disableTransitions) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof to.meta.stageTransition !== 'object') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
const stageTransition = to.meta.stageTransition as StageTransitionCommonParams
|
||||
if (props.usePageSpecificTransitions && stageTransition.pageSpecificAvailable) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
if (typeof props.primaryColor !== 'undefined') {
|
||||
stageTransition.primaryColor = props.primaryColor
|
||||
}
|
||||
@@ -269,11 +280,6 @@ router.beforeEach((to, _from, next) => {
|
||||
stageTransition.zIndex = props.zIndex
|
||||
}
|
||||
|
||||
if (props.disableTransitions) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
triggerTransition(stageTransition, next)
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user