Merge branch 'main' of github.com:gtsteffaniak/filebrowser

This commit is contained in:
Graham Steffaniak 2025-01-08 19:22:45 -06:00
commit e922a92aa6
35 changed files with 509 additions and 584 deletions

View File

@ -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). 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 ## v0.3.5
**New Features** **New Features**

View File

@ -9,11 +9,9 @@
<img width="800" src="https://github.com/user-attachments/assets/b16acd67-0292-437a-a06c-bc83f95758e6" title="Main Screenshot"> <img width="800" src="https://github.com/user-attachments/assets/b16acd67-0292-437a-a06c-bc83f95758e6" title="Main Screenshot">
</p> </p>
> [!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/<database_file>`. Please read the usage below to properly update your config to point the new config location. (open an issue for any help needed)
> [!WARNING] > [!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: FileBrowser Quantum is a fork of the file browser open-source project with the following changes:

View File

@ -189,7 +189,7 @@ func MoveResource(source, realsrc, realdst string, isSrcDir bool) error {
index := GetIndex(source) index := GetIndex(source)
// refresh info for source and dest // refresh info for source and dest
err = index.RefreshFileInfo(FileOptions{ err = index.RefreshFileInfo(FileOptions{
Path: realsrc, Path: filepath.Dir(realsrc),
IsDir: isSrcDir, IsDir: isSrcDir,
}) })
if err != nil { if err != nil {

View File

@ -1,7 +1,6 @@
package files package files
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -74,7 +73,6 @@ func Test_GetRealPath(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
realPath, isDir, _ := idx.GetRealPath(tt.paths...) realPath, isDir, _ := idx.GetRealPath(tt.paths...)
fmt.Println(realPath, trimPrefix)
adjustedRealPath := strings.TrimPrefix(realPath, trimPrefix) adjustedRealPath := strings.TrimPrefix(realPath, trimPrefix)
if tt.want.path != adjustedRealPath || tt.want.isDir != isDir { 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) t.Errorf("expected %v:%v but got: %v:%v", tt.want.path, tt.want.isDir, adjustedRealPath, isDir)

View File

@ -26,9 +26,25 @@ func MoveFile(src, dst string) error {
return nil return nil
} }
// CopyFile copies a file from source to dest and returns // CopyFile copies a file or directory from source to dest and returns an error if any.
// an error if any.
func CopyFile(source, dest string) error { 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. // Open the source file.
src, err := os.Open(source) src, err := os.Open(source)
if err != nil { if err != nil {
@ -36,7 +52,7 @@ func CopyFile(source, dest string) error {
} }
defer src.Close() 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 err = os.MkdirAll(filepath.Dir(dest), 0775) //nolint:gomnd
if err != nil { if err != nil {
return err return err
@ -68,6 +84,43 @@ func CopyFile(source, dest string) error {
return nil 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. // CommonPrefix returns the common directory path of provided files.
func CommonPrefix(sep byte, paths ...string) string { func CommonPrefix(sep byte, paths ...string) string {
// Handle special cases. // Handle special cases.

View File

@ -15,7 +15,7 @@ import (
func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
file, ok := d.raw.(files.ExtendedFileInfo) file, ok := d.raw.(files.ExtendedFileInfo)
if !ok { 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"]) file.Path = strings.TrimPrefix(file.Path, files.RootPaths["default"])
return renderJSON(w, r, file) return renderJSON(w, r, file)
@ -28,23 +28,6 @@ func publicUserGetHandler(w http.ResponseWriter, r *http.Request) {
if err != nil { if err != nil {
http.Error(w, http.StatusText(status), status) 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 // health godoc

View File

@ -45,13 +45,23 @@ func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
if !d.user.Perm.Download { if !d.user.Perm.Download {
return http.StatusAccepted, nil return http.StatusAccepted, nil
} }
filePrefix := ""
file, ok := d.raw.(files.ExtendedFileInfo)
if ok {
filePrefix = file.Path
}
encodedFiles := r.URL.Query().Get("files") encodedFiles := r.URL.Query().Get("files")
// Decode the URL-encoded path // Decode the URL-encoded path
files, err := url.QueryUnescape(encodedFiles) files, err := url.QueryUnescape(encodedFiles)
if err != nil { if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err) 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 { 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 == "/" { if baseDirName == "" || baseDirName == "/" {
baseDirName = "download" baseDirName = "download"
} }
downloadFileName := url.PathEscape(baseDirName + "." + algo) downloadFileName := url.PathEscape(baseDirName + extension)
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+downloadFileName) w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+downloadFileName)
// Create the archive and stream it directly to the response // Create the archive and stream it directly to the response
if extension == ".zip" { if extension == ".zip" {

View File

@ -283,10 +283,10 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
return errToStatus(err), err return errToStatus(err), err
} }
if !d.user.Check(src) || !d.user.Check(dst) { 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 == "/" { 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) idx := files.GetIndex(source)
@ -307,7 +307,7 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
} }
// Permission for overwriting the file // Permission for overwriting the file
if overwrite && !d.user.Perm.Modify { 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 { err = d.RunHook(func() error {
return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source) return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source)

View File

@ -107,7 +107,7 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
// Public routes // Public routes
api.HandleFunc("GET /public/publicUser", publicUserGetHandler) 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)) api.HandleFunc("GET /public/share", withHashFile(publicShareHandler))
// Settings routes // Settings routes

View File

@ -63,6 +63,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
"EnableExec": config.Server.EnableExec, "EnableExec": config.Server.EnableExec,
"ReCaptchaHost": config.Auth.Recaptcha.Host, "ReCaptchaHost": config.Auth.Recaptcha.Host,
"ExternalLinks": config.Frontend.ExternalLinks, "ExternalLinks": config.Frontend.ExternalLinks,
"ExternalUrl": strings.TrimSuffix(config.Server.ExternalUrl, "/"),
} }
if config.Frontend.Files != "" { if config.Frontend.Files != "" {

View File

@ -53,6 +53,7 @@ type Server struct {
UserHomeBasePath string `json:"userHomeBasePath"` UserHomeBasePath string `json:"userHomeBasePath"`
CreateUserDir bool `json:"createUserDir"` CreateUserDir bool `json:"createUserDir"`
Sources map[string]Source `json:"sources"` Sources map[string]Source `json:"sources"`
ExternalUrl string `json:"externalUrl"`
} }
type Source struct { type Source struct {

View File

@ -125,30 +125,38 @@ export async function post(url, content = "", overwrite = false, onupload) {
} }
export async function moveCopy(items, action = "copy", overwrite = false, rename = false) { export async function moveCopy(items, action = "copy", overwrite = false, rename = false) {
let promises = [];
let params = { let params = {
overwrite: overwrite, overwrite: overwrite,
action: action, action: action,
rename: rename, rename: rename,
} };
try { 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 toPath = encodeURIComponent(removePrefix(decodeURI(item.to), "files"));
let fromPath = encodeURIComponent(removePrefix(decodeURI(item.from), "files")); let fromPath = encodeURIComponent(removePrefix(decodeURI(item.from), "files"));
let localParams = { ...params }; let localParams = { ...params, destination: toPath, from: fromPath };
localParams.destination = toPath;
localParams.from = fromPath;
const apiPath = getApiPath("api/resources", localParams); const apiPath = getApiPath("api/resources", localParams);
promises.push(fetch(apiPath, { method: "PATCH" })); 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 promises; return response;
});
});
// Await all promises and ensure errors propagate
await Promise.all(promises);
} catch (err) { } catch (err) {
notify.showError(err.message || "Error moving/copying resources"); 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) { export async function checksum(url, algo) {
try { try {
const data = await resourceAction(`${url}?checksum=${algo}`, "GET"); const data = await resourceAction(`${url}?checksum=${algo}`, "GET");

View File

@ -1,6 +1,7 @@
import { fetchURL, fetchJSON, adjustedData } from "./utils"; import { fetchURL, fetchJSON, adjustedData } from "./utils";
import { notify } from "@/notify"; import { notify } from "@/notify";
import { getApiPath } from "@/utils/url.js"; import { getApiPath } from "@/utils/url.js";
import { externalUrl } from "@/utils/constants";
export async function list() { export async function list() {
const apiPath = getApiPath("api/shares"); const apiPath = getApiPath("api/shares");
@ -41,5 +42,8 @@ export async function create(path, password = "", expires = "", unit = "hours")
} }
export function getShareURL(share) { export function getShareURL(share) {
if (externalUrl) {
return externalUrl+getApiPath(`share/${share.hash}`);
}
return window.origin+getApiPath(`share/${share.hash}`); return window.origin+getApiPath(`share/${share.hash}`);
} }

View File

@ -67,8 +67,7 @@ export function adjustedData(data, url) {
// Combine folders and files into items // Combine folders and files into items
data.items = [...(data.folders || []), ...(data.files || [])]; data.items = [...(data.folders || []), ...(data.files || [])];
data.items = data.items.map((item, index) => { data.items = data.items.map((item) => {
item.index = index;
item.url = `${data.url}${item.name}`; item.url = `${data.url}${item.name}`;
if (item.type === "directory") { if (item.type === "directory") {
item.url += "/"; item.url += "/";

View File

@ -23,10 +23,10 @@ describe('adjustedData', () => {
folders: [], folders: [],
files: [], files: [],
items: [ items: [
{ name: "folder1", type: "directory", index: 0, url: "http://example.com/unit-testing/files/path/to/directory/folder1/" }, { name: "folder1", type: "directory", 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: "folder2", type: "directory", 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: "file1.txt", type: "file", 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: "file2.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file2.txt" },
], ],
}; };

View File

@ -109,7 +109,7 @@ export default {
return state.user; return state.user;
}, },
centered() { centered() {
return getters.isMobile() || ( !this.posX || !this.posY ); return getters.isMobile() || !this.posX || !this.posY;
}, },
showContext() { showContext() {
if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) { if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {

View File

@ -179,7 +179,7 @@ export default {
}, },
methods: { methods: {
handleTouchMove(event) { handleTouchMove(event) {
if (!state.isSafari) return if (!state.isSafari) return;
const touch = event.touches[0]; const touch = event.touches[0];
const deltaX = Math.abs(touch.clientX - this.touchStartX); const deltaX = Math.abs(touch.clientX - this.touchStartX);
const deltaY = Math.abs(touch.clientY - this.touchStartY); const deltaY = Math.abs(touch.clientY - this.touchStartY);
@ -191,7 +191,7 @@ export default {
} }
}, },
handleTouchEnd() { handleTouchEnd() {
if (!state.isSafari) return if (!state.isSafari) return;
this.cancelContext(); // Clear timeout this.cancelContext(); // Clear timeout
this.isSwipe = false; // Reset swipe state this.isSwipe = false; // Reset swipe state
}, },
@ -214,8 +214,8 @@ export default {
}, },
onRightClick(event) { onRightClick(event) {
event.preventDefault(); // Prevent default context menu event.preventDefault(); // Prevent default context menu
// If no items are selected, select the right-clicked item // If one or fewer items are selected, reset the selection
if (!state.multiple) { if (!state.multiple && getters.selectedCount() < 2) {
mutations.resetSelected(); mutations.resetSelected();
mutations.addSelected(this.index); mutations.addSelected(this.index);
} }
@ -247,7 +247,9 @@ export default {
getTime() { getTime() {
if (state.user.dateFormat) { if (state.user.dateFormat) {
// Truncate the fractional seconds to 3 digits (milliseconds) // 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 // Parse the sanitized string into a Date object
const date = new Date(sanitizedString); const date = new Date(sanitizedString);
return date.toLocaleString(); return date.toLocaleString();
@ -333,7 +335,7 @@ export default {
action(overwrite, rename); action(overwrite, rename);
}, },
addSelected(event) { addSelected(event) {
if (!state.isSafari) return if (!state.isSafari) return;
const touch = event.touches[0]; const touch = event.touches[0];
this.touchStartX = touch.clientX; this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY; 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(); event.preventDefault();
} }
setTimeout(() => { setTimeout(() => {
@ -393,7 +399,12 @@ export default {
return; return;
} }
if (!state.user.singleClick && !event.ctrlKey && !event.metaKey && !state.multiple) { if (
!state.user.singleClick &&
!event.ctrlKey &&
!event.metaKey &&
!state.multiple
) {
mutations.resetSelected(); mutations.resetSelected();
} }
mutations.addSelected(this.index); mutations.addSelected(this.index);

View File

@ -52,7 +52,8 @@ import FileList from "./FileList.vue";
import { filesApi } from "@/api"; import { filesApi } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
//import { notify } from "@/notify"; import { removePrefix } from "@/utils/url";
import { notify } from "@/notify";
export default { export default {
name: "copy", name: "copy",
@ -61,6 +62,7 @@ export default {
return { return {
current: window.location.pathname, current: window.location.pathname,
dest: null, dest: null,
items: [],
}; };
}, },
computed: { computed: {
@ -71,57 +73,63 @@ export default {
return mutations.closeHovers(); return mutations.closeHovers();
}, },
}, },
methods: { mounted() {
copy: async function (event) {
event.preventDefault();
let items = [];
// Create a new promise for each file.
for (let item of state.selected) { for (let item of state.selected) {
items.push({ this.items.push({
from: state.req.items[item].url, from: state.req.items[item].url,
to: this.dest + encodeURIComponent(state.req.items[item].name), // add to: dest
name: state.req.items[item].name, name: state.req.items[item].name,
}); });
} }
},
methods: {
copy: async function (event) {
event.preventDefault();
try {
// Define the action function
let action = async (overwrite, rename) => { let action = async (overwrite, rename) => {
buttons.loading("copy"); const loc = removePrefix(this.dest, "files");
await filesApi.moveCopy(items, "copy", overwrite, rename); for (let item of this.items) {
this.$router.push({ path: this.dest }); item.to = loc + "/" + item.name;
mutations.setReload(true);
};
if (state.route.path === this.dest) {
mutations.closeHovers();
action(false, true);
return;
} }
buttons.loading("copy");
let dstItems = (await filesApi.fetchFiles(this.dest)).items; await filesApi.moveCopy(this.items, "copy", overwrite, rename);
let conflict = upload.checkConflict(items, dstItems); };
// Fetch destination files
let dstResp = await filesApi.fetchFiles(this.dest);
let conflict = upload.checkConflict(this.items, dstResp.items);
let overwrite = false; let overwrite = false;
let rename = false; let rename = false;
if (conflict) { if (conflict) {
await new Promise((resolve, reject) => {
mutations.showHover({ mutations.showHover({
name: "replace-rename", name: "replace-rename",
confirm: (event, option) => { confirm: async (event, option) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
mutations.closeHovers(); try {
action(overwrite, rename); await action(overwrite, rename);
resolve(); // Resolve the promise if action succeeds
} catch (e) {
reject(e); // Reject the promise if an error occurs
}
}, },
}); });
});
return; } else {
// Await the action call for non-conflicting cases
await action(overwrite, rename);
}
mutations.closeHovers();
notify.showSuccess("Successfully copied file/folder, redirecting...");
setTimeout(() => {
this.$router.push(this.dest);
}, 1000);
} catch (error) {
notify.error(error);
} }
action(overwrite, rename);
}, },
}, },
}; };

View File

@ -23,6 +23,7 @@
<script> <script>
import { state, mutations } from "@/store"; import { state, mutations } from "@/store";
import url from "@/utils/url.js"; import url from "@/utils/url.js";
import { removePrefix } from "@/utils/url.js";
import { filesApi } from "@/api"; import { filesApi } from "@/api";
export default { export default {
@ -40,7 +41,7 @@ export default {
}, },
computed: { computed: {
nav() { nav() {
return decodeURIComponent(this.current); return removePrefix(decodeURIComponent(this.current), "files");
}, },
}, },
mounted() { mounted() {

View File

@ -53,6 +53,7 @@ import { filesApi } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { notify } from "@/notify"; import { notify } from "@/notify";
import { removePrefix } from "@/utils/url";
export default { export default {
name: "move", name: "move",
@ -61,6 +62,7 @@ export default {
return { return {
current: window.location.pathname, current: window.location.pathname,
dest: null, dest: null,
items: [],
}; };
}, },
computed: { computed: {
@ -71,50 +73,66 @@ export default {
return mutations.closeHovers(); return mutations.closeHovers();
}, },
}, },
methods: { mounted() {
move: async function (event) {
event.preventDefault();
let items = [];
for (let item of state.selected) { for (let item of state.selected) {
items.push({ this.items.push({
from: state.req.items[item].url, from: state.req.items[item].url,
to: this.dest + state.req.items[item].name, // add to: dest
name: state.req.items[item].name, name: state.req.items[item].name,
}); });
} }
},
methods: {
move: async function (event) {
event.preventDefault();
try {
// Define the action function
let action = async (overwrite, rename) => { let action = async (overwrite, rename) => {
const loc = removePrefix(this.dest, "files");
for (let item of this.items) {
item.to = loc + "/" + item.name;
}
buttons.loading("move"); buttons.loading("move");
await filesApi.moveCopy(items, "move", overwrite, rename); await filesApi.moveCopy(this.items, "move", overwrite, rename);
buttons.success("move");
this.$router.push({ path: this.dest });
mutations.closeHovers();
}; };
let dstItems = (await filesApi.fetchFiles(this.dest)).items; // Fetch destination files
let conflict = upload.checkConflict(items, dstItems); let dstResp = await filesApi.fetchFiles(this.dest);
let conflict = upload.checkConflict(this.items, dstResp.items);
let overwrite = false; let overwrite = false;
let rename = false; let rename = false;
try {
if (conflict) { if (conflict) {
await new Promise((resolve, reject) => {
mutations.showHover({ mutations.showHover({
name: "replace-rename", name: "replace-rename",
confirm: (event, option) => { confirm: async (event, option) => {
overwrite = option == "overwrite"; overwrite = option == "overwrite";
rename = option == "rename"; rename = option == "rename";
event.preventDefault(); event.preventDefault();
action(overwrite, rename); try {
await action(overwrite, rename);
resolve(); // Resolve the promise if action succeeds
} catch (e) {
reject(e); // Reject the promise if an error occurs
}
}, },
}); });
return; });
} else {
// Await the action call for non-conflicting cases
await action(overwrite, rename);
} }
action(overwrite, rename); mutations.closeHovers();
notify.showSuccess("Successfully moved file/folder, redirecting...");
setTimeout(() => {
this.$router.push(this.dest);
}, 1000);
} catch (e) { } catch (e) {
// Catch any errors from action or other parts of the flow
notify.showError(e); notify.showError(e);
} }
return;
}, },
}, },
}; };

View File

@ -93,14 +93,13 @@ export default {
this.$router.push({ path: uri }); this.$router.push({ path: uri });
} else if (!this.base) { } else if (!this.base) {
const res = await filesApi.fetchFiles(url.removeLastDir(uri) + "/"); const res = await filesApi.fetchFiles(url.removeLastDir(uri) + "/");
mutations.updateRequest(res); mutations.replaceRequest(res);
} }
mutations.closeHovers(); mutations.closeHovers();
} catch (error) { } catch (error) {
notify.showError(error); notify.showError(error);
} }
}, },
}, },
}; };

View File

@ -206,7 +206,12 @@ export default {
if (isPermanent) { if (isPermanent) {
res = await shareApi.create(this.subpath, this.password); res = await shareApi.create(this.subpath, this.password);
} else { } else {
res = await shareApi.create(this.subpath, this.password, this.time.toString(), this.unit); res = await shareApi.create(
this.subpath,
this.password,
this.time.toString(),
this.unit
);
} }
this.links.push(res); this.links.push(res);

View File

@ -59,10 +59,10 @@ body.rtl #listingView {
#listingView .item i { #listingView .item i {
font-size: 4em; font-size: 4em;
margin-right: 0.1em;
vertical-align: bottom; vertical-align: bottom;
} }
#listingView .item img { #listingView .item img {
width: 4em; width: 4em;
height: 4em; height: 4em;
@ -177,11 +177,6 @@ body.rtl #listingView {
border-radius: 0.5em; border-radius: 0.5em;
} }
#listingView.gallery .size,
#listingView.gallery .modified {
display: none;
}
#listingView.compact { #listingView.compact {
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
@ -220,15 +215,7 @@ body.rtl #listingView {
align-items: center; align-items: center;
} }
#listingView.compact .header .name, #listingView .header .name {
#listingView.list .header .name,
#listingView.list .item .name,
#listingView.compact .item .name {
width: 50%;
}
#listingView.compact .header .name,
#listingView.list .header .name {
margin-right: 1.5em; margin-right: 1.5em;
} }
@ -237,14 +224,23 @@ body.rtl #listingView {
cursor: pointer; cursor: pointer;
} }
#listingView.compact .header .size, #listingView .header .size,
#listingView.list .header .size,
#listingView.list .item .size, #listingView.list .item .size,
#listingView.compact .item .size { #listingView.compact .item .size {
width: 25%; width: 25%;
} }
#listingView.compact .header i { #listingView .header .name,
#listingView.list .item .name,
#listingView.compact .item .name {
width: 50%;
}
#listingView.normal .item .text {
padding-left: 0.3em;
}
#listingView .header i {
font-size: 1.5em; font-size: 1.5em;
vertical-align: middle; vertical-align: middle;
margin-left: .2em; margin-left: .2em;
@ -275,25 +271,21 @@ body.rtl #listingView {
width: 0; width: 0;
} }
#listingView.compact .name {
font-weight: normal;
}
#listingView.compact .header span { #listingView.compact .header span {
vertical-align: middle; vertical-align: middle;
} }
#listingView.compact .header i { #listingView .header i {
opacity: 0; opacity: 0;
transition: .1s ease all; transition: .1s ease all;
} }
#listingView.compact .header p:hover i, #listingView .header p:hover i,
#listingView.compact .header .active i { #listingView .header .active i {
opacity: 1; opacity: 1;
} }
#listingView.compact .header .active { #listingView .header .active {
font-weight: bold; font-weight: bold;
} }
@ -312,11 +304,28 @@ body.rtl #listingView {
border-top: 0; border-top: 0;
} }
#listingView .item[aria-selected=true] { #listingView.compact .item[aria-selected=true],
#listingView.normal .item[aria-selected=true],
#listingView.list .item[aria-selected=true] {
background: var(--primaryColor) !important; background: var(--primaryColor) !important;
color: var(--item-selected) !important; color: var(--item-selected) !important;
} }
#listingView.gallery .item[aria-selected=true] {
border: 1em solid var(--primaryColor) !important;
border-radius: 1em;
padding: 0;
}
#listingView.gallery .item[aria-selected=true] .text {
padding: 0;
}
#listingView.gallery .item .size,
#listingView.gallery .item .modified {
opacity: 0;
}
#listingView.list .item div:first-of-type { #listingView.list .item div:first-of-type {
width: 3em; width: 3em;
} }
@ -337,13 +346,18 @@ body.rtl #listingView {
} }
#listingView .header { #listingView .header {
display: none; display: flex !important;
background: white; background: white;
border-radius: 1em; border-radius: 1em;
border: 1px solid rgba(0, 0, 0, .1); border: 1px solid rgba(0, 0, 0, .1);
z-index: 999;
padding: .85em;
width: 100%;
} }
#listingView.list .header i { #listingView.list .header i,
#listingView.normal .header i,
#listingView.gallary .header i {
font-size: 1.5em; font-size: 1.5em;
vertical-align: middle; vertical-align: middle;
margin-left: .2em; margin-left: .2em;
@ -351,12 +365,8 @@ body.rtl #listingView {
#listingView.compact .header, #listingView.compact .header,
#listingView.list .header { #listingView.list .header {
display: flex !important;
border-top-left-radius: 1em; border-top-left-radius: 1em;
border-top-right-radius: 1em; border-top-right-radius: 1em;
z-index: 999;
padding: .85em;
width: 100%;
} }
#listingView.list .item:first-child { #listingView.list .item:first-child {
@ -383,7 +393,9 @@ body.rtl #listingView {
display:flex; display:flex;
width: 100%; width: 100%;
} }
#listingView.list .header { #listingView.list .header,
#listingView.normal .header,
#listingView.gallery .header {
margin-bottom: .5em; margin-bottom: .5em;
} }
@ -391,11 +403,11 @@ body.rtl #listingView {
color: inherit; color: inherit;
} }
#listingView.list .name { #listingView .name {
font-weight: normal; font-weight: normal;
} }
#listingView.list .header span { #listingView .header span {
vertical-align: middle; vertical-align: middle;
} }

View File

@ -130,10 +130,6 @@ router.beforeResolve(async (to, from, next) => {
return next(false); return next(false);
} }
if (state != null && state.user != null && !('username' in state.user)) {
await validateLogin();
}
// Set the page title using i18n // Set the page title using i18n
const title = i18n.global.t(titles[to.name as keyof typeof titles]); const title = i18n.global.t(titles[to.name as keyof typeof titles]);
document.title = title + " - " + name; document.title = title + " - " + name;
@ -143,6 +139,11 @@ router.beforeResolve(async (to, from, next) => {
// Handle auth requirements // Handle auth requirements
if (to.matched.some((record) => record.meta.requiresAuth)) { if (to.matched.some((record) => record.meta.requiresAuth)) {
if (state != null && state.user != null && !('username' in state.user)) {
await validateLogin();
}
if (!getters.isLoggedIn()) { if (!getters.isLoggedIn()) {
next({ next({
path: "/login", path: "/login",

View File

@ -53,10 +53,6 @@ export const getters = {
state.req.items.forEach((item) => { state.req.items.forEach((item) => {
// Check if the item is a directory // Check if the item is a directory
if (item.type == "directory") { if (item.type == "directory") {
// If hideDotfiles is enabled and the item is a dotfile, skip it
if (state.user.hideDotfiles && item.name.startsWith(".")) {
return;
}
// Otherwise, count this directory // Otherwise, count this directory
dirCount++; dirCount++;
} }
@ -69,10 +65,6 @@ export const getters = {
state.req.items.forEach((item) => { state.req.items.forEach((item) => {
// Check if the item is a directory // Check if the item is a directory
if (item.type != "directory") { if (item.type != "directory") {
// If hideDotfiles is enabled and the item is a dotfile, skip it
if (state.user.hideDotfiles && item.name.startsWith(".")) {
return;
}
// Otherwise, count this directory // Otherwise, count this directory
fileCount++; fileCount++;
} }
@ -88,9 +80,6 @@ export const getters = {
const files = []; const files = [];
state.req.items.forEach((item) => { state.req.items.forEach((item) => {
if (state.user.hideDotfiles && item.name.startsWith(".")) {
return;
}
if (item.type == "directory") { if (item.type == "directory") {
dirs.push(item); dirs.push(item);
} else { } else {

View File

@ -140,16 +140,21 @@ export const mutations = {
emitStateChanged(); emitStateChanged();
}, },
addSelected: (value) => { addSelected: (value) => {
console.log("addSelected", value)
state.selected.push(value); state.selected.push(value);
emitStateChanged(); emitStateChanged();
}, },
removeSelected: (value) => { removeSelected: (value) => {
console.log("removeSelected", value)
let i = state.selected.indexOf(value); let i = state.selected.indexOf(value);
if (i === -1) return; if (i === -1) return;
state.selected.splice(i, 1); state.selected.splice(i, 1);
emitStateChanged(); emitStateChanged();
}, },
resetSelected: () => { resetSelected: () => {
console.log("resetSelected")
state.selected = []; state.selected = [];
mutations.setMultiple(false); mutations.setMultiple(false);
emitStateChanged(); emitStateChanged();
@ -196,17 +201,20 @@ export const mutations = {
// Emit state change event // Emit state change event
emitStateChanged(); emitStateChanged();
}, },
updateRequest: (value) => {
const selectedItems = state.selected.map((i) => state.req.items[i]);
state.oldReq = state.req;
state.req = value;
state.selected = [];
if (!state.req?.items) return;
state.selected = state.req.items
.filter((item) => selectedItems.some((rItem) => rItem.url === item.url))
.map((item) => item.index);
},
replaceRequest: (value) => { replaceRequest: (value) => {
state.selected = [];
if (!value?.items) {
state.req = value;
emitStateChanged();
return
}
if (state.user.hideDotfiles) {
value.items = value.items.filter((item) => !item.name.startsWith("."));
}
value.items.map((item, index) => {
item.index = index;
return item;
})
state.req = value; state.req = value;
emitStateChanged(); emitStateChanged();
}, },
@ -221,6 +229,7 @@ export const mutations = {
}, },
updateListingItems: () => { updateListingItems: () => {
state.req.items = sortedItems(state.req.items, state.user.sorting.by) state.req.items = sortedItems(state.req.items, state.user.sorting.by)
mutations.replaceRequest(state.req);
emitStateChanged(); emitStateChanged();
}, },
updateClipboard: (value) => { updateClipboard: (value) => {

View File

@ -17,6 +17,7 @@ const loginPage = window.FileBrowser.LoginPage;
const enableThumbs = window.FileBrowser.EnableThumbs; const enableThumbs = window.FileBrowser.EnableThumbs;
const resizePreview = window.FileBrowser.ResizePreview; const resizePreview = window.FileBrowser.ResizePreview;
const enableExec = window.FileBrowser.EnableExec; const enableExec = window.FileBrowser.EnableExec;
const externalUrl = window.FileBrowser.ExternalUrl
const origin = window.location.origin; const origin = window.location.origin;
const settings = [ const settings = [
@ -29,6 +30,7 @@ const settings = [
export { export {
name, name,
externalUrl,
disableExternal, disableExternal,
externalLinks, externalLinks,
disableUsedPercentage, disableUsedPercentage,

View File

@ -1,8 +1,32 @@
import { state, mutations, getters } from "@/store" import { state, mutations, getters } from "@/store"
import { filesApi } from "@/api"; import { filesApi } from "@/api";
import { notify } from "@/notify" import { notify } from "@/notify"
import { removePrefix } from "@/utils/url.js";
import { publicApi } from "@/api";
export default function download() { export default function download() {
if (getters.currentView() === "share") {
let urlPath = getters.routePath("share");
// Step 1: Split the path by '/'
let parts = urlPath.split("/");
// Step 2: Assign hash to the second part (index 2) and join the rest for subPath
const hash = parts[1];
const subPath = "/" + parts.slice(2).join("/");
let files = [];
for (let i of state.selected) {
const dlfile = removePrefix(state.req.items[i].url, "share/"+hash);
files.push(dlfile);
}
const share = {
path: subPath,
hash: hash,
token: "",
format: files.length ? "zip" : null,
};
publicApi.download(share, ...files);
return
}
if (getters.isSingleFileSelected()) { if (getters.isSingleFileSelected()) {
filesApi.download(null, [getters.selectedDownloadUrl()]); filesApi.download(null, [getters.selectedDownloadUrl()]);
return; return;

View File

@ -3,6 +3,8 @@ import url from "@/utils/url.js";
import { filesApi } from "@/api"; import { filesApi } from "@/api";
export function checkConflict(files, items) { export function checkConflict(files, items) {
console.log("testing",files)
if (typeof items === "undefined" || items === null) { if (typeof items === "undefined" || items === null) {
items = []; items = [];
} }

View File

@ -80,7 +80,7 @@ export default {
methods: { methods: {
scrollToHash() { scrollToHash() {
if (window.location.hash === this.lastHash) return; if (window.location.hash === this.lastHash) return;
this.lastHash = window.location.hash this.lastHash = window.location.hash;
if (window.location.hash) { if (window.location.hash) {
const id = url.base64Encode(window.location.hash.slice(1)); const id = url.base64Encode(window.location.hash.slice(1));
const element = document.getElementById(id); const element = document.getElementById(id);
@ -94,13 +94,12 @@ export default {
}, },
async fetchData() { async fetchData() {
if (state.route.path === this.lastPath) return; if (state.route.path === this.lastPath) return;
this.lastHash = "" this.lastHash = "";
// Set loading to true and reset the error. // Set loading to true and reset the error.
mutations.setLoading("files", true); mutations.setLoading("files", true);
this.error = null; this.error = null;
// Reset view information using mutations // Reset view information using mutations
mutations.setReload(false); mutations.setReload(false);
mutations.resetSelected();
mutations.setMultiple(false); mutations.setMultiple(false);
mutations.closeHovers(); mutations.closeHovers();
@ -125,7 +124,7 @@ export default {
} catch (e) { } catch (e) {
notify.showError(e); notify.showError(e);
this.error = e; this.error = e;
mutations.replaceRequest(null); mutations.replaceRequest({});
} finally { } finally {
mutations.replaceRequest(data); mutations.replaceRequest(data);
mutations.setLoading("files", false); mutations.setLoading("files", false);

View File

@ -1,12 +1,23 @@
<template v-if="isLoggedIn"> <template>
<div> <div>
<div v-show="showOverlay" @contextmenu.prevent="onOverlayRightClick" @click="resetPrompts" class="overlay"></div> <div
v-show="showOverlay"
@contextmenu.prevent="onOverlayRightClick"
@click="resetPrompts"
class="overlay"
></div>
<div v-if="progress" class="progress"> <div v-if="progress" class="progress">
<div v-bind:style="{ width: this.progress + '%' }"></div> <div v-bind:style="{ width: this.progress + '%' }"></div>
</div> </div>
<listingBar :class="{ 'dark-mode-header': isDarkMode }" v-if="currentView == 'listingView'"></listingBar> <listingBar
<editorBar :class="{ 'dark-mode-header': isDarkMode }" v-else-if="currentView == 'editor'"></editorBar> :class="{ 'dark-mode-header': isDarkMode }"
<defaultBar :class="{ 'dark-mode-header': isDarkMode }" v-else></defaultBar> v-if="currentView == 'listingView'"
></listingBar>
<editorBar
:class="{ 'dark-mode-header': isDarkMode }"
v-else-if="currentView == 'editor'"
></editorBar>
<defaultBar v-else :class="{ 'dark-mode-header': isDarkMode }"></defaultBar>
<sidebar></sidebar> <sidebar></sidebar>
<search v-if="showSearch"></search> <search v-if="showSearch"></search>
<main :class="{ 'dark-mode': isDarkMode, moveWithSidebar: moveWithSidebar }"> <main :class="{ 'dark-mode': isDarkMode, moveWithSidebar: moveWithSidebar }">
@ -14,6 +25,7 @@
</main> </main>
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts> <prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
</div> </div>
<div class="card" id="popup-notification"> <div class="card" id="popup-notification">
<i v-on:click="closePopUp" class="material-icons">close</i> <i v-on:click="closePopUp" class="material-icons">close</i>
<div id="popup-notification-content">no info</div> <div id="popup-notification-content">no info</div>
@ -55,7 +67,7 @@ export default {
mounted() { mounted() {
window.addEventListener("resize", this.updateIsMobile); window.addEventListener("resize", this.updateIsMobile);
if (state.user.themeColor) { if (state.user.themeColor) {
document.documentElement.style.setProperty('--primaryColor', state.user.themeColor); document.documentElement.style.setProperty("--primaryColor", state.user.themeColor);
} }
}, },
computed: { computed: {
@ -114,16 +126,6 @@ export default {
}, },
}, },
methods: { methods: {
onOverlayRightClick(event) {
// Example: Show a custom context menu
mutations.showHover({
name: "ContextMenu", // Assuming ContextMenu is a component you've already imported
props: {
posX: event.clientX,
posY: event.clientY,
},
});
},
updateIsMobile() { updateIsMobile() {
mutations.setMobile(); mutations.setMobile();
}, },

View File

@ -279,7 +279,7 @@ export default {
let file = await publicApi.fetchPub(this.subPath, this.hash, this.password); let file = await publicApi.fetchPub(this.subPath, this.hash, this.password);
file.hash = this.hash; file.hash = this.hash;
this.token = file.token; this.token = file.token;
mutations.updateRequest(file); mutations.replaceRequest(file);
document.title = `${file.name} - ${document.title}`; document.title = `${file.name} - ${document.title}`;
} catch (error) { } catch (error) {
this.error = error; this.error = error;

View File

@ -1,24 +1,15 @@
<template> <template>
<header> <header :class="{ 'dark-mode-header': isDarkMode }">
<action v-if="notShare" icon="close" :label="$t('buttons.close')" @action="close()" /> <action v-if="notShare" icon="close" :label="$t('buttons.close')" @action="close()" />
<title v-if="isSettings" class="topTitle">Settings</title> <title v-if="isSettings" class="topTitle">Settings</title>
<title v-else class="topTitle">{{ req.name }}</title> <title v-else class="topTitle">{{ req.name }}</title>
</header> </header>
</template> </template>
<style>
.flexbar {
display: flex;
flex-direction: block;
justify-content: space-between;
}
</style>
<script> <script>
import { url } from "@/utils"; import { url } from "@/utils";
import router from "@/router"; import router from "@/router";
import { state, mutations, getters } from "@/store"; import { getters, state } from "@/store";
import { filesApi } from "@/api";
import Action from "@/components/Action.vue"; import Action from "@/components/Action.vue";
export default { export default {
@ -26,182 +17,18 @@ export default {
components: { components: {
Action, Action,
}, },
data() {
return {
columnWidth: 350,
width: window.innerWidth,
itemWeight: 0,
viewModes: ["list", "compact", "normal", "gallery"],
};
},
computed: { computed: {
notShare() { notShare() {
return getters.currentView() != "share"; return getters.currentView() != "share";
}, },
isSettings() {
return getters.isSettings();
},
// Map state and getters
req() { req() {
return state.req; return state.req;
}, },
user() { isDarkMode() {
return state.user; return getters.isDarkMode();
},
selected() {
return state.selected;
},
nameSorted() {
return state.user.sorting.by === "name";
},
sizeSorted() {
return state.user.sorting.by === "size";
},
modifiedSorted() {
return state.user.sorting.by === "modified";
},
ascOrdered() {
return state.req.sorting.asc;
},
items() {
const dirs = [];
const files = [];
state.req.items.forEach((item) => {
if (item.type == "directory") {
dirs.push(item);
} else {
files.push(item);
}
});
return { dirs, files };
},
dirs() {
return this.items.dirs.slice(0, this.showLimit);
},
files() {
let showLimit = this.showLimit - this.items.dirs.length;
if (showLimit < 0) showLimit = 0;
return this.items.files.slice(0, showLimit);
},
nameIcon() {
if (this.nameSorted && !this.ascOrdered) {
return "arrow_upward";
}
return "arrow_downward";
},
sizeIcon() {
if (this.sizeSorted && this.ascOrdered) {
return "arrow_downward";
}
return "arrow_upward";
},
modifiedIcon() {
if (this.modifiedSorted && this.ascOrdered) {
return "arrow_downward";
}
return "arrow_upward";
},
viewIcon() {
const icons = {
list: "view_module",
compact: "view_module",
normal: "grid_view",
gallery: "view_list",
};
return icons[state.user.viewMode];
},
headerButtons() {
return {
select: getters.selectedCount() > 0,
upload: state.user.perm?.create && getters.selectedCount() > 0,
download: state.user.perm?.download && getters.selectedCount() > 0,
delete: getters.selectedCount() > 0 && state.user.perm.delete,
rename: getters.selectedCount() === 1 && state.user.perm.rename,
share: getters.selectedCount() === 1 && state.user.perm.share,
move: getters.selectedCount() > 0 && state.user.perm.rename,
copy: getters.selectedCount() > 0 && state.user.perm?.create,
};
}, },
}, },
mounted() {
// How much every listing item affects the window height
this.setItemWeight();
// Fill and fit the window with listing items
this.fillWindow(true);
// Add the needed event listeners to the window and document.
window.addEventListener("keydown", this.keyEvent);
window.addEventListener("scroll", this.scrollEvent);
window.addEventListener("resize", this.windowsResize);
if (state.route.path.startsWith("/share")) {
return;
}
if (!state.user.perm?.create) return;
document.addEventListener("dragover", this.preventDefault);
document.addEventListener("dragenter", this.dragEnter);
document.addEventListener("dragleave", this.dragLeave);
document.addEventListener("drop", this.drop);
},
beforeUnmount() {
// Remove event listeners before destroying this page.
window.removeEventListener("keydown", this.keyEvent);
window.removeEventListener("scroll", this.scrollEvent);
window.removeEventListener("resize", this.windowsResize);
if (state.user && !state.user.perm?.create) return;
document.removeEventListener("dragover", this.preventDefault);
document.removeEventListener("dragenter", this.dragEnter);
document.removeEventListener("dragleave", this.dragLeave);
document.removeEventListener("drop", this.drop);
},
methods: { methods: {
fillWindow(fit = false) {
const totalItems = state.req.numDirs + state.req.numFiles;
// More items are displayed than the total
if (this.showLimit >= totalItems && !fit) return;
const windowHeight = window.innerHeight;
// Quantity of items needed to fill 2x of the window height
const showQuantity = Math.ceil((windowHeight + windowHeight * 2) / this.itemWeight);
// Less items to display than current
if (this.showLimit > showQuantity && !fit) return;
// Set the number of displayed items
this.showLimit = showQuantity > totalItems ? totalItems : showQuantity;
},
setItemWeight() {
// Listing element is not displayed
if (this.$refs.listingView == null) return;
let itemQuantity = state.req.numDirs + state.req.numFiles;
if (itemQuantity > this.showLimit) itemQuantity = this.showLimit;
// How much every listing item affects the window height
this.itemWeight = this.$refs.listingView.offsetHeight / itemQuantity;
},
action() {
if (this.show) {
mutations.showHover(this.show);
}
this.$emit("action");
},
close() { close() {
if (getters.isSettings()) { if (getters.isSettings()) {
// Use this.isSettings to access the computed property // Use this.isSettings to access the computed property
@ -214,146 +41,6 @@ export default {
router.push({ path: uri }); router.push({ path: uri });
mutations.closeHovers(); mutations.closeHovers();
}, },
base64(name) {
return url.base64Encode(name);
},
keyEvent(event) {
// No prompts are shown
if (this.show !== null) {
return;
}
// Esc!
if (event.keyCode === 27) {
// Reset files selection.
mutations.resetSelected();
}
// Del!
if (event.keyCode === 46) {
if (!state.user.perm.delete || getters.selectedCount() == 0) return;
// Show delete prompt.
mutations.showHover("delete");
}
// F2!
if (event.keyCode === 113) {
if (!state.user.perm.rename || getters.selectedCount() !== 1) return;
// Show rename prompt.
mutations.showHover("rename");
}
// Ctrl is pressed
if (!event.ctrlKey && !event.metaKey) {
return;
}
let key = String.fromCharCode(event.which).toLowerCase();
switch (key) {
case "f":
event.preventDefault();
mutations.showHover("search");
break;
case "c":
case "x":
this.copyCut(event, key);
break;
case "v":
this.paste(event);
break;
case "a":
event.preventDefault();
for (let file of this.items.files) {
if (state.selected.indexOf(file.index) === -1) {
this.addSelected(file.index);
}
}
for (let dir of this.items.dirs) {
if (state.selected.indexOf(dir.index) === -1) {
this.addSelected(dir.index);
}
}
break;
case "s":
event.preventDefault();
document.getElementById("download-button").click();
break;
}
},
switchView: async function () {
mutations.closeHovers();
const currentIndex = this.viewModes.indexOf(state.user.viewMode);
const nextIndex = (currentIndex + 1) % this.viewModes.length;
const newView = this.viewModes[nextIndex];
mutations.updateCurrentUser({ viewMode: newView });
},
preventDefault(event) {
// Wrapper around prevent default.
event.preventDefault();
},
copyCut(event, key) {
if (event.target.tagName.toLowerCase() === "input") {
return;
}
let items = [];
for (let i of state.selected) {
items.push({
from: state.req.items[i].url,
name: state.req.items[i].name,
});
}
if (items.length == 0) {
return;
}
mutations.updateClipboard({
key: key,
items: items,
path: state.route.path,
});
},
async paste(event) {
if (event.target.tagName.toLowerCase() === "input") {
return;
}
let items = [];
for (let item of state.clipboard.items) {
const from = item.from.endsWith("/") ? item.from.slice(0, -1) : item.from;
const to = state.route.path + encodeURIComponent(item.name);
items.push({ from, to, name: item.name });
}
if (items.length === 0) {
return;
}
let action = async (overwrite, rename) => {
await filesApi.moveCopy(items, "copy", overwrite, rename);
notify.showSuccess("Items pasted successfully.");
mutations.setReload(true);
};
this.$confirm(
"Are you sure you want to copy these items?",
"Copy",
() => {
action(false, false);
},
() => {
action(true, false);
},
() => {
action(true, true);
}
);
},
}, },
}; };
</script> </script>

View File

@ -16,26 +16,64 @@
<i class="material-icons">sentiment_dissatisfied</i> <i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t("files.lonely") }}</span> <span>{{ $t("files.lonely") }}</span>
</h2> </h2>
<input style="display: none" type="file" id="upload-input" @change="uploadInput($event)" multiple /> <input
<input style="display: none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory style="display: none"
multiple /> type="file"
id="upload-input"
@change="uploadInput($event)"
multiple
/>
<input
style="display: none"
type="file"
id="upload-folder-input"
@change="uploadInput($event)"
webkitdirectory
multiple
/>
</div> </div>
<div v-else id="listingView" ref="listingView" :class="listingViewMode + ' file-icons'"> <div
v-else
id="listingView"
ref="listingView"
:class="listingViewMode + ' file-icons'"
>
<div> <div>
<div class="header" :class="{ 'dark-mode-item-header': isDarkMode }"> <div class="header" :class="{ 'dark-mode-item-header': isDarkMode }">
<p :class="{ active: nameSorted }" class="name" role="button" tabindex="0" @click="sort('name')" <p
:title="$t('files.sortByName')" :aria-label="$t('files.sortByName')"> :class="{ active: nameSorted }"
class="name"
role="button"
tabindex="0"
@click="sort('name')"
:title="$t('files.sortByName')"
:aria-label="$t('files.sortByName')"
>
<span>{{ $t("files.name") }}</span> <span>{{ $t("files.name") }}</span>
<i class="material-icons">{{ nameIcon }}</i> <i class="material-icons">{{ nameIcon }}</i>
</p> </p>
<p :class="{ active: sizeSorted }" class="size" role="button" tabindex="0" @click="sort('size')" <p
:title="$t('files.sortBySize')" :aria-label="$t('files.sortBySize')"> :class="{ active: sizeSorted }"
class="size"
role="button"
tabindex="0"
@click="sort('size')"
:title="$t('files.sortBySize')"
:aria-label="$t('files.sortBySize')"
>
<span>{{ $t("files.size") }}</span> <span>{{ $t("files.size") }}</span>
<i class="material-icons">{{ sizeIcon }}</i> <i class="material-icons">{{ sizeIcon }}</i>
</p> </p>
<p :class="{ active: modifiedSorted }" class="modified" role="button" tabindex="0" @click="sort('modified')" <p
:title="$t('files.sortByLastModified')" :aria-label="$t('files.sortByLastModified')"> :class="{ active: modifiedSorted }"
class="modified"
role="button"
tabindex="0"
@click="sort('modified')"
:title="$t('files.sortByLastModified')"
:aria-label="$t('files.sortByLastModified')"
>
<span>{{ $t("files.lastModified") }}</span> <span>{{ $t("files.lastModified") }}</span>
<i class="material-icons">{{ modifiedIcon }}</i> <i class="material-icons">{{ modifiedIcon }}</i>
</p> </p>
@ -46,10 +84,23 @@
<h2>{{ $t("files.folders") }}</h2> <h2>{{ $t("files.folders") }}</h2>
</div> </div>
</div> </div>
<div v-if="numDirs > 0" class="folder-items" :class="{ lastGroup: numFiles === 0 }"> <div
<item v-for="item in dirs" :key="base64(item.name)" v-bind:index="item.index" v-bind:name="item.name" v-if="numDirs > 0"
v-bind:isDir="item.type == 'directory'" v-bind:url="item.url" v-bind:modified="item.modified" class="folder-items"
v-bind:type="item.type" v-bind:size="item.size" v-bind:path="item.path" /> :class="{ lastGroup: numFiles === 0 }"
>
<item
v-for="item in dirs"
:key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
v-bind:isDir="item.type == 'directory'"
v-bind:url="item.url"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size"
v-bind:path="item.path"
/>
</div> </div>
<div v-if="numFiles > 0"> <div v-if="numFiles > 0">
<div class="header-items"> <div class="header-items">
@ -57,14 +108,35 @@
</div> </div>
</div> </div>
<div v-if="numFiles > 0" class="file-items" :class="{ lastGroup: numFiles > 0 }"> <div v-if="numFiles > 0" class="file-items" :class="{ lastGroup: numFiles > 0 }">
<item v-for="item in files" :key="base64(item.name)" v-bind:index="item.index" v-bind:name="item.name" <item
v-bind:isDir="item.type == 'directory'" v-bind:url="item.url" v-bind:modified="item.modified" v-for="item in files"
v-bind:type="item.type" v-bind:size="item.size" v-bind:path="item.path" /> :key="base64(item.name)"
v-bind:index="item.index"
v-bind:name="item.name"
v-bind:isDir="item.type == 'directory'"
v-bind:url="item.url"
v-bind:modified="item.modified"
v-bind:type="item.type"
v-bind:size="item.size"
v-bind:path="item.path"
/>
</div> </div>
<input style="display: none" type="file" id="upload-input" @change="uploadInput($event)" multiple /> <input
<input style="display: none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory style="display: none"
multiple /> type="file"
id="upload-input"
@change="uploadInput($event)"
multiple
/>
<input
style="display: none"
type="file"
id="upload-folder-input"
@change="uploadInput($event)"
webkitdirectory
multiple
/>
</div> </div>
</div> </div>
</div> </div>
@ -93,6 +165,7 @@ export default {
width: window.innerWidth, width: window.innerWidth,
lastSelected: {}, // Add this to track the currently focused item lastSelected: {}, // Add this to track the currently focused item
contextTimeout: null, // added for safari context menu contextTimeout: null, // added for safari context menu
ctrKeyPressed: false,
}; };
}, },
watch: { watch: {
@ -196,7 +269,6 @@ export default {
this.colunmsResize(); this.colunmsResize();
return state.user.viewMode; return state.user.viewMode;
}, },
selectedCount() { selectedCount() {
return state.selected.length; return state.selected.length;
}, },
@ -215,7 +287,8 @@ export default {
window.addEventListener("keydown", this.keyEvent); window.addEventListener("keydown", this.keyEvent);
window.addEventListener("scroll", this.scrollEvent); window.addEventListener("scroll", this.scrollEvent);
window.addEventListener("resize", this.windowsResize); 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 // Adjust contextmenu listener based on browser
if (state.isSafari) { if (state.isSafari) {
@ -250,7 +323,6 @@ export default {
this.$el.removeEventListener("touchend", this.cancelContext); this.$el.removeEventListener("touchend", this.cancelContext);
this.$el.removeEventListener("mouseup", this.cancelContext); this.$el.removeEventListener("mouseup", this.cancelContext);
this.$el.removeEventListener("touchmove", this.handleTouchMove); this.$el.removeEventListener("touchmove", this.handleTouchMove);
} else { } else {
window.removeEventListener("contextmenu", this.openContext); window.removeEventListener("contextmenu", this.openContext);
} }
@ -425,17 +497,22 @@ export default {
}, 50); }, 50);
} }
}, },
clearCtrKey(event) {
const { ctrlKey } = event;
if (!ctrlKey) {
this.ctrKeyPressed = false;
}
},
keyEvent(event) { keyEvent(event) {
const { key, ctrlKey, metaKey, which } = event; const { key, ctrlKey, metaKey, which } = event;
// Check if the key is alphanumeric // Check if the key is alphanumeric
const isAlphanumeric = /^[a-z0-9]$/i.test(key); const isAlphanumeric = /^[a-z0-9]$/i.test(key);
const noModifierKeys = !ctrlKey && !metaKey; const modifierKeys = ctrlKey || metaKey;
if (isAlphanumeric && !modifierKeys && getters.currentPromptName() == null) {
if (isAlphanumeric && noModifierKeys && getters.currentPromptName() == null) {
this.alphanumericKeyPress(key); // Call the alphanumeric key press function this.alphanumericKeyPress(key); // Call the alphanumeric key press function
return; return;
} }
if (noModifierKeys && getters.currentPromptName() != null) { if (!modifierKeys && getters.currentPromptName() != null) {
return; return;
} }
// Handle the space bar key // Handle the space bar key
@ -452,6 +529,13 @@ export default {
} }
let currentPath = state.route.path.replace(/\/+$/, ""); // Remove trailing slashes let currentPath = state.route.path.replace(/\/+$/, ""); // Remove trailing slashes
let newPath = currentPath.substring(0, currentPath.lastIndexOf("/")); let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
if (modifierKeys) {
if (!ctrlKey) {
this.ctrKeyPressed = true;
}
return;
}
// Handle key events using a switch statement // Handle key events using a switch statement
switch (key) { switch (key) {
case "Enter": case "Enter":
@ -486,11 +570,6 @@ export default {
event.preventDefault(); event.preventDefault();
this.navigateKeboardArrows(key); this.navigateKeboardArrows(key);
break; break;
default:
// Handle keys with ctrl or meta keys
if (!ctrlKey && !metaKey) return;
break;
} }
const charKey = String.fromCharCode(which).toLowerCase(); const charKey = String.fromCharCode(which).toLowerCase();
@ -513,8 +592,6 @@ export default {
break; break;
} }
}, },
// Helper method to select all files and directories
selectAll() { selectAll() {
for (let file of this.items.files) { for (let file of this.items.files) {
if (state.selected.indexOf(file.index) === -1) { if (state.selected.indexOf(file.index) === -1) {
@ -650,12 +727,18 @@ export default {
action(false, false); action(false, false);
}, },
colunmsResize() { 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") { 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 { } else {
document.documentElement.style.setProperty('--item-height', `auto`); document.documentElement.style.setProperty("--item-height", `auto`);
} }
}, },
dragEnter() { 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; const sameAsBefore = state.selected == this.lastSelected;
if (sameAsBefore && !state.multiple) { if (sameAsBefore && !state.multiple && getters.currentPromptName == null) {
mutations.resetSelected(); mutations.resetSelected();
} }
this.lastSelected = state.selected; this.lastSelected = state.selected;

View File

@ -110,7 +110,6 @@ export default {
event.preventDefault(); event.preventDefault();
try { try {
if (this.isNew) { if (this.isNew) {
const loc = await usersApi.create(this.userPayload); // Use the computed property
this.$router.push({ path: "/settings", hash: "#users-main" }); this.$router.push({ path: "/settings", hash: "#users-main" });
notify.showSuccess(this.$t("settings.userCreated")); notify.showSuccess(this.$t("settings.userCreated"));
} else { } else {