Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
302c7c6d06 | ||
|
|
698f30a6d7 | ||
|
|
e24f858ffc | ||
|
|
b6560920bb | ||
|
|
6b21261c2c | ||
|
|
c285bba402 | ||
|
|
7f8c269e4b | ||
|
|
309e9ad285 | ||
|
|
2c2c14a7f7 | ||
|
|
7c72a4f991 | ||
|
|
aa7e33c46f | ||
|
|
5fc8208a7b | ||
|
|
5f08e50811 | ||
|
|
0c5c027a5e | ||
|
|
6b872558e1 | ||
|
|
061bbc1426 | ||
|
|
88f18a4b27 | ||
|
|
c4d5a614e7 | ||
|
|
9da970ec9b |
No files matched your search
@@ -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,7 +1,7 @@
|
||||
name: regular tests
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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: ".",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,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.
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Whitespace-only changes.
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 إلى تعطيل تقطيع التحميل."
|
||||
}
|
||||
}
|
||||
@@ -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í."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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 θα απενεργοποιήσει την ομαδοποίηση μεταφόρτωσης."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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 תשבית את חלוקת הקבצים להעלאה."
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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に設定するとアップロードチャンキングが無効になります。"
|
||||
}
|
||||
}
|
||||
@@ -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으로 설정하면 업로드 청킹이 비활성화됩니다."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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 отключается функция \"кусковой загрузки\"."
|
||||
}
|
||||
}
|
||||
@@ -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í."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
@@ -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, вимкнеться фрагментація завантаження."
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 會停用上傳分塊。"
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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 };
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
Reference in new issue
Block a user