From b2500c7a57599e82c387d65a186253b8533312e2 Mon Sep 17 00:00:00 2001 From: Wim Brand Date: Wed, 19 May 2021 23:17:36 +0200 Subject: [PATCH] Replace file manager with svelte version --- .../script/file_manager/DirectoryElement.js | 325 ------------------ .../script/file_manager/FileManager.js | 127 ------- res/template/account/file_manager.html | 61 +--- res/template/account/file_manager_svelte.html | 21 -- .../user_file_manager/DirectoryElement.svelte | 135 ++++++-- .../src/user_file_manager/FileManager.svelte | 58 ++-- 6 files changed, 138 insertions(+), 589 deletions(-) delete mode 100644 res/include/script/file_manager/DirectoryElement.js delete mode 100644 res/include/script/file_manager/FileManager.js delete mode 100644 res/template/account/file_manager_svelte.html diff --git a/res/include/script/file_manager/DirectoryElement.js b/res/include/script/file_manager/DirectoryElement.js deleted file mode 100644 index 3d9080d..0000000 --- a/res/include/script/file_manager/DirectoryElement.js +++ /dev/null @@ -1,325 +0,0 @@ -function DirectoryElement(directoryArea, footer) { - // Main elements - this.directoryArea = directoryArea - this.footer = footer - this.directorySorters = this.directoryArea.querySelector("#directory_sorters") - - // Create sort buttons - - // Sorting internal state. By default we sort by dateCreated in descending - // order (new to old) - this.currentSortField = "dateCreated" - this.currentSortAscending = false - this.sortButtons = [] - - // Field is the name of the field in the file structure to sort on. Label is - // the text that appears in the sorting button - let makeSortButton = (field, label, width) => { - this.sortButtons[field] = document.createElement("div") - this.sortButtons[field].innerText = label - this.sortButtons[field].style.minWidth = width - this.sortButtons[field].addEventListener("click", () => { - this.sortBy(field) - }) - this.directorySorters.appendChild(this.sortButtons[field]) - } - - // These widths are used for the sorters and the file nodes itself - this.fieldDateWidth = "160px" - this.fieldSizeWidth = "90px" - this.fieldTypeWidth = "200px" - makeSortButton("name", "Name", "") - makeSortButton("dateCreated", "Creation Date", this.fieldDateWidth) - makeSortButton("size", "Size", this.fieldSizeWidth) - makeSortButton("type", "Type", this.fieldTypeWidth) - - - // Scroll event for rendering new file nodes when they become visible - this.frameRequested = false; - this.directoryArea.addEventListener("scroll", (e) => { - if (this.frameRequested) { return } - this.frameRequested = true - requestAnimationFrame(() => { - this.renderVisibleFiles(false) - this.frameRequested = false - }) - }) - - // The directory container itself. This is where the files are rendered - this.dirContainer = document.createElement("div") - this.dirContainer.classList = "directory_node_container" - this.directoryArea.appendChild(this.dirContainer) - - // Internal state, contains a list of all files in the directory, visible - // files in the directory and the last scroll position. These are used for - // rendering the file list correctly - - // type: {icon, name, href, type, size, sizeLabel, dateCreated, selected} - this.allFiles = [] - - // This array contains indexes referring to places in the allFiles array - this.visibleFiles = [] - - this.lastSearchTerm = "" - this.lastScrollTop = 0 -} - -DirectoryElement.prototype.reset = function () { - this.allFiles = [] - this.visibleFiles = [] -} - -DirectoryElement.prototype.addFile = function (icon, name, href, type, size, sizeLabel, dateCreated) { - this.allFiles.push({ - icon: icon, - name: name, - href: href, - type: type, - size: size, - sizeLabel: sizeLabel, - dateCreated: dateCreated, - selected: false, - }) -} - -DirectoryElement.prototype.renderFiles = function () { - this.search(this.lastSearchTerm) -} - -// search filters the allFiles array on a search term. All files which match the -// search term will be put into visibleFiles. The visibleFiles array will then -// be rendered by renderVisibleFiles -DirectoryElement.prototype.search = function (term) { - term = term.toLowerCase() - this.lastSearchTerm = term - this.visibleFiles = [] - - if (term === "") { - for (let i in this.allFiles) { - this.visibleFiles.push(i) - } - this.sortBy("") - this.renderVisibleFiles(true) - return - } - - let fileName = "" - for (let i in this.allFiles) { - fileName = this.allFiles[i].name.toLowerCase() - - // If there's an exact match we'll show it as the only result - if (fileName === term) { - this.visibleFiles = [i] - break - } - - // If a file name contains the search term we include it in the results - if (fileName.includes(term)) { - this.visibleFiles.push(i) - } - } - - this.sortBy("") - this.renderVisibleFiles(true) -} - -// searchSubmit opens the first file in the search results -DirectoryElement.prototype.searchSubmit = function () { - if (this.visibleFiles.length === 0) { - return // There are no files visible - } - - window.location = this.getVisibleFile(0).href -} - -DirectoryElement.prototype.sortBy = function (field) { - if (field === "") { - // If no sort field is provided we use the last used sort field - field = this.currentSortField - } else { - // If a sort field is provided we check in which direction we have to - // sort - if (this.currentSortField !== field) { - // If this field is a different field than before we sort it in - // ascending order - this.currentSortAscending = true - this.currentSortField = field - } else if (this.currentSortField === field) { - // If it is the same field as before we reverse the sort order - this.currentSortAscending = !this.currentSortAscending - } - } - - // Add the arrow to the sort label. First remove the arrow from all sort - // labels - for (let el in this.sortButtons) { - this.sortButtons[el].innerText = this.sortButtons[el].innerText.replace("▲ ", "").replace("▼ ", "") - } - - // Then prepend the arrow to the current sort label - if (this.currentSortAscending) { - this.sortButtons[field].innerText = "▼ " + this.sortButtons[field].innerText - } else { - this.sortButtons[field].innerText = "▲ " + this.sortButtons[field].innerText - } - - let fieldA, fieldB - this.visibleFiles.sort((a, b) => { - fieldA = this.allFiles[a][this.currentSortField] - fieldB = this.allFiles[b][this.currentSortField] - - if (typeof (fieldA) === "number") { - if (this.currentSortAscending) { - return fieldA - fieldB - } else { - return fieldB - fieldA - } - } else { - if (this.currentSortAscending) { - return fieldA.localeCompare(fieldB) - } else { - return fieldB.localeCompare(fieldA) - } - } - }) - this.renderVisibleFiles(true) -} - -DirectoryElement.prototype.createFileButton = function (file, index) { - let el = document.createElement("a") - el.classList = "node" - el.href = file.href - el.target = "_blank" - el.title = file.name - el.setAttribute("fileindex", index) - - { - let cell = document.createElement("div") - let thumb = document.createElement("img") - thumb.src = file.icon - cell.appendChild(thumb) - let label = document.createElement("span") - label.innerText = file.name - cell.appendChild(label) - cell.appendChild(label) - el.appendChild(cell) - } - { - let cell = document.createElement("div") - cell.style.width = this.fieldDateWidth - let label = document.createElement("span") - label.innerText = printDate(new Date(file.dateCreated), true, true, false) - cell.appendChild(label) - el.appendChild(cell) - } - { - let cell = document.createElement("div") - cell.style.width = this.fieldSizeWidth - let label = document.createElement("span") - label.innerText = file.sizeLabel - cell.appendChild(label) - el.appendChild(cell) - } - { - let cell = document.createElement("div") - cell.style.width = this.fieldTypeWidth - let label = document.createElement("span") - label.innerText = file.type - cell.appendChild(label) - el.appendChild(cell) - } - - return el -} - -// This function dereferences an index in the visibleFiles array to a real file -// in the allFiles array. The notation is a bit confusing so the separate -// function is just for clarity -DirectoryElement.prototype.getVisibleFile = function (index) { - return this.allFiles[this.visibleFiles[index]] -} - -DirectoryElement.prototype.renderVisibleFiles = function (freshStart) { - let scrollDown = this.lastScrollTop <= this.directoryArea.scrollTop - this.lastScrollTop = this.directoryArea.scrollTop - - let fileHeight = 40 - let totalHeight = (this.visibleFiles.length * fileHeight) - let viewportHeight = this.directoryArea.clientHeight - - if (freshStart) { - this.dirContainer.innerHTML = "" - this.dirContainer.style.height = totalHeight + "px" - scrollDown = true - - let totalSize = 0 - for (let i in this.visibleFiles) { - totalSize += this.getVisibleFile(i).size - } - this.footer.innerText = this.visibleFiles.length + " items. Total size: " + formatDataVolume(totalSize, 4) - } - - let paddingTop = this.lastScrollTop - this.lastScrollTop % fileHeight - let start = Math.floor(paddingTop / fileHeight) - 5 - if (start < 0) { start = 0 } - - let end = Math.ceil((paddingTop + viewportHeight) / fileHeight) + 5 - if (end > this.visibleFiles.length) { end = this.visibleFiles.length - 1 } - - this.dirContainer.style.paddingTop = (start * fileHeight) + "px" - - // Remove the elements which are out of bounds - let firstEl - let firstIdx = -1 - let lastEl - let lastIdx = -1 - while (!freshStart) { - firstEl = this.dirContainer.firstElementChild - if (firstEl === null) { break } - firstIdx = Number.parseInt(firstEl.getAttribute("fileindex")) - lastEl = this.dirContainer.lastElementChild - lastIdx = Number.parseInt(lastEl.getAttribute("fileindex")) - - if (firstIdx < start) { - this.dirContainer.removeChild(firstEl) - console.debug("Remove start " + firstIdx) - } else if (lastIdx > end) { - this.dirContainer.removeChild(lastEl) - console.debug("Remove end " + lastIdx) - } else { - break - } - } - - console.debug( - "start " + start + - " end " + end + - " firstIdx " + firstIdx + - " lastIdx " + lastIdx + - " freshStart " + freshStart + - " scrollDown " + scrollDown + - " children " + this.dirContainer.childElementCount - ) - - // Then add the elements which have become visible. When the user scrolls - // down we can append the items in chronologic order, but when the user - // scrolls up we have to prepend the items in reverse order to avoid them - // appearing from high to low. - if (scrollDown) { - for (let i = start; i <= end && i < this.visibleFiles.length; i++) { - if (lastIdx !== -1 && i <= lastIdx) { - continue - } - this.dirContainer.append(this.createFileButton(this.getVisibleFile(i), i)) - console.debug("Append " + i); - } - } else { - for (let i = end; i >= start; i--) { - if (firstIdx !== -1 && i >= firstIdx) { - continue - } - this.dirContainer.prepend(this.createFileButton(this.getVisibleFile(i), i)) - console.debug("Prepend " + i); - } - } -} diff --git a/res/include/script/file_manager/FileManager.js b/res/include/script/file_manager/FileManager.js deleted file mode 100644 index 24695c5..0000000 --- a/res/include/script/file_manager/FileManager.js +++ /dev/null @@ -1,127 +0,0 @@ -function FileManager(windowElement) { - this.window = windowElement - this.navBar = this.window.querySelector("#nav_bar") - this.btnMenu = this.navBar.querySelector("#btn_menu") - this.btnBack = this.navBar.querySelector("#btn_back") - this.btnUp = this.navBar.querySelector("#btn_up") - this.btnForward = this.navBar.querySelector("#btn_forward") - this.btnHome = this.navBar.querySelector("#btn_home") - this.breadcrumbs = this.navBar.querySelector("#breadcrumbs") - this.btnReload = this.navBar.querySelector("#btn_reload") - this.inputSearch = this.navBar.querySelector("#input_search") - - // Register keyboard shortcuts - document.addEventListener("keydown", e => { this.keyboardEvent(e) }) - - this.inputSearch.addEventListener("keyup", e => { - if (e.keyCode === 27) { // Escape - e.preventDefault() - this.inputSearch.blur() - return - } else if (e.keyCode === 13) { // Enter - e.preventDefault() - this.directoryElement.searchSubmit() - return - } - requestAnimationFrame(() => { - this.directoryElement.search(this.inputSearch.value) - }) - }) - - this.directoryElement = new DirectoryElement( - this.window.querySelector("#directory_area"), - this.window.querySelector("#directory_footer"), - ) -} - -FileManager.prototype.setSpinner = function () { - this.window.appendChild(document.getElementById("tpl_spinner").content.cloneNode(true)) -} -FileManager.prototype.delSpinner = function () { - for (let i in this.window.children) { - if ( - typeof (this.window.children[i].classList) === "object" && - this.window.children[i].classList.contains("spinner") - ) { - this.window.children[i].remove() - } - } -} - -FileManager.prototype.getUserFiles = function () { - this.setSpinner() - - let getAll = (page) => { - let numFiles = 1000 - fetch(apiEndpoint + "/user/files?page=" + page + "&limit=" + numFiles).then(resp => { - if (!resp.ok) { Promise.reject("yo") } - return resp.json() - }).then(resp => { - for (let i in resp.files) { - this.directoryElement.addFile( - apiEndpoint + "/file/" + resp.files[i].id + "/thumbnail?width=32&height=32", - resp.files[i].name, - "/u/" + resp.files[i].id, - resp.files[i].mime_type, - resp.files[i].size, - formatDataVolume(resp.files[i].size, 4), - resp.files[i].date_upload, - ) - } - - this.directoryElement.renderFiles() - - if (resp.files.length === numFiles) { - getAll(page + 1) - } else { - // Less than the maximum number of results means we're done - // loading, we can remove the loading spinner - this.delSpinner() - } - }).catch((err) => { - this.delSpinner() - throw (err) - }) - } - - this.directoryElement.reset() - getAll(0) -} - -FileManager.prototype.getUserLists = function () { - this.setSpinner() - this.directoryElement.reset() - - fetch(apiEndpoint + "/user/lists").then(resp => { - if (!resp.ok) { Promise.reject("yo") } - return resp.json() - }).then(resp => { - for (let i in resp.lists) { - this.directoryElement.addFile( - apiEndpoint + "/list/" + resp.lists[i].id + "/thumbnail?width=32&height=32", - resp.lists[i].title, - "/l/" + resp.lists[i].id, - "list", - resp.lists[i].file_count, - resp.lists[i].file_count + " files", - resp.lists[i].date_created, - ) - } - - this.directoryElement.renderFiles() - this.delSpinner() - }).catch((err) => { - this.delSpinner() - throw (err) - }) -} - -FileManager.prototype.keyboardEvent = function (e) { - console.log("Pressed: " + e.keyCode) - - // CTRL + F or "/" opens the search bar - if (e.ctrlKey && e.keyCode === 70 || !e.ctrlKey && e.keyCode === 191) { - e.preventDefault() - this.inputSearch.focus() - } -} diff --git a/res/template/account/file_manager.html b/res/template/account/file_manager.html index 260dcb0..3567a23 100644 --- a/res/template/account/file_manager.html +++ b/res/template/account/file_manager.html @@ -3,64 +3,17 @@ {{template "meta_tags" "File Manager"}} {{template "user_style" .}} - + + + + - - {{template "page_menu" .}} - -
-
- - -
-
-
- -
-
- - +
{{template "analytics"}} diff --git a/res/template/account/file_manager_svelte.html b/res/template/account/file_manager_svelte.html deleted file mode 100644 index 65af86d..0000000 --- a/res/template/account/file_manager_svelte.html +++ /dev/null @@ -1,21 +0,0 @@ -{{define "file_manager_svelte"}} - - - {{template "meta_tags" "File Manager"}} - {{template "user_style" .}} - - - - - - - - - {{template "page_menu" .}} -
- {{template "analytics"}} - - -{{end}} diff --git a/svelte/src/user_file_manager/DirectoryElement.svelte b/svelte/src/user_file_manager/DirectoryElement.svelte index d736cd6..c34ac41 100644 --- a/svelte/src/user_file_manager/DirectoryElement.svelte +++ b/svelte/src/user_file_manager/DirectoryElement.svelte @@ -7,30 +7,6 @@ let directorySorters let nodeContainer let statusBar = "Loading..." -// Create sort buttons - -// Sorting internal state. By default we sort by dateCreated in descending -// order (new to old) -let currentSortField = "dateCreated" -let currentSortAscending = false -let tableColumns = [ - { name: "Name", field: "name", width: "" }, - { name: "Creation date", field: "dateCreated", width: "160px" }, - { name: "Size", field: "size", width: "90px" }, - { name: "Type", field: "type", width: "200px" }, -] - -// Scroll event for rendering new file nodes when they become visible -let frameRequested = false; -const onScroll = (e) => { - if (frameRequested) { return } - frameRequested = true - requestAnimationFrame(() => { - renderVisibleFiles() - frameRequested = false - }) -} - // Internal state, contains a list of all files in the directory, visible // files in the directory and the last scroll position. These are used for // rendering the file list correctly @@ -63,7 +39,7 @@ export const renderFiles = () => { // search filters the allFiles array on a search term. All files which match the // search term will be put into visibleFiles. The visibleFiles array will then -// be rendered by renderVisibleFiles +// be rendered by render_visible_files let lastSearchTerm = "" export const search = (term) => { term = term.toLowerCase() @@ -74,7 +50,7 @@ export const search = (term) => { allFiles[i].filtered = false } sortBy("") - renderVisibleFiles() + render_visible_files() return } @@ -93,23 +69,34 @@ export const search = (term) => { allFiles[i].filtered = false } else { allFiles[i].filtered = true + allFiles[i].selected = false } } sortBy("") - renderVisibleFiles() + render_visible_files() } // searchSubmit opens the first file in the search results export const searchSubmit = () => { for (let i in allFiles) { if (allFiles[i].visible && !allFiles[i].filtered) { - window.location = allFiles[i].href + window.open(allFiles[i].href, "_blank") break } } } +// Sorting internal state. By default we sort by dateCreated in descending +// order (new to old) +let currentSortField = "dateCreated" +let currentSortAscending = false +let tableColumns = [ + { name: "Name", field: "name", width: "" }, + { name: "Creation date", field: "dateCreated", width: "160px" }, + { name: "Size", field: "size", width: "90px" }, + { name: "Type", field: "type", width: "200px" }, +] const sortBy = (field) => { if (field === "") { // If no sort field is provided we use the last used sort field @@ -168,17 +155,34 @@ const sortBy = (field) => { } }) - renderVisibleFiles() + render_visible_files() } -const renderVisibleFiles = () => { +// Scroll event for rendering new file nodes when they become visible. For +// performance reasons the files will only be rendered once every 100ms. If a +// scroll event comes in and we're not done with the previous frame yet the +// event will be ignored +let render_timeout = false; +const onScroll = (e) => { + if (render_timeout) { + return + } + + render_timeout = true + setTimeout(() => { + render_visible_files() + render_timeout = false + }, 100) +} + +const render_visible_files = () => { const fileHeight = 40 let paddingTop = directoryArea.scrollTop - directoryArea.scrollTop % fileHeight - let start = Math.floor(paddingTop / fileHeight) - 3 + let start = Math.floor(paddingTop / fileHeight) - 5 if (start < 0) { start = 0 } - let end = Math.ceil((paddingTop + directoryArea.clientHeight) / fileHeight) + 3 + let end = Math.ceil((paddingTop + directoryArea.clientHeight) / fileHeight) + 5 if (end > allFiles.length) { end = allFiles.length - 1 } nodeContainer.style.paddingTop = (start * fileHeight) + "px" @@ -187,6 +191,8 @@ const renderVisibleFiles = () => { // pretend that files with filtered == true do not exist let totalFiles = 0 let totalSize = 0 + let selectedFiles = 0 + let selectedSize = 0 for (let i in allFiles) { if (totalFiles >= start && totalFiles <= end && !allFiles[i].filtered) { @@ -197,14 +203,20 @@ const renderVisibleFiles = () => { if (!allFiles[i].filtered) { totalFiles++ totalSize += allFiles[i].size + + if (allFiles[i].selected) { + selectedFiles++ + selectedSize += allFiles[i].size + } } } nodeContainer.style.height = (totalFiles * fileHeight) + "px" - statusBar = totalFiles + " items. Total size: " + formatDataVolume(totalSize, 4) + statusBar = totalFiles + " items ("+formatDataVolume(totalSize, 4)+")" - // Update the view - allFiles = allFiles + if (selectedFiles !== 0) { + statusBar += ", "+selectedFiles+" selected ("+formatDataVolume(selectedSize, 4)+")" + } console.debug( "start " + start + @@ -212,8 +224,51 @@ const renderVisibleFiles = () => { " children " + nodeContainer.childElementCount ) } + +let selectionMode = false +export const setSelectionMode = (s) => { + selectionMode = s + + // When selection mode is disabled we automatically deselect all files + if (!s) { + for (let i in allFiles) { + allFiles[i].selected = false + } + render_visible_files() + } +} + +let shift_pressed = false +const detect_shift = (e) => { + if (e.key !== "Shift") { + return + } + + shift_pressed = e.type === "keydown" +} + +let last_selected_node = -1 +const node_click = (index) => { + if (selectionMode) { + if (shift_pressed && last_selected_node != -1) { + + for (let i = last_selected_node; i <= index && !allFiles[i].filtered; i++) { + allFiles[i].selected = !allFiles[i].selected + } + } else { + allFiles[index].selected = !allFiles[index].selected + } + + last_selected_node = index + render_visible_files() + } else { + window.open(allFiles[index].href, "_blank") + } +} + +
@@ -222,9 +277,15 @@ const renderVisibleFiles = () => { {/each}
- {#each allFiles as file} + {#each allFiles as file, index} {#if file.visible && !file.filtered} - + {node_click(index)}} + >
file thumbnail {file.name} diff --git a/svelte/src/user_file_manager/FileManager.svelte b/svelte/src/user_file_manager/FileManager.svelte index 5ed6a10..8a40d43 100644 --- a/svelte/src/user_file_manager/FileManager.svelte +++ b/svelte/src/user_file_manager/FileManager.svelte @@ -5,20 +5,18 @@ import Spinner from "../util/Spinner.svelte"; import DirectoryElement from "./DirectoryElement.svelte" let loading = true -let navBar -let breadcrumbs = "" -let btnReload let inputSearch let directoryElement let getUserFiles = () => { loading = true - directoryElement.reset() fetch(window.api_endpoint + "/user/files").then(resp => { if (!resp.ok) { Promise.reject("yo") } return resp.json() }).then(resp => { + directoryElement.reset() + for (let i in resp.files) { directoryElement.addFile( window.api_endpoint + "/file/" + resp.files[i].id + "/thumbnail?width=32&height=32", @@ -41,12 +39,13 @@ let getUserFiles = () => { let getUserLists = () => { loading = true - directoryElement.reset() fetch(window.api_endpoint + "/user/lists").then(resp => { if (!resp.ok) { Promise.reject("yo") } return resp.json() }).then(resp => { + directoryElement.reset() + for (let i in resp.lists) { directoryElement.addFile( window.api_endpoint + "/list/" + resp.lists[i].id + "/thumbnail?width=32&height=32", @@ -97,17 +96,21 @@ let hashChange = () => { if (!initialized) { return } - if (window.location.hash === "#files") { - breadcrumbs = "My Files" - getUserFiles() - } else if (window.location.hash === "#lists") { - breadcrumbs = "My Lists" + if (window.location.hash === "#lists") { + document.title = "My Lists" getUserLists() } else { - alert("invalid file manager type") + document.title = "My Files" + getUserFiles() } } +let selecting = false +const toggleSelecting = () => { + selecting = !selecting + directoryElement.setSelectionMode(selecting) +} + onMount(() => { initialized = true hashChange() @@ -118,16 +121,24 @@ onMount(() => {
- -