diff --git a/CHANGELOG.md b/CHANGELOG.md index 5415c456..8ac55085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version). +## v0.3.6 + + **New Features** + - Adds "externalUrl" server config https://github.com/gtsteffaniak/filebrowser/issues/272 + + **Notes**: + - All views modes to show header bar for sorting. + - other small style changes + + **Bugfixes**: + - select and info bug after sorting https://github.com/gtsteffaniak/filebrowser/issues/277 + - downloading from shares with public user + - Ctrl and Shift key modifiers work on listing views as expected. + - copy/move file/folder error and show errors https://github.com/gtsteffaniak/filebrowser/issues/278 + - file move/copy context fix. + ## v0.3.5 **New Features** diff --git a/README.md b/README.md index 7b484161..108b8639 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,9 @@

-> [!Note] -> Starting with v0.3.3, configuration file mapping is different to support non-root user. Now, the default config file name is `config.yaml` and in docker the path is `/home/filebrowser/config.yaml` and `/home/filebrowser/`. Please read the usage below to properly update your config to point the new config location. (open an issue for any help needed) - > [!WARNING] -> There is no stable version yet. Always check release notes for bug fixes on functionality that may have been changed. If you notice any unexpected behavior -- please open an issue to have it fixed soon. +> There is no stable version yet. +> (planned for later this year after these are complete: multiple sources support, initial onboarding page, official automated docs website) FileBrowser Quantum is a fork of the file browser open-source project with the following changes: diff --git a/backend/files/file.go b/backend/files/file.go index b3bb6801..1b5a72fd 100644 --- a/backend/files/file.go +++ b/backend/files/file.go @@ -189,7 +189,7 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error { index := GetIndex(source) // refresh info for source and dest err = index.RefreshFileInfo(FileOptions{ - Path: realsrc, + Path: filepath.Dir(realsrc), IsDir: isSrcDir, }) if err != nil { diff --git a/backend/files/file_test.go b/backend/files/file_test.go index 0e509a3a..22c49987 100644 --- a/backend/files/file_test.go +++ b/backend/files/file_test.go @@ -1,7 +1,6 @@ package files import ( - "fmt" "os" "path/filepath" "reflect" @@ -74,7 +73,6 @@ func Test_GetRealPath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { realPath, isDir, _ := idx.GetRealPath(tt.paths...) - fmt.Println(realPath, trimPrefix) adjustedRealPath := strings.TrimPrefix(realPath, trimPrefix) if tt.want.path != adjustedRealPath || tt.want.isDir != isDir { t.Errorf("expected %v:%v but got: %v:%v", tt.want.path, tt.want.isDir, adjustedRealPath, isDir) diff --git a/backend/fileutils/file.go b/backend/fileutils/file.go index fe43e3f6..e077093b 100644 --- a/backend/fileutils/file.go +++ b/backend/fileutils/file.go @@ -26,9 +26,25 @@ func MoveFile(src, dst string) error { return nil } -// CopyFile copies a file from source to dest and returns -// an error if any. +// CopyFile copies a file or directory from source to dest and returns an error if any. func CopyFile(source, dest string) error { + // Check if the source exists and whether it's a file or directory. + info, err := os.Stat(source) + if err != nil { + return err + } + + if info.IsDir() { + // If the source is a directory, copy it recursively. + return copyDirectory(source, dest) + } + + // If the source is a file, copy the file. + return copySingleFile(source, dest) +} + +// copySingleFile handles copying a single file. +func copySingleFile(source, dest string) error { // Open the source file. src, err := os.Open(source) if err != nil { @@ -36,7 +52,7 @@ func CopyFile(source, dest string) error { } defer src.Close() - // Makes the directory needed to create the dst file. + // Create the destination directory if needed. err = os.MkdirAll(filepath.Dir(dest), 0775) //nolint:gomnd if err != nil { return err @@ -68,6 +84,43 @@ func CopyFile(source, dest string) error { return nil } +// copyDirectory handles copying directories recursively. +func copyDirectory(source, dest string) error { + // Create the destination directory. + err := os.MkdirAll(dest, 0775) //nolint:gomnd + if err != nil { + return err + } + + // Read the contents of the source directory. + entries, err := os.ReadDir(source) + if err != nil { + return err + } + + // Iterate over each entry in the directory. + for _, entry := range entries { + srcPath := filepath.Join(source, entry.Name()) + destPath := filepath.Join(dest, entry.Name()) + + if entry.IsDir() { + // Recursively copy subdirectories. + err = copyDirectory(srcPath, destPath) + if err != nil { + return err + } + } else { + // Copy files. + err = copySingleFile(srcPath, destPath) + if err != nil { + return err + } + } + } + + return nil +} + // CommonPrefix returns the common directory path of provided files. func CommonPrefix(sep byte, paths ...string) string { // Handle special cases. diff --git a/backend/http/public.go b/backend/http/public.go index f2d0d8e3..d52f929e 100644 --- a/backend/http/public.go +++ b/backend/http/public.go @@ -15,7 +15,7 @@ import ( func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { file, ok := d.raw.(files.ExtendedFileInfo) if !ok { - return http.StatusInternalServerError, fmt.Errorf("failed to assert type *files.FileInfo") + return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo") } file.Path = strings.TrimPrefix(file.Path, files.RootPaths["default"]) return renderJSON(w, r, file) @@ -28,23 +28,6 @@ func publicUserGetHandler(w http.ResponseWriter, r *http.Request) { if err != nil { http.Error(w, http.StatusText(status), status) } - -} - -func publicDlHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { - file, ok := d.raw.(files.ExtendedFileInfo) - if !ok { - return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo") - } - if d.user == nil { - return http.StatusUnauthorized, fmt.Errorf("failed to get user") - } - - if file.Type == "directory" { - return rawFilesHandler(w, r, d, []string{file.Path}) - } - - return rawFileHandler(w, r, file.FileInfo) } // health godoc diff --git a/backend/http/raw.go b/backend/http/raw.go index 4571c023..45ba80b2 100644 --- a/backend/http/raw.go +++ b/backend/http/raw.go @@ -45,13 +45,23 @@ func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, if !d.user.Perm.Download { return http.StatusAccepted, nil } + + filePrefix := "" + file, ok := d.raw.(files.ExtendedFileInfo) + if ok { + filePrefix = file.Path + } encodedFiles := r.URL.Query().Get("files") // Decode the URL-encoded path files, err := url.QueryUnescape(encodedFiles) if err != nil { return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err) } - return rawFilesHandler(w, r, d, strings.Split(files, ",")) + fileList := strings.Split(files, ",") + for i, f := range fileList { + fileList[i] = filepath.Join(filePrefix, f) + } + return rawFilesHandler(w, r, d, fileList) } func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *zip.Writer) error { @@ -186,7 +196,8 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext, if baseDirName == "" || baseDirName == "/" { baseDirName = "download" } - downloadFileName := url.PathEscape(baseDirName + "." + algo) + downloadFileName := url.PathEscape(baseDirName + extension) + w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+downloadFileName) // Create the archive and stream it directly to the response if extension == ".zip" { diff --git a/backend/http/resource.go b/backend/http/resource.go index 5d5a324d..630b3c4f 100644 --- a/backend/http/resource.go +++ b/backend/http/resource.go @@ -283,10 +283,10 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont return errToStatus(err), err } if !d.user.Check(src) || !d.user.Check(dst) { - return http.StatusForbidden, nil + return http.StatusForbidden, fmt.Errorf("forbidden: user rules deny access to source or destination") } if dst == "/" || src == "/" { - return http.StatusForbidden, nil + return http.StatusForbidden, fmt.Errorf("forbidden: source or destination is attempting to modify root") } idx := files.GetIndex(source) @@ -307,7 +307,7 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont } // Permission for overwriting the file if overwrite && !d.user.Perm.Modify { - return http.StatusForbidden, nil + return http.StatusForbidden, fmt.Errorf("forbidden: user does not have permission to overwrite file") } err = d.RunHook(func() error { return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source) diff --git a/backend/http/router.go b/backend/http/router.go index 6a4fdfbc..b9bff04e 100644 --- a/backend/http/router.go +++ b/backend/http/router.go @@ -107,7 +107,7 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) { // Public routes api.HandleFunc("GET /public/publicUser", publicUserGetHandler) - api.HandleFunc("GET /public/dl", withHashFile(publicDlHandler)) + api.HandleFunc("GET /public/dl", withHashFile(rawHandler)) api.HandleFunc("GET /public/share", withHashFile(publicShareHandler)) // Settings routes diff --git a/backend/http/static.go b/backend/http/static.go index f28058ab..c008c2c4 100644 --- a/backend/http/static.go +++ b/backend/http/static.go @@ -63,6 +63,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT "EnableExec": config.Server.EnableExec, "ReCaptchaHost": config.Auth.Recaptcha.Host, "ExternalLinks": config.Frontend.ExternalLinks, + "ExternalUrl": strings.TrimSuffix(config.Server.ExternalUrl, "/"), } if config.Frontend.Files != "" { diff --git a/backend/settings/structs.go b/backend/settings/structs.go index 59db6722..388c2d25 100644 --- a/backend/settings/structs.go +++ b/backend/settings/structs.go @@ -53,6 +53,7 @@ type Server struct { UserHomeBasePath string `json:"userHomeBasePath"` CreateUserDir bool `json:"createUserDir"` Sources map[string]Source `json:"sources"` + ExternalUrl string `json:"externalUrl"` } type Source struct { diff --git a/frontend/src/api/files.js b/frontend/src/api/files.js index a5a2ae48..f767ce52 100644 --- a/frontend/src/api/files.js +++ b/frontend/src/api/files.js @@ -125,30 +125,38 @@ export async function post(url, content = "", overwrite = false, onupload) { } export async function moveCopy(items, action = "copy", overwrite = false, rename = false) { - let promises = []; let params = { overwrite: overwrite, action: action, rename: rename, - } + }; try { - for (let item of items) { + // Create an array of fetch calls + let promises = items.map((item) => { let toPath = encodeURIComponent(removePrefix(decodeURI(item.to), "files")); let fromPath = encodeURIComponent(removePrefix(decodeURI(item.from), "files")); - let localParams = { ...params }; - localParams.destination = toPath; - localParams.from = fromPath; + let localParams = { ...params, destination: toPath, from: fromPath }; const apiPath = getApiPath("api/resources", localParams); - promises.push(fetch(apiPath, { method: "PATCH" })); - } - return promises; + return fetch(apiPath, { method: "PATCH" }).then((response) => { + if (!response.ok) { + // Throw an error if the fetch fails + return response.text().then((text) => { + throw new Error(`Failed to move/copy: ${text || response.statusText}`); + }); + } + return response; + }); + }); + // Await all promises and ensure errors propagate + await Promise.all(promises); } catch (err) { notify.showError(err.message || "Error moving/copying resources"); - throw err; + throw err; // Re-throw the error to propagate it back to the caller } } + export async function checksum(url, algo) { try { const data = await resourceAction(`${url}?checksum=${algo}`, "GET"); diff --git a/frontend/src/api/share.js b/frontend/src/api/share.js index 8108efee..74883b0e 100644 --- a/frontend/src/api/share.js +++ b/frontend/src/api/share.js @@ -1,6 +1,7 @@ import { fetchURL, fetchJSON, adjustedData } from "./utils"; import { notify } from "@/notify"; import { getApiPath } from "@/utils/url.js"; +import { externalUrl } from "@/utils/constants"; export async function list() { const apiPath = getApiPath("api/shares"); @@ -41,5 +42,8 @@ export async function create(path, password = "", expires = "", unit = "hours") } export function getShareURL(share) { + if (externalUrl) { + return externalUrl+getApiPath(`share/${share.hash}`); + } return window.origin+getApiPath(`share/${share.hash}`); } diff --git a/frontend/src/api/utils.js b/frontend/src/api/utils.js index 335f2034..705b8e1f 100644 --- a/frontend/src/api/utils.js +++ b/frontend/src/api/utils.js @@ -67,8 +67,7 @@ export function adjustedData(data, url) { // Combine folders and files into items data.items = [...(data.folders || []), ...(data.files || [])]; - data.items = data.items.map((item, index) => { - item.index = index; + data.items = data.items.map((item) => { item.url = `${data.url}${item.name}`; if (item.type === "directory") { item.url += "/"; diff --git a/frontend/src/api/utils.test.js b/frontend/src/api/utils.test.js index b54b81d2..6b627228 100644 --- a/frontend/src/api/utils.test.js +++ b/frontend/src/api/utils.test.js @@ -23,10 +23,10 @@ describe('adjustedData', () => { folders: [], files: [], items: [ - { name: "folder1", type: "directory", index: 0, url: "http://example.com/unit-testing/files/path/to/directory/folder1/" }, - { name: "folder2", type: "directory", index: 1, url: "http://example.com/unit-testing/files/path/to/directory/folder2/" }, - { name: "file1.txt", type: "file", index: 2, url: "http://example.com/unit-testing/files/path/to/directory/file1.txt" }, - { name: "file2.txt", type: "file", index: 3, url: "http://example.com/unit-testing/files/path/to/directory/file2.txt" }, + { name: "folder1", type: "directory", url: "http://example.com/unit-testing/files/path/to/directory/folder1/" }, + { name: "folder2", type: "directory", url: "http://example.com/unit-testing/files/path/to/directory/folder2/" }, + { name: "file1.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file1.txt" }, + { name: "file2.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file2.txt" }, ], }; diff --git a/frontend/src/components/ContextMenu.vue b/frontend/src/components/ContextMenu.vue index e4f7807f..e9a0ffff 100644 --- a/frontend/src/components/ContextMenu.vue +++ b/frontend/src/components/ContextMenu.vue @@ -109,7 +109,7 @@ export default { return state.user; }, centered() { - return getters.isMobile() || ( !this.posX || !this.posY ); + return getters.isMobile() || !this.posX || !this.posY; }, showContext() { if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) { diff --git a/frontend/src/components/files/ListingItem.vue b/frontend/src/components/files/ListingItem.vue index 01de1d38..44f5f48e 100644 --- a/frontend/src/components/files/ListingItem.vue +++ b/frontend/src/components/files/ListingItem.vue @@ -179,7 +179,7 @@ export default { }, methods: { handleTouchMove(event) { - if (!state.isSafari) return + if (!state.isSafari) return; const touch = event.touches[0]; const deltaX = Math.abs(touch.clientX - this.touchStartX); const deltaY = Math.abs(touch.clientY - this.touchStartY); @@ -191,7 +191,7 @@ export default { } }, handleTouchEnd() { - if (!state.isSafari) return + if (!state.isSafari) return; this.cancelContext(); // Clear timeout this.isSwipe = false; // Reset swipe state }, @@ -214,8 +214,8 @@ export default { }, onRightClick(event) { event.preventDefault(); // Prevent default context menu - // If no items are selected, select the right-clicked item - if (!state.multiple) { + // If one or fewer items are selected, reset the selection + if (!state.multiple && getters.selectedCount() < 2) { mutations.resetSelected(); mutations.addSelected(this.index); } @@ -247,7 +247,9 @@ export default { getTime() { if (state.user.dateFormat) { // Truncate the fractional seconds to 3 digits (milliseconds) - const sanitizedString = this.modified.replace(/\.\d+/, (match) => match.slice(0, 4)); + const sanitizedString = this.modified.replace(/\.\d+/, (match) => + match.slice(0, 4) + ); // Parse the sanitized string into a Date object const date = new Date(sanitizedString); return date.toLocaleString(); @@ -333,7 +335,7 @@ export default { action(overwrite, rename); }, addSelected(event) { - if (!state.isSafari) return + if (!state.isSafari) return; const touch = event.touches[0]; this.touchStartX = touch.clientX; this.touchStartY = touch.clientY; @@ -357,7 +359,11 @@ export default { } } - if (!state.user.singleClick && getters.selectedCount() !== 0 && event.button === 0) { + if ( + !state.user.singleClick && + getters.selectedCount() !== 0 && + event.button === 0 + ) { event.preventDefault(); } setTimeout(() => { @@ -393,7 +399,12 @@ export default { return; } - if (!state.user.singleClick && !event.ctrlKey && !event.metaKey && !state.multiple) { + if ( + !state.user.singleClick && + !event.ctrlKey && + !event.metaKey && + !state.multiple + ) { mutations.resetSelected(); } mutations.addSelected(this.index); @@ -411,4 +422,4 @@ export default { .item { -webkit-touch-callout: none; /* Disable the default long press preview */ } - \ No newline at end of file + diff --git a/frontend/src/components/prompts/Copy.vue b/frontend/src/components/prompts/Copy.vue index 6767444f..5fb1338c 100644 --- a/frontend/src/components/prompts/Copy.vue +++ b/frontend/src/components/prompts/Copy.vue @@ -52,7 +52,8 @@ import FileList from "./FileList.vue"; import { filesApi } from "@/api"; import buttons from "@/utils/buttons"; import * as upload from "@/utils/upload"; -//import { notify } from "@/notify"; +import { removePrefix } from "@/utils/url"; +import { notify } from "@/notify"; export default { name: "copy", @@ -61,6 +62,7 @@ export default { return { current: window.location.pathname, dest: null, + items: [], }; }, computed: { @@ -71,57 +73,63 @@ export default { return mutations.closeHovers(); }, }, + mounted() { + for (let item of state.selected) { + this.items.push({ + from: state.req.items[item].url, + // add to: dest + name: state.req.items[item].name, + }); + } + }, methods: { copy: async function (event) { event.preventDefault(); - let items = []; + try { + // Define the action function + let action = async (overwrite, rename) => { + const loc = removePrefix(this.dest, "files"); + for (let item of this.items) { + item.to = loc + "/" + item.name; + } + buttons.loading("copy"); + await filesApi.moveCopy(this.items, "copy", overwrite, rename); + }; + // Fetch destination files + let dstResp = await filesApi.fetchFiles(this.dest); + let conflict = upload.checkConflict(this.items, dstResp.items); + let overwrite = false; + let rename = false; - // Create a new promise for each file. - for (let item of state.selected) { - items.push({ - from: state.req.items[item].url, - to: this.dest + encodeURIComponent(state.req.items[item].name), - name: state.req.items[item].name, - }); - } - - let action = async (overwrite, rename) => { - buttons.loading("copy"); - await filesApi.moveCopy(items, "copy", overwrite, rename); - this.$router.push({ path: this.dest }); - mutations.setReload(true); - }; - - if (state.route.path === this.dest) { + if (conflict) { + await new Promise((resolve, reject) => { + mutations.showHover({ + name: "replace-rename", + confirm: async (event, option) => { + overwrite = option == "overwrite"; + rename = option == "rename"; + event.preventDefault(); + try { + await action(overwrite, rename); + resolve(); // Resolve the promise if action succeeds + } catch (e) { + reject(e); // Reject the promise if an error occurs + } + }, + }); + }); + } else { + // Await the action call for non-conflicting cases + await action(overwrite, rename); + } mutations.closeHovers(); - action(false, true); - - return; + notify.showSuccess("Successfully copied file/folder, redirecting..."); + setTimeout(() => { + this.$router.push(this.dest); + }, 1000); + } catch (error) { + notify.error(error); } - - let dstItems = (await filesApi.fetchFiles(this.dest)).items; - let conflict = upload.checkConflict(items, dstItems); - - let overwrite = false; - let rename = false; - - if (conflict) { - mutations.showHover({ - name: "replace-rename", - confirm: (event, option) => { - overwrite = option == "overwrite"; - rename = option == "rename"; - - event.preventDefault(); - mutations.closeHovers(); - action(overwrite, rename); - }, - }); - - return; - } - - action(overwrite, rename); }, }, }; diff --git a/frontend/src/components/prompts/FileList.vue b/frontend/src/components/prompts/FileList.vue index 14e6d515..5c23ad45 100644 --- a/frontend/src/components/prompts/FileList.vue +++ b/frontend/src/components/prompts/FileList.vue @@ -23,6 +23,7 @@ diff --git a/frontend/src/views/files/ListingView.vue b/frontend/src/views/files/ListingView.vue index 324b2609..e8fa4aba 100644 --- a/frontend/src/views/files/ListingView.vue +++ b/frontend/src/views/files/ListingView.vue @@ -16,26 +16,64 @@ sentiment_dissatisfied {{ $t("files.lonely") }} - - + + -
+
-

+

{{ $t("files.name") }} {{ nameIcon }}

-

+

{{ $t("files.size") }} {{ sizeIcon }}

-

+

{{ $t("files.lastModified") }} {{ modifiedIcon }}

@@ -46,10 +84,23 @@

{{ $t("files.folders") }}

-
- +
+
@@ -57,14 +108,35 @@
- +
- - + +
@@ -93,6 +165,7 @@ export default { width: window.innerWidth, lastSelected: {}, // Add this to track the currently focused item contextTimeout: null, // added for safari context menu + ctrKeyPressed: false, }; }, watch: { @@ -196,7 +269,6 @@ export default { this.colunmsResize(); return state.user.viewMode; }, - selectedCount() { return state.selected.length; }, @@ -215,7 +287,8 @@ export default { window.addEventListener("keydown", this.keyEvent); window.addEventListener("scroll", this.scrollEvent); window.addEventListener("resize", this.windowsResize); - this.$el.addEventListener("click", this.clickClear); + window.addEventListener("click", this.clickClear); + window.addEventListener("keyup", this.clearCtrKey); // Adjust contextmenu listener based on browser if (state.isSafari) { @@ -250,7 +323,6 @@ export default { this.$el.removeEventListener("touchend", this.cancelContext); this.$el.removeEventListener("mouseup", this.cancelContext); this.$el.removeEventListener("touchmove", this.handleTouchMove); - } else { window.removeEventListener("contextmenu", this.openContext); } @@ -425,17 +497,22 @@ export default { }, 50); } }, + clearCtrKey(event) { + const { ctrlKey } = event; + if (!ctrlKey) { + this.ctrKeyPressed = false; + } + }, keyEvent(event) { const { key, ctrlKey, metaKey, which } = event; // Check if the key is alphanumeric const isAlphanumeric = /^[a-z0-9]$/i.test(key); - const noModifierKeys = !ctrlKey && !metaKey; - - if (isAlphanumeric && noModifierKeys && getters.currentPromptName() == null) { + const modifierKeys = ctrlKey || metaKey; + if (isAlphanumeric && !modifierKeys && getters.currentPromptName() == null) { this.alphanumericKeyPress(key); // Call the alphanumeric key press function return; } - if (noModifierKeys && getters.currentPromptName() != null) { + if (!modifierKeys && getters.currentPromptName() != null) { return; } // Handle the space bar key @@ -452,6 +529,13 @@ export default { } let currentPath = state.route.path.replace(/\/+$/, ""); // Remove trailing slashes let newPath = currentPath.substring(0, currentPath.lastIndexOf("/")); + + if (modifierKeys) { + if (!ctrlKey) { + this.ctrKeyPressed = true; + } + return; + } // Handle key events using a switch statement switch (key) { case "Enter": @@ -486,11 +570,6 @@ export default { event.preventDefault(); this.navigateKeboardArrows(key); break; - - default: - // Handle keys with ctrl or meta keys - if (!ctrlKey && !metaKey) return; - break; } const charKey = String.fromCharCode(which).toLowerCase(); @@ -513,8 +592,6 @@ export default { break; } }, - - // Helper method to select all files and directories selectAll() { for (let file of this.items.files) { if (state.selected.indexOf(file.index) === -1) { @@ -650,12 +727,18 @@ export default { action(false, false); }, colunmsResize() { - document.documentElement.style.setProperty('--item-width', `calc(${100 / this.numColumns}% - 1em)`); + document.documentElement.style.setProperty( + "--item-width", + `calc(${100 / this.numColumns}% - 1em)` + ); if (state.user.viewMode == "gallery") { - document.documentElement.style.setProperty('--item-height', `calc(${this.columnWidth / 25}em)`); + document.documentElement.style.setProperty( + "--item-height", + `calc(${this.columnWidth / 25}em)` + ); } else { - document.documentElement.style.setProperty('--item-height', `auto`); + document.documentElement.style.setProperty("--item-height", `auto`); } }, dragEnter() { @@ -814,9 +897,11 @@ export default { }, }); }, - clickClear() { + clickClear(event) { + // if control or shift is pressed, do not clear the selection + if (this.ctrKeyPressed || event.shiftKey) return; const sameAsBefore = state.selected == this.lastSelected; - if (sameAsBefore && !state.multiple) { + if (sameAsBefore && !state.multiple && getters.currentPromptName == null) { mutations.resetSelected(); } this.lastSelected = state.selected; diff --git a/frontend/src/views/settings/User.vue b/frontend/src/views/settings/User.vue index d251b7df..cb9ac323 100644 --- a/frontend/src/views/settings/User.vue +++ b/frontend/src/views/settings/User.vue @@ -110,7 +110,6 @@ export default { event.preventDefault(); try { if (this.isNew) { - const loc = await usersApi.create(this.userPayload); // Use the computed property this.$router.push({ path: "/settings", hash: "#users-main" }); notify.showSuccess(this.$t("settings.userCreated")); } else {