Compare commits

...
Author SHA1 Message Date
Graham Steffaniak 302c7c6d06 updated with upload settings and tooltip (#946) 2025-07-11 13:55:21 -05:00
Graham Steffaniak 698f30a6d7 updated indexing size calculations (#942) 2025-07-11 10:56:11 -05:00
Graham Steffaniak e24f858ffc editor quickSave and translations (#941) 2025-07-10 19:18:26 -05:00
Graham Steffaniak b6560920bb More fixes for0713 (#938) 2025-07-10 13:40:45 -05:00
Graham Steffaniak 6b21261c2c logout changes (#937) 2025-07-09 15:13:09 -05:00
Valentyn Lavrinenko c285bba402 Add a setting for user groups oidc claim (#936) 2025-07-09 13:55:36 -05:00
Graham Steffaniak 7f8c269e4b updated workflows 2025-07-09 13:53:43 -05:00
Graham Steffaniak 309e9ad285 updated with upload prompt (#935) 2025-07-09 13:41:14 -05:00
Graham Steffaniak 2c2c14a7f7 updated with standardized paths (#933) 2025-07-08 14:13:13 -05:00
Graham Steffaniak 7c72a4f991 updated with cz language (#927) 2025-07-06 19:12:39 -05:00
Graham Steffaniak aa7e33c46f Merge branch 'dev/v0.7.13' of github.com:gtsteffaniak/filebrowser into dev/v0.7.13 2025-07-06 18:25:07 -05:00
mordillo 5fc8208a7b feat: postMessage navigation sync for iframe embedding (#923) 2025-07-06 18:24:37 -05:00
Graham Steffaniak 5f08e50811 Beta/v0.7.12 (#912) 2025-07-06 18:22:56 -05:00
Graham Steffaniak 0c5c027a5e Tweak some changes (#910) 2025-07-04 18:23:50 -05:00
Graham Steffaniak 6b872558e1 Last minute changes (#908) 2025-07-03 20:16:10 -05:00
Graham Steffaniak 061bbc1426 A few more bugfixes (#903) 2025-07-03 16:39:13 -05:00
Graham Steffaniak 88f18a4b27 Minimal UI (#893) 2025-07-02 09:35:42 -05:00
Graham Steffaniak c4d5a614e7 More bugfixes 0.7.12 (#892) 2025-07-01 18:24:40 -05:00
Graham Steffaniak 9da970ec9b updated without default source (#887) 2025-07-01 13:17:01 -05:00
114 changed files with 4590 additions and 1720 deletions

No files matched your search

+4 -4
View File
@@ -3,10 +3,9 @@ name: PR Request
on:
pull_request:
branches:
- "dev/v[0-9]+.[0-9]+.[0-9]+"
- "beta/v[0-9]+.[0-9]+.[0-9]+"
- "stable/v[0-9]+.[0-9]+.[0-9]+"
- "dev/v*.*.*"
- "beta/v*.*.*"
- "stable/v*.*.*"
jobs:
test_playwright:
name: Test Playwright - general
@@ -81,6 +80,7 @@ jobs:
file: ./_docker/Dockerfile.playwright-noauth
push: false
push_pr_to_registry:
if: github.event.pull_request.head.repo.fork == false
name: Build docker image
runs-on: ubuntu-latest
steps:
+1 -1
View File
@@ -1,7 +1,7 @@
name: regular tests
on:
push:
pull_request:
branches:
- "**"
+25
View File
@@ -2,6 +2,31 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## v0.7.13-beta
**New Features**:
- copy and Move files between sources https://github.com/gtsteffaniak/filebrowser/issues/689
- new enhanced upload prompt
- uses chunked uploads https://github.com/gtsteffaniak/filebrowser/issues/770
- all or individual uploads can be paused/resumed
- individual uploads can be retried
- individual file upload progress https://github.com/gtsteffaniak/filebrowser/issues/871
- keeps screen on https://github.com/gtsteffaniak/filebrowser/issues/900
**Notes**:
- lots of UI improvements
- reworked a lot of the frontend path/source logic to be more consistent.
- updated sort behavior to be natural sort https://github.com/gtsteffaniak/filebrowser/issues/551
- optional quick save icon https://github.com/gtsteffaniak/filebrowser/issues/918
- improved language support: zh-tw chinese traditional (tawain)
**BugFixes**:
- more accurate disk used calculation -- accounting for hard links and sparse files. https://github.com/gtsteffaniak/filebrowser/issues/921
- fix api key revoking mechanism
- fixed shift-select https://github.com/gtsteffaniak/filebrowser/issues/929
- video preview images on safari https://github.com/gtsteffaniak/filebrowser/issues/932
- sticky mode isn't sticky https://github.com/gtsteffaniak/filebrowser/issues/916
## v0.7.12-beta
Happy 4th of July!
+6 -2
View File
@@ -12,6 +12,10 @@ var (
revokeMu sync.Mutex
)
func init() {
revokedApiKeyList = make(map[string]bool)
}
// Auther is the authentication interface.
type Auther interface {
// Auth is called to authenticate a request.
@@ -27,6 +31,6 @@ func IsRevokedApiKey(key string) bool {
func RevokeAPIKey(key string) {
revokeMu.Lock()
delete(revokedApiKeyList, key)
revokeMu.Unlock()
defer revokeMu.Unlock()
revokedApiKeyList[key] = true
}
+4 -1
View File
@@ -37,7 +37,10 @@ func validateUserInfo() {
if updateLoginType(user) {
updateUser = true
}
if user.Username == settings.Config.Auth.AdminUsername && settings.Config.Auth.AdminPassword != "" {
adminUser := settings.Config.Auth.AdminUsername
adminPass := settings.Config.Auth.AdminPassword
passwordEnabled := settings.Config.Auth.Methods.PasswordAuth.Enabled
if user.Username == adminUser && adminPass != "" && passwordEnabled {
logger.Info("Resetting admin user to default username and password.")
user.Permissions.Admin = true
user.Password = settings.Config.Auth.AdminPassword
+10 -5
View File
@@ -61,6 +61,7 @@ type OidcConfig struct {
LogoutRedirectUrl string `json:"logoutRedirectUrl"` // if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
CreateUser bool `json:"createUser"` // create user if not exists
AdminGroup string `json:"adminGroup"` // if set, users in this group will be granted admin privileges.
GroupsClaim string `json:"groupsClaim"` // the JSON field name to read groups from. Default is "groups"
Provider *oidc.Provider `json:"-"` // OIDC provider
Verifier *oidc.IDTokenVerifier `json:"-"` // OIDC verifier
}
@@ -68,12 +69,19 @@ type OidcConfig struct {
// ValidateOidcAuth processes the OIDC callback and retrieves user identity
func validateOidcAuth() error {
oidcCfg := &Config.Auth.Methods.OidcAuth // Use a pointer to modify the original config
if !oidcCfg.Enabled {
return errors.New("OIDC is not enabled")
if oidcCfg.GroupsClaim == "" {
oidcCfg.GroupsClaim = "groups"
}
if oidcCfg.UserIdentifier == "" {
oidcCfg.UserIdentifier = "preferred_username"
}
if oidcCfg.Scopes == "" {
oidcCfg.Scopes = "openid email profile"
}
if !oidcCfg.Enabled {
return errors.New("OIDC is not enabled")
}
ctx := context.Background()
@@ -99,9 +107,6 @@ func validateOidcAuth() error {
}
oidcCfg.Provider = provider
oidcCfg.Verifier = provider.Verifier(&oidc.Config{ClientID: oidcCfg.ClientID})
if oidcCfg.Scopes == "" {
oidcCfg.Scopes = "openid email profile"
}
return nil
}
+12 -7
View File
@@ -24,7 +24,7 @@ const (
)
func Initialize(configFile string) {
err := loadConfigWithDefaults(configFile)
err := loadConfigWithDefaults(configFile, false)
if err != nil {
logger.Error("unable to load config, waiting 5 seconds before exiting...")
time.Sleep(5 * time.Second) // allow sleep time before exiting to give docker/kubernetes time before restarting
@@ -162,9 +162,9 @@ func setupAuth(generate bool) {
logger.Warning("Configured with no authentication, this is not recommended.")
Config.Auth.AuthMethods = []string{"disabled"}
}
if Config.Auth.Methods.OidcAuth.Enabled {
if Config.Auth.Methods.OidcAuth.Enabled || generate {
err := validateOidcAuth()
if err != nil {
if err != nil && !generate {
logger.Fatalf("Error validating OIDC auth: %v", err)
}
logger.Info("OIDC Auth configured successfully")
@@ -203,8 +203,8 @@ func setupLogging() {
}
}
func loadConfigWithDefaults(configFile string) error {
Config = setDefaults()
func loadConfigWithDefaults(configFile string, generate bool) error {
Config = setDefaults(generate)
// Open and read the YAML file
yamlFile, err := os.Open(configFile)
if err != nil {
@@ -299,10 +299,11 @@ func loadEnvConfig() {
}
func setDefaults() Settings {
func setDefaults(generate bool) Settings {
// get number of CPUs available
numCpus := 4 // default to 4 CPUs if runtime.NumCPU() fails or is not available
if cpus := runtime.NumCPU(); cpus > 0 {
cpus := runtime.NumCPU()
if cpus > 0 && !generate {
numCpus = cpus
}
database := os.Getenv("FILEBROWSER_DATABASE")
@@ -351,6 +352,10 @@ func setDefaults() Settings {
Admin: false,
Api: false,
},
FileLoading: users.FileLoading{
MaxConcurrent: 10,
ChunkSize: 10, // 10MB
},
},
}
}
+8 -8
View File
@@ -34,7 +34,7 @@ func Test_setDefaults(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := setDefaults(); !reflect.DeepEqual(got, tt.want) {
if got := setDefaults(true); !reflect.DeepEqual(got, tt.want) {
t.Errorf("setDefaults() = %v, want %v", got, tt.want)
}
})
@@ -42,8 +42,8 @@ func Test_setDefaults(t *testing.T) {
}
func TestConfigLoadChanged(t *testing.T) {
defaultConfig := setDefaults()
err := loadConfigWithDefaults("./validConfig.yaml")
defaultConfig := setDefaults(true)
err := loadConfigWithDefaults("./validConfig.yaml", true)
if err != nil {
t.Fatalf("error loading config file: %v", err)
}
@@ -54,11 +54,11 @@ func TestConfigLoadChanged(t *testing.T) {
}
func TestConfigLoadEnvVars(t *testing.T) {
defaultConfig := setDefaults()
defaultConfig := setDefaults(true)
expectedKey := "MYKEY"
// mock environment variables
os.Setenv("FILEBROWSER_ONLYOFFICE_SECRET", expectedKey)
err := loadConfigWithDefaults("./validConfig.yaml")
err := loadConfigWithDefaults("./validConfig.yaml", true)
if err != nil {
t.Fatalf("error loading config file: %v", err)
}
@@ -72,8 +72,8 @@ func TestConfigLoadEnvVars(t *testing.T) {
}
func TestConfigLoadSpecificValues(t *testing.T) {
defaultConfig := setDefaults()
err := loadConfigWithDefaults("./validConfig.yaml")
defaultConfig := setDefaults(true)
err := loadConfigWithDefaults("./validConfig.yaml", true)
if err != nil {
t.Fatalf("error loading config file: %v", err)
}
@@ -94,7 +94,7 @@ func TestConfigLoadSpecificValues(t *testing.T) {
func TestInvalidConfig(t *testing.T) {
configFile := "./invalidConfig.yaml"
err := loadConfigWithDefaults(configFile)
err := loadConfigWithDefaults(configFile, true)
if err == nil {
t.Fatalf("expected error loading config file %s, got nil", configFile)
}
+8 -1
View File
@@ -79,6 +79,13 @@ func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) {
}
switch v.Kind() {
case reflect.String:
return &yaml.Node{
Kind: yaml.ScalarNode,
Tag: "!!str",
Value: v.String(),
Style: yaml.DoubleQuotedStyle,
}, nil
case reflect.Struct:
rt := v.Type()
typeName := rt.Name()
@@ -167,7 +174,7 @@ func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) {
}
func GenerateYaml() {
_ = loadConfigWithDefaults("")
_ = loadConfigWithDefaults("", true)
Config.Server.Sources = []Source{
{
Path: ".",
+2 -1
View File
@@ -121,6 +121,7 @@ type ExternalLink struct {
// UserDefaults is a type that holds the default values
// for some fields on User.
type UserDefaults 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
@@ -144,5 +145,5 @@ type UserDefaults struct {
LoginMethod string `json:"loginMethod,omitempty"` // login method to use: eg. password, proxy, oidc
DisableUpdateNotifications bool `json:"disableUpdateNotifications"` // disable update notifications banner for admin users
DeleteWithoutConfirming bool `json:"deleteWithoutConfirming"` // delete files without confirmation
FileLoading users.FileLoading `json:"fileLoading"` // upload and download settings
}
+21 -19
View File
@@ -86,26 +86,28 @@ func quickSetup(store *Storage) {
utils.CheckErr("store.Settings.Save", err)
err = store.Settings.SaveServer(&settings.Config.Server)
utils.CheckErr("store.Settings.SaveServer", err)
user := &users.User{}
settings.ApplyUserDefaults(user)
user.Username = settings.Config.Auth.AdminUsername
if settings.Config.Auth.AdminPassword == "" {
settings.Config.Auth.AdminPassword = "admin"
if settings.Config.Auth.Methods.PasswordAuth.Enabled {
user := &users.User{}
settings.ApplyUserDefaults(user)
user.Username = settings.Config.Auth.AdminUsername
if settings.Config.Auth.AdminPassword == "" {
settings.Config.Auth.AdminPassword = "admin"
}
user.Password = settings.Config.Auth.AdminPassword
user.Permissions.Admin = true
user.Scopes = []users.SourceScope{}
for _, val := range settings.Config.Server.Sources {
user.Scopes = append(user.Scopes, users.SourceScope{
Name: val.Path, // backend name is path
Scope: "",
})
}
user.LockPassword = false
user.Permissions = settings.AdminPerms()
logger.Debugf("Creating user as admin: %v %v", user.Username, user.Password)
err = store.Users.Save(user, true, true)
utils.CheckErr("store.Users.Save", err)
}
user.Password = settings.Config.Auth.AdminPassword
user.Permissions.Admin = true
user.Scopes = []users.SourceScope{}
for _, val := range settings.Config.Server.Sources {
user.Scopes = append(user.Scopes, users.SourceScope{
Name: val.Path, // backend name is path
Scope: "",
})
}
user.LockPassword = false
user.Permissions = settings.AdminPerms()
logger.Debugf("Creating user as admin: %v %v", user.Username, user.Password)
err = store.Users.Save(user, true, true)
utils.CheckErr("store.Users.Save", err)
}
// create new user
+26 -20
View File
@@ -77,27 +77,33 @@ type SourceScope struct {
// json tags must match variable name with smaller case first letter
type NonAdminEditable struct {
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
DeleteWithoutConfirming bool `json:"deleteWithoutConfirming"` // delete files without confirmation
Preview Preview `json:"preview"`
StickySidebar bool `json:"stickySidebar"` // keep sidebar open when navigating
DarkMode bool `json:"darkMode"` // should dark mode be enabled
Password string `json:"password,omitempty"`
Locale string `json:"locale"` // language to use: eg. de, en, or fr
ViewMode string `json:"viewMode"` // view mode to use: eg. normal, list, grid, or compact
SingleClick bool `json:"singleClick"` // open directory on single click, also enables middle click to open in new tab
Sorting Sorting `json:"sorting"`
ShowHidden bool `json:"showHidden"` // show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
DateFormat bool `json:"dateFormat"` // when false, the date is relative, when true, the date is an exact timestamp
GallerySize int `json:"gallerySize"` // 0-9 - the size of the gallery thumbnails
ThemeColor string `json:"themeColor"` // theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
QuickDownload bool `json:"quickDownload"` // show icon to download in one click
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // comma separated list of file extensions to disable onlyoffice preview for
DisableOfficePreviewExt string `json:"disableOfficePreviewExt"` // comma separated list of file extensions to disable office preview for
DisableUpdateNotifications bool `json:"disableUpdateNotifications"` // disable update notifications
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
DeleteWithoutConfirming bool `json:"deleteWithoutConfirming"` // delete files without confirmation
Preview Preview `json:"preview"`
StickySidebar bool `json:"stickySidebar"` // keep sidebar open when navigating
DarkMode bool `json:"darkMode"` // should dark mode be enabled
Password string `json:"password,omitempty"`
Locale string `json:"locale"` // language to use: eg. de, en, or fr
ViewMode string `json:"viewMode"` // view mode to use: eg. normal, list, grid, or compact
SingleClick bool `json:"singleClick"` // open directory on single click, also enables middle click to open in new tab
Sorting Sorting `json:"sorting"`
ShowHidden bool `json:"showHidden"` // show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
DateFormat bool `json:"dateFormat"` // when false, the date is relative, when true, the date is an exact timestamp
GallerySize int `json:"gallerySize"` // 0-9 - the size of the gallery thumbnails
ThemeColor string `json:"themeColor"` // theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
QuickDownload bool `json:"quickDownload"` // show icon to download in one click
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // comma separated list of file extensions to disable onlyoffice preview for
DisableOfficePreviewExt string `json:"disableOfficePreviewExt"` // comma separated list of file extensions to disable office preview for
DisableUpdateNotifications bool `json:"disableUpdateNotifications"` // disable update notifications
FileLoading FileLoading `json:"fileLoading"` // upload and download settings
}
type FileLoading struct {
MaxConcurrent int `json:"maxConcurrentUpload"`
ChunkSize int `json:"uploadChunkSizeMb"`
}
var PublicUser = User{
+8 -8
View File
@@ -19,11 +19,11 @@ require (
github.com/stretchr/testify v1.10.0
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.4
golang.org/x/crypto v0.39.0
golang.org/x/image v0.28.0
golang.org/x/mod v0.25.0
golang.org/x/crypto v0.40.0
golang.org/x/image v0.29.0
golang.org/x/mod v0.26.0
golang.org/x/oauth2 v0.30.0
golang.org/x/sys v0.33.0
golang.org/x/sys v0.34.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -108,7 +108,7 @@ require (
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/golang/geo v0.0.0-20250704082316-8c117c302233 // indirect
github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
github.com/golangci/go-printf-func-name v0.1.0 // indirect
@@ -235,9 +235,9 @@ require (
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
+16 -16
View File
@@ -285,8 +285,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20250704082316-8c117c302233 h1:awuPENW9GP/dvbO712KkZVg88z8CFNo0XPoEj0zT/1Q=
github.com/golang/geo v0.0.0-20250704082316-8c117c302233/go.mod h1:Vaw7L5b+xa3Rj4/pRtrQkymn3lSBRB/NAEdbF9YEVLA=
github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4 h1:vCeHcs8N7MOccOOsOVIy1xcYu+kBkA4J5urTgigww7c=
github.com/golang/geo v0.0.0-20250707181242-c5087ca84cf4/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -743,8 +743,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -763,8 +763,8 @@ golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rP
golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/image v0.29.0 h1:HcdsyR4Gsuys/Axh0rDEmlBmB68rW1U9BUdB3UVHsas=
golang.org/x/image v0.29.0/go.mod h1:RVJROnf3SLK8d26OW91j4FrIHGbsJ8QnbEocVTOWQDA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -792,8 +792,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -838,8 +838,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -863,8 +863,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -920,8 +920,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -942,8 +942,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+4 -1
View File
@@ -13,6 +13,7 @@ import (
"github.com/golang-jwt/jwt/v4/request"
"golang.org/x/crypto/bcrypt"
"github.com/gtsteffaniak/filebrowser/backend/auth"
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
@@ -116,14 +117,16 @@ func loginHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (in
return printToken(w, r, d.user) // Pass the data object
}
// logoutHandler handles user logout, specifically used for OIDC or proxy users.
// logoutHandler handles user logout
// @Summary User Logout
// @Description Returns a logout URL for the frontend to redirect to.
// @Tags Auth
// @Produce json
// @Param auth query string false "JWT token"
// @Success 200 {object} map[string]string "{"logoutUrl": "http://..."}"
// @Router /api/auth/logout [post]
func logoutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
defer auth.RevokeAPIKey(d.token)
logoutUrl := fmt.Sprintf("%vlogin", config.Server.BaseURL) // Default fallback
if d.user.LoginMethod == users.LoginMethodProxy {
proxyRedirectUrl := config.Auth.Methods.ProxyAuth.LogoutRedirectUrl
+3 -2
View File
@@ -37,8 +37,9 @@ func (d dirFS) Open(name string) (fs.File, error) {
}
var (
store *storage.Storage
config *settings.Settings
store *storage.Storage
config *settings.Settings
//fileCache diskcache.Interface
assetFs fs.FS
)
+1 -1
View File
@@ -186,7 +186,7 @@ func withUserHelper(fn handleFunc) handleFunc {
if !token.Valid {
return http.StatusUnauthorized, fmt.Errorf("invalid token")
}
if auth.IsRevokedApiKey(tk.Key) || tk.Expires < time.Now().Unix() {
if auth.IsRevokedApiKey(data.token) || tk.Expires < time.Now().Unix() {
return http.StatusUnauthorized, fmt.Errorf("token expired or revoked")
}
// Check if the token is about to expire and send a header to renew it
+58 -5
View File
@@ -2,6 +2,7 @@ package http
import (
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
@@ -27,7 +28,53 @@ type userInfo struct {
Email string `json:"email"`
Sub string `json:"sub"`
Phone string `json:"phone_number"`
Groups []string `json:"groups"`
Groups []string `json:"-"` // Handled manually by userInfoUnmarshaller
}
// userInfoUnmarshaller is a custom unmarshaller that handles configurable groups claim
type userInfoUnmarshaller struct {
userInfo *userInfo
groupsClaim string
}
// UnmarshalJSON implements the json.Unmarshaler interface
func (u *userInfoUnmarshaller) UnmarshalJSON(data []byte) error {
// First, unmarshal the basic userInfo fields
if err := json.Unmarshal(data, u.userInfo); err != nil {
return err
}
// Parse the JSON to access the groups claim field
var rawData map[string]interface{}
if err := json.Unmarshal(data, &rawData); err != nil {
return err
}
// Look for the groups claim using the configured field name
if groupsValue, exists := rawData[u.groupsClaim]; exists {
switch v := groupsValue.(type) {
case []interface{}:
// It's already an array, convert to []string
groups := make([]string, len(v))
for i, group := range v {
if str, ok := group.(string); ok {
groups[i] = str
}
}
u.userInfo.Groups = groups
case string:
// It's a string, split by commas
if v != "" {
u.userInfo.Groups = strings.Split(v, ",")
// Trim whitespace from each group
for i, group := range u.userInfo.Groups {
u.userInfo.Groups[i] = strings.TrimSpace(group)
}
}
}
}
return nil
}
// oidcLoginHandler initiates OIDC login.
@@ -127,6 +174,12 @@ func oidcCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestConte
claimsFromIDToken := false // Flag to track if we successfully got claims from ID token
loginUsername := "" // Variable to hold the login username
// Create custom unmarshaller for userInfo
userInfoUnmarshaller := &userInfoUnmarshaller{
userInfo: &userdata,
groupsClaim: oidcCfg.GroupsClaim,
}
// --- Attempt to process ID Token ---
if ok && rawIDToken != "" {
logger.Debug("ID token found in token response, attempting verification.")
@@ -139,9 +192,9 @@ func oidcCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestConte
logger.Debugf("failed to verify ID token: %v. This might be expected, falling back to UserInfo endpoint.", err)
// Verification failed, claimsFromIDToken remains false
} else {
// Decode the ID token claims into a map to handle arbitrary structure
// Decode the ID token claims using custom unmarshaller
// This is where the JWE unmarshalling error occurs if the token is encrypted
if err := idToken.Claims(&userdata); err != nil {
if err := idToken.Claims(userInfoUnmarshaller); err != nil {
logger.Warningf("failed to decode ID token claims: %v. Falling back to UserInfo endpoint.", err)
// Claims decoding failed, claimsFromIDToken remains false
} else {
@@ -188,9 +241,9 @@ func oidcCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestConte
logger.Errorf("failed to fetch user info from endpoint: %v", err)
return http.StatusInternalServerError, fmt.Errorf("failed to fetch user info from endpoint: %v", err)
}
// Decode the UserInfo response directly into the userdata struct
// Decode the UserInfo response using custom unmarshaller
// The UserInfo endpoint is expected to return standard JSON
if err := userInfoResp.Claims(&userdata); err != nil {
if err := userInfoResp.Claims(userInfoUnmarshaller); err != nil {
logger.Errorf("failed to decode user info from endpoint: %v", err)
return http.StatusInternalServerError, fmt.Errorf("failed to decode user info from endpoint: %v", err)
}
+86
View File
@@ -0,0 +1,86 @@
package http
import (
"encoding/json"
"reflect"
"testing"
)
func TestUserInfoUnmarshaller(t *testing.T) {
tests := []struct {
name string
jsonData string
groupsClaim string
expected userInfo
}{
{
name: "standard groups array",
jsonData: `{"name":"John","email":"john@example.com","groups":["admin","users"]}`,
groupsClaim: "groups",
expected: userInfo{
Name: "John",
Email: "john@example.com",
Groups: []string{"admin", "users"},
},
},
{
name: "custom groups claim name",
jsonData: `{"name":"Jane","email":"jane@example.com","roles":["admin","users"]}`,
groupsClaim: "roles",
expected: userInfo{
Name: "Jane",
Email: "jane@example.com",
Groups: []string{"admin", "users"},
},
},
{
name: "groups as comma-separated string",
jsonData: `{"name":"Bob","email":"bob@example.com","groups":"admin, users, guests"}`,
groupsClaim: "groups",
expected: userInfo{
Name: "Bob",
Email: "bob@example.com",
Groups: []string{"admin", "users", "guests"},
},
},
{
name: "no groups field",
jsonData: `{"name":"Alice","email":"alice@example.com"}`,
groupsClaim: "groups",
expected: userInfo{
Name: "Alice",
Email: "alice@example.com",
Groups: nil,
},
},
{
name: "empty groups array",
jsonData: `{"name":"Charlie","email":"charlie@example.com","groups":[]}`,
groupsClaim: "groups",
expected: userInfo{
Name: "Charlie",
Email: "charlie@example.com",
Groups: []string{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var userdata userInfo
unmarshaller := &userInfoUnmarshaller{
userInfo: &userdata,
groupsClaim: tt.groupsClaim,
}
err := json.Unmarshal([]byte(tt.jsonData), unmarshaller)
if err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if !reflect.DeepEqual(userdata, tt.expected) {
t.Errorf("Expected %+v, got %+v", tt.expected, userdata)
}
})
}
}
+2 -1
View File
@@ -143,6 +143,7 @@ func previewHelperFunc(w http.ResponseWriter, r *http.Request, d *requestContext
return http.StatusInternalServerError, err
}
w.Header().Set("Cache-Control", "private")
http.ServeContent(w, r, d.fileInfo.RealPath, d.fileInfo.ModTime, bytes.NewReader(previewImg))
w.Header().Set("Content-Type", "image/jpeg")
http.ServeContent(w, r, d.fileInfo.Name+"-preview.jpg", d.fileInfo.ModTime, bytes.NewReader(previewImg))
return 0, nil
}
+1 -1
View File
@@ -94,7 +94,7 @@ func healthHandler(w http.ResponseWriter, r *http.Request) {
// @Failure 404 {object} map[string]string "File not found"
// @Failure 415 {object} map[string]string "Unsupported file type for preview"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/preview [get]
// @Router /api/public/preview [get]
func publicPreviewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
if config.Server.DisablePreviews {
return http.StatusNotImplemented, fmt.Errorf("preview is disabled")
+113 -5
View File
@@ -2,7 +2,10 @@ package http
import (
"context"
"crypto/md5"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"os"
@@ -163,13 +166,16 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
// decode url encoded source name
source, err = url.QueryUnescape(source)
if err != nil {
logger.Debugf("invalid source encoding: %v", err)
return http.StatusBadRequest, fmt.Errorf("invalid source encoding: %v", err)
}
if !d.user.Permissions.Modify {
logger.Debugf("user is not allowed to create or modify")
return http.StatusForbidden, fmt.Errorf("user is not allowed to create or modify")
}
userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source)
if err != nil {
logger.Debugf("error getting scope from source name: %v", err)
return http.StatusForbidden, err
}
fileOpts := iteminfo.FileOptions{
@@ -178,14 +184,120 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
Modify: d.user.Permissions.Modify,
Expand: false,
}
idx := indexing.GetIndex(source)
if idx == nil {
return http.StatusNotFound, fmt.Errorf("source %s not found", source)
}
realPath, _, _ := idx.GetRealPath(userscope, path)
// Directories creation on POST.
if strings.HasSuffix(path, "/") {
err = files.WriteDirectory(fileOpts)
if err != nil {
logger.Debugf("error writing directory: %v", err)
return errToStatus(err), err
}
return http.StatusOK, nil
}
// Handle Chunked Uploads
chunkOffsetStr := r.Header.Get("X-File-Chunk-Offset")
if chunkOffsetStr != "" {
var offset int64
offset, err = strconv.ParseInt(chunkOffsetStr, 10, 64)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid chunk offset: %v", err)
}
var totalSize int64
totalSizeStr := r.Header.Get("X-File-Total-Size")
totalSize, err = strconv.ParseInt(totalSizeStr, 10, 64)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid total size: %v", err)
}
// On the first chunk, check for conflicts or handle override
if offset == 0 {
var fileInfo iteminfo.ExtendedFileInfo
fileInfo, err = files.FileInfoFaster(fileOpts)
if err == nil { // File exists
if r.URL.Query().Get("override") != "true" {
logger.Debugf("Resource already exists: %v", fileInfo.RealPath)
return http.StatusConflict, nil
}
// If overriding, delete existing thumbnails
preview.DelThumbs(r.Context(), fileInfo)
}
}
// Use a temporary file in the cache directory for chunks.
// Create a unique name for the temporary file to avoid collisions.
hasher := md5.New() //nolint:gosec
hasher.Write([]byte(realPath))
uploadID := hex.EncodeToString(hasher.Sum(nil))
tempFilePath := filepath.Join(settings.Config.Server.CacheDir, "uploads", uploadID)
if err = os.MkdirAll(filepath.Dir(tempFilePath), 0755); err != nil {
return http.StatusInternalServerError, fmt.Errorf("could not create temp dir: %v", err)
}
// Create or open the temporary file
var outFile *os.File
outFile, err = os.OpenFile(tempFilePath, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("could not open temp file: %v", err)
}
defer outFile.Close()
// Seek to the correct offset to write the chunk
_, err = outFile.Seek(offset, 0)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("could not seek in temp file: %v", err)
}
// Write the request body (the chunk) to the file
var chunkSize int64
chunkSize, err = io.Copy(outFile, r.Body)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("could not write chunk to temp file: %v", err)
}
// check if the file is complete
if (offset + chunkSize) >= totalSize {
// close file before renaming
outFile.Close()
// Move the completed file from the temp location to the final destination
err = os.Rename(tempFilePath, fileOpts.Path)
if err != nil {
// If rename fails (e.g., cross-device link), fall back to copy and delete.
var in *os.File
in, err = os.Open(tempFilePath)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("could not open temp file for copying: %v", err)
}
defer in.Close()
var out *os.File
out, err = os.Create(realPath)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("could not create destination file for copying: %v", err)
}
defer out.Close()
if _, err = io.Copy(out, in); err != nil {
return http.StatusInternalServerError, fmt.Errorf("could not copy temp file to destination: %v", err)
}
// Manually close files before removing source
in.Close()
out.Close()
if err = os.Remove(tempFilePath); err != nil {
logger.Errorf("failed to remove temporary upload file: %s", tempFilePath)
}
}
}
return http.StatusOK, nil
}
// Non-chunked upload logic (original code)
fileInfo, err := files.FileInfoFaster(fileOpts)
if err == nil {
if r.URL.Query().Get("override") != "true" {
@@ -193,15 +305,11 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
return http.StatusConflict, nil
}
// Permission for overwriting the file
if !d.user.Permissions.Modify {
return http.StatusForbidden, nil
}
preview.DelThumbs(r.Context(), fileInfo)
}
err = files.WriteFile(fileOpts, r.Body)
if err != nil {
logger.Debugf("error writing file: %v", err)
return errToStatus(err), err
}
+9
View File
@@ -20,3 +20,12 @@ func getPartitionSize(path string) (uint64, error) {
total := stat.Blocks * uint64(stat.Bsize)
return total, nil
}
func getFileDetails(sys any) (uint64, uint64, uint64, bool) {
if stat, ok := sys.(*syscall.Stat_t); ok {
// Use allocated size for `du`-like behavior
realSize := uint64(stat.Blocks * 512)
return realSize, uint64(stat.Nlink), stat.Ino, true
}
return 0, 1, 0, false
}
+7
View File
@@ -44,3 +44,10 @@ func getPartitionSize(path string) (uint64, error) {
}
return totalBytes, nil
}
func getFileDetails(sys any) (uint64, uint64, uint64, bool) {
// On Windows, os.FileInfo.Sys() returns *syscall.Win32FileAttributeData,
// which does not contain inode or link count information.
// We return false to indicate that we should use the fallback mechanism.
return 0, 1, 0, false
}
+47 -2
View File
@@ -47,6 +47,9 @@ type Index struct {
mock bool
mu sync.RWMutex
hasIndex bool
FoundHardLinks map[string]uint64 `json:"-"` // hardlink path -> size
processedInodes map[uint64]bool `json:"-"`
totalSize uint64 `json:"-"`
}
var (
@@ -73,6 +76,8 @@ func Initialize(source settings.Source, mock bool) {
Source: source,
Directories: make(map[string]*iteminfo.FileInfo),
DirectoriesLedger: make(map[string]bool),
processedInodes: make(map[uint64]bool),
FoundHardLinks: make(map[string]uint64),
}
newIndex.ReducedIndex = ReducedIndex{
Status: "indexing",
@@ -289,10 +294,13 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
dirInfos = append(dirInfos, *itemInfo)
idx.NumDirs++
} else {
size, shouldCountSize := idx.handleFile(file, fullCombined)
itemInfo.DetectType(fullCombined, false)
itemInfo.Size = file.Size()
itemInfo.Size = int64(size)
fileInfos = append(fileInfos, *itemInfo)
totalSize += itemInfo.Size
if shouldCountSize {
totalSize += itemInfo.Size
}
idx.NumFiles++
}
}
@@ -537,3 +545,40 @@ func (idx *Index) SetStatus(status IndexStatus) {
idx.mu.Unlock()
idx.SendSourceUpdateEvent()
}
func (idx *Index) handleFile(file os.FileInfo, fullCombined string) (size uint64, shouldCountSize bool) {
var realSize uint64
var nlink uint64 = 1
var ino uint64 = 0
canUseSyscall := false
if sys := file.Sys(); sys != nil {
realSize, nlink, ino, canUseSyscall = getFileDetails(sys)
}
if !canUseSyscall {
// Fallback for non-unix systems or if syscall info is unavailable
realSize = uint64(file.Size())
}
if nlink > 1 {
// It's a hard link
idx.mu.Lock()
defer idx.mu.Unlock()
if _, exists := idx.processedInodes[ino]; exists {
// Already seen, don't count towards global total, or directory total.
return realSize, false
}
// First time seeing this inode.
idx.processedInodes[ino] = true
idx.FoundHardLinks[fullCombined] = realSize
idx.totalSize += realSize
return realSize, true // Count size for directory total.
}
// It's a regular file.
idx.mu.Lock()
idx.totalSize += realSize
idx.mu.Unlock()
return realSize, true // Count size.
}
+7
View File
@@ -137,8 +137,12 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
logger.Debugf("Starting quick scan for [%v]", idx.Name)
} else {
logger.Debugf("Starting full scan for [%v]", idx.Name)
idx.mu.Lock()
idx.NumDirs = 0
idx.NumFiles = 0
idx.processedInodes = make(map[uint64]bool)
idx.FoundHardLinks = make(map[string]uint64)
idx.mu.Unlock()
}
startTime := time.Now()
idx.FilesChangedDuringIndexing = false
@@ -156,6 +160,9 @@ func (idx *Index) RunIndexing(origin string, quick bool) {
logger.Debugf("Time spent indexing [%v]: %v seconds", idx.Name, idx.QuickScanTime)
} else {
idx.FullScanTime = int(time.Since(startTime).Seconds())
idx.mu.Lock()
idx.DiskUsed = idx.totalSize
idx.mu.Unlock()
// update smart indexing
if idx.FullScanTime < 3 || idx.NumDirs < 10000 {
idx.Assessment = "simple"
+139
View File
@@ -25,6 +25,14 @@ const docTemplate = `{
"Auth"
],
"summary": "User Logout",
"parameters": [
{
"type": "string",
"description": "JWT token",
"name": "auth",
"in": "query"
}
],
"responses": {
"200": {
"description": "{\"logoutUrl\": \"http://...\"}",
@@ -468,6 +476,98 @@ const docTemplate = `{
}
},
"/api/preview": {
"get": {
"description": "Returns a preview image based on the requested path and size.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Resources"
],
"summary": "Get image preview",
"parameters": [
{
"type": "string",
"description": "File path of the image to preview",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Preview size ('small' or 'large'). Default is based on server config.",
"name": "size",
"in": "query"
}
],
"responses": {
"200": {
"description": "Preview image content",
"schema": {
"type": "file"
}
},
"202": {
"description": "Download permissions required",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid request path",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "File not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"415": {
"description": "Unsupported file type for preview",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"501": {
"description": "Preview generation not implemented",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/public/preview": {
"get": {
"description": "Returns a preview image based on the requested path and size.",
"consumes": [
@@ -2144,6 +2244,10 @@ const docTemplate = `{
"description": "whether to enable OIDC authentication",
"type": "boolean"
},
"groupsClaim": {
"description": "the JSON field name to read groups from. Default is \"groups\"",
"type": "string"
},
"issuerUrl": {
"description": "authorization URL of the OIDC provider",
"type": "string"
@@ -2459,6 +2563,18 @@ const docTemplate = `{
"description": "disable update notifications banner for admin users",
"type": "boolean"
},
"editorQuickSave": {
"description": "show quick save button in editor",
"type": "boolean"
},
"fileLoading": {
"description": "upload and download settings",
"allOf": [
{
"$ref": "#/definitions/users.FileLoading"
}
]
},
"gallerySize": {
"description": "0-9 - the size of the gallery thumbnails",
"type": "integer"
@@ -2561,6 +2677,17 @@ const docTemplate = `{
}
}
},
"users.FileLoading": {
"type": "object",
"properties": {
"maxConcurrentUpload": {
"type": "integer"
},
"uploadChunkSizeMb": {
"type": "integer"
}
}
},
"users.LoginMethod": {
"type": "string",
"enum": [
@@ -2697,6 +2824,18 @@ const docTemplate = `{
"description": "disable update notifications",
"type": "boolean"
},
"editorQuickSave": {
"description": "show quick save button in editor",
"type": "boolean"
},
"fileLoading": {
"description": "upload and download settings",
"allOf": [
{
"$ref": "#/definitions/users.FileLoading"
}
]
},
"gallerySize": {
"description": "0-9 - the size of the gallery thumbnails",
"type": "integer"
+139
View File
@@ -14,6 +14,14 @@
"Auth"
],
"summary": "User Logout",
"parameters": [
{
"type": "string",
"description": "JWT token",
"name": "auth",
"in": "query"
}
],
"responses": {
"200": {
"description": "{\"logoutUrl\": \"http://...\"}",
@@ -457,6 +465,98 @@
}
},
"/api/preview": {
"get": {
"description": "Returns a preview image based on the requested path and size.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Resources"
],
"summary": "Get image preview",
"parameters": [
{
"type": "string",
"description": "File path of the image to preview",
"name": "path",
"in": "query",
"required": true
},
{
"type": "string",
"description": "Preview size ('small' or 'large'). Default is based on server config.",
"name": "size",
"in": "query"
}
],
"responses": {
"200": {
"description": "Preview image content",
"schema": {
"type": "file"
}
},
"202": {
"description": "Download permissions required",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid request path",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "File not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"415": {
"description": "Unsupported file type for preview",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"501": {
"description": "Preview generation not implemented",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/api/public/preview": {
"get": {
"description": "Returns a preview image based on the requested path and size.",
"consumes": [
@@ -2133,6 +2233,10 @@
"description": "whether to enable OIDC authentication",
"type": "boolean"
},
"groupsClaim": {
"description": "the JSON field name to read groups from. Default is \"groups\"",
"type": "string"
},
"issuerUrl": {
"description": "authorization URL of the OIDC provider",
"type": "string"
@@ -2448,6 +2552,18 @@
"description": "disable update notifications banner for admin users",
"type": "boolean"
},
"editorQuickSave": {
"description": "show quick save button in editor",
"type": "boolean"
},
"fileLoading": {
"description": "upload and download settings",
"allOf": [
{
"$ref": "#/definitions/users.FileLoading"
}
]
},
"gallerySize": {
"description": "0-9 - the size of the gallery thumbnails",
"type": "integer"
@@ -2550,6 +2666,17 @@
}
}
},
"users.FileLoading": {
"type": "object",
"properties": {
"maxConcurrentUpload": {
"type": "integer"
},
"uploadChunkSizeMb": {
"type": "integer"
}
}
},
"users.LoginMethod": {
"type": "string",
"enum": [
@@ -2686,6 +2813,18 @@
"description": "disable update notifications",
"type": "boolean"
},
"editorQuickSave": {
"description": "show quick save button in editor",
"type": "boolean"
},
"fileLoading": {
"description": "upload and download settings",
"allOf": [
{
"$ref": "#/definitions/users.FileLoading"
}
]
},
"gallerySize": {
"description": "0-9 - the size of the gallery thumbnails",
"type": "integer"
+91
View File
@@ -304,6 +304,9 @@ definitions:
enabled:
description: whether to enable OIDC authentication
type: boolean
groupsClaim:
description: the JSON field name to read groups from. Default is "groups"
type: string
issuerUrl:
description: authorization URL of the OIDC provider
type: string
@@ -542,6 +545,13 @@ definitions:
disableUpdateNotifications:
description: disable update notifications banner for admin users
type: boolean
editorQuickSave:
description: show quick save button in editor
type: boolean
fileLoading:
allOf:
- $ref: '#/definitions/users.FileLoading'
description: upload and download settings
gallerySize:
description: 0-9 - the size of the gallery thumbnails
type: integer
@@ -619,6 +629,13 @@ definitions:
name:
type: string
type: object
users.FileLoading:
properties:
maxConcurrentUpload:
type: integer
uploadChunkSizeMb:
type: integer
type: object
users.LoginMethod:
enum:
- password
@@ -718,6 +735,13 @@ definitions:
disableUpdateNotifications:
description: disable update notifications
type: boolean
editorQuickSave:
description: show quick save button in editor
type: boolean
fileLoading:
allOf:
- $ref: '#/definitions/users.FileLoading'
description: upload and download settings
gallerySize:
description: 0-9 - the size of the gallery thumbnails
type: integer
@@ -788,6 +812,11 @@ paths:
/api/auth/logout:
post:
description: Returns a logout URL for the frontend to redirect to.
parameters:
- description: JWT token
in: query
name: auth
type: string
produces:
- application/json
responses:
@@ -1083,6 +1112,68 @@ paths:
tags:
- Auth
/api/preview:
get:
consumes:
- application/json
description: Returns a preview image based on the requested path and size.
parameters:
- description: File path of the image to preview
in: query
name: path
required: true
type: string
- description: Preview size ('small' or 'large'). Default is based on server
config.
in: query
name: size
type: string
produces:
- application/json
responses:
"200":
description: Preview image content
schema:
type: file
"202":
description: Download permissions required
schema:
additionalProperties:
type: string
type: object
"400":
description: Invalid request path
schema:
additionalProperties:
type: string
type: object
"404":
description: File not found
schema:
additionalProperties:
type: string
type: object
"415":
description: Unsupported file type for preview
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
"501":
description: Preview generation not implemented
schema:
additionalProperties:
type: string
type: object
summary: Get image preview
tags:
- Resources
/api/public/preview:
get:
consumes:
- application/json
View File
Whitespace-only changes.
+28 -23
View File
@@ -1,5 +1,5 @@
server:
numImageProcessors: 16 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
numImageProcessors: 4 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
socket: "" # socket to listen on
tlsKey: "" # path to TLS key
tlsCert: "" # path to TLS cert
@@ -7,19 +7,19 @@ server:
disablePreviewResize: false # disable resizing of previews for faster loading over slow connections
disableTypeDetectionByHeader: false # disable type detection by header, useful if filesystem is slow.
port: 80 # port to listen on
baseURL: / # base URL for the server, the subpath that the server is running on.
baseURL: "/" # base URL for the server, the subpath that the server is running on.
logging:
- levels: "" # separated list of log levels to enable. (eg. "info|warning|error|debug")
apiLevels: "" # separated list of log levels to enable for the API. (eg. "info|warning|error")
output: stdout # output location. (eg. "stdout" or "path/to/file.log")
output: "stdout" # output location. (eg. "stdout" or "path/to/file.log")
noColors: false # disable colors in the output
json: false # output in json format, currently not supported
utc: false # use UTC time in the output instead of local time
debugMedia: false # output ffmpeg stdout for media integration -- careful can produces lots of output!
database: database.db # path to the database file
database: "database.db" # path to the database file
sources: # validate:required,dive
- path: /relative/or/absolute/path # file system path. (Can be relative) validate:required
name: backend # display name
- path: "/relative/or/absolute/path" # file system path. (Can be relative) validate:required
name: "backend" # display name
config:
indexingIntervalMinutes: 0 # optional manual overide interval in seconds to re-index the source
disableIndexing: false # disable the indexing of this source
@@ -37,12 +37,12 @@ server:
include: # include files and folders from indexing, if exclude is not set
rootFolders: []
rootFiles: []
defaultUserScope: / # default "/" should match folders under path
defaultUserScope: "/" # default "/" should match folders under path
defaultEnabled: false # should be added as a default source for new users?
createUserDir: false # create a user directory for each user
externalUrl: "" # used by share links if set
internalUrl: "" # used by integrations if set, this is the url that an integration service will use to communicate with filebrowser
cacheDir: tmp # path to the cache directory, used for thumbnails and other cached files
cacheDir: "tmp" # path to the cache directory, used for thumbnails and other cached files
maxArchiveSize: 50 # max pre-archive combined size of files/folder that are allowed to be archived (in GB)
auth:
tokenExpirationHours: 2 # the number of hours until the token expires. Default is 2 hours.
@@ -67,43 +67,45 @@ auth:
clientId: "" # client id of the OIDC application
clientSecret: "" # client secret of the OIDC application
issuerUrl: "" # authorization URL of the OIDC provider
scopes: "" # scopes to request from the OIDC provider
userIdentifier: "" # the field value to use as the username. Default is "preferred_username", can also be "email" or "username", or "phone"
scopes: "openid email profile" # scopes to request from the OIDC provider
userIdentifier: "preferred_username" # the field value to use as the username. Default is "preferred_username", can also be "email" or "username", or "phone"
disableVerifyTLS: false # disable TLS verification for the OIDC provider. This is insecure and should only be used for testing.
logoutRedirectUrl: "" # if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
createUser: false # create user if not exists
adminGroup: "" # if set, users in this group will be granted admin privileges.
groupsClaim: "groups" # the JSON field name to read groups from. Default is "groups"
key: "" # the key used to sign the JWT tokens. If not set, a random key will be generated.
adminUsername: admin # the username of the admin user. If not set, the default is "admin".
adminPassword: admin # the password of the admin user. If not set, the default is "admin".
adminUsername: "admin" # the username of the admin user. If not set, the default is "admin".
adminPassword: "admin" # the password of the admin user. If not set, the default is "admin".
totpSecret: "" # secret used to encrypt TOTP secrets
frontend:
name: FileBrowser Quantum # display name
name: "FileBrowser Quantum" # display name
disableDefaultLinks: false # disable default links in the sidebar
disableUsedPercentage: false # disable used percentage for the sources in the sidebar
externalLinks:
- text: (untracked) # the text to display on the link validate:required
title: untracked # the title to display on hover
url: https://github.com/gtsteffaniak/filebrowser/releases/ # the url to link to validate:required
- text: Help # the text to display on the link validate:required
- text: "(untracked)" # the text to display on the link validate:required
title: "untracked" # the title to display on hover
url: "https://github.com/gtsteffaniak/filebrowser/releases/" # the url to link to validate:required
- text: "Help" # the text to display on the link validate:required
title: "" # the title to display on hover
url: https://github.com/gtsteffaniak/filebrowser/wiki # the url to link to validate:required
url: "https://github.com/gtsteffaniak/filebrowser/wiki" # the url to link to validate:required
disableNavButtons: false # disable the nav buttons in the sidebar
userDefaults:
editorQuickSave: false # show quick save button in editor
hideSidebarFileActions: false # hide the file actions in the sidebar
disableQuickToggles: false # disable the quick toggles in the sidebar
disableSearchOptions: false # disable the search options in the search bar
stickySidebar: true # keep sidebar open when navigating
darkMode: true # should dark mode be enabled
locale: en # language to use: eg. de, en, or fr
viewMode: normal # view mode to use: eg. normal, list, grid, or compact
locale: "en" # language to use: eg. de, en, or fr
viewMode: "normal" # view mode to use: eg. normal, list, grid, or compact
singleClick: false # open directory on single click, also enables middle click to open in new tab
showHidden: false # show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
dateFormat: false # when false, the date is relative, when true, the date is an exact timestamp
gallerySize: 3 # 0-9 - the size of the gallery thumbnails
themeColor: var(--blue) # theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
themeColor: "var(--blue)" # theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
quickDownload: false # show icon to download in one click
disableOnlyOfficeExt: .txt .csv .html .pdf # comma separated list of file extensions to disable onlyoffice preview for
disableOnlyOfficeExt: ".txt .csv .html .pdf" # comma separated list of file extensions to disable onlyoffice preview for
disableOfficePreviewExt: "" # comma separated list of file extensions to disable office preview for
lockPassword: false # disable the user from changing their password
disableSettings: false # disable the user from viewing the settings page
@@ -122,9 +124,12 @@ userDefaults:
modify: false
share: false
realtime: false
loginMethod: password # login method to use: eg. password, proxy, oidc
loginMethod: "password" # login method to use: eg. password, proxy, oidc
disableUpdateNotifications: false # disable update notifications banner for admin users
deleteWithoutConfirming: false # delete files without confirmation
fileLoading: # upload and download settings
maxConcurrentUpload: 10
uploadChunkSizeMb: 10
integrations:
office: # validate:omitempty
url: "" # The URL to the OnlyOffice Document Server, needs to be accessible to the user. validate:required
+12 -2
View File
@@ -23,7 +23,8 @@ if (!DEEPL_API_KEY) {
const translator = new deepl.Translator(DEEPL_API_KEY);
const deeplLangMap = {
'zh-cn': 'ZH',
'zh-cn': 'ZH-HANS',
'zh-tw': 'ZH-HANT',
'pt': 'PT-PT', // or 'PT-BR' if you want Brazilian Portuguese
'pt-br': 'PT-BR',
'en': 'EN',
@@ -32,6 +33,8 @@ const deeplLangMap = {
'sv-se': 'SV',
'ua': 'UK',
'nl-be': 'NL',
'is': 'IS',
'cz': 'CS',
// Add more as needed
};
@@ -77,10 +80,17 @@ async function processKeys(masterObj, targetObj, targetLangCode, currentPathPart
if (Object.prototype.hasOwnProperty.call(masterObj, key)) {
const currentPathPartsNext = [...currentPathParts, key];
const currentKeyPath = currentPathPartsNext.join('.');
if (currentPathParts[0] === 'languages') continue;
const masterValue = masterObj[key];
// Special handling for "languages" key - always copy the entire object from master
if (key === 'languages' && currentPathParts.length === 0) {
console.log(`Copying entire "languages" object from master to ${targetLangCode}.json`);
targetObj[key] = JSON.parse(JSON.stringify(masterValue)); // Deep copy
changesMade = true;
continue;
}
if (typeof masterValue === 'object' && masterValue !== null && !Array.isArray(masterValue)) {
if (!targetObj[key] || typeof targetObj[key] !== 'object') {
console.log(`Creating missing object structure for "${currentKeyPath}" in ${targetLangCode}.json`);
+95 -79
View File
@@ -1,21 +1,20 @@
import { fetchURL, adjustedData } from './utils'
import { getApiPath, extractSourceFromPath,removePrefix } from '@/utils/url.js'
import { state, mutations } from '@/store'
import { getApiPath, removePrefix, doubleEncode } from '@/utils/url.js'
import { mutations } from '@/store'
import { notify } from '@/notify'
import { externalUrl,baseURL,serverHasMultipleSources } from '@/utils/constants'
import { externalUrl,baseURL } from '@/utils/constants'
// Notify if errors occur
export async function fetchFiles(url, content = false) {
export async function fetchFiles(source, path, content = false) {
try {
const result = extractSourceFromPath(url)
const apiPath = getApiPath('api/resources', {
path: encodeURIComponent(result.path),
source: result.source,
path: doubleEncode(path),
source: doubleEncode(source),
...(content && { content: 'true' })
})
const res = await fetchURL(apiPath)
const data = await res.json()
const adjusted = adjustedData(data, url)
const adjusted = adjustedData(data)
return adjusted
} catch (err) {
notify.showError(err.message || 'Error fetching data')
@@ -23,17 +22,15 @@ export async function fetchFiles(url, content = false) {
}
}
async function resourceAction(url, method, content) {
async function resourceAction(source, path, method, content) {
try {
const result = extractSourceFromPath(url)
let source = encodeURIComponent(result.source)
let path = result.path
source = doubleEncode(source)
path = doubleEncode(path)
const apiPath = getApiPath('api/resources', { path, source })
let opts = { method }
if (content) {
opts.body = content
}
path = encodeURIComponent(path)
const apiPath = getApiPath('api/resources', { path: path, source: source })
const res = await fetchURL(apiPath, opts)
return res
} catch (err) {
@@ -42,47 +39,41 @@ async function resourceAction(url, method, content) {
}
}
export async function remove(url) {
export async function remove(source, path) {
try {
return await resourceAction(url, 'DELETE')
return await resourceAction( source, path, 'DELETE')
} catch (err) {
notify.showError(err.message || 'Error deleting resource')
throw err
}
}
export async function put(path,source, content = '') {
export async function put(source, path, content = '') {
try {
if (serverHasMultipleSources) {
path = `/files/${source}${path}`
} else {
path = `/files${path}`
}
return await resourceAction(path, 'PUT', content)
return await resourceAction(source, path, 'PUT', content)
} catch (err) {
notify.showError(err.message || 'Error putting resource')
throw err
}
}
export function download(format, files) {
export function download(format, files, shareHash = "") {
if (format !== 'zip') {
format = 'tar.gz'
}
let fileargs = ''
if (files.length === 1) {
const result = extractSourceFromPath(decodeURIComponent(files[0]))
fileargs = result.source + '::' + result.path + '||'
} else {
for (let file of files) {
const result = extractSourceFromPath(decodeURIComponent(file))
fileargs += result.source + '::' + result.path + '||'
for (let file of files) {
if (shareHash) {
fileargs += encodeURIComponent(file.path) + '||'
} else {
fileargs += encodeURIComponent(file.source) + '::' + encodeURIComponent(file.path) + '||'
}
}
fileargs = fileargs.slice(0, -2) // remove trailing "||"
const apiPath = getApiPath('api/raw', {
files: encodeURIComponent(fileargs),
algo: format
const apiPath = getApiPath(shareHash == "" ? 'api/raw' : 'api/public/dl', {
files: fileargs,
algo: format,
hash: shareHash
})
const url = window.origin + apiPath
@@ -93,54 +84,77 @@ export function download(format, files) {
document.body.appendChild(link)
link.click()
document.body.removeChild(link) // Clean up
}
export async function post(url, content = '', overwrite = false, onupload) {
export function post(
source,
path,
content = "",
overwrite = false,
onupload,
headers = {}
) {
try {
const result = extractSourceFromPath(url)
let bufferContent
if (
content instanceof Blob &&
!['http:', 'https:'].includes(window.location.protocol)
) {
bufferContent = await new Response(content).arrayBuffer()
const apiPath = getApiPath("api/resources", {
path: encodeURIComponent(path),
source: encodeURIComponent(source),
override: overwrite,
});
const request = new XMLHttpRequest();
request.open("POST", apiPath, true);
for (const header in headers) {
request.setRequestHeader(header, headers[header]);
}
const apiPath = getApiPath('api/resources', {
path: encodeURIComponent(result.path),
source: encodeURIComponent(result.source),
override: overwrite
})
return new Promise((resolve, reject) => {
let request = new XMLHttpRequest()
request.open('POST', apiPath, true)
request.setRequestHeader('X-Auth', state.jwt)
if (typeof onupload === 'function') {
request.upload.onprogress = event => {
if (event.lengthComputable) {
const percentComplete = Math.round(
(event.loaded / event.total) * 100
)
onupload(percentComplete) // Pass the percentage to the callback
}
if (typeof onupload === "function") {
request.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percentComplete = Math.round(
(event.loaded / event.total) * 100
);
onupload(percentComplete); // Pass the percentage to the callback
}
}
};
}
const promise = new Promise((resolve, reject) => {
request.onload = () => {
if (request.status === 200) {
resolve(request.responseText)
if (request.status >= 200 && request.status < 300) {
resolve(request.responseText);
} else if (request.status === 409) {
reject(request.status)
const error = new Error("conflict");
error.response = { status: request.status, responseText: request.responseText };
reject(error);
} else {
reject(request.responseText)
reject(new Error(request.responseText || "Upload failed"));
}
};
request.onerror = () => reject(new Error("Network error"));
request.onabort = () => reject(new Error("Upload aborted"));
if (
content instanceof Blob &&
!["http:", "https:"].includes(window.location.protocol)
) {
new Response(content).arrayBuffer()
.then(buffer => request.send(buffer))
.catch(err => reject(err));
} else {
request.send(content);
}
request.send(bufferContent || content)
})
});
return { xhr: request, promise };
} catch (err) {
notify.showError(err.message || 'Error posting resource')
throw err
notify.showError(err.message || "Error posting resource");
// We are returning a promise, so we should return a rejected promise on error.
return {
xhr: null,
promise: Promise.reject(err),
};
}
}
@@ -158,12 +172,10 @@ export async function moveCopy(
try {
// Create an array of fetch calls
let promises = items.map(item => {
const fromResult = extractSourceFromPath(item.from)
const toResult = extractSourceFromPath(item.to)
let localParams = {
...params,
destination: encodeURIComponent(toResult.source) + '::' + toResult.path,
from: fromResult.source + '::' + fromResult.path
destination: doubleEncode(item.toSource) + '::' + doubleEncode(item.to),
from: doubleEncode(item.fromSource) + '::' + doubleEncode(item.from)
}
const apiPath = getApiPath('api/resources', localParams)
@@ -181,10 +193,14 @@ export async function moveCopy(
// Await all promises and ensure errors propagate
await Promise.all(promises)
notify.showSuccess(
action === 'copy' ? 'Resources copied successfully' : 'Resources moved successfully'
)
mutations.setReload(true);
setTimeout(() => {
notify.showSuccess(
action === 'copy' ? 'Resources copied successfully' : 'Resources moved successfully'
)
}, 125);
setTimeout(() => {
mutations.setReload(true);
}, 125);
} catch (err) {
notify.showError(err.message || 'Error moving/copying resources')
throw err // Re-throw the error to propagate it back to the caller
@@ -194,8 +210,8 @@ export async function moveCopy(
export async function checksum(source, path, algo) {
try {
const params = {
path: encodeURIComponent(path),
source: encodeURIComponent(source),
path: doubleEncode(path),
source: doubleEncode(source),
checksum: algo
}
const apiPath = getApiPath('api/resources', params)
-26
View File
@@ -22,32 +22,6 @@ export async function fetchPub(path, hash, password = "") {
return adjusted
}
// Download files with given parameters
export function download(format, files) {
let fileargs = ''
if (files.length === 1) {
fileargs = decodeURI(files[0]) + '||'
} else {
for (let file of files) {
fileargs += decodeURI(file) + '||'
}
}
fileargs = fileargs.slice(0, -2) // remove trailing "||"
const apiPath = getApiPath('api/public/dl', {
files: encodeURIComponent(fileargs),
hash: format.hash,
})
const url = window.origin + apiPath
// Create a temporary <a> element to trigger the download
const link = document.createElement('a')
link.href = url
link.setAttribute('download', '') // Ensures it triggers a download
document.body.appendChild(link)
link.click()
document.body.removeChild(link) // Clean up
}
// Get the public user data
export async function getPublicUser() {
try {
+1 -9
View File
@@ -54,25 +54,17 @@ export async function fetchJSON(url, opts) {
}
}
export function adjustedData(data, url) {
data.url = url;
export function adjustedData(data) {
if (data.type === "directory") {
if (!data.url.endsWith("/")) data.url += "/";
// Combine folders and files into items
data.items = [...(data.folders || []), ...(data.files || [])];
data.items = data.items.map((item) => {
item.url = `${data.url}${encodeURIComponent(item.name)}`;
item.source = data.source
if (data.path == "/") {
item.path = `/${item.name}`
} else {
item.path = `${data.path}/${item.name}`
}
if (item.type === "directory") {
item.url += "/";
}
return item;
});
}
+10 -25
View File
@@ -21,75 +21,60 @@ describe('adjustedData', () => {
path: "/root"
};
const url = "http://example.com/root/";
const expected = {
type: "directory",
url: "http://example.com/root/",
folders: [],
files: [],
items: [
{ name: "folder1", path: "/root/folder1", type: "directory", url: "http://example.com/root/folder1/" },
{ name: "folder2", path: "/root/folder2", type: "directory", url: "http://example.com/root/folder2/" },
{ name: "file1.txt", path: "/root/file1.txt", type: "file", url: "http://example.com/root/file1.txt" },
{ name: "file2.txt", path: "/root/file2.txt", type: "file", url: "http://example.com/root/file2.txt" },
{ name: "folder1", path: "/root/folder1", source: undefined, type: "directory" },
{ name: "folder2", path: "/root/folder2", source: undefined, type: "directory" },
{ name: "file1.txt", path: "/root/file1.txt", source: undefined, type: "file" },
{ name: "file2.txt", path: "/root/file2.txt", source: undefined, type: "file" },
],
path: "/root",
};
expect(adjustedData(input, url)).toEqual(expected);
expect(adjustedData(input)).toEqual(expected);
});
it('should add a trailing slash to the URL if missing for a directory', () => {
const input = { type: "directory", folders: [], files: [] };
const url = "http://example.com/base";
const expected = {
type: "directory",
url: "http://example.com/base/",
folders: [],
files: [],
items: [],
};
expect(adjustedData(input, url)).toEqual(expected);
expect(adjustedData(input)).toEqual(expected);
});
it('should handle non-directory types without modification to items', () => {
const input = { type: "file", name: "file1.txt" };
const url = "http://example.com/base";
const expected = {
type: "file",
name: "file1.txt",
url: "http://example.com/base",
};
expect(adjustedData(input, url)).toEqual(expected);
expect(adjustedData(input)).toEqual(expected);
});
it('should handle missing folders and files gracefully', () => {
const input = { type: "directory" };
const url = "http://example.com/base";
const expected = {
type: "directory",
url: "http://example.com/base/",
items: [],
};
expect(adjustedData(input, url)).toEqual(expected);
expect(adjustedData(input)).toEqual(expected);
});
it('should handle empty input object correctly', () => {
const input = {};
const url = "http://example.com/base";
const expected = {
url: "http://example.com/base",
};
expect(adjustedData(input, url)).toEqual(expected);
const expected = {};
expect(adjustedData(input)).toEqual(expected);
});
});
+19 -15
View File
@@ -36,7 +36,7 @@
<script>
import { state, mutations, getters } from "@/store";
import { extractSourceFromPath } from "@/utils/url.js";
import { extractSourceFromPath, encodedPath } from "@/utils/url.js";
export default {
name: "breadcrumbs",
@@ -67,29 +67,33 @@ export default {
return getters.isCardView();
},
items() {
// double encode # to fix issue with # in path
// replace all # with %23
const req = state.req;
let path = "";
if (req.path !== undefined) {
path = state.req.path.replaceAll('#', "%23");
if (!req.items || !req.path) {
return [];
}
let parts = path.split("/");
if (parts[0] === "") {
parts.shift();
let encodedPathString = encodedPath(state.req.path);
let originalParts = state.req.path.split("/");
let encodedParts = encodedPathString.split("/");
// Remove empty strings from both arrays consistently
if (originalParts[0] === "") {
originalParts.shift();
encodedParts.shift();
}
if (parts[parts.length - 1] === "") {
parts.pop();
if (originalParts[originalParts.length - 1] === "") {
originalParts.pop();
encodedParts.pop();
}
let breadcrumbs = [];
let buildRef = this.base;
parts.forEach((element) => {
buildRef = buildRef + encodeURIComponent(element) + "/";
for (let i = 0; i < originalParts.length; i++) {
const origPart = originalParts[i];
const encodedElement = encodedParts[i];
buildRef = buildRef + encodedElement + "/";
breadcrumbs.push({
name: element,
name: origPart,
url: buildRef,
});
});
}
if (breadcrumbs.length > 3) {
while (breadcrumbs.length !== 4) {
breadcrumbs.shift();
+242 -164
View File
@@ -1,114 +1,132 @@
<template>
<div
id="context-menu"
ref="contextMenu"
v-if="showContext"
:style="{
top: `${top}px`,
left: `${left}px`,
}"
class="button no-select"
:class="{ 'dark-mode': isDarkMode, centered: centered }"
<transition
name="expand"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<div v-if="selectedCount > 0" class="button selected-count-header">
<span>{{ selectedCount }} {{ $t("prompts.selected") }} </span>
<div
id="context-menu"
ref="contextMenu"
v-if="showContext"
:style="centered ? {} : { top: posY + 'px', left: posX + 'px' }"
class="button no-select"
:class="{ 'dark-mode': isDarkMode, 'centered': centered }"
:key="showCreate ? 'create-mode' : 'normal-mode'"
>
<div v-if="selectedCount > 0" class="button selected-count-header">
<span>{{ selectedCount }} {{ $t("prompts.selected") }} </span>
</div>
<action
v-if="!showCreate && !isSearchActive && userPerms.modify"
icon="add"
label="New"
@action="startShowCreate"
/>
<action
v-if="showCreate && !isSearchActive && userPerms.modify"
icon="create_new_folder"
:label="$t('sidebar.newFolder')"
@action="showHover('newDir')"
/>
<action
v-if="showCreate && userPerms.modify && !isSearchActive"
icon="note_add"
:label="$t('sidebar.newFile')"
@action="showHover('newFile')"
/>
<action
v-if="showCreate && userPerms.modify && !isSearchActive"
icon="file_upload"
:label="$t('buttons.upload')"
@action="uploadFunc"
/>
<action
v-if="!showCreate && selectedCount == 1"
icon="info"
:label="$t('buttons.info')"
show="info"
/>
<action
v-if="!isMultiple && !isSearchActive && isMobileDevice"
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
<action
v-if="(!showCreate && selectedCount > 0)"
icon="file_download"
:label="$t('buttons.download')"
@action="startDownload"
:counter="selectedCount"
/>
<action
v-if="selectedCount <= 1 && showShare"
icon="share"
:label="$t('buttons.share')"
show="share"
/>
<action
v-if="!showCreate && selectedCount == 1 && userPerms.modify && !isSearchActive"
icon="mode_edit"
:label="$t('buttons.rename')"
show="rename"
/>
<action
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
icon="content_copy"
:label="$t('buttons.copyFile')"
show="copy"
/>
<action
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
icon="forward"
:label="$t('buttons.moveFile')"
show="move"
/>
<action
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
icon="file_upload"
:label="$t('buttons.replace')"
@action="showUpload"
/>
<action
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
icon="delete"
:label="$t('buttons.delete')"
show="delete"
/>
</div>
<action
v-if="!showCreate && !isSearchActive && userPerms.modify"
icon="add"
label="New"
@action="startShowCreate"
/>
<action
v-if="showCreate && !isSearchActive && userPerms.modify"
icon="create_new_folder"
:label="$t('sidebar.newFolder')"
@action="showHover('newDir')"
/>
<action
v-if="showCreate && userPerms.modify && !isSearchActive"
icon="note_add"
:label="$t('sidebar.newFile')"
@action="showHover('newFile')"
/>
<action
v-if="showCreate && userPerms.modify && !isSearchActive"
icon="file_upload"
:label="$t('buttons.upload')"
@action="uploadFunc"
/>
<action
v-if="!showCreate && selectedCount == 1"
icon="info"
:label="$t('buttons.info')"
show="info"
/>
<action
v-if="!isMultiple && !isSearchActive && isMobileDevice"
icon="check_circle"
:label="$t('buttons.selectMultiple')"
@action="toggleMultipleSelection"
/>
<action
v-if="(!showCreate && selectedCount > 0)"
icon="file_download"
:label="$t('buttons.download')"
@action="startDownload"
:counter="selectedCount"
/>
<action
v-if="selectedCount <= 1 && showShare"
icon="share"
:label="$t('buttons.share')"
show="share"
/>
<action
v-if="!showCreate && selectedCount == 1 && userPerms.modify && !isSearchActive"
icon="mode_edit"
:label="$t('buttons.rename')"
show="rename"
/>
<action
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
icon="content_copy"
:label="$t('buttons.copyFile')"
show="copy"
/>
<action
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
icon="forward"
:label="$t('buttons.moveFile')"
show="move"
/>
<action
v-if="!showCreate && selectedCount > 0 && userPerms.modify"
icon="delete"
:label="$t('buttons.delete')"
show="delete"
/>
</div>
<div
id="context-menu"
ref="contextMenu"
v-else-if="showOverflow"
:style="{
top: '3em',
right: '1em',
}"
class="button no-select"
:class="{ 'dark-mode': isDarkMode }"
</transition>
<transition
name="expand"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<action v-if="showGoToRaw" icon="open_in_new" :label="$t('buttons.openFile')" @action="goToRaw()" />
<action v-if="isPreview" icon="file_download" :label="$t('buttons.download')" @action="startDownload" />
<action v-if="showEdit" icon="edit" :label="$t('buttons.edit')" @action="edit()" />
<action v-if="showSave" icon="save" :label="$t('buttons.save')" @action="save()" />
<action v-if="showDelete" icon="delete" :label="$t('buttons.delete')" show="delete" />
</div>
<div
id="context-menu"
ref="contextMenu"
v-if="showOverflow"
:style="{
top: '3em',
right: '1em',
}"
class="button no-select"
:class="{ 'dark-mode': isDarkMode }"
>
<action v-if="showGoToRaw" icon="open_in_new" :label="$t('buttons.openFile')" @action="goToRaw()" />
<action v-if="isPreview" icon="file_download" :label="$t('buttons.download')" @action="startDownload" />
<action v-if="showEdit" icon="edit" :label="$t('buttons.edit')" @action="edit()" />
<action v-if="showSave" icon="save" :label="$t('buttons.save')" @action="save()" />
<action v-if="showDelete" icon="delete" :label="$t('buttons.delete')" show="delete" />
</div>
</transition>
</template>
<script>
@@ -130,6 +148,8 @@ export default {
posX: 0,
posY: 0,
showCreate: false,
isAnimating: false,
createStateInitialized: false,
};
},
props: {
@@ -176,15 +196,12 @@ export default {
state.user?.permissions &&
state.user?.permissions.share &&
state.user.username != "publicUser" &&
getters.currentView() != "share"
getters.currentView() != "share" &&
!this.isSearchActive
);
},
showContext() {
if (getters.currentPromptName() == "ContextMenu") {
this.setPositions();
return true;
}
return false;
return getters.currentPromptName() == "ContextMenu";
},
onlyofficeEnabled() {
return onlyOfficeUrl !== "";
@@ -199,20 +216,7 @@ export default {
return state.user;
},
centered() {
return this.showCentered || !this.posX || !this.posY;
},
top() {
// Ensure the context menu stays within the viewport
return Math.min(
this.posY,
window.innerHeight - (this.$refs.contextMenu?.clientHeight ?? 0)
);
},
left() {
return Math.min(
this.posX,
window.innerWidth - (this.$refs.contextMenu?.clientWidth ?? 0)
);
return this.showCentered || this.isMobileDevice || !this.posX || !this.posY;
},
isDarkMode() {
return getters.isDarkMode();
@@ -228,7 +232,90 @@ export default {
};
},
},
watch: {
showContext: {
handler(newVal) {
if (newVal) {
// Always set positions when not animating to check for position props.
if (!this.isAnimating) {
this.setPositions();
}
// Initialize create state only once per menu session
if (!this.createStateInitialized) {
this.initializeCreateState();
this.createStateInitialized = true;
}
} else {
// Reset the flag when menu is hidden so it reinitializes next time
this.createStateInitialized = false;
}
},
immediate: true
}
},
methods: {
// Animation methods
beforeEnter(el) {
this.isAnimating = true;
el.style.height = '0';
el.style.opacity = '0';
},
enter(el, done) {
el.style.transition = '';
el.style.height = '0';
el.style.opacity = '0';
// Force reflow
void el.offsetHeight;
// Calculate the height after ensuring all content is rendered
this.$nextTick(() => {
// Temporarily set to auto to get true height, then measure
el.style.height = 'auto';
el.style.visibility = 'hidden';
void el.offsetHeight; // Force reflow
const fullHeight = el.scrollHeight;
const fullWidth = el.scrollWidth;
// Adjust position now that we have dimensions
const BUFFER = 8;
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
let newX = this.posX;
let newY = this.posY;
if (newX + fullWidth + BUFFER > screenWidth) newX = screenWidth - fullWidth - BUFFER;
if (newX < BUFFER) newX = BUFFER;
if (newY + fullHeight + BUFFER > screenHeight) newY = screenHeight - fullHeight - BUFFER;
if (newY < BUFFER) newY = BUFFER;
this.posX = newX;
this.posY = newY;
// Reset to 0 for animation
el.style.height = '0';
el.style.visibility = 'visible';
el.style.transition = 'height 0.3s, opacity 0.3s';
void el.offsetHeight; // Force reflow
// Animate to full height
el.style.height = fullHeight + 'px';
el.style.opacity = '1';
setTimeout(() => {
this.isAnimating = false;
done();
}, 300);
});
},
leave(el, done) {
this.isAnimating = true;
el.style.transition = 'height 0.3s, opacity 0.3s';
el.style.height = el.scrollHeight + 'px';
void el.offsetHeight;
el.style.height = '0';
el.style.opacity = '0';
setTimeout(() => {
this.isAnimating = false;
done();
}, 300);
},
startShowCreate() {
this.showCreate = true;
},
@@ -239,43 +326,12 @@ export default {
return mutations.showHover(value);
},
setPositions() {
const BUFFER = 8; // px
const contextProps = getters.currentPrompt().props;
let tempX = contextProps.posX;
let tempY = contextProps.posY;
// Set initial position
this.posX = tempX;
this.posY = tempY;
// Wait for DOM update, then adjust
this.$nextTick(() => {
const menu = this.$refs.contextMenu;
if (!menu) return;
const menuWidth = menu.clientWidth || 320; // fallback to 20em
const menuHeight = menu.clientHeight || 200; // fallback to min height
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
let newX = tempX;
let newY = tempY;
// Adjust X if overflowing right
if (newX + menuWidth + BUFFER > screenWidth) {
newX = screenWidth - menuWidth - BUFFER;
}
// Adjust X if too close to left
if (newX < BUFFER) {
newX = BUFFER;
}
// Adjust Y if overflowing bottom
if (newY + menuHeight + BUFFER > screenHeight) {
newY = screenHeight - menuHeight - BUFFER;
}
// Adjust Y if too close to top
if (newY < BUFFER) {
newY = BUFFER;
}
this.posX = newX;
this.posY = newY;
});
// Show/hide create as before
this.posX = contextProps.posX;
this.posY = contextProps.posY;
},
initializeCreateState() {
// Only set initial showCreate state, don't override user choices
if (state.selected.length > 0) {
this.showCreate = false;
} else {
@@ -287,7 +343,7 @@ export default {
mutations.closeHovers();
},
startDownload() {
downloadFiles();
downloadFiles(state.selected);
},
goToRaw() {
const downloadUrl = filesApi.getDownloadURL(
@@ -316,6 +372,14 @@ export default {
mutations.closeHovers();
},
showUpload() {
mutations.showHover({
name: "upload",
props: {
filesToReplace: state.selected.map((item) => item.name),
},
});
},
},
};
</script>
@@ -332,6 +396,7 @@ export default {
display: flex;
flex-direction: column;
justify-content: center;
box-shadow: 0px 0px 15px 0px #404040;
}
#context-menu.centered {
@@ -363,4 +428,17 @@ export default {
#context-menu .action span {
display: none;
}
/* Animation styles */
.expand-enter-active,
.expand-leave-active {
transition: height 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
}
.expand-enter,
.expand-leave-to {
height: 0 !important;
opacity: 0;
}
</style>
+79 -22
View File
@@ -6,20 +6,25 @@ https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/compon
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="text.length > 0 && textPosition == 'middle'"
v-if="textPosition == 'middle'"
>
{{ text }}
{{ displayed_text }}
</div>
<div class="vue-simple-progress-bar" :style="bar_style">
<div :style="text_style" v-if="text.length > 0 && textPosition == 'inside'">
{{ text }}
</div>
<div class="vue-simple-progress-bar" :style="bar_style"></div>
<div
class="vue-simple-progress-text"
:style="text_style"
v-if="textPosition == 'inside'"
>
{{ displayed_text }}
</div>
</div>
</template>
<script>
import { getHumanReadableFilesize } from "@/utils/filesizes.js";
// We're leaving this untouched as you can read in the beginning
var isNumber = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n);
@@ -34,10 +39,14 @@ export default {
max: {
default: 100,
},
unit: {
type: String,
default: "",
},
size: {
// either a number (pixel width/height) or 'tiny', 'small',
// 'medium', 'large', 'huge', 'massive' for common sizes
default: 3,
default: "big",
},
"bg-color": {
type: String,
@@ -45,7 +54,7 @@ export default {
},
"bar-color": {
type: String,
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color
default: "var(--primaryColor)", // match .blue color to Material Design's 'Blue 500' color
},
"bar-transition": {
type: String,
@@ -53,7 +62,7 @@ export default {
},
"bar-border-radius": {
type: Number,
default: 0,
default: 4,
},
spacing: {
type: Number,
@@ -69,7 +78,7 @@ export default {
},
"text-position": {
type: String,
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
default: "inside", // 'bottom', 'top', 'middle', 'inside'
},
"font-size": {
type: Number,
@@ -77,14 +86,37 @@ export default {
},
"text-fg-color": {
type: String,
default: "#222",
default: "#000",
},
status: {
type: String,
default: 'default',
},
},
computed: {
isValNumeric() {
return isNumber(this.val);
},
pct() {
if (!this.isValNumeric) return 100;
if (this.max <= 0) return 0;
var pct = (this.val / this.max) * 100;
pct = pct.toFixed(2);
return Math.min(pct, this.max);
return Math.max(0, Math.min(pct.toFixed(2), 100));
},
displayed_text() {
if (!this.isValNumeric) return this.val;
const percentage =
this.max > 0 ? Math.round((this.val / this.max) * 100) : 0;
if (this.unit === "bytes" && this.isValNumeric) {
const valFormatted = getHumanReadableFilesize(this.val);
const maxFormatted = getHumanReadableFilesize(this.max);
return `${valFormatted} / ${maxFormatted} (${percentage}%)`;
}
const unit_string = this.unit ? ` ${this.unit}` : "";
return `${this.val}${unit_string} / ${this.max}${unit_string} (${percentage}%)`;
},
size_px() {
switch (this.size) {
@@ -129,7 +161,7 @@ export default {
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
return Math.min(Math.max(Math.ceil(this.size_px * 0.8), 11), 32);
}
return isNumber(this.fontSize) ? this.fontSize : 13;
@@ -137,6 +169,7 @@ export default {
progress_style() {
var style = {
background: this.bgColor,
position: 'relative'
};
if (this.textPosition == "middle" || this.textPosition == "inside") {
@@ -150,9 +183,17 @@ export default {
return style;
},
bar_style() {
let barColor = this.barColor;
if (this.status === 'error') {
barColor = '#f44336';
} else if (this.status === 'conflict') {
barColor = '#ff9800';
}
var style = {
width: this.pct + "%",
height: this.size_px + "px",
background: barColor,
transition: this.barTransition,
};
@@ -160,33 +201,43 @@ export default {
style["border-radius"] = this.barBorderRadius + "px";
}
if (this.textPosition == "middle" || this.textPosition == "inside") {
//style["position"] = "absolute";
if (this.textPosition == "middle") {
style["position"] = "absolute";
style["top"] = "0";
style["height"] = "100%";
style["min-width"] = "1.5em";
(style["min-height"] = this.size_px + "px"), (style["z-index"] = "-1");
style["min-height"] = this.size_px + "px";
style["z-index"] = "-1";
}
return style;
},
text_style() {
var style = {
color: this.textFgColor,
"color": this.textFgColor,
"font-size": this.text_font_size + "px",
"text-align": this.textAlign,
};
if (this.textPosition === 'inside') {
style['position'] = 'absolute';
style['left'] = '0';
style['right'] = '0';
style['top'] = '50%';
style['transform'] = 'translateY(-50%)';
style['width'] = '100%';
style['padding'] = '0 0.5em';
style['box-sizing'] = 'border-box';
}
if (
this.textPosition == "top" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
this.textPosition == "middle"
)
style["padding-bottom"] = this.text_padding + "px";
if (
this.textPosition == "bottom" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
this.textPosition == "middle"
)
style["padding-top"] = this.text_padding + "px";
return style;
@@ -203,6 +254,12 @@ export default {
.vue-simple-progress,
.vue-simple-progress-bar {
border-radius: 0.5em;
}
.vue-simple-progress {
background: var(--primaryColor);
}
.vue-simple-progress-text {
color: black;
}
</style>
+1 -6
View File
@@ -522,17 +522,12 @@ export default {
if (this.getContext === "/") {
path = s.path;
}
let urlPath = "/files/" + state.sources.current + path;
if (!state.serverHasMultipleSources) {
urlPath = "/files" + path;
}
const modifiedItem = {
name: pathParts.pop(),
path: path,
size: s.size,
type: s.type,
source: this.selectedSource,
url: urlPath,
source: this.selectedSource || state.sources.current,
fullPath: path,
};
mutations.resetSelected();
+47
View File
@@ -0,0 +1,47 @@
<template>
<div
v-show="tooltip.show"
class="floating-tooltip"
:class="{ 'dark-mode': isDarkMode }"
:style="{ top: tooltip.y + 'px', left: tooltip.x + 'px' }"
v-html="tooltip.content"
></div>
</template>
<script>
import { state } from "@/store";
export default {
name: "Tooltip",
computed: {
tooltip() {
return state.tooltip;
},
isDarkMode() {
return state.user.darkMode;
},
},
};
</script>
<style>
.floating-tooltip {
position: fixed;
padding: 0.5em;
background-color: var(--alt-background);
color: var(--textPrimary);
border-radius: 1em;
box-shadow: 0 0.25em 1em rgba(0, 0, 0, 0.2);
z-index: 9999;
pointer-events: none;
transform: translate(15px, 15px); /* Offset from cursor */
max-width: 20em;
white-space: normal;
overflow-wrap: break-word;
}
.tooltip-info-icon {
font-size: 1em !important;
padding: 0.1em !important;
padding-left: 0.5em !important;
}
</style>
+88 -57
View File
@@ -7,6 +7,8 @@
'listing-item': true,
activebutton: isMaximized && isSelected,
hiddenFile: isHiddenNotSelected && !this.isDraggedOver,
'half-selected': isDraggedOver,
'drag-hover': isDraggedOver,
}"
:id="getID"
role="button"
@@ -63,9 +65,9 @@ import { getHumanReadableFilesize } from "@/utils/filesizes";
import { filesApi, shareApi } from "@/api";
import * as upload from "@/utils/upload";
import { state, getters, mutations } from "@/store"; // Import your custom store
import { router } from "@/router";
import { url } from "@/utils";
import Icon from "@/components/files/Icon.vue";
import { baseURL, serverHasMultipleSources } from "@/utils/constants";
export default {
name: "item",
@@ -87,7 +89,7 @@ export default {
props: [
"name",
"isDir",
"url",
"source",
"type",
"size",
"modified",
@@ -95,6 +97,7 @@ export default {
"readOnly",
"path",
"reducedOpacity",
"hash",
],
computed: {
galleryView() {
@@ -133,7 +136,7 @@ export default {
canDrop() {
if (!this.isDir || this.readOnly !== undefined) return false;
for (let i of this.selected) {
if (state.req.items[i].url === this.url) {
if (state.req.items[i].path === this.path && state.req.source === this.source) {
return false;
}
}
@@ -179,7 +182,7 @@ export default {
event.stopPropagation();
mutations.resetSelected();
mutations.addSelected(this.index);
downloadFiles();
downloadFiles(state.selected);
},
handleTouchMove(event) {
if (!state.isSafari) return;
@@ -213,7 +216,13 @@ export default {
window.location.href = this.getRelative(path);
},
getUrl() {
return this.url;
if (this.hash) {
return baseURL + "share/" + this.hash + this.path;
}
if (serverHasMultipleSources) {
return baseURL + "files/" + this.source + this.path;
}
return baseURL + "files/" + this.path;
},
onRightClick(event) {
event.preventDefault(); // Prevent default context menu
@@ -253,16 +262,15 @@ export default {
dragLeave() {
this.isDraggedOver = false;
},
dragStart() {
if (getters.selectedCount() === 0) {
mutations.addSelected(this.index);
return;
}
if (!this.isSelected) {
dragStart(event) {
if (this.selected.indexOf(this.index) === -1) {
mutations.resetSelected();
mutations.addSelected(this.index);
}
event.dataTransfer.setData(
"application/x-filebrowser-internal-drag",
"true"
);
},
dragOver(event) {
if (!this.canDrop) return;
@@ -270,43 +278,39 @@ export default {
this.isDraggedOver = true;
},
async drop(event) {
if (!this.canDrop) return;
event.preventDefault();
event.stopPropagation();
this.isDraggedOver = false;
if (getters.selectedCount() === 0) return;
let el = event.target;
for (let i = 0; i < 5; i++) {
if (el !== null && !el.classList.contains("item")) {
el = el.parentElement;
}
if (!this.canDrop) {
return;
}
let items = [];
for (let i of this.selected) {
for (let i of state.selected) {
items.push({
from: state.req.items[i].url,
to: this.url + encodeURIComponent(state.req.items[i].name),
name: state.req.items[i].name,
from: state.req.items[i].path,
fromSource: state.req.items[i].source,
to: this.path + "/" + state.req.items[i].name,
toSource: this.source,
});
}
let response = await filesApi.fetchFiles(decodeURIComponent(el.__vue__.url));
const conflict = upload.checkConflict(
items,
(await filesApi.fetchFiles(this.source, this.path)).items
);
let action = async (overwrite, rename) => {
await filesApi.moveCopy(items, "move", overwrite, rename);
};
let conflict = upload.checkConflict(items, response.items);
let overwrite = false;
let rename = false;
if (conflict) {
mutations.showHover({
name: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
const overwrite = option === "overwrite";
const rename = option === "rename";
event.preventDefault();
mutations.closeHovers();
@@ -316,22 +320,25 @@ export default {
return;
}
action(overwrite, rename);
action(false, false);
},
addSelected(event) {
if (!state.isSafari) return;
const touch = event.touches[0];
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
this.isLongPress = false; // Reset state
this.isSwipe = false; // Reset swipe detection
if (!state.multiple) {
this.contextTimeout = setTimeout(() => {
if (!this.isSwipe) {
mutations.resetSelected();
mutations.addSelected(this.index);
if (state.isSafari) {
if (event.type === "touchstart") {
const touch = event.touches[0];
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
this.isLongPress = false; // Reset state
this.isSwipe = false; // Reset swipe detection
if (!state.multiple) {
this.contextTimeout = setTimeout(() => {
if (!this.isSwipe) {
mutations.resetSelected();
mutations.addSelected(this.index);
}
}, 500);
}
}, 500);
}
}
},
click(event) {
@@ -358,30 +365,43 @@ export default {
this.open();
}
if (state.selected.indexOf(this.index) !== -1) {
mutations.removeSelected(this.index);
return;
}
if (event.shiftKey && this.selected.length > 0) {
if (event.shiftKey && state.selected.length > 0) {
let fi = 0;
let la = 0;
if (this.index > this.selected[0]) {
fi = this.selected[0] + 1;
if (this.index > state.lastSelectedIndex) {
fi = state.lastSelectedIndex;
la = this.index;
} else {
fi = this.index;
la = this.selected[0] - 1;
la = state.lastSelectedIndex;
}
mutations.resetSelected();
for (; fi <= la; fi++) {
if (state.selected.indexOf(fi) == -1) {
if (state.selected.indexOf(fi) === -1) {
mutations.addSelected(fi);
}
}
return;
}
if (state.selected.indexOf(this.index) !== -1) {
if (event.ctrlKey || event.metaKey) {
mutations.removeSelected(this.index);
mutations.setLastSelectedIndex(this.index);
return;
}
if (state.selected.length > 1) {
mutations.resetSelected();
mutations.addSelected(this.index);
mutations.setLastSelectedIndex(this.index);
}
return;
}
if (
!state.user.singleClick &&
!event.ctrlKey &&
@@ -391,10 +411,16 @@ export default {
mutations.resetSelected();
}
mutations.addSelected(this.index);
mutations.setLastSelectedIndex(this.index);
},
open() {
location.hash = state.req.items[this.index].name;
router.push({ path: this.url });
if (this.hash) {
const shareHash = this.hash;
url.goToItem(this.source, this.path, "", shareHash);
return;
}
const previousHash = state.req.items[this.index].name;
url.goToItem(this.source, this.path, previousHash);
},
},
};
@@ -434,4 +460,9 @@ export default {
width: 9em !important;
margin-right: 1em !important;
}
.half-selected {
border-color: var(--primaryColor) !important;
border-style: solid !important;
}
</style>
-151
View File
@@ -1,151 +0,0 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.copy") }}</h2>
</div>
<div class="card-content">
<p>{{ $t("prompts.copyMessage") }}</p>
<file-list ref="fileList" @update:selected="(val) => (dest = val)"> </file-list>
</div>
<div
class="card-action"
style="display: flex; align-items: center; justify-content: space-between"
>
<template v-if="user.permissions.modify">
<button
class="button button--flat"
@click="$refs.fileList.createDir()"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
style="justify-self: left"
>
<span>{{ $t("sidebar.newFolder") }}</span>
</button>
</template>
<div>
<button
class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
:disabled="destContainsSrc"
class="button button--flat"
@click="copy"
:aria-label="$t('buttons.copy')"
:title="$t('buttons.copy')"
>
{{ $t("buttons.copy") }}
</button>
</div>
</div>
</div>
</template>
<script>
import { mutations, state } from "@/store";
import FileList from "./FileList.vue";
import { filesApi } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
import { removeLastDir } from "@/utils/url";
import { notify } from "@/notify";
export default {
name: "copy",
components: { FileList },
data: function () {
return {
current: window.location.pathname,
dest: null,
items: [],
};
},
computed: {
destContainsSrc() {
if (!this.dest) {
return false; // If dest is not set, we can't check containment
}
const itemPath = this.items[0]?.from
const parentDir = removeLastDir(itemPath) + "/"
return this.dest == parentDir || this.dest.startsWith(itemPath);
},
user() {
return state.user;
},
closeHovers() {
return mutations.closeHovers();
},
},
mounted() {
if (state.isSearchActive) {
this.items = [
{
from: state.selected[0].url,
name: state.selected[0].name,
},
];
} else {
for (let item of state.selected) {
this.items.push({
from: state.req.items[item].url,
// add to: dest
name: state.req.items[item].name,
});
}
}
},
methods: {
copy: async function (event) {
event.preventDefault();
try {
// Define the action function
let action = async (overwrite, rename) => {
for (let item of this.items) {
item.to = this.dest + item.name;
}
buttons.loading("copy");
await filesApi.moveCopy(this.items, "copy", overwrite, rename);
};
// Fetch destination files
let dstResp = await filesApi.fetchFiles(this.dest);
let conflict = upload.checkConflict(this.items, dstResp.items);
let overwrite = false;
let rename = false;
if (conflict) {
await new Promise((resolve, reject) => {
mutations.showHover({
name: "replace-rename",
confirm: async (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
try {
await action(overwrite, rename);
resolve(); // Resolve the promise if action succeeds
} catch (e) {
reject(e); // Reject the promise if an error occurs
}
},
});
});
} else {
// Await the action call for non-conflicting cases
action(overwrite, rename);
}
mutations.closeHovers();
mutations.setSearch(false);
notify.showSuccess("Server is copying files...");
} catch (error) {
notify.showError(error);
}
},
},
};
</script>
+3 -3
View File
@@ -72,7 +72,7 @@ export default {
try {
const currentView = getters.currentView()
if (state.isSearchActive || currentView == "preview") {
await filesApi.remove(state.selected[0].url);
await filesApi.remove(state.selected[0].source, state.selected[0].path);
buttons.success("delete");
notify.showSuccess("Deleted item successfully");
mutations.closeHovers();
@@ -80,7 +80,7 @@ export default {
return;
}
if (!this.isListing) {
await filesApi.remove(state.req.items[state.selected[0]].url);
await filesApi.remove(state.req.items.source, state.req.items[state.selected[0]].path);
buttons.success("delete");
notify.showSuccess("Deleted item successfully");
@@ -97,7 +97,7 @@ export default {
let promises = [];
for (let index of state.selected) {
promises.push(filesApi.remove(state.req.items[index].url));
promises.push(filesApi.remove(state.req.source, state.req.items[index].path));
}
mutations.resetSelected();
await Promise.all(promises);
+119 -19
View File
@@ -1,5 +1,18 @@
<template>
<div>
<!-- Source Selection Dropdown (only show if multiple sources available) -->
<div v-if="availableSources.length > 1" class="source-selector" style="margin-bottom: 1rem;">
<label for="destinationSource" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
{{ $t("prompts.destinationSource") }}
</label>
<select id="destinationSource" v-model="currentSource" @change="onSourceChange"
class="input input--block">
<option v-for="source in availableSources" :key="source" :value="source">
{{ source }}
</option>
</select>
</div>
<div>Source: {{ sourcePath.source }} </div> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<div aria-label="filelist-path" class="searchContext">{{$t('search.path')}} {{ sourcePath.path }}</div>
<ul class="file-list">
@@ -10,10 +23,10 @@
role="button"
tabindex="0"
:aria-label="item.name"
:aria-selected="selected == item.url"
:aria-selected="selected == item.path"
:key="item.name"
v-for="item in items"
:data-url="item.url"
:data-path="item.path"
>
{{ item.name }}
</li>
@@ -23,47 +36,113 @@
<script>
import { state, mutations } from "@/store";
import url from "@/utils/url.js";
import { extractSourceFromPath } from "@/utils/url.js";
import { url } from "@/utils";
import { filesApi } from "@/api";
export default {
name: "file-list",
props: {
browseSource: {
type: String,
default: null,
},
},
data: function () {
const initialSource = this.browseSource || state.req.source;
// Use current path if browsing the same source as current, otherwise start at root
const initialPath = this.browseSource && this.browseSource !== state.req.source ? "/" : state.req.path;
return {
items: [],
path: initialPath,
source: initialSource,
touches: {
id: "",
count: 0,
},
selected: null,
selectedSource: null,
current: window.location.pathname,
currentSource: initialSource,
};
},
computed: {
sourcePath() {
return extractSourceFromPath(decodeURIComponent(this.current), "files");
return { source: this.source, path: this.path };
},
effectiveSource() {
return this.browseSource || state.req.source;
},
availableSources() {
// Get all available sources from state.sources.info
return state.sources && state.sources.info ? Object.keys(state.sources.info) : [state.req.source];
},
},
watch: {
browseSource(newSource) {
if (newSource && newSource !== this.source) {
this.currentSource = newSource;
this.resetToSource(newSource);
}
},
currentSource(newSource) {
if (newSource && newSource !== this.source) {
this.resetToSource(newSource);
}
},
},
mounted() {
this.fillOptions(state.req);
// Use currentSource if provided, otherwise use state.req
const sourceToUse = this.currentSource;
const pathToUse = this.currentSource !== state.req.source ? "/" : state.req.path;
const initialReq = {
...state.req,
source: sourceToUse,
path: pathToUse,
};
// Fetch the initial data for the source
if (this.currentSource !== state.req.source) {
filesApi.fetchFiles(sourceToUse, pathToUse).then(this.fillOptions);
} else {
this.fillOptions(initialReq);
}
},
methods: {
resetToSource(newSource) {
// Use current path if browsing the same source as current, otherwise start at root
const newPath = newSource === state.req.source ? state.req.path : "/";
// Reset to the appropriate path for the new source
this.path = newPath;
this.source = newSource;
this.selected = null;
this.selectedSource = null;
// Fetch files for the new source
filesApi.fetchFiles(newSource, newPath).then(this.fillOptions);
},
fillOptions(req) {
// Sets the current path and resets
// the current items.
this.current = req.url;
this.current = req.path;
this.source = req.source;
this.items = [];
this.$emit("update:selected", this.current);
// Emit both path and source
this.$emit("update:selected", {
path: this.current,
source: this.source
});
// If the path isn't the root path,
// show a button to navigate to the previous
// directory.
if (req.url !== "/files/") {
if (req.path !== "/") {
this.items.push({
name: "..",
url: url.removeLastDir(req.url) + "/",
path: url.removeLastDir(req.path) + "/",
source: req.source,
});
}
@@ -76,7 +155,8 @@ export default {
if (item.type != "directory") continue;
this.items.push({
name: item.name,
url: item.url,
path: item.path,
source: item.source || req.source,
});
}
},
@@ -84,11 +164,16 @@ export default {
// Retrieves the URL of the directory the user
// just clicked in and fill the options with its
// content.
let uri = event.currentTarget.dataset.url;
filesApi.fetchFiles(uri).then(this.fillOptions);
let path = event.currentTarget.dataset.path;
let clickedItem = this.items.find(item => item.path === path);
let sourceToUse = clickedItem ? clickedItem.source : this.source;
this.path = path;
this.source = sourceToUse;
filesApi.fetchFiles(sourceToUse, path).then(this.fillOptions);
},
touchstart(event) {
let url = event.currentTarget.dataset.url;
let url = event.currentTarget.dataset.path;
// In 300 milliseconds, we shall reset the count.
setTimeout(() => {
@@ -117,16 +202,28 @@ export default {
else this.select(event);
},
select: function (event) {
let path = event.currentTarget.dataset.path;
// If the element is already selected, unselect it.
if (this.selected === event.currentTarget.dataset.url) {
if (this.selected === path) {
this.selected = null;
this.$emit("update:selected", this.current);
this.selectedSource = null;
this.$emit("update:selected", {
path: this.current,
source: this.source
});
return;
}
// Otherwise select the element.
this.selected = event.currentTarget.dataset.url;
this.$emit("update:selected", this.selected);
this.selected = path;
let clickedItem = this.items.find(item => item.path === path);
this.selectedSource = clickedItem ? clickedItem.source : this.source;
this.$emit("update:selected", {
path: this.selected,
source: this.selectedSource
});
},
createDir: async function () {
mutations.showHover({
@@ -135,10 +232,13 @@ export default {
confirm: null,
props: {
redirect: false,
base: this.current === state.route.path ? null : this.current,
base: this.current === this.path ? null : this.current,
},
});
},
onSourceChange() {
this.resetToSource(this.currentSource);
},
},
};
</script>
-150
View File
@@ -1,150 +0,0 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t("prompts.move") }}</h2>
</div>
<div class="card-content">
<file-list ref="fileList" @update:selected="(val) => (dest = val)"> </file-list>
</div>
<div
class="card-action"
style="display: flex; align-items: center; justify-content: space-between"
>
<template v-if="user.permissions.modify">
<button
class="button button--flat"
@click="$refs.fileList.createDir()"
:aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')"
style="justify-self: left"
>
<span>{{ $t("sidebar.newFolder") }}</span>
</button>
</template>
<div>
<button
class="button button--flat button--grey"
@click="closeHovers"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
{{ $t("buttons.cancel") }}
</button>
<button
:disabled="destContainsSrc"
class="button button--flat"
@click="move"
:aria-label="$t('buttons.move')"
:title="$t('buttons.move')"
>
{{ $t("buttons.move") }}
</button>
</div>
</div>
</div>
</template>
<script>
import { mutations, state } from "@/store";
import FileList from "./FileList.vue";
import { filesApi } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
import { notify } from "@/notify";
import { removeLastDir } from "@/utils/url";
export default {
name: "move",
components: { FileList },
data: function () {
return {
current: window.location.pathname,
dest: null,
items: [],
};
},
computed: {
destContainsSrc() {
if (!this.dest) {
return false; // If dest is not set, we can't check containment
}
const itemPath = this.items[0]?.from
const parentDir = removeLastDir(itemPath) + "/"
return this.dest == parentDir || this.dest.startsWith(itemPath);
},
user() {
return state.user;
},
closeHovers() {
return mutations.closeHovers();
},
},
mounted() {
if (state.isSearchActive) {
this.items = [
{
from: state.selected[0].url,
name: state.selected[0].name,
},
];
} else {
for (let item of state.selected) {
this.items.push({
from: state.req.items[item].url,
// add to: dest
name: state.req.items[item].name,
});
}
}
},
methods: {
move: async function (event) {
event.preventDefault();
try {
// Define the action function
let action = async (overwrite, rename) => {
for (let item of this.items) {
item.to = this.dest + item.name;
}
buttons.loading("move");
await filesApi.moveCopy(this.items, "move", overwrite, rename);
};
// Fetch destination files
let dstResp = await filesApi.fetchFiles(this.dest);
let conflict = upload.checkConflict(this.items, dstResp.items);
let overwrite = false;
let rename = false;
if (conflict) {
await new Promise((resolve, reject) => {
mutations.showHover({
name: "replace-rename",
confirm: async (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
try {
await action(overwrite, rename);
resolve(); // Resolve the promise if action succeeds
} catch (e) {
reject(e); // Reject the promise if an error occurs
}
},
});
});
} else {
// Await the action call for non-conflicting cases
action(overwrite, rename);
}
mutations.closeHovers();
mutations.setSearch(false);
notify.showSuccess("Server is moving files...");
} catch (error) {
notify.showError(error);
}
},
},
};
</script>
@@ -0,0 +1,183 @@
<template>
<div class="card floating">
<div class="card-title">
<h2>{{ $t(`prompts.${operation}`) }}</h2>
</div>
<div class="card-content">
<p v-if="operation === 'copy'">{{ $t("prompts.copyMessage") }}</p>
<file-list ref="fileList" @update:selected="updateDestination">
</file-list>
</div>
<div class="card-action" style="display: flex; align-items: center; justify-content: space-between">
<template v-if="user.permissions.modify">
<button class="button button--flat" @click="$refs.fileList.createDir()"
:aria-label="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')" style="justify-self: left">
<span>{{ $t("sidebar.newFolder") }}</span>
</button>
</template>
<div>
<button class="button button--flat button--grey" @click="closeHovers" :aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')">
{{ $t("buttons.cancel") }}
</button>
<button :disabled="destContainsSrc" class="button button--flat" @click="performOperation"
:aria-label="$t(`buttons.${operation}`)" :title="$t(`buttons.${operation}`)">
{{ $t(`buttons.${operation}`) }}
</button>
</div>
</div>
</div>
</template>
<script>
import { mutations, state } from "@/store";
import FileList from "./FileList.vue";
import { filesApi } from "@/api";
import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
import { url } from "@/utils";
import { notify } from "@/notify";
import { removeLastDir } from "@/utils/url";
export default {
name: "move-copy",
components: { FileList },
props: {
operation: {
type: String,
required: true,
validator: (value) => ["move", "copy"].includes(value),
},
},
data: function () {
return {
current: window.location.pathname,
destPath: "/", // Start at root of selected source
destSource: null, // Will be set by FileList component
items: [],
};
},
computed: {
destContainsSrc() {
if (!this.destPath) {
return false; // If dest is not set, we can't check containment
}
// Add null checks to prevent undefined errors
if (!this.items || this.items.length === 0) {
return false;
}
const itemPath = this.items[0]?.from;
if (!itemPath) {
return false; // If itemPath is undefined, we can't check containment
}
const parentDir = (this.operation === "copy" ? url.removeLastDir(itemPath) : removeLastDir(itemPath)) + "/";
return this.destPath == parentDir || this.destPath.startsWith(itemPath);
},
user() {
return state.user;
},
closeHovers() {
return mutations.closeHovers();
},
},
mounted() {
if (state.isSearchActive) {
// Add null checks to prevent undefined values
if (state.selected && state.selected[0] && state.selected[0].path) {
this.items = [
{
from: state.selected[0].path,
fromSource: state.selected[0].source,
name: state.selected[0].name,
},
];
}
} else {
if (state.selected && state.req && state.req.items) {
for (let item of state.selected) {
const reqItem = state.req.items[item];
if (reqItem && reqItem.path) {
this.items.push({
from: reqItem.path,
fromSource: state.req.source,
name: reqItem.name,
});
}
}
}
}
},
methods: {
updateDestination(pathOrData) {
// Handle both old format (just path) and new format (object with path and source)
if (typeof pathOrData === 'string') {
this.destPath = pathOrData;
// For backward compatibility, keep the current source
// This will be updated when FileList is modified to emit both
} else if (pathOrData && pathOrData.path) {
this.destPath = pathOrData.path;
// Update destSource from FileList's selection
this.destSource = pathOrData.source;
}
},
performOperation: async function (event) {
event.preventDefault();
try {
// Define the action function
let action = async (overwrite, rename) => {
for (let item of this.items) {
// Ensure proper path construction without double slashes
const destPath = this.destPath.endsWith('/') ? this.destPath : this.destPath + '/';
item.to = destPath + item.name;
// Always set toSource for cross-source operations
item.toSource = this.destSource;
}
buttons.loading(this.operation);
await filesApi.moveCopy(this.items, this.operation, overwrite, rename);
};
// Fetch destination files
let dstResp = await filesApi.fetchFiles(
this.destSource,
this.destPath
);
let conflict = upload.checkConflict(this.items, dstResp.items);
let overwrite = false;
let rename = false;
if (conflict) {
await new Promise((resolve, reject) => {
mutations.showHover({
name: "replace-rename",
confirm: async (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
try {
await action(overwrite, rename);
resolve(); // Resolve the promise if action succeeds
} catch (e) {
reject(e); // Reject the promise if an error occurs
}
},
});
});
} else {
// Await the action call for non-conflicting cases
await action(overwrite, rename);
}
mutations.closeHovers();
mutations.setSearch(false);
notify.showSuccess(`Server is ${this.operation === "copy" ? "copying" : "moving"} files...`);
} catch (error) {
notify.showError(error);
}
},
},
};
</script>
+3 -6
View File
@@ -40,6 +40,7 @@
import { filesApi } from "@/api";
import { getters, mutations, state } from "@/store"; // Import your custom store
import { notify } from "@/notify";
import { goToItem } from "@/utils/url";
export default {
name: "new-dir",
@@ -74,12 +75,8 @@ export default {
try {
event.preventDefault();
if (this.name === "") return;
// Build the path of the new directory.
let uri = decodeURIComponent(state.req.url);
uri += this.name + "/"; // Ensure the path ends with a slash
await filesApi.post(uri);
this.$router.push({ path: state.route.path + encodeURIComponent(this.name) });
await filesApi.post(state.req.source, state.req.path + "/" + this.name + "/");
goToItem(state.req.source, state.req.path + "/" + this.name);
} catch (error) {
notify.showError(error);
}
+3 -5
View File
@@ -41,6 +41,7 @@ import { state } from "@/store";
import { filesApi } from "@/api";
import { getters, mutations } from "@/store"; // Import your custom store
import { notify } from "@/notify";
import { goToItem } from "@/utils/url";
export default {
name: "new-file",
@@ -65,11 +66,8 @@ export default {
try {
event.preventDefault();
if (this.name === "") return;
// Build the path of the new file.
let uri = decodeURIComponent(state.req.url);
uri += this.name
await filesApi.post(uri, "", true);
this.$router.push({ path: state.route.path + encodeURIComponent(this.name) });
await filesApi.post(state.req.source, state.req.path + "/" + this.name, true);
goToItem(state.req.source,state.req.path + "/" + encodeURIComponent(this.name));
} catch (error) {
notify.showError(error);
}
+17 -8
View File
@@ -4,7 +4,7 @@
v-if="showOverlay"
:ref="currentPromptName"
:is="currentPromptName"
v-bind="currentPrompt.props"
v-bind="currentPromptProps"
/>
</div>
</template>
@@ -15,8 +15,7 @@ import Info from "./Info.vue";
import Delete from "./Delete.vue";
import Rename from "./Rename.vue";
import Download from "./Download.vue";
import Move from "./Move.vue";
import Copy from "./Copy.vue";
import MoveCopy from "./MoveCopy.vue";
import NewFile from "./NewFile.vue";
import NewDir from "./NewDir.vue";
import Replace from "./Replace.vue";
@@ -39,8 +38,8 @@ export default {
Delete,
Rename,
Download,
Move,
Copy,
Move: MoveCopy,
Copy: MoveCopy,
Share,
NewFile,
NewDir,
@@ -85,10 +84,8 @@ export default {
prompt.submit();
break;
case "copy":
prompt.copy(event);
break;
case "move":
prompt.move(event);
prompt.performOperation(event);
break;
case "replace":
prompt.showConfirm(event);
@@ -112,6 +109,18 @@ export default {
}
return getters.currentPrompt();
},
currentPromptProps() {
const prompt = this.currentPrompt;
const promptName = this.currentPromptName;
// For move and copy prompts, add the operation prop
if (promptName === "move" || promptName === "copy") {
return {
...prompt.props,
operation: promptName,
};
}
return prompt.props;
},
plugins() {
return state.plugins;
},
+7 -12
View File
@@ -37,7 +37,6 @@
</div>
</template>
<script>
import url from "@/utils/url.js";
import { filesApi } from "@/api";
import { state, getters, mutations } from "@/store";
import { notify } from "@/notify";
@@ -86,18 +85,14 @@ export default {
},
async submit() {
try {
let oldLink = "";
let newLink = "";
const items = [{
from: state.req.path + "/" + state.req.items[this.selected[0]].name,
fromSource: state.req.source,
to: state.req.path + "/" + this.name,
toSource: state.req.source,
}];
if (!this.isListing) {
oldLink = state.req.url;
} else {
oldLink = state.req.items[this.selected[0]].url;
}
newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
await filesApi.moveCopy([{ from: oldLink, to: newLink }], "move");
await filesApi.moveCopy(items, "move");
mutations.closeHovers();
if (!this.isListing) {
this.$router.push({ path: newLink });
+3 -3
View File
@@ -11,7 +11,7 @@
<div class="card-action">
<button
class="button button--flat button--grey"
@click="closeHovers"
@click="closeTopHover"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
@@ -39,8 +39,8 @@ export default {
},
},
methods: {
closeHovers() {
mutations.closeHovers();
closeTopHover() {
mutations.closeTopHover();
},
},
};
+3 -2
View File
@@ -127,6 +127,7 @@ import { state, getters, mutations } from "@/store";
import { shareApi, publicApi } from "@/api";
import Clipboard from "clipboard";
import { fromNow } from "@/utils/moment";
import { buildItemUrl } from "@/utils/url";
export default {
name: "share",
@@ -160,7 +161,7 @@ export default {
},
url() {
if (state.isSearchActive) {
return state.selected[0].url;
return buildItemUrl(state.selected[0].source,state.selected[0].path)
}
if (!this.isListing) {
return state.route.path;
@@ -169,7 +170,7 @@ export default {
// selecting current view image
return state.route.path;
}
return state.req.items[this.selected[0]].url;
return buildItemUrl(state.req.items[this.selected[0]].source,state.req.items[this.selected[0]].path)
},
},
async beforeMount() {
+523 -81
View File
@@ -3,61 +3,314 @@
<div class="card-title">
<h2>{{ $t("prompts.upload") }}</h2>
</div>
<div
class="upload-prompt"
@dragenter.prevent="onDragEnter"
@dragover.prevent="onDragOver"
@dragleave.prevent="onDragLeave"
:class="{ 'drag-over': isDragging }"
>
<div class="upload-prompt-container">
<i v-if="files.length === 0" class="material-icons">cloud_upload</i>
<p v-if="files.length === 0">{{ $t("prompts.dragAndDrop") }}</p>
<div class="button-group">
<button @click="triggerFilePicker" class="button button--flat">
{{ $t("buttons.file") }}
</button>
<button
style="margin-left: 1em"
@click="triggerFolderPicker"
class="button button--flat"
>
{{ $t("buttons.folder") }}
</button>
</div>
</div>
</div>
<div
class="card-content"
@drop.prevent="onDrop"
:class="{ 'drag-over': isDragging }"
>
<div v-if="showConflictPrompt" class="conflict-overlay">
<div class="card">
<div class="card-content">
<p>{{ $t("prompts.conflictsDetected") }}</p>
</div>
<div class="card-actions">
<button
@click="resolveConflict(false)"
class="button button--flat button--grey"
>
{{ $t("buttons.cancel") }}
</button>
<button
@click="resolveConflict(true)"
class="button button--flat button--red"
>
{{ $t("buttons.replace") }}
</button>
</div>
</div>
</div>
<div class="card-content">
<p>{{ $t("prompts.uploadMessage") }}</p>
<div v-if="files.length > 0" class="upload-list">
<div v-for="file in files" :key="file.id" class="upload-item">
<i class="material-icons file-icon">{{file.type === "directory" ? "folder" : "insert_drive_file" }}</i> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
<div class="file-info">
<p class="file-name">{{ file.name }}</p>
<progress-bar
v-if="file.type !== 'directory'"
:val="
file.status === 'completed'
? $t('prompts.completed')
: file.status === 'error'
? $t('prompts.error')
: file.status === 'conflict'
? $t('prompts.conflictsDetected')
: (file.progress / 100) * file.size
"
:unit="file.status === 'completed' || file.status === 'error' ? '' : 'bytes'"
:max="file.size"
:status="file.status"
text-position="inside"
size="20"
>
</progress-bar>
<div v-else class="status-label">
<span>{{ $t(`prompts.${file.status}`) }}</span>
</div>
</div>
<div class="file-actions">
<button
v-if="file.status === 'uploading'"
@click="uploadManager.pause(file.id)"
class="action"
:aria-label="$t('buttons.pause')"
:title="$t('buttons.pause')"
>
<i class="material-icons">pause</i>
</button>
<button
v-if="file.status === 'paused'"
@click="uploadManager.resume(file.id)"
class="action"
:aria-label="$t('buttons.resume')"
:title="$t('buttons.resume')"
>
<i class="material-icons">play_arrow</i>
</button>
<button
v-if="file.status === 'error'"
@click="uploadManager.retry(file.id)"
class="action"
:aria-label="$t('buttons.retry')"
:title="$t('buttons.retry')"
>
<i class="material-icons">replay</i>
</button>
<button
v-if="file.status === 'conflict'"
@click="handleConflictAction(file)"
class="action"
:aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')"
>
<i class="material-icons">sync_problem</i>
</button>
<button
@click="cancelUpload(file.id)"
class="action"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
<i class="material-icons">close</i>
</button>
</div>
</div>
</div>
</div>
<div class="card-action full">
<div
@click="triggerFilePicker"
@keypress.enter="triggerFilePicker"
class="action"
id="focus-prompt"
tabindex="1"
<div class="card-actions">
<button
@click="close"
class="button button--flat button--grey"
:aria-label="$t('buttons.cancel')"
:title="$t('buttons.cancel')"
>
<i class="material-icons">insert_drive_file</i>
<div class="title">{{ $t("buttons.file") }}</div>
</div>
<div
@click="triggerFolderPicker"
@keypress.enter="triggerFolderPicker"
class="action"
tabindex="2"
{{ $t("buttons.close") }}
</button>
<div class="spacer"></div>
<button
v-if="canPauseAll"
@click="uploadManager.pauseAll"
class="button button--flat"
:aria-label="$t('buttons.pauseAll')"
:title="$t('buttons.pauseAll')"
>
<i class="material-icons">folder</i>
<div class="title">{{ $t("buttons.folder") }}</div>
</div>
<input
ref="fileInput"
@change="onFilePicked"
type="file"
multiple
style="display: none"
/>
<input
ref="folderInput"
@change="onFolderPicked"
type="file"
webkitdirectory
directory
multiple
style="display: none"
/>
{{ $t("buttons.pauseAll") }}
</button>
<button
v-if="canResumeAll"
@click="uploadManager.resumeAll"
class="button button--flat"
:aria-label="$t('buttons.resumeAll')"
:title="$t('buttons.resumeAll')"
>
{{ $t("buttons.resumeAll") }}
</button>
<button
@click="clearCompleted"
class="button button--flat"
:disabled="!hasCompleted"
:aria-label="$t('buttons.clearCompleted')"
:title="$t('buttons.clearCompleted')"
>
{{ $t("buttons.clearCompleted") }}
</button>
</div>
<input
ref="fileInput"
@change="onFilePicked"
type="file"
multiple
style="display: none"
/>
<input
ref="folderInput"
@change="onFolderPicked"
type="file"
webkitdirectory
directory
multiple
style="display: none"
/>
</div>
</template>
<script>
import { ref } from "vue";
import * as upload from "@/utils/upload";
import { mutations, state, getters } from "@/store";
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { uploadManager } from "@/utils/upload";
import { mutations, state } from "@/store";
import ProgressBar from "@/components/ProgressBar.vue";
export default {
name: "UploadFiles",
setup() {
components: {
ProgressBar,
},
props: {
initialItems: {
type: Object,
default: null,
},
filesToReplace: {
type: Array,
default: () => [],
},
},
setup(props) {
console.log("Upload.vue: setup started. Props:", props);
const fileInput = ref(null);
const folderInput = ref(null);
const files = computed(() => uploadManager.queue);
const isDragging = ref(false);
const showConflictPrompt = ref(false);
let conflictResolver = null;
let wakeLock = null;
const handleConflict = (resolver) => {
showConflictPrompt.value = true;
conflictResolver = resolver;
};
const resolveConflict = (overwrite) => {
if (conflictResolver) {
conflictResolver(overwrite);
}
showConflictPrompt.value = false;
conflictResolver = null;
};
const acquireWakeLock = async () => {
if (!("wakeLock" in navigator)) {
return;
}
try {
if (wakeLock !== null) return; // Already locked
wakeLock = await navigator.wakeLock.request("screen");
wakeLock.addEventListener("release", () => {
wakeLock = null;
});
} catch (err) {
console.error(`Wake Lock failed: ${err.name}, ${err.message}`);
}
};
const releaseWakeLock = () => {
if (wakeLock !== null) {
wakeLock.release();
wakeLock = null;
}
};
const isUploading = computed(() =>
files.value.some(
(file) => file.status === "pending" || file.status === "uploading"
)
);
watch(isUploading, (active) => {
if (active) {
acquireWakeLock();
} else {
releaseWakeLock();
}
});
const hasCompleted = computed(() =>
files.value.some((file) => file.status === "completed")
);
const canPauseAll = computed(() =>
files.value.some((file) => file.status === "uploading")
);
const canResumeAll = computed(
() =>
!canPauseAll.value &&
files.value.some((file) => file.status === "paused")
);
const close = () => {
mutations.closeHovers();
};
const clearCompleted = () => {
uploadManager.clearCompleted();
};
const handleVisibilityChange = async () => {
if (document.visibilityState === "visible" && isUploading.value) {
acquireWakeLock();
}
};
onMounted(async () => {
document.addEventListener("visibilitychange", handleVisibilityChange);
uploadManager.setOnConflict(handleConflict);
if (props.initialItems) {
await processItems(props.initialItems);
}
});
onUnmounted(() => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
uploadManager.setOnConflict(() => {}); // cleanup
releaseWakeLock();
});
const triggerFilePicker = () => {
if (fileInput.value) fileInput.value.click();
@@ -68,56 +321,109 @@ export default {
};
const onFilePicked = (event) => {
handleFiles(event, false);
const pickedFiles = event.target.files;
if (pickedFiles.length > 0) {
processFileList(pickedFiles);
}
if (event.target) event.target.value = null;
};
const onFolderPicked = (event) => {
handleFiles(event, true);
const pickedFiles = event.target.files;
if (pickedFiles.length > 0) {
processFileList(pickedFiles);
}
if (event.target) event.target.value = null;
};
const handleFiles = async (event) => {
mutations.closeHovers();
const rawFiles = event.target.files;
if (!rawFiles || rawFiles.length === 0) return;
const uploadFiles = [];
for (let i = 0; i < rawFiles.length; i++) {
const file = rawFiles[i];
const fullPath = file.webkitRelativePath || file.name;
uploadFiles.push({
file,
name: file.name,
path: fullPath,
fullPath: fullPath,
source: state.req.source,
size: file.size,
});
}
const path = getters.routePath();
const conflict = upload.checkConflict(uploadFiles, state.req.items);
const doUpload = async () => {
mutations.closeHovers();
await upload.handleFiles(uploadFiles, path, true);
mutations.setReload(true);
};
if (conflict) {
mutations.showHover({
name: "replace",
confirm: async (e) => {
e.preventDefault();
await doUpload();
},
});
const onDrop = async (event) => {
isDragging.value = false;
if (event.dataTransfer.items) {
const items = Array.from(event.dataTransfer.items);
console.log("Upload.vue: Processing dropped items:", items);
await processDroppedItems(items);
} else {
await doUpload();
const droppedFiles = event.dataTransfer.files;
console.log("Upload.vue: Processing dropped files (fallback):", droppedFiles);
processFileList(droppedFiles);
}
};
const onDragEnter = () => {
isDragging.value = true;
};
const onDragOver = () => {
isDragging.value = true;
};
const onDragLeave = () => {
isDragging.value = false;
};
const getFilesFromDirectoryEntry = async (entry) => {
if (entry.isFile) {
return new Promise((resolve) => {
entry.file((file) => {
const relativePath = entry.fullPath.startsWith("/")
? entry.fullPath.substring(1)
: entry.fullPath;
resolve([{ file, relativePath }]);
});
});
}
if (entry.isDirectory) {
const reader = entry.createReader();
const entries = await new Promise((resolve) => {
reader.readEntries((e) => resolve(e));
});
const allFiles = await Promise.all(
entries.map((subEntry) => getFilesFromDirectoryEntry(subEntry))
);
return allFiles.flat();
}
return [];
};
const processDroppedItems = async (items) => {
const filesToUpload = [];
for (const item of items) {
const entry = item.webkitGetAsEntry();
if (entry) {
const files = await getFilesFromDirectoryEntry(entry);
filesToUpload.push(...files);
}
}
if (filesToUpload.length > 0) {
uploadManager.add(state.req.path, filesToUpload);
}
};
const processFileList = (fileList) => {
const filesToAdd = Array.from(fileList).map((file) => ({
file,
relativePath: file.webkitRelativePath || file.name,
}));
if (filesToAdd.length > 0) {
uploadManager.add(state.req.path, filesToAdd);
}
};
const handleConflictAction = (file) => {
mutations.showHover({
name: "replace",
confirm: () => {
uploadManager.retry(file.id, true);
mutations.closeTopHover();
},
});
};
const cancelUpload = (id) => {
uploadManager.cancel(id);
};
return {
triggerFilePicker,
triggerFolderPicker,
@@ -125,7 +431,143 @@ export default {
onFolderPicked,
fileInput,
folderInput,
files,
isDragging,
onDragEnter,
onDragLeave,
onDragOver,
onDrop,
cancelUpload,
uploadManager,
close,
clearCompleted,
hasCompleted,
showConflictPrompt,
resolveConflict,
canPauseAll,
canResumeAll,
handleConflictAction,
};
},
};
</script>
<style scoped>
.upload-prompt {
text-align: center;
padding: 2em;
border: 2px dashed #ccc;
border-radius: 8px;
margin: 0 1em;
}
.upload-prompt.drag-over {
background-color: #f0f0f0;
border-color: #333;
}
.upload-prompt-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.upload-prompt i {
font-size: 4em;
color: #ccc;
}
.upload-list {
overflow-y: auto;
padding-right: 0.5em; /* To avoid scrollbar overlapping content */
flex-grow: 1;
display: flex;
flex-direction: column-reverse;
min-height: 0;
}
.upload-item {
display: flex;
}
.file-icon {
margin-right: 0.5em;
color: #999;
}
.file-info {
flex-grow: 1;
}
.file-name {
margin: 0;
font-size: 0.9em;
}
.status-label {
color: #777;
font-size: 0.8em;
margin-top: 5px;
}
.file-actions .action {
background: none;
border: none;
cursor: pointer;
padding: 0.2em;
}
.file-actions .action i {
font-size: 1.2em;
}
.drag-over {
border-color: #2196f3;
background: #f4f4f4;
}
.card-actions {
display: flex;
justify-content: space-between;
padding: 0.5em;
}
.spacer {
flex-grow: 1;
}
.card.floating {
display: flex !important;
flex-direction: column;
max-height: 85vh;
}
.card-content {
flex-grow: 1;
min-height: 0;
display: flex;
flex-direction: column;
position: relative;
padding: 1em;
}
.conflict-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
}
.conflict-overlay .card {
background-color: var(--card-background-color);
padding: 1em;
border-radius: 8px;
}
</style>
@@ -31,18 +31,19 @@ export default defineComponent({
it: "it",
ja: "ja",
ko: "ko",
"nl-be": "nlBE",
nlBE: "nl-be",
pl: "pl",
"pt-br": "ptBR",
ptBR: "pt-br",
pt: "pt",
ro: "ro",
ru: "ru",
sk: "sk",
"sv-se": "svSE",
svSE: "sv-se",
tr: "tr",
ua: "ua",
"zh-cn": "zhCN",
"zh-tw": "zhTW",
zhCN: "zh-cn",
zhTW: "zh-tw",
cz: "cz",
},
};
},
+7 -19
View File
@@ -165,26 +165,14 @@
<circle class="pulse" cx="50%" cy="50%" r="10px"></circle>
</svg>
<span>{{ name }}</span>
<i class="no-select material-symbols-outlined source-info-icon"
<i class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="updateSourceTooltip($event, info)"
@mouseleave="resetSourceTooltip">
info <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</i>
</div>
<div v-if="hasSourceInfo" class="usage-info">
<ProgressBar
:val="info.usedPercentage"
text-position="inside"
:text="info.usedPercentage + '%'"
size="large"
text-fg-color="white"
></ProgressBar>
<div class="usage-info">
<span>
{{ getHumanReadableFilesize(info.used) }} {{ $t("index.of") }}
{{ getHumanReadableFilesize(info.total) }} {{ $t("index.used") }}
</span>
</div>
<ProgressBar :val="info.used" :max="info.total" unit="bytes"></ProgressBar>
</div>
</button>
</transition-group>
@@ -305,6 +293,10 @@ export default {
mutations.toggleDarkMode();
},
toggleSticky() {
// keep sidebar open if disabling sticky sidebar
if (!state.showSidebar && state.user.stickySidebar) {
mutations.toggleSidebar();
}
mutations.updateCurrentUser({ stickySidebar: !state.user.stickySidebar });
},
navigateTo(path) {
@@ -653,9 +645,5 @@ button.action {
opacity: 0;
}
.source-info-icon {
font-size: 1em !important;
padding: 0.1em !important;
padding-left: 0.5em !important;
}
</style>
+6 -4
View File
@@ -204,13 +204,15 @@ input.sizeInput:disabled {
}
.sizeInput {
height: 100%;
text-align: center;
width: 5em;
border-radius: 1em;
padding: 1em;
backdrop-filter: invert(0.1);
padding: .5em;
border: none !important;
margin-right: 0.5em;
-webkit-backdrop-filter: invert(.1);
backdrop-filter: invert(.1);
height: 2em;
width: 5em;
}
.roundedInputLeft {
+2 -2
View File
@@ -87,9 +87,9 @@
/* Listing items */
.dark-mode #listingView .item {
background: var(--surfacePrimary) !important;
background: var(--surfacePrimary);
color: var(--textPrimary);
border-color: var(--divider) !important;
border-color: var(--divider);
}
.dark-mode #listingView.gallery .item .text {
+10 -1
View File
@@ -129,7 +129,11 @@ body.rtl #listingView {
#listingView.list .item:hover,
#listingView.compact .item:hover,
#listingView.normal .item:hover,
#listingView.gallery .item:hover {
#listingView.gallery .item:hover,
#listingView.list .item.drag-hover,
#listingView.compact .item.drag-hover,
#listingView.normal .item.drag-hover,
#listingView.gallery .item.drag-hover {
box-shadow: inset 0 -3em 3em rgba(217, 217, 217, 0.211), 0 0 0 2px var(--alt-background) !important;
/* Adjust shadow values as needed */
transform: scale(1.02);
@@ -142,6 +146,7 @@ body.rtl #listingView {
transform: translate(0, 5.5em);
text-shadow: 0 0 2px white;
pointer-events: none;
font-weight: bold;
}
#listingView.gallery .item .text .name {
@@ -430,4 +435,8 @@ body.rtl #listingView {
#listingView.list .header .active {
font-weight: bold;
}
.half-selected {
border-color: var(--primaryColor) !important;
}
+31 -9
View File
@@ -40,7 +40,13 @@
"continue": "تابع",
"edit": "تحرير",
"generateNewOtp": "إعادة تعيين وإنشاء رمز ثنائي العامل الجديد",
"verify": "تحقق"
"verify": "تحقق",
"pause": "وقفة",
"resume": "السيرة الذاتية",
"retry": "إعادة المحاولة",
"pauseAll": "إيقاف الكل مؤقتاً",
"resumeAll": "استئناف الكل",
"clearCompleted": "اكتمل المسح"
},
"download": {
"downloadFile": "Download File",
@@ -91,25 +97,27 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Create an account",
@@ -164,7 +172,11 @@
"optionalPassword": "كلمة مرور اختيارية",
"typeName": "النوع:",
"selected": "مختارة",
"deleted": "تم الحذف بنجاح!"
"deleted": "تم الحذف بنجاح!",
"destinationSource": "مصدر الوجهة:",
"dragAndDrop": "سحب الملفات وإفلاتها هنا",
"completed": "مكتمل",
"conflictsDetected": "التعارضات المكتشفة"
},
"search": {
"images": "الصور",
@@ -362,7 +374,8 @@
"toggleDark": "تبديل الوضع الداكن",
"toggleSticky": "تبديل الوضع اللاصق",
"of": "من",
"used": "مستعمل"
"used": "مستعمل",
"noSources": "لا توجد مصادر متاحة لهذا المستخدم. إما أن تقوم بإضافة مصدر لهذا المستخدم أو تكوين مصدر مع defaultEnabled: true"
},
"profileSettings": {
"setDateFormat": "تعيين تنسيق التاريخ الدقيق",
@@ -383,7 +396,9 @@
"sidebarOptions": "خيارات الشريط الجانبي",
"disableQuickToggles": "تعطيل التبديل السريع",
"disableSearchOptions": "تعطيل خيارات البحث",
"hideSidebarFileActions": "إخفاء إجراءات الملف في الشريط الجانبي"
"hideSidebarFileActions": "إخفاء إجراءات الملف في الشريط الجانبي",
"editorViewerOptions": "خيارات المحرر والعارض",
"editorQuickSave": "إظهار زر الحفظ السريع في المحرر"
},
"otp": {
"name": "المصادقة الثنائية",
@@ -395,5 +410,12 @@
"verificationFailed": "فشل التحقق",
"verificationSucceed": "نجاح التحقق",
"generationFailed": "فشل التوليد"
},
"fileLoading": {
"title": "التحميلات والتنزيلات",
"maxConcurrentUpload": "الحد الأقصى للتحميلات المتزامنة",
"maxConcurrentUploadHelp": "الحد الأقصى لعدد التحميلات المتزامنة. عادةً ما يكون أداء القيم الأعلى أفضل، ولكن هذا يعتمد على العديد من العوامل.",
"uploadChunkSizeMb": "حجم قطعة التحميل بالميجابايت",
"uploadChunkSizeMbHelp": "عادةً من 5 إلى 50 ميغابايت، والأصغر هو الأفضل للاتصالات غير المستقرة أو المقيدة. سيؤدي التعيين إلى 0 إلى تعطيل تقطيع التحميل."
}
}
+411
View File
@@ -0,0 +1,411 @@
{
"buttons": {
"generateNewOtp": "Resetovat a vygenerovat nový kód dvoufázového ověření",
"cancel": "Zrušit",
"verify": "Ověřit",
"close": "Zavřít",
"copy": "Kopírovat",
"copyFile": "Kopírovat soubor",
"copyToClipboard": "Kopírovat do schránky",
"copyDownloadLinkToClipboard": "Kopírovat odkaz ke stažení do schránky",
"create": "Vytvořit",
"delete": "Smazat",
"download": "Stáhnout",
"file": "Soubor",
"folder": "Složka",
"info": "Informace",
"more": "Více",
"move": "Přesunout",
"moveFile": "Přesunout soubor",
"new": "Nový",
"next": "Další",
"ok": "OK",
"permalink": "Získat trvalý odkaz",
"previous": "Předchozí",
"publish": "Publikovat",
"rename": "Přejmenovat",
"replace": "Nahradit",
"reportIssue": "Nahlásit problém",
"save": "Uložit",
"schedule": "Naplánovat",
"search": "Hledat",
"select": "Vybrat",
"selectMultiple": "Vybrat více",
"share": "Sdílet",
"shell": "Přepnout shell",
"submit": "Odeslat",
"switchView": "Přepnout zobrazení",
"toggleSidebar": "Přepnout postranní panel",
"update": "Aktualizovat",
"upload": "Nahrát",
"openFile": "Otevřít soubor",
"continue": "Pokračovat",
"edit": "Upravit",
"pause": "Pauza",
"resume": "Životopis",
"retry": "Opakování",
"pauseAll": "Pozastavení všech",
"resumeAll": "Obnovit vše",
"clearCompleted": "Vymazání dokončeno"
},
"download": {
"downloadFile": "Stáhnout soubor",
"downloadFolder": "Stáhnout složku",
"downloadSelected": "Stáhnout vybrané"
},
"upload": {
"abortUpload": "Opravdu chcete přerušit nahrávání?"
},
"errors": {
"forbidden": "Nemáte oprávnění k přístupu.",
"internal": "Něco se opravdu pokazilo.",
"notFound": "Tuto položku nelze najít.",
"connection": "Nelze se připojit k serveru."
},
"files": {
"body": "Tělo",
"clear": "Vymazat",
"closePreview": "Zavřít náhled",
"files": "Soubory",
"folders": "Složky",
"home": "Domů",
"lastModified": "Poslední změna",
"loading": "Načítání...",
"lonely": "Je tu nějak prázdno...",
"metadata": "Metadata",
"multipleSelectionEnabled": "Povoleno vícenásobné označení",
"name": "Název",
"size": "Velikost",
"sortByLastModified": "Řadit podle poslední změny",
"sortByName": "Řadit podle názvu",
"sortBySize": "Řadit podle velikosti",
"noPreview": "Náhled pro tento soubor není dostupný."
},
"help": {
"click": "označit soubor nebo složku",
"ctrl": {
"click": "označit více souborů nebo složek",
"f": "otevře hledání",
"s": "uložit soubor nebo stáhnout aktuální složku"
},
"del": "smazat označené položky",
"doubleClick": "otevřít soubor nebo složku",
"esc": "zrušit označení a/nebo zavřít dialog",
"f1": "tato nápověda",
"f2": "přejmenovat soubor",
"help": "Nápověda"
},
"languages": {
"he": "עברית",
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Vytvořit účet",
"loginInstead": "Už máte účet",
"password": "Heslo",
"passwordConfirm": "Potvrzení hesla",
"passwordsDontMatch": "Hesla se neshodují",
"signup": "Registrovat se",
"submit": "Přihlásit se",
"username": "Uživatelské jméno",
"usernameTaken": "Uživatelské jméno je již obsazeno",
"wrongCredentials": "Nesprávné přihlašovací údaje",
"or": "NEBO"
},
"permanent": "Trvalé",
"share": {
"notice": "Poznámka: Každý, kdo má přístup k odkazu (a volitelnému heslu), může zobrazit sdílené soubory. Není nutné být přihlášen."
},
"prompts": {
"copy": "Kopírovat",
"copyMessage": "Vyberte místo, kam chcete soubory kopírovat:",
"currentlyNavigating": "Aktuálně procházíte:",
"deleteMessageMultiple": "Opravdu chcete smazat {count} soubor(ů)?",
"deleteMessageSingle": "Opravdu chcete smazat tento soubor/složku?",
"deleteMessageShare": "Opravdu chcete smazat toto sdílení: {path} ?",
"deleteUserMessage": "Opravdu chcete tohoto uživatele smazat?",
"deleteTitle": "Smazat soubory",
"displayName": "Zobrazované jméno:",
"download": "Stáhnout soubory",
"downloadMessage": "Vyberte formát pro stažení.",
"error": "Nastala chyba",
"fileInfo": "Informace o souboru",
"selected": "označeno",
"filesSelected": "souborů vybráno",
"lastModified": "Poslední změna",
"move": "Přesunout",
"moveMessage": "Vyberte nové umístění pro soubor(y)/složku(y):",
"newArchetype": "Vytvořit nový příspěvek podle archetypu. Váš soubor bude vytvořen ve složce content.",
"newDir": "Nová složka",
"newDirMessage": "Zadejte název nové složky.",
"newFile": "Nový soubor",
"newFileMessage": "Zadejte název nového souboru.",
"numberDirs": "Počet složek",
"numberFiles": "Počet souborů",
"rename": "Přejmenovat",
"renameMessage": "Zadejte nový název pro",
"replace": "Nahradit",
"replaceMessage": "Jeden ze souborů, které se snažíte nahrát, má konflikt kvůli stejnému názvu. Chcete pokračovat v nahrávání nebo nahradit existující?",
"schedule": "Naplánovat",
"scheduleMessage": "Vyberte datum a čas naplánování publikace tohoto příspěvku.",
"show": "Zobrazit",
"size": "Velikost",
"upload": "Nahrát",
"uploadFiles": "Nahrávám {files} souborů...",
"uploadMessage": "Vyberte možnost pro nahrání.",
"optionalPassword": "Volitelné heslo",
"typeName": "Typ:",
"deleted": "Úspěšně smazáno!",
"destinationSource": "Zdroj určení:",
"dragAndDrop": "Přetáhněte sem soubory",
"completed": "Dokončeno",
"conflictsDetected": "Zjištěné konflikty"
},
"search": {
"images": "Obrázky",
"music": "Hudba",
"pdf": "PDF",
"pressToSearch": "Ve vyhledávání s indexem nebyly nalezeny žádné výsledky.",
"search": "Hledat...",
"typeToSearch": "Začněte psát pro hledání... (minimálně 3 znaky)",
"path": "Cesta:",
"types": "Typy",
"video": "Video",
"smallerThan": "Menší než:",
"largerThan": "Větší než:",
"helpText1": "Vyhledávání probíhá po každém napsaném znaku (minimálně 3 znaky pro hledaný výraz).",
"helpText2": "Index: Vyhledávání využívá index, který se automaticky aktualizuje v nastaveném intervalu (výchozí: 5 minut). Hledání ihned po spuštění programu může vracet neúplné výsledky."
},
"profileSettings": {
"setDateFormat": "Nastavit formát data",
"showHiddenFiles": "Zobrazit skryté soubory",
"showQuickDownload": "Zobrazit ikonu rychlého stahování",
"previewImages": "Náhled obrázků",
"previewVideos": "Náhled videí",
"previewMotionVideos": "Pohyblivý náhled videí",
"highQualityPreview": "Vysoce kvalitní náhled obrázků v galerii",
"editOffice": "Upravit/zobrazit kancelářské soubory",
"popupPreview": "Povolit vyskakovací náhled",
"filePreviewOptions": "Možnosti náhledu souborů",
"disableUpdateNotifications": "Zakázat upozornění na aktualizace v postranním panelu.",
"disableHideSidebar": "Zakázat automatické skrývání postranního panelu při náhledu nebo úpravách souborů.",
"autoplayMedia": "Automaticky přehrávat média v náhledu",
"deleteWithoutConfirming": "Mazat soubory bez potvrzovacího dialogu",
"sidebarOptions": "Možnosti postranního panelu",
"disableQuickToggles": "Zakázat rychlé přepínače",
"disableSearchOptions": "Zakázat možnosti vyhledávání",
"hideSidebarFileActions": "Skrýt akce se soubory v postranním panelu",
"editorViewerOptions": "Možnosti editoru a prohlížeče",
"editorQuickSave": "Zobrazení tlačítka rychlého uložení v editoru"
},
"settings": {
"enterPassword": "Zadejte heslo",
"enterPasswordAgain": "Zadejte heslo znovu",
"passwordsDoNotMatch": "Hesla se neshodují",
"loginMethod": "Způsob přihlášení",
"loginMethodDescription": "Povolený způsob přihlášení pro uživatele.",
"modifyOtherUser": "Upravit uživatele:",
"modifyCurrentUser": "Upravit aktuálního uživatele",
"themeColor": "Barva motivu",
"disableOfficeEditorDescription": "Seznam přípon souborů oddělených mezerou, pro které bude vypnut OnlyOffice prohlížeč (např. '.txt .html').",
"disableOfficeEditor": "Vypnout prohlížeč kancelářských souborů pro určité přípony",
"disableOfficePreviews": "Vypnout náhledy kancelářských souborů pro určité přípony",
"disableOfficePreviewsDescription": "Seznam přípon souborů oddělených mezerou, pro které bude vypnut náhled kancelářských dokumentů (např. '.txt .html').",
"listingOptions": "Možnosti zobrazení",
"searchOptions": "Možnosti vyhledávání",
"emptyGlobal": "Aktuálně nejsou žádné globální možnosti nastavení. Pro úpravu systémových nastavení upravte konfigurační soubor yaml.",
"scopes": "Definujte povolené zdroje a rozsah uživatele v nich",
"UserManagement": "Správa uživatelů",
"admin": "Správce",
"userNotAdmin": "Uživatel není administrátor",
"administrator": "Administrátor",
"allowCommands": "Povolit spouštění příkazů",
"allowEdit": "Upravit, přejmenovat a mazat soubory nebo složky",
"allowNew": "Vytvářet nové soubory a složky",
"allowPublish": "Publikovat nové příspěvky a stránky",
"allowSignup": "Povolit registraci uživatelů",
"avoidChanges": "Zaškrtněte také 'změnit heslo' pro použití tohoto hesla",
"branding": "Branding",
"brandingDirectoryPath": "Cesta k adresáři s brandingem",
"brandingHelp": "Můžete si upravit vzhled File Browseru změnou názvu, loga, přidáním vlastních stylů nebo zakázáním externích odkazů na GitHub.\nPro více informací o úpravách vzhledu navštivte {0}.",
"changePassword": "Změnit heslo",
"commandRunner": "Spouštěč příkazů",
"commandRunnerHelp": "Zde můžete nastavit příkazy, které se budou spouštět při vybraných událostech. Každý příkaz pište na nový řádek. Proměnné prostředí {0} a {1} budou dostupné; {0} je relativní k {1}. Pro více informací o této funkci a dostupných proměnných si přečtěte {2}.",
"commandsUpdated": "Příkazy aktualizovány!",
"createUserDir": "Automaticky vytvářet domovský adresář uživatele při přidání nového uživatele",
"tusUploads": "Dávkové nahrávání",
"tusUploadsHelp": "File Browser podporuje dávkové (chunked) nahrávání souborů, což umožňuje efektivní, spolehlivé a obnovitelné nahrávání i na nestabilních sítích.",
"tusUploadsChunkSize": "Určuje maximální velikost požadavku (přímé nahrávání bude použito pro menší soubory). Můžete zadat celé číslo v bajtech nebo např. 10MB, 1GB apod.",
"tusUploadsRetryCount": "Počet pokusů o opětovné nahrání části při chybě.",
"userHomeBasePath": "Základní cesta pro domovské adresáře uživatelů",
"userScopeGenerationPlaceholder": "Rozsah bude vygenerován automaticky",
"createUserHomeDirectory": "Vytvořit domovský adresář uživatele",
"customStylesheet": "Vlastní styl",
"defaultUserDescription": "Toto jsou výchozí nastavení pro nové uživatele.",
"disableExternalLinks": "Zakázat externí odkazy (kromě dokumentace)",
"disableUsedDiskPercentage": "Zakázat graf procentuálního využití disku",
"documentation": "dokumentace",
"examples": "Příklady",
"executeOnShell": "Spouštět v shellu",
"executeOnShellDescription": "Ve výchozím nastavení File Browser spouští příkazy přímo jako binárky. Pokud je chcete spouštět v shellu (například Bash nebo PowerShell), zadejte jej zde s požadovanými argumenty a přepínači. Pokud je nastaveno, zadaný příkaz bude připojen jako argument. Platí jak pro uživatelské příkazy, tak pro události.",
"globalRules": "Toto je globální sada povolených a zakázaných pravidel. Platí pro všechny uživatele. Pro konkrétní uživatele můžete definovat vlastní pravidla, která přepíší ta globální.",
"globalSettings": "Globální nastavení",
"shareSettings": "Nastavení sdílení",
"api": "API klíče",
"insertPath": "Vložit cestu",
"insertRegex": "Vložit regulární výraz",
"instanceName": "Název instance",
"language": "Jazyk",
"lockPassword": "Zabránit uživateli ve změně hesla",
"newPassword": "Nové heslo",
"newPasswordConfirm": "Potvrdit nové heslo",
"newUser": "Nový uživatel",
"password": "Heslo",
"passwordUpdated": "Heslo bylo aktualizováno!",
"path": "Cesta",
"permissions": {
"admin": "Administrátor",
"modify": "Upravit soubory",
"share": "Sdílet soubory",
"api": "Vytvářet a spravovat dlouhodobé API klíče",
"realtime": "Povolit real-time připojení a aktualizace"
},
"permissions-name": "Oprávnění",
"permissionsHelp": "Uživatele můžete nastavit jako administrátora nebo vybrat jednotlivá oprávnění. Pokud zvolíte \"Administrátor\", ostatní možnosti budou automaticky zaškrtnuty. Správa uživatelů zůstává pouze výsadou administrátora.\n",
"profileSettings": "Nastavení profilu",
"ruleExample1": "zamezí přístup ke všem skrytým souborům (např. .git, .gitignore) ve všech složkách.\n",
"ruleExample2": "blokuje přístup k souboru s názvem Caddyfile v kořeni rozsahu.",
"rules": "Pravidla",
"rulesHelp": "Zde můžete nastavit povolená a zakázaná pravidla pro konkrétního uživatele. Blokované soubory se nezobrazí v seznamu a uživatel k nim nebude mít přístup. Podporujeme regex a cesty relativní k rozsahu uživatele.\n",
"scope": "Rozsah",
"settingsUpdated": "Nastavení aktualizováno!",
"shareDuration": "Platnost vyprší",
"shareManagement": "Správa sdílení",
"shareDeleted": "Sdílení bylo smazáno!",
"singleClick": "Používat jednoduché kliknutí pro otevření souborů a složek",
"themes": {
"dark": "Tmavý",
"light": "Světlý",
"title": "Motiv"
},
"user": "Uživatel",
"userCommands": "Příkazy",
"userCommandsHelp": "Mezerami oddělený seznam dostupných příkazů pro tohoto uživatele. Příklad:\n",
"userCreated": "Uživatel vytvořen!",
"userDefaults": "Výchozí nastavení uživatele",
"userDeleted": "Uživatel smazán!",
"userManagement": "Správa uživatelů",
"userUpdated": "Uživatel aktualizován!",
"username": "Uživatelské jméno",
"users": "Uživatelé",
"adminOptions": "Možnosti administrátora"
},
"sidebar": {
"help": "Nápověda",
"hugoNew": "Nový Hugo",
"login": "Přihlásit se",
"logout": "Odhlásit se",
"myFiles": "Moje soubory",
"newFile": "Nový soubor",
"newFolder": "Nová složka",
"preview": "Náhled",
"settings": "Nastavení",
"sources": "Zdroje",
"signup": "Registrace",
"siteSettings": "Nastavení webu",
"updateIsAvailable": "Je dostupná aktualizace!",
"fileActions": "Akce se soubory"
},
"success": {
"linkCopied": "Odkaz zkopírován!"
},
"time": {
"days": "Dny",
"hours": "Hodiny",
"minutes": "Minuty",
"seconds": "Sekundy",
"unit": "Časová jednotka"
},
"api": {
"title": "API klíče",
"keyName": "Název API klíče:",
"createdAt": "Vytvořeno:",
"expiresAt": "Platnost do:",
"permissions": "Oprávnění:",
"createTitle": "Vytvořit API klíč",
"keyNamePlaceholder": "Zadejte jedinečný název API klíče:",
"tokenDuration": "Doba platnosti tokenu:",
"durationNumberPlaceholder": "Počet",
"days": "Dny",
"months": "Měsíce",
"permissionNote": "Vyberte alespoň jedno oprávnění pro klíč. Váš uživatel musí mít také toto oprávnění.",
"createKeyFailed": "Nepodařilo se vytvořit API klíč.",
"createKeySuccess": "API klíč byl úspěšně vytvořen.",
"description": "API klíče jsou vázané na uživatele, který je vytvořil. Viz",
"swaggerLinkText": "stránka swagger",
"name": "Název",
"created": "Vytvořeno",
"expires": "Vyprší",
"actions": "Akce",
"enabled": "Povoleno",
"disabled": "Zakázáno"
},
"index": {
"status": "Stav",
"assessment": "Vyhodnocení",
"files": "Soubory",
"folders": "Složky",
"lastScanned": "Poslední skenování",
"quickScan": "Rychlý sken (s)",
"fullScan": "Plný sken (s)",
"settingsHover": "Nastavení pro uživatele",
"logout": "Odhlásit se",
"toggleClick": "Přepnout jedno kliknutí",
"toggleDark": "Přepnout tmavý režim",
"toggleSticky": "Přepnout sticky režim",
"of": "z",
"used": "použito",
"noSources": "Pro tohoto uživatele nejsou dostupné žádné zdroje. Přidejte zdroj pro uživatele nebo nastavte zdroj s defaultEnabled: true"
},
"otp": {
"name": "Dvoufázové ověření",
"verifyInstructions": "Zadejte kód z autentizační aplikace",
"generate": "Naskenujte QR kód ve své autentizační aplikaci",
"codeInputPlaceholder": "6místný kód",
"invalidCodeType": "Ověřovací kód musí obsahovat 6 číslic",
"enabledSuccessfully": "OTP úspěšně aktivováno",
"verificationFailed": "Ověření se nezdařilo",
"verificationSucceed": "Ověření proběhlo úspěšně",
"generationFailed": "Generování selhalo"
},
"fileLoading": {
"title": "Nahrávání a stahování",
"maxConcurrentUpload": "Maximální souběžné nahrávání",
"maxConcurrentUploadHelp": "Maximální počet souběžných odesílání. Vyšší hodnoty jsou obvykle výkonnější, ale záleží na mnoha faktorech.",
"uploadChunkSizeMb": "Velikost nahraného souboru v MB",
"uploadChunkSizeMbHelp": "Obvykle 5 až 50 MB, menší velikost je lepší pro nestabilní nebo omezené připojení. Nastavením na hodnotu 0 se vypne rozdělování nahrávání."
}
}
+27 -5
View File
@@ -40,7 +40,13 @@
"copyDownloadLinkToClipboard": "Download-Link in die Zwischenablage kopieren",
"edit": "bearbeiten",
"generateNewOtp": "Zurücksetzen und Generieren eines neuen Zwei-Faktor-Codes",
"verify": "Überprüfen Sie"
"verify": "Überprüfen Sie",
"pause": "Pause",
"resume": "Lebenslauf",
"retry": "Wiederholung",
"pauseAll": "Pause alle",
"resumeAll": "Alle zusammenfassen",
"clearCompleted": "Löschen abgeschlossen"
},
"download": {
"downloadFile": "Datei herunterladen",
@@ -91,6 +97,7 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@@ -109,7 +116,8 @@
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Konto erstellen",
@@ -164,7 +172,11 @@
"filesSelected": "ausgewählte Dateien",
"typeName": "Art:",
"selected": "ausgewählt",
"deleted": "Erfolgreich gelöscht!"
"deleted": "Erfolgreich gelöscht!",
"destinationSource": "Ziel Quelle:",
"dragAndDrop": "Dateien hierher ziehen und ablegen",
"completed": "Abgeschlossen",
"conflictsDetected": "Erkannte Widersprüche"
},
"search": {
"images": "Bilder",
@@ -362,7 +374,8 @@
"toggleDark": "Dunkelmodus umschalten",
"toggleSticky": "Sticky-Modus umschalten",
"of": "von",
"used": "belegt"
"used": "belegt",
"noSources": "Für diesen Benutzer sind keine Quellen verfügbar. Entweder eine Quelle für diesen Benutzer hinzufügen oder eine Quelle mit defaultEnabled: true konfigurieren"
},
"profileSettings": {
"setDateFormat": "Genaues Datumsformat einstellen",
@@ -383,7 +396,9 @@
"sidebarOptions": "Optionen für die Seitenleiste",
"disableQuickToggles": "Quick Toggles deaktivieren",
"disableSearchOptions": "Suchoptionen deaktivieren",
"hideSidebarFileActions": "Datei-Aktionen in der Seitenleiste ausblenden"
"hideSidebarFileActions": "Datei-Aktionen in der Seitenleiste ausblenden",
"editorViewerOptions": "Editor- und Viewer-Optionen",
"editorQuickSave": "Schaltfläche zum schnellen Speichern im Editor anzeigen"
},
"otp": {
"name": "Zwei-Faktoren-Authentifizierung",
@@ -395,5 +410,12 @@
"verificationFailed": "Verifizierung fehlgeschlagen",
"verificationSucceed": "Überprüfung Erfolgreich",
"generationFailed": "Generation gescheitert"
},
"fileLoading": {
"title": "Uploads & Downloads",
"maxConcurrentUpload": "Max. gleichzeitige Uploads",
"maxConcurrentUploadHelp": "Die maximale Anzahl der gleichzeitigen Uploads. Höhere Werte führen in der Regel zu einer besseren Leistung, aber dies hängt von vielen Faktoren ab.",
"uploadChunkSizeMb": "Größe des hochzuladenden Pakets in MB",
"uploadChunkSizeMbHelp": "Normalerweise 5 bis 50 MB, kleiner ist besser für instabile oder eingeschränkte Verbindungen. Die Einstellung 0 deaktiviert das Chunking beim Hochladen."
}
}
+27 -6
View File
@@ -40,7 +40,13 @@
"shell": "Εναλλαγή κελύφους",
"edit": "Επεξεργασία",
"generateNewOtp": "Επαναφορά και δημιουργία νέου κωδικού δύο παραγόντων",
"verify": "Επαλήθευση"
"verify": "Επαλήθευση",
"pause": "Παύση",
"resume": "Βιογραφικό σημείωμα",
"retry": "Επανάληψη",
"pauseAll": "Παύση όλων",
"resumeAll": "Συνέχισε όλα",
"clearCompleted": "Εκκαθάριση ολοκληρώθηκε"
},
"download": {
"downloadFile": "Λήψη αρχείου",
@@ -94,9 +100,9 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"el": "Ελληνικά",
"fr": "Français",
"is": "Icelandic",
"it": "Italiano",
@@ -113,7 +119,8 @@
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Δημιουργία λογαριασμού",
@@ -168,7 +175,11 @@
"filesSelected": "επιλεγμένα αρχεία",
"typeName": "Τύπος:",
"selected": "επιλεγμένο",
"deleted": "Διαγράφηκε με επιτυχία!"
"deleted": "Διαγράφηκε με επιτυχία!",
"destinationSource": "Πηγή προορισμού:",
"dragAndDrop": "Σύρετε και αφήστε αρχεία εδώ",
"completed": "Ολοκληρωμένο",
"conflictsDetected": "Ανιχνεύονται συγκρούσεις"
},
"search": {
"images": "Εικόνες",
@@ -363,7 +374,8 @@
"toggleDark": "Εναλλαγή σκοτεινής λειτουργίας",
"toggleSticky": "Εναλλαγή της λειτουργίας Sticky",
"of": "του",
"used": "χρησιμοποιημένο"
"used": "χρησιμοποιημένο",
"noSources": "Δεν υπάρχουν διαθέσιμες πηγές για αυτόν τον χρήστη. Είτε προσθέστε μια πηγή για αυτόν τον χρήστη είτε ρυθμίστε μια πηγή με defaultEnabled: true"
},
"profileSettings": {
"setDateFormat": "Ορισμός ακριβούς μορφής ημερομηνίας",
@@ -384,7 +396,9 @@
"sidebarOptions": "Επιλογές πλευρικής γραμμής",
"disableQuickToggles": "Απενεργοποίηση γρήγορων εναλλαγών",
"disableSearchOptions": "Απενεργοποίηση επιλογών αναζήτησης",
"hideSidebarFileActions": "Απόκρυψη ενεργειών αρχείων στην πλευρική γραμμή"
"hideSidebarFileActions": "Απόκρυψη ενεργειών αρχείων στην πλευρική γραμμή",
"editorViewerOptions": "Επιλογές επεξεργασίας και προβολής",
"editorQuickSave": "Εμφάνιση κουμπιού γρήγορης αποθήκευσης στον επεξεργαστή"
},
"otp": {
"name": "Αυθεντικοποίηση δύο παραγόντων",
@@ -396,5 +410,12 @@
"verificationFailed": "Η επαλήθευση απέτυχε",
"verificationSucceed": "Επαλήθευση Succeed",
"generationFailed": "Γενιά που απέτυχε"
},
"fileLoading": {
"title": "Ανεβάσματα & λήψεις",
"maxConcurrentUpload": "Μέγιστες ταυτόχρονες μεταφορτώσεις",
"maxConcurrentUploadHelp": "Ο μέγιστος αριθμός ταυτόχρονων μεταφορτώσεων. Υψηλότερες τιμές συνήθως έχουν καλύτερες επιδόσεις, αλλά αυτό εξαρτάται από πολλούς παράγοντες.",
"uploadChunkSizeMb": "Μέγεθος τμήματος φόρτωσης σε MB",
"uploadChunkSizeMbHelp": "Συνήθως 5 έως 50 MB, τα μικρότερα είναι καλύτερα για ασταθείς ή περιορισμένες συνδέσεις. Η ρύθμιση σε 0 θα απενεργοποιήσει την ομαδοποίηση μεταφόρτωσης."
}
}
+24 -4
View File
@@ -40,7 +40,13 @@
"upload": "Upload",
"openFile": "Open file",
"continue": "Continue",
"edit": "Edit"
"edit": "Edit",
"pause": "Pause",
"resume": "Resume",
"retry": "Retry",
"pauseAll": "Pause all",
"resumeAll": "Resume all",
"clearCompleted": "Clear completed"
},
"download": {
"downloadFile": "Download File",
@@ -113,7 +119,8 @@
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Create an account",
@@ -133,6 +140,7 @@
"notice": "Note: anyone who has access to the link (and optional password) can access the shared files. There is no requirement to be logged in."
},
"prompts": {
"destinationSource": "Destination Source:",
"copy": "Copy",
"copyMessage": "Choose the place to copy your files:",
"currentlyNavigating": "Currently navigating on:",
@@ -144,7 +152,9 @@
"displayName": "Display Name:",
"download": "Download files",
"downloadMessage": "Choose the format you want to download.",
"dragAndDrop": "Drag and drop files here",
"error": "Something went wrong",
"completed": "Completed",
"fileInfo": "File information",
"selected": "selected",
"filesSelected": "files selected",
@@ -171,7 +181,8 @@
"uploadMessage": "Select an option to upload.",
"optionalPassword": "Optional password",
"typeName": "Type:",
"deleted": "Deleted successfully!"
"deleted": "Deleted successfully!",
"conflictsDetected": "Conflicts detected"
},
"search": {
"images": "Images",
@@ -206,7 +217,9 @@
"sidebarOptions": "Sidebar options",
"disableQuickToggles": "Disable quick toggles",
"disableSearchOptions": "Disable search options",
"hideSidebarFileActions": "Hide file actions in sidebar"
"hideSidebarFileActions": "Hide file actions in sidebar",
"editorViewerOptions": "Editor and viewer options",
"editorQuickSave": "Show quick save button in editor"
},
"settings": {
"enterPassword": "Enter password",
@@ -387,5 +400,12 @@
"verificationFailed": "Verification Failed",
"verificationSucceed": "Verification Succeed",
"generationFailed": "Generation Failed"
},
"fileLoading": {
"title": "Uploads & Downloads",
"maxConcurrentUpload": "Max concurrent uploads",
"maxConcurrentUploadHelp": "The maximum number of concurrent uploads. Higher values typically perform better, but this depends on many factors.",
"uploadChunkSizeMb": "Upload chunk size in MB",
"uploadChunkSizeMbHelp": "Typically 5 to 50 MB, smaller is better for unstable or restricted connections. Setting to 0 will disable upload chunking."
}
}
+29 -7
View File
@@ -40,7 +40,13 @@
"continue": "Continúe en",
"edit": "Editar",
"generateNewOtp": "Restablecer y generar un nuevo código de dos factores",
"verify": "Verifique"
"verify": "Verifique",
"pause": "Pausa",
"resume": "Currículum",
"retry": "Reintentar",
"pauseAll": "Pausa todo",
"resumeAll": "Reanudar todo",
"clearCompleted": "Despejado completado"
},
"download": {
"downloadFile": "Descargar fichero",
@@ -91,6 +97,7 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@@ -98,18 +105,19 @@
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Niederländisch (Belgien)",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "Românesc",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Crear una cuenta",
@@ -164,7 +172,11 @@
"filesSelected": "archivos seleccionados",
"typeName": "Tipo:",
"selected": "seleccionado",
"deleted": "¡Borrado con éxito!"
"deleted": "¡Borrado con éxito!",
"destinationSource": "Fuente de destino:",
"dragAndDrop": "Arrastre y suelte los archivos aquí",
"completed": "Completado",
"conflictsDetected": "Conflictos detectados"
},
"search": {
"images": "Imágenes",
@@ -362,7 +374,8 @@
"toggleDark": "Activar el modo oscuro",
"toggleSticky": "Conmutar el modo adhesivo",
"of": "de",
"used": "usado"
"used": "usado",
"noSources": "No hay fuentes disponibles para este usuario. Añada una fuente para este usuario o configure una fuente con defaultEnabled: true."
},
"profileSettings": {
"setDateFormat": "Establecer el formato exacto de la fecha",
@@ -383,7 +396,9 @@
"sidebarOptions": "Opciones de la barra lateral",
"disableQuickToggles": "Desactivar alternancias rápidas",
"disableSearchOptions": "Desactivar las opciones de búsqueda",
"hideSidebarFileActions": "Ocultar las acciones de archivo en la barra lateral"
"hideSidebarFileActions": "Ocultar las acciones de archivo en la barra lateral",
"editorViewerOptions": "Opciones de edición y visualización",
"editorQuickSave": "Mostrar botón de guardado rápido en el editor"
},
"otp": {
"name": "Autenticación de dos factores",
@@ -395,5 +410,12 @@
"verificationFailed": "Verificación fallida",
"verificationSucceed": "Verificación Éxito",
"generationFailed": "Generación fallida"
},
"fileLoading": {
"title": "Cargas y descargas",
"maxConcurrentUpload": "Carga máxima simultánea",
"maxConcurrentUploadHelp": "El número máximo de cargas simultáneas. Los valores más altos suelen dar mejores resultados, pero esto depende de muchos factores.",
"uploadChunkSizeMb": "Tamaño del chunk de subida en MB",
"uploadChunkSizeMbHelp": "Típicamente 5 a 50 MB, más pequeño es mejor para conexiones inestables o restringidas. Si se pone a 0, se desactiva la carga en trozos."
}
}
+31 -9
View File
@@ -40,7 +40,13 @@
"continue": "Continuer",
"edit": "Editer",
"generateNewOtp": "Réinitialiser et générer un nouveau code à deux facteurs",
"verify": "Vérifier"
"verify": "Vérifier",
"pause": "Pause",
"resume": "Curriculum vitae",
"retry": "Réessayer",
"pauseAll": "Pause tous",
"resumeAll": "Curriculum vitae",
"clearCompleted": "Effacement terminé"
},
"download": {
"downloadFile": "Télécharger le fichier",
@@ -91,25 +97,27 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "Íslenska",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Nederlands",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "Română",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "Svenska",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Créer un compte",
@@ -164,7 +172,11 @@
"filesSelected": "fichiers sélectionnés",
"typeName": "Type :",
"selected": "sélectionné",
"deleted": "Supprimé avec succès !"
"deleted": "Supprimé avec succès !",
"destinationSource": "Destination Source :",
"dragAndDrop": "Glisser-déposer des fichiers ici",
"completed": "Terminé",
"conflictsDetected": "Conflits détectés"
},
"search": {
"images": "Images",
@@ -362,7 +374,8 @@
"toggleDark": "Mode sombre/clair",
"toggleSticky": "Activer/Désactiver le mode persistent",
"of": "de",
"used": "utilisé"
"used": "utilisé",
"noSources": "Aucune source n'est disponible pour cet utilisateur. Ajoutez une source pour cet utilisateur ou configurez une source avec defaultEnabled : true"
},
"profileSettings": {
"setDateFormat": "Définir le format exact de la date",
@@ -383,7 +396,9 @@
"sidebarOptions": "Options de la barre latérale",
"disableQuickToggles": "Désactiver les bascules rapides",
"disableSearchOptions": "Désactiver les options de recherche",
"hideSidebarFileActions": "Cacher les actions sur les fichiers dans la barre latérale"
"hideSidebarFileActions": "Cacher les actions sur les fichiers dans la barre latérale",
"editorViewerOptions": "Options d'édition et de visualisation",
"editorQuickSave": "Afficher le bouton d'enregistrement rapide dans l'éditeur"
},
"otp": {
"name": "Authentification à deux facteurs",
@@ -395,5 +410,12 @@
"verificationFailed": "Échec de la vérification",
"verificationSucceed": "Vérification réussie",
"generationFailed": "Génération échouée"
},
"fileLoading": {
"title": "Chargements et téléchargements",
"maxConcurrentUpload": "Nombre maximum de téléchargements simultanés",
"maxConcurrentUploadHelp": "Nombre maximal de téléchargements simultanés. Des valeurs plus élevées sont généralement plus performantes, mais cela dépend de nombreux facteurs.",
"uploadChunkSizeMb": "Taille de la zone de téléchargement en Mo",
"uploadChunkSizeMbHelp": "Généralement de 5 à 50 Mo, une taille inférieure est préférable pour les connexions instables ou restreintes. La valeur 0 désactive le fractionnement de l'upload."
}
}
+27 -5
View File
@@ -40,7 +40,13 @@
"verify": "אמת",
"copyDownloadLinkToClipboard": "העתק את קישור ההורדה ללוח",
"continue": "המשך",
"edit": "ערוך"
"edit": "ערוך",
"pause": "השהה",
"resume": "קורות חיים",
"retry": "נסה שוב",
"pauseAll": "השהה הכל",
"resumeAll": "המשך הכל",
"clearCompleted": "השלם"
},
"download": {
"downloadFile": "הורד קובץ",
@@ -91,6 +97,7 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@@ -109,7 +116,8 @@
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "צור חשבון",
@@ -164,7 +172,11 @@
"selected": "נבחר",
"filesSelected": "קבצים שנבחרו",
"typeName": "סוג:",
"deleted": "נמחק בהצלחה!"
"deleted": "נמחק בהצלחה!",
"destinationSource": "מקור היעד:",
"dragAndDrop": "גרור ושחרר קבצים לכאן",
"completed": "הושלם",
"conflictsDetected": "סכסוכים שזוהו"
},
"search": {
"images": "תמונות",
@@ -359,7 +371,8 @@
"toggleDark": "הפעל מצב כהה",
"toggleSticky": "הפעל/כבה מצב דביק",
"of": "של",
"used": "משומש"
"used": "משומש",
"noSources": "אין מקורות זמינים עבור משתמש זה. הוסף מקור עבור משתמש זה או הגדר מקור עם defaultEnabled: true"
},
"profileSettings": {
"setDateFormat": "הגדר פורמט תאריך מדויק",
@@ -379,7 +392,9 @@
"sidebarOptions": "אפשרויות סרגל צד",
"disableQuickToggles": "השבת מתגים מהירים",
"disableSearchOptions": "השבת אפשרויות חיפוש",
"hideSidebarFileActions": "הסתר פעולות קובץ בסרגל הצד"
"hideSidebarFileActions": "הסתר פעולות קובץ בסרגל הצד",
"editorViewerOptions": "אפשרויות עורך וצופה",
"editorQuickSave": "הצג כפתור שמירה מהירה בעורך"
},
"otp": {
"name": "אימות דו-גורמי",
@@ -391,5 +406,12 @@
"verificationFailed": "האימות נכשל",
"verificationSucceed": "האימות הצליח",
"generationFailed": "דור שנכשל"
},
"fileLoading": {
"title": "העלאות והורדות",
"maxConcurrentUpload": "העלאות מקסימליות בו-זמנית",
"maxConcurrentUploadHelp": "המספר המרבי של העלאות בו-זמנית. ערכים גבוהים יותר בדרך כלל מספקים ביצועים טובים יותר, אך הדבר תלוי בגורמים רבים.",
"uploadChunkSizeMb": "גודל הקובץ להעלאה במגה-בייט",
"uploadChunkSizeMbHelp": "בדרך כלל 5 עד 50 MB, קטן יותר עדיף עבור חיבורים לא יציבים או מוגבלים. הגדרה ל-0 תשבית את חלוקת הקבצים להעלאה."
}
}
+27 -5
View File
@@ -40,7 +40,13 @@
"continue": "Folytatás",
"edit": "Szerkesztés",
"generateNewOtp": "Új kétfaktoros kód visszaállítása és generálása",
"verify": "Ellenőrizze a címet."
"verify": "Ellenőrizze a címet.",
"pause": "Szünet",
"resume": "Önéletrajz",
"retry": "Retry",
"pauseAll": "Szünet minden",
"resumeAll": "Folytassa az összes",
"clearCompleted": "Tiszta befejezve"
},
"download": {
"downloadFile": "Fájl letöltése",
@@ -91,6 +97,7 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@@ -109,7 +116,8 @@
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Fiók létrehozása",
@@ -164,7 +172,11 @@
"filesSelected": "kiválasztott fájlok",
"typeName": "Típus:",
"selected": "kiválasztott",
"deleted": "Sikeresen törölve!"
"deleted": "Sikeresen törölve!",
"destinationSource": "Célforrás:",
"dragAndDrop": "Húzza ide a fájlokat",
"completed": "Befejezett",
"conflictsDetected": "Feltárt konfliktusok"
},
"search": {
"images": "Képek",
@@ -362,7 +374,8 @@
"toggleDark": "Sötét üzemmód átkapcsolása",
"toggleSticky": "Ragadós mód átkapcsolása",
"of": "a",
"used": "használt"
"used": "használt",
"noSources": "Ehhez a felhasználóhoz nem állnak rendelkezésre források. Vagy adjon hozzá egy forrást ehhez a felhasználóhoz, vagy konfiguráljon egy forrást a defaultEnabled: true beállítással."
},
"profileSettings": {
"setDateFormat": "Pontos dátumformátum beállítása",
@@ -383,7 +396,9 @@
"sidebarOptions": "Oldalsáv beállítások",
"disableQuickToggles": "Gyorskapcsolók letiltása",
"disableSearchOptions": "Keresési lehetőségek letiltása",
"hideSidebarFileActions": "Fájlműveletek elrejtése az oldalsávban"
"hideSidebarFileActions": "Fájlműveletek elrejtése az oldalsávban",
"editorViewerOptions": "Szerkesztési és megjelenítési lehetőségek",
"editorQuickSave": "Gyors mentés gomb megjelenítése a szerkesztőben"
},
"otp": {
"name": "Kétfaktoros hitelesítés",
@@ -395,5 +410,12 @@
"verificationFailed": "Ellenőrzés sikertelen",
"verificationSucceed": "Ellenőrzés Succeed",
"generationFailed": "Nemzedék sikertelen"
},
"fileLoading": {
"title": "Feltöltések és letöltések",
"maxConcurrentUpload": "Maximális egyidejű feltöltés",
"maxConcurrentUploadHelp": "Az egyidejű feltöltések maximális száma. A magasabb értékek általában jobban teljesítenek, de ez sok tényezőtől függ.",
"uploadChunkSizeMb": "Feltöltési csomag mérete MB-ban",
"uploadChunkSizeMbHelp": "Általában 5 és 50 MB között, instabil vagy korlátozott kapcsolatok esetén a kisebb méret jobb. A 0 értékre állítás kikapcsolja a feltöltési darabolás funkciót."
}
}
+15 -12
View File
@@ -25,6 +25,7 @@ import ua from './ua.json';
import svSE from './sv-se.json';
import zhCN from './zh-cn.json';
import zhTW from './zh-tw.json';
import cz from './cz.json';
type LocaleMap = { [key: string]: string };
@@ -40,23 +41,24 @@ export function detectLocale(): string {
'is': 'is',
'it': 'it',
'fr': 'fr',
'pt-br': 'pt-br',
'pt-br': 'ptBR',
'pt': 'pt',
'ja': 'ja',
'zh-tw': 'zh-tw',
'zh-cn': 'zh-cn',
'zh': 'zh-cn',
'zh-tw': 'zhTW',
'zh-cn': 'zhCN',
'zh': 'zhCN',
'de': 'de',
'ro': 'ro',
'ru': 'ru',
'pl': 'pl',
'ko': 'ko',
'cz': 'cz',
'sk': 'sk',
'tr': 'tr',
'uk': 'uk',
'sv-se': 'sv',
'sv': 'sv',
'nl-be': 'nl-be',
'sv-se': 'svSE',
'sv': 'svSE',
'nl-be': 'nlBE',
};
for (const key in localeMap) {
@@ -103,17 +105,18 @@ const i18n = createI18n({
it,
ja,
ko,
'nl-be': nlBE,
nlBE,
pl,
'pt-br': ptBR,
ptBR,
pt,
ru,
ro,
sk,
'sv-se': svSE,
svSE,
ua,
'zh-cn': zhCN,
'zh-tw': zhTW,
zhCN,
zhTW,
cz,
},
});
+31 -9
View File
@@ -40,7 +40,13 @@
"continue": "Continua",
"edit": "Modifica",
"generateNewOtp": "Reimpostare e generare un nuovo codice a due fattori",
"verify": "Verifica"
"verify": "Verifica",
"pause": "Pausa",
"resume": "Il curriculum",
"retry": "Riprova",
"pauseAll": "Mettere in pausa tutti",
"resumeAll": "Riprendete tutti",
"clearCompleted": "Completato il lavoro di pulizia"
},
"download": {
"downloadFile": "Scarica file",
@@ -91,25 +97,27 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Crea un account",
@@ -164,7 +172,11 @@
"optionalPassword": "Password opzionale",
"typeName": "Tipo:",
"selected": "selezionato",
"deleted": "Eliminato con successo!"
"deleted": "Eliminato con successo!",
"destinationSource": "Sorgente di destinazione:",
"dragAndDrop": "Trascinare e rilasciare i file qui",
"completed": "Completato",
"conflictsDetected": "Conflitti rilevati"
},
"search": {
"images": "Immagini",
@@ -362,7 +374,8 @@
"toggleDark": "Alterna la modalità scura",
"toggleSticky": "Alterna la modalità appiccicosa",
"of": "di",
"used": "usato"
"used": "usato",
"noSources": "Non ci sono fonti disponibili per questo utente. Aggiungere una fonte per questo utente o configurare una fonte con defaultEnabled: true."
},
"profileSettings": {
"setDateFormat": "Impostare il formato esatto della data",
@@ -383,7 +396,9 @@
"sidebarOptions": "Opzioni della barra laterale",
"disableQuickToggles": "Disattivare i toggle rapidi",
"disableSearchOptions": "Disattivare le opzioni di ricerca",
"hideSidebarFileActions": "Nascondere le azioni dei file nella barra laterale"
"hideSidebarFileActions": "Nascondere le azioni dei file nella barra laterale",
"editorViewerOptions": "Opzioni dell'editor e del visualizzatore",
"editorQuickSave": "Mostra il pulsante di salvataggio rapido nell'editor"
},
"otp": {
"name": "Autenticazione a due fattori",
@@ -395,5 +410,12 @@
"verificationFailed": "Verifica fallita",
"verificationSucceed": "Verifica riuscita",
"generationFailed": "Generazione fallita"
},
"fileLoading": {
"title": "Caricamenti e download",
"maxConcurrentUpload": "Carichi massimi contemporanei",
"maxConcurrentUploadHelp": "Il numero massimo di upload contemporanei. I valori più alti sono in genere più performanti, ma ciò dipende da molti fattori.",
"uploadChunkSizeMb": "Dimensione dei pezzi caricati in MB",
"uploadChunkSizeMbHelp": "In genere da 5 a 50 MB, ma per connessioni instabili o limitate è meglio che sia più piccolo. Impostando 0 si disattiva il chunking del caricamento."
}
}
+31 -9
View File
@@ -40,7 +40,13 @@
"continue": "続ける",
"edit": "編集",
"generateNewOtp": "リセットして新しい2ファクタコードを生成する",
"verify": "ベリファイ"
"verify": "ベリファイ",
"pause": "ポーズ",
"resume": "履歴書",
"retry": "リトライ",
"pauseAll": "一時停止",
"resumeAll": "すべての履歴書",
"clearCompleted": "クリア完了"
},
"download": {
"downloadFile": "Download File",
@@ -91,25 +97,27 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Create an account",
@@ -164,7 +172,11 @@
"optionalPassword": "任意のパスワード",
"typeName": "タイプ",
"selected": "選ばれた",
"deleted": "正常に削除されました!"
"deleted": "正常に削除されました!",
"destinationSource": "デスティネーション・ソース",
"dragAndDrop": "ここにファイルをドラッグ&ドロップする",
"completed": "完成",
"conflictsDetected": "コンフリクト検出"
},
"search": {
"images": "画像",
@@ -362,7 +374,8 @@
"toggleDark": "ダークモードの切り替え",
"toggleSticky": "スティッキーモードの切り替え",
"of": "の",
"used": "中古"
"used": "中古",
"noSources": "このユーザが使用できるソースはありません。このユーザのソースを追加するか、defaultEnabled: true でソースを構成します。"
},
"profileSettings": {
"setDateFormat": "正確な日付書式を設定する",
@@ -383,7 +396,9 @@
"sidebarOptions": "サイドバーオプション",
"disableQuickToggles": "クイックトグルを無効にする",
"disableSearchOptions": "検索オプションを無効にする",
"hideSidebarFileActions": "サイドバーにファイルアクションを隠す"
"hideSidebarFileActions": "サイドバーにファイルアクションを隠す",
"editorViewerOptions": "エディターとビューアーのオプション",
"editorQuickSave": "エディタにクイック保存ボタンを表示"
},
"otp": {
"name": "二要素認証",
@@ -395,5 +410,12 @@
"verificationFailed": "検証失敗",
"verificationSucceed": "検証の成功",
"generationFailed": "世代が失敗"
},
"fileLoading": {
"title": "アップロードとダウンロード",
"maxConcurrentUpload": "最大同時アップロード数",
"maxConcurrentUploadHelp": "同時アップロードの最大数。一般的に高い値の方がパフォーマンスが良いですが、これは多くの要因に依存します。",
"uploadChunkSizeMb": "アップロードチャンクサイズ(MB",
"uploadChunkSizeMbHelp": "通常5~50MBですが、不安定な接続や制限のある接続の場合は小さい方が良いでしょう。0に設定するとアップロードチャンキングが無効になります。"
}
}
+31 -9
View File
@@ -40,7 +40,13 @@
"continue": "계속하기",
"edit": "편집",
"generateNewOtp": "새로운 2단계 인증 코드 재설정 및 생성",
"verify": "확인"
"verify": "확인",
"pause": "일시 중지",
"resume": "이력서",
"retry": "다시 시도",
"pauseAll": "모두 일시 중지",
"resumeAll": "모두 다시 시작",
"clearCompleted": "완료 지우기"
},
"download": {
"downloadFile": "파일 다운로드",
@@ -91,25 +97,27 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "계정 생성",
@@ -164,7 +172,11 @@
"optionalPassword": "선택적 비밀번호",
"typeName": "유형:",
"selected": "선택",
"deleted": "성공적으로 삭제되었습니다!"
"deleted": "성공적으로 삭제되었습니다!",
"destinationSource": "대상 소스:",
"dragAndDrop": "파일을 여기로 끌어다 놓기",
"completed": "완료",
"conflictsDetected": "충돌 감지"
},
"search": {
"images": "이미지",
@@ -362,7 +374,8 @@
"toggleDark": "다크 모드 전환",
"toggleSticky": "고정 모드 토글",
"of": "의",
"used": "사용됨"
"used": "사용됨",
"noSources": "이 사용자에 대해 사용할 수 있는 소스가 없습니다. 이 사용자에 대한 소스를 추가하거나 defaultEnabled: true로 소스를 구성하세요."
},
"profileSettings": {
"setDateFormat": "정확한 날짜 형식 설정",
@@ -383,7 +396,9 @@
"sidebarOptions": "사이드바 옵션",
"disableQuickToggles": "빠른 토글 비활성화",
"disableSearchOptions": "검색 옵션 비활성화",
"hideSidebarFileActions": "사이드바에서 파일 작업 숨기기"
"hideSidebarFileActions": "사이드바에서 파일 작업 숨기기",
"editorViewerOptions": "편집기 및 뷰어 옵션",
"editorQuickSave": "편집기에 빠른 저장 버튼 표시"
},
"otp": {
"name": "2단계 인증",
@@ -395,5 +410,12 @@
"verificationFailed": "인증 실패",
"verificationSucceed": "인증 성공",
"generationFailed": "세대 실패"
},
"fileLoading": {
"title": "업로드 및 다운로드",
"maxConcurrentUpload": "최대 동시 업로드 수",
"maxConcurrentUploadHelp": "최대 동시 업로드 수입니다. 일반적으로 값이 높을수록 성능이 향상되지만, 이는 여러 요인에 따라 달라집니다.",
"uploadChunkSizeMb": "업로드 청크 크기(MB)",
"uploadChunkSizeMbHelp": "일반적으로 5~50MB이며, 연결이 불안정하거나 제한적인 경우에는 이보다 작을수록 좋습니다. 0으로 설정하면 업로드 청킹이 비활성화됩니다."
}
}
+44 -22
View File
@@ -40,7 +40,13 @@
"submit": "Stuur in",
"openFile": "Bestand openen",
"continue": "Ga verder",
"edit": "Bewerk"
"edit": "Bewerk",
"pause": "Pauze",
"resume": "CV",
"retry": "Opnieuw proberen",
"pauseAll": "Alles onderbreken",
"resumeAll": "Alles hervatten",
"clearCompleted": "Gereed"
},
"download": {
"downloadFile": "Bestand downloaden",
@@ -89,27 +95,29 @@
"languages": {
"he": "עברית",
"hu": "Magyar",
"ar": "Arabisch",
"de": "Duits",
"en": "Engels",
"es": "Spaans",
"fr": "Frans",
"is": "",
"it": "Italiaans",
"ja": "Japans",
"ko": "Koreaans",
"nlBE": "",
"pl": "Pools",
"pt": "Portugees",
"ptBR": "Portugees (Brazilië)",
"ro": "",
"ru": "Russisch",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "Chinees (vereenvoudigd)",
"zhTW": "Chinees (traditioneel)"
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Account aanmaken",
@@ -164,7 +172,11 @@
"uploadFiles": "Uploading {files} files...",
"optionalPassword": "Optioneel wachtwoord",
"typeName": "Type:",
"deleted": "Met succes verwijderd!"
"deleted": "Met succes verwijderd!",
"destinationSource": "Bestemming Bron:",
"dragAndDrop": "Sleep bestanden hier naartoe",
"completed": "Voltooid",
"conflictsDetected": "Gedetecteerde conflicten"
},
"search": {
"images": "Afbeeldingen",
@@ -358,7 +370,8 @@
"toggleDark": "Donkere modus inschakelen",
"toggleSticky": "Kleefmodus schakelen",
"of": "van",
"used": "tweedehands"
"used": "tweedehands",
"noSources": "Geen bronnen beschikbaar voor deze gebruiker. Voeg een bron toe voor deze gebruiker of configureer een bron met defaultEnabled: true"
},
"profileSettings": {
"setDateFormat": "Exact datumformaat instellen",
@@ -378,7 +391,9 @@
"sidebarOptions": "Zijbalk opties",
"disableQuickToggles": "Snel wisselen uitschakelen",
"disableSearchOptions": "Zoekopties uitschakelen",
"hideSidebarFileActions": "Verberg bestandsacties in de zijbalk"
"hideSidebarFileActions": "Verberg bestandsacties in de zijbalk",
"editorViewerOptions": "Opties voor editor en viewer",
"editorQuickSave": "Snel opslaan knop tonen in editor"
},
"otp": {
"name": "Authenticatie met twee factoren",
@@ -390,5 +405,12 @@
"verificationFailed": "Verificatie mislukt",
"verificationSucceed": "Verificatie slagen",
"generationFailed": "Generatie mislukt"
},
"fileLoading": {
"title": "Uploaden en downloads",
"maxConcurrentUpload": "Max. gelijktijdige uploads",
"maxConcurrentUploadHelp": "Het maximum aantal gelijktijdige uploads. Hogere waarden presteren meestal beter, maar dit hangt van veel factoren af.",
"uploadChunkSizeMb": "Upload brokgrootte in MB",
"uploadChunkSizeMbHelp": "Gewoonlijk 5 tot 50 MB, kleiner is beter voor onstabiele of beperkte verbindingen. Instellen op 0 schakelt chunking uit."
}
}
+31 -9
View File
@@ -40,7 +40,13 @@
"continue": "Kontynuuj",
"edit": "Edytuj",
"generateNewOtp": "Resetowanie i generowanie nowego kodu dwuskładnikowego",
"verify": "Weryfikacja"
"verify": "Weryfikacja",
"pause": "Pauza",
"resume": "Wznowienie",
"retry": "Ponów próbę",
"pauseAll": "Wstrzymaj wszystko",
"resumeAll": "Resume all",
"clearCompleted": "Czyszczenie zakończone"
},
"download": {
"downloadFile": "Pobierz plik",
@@ -91,25 +97,27 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "Íslenska",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Nederlands (België)",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "Română",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "Svenska (Sverige)",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Utwórz konto",
@@ -164,7 +172,11 @@
"optionalPassword": "Hasło opcjonalne",
"typeName": "Typ:",
"selected": "wybrany",
"deleted": "Usunięto pomyślnie!"
"deleted": "Usunięto pomyślnie!",
"destinationSource": "Źródło docelowe:",
"dragAndDrop": "Przeciągnij i upuść pliki tutaj",
"completed": "Zakończono",
"conflictsDetected": "Wykryte konflikty"
},
"search": {
"images": "Zdjęcia",
@@ -362,7 +374,8 @@
"toggleDark": "Przełączanie trybu ciemnego",
"toggleSticky": "Przełączanie trybu samoprzylepnego",
"of": "z",
"used": "używany"
"used": "używany",
"noSources": "Brak dostępnych źródeł dla tego użytkownika. Dodaj źródło dla tego użytkownika lub skonfiguruj źródło z defaultEnabled: true."
},
"profileSettings": {
"setDateFormat": "Ustaw dokładny format daty",
@@ -383,7 +396,9 @@
"sidebarOptions": "Opcje paska bocznego",
"disableQuickToggles": "Wyłączanie szybkich przełączników",
"disableSearchOptions": "Wyłącz opcje wyszukiwania",
"hideSidebarFileActions": "Ukryj akcje związane z plikami na pasku bocznym"
"hideSidebarFileActions": "Ukryj akcje związane z plikami na pasku bocznym",
"editorViewerOptions": "Opcje edytora i przeglądarki",
"editorQuickSave": "Pokaż przycisk szybkiego zapisu w edytorze"
},
"otp": {
"name": "Uwierzytelnianie dwuskładnikowe",
@@ -395,5 +410,12 @@
"verificationFailed": "Weryfikacja nie powiodła się",
"verificationSucceed": "Sukces weryfikacji",
"generationFailed": "Generacja nieudana"
},
"fileLoading": {
"title": "Przesyłanie i pobieranie",
"maxConcurrentUpload": "Maksymalne jednoczesne przesyłanie",
"maxConcurrentUploadHelp": "Maksymalna liczba jednocześnie przesyłanych plików. Wyższe wartości zazwyczaj działają lepiej, ale zależy to od wielu czynników.",
"uploadChunkSizeMb": "Rozmiar przesyłanego fragmentu w MB",
"uploadChunkSizeMbHelp": "Zazwyczaj od 5 do 50 MB, mniejsze są lepsze dla niestabilnych lub ograniczonych połączeń. Ustawienie wartości 0 spowoduje wyłączenie podziału na fragmenty."
}
}
+31 -9
View File
@@ -40,7 +40,13 @@
"continue": "Continuar",
"edit": "Editar",
"generateNewOtp": "Redefinir e gerar um novo código de dois fatores",
"verify": "Verificar"
"verify": "Verificar",
"pause": "Pausa",
"resume": "Currículo",
"retry": "Repetir",
"pauseAll": "Pausar tudo",
"resumeAll": "Retomar tudo",
"clearCompleted": "Limpar concluído"
},
"download": {
"downloadFile": "Baixar arquivo",
@@ -91,25 +97,27 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Criar uma conta",
@@ -164,7 +172,11 @@
"filesSelected": "arquivos selecionados",
"typeName": "Tipo:",
"selected": "selecionado",
"deleted": "Excluído com sucesso!"
"deleted": "Excluído com sucesso!",
"destinationSource": "Origem do destino:",
"dragAndDrop": "Arraste e solte os arquivos aqui",
"completed": "Concluído",
"conflictsDetected": "Conflitos detectados"
},
"search": {
"images": "Imagens",
@@ -362,7 +374,8 @@
"toggleDark": "Alternar o modo escuro",
"toggleSticky": "Alternar o modo de aderência",
"of": "de",
"used": "usado"
"used": "usado",
"noSources": "Não há fontes disponíveis para esse usuário. Adicione uma fonte para esse usuário ou configure uma fonte com defaultEnabled: true"
},
"profileSettings": {
"setDateFormat": "Definir o formato exato da data",
@@ -383,7 +396,9 @@
"sidebarOptions": "Opções da barra lateral",
"disableQuickToggles": "Desativar as alternâncias rápidas",
"disableSearchOptions": "Desativar opções de pesquisa",
"hideSidebarFileActions": "Ocultar ações de arquivo na barra lateral"
"hideSidebarFileActions": "Ocultar ações de arquivo na barra lateral",
"editorViewerOptions": "Opções de editor e visualizador",
"editorQuickSave": "Mostrar o botão de salvamento rápido no editor"
},
"otp": {
"name": "Autenticação de dois fatores",
@@ -395,5 +410,12 @@
"verificationFailed": "Falha na verificação",
"verificationSucceed": "Verificação de sucesso",
"generationFailed": "Falha na geração"
},
"fileLoading": {
"title": "Uploads e downloads",
"maxConcurrentUpload": "Máximo de uploads simultâneos",
"maxConcurrentUploadHelp": "O número máximo de uploads simultâneos. Valores mais altos costumam ter melhor desempenho, mas isso depende de muitos fatores.",
"uploadChunkSizeMb": "Tamanho do bloco de upload em MB",
"uploadChunkSizeMbHelp": "Normalmente, de 5 a 50 MB; quanto menor, melhor para conexões instáveis ou restritas. A configuração como 0 desabilitará o chunking de upload."
}
}
+41 -19
View File
@@ -40,7 +40,13 @@
"submit": "Enviar",
"openFile": "Abrir ficheiro",
"continue": "Continuar",
"edit": "Editar"
"edit": "Editar",
"pause": "Pausa",
"resume": "Currículo",
"retry": "Repetir",
"pauseAll": "Pausa tudo",
"resumeAll": "Retomar tudo",
"clearCompleted": "Limpar concluído"
},
"download": {
"downloadFile": "Descarregar ficheiro",
@@ -89,27 +95,29 @@
"languages": {
"he": "עברית",
"hu": "Magyar",
"ar": "Árabe",
"de": "Alemão",
"en": "Inglês",
"es": "Espanhol",
"fr": "Francês",
"is": "",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "Icelandic",
"it": "Italiano",
"ja": "Japonês",
"ko": "Coreano",
"nlBE": "",
"pl": "Polaco",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ru": "Russo",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "Chinês simplificado",
"zhTW": "Chinês tradicional"
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Criar uma conta",
@@ -164,7 +172,11 @@
"uploadFiles": "Uploading {files} files...",
"optionalPassword": "Palavra-passe opcional",
"typeName": "Tipo:",
"deleted": "Eliminado com êxito!"
"deleted": "Eliminado com êxito!",
"destinationSource": "Fonte de destino:",
"dragAndDrop": "Arrastar e largar ficheiros aqui",
"completed": "Concluído",
"conflictsDetected": "Conflitos detectados"
},
"search": {
"images": "Imagens",
@@ -358,7 +370,8 @@
"toggleDark": "Alternar o modo escuro",
"toggleSticky": "Alternar o modo de aderência",
"of": "de",
"used": "utilizado"
"used": "utilizado",
"noSources": "Não existem fontes disponíveis para este utilizador. Adicione uma fonte para este utilizador ou configure uma fonte com defaultEnabled: true"
},
"profileSettings": {
"setDateFormat": "Definir o formato exato da data",
@@ -378,7 +391,9 @@
"sidebarOptions": "Opções da barra lateral",
"disableQuickToggles": "Desativar as alternâncias rápidas",
"disableSearchOptions": "Desativar opções de pesquisa",
"hideSidebarFileActions": "Ocultar acções de ficheiros na barra lateral"
"hideSidebarFileActions": "Ocultar acções de ficheiros na barra lateral",
"editorViewerOptions": "Opções do editor e do visualizador",
"editorQuickSave": "Mostrar botão de gravação rápida no editor"
},
"otp": {
"name": "Autenticação de dois factores",
@@ -390,5 +405,12 @@
"verificationFailed": "Falha na verificação",
"verificationSucceed": "Verificação de sucesso",
"generationFailed": "Geração falhada"
},
"fileLoading": {
"title": "Carregamentos e transferências",
"maxConcurrentUpload": "Máximo de carregamentos simultâneos",
"maxConcurrentUploadHelp": "O número máximo de carregamentos simultâneos. Os valores mais elevados têm normalmente um melhor desempenho, mas isto depende de muitos factores.",
"uploadChunkSizeMb": "Tamanho do bloco de carregamento em MB",
"uploadChunkSizeMbHelp": "Tipicamente, 5 a 50 MB, mas um valor mais pequeno é melhor para ligações instáveis ou restritas. Definir como 0 desactivará a fragmentação do carregamento."
}
}
+31 -9
View File
@@ -40,7 +40,13 @@
"continue": "Continuați",
"edit": "Editare",
"generateNewOtp": "Resetați și generați un nou cod cu doi factori",
"verify": "Verificare"
"verify": "Verificare",
"pause": "Pauză",
"resume": "CV",
"retry": "Reîncercare",
"pauseAll": "Pauză toate",
"resumeAll": "CV toate",
"clearCompleted": "Golire finalizată"
},
"download": {
"downloadFile": "Descarcă fișier",
@@ -91,25 +97,27 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Crează cont",
@@ -164,7 +172,11 @@
"optionalPassword": "Parolă opțională",
"typeName": "Tip:",
"selected": "selectat",
"deleted": "Șterse cu succes!"
"deleted": "Șterse cu succes!",
"destinationSource": "Sursa de destinație:",
"dragAndDrop": "Drag and drop fișiere aici",
"completed": "Completat",
"conflictsDetected": "Conflicte detectate"
},
"search": {
"images": "Imagini",
@@ -362,7 +374,8 @@
"toggleDark": "Comutarea modului întunecat",
"toggleSticky": "Schimbarea modului lipicios",
"of": "de",
"used": "utilizate"
"used": "utilizate",
"noSources": "Nu există surse disponibile pentru acest utilizator. Adăugați o sursă pentru acest utilizator sau configurați o sursă cu defaultEnabled: true"
},
"profileSettings": {
"setDateFormat": "Setați formatul exact al datei",
@@ -383,7 +396,9 @@
"sidebarOptions": "Opțiuni pentru bara laterală",
"disableQuickToggles": "Dezactivați comutatoarele rapide",
"disableSearchOptions": "Dezactivați opțiunile de căutare",
"hideSidebarFileActions": "Ascundeți acțiunile fișierelor în bara laterală"
"hideSidebarFileActions": "Ascundeți acțiunile fișierelor în bara laterală",
"editorViewerOptions": "Opțiuni pentru editor și vizualizator",
"editorQuickSave": "Afișați butonul de salvare rapidă în editor"
},
"otp": {
"name": "Autentificare cu doi factori",
@@ -395,5 +410,12 @@
"verificationFailed": "Verificarea a eșuat",
"verificationSucceed": "Verificarea reușită",
"generationFailed": "Generație eșuată"
},
"fileLoading": {
"title": "Încărcări și descărcări",
"maxConcurrentUpload": "Încărcări simultane maxime",
"maxConcurrentUploadHelp": "Numărul maxim de încărcări simultane. Valorile mai mari au de obicei performanțe mai bune, dar acest lucru depinde de mai mulți factori.",
"uploadChunkSizeMb": "Mărimea pachetului de încărcare în MB",
"uploadChunkSizeMbHelp": "De obicei între 5 și 50 MB, mai mic este mai bine pentru conexiunile instabile sau restricționate. Setarea la 0 va dezactiva fragmentarea încărcării."
}
}
+27 -5
View File
@@ -40,7 +40,13 @@
"continue": "Продолжить",
"edit": "Редактировать",
"generateNewOtp": "Сброс и генерация нового двухфакторного кода",
"verify": "Проверьте"
"verify": "Проверьте",
"pause": "Пауза",
"resume": "Резюме",
"retry": "Повторная попытка",
"pauseAll": "Приостановить все",
"resumeAll": "Резюме все",
"clearCompleted": "Очистка завершена"
},
"download": {
"downloadFile": "Скачать файл",
@@ -91,6 +97,7 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@@ -109,7 +116,8 @@
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Создать аккаунт",
@@ -164,7 +172,11 @@
"uploadFiles": "Uploading {files} files...",
"typeName": "Тип:",
"selected": "выбранные",
"deleted": "Удалено успешно!"
"deleted": "Удалено успешно!",
"destinationSource": "Источник назначения:",
"dragAndDrop": "Перетащите файлы сюда",
"completed": "Завершено",
"conflictsDetected": "Обнаруженные конфликты"
},
"search": {
"images": "Изображения",
@@ -362,7 +374,8 @@
"toggleDark": "Переключение темного режима",
"toggleSticky": "Переключение режима липкости",
"of": "из",
"used": "используется"
"used": "используется",
"noSources": "Для этого пользователя нет доступных источников. Либо добавьте источник для этого пользователя, либо настройте источник с параметром defaultEnabled: true"
},
"profileSettings": {
"setDateFormat": "Установите точный формат даты",
@@ -383,7 +396,9 @@
"sidebarOptions": "Параметры боковой панели",
"disableQuickToggles": "Отключение быстрых переключений",
"disableSearchOptions": "Отключить параметры поиска",
"hideSidebarFileActions": "Скрыть действия с файлами в боковой панели"
"hideSidebarFileActions": "Скрыть действия с файлами в боковой панели",
"editorViewerOptions": "Параметры редактора и программы просмотра",
"editorQuickSave": "Показать кнопку быстрого сохранения в редакторе"
},
"otp": {
"name": "Двухфакторная аутентификация",
@@ -395,5 +410,12 @@
"verificationFailed": "Верификация не удалась",
"verificationSucceed": "Проверка Успех",
"generationFailed": "Поколение не удалось"
},
"fileLoading": {
"title": "Загрузка и скачивание",
"maxConcurrentUpload": "Максимальное количество одновременных загрузок",
"maxConcurrentUploadHelp": "Максимальное количество одновременных загрузок. Более высокие значения обычно работают лучше, но это зависит от многих факторов.",
"uploadChunkSizeMb": "Размер загружаемого фрагмента в МБ",
"uploadChunkSizeMbHelp": "Обычно от 5 до 50 МБ, меньший размер лучше для нестабильных или ограниченных соединений. При установке значения 0 отключается функция \"кусковой загрузки\"."
}
}
+27 -5
View File
@@ -40,7 +40,13 @@
"continue": "Pokračovať",
"edit": "Upraviť",
"generateNewOtp": "Obnovenie a generovanie nového dvojfaktorového kódu",
"verify": "Overiť"
"verify": "Overiť",
"pause": "Pauza",
"resume": "Životopis",
"retry": "Opakovanie",
"pauseAll": "Pozastavenie všetkých",
"resumeAll": "Obnoviť všetko",
"clearCompleted": "Vyčistiť dokončené"
},
"download": {
"downloadFile": "Stiahnuť súbor",
@@ -91,6 +97,7 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@@ -109,7 +116,8 @@
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Vytvoriť účet",
@@ -164,7 +172,11 @@
"uploadFiles": "Uploading {files} files...",
"typeName": "Typ:",
"selected": "vybrané",
"deleted": "Úspešne vymazané!"
"deleted": "Úspešne vymazané!",
"destinationSource": "Cieľový zdroj:",
"dragAndDrop": "Presuňte sem súbory",
"completed": "Dokončené",
"conflictsDetected": "Zistené konflikty"
},
"search": {
"images": "Obrázky",
@@ -362,7 +374,8 @@
"toggleDark": "Prepínanie tmavého režimu",
"toggleSticky": "Prepnutie režimu Sticky",
"of": "z",
"used": "použité"
"used": "použité",
"noSources": "Pre tohto používateľa nie sú k dispozícii žiadne zdroje. Buď pridajte zdroj pre tohto používateľa, alebo nakonfigurujte zdroj s defaultEnabled: true"
},
"profileSettings": {
"setDateFormat": "Nastavenie presného formátu dátumu",
@@ -383,7 +396,9 @@
"sidebarOptions": "Možnosti bočného panela",
"disableQuickToggles": "Zakázanie rýchlych prepínačov",
"disableSearchOptions": "Zakázanie možností vyhľadávania",
"hideSidebarFileActions": "Skrytie akcií súborov v bočnom paneli"
"hideSidebarFileActions": "Skrytie akcií súborov v bočnom paneli",
"editorViewerOptions": "Možnosti editora a prehliadača",
"editorQuickSave": "Zobrazenie tlačidla rýchleho uloženia v editore"
},
"otp": {
"name": "Dvojfaktorové overovanie",
@@ -395,5 +410,12 @@
"verificationFailed": "Overenie sa nepodarilo",
"verificationSucceed": "Overovanie Úspech",
"generationFailed": "Generácia zlyhala"
},
"fileLoading": {
"title": "Nahrávanie a sťahovanie",
"maxConcurrentUpload": "Maximálne súbežné nahrávanie",
"maxConcurrentUploadHelp": "Maximálny počet súbežných odosielaní. Vyššie hodnoty sú zvyčajne výkonnejšie, ale závisí to od mnohých faktorov.",
"uploadChunkSizeMb": "Veľkosť nahraného úseku v MB",
"uploadChunkSizeMbHelp": "Zvyčajne 5 až 50 MB, v prípade nestabilných alebo obmedzených pripojení je vhodnejšie použiť menšiu veľkosť. Nastavením na hodnotu 0 sa vypne zoskupovanie pri odosielaní."
}
}
+31 -9
View File
@@ -40,7 +40,13 @@
"submit": "Skicka in",
"openFile": "Öppna fil",
"continue": "Fortsätt",
"edit": "Redigera"
"edit": "Redigera",
"pause": "Paus",
"resume": "Meritförteckning",
"retry": "Försök igen",
"pauseAll": "Pausa alla",
"resumeAll": "Återuppta alla",
"clearCompleted": "Klart avslutat"
},
"download": {
"downloadFile": "Ladda ner fil",
@@ -91,25 +97,27 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
"is": "",
"is": "Icelandic",
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Skapa ett konto",
@@ -164,7 +172,11 @@
"uploadFiles": "Uploading {files} files...",
"optionalPassword": "Valfritt lösenord",
"typeName": "Typ:",
"deleted": "Raderad framgångsrikt!"
"deleted": "Raderad framgångsrikt!",
"destinationSource": "Destination Källa:",
"dragAndDrop": "Dra och släpp filer här",
"completed": "Färdigställd",
"conflictsDetected": "Konflikter upptäckta"
},
"search": {
"images": "Bilder",
@@ -358,7 +370,8 @@
"toggleDark": "Växla till mörkt läge",
"toggleSticky": "Växla klisterläge",
"of": "av",
"used": "används"
"used": "används",
"noSources": "Inga källor tillgängliga för den här användaren. Lägg antingen till en källa för den här användaren eller konfigurera en källa med defaultEnabled: true"
},
"profileSettings": {
"setDateFormat": "Ange exakt datumformat",
@@ -378,7 +391,9 @@
"sidebarOptions": "Alternativ för sidofält",
"disableQuickToggles": "Inaktivera snabba växlingar",
"disableSearchOptions": "Inaktivera sökalternativ",
"hideSidebarFileActions": "Dölj filåtgärder i sidofältet"
"hideSidebarFileActions": "Dölj filåtgärder i sidofältet",
"editorViewerOptions": "Redigerings- och visningsalternativ",
"editorQuickSave": "Visa knappen för snabbsparande i redigeraren"
},
"otp": {
"name": "Tvåfaktorsautentisering",
@@ -390,5 +405,12 @@
"verificationFailed": "Verifiering misslyckades",
"verificationSucceed": "Verifiering lyckas",
"generationFailed": "Generation misslyckad"
},
"fileLoading": {
"title": "Uppladdningar och nedladdningar",
"maxConcurrentUpload": "Max samtidiga uppladdningar",
"maxConcurrentUploadHelp": "Det maximala antalet samtidiga uppladdningar. Högre värden ger normalt bättre prestanda, men detta beror på många faktorer.",
"uploadChunkSizeMb": "Ladda upp chunkstorlek i MB",
"uploadChunkSizeMbHelp": "Vanligtvis 5 till 50 MB, men mindre är bättre för instabila eller begränsade anslutningar. Om du ställer in 0 inaktiveras chunking för uppladdning."
}
}
+27 -5
View File
@@ -40,7 +40,13 @@
"continue": "Devam et",
"edit": "Düzenle",
"generateNewOtp": "Sıfırlama ve yeni iki faktörlü kod oluşturma",
"verify": "Doğrulama"
"verify": "Doğrulama",
"pause": "Duraklat",
"resume": "Özgeçmiş",
"retry": "Yeniden Dene",
"pauseAll": "Tümünü duraklat",
"resumeAll": "Tümünü devam ettir",
"clearCompleted": "Temizleme tamamlandı"
},
"download": {
"downloadFile": "Dosyayı indir",
@@ -91,6 +97,7 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@@ -109,7 +116,8 @@
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Bir hesap oluşturun",
@@ -164,7 +172,11 @@
"uploadFiles": "Uploading {files} files...",
"typeName": "Tip:",
"selected": "seçilmiş",
"deleted": "Başarıyla silindi!"
"deleted": "Başarıyla silindi!",
"destinationSource": "Hedef Kaynak:",
"dragAndDrop": "Dosyaları buraya sürükleyip bırakın",
"completed": "Tamamlandı",
"conflictsDetected": "Tespit edilen çatışmalar"
},
"search": {
"images": "Görseller",
@@ -362,7 +374,8 @@
"toggleDark": "Karanlık Modu Aç / Kapat",
"toggleSticky": "Yapışkan Modunu Değiştir",
"of": ".",
"used": "kullanılmış"
"used": "kullanılmış",
"noSources": "Bu kullanıcı için kullanılabilir kaynak yok. Ya bu kullanıcı için bir kaynak ekleyin ya da defaultEnabled: true ile bir kaynak yapılandırın"
},
"profileSettings": {
"setDateFormat": "Tam tarih biçimini ayarlama",
@@ -383,7 +396,9 @@
"sidebarOptions": "Kenar çubuğu seçenekleri",
"disableQuickToggles": "Hızlı geçişleri devre dışı bırakma",
"disableSearchOptions": "Arama seçeneklerini devre dışı bırak",
"hideSidebarFileActions": "Kenar çubuğundaki dosya eylemlerini gizle"
"hideSidebarFileActions": "Kenar çubuğundaki dosya eylemlerini gizle",
"editorViewerOptions": "Editör ve görüntüleyici seçenekleri",
"editorQuickSave": "Düzenleyicide hızlı kaydet düğmesini göster"
},
"otp": {
"name": "İki Faktörlü Kimlik Doğrulama",
@@ -395,5 +410,12 @@
"verificationFailed": "Doğrulama Başarısız",
"verificationSucceed": "Doğrulama Başarılı",
"generationFailed": "Nesil Başarısız"
},
"fileLoading": {
"title": "Yüklemeler & İndirmeler",
"maxConcurrentUpload": "Maksimum eşzamanlı yükleme",
"maxConcurrentUploadHelp": "Eşzamanlı yüklemelerin maksimum sayısı. Daha yüksek değerler genellikle daha iyi performans gösterir, ancak bu birçok faktöre bağlıdır.",
"uploadChunkSizeMb": "MB cinsinden yığın boyutu yükleme",
"uploadChunkSizeMbHelp": "Tipik olarak 5 ila 50 MB, daha küçük kararsız veya kısıtlı bağlantılar için daha iyidir. 0 olarak ayarlandığında yükleme yığınlaması devre dışı bırakılır."
}
}
+27 -5
View File
@@ -40,7 +40,13 @@
"verify": "Підтвердити",
"copyDownloadLinkToClipboard": "Скопіюйте посилання для завантаження в буфер обміну",
"continue": "Продовжуйте",
"edit": "Редагувати"
"edit": "Редагувати",
"pause": "Пауза.",
"resume": "Резюме",
"retry": "Спробуйте ще раз.",
"pauseAll": "Призупинити все",
"resumeAll": "Відновити все",
"clearCompleted": "Очищення завершено"
},
"download": {
"downloadFile": "Завантажити файл",
@@ -91,6 +97,7 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@@ -109,7 +116,8 @@
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "Створити обліковий запис",
@@ -164,7 +172,11 @@
"filesSelected": "вибрані файли",
"uploadFiles": "Uploading {files} files...",
"typeName": "Друкуй:",
"deleted": "Видалено успішно!"
"deleted": "Видалено успішно!",
"destinationSource": "Місце призначення Джерело:",
"dragAndDrop": "Перетягніть файли сюди",
"completed": "Завершено",
"conflictsDetected": "Виявлені конфлікти"
},
"search": {
"images": "Зображення",
@@ -359,7 +371,8 @@
"toggleDark": "Увімкнути темний режим",
"toggleSticky": "Увімкнути липкий режим",
"of": "з",
"used": "використаний"
"used": "використаний",
"noSources": "Для цього користувача немає доступних джерел. Або додайте джерело для цього користувача, або налаштуйте джерело з defaultEnabled: true"
},
"profileSettings": {
"setDateFormat": "Встановіть точний формат дати",
@@ -379,7 +392,9 @@
"sidebarOptions": "Параметри бічної панелі",
"disableQuickToggles": "Вимкнути швидкі перемикання",
"disableSearchOptions": "Вимкнути параметри пошуку",
"hideSidebarFileActions": "Приховати дії з файлами на бічній панелі"
"hideSidebarFileActions": "Приховати дії з файлами на бічній панелі",
"editorViewerOptions": "Параметри редактора та перегляду",
"editorQuickSave": "Показати кнопку швидкого збереження в редакторі"
},
"otp": {
"name": "Двофакторна автентифікація",
@@ -391,5 +406,12 @@
"verificationFailed": "Перевірка не вдалася",
"verificationSucceed": "Перевірка пройшла успішно",
"generationFailed": "Покоління зазнало невдачі"
},
"fileLoading": {
"title": "Завантаження та вивантаження",
"maxConcurrentUpload": "Максимальне одночасне завантаження",
"maxConcurrentUploadHelp": "Максимальна кількість одночасних завантажень. Вищі значення зазвичай працюють краще, але це залежить від багатьох факторів.",
"uploadChunkSizeMb": "Розмір завантажуваного фрагмента в МБ",
"uploadChunkSizeMbHelp": "Зазвичай від 5 до 50 МБ, менший розмір краще для нестабільних або обмежених з'єднань. Якщо встановити значення 0, вимкнеться фрагментація завантаження."
}
}
+31 -9
View File
@@ -40,7 +40,13 @@
"verify": "验证",
"copyDownloadLinkToClipboard": "将下载链接复制到剪贴板",
"continue": "继续",
"edit": "编辑"
"edit": "编辑",
"pause": "暂停",
"resume": "简历",
"retry": "重试",
"pauseAll": "全部暂停",
"resumeAll": "全部简历",
"clearCompleted": "清除完毕"
},
"download": {
"downloadFile": "下载文件",
@@ -91,6 +97,7 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@@ -98,18 +105,19 @@
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Dutch(Belgium)",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português(Brasil)",
"ptBR": "Português (Brasil)",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "Swedish(Sweden)",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文(简体)",
"zhTW": "中文(繁體)"
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "创建用户",
@@ -164,7 +172,11 @@
"filesSelected": "所选文件",
"uploadFiles": "Uploading {files} files...",
"typeName": "类型",
"deleted": "已成功删除!"
"deleted": "已成功删除!",
"destinationSource": "目的地来源:",
"dragAndDrop": "在此处拖放文件",
"completed": "已完成",
"conflictsDetected": "检测到冲突"
},
"search": {
"images": "图像",
@@ -359,7 +371,8 @@
"toggleDark": "切换黑暗模式",
"toggleSticky": "切换粘贴模式",
"of": "的",
"used": "中古"
"used": "中古",
"noSources": "该用户没有可用的信息源。要么为该用户添加一个信息源,要么配置一个默认为 true 的信息源。"
},
"profileSettings": {
"setDateFormat": "设置准确的日期格式",
@@ -379,7 +392,9 @@
"sidebarOptions": "侧边栏选项",
"disableQuickToggles": "禁用快速切换",
"disableSearchOptions": "禁用搜索选项",
"hideSidebarFileActions": "隐藏侧边栏中的文件操作"
"hideSidebarFileActions": "隐藏侧边栏中的文件操作",
"editorViewerOptions": "编辑器和查看器选项",
"editorQuickSave": "在编辑器中显示快速保存按钮"
},
"otp": {
"name": "双因素验证",
@@ -391,5 +406,12 @@
"verificationFailed": "验证失败",
"verificationSucceed": "验证成功",
"generationFailed": "一代失败"
},
"fileLoading": {
"title": "上传和下载",
"maxConcurrentUpload": "最大并发上传",
"maxConcurrentUploadHelp": "最大并发上传次数。数值越大,性能通常越好,但这取决于很多因素。",
"uploadChunkSizeMb": "上传块大小(以 MB 为单位",
"uploadChunkSizeMbHelp": "通常为 5 至 50 MB,对于不稳定或受限制的连接,越小越好。设置为 0 将禁用上传分块。"
}
}
+175 -20
View File
@@ -31,17 +31,33 @@
"switchView": "切換顯示方式",
"toggleSidebar": "切換側邊欄",
"update": "更新",
"upload": "上傳"
"upload": "上傳",
"generateNewOtp": "重設並產生新的雙重因素代碼",
"verify": "驗證",
"copyDownloadLinkToClipboard": "複製下載連結到剪貼簿",
"file": "檔案",
"folder": "資料夾",
"submit": "提交",
"openFile": "開啟檔案",
"continue": "繼續",
"edit": "編輯",
"pause": "暫停",
"resume": "簡歷",
"retry": "重試",
"pauseAll": "全部暫停",
"resumeAll": "所有履歷",
"clearCompleted": "清除完成"
},
"download": {
"downloadFile": "下載檔案",
"downloadFolder": "下載資料夾",
"downloadSelected": ""
"downloadSelected": "下載精選"
},
"errors": {
"forbidden": "您無權訪問。",
"internal": "伺服器出了點問題。",
"notFound": "找不到檔案。"
"notFound": "找不到檔案。",
"connection": "伺服器無法連線。"
},
"files": {
"body": "内容",
@@ -59,7 +75,8 @@
"size": "大小",
"sortByLastModified": "按最後修改時間排序",
"sortByName": "按名稱排序",
"sortBySize": "按大小排序"
"sortBySize": "按大小排序",
"noPreview": "此檔案無法預覽。"
},
"help": {
"click": "選擇檔案或目錄",
@@ -80,6 +97,7 @@
"hu": "Magyar",
"ar": "العربية",
"de": "Deutsch",
"el": "Ελληνικά",
"en": "English",
"es": "Español",
"fr": "Français",
@@ -87,18 +105,19 @@
"it": "Italiano",
"ja": "日本語",
"ko": "한국어",
"nlBE": "Dutch(Belgium)",
"nlBE": "Dutch (Belgium)",
"pl": "Polski",
"pt": "Português",
"ptBR": "Português (Brasil)",
"ro": "Romanian",
"ru": "Русский",
"sk": "Slovenčina",
"svSE": "Swedish(Sweden)",
"svSE": "Swedish (Sweden)",
"tr": "Türkçe",
"ua": "Українська",
"zhCN": "中文 (简体)",
"zhTW": "中文 (繁體)"
"zhTW": "中文 (繁體)",
"cz": "Čeština"
},
"login": {
"createAnAccount": "新建賬戶",
@@ -110,7 +129,8 @@
"submit": "登入",
"username": "帳號",
"usernameTaken": "用戶名已存在",
"wrongCredentials": "帳號或密碼錯誤"
"wrongCredentials": "帳號或密碼錯誤",
"or": "或"
},
"permanent": "永久",
"prompts": {
@@ -145,7 +165,18 @@
"size": "大小",
"upload": "上傳",
"uploadMessage": "選擇上傳項。",
"deleteMessageShare": "Are you sure you want to delete this share: {path} ?"
"deleteMessageShare": "Are you sure you want to delete this share: {path} ?",
"destinationSource": "目的地來源:",
"deleteUserMessage": "您確定要刪除此使用者嗎?",
"dragAndDrop": "將檔案拖放至此",
"completed": "已完成",
"selected": "選定",
"filesSelected": "選取的檔案",
"uploadFiles": "Uploading {files} files...",
"optionalPassword": "選購密碼",
"typeName": "類型:",
"deleted": "已成功刪除!",
"conflictsDetected": "偵測到衝突"
},
"search": {
"images": "影像",
@@ -155,7 +186,12 @@
"search": "搜尋...",
"typeToSearch": "輸入以搜尋...",
"types": "類型",
"video": "影片"
"video": "影片",
"path": "路徑:",
"smallerThan": "小於:",
"largerThan": "大於:",
"helpText1": "您輸入的每個字元都會進行搜尋(搜尋字詞最少 3 個字元)。",
"helpText2": "索引:搜尋會使用索引,該索引會在設定的間隔 (預設值:5 分鐘) 自動更新。當程式剛開始搜尋時,可能會導致結果不完整。"
},
"settings": {
"admin": "管理員",
@@ -194,7 +230,7 @@
"newUser": "建立使用者",
"password": "密碼",
"passwordUpdated": "密碼已更新!",
"path": "",
"path": "路徑",
"permissions": {
"create": "建立檔案和資料夾",
"delete": "刪除檔案和資料夾",
@@ -202,7 +238,10 @@
"execute": "執行命令",
"modify": "編輯檔案",
"rename": "重命名或移動檔案/資料夾",
"share": "分享檔案"
"share": "分享檔案",
"admin": "管理員",
"api": "建立與管理長效 API 金鑰",
"realtime": "啟用即時連線與更新"
},
"permissions-name": "權限",
"permissionsHelp": "您可以將該使用者設置為管理員,也可以單獨選擇各項權限。如果選擇了“管理員”,則其他的選項會被自動勾上,同時該使用者可以管理其他使用者。",
@@ -213,8 +252,8 @@
"rulesHelp": "您可以為該使用者製定一組黑名單或白名單式的規則,被屏蔽的檔案將不會顯示在清單中,使用者也無權限存取,支持相對於目錄範圍的路徑。",
"scope": "目錄範圍",
"settingsUpdated": "設定已更新!",
"shareManagement": "",
"singleClick": "",
"shareManagement": "股份管理",
"singleClick": "使用單擊開啟檔案和目錄",
"themes": {
"dark": "深色",
"light": "淺色",
@@ -229,7 +268,37 @@
"userManagement": "使用者管理",
"userUpdated": "使用者已更新!",
"username": "使用者名稱",
"users": "使用者"
"users": "使用者",
"enterPassword": "輸入密碼",
"enterPasswordAgain": "再次輸入密碼",
"passwordsDoNotMatch": "密碼不匹配",
"loginMethod": "登入方法",
"loginMethodDescription": "使用者允許的登入方式。",
"modifyOtherUser": "修改使用者:",
"modifyCurrentUser": "修改目前使用者",
"themeColor": "主題顏色",
"disableOfficeEditorDescription": "以空格分隔的檔案副檔名清單,以停用 OnlyOffice 檢視器。(例如:'.txt .html')。",
"disableOfficeEditor": "停用特定檔案副檔名的 Office 檢視器",
"disableOfficePreviews": "停用特定檔案副檔名的辦公室預覽",
"disableOfficePreviewsDescription": "以空格分隔的檔案副檔名清單,以停用辦公室預覽。(例如:'.txt .html')。",
"listingOptions": "列名選項",
"searchOptions": "搜尋選項",
"emptyGlobal": "目前沒有全局設定選項,您可以編輯設定 yaml 來調整系統設定。",
"scopes": "定義允許的來源和其中的使用者範圍",
"UserManagement": "使用者管理",
"userNotAdmin": "使用者不是管理員",
"tusUploads": "分塊上載",
"tusUploadsHelp": "File Browser 支援分塊檔案上傳,即使在不可靠的網路中,也能建立有效率、可靠、可恢復和分塊的檔案上傳。",
"tusUploadsChunkSize": "表示請求的最大大小(直接上傳會用於較小的上傳)。您可以輸入表示輸入位元組的純整數或字串,如 10MB、1GB 等。",
"tusUploadsRetryCount": "若資料塊上傳失敗,則執行重試的次數。",
"userHomeBasePath": "使用者主目錄的基本路徑",
"userScopeGenerationPlaceholder": "範圍會自動產生",
"createUserHomeDirectory": "建立使用者主目錄",
"shareSettings": "分享設定",
"api": "API 金鑰",
"shareDuration": "到期",
"shareDeleted": "分享已刪除!",
"adminOptions": "管理員使用者選項"
},
"sidebar": {
"help": "幫助",
@@ -242,7 +311,10 @@
"preview": "預覽",
"settings": "設定",
"signup": "註冊",
"siteSettings": "網站設定"
"siteSettings": "網站設定",
"sources": "來源",
"updateIsAvailable": "可提供更新!",
"fileActions": "檔案動作"
},
"success": {
"linkCopied": "連結已複製!"
@@ -254,8 +326,91 @@
"seconds": "秒",
"unit": "時間單位"
},
"upload": {},
"share": {},
"api": {},
"index": {}
"upload": {
"abortUpload": "你確定要流產嗎?"
},
"share": {
"notice": "請注意:任何能夠存取連結(以及可選密碼)的人都可以存取共用檔案。無需登入。"
},
"api": {
"title": "API 金鑰",
"keyName": "API 金鑰名稱:",
"createdAt": "創建於",
"expiresAt": "到期日::",
"permissions": "權限:",
"createTitle": "建立 API 金鑰",
"keyNamePlaceholder": "輸入唯一的 API 金鑰名稱:",
"tokenDuration": "代幣持續時間:",
"durationNumberPlaceholder": "數量",
"days": "天數",
"months": "月數",
"permissionNote": "為金鑰選擇至少一個權限。您的使用者也必須擁有該權限。",
"createKeyFailed": "建立 API 金鑰失敗。",
"createKeySuccess": "API 金鑰已成功建立。",
"description": "API 金鑰以建立金鑰的使用者為基礎。請參閱",
"swaggerLinkText": "跩頁",
"name": "名稱",
"created": "創建",
"expires": "到期",
"actions": "行動",
"enabled": "已啟用",
"disabled": "殘障"
},
"index": {
"status": "狀態",
"assessment": "評估",
"files": "檔案",
"folders": "資料夾",
"lastScanned": "最後一次掃描",
"quickScan": "快速掃描秒數",
"fullScan": "完整掃描秒數",
"settingsHover": "使用者設定",
"logout": "登出",
"toggleClick": "切換單擊",
"toggleDark": "切換黑暗模式",
"toggleSticky": "切換黏貼模式",
"of": "的",
"used": "中古",
"noSources": "此使用者沒有可用的來源。請為此使用者新增來源,或設定預設啟用:true 的來源。"
},
"profileSettings": {
"setDateFormat": "設定精確的日期格式",
"showHiddenFiles": "顯示隱藏的檔案",
"showQuickDownload": "顯示下載圖示以便快速存取",
"previewImages": "預覽圖片",
"previewVideos": "預覽影片",
"previewMotionVideos": "影片動態預覽",
"highQualityPreview": "圖庫影像的高品質預覽",
"editOffice": "編輯/檢視辦公室檔案",
"popupPreview": "啟用彈出預覽",
"filePreviewOptions": "檔案預覽選項",
"disableUpdateNotifications": "停用在側邊欄顯示通知更新橫幅。",
"disableHideSidebar": "當預覽或編輯檔案時,停用自動隱藏側邊列。",
"autoplayMedia": "在預覽中自動播放媒體檔案",
"deleteWithoutConfirming": "刪除檔案無須確認提示",
"sidebarOptions": "側邊欄選項",
"disableQuickToggles": "停用快速切換",
"disableSearchOptions": "停用搜尋選項",
"hideSidebarFileActions": "隱藏側邊列中的檔案動作",
"editorViewerOptions": "編輯器和檢視器選項",
"editorQuickSave": "在編輯器中顯示快速儲存按鈕"
},
"otp": {
"name": "雙因素驗證",
"verifyInstructions": "從驗證器應用程式輸入驗證碼",
"generate": "使用驗證器應用程式掃描 QR 代碼",
"codeInputPlaceholder": "6 位數字代碼",
"invalidCodeType": "驗證碼應為 6 位英文數字",
"enabledSuccessfully": "成功啟用 OTP",
"verificationFailed": "驗證失敗",
"verificationSucceed": "驗證成功",
"generationFailed": "世代失敗"
},
"fileLoading": {
"title": "上傳與下載",
"maxConcurrentUpload": "最大同時上傳數量",
"maxConcurrentUploadHelp": "最大同時上傳數量。較高的值通常效能較佳,但這取決於許多因素。",
"uploadChunkSizeMb": "上傳區塊大小(以 MB 為單位",
"uploadChunkSizeMbHelp": "通常為 5 到 50 MB,對於不穩定或受限制的連線,越小越好。設定為 0 會停用上傳分塊。"
}
}
+59 -1
View File
@@ -6,6 +6,7 @@ import { filesApi } from '@/api'
let eventSrc = null
let reconnectTimeout = null
let isManuallyClosed = false
let authenticationFailed = false
async function updateSourceInfo () {
try {
@@ -25,6 +26,12 @@ function cleanup () {
}
function scheduleReconnect () {
// Don't reconnect if authentication has failed
if (authenticationFailed) {
console.log('🚫 Not reconnecting due to authentication failure')
return
}
reconnectTimeout = setTimeout(() => {
console.log('🔁 Attempting SSE reconnect...')
setupSSE()
@@ -38,7 +45,46 @@ function clearReconnect () {
}
}
function setupSSE () {
// Test the events endpoint to check for authentication before setting up EventSource
async function testEventsEndpoint() {
const url = `${baseURL}api/events?sessionId=${state.sessionId}`
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Accept': 'text/event-stream',
'Cache-Control': 'no-cache'
}
})
if (response.status === 401) {
console.log('🚫 Events endpoint returned 401, authentication failed')
authenticationFailed = true
return false
}
// Close the test connection immediately
response.body?.cancel()
return true
} catch (error) {
// For network errors (like ERR_CONNECTION_REFUSED), we'll try the EventSource anyway
// Only actual 401 responses should stop reconnection
return true
}
}
async function setupSSE () {
// Only test authentication if we haven't already failed
if (!authenticationFailed) {
const isAuthenticated = await testEventsEndpoint()
if (!isAuthenticated) {
console.log('🚫 Authentication failed, not setting up EventSource')
authenticationFailed = true
notify.showError('Authentication failed. Please refresh the page to log in again.')
return
}
}
const url = `${baseURL}api/events?sessionId=${state.sessionId}`
eventSrc = new EventSource(url)
isManuallyClosed = false
@@ -53,6 +99,8 @@ function setupSSE () {
clearReconnect()
mutations.setRealtimeActive(true)
updateSourceInfo()
// Reset authentication failure flag on successful connection
authenticationFailed = false
}
eventSrc.onmessage = event => {
@@ -69,6 +117,14 @@ function setupSSE () {
cleanup()
mutations.setRealtimeActive(false)
mutations.updateSourceInfo('error')
// Don't reconnect if authentication has failed
if (authenticationFailed) {
console.log('🚫 Not reconnecting due to authentication failure')
return
}
// Original notification logic - only show error after multiple failures
if (state.realtimeDownCount == 2 && !isManuallyClosed) {
notify.showError('The connection to server was lost. Trying to reconnect...')
}
@@ -77,6 +133,8 @@ function setupSSE () {
}
export function startSSE () {
// Reset authentication failure flag when starting SSE
authenticationFailed = false
setupSSE()
}
+14 -1
View File
@@ -123,7 +123,7 @@ const router = createRouter({
// Helper function to check if a route resolves to itself
function isSameRoute(to: RouteLocation, from: RouteLocation) {
return to.path === from.path && JSON.stringify(to.params) === JSON.stringify(from.params) && to.hash === from.hash;
return to.path === from.path && JSON.stringify(to.params) === JSON.stringify(from.params);
}
router.beforeResolve(async (to, from, next) => {
if (isSameRoute(to, from)) {
@@ -181,4 +181,17 @@ router.beforeResolve(async (to, from, next) => {
next();
});
router.afterEach((to) => {
// Only postMessage if running in an iframe
if (window.self !== window.top) {
window.parent.postMessage(
{
type: "filebrowser:navigation",
url: to.fullPath,
},
"*"
);
}
});
export { router, router as default };
+4 -5
View File
@@ -1,4 +1,4 @@
import { removePrefix } from '@/utils/url.js'
import { removePrefix, buildItemUrl } from '@/utils/url.js'
import { getFileExtension } from '@/utils/files.js'
import { state, mutations } from '@/store'
import { noAuth } from '@/utils/constants.js'
@@ -81,10 +81,9 @@ export const getters = {
getters.getFirstSelected()?.type != 'directory',
selectedDownloadUrl () {
if (state.isSearchActive) {
return state.selected[0].url
return buildItemUrl(state.selected[0].source, state.selected[0].path)
}
let selectedItem = state.selected[0]
return state.req.items[selectedItem].url
return buildItemUrl(state.req.items[state.selected[0]].source, state.req.items[state.selected[0]].path)
},
reqNumDirs: () => {
let dirCount = 0
@@ -340,4 +339,4 @@ export const getters = {
}
return true
}
}
}
+2 -2
View File
@@ -85,7 +85,7 @@ const actions = {
context.commit("moveJob");
if (item.file.type == "directory") {
await filesApi.post(item.path).catch(Vue.prototype.$showError);
await filesApi.post(item.source, item.path).catch(Vue.prototype.$showError);
} else {
let onUpload = throttle(
(event) =>
@@ -98,7 +98,7 @@ const actions = {
);
await filesApi
.post(item.path, item.file, item.overwrite, onUpload)
.post(item.source, item.path, item.file, item.overwrite, onUpload)
.catch(Vue.prototype.$showError);
}
+72 -34
View File
@@ -142,13 +142,7 @@ export const mutations = {
emitStateChanged();
},
toggleSidebar() {
if (state.user.stickySidebar) {
localStorage.setItem("stickySidebar", "false");
mutations.updateCurrentUser({ "stickySidebar": false }); // turn off sticky when closed
state.showSidebar = false;
} else {
state.showSidebar = !state.showSidebar;
}
state.showSidebar = !state.showSidebar;
if (state.showSidebar) {
state.multiButtonState = "back";
} else {
@@ -172,7 +166,7 @@ export const mutations = {
},
closeHovers: () => {
const previousState = state.multiButtonLastState;
state.multiButtonLastState = mutations.multiButtonState;
state.multiButtonLastState = state.multiButtonState;
state.multiButtonState = previousState;
state.prompts = [];
if (!state.stickySidebar) {
@@ -180,6 +174,18 @@ export const mutations = {
}
emitStateChanged();
},
closeTopHover: () => {
state.prompts.pop();
if (state.prompts.length === 0) {
const previousState = state.multiButtonLastState;
state.multiButtonLastState = state.multiButtonState;
state.multiButtonState = previousState;
if (!state.stickySidebar) {
state.showSidebar = false;
}
}
emitStateChanged();
},
showHover: (value) => {
if (typeof value === "object") {
state.prompts.push({
@@ -230,7 +236,6 @@ export const mutations = {
state.user = value;
state.user.sorting.by = "name";
state.user.sorting.asc = true;
} catch (error) {
console.log(error);
}
@@ -266,6 +271,10 @@ export const mutations = {
mutations.setMultiple(false);
emitStateChanged();
},
setLastSelectedIndex: (index) => {
state.lastSelectedIndex = index;
emitStateChanged();
},
setRaw: (value) => {
state.previewRaw = value;
emitStateChanged();
@@ -293,31 +302,37 @@ export const mutations = {
}
// Update localStorage if stickySidebar exists
if ('stickySidebar' in state.user) {
localStorage.setItem("stickySidebar", state.user.stickySidebar);
if (state.user.stickySidebar && getters.currentView() == "listingView") {
state.multiButtonState = "menu";
} else if (state.showSidebar) {
state.multiButtonState = "back";
}
if (state.user.stickySidebar && getters.currentView() == "listingView") {
state.multiButtonState = "menu";
} else if (state.showSidebar) {
state.multiButtonState = "back";
}
// Update users if there's any change in state.user
if (JSON.stringify(state.user) !== JSON.stringify(previousUser)) {
usersApi.update(state.user, [
"locale",
"dateFormat",
"themeColor",
"quickDownload",
"disableOnlyOfficeExt",
"preview",
"stickySidebar",
"darkMode",
"showHidden",
"sorting",
"gallerySize",
"viewMode",
]);
// Only update the properties that were actually provided in the input
const updatedProperties = Object.keys(value).filter(key =>
[
"locale",
"dateFormat",
"themeColor",
"quickDownload",
"disableOnlyOfficeExt",
"preview",
"stickySidebar",
"singleClick",
"darkMode",
"showHidden",
"sorting",
"gallerySize",
"viewMode",
].includes(key)
);
value.id = state.user.id;
value.username = state.user.username;
if (updatedProperties.length > 0) {
usersApi.update(value, updatedProperties);
}
}
// Emit state change event
@@ -339,8 +354,17 @@ export const mutations = {
sortby = state.user.sorting.by;
asc = state.user.sorting.asc;
}
// map must be last to ensure the index is set correctly
value.items = sortedItems(value.items, sortby, asc)
// Separate directories and files
const dirs = value.items.filter((item) => item.type === 'directory');
const files = value.items.filter((item) => item.type !== 'directory');
// Sort them separately
const sortedDirs = sortedItems(dirs, sortby, asc);
const sortedFiles = sortedItems(files, sortby, asc);
// Combine them and assign indices
value.items = [...sortedDirs, ...sortedFiles];
value.items.map((item, index) => {
item.index = index;
return item;
@@ -380,5 +404,19 @@ export const mutations = {
state.isSearchActive = value;
emitStateChanged();
},
};
showTooltip(value) {
state.tooltip.content = value.content;
state.tooltip.x = value.x;
state.tooltip.y = value.y;
state.tooltip.show = true;
emitStateChanged();
},
hideTooltip() {
state.tooltip.show = false;
emitStateChanged();
},
setMaxConcurrentUpload: (value) => {
state.user.fileLoading.maxConcurrentUpload = value;
emitStateChanged();
},
};
+7
View File
@@ -2,6 +2,12 @@ import { reactive } from 'vue';
import { detectLocale } from "@/i18n";
export const state = reactive({
tooltip: {
show: false,
content: "",
x: 0,
y: 0,
},
deletedItem: false,
previousHash: "",
multiButtonState: "menu",
@@ -77,6 +83,7 @@ export const state = reactive({
loading: [],
reload: false,
selected: [],
lastSelectedIndex: null,
multiple: false,
upload: {
uploads: {},
+21 -48
View File
@@ -1,5 +1,4 @@
import { mutations, getters,state } from "@/store";
import router from "@/router";
import { usersApi } from "@/api";
import { getApiPath } from "@/utils/url.js";
import { recaptcha, loginPage, baseURL } from "@/utils/constants";
@@ -57,57 +56,31 @@ export function generateRandomCode(length) {
}
export async function logout() {
if (state.user.loginMethod === "oidc" || state.user.loginMethod === "proxy") {
try {
const res = await fetch(getApiPath("api/auth/logout"), {
method: "POST",
headers: {
"Content-Type": "application/json", // Good practice
"Authorization": `Bearer ${state.jwt}`,
},
});
// --- CHANGE IS HERE ---
// Check if the request was successful and handle the JSON response
if (res.ok) {
const data = await res.json(); // Parse the JSON body
let logoutUrl = data.logoutUrl;
// Clean up local state *before* navigating away
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
mutations.setCurrentUser(null);
mutations.setJWT("");
if (!logoutUrl) {
logoutUrl = baseURL+"login";
}
// Add a small delay to ensure cookie deletion completes before redirect
setTimeout(() => {
window.location.href = logoutUrl;
}, 100);
return; // Stop execution
} else {
// Handle potential errors from the API, e.g., res.status 401, 500
console.error("Logout API call failed:", res.status, res.statusText);
try {
const res = await fetch(getApiPath("api/auth/logout"), { method: "POST" });
if (res.ok) {
const data = await res.json();
let logoutUrl = data.logoutUrl;
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
mutations.setCurrentUser(null);
mutations.setJWT("");
if (!logoutUrl) {
logoutUrl = baseURL+"login";
}
} catch (e) {
console.error("An error occurred during logout:", e);
// Add a small delay to ensure cookie deletion completes before redirect
setTimeout(() => {
window.location.href = logoutUrl;
}, 100);
return; // Stop execution
} else {
// Handle potential errors from the API, e.g., res.status 401, 500
console.error("Logout API call failed:", res.status, res.statusText);
}
} catch (e) {
console.error("An error occurred during logout:", e);
}
// Fallback for non-oidc/proxy users or if the above logic fails
// More aggressive cookie clearing to handle different browser behaviors
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/; domain=" + window.location.hostname;
document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/; domain=." + window.location.hostname;
mutations.setCurrentUser(null);
mutations.setJWT("");
// Clear any session/local storage that might persist
sessionStorage.removeItem('auth');
localStorage.removeItem('auth');
router.push({ path: "/login" });
}
// Helper function to retrieve the value of a specific cookie
+1
View File
@@ -26,6 +26,7 @@ const origin = window.location.origin;
const settings = [
{ id: 'profile', label: 'settings.profileSettings', component: 'ProfileSettings' },
{ id: 'fileLoading', label: 'fileLoading.title', component: 'FileLoading' },
{ id: 'shares', label: 'settings.shareSettings', component: 'SharesSettings', permissions: { share: true } },
{ id: 'api', label: 'api.title', component: 'ApiKeys', permissions: { api: true } },
//{ id: 'global', label: 'Global', component: 'GlobalSettings', permissions: { admin: true } },
+21 -48
View File
@@ -1,72 +1,45 @@
import { state, mutations, getters } from "@/store";
import { filesApi } from "@/api";
import { notify } from "@/notify";
import { removePrefix } from "@/utils/url.js";
import { publicApi } from "@/api";
export default function download() {
const currentView = getters.currentView();
if (currentView === "preview") {
startDownload(null, [state.req.url]);
export default function downloadFiles(items) {
if (items.length == 0) {
notify.showError("No files selected");
return;
}
if (typeof items[0] === "number") {
// map the index to state.req.items
items = items.map(i => state.req.items[i]);
}
const currentView = getters.currentView();
if (currentView === "share") {
let urlPath = getters.routePath("share");
let parts = urlPath.split("/");
const hash = parts[1];
const subPath = "/" + parts.slice(2).join("/");
let files = [];
for (let i of state.selected) {
const dlfile = removePrefix(state.req.items[i].url, "share/" + hash);
files.push(dlfile);
}
const share = {
path: subPath,
hash: hash,
token: "",
format: files.length ? "zip" : null,
};
// Perform download without opening a new window
startDownload(share, files, true);
return;
}
if (state.isSearchActive) {
startDownload(null, [state.selected[0].url]);
startDownload(null, items, hash);
return;
}
if (getters.isSingleFileSelected()) {
startDownload(null, [getters.selectedDownloadUrl()]);
startDownload(null, items);
return;
} else {
// Multiple files download with user confirmation
mutations.showHover({
name: "download",
confirm: (format) => {
mutations.closeHovers();
startDownload(format, items);
},
});
}
// Multiple files download with user confirmation
mutations.showHover({
name: "download",
confirm: (format) => {
mutations.closeHovers();
let files = [];
if (state.selected.length > 0) {
for (let i of state.selected) {
files.push(state.req.items[i].url);
}
} else {
files.push(state.route.path);
}
startDownload(format, files);
},
});
}
async function startDownload(config, files, isPublic) {
async function startDownload(config, files, hash = "") {
try {
if (isPublic) {
publicApi.download(config, files);
} else {
filesApi.download(config, files);
}
filesApi.download(config, files, hash);
notify.showSuccess("Downloading...");
} catch (e) {
notify.showError(`Error downloading: ${e}`);
+3 -17
View File
@@ -1,26 +1,12 @@
export function sortedItems(items = [], sortby="name", asc=true) {
return items.sort((a, b) => {
let valueA = a[sortby];
let valueB = b[sortby];
if (sortby === "name") {
valueA = valueA.split(".")[0]
valueB = valueB.split(".")[0]
// Handle sorting for "name" field
const isNumericA = !isNaN(valueA);
const isNumericB = !isNaN(valueB);
if (isNumericA && isNumericB) {
// Compare numeric strings as numbers
return asc
? parseFloat(valueA) - parseFloat(valueB)
: parseFloat(valueB) - parseFloat(valueA);
}
// Compare non-numeric values as strings
return asc
? valueA.localeCompare(valueB)
: valueB.localeCompare(valueA);
// Use localeCompare with numeric option for natural sorting
const comparison = valueA.localeCompare(valueB, undefined, { numeric: true, sensitivity: 'base' });
return asc ? comparison : -comparison;
}
// Default sorting for other fields
+8 -4
View File
@@ -6,23 +6,27 @@ describe('testSort', () => {
it('sort items by name correctly', () => {
const input = [
{ name: "zebra" },
{ name: "1" },
{ name: "10" },
{ name: "10 something.txt" },
{ name: "1 something.txt" },
{ name: "2 something.txt" },
{ name: "Apple" },
{ name: "bee 20" },
{ name: "cave 10.txt" },
{ name: "cave 1.txt" },
{ name: "cave 2.txt" },
{ name: "2" },
{ name: "bee 2" },
]
const expected = [
{ name: "1" },
{ name: "1 something.txt" },
{ name: "2" },
{ name: "10" },
{ name: "2 something.txt" },
{ name: "10 something.txt" },
{ name: "Apple" },
{ name: "bee 2" },
{ name: "bee 20" },
{ name: "cave 1.txt" },
{ name: "cave 2.txt" },
{ name: "cave 10.txt" },
{ name: "zebra" }
]
+361 -151
View File
@@ -1,173 +1,383 @@
import { state, mutations } from '@/store'
import { filesApi } from '@/api'
import { notify } from '@/notify'
import { serverHasMultipleSources } from './constants'
import { reactive } from "vue";
import { filesApi } from "@/api";
import { state } from "@/store";
export function checkConflict (files, items) {
if (typeof items === 'undefined' || items === null) {
items = []
class UploadManager {
constructor() {
this.queue = reactive([]);
this.activeUploads = 0;
this.nextId = 0;
this.overwriteAll = null; // null: ask, true: overwrite, false: skip
this.isPausedForConflict = false;
this.isOverallPaused = false;
this.onConflict = () => {}; // Callback for UI
}
let folder_upload = files[0].path !== undefined
setOnConflict(handler) {
this.onConflict = handler;
}
let conflict = false
for (let i = 0; i < files.length; i++) {
let file = files[i]
let name = file.name
async add(basePath, items, overwrite = false) {
if (folder_upload) {
let dirs = file.path.split('/')
if (dirs.length > 1) {
name = dirs[0]
if (basePath.slice(-1) !== "/") {
basePath += "/";
}
const dirs = new Set();
for (const item of items) {
if (item.relativePath) {
const pathParts = item.relativePath.split("/");
pathParts.pop(); // Grab the directory path by removing the filename.
let currentPath = "";
for (const part of pathParts) {
currentPath += part + "/";
dirs.add(currentPath);
}
}
}
let res = items.findIndex(function hasConflict (element) {
return element.name === this
}, name)
const newUploads = [];
if (res >= 0) {
conflict = true
break
if (dirs.size > 0) {
// Sort paths to ensure parent directories are created before children.
const sortedDirs = [...dirs].sort();
for (const dir of sortedDirs) {
const pathParts = dir.slice(0, -1).split("/");
const dirName = pathParts[pathParts.length - 1];
const upload = {
id: this.nextId++,
name: dirName,
size: 0,
progress: 0,
status: "pending",
type: "directory",
path: `${basePath}${dir}`,
source: state.req.source,
overwrite: overwrite,
};
newUploads.push(upload);
}
}
const fileUploads = Array.from(items).map((item) => {
const id = this.nextId++;
const file = item.file;
const relativePath = item.relativePath || file.name;
let destinationPath = `${basePath}${relativePath}`;
const upload = {
id,
file,
name: file.name,
size: file.size,
progress: 0,
chunkOffset: 0,
status: "pending", // pending, uploading, paused, completed, error
xhr: null,
path: destinationPath, // Full destination path
source: state.req.source,
overwrite: overwrite,
};
return upload;
});
this.queue.push(...newUploads, ...fileUploads);
this.processQueue();
return newUploads;
}
async processQueue() {
if (this.isPausedForConflict) {
return;
}
if (this.isOverallPaused) {
return;
}
while (
this.activeUploads < state.user.fileLoading.maxConcurrentUpload &&
this.hasPending()
) {
const upload = this.queue.find((item) => item.status === "pending");
if (upload) {
if (this.overwriteAll) {
upload.overwrite = true;
}
this.start(upload.id);
}
}
}
return conflict
}
start(id) {
const upload = this.findById(id);
if (!upload || upload.status !== "pending") {
console.log(
`upload.js: Cannot start upload for id ${id}. Status is not 'pending' or upload not found.`,
upload
);
return;
}
export function scanFiles (dt) {
return new Promise(resolve => {
let reading = 0
const contents = []
if (upload.type === "directory") {
this.startDirectoryUpload(upload);
} else {
this.startFileUpload(upload);
}
}
if (dt.items !== undefined) {
for (let item of dt.items) {
if (
item.kind === 'file' &&
typeof item.webkitGetAsEntry === 'function'
) {
const entry = item.webkitGetAsEntry()
readEntry(entry)
async startDirectoryUpload(upload) {
this.activeUploads++;
upload.status = "uploading";
try {
const { promise } = filesApi.post(
upload.source,
upload.path,
new Blob([]),
upload.overwrite
);
await promise;
upload.status = "completed";
upload.progress = 100;
} catch (err) {
await this.handleUploadError(upload, err);
} finally {
this.activeUploads--;
this.processQueue();
}
}
async startFileUpload(upload) {
this.activeUploads++;
upload.status = "uploading";
const chunkSize = state.user.fileLoading.uploadChunkSizeMb * 1024 * 1024;
if (chunkSize === 0) {
const progress = (percent) => {
upload.progress = percent;
};
try {
const { xhr, promise } = filesApi.post(
upload.source,
upload.path,
upload.file,
upload.overwrite,
progress,
{
"X-File-Total-Size": upload.size,
}
);
upload.xhr = xhr;
await promise;
upload.status = "completed";
upload.progress = 100;
} catch (err) {
await this.handleUploadError(upload, err);
} finally {
this.activeUploads--;
upload.xhr = null;
this.processQueue();
}
return;
}
while (upload.chunkOffset < upload.size && upload.status === "uploading") {
const chunk = upload.file.slice(
upload.chunkOffset,
upload.chunkOffset + chunkSize
);
const chunkProgress = (percent) => {
const chunkLoaded = (percent / 100) * chunk.size;
const totalLoaded = upload.chunkOffset + chunkLoaded;
const progress = (totalLoaded / upload.size) * 100;
upload.progress = Math.round(progress * 10) / 10;
};
try {
const { xhr, promise } = filesApi.post(
upload.source,
upload.path,
chunk,
upload.overwrite,
chunkProgress,
{
"X-File-Chunk-Offset": upload.chunkOffset,
"X-File-Total-Size": upload.size,
}
);
upload.xhr = xhr;
await promise;
upload.chunkOffset += chunk.size;
} catch (err) {
await this.handleUploadError(upload, err);
break; // Exit loop on error or pause
}
}
if (upload.status === "uploading") {
// If the loop finished without being paused/errored
upload.status = "completed";
upload.progress = 100;
}
this.activeUploads--;
upload.xhr = null;
this.processQueue();
}
pauseAll() {
this.isOverallPaused = true;
this.queue.forEach((upload) => {
if (upload.status === "uploading") {
this.pause(upload.id);
}
});
}
resumeAll() {
this.isOverallPaused = false;
this.queue.forEach((upload) => {
if (upload.status === "paused") {
this.resume(upload.id);
}
});
}
pause(id) {
const upload = this.findById(id);
if (upload && upload.status === "uploading" && upload.xhr) {
upload.xhr.abort();
upload.status = "paused";
}
}
resume(id) {
const upload = this.findById(id);
if (upload && upload.status === "paused") {
this.isOverallPaused = false;
upload.status = "pending";
const progress =
upload.size > 0 ? (upload.chunkOffset / upload.size) * 100 : 0;
upload.progress = Math.round(progress * 10) / 10;
this.processQueue();
}
}
cancel(id) {
this.pause(id); // Abort if in progress
const index = this.queue.findIndex((item) => item.id === id);
if (index !== -1) {
this.queue.splice(index, 1);
}
}
retry(id, overwrite = false) {
const upload = this.findById(id);
if (upload && ["error", "conflict"].includes(upload.status)) {
upload.overwrite = overwrite;
upload.status = "pending";
if (upload.type !== 'directory') {
upload.chunkOffset = 0; // Reset chunk offset for retries
}
upload.progress = 0;
this.processQueue();
}
}
clearCompleted() {
for (let i = this.queue.length - 1; i >= 0; i--) {
if (this.queue[i].status === "completed") {
this.queue.splice(i, 1);
}
}
}
findById(id) {
return this.queue.find((item) => item.id === id);
}
hasPending() {
return this.queue.some((item) => item.status === "pending");
}
async handleUploadError(upload, err) {
// Check if the error is a 409 Conflict
if (err?.response?.status === 409) {
upload.status = "conflict";
} else if (err.message !== "Upload aborted") {
upload.status = "error";
} else {
console.log(`upload.js: Upload aborted for id ${upload.id}`, upload);
}
}
resolveConflict(overwrite) {
this.overwriteAll = overwrite;
this.isPausedForConflict = false;
if (overwrite) {
// Find all items that hit a conflict and requeue them.
for (const item of this.queue) {
if (item.status === "conflict") {
item.status = "pending";
item.overwrite = true;
if (item.type !== 'directory') {
item.chunkOffset = 0; // Reset progress for resume
}
}
}
} else {
resolve(dt.files)
}
function readEntry (entry, directory = '') {
if (entry.isFile) {
reading++
entry.file(file => {
reading--
file.fullPath = directory + file.name
contents.push(file)
if (reading === 0) {
resolve(contents)
}
})
} else if (entry.isDirectory) {
const dir = {
isDir: true,
size: 0,
fullPath: directory + entry.name + '/',
name: entry.name
}
contents.push(dir)
readReaderContent(entry.createReader(), directory + entry.name + '/')
// Cancel all uploads in the queue.
for (let i = this.queue.length - 1; i >= 0; i--) {
this.cancel(this.queue[i].id)
}
}
function readReaderContent (reader, directory) {
reading++
reader.readEntries(function (entries) {
reading--
if (entries.length > 0) {
for (const entry of entries) {
readEntry(entry, `${directory}/`)
}
readReaderContent(reader, `${directory}/`)
}
if (reading === 0) {
resolve(contents)
}
})
}
})
}
export async function handleFiles(files, base, overwrite = false) {
console.log('handleFiles called with base:', base, 'and overwrite:', overwrite)
console.log('Files to upload:', files)
let blockUpdates = false
let c = 0
const count = files.length
for (const file of files) {
c += 1
const id = state.upload.id
// Inside handleFiles
let basePath = base
let relativePath = file.fullPath || file.webkitRelativePath || file.name
// Do not allow paths to go backwards
if (relativePath.includes('../')) {
console.error('Path traversal detected, upload blocked for file:', relativePath);
return; // or throw an error
}
if (basePath.endsWith('/')) basePath = basePath.slice(0, -1)
if (relativePath.startsWith('/')) relativePath = relativePath.slice(1)
const path = basePath + '/' + relativePath
// If it's a directory entry (from scanFiles)
const isDirectory = file.type === 'directory' || file.isDir
const item = {
id,
path: isDirectory ? path + '/' : path,
file: isDirectory ? new Blob([]) : file.file || file, // support both wrapped and raw files
overwrite
}
let last = 0
notify.showPopup(
'success',
`(${c} of ${count}) Uploading ${relativePath}`,
false
)
if (serverHasMultipleSources) {
item.path = `/files/${file.source}${item.path}`
}
console.log(`(${c} of ${count}) Uploading ${relativePath} to ${item.path}`)
await filesApi
.post(item.path, item.file, item.overwrite, percentComplete => {
if (blockUpdates) return
blockUpdates = true
notify.startLoading(last, percentComplete)
last = percentComplete
setTimeout(() => {
blockUpdates = false
}, 250)
})
.then(response => {
const spinner = document.querySelector('.notification-spinner')
if (spinner) spinner.classList.add('hidden')
console.log('Upload successful!', response)
})
.catch(error => {
const spinner = document.querySelector('.notification-spinner')
if (spinner) spinner.classList.add('hidden')
notify.showError('Error uploading file: ' + error)
throw error
})
this.processQueue();
}
mutations.setReload(true);
}
export const uploadManager = new UploadManager();
export function checkConflict(files, items) {
if (typeof items === 'undefined' || items === null) {
items = [];
}
let folder_upload = files[0].path !== undefined;
let conflict = false;
for (let i = 0; i < files.length; i++) {
let file = files[i];
let name = file.name;
if (folder_upload) {
let dirs = file.path.split('/');
if (dirs.length > 1) {
name = dirs[0];
}
}
let res = items.findIndex(function hasConflict(element) {
return element.name === this;
}, name);
if (res >= 0) {
conflict = true;
break;
}
}
return conflict;
}
+45
View File
@@ -1,5 +1,6 @@
import { baseURL } from "@/utils/constants.js";
import { state } from "@/store";
import { router } from "@/router";
export function removeLastDir(url) {
var arr = url.split("/");
@@ -128,3 +129,47 @@ export function extractSourceFromPath(url) {
return { source, path };
}
export function buildItemUrl(source, path) {
if (state.serverHasMultipleSources) {
return `/files/${source}${path}`;
} else {
return `/files${path}`;
}
}
export function encodedPath(path) {
if (path === undefined) {
return "";
}
path = path.replace(/#/g, "%23"); // preseve # in path
// break apart path into parts and url encode each part
const parts = path.split("/");
const encodedParts = parts.map(part => encodeURIComponent(part));
return encodedParts.join("/").replace("//", "/");
}
export function goToItem(source, path, previousHash, shareHash) {
if (previousHash) {
location.hash = previousHash;
}
let newPath = encodedPath(path);
let fullPath;
if (shareHash) {
fullPath = `/share/${shareHash}${newPath}`;
router.push({ path: fullPath });
return;
}
if (state.serverHasMultipleSources) {
fullPath = `/${encodeURIComponent(source)}${newPath}`;
} else {
fullPath = `${newPath}`;
}
router.push({ path: fullPath });
return
}
export function doubleEncode(str) {
return encodeURIComponent(encodeURIComponent(str));
}
+13 -5
View File
@@ -34,6 +34,7 @@ import { notify } from "@/notify";
import router from "@/router";
import { baseURL } from "@/utils/constants";
import PopupPreview from "@/components/files/PopupPreview.vue";
import { extractSourceFromPath } from "@/utils/url";
export default {
name: "files",
@@ -124,10 +125,15 @@ export default {
// lets redirect if multiple sources and user went to /files/
if (state.serverHasMultipleSources && rootRoute) {
const urlEncodedSource = encodeURIComponent(state.sources.current)
router.push(`${routePath}/${urlEncodedSource}`);
return;
const targetPath = `${routePath}/${urlEncodedSource}`;
// Prevent infinite loop by checking if we're already at the target path
if (this.$route.path !== targetPath) {
router.push(targetPath);
return;
}
}
if (state.sources.current === "") {
const result = extractSourceFromPath(getters.routePath());
if (result.source === "") {
notify.showError($t("index.noSources"));
return;
}
@@ -140,12 +146,14 @@ export default {
let data = {};
try {
const fetchSource = decodeURIComponent(result.source);
const fetchPath = decodeURIComponent(result.path);
// Fetch initial data
let res = await filesApi.fetchFiles(getters.routePath());
let res = await filesApi.fetchFiles(fetchSource,fetchPath );
// If not a directory, fetch content
if (res.type != "directory") {
const content = !getters.onlyOfficeEnabled();
res = await filesApi.fetchFiles(getters.routePath(), content);
res = await filesApi.fetchFiles(res.source, res.path, content);
}
data = res;
if (state.sources.count > 1) {
+9 -3
View File
@@ -27,6 +27,7 @@
</div>
<Notifications />
<ContextMenu></ContextMenu>
<Tooltip />
</template>
<script>
@@ -36,6 +37,7 @@ import Sidebar from "@/components/sidebar/Sidebar.vue";
import ContextMenu from "@/components/ContextMenu.vue";
import Notifications from "@/components/Notifications.vue";
import Scrollbar from "@/components/files/Scrollbar.vue";
import Tooltip from "@/components/Tooltip.vue";
import { filesApi } from "@/api";
import { state, getters, mutations } from "@/store";
import { events } from "@/notify";
@@ -50,6 +52,7 @@ export default {
Sidebar,
Prompts,
Scrollbar,
Tooltip,
},
data() {
return {
@@ -68,7 +71,7 @@ export default {
mutations.setSession(generateRandomCode(8));
}
this.reEval()
this.updateSourceInfo();
this.initialize();
},
computed: {
isOnlyOffice() {
@@ -134,14 +137,17 @@ export default {
}
}
},
async updateSourceInfo() {
async initialize() {
if (getters.isLoggedIn()) {
console.log("Updating source info");
const sourceinfo = await filesApi.sources();
mutations.updateSourceInfo(sourceinfo);
if (state.user.permissions.realtime) {
events.startSSE();
}
const maxUploads = state.user.fileLoading.maxConcurrentUpload;
if (maxUploads > 10 || maxUploads < 1) {
mutations.setMaxConcurrentUpload(1);
}
}
},
updateIsMobile() {
+2
View File
@@ -38,6 +38,7 @@ import ProfileSettings from "@/views/settings/Profile.vue";
import SharesSettings from "@/views/settings/Shares.vue";
import UserManagement from "@/views/settings/Users.vue";
import UserSettings from "@/views/settings/User.vue";
import FileLoading from "@/views/settings/FileLoading.vue";
import ApiKeys from "@/views/settings/Api.vue";
export default {
name: "settings",
@@ -48,6 +49,7 @@ export default {
ProfileSettings,
SharesSettings,
ApiKeys,
FileLoading,
},
data() {
return {
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff. Show more