Beta/v0.3.6 release (#279)
This commit is contained in:
parent
9cc30f25e4
commit
cd3e111e39
|
@ -1,4 +1,4 @@
|
|||
name: dev tests
|
||||
name: regular tests
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
name: beta release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "beta/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create_release_tag:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.PAT }}
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: |
|
||||
original_branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
|
||||
echo "branch_name=$transformed_branch" >> $GITHUB_OUTPUT
|
||||
transformed_branch=$(echo "$original_branch" | sed 's/^beta\/v/beta_v/')
|
||||
echo "tag_name=$transformed_branch" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch_name }}
|
||||
token: ${{ secrets.PAT }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
prerelease: false # change this to false when stable gets released
|
||||
make_latest: true # change this to false when stable gets released
|
||||
draft: false
|
||||
generate_release_notes: true
|
||||
name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
|
||||
push_release_to_registry:
|
||||
name: Push release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: gtstef/filebrowser
|
||||
- name: Strip v from version number
|
||||
id: modify-json
|
||||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:beta\/v/filebrowser:beta_v/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
build-args: |
|
||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
|
@ -3,7 +3,7 @@ name: dev release
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- "dev_v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "dev/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
@ -34,7 +34,7 @@ jobs:
|
|||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:dev\/v/filebrowser:dev_v/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
name: version release
|
||||
name: stable release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+"
|
||||
- "stable/v[0-9]+.[0-9]+.[0-9]+"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create_release:
|
||||
create_release_tag:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
@ -19,7 +19,11 @@ jobs:
|
|||
token: ${{ secrets.PAT }}
|
||||
- name: Extract branch name
|
||||
shell: bash
|
||||
run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
run: |
|
||||
original_branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}
|
||||
echo "branch_name=$transformed_branch" >> $GITHUB_OUTPUT
|
||||
transformed_branch=$(echo "$original_branch" | sed 's/^stable\/v/stable_v/')
|
||||
echo "tag_name=$transformed_branch" >> $GITHUB_OUTPUT
|
||||
id: extract_branch
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
|
@ -27,13 +31,13 @@ jobs:
|
|||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch }}
|
||||
target_commitish: ${{ steps.extract_branch.outputs.branch_name }}
|
||||
token: ${{ secrets.PAT }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.branch }}
|
||||
tag_name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
prerelease: false
|
||||
draft: false
|
||||
generate_release_notes: true
|
||||
name: ${{ steps.extract_branch.outputs.branch }}
|
||||
name: ${{ steps.extract_branch.outputs.tag_name }}
|
||||
|
||||
push_release_to_registry:
|
||||
name: Push release
|
||||
|
@ -60,7 +64,7 @@ jobs:
|
|||
run: |
|
||||
JSON="${{ steps.meta.outputs.tags }}"
|
||||
# Use jq to remove 'v' from the version field
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
|
||||
JSON=$(echo "$JSON" | sed 's/filebrowser:stable\/v/filebrowser:/')
|
||||
echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
|
@ -1,9 +1,9 @@
|
|||
name: tag
|
||||
name: beta tag update
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
- "beta_v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
|
@ -0,0 +1,31 @@
|
|||
name: stable tag update
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "stable_v*"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update_tag:
|
||||
name: Update Release tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
- name: Install dependencies and build frontend
|
||||
run: npm i && npm run build
|
||||
working-directory: frontend
|
||||
- name: Install UPX
|
||||
run: sudo apt-get install -y upx
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
args: release --clean
|
||||
workdir: backend
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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).
|
||||
|
||||
## v0.3.6
|
||||
|
||||
**New Features**
|
||||
- Adds "externalUrl" server config https://github.com/gtsteffaniak/filebrowser/issues/272
|
||||
|
||||
**Notes**:
|
||||
- All views modes to show header bar for sorting.
|
||||
- other small style changes
|
||||
|
||||
**Bugfixes**:
|
||||
- select and info bug after sorting https://github.com/gtsteffaniak/filebrowser/issues/277
|
||||
- downloading from shares with public user
|
||||
- Ctrl and Shift key modifiers work on listing views as expected.
|
||||
- copy/move file/folder error and show errors https://github.com/gtsteffaniak/filebrowser/issues/278
|
||||
- file move/copy context fix.
|
||||
|
||||
## v0.3.5
|
||||
|
||||
**New Features**
|
||||
|
|
|
@ -9,11 +9,9 @@
|
|||
<img width="800" src="https://github.com/user-attachments/assets/b16acd67-0292-437a-a06c-bc83f95758e6" title="Main Screenshot">
|
||||
</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]
|
||||
> 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.
|
||||
> Stable release is planned for later this year after the following have been added: 1) multiple sources support 2) initial onboarding page 3) official automated docs website
|
||||
|
||||
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)
|
||||
// refresh info for source and dest
|
||||
err = index.RefreshFileInfo(FileOptions{
|
||||
Path: realsrc,
|
||||
Path: filepath.Dir(realsrc),
|
||||
IsDir: isSrcDir,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package files
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
@ -74,7 +73,6 @@ func Test_GetRealPath(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
realPath, isDir, _ := idx.GetRealPath(tt.paths...)
|
||||
fmt.Println(realPath, trimPrefix)
|
||||
adjustedRealPath := strings.TrimPrefix(realPath, trimPrefix)
|
||||
if tt.want.path != adjustedRealPath || tt.want.isDir != isDir {
|
||||
t.Errorf("expected %v:%v but got: %v:%v", tt.want.path, tt.want.isDir, adjustedRealPath, isDir)
|
||||
|
|
|
@ -26,9 +26,25 @@ func MoveFile(src, dst string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// CopyFile copies a file from source to dest and returns
|
||||
// an error if any.
|
||||
// CopyFile copies a file or directory from source to dest and returns an error if any.
|
||||
func CopyFile(source, dest string) error {
|
||||
// Check if the source exists and whether it's a file or directory.
|
||||
info, err := os.Stat(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
// If the source is a directory, copy it recursively.
|
||||
return copyDirectory(source, dest)
|
||||
}
|
||||
|
||||
// If the source is a file, copy the file.
|
||||
return copySingleFile(source, dest)
|
||||
}
|
||||
|
||||
// copySingleFile handles copying a single file.
|
||||
func copySingleFile(source, dest string) error {
|
||||
// Open the source file.
|
||||
src, err := os.Open(source)
|
||||
if err != nil {
|
||||
|
@ -36,7 +52,7 @@ func CopyFile(source, dest string) error {
|
|||
}
|
||||
defer src.Close()
|
||||
|
||||
// Makes the directory needed to create the dst file.
|
||||
// Create the destination directory if needed.
|
||||
err = os.MkdirAll(filepath.Dir(dest), 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -68,6 +84,43 @@ func CopyFile(source, dest string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// copyDirectory handles copying directories recursively.
|
||||
func copyDirectory(source, dest string) error {
|
||||
// Create the destination directory.
|
||||
err := os.MkdirAll(dest, 0775) //nolint:gomnd
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Read the contents of the source directory.
|
||||
entries, err := os.ReadDir(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Iterate over each entry in the directory.
|
||||
for _, entry := range entries {
|
||||
srcPath := filepath.Join(source, entry.Name())
|
||||
destPath := filepath.Join(dest, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
// Recursively copy subdirectories.
|
||||
err = copyDirectory(srcPath, destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Copy files.
|
||||
err = copySingleFile(srcPath, destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommonPrefix returns the common directory path of provided files.
|
||||
func CommonPrefix(sep byte, paths ...string) string {
|
||||
// Handle special cases.
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type *files.FileInfo")
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo")
|
||||
}
|
||||
file.Path = strings.TrimPrefix(file.Path, files.RootPaths["default"])
|
||||
return renderJSON(w, r, file)
|
||||
|
@ -28,23 +28,6 @@ func publicUserGetHandler(w http.ResponseWriter, r *http.Request) {
|
|||
if err != nil {
|
||||
http.Error(w, http.StatusText(status), status)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func publicDlHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to assert type files.FileInfo")
|
||||
}
|
||||
if d.user == nil {
|
||||
return http.StatusUnauthorized, fmt.Errorf("failed to get user")
|
||||
}
|
||||
|
||||
if file.Type == "directory" {
|
||||
return rawFilesHandler(w, r, d, []string{file.Path})
|
||||
}
|
||||
|
||||
return rawFileHandler(w, r, file.FileInfo)
|
||||
}
|
||||
|
||||
// health godoc
|
||||
|
|
|
@ -45,13 +45,23 @@ func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
|
|||
if !d.user.Perm.Download {
|
||||
return http.StatusAccepted, nil
|
||||
}
|
||||
|
||||
filePrefix := ""
|
||||
file, ok := d.raw.(files.ExtendedFileInfo)
|
||||
if ok {
|
||||
filePrefix = file.Path
|
||||
}
|
||||
encodedFiles := r.URL.Query().Get("files")
|
||||
// Decode the URL-encoded path
|
||||
files, err := url.QueryUnescape(encodedFiles)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
return rawFilesHandler(w, r, d, strings.Split(files, ","))
|
||||
fileList := strings.Split(files, ",")
|
||||
for i, f := range fileList {
|
||||
fileList[i] = filepath.Join(filePrefix, f)
|
||||
}
|
||||
return rawFilesHandler(w, r, d, fileList)
|
||||
}
|
||||
|
||||
func addFile(path string, d *requestContext, tarWriter *tar.Writer, zipWriter *zip.Writer) error {
|
||||
|
@ -186,7 +196,8 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
|
|||
if baseDirName == "" || baseDirName == "/" {
|
||||
baseDirName = "download"
|
||||
}
|
||||
downloadFileName := url.PathEscape(baseDirName + "." + algo)
|
||||
downloadFileName := url.PathEscape(baseDirName + extension)
|
||||
|
||||
w.Header().Set("Content-Disposition", "attachment; filename*=utf-8''"+downloadFileName)
|
||||
// Create the archive and stream it directly to the response
|
||||
if extension == ".zip" {
|
||||
|
|
|
@ -283,10 +283,10 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
|||
return errToStatus(err), err
|
||||
}
|
||||
if !d.user.Check(src) || !d.user.Check(dst) {
|
||||
return http.StatusForbidden, nil
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: user rules deny access to source or destination")
|
||||
}
|
||||
if dst == "/" || src == "/" {
|
||||
return http.StatusForbidden, nil
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: source or destination is attempting to modify root")
|
||||
}
|
||||
|
||||
idx := files.GetIndex(source)
|
||||
|
@ -307,7 +307,7 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
|||
}
|
||||
// Permission for overwriting the file
|
||||
if overwrite && !d.user.Perm.Modify {
|
||||
return http.StatusForbidden, nil
|
||||
return http.StatusForbidden, fmt.Errorf("forbidden: user does not have permission to overwrite file")
|
||||
}
|
||||
err = d.RunHook(func() error {
|
||||
return patchAction(r.Context(), action, realSrc, realDest, d, fileCache, isSrcDir, source)
|
||||
|
|
|
@ -107,7 +107,7 @@ func StartHttp(Service ImgService, storage *storage.Storage, cache FileCache) {
|
|||
|
||||
// Public routes
|
||||
api.HandleFunc("GET /public/publicUser", publicUserGetHandler)
|
||||
api.HandleFunc("GET /public/dl", withHashFile(publicDlHandler))
|
||||
api.HandleFunc("GET /public/dl", withHashFile(rawHandler))
|
||||
api.HandleFunc("GET /public/share", withHashFile(publicShareHandler))
|
||||
|
||||
// Settings routes
|
||||
|
|
|
@ -63,6 +63,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
|||
"EnableExec": config.Server.EnableExec,
|
||||
"ReCaptchaHost": config.Auth.Recaptcha.Host,
|
||||
"ExternalLinks": config.Frontend.ExternalLinks,
|
||||
"ExternalUrl": strings.TrimSuffix(config.Server.ExternalUrl, "/"),
|
||||
}
|
||||
|
||||
if config.Frontend.Files != "" {
|
||||
|
|
|
@ -53,6 +53,7 @@ type Server struct {
|
|||
UserHomeBasePath string `json:"userHomeBasePath"`
|
||||
CreateUserDir bool `json:"createUserDir"`
|
||||
Sources map[string]Source `json:"sources"`
|
||||
ExternalUrl string `json:"externalUrl"`
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
|
|
|
@ -125,30 +125,38 @@ export async function post(url, content = "", overwrite = false, onupload) {
|
|||
}
|
||||
|
||||
export async function moveCopy(items, action = "copy", overwrite = false, rename = false) {
|
||||
let promises = [];
|
||||
let params = {
|
||||
overwrite: overwrite,
|
||||
action: action,
|
||||
rename: rename,
|
||||
}
|
||||
};
|
||||
try {
|
||||
for (let item of items) {
|
||||
// Create an array of fetch calls
|
||||
let promises = items.map((item) => {
|
||||
let toPath = encodeURIComponent(removePrefix(decodeURI(item.to), "files"));
|
||||
let fromPath = encodeURIComponent(removePrefix(decodeURI(item.from), "files"));
|
||||
let localParams = { ...params };
|
||||
localParams.destination = toPath;
|
||||
localParams.from = fromPath;
|
||||
let localParams = { ...params, destination: toPath, from: fromPath };
|
||||
const apiPath = getApiPath("api/resources", localParams);
|
||||
promises.push(fetch(apiPath, { method: "PATCH" }));
|
||||
}
|
||||
return promises;
|
||||
return fetch(apiPath, { method: "PATCH" }).then((response) => {
|
||||
if (!response.ok) {
|
||||
// Throw an error if the fetch fails
|
||||
return response.text().then((text) => {
|
||||
throw new Error(`Failed to move/copy: ${text || response.statusText}`);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
});
|
||||
});
|
||||
|
||||
// Await all promises and ensure errors propagate
|
||||
await Promise.all(promises);
|
||||
} catch (err) {
|
||||
notify.showError(err.message || "Error moving/copying resources");
|
||||
throw err;
|
||||
throw err; // Re-throw the error to propagate it back to the caller
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function checksum(url, algo) {
|
||||
try {
|
||||
const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { fetchURL, fetchJSON, adjustedData } from "./utils";
|
||||
import { notify } from "@/notify";
|
||||
import { getApiPath } from "@/utils/url.js";
|
||||
import { externalUrl } from "@/utils/constants";
|
||||
|
||||
export async function list() {
|
||||
const apiPath = getApiPath("api/shares");
|
||||
|
@ -41,5 +42,8 @@ export async function create(path, password = "", expires = "", unit = "hours")
|
|||
}
|
||||
|
||||
export function getShareURL(share) {
|
||||
if (externalUrl) {
|
||||
return externalUrl+getApiPath(`share/${share.hash}`);
|
||||
}
|
||||
return window.origin+getApiPath(`share/${share.hash}`);
|
||||
}
|
||||
|
|
|
@ -67,8 +67,7 @@ export function adjustedData(data, url) {
|
|||
// Combine folders and files into items
|
||||
data.items = [...(data.folders || []), ...(data.files || [])];
|
||||
|
||||
data.items = data.items.map((item, index) => {
|
||||
item.index = index;
|
||||
data.items = data.items.map((item) => {
|
||||
item.url = `${data.url}${item.name}`;
|
||||
if (item.type === "directory") {
|
||||
item.url += "/";
|
||||
|
|
|
@ -23,10 +23,10 @@ describe('adjustedData', () => {
|
|||
folders: [],
|
||||
files: [],
|
||||
items: [
|
||||
{ name: "folder1", type: "directory", index: 0, url: "http://example.com/unit-testing/files/path/to/directory/folder1/" },
|
||||
{ name: "folder2", type: "directory", index: 1, url: "http://example.com/unit-testing/files/path/to/directory/folder2/" },
|
||||
{ name: "file1.txt", type: "file", index: 2, url: "http://example.com/unit-testing/files/path/to/directory/file1.txt" },
|
||||
{ name: "file2.txt", type: "file", index: 3, url: "http://example.com/unit-testing/files/path/to/directory/file2.txt" },
|
||||
{ name: "folder1", type: "directory", url: "http://example.com/unit-testing/files/path/to/directory/folder1/" },
|
||||
{ name: "folder2", type: "directory", url: "http://example.com/unit-testing/files/path/to/directory/folder2/" },
|
||||
{ name: "file1.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file1.txt" },
|
||||
{ name: "file2.txt", type: "file", url: "http://example.com/unit-testing/files/path/to/directory/file2.txt" },
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -109,7 +109,7 @@ export default {
|
|||
return state.user;
|
||||
},
|
||||
centered() {
|
||||
return getters.isMobile() || ( !this.posX || !this.posY );
|
||||
return getters.isMobile() || !this.posX || !this.posY;
|
||||
},
|
||||
showContext() {
|
||||
if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {
|
||||
|
|
|
@ -179,7 +179,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
handleTouchMove(event) {
|
||||
if (!state.isSafari) return
|
||||
if (!state.isSafari) return;
|
||||
const touch = event.touches[0];
|
||||
const deltaX = Math.abs(touch.clientX - this.touchStartX);
|
||||
const deltaY = Math.abs(touch.clientY - this.touchStartY);
|
||||
|
@ -191,7 +191,7 @@ export default {
|
|||
}
|
||||
},
|
||||
handleTouchEnd() {
|
||||
if (!state.isSafari) return
|
||||
if (!state.isSafari) return;
|
||||
this.cancelContext(); // Clear timeout
|
||||
this.isSwipe = false; // Reset swipe state
|
||||
},
|
||||
|
@ -214,8 +214,8 @@ export default {
|
|||
},
|
||||
onRightClick(event) {
|
||||
event.preventDefault(); // Prevent default context menu
|
||||
// If no items are selected, select the right-clicked item
|
||||
if (!state.multiple) {
|
||||
// If one or fewer items are selected, reset the selection
|
||||
if (!state.multiple && getters.selectedCount() < 2) {
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected(this.index);
|
||||
}
|
||||
|
@ -247,7 +247,9 @@ export default {
|
|||
getTime() {
|
||||
if (state.user.dateFormat) {
|
||||
// Truncate the fractional seconds to 3 digits (milliseconds)
|
||||
const sanitizedString = this.modified.replace(/\.\d+/, (match) => match.slice(0, 4));
|
||||
const sanitizedString = this.modified.replace(/\.\d+/, (match) =>
|
||||
match.slice(0, 4)
|
||||
);
|
||||
// Parse the sanitized string into a Date object
|
||||
const date = new Date(sanitizedString);
|
||||
return date.toLocaleString();
|
||||
|
@ -333,7 +335,7 @@ export default {
|
|||
action(overwrite, rename);
|
||||
},
|
||||
addSelected(event) {
|
||||
if (!state.isSafari) return
|
||||
if (!state.isSafari) return;
|
||||
const touch = event.touches[0];
|
||||
this.touchStartX = touch.clientX;
|
||||
this.touchStartY = touch.clientY;
|
||||
|
@ -357,7 +359,11 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
if (!state.user.singleClick && getters.selectedCount() !== 0 && event.button === 0) {
|
||||
if (
|
||||
!state.user.singleClick &&
|
||||
getters.selectedCount() !== 0 &&
|
||||
event.button === 0
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
setTimeout(() => {
|
||||
|
@ -393,7 +399,12 @@ export default {
|
|||
|
||||
return;
|
||||
}
|
||||
if (!state.user.singleClick && !event.ctrlKey && !event.metaKey && !state.multiple) {
|
||||
if (
|
||||
!state.user.singleClick &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey &&
|
||||
!state.multiple
|
||||
) {
|
||||
mutations.resetSelected();
|
||||
}
|
||||
mutations.addSelected(this.index);
|
||||
|
@ -411,4 +422,4 @@ export default {
|
|||
.item {
|
||||
-webkit-touch-callout: none; /* Disable the default long press preview */
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -52,7 +52,8 @@ import FileList from "./FileList.vue";
|
|||
import { filesApi } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
//import { notify } from "@/notify";
|
||||
import { removePrefix } from "@/utils/url";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
export default {
|
||||
name: "copy",
|
||||
|
@ -61,6 +62,7 @@ export default {
|
|||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null,
|
||||
items: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -71,57 +73,63 @@ export default {
|
|||
return mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
for (let item of state.selected) {
|
||||
this.items.push({
|
||||
from: state.req.items[item].url,
|
||||
// add to: dest
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copy: async function (event) {
|
||||
event.preventDefault();
|
||||
let items = [];
|
||||
try {
|
||||
// Define the action function
|
||||
let action = async (overwrite, rename) => {
|
||||
const loc = removePrefix(this.dest, "files");
|
||||
for (let item of this.items) {
|
||||
item.to = loc + "/" + item.name;
|
||||
}
|
||||
buttons.loading("copy");
|
||||
await filesApi.moveCopy(this.items, "copy", overwrite, rename);
|
||||
};
|
||||
// Fetch destination files
|
||||
let dstResp = await filesApi.fetchFiles(this.dest);
|
||||
let conflict = upload.checkConflict(this.items, dstResp.items);
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
// Create a new promise for each file.
|
||||
for (let item of state.selected) {
|
||||
items.push({
|
||||
from: state.req.items[item].url,
|
||||
to: this.dest + encodeURIComponent(state.req.items[item].name),
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
}
|
||||
|
||||
let action = async (overwrite, rename) => {
|
||||
buttons.loading("copy");
|
||||
await filesApi.moveCopy(items, "copy", overwrite, rename);
|
||||
this.$router.push({ path: this.dest });
|
||||
mutations.setReload(true);
|
||||
};
|
||||
|
||||
if (state.route.path === this.dest) {
|
||||
if (conflict) {
|
||||
await new Promise((resolve, reject) => {
|
||||
mutations.showHover({
|
||||
name: "replace-rename",
|
||||
confirm: async (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
event.preventDefault();
|
||||
try {
|
||||
await action(overwrite, rename);
|
||||
resolve(); // Resolve the promise if action succeeds
|
||||
} catch (e) {
|
||||
reject(e); // Reject the promise if an error occurs
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Await the action call for non-conflicting cases
|
||||
await action(overwrite, rename);
|
||||
}
|
||||
mutations.closeHovers();
|
||||
action(false, true);
|
||||
|
||||
return;
|
||||
notify.showSuccess("Successfully copied file/folder, redirecting...");
|
||||
setTimeout(() => {
|
||||
this.$router.push(this.dest);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
notify.error(error);
|
||||
}
|
||||
|
||||
let dstItems = (await filesApi.fetchFiles(this.dest)).items;
|
||||
let conflict = upload.checkConflict(items, dstItems);
|
||||
|
||||
let overwrite = false;
|
||||
let rename = false;
|
||||
|
||||
if (conflict) {
|
||||
mutations.showHover({
|
||||
name: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
|
||||
event.preventDefault();
|
||||
mutations.closeHovers();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
action(overwrite, rename);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
<script>
|
||||
import { state, mutations } from "@/store";
|
||||
import url from "@/utils/url.js";
|
||||
import { removePrefix } from "@/utils/url.js";
|
||||
import { filesApi } from "@/api";
|
||||
|
||||
export default {
|
||||
|
@ -40,7 +41,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
nav() {
|
||||
return decodeURIComponent(this.current);
|
||||
return removePrefix(decodeURIComponent(this.current), "files");
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -53,6 +53,7 @@ import { filesApi } from "@/api";
|
|||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { notify } from "@/notify";
|
||||
import { removePrefix } from "@/utils/url";
|
||||
|
||||
export default {
|
||||
name: "move",
|
||||
|
@ -61,6 +62,7 @@ export default {
|
|||
return {
|
||||
current: window.location.pathname,
|
||||
dest: null,
|
||||
items: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -71,50 +73,66 @@ export default {
|
|||
return mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
for (let item of state.selected) {
|
||||
this.items.push({
|
||||
from: state.req.items[item].url,
|
||||
// add to: dest
|
||||
name: state.req.items[item].name,
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move: async function (event) {
|
||||
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 {
|
||||
// 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) {
|
||||
mutations.showHover({
|
||||
name: "replace-rename",
|
||||
confirm: (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
event.preventDefault();
|
||||
action(overwrite, rename);
|
||||
},
|
||||
await new Promise((resolve, reject) => {
|
||||
mutations.showHover({
|
||||
name: "replace-rename",
|
||||
confirm: async (event, option) => {
|
||||
overwrite = option == "overwrite";
|
||||
rename = option == "rename";
|
||||
event.preventDefault();
|
||||
try {
|
||||
await action(overwrite, rename);
|
||||
resolve(); // Resolve the promise if action succeeds
|
||||
} catch (e) {
|
||||
reject(e); // Reject the promise if an error occurs
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
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 any errors from action or other parts of the flow
|
||||
notify.showError(e);
|
||||
}
|
||||
return;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -93,14 +93,13 @@ export default {
|
|||
this.$router.push({ path: uri });
|
||||
} else if (!this.base) {
|
||||
const res = await filesApi.fetchFiles(url.removeLastDir(uri) + "/");
|
||||
mutations.updateRequest(res);
|
||||
mutations.replaceRequest(res);
|
||||
}
|
||||
|
||||
mutations.closeHovers();
|
||||
} catch (error) {
|
||||
notify.showError(error);
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -206,7 +206,12 @@ export default {
|
|||
if (isPermanent) {
|
||||
res = await shareApi.create(this.subpath, this.password);
|
||||
} 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);
|
||||
|
|
|
@ -59,10 +59,10 @@ body.rtl #listingView {
|
|||
|
||||
#listingView .item i {
|
||||
font-size: 4em;
|
||||
margin-right: 0.1em;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
|
||||
#listingView .item img {
|
||||
width: 4em;
|
||||
height: 4em;
|
||||
|
@ -177,11 +177,6 @@ body.rtl #listingView {
|
|||
border-radius: 0.5em;
|
||||
}
|
||||
|
||||
#listingView.gallery .size,
|
||||
#listingView.gallery .modified {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#listingView.compact {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
@ -220,15 +215,7 @@ body.rtl #listingView {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
#listingView.compact .header .name,
|
||||
#listingView.list .header .name,
|
||||
#listingView.list .item .name,
|
||||
#listingView.compact .item .name {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
#listingView.compact .header .name,
|
||||
#listingView.list .header .name {
|
||||
#listingView .header .name {
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
|
@ -237,14 +224,23 @@ body.rtl #listingView {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
#listingView.compact .header .size,
|
||||
#listingView.list .header .size,
|
||||
#listingView .header .size,
|
||||
#listingView.list .item .size,
|
||||
#listingView.compact .item .size {
|
||||
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;
|
||||
vertical-align: middle;
|
||||
margin-left: .2em;
|
||||
|
@ -275,25 +271,21 @@ body.rtl #listingView {
|
|||
width: 0;
|
||||
}
|
||||
|
||||
#listingView.compact .name {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#listingView.compact .header span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#listingView.compact .header i {
|
||||
#listingView .header i {
|
||||
opacity: 0;
|
||||
transition: .1s ease all;
|
||||
}
|
||||
|
||||
#listingView.compact .header p:hover i,
|
||||
#listingView.compact .header .active i {
|
||||
#listingView .header p:hover i,
|
||||
#listingView .header .active i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#listingView.compact .header .active {
|
||||
#listingView .header .active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
@ -312,11 +304,28 @@ body.rtl #listingView {
|
|||
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;
|
||||
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 {
|
||||
width: 3em;
|
||||
}
|
||||
|
@ -337,13 +346,18 @@ body.rtl #listingView {
|
|||
}
|
||||
|
||||
#listingView .header {
|
||||
display: none;
|
||||
display: flex !important;
|
||||
background: white;
|
||||
border-radius: 1em;
|
||||
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;
|
||||
vertical-align: middle;
|
||||
margin-left: .2em;
|
||||
|
@ -351,12 +365,8 @@ body.rtl #listingView {
|
|||
|
||||
#listingView.compact .header,
|
||||
#listingView.list .header {
|
||||
display: flex !important;
|
||||
border-top-left-radius: 1em;
|
||||
border-top-right-radius: 1em;
|
||||
z-index: 999;
|
||||
padding: .85em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#listingView.list .item:first-child {
|
||||
|
@ -383,7 +393,9 @@ body.rtl #listingView {
|
|||
display:flex;
|
||||
width: 100%;
|
||||
}
|
||||
#listingView.list .header {
|
||||
#listingView.list .header,
|
||||
#listingView.normal .header,
|
||||
#listingView.gallery .header {
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
|
@ -391,11 +403,11 @@ body.rtl #listingView {
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
#listingView.list .name {
|
||||
#listingView .name {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
#listingView.list .header span {
|
||||
#listingView .header span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
|
|
@ -130,10 +130,6 @@ router.beforeResolve(async (to, from, next) => {
|
|||
return next(false);
|
||||
}
|
||||
|
||||
if (state != null && state.user != null && !('username' in state.user)) {
|
||||
await validateLogin();
|
||||
}
|
||||
|
||||
// Set the page title using i18n
|
||||
const title = i18n.global.t(titles[to.name as keyof typeof titles]);
|
||||
document.title = title + " - " + name;
|
||||
|
@ -143,6 +139,11 @@ router.beforeResolve(async (to, from, next) => {
|
|||
|
||||
// Handle auth requirements
|
||||
if (to.matched.some((record) => record.meta.requiresAuth)) {
|
||||
|
||||
if (state != null && state.user != null && !('username' in state.user)) {
|
||||
await validateLogin();
|
||||
}
|
||||
|
||||
if (!getters.isLoggedIn()) {
|
||||
next({
|
||||
path: "/login",
|
||||
|
|
|
@ -53,10 +53,6 @@ export const getters = {
|
|||
state.req.items.forEach((item) => {
|
||||
// Check if the item is a 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
|
||||
dirCount++;
|
||||
}
|
||||
|
@ -69,10 +65,6 @@ export const getters = {
|
|||
state.req.items.forEach((item) => {
|
||||
// Check if the item is a 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
|
||||
fileCount++;
|
||||
}
|
||||
|
@ -88,9 +80,6 @@ export const getters = {
|
|||
const files = [];
|
||||
|
||||
state.req.items.forEach((item) => {
|
||||
if (state.user.hideDotfiles && item.name.startsWith(".")) {
|
||||
return;
|
||||
}
|
||||
if (item.type == "directory") {
|
||||
dirs.push(item);
|
||||
} else {
|
||||
|
|
|
@ -140,16 +140,21 @@ export const mutations = {
|
|||
emitStateChanged();
|
||||
},
|
||||
addSelected: (value) => {
|
||||
console.log("addSelected", value)
|
||||
state.selected.push(value);
|
||||
emitStateChanged();
|
||||
},
|
||||
removeSelected: (value) => {
|
||||
console.log("removeSelected", value)
|
||||
|
||||
let i = state.selected.indexOf(value);
|
||||
if (i === -1) return;
|
||||
state.selected.splice(i, 1);
|
||||
emitStateChanged();
|
||||
},
|
||||
resetSelected: () => {
|
||||
console.log("resetSelected")
|
||||
|
||||
state.selected = [];
|
||||
mutations.setMultiple(false);
|
||||
emitStateChanged();
|
||||
|
@ -196,17 +201,20 @@ export const mutations = {
|
|||
// Emit state change event
|
||||
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) => {
|
||||
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;
|
||||
emitStateChanged();
|
||||
},
|
||||
|
@ -220,7 +228,8 @@ export const mutations = {
|
|||
emitStateChanged();
|
||||
},
|
||||
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();
|
||||
},
|
||||
updateClipboard: (value) => {
|
||||
|
|
|
@ -17,6 +17,7 @@ const loginPage = window.FileBrowser.LoginPage;
|
|||
const enableThumbs = window.FileBrowser.EnableThumbs;
|
||||
const resizePreview = window.FileBrowser.ResizePreview;
|
||||
const enableExec = window.FileBrowser.EnableExec;
|
||||
const externalUrl = window.FileBrowser.ExternalUrl
|
||||
const origin = window.location.origin;
|
||||
|
||||
const settings = [
|
||||
|
@ -29,6 +30,7 @@ const settings = [
|
|||
|
||||
export {
|
||||
name,
|
||||
externalUrl,
|
||||
disableExternal,
|
||||
externalLinks,
|
||||
disableUsedPercentage,
|
||||
|
|
|
@ -1,8 +1,32 @@
|
|||
import { state, mutations, getters } from "@/store"
|
||||
import { filesApi } from "@/api";
|
||||
import { notify } from "@/notify"
|
||||
import { removePrefix } from "@/utils/url.js";
|
||||
import { publicApi } from "@/api";
|
||||
|
||||
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()) {
|
||||
filesApi.download(null, [getters.selectedDownloadUrl()]);
|
||||
return;
|
||||
|
|
|
@ -3,6 +3,8 @@ import url from "@/utils/url.js";
|
|||
import { filesApi } from "@/api";
|
||||
|
||||
export function checkConflict(files, items) {
|
||||
console.log("testing",files)
|
||||
|
||||
if (typeof items === "undefined" || items === null) {
|
||||
items = [];
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ export default {
|
|||
methods: {
|
||||
scrollToHash() {
|
||||
if (window.location.hash === this.lastHash) return;
|
||||
this.lastHash = window.location.hash
|
||||
this.lastHash = window.location.hash;
|
||||
if (window.location.hash) {
|
||||
const id = url.base64Encode(window.location.hash.slice(1));
|
||||
const element = document.getElementById(id);
|
||||
|
@ -94,13 +94,12 @@ export default {
|
|||
},
|
||||
async fetchData() {
|
||||
if (state.route.path === this.lastPath) return;
|
||||
this.lastHash = ""
|
||||
this.lastHash = "";
|
||||
// Set loading to true and reset the error.
|
||||
mutations.setLoading("files", true);
|
||||
this.error = null;
|
||||
// Reset view information using mutations
|
||||
mutations.setReload(false);
|
||||
mutations.resetSelected();
|
||||
mutations.setMultiple(false);
|
||||
mutations.closeHovers();
|
||||
|
||||
|
@ -125,7 +124,7 @@ export default {
|
|||
} catch (e) {
|
||||
notify.showError(e);
|
||||
this.error = e;
|
||||
mutations.replaceRequest(null);
|
||||
mutations.replaceRequest({});
|
||||
} finally {
|
||||
mutations.replaceRequest(data);
|
||||
mutations.setLoading("files", false);
|
||||
|
|
|
@ -1,12 +1,23 @@
|
|||
<template v-if="isLoggedIn">
|
||||
<template>
|
||||
<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-bind:style="{ width: this.progress + '%' }"></div>
|
||||
</div>
|
||||
<listingBar :class="{ 'dark-mode-header': isDarkMode }" v-if="currentView == 'listingView'"></listingBar>
|
||||
<editorBar :class="{ 'dark-mode-header': isDarkMode }" v-else-if="currentView == 'editor'"></editorBar>
|
||||
<defaultBar :class="{ 'dark-mode-header': isDarkMode }" v-else></defaultBar>
|
||||
<listingBar
|
||||
:class="{ 'dark-mode-header': isDarkMode }"
|
||||
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>
|
||||
<search v-if="showSearch"></search>
|
||||
<main :class="{ 'dark-mode': isDarkMode, moveWithSidebar: moveWithSidebar }">
|
||||
|
@ -14,6 +25,7 @@
|
|||
</main>
|
||||
<prompts :class="{ 'dark-mode': isDarkMode }"></prompts>
|
||||
</div>
|
||||
|
||||
<div class="card" id="popup-notification">
|
||||
<i v-on:click="closePopUp" class="material-icons">close</i>
|
||||
<div id="popup-notification-content">no info</div>
|
||||
|
@ -55,7 +67,7 @@ export default {
|
|||
mounted() {
|
||||
window.addEventListener("resize", this.updateIsMobile);
|
||||
if (state.user.themeColor) {
|
||||
document.documentElement.style.setProperty('--primaryColor', state.user.themeColor);
|
||||
document.documentElement.style.setProperty("--primaryColor", state.user.themeColor);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -114,16 +126,6 @@ export default {
|
|||
},
|
||||
},
|
||||
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() {
|
||||
mutations.setMobile();
|
||||
},
|
||||
|
|
|
@ -279,7 +279,7 @@ export default {
|
|||
let file = await publicApi.fetchPub(this.subPath, this.hash, this.password);
|
||||
file.hash = this.hash;
|
||||
this.token = file.token;
|
||||
mutations.updateRequest(file);
|
||||
mutations.replaceRequest(file);
|
||||
document.title = `${file.name} - ${document.title}`;
|
||||
} catch (error) {
|
||||
this.error = error;
|
||||
|
|
|
@ -1,24 +1,15 @@
|
|||
<template>
|
||||
<header>
|
||||
<header :class="{ 'dark-mode-header': isDarkMode }">
|
||||
<action v-if="notShare" icon="close" :label="$t('buttons.close')" @action="close()" />
|
||||
<title v-if="isSettings" class="topTitle">Settings</title>
|
||||
<title v-else class="topTitle">{{ req.name }}</title>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.flexbar {
|
||||
display: flex;
|
||||
flex-direction: block;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { url } from "@/utils";
|
||||
import router from "@/router";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { filesApi } from "@/api";
|
||||
import { getters, state } from "@/store";
|
||||
import Action from "@/components/Action.vue";
|
||||
|
||||
export default {
|
||||
|
@ -26,182 +17,18 @@ export default {
|
|||
components: {
|
||||
Action,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
columnWidth: 350,
|
||||
width: window.innerWidth,
|
||||
itemWeight: 0,
|
||||
viewModes: ["list", "compact", "normal", "gallery"],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
notShare() {
|
||||
return getters.currentView() != "share";
|
||||
},
|
||||
isSettings() {
|
||||
return getters.isSettings();
|
||||
},
|
||||
// Map state and getters
|
||||
req() {
|
||||
return state.req;
|
||||
},
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
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,
|
||||
};
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
},
|
||||
},
|
||||
|
||||
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: {
|
||||
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() {
|
||||
if (getters.isSettings()) {
|
||||
// Use this.isSettings to access the computed property
|
||||
|
@ -214,146 +41,6 @@ export default {
|
|||
router.push({ path: uri });
|
||||
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>
|
||||
|
|
|
@ -16,26 +16,64 @@
|
|||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t("files.lonely") }}</span>
|
||||
</h2>
|
||||
<input style="display: none" type="file" id="upload-input" @change="uploadInput($event)" multiple />
|
||||
<input style="display: none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory
|
||||
multiple />
|
||||
<input
|
||||
style="display: none"
|
||||
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 v-else id="listingView" ref="listingView" :class="listingViewMode + ' file-icons'">
|
||||
<div
|
||||
v-else
|
||||
id="listingView"
|
||||
ref="listingView"
|
||||
:class="listingViewMode + ' file-icons'"
|
||||
>
|
||||
<div>
|
||||
<div class="header" :class="{ 'dark-mode-item-header': isDarkMode }">
|
||||
<p :class="{ active: nameSorted }" class="name" role="button" tabindex="0" @click="sort('name')"
|
||||
:title="$t('files.sortByName')" :aria-label="$t('files.sortByName')">
|
||||
<p
|
||||
: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>
|
||||
<i class="material-icons">{{ nameIcon }}</i>
|
||||
</p>
|
||||
|
||||
<p :class="{ active: sizeSorted }" class="size" role="button" tabindex="0" @click="sort('size')"
|
||||
:title="$t('files.sortBySize')" :aria-label="$t('files.sortBySize')">
|
||||
<p
|
||||
: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>
|
||||
<i class="material-icons">{{ sizeIcon }}</i>
|
||||
</p>
|
||||
<p :class="{ active: modifiedSorted }" class="modified" role="button" tabindex="0" @click="sort('modified')"
|
||||
:title="$t('files.sortByLastModified')" :aria-label="$t('files.sortByLastModified')">
|
||||
<p
|
||||
: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>
|
||||
<i class="material-icons">{{ modifiedIcon }}</i>
|
||||
</p>
|
||||
|
@ -46,10 +84,23 @@
|
|||
<h2>{{ $t("files.folders") }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="numDirs > 0" class="folder-items" :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
|
||||
v-if="numDirs > 0"
|
||||
class="folder-items"
|
||||
: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 v-if="numFiles > 0">
|
||||
<div class="header-items">
|
||||
|
@ -57,14 +108,35 @@
|
|||
</div>
|
||||
</div>
|
||||
<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"
|
||||
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" />
|
||||
<item
|
||||
v-for="item in files"
|
||||
: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>
|
||||
|
||||
<input style="display: none" type="file" id="upload-input" @change="uploadInput($event)" multiple />
|
||||
<input style="display: none" type="file" id="upload-folder-input" @change="uploadInput($event)" webkitdirectory
|
||||
multiple />
|
||||
<input
|
||||
style="display: none"
|
||||
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>
|
||||
|
@ -93,6 +165,7 @@ export default {
|
|||
width: window.innerWidth,
|
||||
lastSelected: {}, // Add this to track the currently focused item
|
||||
contextTimeout: null, // added for safari context menu
|
||||
ctrKeyPressed: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -196,7 +269,6 @@ export default {
|
|||
this.colunmsResize();
|
||||
return state.user.viewMode;
|
||||
},
|
||||
|
||||
selectedCount() {
|
||||
return state.selected.length;
|
||||
},
|
||||
|
@ -215,7 +287,8 @@ export default {
|
|||
window.addEventListener("keydown", this.keyEvent);
|
||||
window.addEventListener("scroll", this.scrollEvent);
|
||||
window.addEventListener("resize", this.windowsResize);
|
||||
this.$el.addEventListener("click", this.clickClear);
|
||||
window.addEventListener("click", this.clickClear);
|
||||
window.addEventListener("keyup", this.clearCtrKey);
|
||||
|
||||
// Adjust contextmenu listener based on browser
|
||||
if (state.isSafari) {
|
||||
|
@ -250,7 +323,6 @@ export default {
|
|||
this.$el.removeEventListener("touchend", this.cancelContext);
|
||||
this.$el.removeEventListener("mouseup", this.cancelContext);
|
||||
this.$el.removeEventListener("touchmove", this.handleTouchMove);
|
||||
|
||||
} else {
|
||||
window.removeEventListener("contextmenu", this.openContext);
|
||||
}
|
||||
|
@ -425,17 +497,22 @@ export default {
|
|||
}, 50);
|
||||
}
|
||||
},
|
||||
clearCtrKey(event) {
|
||||
const { ctrlKey } = event;
|
||||
if (!ctrlKey) {
|
||||
this.ctrKeyPressed = false;
|
||||
}
|
||||
},
|
||||
keyEvent(event) {
|
||||
const { key, ctrlKey, metaKey, which } = event;
|
||||
// Check if the key is alphanumeric
|
||||
const isAlphanumeric = /^[a-z0-9]$/i.test(key);
|
||||
const noModifierKeys = !ctrlKey && !metaKey;
|
||||
|
||||
if (isAlphanumeric && noModifierKeys && getters.currentPromptName() == null) {
|
||||
const modifierKeys = ctrlKey || metaKey;
|
||||
if (isAlphanumeric && !modifierKeys && getters.currentPromptName() == null) {
|
||||
this.alphanumericKeyPress(key); // Call the alphanumeric key press function
|
||||
return;
|
||||
}
|
||||
if (noModifierKeys && getters.currentPromptName() != null) {
|
||||
if (!modifierKeys && getters.currentPromptName() != null) {
|
||||
return;
|
||||
}
|
||||
// Handle the space bar key
|
||||
|
@ -452,6 +529,13 @@ export default {
|
|||
}
|
||||
let currentPath = state.route.path.replace(/\/+$/, ""); // Remove trailing slashes
|
||||
let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
|
||||
|
||||
if (modifierKeys) {
|
||||
if (!ctrlKey) {
|
||||
this.ctrKeyPressed = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Handle key events using a switch statement
|
||||
switch (key) {
|
||||
case "Enter":
|
||||
|
@ -486,11 +570,6 @@ export default {
|
|||
event.preventDefault();
|
||||
this.navigateKeboardArrows(key);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Handle keys with ctrl or meta keys
|
||||
if (!ctrlKey && !metaKey) return;
|
||||
break;
|
||||
}
|
||||
|
||||
const charKey = String.fromCharCode(which).toLowerCase();
|
||||
|
@ -513,8 +592,6 @@ export default {
|
|||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// Helper method to select all files and directories
|
||||
selectAll() {
|
||||
for (let file of this.items.files) {
|
||||
if (state.selected.indexOf(file.index) === -1) {
|
||||
|
@ -650,12 +727,18 @@ export default {
|
|||
action(false, false);
|
||||
},
|
||||
colunmsResize() {
|
||||
document.documentElement.style.setProperty('--item-width', `calc(${100 / this.numColumns}% - 1em)`);
|
||||
document.documentElement.style.setProperty(
|
||||
"--item-width",
|
||||
`calc(${100 / this.numColumns}% - 1em)`
|
||||
);
|
||||
|
||||
if (state.user.viewMode == "gallery") {
|
||||
document.documentElement.style.setProperty('--item-height', `calc(${this.columnWidth / 25}em)`);
|
||||
document.documentElement.style.setProperty(
|
||||
"--item-height",
|
||||
`calc(${this.columnWidth / 25}em)`
|
||||
);
|
||||
} else {
|
||||
document.documentElement.style.setProperty('--item-height', `auto`);
|
||||
document.documentElement.style.setProperty("--item-height", `auto`);
|
||||
}
|
||||
},
|
||||
dragEnter() {
|
||||
|
@ -814,9 +897,11 @@ export default {
|
|||
},
|
||||
});
|
||||
},
|
||||
clickClear() {
|
||||
clickClear(event) {
|
||||
// if control or shift is pressed, do not clear the selection
|
||||
if (this.ctrKeyPressed || event.shiftKey) return;
|
||||
const sameAsBefore = state.selected == this.lastSelected;
|
||||
if (sameAsBefore && !state.multiple) {
|
||||
if (sameAsBefore && !state.multiple && getters.currentPromptName == null) {
|
||||
mutations.resetSelected();
|
||||
}
|
||||
this.lastSelected = state.selected;
|
||||
|
|
|
@ -110,7 +110,6 @@ export default {
|
|||
event.preventDefault();
|
||||
try {
|
||||
if (this.isNew) {
|
||||
const loc = await usersApi.create(this.userPayload); // Use the computed property
|
||||
this.$router.push({ path: "/settings", hash: "#users-main" });
|
||||
notify.showSuccess(this.$t("settings.userCreated"));
|
||||
} else {
|
||||
|
|
Loading…
Reference in New Issue