Add view caching and preserve folder/editor UI state
This commit is contained in:
+15
-3
@@ -24,14 +24,21 @@
|
||||
/>
|
||||
</header>
|
||||
<main class="transition-wrapper">
|
||||
<RouterView v-slot="{ Component }">
|
||||
<Transition
|
||||
:name="routeTransitionName"
|
||||
@after-enter="store.transitionDirection = 'none'"
|
||||
>
|
||||
<div :key="routeViewKey" class="explorer-content">
|
||||
<RouterView :path="path.pathList" :query="path.query" />
|
||||
</div>
|
||||
<KeepAlive>
|
||||
<component
|
||||
:is="Component"
|
||||
:key="routeViewKey"
|
||||
class="explorer-content"
|
||||
v-bind="routeViewProps"
|
||||
/>
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
</RouterView>
|
||||
</main>
|
||||
<footer v-if="store.selected.size || store.uprogress.total || store.dprogress.total">
|
||||
<SelectionToolbar :path="path.pathList" />
|
||||
@@ -103,6 +110,11 @@ const routeViewKey = computed(() => {
|
||||
const route = Router.currentRoute.value
|
||||
return route.name === 'editor' ? route.path : String(route.name ?? route.path)
|
||||
})
|
||||
const routeViewProps = computed(() =>
|
||||
path.value.isEditorPath
|
||||
? {}
|
||||
: { path: path.value.pathList, query: path.value.query }
|
||||
)
|
||||
watch(
|
||||
() => path.value.path,
|
||||
() => {
|
||||
|
||||
@@ -23,6 +23,8 @@ import type { SortOrder } from '@/utils/docsort'
|
||||
import { createKeyboardFollowScroll } from '@/utils/keyboardFollowScroll'
|
||||
import ContextMenu from '@imengyu/vue3-context-menu'
|
||||
import {
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
computed,
|
||||
nextTick,
|
||||
onMounted,
|
||||
@@ -170,6 +172,7 @@ const onImgLoad = (e: Event) => {
|
||||
}
|
||||
const updateColumns = () => {
|
||||
if (!gallery.value) return
|
||||
if (gallery.value.getBoundingClientRect().width <= 0) return
|
||||
const style = getComputedStyle(gallery.value)
|
||||
const templates = style.gridTemplateColumns
|
||||
.split(' ')
|
||||
@@ -412,6 +415,19 @@ watchEffect(() => {
|
||||
}
|
||||
})
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
const attachGalleryObservers = () => {
|
||||
if (!gallery.value || resizeObserver) return
|
||||
resizeObserver = new ResizeObserver(updateColumns)
|
||||
resizeObserver.observe(gallery.value)
|
||||
gallery.value.addEventListener('load', onImgLoad, { capture: true })
|
||||
}
|
||||
|
||||
const detachGalleryObservers = () => {
|
||||
resizeObserver?.disconnect()
|
||||
resizeObserver = null
|
||||
gallery.value?.removeEventListener('load', onImgLoad, { capture: true })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const active = document.querySelector('.cursor') as HTMLElement | null
|
||||
if (active) {
|
||||
@@ -419,16 +435,20 @@ onMounted(() => {
|
||||
}
|
||||
updateColumns()
|
||||
seedFromDocs()
|
||||
if (gallery.value) {
|
||||
resizeObserver = new ResizeObserver(updateColumns)
|
||||
resizeObserver.observe(gallery.value)
|
||||
gallery.value.addEventListener('load', onImgLoad, { capture: true })
|
||||
}
|
||||
attachGalleryObservers()
|
||||
})
|
||||
onActivated(() => {
|
||||
nextTick(() => {
|
||||
updateColumns()
|
||||
attachGalleryObservers()
|
||||
})
|
||||
})
|
||||
onDeactivated(() => {
|
||||
detachGalleryObservers()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
keyboardFollowScroll.cancel()
|
||||
resizeObserver?.disconnect()
|
||||
gallery.value?.removeEventListener('load', onImgLoad, { capture: true })
|
||||
detachGalleryObservers()
|
||||
})
|
||||
|
||||
// Re-seed aspect ratios whenever docs update (e.g., ar patch from server)
|
||||
|
||||
@@ -2,24 +2,20 @@
|
||||
<div class="transition-wrapper">
|
||||
<Transition
|
||||
:name="transitionName"
|
||||
@after-enter="store.transitionDirection = 'none'"
|
||||
@after-enter="onAfterEnter"
|
||||
>
|
||||
<div :key="folderPath" class="explorer-content">
|
||||
<Gallery
|
||||
v-if="store.prefs.gallery"
|
||||
<KeepAlive>
|
||||
<component
|
||||
:is="store.prefs.gallery ? Gallery : FileExplorer"
|
||||
:key="cacheKey"
|
||||
ref="fileExplorer"
|
||||
class="explorer-content"
|
||||
:path="props.path"
|
||||
:documents="documents"
|
||||
/>
|
||||
<FileExplorer
|
||||
v-else
|
||||
ref="fileExplorer"
|
||||
:path="props.path"
|
||||
:documents="documents"
|
||||
/>
|
||||
<EmptyFolder :documents="documents" :path="props.path" />
|
||||
</div>
|
||||
</KeepAlive>
|
||||
</Transition>
|
||||
<EmptyFolder :documents="documents" :path="props.path" />
|
||||
</div>
|
||||
<div v-if="store.searchLoading" class="search-loading">Searching...</div>
|
||||
</template>
|
||||
@@ -31,7 +27,7 @@ import { getDocuments } from '@/stores/documentStore'
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { collator } from '@/utils'
|
||||
import { sorted, sortedGrouped } from '@/utils/docsort'
|
||||
import { computed, ref, watch, watchEffect } from 'vue'
|
||||
import { computed, nextTick, ref, watch, watchEffect } from 'vue'
|
||||
|
||||
const store = useMainStore()
|
||||
const fileExplorer = ref()
|
||||
@@ -42,6 +38,7 @@ const props = defineProps<{
|
||||
|
||||
// Folder path for component keys - only recreate component when folder changes, not search
|
||||
const folderPath = computed(() => props.path.join('/'))
|
||||
const cacheKey = computed(() => `${store.prefs.gallery ? 'gallery' : 'list'}:${folderPath.value}`)
|
||||
|
||||
const transitionName = computed(() => {
|
||||
if (store.transitionDirection === 'forward') return 'slide-forward'
|
||||
@@ -49,6 +46,22 @@ const transitionName = computed(() => {
|
||||
return ''
|
||||
})
|
||||
|
||||
const folderScrollTop = new Map<string, number>()
|
||||
const scrollKey = (path: string) => path || '/'
|
||||
const getMainScroller = () => document.querySelector('main') as HTMLElement | null
|
||||
|
||||
const restoreScroll = (path: string) => {
|
||||
const scroller = getMainScroller()
|
||||
if (!scroller) return
|
||||
const top = folderScrollTop.get(scrollKey(path)) ?? 0
|
||||
scroller.scrollTop = top
|
||||
}
|
||||
|
||||
const onAfterEnter = () => {
|
||||
store.transitionDirection = 'none'
|
||||
restoreScroll(folderPath.value)
|
||||
}
|
||||
|
||||
// Handle route-based search changes (back/forward navigation, direct URL)
|
||||
// Skip if store.query already matches (means we triggered this via typing)
|
||||
watch(
|
||||
@@ -101,6 +114,19 @@ watchEffect(() => {
|
||||
store.fileExplorer = fileExplorer.value
|
||||
})
|
||||
|
||||
watch(
|
||||
folderPath,
|
||||
async (path, oldPath) => {
|
||||
const scroller = getMainScroller()
|
||||
if (scroller && oldPath !== undefined) {
|
||||
folderScrollTop.set(scrollKey(oldPath), scroller.scrollTop)
|
||||
}
|
||||
await nextTick()
|
||||
requestAnimationFrame(() => restoreScroll(path))
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// Only auto-switch gallery mode when entering a new folder or on initial file list load
|
||||
watch(
|
||||
[() => props.path.join('/'), () => store.documentCount],
|
||||
|
||||
@@ -18,8 +18,16 @@ import { EditorView, keymap } from '@codemirror/view'
|
||||
import { basicSetup } from 'codemirror'
|
||||
import { apiFetch } from '@/repositories/Client'
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { onBeforeRouteLeave, useRoute } from 'vue-router'
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref
|
||||
} from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useMainStore()
|
||||
@@ -50,21 +58,32 @@ const languageCompartment = new Compartment()
|
||||
|
||||
const dirty = computed(() => content.value !== original.value)
|
||||
|
||||
onBeforeRouteLeave((_to, _from, next) => {
|
||||
if (!dirty.value) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
const discard = window.confirm('You have unsaved changes. Discard them?')
|
||||
next(discard)
|
||||
})
|
||||
|
||||
const beforeUnload = (event: BeforeUnloadEvent) => {
|
||||
if (!dirty.value) return
|
||||
event.preventDefault()
|
||||
event.returnValue = ''
|
||||
}
|
||||
|
||||
let beforeUnloadActive = false
|
||||
|
||||
const activateEditorBindings = () => {
|
||||
store.editorSave = save
|
||||
if (!beforeUnloadActive) {
|
||||
window.addEventListener('beforeunload', beforeUnload)
|
||||
beforeUnloadActive = true
|
||||
}
|
||||
}
|
||||
|
||||
const deactivateEditorBindings = () => {
|
||||
if (store.editorSave === save) {
|
||||
store.editorSave = null
|
||||
}
|
||||
if (beforeUnloadActive) {
|
||||
window.removeEventListener('beforeunload', beforeUnload)
|
||||
beforeUnloadActive = false
|
||||
}
|
||||
}
|
||||
|
||||
const detectLanguage = async () => {
|
||||
const language = LanguageDescription.matchFilename(languages, filename.value)
|
||||
if (!language) return []
|
||||
@@ -129,8 +148,7 @@ const save = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
store.editorSave = save
|
||||
window.addEventListener('beforeunload', beforeUnload)
|
||||
activateEditorBindings()
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
@@ -158,13 +176,18 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
activateEditorBindings()
|
||||
})
|
||||
|
||||
onDeactivated(() => {
|
||||
deactivateEditorBindings()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (store.editorSave === save) {
|
||||
store.editorSave = null
|
||||
}
|
||||
deactivateEditorBindings()
|
||||
editorView?.destroy()
|
||||
editorView = null
|
||||
window.removeEventListener('beforeunload', beforeUnload)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user