mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-15 13:35:55 -05:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91bc608f0e | ||
|
|
e21d553559 | ||
|
|
fde6857344 | ||
|
|
37aec9ebd8 | ||
|
|
4bf1ba312a | ||
|
+5 |
c8459eb404 | ||
|
|
241bbd69b4 | ||
|
|
9de21cd896 | ||
|
|
fcec78b57c | ||
|
|
12f27133b5 | ||
|
|
20a5d75d7e |
No files matched your search
+16
-1
@@ -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).
|
||||
|
||||
## v2.1.2
|
||||
|
||||
**Security**:
|
||||
- [High] New accounts named with ".." breaks out of scoped folder on creation (GHSA-c84q-q5g6-r47f) -- reporter @dylanzonix
|
||||
- [Medium] Fixed session token collision issue when multiple users login simultaneously. (GHSA-c9cr-w724-gqj3) (#3135) Thanks @dl-pt reporters: @Ishtiaque-Ahmed-Risat @AlexWhiplash @tonghuaroot
|
||||
|
||||
**Notes**:
|
||||
- Removed "Editor quick save" setting (`userDefaults.fileViewer.quickSave`), the save button is now always present in the editor toolbar. (#3122)
|
||||
- add Basque (eu) translation and fix Spanish (es) strings (#3118)
|
||||
|
||||
**Bug Fixes**:
|
||||
- fix(webdav): OPTIONS now advertises only methods actually supported and permitted for the user; the DAV header reports class 1 only since LOCK/UNLOCK are not enabled (#3110)
|
||||
- fix(download): revalidate single-file downloads so a changed file is not served stale (#3133)
|
||||
- Language switcher not working (#3114) (#3117)
|
||||
- Viewer showing a stale image after the file changed on disk: single-file downloads are now revalidated by the browser (#3132)
|
||||
|
||||
## v2.1.1
|
||||
|
||||
Note: this release also contains all changes from v2.0.10-beta changelog
|
||||
@@ -34,7 +50,6 @@ Note: this release also contains all changes from v2.0.10-beta changelog
|
||||
- fix(backend): skip denied paths in multi-file downloads, 404 when none remain (#3100) @hippi345
|
||||
- fix: persist the user scope path chosen in the edit dialog (#3065) @mvanhorn
|
||||
|
||||
|
||||
## v2.1.0
|
||||
|
||||
**Security**:
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
:pushpin: [Read The Official Docs](https://filebrowserquantum.com/) (currently english-only)
|
||||
|
||||
:pushpin: [v2.0.0 is now in beta!](https://filebrowserquantum.com/en/docs/getting-started/v2/about/)
|
||||
:pushpin: [v2.0.0 is now stable!](https://filebrowserquantum.com/en/docs/getting-started/v2/about/) --> See [migration support discussion](https://github.com/gtsteffaniak/filebrowser/discussions/3105)
|
||||
|
||||
## About
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ server:
|
||||
|
||||
userDefaults:
|
||||
fileViewer:
|
||||
editorQuickSave: false
|
||||
disableOnlyOfficeExt: ".md .txt .pdf"
|
||||
disableViewingExt: ""
|
||||
sidebar:
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/access"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
// Auther is the authentication interface.
|
||||
@@ -39,12 +40,17 @@ func MakeSignedTokenAPI(user *users.User, name string, duration time.Duration, p
|
||||
if _, ok := user.Tokens[name]; ok {
|
||||
return "", users.AuthToken{}, fmt.Errorf("key already exists with same name %v ", name)
|
||||
}
|
||||
id, err := utils.RandomHex(16)
|
||||
if err != nil {
|
||||
return "", users.AuthToken{}, fmt.Errorf("generate token id: %w", err)
|
||||
}
|
||||
now := time.Now()
|
||||
expires := now.Add(duration)
|
||||
// Create minimal token with only JWT standard claims
|
||||
claim := users.AuthToken{
|
||||
MinimalAuthToken: users.MinimalAuthToken{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ID: id,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(expires),
|
||||
Issuer: FB_ISSUER,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"testing/synctest"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
func TestSignedTokensAreUniqueAtSameTime(t *testing.T) {
|
||||
origKey := settings.Config.Auth.Key
|
||||
settings.Config.Auth.Key = "test-signing-key-for-token-uniqueness"
|
||||
t.Cleanup(func() { settings.Config.Auth.Key = origKey })
|
||||
|
||||
// Freeze the clock so every token has identical issue and expiry times.
|
||||
synctest.Test(t, func(t *testing.T) {
|
||||
now := time.Now()
|
||||
tokens := make(map[string]users.AuthToken)
|
||||
for _, userID := range []uint64{1, 2} {
|
||||
user := &users.User{ID: userID}
|
||||
for _, minimal := range []bool{true, false} {
|
||||
for range 2 {
|
||||
tokenString, claim, err := MakeSignedTokenAPI(user, "test-token", time.Hour, users.Permissions{Api: true}, minimal)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, exists := tokens[tokenString]; exists {
|
||||
t.Fatalf("token collision for user %d with minimal=%v", userID, minimal)
|
||||
}
|
||||
tokens[tokenString] = claim
|
||||
}
|
||||
}
|
||||
}
|
||||
ids := make(map[string]bool)
|
||||
for tokenString, claim := range tokens {
|
||||
var parsed users.MinimalAuthToken
|
||||
if _, err := jwt.ParseWithClaims(tokenString, &parsed, JWTSigningKeyFunc()); err != nil {
|
||||
t.Fatalf("parse signed token: %v", err)
|
||||
}
|
||||
if parsed.ID == "" || ids[parsed.ID] {
|
||||
t.Fatal("each signed token must have a unique nonempty id")
|
||||
}
|
||||
ids[parsed.ID] = true
|
||||
if parsed.ID != claim.ID {
|
||||
t.Fatal("signed token id must match returned metadata")
|
||||
}
|
||||
if parsed.IssuedAt == nil || !parsed.IssuedAt.Equal(now) || parsed.ExpiresAt == nil || !parsed.ExpiresAt.Equal(now.Add(time.Hour)) {
|
||||
t.Fatal("token uniqueness must not depend on changing timestamps")
|
||||
}
|
||||
assertMinimalJWTClaims(t, tokenString)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -21,3 +21,16 @@ func TestValidateUsernameAcceptsNormalName(t *testing.T) {
|
||||
t.Fatalf("expected valid username, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateUsernameRejectsUnsafeNames(t *testing.T) {
|
||||
for _, username := range []string{"..", "....", ".", "-", "al..ice", "bob/", "./bob", `bob\`, "alice/docs", " alice", "alice "} {
|
||||
if err := ValidateUsername(username); err == nil {
|
||||
t.Fatalf("expected %q to be rejected", username)
|
||||
}
|
||||
}
|
||||
for _, username := range []string{"alice", "first.last", "j.r.r", "a-b_c", "user@example.com", "José", "李雷", ".alice", "users"} {
|
||||
if err := ValidateUsername(username); err != nil {
|
||||
t.Fatalf("expected %q to be accepted, got %v", username, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,10 @@ var reservedUsernames = map[string]struct{}{
|
||||
strings.ToLower(AnonymousUserName): {},
|
||||
}
|
||||
|
||||
// ValidateUsername rejects empty and reserved login names.
|
||||
// ValidateUsername rejects empty and reserved login names, and names that do not map to their
|
||||
// own createUserDir home directory.
|
||||
func ValidateUsername(username string) error {
|
||||
raw := username
|
||||
username = strings.TrimSpace(username)
|
||||
if username == "" {
|
||||
return fmt.Errorf("username is empty")
|
||||
@@ -56,6 +58,11 @@ func ValidateUsername(username string) error {
|
||||
if _, reserved := reservedUsernames[strings.ToLower(username)]; reserved {
|
||||
return fmt.Errorf("username %q is reserved", username)
|
||||
}
|
||||
// The per-user home directory is derived from CleanUsername(username), so reject names
|
||||
// that it would rewrite, the names "." and "-", and names containing path separators.
|
||||
if CleanUsername(raw) != raw || raw == "." || raw == "-" || strings.ContainsAny(raw, `/\`) {
|
||||
return fmt.Errorf("username %q contains characters that are not allowed", raw)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -235,7 +242,6 @@ type BackendScope struct {
|
||||
|
||||
// json tags must match variable name with smaller case first letter
|
||||
type NonAdminEditable struct {
|
||||
EditorQuickSave bool `json:"editorQuickSave"` // show quick save button in editor
|
||||
HideSidebarFileActions bool `json:"hideSidebarFileActions"` // hide the file actions in the sidebar
|
||||
DisableQuickToggles bool `json:"disableQuickToggles"` // disable the quick toggles in the sidebar
|
||||
DisableSearchOptions bool `json:"disableSearchOptions"` // disable the search options in the search bar
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/activity"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/app"
|
||||
@@ -497,6 +498,102 @@ func TestServeSingleFile_MissingScopedPathReturns404(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestServeSingleFile_RequiresRevalidation verifies that a single-file download is not
|
||||
// reused from the browser cache without revalidation, so a file changed on disk is not
|
||||
// served stale, and that an unchanged file still answers 304.
|
||||
func TestServeSingleFile_RequiresRevalidation(t *testing.T) {
|
||||
sourceRoot, sourceName, _ := setupShareArchiveDownloadTest(t)
|
||||
ownerUser := users.User{
|
||||
FrontendUser: users.FrontendUser{Username: "owner"},
|
||||
BackendScopes: []users.BackendScope{{
|
||||
Path: sourceRoot,
|
||||
Scope: "/",
|
||||
Permissions: users.SourceFilePermissions{
|
||||
View: true, Download: true, Modify: true, Create: true, Delete: true,
|
||||
},
|
||||
}},
|
||||
}
|
||||
users.SyncBackendSourcePermissionsMap(&ownerUser)
|
||||
d := &Context{User: &ownerUser}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
status, err := ServeSingleFile(rec, httptest.NewRequest(http.MethodGet, "/", nil), d, sourceName, "/shared/hello.txt", "hello.txt", ServeSingleFileOptions{ForceInline: true})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: status=%d err=%v", status, err)
|
||||
}
|
||||
if got := rec.Header().Get("Cache-Control"); got != "private, no-cache" {
|
||||
t.Fatalf("expected Cache-Control %q, got %q", "private, no-cache", got)
|
||||
}
|
||||
lastModified := rec.Header().Get("Last-Modified")
|
||||
if lastModified == "" {
|
||||
t.Fatal("expected a Last-Modified header to revalidate against")
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.Header.Set("If-Modified-Since", lastModified)
|
||||
rec = httptest.NewRecorder()
|
||||
if _, err := ServeSingleFile(rec, req, d, sourceName, "/shared/hello.txt", "hello.txt", ServeSingleFileOptions{ForceInline: true}); err != nil {
|
||||
t.Fatalf("conditional request: unexpected error: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusNotModified {
|
||||
t.Fatalf("expected 304 for an unchanged file, got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestServeSingleFile_SameSecondEditIsNotServedStale verifies that an edit within the same
|
||||
// second as the cached copy, which If-Modified-Since cannot see, changes the ETag so a
|
||||
// conditional request gets the new content rather than 304.
|
||||
func TestServeSingleFile_SameSecondEditIsNotServedStale(t *testing.T) {
|
||||
sourceRoot, sourceName, _ := setupShareArchiveDownloadTest(t)
|
||||
ownerUser := users.User{
|
||||
FrontendUser: users.FrontendUser{Username: "owner"},
|
||||
BackendScopes: []users.BackendScope{{
|
||||
Path: sourceRoot,
|
||||
Scope: "/",
|
||||
Permissions: users.SourceFilePermissions{
|
||||
View: true, Download: true, Modify: true, Create: true, Delete: true,
|
||||
},
|
||||
}},
|
||||
}
|
||||
users.SyncBackendSourcePermissionsMap(&ownerUser)
|
||||
d := &Context{User: &ownerUser}
|
||||
filePath := filepath.Join(sourceRoot, "shared", "hello.txt")
|
||||
modTime := time.Date(2026, 1, 2, 3, 4, 5, 100_000_000, time.UTC)
|
||||
if err := os.Chtimes(filePath, modTime, modTime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
if _, err := ServeSingleFile(rec, httptest.NewRequest(http.MethodGet, "/", nil), d, sourceName, "/shared/hello.txt", "hello.txt", ServeSingleFileOptions{ForceInline: true}); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
etag, lastModified := rec.Header().Get("ETag"), rec.Header().Get("Last-Modified")
|
||||
|
||||
// Same second, same size: only the ETag can tell the two versions apart.
|
||||
if err := os.WriteFile(filePath, []byte("share-zip-CHANGED!"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chtimes(filePath, modTime.Add(time.Millisecond), modTime.Add(time.Millisecond)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
if etag != "" {
|
||||
req.Header.Set("If-None-Match", etag)
|
||||
}
|
||||
req.Header.Set("If-Modified-Since", lastModified)
|
||||
rec = httptest.NewRecorder()
|
||||
if _, err := ServeSingleFile(rec, req, d, sourceName, "/shared/hello.txt", "hello.txt", ServeSingleFileOptions{ForceInline: true}); err != nil {
|
||||
t.Fatalf("conditional request: unexpected error: %v", err)
|
||||
}
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 for a file edited within the same second, got %d", rec.Code)
|
||||
}
|
||||
if got := rec.Body.String(); got != "share-zip-CHANGED!" {
|
||||
t.Fatalf("expected the new content, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRawFilesHandler_ActivityLogExcludesDeniedPaths verifies that when a download
|
||||
// mixes allowed and denied paths, only the allowed paths appear in the recorded
|
||||
// activity entry (the denied path name must not be revealed via the audit log).
|
||||
|
||||
@@ -6,12 +6,87 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/auth"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/state"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
|
||||
)
|
||||
|
||||
func TestSessionTokensRemainIndependent(t *testing.T) {
|
||||
setupTestEnv(t)
|
||||
origKey := settings.Config.Auth.Key
|
||||
origExp := settings.Config.Auth.TokenExpirationHours
|
||||
settings.Config.Auth.Key = "test-signing-key-for-independent-sessions"
|
||||
settings.Config.Auth.TokenExpirationHours = 2
|
||||
t.Cleanup(func() {
|
||||
settings.Config.Auth.Key = origKey
|
||||
settings.Config.Auth.TokenExpirationHours = origExp
|
||||
})
|
||||
|
||||
user := createTokenAuthUser(t, "session-user", users.Permissions{Api: true})
|
||||
admin := createTokenAuthUser(t, "session-admin", users.Permissions{Admin: true, Api: true})
|
||||
owners := []*users.User{user, user, admin}
|
||||
tokens := make([]string, len(owners))
|
||||
for i, owner := range owners {
|
||||
var err error
|
||||
tokens[i], err = mintAndRegisterSessionToken(owner)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
for i, token := range tokens {
|
||||
if userID, isSession, ok := state.HashedTokenOwner(token); !ok || !isSession || userID != owners[i].ID {
|
||||
t.Fatalf("session %d lost its owner: userID=%d isSession=%v ok=%v", i, userID, isSession, ok)
|
||||
}
|
||||
want := http.StatusForbidden
|
||||
if owners[i].Permissions.Admin {
|
||||
want = http.StatusOK
|
||||
}
|
||||
if rec := cookieRequest(t, withAdmin(mockHandler), token); rec.Code != want {
|
||||
t.Fatalf("session %d: got status %d, want %d", i, rec.Code, want)
|
||||
}
|
||||
}
|
||||
|
||||
if err := state.RevokeToken(tokens[0]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, token := range tokens {
|
||||
want := http.StatusOK
|
||||
if i == 0 {
|
||||
want = http.StatusUnauthorized
|
||||
}
|
||||
if rec := cookieRequest(t, withUser(mockHandler), token); rec.Code != want {
|
||||
t.Fatalf("session %d after revocation: got status %d, want %d", i, rec.Code, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionTokenWithoutIDRemainsValid(t *testing.T) {
|
||||
setupTestEnv(t)
|
||||
origKey := settings.Config.Auth.Key
|
||||
settings.Config.Auth.Key = "test-signing-key-for-legacy-session"
|
||||
t.Cleanup(func() { settings.Config.Auth.Key = origKey })
|
||||
|
||||
user := createTokenAuthUser(t, "legacy-session-user", users.Permissions{Api: true})
|
||||
now := time.Now()
|
||||
token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
|
||||
Issuer: auth.FB_ISSUER,
|
||||
}).SignedString([]byte(settings.Config.Auth.Key))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := state.RegisterSessionToken(token, user.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if rec := cookieRequest(t, withUser(mockHandler), token); rec.Code != http.StatusOK {
|
||||
t.Fatalf("legacy session: got status %d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplaceSessionTokenKeepsOldTokenWhenMintFails(t *testing.T) {
|
||||
setupTestEnv(t)
|
||||
user := createTokenAuthUser(t, "rotation-user", users.Permissions{Api: true})
|
||||
|
||||
@@ -552,7 +552,12 @@ func ServeSingleFile(w http.ResponseWriter, r *http.Request, d *Context, source
|
||||
}
|
||||
|
||||
SetContentDisposition(w, r, displayFileName, opts.ForceInline)
|
||||
w.Header().Set("Cache-Control", "private")
|
||||
// no-cache: the URL does not change when the file does, so browsers must revalidate
|
||||
// (ServeContent answers 304 when unchanged) instead of reusing a stale copy. The ETag,
|
||||
// from mtime in nanoseconds and size, catches an edit within the same second, which
|
||||
// If-Modified-Since cannot; it costs no read of the file.
|
||||
w.Header().Set("Cache-Control", "private, no-cache")
|
||||
w.Header().Set("ETag", fmt.Sprintf(`"%x-%x"`, fileInfo.ModTime().UnixNano(), fileInfo.Size()))
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
var reader io.ReadSeeker = fd
|
||||
|
||||
@@ -51,11 +51,11 @@ func (f *fileInfoWrapper) mode() os.FileMode {
|
||||
|
||||
// filteredFileSystem wraps a webdav.FileSystem and filters directory listings using FileInfoFaster
|
||||
type filteredFileSystem struct {
|
||||
fs webdav.FileSystem
|
||||
source string
|
||||
user *users.User
|
||||
filePerms users.SourceFilePermissions
|
||||
httpReq *http.Request
|
||||
fs webdav.FileSystem
|
||||
source string
|
||||
user *users.User
|
||||
filePerms users.SourceFilePermissions
|
||||
httpReq *http.Request
|
||||
// Cache FileInfoFaster results per path to avoid redundant calls within the same request
|
||||
fileInfoCache map[string]*iteminfo.ExtendedFileInfo
|
||||
}
|
||||
@@ -626,6 +626,10 @@ func webDAVHandler(w http.ResponseWriter, r *http.Request, d *Context) (int, err
|
||||
httpReq: r,
|
||||
}
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
return writeWebDAVOptions(w, r, filteredFS, prefix, filePerms)
|
||||
}
|
||||
|
||||
wd := &webdav.Handler{
|
||||
Prefix: prefix,
|
||||
FileSystem: filteredFS,
|
||||
@@ -657,6 +661,46 @@ func webDAVHandler(w http.ResponseWriter, r *http.Request, d *Context) (int, err
|
||||
return 200, nil // errors and responses (XML-formatted) are handled by webdav handler
|
||||
}
|
||||
|
||||
// writeWebDAVOptions answers OPTIONS directly so the Allow/DAV headers reflect
|
||||
// the methods this server actually supports and the user's permissions, instead
|
||||
// of the webdav library's hardcoded advertisement (which includes LOCK etc.).
|
||||
func writeWebDAVOptions(w http.ResponseWriter, r *http.Request, ffs *filteredFileSystem, prefix string, perms users.SourceFilePermissions) (int, error) {
|
||||
rest, ok := strings.CutPrefix(r.URL.Path, prefix)
|
||||
if !ok || (rest != "" && rest[0] != '/') {
|
||||
return http.StatusNotFound, fmt.Errorf("request path %q outside webdav prefix %q", r.URL.Path, prefix)
|
||||
}
|
||||
requestPath := path.Clean("/" + rest)
|
||||
|
||||
var candidates []string
|
||||
fi, err := ffs.Stat(r.Context(), requestPath)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
if err != nil {
|
||||
candidates = []string{http.MethodPut, "MKCOL"}
|
||||
} else if fi.IsDir() {
|
||||
candidates = []string{"PROPFIND", http.MethodDelete, "COPY", "MOVE"}
|
||||
} else {
|
||||
candidates = []string{http.MethodGet, http.MethodHead, "PROPFIND", http.MethodPut, http.MethodDelete, "COPY", "MOVE"}
|
||||
}
|
||||
|
||||
allowed := []string{http.MethodOptions}
|
||||
for _, m := range candidates {
|
||||
if _, err := webDAVMethodPermission(m, perms); err == nil {
|
||||
allowed = append(allowed, m)
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Allow", strings.Join(allowed, ", "))
|
||||
w.Header().Set("DAV", "1")
|
||||
w.Header().Set("MS-Author-Via", "DAV")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func webDAVMethodPermission(method string, perms users.SourceFilePermissions) (int, error) {
|
||||
switch method {
|
||||
case "PROPFIND", http.MethodOptions:
|
||||
|
||||
@@ -874,7 +874,6 @@ func TestWebDAV_CopyPreservesModTime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// TestWebDAV_ListingsIncludeHidden verifies that dotfiles/dotdirs are listed
|
||||
// over WebDAV even when the user's showHidden UI preference is false, and that
|
||||
// hidden paths are writable (needed for two-way sync clients like rclone).
|
||||
@@ -994,3 +993,156 @@ func initTestIndex(t *testing.T, name, path string) {
|
||||
// For WebDAV tests, indices are already initialized in setupWebDAVTestEnv
|
||||
t.Helper()
|
||||
}
|
||||
|
||||
// Test that OPTIONS advertises only the methods actually supported and
|
||||
// permitted for the user, rather than the webdav library's hardcoded list.
|
||||
func TestWebDAV_OPTIONS(t *testing.T) {
|
||||
source1Path, _ := setupWebDAVTestEnv(t)
|
||||
|
||||
fullAccessUser := &users.User{
|
||||
ID: 1,
|
||||
FrontendUser: users.FrontendUser{
|
||||
Username: "optionsfull",
|
||||
},
|
||||
BackendScopes: []users.BackendScope{
|
||||
{Path: source1Path, Scope: "/"},
|
||||
},
|
||||
BackendSourcePermissions: webDAVPermsForPaths(true, true, true, true, source1Path),
|
||||
Version: users.SourcePermissionsMigrationVersion,
|
||||
}
|
||||
applyBackendSourcePerms(fullAccessUser, fullAccessUser.BackendSourcePermissions)
|
||||
|
||||
readOnlyUser := &users.User{
|
||||
ID: 2,
|
||||
FrontendUser: users.FrontendUser{
|
||||
Username: "optionsreadonly",
|
||||
},
|
||||
BackendScopes: []users.BackendScope{
|
||||
{Path: source1Path, Scope: "/"},
|
||||
},
|
||||
BackendSourcePermissions: webDAVPermsForPaths(false, false, false, false, source1Path),
|
||||
Version: users.SourcePermissionsMigrationVersion,
|
||||
}
|
||||
applyBackendSourcePerms(readOnlyUser, readOnlyUser.BackendSourcePermissions)
|
||||
|
||||
initTestIndex(t, "source1", source1Path)
|
||||
|
||||
// Methods the handler can never allow (they hit the default: -> 403 branch
|
||||
// of webDAVMethodPermission) must never be advertised.
|
||||
neverAllowed := []string{"LOCK", "UNLOCK", "PROPPATCH", "POST"}
|
||||
|
||||
doOptions := func(t *testing.T, user *users.User, path string) map[string]bool {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodOptions, "/dav/source1"+path, nil)
|
||||
req.SetPathValue("source", "source1")
|
||||
req.SetPathValue("path", path)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
status, err := webDAVHandler(w, req, &requestContext{User: user})
|
||||
if err != nil {
|
||||
t.Fatalf("webDAVHandler OPTIONS %s: %v", path, err)
|
||||
}
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("OPTIONS %s: expected status 200, got %d (status=%d)", path, w.Code, status)
|
||||
}
|
||||
if dav := w.Header().Get("DAV"); dav != "1" {
|
||||
t.Errorf("OPTIONS %s: expected DAV \"1\", got %q", path, dav)
|
||||
}
|
||||
if via := w.Header().Get("MS-Author-Via"); via != "DAV" {
|
||||
t.Errorf("OPTIONS %s: expected MS-Author-Via \"DAV\", got %q", path, via)
|
||||
}
|
||||
|
||||
allowed := make(map[string]bool)
|
||||
for _, m := range strings.Split(w.Header().Get("Allow"), ", ") {
|
||||
if m != "" {
|
||||
allowed[m] = true
|
||||
}
|
||||
}
|
||||
if !allowed[http.MethodOptions] {
|
||||
t.Errorf("OPTIONS %s: Allow %q missing OPTIONS", path, w.Header().Get("Allow"))
|
||||
}
|
||||
for _, m := range neverAllowed {
|
||||
if allowed[m] {
|
||||
t.Errorf("OPTIONS %s: Allow %q advertises unsupported method %s", path, w.Header().Get("Allow"), m)
|
||||
}
|
||||
}
|
||||
// Consistency: everything advertised must pass webDAVMethodPermission
|
||||
// for this user, so Allow never lies about what would succeed.
|
||||
perms := user.BackendSourcePermissions[source1Path]
|
||||
for m := range allowed {
|
||||
if _, permErr := webDAVMethodPermission(m, perms); permErr != nil {
|
||||
t.Errorf("OPTIONS %s: Allow advertises %s but webDAVMethodPermission denies it: %v", path, m, permErr)
|
||||
}
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
|
||||
t.Run("directory advertises only supported methods", func(t *testing.T) {
|
||||
allowed := doOptions(t, fullAccessUser, "/public/")
|
||||
for _, m := range []string{"PROPFIND", http.MethodDelete, "COPY", "MOVE"} {
|
||||
if !allowed[m] {
|
||||
t.Errorf("expected %s in Allow for directory, got %v", m, allowed)
|
||||
}
|
||||
}
|
||||
// GET/PUT/MKCOL don't apply to a collection URL itself
|
||||
for _, m := range []string{http.MethodGet, http.MethodHead, http.MethodPut, "MKCOL"} {
|
||||
if allowed[m] {
|
||||
t.Errorf("unexpected %s in Allow for directory, got %v", m, allowed)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("file advertises read and write methods", func(t *testing.T) {
|
||||
allowed := doOptions(t, fullAccessUser, "/public/readme.txt")
|
||||
for _, m := range []string{http.MethodGet, http.MethodHead, "PROPFIND", http.MethodPut, http.MethodDelete, "COPY", "MOVE"} {
|
||||
if !allowed[m] {
|
||||
t.Errorf("expected %s in Allow for file, got %v", m, allowed)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing path advertises creation methods", func(t *testing.T) {
|
||||
allowed := doOptions(t, fullAccessUser, "/public/newfile.txt")
|
||||
for _, m := range []string{http.MethodPut, "MKCOL"} {
|
||||
if !allowed[m] {
|
||||
t.Errorf("expected %s in Allow for missing path, got %v", m, allowed)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("restricted path does not advertise creation methods", func(t *testing.T) {
|
||||
path := "/public/not-viewable.txt"
|
||||
req := httptest.NewRequest(http.MethodOptions, "/dav/source1"+path, nil)
|
||||
req.SetPathValue("source", "source1")
|
||||
req.SetPathValue("path", path)
|
||||
w := httptest.NewRecorder()
|
||||
status, err := webDAVHandler(w, req, &requestContext{User: fullAccessUser})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for restricted path, got status %d with Allow %q", status, w.Header().Get("Allow"))
|
||||
}
|
||||
if allow := w.Header().Get("Allow"); strings.Contains(allow, http.MethodPut) || strings.Contains(allow, "MKCOL") {
|
||||
t.Errorf("restricted path advertises creation methods: %q", allow)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("read-only user is not told writes are allowed", func(t *testing.T) {
|
||||
allowed := doOptions(t, readOnlyUser, "/public/readme.txt")
|
||||
for _, m := range []string{http.MethodPut, http.MethodDelete, "MOVE", "COPY", "MKCOL"} {
|
||||
if allowed[m] {
|
||||
t.Errorf("read-only user unexpectedly sees %s in Allow: %v", m, allowed)
|
||||
}
|
||||
}
|
||||
if !allowed["PROPFIND"] {
|
||||
t.Errorf("expected PROPFIND in Allow for read-only user, got %v", allowed)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("read-only user sees no read methods without download", func(t *testing.T) {
|
||||
allowed := doOptions(t, readOnlyUser, "/public/readme.txt")
|
||||
for _, m := range []string{http.MethodGet, http.MethodHead} {
|
||||
if allowed[m] {
|
||||
t.Errorf("read-only (no download) user unexpectedly sees %s in Allow: %v", m, allowed)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -54,7 +54,6 @@ func TestApplyUserDefaults_copiesUserDefaultsOntoUser(t *testing.T) {
|
||||
Models: boolPtr(false),
|
||||
},
|
||||
FileViewer: UserDefaultsFileViewer{
|
||||
EditorQuickSave: true,
|
||||
AutoplayMedia: boolPtr(false),
|
||||
DisableViewingExt: ".bat",
|
||||
DisableOnlyOfficeExt: ".x",
|
||||
@@ -109,7 +108,6 @@ func TestApplyUserDefaults_copiesUserDefaultsOntoUser(t *testing.T) {
|
||||
Realtime: true,
|
||||
},
|
||||
NonAdminEditable: users.NonAdminEditable{
|
||||
EditorQuickSave: true,
|
||||
HideSidebarFileActions: true,
|
||||
DisableQuickToggles: true,
|
||||
DisableSearchOptions: true,
|
||||
|
||||
@@ -957,7 +957,6 @@ func SetDefaults(generate bool) Settings {
|
||||
Models: boolPtr(true),
|
||||
},
|
||||
FileViewer: UserDefaultsFileViewer{
|
||||
EditorQuickSave: false,
|
||||
AutoplayMedia: boolPtr(true),
|
||||
DisableViewingExt: "",
|
||||
DisableOnlyOfficeExt: ".md .txt .pdf .html .xml",
|
||||
|
||||
@@ -52,7 +52,6 @@ type UserDefaultsPreviewEnforcement struct {
|
||||
}
|
||||
|
||||
type UserDefaultsFileViewerEnforcement struct {
|
||||
EditorQuickSave bool `json:"editorQuickSave,omitempty"`
|
||||
AutoplayMedia bool `json:"autoplayMedia,omitempty"`
|
||||
DisableViewingExt bool `json:"disableViewingExt,omitempty"`
|
||||
DisableOnlyOfficeExt bool `json:"disableOnlyOfficeExt,omitempty"`
|
||||
|
||||
@@ -388,7 +388,6 @@ type UserDefaultsPreview struct {
|
||||
|
||||
// UserDefaultsFileViewer holds file viewer/editor settings
|
||||
type UserDefaultsFileViewer struct {
|
||||
EditorQuickSave bool `json:"editorQuickSave"` // show quick save button in editor
|
||||
AutoplayMedia *bool `json:"autoplayMedia"` // autoplay media files in preview
|
||||
DisableViewingExt string `json:"disableViewingExt"` // comma separated list of file extensions to disable viewing for
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // list of file extensions to disable onlyoffice editor for
|
||||
|
||||
@@ -31,7 +31,6 @@ var userJSONFieldEnforcementPaths = map[string]string{
|
||||
"newFileTemplate": "listing.newFileTemplate",
|
||||
"disablePreviewExt": "preview.disablePreviewExt",
|
||||
"disableSearchOptions": "search.disableOptions",
|
||||
"editorQuickSave": "fileViewer.editorQuickSave",
|
||||
"preferEditorForMarkdown": "fileViewer.preferEditorForMarkdown",
|
||||
"debugOffice": "fileViewer.debugOffice",
|
||||
"disableViewingExt": "fileViewer.disableViewingExt",
|
||||
|
||||
@@ -50,7 +50,6 @@ func ProfileFromUser(u *users.User) UserProfile {
|
||||
},
|
||||
FileViewer: UserDefaultsFileViewer{
|
||||
AutoplayMedia: boolPtr(u.Preview.AutoplayMedia),
|
||||
EditorQuickSave: u.EditorQuickSave,
|
||||
DisableViewingExt: u.DisableViewingExt,
|
||||
DisableOnlyOfficeExt: u.DisableOnlyOfficeExt,
|
||||
PreferEditorForMarkdown: u.PreferEditorForMarkdown,
|
||||
@@ -130,7 +129,6 @@ func ExpandProfileIntoUser(u *users.User, p UserProfile) {
|
||||
u.Preview.Models = boolValueOrDefault(p.Preview.Models, true)
|
||||
u.DisablePreviewExt = p.Preview.DisablePreviewExt
|
||||
|
||||
u.EditorQuickSave = p.FileViewer.EditorQuickSave
|
||||
u.Preview.AutoplayMedia = boolValueOrDefault(p.FileViewer.AutoplayMedia, true)
|
||||
u.DisableViewingExt = p.FileViewer.DisableViewingExt
|
||||
u.DisableOnlyOfficeExt = p.FileViewer.DisableOnlyOfficeExt
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
@@ -22,12 +23,23 @@ func ExpandBackendScopesForCreateUserDir(u *users.User) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if source.Config.CreateUserDir && scopeBaseName(scope.Scope) != cleanedUserName {
|
||||
u.BackendScopes[i].Scope = joinUnixScopePath(scope.Scope, cleanedUserName)
|
||||
if !source.Config.CreateUserDir {
|
||||
continue
|
||||
}
|
||||
// This runs more than once for a new user, so a scope that already ends in the user's
|
||||
// directory is left alone. The source's defaultUserScope itself is not a user's home:
|
||||
// a user named after its last element ("users" with "/users") gets "/users/users".
|
||||
if scopeBaseName(scope.Scope) == cleanedUserName && !sameScopePath(scope.Scope, source.Config.DefaultUserScope) {
|
||||
continue
|
||||
}
|
||||
u.BackendScopes[i].Scope = joinUnixScopePath(scope.Scope, cleanedUserName)
|
||||
}
|
||||
}
|
||||
|
||||
func sameScopePath(a, b string) bool {
|
||||
return path.Clean("/"+a) == path.Clean("/"+b)
|
||||
}
|
||||
|
||||
func scopeBaseName(scope string) string {
|
||||
scope = strings.TrimSuffix(scope, "/")
|
||||
if scope == "" || scope == "/" {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
|
||||
)
|
||||
|
||||
func TestExpandBackendScopesForCreateUserDirNeverKeepsDefaultScope(t *testing.T) {
|
||||
saved := Config
|
||||
defer func() { Config = saved }()
|
||||
Config = Settings{Server: Server{SourceMap: map[string]*Source{
|
||||
"/srv": {Path: "/srv", Name: "main", Config: SourceConfig{CreateUserDir: true, DefaultUserScope: "/users"}},
|
||||
}}}
|
||||
cases := map[string]string{"bob": "/users/bob", "users": "/users/users", "first.last": "/users/first.last"}
|
||||
for name, want := range cases {
|
||||
u := &users.User{
|
||||
FrontendUser: users.FrontendUser{Username: name},
|
||||
BackendScopes: []users.BackendScope{{Path: "/srv", Scope: "/users"}},
|
||||
}
|
||||
ExpandBackendScopesForCreateUserDir(u)
|
||||
ExpandBackendScopesForCreateUserDir(u) // runs more than once per new user; must be idempotent
|
||||
if got := u.BackendScopes[0].Scope; got != want {
|
||||
t.Fatalf("%s: scope=%q want %q", name, got, want)
|
||||
}
|
||||
}
|
||||
// An admin-assigned scope that already ends in the username is kept.
|
||||
u := &users.User{
|
||||
FrontendUser: users.FrontendUser{Username: "bob"},
|
||||
BackendScopes: []users.BackendScope{{Path: "/srv", Scope: "/users/bob"}},
|
||||
}
|
||||
ExpandBackendScopesForCreateUserDir(u)
|
||||
if got := u.BackendScopes[0].Scope; got != "/users/bob" {
|
||||
t.Fatalf("explicit scope changed: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -6924,10 +6924,6 @@ const docTemplate = `{
|
||||
"description": "comma separated list of file extensions to disable viewing for",
|
||||
"type": "string"
|
||||
},
|
||||
"editorQuickSave": {
|
||||
"description": "show quick save button in editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"preferEditorForMarkdown": {
|
||||
"description": "prefer editor first for markdown files instead of the Markdown Viewer",
|
||||
"type": "boolean"
|
||||
@@ -7763,10 +7759,6 @@ const docTemplate = `{
|
||||
"description": "space separated list of file extensions to disable viewing for",
|
||||
"type": "string"
|
||||
},
|
||||
"editorQuickSave": {
|
||||
"description": "show quick save button in editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"effectiveToolAccess": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
@@ -8218,10 +8210,6 @@ const docTemplate = `{
|
||||
"description": "space separated list of file extensions to disable viewing for",
|
||||
"type": "string"
|
||||
},
|
||||
"editorQuickSave": {
|
||||
"description": "show quick save button in editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"effectiveToolAccess": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
|
||||
@@ -6913,10 +6913,6 @@
|
||||
"description": "comma separated list of file extensions to disable viewing for",
|
||||
"type": "string"
|
||||
},
|
||||
"editorQuickSave": {
|
||||
"description": "show quick save button in editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"preferEditorForMarkdown": {
|
||||
"description": "prefer editor first for markdown files instead of the Markdown Viewer",
|
||||
"type": "boolean"
|
||||
@@ -7752,10 +7748,6 @@
|
||||
"description": "space separated list of file extensions to disable viewing for",
|
||||
"type": "string"
|
||||
},
|
||||
"editorQuickSave": {
|
||||
"description": "show quick save button in editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"effectiveToolAccess": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
@@ -8207,10 +8199,6 @@
|
||||
"description": "space separated list of file extensions to disable viewing for",
|
||||
"type": "string"
|
||||
},
|
||||
"editorQuickSave": {
|
||||
"description": "show quick save button in editor",
|
||||
"type": "boolean"
|
||||
},
|
||||
"effectiveToolAccess": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
|
||||
@@ -1162,9 +1162,6 @@ definitions:
|
||||
disableViewingExt:
|
||||
description: comma separated list of file extensions to disable viewing for
|
||||
type: string
|
||||
editorQuickSave:
|
||||
description: show quick save button in editor
|
||||
type: boolean
|
||||
preferEditorForMarkdown:
|
||||
description: prefer editor first for markdown files instead of the Markdown
|
||||
Viewer
|
||||
@@ -1749,9 +1746,6 @@ definitions:
|
||||
disableViewingExt:
|
||||
description: space separated list of file extensions to disable viewing for
|
||||
type: string
|
||||
editorQuickSave:
|
||||
description: show quick save button in editor
|
||||
type: boolean
|
||||
effectiveToolAccess:
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
@@ -2081,9 +2075,6 @@ definitions:
|
||||
disableViewingExt:
|
||||
description: space separated list of file extensions to disable viewing for
|
||||
type: string
|
||||
editorQuickSave:
|
||||
description: show quick save button in editor
|
||||
type: boolean
|
||||
effectiveToolAccess:
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
|
||||
@@ -206,7 +206,6 @@ userDefaults: # optional signup/CLI defaults; per-us
|
||||
folder: true # show thumbnails for folders that have previewable contents
|
||||
models: true # show live thumbnails for 3D models files
|
||||
fileViewer:
|
||||
editorQuickSave: false # show quick save button in editor
|
||||
autoplayMedia: true # autoplay media files in preview
|
||||
disableViewingExt: "" # comma separated list of file extensions to disable viewing for
|
||||
disableOnlyOfficeExt: ".md .txt .pdf .html .xml" # list of file extensions to disable onlyoffice editor for
|
||||
|
||||
@@ -230,7 +230,6 @@
|
||||
<action v-if="showUnarchiveInOverflow" icon="folder_open" :label="$t('prompts.unarchive')" @action="showUnarchivePromptFromPreview" />
|
||||
<action v-if="showEditButton" icon="edit" :label="$t('general.edit')" @action="goToEdit" />
|
||||
<action v-if="showPreviewButton" icon="visibility" :label="$t('general.preview')" @action="goToPreview" />
|
||||
<action v-if="showSave" icon="save" :label="$t('general.save')" @action="save()" />
|
||||
<action v-if="showDelete" icon="delete" :label="$t('general.delete')" @action="showDeletePrompt" />
|
||||
</div>
|
||||
</transition>
|
||||
@@ -246,7 +245,6 @@ import {
|
||||
showHoverTooltip,
|
||||
} from "@/utils/tooltipHelp.js";
|
||||
import { url } from "@/utils";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { copyToClipboard } from "@/utils/clipboard";
|
||||
import { globalVars } from "@/utils/constants.js";
|
||||
import downloadFiles from "@/utils/download";
|
||||
@@ -505,7 +503,6 @@ export default {
|
||||
},
|
||||
hasOverflowItems() {
|
||||
return this.showDelete
|
||||
|| this.showSave
|
||||
|| this.showGoToRaw
|
||||
|| this.hasDownload
|
||||
|| this.showUnarchiveInOverflow
|
||||
@@ -556,10 +553,6 @@ export default {
|
||||
if (getters.currentView() !== "editor") return false;
|
||||
return isRichTextPreviewMimeType(state.req.type);
|
||||
},
|
||||
showSave() {
|
||||
const allowEdit = this.permissions.modify || (getters.isShare() && state.shareInfo.allowModify);
|
||||
return getters.currentView() === "editor" && allowEdit;
|
||||
},
|
||||
showOverflow() {
|
||||
return getters.currentPromptName() === "OverflowMenu";
|
||||
},
|
||||
@@ -1040,26 +1033,6 @@ export default {
|
||||
mutations.closeHovers();
|
||||
void this.$router.replace({ hash: "#preview" });
|
||||
},
|
||||
async save() {
|
||||
const button = "save";
|
||||
buttons.loading("save");
|
||||
try {
|
||||
// Call the editor save handler directly and await completion
|
||||
if (state.editor.saveHandler) {
|
||||
await state.editor.saveHandler();
|
||||
buttons.success(button);
|
||||
} else {
|
||||
const errorMsg = "No editor save handler registered";
|
||||
notify.showError(errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
} catch (e) {
|
||||
buttons.done(button);
|
||||
mutations.closeHovers();
|
||||
throw e;
|
||||
}
|
||||
mutations.closeHovers();
|
||||
},
|
||||
showUploadPrompt() {
|
||||
mutations.closeHovers();
|
||||
let targetPath = state.req.path;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -291,7 +291,6 @@ const PROFILE_SNAPSHOT_FIELDS = [
|
||||
"showTypeColumn",
|
||||
"showCreationDateColumn",
|
||||
"hideFilesInTree",
|
||||
"editorQuickSave",
|
||||
"showSelectMultiple",
|
||||
"debugOffice",
|
||||
"viewMode",
|
||||
|
||||
@@ -27,12 +27,6 @@
|
||||
:name="$t('profileSettings.disableHideSidebar')"
|
||||
:description="$t('profileSettings.disableHideSidebarDescription')"
|
||||
/>
|
||||
<ProfilePreferenceToggle
|
||||
field="editorQuickSave"
|
||||
section="fileViewer"
|
||||
:name="$t('profileSettings.editorQuickSave')"
|
||||
:description="$t('profileSettings.editorQuickSaveDescription')"
|
||||
/>
|
||||
<ToggleSwitch
|
||||
class="item"
|
||||
:enforceable="enforceable"
|
||||
@@ -511,12 +505,6 @@
|
||||
:name="$t('profileSettings.autoplayMedia')"
|
||||
:description="helpText('fileViewer', 'autoplayMedia', $t('profileSettings.autoplayMediaDescription'))"
|
||||
/>
|
||||
<ProfilePreferenceToggle
|
||||
field="editorQuickSave"
|
||||
section="fileViewer"
|
||||
:name="$t('profileSettings.editorQuickSave')"
|
||||
:description="$t('profileSettings.editorQuickSaveDescription')"
|
||||
/>
|
||||
<ProfilePreferenceToggle
|
||||
field="preferEditorForMarkdown"
|
||||
section="fileViewer"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -800,8 +801,6 @@
|
||||
"defaultGallerySize": "الحجم الافتراضي للصورة المصغرة",
|
||||
"defaultGallerySizeDescription": "مقياس الصور المصغرة الأولي للمعرض أو القائمة (1–9) في المتصفحات أو الأجهزة الجديدة. لا يلغي الأحجام المحفوظة مسبقًا محليًّا.",
|
||||
"disableHideSidebar": "إظهار الشريط الجانبي دائمًا",
|
||||
"editorQuickSave": "إظهار زر الحفظ السريع في المحرر",
|
||||
"editorQuickSaveDescription": "سيعرض محرر النصوص رمز الحفظ فقط، مع إخفاء أي إجراءات أخرى",
|
||||
"showAdvancedProfile": "إظهار إعدادات الملف الشخصي المتقدمة",
|
||||
"showAdvancedProfileDescription": "اعرض إعدادات الملف الشخصي المصنفة في الشريط الجانبي بدلاً من عرض الملف الشخصي المبسط.",
|
||||
"accountOptions": "خيارات الحساب"
|
||||
|
||||
@@ -292,6 +292,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -605,8 +606,6 @@
|
||||
"defaultGallerySize": "Стандартен размер на миниатюрите",
|
||||
"defaultGallerySizeDescription": "Начален мащаб на миниатюрите в галерията или списъка (1–9) при нови браузъри или устройства. Не отменя размерите, които вече са запазени локално.",
|
||||
"disableHideSidebar": "Винаги показвай страничната лента",
|
||||
"editorQuickSave": "Показване на бутона за бързо запаметяване в редактора",
|
||||
"editorQuickSaveDescription": "Текстовият редактор ще показва само иконата за запазване, като скрива всички други действия",
|
||||
"showAdvancedProfile": "Покажи разширени настройки на профила",
|
||||
"showAdvancedProfileDescription": "Показване на настройките на профила по категории в страничната лента вместо опростения изглед на профила.",
|
||||
"accountOptions": "Опции за профила"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -432,8 +433,6 @@
|
||||
"defaultGallerySize": "Výchozí velikost miniatury",
|
||||
"defaultGallerySizeDescription": "Výchozí měřítko miniatur v galerii nebo seznamu (1–9) v nových prohlížečích nebo na nových zařízeních. Nepřepíše velikosti, které jsou již uloženy lokálně.",
|
||||
"disableHideSidebar": "Vždy zobrazovat postranní panel",
|
||||
"editorQuickSave": "Zobrazit tlačítko pro rychlé uložení v editoru",
|
||||
"editorQuickSaveDescription": "Textový editor zobrazí pouze ikonu pro uložení a skryje všechny ostatní akce",
|
||||
"showAdvancedProfile": "Zobrazit pokročilá nastavení profilu",
|
||||
"showAdvancedProfileDescription": "Zobrazit nastavení profilu rozdělená do kategorií v postranním panelu namísto zjednodušeného zobrazení profilu.",
|
||||
"accountOptions": "Možnosti účtu"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "Standardgröße der Miniaturansicht",
|
||||
"defaultGallerySizeDescription": "Anfängliche Skalierung der Miniaturansichten in Galerien oder Listen (1–9) bei neuen Browsern oder Geräten. Überschreibt keine bereits lokal gespeicherten Größen.",
|
||||
"disableHideSidebar": "Seitenleiste immer anzeigen",
|
||||
"editorQuickSave": "Schaltfläche „Schnellspeichern“ im Editor anzeigen",
|
||||
"editorQuickSaveDescription": "Der Texteditor zeigt nur das Speichersymbol an und blendet alle anderen Funktionen aus.",
|
||||
"showAdvancedProfile": "Erweiterte Profileinstellungen anzeigen",
|
||||
"showAdvancedProfileDescription": "Zeige die nach Kategorien geordneten Profileinstellungen in der Seitenleiste anstelle der vereinfachten Profilansicht an.",
|
||||
"accountOptions": "Kontooptionen"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "Προεπιλεγμένο μέγεθος μικρογραφίας",
|
||||
"defaultGallerySizeDescription": "Αρχική κλίμακα μικρογραφιών της γκαλερί ή της λίστας (1–9) σε νέους περιηγητές ή συσκευές. Δεν αντικαθιστά τα μεγέθη που έχουν ήδη αποθηκευτεί τοπικά.",
|
||||
"disableHideSidebar": "Να εμφανίζεται πάντα η πλευρική στήλη",
|
||||
"editorQuickSave": "Εμφάνιση του κουμπιού γρήγορης αποθήκευσης στον επεξεργαστή",
|
||||
"editorQuickSaveDescription": "Ο επεξεργαστής κειμένου θα εμφανίζει μόνο το εικονίδιο αποθήκευσης, κρύβοντας όλες τις άλλες ενέργειες",
|
||||
"showAdvancedProfile": "Εμφάνιση προηγμένων ρυθμίσεων προφίλ",
|
||||
"showAdvancedProfileDescription": "Εμφάνιση των ρυθμίσεων προφίλ ανά κατηγορία στην πλευρική στήλη αντί της απλοποιημένης προβολής προφίλ.",
|
||||
"accountOptions": "Επιλογές λογαριασμού"
|
||||
|
||||
@@ -292,6 +292,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -604,8 +605,6 @@
|
||||
"showCreationDateColumnDescription": "Show the creation date column in list and compact views in the main listing.",
|
||||
"deleteWithoutConfirming": "Delete files without confirmation prompt",
|
||||
"deleteWithoutConfirmingDescription": "Delete files without confirmation prompt. This will only work if the file is a media file.",
|
||||
"editorQuickSave": "Show quick save button in editor",
|
||||
"editorQuickSaveDescription": "The text editor will only show the save icon, hiding any other actions",
|
||||
"previewOfficeDescription": "Show thumbnails for office file types in the listing view",
|
||||
"disableThumbnailPreviews": "Disable thumbnails for certain file extensions",
|
||||
"disableThumbnailPreviewsDescription": "A space-separated list of file extensions to disable thumbnail previews for (e.g., '.txt .html').",
|
||||
|
||||
+86
-87
@@ -38,7 +38,7 @@
|
||||
"internal": "Error interno :(",
|
||||
"notFound": "No se encontró el directorio",
|
||||
"connection": "No se puede conectar al servidor.",
|
||||
"shareNotFound": "No pudimos encontrar lo que buscas, quizá enlace expiró",
|
||||
"shareNotFound": "No pudimos encontrar lo que buscas, quizá el enlace expiró",
|
||||
"failedToConnectToServer": "No se pudo conectar con el servidor",
|
||||
"requestTimedOut": "La solicitud ha superado el tiempo de espera. Comprueba la configuración de tu red o del proxy inverso."
|
||||
},
|
||||
@@ -46,7 +46,7 @@
|
||||
"sortByCreationTime": "Ordenar por fecha de creación",
|
||||
"creationTime": "Fecha de creación",
|
||||
"emptyFile": "Archivo vacío",
|
||||
"lastModified": "Última modificación",
|
||||
"lastModified": "Última modificación{suffix}",
|
||||
"multipleSelectionEnabled": "Selección múltiple activada",
|
||||
"sortByLastModified": "Ordenar por fecha de modificación",
|
||||
"sortByName": "Ordenar por nombre",
|
||||
@@ -85,7 +85,7 @@
|
||||
"click": "Selecciona varios archivos o carpetas"
|
||||
},
|
||||
"del": "Eliminar elementos seleccionados",
|
||||
"esc": "Cancela la selección o cierra el archivo actual",
|
||||
"esc": "Cancela la selección o cierra la ventana",
|
||||
"f4": "actualizar las listas y metadata",
|
||||
"f2": "Cambiar nombre del archivo o carpeta",
|
||||
"f1": "Abrir ventana de ayuda",
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -170,7 +171,7 @@
|
||||
"newFileMessage": "Nombre del archivo:",
|
||||
"replaceMessage": "Un archivo con el mismo nombre ya existe. ¿Quieres cambiar el nombre?\n",
|
||||
"optionalPassword": "Contraseña (opcional):",
|
||||
"deleteUserMessage": "¿Estas seguro que quieres borrar este usuario?",
|
||||
"deleteUserMessage": "¿Estás seguro de que quieres borrar este usuario?",
|
||||
"deleted": "¡Borrado con éxito!",
|
||||
"destinationSource": "Destino:",
|
||||
"downloadsLimit": "Límite de descargas",
|
||||
@@ -203,12 +204,12 @@
|
||||
"copySuccess": "¡Archivos copiados correctamente!",
|
||||
"operationInProgress": "Operación en curso, espera un momento...",
|
||||
"saveBeforeExit": "Cambios sin guardar",
|
||||
"saveBeforeExitMessage": "Tienes cambios sin guardar. ¿Que quieres hacer?",
|
||||
"saveBeforeExitMessage": "Tienes cambios sin guardar. ¿Qué quieres hacer?",
|
||||
"copyPasteConfirm": "Confirmar",
|
||||
"copyItemQuestion": "¿Quieres copiar este archivo a la nueva ubicación?",
|
||||
"moveItemQuestion": "¿Quieres mover este archivo a la nueva ubicación?",
|
||||
"destinationPath": "Nueva ubicación:",
|
||||
"closeWithActiveUploadsMessage": "Si cierras esta ventana, puede la subida de archivos sea interrumpida, también que algunos queden incompletos. ¿Que quieres hacer?",
|
||||
"closeWithActiveUploadsMessage": "Si cierras esta ventana, puede que la subida de archivos se interrumpa y que algunos queden incompletos. ¿Qué quieres hacer?",
|
||||
"closeWithActiveUploads": "Subida de archivos activa",
|
||||
"basicInfo": "Información básica",
|
||||
"additionalInfo": "Información adicional",
|
||||
@@ -234,23 +235,23 @@
|
||||
"renameFailed": "Error al cambiar de nombre",
|
||||
"archive": "Comprimir",
|
||||
"archiveMessage": "Crea un archivo comprimido en el servidor con los elementos que hayas seleccionado",
|
||||
"archiveDestination": "Elije la carpeta de destino:",
|
||||
"archiveDestination": "Elige la carpeta de destino:",
|
||||
"archiveName": "Nombre del archivo comprimido (por defecto: 'archive'):",
|
||||
"archiveCompression": "Nivel de compresión (0-9):",
|
||||
"archiveSuccess": "¡Se creó el archivo comprimido!",
|
||||
"unarchive": "Extraer",
|
||||
"unarchiveMessage": "Elije dónde quieres extraer el archivo:",
|
||||
"unarchiveMessage": "Elige dónde quieres extraer el archivo:",
|
||||
"unarchiveDestination": "Extraer a:",
|
||||
"unarchiveSuccess": "¡Archivo extraído con éxito!",
|
||||
"zipFilenameEncoding": "Filename encoding",
|
||||
"zipEncodingLoading": "Checking ZIP filenames…",
|
||||
"zipEncodingError": "Could not read the ZIP filename preview.",
|
||||
"zipEncodingRetry": "Retry preview",
|
||||
"zipEncodingChoose": "Choose an encoding",
|
||||
"zipEncodingSuggested": "suggested",
|
||||
"zipEncodingHint": "Check the filenames below before extracting. Suggestions may be incorrect; ZIP Unicode metadata takes precedence.",
|
||||
"zipFilenamePreview": "Filename preview",
|
||||
"zipEncodingUnavailable": "No supported encoding can decode all filenames in this ZIP.",
|
||||
"zipFilenameEncoding": "Codificación de los nombres de archivo",
|
||||
"zipEncodingLoading": "Comprobando los nombres de archivo del ZIP…",
|
||||
"zipEncodingError": "No se pudo leer la vista previa de los nombres de archivo del ZIP.",
|
||||
"zipEncodingRetry": "Reintentar vista previa",
|
||||
"zipEncodingChoose": "Elige una codificación",
|
||||
"zipEncodingSuggested": "sugerida",
|
||||
"zipEncodingHint": "Revisa los nombres de archivo de abajo antes de extraer. Las sugerencias pueden ser incorrectas; los metadatos Unicode del ZIP tienen prioridad.",
|
||||
"zipFilenamePreview": "Vista previa de los nombres de archivo",
|
||||
"zipEncodingUnavailable": "Ninguna codificación compatible puede decodificar todos los nombres de archivo de este ZIP.",
|
||||
"downloadFailed": "Fallo en la descarga por trozos. Si se repite, intente reducir el tamaño de los trozos: {message}",
|
||||
"confirmPasswordToSaveUser": "Introduzca su contraseña actual para guardar estos cambios de usuario."
|
||||
},
|
||||
@@ -269,7 +270,7 @@
|
||||
"noResults": "No se han encontrado resultados.",
|
||||
"helpText1": "La búsqueda se produce en cada carácter que escriba ({minSearchLength} carácter mínimo).",
|
||||
"helpText2": "Coincidencia de subcadenas sólo en nombres de archivos o carpetas (no en rutas completas). case:exact para coincidencia sensible a mayúsculas y minúsculas.\n\n para OR entre términos utilice el carácter pipa; comillas dobles para una frase.\n\nHabilite la búsqueda con comodines para opciones más avanzadas.",
|
||||
"useWildcardSearch": "Búsqueda con comodín (distingue mayúsculas de minúsculas)",
|
||||
"useWildcardSearch": "Búsqueda con comodines",
|
||||
"useWildcardSearchDescription": "Opciones de comodín admitidas:\n\n- Coinciden con cualquier secuencia: *\n\n- Coincidir con un carácter: ?\n\n- Clase de caracteres: [a-z], [0-9]\n\n- Negación: [^0-9]\n\n- Los caracteres *, ? o [ deben ir entre paréntesis, por ejemplo [*], [?], [[].\n\n- Todos los resultados distinguen entre mayúsculas y minúsculas",
|
||||
"openInAdvancedSearch": "Abrir en búsqueda avanzada",
|
||||
"caseExact": "Coincidencia que distingue entre mayúsculas y minúsculas",
|
||||
@@ -279,7 +280,7 @@
|
||||
},
|
||||
"settings": {
|
||||
"scopes": "Fuentes a las que el usuario tendrá acceso:",
|
||||
"avoidChanges": "(dejalo en blanco para evitar cambios)",
|
||||
"avoidChanges": "marca también 'cambiar contraseña' para aplicar esta contraseña",
|
||||
"lockPassword": "Impedir que el usuario cambie su contraseña",
|
||||
"permissions": {
|
||||
"admin": "Administrador",
|
||||
@@ -294,7 +295,7 @@
|
||||
"modifyOtherUserTitle": "Modificar usuario: {username}",
|
||||
"themeColor": "Color de acento",
|
||||
"listingOptions": "Opciones de listado",
|
||||
"emptyGlobal": "Actualmente no hay configuracion global, edita el archivo YAML para establecer las configuraciones del sistema",
|
||||
"emptyGlobal": "Actualmente no hay configuración global, edita el archivo YAML para establecer las configuraciones del sistema",
|
||||
"userNotAdmin": "El usuario no es administrador",
|
||||
"enterPassword": "Escribe una contraseña",
|
||||
"enterPasswordAgain": "Vuelva a escribir la contraseña",
|
||||
@@ -302,7 +303,7 @@
|
||||
"loginMethods": {
|
||||
"password": "Contraseña"
|
||||
},
|
||||
"searchOptions": "Opciones de busqueda",
|
||||
"searchOptions": "Opciones de búsqueda",
|
||||
"systemAdmin": "Sistema y administración",
|
||||
"configViewerShowFull": "Mostrar configuración completa",
|
||||
"configViewerShowComments": "Mostrar comentarios",
|
||||
@@ -345,9 +346,9 @@
|
||||
"selectLinkType": "Selecciona un tipo de acceso directo",
|
||||
"customLink": "Acceso directo personalizado",
|
||||
"selectSource": "Seleccionar Fuente",
|
||||
"chooseSource": "Elije una fuente de archivos",
|
||||
"chooseSource": "Elige una fuente de archivos",
|
||||
"selectTool": "Selecciona una herramienta",
|
||||
"chooseTool": "Elije una herramienta",
|
||||
"chooseTool": "Elige una herramienta",
|
||||
"linkName": "Nombre del acceso directo",
|
||||
"linkNamePlaceholder": "Introduzca el nombre del acceso directo",
|
||||
"linkUrl": "Enlace",
|
||||
@@ -359,7 +360,7 @@
|
||||
"selectShare": "Seleccionar recurso compartido",
|
||||
"chooseShare": "Seleccionar recurso compartido",
|
||||
"browseIcons": "Material Icons",
|
||||
"pickIcon": "Elije un icono",
|
||||
"pickIcon": "Elige un icono",
|
||||
"pickIconDescription": "Aquí puedes explorar los iconos más populares de la biblioteca \"Material Icons\"",
|
||||
"linkIconPlaceholder": "Opcionalmente, puedes elegir un Material Icon que más te guste",
|
||||
"showIndexedUsageDescription": "Muestra la barra de uso a partir de los archivos indexados en la fuente.",
|
||||
@@ -380,22 +381,22 @@
|
||||
"rootFilesystemOnlyDescription": "Cuando está activada, solo se incluye en el informe el sistema de archivos que contiene la ruta de origen. Cuando está desactivada (por defecto), el total del disco también incluye los sistemas de archivos montados bajo la ruta de origen, y los conjuntos de datos compartidos, como los de ZFS, se cuentan una sola vez."
|
||||
},
|
||||
"share": {
|
||||
"notice": "Cuando se crea un recurso compartido, este quedará más expuesto al público y tendrá diferentes reglas de acceso. Si no está configurado de forma forma segura, cualquiera podrá acceder a él.",
|
||||
"notice": "Cuando se crea un recurso compartido, este quedará más expuesto al público y tendrá diferentes reglas de acceso. Si no está configurado de forma segura, cualquiera podrá acceder a él.",
|
||||
"disableAnonymousDescription": "Cuando está activado, solo los usuarios registrados podrán acceder al recurso compartido.",
|
||||
"disableThumbnailsDescription": "Las miniaturas de archivos, fotos, audios, videos, etc. No estarán activadas.",
|
||||
"enableAllowedUsernamesDescription": "Cuando está activado, sólo los usuarios que especifiques podrán acceder al recurso compartido. El usuario deberá tener acceso al origen del recurso compartido para poder entrar",
|
||||
"downloadsLimitDescription": "Número máximo de veces que se puede descargar un archivo/carpeta del recurso compartido - Dejar en blanco para ilimitado.",
|
||||
"maxBandwidthDescription": "Velocidad máxima de descarga en kilobytes por segundo (kb/s) - Dejar en blanco para ilimitado.",
|
||||
"shareThemeDescription": "El tema que se usará para los usuarios que accedan al recurso compartidos",
|
||||
"shareThemeDescription": "El tema que se usará para los usuarios que accedan al recurso compartido",
|
||||
"disableAnonymous": "Desactivar el acceso anónimo",
|
||||
"disableThumbnails": "Desactivar miniaturas",
|
||||
"enableAllowedUsernames": "Compartir sólo con usuarios registrados",
|
||||
"enableAllowedUsernames": "Compartir solo con determinados usuarios",
|
||||
"allowedUsernamesPlaceholder": "Nombres de usuario (separado por comas)",
|
||||
"keepAfterExpiration": "No eliminar el recurso compartido una vez expirado",
|
||||
"keepAfterExpirationDescription": "Los recursos compartidos no se borraran cuando expiren. Es útil si después quieres ampliar la duración del recurso, o editarlo.",
|
||||
"keepAfterExpirationDescription": "Los recursos compartidos no se borrarán cuando expiren. Es útil si después quieres ampliar la duración del recurso, o editarlo.",
|
||||
"shareDurationDescription": "Tiempo que durará el recurso compartido - puedes dejarlo en blanco para indefinido (o sea, sin expiración).",
|
||||
"passwordDescription": "Contraseña opcional necesaria para acceder al recurso compartido, incluyedo usuarios registrados",
|
||||
"shareThemeColorDescription": "Valor de color CSS aplicado al tema de la acción (por ejemplo: red, #0ea5e9, rgb(14,165,233)).",
|
||||
"passwordDescription": "Contraseña opcional necesaria para acceder al recurso compartido, incluyendo usuarios registrados",
|
||||
"shareThemeColorDescription": "Valor de color CSS aplicado al tema del recurso compartido (por ejemplo: red, #0ea5e9, rgb(14,165,233)).",
|
||||
"shareTitleDescription": "Título de la página que se mostrará en el navegador",
|
||||
"shareDescriptionHelp": "Breve descripción mostrada en la página (que suele aparecer cuando compartes el enlace)",
|
||||
"shareBannerDescription": "Enlace (URL), o ruta de la imagen del banner que se mostrará en el enlace -- por ejemplo, 'https://ejemplo.com/banner.png' o '/ruta/a/banner.png'.",
|
||||
@@ -422,7 +423,7 @@
|
||||
"shareTypeDescription": "El tipo de recurso compartido que se va a crear. Los recursos compartidos normales permiten ver y descargar archivos. Los recursos compartidos de subida permiten subir archivos al recurso compartido.",
|
||||
"normalShare": "Normal",
|
||||
"uploadShare": "Solo subir archivos",
|
||||
"perUserDownloadLimit": "Límitar descargas para cada usuario",
|
||||
"perUserDownloadLimit": "Limitar descargas para cada usuario",
|
||||
"perUserDownloadLimitDescription": "Cuando se activa, el límite de descargas se aplicará a cada usuario. Los usuarios anónimos no podrán descargar ningún archivo de este recurso compartido.",
|
||||
"descriptionUploadDefault": "Te han compartido un enlace para que subas archivos",
|
||||
"extractEmbeddedSubtitles": "Extraer subtítulos incrustados",
|
||||
@@ -469,7 +470,7 @@
|
||||
"minimalInfo": "Excluye las personalizaciones de token, los permisos coincidirán con el usuario asociado y no tendrán otros atributos.",
|
||||
"createTitle": "Crear Token",
|
||||
"keyNamePlaceholder": "Escribe un nombre único para el Token",
|
||||
"customizeTokenInfo": "Cuando activas esta opción podrás personalizar los permisos y atributos del Token, esto hará el Token más largo y puede que no funcione con algunos clientes WebDAV."
|
||||
"customizeTokenInfo": "Personaliza los permisos globales del token (administrador, API, compartir, tiempo real). Los permisos de archivos siempre provienen de la cuenta de usuario asociada en cada fuente."
|
||||
},
|
||||
"index": {
|
||||
"assessment": "Tipo",
|
||||
@@ -493,7 +494,7 @@
|
||||
"otp": {
|
||||
"name": "Autenticación de dos factores (2FA)",
|
||||
"verifyInstructions": "Ingresa el código de autenticación",
|
||||
"generate": "Escanear el código QR con tú app de autenticación",
|
||||
"generate": "Escanea el código QR con tu app de autenticación",
|
||||
"codeInputPlaceholder": "Código de 6 dígitos",
|
||||
"invalidCodeType": "Código inválido",
|
||||
"verificationFailed": "No se pudo autenticar",
|
||||
@@ -504,19 +505,19 @@
|
||||
"general": {
|
||||
"import": "Importar",
|
||||
"export": "Exportar",
|
||||
"name": "Nombre",
|
||||
"path": "Ruta:",
|
||||
"user": "Usuario",
|
||||
"group": "Grupo",
|
||||
"signup": "Crear cuenta",
|
||||
"password": "Contraseña",
|
||||
"username": "Nombre de usuario",
|
||||
"files": "Archivos",
|
||||
"folders": "Carpetas",
|
||||
"exit": "Salir",
|
||||
"permanent": "Permanente",
|
||||
"hash": "Hash",
|
||||
"of": "de",
|
||||
"name": "Nombre{suffix}",
|
||||
"path": "Ruta{suffix}",
|
||||
"user": "Usuario{suffix}",
|
||||
"group": "Grupo{suffix}",
|
||||
"signup": "Crear cuenta{suffix}",
|
||||
"password": "Contraseña{suffix}",
|
||||
"username": "Nombre de usuario{suffix}",
|
||||
"files": "Archivos{suffix}",
|
||||
"folders": "Carpetas{suffix}",
|
||||
"exit": "Salir{suffix}",
|
||||
"permanent": "Permanente{suffix}",
|
||||
"hash": "Hash{suffix}",
|
||||
"of": "de{suffix}",
|
||||
"percentage": "{percentage}%",
|
||||
"type": "Tipo{suffix}",
|
||||
"size": "Tamaño{suffix}",
|
||||
@@ -592,7 +593,7 @@
|
||||
"lastUpdate": "Última actualización{suffix}",
|
||||
"latency": "Latencia{suffix}",
|
||||
"all": "Todos{suffix}",
|
||||
"acknowledge": "Acuse recibo{suffix}",
|
||||
"acknowledge": "Entendido{suffix}",
|
||||
"expiration": "Caducidad{suffix}",
|
||||
"zoom": "Zoom{suffix}",
|
||||
"background": "Fondo{suffix}",
|
||||
@@ -623,7 +624,7 @@
|
||||
"created": "Creado{suffix}",
|
||||
"actions": "Acciones{suffix}",
|
||||
"enabled": "Activado{suffix}",
|
||||
"disabled": "Discapacitado{suffix}",
|
||||
"disabled": "Desactivado{suffix}",
|
||||
"permissions": "Permisos{suffix}",
|
||||
"management": "Gestión{suffix}",
|
||||
"profile": "Perfil{suffix}",
|
||||
@@ -681,7 +682,7 @@
|
||||
"cascadeDelete": "Eliminación en cascada",
|
||||
"cascadeDeleteDescription": "Si eliminas una regla de acceso con esta opción, también la eliminará para todo el contenido (archivos y carpetas), lo cual nos permite eliminar por completo un usuario o grupo de todo el árbol de directorios",
|
||||
"hasRules": "Normas de acceso configuradas",
|
||||
"manageDescription": "Normas de libre acceso para esta ruta.",
|
||||
"manageDescription": "Abrir las reglas de acceso de esta ruta.",
|
||||
"groups": "Grupos",
|
||||
"newGroup": "Nuevo grupo",
|
||||
"editGroup": "Editar grupo",
|
||||
@@ -694,7 +695,7 @@
|
||||
"membersCount": "Miembros: {count}",
|
||||
"userGroupsCount": "Grupos: {count}",
|
||||
"groupNameRequired": "Es obligatorio indicar el nombre del grupo.",
|
||||
"groupNameTooShort": "Group name must be at least {min} characters.",
|
||||
"groupNameTooShort": "El nombre del grupo debe tener al menos {min} caracteres.",
|
||||
"groupNameTooLong": "El nombre del grupo debe tener un máximo de {max} caracteres.",
|
||||
"groupNameInvalid": "El nombre del grupo no debe contener caracteres de control.",
|
||||
"groupExists": "Ya existe un grupo con este nombre.",
|
||||
@@ -703,14 +704,14 @@
|
||||
"selectUsers": "Seleccionar usuarios",
|
||||
"selectGroup": "Seleccionar grupo",
|
||||
"selectGroups": "Seleccionar grupos",
|
||||
"refineSearch": "Se muestran los primeros resultados de « {count} ». Refina tu búsqueda para ver más."
|
||||
"refineSearch": "Se muestran las primeras {count} coincidencias. Refina tu búsqueda para ver más."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Subida y Descarga",
|
||||
"maxConcurrentUpload": "Archivos a subir a la vez",
|
||||
"maxConcurrentUploadHelp": "El número máximo de archivos a subir a la vez -- Por ejemplo si el valor es 5 y subes 10 archivos, subirá los archivos de 5 en 5. Los valores más altos suelen ser mejores, pero depende de varios factores",
|
||||
"uploadChunkSizeMb": "Velocidad de subida (en MB)",
|
||||
"uploadChunkSizeMbHelp": "Subida de archivos fragmentada o \"Chunked uploads\" en inglés - Lo que está opción hace es controlar la velocidad de subida en la que los fragmentos de archivo se subirán.",
|
||||
"uploadChunkSizeMb": "Tamaño del fragmento de subida en MB",
|
||||
"uploadChunkSizeMbHelp": "Normalmente entre 5 y 50 MB; un tamaño menor es mejor para conexiones inestables o limitadas. Con 0 se desactiva la subida fragmentada.",
|
||||
"clearAll": "\"Limpiar completados\" debe borrar todo",
|
||||
"clearAllDescription": "Hará que el botón de \"Limpiar completados\" en la ventana de subir archivos borre todos los estados: Finalizado, error, pausa y conflicto.",
|
||||
"uploadSettings": "Opciones de subida",
|
||||
@@ -733,15 +734,15 @@
|
||||
"promptRightCloseButton": "Mostrar botón de cerrar en el lado derecho de las ventanas",
|
||||
"promptRightCloseButtonDescription": "Mostrar el botón de cerrar en el lado derecho de las ventanas en vez del lado izquierdo",
|
||||
"hideFileExt": "Ocultar ciertos archivos (por extensión)",
|
||||
"hideFileExtDescription": "Una lista de extensiones de archivo separadas por espacios. Las extensiones que agreges aquí se ocultarán de toda la interfaz (visualmente, como en la lista o en la navegación de árbol) — Los archivos siguen siendo accesibles.",
|
||||
"hideFileExtDescription": "Una lista de extensiones de archivo separadas por espacios. Las extensiones que agregues aquí se ocultarán de toda la interfaz (visualmente, como en la lista o en la navegación de árbol). Los archivos siguen siendo accesibles.",
|
||||
"preferEditorForMarkdown": "Preferir editor para archivos Markdown",
|
||||
"preferEditorForMarkdownDescription": "Cuando está activado, los archivos markdown abrirán el editor en lugar del visor de Markdown",
|
||||
"deleteAfterArchive": "Eliminar archivos originales después de la creación/extracción de archivos",
|
||||
"deleteAfterArchiveDescription": "Cuando está activado, eliminará los archivos originales después de la creación o extracción de archivos",
|
||||
"hideFilesInTree": "Ocultar archivos en la navegación de árbol",
|
||||
"hideFilesInTreeDescription": "Cuando está activado, solo se muestrarán carpetas en la navegación del panel lateral",
|
||||
"hideFilesInTreeDescription": "Cuando está activado, solo se mostrarán carpetas en la navegación del panel lateral",
|
||||
"autoplayMedia": "Reproducción automática de archivos multimedia",
|
||||
"autoplayMediaDescription": "Reproduce automaticamente los archivos multimedia al abrirlos",
|
||||
"autoplayMediaDescription": "Reproduce automáticamente los archivos multimedia al abrirlos",
|
||||
"sidebarOptions": "Opciones del panel lateral",
|
||||
"previewOfficeDescription": "Mostrar miniaturas para los archivos de Office",
|
||||
"disableThumbnailPreviews": "Desactivar miniaturas para las siguientes extensiones:",
|
||||
@@ -763,9 +764,9 @@
|
||||
"disableHideSidebarDescription": "Si está activado, el panel lateral no se ocultará automáticamente al visualizar o editar archivos.",
|
||||
"disableQuickToggles": "Desactivar ajustes rápidos",
|
||||
"disableQuickTogglesDescription": "Desactiva los ajustes rápidos que aparecen en el panel lateral. (cambiar tema, visibilidad del panel y modo de un solo click)",
|
||||
"disableSearchOptions": "Desactivar las opciones de busqueda",
|
||||
"disableSearchOptions": "Desactivar las opciones de búsqueda",
|
||||
"disableFileExtensions": "Escribe aquí las extensiones para desactivar",
|
||||
"disableSearchOptionsDescription": "Desactivar las opciones de busqueda que aparecen en el panel.",
|
||||
"disableSearchOptionsDescription": "Desactivar las opciones de búsqueda que aparecen en el panel.",
|
||||
"hideSidebarFileActions": "Ocultar el botón de acciones de archivo en el panel",
|
||||
"deleteWithoutConfirming": "Eliminar archivos sin mensaje de confirmación",
|
||||
"deleteWithoutConfirmingDescription": "Eliminar archivos sin mensaje de confirmación. Esto sólo funcionará si el archivo es un archivo multimedia.",
|
||||
@@ -782,7 +783,7 @@
|
||||
"thumbnailOptions": "Opciones de miniaturas",
|
||||
"showThumbnails": "Mostrar miniaturas soportadas",
|
||||
"showThumbnailsDescription": "Cuando está activada, se mostrarán miniaturas para todos los archivos soportados. Puedes desactivarlo para desactivar todas las miniaturas.",
|
||||
"showCopyPath": "Mostrar ruta de copia en el menú contextual",
|
||||
"showCopyPath": "Mostrar «Copiar ruta» en el menú contextual",
|
||||
"showCopyPathDescription": "Mostrar una acción Copiar ruta en el menú contextual de archivos y carpetas.",
|
||||
"previewDescription": "Mostrar vistas previas en miniatura para {filetype}, mostrará el icono genérico si está desactivado.",
|
||||
"passkeys": "Claves de acceso",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "Tamaño predeterminado de las miniaturas",
|
||||
"defaultGallerySizeDescription": "Escala inicial de las miniaturas de la galería o la lista (1–9) en navegadores o dispositivos nuevos. No anula los tamaños ya guardados localmente.",
|
||||
"disableHideSidebar": "Mostrar siempre la barra lateral",
|
||||
"editorQuickSave": "Mostrar el botón de guardado rápido en el editor",
|
||||
"editorQuickSaveDescription": "El editor de texto solo mostrará el icono de guardar, ocultando cualquier otra acción.",
|
||||
"showAdvancedProfile": "Mostrar opciones avanzadas del perfil",
|
||||
"showAdvancedProfileDescription": "Mostrar la configuración del perfil por categorías en la barra lateral, en lugar de la vista simplificada del perfil.",
|
||||
"accountOptions": "Opciones de la cuenta"
|
||||
@@ -832,7 +831,7 @@
|
||||
"vscode": "VSCode"
|
||||
},
|
||||
"overscroll": {
|
||||
"label": "Desplazamiento más del limite",
|
||||
"label": "Desplazamiento más allá del límite",
|
||||
"description": "Cuánto puede desplazarse el editor más allá del final del contenido",
|
||||
"none": "Ninguno",
|
||||
"half": "Mitad",
|
||||
@@ -856,12 +855,12 @@
|
||||
"customScrollbarDescription": "Reemplazar la barra de desplazamiento nativa del navegador por la integrada de Ace",
|
||||
"wrapContent": "Ajustar líneas largas en el editor",
|
||||
"wrapContentDescription": "Ajusta el contenido del editor para mantener siempre visibles las líneas muy largas",
|
||||
"indentedSoftWrap": "Ajustar lineas largas con sangría",
|
||||
"indentedSoftWrap": "Ajustar líneas largas con sangría",
|
||||
"indentedSoftWrapDescription": "Cuando está habilitado, agrega una sangría al inicio de la línea del texto que fue ajustado",
|
||||
"autocompletion": "Autocompletado",
|
||||
"autocompletionDescription": "Habilita el autocompletado presionando manualmente las teclas 'ctrl + espacio'",
|
||||
"liveAutocompletion": "Autocompletado automático",
|
||||
"liveAutocompletionDescription": "Cuando lo activas, aparecerá un cuadro flotante automaticamente mientras escribes"
|
||||
"liveAutocompletionDescription": "Cuando lo activas, aparecerá un cuadro flotante automáticamente mientras escribes"
|
||||
},
|
||||
"uninitialized": "Error al inicializar el editor. Por favor, recargue.",
|
||||
"saveAbortedMessage": "Operación de guardado cancelada. El archivo activo de la aplicación ({activeFile}) no coincide con el archivo que intenta guardar ({tryingToSave}).",
|
||||
@@ -899,8 +898,8 @@
|
||||
"fontColor": "Color de fuente"
|
||||
},
|
||||
"json": {
|
||||
"formatJSON": "Formato JSON",
|
||||
"formatJSONSuccess": "El formato JSON se ha generado correctamente",
|
||||
"formatJSON": "Formatear JSON",
|
||||
"formatJSONSuccess": "JSON formateado correctamente",
|
||||
"invalidJSON": "JSON no válido: {message}"
|
||||
}
|
||||
},
|
||||
@@ -916,7 +915,7 @@
|
||||
"PlayAllLoopedPlayback": "Reproducir todo en bucle",
|
||||
"QueueButtonHint": "Mostrar cola de Reproducción",
|
||||
"QueuePlayback": "Cola de Reproducción",
|
||||
"emptyQueue": "La cola de reproducción está vacia...",
|
||||
"emptyQueue": "La cola de reproducción está vacía...",
|
||||
"queueCleared": "Cola vaciada",
|
||||
"queue": "Cola",
|
||||
"lyrics": "Letra",
|
||||
@@ -978,7 +977,7 @@
|
||||
"other": "Otros"
|
||||
},
|
||||
"tools": {
|
||||
"description": "Utilidades y herramientas que te seŕan de ayuda",
|
||||
"description": "Utilidades y herramientas que te serán de ayuda",
|
||||
"sizeAnalyzer": {
|
||||
"name": "Analizador de archivos",
|
||||
"description": "Visualiza archivos y carpetas de gran tamaño con un mapa de árbol interactivo"
|
||||
@@ -994,19 +993,19 @@
|
||||
"searchPlaceholder": "Buscar iconos...",
|
||||
"popularIcons": "Mostrando {count} iconos populares",
|
||||
"showingResults": "Se encontraron {count} iconos",
|
||||
"noResults": "No se pudieorn encontrar iconos",
|
||||
"noResults": "No se pudieron encontrar iconos",
|
||||
"tryDifferentSearch": "Prueba con otro nombre",
|
||||
"custom": "Personalizar",
|
||||
"useCustomIcon": "Utilizar icono personalizado: {name}",
|
||||
"iconLibraryUrl": "fonts.google.com/icons",
|
||||
"browseFullLibrary": "Mira la la colección completa aqui"
|
||||
"browseFullLibrary": "Mira la colección completa aquí"
|
||||
},
|
||||
"toolNotFound": "No se encontró la herramienta",
|
||||
"fileWatcher": {
|
||||
"name": "Visor en tiempo real",
|
||||
"description": "Visualiza el contenido de archivos en tiempo real",
|
||||
"emptyState": "Elije un archivo y haz clic en el botón para empezar a ver los cambios en el archivo",
|
||||
"chooseFile": "Elije un archivo"
|
||||
"emptyState": "Elige un archivo y haz clic en el botón para empezar a ver los cambios en el archivo",
|
||||
"chooseFile": "Elige un archivo"
|
||||
},
|
||||
"advancedSearch": {
|
||||
"name": "Búsqueda avanzada",
|
||||
@@ -1018,7 +1017,7 @@
|
||||
"noTerms": "Introduzca al menos un término de búsqueda.",
|
||||
"termTooShort": "Cada término de búsqueda debe tener al menos {minSearchLength} caracteres",
|
||||
"resultsTitle": "Resultados de la búsqueda",
|
||||
"invalidSources": "Enciende al menos una fuente.",
|
||||
"invalidSources": "Activa al menos una fuente.",
|
||||
"noResults": "No hay archivos o carpetas coincidentes.",
|
||||
"resultLimit": "Máximo de resultados de búsqueda"
|
||||
},
|
||||
@@ -1031,7 +1030,7 @@
|
||||
"last24h": "Últimas 24 horas",
|
||||
"last7d": "Últimos 7 días",
|
||||
"last30d": "Últimos 30 días",
|
||||
"customRange": "Gama personalizada",
|
||||
"customRange": "Rango personalizado",
|
||||
"eventType": "Tipo de evento",
|
||||
"eventSummary": "Evento",
|
||||
"interval": "Intervalo de tiempo",
|
||||
@@ -1053,7 +1052,7 @@
|
||||
"pathFilterMode": "Filtrar por",
|
||||
"pathFilterPattern": "Patrón",
|
||||
"pathGlob": "Patrón de ruta (glob)",
|
||||
"shareFilter": "Compartir filtro",
|
||||
"shareFilter": "Filtro de enlaces",
|
||||
"scopeAll": "Toda la actividad",
|
||||
"scopeFiles": "Actividad de archivos",
|
||||
"scopeShares": "Actividad de enlaces",
|
||||
@@ -1082,8 +1081,8 @@
|
||||
"viewActivity": "Ver actividad",
|
||||
"tableColumns": "Columnas de la tabla",
|
||||
"scopeAccess": "Actividad de acceso",
|
||||
"eventAccessCreate": "Acceder a «Crear»",
|
||||
"eventAccessDelete": "Acceder a «Eliminar»"
|
||||
"eventAccessCreate": "Creación de acceso",
|
||||
"eventAccessDelete": "Eliminación de acceso"
|
||||
},
|
||||
"toolAccessDefaults": "Configuración predeterminada del acceso a las herramientas",
|
||||
"toolAccessDefaultsDescription": "Configura a qué herramientas pueden acceder los nuevos usuarios de forma predeterminada",
|
||||
@@ -1096,7 +1095,7 @@
|
||||
"reassignPath": "Reasignar ruta",
|
||||
"pathReassigned": "¡La ruta se actualizó correctamente!",
|
||||
"pathReassignFailed": "No se pudo actualizar la ruta. Inténtalo de nuevo",
|
||||
"incompleteResultsDetails": "Mostrando resultados máximos ({max}). Intenta restringir la búsqueda para obtener más resultados en {max}.",
|
||||
"incompleteResultsDetails": "Mostrando el máximo de resultados ({max}). Acota la búsqueda para obtener menos de {max} resultados.",
|
||||
"noCreateOptions": "No hay opciones de creación disponibles"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
@@ -1127,9 +1126,9 @@
|
||||
"operationFailedTitle": "La operación ha fallado"
|
||||
},
|
||||
"events": {
|
||||
"serverShutdown": "El servidor fúe apagado",
|
||||
"serverShutdown": "El servidor fue apagado",
|
||||
"authenticationFailed": "Hubo un error de autenticación - Actualiza la página y vuelve a iniciar sesión",
|
||||
"reconnected": "!Se reconecto al servidor!",
|
||||
"reconnected": "¡Se reconectó al servidor!",
|
||||
"connectionLost": "Se perdió la conexión con el servidor. Intentando reconectar..."
|
||||
},
|
||||
"threejs": {
|
||||
@@ -1142,7 +1141,7 @@
|
||||
"changeBackground": "Cambiar el color de fondo"
|
||||
},
|
||||
"onlyoffice": {
|
||||
"debugTitle": "OnlyOffice debug",
|
||||
"debugTitle": "Traza de depuración de OnlyOffice",
|
||||
"debug": "Depuración de OnlyOffice",
|
||||
"basicConfiguration": "Configuración básica",
|
||||
"configurationDetails": "Detalles de configuración de OnlyOffice",
|
||||
@@ -1151,7 +1150,7 @@
|
||||
"fileType": "Tipo de archivo",
|
||||
"editMode": "Modo Edición",
|
||||
"downloadURL": "Descargar URL",
|
||||
"callbackURL": "Callback URL",
|
||||
"callbackURL": "URL de callback",
|
||||
"networkFlow": "Flujo de red",
|
||||
"browser": "Navegador",
|
||||
"onlyOffice": "OnlyOffice",
|
||||
@@ -1170,7 +1169,7 @@
|
||||
"startingInit": "Inicializando OnlyOffice...",
|
||||
"configCheck": "Comprobando configuración - URL construida",
|
||||
"apiRequest": "Solicitud API - Configuración recibida",
|
||||
"serverConnection": "OnlyOffice Server Connection - Configuración enviada",
|
||||
"serverConnection": "Conexión con el servidor de OnlyOffice - Configuración enviada",
|
||||
"testingConnectivity": "Probando la conectividad con OnlyOffice...",
|
||||
"serverReachable": "OnlyOffice parece accesible",
|
||||
"documentDownload": "Descarga de documentos - Completa",
|
||||
@@ -1183,9 +1182,9 @@
|
||||
"time": {
|
||||
"time": "{prefix}Hora{suffix}",
|
||||
"minute": "{prefix}minuto{suffix}",
|
||||
"minutes": "{prefix}Acta{suffix}",
|
||||
"minutes": "{prefix}Minutos{suffix}",
|
||||
"hour": "{prefix}hora{suffix}",
|
||||
"hours": "{prefix}Horario{suffix}",
|
||||
"hours": "{prefix}Horas{suffix}",
|
||||
"day": "{prefix}día{suffix}",
|
||||
"days": "{prefix}Días{suffix}",
|
||||
"months": "{prefix}Meses{suffix}",
|
||||
@@ -1195,10 +1194,10 @@
|
||||
"title": "Cuota de almacenamiento",
|
||||
"openDescription": "Abre el cuadro de diálogo de cuota de almacenamiento de esta carpeta.",
|
||||
"folderDescription": "Limitar el espacio de almacenamiento de esta carpeta.",
|
||||
"shareLimit": "Limitar el almacenamiento en esta unidad compartida",
|
||||
"shareLimit": "Limitar el almacenamiento a través de este recurso compartido",
|
||||
"shareLimitDescription": "Cuenta las subidas realizadas a través de este enlace. También se descuenta de la cuota de origen del propietario.",
|
||||
"scopeLimit": "Límite de almacenamiento para esta fuente",
|
||||
"scopeLimitDescription": "Límite de almacenamiento para la tarea de este usuario en esta fuente.",
|
||||
"scopeLimitDescription": "Limita el almacenamiento asignado a este usuario en esta fuente.",
|
||||
"indexingDisabledMeterHint": "La indexación está desactivada en esta fuente. Solo está disponible el seguimiento del uso.",
|
||||
"meterIndexHint": "Utiliza el tamaño total de los archivos indexados en esta carpeta. Incluye los cambios realizados fuera de FileBrowser cuando se actualiza el índice.",
|
||||
"meterAccountedHint": "Solo cuenta las subidas, guardados y eliminaciones realizadas a través de FileBrowser.",
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "Taille par défaut des vignettes",
|
||||
"defaultGallerySizeDescription": "Échelle initiale des vignettes de la galerie ou de la liste (1 à 9) sur les nouveaux navigateurs ou appareils. Ne remplace pas les tailles déjà enregistrées localement.",
|
||||
"disableHideSidebar": "Toujours afficher la barre latérale",
|
||||
"editorQuickSave": "Afficher le bouton « Enregistrement rapide » dans l'éditeur",
|
||||
"editorQuickSaveDescription": "L'éditeur de texte n'affichera que l'icône d'enregistrement, en masquant toutes les autres actions",
|
||||
"showAdvancedProfile": "Afficher les paramètres avancés du profil",
|
||||
"showAdvancedProfileDescription": "Afficher les paramètres de profil classés par catégorie dans la barre latérale, au lieu de l'affichage simplifié du profil.",
|
||||
"accountOptions": "Options du compte"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "גודל תמונה ממוזערת ברירת מחדל",
|
||||
"defaultGallerySizeDescription": "גודל התמונות הממוזערות הראשוני בגלריה או ברשימה (1–9) בדפדפנים או במכשירים חדשים. הגדרה זו אינה מבטלת את הגדלים שכבר נשמרו באופן מקומי.",
|
||||
"disableHideSidebar": "הצג תמיד את סרגל הצד",
|
||||
"editorQuickSave": "הצג את כפתור השמירה המהירה בעורך",
|
||||
"editorQuickSaveDescription": "עורך הטקסט יציג רק את סמל השמירה, ויסתיר את כל הפעולות האחרות",
|
||||
"showAdvancedProfile": "הצג הגדרות פרופיל מתקדמות",
|
||||
"showAdvancedProfileDescription": "הצג את הגדרות הפרופיל המסווגות בסרגל הצד במקום תצוגת הפרופיל הפשוטה.",
|
||||
"accountOptions": "אפשרויות חשבון"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "Alapértelmezett miniatűrméret",
|
||||
"defaultGallerySizeDescription": "A galéria vagy lista miniatűrjeinek kezdeti mérete (1–9) új böngészőkön vagy eszközökön. A már helyileg elmentett méreteket nem írja felül.",
|
||||
"disableHideSidebar": "Az oldalsávot mindig mutasd”",
|
||||
"editorQuickSave": "A gyorsmentés gomb megjelenítése a szerkesztőben",
|
||||
"editorQuickSaveDescription": "A szövegszerkesztő csak a mentés gombot jeleníti meg, az összes többi műveletet elrejti",
|
||||
"showAdvancedProfile": "A profil speciális beállításainak megjelenítése",
|
||||
"showAdvancedProfileDescription": "Az egyszerűsített profilnézet helyett az oldalsávban jelenítse meg a kategóriák szerint csoportosított profilbeállításokat.",
|
||||
"accountOptions": "Fiókbeállítások"
|
||||
|
||||
@@ -15,6 +15,7 @@ export const availableLocales: Record<string, string> = {
|
||||
el: 'el',
|
||||
en: 'en',
|
||||
es: 'es',
|
||||
eu: 'eu',
|
||||
fr: 'fr',
|
||||
he: 'he',
|
||||
hu: 'hu',
|
||||
@@ -98,7 +99,8 @@ setLanguage('en');
|
||||
|
||||
// import.meta.glob is vite-specific
|
||||
// here we are preloading all json files as lazy chunks, except 'en.json' since this one will be always loaded.
|
||||
const localeModules = import.meta.glob<{ default: Record<string, unknown> }>('./!(en).json');
|
||||
const localeModules = import.meta.glob<{ default: Record<string, unknown> }>('./*.json');
|
||||
delete localeModules['./en.json'];
|
||||
|
||||
export async function setLocale(locale: string) {
|
||||
// If the locale doesn't exist in our list, fallback to English
|
||||
|
||||
@@ -292,6 +292,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -605,8 +606,6 @@
|
||||
"defaultGallerySize": "Dimensione predefinita delle miniature",
|
||||
"defaultGallerySizeDescription": "Scala iniziale delle miniature della galleria o dell'elenco (1–9) sui nuovi browser o dispositivi. Non sovrascrive le dimensioni già salvate localmente.",
|
||||
"disableHideSidebar": "Mostra sempre la barra laterale",
|
||||
"editorQuickSave": "Mostra il pulsante \"Salva rapidamente\" nell'editor",
|
||||
"editorQuickSaveDescription": "L'editor di testo mostrerà solo l'icona di salvataggio, nascondendo tutte le altre azioni",
|
||||
"showAdvancedProfile": "Mostra le impostazioni avanzate del profilo",
|
||||
"showAdvancedProfileDescription": "Mostra le impostazioni del profilo suddivise per categorie nella barra laterale, anziché la visualizzazione semplificata del profilo.",
|
||||
"accountOptions": "Opzioni dell'account"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "デフォルトのサムネイルサイズ",
|
||||
"defaultGallerySizeDescription": "新しいブラウザやデバイスでのギャラリーまたはリストのサムネイルの初期スケール(1~9)。ローカルにすでに保存されているサイズを上書きすることはありません。",
|
||||
"disableHideSidebar": "サイドバーを常に表示する",
|
||||
"editorQuickSave": "エディタにクイックセーブボタンを表示する",
|
||||
"editorQuickSaveDescription": "テキストエディタには保存アイコンのみが表示され、その他の操作は非表示になります",
|
||||
"showAdvancedProfile": "詳細なプロフィール設定を表示する",
|
||||
"showAdvancedProfileDescription": "簡易プロフィール表示の代わりに、サイドバーに分類されたプロフィール設定を表示します。",
|
||||
"accountOptions": "アカウント設定"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "기본 썸네일 크기",
|
||||
"defaultGallerySizeDescription": "새로운 브라우저나 기기에서 갤러리 또는 목록의 썸네일 초기 크기(1–9). 이미 로컬에 저장된 크기는 덮어쓰지 않습니다.",
|
||||
"disableHideSidebar": "사이드바 항상 표시",
|
||||
"editorQuickSave": "편집기에 빠른 저장 버튼 표시",
|
||||
"editorQuickSaveDescription": "텍스트 편집기에는 저장 아이콘만 표시되고, 다른 모든 작업은 숨겨집니다.",
|
||||
"showAdvancedProfile": "고급 프로필 설정 표시",
|
||||
"showAdvancedProfileDescription": "간소화된 프로필 보기 대신 사이드바에 분류별 프로필 설정을 표시합니다.",
|
||||
"accountOptions": "계정 설정"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -604,8 +605,6 @@
|
||||
"defaultGallerySize": "Standaardgrootte van miniatuurafbeeldingen",
|
||||
"defaultGallerySizeDescription": "Standaardschaal (1–9) voor miniatuurafbeeldingen in galerijen of lijsten in nieuwe browsers of op nieuwe apparaten. Dit overschrijft geen afmetingen die al lokaal zijn opgeslagen.",
|
||||
"disableHideSidebar": "Zijbalk altijd weergeven",
|
||||
"editorQuickSave": "Knop voor snel opslaan weergeven in de editor",
|
||||
"editorQuickSaveDescription": "De teksteditor toont alleen het pictogram voor opslaan; alle andere acties worden verborgen",
|
||||
"showAdvancedProfile": "Geavanceerde profielinstellingen weergeven",
|
||||
"showAdvancedProfileDescription": "Toon de profielinstellingen per categorie in de zijbalk in plaats van de vereenvoudigde profielweergave.",
|
||||
"accountOptions": "Accountinstellingen"
|
||||
|
||||
@@ -292,6 +292,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -604,8 +605,6 @@
|
||||
"showCopyPathDescription": "Een actie Pad kopiëren weergeven in het contextmenu van bestanden en mappen.",
|
||||
"deleteWithoutConfirming": "Bestanden verwijderen zonder bevestigingsvenster",
|
||||
"deleteWithoutConfirmingDescription": "Bestanden verwijderen zonder bevestigingsvenster. Dit werkt alleen als het bestand een mediabestand is.",
|
||||
"editorQuickSave": "Knop voor snel opslaan weergeven in de editor",
|
||||
"editorQuickSaveDescription": "De teksteditor toont alleen het opslagpictogram; alle andere acties worden verborgen",
|
||||
"previewOfficeDescription": "Toon miniaturen voor Office-bestandstypen in de listingweergave",
|
||||
"disableThumbnailPreviews": "Miniaturen uitschakelen voor bepaalde bestandsextensies",
|
||||
"disableThumbnailPreviewsDescription": "Een door spaties gescheiden lijst met bestandsextensies waarvoor miniatuurafbeeldingen uitgeschakeld moeten worden (bijv. '.txt .html').",
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "Domyślny rozmiar miniatury",
|
||||
"defaultGallerySizeDescription": "Początkowa skala miniatur w galerii lub na liście (1–9) w nowych przeglądarkach lub na nowych urządzeniach. Nie zastępuje rozmiarów już zapisanych lokalnie.",
|
||||
"disableHideSidebar": "Zawsze wyświetlaj pasek boczny",
|
||||
"editorQuickSave": "Pokaż przycisk szybkiego zapisywania w edytorze",
|
||||
"editorQuickSaveDescription": "Edytor tekstu wyświetli jedynie ikonę zapisywania, ukrywając wszystkie inne opcje",
|
||||
"showAdvancedProfile": "Pokaż zaawansowane ustawienia profilu",
|
||||
"showAdvancedProfileDescription": "Wyświetlaj ustawienia profilu pogrupowane według kategorii na pasku bocznym zamiast uproszczonego widoku profilu.",
|
||||
"accountOptions": "Opcje konta"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "Tamanho padrão da miniatura",
|
||||
"defaultGallerySizeDescription": "Escala inicial das miniaturas da galeria ou da lista (1–9) em novos navegadores ou dispositivos. Não substitui os tamanhos já salvos localmente.",
|
||||
"disableHideSidebar": "Sempre exibir a barra lateral",
|
||||
"editorQuickSave": "Mostrar o botão de salvamento rápido no editor",
|
||||
"editorQuickSaveDescription": "O editor de texto exibirá apenas o ícone de salvar, ocultando todas as outras ações",
|
||||
"showAdvancedProfile": "Mostrar configurações avançadas do perfil",
|
||||
"showAdvancedProfileDescription": "Mostrar as configurações do perfil categorizadas na barra lateral, em vez da visualização simplificada do perfil.",
|
||||
"accountOptions": "Opções da conta"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -604,8 +605,6 @@
|
||||
"defaultGallerySize": "Tamanho padrão da miniatura",
|
||||
"defaultGallerySizeDescription": "Escala inicial das miniaturas da galeria ou da lista (1–9) em novos navegadores ou dispositivos. Não substitui os tamanhos já guardados localmente.",
|
||||
"disableHideSidebar": "Mostrar sempre a barra lateral",
|
||||
"editorQuickSave": "Mostrar o botão de gravação rápida no editor",
|
||||
"editorQuickSaveDescription": "O editor de texto irá mostrar apenas o ícone de guardar, ocultando quaisquer outras ações",
|
||||
"showAdvancedProfile": "Mostrar definições avançadas do perfil",
|
||||
"showAdvancedProfileDescription": "Mostrar as definições do perfil por categorias na barra lateral, em vez da visualização simplificada do perfil.",
|
||||
"accountOptions": "Opções da conta"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "Dimensiunea implicită a miniaturilor",
|
||||
"defaultGallerySizeDescription": "Scala inițială a miniaturilor din galerie sau listă (1–9) pe browserele sau dispozitivele noi. Nu înlocuiește dimensiunile deja salvate local.",
|
||||
"disableHideSidebar": "Afișează întotdeauna bara laterală",
|
||||
"editorQuickSave": "Afișează butonul de salvare rapidă în editor",
|
||||
"editorQuickSaveDescription": "Editorul de text va afișa doar pictograma de salvare, ascunzând orice alte acțiuni",
|
||||
"showAdvancedProfile": "Afișează setările avansate ale profilului",
|
||||
"showAdvancedProfileDescription": "Afișează setările de profil clasificate în bara laterală, în locul vizualizării simplificate a profilului.",
|
||||
"accountOptions": "Opțiuni de cont"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "Размер миниатюры по умолчанию",
|
||||
"defaultGallerySizeDescription": "Начальный масштаб миниатюр в галерее или списке (1–9) в новых браузерах или на новых устройствах. Не переопределяет размеры, уже сохраненные локально.",
|
||||
"disableHideSidebar": "Всегда отображать боковую панель",
|
||||
"editorQuickSave": "Показать кнопку быстрого сохранения в редакторе",
|
||||
"editorQuickSaveDescription": "В текстовом редакторе будет отображаться только значок сохранения, а все остальные действия будут скрыты",
|
||||
"showAdvancedProfile": "Показать расширенные настройки профиля",
|
||||
"showAdvancedProfileDescription": "Отображать настройки профиля по категориям в боковой панели вместо упрощённого вида профиля.",
|
||||
"accountOptions": "Настройки учетной записи"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "Predvolená veľkosť náhľadu",
|
||||
"defaultGallerySizeDescription": "Počiatočné zväčšenie miniatúr v galérii alebo zozname (1–9) v nových prehliadačoch alebo na nových zariadeniach. Neprepisuje veľkosti, ktoré sú už uložené lokálne.",
|
||||
"disableHideSidebar": "Vždy zobraziť bočný panel",
|
||||
"editorQuickSave": "Zobraziť tlačidlo rýchleho uloženia v editore",
|
||||
"editorQuickSaveDescription": "Textový editor zobrazí iba ikonu uloženia a všetky ostatné akcie skryje",
|
||||
"showAdvancedProfile": "Zobraziť pokročilé nastavenia profilu",
|
||||
"showAdvancedProfileDescription": "Zobraziť nastavenia profilu podľa kategórií v bočnom paneli namiesto zjednodušeného zobrazenia profilu.",
|
||||
"accountOptions": "Možnosti účtu"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -604,8 +605,6 @@
|
||||
"defaultGallerySize": "Standardstorlek på miniatyrbilder",
|
||||
"defaultGallerySizeDescription": "Initial skala för miniatyrbilder i gallerier eller listor (1–9) i nya webbläsare eller på nya enheter. Åsidosätter inte storlekar som redan sparats lokalt.",
|
||||
"disableHideSidebar": "Visa alltid sidofältet",
|
||||
"editorQuickSave": "Visa knappen för snabbsparning i redigeraren",
|
||||
"editorQuickSaveDescription": "Textredigeraren visar endast spara-ikonen och döljer alla andra åtgärder",
|
||||
"showAdvancedProfile": "Visa avancerade profilinställningar",
|
||||
"showAdvancedProfileDescription": "Visa de kategoriserade profilinställningarna i sidofältet istället för den förenklade profilvyn.",
|
||||
"accountOptions": "Kontoalternativ"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -808,8 +809,6 @@
|
||||
"defaultGallerySize": "Varsayılan küçük resim boyutu",
|
||||
"defaultGallerySizeDescription": "Yeni tarayıcılarda veya cihazlarda galerinin veya listenin başlangıçtaki küçük resim ölçeği (1–9). Yerel olarak önceden kaydedilmiş boyutları geçersiz kılmaz.",
|
||||
"disableHideSidebar": "Kenar çubuğunu her zaman göster",
|
||||
"editorQuickSave": "Düzenleyicide hızlı kaydetme düğmesini göster",
|
||||
"editorQuickSaveDescription": "Metin düzenleyicisi yalnızca kaydetme simgesini gösterecek, diğer tüm işlemleri gizleyecektir",
|
||||
"showAdvancedProfile": "Gelişmiş profil ayarlarını göster",
|
||||
"showAdvancedProfileDescription": "Basitleştirilmiş profil görünümü yerine, kenar çubuğunda kategorilere ayrılmış profil ayarlarını göster.",
|
||||
"accountOptions": "Hesap seçenekleri"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -604,8 +605,6 @@
|
||||
"defaultGallerySize": "Розмір мініатюри за замовчуванням",
|
||||
"defaultGallerySizeDescription": "Початковий масштаб мініатюр у галереї або списку (1–9) у нових браузерах або на нових пристроях. Не замінює розміри, які вже збережено локально.",
|
||||
"disableHideSidebar": "Завжди показувати бічну панель",
|
||||
"editorQuickSave": "Показати кнопку швидкого збереження в редакторі",
|
||||
"editorQuickSaveDescription": "У текстовому редакторі буде відображатися лише піктограма збереження, а всі інші дії будуть приховані",
|
||||
"showAdvancedProfile": "Показати розширені налаштування профілю",
|
||||
"showAdvancedProfileDescription": "Відображати на бічній панелі налаштування профілю за категоріями замість спрощеного виду профілю.",
|
||||
"accountOptions": "Параметри облікового запису"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -604,8 +605,6 @@
|
||||
"defaultGallerySize": "默认缩略图大小",
|
||||
"defaultGallerySizeDescription": "在新浏览器或设备上显示的图库或列表缩略图初始缩放比例(1–9)。不会覆盖已保存在本地中的尺寸设置。",
|
||||
"disableHideSidebar": "始终显示侧边栏",
|
||||
"editorQuickSave": "在编辑器中显示“快速保存”按钮",
|
||||
"editorQuickSaveDescription": "文本编辑器只会显示“保存”图标,隐藏其他所有操作",
|
||||
"showAdvancedProfile": "显示高级个人资料设置",
|
||||
"showAdvancedProfileDescription": "在侧边栏中显示分类后的个人资料设置,而不是简化版个人资料视图。",
|
||||
"accountOptions": "账户选项"
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"eu": "Euskara",
|
||||
"fr": "Français",
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
@@ -573,8 +574,6 @@
|
||||
"defaultGallerySize": "預設縮圖大小",
|
||||
"defaultGallerySizeDescription": "在新瀏覽器或裝置上顯示的初始圖庫或清單縮圖比例(1–9)。此設定不會覆寫已儲存於本地的尺寸。",
|
||||
"disableHideSidebar": "始終顯示側邊欄",
|
||||
"editorQuickSave": "在編輯器中顯示「快速儲存」按鈕",
|
||||
"editorQuickSaveDescription": "文字編輯器將僅顯示儲存圖示,並隱藏其他所有操作",
|
||||
"showAdvancedProfile": "顯示進階個人資料設定",
|
||||
"showAdvancedProfileDescription": "在側邊欄中顯示分類後的個人資料設定,而非簡化版個人資料檢視。",
|
||||
"accountOptions": "帳戶選項"
|
||||
|
||||
@@ -781,7 +781,6 @@ export const mutations = {
|
||||
"showTypeColumn",
|
||||
"showCreationDateColumn",
|
||||
"hideFilesInTree",
|
||||
"editorQuickSave",
|
||||
"showSelectMultiple",
|
||||
"debugOffice",
|
||||
"disableUpdateNotifications",
|
||||
|
||||
@@ -68,7 +68,6 @@ export function sectionsFromFlatUser(user) {
|
||||
},
|
||||
fileViewer: {
|
||||
autoplayMedia: boolPtr(preview.autoplayMedia),
|
||||
editorQuickSave: !!u.editorQuickSave,
|
||||
preferEditorForMarkdown: !!u.preferEditorForMarkdown,
|
||||
debugOffice: !!u.debugOffice,
|
||||
disableViewingExt: u.disableViewingExt || "",
|
||||
@@ -152,7 +151,6 @@ export function applySectionsToFlatUser(user, sections) {
|
||||
user.disablePreviewExt = preview.disablePreviewExt || "";
|
||||
|
||||
user.preview.autoplayMedia = boolPtr(fileViewer.autoplayMedia);
|
||||
user.editorQuickSave = !!fileViewer.editorQuickSave;
|
||||
user.preferEditorForMarkdown = !!fileViewer.preferEditorForMarkdown;
|
||||
user.debugOffice = !!fileViewer.debugOffice;
|
||||
user.disableViewingExt = fileViewer.disableViewingExt || "";
|
||||
@@ -219,7 +217,6 @@ const FLAT_PROFILE_FIELD_ENFORCED_PATHS = {
|
||||
showTypeColumn: ["listing", "showTypeColumn"],
|
||||
showCreationDateColumn: ["listing", "showCreationDateColumn"],
|
||||
hideFilesInTree: ["sidebar", "hideFiles"],
|
||||
editorQuickSave: ["fileViewer", "editorQuickSave"],
|
||||
showSelectMultiple: ["listing", "showSelectMultiple"],
|
||||
debugOffice: ["fileViewer", "debugOffice"],
|
||||
viewMode: ["listing", "viewMode"],
|
||||
|
||||
@@ -29,15 +29,6 @@
|
||||
/>
|
||||
<template v-else>
|
||||
<action
|
||||
v-if="showQuickSave"
|
||||
class="save-button"
|
||||
id="save-button"
|
||||
icon="save"
|
||||
:label="$t('general.save')"
|
||||
@action="save()"
|
||||
/>
|
||||
<action
|
||||
v-else
|
||||
class="overflow-menu-button"
|
||||
:icon="iconName"
|
||||
:disabled="noItems || isSidebarFloating"
|
||||
@@ -49,8 +40,6 @@
|
||||
|
||||
<script>
|
||||
import { router } from "@/router";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { notify } from "@/notify";
|
||||
import { getters, state, mutations } from "@/store";
|
||||
import Action from "@/components/Action.vue";
|
||||
import { globalVars } from "@/utils/constants";
|
||||
@@ -83,12 +72,6 @@ export default {
|
||||
}
|
||||
return state.req.name;
|
||||
},
|
||||
showQuickSave() {
|
||||
if (getters.currentView() !== "editor" || !getters.sourcePermissions().modify) {
|
||||
return false;
|
||||
}
|
||||
return state.user.editorQuickSave;
|
||||
},
|
||||
disableNavButtons() {
|
||||
const isShare = getters.isShare();
|
||||
const regularDisabled = globalVars.disableNavButtons && this.isListingView;
|
||||
@@ -131,9 +114,6 @@ export default {
|
||||
noItems() {
|
||||
return !state.contextMenuHasItems && !getters.isPreviewView();
|
||||
},
|
||||
showSave() {
|
||||
return getters.currentView() === "editor" && getters.sourcePermissions().modify;
|
||||
},
|
||||
showSearch() {
|
||||
return getters.isLoggedIn() && getters.currentView() === "listingView" && !getters.isShare();
|
||||
},
|
||||
@@ -185,26 +165,6 @@ export default {
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
async save() {
|
||||
const button = "save";
|
||||
buttons.loading("save");
|
||||
try {
|
||||
// Call the editor's save handler directly
|
||||
if (state.editor.saveHandler) {
|
||||
await state.editor.saveHandler();
|
||||
buttons.success(button);
|
||||
// Note: Success notification is shown by the editor
|
||||
} else {
|
||||
const errorMsg = "No editor save handler registered";
|
||||
notify.showError(errorMsg);
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
} catch (e) {
|
||||
buttons.done(button);
|
||||
// Note: Error notification is already shown by the editor
|
||||
throw e; // Re-throw so caller knows save failed
|
||||
}
|
||||
},
|
||||
toggleOverflow() {
|
||||
if (getters.currentPromptName() === "OverflowMenu") {
|
||||
mutations.closeHovers();
|
||||
@@ -305,7 +265,10 @@ export default {
|
||||
confirm: async () => {
|
||||
// Save and exit - trigger the save action
|
||||
// If save fails, this will throw and be caught by SaveBeforeExit component
|
||||
await this.save();
|
||||
if (!state.editor.saveHandler) {
|
||||
throw new Error("No editor save handler registered");
|
||||
}
|
||||
await state.editor.saveHandler();
|
||||
mutations.setEditorDirty(false);
|
||||
onConfirmAction();
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:class="{ 'viewer-mode': viewerMode }"
|
||||
:style="isSplitActive ? { flexBasis: `${editorPanePercent}%` } : {}"
|
||||
>
|
||||
<EditorToolbar v-if="showEditorToolbar" :editor="editor" :is-markdown="isMarkdownFile" />
|
||||
<EditorToolbar v-if="showEditorToolbar" ref="toolbar" :editor="editor" :is-markdown="isMarkdownFile" :show-save="!viewerMode" :save-handler="handleEditorValueRequest" />
|
||||
<div id="editor" ref="editorEl"></div>
|
||||
</div>
|
||||
<MarkdownSplitView
|
||||
@@ -76,6 +76,7 @@ const { t } = useI18n();
|
||||
const editorRoot = ref<HTMLElement | null>(null);
|
||||
const editorEl = ref<HTMLElement | null>(null);
|
||||
const splitView = ref<InstanceType<typeof MarkdownSplitView> | null>(null);
|
||||
const toolbar = ref<InstanceType<typeof EditorToolbar> | null>(null);
|
||||
|
||||
const editor = shallowRef<Ace.Editor | null>(null); // The editor instance
|
||||
const originalReq = ref<Req | null>(null);
|
||||
@@ -538,7 +539,8 @@ function keyEvent(event: KeyboardEvent) {
|
||||
if (props.viewerMode) return;
|
||||
if ((ctrlKey || metaKey) && key.toLowerCase() === "s") {
|
||||
event.preventDefault();
|
||||
handleEditorValueRequest().catch(() => { /* ignore */ });
|
||||
if (event.repeat) return;
|
||||
void toolbar.value?.save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,12 +553,11 @@ function openEditorSettings() {
|
||||
function setupNavigationGuard() {
|
||||
if (props.viewerMode) return;
|
||||
|
||||
navigationGuard = router.beforeEach((to, from, next) => {
|
||||
navigationGuard = router.beforeEach((to, from) => {
|
||||
// If prompt is already open, block any new navigation attempts
|
||||
if (isPromptOpen) {
|
||||
if (getters.currentPromptName() === "SaveBeforeExit") {
|
||||
next(false);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
isPromptOpen = false;
|
||||
pendingNavigation = null;
|
||||
@@ -565,12 +566,11 @@ function setupNavigationGuard() {
|
||||
const isDifferentRoute = to.path !== from.path || to.hash !== from.hash;
|
||||
|
||||
if (isDirty && !props.viewerMode && isDifferentRoute && req.value) {
|
||||
next(false);
|
||||
pendingNavigation = to;
|
||||
showSaveBeforeExitPrompt();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
next();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -719,11 +719,10 @@ const beforeUnloadHandler = (event: BeforeUnloadEvent) => {
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", keyEvent, true);
|
||||
window.addEventListener("beforeunload", beforeUnloadHandler);
|
||||
setupNavigationGuard();
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener("keydown", keyEvent, true);
|
||||
window.addEventListener("beforeunload", beforeUnloadHandler);
|
||||
setupNavigationGuard();
|
||||
resizeContainerEl.value = editorRoot.value;
|
||||
resizeContainerEl.value?.addEventListener("keydown", stopEnterPropagation); // to avoid trigger prompts primary button when the editor is embedded
|
||||
if (props.viewerMode) {
|
||||
@@ -742,7 +741,9 @@ onMounted(async () => {
|
||||
}
|
||||
initializeEditor(state.editor.scrollRatio);
|
||||
// Register save handler so other components can trigger save
|
||||
mutations.setEditorSaveHandler(() => handleEditorValueRequest());
|
||||
if (!props.viewerMode) {
|
||||
mutations.setEditorSaveHandler(() => handleEditorValueRequest());
|
||||
}
|
||||
applyFontSize();
|
||||
void setupViewerResizeObserver();
|
||||
});
|
||||
@@ -770,6 +771,7 @@ onBeforeUnmount(() => {
|
||||
// Clear navigation guard
|
||||
if (navigationGuard) {
|
||||
navigationGuard();
|
||||
navigationGuard = null;
|
||||
}
|
||||
// Clear dirty state and save handler when leaving editor
|
||||
mutations.setEditorDirty(false);
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
show-thumbnail-master
|
||||
@change="onPreferenceChange"
|
||||
@theme-color="onThemeColor"
|
||||
@locale-change="onLocaleChange"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
@@ -124,9 +123,6 @@ export default {
|
||||
onPreferenceChange() {
|
||||
void this.updateSettings();
|
||||
},
|
||||
onLocaleChange() {
|
||||
void this.updateSettings();
|
||||
},
|
||||
async updateSettings(event) {
|
||||
if (typeof event?.preventDefault === "function") {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -42,6 +42,31 @@ test("choose custom theme", async({ page, checkForErrors }) => {
|
||||
checkForErrors();
|
||||
});
|
||||
|
||||
test("switching language loads translations", async({ page, checkForErrors }) => {
|
||||
await page.goto("/files/");
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||
await openProfileSettings(page);
|
||||
|
||||
const userPatches: string[] = [];
|
||||
page.on("request", (request) => {
|
||||
if (request.method() === "PATCH" && new URL(request.url()).pathname.endsWith("/api/users")) {
|
||||
userPatches.push(request.url());
|
||||
}
|
||||
});
|
||||
try {
|
||||
await selectExpandDropdownOption(page, "Language", /^Español/);
|
||||
await checkForNotification(page, "¡Ajustes actualizados!");
|
||||
// the UI should actually switch and not just the stored setting
|
||||
await expect(page.locator('button[aria-label="Idioma"]')).toBeVisible();
|
||||
await expect(page.locator("html")).toHaveAttribute("lang", "es");
|
||||
expect(userPatches).toHaveLength(1);
|
||||
} finally {
|
||||
await selectExpandDropdownOption(page, "Idioma", /^English/);
|
||||
await expect(page.locator('button[aria-label="Language"]')).toBeVisible();
|
||||
}
|
||||
checkForErrors();
|
||||
});
|
||||
|
||||
test("view config", async({ page, checkForErrors }) => {
|
||||
await page.goto("/files/");
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files");
|
||||
|
||||
@@ -74,8 +74,7 @@ test("share file creation actions", async ({ page, checkForErrors, openContextMe
|
||||
await expect(page).toHaveTitle("Graham's Filebrowser - Share - dfsaf.txt");
|
||||
await page.locator(".ace_content").click();
|
||||
await page.keyboard.type("test content");
|
||||
await page.locator(".overflow-menu-button").click();
|
||||
await page.locator('button[aria-label="Save"]').click();
|
||||
await page.locator('.editor-toolbar button[aria-label="Save"]').click();
|
||||
await checkForNotification(page, "dfsaf.txt saved successfully.");
|
||||
checkForErrors();
|
||||
});
|
||||
Reference in new issue
Block a user