Merge branch 'main' of github.com:gtsteffaniak/filebrowser
This commit is contained in:
commit
e922a92aa6
16
CHANGELOG.md
16
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).
|
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**
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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" {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 != "" {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
return promises;
|
// 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) {
|
} 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");
|
||||||
|
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 += "/";
|
||||||
|
|
|
@ -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" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 != []) {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
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: {
|
methods: {
|
||||||
copy: async function (event) {
|
copy: async function (event) {
|
||||||
event.preventDefault();
|
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.
|
if (conflict) {
|
||||||
for (let item of state.selected) {
|
await new Promise((resolve, reject) => {
|
||||||
items.push({
|
mutations.showHover({
|
||||||
from: state.req.items[item].url,
|
name: "replace-rename",
|
||||||
to: this.dest + encodeURIComponent(state.req.items[item].name),
|
confirm: async (event, option) => {
|
||||||
name: state.req.items[item].name,
|
overwrite = option == "overwrite";
|
||||||
});
|
rename = option == "rename";
|
||||||
}
|
event.preventDefault();
|
||||||
|
try {
|
||||||
let action = async (overwrite, rename) => {
|
await action(overwrite, rename);
|
||||||
buttons.loading("copy");
|
resolve(); // Resolve the promise if action succeeds
|
||||||
await filesApi.moveCopy(items, "copy", overwrite, rename);
|
} catch (e) {
|
||||||
this.$router.push({ path: this.dest });
|
reject(e); // Reject the promise if an error occurs
|
||||||
mutations.setReload(true);
|
}
|
||||||
};
|
},
|
||||||
|
});
|
||||||
if (state.route.path === this.dest) {
|
});
|
||||||
|
} else {
|
||||||
|
// Await the action call for non-conflicting cases
|
||||||
|
await action(overwrite, rename);
|
||||||
|
}
|
||||||
mutations.closeHovers();
|
mutations.closeHovers();
|
||||||
action(false, true);
|
notify.showSuccess("Successfully copied file/folder, redirecting...");
|
||||||
|
setTimeout(() => {
|
||||||
return;
|
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);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
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: {
|
methods: {
|
||||||
move: async function (event) {
|
move: async function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
let items = [];
|
|
||||||
|
|
||||||
for (let item of state.selected) {
|
|
||||||
items.push({
|
|
||||||
from: state.req.items[item].url,
|
|
||||||
to: this.dest + state.req.items[item].name,
|
|
||||||
name: state.req.items[item].name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let action = async (overwrite, rename) => {
|
|
||||||
buttons.loading("move");
|
|
||||||
await filesApi.moveCopy(items, "move", overwrite, rename);
|
|
||||||
buttons.success("move");
|
|
||||||
this.$router.push({ path: this.dest });
|
|
||||||
mutations.closeHovers();
|
|
||||||
};
|
|
||||||
|
|
||||||
let dstItems = (await filesApi.fetchFiles(this.dest)).items;
|
|
||||||
let conflict = upload.checkConflict(items, dstItems);
|
|
||||||
|
|
||||||
let overwrite = false;
|
|
||||||
let rename = false;
|
|
||||||
|
|
||||||
try {
|
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("move");
|
||||||
|
await filesApi.moveCopy(this.items, "move", 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;
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
mutations.showHover({
|
await new Promise((resolve, reject) => {
|
||||||
name: "replace-rename",
|
mutations.showHover({
|
||||||
confirm: (event, option) => {
|
name: "replace-rename",
|
||||||
overwrite = option == "overwrite";
|
confirm: async (event, option) => {
|
||||||
rename = option == "rename";
|
overwrite = option == "overwrite";
|
||||||
event.preventDefault();
|
rename = option == "rename";
|
||||||
action(overwrite, 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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
|
@ -220,7 +228,8 @@ export const mutations = {
|
||||||
emitStateChanged();
|
emitStateChanged();
|
||||||
},
|
},
|
||||||
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) => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 = [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue