177 lines
4.6 KiB
TypeScript
177 lines
4.6 KiB
TypeScript
/**
|
|
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
import type { TreeNode } from '../services/FolderTree.ts'
|
|
|
|
import PQueue from 'p-queue'
|
|
import { Folder, Node, View, getNavigation } from '@nextcloud/files'
|
|
import { translate as t } from '@nextcloud/l10n'
|
|
import { emit, subscribe } from '@nextcloud/event-bus'
|
|
import { isSamePath } from '@nextcloud/paths'
|
|
import { loadState } from '@nextcloud/initial-state'
|
|
|
|
import FolderSvg from '@mdi/svg/svg/folder.svg?raw'
|
|
import FolderMultipleSvg from '@mdi/svg/svg/folder-multiple.svg?raw'
|
|
|
|
import {
|
|
folderTreeId,
|
|
getContents,
|
|
getFolderTreeNodes,
|
|
getSourceParent,
|
|
sourceRoot,
|
|
} from '../services/FolderTree.ts'
|
|
|
|
const isFolderTreeEnabled = loadState('files', 'config', { folder_tree: true }).folder_tree
|
|
|
|
let showHiddenFiles = loadState('files', 'config', { show_hidden: false }).show_hidden
|
|
|
|
const Navigation = getNavigation()
|
|
|
|
const queue = new PQueue({ concurrency: 5, intervalCap: 5, interval: 200 })
|
|
|
|
const registerQueue = new PQueue({ concurrency: 5, intervalCap: 5, interval: 200 })
|
|
|
|
const registerTreeChildren = async (path: string = '/') => {
|
|
await queue.add(async () => {
|
|
const nodes = await getFolderTreeNodes(path)
|
|
const promises = nodes.map(node => registerQueue.add(() => registerNodeView(node)))
|
|
await Promise.allSettled(promises)
|
|
})
|
|
}
|
|
|
|
const getLoadChildViews = (node: TreeNode | Folder) => {
|
|
return async (view: View): Promise<void> => {
|
|
// @ts-expect-error Custom property on View instance
|
|
if (view.loading || view.loaded) {
|
|
return
|
|
}
|
|
// @ts-expect-error Custom property
|
|
view.loading = true
|
|
await registerTreeChildren(node.path)
|
|
// @ts-expect-error Custom property
|
|
view.loading = false
|
|
// @ts-expect-error Custom property
|
|
view.loaded = true
|
|
// @ts-expect-error No payload
|
|
emit('files:navigation:updated')
|
|
// @ts-expect-error No payload
|
|
emit('files:folder-tree:expanded')
|
|
}
|
|
}
|
|
|
|
const registerNodeView = (node: TreeNode | Folder) => {
|
|
const registeredView = Navigation.views.find(view => view.id === node.encodedSource)
|
|
if (registeredView) {
|
|
Navigation.remove(registeredView.id)
|
|
}
|
|
if (!showHiddenFiles && node.basename.startsWith('.')) {
|
|
return
|
|
}
|
|
Navigation.register(new View({
|
|
id: node.encodedSource,
|
|
parent: getSourceParent(node.source),
|
|
|
|
// @ts-expect-error Casing differences
|
|
name: node.displayName ?? node.displayname ?? node.basename,
|
|
|
|
icon: FolderSvg,
|
|
|
|
getContents,
|
|
loadChildViews: getLoadChildViews(node),
|
|
|
|
params: {
|
|
view: folderTreeId,
|
|
fileid: String(node.fileid), // Needed for matching exact routes
|
|
dir: node.path,
|
|
},
|
|
}))
|
|
}
|
|
|
|
const removeFolderView = (folder: Folder) => {
|
|
const viewId = folder.encodedSource
|
|
Navigation.remove(viewId)
|
|
}
|
|
|
|
const removeFolderViewSource = (source: string) => {
|
|
Navigation.remove(source)
|
|
}
|
|
|
|
const onCreateNode = (node: Node) => {
|
|
if (!(node instanceof Folder)) {
|
|
return
|
|
}
|
|
registerNodeView(node)
|
|
}
|
|
|
|
const onDeleteNode = (node: Node) => {
|
|
if (!(node instanceof Folder)) {
|
|
return
|
|
}
|
|
removeFolderView(node)
|
|
}
|
|
|
|
const onMoveNode = ({ node, oldSource }) => {
|
|
if (!(node instanceof Folder)) {
|
|
return
|
|
}
|
|
removeFolderViewSource(oldSource)
|
|
registerNodeView(node)
|
|
|
|
const newPath = node.source.replace(sourceRoot, '')
|
|
const oldPath = oldSource.replace(sourceRoot, '')
|
|
const childViews = Navigation.views.filter(view => {
|
|
if (!view.params?.dir) {
|
|
return false
|
|
}
|
|
if (isSamePath(view.params.dir, oldPath)) {
|
|
return false
|
|
}
|
|
return view.params.dir.startsWith(oldPath)
|
|
})
|
|
for (const view of childViews) {
|
|
// @ts-expect-error FIXME Allow setting parent
|
|
view.parent = getSourceParent(node.source)
|
|
// @ts-expect-error dir param is defined
|
|
view.params.dir = view.params.dir.replace(oldPath, newPath)
|
|
}
|
|
}
|
|
|
|
const onUserConfigUpdated = async ({ key, value }) => {
|
|
if (key === 'show_hidden') {
|
|
showHiddenFiles = value
|
|
await registerTreeChildren()
|
|
// @ts-expect-error No payload
|
|
emit('files:folder-tree:initialized')
|
|
}
|
|
}
|
|
|
|
const registerTreeRoot = () => {
|
|
Navigation.register(new View({
|
|
id: folderTreeId,
|
|
|
|
name: t('files', 'All folders'),
|
|
caption: t('files', 'List of your files and folders.'),
|
|
|
|
icon: FolderMultipleSvg,
|
|
order: 50, // Below all other views
|
|
|
|
getContents,
|
|
}))
|
|
}
|
|
|
|
export const registerFolderTreeView = async () => {
|
|
if (!isFolderTreeEnabled) {
|
|
return
|
|
}
|
|
registerTreeRoot()
|
|
await registerTreeChildren()
|
|
subscribe('files:node:created', onCreateNode)
|
|
subscribe('files:node:deleted', onDeleteNode)
|
|
subscribe('files:node:moved', onMoveNode)
|
|
subscribe('files:config:updated', onUserConfigUpdated)
|
|
// @ts-expect-error No payload
|
|
emit('files:folder-tree:initialized')
|
|
}
|