feat(frontend): add simultaneous slide transitions on directory navigation
- Detect navigation direction (forward/backward) via router beforeEach guard - Store transition direction in Pinia for cross-component access - Wrap ExplorerView content in a CSS grid transition wrapper so old and new views overlap in the same grid cell during animation - Add slide-forward/slide-backward transition classes with translate3d for GPU-accelerated, simultaneous enter/leave without gaps - Keep fixed-position search loader outside the transition
This commit is contained in:
@@ -57,6 +57,66 @@
|
|||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Directory navigation slide transitions */
|
||||||
|
.transition-wrapper {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explorer-content {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-forward-enter-active,
|
||||||
|
.slide-backward-enter-active {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-forward-leave-active,
|
||||||
|
.slide-backward-leave-active {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-forward-enter-active,
|
||||||
|
.slide-forward-leave-active,
|
||||||
|
.slide-backward-enter-active,
|
||||||
|
.slide-backward-leave-active {
|
||||||
|
transition: transform 0.22s cubic-bezier(0.32, 0.72, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-forward-enter-from {
|
||||||
|
transform: translate3d(100%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-forward-enter-to {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-forward-leave-from {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-forward-leave-to {
|
||||||
|
transform: translate3d(-100%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-backward-enter-from {
|
||||||
|
transform: translate3d(-100%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-backward-enter-to {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-backward-leave-from {
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-backward-leave-to {
|
||||||
|
transform: translate3d(100%, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
:root {
|
:root {
|
||||||
--primary-color: black;
|
--primary-color: black;
|
||||||
@@ -206,6 +266,8 @@ main {
|
|||||||
min-height: 0; /* Allow flex child to shrink below content size */
|
min-height: 0; /* Allow flex child to shrink below content size */
|
||||||
padding-bottom: 3em; /* convenience space on the bottom */
|
padding-bottom: 3em; /* convenience space on the bottom */
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
position: relative;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
|
import { useMainStore } from '@/stores/main'
|
||||||
import ExplorerView from '@/views/ExplorerView.vue'
|
import ExplorerView from '@/views/ExplorerView.vue'
|
||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
|
||||||
|
function getPathDepth(path: string): number {
|
||||||
|
const pathPart = decodeURIComponent(path).split('//')[0] ?? ''
|
||||||
|
return pathPart.split('/').filter(Boolean).length
|
||||||
|
}
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
@@ -12,4 +18,17 @@ const router = createRouter({
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from) => {
|
||||||
|
const store = useMainStore()
|
||||||
|
const toDepth = getPathDepth(to.path)
|
||||||
|
const fromDepth = getPathDepth(from.path)
|
||||||
|
if (toDepth > fromDepth) {
|
||||||
|
store.transitionDirection = 'forward'
|
||||||
|
} else if (toDepth < fromDepth) {
|
||||||
|
store.transitionDirection = 'backward'
|
||||||
|
} else {
|
||||||
|
store.transitionDirection = 'none'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const useMainStore = defineStore('main', {
|
|||||||
privileged: false as boolean,
|
privileged: false as boolean,
|
||||||
isLoggedIn: false as boolean
|
isLoggedIn: false as boolean
|
||||||
},
|
},
|
||||||
|
transitionDirection: 'none' as 'forward' | 'backward' | 'none',
|
||||||
space: {
|
space: {
|
||||||
disk: 0,
|
disk: 0,
|
||||||
free: 0,
|
free: 0,
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<Gallery
|
<div class="transition-wrapper">
|
||||||
v-if="store.prefs.gallery"
|
<Transition
|
||||||
ref="fileExplorer"
|
:name="transitionName"
|
||||||
:key="`gallery-${folderPath}`"
|
@after-enter="store.transitionDirection = 'none'"
|
||||||
:path="props.path"
|
>
|
||||||
:documents="documents"
|
<div :key="folderPath" class="explorer-content">
|
||||||
/>
|
<Gallery
|
||||||
<FileExplorer
|
v-if="store.prefs.gallery"
|
||||||
v-else
|
ref="fileExplorer"
|
||||||
ref="fileExplorer"
|
:path="props.path"
|
||||||
:key="`explorer-${folderPath}`"
|
:documents="documents"
|
||||||
:path="props.path"
|
/>
|
||||||
:documents="documents"
|
<FileExplorer
|
||||||
/>
|
v-else
|
||||||
|
ref="fileExplorer"
|
||||||
|
:path="props.path"
|
||||||
|
:documents="documents"
|
||||||
|
/>
|
||||||
|
<EmptyFolder :documents="documents" :path="props.path" />
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
<div v-if="store.searchLoading" class="search-loading">Searching...</div>
|
<div v-if="store.searchLoading" class="search-loading">Searching...</div>
|
||||||
<EmptyFolder :documents=documents :path=props.path />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import FileExplorer from '@/components/FileExplorer.vue'
|
import FileExplorer from '@/components/FileExplorer.vue'
|
||||||
|
import Gallery from '@/components/Gallery.vue'
|
||||||
import { getDocuments } from '@/stores/documentStore'
|
import { getDocuments } from '@/stores/documentStore'
|
||||||
import { useMainStore } from '@/stores/main'
|
import { useMainStore } from '@/stores/main'
|
||||||
import { collator } from '@/utils'
|
import { collator } from '@/utils'
|
||||||
@@ -35,6 +43,12 @@ const props = defineProps<{
|
|||||||
// Folder path for component keys - only recreate component when folder changes, not search
|
// Folder path for component keys - only recreate component when folder changes, not search
|
||||||
const folderPath = computed(() => props.path.join('/'))
|
const folderPath = computed(() => props.path.join('/'))
|
||||||
|
|
||||||
|
const transitionName = computed(() => {
|
||||||
|
if (store.transitionDirection === 'forward') return 'slide-forward'
|
||||||
|
if (store.transitionDirection === 'backward') return 'slide-backward'
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
// Handle route-based search changes (back/forward navigation, direct URL)
|
// Handle route-based search changes (back/forward navigation, direct URL)
|
||||||
// Skip if store.query already matches (means we triggered this via typing)
|
// Skip if store.query already matches (means we triggered this via typing)
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
Reference in New Issue
Block a user