Compare commits
12 Commits
main
...
view-trans
Author | SHA1 | Date | |
---|---|---|---|
![]() |
76dc7affe0 | ||
![]() |
1c9d9923f4 | ||
![]() |
c8db2043b5 | ||
![]() |
f40c5f27dd | ||
![]() |
386494dd6c | ||
![]() |
f5c946b44d | ||
![]() |
5fe23d3f69 | ||
![]() |
7d86efd087 | ||
![]() |
361f810da8 | ||
![]() |
be16622de2 | ||
![]() |
f930cd7842 | ||
![]() |
f1014bc7f7 |
@@ -11,18 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
@touchmove.passive="touchMove"
|
||||
@touchend.passive="touchEnd"
|
||||
>
|
||||
<Transition
|
||||
:class="[$style.transitionChildren, { [$style.swiping]: isSwipingForClass }]"
|
||||
:enterActiveClass="$style.swipeAnimation_enterActive"
|
||||
:leaveActiveClass="$style.swipeAnimation_leaveActive"
|
||||
:enterFromClass="transitionName === 'swipeAnimationLeft' ? $style.swipeAnimationLeft_enterFrom : $style.swipeAnimationRight_enterFrom"
|
||||
:leaveToClass="transitionName === 'swipeAnimationLeft' ? $style.swipeAnimationLeft_leaveTo : $style.swipeAnimationRight_leaveTo"
|
||||
:style="`--swipe: ${pullDistance}px;`"
|
||||
>
|
||||
<!-- 【注意】slot内の最上位要素に動的にkeyを設定すること -->
|
||||
<!-- 各最上位要素にユニークなkeyの指定がないとTransitionがうまく動きません -->
|
||||
<slot></slot>
|
||||
</Transition>
|
||||
<!-- 【注意】slot内の最上位要素に動的にkeyを設定すること -->
|
||||
<!-- 各最上位要素にユニークなkeyの指定がないとTransitionがうまく動きません -->
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
|
@@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<article v-else :class="$style.article" @contextmenu.stop="onContextmenu">
|
||||
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
|
||||
<MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/>
|
||||
<MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock" :style="{ viewTransitionName: transitionName }"/>
|
||||
<div :class="$style.main">
|
||||
<MkNoteHeader :note="appearNote" :mini="true"/>
|
||||
<MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
|
||||
@@ -177,7 +177,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, onMounted, ref, shallowRef, watch, provide } from 'vue';
|
||||
import { computed, inject, onMounted, ref, shallowRef, watch, provide, reactive, nextTick } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
@@ -223,6 +223,7 @@ import { focusPrev, focusNext } from '@/utility/focus.js';
|
||||
import { getAppearNote } from '@/utility/get-appear-note.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { getPluginHandlers } from '@/plugin.js';
|
||||
import { prepareViewTransition } from '@/page.js';
|
||||
import { DI } from '@/di.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -234,7 +235,18 @@ const props = withDefaults(defineProps<{
|
||||
mock: false,
|
||||
});
|
||||
|
||||
const transitionNames = reactive({
|
||||
avatar: '',
|
||||
});
|
||||
|
||||
provide(DI.mock, props.mock);
|
||||
provide(DI.navHook, (path, flag) => {
|
||||
const names = prepareViewTransition(path);
|
||||
transitionNames.avatar = names.avatar;
|
||||
nextTick(() => {
|
||||
router.push(path, flag);
|
||||
});
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'reaction', emoji: string): void;
|
||||
@@ -853,6 +865,8 @@ function emitUpdReaction(emoji: string, delta: number) {
|
||||
position: sticky !important;
|
||||
top: calc(22px + var(--MI-stickyTop, 0px));
|
||||
left: 0;
|
||||
|
||||
contain: paint;
|
||||
}
|
||||
|
||||
.main {
|
||||
|
@@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
<article :class="$style.note" @contextmenu.stop="onContextmenu">
|
||||
<header :class="$style.noteHeader">
|
||||
<MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/>
|
||||
<MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview :style="{ viewTransitionName: transitionName }"/>
|
||||
<div :class="$style.noteHeaderBody">
|
||||
<div>
|
||||
<MkA v-user-preview="appearNote.user.id" :class="$style.noteHeaderName" :to="userPage(appearNote.user)">
|
||||
@@ -255,6 +255,7 @@ import { isEnabledUrlPreview } from '@/instance.js';
|
||||
import { getAppearNote } from '@/utility/get-appear-note.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { getPluginHandlers } from '@/plugin.js';
|
||||
import { prepareViewTransition } from '@/page.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
@@ -263,6 +264,8 @@ const props = withDefaults(defineProps<{
|
||||
initialTab: 'replies',
|
||||
});
|
||||
|
||||
const transitionName = prepareViewTransition('note-noteDetailed', props.note.id).avatar;
|
||||
|
||||
const inChannel = inject('inChannel', null);
|
||||
|
||||
const note = ref(deepClone(props.note));
|
||||
@@ -669,6 +672,8 @@ function loadConversation() {
|
||||
flex-shrink: 0;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
|
||||
contain: paint;
|
||||
}
|
||||
|
||||
.noteHeaderBody {
|
||||
|
@@ -20,6 +20,7 @@ import * as os from '@/os.js';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { DI } from '@/di.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
to: string;
|
||||
@@ -37,6 +38,7 @@ const el = shallowRef<HTMLElement>();
|
||||
defineExpose({ $el: el });
|
||||
|
||||
const router = useRouter();
|
||||
const navHook = inject(DI.navHook, null);
|
||||
|
||||
const active = computed(() => {
|
||||
if (props.activeClass == null) return false;
|
||||
@@ -99,6 +101,10 @@ function nav(ev: MouseEvent) {
|
||||
return openWindow();
|
||||
}
|
||||
|
||||
router.push(props.to, ev.ctrlKey ? 'forcePage' : null);
|
||||
if (navHook != null) {
|
||||
navHook(props.to, ev.ctrlKey ? 'forcePage' : null);
|
||||
} else {
|
||||
router.push(props.to, ev.ctrlKey ? 'forcePage' : null);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
:exclude="pageCacheController"
|
||||
>
|
||||
<Suspense :timeout="0">
|
||||
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
|
||||
<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)" :style="{ viewTransitionName: viewId }"/>
|
||||
|
||||
<template #fallback>
|
||||
<MkLoading/>
|
||||
@@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { IRouter, Resolved, RouteDef } from '@/nirax.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
@@ -40,6 +41,12 @@ if (router == null) {
|
||||
const currentDepth = inject(DI.routerCurrentDepth, 0);
|
||||
provide(DI.routerCurrentDepth, currentDepth + 1);
|
||||
|
||||
const viewId = uuid();
|
||||
provide(DI.viewId, viewId);
|
||||
|
||||
const viewTransitionId = ref(uuid());
|
||||
provide(DI.viewTransitionId, viewTransitionId);
|
||||
|
||||
function resolveNested(current: Resolved, d = 0): Resolved | null {
|
||||
if (!props.nested) return current;
|
||||
|
||||
@@ -59,18 +66,30 @@ const currentPageComponent = shallowRef('component' in current.route ? current.r
|
||||
const currentPageProps = ref(current.props);
|
||||
const key = ref(router.getCurrentKey() + JSON.stringify(Object.fromEntries(current.props)));
|
||||
|
||||
function onChange({ resolved, key: newKey }) {
|
||||
async function onChange({ resolved, key: newKey }) {
|
||||
const current = resolveNested(resolved);
|
||||
if (current == null || 'redirect' in current.route) return;
|
||||
currentPageComponent.value = current.route.component;
|
||||
currentPageProps.value = current.props;
|
||||
key.value = newKey + JSON.stringify(Object.fromEntries(current.props));
|
||||
|
||||
viewTransitionId.value = uuid();
|
||||
await nextTick();
|
||||
nextTick(() => {
|
||||
// ページ遷移完了後に再びキャッシュを有効化
|
||||
if (clearCacheRequested.value) {
|
||||
clearCacheRequested.value = false;
|
||||
}
|
||||
console.log('onChange', viewTransitionId.value);
|
||||
document.startViewTransition(() => new Promise((res) => {
|
||||
console.log('startViewTransition', viewTransitionId.value);
|
||||
currentPageComponent.value = current.route.component;
|
||||
currentPageProps.value = current.props;
|
||||
key.value = newKey + JSON.stringify(Object.fromEntries(current.props));
|
||||
|
||||
nextTick(async () => {
|
||||
//res();
|
||||
setTimeout(res, 100);
|
||||
|
||||
// ページ遷移完了後に再びキャッシュを有効化
|
||||
if (clearCacheRequested.value) {
|
||||
clearCacheRequested.value = false;
|
||||
}
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,3 +119,31 @@ onBeforeUnmount(() => {
|
||||
router.removeListener('change', onChange);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes fade-out {
|
||||
to { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes slide-from-right {
|
||||
from { transform: translateX(300px); }
|
||||
}
|
||||
|
||||
@keyframes slide-to-left {
|
||||
to { transform: translateX(-300px); }
|
||||
}
|
||||
|
||||
::view-transition-old(v-bind(viewId)) {
|
||||
animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
|
||||
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
|
||||
}
|
||||
|
||||
::view-transition-new(v-bind(viewId)) {
|
||||
animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
|
||||
300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
|
||||
}
|
||||
</style>
|
||||
|
@@ -4,10 +4,13 @@
|
||||
*/
|
||||
|
||||
import type { InjectionKey, Ref } from 'vue';
|
||||
import type { IRouter } from '@/nirax.js';
|
||||
import type { IRouter, RouterFlag } from '@/nirax.js';
|
||||
|
||||
export const DI = {
|
||||
routerCurrentDepth: Symbol() as InjectionKey<number>,
|
||||
router: Symbol() as InjectionKey<IRouter>,
|
||||
viewId: Symbol() as InjectionKey<string>,
|
||||
viewTransitionId: Symbol() as InjectionKey<Ref<string>>,
|
||||
mock: Symbol() as InjectionKey<boolean>,
|
||||
navHook: Symbol() as InjectionKey<(path: string, flag?: RouterFlag) => void>,
|
||||
};
|
||||
|
@@ -37,7 +37,7 @@ interface RouteDefWithRedirect extends RouteDefBase {
|
||||
|
||||
export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
|
||||
|
||||
export type RouterFlag = 'forcePage';
|
||||
export type RouterFlag = 'forcePage' | null;
|
||||
|
||||
type ParsedPath = (string | {
|
||||
name: string;
|
||||
|
@@ -4,8 +4,9 @@
|
||||
*/
|
||||
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { inject, isRef, onActivated, onBeforeUnmount, provide, ref, toValue, watch } from 'vue';
|
||||
import { computed, inject, isRef, onActivated, onBeforeUnmount, provide, ref, toValue, watch } from 'vue';
|
||||
import type { MaybeRefOrGetter, Ref } from 'vue';
|
||||
import { DI } from '@/di.js';
|
||||
|
||||
export type PageMetadata = {
|
||||
title: string;
|
||||
@@ -69,3 +70,12 @@ export const injectReactiveMetadata = (): Ref<PageMetadata | null> => {
|
||||
const metadataRef = getMetadata();
|
||||
return isRef(metadataRef) ? metadataRef : ref(null);
|
||||
};
|
||||
|
||||
export function prepareViewTransition(type: string, id: string) {
|
||||
const viewId = inject(DI.viewId);
|
||||
const viewTransitionId = inject(DI.viewTransitionId);
|
||||
return {
|
||||
avatar: computed(() => 'adsfsdfsfg' + viewId + viewTransitionId.value + id),
|
||||
//avatar: computed(() => 'adsfsdfsfg' + id),
|
||||
};
|
||||
}
|
||||
|
@@ -8,40 +8,38 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div>
|
||||
<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="note">
|
||||
<div v-if="showNext" class="_margin">
|
||||
<MkNotes class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/>
|
||||
</div>
|
||||
<div v-if="note">
|
||||
<div v-if="showNext" class="_margin">
|
||||
<MkNotes class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/>
|
||||
</div>
|
||||
|
||||
<div class="_margin">
|
||||
<div v-if="!showNext" class="_buttons" :class="$style.loadNext">
|
||||
<MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showNext = 'channel'"><i class="ti ti-chevron-up"></i> <i class="ti ti-device-tv"></i></MkButton>
|
||||
<MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ti ti-chevron-up"></i> <i class="ti ti-user"></i></MkButton>
|
||||
</div>
|
||||
<div class="_margin _gaps_s">
|
||||
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
|
||||
<MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note"/>
|
||||
</div>
|
||||
<div v-if="clips && clips.length > 0" class="_margin">
|
||||
<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
|
||||
<div class="_gaps">
|
||||
<MkClipPreview v-for="item in clips" :key="item.id" :clip="item"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!showPrev" class="_buttons" :class="$style.loadPrev">
|
||||
<MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showPrev = 'channel'"><i class="ti ti-chevron-down"></i> <i class="ti ti-device-tv"></i></MkButton>
|
||||
<MkButton rounded :class="$style.loadButton" @click="showPrev = 'user'"><i class="ti ti-chevron-down"></i> <i class="ti ti-user"></i></MkButton>
|
||||
<div class="_margin">
|
||||
<div v-if="!showNext" class="_buttons" :class="$style.loadNext">
|
||||
<MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showNext = 'channel'"><i class="ti ti-chevron-up"></i> <i class="ti ti-device-tv"></i></MkButton>
|
||||
<MkButton rounded :class="$style.loadButton" @click="showNext = 'user'"><i class="ti ti-chevron-up"></i> <i class="ti ti-user"></i></MkButton>
|
||||
</div>
|
||||
<div class="_margin _gaps_s">
|
||||
<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
|
||||
<MkNoteDetailed :key="note.id" v-model:note="note" :initialTab="initialTab" :class="$style.note"/>
|
||||
</div>
|
||||
<div v-if="clips && clips.length > 0" class="_margin">
|
||||
<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
|
||||
<div class="_gaps">
|
||||
<MkClipPreview v-for="item in clips" :key="item.id" :clip="item"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showPrev" class="_margin">
|
||||
<MkNotes class="" :pagination="showPrev === 'channel' ? prevChannelPagination : prevUserPagination" :noGap="true"/>
|
||||
<div v-if="!showPrev" class="_buttons" :class="$style.loadPrev">
|
||||
<MkButton v-if="note.channelId" rounded :class="$style.loadButton" @click="showPrev = 'channel'"><i class="ti ti-chevron-down"></i> <i class="ti ti-device-tv"></i></MkButton>
|
||||
<MkButton rounded :class="$style.loadButton" @click="showPrev = 'user'"><i class="ti ti-chevron-down"></i> <i class="ti ti-user"></i></MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetchNote()"/>
|
||||
<MkLoading v-else/>
|
||||
</Transition>
|
||||
|
||||
<div v-if="showPrev" class="_margin">
|
||||
<MkNotes class="" :pagination="showPrev === 'channel' ? prevChannelPagination : prevUserPagination" :noGap="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetchNote()"/>
|
||||
<MkLoading v-else/>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
|
@@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSpacer :contentMax="800">
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div v-if="tab === 'all'" key="all">
|
||||
<div style="view-transition-name: a; contain: paint; margin: 64px;">BBBBBBBBB</div>
|
||||
<XNotifications :class="$style.notifications" :excludeTypes="excludeTypes"/>
|
||||
</div>
|
||||
<div v-else-if="tab === 'mentions'" key="mention">
|
||||
@@ -24,13 +25,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { notificationTypes } from '@@/js/const.js';
|
||||
import XNotifications from '@/components/MkNotifications.vue';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { notificationTypes } from '@@/js/const.js';
|
||||
|
||||
const tab = ref('all');
|
||||
const includeTypes = ref<string[] | null>(null);
|
||||
|
@@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkSpacer :contentMax="800">
|
||||
<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
|
||||
<div :key="src" ref="rootEl">
|
||||
<div style="view-transition-name: a; contain: paint;">AAAAAAAAAA</div>
|
||||
<MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
|
||||
{{ i18n.ts._timelineDescription[src] }}
|
||||
</MkInfo>
|
||||
|
Reference in New Issue
Block a user