From abddd40c09c7c48d9c741db9cc322517085d8f67 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 20 Mar 2025 18:55:32 +0900 Subject: [PATCH] =?UTF-8?q?enhance(frontend):=20=E9=80=9A=E5=B8=B8?= =?UTF-8?q?=E3=81=AERouterView=E3=81=ABTransition=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/global/MkPageHeader.vue | 3 + .../src/components/global/RouterView.vue | 70 +++++++++++++++++-- packages/frontend/src/di.ts | 1 + packages/frontend/src/utility/random-id.ts | 15 ++++ 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 packages/frontend/src/utility/random-id.ts diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 4321d69253..59bf80cfca 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -69,6 +69,8 @@ const emit = defineEmits<{ (ev: 'update:tab', key: string); }>(); +const viewId = inject(DI.viewId); +const viewTransitionName = computed(() => `${viewId}---pageHeader`); const injectedPageMetadata = inject(DI.pageMetadata); const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value); @@ -140,6 +142,7 @@ onUnmounted(() => { backdrop-filter: var(--MI-blur, blur(15px)); border-bottom: solid 0.5px var(--MI_THEME-divider); width: 100%; + view-transition-name: v-bind(viewTransitionName); } .upper, diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue index fbdb7d261e..45cb1e3bd5 100644 --- a/packages/frontend/src/components/global/RouterView.vue +++ b/packages/frontend/src/components/global/RouterView.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="_pageContainer" style="height: 100%;"> +<div ref="rootEl" class="_pageContainer" :class="$style.root"> <KeepAlive :max="prefer.s.numberOfPageCache"> <Suspense :timeout="0"> <component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/> @@ -18,11 +18,13 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, provide, ref, shallowRef } from 'vue'; +import { inject, nextTick, onMounted, provide, ref, shallowRef, useTemplateRef } from 'vue'; import type { Router } from '@/router.js'; import { prefer } from '@/preferences.js'; import MkLoadingPage from '@/pages/_loading_.vue'; import { DI } from '@/di.js'; +import { randomId } from '@/utility/random-id.js'; +import { deepEqual } from '@/utility/deep-equal.js'; const props = defineProps<{ router?: Router; @@ -34,18 +36,76 @@ if (router == null) { throw new Error('no router provided'); } +const viewId = randomId(); +provide(DI.viewId, viewId); + const currentDepth = inject(DI.routerCurrentDepth, 0); provide(DI.routerCurrentDepth, currentDepth + 1); +const rootEl = useTemplateRef('rootEl'); +onMounted(() => { + rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入 +}); + +// view-transition-newなどの<pt-name-selector>にはcss varが使えず、v-bindできないため直接スタイルを生成 +const viewTransitionStylesTag = document.createElement('style'); +viewTransitionStylesTag.textContent = ` +@keyframes ${viewId}-old { + to { transform: scale(0.95); opacity: 0; } +} + +@keyframes ${viewId}-new { + from { transform: scale(0.95); opacity: 0; } +} + +::view-transition-old(${viewId}) { + animation-duration: 0.2s; + animation-name: ${viewId}-old; +} + +::view-transition-new(${viewId}) { + animation-duration: 0.2s; + animation-name: ${viewId}-new; +} +`; + +window.document.head.appendChild(viewTransitionStylesTag); + const current = router.current!; const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage); const currentPageProps = ref(current.props); +let currentRoutePath = current.route.path; const key = ref(router.getCurrentFullPath()); router.useListener('change', ({ resolved }) => { if (resolved == null || 'redirect' in resolved.route) return; - currentPageComponent.value = resolved.route.component; - currentPageProps.value = resolved.props; - key.value = router.getCurrentFullPath(); + if (resolved.route.path === currentRoutePath && deepEqual(resolved.props, currentPageProps.value)) return; + + function _() { + currentPageComponent.value = resolved.route.component; + currentPageProps.value = resolved.props; + key.value = router.getCurrentFullPath(); + currentRoutePath = resolved.route.path; + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (prefer.s.animation && document.startViewTransition) { + document.startViewTransition(() => new Promise((res) => { + _(); + nextTick(() => { + res(); + //setTimeout(res, 100); + }); + })); + } else { + _(); + } }); </script> + +<style lang="scss" module> +.root { + height: 100%; + background-color: var(--MI_THEME-bg); +} +</style> diff --git a/packages/frontend/src/di.ts b/packages/frontend/src/di.ts index e9b2c2b650..4977cdbd62 100644 --- a/packages/frontend/src/di.ts +++ b/packages/frontend/src/di.ts @@ -11,4 +11,5 @@ export const DI = { router: Symbol() as InjectionKey<Router>, mock: Symbol() as InjectionKey<boolean>, pageMetadata: Symbol() as InjectionKey<Ref<Record<string, any>>>, + viewId: Symbol() as InjectionKey<string>, }; diff --git a/packages/frontend/src/utility/random-id.ts b/packages/frontend/src/utility/random-id.ts new file mode 100644 index 0000000000..4e5943a97f --- /dev/null +++ b/packages/frontend/src/utility/random-id.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const CHARS = 'abcdefghijklmnopqrstuvwxyz'; // CSSの<custom-ident>などで使われることもあるのでa-z以外使うな + +export function randomId(length = 32, characters = CHARS) { + let result = ''; + const charactersLength = characters.length; + for ( let i = 0; i < length; i++ ) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +}