<template> <div class="w-full h-full bg-bg absolute top-0 left-0 px-4 py-4 z-10"> <div class="flex items-center py-1 mb-2"> <span class="material-symbols text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span> <p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p> </div> <div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2"> <p class="font-mono truncate">{{ selectedPath || '/' }}</p> </div> <div v-if="rootDirs.length" class="relative flex bg-primary bg-opacity-50 p-4 folder-container"> <div class="w-1/2 border-r border-bg h-full overflow-y-auto"> <div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center hover:bg-white/10" @click="goBack"> <span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span> <p class="text-base font-mono px-2">..</p> </div> <div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" :class="dir.className" @click="selectDir(dir)"> <span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span> <p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p> <span v-if="dir.path === selectedPath" class="material-symbols" style="font-size: 1.1rem">arrow_right</span> </div> </div> <div class="w-1/2 h-full overflow-y-auto"> <div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" @click="selectSubDir(dir)"> <span class="material-symbols fill bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span> <p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p> </div> </div> <div v-if="loadingDirs" class="absolute inset-0 w-full h-full flex items-center justify-center bg-black/10"> <ui-loading-indicator /> </div> </div> <div v-else-if="initialLoad" class="py-12 text-center"> <p>{{ $strings.MessageLoadingFolders }}</p> </div> <div v-else class="py-12 text-center max-w-sm mx-auto"> <p class="text-lg mb-2">{{ $strings.MessageNoFoldersAvailable }}</p> <p class="text-gray-300 mb-2">{{ $strings.NoteFolderPicker }}</p> </div> <div class="w-full py-2"> <ui-btn :disabled="!selectedPath" color="primary" class="w-full mt-2" @click="selectFolder">{{ $strings.ButtonSelectFolderPath }}</ui-btn> </div> </div> </template> <script> export default { props: { paths: { type: Array, default: () => [] } }, data() { return { initialLoad: false, loadingDirs: false, isPosix: true, rootDirs: [], directories: [], selectedPath: '', subdirs: [], level: 0, currentDir: null, previousDir: null } }, computed: { _directories() { return this.directories.map((d) => { var isUsed = !!this.paths.find((path) => path.endsWith(d.path)) var isSelected = d.path === this.selectedPath var classes = [] if (isSelected) classes.push('dir-selected') if (isUsed) classes.push('dir-used') return { isUsed, isSelected, className: classes.join(' '), ...d } }) }, _subdirs() { return this.subdirs.map((d) => { var isUsed = !!this.paths.find((path) => path.endsWith(d.path)) var classes = [] if (isUsed) classes.push('dir-used') return { isUsed, className: classes.join(' '), ...d } }) } }, methods: { async goBack() { let selPath = this.selectedPath.replace(/^\//, '') var splitPaths = selPath.split('/') let previousPath = '' let lookupPath = '' if (splitPaths.length > 2) { lookupPath = splitPaths.slice(0, -2).join('/') } previousPath = splitPaths.slice(0, -1).join('/') if (!this.isPosix) { // For windows drives add a trailing slash. e.g. C:/ if (!this.isPosix && lookupPath.endsWith(':')) { lookupPath += '/' } if (!this.isPosix && previousPath.endsWith(':')) { previousPath += '/' } } else { // Add leading slash if (previousPath) previousPath = '/' + previousPath if (lookupPath) lookupPath = '/' + lookupPath } this.level-- this.subdirs = this.directories this.selectedPath = previousPath this.directories = await this.fetchDirs(lookupPath, this.level) }, async selectDir(dir) { if (dir.isUsed) return this.selectedPath = dir.path this.level = dir.level this.subdirs = await this.fetchDirs(dir.path, dir.level + 1) }, async selectSubDir(dir) { if (dir.isUsed) return this.selectedPath = dir.path this.level = dir.level this.directories = this.subdirs this.subdirs = await this.fetchDirs(dir.path, dir.level + 1) }, selectFolder() { if (!this.selectedPath) { console.error('No Selected path') return } if (this.paths.find((p) => p.startsWith(this.selectedPath))) { this.$toast.error(`Oops, you cannot add a parent directory of a folder already added`) return } this.$emit('select', this.selectedPath) this.selectedPath = '' }, fetchDirs(path, level) { this.loadingDirs = true return this.$axios .$get(`/api/filesystem?path=${path}&level=${level}`) .then((data) => { console.log('Fetched directories', data.directories) this.isPosix = !!data.posix return data.directories }) .catch((error) => { console.error('Failed to get filesystem paths', error) this.$toast.error(this.$strings.ToastFailedToLoadData) return [] }) .finally(() => { this.loadingDirs = false }) }, async init() { this.initialLoad = true this.rootDirs = await this.fetchDirs('', 0) this.initialLoad = false this.directories = this.rootDirs this.subdirs = [] this.selectedPath = '' } }, mounted() { this.init() } } </script> <style> .dir-item.dir-selected { background-color: rgba(255, 255, 255, 0.1); } .dir-item.dir-used { background-color: rgba(255, 25, 0, 0.1); } .folder-container { max-height: calc(100% - 130px); height: calc(100% - 130px); min-height: calc(100% - 130px); } </style>