Compare commits

...
3 Commits
Author SHA1 Message Date
LeoVasanko 4f646fb344 Fix layout when there is more space than needed to display file explorer (don't scale larger) or gallery (don't bottom align). 2026-06-17 03:30:20 +00:00
LeoVasanko d6304d0029 Frontend linter changes. 2026-06-16 22:13:27 +00:00
LeoVasanko 77e35cf0fc fix(frontend): new file and folder creation hang, empty folder UX
- Replace circular watchEffects in FileExplorer/Gallery with explicit watchers

  to stop recursive Vue updates when creating items in empty folders.

- Move EmptyFolder rendering inside FileExplorer/Gallery so empty/list swaps

  no longer trigger folder slide transitions.

- Keep EmptyFolder text size consistent across list and gallery views.
2026-06-16 22:09:53 +00:00
10 changed files with 204 additions and 153 deletions
+1 -3
View File
@@ -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
) )
+8 -14
View File
@@ -58,13 +58,13 @@ import Router from '@/router/index'
import { computed } from 'vue' import { computed } from 'vue'
import AboutModal from './components/AboutModal.vue' import AboutModal from './components/AboutModal.vue'
import AccessDeniedModal from './components/AccessDeniedModal.vue' import AccessDeniedModal from './components/AccessDeniedModal.vue'
import ExplorerView from './views/ExplorerView.vue'
import SelectionToolbar from './components/SelectionToolbar.vue' import SelectionToolbar from './components/SelectionToolbar.vue'
import TextEditorView from './views/TextEditorView.vue'
import type SettingsModalVue from './components/SettingsModal.vue' 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
@@ -78,7 +78,9 @@ interface Path {
const store = useMainStore() const store = useMainStore()
const getDocByPath = (fullPath: string) => const getDocByPath = (fullPath: string) =>
getDocuments().find(doc => (doc.loc ? `${doc.loc}/${doc.name}` : doc.name) === fullPath) 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('//')
@@ -90,11 +92,7 @@ const path: ComputedRef<Path> = computed(() => {
void store.docVersion void store.docVersion
const doc = fullPath ? getDocByPath(fullPath) : null const doc = fullPath ? getDocByPath(fullPath) : null
const isEditorPath = !!(doc && !doc.dir && doc.text) const isEditorPath = !!(doc && !doc.dir && doc.text)
const canonicalBase = !fullPath const canonicalBase = !fullPath ? '/' : doc?.dir ? `/${fullPath}/` : `/${fullPath}`
? '/'
: doc?.dir
? `/${fullPath}/`
: `/${fullPath}`
const canonicalPath = query const canonicalPath = query
? rawPath // keep search URL shape untouched ? rawPath // keep search URL shape untouched
: canonicalBase : canonicalBase
@@ -128,14 +126,10 @@ const routeViewComponent = computed(() =>
path.value.isEditorPath ? TextEditorView : ExplorerView path.value.isEditorPath ? TextEditorView : ExplorerView
) )
const routeViewKey = computed(() => { const routeViewKey = computed(() => {
return path.value.isEditorPath return path.value.isEditorPath ? `editor:${path.value.path}` : 'explorer'
? `editor:${path.value.path}`
: 'explorer'
}) })
const routeViewProps = computed(() => const routeViewProps = computed(() =>
path.value.isEditorPath path.value.isEditorPath ? {} : { path: path.value.pathList, query: path.value.query }
? {}
: { path: path.value.pathList, query: path.value.query }
) )
watch( watch(
() => path.value.canonicalPath, () => path.value.canonicalPath,
+1 -2
View File
@@ -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 {
+8 -2
View File
@@ -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())
+22 -1
View File
@@ -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); }
+112 -90
View File
@@ -1,74 +1,77 @@
<template> <template>
<table v-if="props.documents.length || editing"> <div class="file-explorer">
<thead> <table v-if="props.documents.length || editing">
<tr> <thead>
<th class="selection"> <tr>
<input type="checkbox" tabindex="-1" v-model="allSelected" :indeterminate="selectionIndeterminate"> <th class="selection">
</th> <input type="checkbox" tabindex="-1" v-model="allSelected" :indeterminate="selectionIndeterminate">
<th class="sortcolumn" :class="{ sortactive: store.sortOrder === 'name' }" @click="store.toggleSort('name')">Name</th> </th>
<th class="sortcolumn modified right" :class="{ sortactive: store.sortOrder === 'modified' }" @click="store.toggleSort('modified')">Modified</th> <th class="sortcolumn" :class="{ sortactive: store.sortOrder === 'name' }" @click="store.toggleSort('name')">Name</th>
<th class="sortcolumn size right" :class="{ sortactive: store.sortOrder === 'size' }" @click="store.toggleSort('size')">Size</th> <th class="sortcolumn modified right" :class="{ sortactive: store.sortOrder === 'modified' }" @click="store.toggleSort('modified')">Modified</th>
<th class="menu"></th> <th class="sortcolumn size right" :class="{ sortactive: store.sortOrder === 'size' }" @click="store.toggleSort('size')">Size</th>
</tr> <th class="menu"></th>
</thead>
<tbody>
<tr v-if="editing?.key === 'new'" :class="editing.dir ? 'folder' : 'file'">
<td class="selection"></td>
<td class="name">
<FileRenameInput :doc="editing" :rename="createItem" :exit="() => {editing = null}" />
</td>
<FileModified :doc=editing :now=nowkey />
<FileSize :doc=editing />
<td class="menu"></td>
</tr>
<template v-for="(doc, index) in documents" :key="doc.key">
<tr class="folder-change" v-if="showFolderBreadcrumb(index)">
<th colspan="5"><BreadCrumb :path="doc.loc ? doc.loc.split('/') : []" /></th>
</tr> </tr>
</thead>
<tr <tbody>
:id="`file-${doc.key}`" <tr v-if="editing?.key === 'new'" :class="editing.dir ? 'folder' : 'file'">
:class="{ file: !doc.dir, folder: doc.dir, cursor: store.cursor === doc.key, ghost: doc.ghost }" <td class="selection"></td>
@click="store.cursor = store.cursor === doc.key ? '' : doc.key"
@contextmenu.prevent="contextMenu($event, doc)"
>
<td class="selection" @click.up.stop="store.cursor = store.cursor === doc.key ? doc.key : ''">
<input
type="checkbox"
tabindex="-1"
:checked="store.selected.has(doc.key)"
@change="
($event.target as HTMLInputElement).checked
? store.selected.add(doc.key)
: store.selected.delete(doc.key)
"
/>
</td>
<td class="name"> <td class="name">
<template v-if="editing === doc"> <FileRenameInput :doc="editing" :rename="createItem" :exit="exitEditing" />
<FileRenameInput :doc="doc" :rename="rename" :exit="() => {editing = null}" />
</template>
<template v-else>
<a :href="doc.text ? doc.editurl : doc.url" tabindex=-1 @contextmenu.stop @focus.stop="store.cursor = doc.key">
{{ doc.name }}
</a>
<button tabindex=-1 v-if="store.cursor == doc.key" class="rename-button" @click="() => (editing = doc)">🖊</button>
</template>
</td>
<FileModified :doc=doc :now=nowkey />
<FileSize :doc=doc />
<td class="menu">
<button tabindex=-1 @click.stop="contextMenu($event, doc)"></button>
</td> </td>
<FileModified :doc=editing :now=nowkey />
<FileSize :doc=editing />
<td class="menu"></td>
</tr> </tr>
</template> <template v-for="(doc, index) in documents" :key="doc.key">
<tr class="summary" v-if="props.documents.length > 1"> <tr class="folder-change" v-if="showFolderBreadcrumb(index)">
<td colspan="3" class="right">{{props.documents.length}} items</td> <th colspan="5"><BreadCrumb :path="doc.loc ? doc.loc.split('/') : []" /></th>
<td class="size right">{{ formatSize(props.documents.reduce((a, b) => a + b.size, 0)) }}</td> </tr>
<td class="menu"></td>
</tr> <tr
</tbody> :id="`file-${doc.key}`"
</table> :class="{ file: !doc.dir, folder: doc.dir, cursor: store.cursor === doc.key, ghost: doc.ghost }"
@click="store.cursor = store.cursor === doc.key ? '' : doc.key"
@contextmenu.prevent="contextMenu($event, doc)"
>
<td class="selection" @click.up.stop="store.cursor = store.cursor === doc.key ? doc.key : ''">
<input
type="checkbox"
tabindex="-1"
:checked="store.selected.has(doc.key)"
@change="
($event.target as HTMLInputElement).checked
? store.selected.add(doc.key)
: store.selected.delete(doc.key)
"
/>
</td>
<td class="name">
<template v-if="editing === doc">
<FileRenameInput :doc="doc" :rename="rename" :exit="exitEditing" />
</template>
<template v-else>
<a :href="doc.text ? doc.editurl : doc.url" tabindex=-1 @contextmenu.stop @focus.stop="store.cursor = doc.key">
{{ doc.name }}
</a>
<button tabindex=-1 v-if="store.cursor == doc.key" class="rename-button" @click="() => (editing = doc)">🖊</button>
</template>
</td>
<FileModified :doc=doc :now=nowkey />
<FileSize :doc=doc />
<td class="menu">
<button tabindex=-1 @click.stop="contextMenu($event, doc)"></button>
</td>
</tr>
</template>
<tr class="summary" v-if="props.documents.length > 1">
<td colspan="3" class="right">{{props.documents.length}} items</td>
<td class="size right">{{ formatSize(props.documents.reduce((a, b) => a + b.size, 0)) }}</td>
<td class="menu"></td>
</tr>
</tbody>
</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) {
const a = document.querySelector( exitEditing()
`#file-${store.cursor} .name a` }
) as HTMLAnchorElement | null
if (a) a.focus({ preventScroll: true })
} }
}) )
watchEffect(() => { watch(
if (!props.documents.length && store.cursor && !store.query) { () => store.cursor,
store.cursor = '' cursor => {
focusBreadcrumb() if (cursor && !editing.value) {
const a = document.querySelector(
`#file-${cursor} .name a`
) as HTMLAnchorElement | null
if (a) a.focus({ preventScroll: true })
}
},
{ flush: 'post' }
)
watch(
() => [props.documents.length, store.cursor, store.query, editing.value] as const,
([len, cursor, query, editingDoc]) => {
if (!len && cursor && !query && !editingDoc) {
store.cursor = ''
focusBreadcrumb()
}
} }
}) )
let nowkey = ref(0) let nowkey = ref(0)
let modifiedTimer: any = null let modifiedTimer: any = null
const updateModified = () => { const updateModified = () => {
@@ -358,6 +378,9 @@ onMounted(() => {
active.focus({ preventScroll: true }) active.focus({ preventScroll: true })
} }
}) })
onDeactivated(() => {
if (editing.value) exitEditing()
})
onUnmounted(() => { onUnmounted(() => {
keyboardFollowScroll.cancel() keyboardFollowScroll.cancel()
clearInterval(modifiedTimer) clearInterval(modifiedTimer)
@@ -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
+36 -22
View File
@@ -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">
@@ -23,16 +24,15 @@ import type { SortOrder } from '@/utils/docsort'
import { createKeyboardFollowScroll } from '@/utils/keyboardFollowScroll' import { createKeyboardFollowScroll } from '@/utils/keyboardFollowScroll'
import ContextMenu from '@imengyu/vue3-context-menu' import ContextMenu from '@imengyu/vue3-context-menu'
import { import {
onActivated,
onDeactivated,
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'
@@ -304,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}`
@@ -396,24 +396,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 && !editing.value) { if (cursor && editing.value && cursor !== editing.value.key) {
const a = document.querySelector( exit()
`#file-${store.cursor}`
) as HTMLAnchorElement | null
if (a) {
a.focus({ preventScroll: true })
} }
} }
}) )
watchEffect(() => { watch(
if (!props.documents.length && store.cursor && !store.query) { () => store.cursor,
store.cursor = '' cursor => {
focusBreadcrumb() if (cursor && !editing.value) {
const a = document.querySelector(`#file-${cursor}`) as HTMLAnchorElement | null
if (a) {
a.focus({ preventScroll: true })
}
}
},
{ flush: 'post' }
)
watch(
() => [props.documents.length, store.cursor, store.query, editing.value] as const,
([len, cursor, query, editingDoc]) => {
if (!len && cursor && !query && !editingDoc) {
store.cursor = ''
focusBreadcrumb()
}
} }
}) )
let resizeObserver: ResizeObserver | null = null let resizeObserver: ResizeObserver | null = null
const attachGalleryObservers = () => { const attachGalleryObservers = () => {
if (!gallery.value || resizeObserver) return if (!gallery.value || resizeObserver) return
@@ -445,6 +456,7 @@ onActivated(() => {
}) })
onDeactivated(() => { onDeactivated(() => {
detachGalleryObservers() detachGalleryObservers()
if (editing.value) exit()
}) })
onUnmounted(() => { onUnmounted(() => {
keyboardFollowScroll.cancel() keyboardFollowScroll.cancel()
@@ -464,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
@@ -612,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;
+10 -3
View File
@@ -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.) */
+3 -12
View File
@@ -15,7 +15,6 @@
/> />
</KeepAlive> </KeepAlive>
</Transition> </Transition>
<EmptyFolder :documents="documents" :path="props.path" />
</div> </div>
<div v-if="store.searchLoading" class="search-loading">Searching...</div> <div v-if="store.searchLoading" class="search-loading">Searching...</div>
</template> </template>
@@ -38,7 +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 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'
@@ -140,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;
+3 -4
View File
@@ -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,8 +18,6 @@ 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 { useMainStore } from '@/stores/main'
import { import {
computed, computed,
nextTick, nextTick,
@@ -173,8 +173,7 @@ 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
} }
}) })