Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
718d46e3f9 | ||
|
|
92d9c40a28 | ||
|
|
4f646fb344 | ||
|
|
d6304d0029 | ||
|
|
77e35cf0fc | ||
|
|
bf8a049b92 | ||
|
|
6d7f44bd88 | ||
|
|
e2097a1563 | ||
|
|
b864936eaa |
+1
-3
@@ -1293,9 +1293,7 @@ def _token_belongs_to_user(token, username, sso_user_id):
|
|||||||
|
|
||||||
def _is_anonymous_share_token(token: config.Token) -> bool:
|
def _is_anonymous_share_token(token: config.Token) -> bool:
|
||||||
return (
|
return (
|
||||||
sharefs.is_share_token(token)
|
sharefs.is_share_token(token) and not token.username and not token.sso_user_id
|
||||||
and not token.username
|
|
||||||
and not token.sso_user_id
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+52
-14
@@ -29,7 +29,13 @@
|
|||||||
@after-enter="store.transitionDirection = 'none'"
|
@after-enter="store.transitionDirection = 'none'"
|
||||||
>
|
>
|
||||||
<div :key="routeViewKey" class="explorer-content">
|
<div :key="routeViewKey" class="explorer-content">
|
||||||
<RouterView :path="path.pathList" :query="path.query" />
|
<KeepAlive>
|
||||||
|
<component
|
||||||
|
:is="routeViewComponent"
|
||||||
|
:key="routeViewKey"
|
||||||
|
v-bind="routeViewProps"
|
||||||
|
/>
|
||||||
|
</KeepAlive>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</main>
|
</main>
|
||||||
@@ -43,10 +49,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type HeaderMain from '@/components/HeaderMain.vue'
|
import type HeaderMain from '@/components/HeaderMain.vue'
|
||||||
import { loadSession, watchConnect, watchDisconnect } from '@/repositories/WS'
|
import { loadSession, watchConnect, watchDisconnect } from '@/repositories/WS'
|
||||||
|
import { getDocuments } from '@/stores/documentStore'
|
||||||
import { useMainStore } from '@/stores/main'
|
import { useMainStore } from '@/stores/main'
|
||||||
import type { ComputedRef } from 'vue'
|
import type { ComputedRef } from 'vue'
|
||||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||||
import { RouterView } from 'vue-router'
|
|
||||||
|
|
||||||
import Router from '@/router/index'
|
import Router from '@/router/index'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
@@ -57,9 +63,12 @@ import type SettingsModalVue from './components/SettingsModal.vue'
|
|||||||
import UserManagementModal from './components/UserManagementModal.vue'
|
import UserManagementModal from './components/UserManagementModal.vue'
|
||||||
import UserTokensModal from './components/UserTokensModal.vue'
|
import UserTokensModal from './components/UserTokensModal.vue'
|
||||||
import type { SortOrder } from './utils/docsort'
|
import type { SortOrder } from './utils/docsort'
|
||||||
|
import ExplorerView from './views/ExplorerView.vue'
|
||||||
|
import TextEditorView from './views/TextEditorView.vue'
|
||||||
|
|
||||||
interface Path {
|
interface Path {
|
||||||
path: string
|
path: string
|
||||||
|
canonicalPath: string
|
||||||
isEditorPath: boolean
|
isEditorPath: boolean
|
||||||
pathList: string[]
|
pathList: string[]
|
||||||
breadcrumbPathList: string[]
|
breadcrumbPathList: string[]
|
||||||
@@ -67,26 +76,40 @@ interface Path {
|
|||||||
query: string
|
query: string
|
||||||
}
|
}
|
||||||
const store = useMainStore()
|
const store = useMainStore()
|
||||||
|
|
||||||
|
const getDocByPath = (fullPath: string) =>
|
||||||
|
getDocuments().find(
|
||||||
|
doc => (doc.loc ? `${doc.loc}/${doc.name}` : doc.name) === fullPath
|
||||||
|
)
|
||||||
|
|
||||||
const path: ComputedRef<Path> = computed(() => {
|
const path: ComputedRef<Path> = computed(() => {
|
||||||
const p = decodeURIComponent(Router.currentRoute.value.path).split('//')
|
const p = decodeURIComponent(Router.currentRoute.value.path).split('//')
|
||||||
const routePathList = (p[0] ?? '').split('/').filter(value => value !== '')
|
const rawPath = p[0] ?? ''
|
||||||
|
const routePathList = rawPath.split('/').filter(value => value !== '')
|
||||||
const query = p.slice(1).join('//')
|
const query = p.slice(1).join('//')
|
||||||
const isEditorPath = routePathList[0] === 'edit'
|
const fullPath = routePathList.join('/')
|
||||||
const pathList = isEditorPath ? routePathList.slice(1, -1) : routePathList
|
// Access docVersion to make route mode reactive to tree updates
|
||||||
const breadcrumbPathList = isEditorPath
|
void store.docVersion
|
||||||
? routePathList.slice(1)
|
const doc = fullPath ? getDocByPath(fullPath) : null
|
||||||
: routePathList
|
const isEditorPath = !!(doc && !doc.dir && doc.text)
|
||||||
|
const canonicalBase = !fullPath ? '/' : doc?.dir ? `/${fullPath}/` : `/${fullPath}`
|
||||||
|
const canonicalPath = query
|
||||||
|
? `${rawPath}//${query}` // keep search URL shape untouched
|
||||||
|
: canonicalBase
|
||||||
|
const pathList = isEditorPath ? routePathList.slice(0, -1) : routePathList
|
||||||
|
const breadcrumbPathList = routePathList
|
||||||
const breadcrumbLinks = isEditorPath
|
const breadcrumbLinks = isEditorPath
|
||||||
? [
|
? [
|
||||||
'/',
|
'/',
|
||||||
...routePathList
|
...routePathList
|
||||||
.slice(1, -1)
|
.slice(0, -1)
|
||||||
.map((_, index) => `/${routePathList.slice(1, index + 2).join('/')}/`),
|
.map((_, index) => `/${routePathList.slice(0, index + 1).join('/')}/`),
|
||||||
`/${routePathList.join('/')}`
|
`/${fullPath}`
|
||||||
]
|
]
|
||||||
: undefined
|
: undefined
|
||||||
return {
|
return {
|
||||||
path: p[0] ?? '',
|
path: rawPath,
|
||||||
|
canonicalPath,
|
||||||
isEditorPath,
|
isEditorPath,
|
||||||
pathList,
|
pathList,
|
||||||
breadcrumbPathList,
|
breadcrumbPathList,
|
||||||
@@ -99,10 +122,25 @@ const routeTransitionName = computed(() => {
|
|||||||
if (store.transitionDirection === 'backward') return 'slide-backward'
|
if (store.transitionDirection === 'backward') return 'slide-backward'
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
|
const routeViewComponent = computed(() =>
|
||||||
|
path.value.isEditorPath ? TextEditorView : ExplorerView
|
||||||
|
)
|
||||||
const routeViewKey = computed(() => {
|
const routeViewKey = computed(() => {
|
||||||
const route = Router.currentRoute.value
|
return path.value.isEditorPath ? `editor:${path.value.path}` : 'explorer'
|
||||||
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.canonicalPath,
|
||||||
|
canonical => {
|
||||||
|
const current = decodeURIComponent(Router.currentRoute.value.path)
|
||||||
|
if (canonical && current !== canonical) {
|
||||||
|
Router.replace(canonical.replaceAll('?', '%3F').replaceAll('#', '%23'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
watch(
|
watch(
|
||||||
() => path.value.path,
|
() => path.value.path,
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -62,10 +62,12 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-template-rows: 1fr;
|
grid-template-rows: 1fr;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.explorer-content {
|
.explorer-content {
|
||||||
grid-area: 1 / 1;
|
grid-area: 1 / 1;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.slide-forward-enter-active,
|
.slide-forward-enter-active,
|
||||||
|
|||||||
@@ -122,8 +122,7 @@ watchEffect(() => {
|
|||||||
if (!same) {
|
if (!same) {
|
||||||
longest.value = props.path
|
longest.value = props.path
|
||||||
longestLinks.value = currentLinks
|
longestLinks.value = currentLinks
|
||||||
}
|
} else if (props.path.length > longcut.length) {
|
||||||
else if (props.path.length > longcut.length) {
|
|
||||||
longest.value = longcut.concat(props.path.slice(longcut.length))
|
longest.value = longcut.concat(props.path.slice(longcut.length))
|
||||||
longestLinks.value.splice(0, currentLinks.length, ...currentLinks)
|
longestLinks.value.splice(0, currentLinks.length, ...currentLinks)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -104,7 +104,9 @@ const showOtherCategory = computed(() => {
|
|||||||
return !!s.disk && otherBytes.value / s.disk >= 0.01
|
return !!s.disk && otherBytes.value / s.disk >= 0.01
|
||||||
})
|
})
|
||||||
const freeSliceBytes = computed(() =>
|
const freeSliceBytes = computed(() =>
|
||||||
showOtherCategory.value ? store.space.free : Math.max(0, store.space.disk - store.space.allocated)
|
showOtherCategory.value
|
||||||
|
? store.space.free
|
||||||
|
: Math.max(0, store.space.disk - store.space.allocated)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Calculate max label length based on angular gap to neighbor labels
|
// Calculate max label length based on angular gap to neighbor labels
|
||||||
@@ -295,7 +297,11 @@ const freeLabelPath = computed(() =>
|
|||||||
createArcPath(adjustedLabelAngles.value.free!, 'free', 4)
|
createArcPath(adjustedLabelAngles.value.free!, 'free', 4)
|
||||||
)
|
)
|
||||||
const otherLabelPath = computed(() =>
|
const otherLabelPath = computed(() =>
|
||||||
createArcPath(adjustedLabelAngles.value.other ?? sectorInfo.value.other.angle, 'other', 5)
|
createArcPath(
|
||||||
|
adjustedLabelAngles.value.other ?? sectorInfo.value.other.angle,
|
||||||
|
'other',
|
||||||
|
5
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleClick = () => (isExpanded.value ? collapse() : expand())
|
const handleClick = () => (isExpanded.value ? collapse() : expand())
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!props.path || documents.length === 0" class="empty-container">
|
<div v-if="showEmpty" class="empty-container">
|
||||||
<component :is="cog" :class="['cog', { stopped: store.dialog === 'accessdenied' || store.authInProgress }]"/>
|
<component :is="cog" :class="['cog', { stopped: store.dialog === 'accessdenied' || store.authInProgress }]"/>
|
||||||
<p v-if="store.dialog === 'accessdenied'">Access Denied</p>
|
<p v-if="store.dialog === 'accessdenied'">Access Denied</p>
|
||||||
<p v-else-if="!store.connected">No Connection</p>
|
<p v-else-if="!store.connected">No Connection</p>
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
import { Cog } from '@/assets/svg'
|
import { Cog } from '@/assets/svg'
|
||||||
import { useMainStore } from '@/stores/main'
|
import { useMainStore } from '@/stores/main'
|
||||||
import { exists } from '@/utils/fileutil'
|
import { exists } from '@/utils/fileutil'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const cog = Cog
|
const cog = Cog
|
||||||
const store = useMainStore()
|
const store = useMainStore()
|
||||||
@@ -21,9 +22,29 @@ const props = defineProps<{
|
|||||||
path: string[]
|
path: string[]
|
||||||
documents: Document[]
|
documents: Document[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const showEmpty = computed(() => {
|
||||||
|
const loc = props.path.join('/')
|
||||||
|
const hasVisibleGhost = store.ghosts.some(g => {
|
||||||
|
const full = g.loc ? `${g.loc}/${g.name}` : g.name
|
||||||
|
return g.loc === loc && !store.hiddenPaths.has(full)
|
||||||
|
})
|
||||||
|
|
||||||
|
return !props.path || (props.documents.length === 0 && !hasVisibleGhost)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 2rem;
|
||||||
|
text-shadow: 0 0 .3rem #000, 0 0 2rem #0008;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
@keyframes rotate {
|
@keyframes rotate {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<div class="file-explorer">
|
||||||
<table v-if="props.documents.length || editing">
|
<table v-if="props.documents.length || editing">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -15,7 +16,7 @@
|
|||||||
<tr v-if="editing?.key === 'new'" :class="editing.dir ? 'folder' : 'file'">
|
<tr v-if="editing?.key === 'new'" :class="editing.dir ? 'folder' : 'file'">
|
||||||
<td class="selection"></td>
|
<td class="selection"></td>
|
||||||
<td class="name">
|
<td class="name">
|
||||||
<FileRenameInput :doc="editing" :rename="createItem" :exit="() => {editing = null}" />
|
<FileRenameInput :doc="editing" :rename="createItem" :exit="exitEditing" />
|
||||||
</td>
|
</td>
|
||||||
<FileModified :doc=editing :now=nowkey />
|
<FileModified :doc=editing :now=nowkey />
|
||||||
<FileSize :doc=editing />
|
<FileSize :doc=editing />
|
||||||
@@ -46,7 +47,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="name">
|
<td class="name">
|
||||||
<template v-if="editing === doc">
|
<template v-if="editing === doc">
|
||||||
<FileRenameInput :doc="doc" :rename="rename" :exit="() => {editing = null}" />
|
<FileRenameInput :doc="doc" :rename="rename" :exit="exitEditing" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a :href="doc.text ? doc.editurl : doc.url" tabindex=-1 @contextmenu.stop @focus.stop="store.cursor = doc.key">
|
<a :href="doc.text ? doc.editurl : doc.url" tabindex=-1 @contextmenu.stop @focus.stop="store.cursor = doc.key">
|
||||||
@@ -69,6 +70,8 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<EmptyFolder v-else :documents="documents" :path="props.path" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -81,11 +84,12 @@ import ContextMenu from '@imengyu/vue3-context-menu'
|
|||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
onDeactivated,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
ref,
|
ref,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
watchEffect
|
watch
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import FileRenameInput from './FileRenameInput.vue'
|
import FileRenameInput from './FileRenameInput.vue'
|
||||||
@@ -189,6 +193,9 @@ const pageMove = (direction: 1 | -1, ev: KeyboardEvent) => {
|
|||||||
|
|
||||||
// File rename
|
// File rename
|
||||||
const editing = shallowRef<Doc | null>(null)
|
const editing = shallowRef<Doc | null>(null)
|
||||||
|
const exitEditing = () => {
|
||||||
|
editing.value = null
|
||||||
|
}
|
||||||
const rename = async (doc: Doc, newName: string) => {
|
const rename = async (doc: Doc, newName: string) => {
|
||||||
const oldName = doc.name
|
const oldName = doc.name
|
||||||
doc.name = newName // We should get an update from watch but this is quicker
|
doc.name = newName // We should get an update from watch but this is quicker
|
||||||
@@ -243,7 +250,7 @@ defineExpose({
|
|||||||
const docs = props.documents
|
const docs = props.documents
|
||||||
if (docs.length > 0) {
|
if (docs.length > 0) {
|
||||||
store.cursor = docs[0]!.key
|
store.cursor = docs[0]!.key
|
||||||
// Also focus the element directly (watchEffect won't trigger if cursor unchanged)
|
// Also focus the element directly (post-flush watcher won't trigger if cursor unchanged)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const a = document.querySelector(
|
const a = document.querySelector(
|
||||||
`#file-${store.cursor} .name a`
|
`#file-${store.cursor} .name a`
|
||||||
@@ -329,22 +336,35 @@ const focusBreadcrumb = () => {
|
|||||||
const keyboardFollowScroll = createKeyboardFollowScroll()
|
const keyboardFollowScroll = createKeyboardFollowScroll()
|
||||||
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
|
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
|
||||||
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
|
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
|
||||||
watchEffect(() => {
|
watch(
|
||||||
if (store.cursor && store.cursor !== editing.value?.key) editing.value = null
|
() => store.cursor,
|
||||||
if (editing.value) store.cursor = editing.value?.key
|
cursor => {
|
||||||
if (store.cursor) {
|
if (cursor && editing.value && cursor !== editing.value.key) {
|
||||||
|
exitEditing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => store.cursor,
|
||||||
|
cursor => {
|
||||||
|
if (cursor && !editing.value) {
|
||||||
const a = document.querySelector(
|
const a = document.querySelector(
|
||||||
`#file-${store.cursor} .name a`
|
`#file-${cursor} .name a`
|
||||||
) as HTMLAnchorElement | null
|
) as HTMLAnchorElement | null
|
||||||
if (a) a.focus({ preventScroll: true })
|
if (a) a.focus({ preventScroll: true })
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
watchEffect(() => {
|
{ flush: 'post' }
|
||||||
if (!props.documents.length && store.cursor && !store.query) {
|
)
|
||||||
|
watch(
|
||||||
|
() => [props.documents.length, store.cursor, store.query, editing.value] as const,
|
||||||
|
([len, cursor, query, editingDoc]) => {
|
||||||
|
if (!len && cursor && !query && !editingDoc) {
|
||||||
store.cursor = ''
|
store.cursor = ''
|
||||||
focusBreadcrumb()
|
focusBreadcrumb()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
let nowkey = ref(0)
|
let nowkey = ref(0)
|
||||||
let modifiedTimer: any = null
|
let modifiedTimer: any = null
|
||||||
const updateModified = () => {
|
const updateModified = () => {
|
||||||
@@ -358,12 +378,15 @@ onMounted(() => {
|
|||||||
active.focus({ preventScroll: true })
|
active.focus({ preventScroll: true })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
onDeactivated(() => {
|
||||||
|
if (editing.value) exitEditing()
|
||||||
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
keyboardFollowScroll.cancel()
|
keyboardFollowScroll.cancel()
|
||||||
clearInterval(modifiedTimer)
|
clearInterval(modifiedTimer)
|
||||||
})
|
})
|
||||||
const editRoute = (path: string) =>
|
const editRoute = (path: string) =>
|
||||||
'/edit/' +
|
'/' +
|
||||||
path
|
path
|
||||||
.split('/')
|
.split('/')
|
||||||
.map(part => encodeURIComponent(part))
|
.map(part => encodeURIComponent(part))
|
||||||
@@ -373,7 +396,8 @@ const createItem = async (doc: Doc, name: string) => {
|
|||||||
doc.name = name
|
doc.name = name
|
||||||
doc.key = crypto.randomUUID()
|
doc.key = crypto.randomUUID()
|
||||||
store.addGhost(doc)
|
store.addGhost(doc)
|
||||||
editing.value = null
|
store.cursor = doc.key
|
||||||
|
exitEditing()
|
||||||
const path = doc.loc ? `${doc.loc}/${name}` : name
|
const path = doc.loc ? `${doc.loc}/${name}` : name
|
||||||
try {
|
try {
|
||||||
const res = doc.dir
|
const res = doc.dir
|
||||||
@@ -525,9 +549,14 @@ const contextMenu = (ev: MouseEvent, doc: Doc) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.file-explorer {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
thead tr {
|
thead tr {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@@ -658,12 +687,6 @@ tbody .selection input {
|
|||||||
content: '📁';
|
content: '📁';
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
.empty-container {
|
|
||||||
padding-top: 3rem;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 3rem;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
.folder-change {
|
.folder-change {
|
||||||
margin-left: -.5rem;
|
margin-left: -.5rem;
|
||||||
}
|
}
|
||||||
@@ -674,4 +697,3 @@ tbody .selection input {
|
|||||||
color: #888;
|
color: #888;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@/stores/main
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ input#FileRenameInput {
|
|||||||
padding: .75em;
|
padding: .75em;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<EmptyFolder v-else :documents="documents" :path="props.path" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -25,12 +26,13 @@ import ContextMenu from '@imengyu/vue3-context-menu'
|
|||||||
import {
|
import {
|
||||||
computed,
|
computed,
|
||||||
nextTick,
|
nextTick,
|
||||||
|
onActivated,
|
||||||
|
onDeactivated,
|
||||||
onMounted,
|
onMounted,
|
||||||
onUnmounted,
|
onUnmounted,
|
||||||
ref,
|
ref,
|
||||||
shallowRef,
|
shallowRef,
|
||||||
watch,
|
watch
|
||||||
watchEffect
|
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
@@ -170,6 +172,7 @@ const onImgLoad = (e: Event) => {
|
|||||||
}
|
}
|
||||||
const updateColumns = () => {
|
const updateColumns = () => {
|
||||||
if (!gallery.value) return
|
if (!gallery.value) return
|
||||||
|
if (gallery.value.getBoundingClientRect().width <= 0) return
|
||||||
const style = getComputedStyle(gallery.value)
|
const style = getComputedStyle(gallery.value)
|
||||||
const templates = style.gridTemplateColumns
|
const templates = style.gridTemplateColumns
|
||||||
.split(' ')
|
.split(' ')
|
||||||
@@ -301,7 +304,7 @@ defineExpose({
|
|||||||
const docs = props.documents
|
const docs = props.documents
|
||||||
if (docs.length > 0) {
|
if (docs.length > 0) {
|
||||||
store.cursor = docs[0]!.key
|
store.cursor = docs[0]!.key
|
||||||
// Also focus the element directly (watchEffect won't trigger if cursor unchanged)
|
// Also focus the element directly (post-flush watcher won't trigger if cursor unchanged)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const a = document.querySelector(
|
const a = document.querySelector(
|
||||||
`#file-${store.cursor}`
|
`#file-${store.cursor}`
|
||||||
@@ -393,25 +396,49 @@ const focusBreadcrumb = () => {
|
|||||||
const keyboardFollowScroll = createKeyboardFollowScroll()
|
const keyboardFollowScroll = createKeyboardFollowScroll()
|
||||||
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
|
const markKeyboardFollow = keyboardFollowScroll.markKeyboardFollow
|
||||||
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
|
const keepCursorVisibleSmooth = keyboardFollowScroll.keepVisible
|
||||||
watchEffect(() => {
|
watch(
|
||||||
if (store.cursor && store.cursor !== editing.value?.key) editing.value = null
|
() => store.cursor,
|
||||||
if (editing.value) store.cursor = editing.value.key
|
cursor => {
|
||||||
if (store.cursor && !editing.value) {
|
if (cursor && editing.value && cursor !== editing.value.key) {
|
||||||
const a = document.querySelector(
|
exit()
|
||||||
`#file-${store.cursor}`
|
}
|
||||||
) as HTMLAnchorElement | null
|
}
|
||||||
|
)
|
||||||
|
watch(
|
||||||
|
() => store.cursor,
|
||||||
|
cursor => {
|
||||||
|
if (cursor && !editing.value) {
|
||||||
|
const a = document.querySelector(`#file-${cursor}`) as HTMLAnchorElement | null
|
||||||
if (a) {
|
if (a) {
|
||||||
a.focus({ preventScroll: true })
|
a.focus({ preventScroll: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
watchEffect(() => {
|
{ flush: 'post' }
|
||||||
if (!props.documents.length && store.cursor && !store.query) {
|
)
|
||||||
|
watch(
|
||||||
|
() => [props.documents.length, store.cursor, store.query, editing.value] as const,
|
||||||
|
([len, cursor, query, editingDoc]) => {
|
||||||
|
if (!len && cursor && !query && !editingDoc) {
|
||||||
store.cursor = ''
|
store.cursor = ''
|
||||||
focusBreadcrumb()
|
focusBreadcrumb()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
let resizeObserver: ResizeObserver | null = null
|
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(() => {
|
onMounted(() => {
|
||||||
const active = document.querySelector('.cursor') as HTMLElement | null
|
const active = document.querySelector('.cursor') as HTMLElement | null
|
||||||
if (active) {
|
if (active) {
|
||||||
@@ -419,22 +446,27 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
updateColumns()
|
updateColumns()
|
||||||
seedFromDocs()
|
seedFromDocs()
|
||||||
if (gallery.value) {
|
attachGalleryObservers()
|
||||||
resizeObserver = new ResizeObserver(updateColumns)
|
})
|
||||||
resizeObserver.observe(gallery.value)
|
onActivated(() => {
|
||||||
gallery.value.addEventListener('load', onImgLoad, { capture: true })
|
nextTick(() => {
|
||||||
}
|
updateColumns()
|
||||||
|
attachGalleryObservers()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
onDeactivated(() => {
|
||||||
|
detachGalleryObservers()
|
||||||
|
if (editing.value) exit()
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
keyboardFollowScroll.cancel()
|
keyboardFollowScroll.cancel()
|
||||||
resizeObserver?.disconnect()
|
detachGalleryObservers()
|
||||||
gallery.value?.removeEventListener('load', onImgLoad, { capture: true })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Re-seed aspect ratios whenever docs update (e.g., ar patch from server)
|
// Re-seed aspect ratios whenever docs update (e.g., ar patch from server)
|
||||||
watch(() => props.documents, seedFromDocs)
|
watch(() => props.documents, seedFromDocs)
|
||||||
const editRoute = (path: string) =>
|
const editRoute = (path: string) =>
|
||||||
'/edit/' +
|
'/' +
|
||||||
path
|
path
|
||||||
.split('/')
|
.split('/')
|
||||||
.map(part => encodeURIComponent(part))
|
.map(part => encodeURIComponent(part))
|
||||||
@@ -444,7 +476,8 @@ const createItem = async (doc: Doc, name: string) => {
|
|||||||
doc.name = name
|
doc.name = name
|
||||||
doc.key = crypto.randomUUID()
|
doc.key = crypto.randomUUID()
|
||||||
store.addGhost(doc)
|
store.addGhost(doc)
|
||||||
editing.value = null
|
store.cursor = doc.key
|
||||||
|
exit()
|
||||||
const path = doc.loc ? `${doc.loc}/${name}` : name
|
const path = doc.loc ? `${doc.loc}/${name}` : name
|
||||||
try {
|
try {
|
||||||
const res = doc.dir
|
const res = doc.dir
|
||||||
@@ -592,7 +625,8 @@ const contextMenu = (ev: MouseEvent, doc: Doc) => {
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(15em, 1fr));
|
||||||
align-items: end;
|
align-items: start;
|
||||||
|
align-content: start;
|
||||||
}
|
}
|
||||||
.folder-indicator {
|
.folder-indicator {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export class Doc {
|
|||||||
get editurl(): string {
|
get editurl(): string {
|
||||||
if (!this.text) return ''
|
if (!this.text) return ''
|
||||||
const p = this.loc ? `${this.loc}/${this.name}` : this.name
|
const p = this.loc ? `${this.loc}/${this.name}` : this.name
|
||||||
return '/#/edit/' + p.replaceAll('?', '%3F').replaceAll('#', '%23')
|
return '/#/' + p.replaceAll('?', '%3F').replaceAll('#', '%23')
|
||||||
}
|
}
|
||||||
get complete(): boolean {
|
get complete(): boolean {
|
||||||
return !this.ghost && (this.dir || this.size <= this.allocated)
|
return !this.ghost && (this.dir || this.size <= this.allocated)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useMainStore } from '@/stores/main'
|
import { useMainStore } from '@/stores/main'
|
||||||
import ExplorerView from '@/views/ExplorerView.vue'
|
import ExplorerView from '@/views/ExplorerView.vue'
|
||||||
import TextEditorView from '@/views/TextEditorView.vue'
|
|
||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
|
||||||
function getPathDepth(path: string): number {
|
function getPathDepth(path: string): number {
|
||||||
@@ -11,11 +10,6 @@ function getPathDepth(path: string): number {
|
|||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
|
||||||
path: '/edit/:pathMatch(.*)*',
|
|
||||||
name: 'editor',
|
|
||||||
component: TextEditorView
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
name: 'explorer',
|
name: 'explorer',
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ export const exists = (path: string[]) => {
|
|||||||
void store.docVersion
|
void store.docVersion
|
||||||
if (path.length === 0) return true
|
if (path.length === 0) return true
|
||||||
const p = path.join('/')
|
const p = path.join('/')
|
||||||
return getDocuments().some(
|
const hidden = store.hiddenPaths
|
||||||
doc => (doc.loc ? `${doc.loc}/${doc.name}` : doc.name) === p
|
const inDocs = getDocuments().some(doc => {
|
||||||
)
|
const full = doc.loc ? `${doc.loc}/${doc.name}` : doc.name
|
||||||
|
return full === p && !hidden.has(full)
|
||||||
|
})
|
||||||
|
if (inDocs) return true
|
||||||
|
return store.ghosts.some(g => {
|
||||||
|
const full = g.loc ? `${g.loc}/${g.name}` : g.name
|
||||||
|
return full === p && !hidden.has(full)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Strip file extension intelligently (handles .tar.gz, name.with.dots.pdf, etc.) */
|
/** Strip file extension intelligently (handles .tar.gz, name.with.dots.pdf, etc.) */
|
||||||
|
|||||||
@@ -2,23 +2,18 @@
|
|||||||
<div class="transition-wrapper">
|
<div class="transition-wrapper">
|
||||||
<Transition
|
<Transition
|
||||||
:name="transitionName"
|
:name="transitionName"
|
||||||
@after-enter="store.transitionDirection = 'none'"
|
@after-enter="onAfterEnter"
|
||||||
>
|
>
|
||||||
<div :key="folderPath" class="explorer-content">
|
<KeepAlive>
|
||||||
<Gallery
|
<component
|
||||||
v-if="store.prefs.gallery"
|
:is="store.prefs.gallery ? Gallery : FileExplorer"
|
||||||
|
:key="cacheKey"
|
||||||
ref="fileExplorer"
|
ref="fileExplorer"
|
||||||
|
class="explorer-content"
|
||||||
:path="props.path"
|
:path="props.path"
|
||||||
:documents="documents"
|
:documents="documents"
|
||||||
/>
|
/>
|
||||||
<FileExplorer
|
</KeepAlive>
|
||||||
v-else
|
|
||||||
ref="fileExplorer"
|
|
||||||
:path="props.path"
|
|
||||||
:documents="documents"
|
|
||||||
/>
|
|
||||||
<EmptyFolder :documents="documents" :path="props.path" />
|
|
||||||
</div>
|
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="store.searchLoading" class="search-loading">Searching...</div>
|
<div v-if="store.searchLoading" class="search-loading">Searching...</div>
|
||||||
@@ -31,7 +26,7 @@ import { getDocuments } from '@/stores/documentStore'
|
|||||||
import { useMainStore } from '@/stores/main'
|
import { useMainStore } from '@/stores/main'
|
||||||
import { collator } from '@/utils'
|
import { collator } from '@/utils'
|
||||||
import { sorted, sortedGrouped } from '@/utils/docsort'
|
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 store = useMainStore()
|
||||||
const fileExplorer = ref()
|
const fileExplorer = ref()
|
||||||
@@ -42,6 +37,9 @@ 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 cacheKey = computed(
|
||||||
|
() => `${store.prefs.gallery ? 'gallery' : 'list'}:${folderPath.value}`
|
||||||
|
)
|
||||||
|
|
||||||
const transitionName = computed(() => {
|
const transitionName = computed(() => {
|
||||||
if (store.transitionDirection === 'forward') return 'slide-forward'
|
if (store.transitionDirection === 'forward') return 'slide-forward'
|
||||||
@@ -49,6 +47,22 @@ const transitionName = computed(() => {
|
|||||||
return ''
|
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)
|
// 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(
|
||||||
@@ -101,6 +115,19 @@ watchEffect(() => {
|
|||||||
store.fileExplorer = fileExplorer.value
|
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
|
// Only auto-switch gallery mode when entering a new folder or on initial file list load
|
||||||
watch(
|
watch(
|
||||||
[() => props.path.join('/'), () => store.documentCount],
|
[() => props.path.join('/'), () => store.documentCount],
|
||||||
@@ -114,16 +141,6 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.empty-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 2rem;
|
|
||||||
text-shadow: 0 0 .3rem #000, 0 0 2rem #0008;
|
|
||||||
color: var(--accent-color);
|
|
||||||
}
|
|
||||||
.search-loading {
|
.search-loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 1rem;
|
bottom: 1rem;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { apiFetch } from '@/repositories/Client'
|
||||||
|
import { useMainStore } from '@/stores/main'
|
||||||
import { indentWithTab } from '@codemirror/commands'
|
import { indentWithTab } from '@codemirror/commands'
|
||||||
import { LanguageDescription } from '@codemirror/language'
|
import { LanguageDescription } from '@codemirror/language'
|
||||||
import { languages } from '@codemirror/language-data'
|
import { languages } from '@codemirror/language-data'
|
||||||
@@ -16,17 +18,26 @@ import { Compartment, EditorState } from '@codemirror/state'
|
|||||||
import { oneDark } from '@codemirror/theme-one-dark'
|
import { oneDark } from '@codemirror/theme-one-dark'
|
||||||
import { EditorView, keymap } from '@codemirror/view'
|
import { EditorView, keymap } from '@codemirror/view'
|
||||||
import { basicSetup } from 'codemirror'
|
import { basicSetup } from 'codemirror'
|
||||||
import { apiFetch } from '@/repositories/Client'
|
import {
|
||||||
import { useMainStore } from '@/stores/main'
|
computed,
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
nextTick,
|
||||||
import { onBeforeRouteLeave, useRoute } from 'vue-router'
|
onActivated,
|
||||||
|
onDeactivated,
|
||||||
|
onMounted,
|
||||||
|
onUnmounted,
|
||||||
|
ref
|
||||||
|
} from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const store = useMainStore()
|
const store = useMainStore()
|
||||||
|
|
||||||
const MAX_SIZE = 1024 * 1024 // 1 MiB
|
const MAX_SIZE = 1024 * 1024 // 1 MiB
|
||||||
|
|
||||||
const filePath = computed(() => decodeURIComponent(route.path.slice(6))) // strip /edit/
|
const filePath = computed(() => {
|
||||||
|
const raw = decodeURIComponent(route.path).split('//')[0] ?? ''
|
||||||
|
return raw.replace(/^\//, '').replace(/\/$/, '')
|
||||||
|
})
|
||||||
const filename = computed(() => filePath.value.split('/').pop() || '')
|
const filename = computed(() => filePath.value.split('/').pop() || '')
|
||||||
|
|
||||||
const filesUrl = computed(() => {
|
const filesUrl = computed(() => {
|
||||||
@@ -50,21 +61,32 @@ const languageCompartment = new Compartment()
|
|||||||
|
|
||||||
const dirty = computed(() => content.value !== original.value)
|
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) => {
|
const beforeUnload = (event: BeforeUnloadEvent) => {
|
||||||
if (!dirty.value) return
|
if (!dirty.value) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.returnValue = ''
|
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 detectLanguage = async () => {
|
||||||
const language = LanguageDescription.matchFilename(languages, filename.value)
|
const language = LanguageDescription.matchFilename(languages, filename.value)
|
||||||
if (!language) return []
|
if (!language) return []
|
||||||
@@ -129,8 +151,7 @@ const save = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
store.editorSave = save
|
activateEditorBindings()
|
||||||
window.addEventListener('beforeunload', beforeUnload)
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
try {
|
try {
|
||||||
@@ -152,19 +173,23 @@ onMounted(async () => {
|
|||||||
await initEditor(text)
|
await initEditor(text)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err instanceof Error ? err.message : 'Failed to load file'
|
error.value = err instanceof Error ? err.message : 'Failed to load file'
|
||||||
}
|
} finally {
|
||||||
finally {
|
|
||||||
if (loading.value) loading.value = false
|
if (loading.value) loading.value = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
activateEditorBindings()
|
||||||
|
})
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
deactivateEditorBindings()
|
||||||
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
if (store.editorSave === save) {
|
deactivateEditorBindings()
|
||||||
store.editorSave = null
|
|
||||||
}
|
|
||||||
editorView?.destroy()
|
editorView?.destroy()
|
||||||
editorView = null
|
editorView = null
|
||||||
window.removeEventListener('beforeunload', beforeUnload)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user