Beta/v0.3.6 release (#279)

This commit is contained in:
Graham Steffaniak 2025-01-08 20:02:57 -05:00 committed by GitHub
parent 9cc30f25e4
commit cd3e111e39
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 638 additions and 597 deletions

View File

@ -1,4 +1,4 @@
name: dev tests
name: regular tests
on:
push:

81
.github/workflows/release_beta.yaml vendored Normal file
View File

@ -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 }}

View File

@ -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

View File

@ -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

View File

@ -1,9 +1,9 @@
name: tag
name: beta tag update
on:
push:
tags:
- "v*"
- "beta_v*"
permissions:
contents: write

31
.github/workflows/tag_stable.yaml vendored Normal file
View File

@ -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 }}

View File

@ -2,6 +2,22 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## 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**

View File

@ -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:

View File

@ -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 {

View File

@ -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)

View File

@ -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.

View File

@ -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

View File

@ -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" {

View File

@ -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)

View File

@ -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

View File

@ -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 != "" {

View File

@ -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 {

View File

@ -125,30 +125,38 @@ export async function post(url, content = "", overwrite = false, onupload) {
}
export async function moveCopy(items, action = "copy", overwrite = false, rename = false) {
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");

View File

@ -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}`);
}

View File

@ -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 += "/";

View File

@ -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" },
],
};

View File

@ -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 != []) {

View File

@ -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);

View File

@ -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);
},
},
};

View File

@ -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() {

View File

@ -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;
},
},
};

View File

@ -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);
}
},
},
};

View File

@ -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);

View File

@ -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;
}

View File

@ -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",

View File

@ -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 {

View File

@ -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) => {

View File

@ -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,

View File

@ -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;

View File

@ -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 = [];
}

View File

@ -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);

View File

@ -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();
},

View File

@ -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;

View File

@ -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>

View File

@ -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;

View File

@ -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 {