Add view caching and preserve folder/editor UI state

This commit is contained in:
2026-05-07 02:12:03 +00:00
parent e2097a1563
commit 6d7f44bd88
4 changed files with 126 additions and 45 deletions
+15 -3
View File
@@ -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,
() => {
+27 -7
View File
@@ -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)
+39 -13
View File
@@ -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],
+40 -17
View File
@@ -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>