diff --git a/CHANGELOG.md b/CHANGELOG.md index 80aee2563..f79e16078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ 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.8.4-beta + + **New Features**: + - New media player styles and features + - Custom Media Player: enhanced media player using plyr thanks @Kurami32 (see #1160) + - Custom Media Player: also adds support for metadata + - added embeded video subtitle support (for both native and custom player). @maxbin123 #1072 #1157 + - Users can disable the customer player and opt of native in profile settings. + - Option to disable backend update check via `server.disableUpdateCheck` #1134 + - added `frontend.favicon` and `frontend.description` for html overrides + - onlyoffice is now supported in shares. Both viewing and editing can be configured per-share. + - Added only office debug view and wiki to assist with debugging issues #1068 #911 #1074 + - Dark mode enforcement possible for shared links #1029 + - added `System & Admin` section to settings + - includes a new config viewer to see current running config (hides secrets) #838 + - added `server.minSearchLength` to allow adjusting the length requirement for search #1174 + + **Notes**: + - access management: specific folders/files with access are shown instead permission denied for parent folder + - navigation no longer appends last location hash which should fix some unwanted navation behavior #1070 + - altered the context menu style and behavior. + - documentation update: comma or Space separated extensions #1138 + - Files and folders can be created with "/" or "\" on the name #1126 + - Share management should not be allowed without authentication #1163 + - Question about customizing session timeout #1184 + + **BugFixes**: + - access management: delay showing rule changes in the list fixed. #1131 + - Color names are not localized #1159 + - rename issues #1170 #1171 + - some shortcuts not working #1056 + - Can't copy/paste text on mobile #1168 + - Can't change between images inside of the share image viewer. #1144 + - fixed and updated translations with variables always showing english. + ## v0.8.3-beta **BugFixes**: @@ -18,7 +53,7 @@ All notable changes to this project will be documented in this file. For commit **Notes**: - 8.0 ffmpeg version bundled with docker - go 1.25 upgrade with green tea GC enabled - - totp secrets accept non-secure strings, only throwing warning + - totp secrets accept non-secure strings, only throwing warning - adjusted download limit so it also counts viewing text "content" of files (like in editor). You can also "disable file viewing" to stop the editor from showing. lower quality file image previews are not counted as downloads. - updated invalid share message to be more clear https://github.com/gtsteffaniak/filebrowser/issues/1120 diff --git a/_docker/Dockerfile b/_docker/Dockerfile index 8ccceb8e4..705e38097 100644 --- a/_docker/Dockerfile +++ b/_docker/Dockerfile @@ -16,6 +16,9 @@ RUN upx filebrowser FROM node:lts-slim AS nbuild WORKDIR /app COPY ./frontend/package.json ./ +RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \ + && git config --global url."https://github.com/".insteadOf "git@github.com:" \ + && git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" RUN npm i --maxsockets 1 COPY ./frontend/ ./ RUN npm run build-docker diff --git a/_docker/Dockerfile.slim b/_docker/Dockerfile.slim index 04c644d8a..18c1d8b12 100644 --- a/_docker/Dockerfile.slim +++ b/_docker/Dockerfile.slim @@ -14,6 +14,9 @@ RUN upx filebrowser FROM node:lts-slim AS nbuild WORKDIR /app COPY ./frontend/package.json ./ +RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \ + && git config --global url."https://github.com/".insteadOf "git@github.com:" \ + && git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" RUN npm i --maxsockets 1 COPY ./frontend/ ./ RUN npm run build-docker diff --git a/_docker/src/proxy/frontend/playwright.config.ts b/_docker/src/proxy/frontend/playwright.config.ts index 01dd3d689..42379bcd4 100644 --- a/_docker/src/proxy/frontend/playwright.config.ts +++ b/_docker/src/proxy/frontend/playwright.config.ts @@ -11,7 +11,7 @@ import { defineConfig, devices } from "@playwright/test"; */ export default defineConfig({ globalSetup: "./tests/playwright/proxy-setup.ts", - timeout: 3000, + timeout: 5000, testDir: "./tests/playwright/proxy", /* Run tests in files in parallel */ fullyParallel: false, diff --git a/backend/adapters/fs/files/files.go b/backend/adapters/fs/files/files.go index 363b90aaa..1b06c58f7 100644 --- a/backend/adapters/fs/files/files.go +++ b/backend/adapters/fs/files/files.go @@ -28,18 +28,16 @@ import ( "github.com/gtsteffaniak/go-logger/logger" ) -func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error) { - response := iteminfo.ExtendedFileInfo{} +func FileInfoFaster(opts iteminfo.FileOptions) (*iteminfo.ExtendedFileInfo, error) { + response := &iteminfo.ExtendedFileInfo{} index := indexing.GetIndex(opts.Source) if index == nil { return response, fmt.Errorf("could not get index: %v ", opts.Source) } - if opts.Access != nil && !opts.Access.Permitted(index.Path, opts.Path, opts.Username) { - return response, errors.ErrPermissionDenied - } + realPath, isDir, err := index.GetRealPath(opts.Path) if err != nil { - return response, err + return response, fmt.Errorf("could not get real path for requested path: %v", opts.Path) } opts.IsDir = isDir var info *iteminfo.FileInfo @@ -60,31 +58,69 @@ func FileInfoFaster(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error return response, fmt.Errorf("could not get metadata for path: %v", opts.Path) } } - if opts.Content { - if info.Size < 20*1024*1024 { // 20 megabytes in bytes - content, err := getContent(realPath) - if err != nil { - logger.Debugf("could not get content for file: "+info.Path, info.Name, err) - return response, err + response.FileInfo = *info + response.RealPath = realPath + response.Source = opts.Source + + if opts.Access != nil && !opts.Access.Permitted(index.Path, opts.Path, opts.Username) { + // check if any subpath is permitted + // keep track of permitted paths and only show them at the end + subFolders := info.Folders + subFiles := info.Files + response.Folders = make([]iteminfo.ItemInfo, 0) + response.Files = make([]iteminfo.ItemInfo, 0) + hasPermittedPaths := false + for _, subFolder := range subFolders { + indexPath := info.Path + subFolder.Name + if opts.Access.Permitted(index.Path, indexPath, opts.Username) { + hasPermittedPaths = true + response.Folders = append(response.Folders, subFolder) } - response.Content = content - } else { - logger.Debug("skipping large text file contents (20MB limit): "+info.Path, info.Name) + } + for _, subFile := range subFiles { + indexPath := info.Path + subFile.Name + if opts.Access.Permitted(index.Path, indexPath, opts.Username) { + hasPermittedPaths = true + response.Files = append(response.Files, subFile) + } + } + if !hasPermittedPaths { + return response, errors.ErrPermissionDenied } } - response.FileInfo = *info - response.RealPath = realPath - response.Source = index.Name + if opts.Content { + processContent(response, index) + } if settings.Config.Integrations.OnlyOffice.Secret != "" && info.Type != "directory" && iteminfo.IsOnlyOffice(info.Name) { response.OnlyOfficeId = generateOfficeId(realPath) } - if strings.HasPrefix(info.Type, "video") { - parentInfo, exists := index.GetReducedMetadata(filepath.Dir(info.Path), true) + + return response, nil +} + +func processContent(info *iteminfo.ExtendedFileInfo, idx *indexing.Index) { + isVideo := strings.HasPrefix(info.Type, "video") + if isVideo { + parentInfo, exists := idx.GetReducedMetadata(filepath.Dir(info.Path), true) if exists { - response.DetectSubtitles(parentInfo) + info.DetectSubtitles(parentInfo) + err := info.LoadSubtitleContent() + if err != nil { + logger.Debug("failed to load subtitle content: " + err.Error()) + } } + return + } + if info.Size < 20*1024*1024 { // 20 megabytes in bytes + content, err := getContent(info.RealPath) + if err != nil { + logger.Debugf("could not get content for file: "+info.RealPath, info.Name, err) + return + } + info.Content = content + } else { + logger.Debug("skipping large text file contents (20MB limit): "+info.Path, info.Name) } - return response, nil } func generateOfficeId(realPath string) string { diff --git a/backend/cmd/root.go b/backend/cmd/root.go index 05051e7b3..fb6dfd9b5 100644 --- a/backend/cmd/root.go +++ b/backend/cmd/root.go @@ -54,12 +54,14 @@ func StartFilebrowser() { if !keepGoing { return } - info, _ := utils.CheckForUpdates() - if info.LatestVersion != "" { - logger.Infof("A new version is available: %s (current: %s)", info.LatestVersion, info.CurrentVersion) - logger.Infof("Release notes: %s", info.ReleaseNotes) + if !settings.Config.Server.DisableUpdateCheck { + info, _ := utils.CheckForUpdates() + if info.LatestVersion != "" { + logger.Infof("A new version is available: %s (current: %s)", info.LatestVersion, info.CurrentVersion) + logger.Infof("Release notes: %s", info.ReleaseNotes) + } + go utils.StartCheckForUpdates() } - go utils.StartCheckForUpdates() // Create context and channels for graceful shutdown ctx, cancel := context.WithCancel(context.Background()) diff --git a/backend/common/settings/auth.go b/backend/common/settings/auth.go index 237637d35..20b7dcfd9 100644 --- a/backend/common/settings/auth.go +++ b/backend/common/settings/auth.go @@ -12,12 +12,12 @@ import ( ) type Auth struct { - TokenExpirationHours int `json:"tokenExpirationHours"` // the number of hours until the token expires. Default is 2 hours. + TokenExpirationHours int `json:"tokenExpirationHours"` // time in hours each web UI session token is valid for. Default is 2 hours. Methods LoginMethods `json:"methods"` - Key string `json:"key"` // the key used to sign the JWT tokens. If not set, a random key will be generated. - AdminUsername string `json:"adminUsername"` // the username of the admin user. If not set, the default is "admin". - AdminPassword string `json:"adminPassword"` // the password of the admin user. If not set, the default is "admin". - TotpSecret string `json:"totpSecret"` // secret used to encrypt TOTP secrets + Key string `json:"key"` // secret: the key used to sign the JWT tokens. If not set, a random key will be generated. + AdminUsername string `json:"adminUsername"` // secret: the username of the admin user. If not set, the default is "admin". + AdminPassword string `json:"adminPassword"` // secret: the password of the admin user. If not set, the default is "admin". + TotpSecret string `json:"totpSecret"` // secret: secret used to encrypt TOTP secrets AuthMethods []string `json:"-"` } @@ -52,8 +52,8 @@ type Recaptcha struct { // OpenID OAuth2.0 type OidcConfig struct { Enabled bool `json:"enabled"` // whether to enable OIDC authentication - ClientID string `json:"clientId"` // client id of the OIDC application - ClientSecret string `json:"clientSecret"` // client secret of the OIDC application + ClientID string `json:"clientId"` // secret: client id of the OIDC application + ClientSecret string `json:"clientSecret"` // secret: client secret of the OIDC application IssuerUrl string `json:"issuerUrl"` // authorization URL of the OIDC provider Scopes string `json:"scopes"` // scopes to request from the OIDC provider UserIdentifier string `json:"userIdentifier"` // the field value to use as the username. Default is "preferred_username", can also be "email" or "username", or "phone" diff --git a/backend/common/settings/config.go b/backend/common/settings/config.go index fa85d6cda..3a139374f 100644 --- a/backend/common/settings/config.go +++ b/backend/common/settings/config.go @@ -48,6 +48,9 @@ func Initialize(configFile string) { } func setupFrontend(generate bool) { + if Config.Server.MinSearchLength == 0 { + Config.Server.MinSearchLength = 3 + } if !Config.Frontend.DisableDefaultLinks { Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{ Text: fmt.Sprintf("(%v)", version.Version), @@ -56,12 +59,15 @@ func setupFrontend(generate bool) { }) Config.Frontend.ExternalLinks = append(Config.Frontend.ExternalLinks, ExternalLink{ Text: "Help", - Url: "https://github.com/gtsteffaniak/filebrowser/wiki", + Url: "help prompt", }) } + if Config.Frontend.Description == "" { + Config.Frontend.Description = "FileBrowser Quantum is a file manager for the web which can be used to manage files on your server" + } Config.Frontend.Styling.LightBackground = FallbackColor(Config.Frontend.Styling.LightBackground, "#f5f5f5") Config.Frontend.Styling.DarkBackground = FallbackColor(Config.Frontend.Styling.DarkBackground, "#141D24") - Config.Frontend.Styling.CustomCSS = readCustomCSS(Config.Frontend.Styling.CustomCSS) + Config.Frontend.Styling.CustomCSSRaw = readCustomCSS(Config.Frontend.Styling.CustomCSS) Config.Frontend.Styling.CustomThemeOptions = map[string]CustomTheme{} Config.Frontend.Styling.CustomThemes = map[string]CustomTheme{} for name, theme := range Config.Frontend.Styling.CustomThemes { @@ -85,6 +91,9 @@ func setupFrontend(generate bool) { if !ok { addCustomTheme("default", "The default theme", "") } + + // Load custom favicon if configured + loadCustomFavicon() } func getRealPath(path string) string { @@ -511,6 +520,54 @@ func GetSources(u *users.User) []string { return sources } +func loadCustomFavicon() { + // Check if a custom favicon path is configured + if Config.Frontend.Favicon == "" { + logger.Debug("No custom favicon configured, using default") + return + } + + // Get absolute path for the favicon + faviconPath, err := filepath.Abs(Config.Frontend.Favicon) + if err != nil { + logger.Warningf("Could not resolve favicon path '%v': %v", Config.Frontend.Favicon, err) + Config.Frontend.Favicon = "" // Unset invalid path + return + } + + // Check if the favicon file exists and get info + stat, err := os.Stat(faviconPath) + if err != nil { + logger.Warningf("Could not access custom favicon file '%v': %v", faviconPath, err) + Config.Frontend.Favicon = "" // Unset invalid path + return + } + + // Check file size (limit to 1MB for security) + const maxFaviconSize = 1024 * 1024 // 1MB + if stat.Size() > maxFaviconSize { + logger.Warningf("Favicon file '%v' is too large (%d bytes), maximum allowed is %d bytes", faviconPath, stat.Size(), maxFaviconSize) + Config.Frontend.Favicon = "" // Unset invalid path + return + } + + // Validate file format based on extension + ext := strings.ToLower(filepath.Ext(faviconPath)) + switch ext { + case ".ico", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp": + // Valid favicon formats + default: + logger.Warningf("Unsupported favicon format '%v', supported formats: .ico, .png, .jpg, .gif, .svg, .webp", ext) + Config.Frontend.Favicon = "" // Unset invalid path + return + } + + // Update to absolute path and mark as valid + Config.Frontend.Favicon = faviconPath + + logger.Infof("Successfully validated custom favicon at '%v' (%d bytes, %s)", faviconPath, stat.Size(), ext) +} + func modifyExcludeInclude(config *Source) { normalize := func(s []string, checkExists bool) { for i, v := range s { diff --git a/backend/common/settings/generator.go b/backend/common/settings/generator.go index 35275b0e7..3dce04403 100644 --- a/backend/common/settings/generator.go +++ b/backend/common/settings/generator.go @@ -14,19 +14,44 @@ import ( "gopkg.in/yaml.v3" ) -// commentsMap[typeName][fieldName] = combined doc+inline comment text -type commentsMap map[string]map[string]string +// CommentsMap[typeName][fieldName] = combined doc+inline comment text +type CommentsMap map[string]map[string]string -// collectComments parses all Go source in the directory of srcPath and returns commentsMap. -func collectComments(srcPath string) (commentsMap, error) { +// SecretFieldsMap[typeName][fieldName] = true if field should be redacted +type SecretFieldsMap map[string]map[string]bool + +// DeprecatedFieldsMap[typeName][fieldName] = true if field is deprecated +type DeprecatedFieldsMap map[string]map[string]bool + +// getStringStyle determines whether a string should be quoted in YAML +func getStringStyle(value string) yaml.Style { + // Always quote all strings for consistency + return yaml.DoubleQuotedStyle +} + +// CollectComments parses all Go source in the directory of srcPath and returns CommentsMap. +func CollectComments(srcPath string) (CommentsMap, error) { + comments, _, _, err := CollectCommentsAndSecrets(srcPath) + return comments, err +} + +// CollectCommentsAndSecrets parses all Go source and returns comments, secrets, and deprecated field mappings +func CollectCommentsAndSecrets(srcPath string) (CommentsMap, SecretFieldsMap, DeprecatedFieldsMap, error) { // parse entire package so we capture comments on all types - dir := filepath.Dir(srcPath) + dir := srcPath + if filepath.IsAbs(srcPath) { + // If it's an absolute path to a file, get the directory + dir = filepath.Dir(srcPath) + } + fset := token.NewFileSet() pkgs, err := parser.ParseDir(fset, dir, nil, parser.ParseComments) if err != nil { - return nil, err + return nil, nil, nil, err } - out := make(commentsMap) + comments := make(CommentsMap) + secrets := make(SecretFieldsMap) + deprecated := make(DeprecatedFieldsMap) for _, pkg := range pkgs { for _, file := range pkg.Files { for _, decl := range file.Decls { @@ -43,48 +68,81 @@ func collectComments(srcPath string) (commentsMap, error) { if !ok { continue } - m := make(map[string]string) - out[ts.Name.Name] = m + typeName := ts.Name.Name + commentMap := make(map[string]string) + secretMap := make(map[string]bool) + deprecatedMap := make(map[string]bool) + comments[typeName] = commentMap + secrets[typeName] = secretMap + deprecated[typeName] = deprecatedMap + for _, field := range st.Fields.List { if len(field.Names) == 0 { continue } name := field.Names[0].Name var parts []string + var fullComment string + if field.Doc != nil { - parts = append(parts, strings.TrimSpace(field.Doc.Text())) + docText := strings.TrimSpace(field.Doc.Text()) + parts = append(parts, docText) + fullComment += docText + " " } if field.Comment != nil { - parts = append(parts, strings.TrimSpace(field.Comment.Text())) + commentText := strings.TrimSpace(field.Comment.Text()) + parts = append(parts, commentText) + fullComment += commentText } + if len(parts) > 0 { - m[name] = strings.Join(parts, " : ") + commentMap[name] = strings.Join(parts, " : ") + } + + // Check if field should be treated as secret + if strings.Contains(strings.ToLower(fullComment), "secret:") { + secretMap[name] = true + } + + // Check if field should be treated as deprecated + if strings.Contains(strings.ToLower(fullComment), "deprecated:") { + deprecatedMap[name] = true } } } } } } - return out, nil + return comments, secrets, deprecated, nil +} + +// BuildNode constructs a yaml.Node for any Go value, injecting comments on struct fields. +func BuildNode(v reflect.Value, comm CommentsMap) (*yaml.Node, error) { + return buildNodeWithDefaults(v, comm, reflect.Value{}, SecretFieldsMap{}, DeprecatedFieldsMap{}) } -// buildNode constructs a yaml.Node for any Go value, injecting comments on struct fields. -func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) { +// buildNodeWithDefaults constructs a yaml.Node for any Go value, skipping fields that match defaults, redacting secrets, and filtering deprecated fields +func buildNodeWithDefaults(v reflect.Value, comm CommentsMap, defaults reflect.Value, secrets SecretFieldsMap, deprecated DeprecatedFieldsMap) (*yaml.Node, error) { // Dereference pointers if v.Kind() == reflect.Ptr { if v.IsNil() { return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: "null"}, nil } - return buildNode(v.Elem(), comm) + var defaultsElem reflect.Value + if defaults.IsValid() && !defaults.IsNil() { + defaultsElem = defaults.Elem() + } + return buildNodeWithDefaults(v.Elem(), comm, defaultsElem, secrets, deprecated) } switch v.Kind() { case reflect.String: + value := v.String() return &yaml.Node{ Kind: yaml.ScalarNode, Tag: "!!str", - Value: v.String(), - Style: yaml.DoubleQuotedStyle, + Value: value, + Style: getStringStyle(value), }, nil case reflect.Struct: rt := v.Type() @@ -104,6 +162,26 @@ func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) { continue } + // Skip deprecated fields if filtering is enabled + if len(deprecated) > 0 && deprecated[typeName][sf.Name] { + continue + } + + currentField := v.Field(i) + + // If we have defaults, compare and skip if values match + if defaults.IsValid() && i < defaults.NumField() { + defaultField := defaults.Field(i) + currentValue := currentField.Interface() + defaultValue := defaultField.Interface() + + isEqual := reflect.DeepEqual(currentValue, defaultValue) + + if isEqual { + continue // Skip this field as it matches the default + } + } + // determine key: yaml tag > json tag > field name yamlTag := sf.Tag.Get("yaml") jsonTag := sf.Tag.Get("json") @@ -118,22 +196,43 @@ func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) { keyNode := &yaml.Node{Kind: yaml.ScalarNode, Value: keyName} - // attach validate and comments inline + // attach validate and comments inline (only when comments are enabled) var parts []string if cm := comm[typeName][sf.Name]; cm != "" { parts = append(parts, cm) } - if vt := sf.Tag.Get("validate"); vt != "" { - parts = append(parts, fmt.Sprintf(" validate:%s", vt)) + // Only add validation tags if comments map is not empty (meaning comments are enabled) + if len(comm) > 0 { + if vt := sf.Tag.Get("validate"); vt != "" { + parts = append(parts, fmt.Sprintf(" validate:%s", vt)) + } } if len(parts) > 0 { keyNode.LineComment = strings.Join(parts, " ") } - valNode, err := buildNode(v.Field(i), comm) - if err != nil { - return nil, err + // Check if this field should be redacted as a secret + var valNode *yaml.Node + var err error + if secrets[typeName][sf.Name] { + valNode = &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Value: "**hidden**", + Style: yaml.DoubleQuotedStyle, // Keep secrets quoted for clarity + } + } else { + // Pass through the corresponding default field for recursive comparison + var defaultField reflect.Value + if defaults.IsValid() && i < defaults.NumField() { + defaultField = defaults.Field(i) + } + + valNode, err = buildNodeWithDefaults(currentField, comm, defaultField, secrets, deprecated) + if err != nil { + return nil, err + } } mapNode.Content = append(mapNode.Content, keyNode, valNode) } @@ -148,7 +247,11 @@ func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) { // placeholder for empty slice of structs if v.Len() == 0 && v.Type().Elem().Kind() == reflect.Struct { zero := reflect.Zero(v.Type().Elem()) - n, err := buildNode(zero, comm) + var defaultElem reflect.Value + if defaults.IsValid() && defaults.Len() > 0 { + defaultElem = defaults.Index(0) + } + n, err := buildNodeWithDefaults(zero, comm, defaultElem, secrets, deprecated) if err != nil { return nil, err } @@ -156,7 +259,11 @@ func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) { return seq, nil } for i := 0; i < v.Len(); i++ { - n, err := buildNode(v.Index(i), comm) + var defaultElem reflect.Value + if defaults.IsValid() && i < defaults.Len() { + defaultElem = defaults.Index(i) + } + n, err := buildNodeWithDefaults(v.Index(i), comm, defaultElem, secrets, deprecated) if err != nil { return nil, err } @@ -164,6 +271,33 @@ func buildNode(v reflect.Value, comm commentsMap) (*yaml.Node, error) { } return seq, nil + case reflect.Map: + mapNode := &yaml.Node{Kind: yaml.MappingNode} + + for _, key := range v.MapKeys() { + // Handle key + keyStr := fmt.Sprintf("%v", key.Interface()) + keyNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Value: keyStr, + } + + // Handle value recursively + mapVal := v.MapIndex(key) + var defaultVal reflect.Value + if defaults.IsValid() { + defaultVal = defaults.MapIndex(key) + } + + valNode, err := buildNodeWithDefaults(mapVal, comm, defaultVal, secrets, deprecated) + if err != nil { + return nil, err + } + + mapNode.Content = append(mapNode.Content, keyNode, valNode) + } + return mapNode, nil + default: n := &yaml.Node{} if err := n.Encode(v.Interface()); err != nil { @@ -186,21 +320,73 @@ func GenerateYaml() { setupSources(true) setupUrls() setupFrontend(true) - input := "common/settings/settings.go" // "path to Go source file or directory containing structs" - output := "generated.yaml" // "output YAML file" + output := "generated.yaml" // "output YAML file" - comm, err := collectComments(input) + // Generate YAML with comments enabled, full config, and deprecated fields filtered + // Force the source path to be correct for static generation + yamlContent, err := GenerateConfigYamlWithSource(&Config, true, true, true, "common/settings") if err != nil { - fmt.Fprintf(os.Stderr, "error parsing comments: %v\n", err) + fmt.Fprintf(os.Stderr, "error generating YAML: %v\n", err) os.Exit(1) } - node, err := buildNode(reflect.ValueOf(Config), comm) - if err != nil { - fmt.Fprintf(os.Stderr, "error building YAML node: %v\n", err) + if err := os.WriteFile(output, []byte(yamlContent), 0644); err != nil { + fmt.Fprintf(os.Stderr, "error writing YAML: %v\n", err) os.Exit(1) } + fmt.Printf("Generated YAML with comments (deprecated fields filtered): %s\n", output) +} + +// GenerateConfigYaml generates YAML from a given config with options for comments and filtering +func GenerateConfigYaml(config *Settings, showComments bool, showFull bool, filterDeprecated bool) (string, error) { + // Try different source paths to handle both runtime and test scenarios + sourcePaths := []string{ + "common/settings", // When running from backend directory + ".", // When running tests from settings directory + "../settings", // Alternative test path + } + + for _, sourcePath := range sourcePaths { + yamlOutput, err := GenerateConfigYamlWithSource(config, showComments, showFull, filterDeprecated, sourcePath) + if err == nil { + return yamlOutput, nil + } + // If it's not a file not found error, return immediately + if !strings.Contains(err.Error(), "no such file or directory") && !strings.Contains(err.Error(), "cannot find") { + return "", err + } + } + + // If all paths failed, try with empty maps to at least generate basic YAML + return GenerateConfigYamlWithEmptyMaps(config, showFull) +} +// GenerateConfigYamlWithEmptyMaps generates YAML without comment parsing when source files are unavailable +func GenerateConfigYamlWithEmptyMaps(config *Settings, showFull bool) (string, error) { + // Create empty maps + comm := make(CommentsMap) + secrets := make(SecretFieldsMap) + deprecated := make(DeprecatedFieldsMap) + + var node *yaml.Node + var err error + + if showFull { + // Show the full current config + node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.Value{}, secrets, deprecated) + } else { + // Show only non-default values by comparing with defaults during node building + defaultConfig := setDefaults(true) + defaultConfig.Server.Sources = []Source{{Path: "."}} + + node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.ValueOf(&defaultConfig), secrets, deprecated) + } + + if err != nil { + return "", err + } + + // Convert to YAML doc := &yaml.Node{Kind: yaml.DocumentNode} doc.Content = []*yaml.Node{node} @@ -208,17 +394,78 @@ func GenerateYaml() { enc := yaml.NewEncoder(&rawBuf) enc.SetIndent(2) if err := enc.Encode(doc); err != nil { - fmt.Fprintf(os.Stderr, "error encoding YAML: %v\n", err) - os.Exit(1) + return "", err } - aligned := alignComments(rawBuf.String()) + return AlignComments(rawBuf.String()), nil +} - if err := os.WriteFile(output, []byte(aligned), 0644); err != nil { - fmt.Fprintf(os.Stderr, "error writing YAML: %v\n", err) - os.Exit(1) +// GenerateConfigYamlWithSource generates YAML from a given config with options for comments and filtering, using a custom source path +func GenerateConfigYamlWithSource(config *Settings, showComments bool, showFull bool, filterDeprecated bool, sourcePath string) (string, error) { + var comm CommentsMap + var secrets SecretFieldsMap + var deprecated DeprecatedFieldsMap + var err error + + if showComments { + // Collect comments, secrets, and deprecated fields from the settings source files + comm, secrets, deprecated, err = CollectCommentsAndSecrets(sourcePath) + if err != nil { + return "", err + } + } else { + // Still need to collect secrets and deprecated fields even if not showing comments + _, secrets, deprecated, err = CollectCommentsAndSecrets(sourcePath) + if err != nil { + return "", err + } + // Create empty comments map + comm = make(CommentsMap) + } + + // If not filtering deprecated fields, clear the deprecated map + if !filterDeprecated { + deprecated = make(DeprecatedFieldsMap) + } + + var node *yaml.Node + + if showFull { + // Show the full current config + node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.Value{}, secrets, deprecated) + } else { + // Show only non-default values by comparing with defaults during node building + // Create a clean default config (no file loading, just pure defaults) + defaultConfig := setDefaults(true) + // Apply same setup as a fresh instance would have + defaultConfig.Server.Sources = []Source{{Path: "."}} + + node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.ValueOf(&defaultConfig), secrets, deprecated) + } + + if err != nil { + return "", err } - fmt.Printf("Generated YAML with comments: %s\n", output) + + // Create document + doc := &yaml.Node{Kind: yaml.DocumentNode} + doc.Content = []*yaml.Node{node} + + // Encode to YAML + var rawBuf bytes.Buffer + enc := yaml.NewEncoder(&rawBuf) + enc.SetIndent(2) + if err := enc.Encode(doc); err != nil { + return "", err + } + + // Apply comment alignment if comments are enabled + yamlOutput := rawBuf.String() + if showComments { + yamlOutput = AlignComments(yamlOutput) + } + + return yamlOutput, nil } // formatLine applies padding to a single line so its comment starts at a target column. @@ -257,8 +504,8 @@ func formatLine(line string) string { return line } -// alignComments formats each line of the YAML string independently. -func alignComments(input string) string { +// AlignComments formats each line of the YAML string independently. +func AlignComments(input string) string { lines := strings.Split(input, "\n") outputLines := make([]string, len(lines)) diff --git a/backend/common/settings/generator_simple_test.go b/backend/common/settings/generator_simple_test.go new file mode 100644 index 000000000..162a9a14f --- /dev/null +++ b/backend/common/settings/generator_simple_test.go @@ -0,0 +1,230 @@ +package settings + +import ( + "regexp" + "strings" + "testing" +) + +func TestGenerateConfigYaml_Basic(t *testing.T) { + reNumber := regexp.MustCompile(`^-?\d+(\.\d+)?$`) + // Test using the actual source directory structure + config := &Settings{ + UserDefaults: UserDefaults{ + Locale: "es", // Non-default value + DarkMode: true, // Default value + DisableOfficePreviewExt: ".docx .xlsx", // This field is deprecated + }, + Auth: Auth{ + Key: "secret123", // This is a secret field + AdminUsername: "testadmin", // This is a secret field + AdminPassword: "testpassword", // This is a secret field + }, + } + + // Test different combinations + testCases := []struct { + name string + showComments bool + showFull bool + filterDeprecated bool + expectSecrets bool + expectDeprecated bool + }{ + { + name: "API_mode_all_fields", + showComments: false, + showFull: true, + filterDeprecated: false, + expectSecrets: true, // Secrets should be hidden + expectDeprecated: true, // Deprecated should be shown + }, + { + name: "Static_config_filter_deprecated", + showComments: true, + showFull: true, + filterDeprecated: true, + expectSecrets: true, // Secrets should be hidden + expectDeprecated: false, // Deprecated should be filtered + }, + { + name: "Filtered_non_defaults", + showComments: false, + showFull: false, + filterDeprecated: false, + expectSecrets: true, // Secrets should be hidden + expectDeprecated: false, // Might not appear if matches default + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Use the current directory structure for real testing + yamlOutput, err := GenerateConfigYaml(config, tc.showComments, tc.showFull, tc.filterDeprecated) + if err != nil { + t.Fatalf("GenerateConfigYaml failed: %v", err) + } + + // Basic validation - should produce some output + if len(yamlOutput) == 0 { + t.Fatal("Generated empty YAML") + } + + // Test string quoting - string values should be quoted, numbers and booleans should not + lines := strings.Split(yamlOutput, "\n") + for _, line := range lines { + if strings.Contains(line, ":") && !strings.HasSuffix(strings.TrimSpace(line), ":") { + // This is a key-value line, skip comment portion + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 { + value := strings.TrimSpace(parts[1]) + // Remove comments + if commentIdx := strings.Index(value, "#"); commentIdx >= 0 { + value = strings.TrimSpace(value[:commentIdx]) + } + + // Skip empty values, arrays, and objects + if value == "" || strings.HasPrefix(value, "[") || strings.HasPrefix(value, "{") { + continue + } + + // Check if it's a number (should NOT be quoted) + if reNumber.MatchString(value) { + if strings.HasPrefix(value, "\"") { + t.Errorf("Numeric value should not be quoted in line: %s (value: '%s')", line, value) + } + continue + } + + // Check if it's a boolean (should NOT be quoted) + if value == "true" || value == "false" { + if strings.HasPrefix(value, "\"") { + t.Errorf("Boolean value should not be quoted in line: %s (value: '%s')", line, value) + } + continue + } + + // Everything else should be a quoted string + if !strings.HasPrefix(value, "\"") || !strings.HasSuffix(value, "\"") { + // Add debug info to see what's wrong + t.Logf("DEBUG: Checking value '%s' from line '%s'", value, strings.TrimSpace(line)) + t.Errorf("String value should be quoted in line: %s (value: %s)", strings.TrimSpace(line), value) + } + } + } + } + + // Test secret hiding + if tc.expectSecrets { + if !strings.Contains(yamlOutput, "**hidden**") { + t.Error("Expected secrets to be hidden with **hidden**") + } + if strings.Contains(yamlOutput, "secret123") || strings.Contains(yamlOutput, "testadmin") || strings.Contains(yamlOutput, "testpassword") { + t.Error("Secret values should not appear in output") + } + } + + // Test deprecated field filtering + hasDeprecated := strings.Contains(yamlOutput, "disableOfficePreviewExt") + if tc.expectDeprecated && !hasDeprecated { + t.Error("Expected deprecated field to be present") + } + if !tc.expectDeprecated && hasDeprecated && tc.filterDeprecated { + t.Error("Expected deprecated field to be filtered out when filterDeprecated=true") + } + + // Test comments + hasComments := strings.Contains(yamlOutput, "#") + if tc.showComments && !hasComments { + t.Error("Expected comments to be present when showComments=true") + } + + t.Logf("Test case: %s\nGenerated YAML:\n%s\n", tc.name, yamlOutput) + }) + } +} + +func TestCollectCommentsAndSecrets_Basic(t *testing.T) { + // Test the comment and secret collection directly + comments, secrets, deprecated, err := CollectCommentsAndSecrets(".") + if err != nil { + t.Fatalf("CollectCommentsAndSecrets failed: %v", err) + } + + // Should find some secrets in the actual source files + foundSecrets := false + for typeName, typeSecrets := range secrets { + for fieldName := range typeSecrets { + t.Logf("Found secret: %s.%s", typeName, fieldName) + foundSecrets = true + } + } + if !foundSecrets { + t.Error("Expected to find some secret fields in the actual source code") + } + + // Should find some deprecated fields + foundDeprecated := false + for typeName, typeDeprecated := range deprecated { + for fieldName := range typeDeprecated { + t.Logf("Found deprecated: %s.%s", typeName, fieldName) + foundDeprecated = true + } + } + if !foundDeprecated { + t.Error("Expected to find some deprecated fields in the actual source code") + } + + t.Logf("Comments: %d types", len(comments)) + t.Logf("Secrets: %d types", len(secrets)) + t.Logf("Deprecated: %d types", len(deprecated)) +} + +func TestGenerateYaml_StaticGeneration(t *testing.T) { + // Test the static generation function that's used by FILEBROWSER_GENERATE_CONFIG=true + + // Create a temporary config for testing + config := &Settings{ + UserDefaults: UserDefaults{ + Locale: "en", + DisableOfficePreviewExt: ".docx .xlsx", // This should be filtered out + }, + Auth: Auth{ + Key: "test-secret", // This should be redacted + }, + } + + // Test static generation with comments and deprecated filtering + yamlOutput, err := GenerateConfigYamlWithSource(config, true, true, true, ".") + if err != nil { + t.Fatalf("Static generation failed: %v", err) + } + + // Verify comments are included + if !strings.Contains(yamlOutput, "#") { + t.Error("Static generation should include comments") + } + + // Verify deprecated field is filtered out + if strings.Contains(yamlOutput, "disableOfficePreviewExt") { + t.Error("Static generation should filter out deprecated field disableOfficePreviewExt") + } + + // Verify secrets are redacted + if !strings.Contains(yamlOutput, "**hidden**") { + t.Error("Static generation should redact secrets") + } + if strings.Contains(yamlOutput, "test-secret") { + t.Error("Static generation should not contain actual secret values") + } + + // Verify it has proper structure + if !strings.Contains(yamlOutput, "userDefaults:") { + t.Error("Static generation should contain userDefaults section") + } + if !strings.Contains(yamlOutput, "auth:") { + t.Error("Static generation should contain auth section") + } + + t.Logf("Static generation successful with comments and proper filtering") +} diff --git a/backend/common/settings/generator_test.go b/backend/common/settings/generator_test.go new file mode 100644 index 000000000..8cd9e97c7 --- /dev/null +++ b/backend/common/settings/generator_test.go @@ -0,0 +1,470 @@ +package settings + +import ( + "os" + "regexp" + "strings" + "testing" +) + +// Test structs for comprehensive testing +type TestConfig struct { + PublicField string `json:"publicField"` // A regular public field + SecretField string `json:"secretField"` // secret: this should be hidden + DeprecatedField string `json:"deprecatedField"` // deprecated: this field is old + EmptyField string `json:"emptyField"` // An empty string field + DefaultField string `json:"defaultField"` // A field that matches default + NonDefaultField string `json:"nonDefaultField"` // A field that differs from default + NestedStruct TestNested `json:"nestedStruct"` +} + +type TestNested struct { + NestedPublic string `json:"nestedPublic"` // A nested public field + NestedSecret string `json:"nestedSecret"` // secret: nested secret field + NestedDeprecated string `json:"nestedDeprecated"` // deprecated: nested deprecated field +} + +// Helper to create test source files for comment parsing +func createTestSourceFile(t *testing.T, content string) (string, func()) { + tmpFile, err := os.CreateTemp("", "test_*.go") + if err != nil { + t.Fatal(err) + } + + if _, err := tmpFile.WriteString(content); err != nil { + t.Fatal(err) + } + tmpFile.Close() + + cleanup := func() { + os.Remove(tmpFile.Name()) + } + + return tmpFile.Name(), cleanup +} + +func TestGenerateConfigYaml_StringQuoting(t *testing.T) { + reNumber := regexp.MustCompile(`^-?\d+(\.\d+)?$`) + // Create a simple test source file for this test + sourceContent := `package settings + +type Settings struct { + UserDefaults UserDefaults ` + "`json:\"userDefaults\"`" + ` + Auth Auth ` + "`json:\"auth\"`" + ` +} + +type UserDefaults struct { + Locale string ` + "`json:\"locale\"`" + ` +} + +type Auth struct { + AdminUsername string ` + "`json:\"adminUsername\"`" + ` +} +` + tmpFile, cleanup := createTestSourceFile(t, sourceContent) + defer cleanup() + sourcePath := tmpFile[:strings.LastIndex(tmpFile, "/")] + + // Create real Settings with string values to test quoting + settings := &Settings{ + UserDefaults: UserDefaults{ + Locale: "en-US", + }, + Auth: Auth{ + AdminUsername: "admin", + }, + } + + // Test with no filtering - should quote all strings + yamlOutput, err := GenerateConfigYamlWithSource(settings, false, true, false, sourcePath) + if err != nil { + t.Fatalf("GenerateConfigYaml failed: %v", err) + } + + // All string values should be quoted + lines := strings.Split(yamlOutput, "\n") + for _, line := range lines { + if strings.Contains(line, ":") && !strings.HasSuffix(strings.TrimSpace(line), ":") { + // This is a key-value line + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 { + value := strings.TrimSpace(parts[1]) + + // Remove comments + if commentIdx := strings.Index(value, "#"); commentIdx >= 0 { + value = strings.TrimSpace(value[:commentIdx]) + } + + // Skip empty values, arrays, objects, booleans + if value == "" || value == "false" || value == "true" || strings.HasPrefix(value, "[") || strings.HasPrefix(value, "{") { + continue + } + + // Check if it's a number (should NOT be quoted) + if reNumber.MatchString(value) { + if strings.HasPrefix(value, "\"") { + t.Errorf("Numeric value should not be quoted: %s (value: '%s')", line, value) + } + continue + } + + // Everything else should be a quoted string + if !strings.HasPrefix(value, "\"") || !strings.HasSuffix(value, "\"") { + t.Errorf("String value should be quoted: %s (value: '%s')", line, value) + } + } + } + } +} + +func TestGenerateConfigYaml_SecretHiding(t *testing.T) { + // Create temporary source file with our test structs + sourceContent := `package settings + +type TestConfig struct { + PublicField string ` + "`json:\"publicField\"`" + ` // A regular public field + SecretField string ` + "`json:\"secretField\"`" + ` // secret: this should be hidden +} + +type TestNested struct { + NestedPublic string ` + "`json:\"nestedPublic\"`" + ` // A nested public field + NestedSecret string ` + "`json:\"nestedSecret\"`" + ` // secret: nested secret field +} +` + + tmpFile, cleanup := createTestSourceFile(t, sourceContent) + defer cleanup() + + // Test secret detection + _, secrets, _, err := CollectCommentsAndSecrets(tmpFile) + if err != nil { + t.Fatalf("CollectCommentsAndSecrets failed: %v", err) + } + + // Verify secret fields are detected + if !secrets["TestConfig"]["SecretField"] { + t.Error("SecretField should be marked as secret") + } + if !secrets["TestNested"]["NestedSecret"] { + t.Error("NestedSecret should be marked as secret") + } + if secrets["TestConfig"]["PublicField"] { + t.Error("PublicField should not be marked as secret") + } + + // Test that secrets are properly collected + if len(secrets["TestConfig"]) != 1 { + t.Errorf("Expected 1 secret field in TestConfig, got %d", len(secrets["TestConfig"])) + } +} + +func TestGenerateConfigYaml_DeprecatedFiltering(t *testing.T) { + // Create temporary source file with deprecated fields + sourceContent := `package settings + +type TestConfig struct { + PublicField string ` + "`json:\"publicField\"`" + ` // A regular public field + DeprecatedField string ` + "`json:\"deprecatedField\"`" + ` // deprecated: this field is old +} +` + + tmpFile, cleanup := createTestSourceFile(t, sourceContent) + defer cleanup() + + // Test deprecated detection + _, _, deprecated, err := CollectCommentsAndSecrets(tmpFile) + if err != nil { + t.Fatalf("CollectCommentsAndSecrets failed: %v", err) + } + + // Verify deprecated fields are detected + if !deprecated["TestConfig"]["DeprecatedField"] { + t.Error("DeprecatedField should be marked as deprecated") + } + if deprecated["TestConfig"]["PublicField"] { + t.Error("PublicField should not be marked as deprecated") + } +} + +func TestGenerateConfigYaml_FullVsFiltered(t *testing.T) { + // Create minimal test source + sourceContent := `package settings + +type Settings struct { + UserDefaults UserDefaults ` + "`json:\"userDefaults\"`" + ` +} + +type UserDefaults struct { + DarkMode bool ` + "`json:\"darkMode\"`" + ` + Locale string ` + "`json:\"locale\"`" + ` +} +` + tmpFile, cleanup := createTestSourceFile(t, sourceContent) + defer cleanup() + sourcePath := tmpFile[:strings.LastIndex(tmpFile, "/")] + + // Create a config with both default and non-default values + config := &Settings{ + UserDefaults: UserDefaults{ + DarkMode: true, // This matches default + Locale: "es", // This differs from default ("en") + }, + } + + // Test full=true - should show all fields + fullYaml, err := GenerateConfigYamlWithSource(config, false, true, false, sourcePath) + if err != nil { + t.Fatalf("GenerateConfigYaml with full=true failed: %v", err) + } + + // Test full=false - should only show non-default fields + filteredYaml, err := GenerateConfigYamlWithSource(config, false, false, false, sourcePath) + if err != nil { + t.Fatalf("GenerateConfigYaml with full=false failed: %v", err) + } + + // Full output should be longer than filtered + if len(fullYaml) <= len(filteredYaml) { + t.Error("Full YAML should be longer than filtered YAML") + } + + // Filtered output should contain the non-default field + if !strings.Contains(filteredYaml, "locale") { + t.Error("Filtered YAML should contain non-default locale field") + } + + // Full output should contain default fields + if !strings.Contains(fullYaml, "darkMode") { + t.Error("Full YAML should contain darkMode field") + } +} + +func TestGenerateConfigYaml_CommentsOnOff(t *testing.T) { + // Use the actual settings source directory since comments work there + config := &Settings{ + UserDefaults: UserDefaults{ + Locale: "en", + }, + } + + // Test with comments=true using the real source + withComments, err := GenerateConfigYaml(config, true, true, false) + if err != nil { + t.Fatalf("GenerateConfigYaml with comments=true failed: %v", err) + } + + // Test with comments=false using the real source + withoutComments, err := GenerateConfigYaml(config, false, true, false) + if err != nil { + t.Fatalf("GenerateConfigYaml with comments=false failed: %v", err) + } + + // Output with comments should be longer + if len(withComments) <= len(withoutComments) { + t.Errorf("YAML with comments (%d chars) should be longer than without comments (%d chars)", len(withComments), len(withoutComments)) + } + + // With comments should contain '#' characters + if !strings.Contains(withComments, "#") { + t.Error("YAML with comments should contain '#' characters") + } + + // Without comments should not contain '#' characters + if strings.Contains(withoutComments, "#") { + t.Error("YAML without comments should not contain '#' characters") + } +} + +func TestGenerateConfigYaml_IntegrationTest(t *testing.T) { + reNumber := regexp.MustCompile(`^-?\d+(\.\d+)?$`) + // Comprehensive test with all features + config := &Settings{ + UserDefaults: UserDefaults{ + Locale: "es", // Non-default + DarkMode: true, // Default + DisableOfficePreviewExt: ".docx .xlsx", // This is deprecated + }, + Auth: Auth{ + Key: "secret123", // This is secret + AdminUsername: "admin", // This is secret + AdminPassword: "password", // This is secret + }, + } + + tests := []struct { + name string + showComments bool + showFull bool + filterDeprecated bool + expectSecret bool + expectDeprecated bool + expectFull bool + expectComments bool + }{ + { + name: "API mode: show all including deprecated", + showComments: false, + showFull: true, + filterDeprecated: false, + expectSecret: true, // secrets should be hidden + expectDeprecated: true, // deprecated should be shown + expectFull: true, // all fields shown + expectComments: false, // no comments + }, + { + name: "Static config mode: filter deprecated", + showComments: true, + showFull: true, + filterDeprecated: true, + expectSecret: true, // secrets should be hidden + expectDeprecated: false, // deprecated should be filtered + expectFull: true, // all non-deprecated fields shown + expectComments: true, // comments included + }, + { + name: "Filtered output: only changes", + showComments: false, + showFull: false, + filterDeprecated: false, + expectSecret: true, // secrets should be hidden + expectDeprecated: false, // might not appear if matches default + expectFull: false, // only non-default fields + expectComments: false, // no comments + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + yamlOutput, err := GenerateConfigYaml(config, tt.showComments, tt.showFull, tt.filterDeprecated) + if err != nil { + t.Fatalf("GenerateConfigYaml failed: %v", err) + } + + // Check secret hiding + if tt.expectSecret { + if !strings.Contains(yamlOutput, "**hidden**") { + t.Error("Expected secrets to be hidden with **hidden**") + } + if strings.Contains(yamlOutput, "secret123") { + t.Error("Secret values should not appear in output") + } + } + + // Check deprecated field filtering + hasDeprecated := strings.Contains(yamlOutput, "disableOfficePreviewExt") + if tt.expectDeprecated && !hasDeprecated { + t.Error("Expected deprecated field to be present") + } + if !tt.expectDeprecated && hasDeprecated && tt.filterDeprecated { + t.Error("Expected deprecated field to be filtered out") + } + + // Check comments + hasComments := strings.Contains(yamlOutput, "#") + if tt.expectComments && !hasComments { + t.Error("Expected comments to be present") + } + if !tt.expectComments && hasComments { + t.Error("Expected no comments") + } + + // Check string quoting - all string values should be quoted + lines := strings.Split(yamlOutput, "\n") + for _, line := range lines { + if strings.Contains(line, ":") && !strings.HasSuffix(strings.TrimSpace(line), ":") { + // This is a key-value line + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 { + value := strings.TrimSpace(parts[1]) + // Skip comments and empty values + if commentIdx := strings.Index(value, "#"); commentIdx >= 0 { + // If comment starts at the beginning, there's no value, just a comment + if commentIdx == 0 { + continue + } + value = strings.TrimSpace(value[:commentIdx]) + } + // Check if it's a number (should NOT be quoted) + if reNumber.MatchString(value) { + if strings.HasPrefix(value, "\"") { + t.Errorf("Numeric value should not be quoted in line: %s (value: '%s')", line, value) + } + continue + } + + // Check if it's a boolean (should NOT be quoted) + if value == "true" || value == "false" { + if strings.HasPrefix(value, "\"") { + t.Errorf("Boolean value should not be quoted in line: %s (value: '%s')", line, value) + } + continue + } + + // Skip arrays, objects, and empty values + if value == "" || strings.HasPrefix(value, "[") || strings.HasPrefix(value, "{") || strings.Contains(value, ":") { + continue + } + + // Everything else should be a quoted string + if !strings.HasPrefix(value, "\"") || !strings.HasSuffix(value, "\"") { + t.Errorf("String value should be quoted in line: %s (value: '%s')", line, value) + } + } + } + } + + t.Logf("Generated YAML for %s:\n%s", tt.name, yamlOutput) + }) + } +} + +func TestGenerateConfigYaml_EdgeCases(t *testing.T) { + tests := []struct { + name string + config *Settings + desc string + }{ + { + name: "empty_config", + config: &Settings{}, + desc: "Empty config should generate valid YAML", + }, + { + name: "only_defaults", + config: &Settings{ + UserDefaults: UserDefaults{ + DarkMode: true, // default value + Locale: "en", // default value + }, + }, + desc: "Config with only default values", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test all combinations + for _, showComments := range []bool{false, true} { + for _, showFull := range []bool{false, true} { + for _, filterDeprecated := range []bool{false, true} { + yamlOutput, err := GenerateConfigYaml(tt.config, showComments, showFull, filterDeprecated) + if err != nil { + t.Fatalf("GenerateConfigYaml failed for %s (comments=%v, full=%v, filterDeprecated=%v): %v", + tt.desc, showComments, showFull, filterDeprecated, err) + } + + // Basic validation - should produce valid YAML structure + if yamlOutput == "" { + t.Errorf("Generated empty YAML for %s", tt.desc) + } + + // Should not contain obvious error messages (but allow "error" in log levels etc) + if strings.Contains(yamlOutput, "Error:") || strings.Contains(yamlOutput, "ERROR:") { + t.Errorf("YAML contains error for %s", tt.desc) + } + } + } + } + }) + } +} diff --git a/backend/common/settings/structs.go b/backend/common/settings/structs.go index bf20ccedd..6848bc781 100644 --- a/backend/common/settings/structs.go +++ b/backend/common/settings/structs.go @@ -21,6 +21,8 @@ type Settings struct { } type Server struct { + MinSearchLength int `json:"minSearchLength"` // minimum length of search query to begin searching (default: 3) + DisableUpdateCheck bool `json:"disableUpdateCheck"` // disables backend update check service NumImageProcessors int `json:"numImageProcessors"` // number of concurrent image processing jobs used to create previews, default is number of cpu cores available. Socket string `json:"socket"` // socket to listen on TLSKey string `json:"tlsKey"` // path to TLS key @@ -52,9 +54,9 @@ type Integrations struct { // onlyoffice secret is stored in the local.json file // docker exec /var/www/onlyoffice/documentserver/npm/json -f /etc/onlyoffice/documentserver/local.json 'services.CoAuthoring.secret.session.string' type OnlyOffice struct { - Url string `json:"url" validate:"required"` // The URL to the OnlyOffice Document Server, needs to be accessible to the user. - InternalUrl string `json:"internalUrl"` // An optional internal address that the filebrowser server can use to communicate with the OnlyOffice Document Server, could be useful to bypass proxy. - Secret string `json:"secret" validate:"required"` + Url string `json:"url" validate:"required"` // The URL to the OnlyOffice Document Server, needs to be accessible to the user. + InternalUrl string `json:"internalUrl"` // An optional internal address that the filebrowser server can use to communicate with the OnlyOffice Document Server, could be useful to bypass proxy. + Secret string `json:"secret" validate:"required"` // secret: authentication key for OnlyOffice integration } type Media struct { @@ -73,22 +75,22 @@ type LogConfig struct { type Source struct { Path string `json:"path" validate:"required"` // file system path. (Can be relative) Name string `json:"name"` // display name - Config SourceConfig `json:"config"` + Config SourceConfig `json:"config,omitempty"` } type SourceConfig struct { - DenyByDefault bool `json:"denyByDefault"` // deny access unless an "allow" access rule was specifically created. - Private bool `json:"private"` // designate as source as private -- currently just means no sharing permitted. - Disabled bool `json:"disabled"` // disable the source, this is useful so you don't need to remove it from the config file - IndexingInterval uint32 `json:"indexingIntervalMinutes"` // optional manual overide interval in minutes to re-index the source - DisableIndexing bool `json:"disableIndexing"` // disable the indexing of this source - MaxWatchers int `json:"maxWatchers"` // number of concurrent watchers to use for this source, currently not supported - NeverWatchPaths []string `json:"neverWatchPaths"` // paths that get initially once. Useful for folders that rarely change contents (without source path prefix) - Exclude ExcludeIndexFilter `json:"exclude"` // exclude files and folders from indexing, if include is not set - Include IncludeIndexFilter `json:"include"` // include files and folders from indexing, if exclude is not set - DefaultUserScope string `json:"defaultUserScope"` // default "/" should match folders under path - DefaultEnabled bool `json:"defaultEnabled"` // should be added as a default source for new users? - CreateUserDir bool `json:"createUserDir"` // create a user directory for each user + DenyByDefault bool `json:"denyByDefault,omitempty"` // deny access unless an "allow" access rule was specifically created. + Private bool `json:"private"` // designate as source as private -- currently just means no sharing permitted. + Disabled bool `json:"disabled,omitempty"` // disable the source, this is useful so you don't need to remove it from the config file + IndexingInterval uint32 `json:"indexingIntervalMinutes,omitempty"` // optional manual overide interval in minutes to re-index the source + DisableIndexing bool `json:"disableIndexing,omitempty"` // disable the indexing of this source + MaxWatchers int `json:"maxWatchers"` // number of concurrent watchers to use for this source, currently not supported + NeverWatchPaths []string `json:"neverWatchPaths"` // paths that get initially once. Useful for folders that rarely change contents (without source path prefix) + Exclude ExcludeIndexFilter `json:"exclude"` // exclude files and folders from indexing, if include is not set + Include IncludeIndexFilter `json:"include"` // include files and folders from indexing, if exclude is not set + DefaultUserScope string `json:"defaultUserScope"` // default "/" should match folders under path + DefaultEnabled bool `json:"defaultEnabled"` // should be added as a default source for new users? + CreateUserDir bool `json:"createUserDir"` // create a user directory for each user } type IncludeIndexFilter struct { @@ -114,19 +116,24 @@ type Frontend struct { ExternalLinks []ExternalLink `json:"externalLinks"` DisableNavButtons bool `json:"disableNavButtons"` // disable the nav buttons in the sidebar Styling StylingConfig `json:"styling"` + Favicon string `json:"favicon"` // path to a favicon to use for the frontend + Description string `json:"description"` // description that shows up in html head meta description } type StylingConfig struct { - CustomCSS string `json:"customCSS"` // if a valid path to a css file is provided, it will be applied for all users. (eg. "reduce-rounded-corners.css") - LightBackground string `json:"lightBackground"` // specify a valid CSS color property value to use as the background color in light mode - DarkBackground string `json:"darkBackground"` // Specify a valid CSS color property value to use as the background color in dark mode - CustomThemes map[string]CustomTheme `json:"customThemes"` // A list of custom css files that each user can select to override the default styling. if "default" is key name then it will be the default option. - CustomThemeOptions map[string]CustomTheme `json:"-"` // not exposed + CustomCSS string `json:"customCSS"` // if a valid path to a css file is provided, it will be applied for all users. (eg. "reduce-rounded-corners.css") + CustomCSSRaw string `json:"-"` // The css raw content to use for the custom css. + LightBackground string `json:"lightBackground"` // specify a valid CSS color property value to use as the background color in light mode + DarkBackground string `json:"darkBackground"` // Specify a valid CSS color property value to use as the background color in dark mode + CustomThemes map[string]CustomTheme `json:"customThemes"` // A list of custom css files that each user can select to override the default styling. if "default" is key name then it will be the default option. + // In-memory (not exposed to config) + CustomThemeOptions map[string]CustomTheme `json:"-"` // not exposed } type CustomTheme struct { Description string `json:"description"` // The description of the theme to display in the UI. CSS string `json:"css,omitempty"` // The css file path and filename to use for the theme. + CssRaw string `json:"-"` // The css raw content to use for the theme. } type ExternalLink struct { @@ -152,8 +159,8 @@ type UserDefaults struct { 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 - DisablePreviewExt string `json:"disablePreviewExt"` // comma separated list of file extensions to disable preview for - DisableViewingExt string `json:"disableViewingExt"` // comma separated list of file extensions to disable viewing for + DisablePreviewExt string `json:"disablePreviewExt"` // space separated list of file extensions to disable preview for + DisableViewingExt string `json:"disableViewingExt"` // space separated list of file extensions to disable viewing for LockPassword bool `json:"lockPassword"` // disable the user from changing their password DisableSettings bool `json:"disableSettings,omitempty"` // disable the user from viewing the settings page Preview users.Preview `json:"preview"` @@ -167,4 +174,5 @@ type UserDefaults struct { DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // list of file extensions to disable onlyoffice editor for CustomTheme string `json:"customTheme"` // Name of theme to use chosen from custom themes config. ShowSelectMultiple bool `json:"showSelectMultiple"` // show select multiple files on desktop + DebugOffice bool `json:"debugOffice"` // debug onlyoffice editor } diff --git a/backend/common/settings/styling.go b/backend/common/settings/styling.go index 1b92bc641..f75d7d952 100644 --- a/backend/common/settings/styling.go +++ b/backend/common/settings/styling.go @@ -74,17 +74,21 @@ func FallbackColor(val, defaultColor string) string { } func addCustomTheme(name, description, cssFilePath string) { + // Store only file path in config (for YAML export) + Config.Frontend.Styling.CustomThemes[name] = CustomTheme{ + Description: description, + CSS: cssFilePath, // Store file path, not content + } + + // Load CSS content for runtime use (frontend consumption) cssContent := "" if cssFilePath != "" { cssContent = readCustomCSS(cssFilePath) } - Config.Frontend.Styling.CustomThemes[name] = CustomTheme{ - Description: description, - CSS: cssContent, - } Config.Frontend.Styling.CustomThemeOptions[name] = CustomTheme{ Description: description, CSS: cssFilePath, + CssRaw: cssContent, // Store loaded content } } diff --git a/backend/common/utils/cache.go b/backend/common/utils/cache.go index e6234b47b..243c6a4c0 100644 --- a/backend/common/utils/cache.go +++ b/backend/common/utils/cache.go @@ -12,4 +12,5 @@ var ( SearchResultsCache = cache.NewCache(15*time.Second, 1*time.Hour) OnlyOfficeCache = cache.NewCache(48*time.Hour, 1*time.Hour) JwtCache = cache.NewCache(1*time.Hour, 72*time.Hour) + MediaCache = cache.NewCache(24 * time.Hour) // subtitles get cached for 24 hours ) diff --git a/backend/common/utils/main.go b/backend/common/utils/main.go index 66f22209f..ec4d67ac1 100644 --- a/backend/common/utils/main.go +++ b/backend/common/utils/main.go @@ -121,3 +121,10 @@ func NonNilSlice[T any](in []T) []T { } return in } + +func Ternary[T any](cond bool, vtrue, vfalse T) T { + if cond { + return vtrue + } + return vfalse +} diff --git a/backend/database/access/access.go b/backend/database/access/access.go index bfabf9619..8c0f6976d 100644 --- a/backend/database/access/access.go +++ b/backend/database/access/access.go @@ -176,6 +176,7 @@ func (s *Storage) DenyUser(sourcePath, indexPath, username string) error { } rule.Deny.Users[username] = struct{}{} s.incrementSourceVersion(sourcePath) + accessCache.Set(accessChangedKey+sourcePath, false) return s.SaveToDB() } @@ -195,6 +196,7 @@ func (s *Storage) AllowUser(sourcePath, indexPath, username string) error { } rule.Allow.Users[username] = struct{}{} s.incrementSourceVersion(sourcePath) + accessCache.Set(accessChangedKey+sourcePath, false) return s.SaveToDB() } @@ -212,6 +214,7 @@ func (s *Storage) DenyGroup(sourcePath, indexPath, groupname string) error { } rule.Deny.Groups[groupname] = struct{}{} s.incrementSourceVersion(sourcePath) + accessCache.Set(accessChangedKey+sourcePath, false) return s.SaveToDB() } @@ -229,6 +232,7 @@ func (s *Storage) AllowGroup(sourcePath, indexPath, groupname string) error { } rule.Allow.Groups[groupname] = struct{}{} s.incrementSourceVersion(sourcePath) + accessCache.Set(accessChangedKey+sourcePath, false) return s.SaveToDB() } @@ -242,6 +246,7 @@ func (s *Storage) DenyAll(sourcePath, indexPath string) error { } rule.DenyAll = true s.incrementSourceVersion(sourcePath) + accessCache.Set(accessChangedKey+sourcePath, false) return s.SaveToDB() } diff --git a/backend/database/share/share.go b/backend/database/share/share.go index 0637e7891..9c1d1e4a4 100644 --- a/backend/database/share/share.go +++ b/backend/database/share/share.go @@ -4,28 +4,31 @@ import "sync" type CommonShare struct { //AllowEdit bool `json:"allowEdit,omitempty"` - AllowUpload bool `json:"allowUpload,omitempty"` - DisableFileViewer bool `json:"disableFileViewer,omitempty"` - DownloadsLimit int `json:"downloadsLimit,omitempty"` - ShareTheme string `json:"shareTheme,omitempty"` - DisableAnonymous bool `json:"disableAnonymous,omitempty"` - MaxBandwidth int `json:"maxBandwidth,omitempty"` - DisableThumbnails bool `json:"disableThumbnails,omitempty"` - KeepAfterExpiration bool `json:"keepAfterExpiration,omitempty"` - AllowedUsernames []string `json:"allowedUsernames,omitempty"` - ThemeColor string `json:"themeColor,omitempty"` - Banner string `json:"banner,omitempty"` - Title string `json:"title,omitempty"` - Description string `json:"description,omitempty"` - Favicon string `json:"favicon,omitempty"` - QuickDownload bool `json:"quickDownload,omitempty"` - HideNavButtons bool `json:"hideNavButtons,omitempty"` - DisableSidebar bool `json:"disableSidebar"` - ViewMode string `json:"viewMode,omitempty"` - Source string `json:"source,omitempty"` // backend source is path to maintain between name changes - Path string `json:"path,omitempty"` - DownloadURL string `json:"downloadURL,omitempty"` - DisableShareCard bool `json:"disableShareCard,omitempty"` + AllowUpload bool `json:"allowUpload,omitempty"` + DisableFileViewer bool `json:"disableFileViewer,omitempty"` + DownloadsLimit int `json:"downloadsLimit,omitempty"` + ShareTheme string `json:"shareTheme,omitempty"` + DisableAnonymous bool `json:"disableAnonymous,omitempty"` + MaxBandwidth int `json:"maxBandwidth,omitempty"` + DisableThumbnails bool `json:"disableThumbnails,omitempty"` + KeepAfterExpiration bool `json:"keepAfterExpiration,omitempty"` + AllowedUsernames []string `json:"allowedUsernames,omitempty"` + ThemeColor string `json:"themeColor,omitempty"` + Banner string `json:"banner,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Favicon string `json:"favicon,omitempty"` + QuickDownload bool `json:"quickDownload,omitempty"` + HideNavButtons bool `json:"hideNavButtons,omitempty"` + DisableSidebar bool `json:"disableSidebar"` + ViewMode string `json:"viewMode,omitempty"` + Source string `json:"source,omitempty"` // backend source is path to maintain between name changes + Path string `json:"path,omitempty"` + DownloadURL string `json:"downloadURL,omitempty"` + DisableShareCard bool `json:"disableShareCard,omitempty"` + EnforceDarkLightMode string `json:"enforceDarkLightMode,omitempty"` // "dark" or "light" + EnableOnlyOffice bool `json:"enableOnlyOffice,omitempty"` + EnableOnlyOfficeEditing bool `json:"enableOnlyOfficeEditing,omitempty"` } type CreateBody struct { CommonShare diff --git a/backend/database/users/users.go b/backend/database/users/users.go index 7657c43fe..080630b4e 100644 --- a/backend/database/users/users.go +++ b/backend/database/users/users.go @@ -49,6 +49,7 @@ type Preview struct { Office bool `json:"office"` // show preview image for office files PopUp bool `json:"popup"` // show larger popup preview when hovering AutoplayMedia bool `json:"autoplayMedia"` // autoplay media files in preview + DefaultMediaPlayer bool `json:"defaultMediaPlayer"` // disable html5 media player and use the default media player } // User describes a user. @@ -99,10 +100,11 @@ type NonAdminEditable struct { FileLoading FileLoading `json:"fileLoading"` // upload and download settings DisableOfficePreviewExt string `json:"disableOfficePreviewExt"` // deprecated DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // deprecated - DisablePreviewExt string `json:"disablePreviewExt"` // comma separated list of file extensions to disable preview for - DisableViewingExt string `json:"disableViewingExt"` // comma separated list of file extensions to disable viewing for + DisablePreviewExt string `json:"disablePreviewExt"` // space separated list of file extensions to disable preview for + DisableViewingExt string `json:"disableViewingExt"` // space separated list of file extensions to disable viewing for CustomTheme string `json:"customTheme"` // Name of theme to use chosen from custom themes config. ShowSelectMultiple bool `json:"showSelectMultiple"` // show select multiple files on desktop + DebugOffice bool `json:"debugOffice"` // debug onlyoffice editor } type FileLoading struct { @@ -110,10 +112,6 @@ type FileLoading struct { ChunkSize int `json:"uploadChunkSizeMb"` } -var AnonymousUser = User{ - Username: "anonymous", // temp user not registered -} - func CleanUsername(s string) string { // Remove any trailing space to avoid ending on - s = strings.Trim(s, " ") diff --git a/backend/events/eventRouter.go b/backend/events/eventRouter.go index 2138fde24..d429436c2 100644 --- a/backend/events/eventRouter.go +++ b/backend/events/eventRouter.go @@ -138,3 +138,14 @@ func handleSourceUpdates() { } } } + +func Shutdown() { + userClientsMu.Lock() + defer userClientsMu.Unlock() + + for _, clientChannels := range userClients { + for _, ch := range clientChannels { + close(ch) + } + } +} diff --git a/backend/ffmpeg/subtitles.go b/backend/ffmpeg/subtitles.go new file mode 100644 index 000000000..07cc00859 --- /dev/null +++ b/backend/ffmpeg/subtitles.go @@ -0,0 +1,289 @@ +package ffmpeg + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/gtsteffaniak/filebrowser/backend/common/utils" + "github.com/gtsteffaniak/go-logger/logger" +) + +// SubtitleTrack represents a subtitle track (embedded or external file) +type SubtitleTrack struct { + Name string `json:"name"` // filename for external, or descriptive name for embedded + Language string `json:"language,omitempty"` // language code + Title string `json:"title,omitempty"` // title/description + Index *int `json:"index,omitempty"` // stream index for embedded subtitles (nil for external) + Codec string `json:"codec,omitempty"` // codec name for embedded subtitles + Content string `json:"content,omitempty"` // subtitle content + IsFile bool `json:"isFile"` // true for external files, false for embedded streams +} + +// FFProbeOutput represents the JSON output from ffprobe +type FFProbeOutput struct { + Streams []struct { + Index int `json:"index"` + CodecType string `json:"codec_type"` + CodecName string `json:"codec_name"` + Tags map[string]string `json:"tags,omitempty"` + Disposition map[string]int `json:"disposition,omitempty"` + } `json:"streams"` +} + +// DetectAllSubtitles finds both embedded and external subtitle tracks +func DetectAllSubtitles(videoPath string, parentDir string, modtime time.Time) []SubtitleTrack { + key := "all_subtitles:" + videoPath + ":" + modtime.Format(time.RFC3339) + + // Check cache first + if cached, ok := utils.MediaCache.Get(key).([]SubtitleTrack); ok { + return cached + } + + var allSubtitles []SubtitleTrack + + // First, get embedded subtitles + embeddedSubs := detectEmbeddedSubtitles(videoPath) + allSubtitles = append(allSubtitles, embeddedSubs...) + + // Then, get external subtitle files + externalSubs := detectExternalSubtitles(videoPath, parentDir) + allSubtitles = append(allSubtitles, externalSubs...) + + // Cache the complete list + utils.MediaCache.Set(key, allSubtitles) + + return allSubtitles +} + +// detectEmbeddedSubtitles uses ffprobe to find embedded subtitle tracks +func detectEmbeddedSubtitles(realPath string) []SubtitleTrack { + cmd := exec.Command("ffprobe", + "-v", "quiet", + "-print_format", "json", + "-show_streams", + "-select_streams", "s", + realPath) + + output, err := cmd.Output() + if err != nil { + logger.Debug("ffprobe failed for file: " + realPath + ", error: " + err.Error()) + return nil + } + + var probeOutput FFProbeOutput + if err := json.Unmarshal(output, &probeOutput); err != nil { + logger.Debug("failed to parse ffprobe output for file: " + realPath) + return nil + } + + var subtitles []SubtitleTrack + for _, stream := range probeOutput.Streams { + if stream.CodecType == "subtitle" { + track := SubtitleTrack{ + Index: &stream.Index, + Codec: stream.CodecName, + IsFile: false, // This is an embedded subtitle + } + + // Extract language and title from tags + if stream.Tags != nil { + if lang, ok := stream.Tags["language"]; ok { + track.Language = lang + } + if title, ok := stream.Tags["title"]; ok { + track.Title = title + } + } + + // Generate a descriptive name + if track.Title != "" { + track.Name = track.Title + } else if track.Language != "" { + track.Name = "Embedded (" + track.Language + ")" + } else { + track.Name = "Embedded Subtitle " + strconv.Itoa(stream.Index) + } + + subtitles = append(subtitles, track) + } + } + return subtitles +} + +// detectExternalSubtitles finds external subtitle files in the same directory +func detectExternalSubtitles(videoPath string, parentDir string) []SubtitleTrack { + var subtitles []SubtitleTrack + + // Get the base name of the video (without extension) + videoBaseName := strings.TrimSuffix(filepath.Base(videoPath), filepath.Ext(videoPath)) + + // Common subtitle extensions + subtitleExts := []string{".srt", ".vtt", ".lrc", ".sbv", ".ass", ".ssa", ".sub", ".smi"} + + // Look for subtitle files with matching base name + for _, ext := range subtitleExts { + subtitlePath := filepath.Join(parentDir, videoBaseName+ext) + if _, err := os.Stat(subtitlePath); err == nil { + // File exists + track := SubtitleTrack{ + Name: filepath.Base(subtitlePath), + IsFile: true, // This is an external file + } + + // Try to infer language from filename patterns like "video.en.srt" + parts := strings.Split(videoBaseName, ".") + if len(parts) > 1 { + // Check if the last part before extension looks like a language code + lastPart := parts[len(parts)-1] + if len(lastPart) == 2 || len(lastPart) == 3 { + track.Language = lastPart + } + } + + subtitles = append(subtitles, track) + } + } + + return subtitles +} + +// ExtractSubtitleContent extracts subtitle content without service management +func ExtractSubtitleContent(videoPath string, streamIndex int) (string, error) { + cmd := exec.Command("ffmpeg", + "-i", videoPath, + "-map", fmt.Sprintf("0:%d", streamIndex), + "-c:s", "webvtt", + "-f", "webvtt", + "-") // output to stdout + + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("subtitle extraction failed: %v", err) + } + + return string(output), nil +} + +// LoadAndConvertSubtitleFile loads a subtitle file and converts it to WebVTT format +func LoadAndConvertSubtitleFile(subtitlePath string) (string, error) { + ext := strings.ToLower(filepath.Ext(subtitlePath)) + + if ext == ".vtt" { + // Already WebVTT, just read it + content, err := os.ReadFile(subtitlePath) + if err != nil { + return "", err + } + return string(content), nil + } + + if ext == ".srt" { + // Convert SRT to WebVTT using ffmpeg + cmd := exec.Command("ffmpeg", + "-i", subtitlePath, + "-c:s", "webvtt", + "-f", "webvtt", + "-") // output to stdout + + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to convert SRT to WebVTT: %v", err) + } + return string(output), nil + } + + // For other formats, try to read as plain text and hope for the best + // This is a fallback - ideally you'd want format-specific converters + content, err := os.ReadFile(subtitlePath) + if err != nil { + return "", err + } + + // Basic conversion wrapper for non-VTT formats + vttHeader := "WEBVTT\n\n" + return vttHeader + string(content), nil +} + +// ExtractSingleSubtitle extracts content for a specific subtitle track by array index +func ExtractSingleSubtitle(videoPath string, parentDir string, trackIndex int, modtime time.Time) (SubtitleTrack, error) { + // Get all subtitle tracks + allTracks := DetectAllSubtitles(videoPath, parentDir, modtime) + + if trackIndex >= len(allTracks) { + return SubtitleTrack{}, fmt.Errorf("subtitle track %d not found (only %d tracks available)", trackIndex, len(allTracks)) + } + + track := allTracks[trackIndex] + + // Load content based on type + if track.IsFile { + // Load external subtitle file + subtitlePath := filepath.Join(parentDir, track.Name) + content, err := LoadAndConvertSubtitleFile(subtitlePath) + if err != nil { + return SubtitleTrack{}, fmt.Errorf("failed to load external subtitle: %v", err) + } + track.Content = content + } else { + // Extract embedded subtitle content + if track.Index == nil { + return SubtitleTrack{}, fmt.Errorf("embedded subtitle track has no stream index") + } + content, err := ExtractSubtitleContent(videoPath, *track.Index) + if err != nil { + return SubtitleTrack{}, fmt.Errorf("failed to extract embedded subtitle: %v", err) + } + track.Content = content + } + + return track, nil +} + +// LoadAllSubtitleContent loads the actual content for all detected subtitle tracks +func LoadAllSubtitleContent(videoPath string, subtitles []SubtitleTrack, modtime time.Time) error { + for idx := range subtitles { + subtitle := &subtitles[idx] + + // Check if content is already cached + contentKey := fmt.Sprintf("subtitle_content:%s:%d:%s", videoPath, idx, modtime.Format(time.RFC3339)) + if cached, ok := utils.MediaCache.Get(contentKey).(string); ok { + subtitle.Content = cached + continue + } + + var content string + var err error + + if subtitle.IsFile { + // Load external subtitle file content and convert to WebVTT + subtitlePath := filepath.Join(filepath.Dir(videoPath), subtitle.Name) + content, err = LoadAndConvertSubtitleFile(subtitlePath) + if err != nil { + logger.Debug("failed to read/convert subtitle file " + subtitlePath + ": " + err.Error()) + continue + } + } else { + // Load embedded subtitle content + if subtitle.Index == nil { + logger.Debug("embedded subtitle has no stream index") + continue + } + content, err = ExtractSubtitleContent(videoPath, *subtitle.Index) + if err != nil { + logger.Debug("failed to extract embedded subtitle: " + err.Error()) + continue + } + } + + subtitle.Content = content + // Cache the content for future requests + utils.MediaCache.Set(contentKey, content) + } + return nil +} diff --git a/backend/ffmpeg/video.go b/backend/ffmpeg/video.go new file mode 100644 index 000000000..4b83da834 --- /dev/null +++ b/backend/ffmpeg/video.go @@ -0,0 +1,147 @@ +package ffmpeg + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/gtsteffaniak/go-logger/logger" +) + +// VideoService handles video preview operations with ffmpeg +type VideoService struct { + ffmpegPath string + ffprobePath string + debug bool + semaphore chan struct{} +} + +// NewVideoService creates a new video service instance +func NewVideoService(ffmpegPath, ffprobePath string, maxConcurrent int, debug bool) *VideoService { + return &VideoService{ + ffmpegPath: ffmpegPath, + ffprobePath: ffprobePath, + debug: debug, + semaphore: make(chan struct{}, maxConcurrent), + } +} + +func (s *VideoService) acquire(ctx context.Context) error { + select { + case s.semaphore <- struct{}{}: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +func (s *VideoService) release() { + <-s.semaphore +} + +// GenerateVideoPreview generates a single preview image from a video using ffmpeg. +// videoPath: path to the input video file. +// outputPath: path where the generated preview image will be saved (e.g., "/tmp/preview.jpg"). +// seekTime: how many seconds into the video to seek before capturing the frame. +func (s *VideoService) GenerateVideoPreview(videoPath, outputPath string, percentageSeek int) ([]byte, error) { + if err := s.acquire(context.Background()); err != nil { + return nil, err + } + defer s.release() + + // Step 1: Get video duration from the container format + probeCmd := exec.Command( + s.ffprobePath, + "-v", "error", + // Use format=duration for better compatibility + "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1", + videoPath, + ) + + var probeOut bytes.Buffer + probeCmd.Stdout = &probeOut + if s.debug { + probeCmd.Stderr = os.Stderr + } + if err := probeCmd.Run(); err != nil { + logger.Errorf("ffprobe command failed on file '%v' : %v", videoPath, err) + return nil, fmt.Errorf("ffprobe failed: %w", err) + } + + durationStr := strings.TrimSpace(probeOut.String()) + if durationStr == "" || durationStr == "N/A" { + logger.Errorf("could not determine video duration for file '%v' using duration info '%v'", videoPath, durationStr) + return nil, fmt.Errorf("could not determine video duration") + } + + durationFloat, err := strconv.ParseFloat(durationStr, 64) + if err != nil { + // The original error you saw would be caught here if "N/A" was still the output + return nil, fmt.Errorf("invalid duration: %v", err) + } + + if durationFloat <= 0 { + return nil, fmt.Errorf("video duration must be positive") + } + + // Step 2: Get the duration of the video in whole seconds + duration := int(durationFloat) + + // Step 3: Calculate seek time based on percentageSeek (percentage value) + seekSeconds := duration * percentageSeek / 100 + + // Step 4: Convert seekSeconds to string for ffmpeg command + seekTime := strconv.Itoa(seekSeconds) + + // Step 5: Extract frame at seek time + cmd := exec.Command( + s.ffmpegPath, + "-ss", seekTime, + "-i", videoPath, + "-frames:v", "1", + "-q:v", "10", + "-y", // overwrite output + outputPath, + ) + + if s.debug { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + err = cmd.Run() + if err != nil { + return nil, fmt.Errorf("ffmpeg command failed on file '%v' : %w", videoPath, err) + } + return os.ReadFile(outputPath) +} + +// GetVideoDuration extracts the duration of a video file using ffprobe +func GetVideoDuration(ffprobePath string, videoPath string) (float64, error) { + cmd := exec.Command(ffprobePath, + "-v", "error", + "-show_entries", "format=duration", + "-of", "default=noprint_wrappers=1:nokey=1", + videoPath) + + output, err := cmd.Output() + if err != nil { + return 0, fmt.Errorf("ffprobe failed: %w", err) + } + + durationStr := strings.TrimSpace(string(output)) + if durationStr == "" || durationStr == "N/A" { + return 0, fmt.Errorf("could not determine video duration") + } + + duration, err := strconv.ParseFloat(durationStr, 64) + if err != nil { + return 0, fmt.Errorf("invalid duration: %v", err) + } + + return duration, nil +} diff --git a/backend/go.mod b/backend/go.mod index f7ee1432b..657ad6206 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -31,27 +31,31 @@ require ( require ( 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect + codeberg.org/chavacava/garif v0.2.0 // indirect dario.cat/mergo v1.0.2 // indirect - github.com/4meepo/tagalign v1.4.2 // indirect - github.com/Abirdcfly/dupword v0.1.3 // indirect + dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect + dev.gaijin.team/go/golib v0.6.0 // indirect + github.com/4meepo/tagalign v1.4.3 // indirect + github.com/Abirdcfly/dupword v0.1.6 // indirect + github.com/AlwxSin/noinlineerr v1.0.5 // indirect github.com/Antonboom/errname v1.1.0 // indirect github.com/Antonboom/nilnil v1.1.0 // indirect github.com/Antonboom/testifylint v1.6.1 // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 // indirect - github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect github.com/air-verse/air v1.62.0 // indirect - github.com/alecthomas/chroma/v2 v2.17.2 // indirect + github.com/alecthomas/chroma/v2 v2.20.0 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect github.com/alexkohler/nakedret/v2 v2.0.6 // indirect github.com/alexkohler/prealloc v1.0.0 // indirect + github.com/alfatraining/structtag v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect github.com/alingse/nilnesserr v0.2.0 // indirect - github.com/ashanbrown/forbidigo v1.6.0 // indirect - github.com/ashanbrown/makezero v1.2.0 // indirect + github.com/ashanbrown/forbidigo/v2 v2.1.0 // indirect + github.com/ashanbrown/makezero/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bep/godartsass/v2 v2.5.0 // indirect @@ -59,13 +63,14 @@ require ( github.com/bkielbasa/cyclop v1.2.3 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/bombsimon/wsl/v4 v4.7.0 // indirect + github.com/bombsimon/wsl/v5 v5.1.1 // indirect github.com/boombuler/barcode v1.1.0 // indirect github.com/breml/bidichk v0.3.3 // indirect github.com/breml/errchkjson v0.4.1 // indirect github.com/butuzov/ireturn v0.4.0 // indirect github.com/butuzov/mirror v1.3.0 // indirect github.com/catenacyber/perfsprint v0.9.1 // indirect - github.com/ccojocar/zxcvbn-go v1.0.2 // indirect + github.com/ccojocar/zxcvbn-go v1.0.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charithe/durationcheck v0.0.10 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect @@ -73,12 +78,11 @@ require ( github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/chavacava/garif v0.1.0 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/creack/pty v1.1.24 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect - github.com/daixiang0/gci v0.13.6 // indirect + github.com/daixiang0/gci v0.13.7 // indirect github.com/dave/dst v0.27.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect @@ -92,7 +96,7 @@ require ( github.com/firefart/nonamedreturns v1.0.6 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/ghostiam/protogetter v0.3.15 // indirect github.com/go-critic/go-critic v0.13.0 // indirect github.com/go-errors/errors v1.5.1 // indirect @@ -110,21 +114,22 @@ require ( github.com/go-toolsmith/astp v1.1.0 // indirect github.com/go-toolsmith/strparse v1.1.0 // indirect github.com/go-toolsmith/typep v1.1.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 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/gohugoio/hugo v0.147.6 // indirect - github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac // indirect + github.com/golang/geo v0.0.0-20250825151631-54d70cc7cb31 // 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 github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect - github.com/golangci/golangci-lint/v2 v2.1.6 // indirect + github.com/golangci/golangci-lint/v2 v2.4.0 // indirect github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 // indirect - github.com/golangci/misspell v0.6.0 // indirect - github.com/golangci/plugin-module-register v0.1.1 // indirect + github.com/golangci/misspell v0.7.0 // indirect + github.com/golangci/plugin-module-register v0.1.2 // indirect github.com/golangci/revgrep v0.8.0 // indirect + github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect github.com/gordonklaus/ineffassign v0.1.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect @@ -137,9 +142,9 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jgautheron/goconst v1.8.1 // indirect + github.com/jgautheron/goconst v1.8.2 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect - github.com/jjti/go-spancheck v0.6.4 // indirect + github.com/jjti/go-spancheck v0.6.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/julz/importas v0.2.0 // indirect github.com/jupiterrider/ffi v0.5.1 // indirect @@ -149,18 +154,19 @@ require ( github.com/kulti/thelper v0.6.3 // indirect github.com/kunwardeep/paralleltest v1.0.14 // indirect github.com/lasiar/canonicalheader v1.1.2 // indirect - github.com/ldez/exptostd v0.4.3 // indirect - github.com/ldez/gomoddirectives v0.6.1 // indirect - github.com/ldez/grignotin v0.9.0 // indirect + github.com/ldez/exptostd v0.4.4 // indirect + github.com/ldez/gomoddirectives v0.7.0 // indirect + github.com/ldez/grignotin v0.10.0 // indirect github.com/ldez/tagliatelle v0.7.1 // indirect - github.com/ldez/usetesting v0.4.3 // indirect + github.com/ldez/usetesting v0.5.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.9.0 // indirect - github.com/manuelarte/funcorder v0.2.1 // indirect + github.com/manuelarte/embeddedstructfieldcheck v0.3.0 // indirect + github.com/manuelarte/funcorder v0.5.0 // indirect github.com/maratori/testableexamples v1.0.0 // indirect github.com/maratori/testpackage v1.1.1 // indirect github.com/matoous/godox v1.1.0 // indirect @@ -168,7 +174,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mgechev/revive v1.9.0 // indirect + github.com/mgechev/revive v1.11.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect github.com/moricho/tparallel v0.3.2 // indirect @@ -176,8 +182,7 @@ require ( github.com/nakabonne/nestif v0.3.1 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.19.1 // indirect - github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/nunnatsa/ginkgolinter v0.20.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -198,18 +203,18 @@ require ( github.com/ryancurrah/gomodguard v1.4.1 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect - github.com/sashamelentyev/usestdlibvars v1.28.0 // indirect - github.com/securego/gosec/v2 v2.22.3 // indirect + github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect + github.com/securego/gosec/v2 v2.22.7 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sivchari/containedctx v1.0.3 // indirect - github.com/sonatard/noctx v0.1.0 // indirect + github.com/sonatard/noctx v0.4.0 // indirect github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/spf13/cast v1.8.0 // indirect github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/pflag v1.0.7 // indirect github.com/spf13/viper v1.12.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect @@ -227,7 +232,7 @@ require ( github.com/ultraware/whitespace v0.2.0 // indirect github.com/urfave/cli/v2 v2.3.0 // indirect github.com/uudashr/gocognit v1.2.0 // indirect - github.com/uudashr/iface v1.3.1 // indirect + github.com/uudashr/iface v1.4.1 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yagipy/maintidx v1.0.0 // indirect @@ -235,20 +240,18 @@ require ( github.com/ykadowak/zerologlint v0.1.5 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.13.1 // indirect - go-simpler.org/sloglint v0.11.0 // indirect + go-simpler.org/sloglint v0.11.1 // indirect + go.augendre.info/arangolint v0.2.0 // indirect go.augendre.info/fatcontext v0.8.0 // indirect go.etcd.io/bbolt v1.4.3 // indirect - go.uber.org/atomic v1.7.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect - 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 + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/tools v0.36.0 // indirect - golang.org/x/tools/go/expect v0.1.1-deprecated // indirect - golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 6438b89b0..6319e6a93 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -34,13 +34,21 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= +codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y= +dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= +dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= +dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E= -github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI= -github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE= -github.com/Abirdcfly/dupword v0.1.3/go.mod h1:8VbB2t7e10KRNdwTVoxdBaxla6avbhGzb8sCTygUMhw= +github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= +github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= +github.com/Abirdcfly/dupword v0.1.6 h1:qeL6u0442RPRe3mcaLcbaCi2/Y/hOcdtw6DE9odjz9c= +github.com/Abirdcfly/dupword v0.1.6/go.mod h1:s+BFMuL/I4YSiFv29snqyjwzDp4b65W2Kvy+PKzZ6cw= +github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY= +github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc= github.com/Antonboom/errname v1.1.0 h1:A+ucvdpMwlo/myWrkHEUEBWc/xuXdud23S8tmTb/oAE= github.com/Antonboom/errname v1.1.0/go.mod h1:O1NMrzgUcVBGIfi3xlVuvX8Q/VP/73sseCaAppfjqZw= github.com/Antonboom/nilnil v1.1.0 h1:jGxJxjgYS3VUUtOTNk8Z1icwT5ESpLH/426fjmQG+ng= @@ -57,8 +65,6 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24 h1:sHglBQTwgx+rWPdisA5ynNEsoARbiCBOyGcJM4/OzsM= github.com/Djarvur/go-err113 v0.0.0-20210108212216-aea10b59be24/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1 h1:Sz1JIXEcSfhz7fUi7xHnhpIE0thVASYjvosApmHuD2k= -github.com/GaijinEntertainment/go-exhaustruct/v3 v3.3.1/go.mod h1:n/LSCXNuIYqVfBlVXyHfMQkZDdp1/mmxfSjADd3z1Zg= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= @@ -71,12 +77,12 @@ github.com/air-verse/air v1.62.0 h1:6CoXL4MAX9dc4xAzLfjMcDfbBoGmW5VjuuTV/1+bI+M= github.com/air-verse/air v1.62.0/go.mod h1:EO+jWuetL10tS9raffwg8WEV0t0KUeucRRaf9ii86dA= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI= -github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= +github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw= +github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg= +github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -86,6 +92,8 @@ github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQ github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q= github.com/alexkohler/prealloc v1.0.0 h1:Hbq0/3fJPQhNkN0dR95AVrr6R7tou91y0uHG5pOcUuw= github.com/alexkohler/prealloc v1.0.0/go.mod h1:VetnK3dIgFBBKmg0YnD9F9x6Icjd+9cvfHR56wJVlKE= +github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc= +github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus= github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w= @@ -94,14 +102,12 @@ github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtS github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac= github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0= -github.com/ashanbrown/forbidigo v1.6.0 h1:D3aewfM37Yb3pxHujIPSpTf6oQk9sc9WZi8gerOIVIY= -github.com/ashanbrown/forbidigo v1.6.0/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1MUyfOKL+AjcU= -github.com/ashanbrown/makezero v1.2.0 h1:/2Lp1bypdmK9wDIq7uWBlDF1iMUpIIS4A+pF6C9IEUU= -github.com/ashanbrown/makezero v1.2.0/go.mod h1:dxlPhHbDMC6N6xICzFBSK+4njQDdK8euNO0qjQMtGY4= +github.com/ashanbrown/forbidigo/v2 v2.1.0 h1:NAxZrWqNUQiDz19FKScQ/xvwzmij6BiOw3S0+QUQ+Hs= +github.com/ashanbrown/forbidigo/v2 v2.1.0/go.mod h1:0zZfdNAuZIL7rSComLGthgc/9/n2FqspBOH90xlCHdA= +github.com/ashanbrown/makezero/v2 v2.0.1 h1:r8GtKetWOgoJ4sLyUx97UTwyt2dO7WkGFHizn/Lo8TY= +github.com/ashanbrown/makezero/v2 v2.0.1/go.mod h1:kKU4IMxmYW1M4fiEHMb2vc5SFoPzXvgbMR9gIp5pjSw= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -138,6 +144,8 @@ github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ= github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg= +github.com/bombsimon/wsl/v5 v5.1.1 h1:cQg5KJf9FlctAH4cpL9vLKnziYknoCMCdqXl0wjl72Q= +github.com/bombsimon/wsl/v5 v5.1.1/go.mod h1:Gp8lD04z27wm3FANIUPZycXp+8huVsn0oxc+n4qfV9I= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -151,8 +159,8 @@ github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= github.com/catenacyber/perfsprint v0.9.1 h1:5LlTp4RwTooQjJCvGEFV6XksZvWE7wCOUvjD2z0vls0= github.com/catenacyber/perfsprint v0.9.1/go.mod h1:q//VWC2fWbcdSLEY1R3l8n0zQCDPdE4IjZwyY1HMunM= -github.com/ccojocar/zxcvbn-go v1.0.2 h1:na/czXU8RrhXO4EZme6eQJLR4PzcGsahsBOAwU6I3Vg= -github.com/ccojocar/zxcvbn-go v1.0.2/go.mod h1:g1qkXtUSvHP8lhHp5GrSmTz6uWALGRMQdw6Qnz/hi60= +github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= +github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -170,8 +178,6 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= -github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -190,8 +196,8 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= -github.com/daixiang0/gci v0.13.6 h1:RKuEOSkGpSadkGbvZ6hJ4ddItT3cVZ9Vn9Rybk6xjl8= -github.com/daixiang0/gci v0.13.6/go.mod h1:12etP2OniiIdP4q+kjUGrC/rUagga7ODbqsom5Eo5Yk= +github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= +github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ= github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= @@ -245,8 +251,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= -github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= +github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo= github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo= github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk= @@ -274,8 +280,8 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA= github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= @@ -316,8 +322,8 @@ github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQi github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= @@ -350,8 +356,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-20250821133510-ecfc33a939ac h1:gBtvd74wZT02fkOyoG651Sx2imv5lKdVvDwpUjKPFnw= -github.com/golang/geo v0.0.0-20250821133510-ecfc33a939ac/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw= +github.com/golang/geo v0.0.0-20250825151631-54d70cc7cb31 h1:226lwSa0uZO7sd7QU88n43nDIqGoc+bOh0vSO3Q/byU= +github.com/golang/geo v0.0.0-20250825151631-54d70cc7cb31/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= @@ -389,16 +395,19 @@ github.com/golangci/go-printf-func-name v0.1.0 h1:dVokQP+NMTO7jwO4bwsRwLWeudOVUP github.com/golangci/go-printf-func-name v0.1.0/go.mod h1:wqhWFH5mUdJQhweRnldEywnR5021wTdZSNgwYceV14s= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= -github.com/golangci/golangci-lint/v2 v2.1.6 h1:LXqShFfAGM5BDzEOWD2SL1IzJAgUOqES/HRBsfKjI+w= -github.com/golangci/golangci-lint/v2 v2.1.6/go.mod h1:EPj+fgv4TeeBq3TcqaKZb3vkiV5dP4hHHKhXhEhzci8= +github.com/golangci/golangci-lint v1.64.8 h1:y5TdeVidMtBGG32zgSC7ZXTFNHrsJkDnpO4ItB3Am+I= +github.com/golangci/golangci-lint/v2 v2.4.0 h1:qz6O6vr7kVzXJqyvHjHSz5fA3D+PM8v96QU5gxZCNWM= +github.com/golangci/golangci-lint/v2 v2.4.0/go.mod h1:Oq7vuAf6L1iNL34uHDcsIF6Mnc0amOPdsT3/GlpHD+I= github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 h1:AkK+w9FZBXlU/xUmBtSJN1+tAI4FIvy5WtnUnY8e4p8= github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95/go.mod h1:k9mmcyWKSTMcPPvQUCfRWWQ9VHJ1U9Dc0R7kaXAgtnQ= -github.com/golangci/misspell v0.6.0 h1:JCle2HUTNWirNlDIAUO44hUsKhOFqGPoC4LZxlaSXDs= -github.com/golangci/misspell v0.6.0/go.mod h1:keMNyY6R9isGaSAu+4Q8NMBwMPkh15Gtc8UCVoDtAWo= -github.com/golangci/plugin-module-register v0.1.1 h1:TCmesur25LnyJkpsVrupv1Cdzo+2f7zX0H6Jkw1Ol6c= -github.com/golangci/plugin-module-register v0.1.1/go.mod h1:TTpqoB6KkwOJMV8u7+NyXMrkwwESJLOkfl9TxR1DGFc= +github.com/golangci/misspell v0.7.0 h1:4GOHr/T1lTW0hhR4tgaaV1WS/lJ+ncvYCoFKmqJsj0c= +github.com/golangci/misspell v0.7.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg= +github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= +github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= +github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM= +github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -413,7 +422,6 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -427,8 +435,8 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18= +github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -475,12 +483,12 @@ github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/jgautheron/goconst v1.8.1 h1:PPqCYp3K/xlOj5JmIe6O1Mj6r1DbkdbLtR3AJuZo414= -github.com/jgautheron/goconst v1.8.1/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako= +github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4= +github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= -github.com/jjti/go-spancheck v0.6.4 h1:Tl7gQpYf4/TMU7AT84MN83/6PutY21Nb9fuQjFTpRRc= -github.com/jjti/go-spancheck v0.6.4/go.mod h1:yAEYdKJ2lRkDA8g7X+oKUHXOWVAXSBJRv04OhF+QUjk= +github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= +github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -523,16 +531,16 @@ github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= -github.com/ldez/exptostd v0.4.3 h1:Ag1aGiq2epGePuRJhez2mzOpZ8sI9Gimcb4Sb3+pk9Y= -github.com/ldez/exptostd v0.4.3/go.mod h1:iZBRYaUmcW5jwCR3KROEZ1KivQQp6PHXbDPk9hqJKCQ= -github.com/ldez/gomoddirectives v0.6.1 h1:Z+PxGAY+217f/bSGjNZr/b2KTXcyYLgiWI6geMBN2Qc= -github.com/ldez/gomoddirectives v0.6.1/go.mod h1:cVBiu3AHR9V31em9u2kwfMKD43ayN5/XDgr+cdaFaKs= -github.com/ldez/grignotin v0.9.0 h1:MgOEmjZIVNn6p5wPaGp/0OKWyvq42KnzAt/DAb8O4Ow= -github.com/ldez/grignotin v0.9.0/go.mod h1:uaVTr0SoZ1KBii33c47O1M8Jp3OP3YDwhZCmzT9GHEk= +github.com/ldez/exptostd v0.4.4 h1:58AtQjnLcT/tI5W/1KU7xE/O7zW9RAWB6c/ScQAnfus= +github.com/ldez/exptostd v0.4.4/go.mod h1:QfdzPw6oHjFVdNV7ILoPu5sw3OZ3OG1JS0I5JN3J4Js= +github.com/ldez/gomoddirectives v0.7.0 h1:EOx8Dd56BZYSez11LVgdj025lKwlP0/E5OLSl9HDwsY= +github.com/ldez/gomoddirectives v0.7.0/go.mod h1:wR4v8MN9J8kcwvrkzrx6sC9xe9Cp68gWYCsda5xvyGc= +github.com/ldez/grignotin v0.10.0 h1:NQPeh1E/Eza4F0exCeC1WkpnLvgUcQDT8MQ1vOLML0E= +github.com/ldez/grignotin v0.10.0/go.mod h1:oR4iCKUP9fwoeO6vCQeD7M5SMxCT6xdVas4vg0h1LaI= github.com/ldez/tagliatelle v0.7.1 h1:bTgKjjc2sQcsgPiT902+aadvMjCeMHrY7ly2XKFORIk= github.com/ldez/tagliatelle v0.7.1/go.mod h1:3zjxUpsNB2aEZScWiZTHrAXOl1x25t3cRmzfK1mlo2I= -github.com/ldez/usetesting v0.4.3 h1:pJpN0x3fMupdTf/IapYjnkhiY1nSTN+pox1/GyBRw3k= -github.com/ldez/usetesting v0.4.3/go.mod h1:eEs46T3PpQ+9RgN9VjpY6qWdiw2/QmfiDeWmdZdrjIQ= +github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc= +github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= @@ -547,8 +555,10 @@ github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4 github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE= github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc= -github.com/manuelarte/funcorder v0.2.1 h1:7QJsw3qhljoZ5rH0xapIvjw31EcQeFbF31/7kQ/xS34= -github.com/manuelarte/funcorder v0.2.1/go.mod h1:BQQ0yW57+PF9ZpjpeJDKOffEsQbxDFKW8F8zSMe/Zd0= +github.com/manuelarte/embeddedstructfieldcheck v0.3.0 h1:VhGqK8gANDvFYDxQkjPbv7/gDJtsGU9k6qj/hC2hgso= +github.com/manuelarte/embeddedstructfieldcheck v0.3.0/go.mod h1:LSo/IQpPfx1dXMcX4ibZCYA7Yy6ayZHIaOGM70+1Wy8= +github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8= +github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA= github.com/maratori/testableexamples v1.0.0 h1:dU5alXRrD8WKSjOUnmJZuzdxWOEQ57+7s93SLMxb2vI= github.com/maratori/testableexamples v1.0.0/go.mod h1:4rhjL1n20TUTT4vdh3RDqSizKLyXp7K2u6HgraZCGzE= github.com/maratori/testpackage v1.1.1 h1:S58XVV5AD7HADMmD0fNnziNHqKvSdDuEKdPD1rNTU04= @@ -563,13 +573,12 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgechev/revive v1.9.0 h1:8LaA62XIKrb8lM6VsBSQ92slt/o92z5+hTw3CmrvSrM= -github.com/mgechev/revive v1.9.0/go.mod h1:LAPq3+MgOf7GcL5PlWIkHb0PT7XH4NuC2LdWymhb9Mo= +github.com/mgechev/revive v1.11.0 h1:b/gLLpBE427o+Xmd8G58gSA+KtBwxWinH/A565Awh0w= +github.com/mgechev/revive v1.11.0/go.mod h1:tI0oLF/2uj+InHCBLrrqfTKfjtFTBCFFfG05auyzgdw= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= @@ -597,18 +606,18 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.19.1 h1:mjwbOlDQxZi9Cal+KfbEJTCz327OLNfwNvoZ70NJ+c4= -github.com/nunnatsa/ginkgolinter v0.19.1/go.mod h1:jkQ3naZDmxaZMXPWaS9rblH+i+GWXQCaS/JFIWcOH2s= +github.com/nunnatsa/ginkgolinter v0.20.0 h1:OmWLkAFO2HUTYcU6mprnKud1Ey5pVdiVNYGO5HVicx8= +github.com/nunnatsa/ginkgolinter v0.20.0/go.mod h1:dCIuFlTPfQerXgGUju3VygfAFPdC5aE1mdacCDKDJcQ= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= -github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= -github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= -github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= +github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= @@ -685,14 +694,14 @@ github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9f github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= -github.com/sashamelentyev/usestdlibvars v1.28.0 h1:jZnudE2zKCtYlGzLVreNp5pmCdOxXUzwsMDBkR21cyQ= -github.com/sashamelentyev/usestdlibvars v1.28.0/go.mod h1:9nl0jgOfHKWNFS43Ojw0i7aRoS4j6EBye3YBhmAIRF8= -github.com/securego/gosec/v2 v2.22.3 h1:mRrCNmRF2NgZp4RJ8oJ6yPJ7G4x6OCiAXHd8x4trLRc= -github.com/securego/gosec/v2 v2.22.3/go.mod h1:42M9Xs0v1WseinaB/BmNGO8AVqG8vRfhC2686ACY48k= +github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ= +github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= +github.com/securego/gosec/v2 v2.22.7 h1:8/9P+oTYI4yIpAzccQKVsg1/90Po+JzGtAhqoHImDeM= +github.com/securego/gosec/v2 v2.22.7/go.mod h1:510TFNDMrIPytokyHQAVLvPeDr41Yihn2ak8P+XQfNE= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= @@ -705,8 +714,8 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= -github.com/sonatard/noctx v0.1.0 h1:JjqOc2WN16ISWAjAk8M5ej0RfExEXtkEyExl2hLW+OM= -github.com/sonatard/noctx v0.1.0/go.mod h1:0RvBxqY8D4j9cTTTWE8ylt2vqj2EPI8fHmrxHdsaZ2c= +github.com/sonatard/noctx v0.4.0 h1:7MC/5Gg4SQ4lhLYR6mvOP6mQVSxCrdyiExo7atBs27o= +github.com/sonatard/noctx v0.4.0/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas= github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= @@ -718,8 +727,9 @@ github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wx github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= @@ -728,18 +738,12 @@ github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8B github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= @@ -782,8 +786,8 @@ github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA= github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= -github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U= -github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= +github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU= +github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= @@ -813,8 +817,10 @@ go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= go-simpler.org/musttag v0.13.1 h1:lw2sJyu7S1X8lc8zWUAdH42y+afdcCnHhWpnkWvd6vU= go-simpler.org/musttag v0.13.1/go.mod h1:8r450ehpMLQgvpb6sg+hV5Ur47eH6olp/3yEanfG97k= -go-simpler.org/sloglint v0.11.0 h1:JlR1X4jkbeaffiyjLtymeqmGDKBDO1ikC6rjiuFAOco= -go-simpler.org/sloglint v0.11.0/go.mod h1:CFDO8R1i77dlciGfPEPvYke2ZMx4eyGiEIWkyeW2Pvw= +go-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s= +go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ= +go.augendre.info/arangolint v0.2.0 h1:2NP/XudpPmfBhQKX4rMk+zDYIj//qbt4hfZmSSTcpj8= +go.augendre.info/arangolint v0.2.0/go.mod h1:Vx4KSJwu48tkE+8uxuf0cbBnAPgnt8O1KWiT7bljq7w= go.augendre.info/fatcontext v0.8.0 h1:2dfk6CQbDGeu1YocF59Za5Pia7ULeAM6friJ3LP7lmk= go.augendre.info/fatcontext v0.8.0/go.mod h1:oVJfMgwngMsHO+KB2MdgzcO+RvtNdiCEOlWvSFtax/s= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= @@ -825,16 +831,14 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -860,8 +864,8 @@ golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjG golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4= -golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b h1:KdrhdYPDUvJTvrDK9gdjfFd6JTk8vA1WJoldYSi0kHo= +golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ= 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.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4= @@ -888,9 +892,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= @@ -932,10 +934,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -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= @@ -1014,7 +1014,6 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1024,9 +1023,7 @@ golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.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= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= @@ -1037,9 +1034,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= @@ -1100,9 +1095,7 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= diff --git a/backend/http/httpEvents.go b/backend/http/httpEvents.go index 4416c556f..c5328eb50 100644 --- a/backend/http/httpEvents.go +++ b/backend/http/httpEvents.go @@ -71,7 +71,11 @@ func sseHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, return http.StatusInternalServerError, fmt.Errorf("error sending broadcast: %v, user: %s", err, username) } - case msg := <-sendChan: + case msg, ok := <-sendChan: + if !ok { + logger.Debugf("SSE channel closed for user: %s, SessionId: %s", username, sessionId) + return http.StatusOK, nil + } if err := msgr.sendEvent(msg.EventType, msg.Message); err != nil { return http.StatusInternalServerError, fmt.Errorf("error sending targeted message: %v, user: %s", err, username) } diff --git a/backend/http/httpRouter.go b/backend/http/httpRouter.go index 0fed5aff4..35ccf1ea1 100644 --- a/backend/http/httpRouter.go +++ b/backend/http/httpRouter.go @@ -15,6 +15,7 @@ import ( "github.com/gtsteffaniak/filebrowser/backend/common/settings" "github.com/gtsteffaniak/filebrowser/backend/common/version" "github.com/gtsteffaniak/filebrowser/backend/database/storage/bolt" + "github.com/gtsteffaniak/filebrowser/backend/events" "github.com/gtsteffaniak/go-logger/logger" // http-swagger middleware ) @@ -116,6 +117,7 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch api.HandleFunc("PATCH /resources", withUser(resourcePatchHandler)) api.HandleFunc("GET /raw", withUser(rawHandler)) api.HandleFunc("GET /preview", withUser(previewHandler)) + api.HandleFunc("GET /media/subtitles", withUser(subtitlesHandler)) if version.Version == "testing" || version.Version == "untracked" { api.HandleFunc("GET /inspectIndex", inspectIndex) api.HandleFunc("GET /mockData", mockData) @@ -127,22 +129,28 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch api.HandleFunc("GET /access/groups", withAdmin(groupGetHandler)) api.HandleFunc("POST /access/group", withAdmin(groupPostHandler)) api.HandleFunc("DELETE /access/group", withAdmin(groupDeleteHandler)) + + // Share management routes (require permission) + api.HandleFunc("GET /shares", withPermShare(shareListHandler)) + api.HandleFunc("GET /share/direct", withPermShare(shareDirectDownloadHandler)) + api.HandleFunc("GET /share", withPermShare(shareGetHandler)) + api.HandleFunc("POST /share", withPermShare(sharePostHandler)) + api.HandleFunc("DELETE /share", withPermShare(shareDeleteHandler)) + // Create API sub-router for public API endpoints publicAPI := http.NewServeMux() // NEW PUBLIC ROUTES - All publicly accessible endpoints - // Share management routes (require permission but are publicly accessible) - publicRoutes.HandleFunc("GET /shares", withPermShare(shareListHandler)) - publicRoutes.HandleFunc("GET /share/direct", withPermShare(shareDirectDownloadHandler)) - publicRoutes.HandleFunc("GET /share", withPermShare(shareGetHandler)) - publicRoutes.HandleFunc("POST /share", withPermShare(sharePostHandler)) - publicRoutes.HandleFunc("DELETE /share", withPermShare(shareDeleteHandler)) // Public API routes (hash-based authentication) publicAPI.HandleFunc("GET /raw", withHashFile(publicRawHandler)) publicAPI.HandleFunc("GET /preview", withHashFile(publicPreviewHandler)) publicAPI.HandleFunc("GET /resources", withHashFile(publicShareHandler)) publicAPI.HandleFunc("GET /users", withUser(userGetHandler)) + publicAPI.HandleFunc("POST /onlyoffice/callback", withHashFile(onlyofficeCallbackHandler)) + publicAPI.HandleFunc("GET /onlyoffice/config", withHashFile(onlyofficeClientConfigGetHandler)) + // Settings routes api.HandleFunc("GET /settings", withAdmin(settingsGetHandler)) + api.HandleFunc("GET /settings/config", withAdmin(settingsConfigHandler)) // Events routes api.HandleFunc("GET /events", withUser(func(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { @@ -153,63 +161,9 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch api.HandleFunc("GET /jobs/{action}/{target}", withUser(getJobsHandler)) api.HandleFunc("GET /onlyoffice/config", withUser(onlyofficeClientConfigGetHandler)) - publicAPI.HandleFunc("GET /onlyoffice/config", withHashFile(onlyofficeClientConfigGetHandler)) api.HandleFunc("POST /onlyoffice/callback", withUser(onlyofficeCallbackHandler)) - publicAPI.HandleFunc("POST /onlyoffice/callback", withHashFile(onlyofficeCallbackHandler)) - api.HandleFunc("GET /onlyoffice/getToken", withUser(onlyofficeGetTokenHandler)) - publicAPI.HandleFunc("GET /onlyoffice/getToken", withHashFile(onlyofficeGetTokenHandler)) api.HandleFunc("GET /search", withUser(searchHandler)) - - // Share routes (DEPRECATED - maintain for backwards compatibility) - // These will redirect to the new /public/shares endpoints - api.HandleFunc("GET /shares", func(w http.ResponseWriter, r *http.Request) { - newURL := config.Server.BaseURL + "public/shares" - if r.URL.RawQuery != "" { - newURL += "?" + r.URL.RawQuery - } - http.Redirect(w, r, newURL, http.StatusMovedPermanently) - }) - api.HandleFunc("GET /share", func(w http.ResponseWriter, r *http.Request) { - newURL := config.Server.BaseURL + "public/share" - if r.URL.RawQuery != "" { - newURL += "?" + r.URL.RawQuery - } - http.Redirect(w, r, newURL, http.StatusMovedPermanently) - }) - api.HandleFunc("POST /share", func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, config.Server.BaseURL+"public/share", http.StatusPermanentRedirect) - }) - api.HandleFunc("DELETE /share", func(w http.ResponseWriter, r *http.Request) { - newURL := config.Server.BaseURL + "public/share" - if r.URL.RawQuery != "" { - newURL += "?" + r.URL.RawQuery - } - http.Redirect(w, r, newURL, http.StatusPermanentRedirect) - }) - - api.HandleFunc("GET /public/raw", func(w http.ResponseWriter, r *http.Request) { - newURL := config.Server.BaseURL + "public/api/raw" - if r.URL.RawQuery != "" { - newURL += "?" + r.URL.RawQuery - } - http.Redirect(w, r, newURL, http.StatusMovedPermanently) - }) - api.HandleFunc("GET /public/share", func(w http.ResponseWriter, r *http.Request) { - newURL := config.Server.BaseURL + "public/api/shared" - if r.URL.RawQuery != "" { - newURL += "?" + r.URL.RawQuery - } - http.Redirect(w, r, newURL, http.StatusMovedPermanently) - }) - api.HandleFunc("GET /public/preview", func(w http.ResponseWriter, r *http.Request) { - newURL := config.Server.BaseURL + "public/api/preview" - if r.URL.RawQuery != "" { - newURL += "?" + r.URL.RawQuery - } - http.Redirect(w, r, newURL, http.StatusMovedPermanently) - }) - // Mount the public API sub-router publicRoutes.Handle("/api/", http.StripPrefix("/api", publicAPI)) @@ -302,6 +256,9 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch <-ctx.Done() logger.Info("Shutting down HTTP server...") + // Close all SSE sessions + events.Shutdown() + // Persist in-memory state before shutting down the HTTP server if store != nil { if store.Share != nil { diff --git a/backend/http/media.go b/backend/http/media.go new file mode 100644 index 000000000..3ed85c244 --- /dev/null +++ b/backend/http/media.go @@ -0,0 +1,83 @@ +package http + +import ( + "bytes" + "fmt" + "net/http" + "net/url" + "path/filepath" + "strconv" + "time" + + "github.com/gtsteffaniak/filebrowser/backend/common/settings" + "github.com/gtsteffaniak/filebrowser/backend/ffmpeg" + "github.com/gtsteffaniak/filebrowser/backend/indexing" +) + +// subtitlesHandler handles subtitle extraction requests +// @Summary Extract embedded subtitles +// @Description Extracts embedded subtitle content from video files by stream index and returns raw WebVTT content +// @Tags Subtitles +// @Accept json +// @Produce text/vtt +// @Param path query string true "Index path to the video file" +// @Param source query string true "Source name for the desired source" +// @Param index query int false "Stream index for embedded subtitle extraction, defaults to 0" +// @Success 200 {string} string "Raw WebVTT subtitle content" +// @Failure 400 {object} map[string]string "Bad request" +// @Failure 403 {object} map[string]string "Forbidden" +// @Failure 404 {object} map[string]string "Resource not found" +// @Failure 500 {object} map[string]string "Internal server error" +// @Router /api/media/subtitles [get] +func subtitlesHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { + encodedPath := r.URL.Query().Get("path") + source := r.URL.Query().Get("source") + indexParam := r.URL.Query().Get("index") + + if indexParam == "" { + indexParam = "0" // default to first subtitle stream + } + + path, err := url.QueryUnescape(encodedPath) + if err != nil { + return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err) + } + source, err = url.QueryUnescape(source) + if err != nil { + return http.StatusBadRequest, fmt.Errorf("invalid source encoding: %v", err) + } + + userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source) + if err != nil { + return http.StatusForbidden, err + } + + idx := indexing.GetIndex(source) + if idx == nil { + return http.StatusNotFound, fmt.Errorf("source %s not found", source) + } + realPath, _, err := idx.GetRealPath(userscope, path) + if err != nil { + return http.StatusNotFound, fmt.Errorf("file not found: %v", err) + } + metadata, exists := idx.GetMetadataInfo(userscope, true) + if !exists { + return http.StatusNotFound, fmt.Errorf("file not found: %v", err) + } + + index, err := strconv.Atoi(indexParam) + if err != nil { + return http.StatusBadRequest, fmt.Errorf("invalid index parameter: %v", err) + } + parentDir := filepath.Dir(realPath) + subtitle, err := ffmpeg.ExtractSingleSubtitle(realPath, parentDir, index, metadata.ModTime) + if err != nil { + return http.StatusInternalServerError, fmt.Errorf("failed to extract subtitle: %v", err) + } + w.Header().Set("Content-Type", "text/vtt; charset=utf-8") + w.Header().Set("Content-Disposition", "inline") + w.Header().Set("Cache-Control", "private") + http.ServeContent(w, r, fmt.Sprintf("%s-%d.vtt", subtitle.Name, index), + time.Now(), bytes.NewReader([]byte(subtitle.Content))) + return http.StatusOK, nil +} diff --git a/backend/http/middleware.go b/backend/http/middleware.go index 25a39f936..198f9a8b8 100644 --- a/backend/http/middleware.go +++ b/backend/http/middleware.go @@ -15,6 +15,7 @@ import ( "github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files" "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" "github.com/gtsteffaniak/filebrowser/backend/database/share" "github.com/gtsteffaniak/filebrowser/backend/database/users" @@ -90,11 +91,16 @@ func withHashFileHelper(fn handleFunc) handleFunc { Expand: true, Content: getContent, }) - file.Token = link.Token if err != nil { logger.Errorf("error fetching file info for share. hash=%v path=%v error=%v", hash, path, err) return errToStatus(err), fmt.Errorf("error fetching share from server") } + file.Token = link.Token + file.Source = "" + file.Hash = link.Hash + if !link.EnableOnlyOffice || !link.DisableFileViewer { + file.OnlyOfficeId = "" + } if getContent && file.Content != "" { link.Mu.Lock() link.Downloads++ @@ -102,7 +108,7 @@ func withHashFileHelper(fn handleFunc) handleFunc { } file.Path = "/" + strings.TrimPrefix(strings.TrimPrefix(file.Path, link.Path), "/") // Set the file info in the `data` object - data.fileInfo = file + data.fileInfo = *file // Call the next handler with the data return fn(w, r, data) }) @@ -180,7 +186,8 @@ func withOrWithoutUserHelper(fn handleFunc) handleFunc { } // Only fall back to anonymous if authentication actually failed if status == http.StatusUnauthorized || status == http.StatusForbidden { - data.user = &users.AnonymousUser + data.user = &users.User{Username: "anonymous"} + settings.ApplyUserDefaults(data.user) // If user authentication failed, call the handler without user context // Clear any user data that might have been partially set data.token = "" diff --git a/backend/http/middleware_test.go b/backend/http/middleware_test.go index bc0e996e1..332d07682 100644 --- a/backend/http/middleware_test.go +++ b/backend/http/middleware_test.go @@ -43,8 +43,8 @@ func mockFileInfoFaster(t *testing.T) { t.Cleanup(func() { FileInfoFasterFunc = originalFileInfoFaster }) // Mock the function to skip execution - FileInfoFasterFunc = func(opts iteminfo.FileOptions) (iteminfo.ExtendedFileInfo, error) { - return iteminfo.ExtendedFileInfo{ + FileInfoFasterFunc = func(opts iteminfo.FileOptions) (*iteminfo.ExtendedFileInfo, error) { + return &iteminfo.ExtendedFileInfo{ FileInfo: iteminfo.FileInfo{ Path: opts.Path, ItemInfo: iteminfo.ItemInfo{ diff --git a/backend/http/onlyOffice.go b/backend/http/onlyOffice.go index 648e5e7ce..5b4ab400f 100644 --- a/backend/http/onlyOffice.go +++ b/backend/http/onlyOffice.go @@ -38,68 +38,100 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d if settings.Config.Integrations.OnlyOffice.Url == "" { return http.StatusInternalServerError, errors.New("only-office integration must be configured in settings") } - encodedUrl := r.URL.Query().Get("url") - // Decode the URL-encoded path - givenUrl, err := url.QueryUnescape(encodedUrl) - if err != nil { - return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err) - } - // get path from url - pathParts := strings.Split(givenUrl, "/api/raw?files=") - origPathParts := strings.Split(encodedUrl, "/api/raw?files=") - encodedPath := origPathParts[len(origPathParts)-1] - sourceFile := pathParts[len(pathParts)-1] - sourceSplit := strings.Split(sourceFile, "::") - if len(sourceSplit) != 2 { - return http.StatusBadRequest, fmt.Errorf("invalid url path %v", givenUrl) - } - source := sourceSplit[0] - path := sourceSplit[1] - urlFirst := pathParts[0] - if settings.Config.Server.InternalUrl != "" { - urlFirst = settings.Config.Server.InternalUrl + settings.Config.Server.BaseURL - whatToReplace := strings.Split(givenUrl, "/api/raw")[0] + "/" - givenUrl = strings.Replace(givenUrl, whatToReplace, urlFirst, 1) + + // Extract clean parameters from request + source := r.URL.Query().Get("source") + path := r.URL.Query().Get("path") + + // Validate required parameters + if (path == "" || source == "") && d.fileInfo.Hash == "" { + logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, path) + return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required") } - userscope, err := settings.GetScopeFromSourceName(d.user.Scopes, source) - if err != nil { - return http.StatusForbidden, err - } - fileInfo, err := files.FileInfoFaster(iteminfo.FileOptions{ - Path: utils.JoinPathAsUnix(userscope, path), - Modify: d.user.Permissions.Modify, - Source: source, - Expand: false, - }) - if err != nil { - logger.Debugf("onlyofficeClientConfigGetHandler: failed to get file info for file source %v, path %v: %v", source, path, err) - return errToStatus(err), err + themeMode := utils.Ternary(d.user.DarkMode, "dark", "light") + + if d.fileInfo.Hash == "" { + // Build file info based on whether this is a share or regular request + // Regular user request - need to resolve scope + userScope, scopeErr := settings.GetScopeFromSourceName(d.user.Scopes, source) + if scopeErr != nil { + logger.Errorf("OnlyOffice: source %s not available for user %s: %v", source, d.user.Username, scopeErr) + return http.StatusForbidden, fmt.Errorf("source %s is not available", source) + } + resolvedPath := utils.JoinPathAsUnix(userScope, path) + logger.Debugf("OnlyOffice user request: resolved path=%s", resolvedPath) + + fileInfo, err := files.FileInfoFaster(iteminfo.FileOptions{ + Path: resolvedPath, + Modify: d.user.Permissions.Modify, + Source: source, + Expand: false, + }) + if err != nil { + logger.Errorf("OnlyOffice: failed to get file info for source=%s, path=%s: %v", source, resolvedPath, err) + return errToStatus(err), err + } + d.fileInfo = *fileInfo + } else { + // is a share, use the file info from the share middleware + sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source] + if !ok { + logger.Error("OnlyOffice: source from share not found") + return http.StatusInternalServerError, fmt.Errorf("source not found for share") + } + source = sourceInfo.Name + // path is index path, so we build from share path + path = utils.JoinPathAsUnix(d.share.Path, path) + if d.share.EnforceDarkLightMode == "dark" { + themeMode = "dark" + } + if d.share.EnforceDarkLightMode == "light" { + themeMode = "light" + } + } - id, err := getOnlyOfficeId(source, fileInfo.Path) - if err != nil { - logger.Debugf("getOnlyOfficeId failed for file source %v, path %v: %v", source, fileInfo.Path, err) - return http.StatusNotFound, err + + // Determine file type and editing permissions + fileType := getFileExtension(d.fileInfo.Name) + canEdit := iteminfo.CanEditOnlyOffice(d.user.Permissions.Modify, fileType) + canEditMode := utils.Ternary(canEdit, "edit", "view") + if d.fileInfo.Hash != "" { + if d.share.EnableOnlyOfficeEditing { + canEditMode = "edit" + } } - split := strings.Split(fileInfo.Name, ".") - fileType := split[len(split)-1] - theme := "light" - if d.user.DarkMode { - theme = "dark" + // For shares, we need to keep track of the original relative path for the callback URL + var callbackPath string + if d.fileInfo.Hash != "" { + // For shares, use the original path parameter (relative to share) + callbackPath = r.URL.Query().Get("path") + } else { + // For regular requests, use the processed path + callbackPath = path } - canEdit := iteminfo.CanEditOnlyOffice(d.user.Permissions.Modify, fileType) - mode := "view" - if canEdit { - mode = "edit" + + // Generate document ID for OnlyOffice + documentId, err := getOnlyOfficeId(source, path) + if err != nil { + logger.Errorf("OnlyOffice: failed to generate document ID for source=%s, path=%s: %v", source, path, err) + return http.StatusNotFound, fmt.Errorf("failed to generate document ID: %v", err) } - callbackURL := fmt.Sprintf("%v/api/onlyoffice/callback?files=%v&auth=%v", strings.TrimSuffix(urlFirst, "/"), encodedPath, d.token) + + // Build download URL that OnlyOffice server will use + downloadURL := buildOnlyOfficeDownloadURL(source, callbackPath, d.fileInfo.Hash, d.token) + + // Build callback URL for OnlyOffice to notify us of changes + callbackURL := buildOnlyOfficeCallbackURL(source, callbackPath, d.fileInfo.Hash, d.token) + + // Build OnlyOffice client configuration clientConfig := map[string]interface{}{ "document": map[string]interface{}{ "fileType": fileType, - "key": id, - "title": fileInfo.Name, - "url": givenUrl + "&auth=" + d.token, + "key": documentId, + "title": d.fileInfo.Name, + "url": downloadURL, "permissions": map[string]interface{}{ - "edit": canEdit, + "edit": canEditMode, "download": true, "print": true, }, @@ -113,78 +145,194 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d "customization": map[string]interface{}{ "autosave": true, "forcesave": true, - "uiTheme": theme, + "uiTheme": themeMode, }, "lang": d.user.Locale, - "mode": mode, + "mode": canEditMode, }, } + + // Sign configuration with JWT if secret is configured if settings.Config.Integrations.OnlyOffice.Secret != "" { token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(clientConfig)) signature, err := token.SignedString([]byte(settings.Config.Integrations.OnlyOffice.Secret)) if err != nil { - return http.StatusInternalServerError, fmt.Errorf("failed to sign JWT") + logger.Errorf("OnlyOffice: failed to sign JWT: %v", err) + return http.StatusInternalServerError, fmt.Errorf("failed to sign configuration") } clientConfig["token"] = signature } + return renderJSON(w, r, clientConfig) } +// getFileExtension extracts file extension from filename +func getFileExtension(filename string) string { + parts := strings.Split(filename, ".") + if len(parts) < 2 { + return "" + } + return parts[len(parts)-1] +} + +// buildOnlyOfficeDownloadURL constructs the download URL that OnlyOffice server will use to fetch the file +func buildOnlyOfficeDownloadURL(source, path, hash, token string) string { + // Determine base URL (internal URL takes priority for OnlyOffice server communication) + baseURL := settings.Config.Server.BaseURL + if settings.Config.Server.InternalUrl != "" { + baseURL = settings.Config.Server.InternalUrl + settings.Config.Server.BaseURL + } + + var downloadURL string + if hash != "" { + // Share download URL - don't expose source name, just use the path relative to share + filesParam := url.QueryEscape(path) + downloadURL = fmt.Sprintf("%s/public/api/raw?files=%s&hash=%s&token=%s&auth=%s", + strings.TrimSuffix(baseURL, "/"), filesParam, hash, token, token) + } else { + // Regular download URL - include source for non-share requests + filesParam := url.QueryEscape(source + "::" + path) + downloadURL = fmt.Sprintf("%s/api/raw?files=%s&auth=%s", + strings.TrimSuffix(baseURL, "/"), filesParam, token) + } + + return downloadURL +} + +// buildOnlyOfficeCallbackURL constructs the callback URL that OnlyOffice server will use to notify us of changes +func buildOnlyOfficeCallbackURL(source, path, hash, token string) string { + baseURL := settings.Config.Server.BaseURL + if settings.Config.Server.InternalUrl != "" { + baseURL = settings.Config.Server.InternalUrl + settings.Config.Server.BaseURL + } + + var callbackURL string + if hash != "" { + // Share callback URL - use public API and don't expose source, use path relative to share + params := url.Values{} + params.Set("hash", hash) + params.Set("path", path) // This should be the path relative to the share, not the full filesystem path + params.Set("auth", token) + + callbackURL = fmt.Sprintf("%s/public/api/onlyoffice/callback?%s", + strings.TrimSuffix(baseURL, "/"), params.Encode()) + } else { + // Regular callback URL - include source for non-share requests + params := url.Values{} + params.Set("source", source) + params.Set("path", path) + params.Set("auth", token) + + callbackURL = fmt.Sprintf("%s/api/onlyoffice/callback?%s", + strings.TrimSuffix(baseURL, "/"), params.Encode()) + } + + return callbackURL +} + func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { + // Parse OnlyOffice callback data body, err := io.ReadAll(r.Body) if err != nil { + logger.Errorf("OnlyOffice callback: failed to read request body: %v", err) return http.StatusInternalServerError, err } + var data OnlyOfficeCallback err = json.Unmarshal(body, &data) if err != nil { + logger.Errorf("OnlyOffice callback: failed to parse JSON: %v", err) return http.StatusInternalServerError, err } - encodedPath := r.URL.Query().Get("files") - pathParts := strings.Split(encodedPath, "::") - if len(pathParts) < 2 { - return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err) + // Extract clean parameters from query string + source := r.URL.Query().Get("source") + path := r.URL.Query().Get("path") + + // Validate required parameters + if (path == "" || source == "") && d.fileInfo.Hash == "" { + logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, path) + return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required") } - source := pathParts[0] - // Decode the URL-encoded path - path, err := url.QueryUnescape(pathParts[1]) - if err != nil { - return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err) + + if d.fileInfo.Hash == "" { + // Regular user request - need to resolve scope + userScope, scopeErr := settings.GetScopeFromSourceName(d.user.Scopes, source) + if scopeErr != nil { + logger.Errorf("OnlyOffice callback: source %s not available for user %s: %v", source, d.user.Username, scopeErr) + return http.StatusForbidden, fmt.Errorf("source %s is not available", source) + } + path = utils.JoinPathAsUnix(userScope, path) + } else { + // is a share, use the file info from the share middleware + sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source] + if !ok { + logger.Error("OnlyOffice: source from share not found") + return http.StatusInternalServerError, fmt.Errorf("source not found for share") + } + source = sourceInfo.Name + // path is index path, so we build from share path + path = utils.JoinPathAsUnix(d.share.Path, path) } + // Handle document closure - clean up document key cache if data.Status == onlyOfficeStatusDocumentClosedWithChanges || data.Status == onlyOfficeStatusDocumentClosedWithNoChanges { - // Refer to only-office documentation + // Refer to OnlyOffice documentation: // - https://api.onlyoffice.com/editors/coedit // - https://api.onlyoffice.com/editors/callback // // When the document is fully closed by all editors, - // then the document key should no longer be re-used. + // the document key should no longer be re-used. + logger.Debugf("OnlyOffice: document closed, cleaning up document ID for source=%s, path=%s", source, path) deleteOfficeId(source, path) } + // Handle document save operations if data.Status == onlyOfficeStatusDocumentClosedWithChanges || data.Status == onlyOfficeStatusForceSaveWhileDocumentStillOpen { + + // Verify user has modify permissions if !d.user.Permissions.Modify { + logger.Warningf("OnlyOffice callback: user %s lacks modify permissions for source=%s, path=%s", + d.user.Username, source, path) return http.StatusForbidden, nil } + // Download the updated document from OnlyOffice server doc, err := http.Get(data.URL) if err != nil { + logger.Errorf("OnlyOffice callback: failed to download updated document: %v", err) return http.StatusInternalServerError, err } defer doc.Body.Close() + // Resolve file path for writing (same logic as in config handler) + var resolvedPath string + if d.fileInfo.Hash == "" { + // Regular user request - need to resolve scope + userScope, scopeErr := settings.GetScopeFromSourceName(d.user.Scopes, source) + if scopeErr != nil { + logger.Errorf("OnlyOffice callback: source %s not available for user %s: %v", + source, d.user.Username, scopeErr) + return http.StatusForbidden, fmt.Errorf("source %s is not available", source) + } + resolvedPath = utils.JoinPathAsUnix(userScope, path) + } + + // Write the updated document fileOpts := iteminfo.FileOptions{ - Path: path, + Path: resolvedPath, Source: source, } writeErr := files.WriteFile(fileOpts, doc.Body) if writeErr != nil { + logger.Errorf("OnlyOffice callback: failed to write updated document: %v", writeErr) return http.StatusInternalServerError, writeErr } + } + // Return success response to OnlyOffice server resp := map[string]int{ "error": 0, } @@ -215,29 +363,3 @@ func deleteOfficeId(source, path string) { realpath, _, _ := idx.GetRealPath(path) utils.OnlyOfficeCache.Delete(realpath) } - -func onlyofficeGetTokenHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { - // get config from body - body, err := io.ReadAll(r.Body) - if err != nil { - return http.StatusInternalServerError, err - } - defer r.Body.Close() - - var payload map[string]interface{} - // marshall to struct - err = json.Unmarshal(body, &payload) - if err != nil { - return http.StatusInternalServerError, err - } - - if settings.Config.Integrations.OnlyOffice.Secret != "" { - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(payload)) - ss, err := token.SignedString([]byte(settings.Config.Integrations.OnlyOffice.Secret)) - if err != nil { - return 500, errors.New("could not generate a new jwt") - } - return renderJSON(w, r, map[string]string{"token": ss}) - } - return 400, fmt.Errorf("bad request") -} diff --git a/backend/http/preview.go b/backend/http/preview.go index 951520b1a..5356f106d 100644 --- a/backend/http/preview.go +++ b/backend/http/preview.go @@ -70,7 +70,7 @@ func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) ( if err != nil { return errToStatus(err), err } - d.fileInfo = fileInfo + d.fileInfo = *fileInfo return previewHelperFunc(w, r, d) } diff --git a/backend/http/public.go b/backend/http/public.go index 5896dc3b6..d3cae21d3 100644 --- a/backend/http/public.go +++ b/backend/http/public.go @@ -7,6 +7,7 @@ import ( "net/url" "strings" + "github.com/gtsteffaniak/filebrowser/backend/common/settings" "github.com/gtsteffaniak/filebrowser/backend/common/utils" "github.com/gtsteffaniak/go-logger/logger" @@ -37,12 +38,19 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) d.share.Mu.Unlock() encodedFiles := r.URL.Query().Get("files") - // Decode the URL-encoded path - f, err := url.QueryUnescape(encodedFiles) + // Decode the URL-encoded path - use PathUnescape to preserve + as literal character + f, err := url.PathUnescape(encodedFiles) if err != nil { return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err) } + // Get the actual source name from the share's source mapping + sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source] + if !ok { + return http.StatusInternalServerError, fmt.Errorf("source not found for share") + } + actualSourceName := sourceInfo.Name + fileList := []string{} for _, file := range strings.Split(f, "||") { // Check if file already contains source prefix (source::path format) @@ -57,12 +65,12 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) } else { // Fallback: treat as plain path filePath := utils.JoinPathAsUnix(d.share.Path, file) - fileList = append(fileList, d.fileInfo.Source+"::"+filePath) + fileList = append(fileList, actualSourceName+"::"+filePath) } } else { - // Plain path without source prefix + // Plain path without source prefix - use the actual source name from share filePath := utils.JoinPathAsUnix(d.share.Path, file) - fileList = append(fileList, d.fileInfo.Source+"::"+filePath) + fileList = append(fileList, actualSourceName+"::"+filePath) } } @@ -76,8 +84,6 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) } func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { - // disable onlyoffice for public share - d.fileInfo.OnlyOfficeId = "" return renderJSON(w, r, d.fileInfo) } diff --git a/backend/http/resource.go b/backend/http/resource.go index 80ab3a142..b2592a250 100644 --- a/backend/http/resource.go +++ b/backend/http/resource.go @@ -176,7 +176,7 @@ func resourceDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestCon } // delete thumbnails - preview.DelThumbs(r.Context(), fileInfo) + preview.DelThumbs(r.Context(), *fileInfo) err = files.DeleteFiles(source, fileInfo.RealPath, filepath.Dir(fileInfo.RealPath)) if err != nil { @@ -269,7 +269,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte } // On the first chunk, check for conflicts or handle override if offset == 0 { - var fileInfo iteminfo.ExtendedFileInfo + var fileInfo *iteminfo.ExtendedFileInfo fileInfo, err = files.FileInfoFaster(fileOpts) if err == nil { // File exists if r.URL.Query().Get("override") != "true" { @@ -278,7 +278,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte return http.StatusConflict, nil } // If overriding, delete existing thumbnails - preview.DelThumbs(r.Context(), fileInfo) + preview.DelThumbs(r.Context(), *fileInfo) } } @@ -341,7 +341,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte return http.StatusConflict, nil } - preview.DelThumbs(r.Context(), fileInfo) + preview.DelThumbs(r.Context(), *fileInfo) } err = files.WriteFile(fileOpts, r.Body) if err != nil { @@ -571,7 +571,7 @@ func patchAction(ctx context.Context, params patchActionParams) error { } // delete thumbnails - preview.DelThumbs(ctx, fileInfo) + preview.DelThumbs(ctx, *fileInfo) return files.MoveResource(params.isSrcDir, params.isDstDir, params.srcIndex, params.dstIndex, params.src, params.dst, store.Share) default: return fmt.Errorf("unsupported action %s: %w", params.action, errors.ErrInvalidRequestParams) diff --git a/backend/http/search.go b/backend/http/search.go index 5e0ae38d2..2c868602c 100644 --- a/backend/http/search.go +++ b/backend/http/search.go @@ -64,6 +64,9 @@ func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i if err != nil { return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err) } + if len(query) < settings.Config.Server.MinSearchLength { + return http.StatusBadRequest, fmt.Errorf("query is too short, minimum length is %d", settings.Config.Server.MinSearchLength) + } searchScope := strings.TrimPrefix(unencodedScope, ".") // Retrieve the User-Agent and X-Auth headers from the request sessionId := r.Header.Get("SessionId") diff --git a/backend/http/settings.go b/backend/http/settings.go index 14fb76f4f..86411b4d4 100644 --- a/backend/http/settings.go +++ b/backend/http/settings.go @@ -2,6 +2,8 @@ package http import ( "net/http" + + "github.com/gtsteffaniak/filebrowser/backend/common/settings" ) // settingsGetHandler retrieves the current system settings. @@ -34,3 +36,36 @@ func settingsGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex } return renderJSON(w, r, config) } + +// settingsConfigHandler returns the current system settings as YAML. +// @Summary Get system settings as YAML +// @Description Returns the current running configuration settings as YAML format with optional comments and filtering. +// @Tags Settings +// @Accept json +// @Produce text/plain +// @Param full query boolean false "Show all values (true) or only non-default values (false, default)" +// @Param comments query boolean false "Include comments in YAML (true) or plain YAML (false, default)" +// @Success 200 {string} string "System settings in YAML format" +// @Router /api/settings/config [get] +func settingsConfigHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { + // Parse query parameters + fullParam := r.URL.Query().Get("full") + commentsParam := r.URL.Query().Get("comments") + + showFull := fullParam == "true" + showComments := commentsParam == "true" + + // Generate YAML using the existing generator function (don't filter deprecated fields in API) + yamlOutput, err := settings.GenerateConfigYaml(config, showComments, showFull, false) + if err != nil { + return http.StatusInternalServerError, err + } + + // Set content type and write response + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + if _, err := w.Write([]byte(yamlOutput)); err != nil { + return http.StatusInternalServerError, err + } + + return http.StatusOK, nil +} diff --git a/backend/http/static.go b/backend/http/static.go index d3e209f6d..4eea654c6 100644 --- a/backend/http/static.go +++ b/backend/http/static.go @@ -48,28 +48,37 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont } userSelectedTheme := "" if d.user != nil { - theme, ok := config.Frontend.Styling.CustomThemes[d.user.CustomTheme] + theme, ok := config.Frontend.Styling.CustomThemeOptions[d.user.CustomTheme] if ok { - userSelectedTheme = theme.CSS + userSelectedTheme = theme.CssRaw } } defaultThemeColor := "#455a64" staticURL := config.Server.BaseURL + "static" publicStaticURL := config.Server.BaseURL + "public/static" + + // Use custom favicon if configured and validated, otherwise fall back to default + var favicon string + if config.Frontend.Favicon != "" { + favicon = staticURL + "/favicon" + } else { + // Default favicon + favicon = staticURL + "/img/icons/favicon-256x256.png" + } data := map[string]interface{}{ "title": config.Frontend.Name, - "customCSS": config.Frontend.Styling.CustomCSS, + "customCSS": config.Frontend.Styling.CustomCSSRaw, "userSelectedTheme": userSelectedTheme, "lightBackground": config.Frontend.Styling.LightBackground, "darkBackground": config.Frontend.Styling.DarkBackground, "staticURL": staticURL, "baseURL": config.Server.BaseURL, - "favicon": staticURL + "/img/icons/favicon-256x256.png", + "favicon": favicon, "color": defaultThemeColor, "winIcon": staticURL + "/img/icons/mstile-144x144.png", "appIcon": staticURL + "/img/icons/android-chrome-256x256.png", - "description": "FileBrowser Quantum is a file manager for the web which can be used to manage files on your server", + "description": config.Frontend.Description, } shareProps := map[string]interface{}{ "isShare": false, @@ -107,6 +116,9 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont shareProps["disableSidebar"] = d.share.DisableSidebar shareProps["isPasswordProtected"] = d.share.PasswordHash != "" shareProps["downloadURL"] = getDownloadURL(r, d.share.Hash) + shareProps["enforceDarkLightMode"] = d.share.EnforceDarkLightMode + shareProps["enableOnlyOffice"] = d.share.EnableOnlyOffice + shareProps["enableOnlyOfficeEditing"] = d.share.EnableOnlyOfficeEditing if d.share.Favicon != "" { if strings.HasPrefix(d.share.Favicon, "http") { data["favicon"] = d.share.Favicon @@ -124,13 +136,19 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont // base url could be different for routes behind proxy data["staticURL"] = publicStaticURL - data["favicon"] = publicStaticURL + "/img/icons/favicon-256x256.png" + // Use custom favicon for shares too if configured + if config.Frontend.Favicon != "" { + data["favicon"] = publicStaticURL + "/favicon" + } else { + data["favicon"] = publicStaticURL + "/img/icons/favicon-256x256.png" + } data["winIcon"] = publicStaticURL + "/img/icons/mstile-144x144.png" data["appIcon"] = publicStaticURL + "/img/icons/android-chrome-256x256.png" } // variables consumed by frontend as json data["globalVars"] = map[string]interface{}{ "name": config.Frontend.Name, + "minSearchLength": config.Server.MinSearchLength, "disableExternal": config.Frontend.DisableDefaultLinks, "darkMode": settings.Config.UserDefaults.DarkMode, "baseURL": config.Server.BaseURL, @@ -171,6 +189,12 @@ func staticFilesHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%v", maxAge)) w.Header().Set("Content-Security-Policy", `default-src 'self'; style-src 'unsafe-inline';`) + // Handle custom favicon if configured and requested + if r.URL.Path == "favicon" && config.Frontend.Favicon != "" { + http.ServeFile(w, r, config.Frontend.Favicon) + return + } + adjustedCompressed := r.URL.Path + ".gz" if strings.HasSuffix(r.URL.Path, ".js") { w.Header().Set("Content-Type", "application/javascript; charset=utf-8") // Set the correct MIME type for JavaScript files diff --git a/backend/indexing/iteminfo/fileinfo.go b/backend/indexing/iteminfo/fileinfo.go index 46dca5cc1..bd8c47795 100644 --- a/backend/indexing/iteminfo/fileinfo.go +++ b/backend/indexing/iteminfo/fileinfo.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gtsteffaniak/filebrowser/backend/database/access" + "github.com/gtsteffaniak/filebrowser/backend/ffmpeg" ) type ItemInfo struct { @@ -28,13 +29,14 @@ type FileInfo struct { // extra calculated fields can be added here type ExtendedFileInfo struct { FileInfo - Content string `json:"content,omitempty"` // text content of a file, if requested - Subtitles []string `json:"subtitles,omitempty"` // subtitles for video files - Checksums map[string]string `json:"checksums,omitempty"` // checksums for the file - Token string `json:"token,omitempty"` // token for the file -- used for sharing - OnlyOfficeId string `json:"onlyOfficeId,omitempty"` // id for onlyoffice files - Source string `json:"source"` // associated index source for the file - RealPath string `json:"-"` + Content string `json:"content,omitempty"` // text content of a file, if requested + Subtitles []ffmpeg.SubtitleTrack `json:"subtitles,omitempty"` // subtitles for video files + Checksums map[string]string `json:"checksums,omitempty"` // checksums for the file + Token string `json:"token,omitempty"` // token for the file -- used for sharing + OnlyOfficeId string `json:"onlyOfficeId,omitempty"` // id for onlyoffice files + Source string `json:"source,omitempty"` // associated index source for the file + Hash string `json:"hash,omitempty"` // hash for the file -- used for sharing + RealPath string `json:"-"` } // FileOptions are the options when getting a file info. diff --git a/backend/indexing/iteminfo/utils.go b/backend/indexing/iteminfo/utils.go index a558c10e3..01a36ef85 100644 --- a/backend/indexing/iteminfo/utils.go +++ b/backend/indexing/iteminfo/utils.go @@ -8,29 +8,43 @@ import ( "strconv" "strings" + "github.com/gtsteffaniak/filebrowser/backend/ffmpeg" "github.com/gtsteffaniak/go-logger/logger" ) +type SubtitleTrack struct { + Name string `json:"name"` // filename for external, or descriptive name for embedded + Language string `json:"language,omitempty"` // language code + Title string `json:"title,omitempty"` // title/description + Index *int `json:"index,omitempty"` // stream index for embedded subtitles (nil for external) + Codec string `json:"codec,omitempty"` // codec name for embedded subtitles + IsFile bool `json:"isFile"` // true for external files, false for embedded +} + +type FFProbeOutput struct { + Streams []struct { + Index int `json:"index"` + CodecType string `json:"codec_type"` + CodecName string `json:"codec_name"` + Tags map[string]string `json:"tags,omitempty"` + Disposition map[string]int `json:"disposition,omitempty"` + } `json:"streams"` +} + // detects subtitles for video files. func (i *ExtendedFileInfo) DetectSubtitles(parentInfo *FileInfo) { if !strings.HasPrefix(i.Type, "video") { logger.Debug("subtitles are not supported for this file : " + i.Name) return } - ext := filepath.Ext(i.Name) - baseWithoutExt := strings.TrimSuffix(i.Name, ext) - for _, f := range parentInfo.Files { - baseName := strings.TrimSuffix(i.Name, ext) - if baseName != baseWithoutExt { - continue - } + // Use unified subtitle detection that finds both embedded and external + parentDir := filepath.Dir(i.RealPath) + i.Subtitles = ffmpeg.DetectAllSubtitles(i.RealPath, parentDir, i.ModTime) +} - for _, subtitleExt := range SubtitleExts { - if strings.HasSuffix(f.Name, subtitleExt) { - i.Subtitles = append(i.Subtitles, f.Name) - } - } - } +// LoadSubtitleContent loads the actual content for all detected subtitle tracks +func (i *ExtendedFileInfo) LoadSubtitleContent() error { + return ffmpeg.LoadAllSubtitleContent(i.RealPath, i.Subtitles, i.ModTime) } func (info *FileInfo) SortItems() { diff --git a/backend/preview/video.go b/backend/preview/video.go index 09adc0ed9..60fb96046 100644 --- a/backend/preview/video.go +++ b/backend/preview/video.go @@ -1,15 +1,9 @@ package preview import ( - "bytes" "context" - "fmt" - "os" - "os/exec" - "strconv" - "strings" - "github.com/gtsteffaniak/go-logger/logger" + "github.com/gtsteffaniak/filebrowser/backend/ffmpeg" ) // GenerateVideoPreview generates a single preview image from a video using ffmpeg. @@ -21,69 +15,8 @@ func (s *Service) GenerateVideoPreview(videoPath, outputPath string, percentageS return nil, err } defer s.release() - // Step 1: Get video duration from the container format - probeCmd := exec.Command( - s.ffprobePath, - "-v", "error", - // Use format=duration for better compatibility - "-show_entries", "format=duration", - "-of", "default=noprint_wrappers=1:nokey=1", - videoPath, - ) - var probeOut bytes.Buffer - probeCmd.Stdout = &probeOut - if s.debug { - probeCmd.Stderr = os.Stderr - } - if err := probeCmd.Run(); err != nil { - logger.Errorf("ffprobe command failed on file '%v' : %v", videoPath, err) - return nil, fmt.Errorf("ffprobe failed: %w", err) - } - - durationStr := strings.TrimSpace(probeOut.String()) - if durationStr == "" || durationStr == "N/A" { - logger.Errorf("could not determine video duration for file '%v' using duration info '%v'", videoPath, durationStr) - return nil, fmt.Errorf("could not determine video duration") - } - - durationFloat, err := strconv.ParseFloat(durationStr, 64) - if err != nil { - // The original error you saw would be caught here if "N/A" was still the output - return nil, fmt.Errorf("invalid duration: %v", err) - } - - if durationFloat <= 0 { - return nil, fmt.Errorf("video duration must be positive") - } - - // The rest of your function remains the same... - // Step 2: Get the duration of the video in whole seconds - duration := int(durationFloat) - - // Step 3: Calculate seek time based on percentageSeek (percentage value) - seekSeconds := duration * percentageSeek / 100 - - // Step 4: Convert seekSeconds to string for ffmpeg command - seekTime := strconv.Itoa(seekSeconds) - // Step 5: Extract frame at seek time - cmd := exec.Command( - s.ffmpegPath, - "-ss", seekTime, - "-i", videoPath, - "-frames:v", "1", - "-q:v", "10", - "-y", // overwrite output - outputPath, - ) - - if s.debug { - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - } - err = cmd.Run() - if err != nil { - return nil, fmt.Errorf("ffmpeg command failed on file '%v' : %w", videoPath, err) - } - return os.ReadFile(outputPath) + // Create a temporary video service for this operation + videoSvc := ffmpeg.NewVideoService(s.ffmpegPath, s.ffprobePath, 1, s.debug) + return videoSvc.GenerateVideoPreview(videoPath, outputPath, percentageSeek) } diff --git a/backend/swagger/docs/docs.go b/backend/swagger/docs/docs.go index 2e673a6ec..6f7ebd3fd 100644 --- a/backend/swagger/docs/docs.go +++ b/backend/swagger/docs/docs.go @@ -854,6 +854,87 @@ const docTemplate = `{ } } }, + "/api/media/subtitles": { + "get": { + "description": "Extracts embedded subtitle content from video files by stream index and returns raw WebVTT content", + "consumes": [ + "application/json" + ], + "produces": [ + "text/vtt" + ], + "tags": [ + "Subtitles" + ], + "summary": "Extract embedded subtitles", + "parameters": [ + { + "type": "string", + "description": "Index path to the video file", + "name": "path", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Source name for the desired source", + "name": "source", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Stream index for embedded subtitle extraction, defaults to 0", + "name": "index", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Raw WebVTT subtitle content", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Resource not found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/preview": { "get": { "description": "Returns a preview image based on the requested path and size.", @@ -1616,6 +1697,43 @@ const docTemplate = `{ } } }, + "/api/settings/config": { + "get": { + "description": "Returns the current running configuration settings as YAML format with optional comments and filtering.", + "consumes": [ + "application/json" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "Settings" + ], + "summary": "Get system settings as YAML", + "parameters": [ + { + "type": "boolean", + "description": "Show all values (true) or only non-default values (false, default)", + "name": "full", + "in": "query" + }, + { + "type": "boolean", + "description": "Include comments in YAML (true) or plain YAML (false, default)", + "name": "comments", + "in": "query" + } + ], + "responses": { + "200": { + "description": "System settings in YAML format", + "schema": { + "type": "string" + } + } + } + } + }, "/api/share": { "get": { "description": "Retrieves all share links associated with a specific resource path for the current user.", @@ -2486,26 +2604,26 @@ const docTemplate = `{ "type": "object", "properties": { "adminPassword": { - "description": "the password of the admin user. If not set, the default is \"admin\".", + "description": "secret: the password of the admin user. If not set, the default is \"admin\".", "type": "string" }, "adminUsername": { - "description": "the username of the admin user. If not set, the default is \"admin\".", + "description": "secret: the username of the admin user. If not set, the default is \"admin\".", "type": "string" }, "key": { - "description": "the key used to sign the JWT tokens. If not set, a random key will be generated.", + "description": "secret: the key used to sign the JWT tokens. If not set, a random key will be generated.", "type": "string" }, "methods": { "$ref": "#/definitions/settings.LoginMethods" }, "tokenExpirationHours": { - "description": "the number of hours until the token expires. Default is 2 hours.", + "description": "time in hours each web UI session token is valid for. Default is 2 hours.", "type": "integer" }, "totpSecret": { - "description": "secret used to encrypt TOTP secrets", + "description": "secret: secret used to encrypt TOTP secrets", "type": "string" } } @@ -2602,6 +2720,10 @@ const docTemplate = `{ "settings.Frontend": { "type": "object", "properties": { + "description": { + "description": "description that shows up in html head meta description", + "type": "string" + }, "disableDefaultLinks": { "description": "disable default links in the sidebar", "type": "boolean" @@ -2620,6 +2742,10 @@ const docTemplate = `{ "$ref": "#/definitions/settings.ExternalLink" } }, + "favicon": { + "description": "path to a favicon to use for the frontend", + "type": "string" + }, "name": { "description": "display name", "type": "string" @@ -2723,11 +2849,11 @@ const docTemplate = `{ "type": "string" }, "clientId": { - "description": "client id of the OIDC application", + "description": "secret: client id of the OIDC application", "type": "string" }, "clientSecret": { - "description": "client secret of the OIDC application", + "description": "secret: client secret of the OIDC application", "type": "string" }, "createUser": { @@ -2776,6 +2902,7 @@ const docTemplate = `{ "type": "string" }, "secret": { + "description": "secret: authentication key for OnlyOffice integration", "type": "string" }, "url": { @@ -2885,6 +3012,10 @@ const docTemplate = `{ "description": "disable type detection by header, useful if filesystem is slow.", "type": "boolean" }, + "disableUpdateCheck": { + "description": "disables backend update check service", + "type": "boolean" + }, "externalUrl": { "description": "used by share links if set (eg. http://mydomain.com)", "type": "string" @@ -2903,6 +3034,10 @@ const docTemplate = `{ "description": "max pre-archive combined size of files/folder that are allowed to be archived (in GB)", "type": "integer" }, + "minSearchLength": { + "description": "minimum length of search query to begin searching (default: 3)", + "type": "integer" + }, "numImageProcessors": { "description": "number of concurrent image processing jobs used to create previews, default is number of cpu cores available.", "type": "integer" @@ -3073,6 +3208,10 @@ const docTemplate = `{ "description": "when false, the date is relative, when true, the date is an exact timestamp", "type": "boolean" }, + "debugOffice": { + "description": "debug onlyoffice editor", + "type": "boolean" + }, "deleteWithoutConfirming": { "description": "delete files without confirmation", "type": "boolean" @@ -3086,7 +3225,7 @@ const docTemplate = `{ "type": "string" }, "disablePreviewExt": { - "description": "comma separated list of file extensions to disable preview for", + "description": "space separated list of file extensions to disable preview for", "type": "string" }, "disableQuickToggles": { @@ -3106,7 +3245,7 @@ const docTemplate = `{ "type": "boolean" }, "disableViewingExt": { - "description": "comma separated list of file extensions to disable viewing for", + "description": "space separated list of file extensions to disable viewing for", "type": "string" }, "editorQuickSave": { @@ -3217,6 +3356,16 @@ const docTemplate = `{ "downloadsLimit": { "type": "integer" }, + "enableOnlyOffice": { + "type": "boolean" + }, + "enableOnlyOfficeEditing": { + "type": "boolean" + }, + "enforceDarkLightMode": { + "description": "\"dark\" or \"light\"", + "type": "string" + }, "expires": { "type": "string" }, @@ -3308,6 +3457,16 @@ const docTemplate = `{ "downloadsLimit": { "type": "integer" }, + "enableOnlyOffice": { + "type": "boolean" + }, + "enableOnlyOfficeEditing": { + "type": "boolean" + }, + "enforceDarkLightMode": { + "description": "\"dark\" or \"light\"", + "type": "string" + }, "expire": { "type": "integer" }, @@ -3434,6 +3593,10 @@ const docTemplate = `{ "description": "autoplay media files in preview", "type": "boolean" }, + "defaultMediaPlayer": { + "description": "disable html5 media player and use the default media player", + "type": "boolean" + }, "disableHideSidebar": { "description": "disable the hide sidebar preview for previews and editors", "type": "boolean" @@ -3507,6 +3670,10 @@ const docTemplate = `{ "description": "when false, the date is relative, when true, the date is an exact timestamp", "type": "boolean" }, + "debugOffice": { + "description": "debug onlyoffice editor", + "type": "boolean" + }, "deleteWithoutConfirming": { "description": "delete files without confirmation", "type": "boolean" @@ -3520,7 +3687,7 @@ const docTemplate = `{ "type": "string" }, "disablePreviewExt": { - "description": "comma separated list of file extensions to disable preview for", + "description": "space separated list of file extensions to disable preview for", "type": "string" }, "disableQuickToggles": { @@ -3539,7 +3706,7 @@ const docTemplate = `{ "type": "boolean" }, "disableViewingExt": { - "description": "comma separated list of file extensions to disable viewing for", + "description": "space separated list of file extensions to disable viewing for", "type": "string" }, "editorQuickSave": { diff --git a/backend/swagger/docs/swagger.json b/backend/swagger/docs/swagger.json index ee176c57a..7117da0c2 100644 --- a/backend/swagger/docs/swagger.json +++ b/backend/swagger/docs/swagger.json @@ -843,6 +843,87 @@ } } }, + "/api/media/subtitles": { + "get": { + "description": "Extracts embedded subtitle content from video files by stream index and returns raw WebVTT content", + "consumes": [ + "application/json" + ], + "produces": [ + "text/vtt" + ], + "tags": [ + "Subtitles" + ], + "summary": "Extract embedded subtitles", + "parameters": [ + { + "type": "string", + "description": "Index path to the video file", + "name": "path", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Source name for the desired source", + "name": "source", + "in": "query", + "required": true + }, + { + "type": "integer", + "description": "Stream index for embedded subtitle extraction, defaults to 0", + "name": "index", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Raw WebVTT subtitle content", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Resource not found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, "/api/preview": { "get": { "description": "Returns a preview image based on the requested path and size.", @@ -1605,6 +1686,43 @@ } } }, + "/api/settings/config": { + "get": { + "description": "Returns the current running configuration settings as YAML format with optional comments and filtering.", + "consumes": [ + "application/json" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "Settings" + ], + "summary": "Get system settings as YAML", + "parameters": [ + { + "type": "boolean", + "description": "Show all values (true) or only non-default values (false, default)", + "name": "full", + "in": "query" + }, + { + "type": "boolean", + "description": "Include comments in YAML (true) or plain YAML (false, default)", + "name": "comments", + "in": "query" + } + ], + "responses": { + "200": { + "description": "System settings in YAML format", + "schema": { + "type": "string" + } + } + } + } + }, "/api/share": { "get": { "description": "Retrieves all share links associated with a specific resource path for the current user.", @@ -2475,26 +2593,26 @@ "type": "object", "properties": { "adminPassword": { - "description": "the password of the admin user. If not set, the default is \"admin\".", + "description": "secret: the password of the admin user. If not set, the default is \"admin\".", "type": "string" }, "adminUsername": { - "description": "the username of the admin user. If not set, the default is \"admin\".", + "description": "secret: the username of the admin user. If not set, the default is \"admin\".", "type": "string" }, "key": { - "description": "the key used to sign the JWT tokens. If not set, a random key will be generated.", + "description": "secret: the key used to sign the JWT tokens. If not set, a random key will be generated.", "type": "string" }, "methods": { "$ref": "#/definitions/settings.LoginMethods" }, "tokenExpirationHours": { - "description": "the number of hours until the token expires. Default is 2 hours.", + "description": "time in hours each web UI session token is valid for. Default is 2 hours.", "type": "integer" }, "totpSecret": { - "description": "secret used to encrypt TOTP secrets", + "description": "secret: secret used to encrypt TOTP secrets", "type": "string" } } @@ -2591,6 +2709,10 @@ "settings.Frontend": { "type": "object", "properties": { + "description": { + "description": "description that shows up in html head meta description", + "type": "string" + }, "disableDefaultLinks": { "description": "disable default links in the sidebar", "type": "boolean" @@ -2609,6 +2731,10 @@ "$ref": "#/definitions/settings.ExternalLink" } }, + "favicon": { + "description": "path to a favicon to use for the frontend", + "type": "string" + }, "name": { "description": "display name", "type": "string" @@ -2712,11 +2838,11 @@ "type": "string" }, "clientId": { - "description": "client id of the OIDC application", + "description": "secret: client id of the OIDC application", "type": "string" }, "clientSecret": { - "description": "client secret of the OIDC application", + "description": "secret: client secret of the OIDC application", "type": "string" }, "createUser": { @@ -2765,6 +2891,7 @@ "type": "string" }, "secret": { + "description": "secret: authentication key for OnlyOffice integration", "type": "string" }, "url": { @@ -2874,6 +3001,10 @@ "description": "disable type detection by header, useful if filesystem is slow.", "type": "boolean" }, + "disableUpdateCheck": { + "description": "disables backend update check service", + "type": "boolean" + }, "externalUrl": { "description": "used by share links if set (eg. http://mydomain.com)", "type": "string" @@ -2892,6 +3023,10 @@ "description": "max pre-archive combined size of files/folder that are allowed to be archived (in GB)", "type": "integer" }, + "minSearchLength": { + "description": "minimum length of search query to begin searching (default: 3)", + "type": "integer" + }, "numImageProcessors": { "description": "number of concurrent image processing jobs used to create previews, default is number of cpu cores available.", "type": "integer" @@ -3062,6 +3197,10 @@ "description": "when false, the date is relative, when true, the date is an exact timestamp", "type": "boolean" }, + "debugOffice": { + "description": "debug onlyoffice editor", + "type": "boolean" + }, "deleteWithoutConfirming": { "description": "delete files without confirmation", "type": "boolean" @@ -3075,7 +3214,7 @@ "type": "string" }, "disablePreviewExt": { - "description": "comma separated list of file extensions to disable preview for", + "description": "space separated list of file extensions to disable preview for", "type": "string" }, "disableQuickToggles": { @@ -3095,7 +3234,7 @@ "type": "boolean" }, "disableViewingExt": { - "description": "comma separated list of file extensions to disable viewing for", + "description": "space separated list of file extensions to disable viewing for", "type": "string" }, "editorQuickSave": { @@ -3206,6 +3345,16 @@ "downloadsLimit": { "type": "integer" }, + "enableOnlyOffice": { + "type": "boolean" + }, + "enableOnlyOfficeEditing": { + "type": "boolean" + }, + "enforceDarkLightMode": { + "description": "\"dark\" or \"light\"", + "type": "string" + }, "expires": { "type": "string" }, @@ -3297,6 +3446,16 @@ "downloadsLimit": { "type": "integer" }, + "enableOnlyOffice": { + "type": "boolean" + }, + "enableOnlyOfficeEditing": { + "type": "boolean" + }, + "enforceDarkLightMode": { + "description": "\"dark\" or \"light\"", + "type": "string" + }, "expire": { "type": "integer" }, @@ -3423,6 +3582,10 @@ "description": "autoplay media files in preview", "type": "boolean" }, + "defaultMediaPlayer": { + "description": "disable html5 media player and use the default media player", + "type": "boolean" + }, "disableHideSidebar": { "description": "disable the hide sidebar preview for previews and editors", "type": "boolean" @@ -3496,6 +3659,10 @@ "description": "when false, the date is relative, when true, the date is an exact timestamp", "type": "boolean" }, + "debugOffice": { + "description": "debug onlyoffice editor", + "type": "boolean" + }, "deleteWithoutConfirming": { "description": "delete files without confirmation", "type": "boolean" @@ -3509,7 +3676,7 @@ "type": "string" }, "disablePreviewExt": { - "description": "comma separated list of file extensions to disable preview for", + "description": "space separated list of file extensions to disable preview for", "type": "string" }, "disableQuickToggles": { @@ -3528,7 +3695,7 @@ "type": "boolean" }, "disableViewingExt": { - "description": "comma separated list of file extensions to disable viewing for", + "description": "space separated list of file extensions to disable viewing for", "type": "string" }, "editorQuickSave": { diff --git a/backend/swagger/docs/swagger.yaml b/backend/swagger/docs/swagger.yaml index 8332d6677..2d60ee5f6 100644 --- a/backend/swagger/docs/swagger.yaml +++ b/backend/swagger/docs/swagger.yaml @@ -140,22 +140,25 @@ definitions: settings.Auth: properties: adminPassword: - description: the password of the admin user. If not set, the default is "admin". + description: 'secret: the password of the admin user. If not set, the default + is "admin".' type: string adminUsername: - description: the username of the admin user. If not set, the default is "admin". + description: 'secret: the username of the admin user. If not set, the default + is "admin".' type: string key: - description: the key used to sign the JWT tokens. If not set, a random key - will be generated. + description: 'secret: the key used to sign the JWT tokens. If not set, a random + key will be generated.' type: string methods: $ref: '#/definitions/settings.LoginMethods' tokenExpirationHours: - description: the number of hours until the token expires. Default is 2 hours. + description: time in hours each web UI session token is valid for. Default + is 2 hours. type: integer totpSecret: - description: secret used to encrypt TOTP secrets + description: 'secret: secret used to encrypt TOTP secrets' type: string type: object settings.CustomTheme: @@ -229,6 +232,9 @@ definitions: type: object settings.Frontend: properties: + description: + description: description that shows up in html head meta description + type: string disableDefaultLinks: description: disable default links in the sidebar type: boolean @@ -242,6 +248,9 @@ definitions: items: $ref: '#/definitions/settings.ExternalLink' type: array + favicon: + description: path to a favicon to use for the frontend + type: string name: description: display name type: string @@ -316,10 +325,10 @@ definitions: description: if set, users in this group will be granted admin privileges. type: string clientId: - description: client id of the OIDC application + description: 'secret: client id of the OIDC application' type: string clientSecret: - description: client secret of the OIDC application + description: 'secret: client secret of the OIDC application' type: string createUser: description: create user if not exists @@ -357,6 +366,7 @@ definitions: to bypass proxy. type: string secret: + description: 'secret: authentication key for OnlyOffice integration' type: string url: description: The URL to the OnlyOffice Document Server, needs to be accessible @@ -440,6 +450,9 @@ definitions: disableTypeDetectionByHeader: description: disable type detection by header, useful if filesystem is slow. type: boolean + disableUpdateCheck: + description: disables backend update check service + type: boolean externalUrl: description: used by share links if set (eg. http://mydomain.com) type: string @@ -455,6 +468,10 @@ definitions: description: max pre-archive combined size of files/folder that are allowed to be archived (in GB) type: integer + minSearchLength: + description: 'minimum length of search query to begin searching (default: + 3)' + type: integer numImageProcessors: description: number of concurrent image processing jobs used to create previews, default is number of cpu cores available. @@ -585,6 +602,9 @@ definitions: description: when false, the date is relative, when true, the date is an exact timestamp type: boolean + debugOffice: + description: debug onlyoffice editor + type: boolean deleteWithoutConfirming: description: delete files without confirmation type: boolean @@ -595,7 +615,7 @@ definitions: description: list of file extensions to disable onlyoffice editor for type: string disablePreviewExt: - description: comma separated list of file extensions to disable preview for + description: space separated list of file extensions to disable preview for type: string disableQuickToggles: description: disable the quick toggles in the sidebar @@ -610,7 +630,7 @@ definitions: description: disable update notifications banner for admin users type: boolean disableViewingExt: - description: comma separated list of file extensions to disable viewing for + description: space separated list of file extensions to disable viewing for type: string editorQuickSave: description: show quick save button in editor @@ -690,6 +710,13 @@ definitions: type: string downloadsLimit: type: integer + enableOnlyOffice: + type: boolean + enableOnlyOfficeEditing: + type: boolean + enforceDarkLightMode: + description: '"dark" or "light"' + type: string expires: type: string favicon: @@ -751,6 +778,13 @@ definitions: type: integer downloadsLimit: type: integer + enableOnlyOffice: + type: boolean + enableOnlyOfficeEditing: + type: boolean + enforceDarkLightMode: + description: '"dark" or "light"' + type: string expire: type: integer favicon: @@ -839,6 +873,9 @@ definitions: autoplayMedia: description: autoplay media files in preview type: boolean + defaultMediaPlayer: + description: disable html5 media player and use the default media player + type: boolean disableHideSidebar: description: disable the hide sidebar preview for previews and editors type: boolean @@ -891,6 +928,9 @@ definitions: description: when false, the date is relative, when true, the date is an exact timestamp type: boolean + debugOffice: + description: debug onlyoffice editor + type: boolean deleteWithoutConfirming: description: delete files without confirmation type: boolean @@ -901,7 +941,7 @@ definitions: description: deprecated type: string disablePreviewExt: - description: comma separated list of file extensions to disable preview for + description: space separated list of file extensions to disable preview for type: string disableQuickToggles: description: disable the quick toggles in the sidebar @@ -915,7 +955,7 @@ definitions: description: disable update notifications type: boolean disableViewingExt: - description: comma separated list of file extensions to disable viewing for + description: space separated list of file extensions to disable viewing for type: string editorQuickSave: description: show quick save button in editor @@ -1554,6 +1594,61 @@ paths: summary: User login tags: - Auth + /api/media/subtitles: + get: + consumes: + - application/json + description: Extracts embedded subtitle content from video files by stream index + and returns raw WebVTT content + parameters: + - description: Index path to the video file + in: query + name: path + required: true + type: string + - description: Source name for the desired source + in: query + name: source + required: true + type: string + - description: Stream index for embedded subtitle extraction, defaults to 0 + in: query + name: index + type: integer + produces: + - text/vtt + responses: + "200": + description: Raw WebVTT subtitle content + schema: + type: string + "400": + description: Bad request + schema: + additionalProperties: + type: string + type: object + "403": + description: Forbidden + schema: + additionalProperties: + type: string + type: object + "404": + description: Resource not found + schema: + additionalProperties: + type: string + type: object + "500": + description: Internal server error + schema: + additionalProperties: + type: string + type: object + summary: Extract embedded subtitles + tags: + - Subtitles /api/preview: get: consumes: @@ -2077,6 +2172,31 @@ paths: summary: Get system settings tags: - Settings + /api/settings/config: + get: + consumes: + - application/json + description: Returns the current running configuration settings as YAML format + with optional comments and filtering. + parameters: + - description: Show all values (true) or only non-default values (false, default) + in: query + name: full + type: boolean + - description: Include comments in YAML (true) or plain YAML (false, default) + in: query + name: comments + type: boolean + produces: + - text/plain + responses: + "200": + description: System settings in YAML format + schema: + type: string + summary: Get system settings as YAML + tags: + - Settings /api/share: get: consumes: diff --git a/file.txt b/file.txt deleted file mode 100644 index b8d65adb7..000000000 --- a/file.txt +++ /dev/null @@ -1,740 +0,0 @@ -diff --git a/CHANGELOG.md b/CHANGELOG.md -index 92478a24..5085e058 100644 ---- a/CHANGELOG.md -+++ b/CHANGELOG.md -@@ -11,17 +11,16 @@ All notable changes to this project will be documented in this file. For commit - - **New Features**: - - New access control system that works like shares. You can add new allow / deny rules for users/groups for specific paths on specific sources. -- #- The concept of 'groups', either automatically from OIDC groups claim or defined https://github.com/gtsteffaniak/filebrowser/issues/545 -+ - The concept of 'groups', either automatically from OIDC groups claim or defined https://github.com/gtsteffaniak/filebrowser/issues/545 - - share view changes -- now aligns with the standard listing view. This means files can be viewed and edited (if permission allows) just like a normal listing. - - many share links customization enhancements -- #- only share to certain authenticated users https://github.com/gtsteffaniak/filebrowser/issues/656 https://github.com/gtsteffaniak/filebrowser/issues/985 -- #- one-time download links -- #- customize share theme, banner image, and icons https://github.com/gtsteffaniak/filebrowser/issues/827 https://github.com/gtsteffaniak/filebrowser/issues/1029 -- #- share link info customization https://github.com/gtsteffaniak/filebrowser/issues/792 https://github.com/gtsteffaniak/filebrowser/issues/841 https://github.com/gtsteffaniak/filebrowser/issues/520 -- #- "shared with me" and "share history" https://github.com/gtsteffaniak/filebrowser/issues/943 -+ - only share to certain authenticated users https://github.com/gtsteffaniak/filebrowser/issues/656 https://github.com/gtsteffaniak/filebrowser/issues/985 -+ - one-time download links -+ - customize share theme, banner image, and icons https://github.com/gtsteffaniak/filebrowser/issues/827 https://github.com/gtsteffaniak/filebrowser/issues/1029 -+ - share link info customization https://github.com/gtsteffaniak/filebrowser/issues/792 https://github.com/gtsteffaniak/filebrowser/issues/841 https://github.com/gtsteffaniak/filebrowser/issues/520 -+ - "shared with me" and "share history" https://github.com/gtsteffaniak/filebrowser/issues/943 - - upload to share links https://github.com/gtsteffaniak/filebrowser/issues/661 - - share link public changes https://github.com/gtsteffaniak/filebrowser/issues/473 -- - - - public/private folder logic https://github.com/gtsteffaniak/filebrowser/issues/505 - - **BugFixes**: -diff --git a/backend/http/middleware.go b/backend/http/middleware.go -index 5ba397fe..595f00e1 100644 ---- a/backend/http/middleware.go -+++ b/backend/http/middleware.go -@@ -6,7 +6,6 @@ import ( - "encoding/json" - "fmt" - "net/http" -- "net/url" - "runtime" - "strings" - "time" -@@ -44,14 +43,8 @@ type handleFunc func(w http.ResponseWriter, r *http.Request, data *requestContex - // Middleware to handle file requests by hash and pass it to the handler - func withHashFileHelper(fn handleFunc) handleFunc { - return withOrWithoutUserHelper(func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) { -+ path := r.URL.Query().Get("path") - hash := r.URL.Query().Get("hash") -- encodedPath := r.URL.Query().Get("path") -- // Decode the URL-encoded path -- path, err := url.QueryUnescape(encodedPath) -- if err != nil { -- return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err) -- } -- data.path = path - // Get the file link by hash - link, err := store.Share.GetByHash(hash) - if err != nil { -@@ -71,6 +64,11 @@ func withHashFileHelper(fn handleFunc) handleFunc { - return status, fmt.Errorf("could not authenticate share request") - } - } -+ data.path = strings.TrimSuffix(link.Path, "/") + "/" + strings.TrimPrefix(path, "/") -+ if path == "" || path == "/" { -+ data.path = link.Path -+ } -+ - source, ok := config.Server.SourceMap[link.Source] - if !ok { - return http.StatusNotFound, fmt.Errorf("source not found") -diff --git a/backend/http/middleware_test.go b/backend/http/middleware_test.go -index ff7c173e..eb257951 100644 ---- a/backend/http/middleware_test.go -+++ b/backend/http/middleware_test.go -@@ -155,20 +155,6 @@ func TestPublicShareHandlerAuthentication(t *testing.T) { - }, - expectedStatusCode: 0, // zero means 200 on helpers - }, -- { -- name: "Private share, valid password when token exists", -- share: &share.Link{ -- Hash: "pw_and_token_hash", -- UserID: 1, -- PasswordHash: passwordBcrypt, -- Token: "some_random_token", -- Source: "/srv", -- }, -- extraHeaders: map[string]string{ -- "X-SHARE-PASSWORD": "password", -- }, -- expectedStatusCode: 0, // zero means 200 on helpers -- }, - { - name: "Private share, no auth provided", - share: &share.Link{ -diff --git a/backend/http/public.go b/backend/http/public.go -index ef65dfa6..0e641fbd 100644 ---- a/backend/http/public.go -+++ b/backend/http/public.go -@@ -7,6 +7,9 @@ import ( - "net/url" - "strings" - -+ "github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files" -+ "github.com/gtsteffaniak/filebrowser/backend/common/utils" -+ "github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo" - "github.com/gtsteffaniak/go-logger/logger" - - _ "github.com/gtsteffaniak/filebrowser/backend/swagger/docs" -@@ -49,7 +52,7 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) - } - - func publicShareHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) { -- d.fileInfo.Path = "/" + strings.TrimPrefix(d.fileInfo.Path, d.share.Path) -+ d.fileInfo.Path = strings.TrimPrefix(d.fileInfo.Path, d.share.Path) - return renderJSON(w, r, d.fileInfo) - } - -@@ -91,5 +94,24 @@ func publicPreviewHandler(w http.ResponseWriter, r *http.Request, d *requestCont - if config.Server.DisablePreviews { - return http.StatusNotImplemented, fmt.Errorf("preview is disabled") - } -+ path := r.URL.Query().Get("path") -+ var err error -+ if path == "" { -+ return http.StatusBadRequest, fmt.Errorf("invalid request path") -+ } -+ fileInfo, err := files.FileInfoFaster(iteminfo.FileOptions{ -+ Path: utils.JoinPathAsUnix(d.share.Path, path), -+ Modify: false, // TODO make this configurable -+ Source: d.fileInfo.Source, -+ Expand: true, -+ }) -+ if err != nil { -+ logger.Debugf("public preview handler: error getting file info: %v", err) -+ return 400, fmt.Errorf("file not found") -+ } -+ if fileInfo.Type == "directory" { -+ return http.StatusBadRequest, fmt.Errorf("can't create preview for directory") -+ } -+ d.fileInfo = fileInfo - return previewHelperFunc(w, r, d) - } -diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js -index 9399a292..2ef5f38e 100644 ---- a/frontend/src/api/index.js -+++ b/frontend/src/api/index.js -@@ -7,3 +7,6 @@ import search from "./search"; - - // Note: shareApi has been consolidated into publicApi - export { filesApi, publicApi, usersApi, settingsApi, search, accessApi }; -+ -+// For backward compatibility, export publicApi as shareApi as well -+export { publicApi as shareApi }; -diff --git a/frontend/src/api/public.js b/frontend/src/api/public.js -index 1dcb4125..d94713c5 100644 ---- a/frontend/src/api/public.js -+++ b/frontend/src/api/public.js -@@ -2,7 +2,6 @@ import { fetchURL, fetchJSON, adjustedData } from "./utils"; - import { notify } from "@/notify"; - import { getApiPath, removePrefix } from "@/utils/url.js"; - import { externalUrl, baseURL } from "@/utils/constants"; --import { state } from "@/store"; - - // ============================================================================ - // PUBLIC API ENDPOINTS (hash-based authentication) -@@ -13,8 +12,7 @@ export async function fetchPub(path, hash, password = "", content = false) { - const params = { - path, - hash, -- ...(content && { content: 'true' }), -- ...(state.share.token && { token: state.share.token }) -+ ...(content && { content: 'true' }) - } - const apiPath = getApiPath("public/api/shared", params); - const response = await fetch(apiPath, { -@@ -24,17 +22,7 @@ export async function fetchPub(path, hash, password = "", content = false) { - }); - - if (!response.ok) { -- const error = new Error(response.statusText); -- // attempt to marshal json response -- let data = null; -- try { -- data = await response.json() -- } catch (e) { -- // ignore -- } -- if (data) { -- error.message = data.message; -- } -+ const error = new Error("Failed to connect to the server."); - error.status = response.status; - throw error; - } -@@ -57,14 +45,13 @@ export function getDownloadURL(share, files) { - } - - // Generate a preview URL for public shares --export function getPreviewURL(path) { -+export function getPreviewURL(hash, path) { - try { - const params = { - path: encodeURIComponent(path), - size: 'small', -- hash: state.share.hash, -- inline: 'true', -- ...(state.share.token && { token: state.share.token }) -+ hash: hash, -+ inline: 'true' - } - const apiPath = getApiPath('public/api/preview', params) - return window.origin + apiPath -diff --git a/frontend/src/api/share.js b/frontend/src/api/share.js -new file mode 100644 -index 00000000..729b7599 ---- /dev/null -+++ b/frontend/src/api/share.js -@@ -0,0 +1,66 @@ -+import { fetchURL, fetchJSON, adjustedData } from "./utils"; -+import { notify } from "@/notify"; -+import { getApiPath } from "@/utils/url.js"; -+import { externalUrl } from "@/utils/constants"; -+ -+export async function list() { -+ const apiPath = getApiPath("api/shares"); -+ return fetchJSON(apiPath); -+} -+ -+export async function get(path, source) { -+ try { -+ const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) }; -+ const apiPath = getApiPath("api/share",params); -+ let data = fetchJSON(apiPath); -+ return adjustedData(data, path); -+ } catch (err) { -+ notify.showError(err.message || "Error fetching data"); -+ throw err; -+ } -+} -+ -+export async function remove(hash) { -+ const params = { hash }; -+ const apiPath = getApiPath("api/share",params); -+ await fetchURL(apiPath, { -+ method: "DELETE", -+ }); -+} -+ -+export async function create(path, source, password = "", expires = "", unit = "hours") { -+ const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) }; -+ const apiPath = getApiPath("api/share",params); -+ let body = "{}"; -+ if (password != "" || expires !== "" || unit !== "hours") { -+ body = JSON.stringify({ password: password, expires: expires, unit: unit }); -+ } -+ return fetchJSON(apiPath, { -+ method: "POST", -+ body: body, -+ }); -+} -+ -+export function getShareURL(share) { -+ if (externalUrl) { -+ const apiPath = getApiPath(`share/${share.hash}`) -+ return externalUrl + apiPath -+ } -+ return window.origin+getApiPath(`share/${share.hash}`); -+} -+ -+export function getPreviewURL(hash, path) { -+ try { -+ const params = { -+ path: encodeURIComponent(path), -+ size: 'small', -+ hash: hash, -+ inline: 'true' -+ } -+ const apiPath = getApiPath('api/public/preview', params) -+ return window.origin + apiPath -+ } catch (err) { -+ notify.showError(err.message || 'Error getting preview URL') -+ throw err -+ } -+} -diff --git a/frontend/src/components/files/ListingItem.vue b/frontend/src/components/files/ListingItem.vue -index 29c2749f..93848c1c 100644 ---- a/frontend/src/components/files/ListingItem.vue -+++ b/frontend/src/components/files/ListingItem.vue -@@ -67,7 +67,7 @@ import { enableThumbs } from "@/utils/constants"; - import downloadFiles from "@/utils/download"; - - import { getHumanReadableFilesize } from "@/utils/filesizes"; --import { filesApi,publicApi } from "@/api"; -+import { filesApi, shareApi } from "@/api"; - import * as upload from "@/utils/upload"; - import { state, getters, mutations } from "@/store"; // Import your custom store - import { url } from "@/utils"; -@@ -162,8 +162,8 @@ export default { - // @ts-ignore - let path = url.removeTrailingSlash(state.req.path) + "/" + this.name; - if (getters.isShare()) { -- let urlPath = getters.getSharePath(this.name) ; -- return publicApi.getPreviewURL(urlPath); -+ let urlPath = getters.getSharePath() + "/" + this.name; -+ return shareApi.getPreviewURL(state.share.hash, urlPath, state.req.modified); - } - // @ts-ignore - return filesApi.getPreviewURL(state.req.source, path, this.modified); -@@ -226,7 +226,7 @@ export default { - }, - getUrl() { - if (this.hash) { -- return baseURL + "public/share/" + this.hash + "/" + url.encodedPath(this.path); -+ return baseURL + "share/" + this.hash + url.encodedPath(this.path); - } - if (serverHasMultipleSources) { - return baseURL + "files/" + encodeURIComponent(this.source) + url.encodedPath(this.path); -diff --git a/frontend/src/components/prompts/ActionApi.vue b/frontend/src/components/prompts/ActionApi.vue -index f2ebaa3f..1a381565 100644 ---- a/frontend/src/components/prompts/ActionApi.vue -+++ b/frontend/src/components/prompts/ActionApi.vue -@@ -90,3 +90,20 @@ export default { - }, - }; - -+ -+ -diff --git a/frontend/src/components/prompts/Share.vue b/frontend/src/components/prompts/Share.vue -index be1ad5ad..8689100d 100644 ---- a/frontend/src/components/prompts/Share.vue -+++ b/frontend/src/components/prompts/Share.vue -@@ -2,11 +2,10 @@ -
-

{{ $t("buttons.share") }}

-
--
--
{{ $t('search.path') }} {{ subpath }}
--

{{ $t('share.notice') }}

-- --
-+
{{ $t('search.path') }} {{ subpath }}
-+

{{ $t('share.notice') }}

-+ -+ -+ - - - -- -diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue -index 4547e3d2..38d75dc1 100644 ---- a/frontend/src/views/Settings.vue -+++ b/frontend/src/views/Settings.vue -@@ -122,4 +122,7 @@ export default { - background-color: var(--surfaceSecondary); - } - -+.card-content { -+ padding: 1em; -+} - -diff --git a/frontend/src/views/settings/Profile.vue b/frontend/src/views/settings/Profile.vue -index f65fe30f..e34e405a 100644 ---- a/frontend/src/views/settings/Profile.vue -+++ b/frontend/src/views/settings/Profile.vue -@@ -428,6 +428,9 @@ export default { - - - diff --git a/frontend/src/components/Search.vue b/frontend/src/components/Search.vue index 190524056..9b41f9b96 100644 --- a/frontend/src/components/Search.vue +++ b/frontend/src/components/Search.vue @@ -50,7 +50,7 @@ -
{{ contextText }}
+
{{ $t("search.searchContext", { context: getContext }) }}
@@ -87,7 +87,7 @@ v-model="smallerThan" type="number" min="0" - placeholder="number" + :placeholder="$t('search.number')" />

MB

@@ -96,7 +96,7 @@ class="sizeInput" v-model="largerThan" type="number" - placeholder="number" + :placeholder="$t('search.number')" />

MB

@@ -146,9 +146,8 @@ import { search } from "@/api"; import { getters, mutations, state } from "@/store"; import { getHumanReadableFilesize } from "@/utils/filesizes"; import { url } from "@/utils/"; - import Icon from "@/components/files/Icon.vue"; -import { serverHasMultipleSources, baseURL } from "@/utils/constants"; +import { serverHasMultipleSources, baseURL, minSearchLength } from "@/utils/constants"; var boxes = { folder: { label: "folders", icon: "folder" }, @@ -170,22 +169,22 @@ export default { return { largerThan: "", smallerThan: "", - noneMessage: "Start typing 3 or more characters to begin searching.", + noneMessage: this.$t("search.typeToSearch", { minSearchLength: minSearchLength }), searchTypes: "", isTypeSelectDisabled: false, showHelp: false, folderSelect: [ - { label: "Only Folders", value: "type:folder" }, - { label: "Only Files", value: "type:file" }, + { label: this.$t("search.onlyFolders"), value: "type:folder" }, + { label: this.$t("search.onlyFiles"), value: "type:file" }, ], typeSelect: [ - { label: "Photos", value: "type:image" }, - { label: "Audio", value: "type:audio" }, - { label: "Videos", value: "type:video" }, - { label: "Documents", value: "type:doc" }, - { label: "Archives", value: "type:archive" }, + { label: this.$t("search.photos"), value: "type:image" }, + { label: this.$t("search.audio"), value: "type:audio" }, + { label: this.$t("search.videos"), value: "type:video" }, + { label: this.$t("search.documents"), value: "type:doc" }, + { label: this.$t("search.archives"), value: "type:archive" }, ], - toggleOptionButton: [{ label: "Show Options" }], + toggleOptionButton: [{ label: this.$t("search.showOptions") }], value: "", ongoing: false, results: [], @@ -295,9 +294,7 @@ export default { if (this.ongoing) { return ""; } - return this.value === "" - ? this.$t("search.typeToSearch") - : this.$t("search.pressToSearch"); + return this.$t("search.typeToSearch", { minSearchLength: minSearchLength }) }, isRunning() { return this.ongoing; @@ -330,9 +327,6 @@ export default { return "/"; // if searching on non-current source, search the whole thing } }, - contextText() { - return `Search Context: ${this.getContext}`; // FIX: Use `this.getContext` as a property, not a function call - }, }, methods: { openContext(event) { @@ -489,9 +483,9 @@ export default { if (event != undefined) { event.preventDefault(); } - if (this.value === "" || this.value.length < 3) { + if (this.value === "" || this.value.length < minSearchLength) { this.ongoing = false; - this.noneMessage = "Not enough characters to search (min 3)"; + this.noneMessage = this.$t("search.notEnoughCharacters", { minSearchLength: minSearchLength }); return; } let searchTypesFull = this.searchTypes; @@ -510,7 +504,7 @@ export default { this.ongoing = false; if (this.results.length == 0) { - this.noneMessage = "No results found in indexed search."; + this.noneMessage = this.$t("search.noResults"); } }, toggleHelp() { diff --git a/frontend/src/components/Tooltip.vue b/frontend/src/components/Tooltip.vue index d509548f8..19cb6966a 100644 --- a/frontend/src/components/Tooltip.vue +++ b/frontend/src/components/Tooltip.vue @@ -3,14 +3,14 @@ ref="tooltipRef" v-show="tooltip.show" class="floating-tooltip fb-shadow" - :class="{ 'dark-mode': isDarkMode }" + :class="{ 'dark-mode': isDarkMode, 'pointer-enabled': tooltip.pointerEvents }" :style="tooltipStyle" v-html="tooltip.content" > + + diff --git a/frontend/src/components/sidebar/General.vue b/frontend/src/components/sidebar/General.vue index e8cbd4d2c..1eb84eb17 100644 --- a/frontend/src/components/sidebar/General.vue +++ b/frontend/src/components/sidebar/General.vue @@ -3,7 +3,7 @@
state.sources.info, activeSource: () => state.sources.current, realtimeActive: () => state.realtimeActive, + darkModeTogglePossible: () => shareInfo.enforceDarkLightMode != "dark" && shareInfo.enforceDarkLightMode != "light", }, watch: { route() { @@ -218,17 +220,13 @@ export default { } mutations.updateCurrentUser({ stickySidebar: !state.user.stickySidebar }); }, - navigateTo(path) { - const hashIndex = path.indexOf("#"); - if (hashIndex !== -1) { - // Extract the hash - const hash = path.substring(hashIndex); - // Remove the hash from the path - const cleanPath = path.substring(0, hashIndex); - this.$router.push({ path: cleanPath, hash: hash }, () => {}); - } else { - this.$router.push({ path: path }, () => {}); - } + navigateTo(path,hash) { + mutations.setPreviousHistoryItem({ + name: state.req.name, + source: state.req.source, + path: state.req.path, + }); + this.$router.push({ path: path, hash: hash }); mutations.closeHovers(); }, navigateToLogin() { @@ -406,6 +404,7 @@ button.action { .quick-toggles .active { background-color: var(--primaryColor) !important; border-radius: 10em; + color: white; } .inner-card { diff --git a/frontend/src/components/sidebar/Sidebar.vue b/frontend/src/components/sidebar/Sidebar.vue index 718faa484..387a59f6c 100644 --- a/frontend/src/components/sidebar/Sidebar.vue +++ b/frontend/src/components/sidebar/Sidebar.vue @@ -14,7 +14,16 @@
- {{ item.text }} + {{ $t("sidebar.help") }} + {{ + item.text + }}

{{ name }}

diff --git a/frontend/src/css/_variables.css b/frontend/src/css/_variables.css index 6fbbccd04..246c6c751 100644 --- a/frontend/src/css/_variables.css +++ b/frontend/src/css/_variables.css @@ -1,10 +1,9 @@ :root { --blue: #2196f3; --dark-blue: #1E88E5; - --red: #F44336; + --red: #f44336; --dark-red: #D32F2F; --moon-grey: #f2f2f2; - --icon-red: #da4453; --icon-orange: #f47750; --icon-yellow: #fdbc4b; diff --git a/frontend/src/css/styles.css b/frontend/src/css/styles.css index 518550766..844ff221b 100644 --- a/frontend/src/css/styles.css +++ b/frontend/src/css/styles.css @@ -8,52 +8,52 @@ @import "./listing.css"; @import "./dashboard.css"; @import "./login.css"; -@import './mobile.css'; +@import "./mobile.css"; .link { - color: var(--blue); + color: var(--blue); } #main .spinner { - display: block; - text-align: center; - line-height: 0; - padding: 1em 0; + display: block; + text-align: center; + line-height: 0; + padding: 1em 0; } #main .spinner > div { - width: .8em; - height: .8em; - margin: 0 .1em; - font-size: 1em; - background-color: rgba(0, 0, 0, 0.3); - border-radius: 100%; - display: inline-block; - animation: sk-bouncedelay 1.4s infinite ease-in-out both; + width: 0.8em; + height: 0.8em; + margin: 0 0.1em; + font-size: 1em; + background-color: rgba(0, 0, 0, 0.3); + border-radius: 100%; + display: inline-block; + animation: sk-bouncedelay 1.4s infinite ease-in-out both; } #main .spinner .bounce1 { - animation-delay: -0.32s; + animation-delay: -0.32s; } #main .spinner .bounce2 { - animation-delay: -0.16s; + animation-delay: -0.16s; } .delayed { - animation: delayed linear 100ms; + animation: delayed linear 100ms; } @keyframes delayed { - 0% { - opacity: 0; - } - 99% { - opacity: 0; - } - 100% { - opacity: 1; - } + 0% { + opacity: 0; + } + 99% { + opacity: 0; + } + 100% { + opacity: 1; + } } /* * * * * * * * * * * * * * * * @@ -61,223 +61,222 @@ * * * * * * * * * * * * * * * */ .action { - display: inline-block; - cursor: pointer; - transition: 0.2s ease all; - border: 0; - margin: 0; - color: #546E7A; - border-radius: 50%; - background: transparent; - padding: 0; - box-shadow: none; - vertical-align: middle; - text-align: left; - position: relative; + display: inline-block; + cursor: pointer; + transition: 0.2s ease all; + border: 0; + margin: 0; + color: #546e7a; + border-radius: 50%; + background: transparent; + padding: 0; + box-shadow: none; + vertical-align: middle; + text-align: left; + position: relative; } .action.disabled { - opacity: 0.2; - cursor: not-allowed; + opacity: 0.2; + cursor: not-allowed; } .action i { - padding: 0.4em; - transition: .1s ease-in-out all; - border-radius: 50%; + padding: 0.4em; + transition: 0.1s ease-in-out all; + border-radius: 50%; } .action:not(:disabled):hover { - background-color: rgba(0, 0, 0, .1); + background-color: rgba(0, 0, 0, 0.1); } .action ul { - position: absolute; - top: 0; - color: #7d7d7d; - list-style: none; - margin: 0; - padding: 0; - flex-direction: column; - display: flex; + position: absolute; + top: 0; + color: #7d7d7d; + list-style: none; + margin: 0; + padding: 0; + flex-direction: column; + display: flex; } .action ul li { - line-height: 1; - padding: .7em; - transition: .1s ease background-color; + line-height: 1; + padding: 0.7em; + transition: 0.1s ease background-color; } .action ul li:hover { - background-color: rgba(0, 0, 0, 0.04); + background-color: rgba(0, 0, 0, 0.04); } .action .counter { - display: block; - position: absolute; - bottom: 0; - right: 0; - background: var(--primaryColor); - color: #fff; - border-radius: 50%; - font-size: .75em; - width: 1.8em; - height: 1.8em; - text-align: center; - line-height: 1.55em; - font-weight: bold; + display: block; + position: absolute; + bottom: 0; + right: 0; + background: var(--primaryColor); + color: #fff; + border-radius: 50%; + font-size: 0.75em; + width: 1.8em; + height: 1.8em; + text-align: center; + line-height: 1.55em; + font-weight: bold; } - /* PREVIEWER */ #previewer { - background-color: rgba(0, 0, 0, 0.9); - width: 100%; - height: 100%; - z-index: 9999; + background-color: rgba(0, 0, 0, 0.9); + width: 100%; + height: 100%; + z-index: 9999; } #previewer header { - background: none; - color: #fff; + background: none; + color: #fff; } #previewer header > .action i { - color: #fff; + color: #fff; } @media (min-width: 800px) { - #previewer header #dropdown .action i { - color: #fff; - } + #previewer header #dropdown .action i { + color: #fff; + } } #previewer header .action:hover { - background-color: rgba(255, 255, 255, 0.3) + background-color: rgba(255, 255, 255, 0.3); } #previewer header .action span { - display: none; + display: none; } #previewer .preview { - text-align: center; - max-height: 100%; - max-width: 100%; - height: 100%; + text-align: center; + max-height: 100%; + max-width: 100%; + height: 100%; } #previewer .preview pre { - text-align: left; - overflow: auto; + text-align: left; + overflow: auto; } #previewer .preview pre, #previewer .preview video, #previewer .preview img { - max-height: 100%; - margin: 0; + max-height: 100%; + margin: 0; } #previewer .preview video { - height: 100%; + height: 100%; } #previewer .preview .info { - display: flex; - height: 100%; - flex-direction: column; - justify-content: center; - align-items: center; - font-size: 1.5em; - color: #fff; + display: flex; + height: 100%; + flex-direction: column; + justify-content: center; + align-items: center; + font-size: 1.5em; + color: #fff; } #previewer .preview .info .title { - margin-bottom: 1em; + margin-bottom: 1em; } #previewer .preview .info .title i { - display: block; - margin-bottom: .1em; - font-size: 4em; + display: block; + margin-bottom: 0.1em; + font-size: 4em; } #previewer .preview .info .button { - display: inline-block; + display: inline-block; } #previewer .preview .info .button:hover { - background-color: rgba(255, 255, 255, 0.2) + background-color: rgba(255, 255, 255, 0.2); } #previewer .preview .info .button i { - display: block; - margin-bottom: 4px; - font-size: 1.3em; + display: block; + margin-bottom: 4px; + font-size: 1.3em; } #previewer .pdf { - width: 100%; - height: 100%; + width: 100%; + height: 100%; } #previewer h2.message { - color: rgba(255, 255, 255, 0.5) + color: rgba(255, 255, 255, 0.5); } -#previewer>button { - margin: 0; - position: fixed; - top: calc(50% + 1.85em); - transform: translateY(-50%); - background-color: rgba(80, 80, 80, .5); - color: white; - border-radius: 50%; - cursor: pointer; - border: 0; - margin: 0; - padding: 0; - transition: 0.2s ease all; +#previewer > button { + margin: 0; + position: fixed; + top: calc(50% + 1.85em); + transform: translateY(-50%); + background-color: rgba(80, 80, 80, 0.5); + color: white; + border-radius: 50%; + cursor: pointer; + border: 0; + margin: 0; + padding: 0; + transition: 0.2s ease all; } -#previewer>button.hidden { - opacity: 0; - visibility: hidden; +#previewer > button.hidden { + opacity: 0; + visibility: hidden; } -#previewer>button>i { - padding: 0.4em; +#previewer > button > i { + padding: 0.4em; } -#previewer>button:last-of-type { - right: 0.5em; +#previewer > button:last-of-type { + right: 0.5em; } #previewer .spinner { - text-align: center; - position: fixed; - top: calc(50% + 1.85em); - left: 50%; - transform: translate(-50%, -50%); + text-align: center; + position: fixed; + top: calc(50% + 1.85em); + left: 50%; + transform: translate(-50%, -50%); } #previewer .spinner > div { - width: 18px; - height: 18px; - background-color: white; + width: 18px; + height: 18px; + background-color: white; } /* EDITOR */ #editor-container { - display: flex; - flex-direction: column; - background-color: none; - height: 100%; - width: 100%; - overflow: hidden; - position: relative; + display: flex; + flex-direction: column; + background-color: none; + height: 100%; + width: 100%; + overflow: hidden; + position: relative; } #editor-container #editor { - flex: 1; + flex: 1; } /* * * * * * * * * * * * * * * * @@ -285,9 +284,9 @@ * * * * * * * * * * * * * * * */ @keyframes spin { - 100% { - transform: rotate(360deg); - } + 100% { + transform: rotate(360deg); + } } /* * * * * * * * * * * * * * * * @@ -295,31 +294,31 @@ * * * * * * * * * * * * * * * */ .rules > div { - display: flex; - align-items: center; - margin: .5rem 0; + display: flex; + align-items: center; + margin: 0.5rem 0; } .rules input[type="checkbox"] { - margin-right: .2rem; + margin-right: 0.2rem; } .rules input[type="text"] { - border: 1px solid#ddd; - padding: .2rem; + border: 1px solid#ddd; + padding: 0.2rem; } .rules label { - margin-right: .5rem; + margin-right: 0.5rem; } .rules button { - margin-left: auto; + margin-left: auto; } .rules button.delete { - padding: .2rem .5rem; - margin-left: .5rem; + padding: 0.2rem 0.5rem; + margin-left: 0.5rem; } /* * * * * * * * * * * * * * * * @@ -327,78 +326,90 @@ * * * * * * * * * * * * * * * */ body.rtl .card-content textarea { - direction: ltr; - text-align: left; + direction: ltr; + text-align: left; } body.rtl .card-content .small + input { - direction: ltr; - text-align: left; + direction: ltr; + text-align: left; } body.rtl .card.floating .card-content .file-list { - direction: ltr; - text-align: left; + direction: ltr; + text-align: left; } .fb-shadow { - box-shadow: 0px 0px 15px 0px rgb(100 100 100 / 75%); - -webkit-box-shadow: 0px 0px 15px 0px rgb(100 100 100 / 75%); + box-shadow: 0px 0px 15px 0px rgb(100 100 100 / 75%); + -webkit-box-shadow: 0px 0px 15px 0px rgb(100 100 100 / 75%); } .clickable { - cursor: pointer; + cursor: pointer; } .clickable:hover, +.plyr .plyr__control:hover, button:hover, .action:hover, .listing-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); - /* Slightly enlarges the element */ + 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); + /* Slightly enlarges the element */ } .flat-right { - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; } .flat-left { - border-top-left-radius: 0 !important; - border-bottom-left-radius: 0 !important; + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; } .form-grow { - flex-grow: 1; - width: inherit; - min-width: 1em; + flex-grow: 1; + width: inherit; + min-width: 1em; } .form-flex-group { - display: flex; - flex-direction: row; - align-items: center; + display: flex; + flex-direction: row; + align-items: center; } .form-flex-group > .form-compact { - min-height: 2em; - width: unset !important; + min-height: 2em; + width: unset !important; } .form-button { - height: auto !important + height: auto !important; } .form-invalid { - border-color: red !important; + border-color: red !important; } .form-compact { - padding: 0em; + padding: 0em; } .form-dropdown { - width: unset !important; + width: unset !important; } + +.divider { + border-color: var(--divider); + width: 90%; +} + +.tooltip-close-button { + max-width: 4em !important; +} \ No newline at end of file diff --git a/frontend/src/i18n/ar.json b/frontend/src/i18n/ar.json index ae5f413da..88d0ebb8c 100644 --- a/frontend/src/i18n/ar.json +++ b/frontend/src/i18n/ar.json @@ -49,7 +49,8 @@ "clearCompleted": "اكتمل المسح", "showMore": "اعرض المزيد", "showLess": "اعرض أقل", - "openParentFolder": "فتح المجلد الأصل" + "openParentFolder": "فتح المجلد الأصل", + "selectedCount": "عدد العناصر المحددة لتنفيذ إجراء ما" }, "download": { "downloadFile": "Download File", @@ -84,15 +85,15 @@ "click": "حدد الملف أو المجلد", "ctrl": { "click": "حدد أكثر من ملف او مجلد", - "f": "إبدأ البحث", - "s": "حمل الملف او المجلد في هذا المكان" + "d": "تنزيل عناصر مختارة" }, "del": "حذف البيانات المحددة", - "doubleClick": "فتح المجلد او الملف", "esc": "مسح التحديد وإغلاق النافذة المنبثقة", - "f1": "هذه المعلومات", "f2": "إعادة تسمية الملف", - "help": "مساعدة" + "help": "مساعدة", + "f1": "إظهار موجه المساعدة", + "description": "يمكنك عرض خيارات التنقل الأساسية أدناه. للحصول على معلومات إضافية، يرجى زيارة", + "wiki": "ويكي متصفح الملفات الكمي" }, "languages": { "he": "עברית", @@ -134,7 +135,6 @@ "prompts": { "copy": "نسخ", "copyMessage": "رجاء حدد المكان لنسخ ملفاتك فيه:", - "deleteMessageMultiple": "هل تريد بالتأكيد حذف {count} ملف؟", "deleteMessageSingle": "هل تريد بالتأكيد حذف هذا الملف/المجلد؟", "deleteTitle": "حذف الملفات", "displayName": "الإسم:", @@ -142,7 +142,6 @@ "downloadMessage": "حدد إمتداد الملف المراد تحميله.", "error": "لقد حدث خطأ ما", "fileInfo": "معلومات الملف", - "lastModified": "آخر تعديل{suffix}", "move": "نقل", "moveMessage": "إختر مكان جديد للملفات أو المجلدات المراد نقلها:", "newArchetype": "إنشاء منشور من المنشور الأصلي. الملف سيتم انشاءه في مجلد المحتويات.", @@ -150,10 +149,7 @@ "newDirMessage": "رجاء أدخل اسم المجلد الجديد.", "newFile": "ملف جديد", "newFileMessage": "رجاء ادخل اسم الملف الجديد.", - "numberDirs": "عدد المجلدات{suffix}", - "numberFiles": "عدد الملفات{suffix}", "rename": "إعادة تسمية", - "renameMessage": "إدراج اسم جديد لـ", "replace": "إستبدال", "replaceMessage": "أحد الملفات التي تحاول رفعها يتعارض مع ملف موجود بنفس الإسم. هل تريد إستبدال الملف الموجود؟\n", "schedule": "جدولة", @@ -161,7 +157,6 @@ "show": "عرض", "size": "الحجم", "upload": "التحميل", - "deleteMessageShare": "Are you sure you want to delete this share: {path} ?", "deleteUserMessage": "هل أنت متأكد من رغبتك في حذف هذا المستخدم؟", "filesSelected": "الملفات المحددة", "optionalPassword": "كلمة مرور اختيارية", @@ -172,10 +167,6 @@ "dragAndDrop": "سحب الملفات وإفلاتها هنا", "completed": "مكتمل", "conflictsDetected": "التعارضات المكتشفة", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "الاسم المقدم غير صالح: لا يمكن أن يحتوي على \"/\" أو \"\\\"", - "source": "Source{suffix}", "downloadsLimit": "حد عدد التنزيلات", "maxBandwidth": "أقصى نطاق ترددي للتنزيل (كيلو بايت/ثانية)", "shareTheme": "سمة المشاركة", @@ -186,22 +177,44 @@ "shareBanner": "مشاركة عنوان URL للشعار", "shareFavicon": "مشاركة عنوان URL المفضل", "optionalPasswordHelp": "إذا تم تعيينها، يجب على المستخدمين إدخال كلمة المرور هذه لعرض المحتوى المشترك.", - "viewMode": "وضع العرض" + "viewMode": "وضع العرض", + "renameMessage": "أدخل اسماً جديداً:", + "renameMessageInvalid": "لا يمكن أن يحتوي الملف على \"/\" أو \"\\\"", + "source": "المصدر{suffix}", + "deleteMessageMultiple": "هل تريد بالتأكيد حذف الملف (الملفات) {count} ؟", + "deleteMessageShare": "هل أنت متأكد أنك تريد حذف هذه المشاركة؟ {path} ?", + "lastModified": "آخر تعديل{suffix}", + "numberDirs": "عدد الدلائل{suffix}", + "numberFiles": "عدد الملفات{suffix}", + "renameMessageConflict": "عنصر باسم \"{filename}\" موجود بالفعل!", + "uploadSettingsChunked": "الحد الأقصى للتحميلات المتزامنة: {maxConcurrentUpload} ، حجم القطعة: {uploadChunkSizeMb} ميغابايت", + "uploadSettingsNoChunk": "الحد الأقصى للتحميلات المتزامنة: {maxConcurrentUpload} ، تم تعطيل التحميل المقطّع" }, "search": { "images": "الصور", "music": "الموسيقى", "pdf": "PDF", - "pressToSearch": "Press enter to search...", "search": "البحث...", - "typeToSearch": "Type to search...", "types": "الأنواع", "video": "فيديوهات", "path": "المسار:", "smallerThan": "أصغر من:", "largerThan": "أكبر من:", "helpText1": "يحدث البحث على كل حرف تكتبه (3 أحرف كحد أدنى لمصطلحات البحث).", - "helpText2": "الفهرس: يستخدم البحث الفهرس الذي يتم تحديثه تلقائيًا على الفاصل الزمني الذي تم تكوينه (افتراضيًا: 5 دقائق). قد يؤدي البحث عندما يكون البرنامج قد بدأ للتو إلى نتائج غير مكتملة." + "helpText2": "الفهرس: يستخدم البحث الفهرس الذي يتم تحديثه تلقائيًا على الفاصل الزمني الذي تم تكوينه (افتراضيًا: 5 دقائق). قد يؤدي البحث عندما يكون البرنامج قد بدأ للتو إلى نتائج غير مكتملة.", + "noResults": "لم يتم العثور على نتائج في البحث المفهرس.", + "onlyFolders": "المجلدات فقط", + "onlyFiles": "الملفات فقط", + "showOptions": "عرض الخيارات", + "photos": "الصور", + "audio": "الصوت", + "videos": "مقاطع الفيديو", + "documents": "المستندات", + "archives": "المحفوظات", + "number": "العدد", + "searchContext": "سياق البحث: {context}", + "notEnoughCharacters": "أحرف غير كافية للبحث (الحد الأدنى {minSearchLength})", + "typeToSearch": "ابدأ الكتابة {minSearchLength} أو أكثر لبدء البحث." }, "settings": { "admin": "Admin", @@ -214,10 +227,8 @@ "avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)", "branding": "Branding", "brandingDirectoryPath": "Branding directory path", - "brandingHelp": "You can customize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.", "changePassword": "تغيير كلمة المرور", "commandRunner": "Command runner", - "commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.", "commandsUpdated": "تم تحديث الأوامر", "createUserDir": "Auto create user home dir while adding new user", "customStylesheet": "ستايل مخصص", @@ -300,7 +311,14 @@ "adminOptions": "خيارات المستخدم المسؤول", "shareDuration": "تنتهي صلاحيتها", "searchOptions": "خيارات البحث", - "downloads": "التنزيلات" + "downloads": "التنزيلات", + "systemAdmin": "النظام والإدارة", + "configViewer": "عارض التكوين", + "configViewerShowFull": "إظهار التكوين الكامل", + "configViewerShowComments": "إظهار التعليقات", + "configViewerLoadConfig": "تكوين التحميل", + "brandingHelp": "يمكنك تخصيص شكل ومظهر مثيل متصفح الملفات الخاص بك عن طريق تغيير اسمه، واستبدال الشعار، وإضافة أنماط مخصصة، وحتى تعطيل الروابط الخارجية إلى GitHub.\nلمزيد من المعلومات حول العلامة التجارية المخصصة، يرجى الاطلاع على {0}.", + "commandRunnerHelp": "هنا يمكنك تعيين الأوامر التي يتم تنفيذها في الأحداث المسماة. يجب عليك كتابة أمر واحد لكل سطر. سيكون متغيرا البيئة {0} و {1} متاحين، حيث {0} بالنسبة إلى {1}. لمزيد من المعلومات حول هذه الميزة ومتغيرات البيئة المتاحة، يرجى قراءة {2}." }, "sidebar": { "help": "مساعدة", @@ -358,12 +376,21 @@ "hideNavButtons": "إخفاء أزرار التنقل", "hideNavButtonsDescription": "قم بإخفاء أزرار التنقل على شريط التنقل في المشاركة لإضفاء مظهر بسيط.", "viewModeDescription": "التخطيط الافتراضي للصفحة المشتركة.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "تم إرسال مشاركة إليك لعرضها أو تنزيلها.", "disableShareCard": "تعطيل بطاقة المشاركة المعطلة", "disableShareCardDescription": "قم بتعطيل بطاقة المشاركة على الصفحة المشتركة في الشريط الجانبي أو في أعلى الصفحة على الهاتف المحمول.", "disableSidebar": "تعطيل الشريط الجانبي", - "disableSidebarDescription": "تعطيل الشريط الجانبي في الصفحة المشتركة." + "disableSidebarDescription": "تعطيل الشريط الجانبي في الصفحة المشتركة.", + "enforceDarkLightMode": "فرض وضع السمة", + "enforceDarkLightModeDescription": "فرض وضع سمة معينة (داكنة أو فاتحة) لهذه المشاركة، وتجاوز تفضيلات المستخدم.", + "default": "لا تفرض وضعاً", + "dark": "داكن", + "light": "خفيف", + "enableOnlyOffice": "تمكين عارض OnlyOffice فقط", + "enableOnlyOfficeDescription": "السماح بعرض ملفات المكتب باستخدام OnlyOffice في هذه المشاركة.", + "enableOnlyOfficeEditing": "تمكين التحرير في المكتب فقط", + "enableOnlyOfficeEditingDescription": "السماح بتحرير الملفات المكتبية باستخدام OnlyOffice في هذه الحصة.", + "titleDefault": "الملفات المشتركة - {title}" }, "api": { "title": "مفاتيح واجهة برمجة التطبيقات (API):", @@ -447,8 +474,8 @@ "totalDenied": "مرفوض", "totalAllowed": "المسموح به", "all": "رفض الكل", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "يوضح هذا ما يحدث عندما يحاول المستخدم الوصول إلى مسار لا توجد فيه قواعد وصول محددة. تعني \"السماح\" أنه يمكن للمستخدمين الوصول بشكل افتراضي، وتعني \"الرفض\" أنه يتم حظر المستخدمين بشكل افتراضي ما لم يُسمح لهم صراحةً." + "defaultBehaviorDescription": "يوضح هذا ما يحدث عندما يحاول المستخدم الوصول إلى مسار لا توجد فيه قواعد وصول محددة. تعني \"السماح\" أنه يمكن للمستخدمين الوصول بشكل افتراضي، وتعني \"الرفض\" أنه يتم حظر المستخدمين بشكل افتراضي ما لم يُسمح لهم صراحةً.", + "defaultBehavior": "الإجراء الافتراضي{suffix}" }, "fileLoading": { "title": "التحميلات والتنزيلات", @@ -506,7 +533,13 @@ "defaultThemeDescription": "افتراضي - السمة الافتراضية", "customTheme": "اختر المظهر الخاص بك", "showSelectMultiple": "إظهار تحديد متعدد في قائمة السياق على سطح المكتب", - "showSelectMultipleDescription": "بشكل افتراضي، لا تحتوي قائمة سياق سطح المكتب على خيار \"تحديد متعدد\" مثل الهاتف المحمول. يظهر هذا الخيار دائمًا في قائمة السياق." + "showSelectMultipleDescription": "بشكل افتراضي، لا تحتوي قائمة سياق سطح المكتب على خيار \"تحديد متعدد\" مثل الهاتف المحمول. يظهر هذا الخيار دائمًا في قائمة السياق.", + "defaultMediaPlayer": "استخدم مشغل الوسائط الأصلي في متصفحك", + "defaultMediaPlayerDescription": "استخدم مشغل الوسائط الأصلي في متصفحك لملفات الفيديو والصوت بدلاً من مشغل الوسائط \"vue-plyr\" المضمن في متصفح FileBrowser.", + "debugOfficeEditor": "تمكين وضع التصحيح فقط", + "debugOfficeEditorDescription": "إظهار نافذة منبثقة للتصحيح مع معلومات إضافية في محرر OnlyOffice.", + "fileViewerOptions": "خيارات عارض الملفات", + "themeAndLanguage": "الموضوع واللغة" }, "colors": { "red": "أحمر", @@ -521,7 +554,11 @@ "saveFailed": "فشل حفظ الملف. يرجى المحاولة مرة أخرى.", "saveAborted": "تم إجهاض الحفظ. اسم الملف غير مطابق للملف النشط.", "saveDisabled": "حفظ الملفات المشتركة غير مدعوم.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "تم إجهاض عملية الحفظ. الملف النشط للتطبيق ({activeFile}) لا يتطابق مع الملف الذي تحاول حفظه ({tryingToSave}).", + "syncFailed": "فشل مزامنة الحالة مع مسار \"{filename}\" بعد 5 محاولات. إجهاض إعداد المحرر لمنع تلف البيانات." + }, + "player": { + "LoopEnabled": "تم تمكين الحلقة", + "LoopDisabled": "حلقة معطلة" } } diff --git a/frontend/src/i18n/cz.json b/frontend/src/i18n/cz.json index 2d62ae504..fd5263978 100644 --- a/frontend/src/i18n/cz.json +++ b/frontend/src/i18n/cz.json @@ -49,7 +49,8 @@ "clearCompleted": "Vymazání dokončeno", "showMore": "Zobrazit více", "showLess": "Zobrazit méně", - "openParentFolder": "Otevření nadřazené složky" + "openParentFolder": "Otevření nadřazené složky", + "selectedCount": "Počet položek vybraných k provedení akce" }, "download": { "downloadFile": "Stáhnout soubor", @@ -87,15 +88,15 @@ "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" + "d": "stáhnout vybrané položky" }, "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" + "help": "Nápověda", + "f1": "zobrazit výzvu nápovědy", + "description": "Základní možnosti navigace si můžete prohlédnout níže. Další informace naleznete na adrese", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -161,19 +162,26 @@ "hideNavButtons": "Skrytí navigačních tlačítek", "hideNavButtonsDescription": "Skryjte navigační tlačítka na panelu navigace ve sdíleném okně, abyste vytvořili minimalistický vzhled.", "viewModeDescription": "Výchozí rozložení sdílené stránky.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Byl vám zaslán podíl, který si můžete prohlédnout nebo stáhnout.", "disableShareCard": "Zakázat sdílenou kartu", "disableShareCardDescription": "Zakázat kartu sdílení na sdílené stránce v postranním panelu nebo v horní části stránky na mobilním telefonu.", "disableSidebar": "Zakázat postranní panel", - "disableSidebarDescription": "Zakázat postranní panel na sdílené stránce." + "disableSidebarDescription": "Zakázat postranní panel na sdílené stránce.", + "enforceDarkLightMode": "Vynucení režimu tématu", + "enforceDarkLightModeDescription": "Vynucení určitého režimu motivu (tmavého nebo světlého) pro tuto sdílenou složku, který je nadřazen uživatelským preferencím.", + "default": "Nevynucujte režim", + "dark": "Dark", + "light": "Světlo", + "enableOnlyOffice": "Povolení prohlížeče OnlyOffice", + "enableOnlyOfficeDescription": "Povolit prohlížení kancelářských souborů pomocí OnlyOffice v této sdílené složce.", + "enableOnlyOfficeEditing": "Povolení úprav OnlyOffice", + "enableOnlyOfficeEditingDescription": "Povolit úpravy kancelářských souborů pomocí OnlyOffice v této sdílené složce.", + "titleDefault": "Sdílené soubory - {title}" }, "prompts": { "copy": "Kopírovat", "copyMessage": "Vyberte místo, kam chcete soubory kopírovat:", - "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:", @@ -183,7 +191,6 @@ "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.", @@ -191,10 +198,7 @@ "newDirMessage": "Zadejte název nové složky.", "newFile": "Nový soubor", "newFileMessage": "Zadejte název nového souboru.", - "numberDirs": "Počet složek{suffix}", - "numberFiles": "Počet souborů{suffix}", "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", @@ -209,10 +213,6 @@ "dragAndDrop": "Přetáhněte sem soubory", "completed": "Dokončeno", "conflictsDetected": "Zjištěné konflikty", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Nesprávný zadaný název: nelze použít kontejner \"/\" nebo \"\\\".", - "source": "Source{suffix}", "downloadsLimit": "Limit počtu stažení", "maxBandwidth": "Maximální šířka pásma stahování (kbps)", "shareTheme": "Sdílet téma", @@ -223,22 +223,44 @@ "shareBanner": "Sdílet adresu URL banneru", "shareFavicon": "Sdílet adresu URL favicon", "optionalPasswordHelp": "Pokud je nastaveno, musí uživatelé toto heslo zadat, aby mohli sdílený obsah zobrazit.", - "viewMode": "Režim zobrazení" + "viewMode": "Režim zobrazení", + "renameMessage": "Zadejte nový název:", + "renameMessageInvalid": "Soubor nemůže obsahovat \"/\" nebo \"\\\".", + "source": "Zdroj:{suffix}", + "deleteMessageMultiple": "Opravdu chcete odstranit soubor(y) {count}?", + "deleteMessageShare": "Jste si jisti, že chcete tuto sdílenou složku odstranit: {path} ?", + "lastModified": "Poslední úprava{suffix}", + "numberDirs": "Počet adresářů{suffix}", + "numberFiles": "Počet souborů{suffix}", + "renameMessageConflict": "Položka s názvem \"{filename}\" již existuje!", + "uploadSettingsChunked": "Maximální souběžné nahrávání: {maxConcurrentUpload}, velikost chunků: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Maximální souběžné nahrávání: {maxConcurrentUpload}, vypnuto hromadné nahrávání" }, "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." + "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.", + "noResults": "V indexovaném vyhledávání nebyly nalezeny žádné výsledky.", + "onlyFolders": "Pouze složky", + "onlyFiles": "Pouze soubory", + "showOptions": "Zobrazit možnosti", + "photos": "Fotografie", + "audio": "Audio", + "videos": "Videa", + "documents": "Dokumenty", + "archives": "Archivy", + "number": "Číslo", + "searchContext": "Kontext vyhledávání: {context}", + "notEnoughCharacters": "Nedostatek znaků pro vyhledávání (min. {minSearchLength})", + "typeToSearch": "Pro zahájení vyhledávání začněte psát {minSearchLength} nebo více znaků." }, "profileSettings": { "setDateFormat": "Nastavit formát data", @@ -289,7 +311,13 @@ "defaultThemeDescription": "default - Výchozí téma", "customTheme": "Vyberte si téma", "showSelectMultiple": "Zobrazení výběru více položek v kontextové nabídce na ploše", - "showSelectMultipleDescription": "Ve výchozím nastavení nemá kontextová nabídka na ploše možnost \"vybrat více\" jako v mobilním telefonu. Tato možnost se v kontextové nabídce zobrazuje vždy." + "showSelectMultipleDescription": "Ve výchozím nastavení nemá kontextová nabídka na ploše možnost \"vybrat více\" jako v mobilním telefonu. Tato možnost se v kontextové nabídce zobrazuje vždy.", + "defaultMediaPlayer": "Použití nativního přehrávače médií v prohlížeči", + "defaultMediaPlayerDescription": "Pro video a zvukové soubory používejte nativní přehrávač médií prohlížeče, nikoli přehrávač médií vue-plyr, který je součástí aplikace FileBrowser.", + "debugOfficeEditor": "Povolení režimu ladění OnlyOffice", + "debugOfficeEditorDescription": "Zobrazení vyskakovacího okna pro ladění s dalšími informacemi v editoru OnlyOffice.", + "fileViewerOptions": "Možnosti prohlížeče souborů", + "themeAndLanguage": "Téma a jazyk" }, "settings": { "enterPassword": "Zadejte heslo", @@ -316,10 +344,8 @@ "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í", @@ -388,7 +414,14 @@ "username": "Uživatelské jméno", "users": "Uživatelé", "adminOptions": "Možnosti administrátora", - "downloads": "Ke stažení na" + "downloads": "Ke stažení na", + "systemAdmin": "Systém a správce", + "configViewer": "Prohlížeč konfigurace", + "configViewerShowFull": "Zobrazit úplnou konfiguraci", + "configViewerShowComments": "Zobrazit komentáře", + "configViewerLoadConfig": "Konfigurace načítání", + "brandingHelp": "Vzhled instance Průzkumníka souborů si můžete přizpůsobit změnou názvu, nahrazením loga, přidáním vlastních stylů a dokonce i zakázáním externích odkazů na GitHub.\nDalší informace o vlastním brandingu naleznete na stránce {0}.", + "commandRunnerHelp": "Zde můžete nastavit příkazy, které se provedou v pojmenovaných událostech. Na každý řádek musíte napsat jeden. K dispozici budou proměnné prostředí {0} a {1}, přičemž {0} bude relativní vůči {1}. Další informace o této funkci a dostupných proměnných prostředí naleznete na {2}." }, "sidebar": { "help": "Nápověda", @@ -513,15 +546,19 @@ "name": "Název", "accessManagement": "Správa přístupu", "all": "Odmítnout vše", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "To ukazuje, co se stane, když se uživatel pokusí získat přístup k cestě, kde neexistují žádná specifická pravidla přístupu. \"Povolit\" znamená, že uživatelé mají ve výchozím nastavení přístup, \"Odmítnout\" znamená, že uživatelé jsou ve výchozím nastavení blokováni, pokud není výslovně povoleno." + "defaultBehaviorDescription": "To ukazuje, co se stane, když se uživatel pokusí získat přístup k cestě, kde neexistují žádná specifická pravidla přístupu. \"Povolit\" znamená, že uživatelé mají ve výchozím nastavení přístup, \"Odmítnout\" znamená, že uživatelé jsou ve výchozím nastavení blokováni, pokud není výslovně povoleno.", + "defaultBehavior": "Výchozí akce{suffix}" }, "editor": { "uninitialized": "Nepodařilo se inicializovat editor. Prosím, načtěte jej znovu.", "saveFailed": "Soubor se nepodařilo uložit. Zkuste to prosím znovu.", "saveAborted": "Uložení přerušeno. Název souboru neodpovídá aktivnímu souboru.", "saveDisabled": "Ukládání sdílených souborů není podporováno.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Operace ukládání byla přerušena. Aktivní soubor aplikace ({activeFile}) neodpovídá souboru, který se pokoušíte uložit ({tryingToSave}).", + "syncFailed": "po 5 pokusech se nepodařilo synchronizovat stav s trasou pro \"{filename}\". Přerušení nastavení editoru, aby se zabránilo poškození dat." + }, + "player": { + "LoopEnabled": "Povolená smyčka", + "LoopDisabled": "Vypnutá smyčka" } } diff --git a/frontend/src/i18n/de.json b/frontend/src/i18n/de.json index c84fe5814..249ab7d40 100644 --- a/frontend/src/i18n/de.json +++ b/frontend/src/i18n/de.json @@ -49,7 +49,8 @@ "clearCompleted": "Abgeschlossene löschen", "showMore": "Mehr anzeigen", "showLess": "Weniger anzeigen", - "openParentFolder": "Übergeordneten Ordner öffnen" + "openParentFolder": "Übergeordneten Ordner öffnen", + "selectedCount": "Anzahl der zur Durchführung einer Aktion ausgewählten Elemente" }, "download": { "downloadFile": "Datei herunterladen", @@ -84,15 +85,15 @@ "click": "Wähle Datei oder Ordner", "ctrl": { "click": "Markiere mehrere Dateien oder Ordner", - "f": "Öffnet eine neue Suche", - "s": "Speichert eine Datei oder einen Ordner am akutellen Ort" + "d": "ausgewählte Artikel herunterladen" }, "del": "Löscht die ausgewählten Elemente", - "doubleClick": "Öffnet eine Datei oder einen Ordner", "esc": "Auswahl zurücksetzen und/oder Dialog schließen", - "f1": "Diese Informationsseite", "f2": "Datei umbenennen", - "help": "Hilfe" + "help": "Hilfe", + "f1": "Hilfeprompt anzeigen", + "description": "Nachstehend finden Sie die grundlegenden Navigationsoptionen. Für zusätzliche Informationen besuchen Sie bitte", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -134,16 +135,13 @@ "prompts": { "copy": "Kopieren", "copyMessage": "Wählen Sie einen Zielort zum Kopieren:", - "deleteMessageMultiple": "Sind Sie sicher, dass Sie {count} Datei(en) löschen möchten?", "deleteMessageSingle": "Sind Sie sicher, dass Sie diesen Ordner/diese Datei löschen möchten?", - "deleteMessageShare": "Sind Sie sicher, dass Sie diese Freigabe löschen möchten ({path})?", "deleteTitle": "Dateien löschen", "displayName": "Anzeigename:", "download": "Dateien herunterladen", "downloadMessage": "Wählen Sie ein Format zum Herunterladen aus.", "error": "Etwas ist schief gelaufen", "fileInfo": "Dateiinformation", - "lastModified": "Zuletzt geändert{suffix}", "move": "Verschieben", "moveMessage": "Wählen Sie einen neuen Ort für ihre Datei(en)/Ordner:", "newArchetype": "Erstelle neuen Beitrag auf Basis eines Archetyps. Ihre Datei wird im Inhalteordner erstellt.", @@ -151,10 +149,7 @@ "newDirMessage": "Geben Sie den Namen des neuen Ordners an.", "newFile": "Neue Datei", "newFileMessage": "Geben Sie den Namen der neuen Datei an.", - "numberDirs": "Anzahl der Ordner{suffix}", - "numberFiles": "Anzahl der Dateien{suffix}", "rename": "Umbenennen", - "renameMessage": "Geben Sie einen Namen ein für", "replace": "Ersetzen", "replaceMessage": "Eine der Datei mit dem gleichen Namen, wie die Sie hochladen wollen, existiert bereits. Soll die vorhandene Datei übersprungen oder ersetzt werden?\n", "schedule": "Zeitplan", @@ -172,10 +167,6 @@ "dragAndDrop": "Dateien hierher ziehen und ablegen", "completed": "Abgeschlossen", "conflictsDetected": "Erkannte Widersprüche", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Ungültiger Name angegeben: kann nicht Container \"/\" oder \"\\\"", - "source": "Source{suffix}", "downloadsLimit": "Limit für die Anzahl der Downloads", "maxBandwidth": "Maximale Download-Bandbreite (kbps)", "shareTheme": "Thema teilen", @@ -186,22 +177,44 @@ "shareBanner": "Banner-URL freigeben", "shareFavicon": "Favicon-URL freigeben", "optionalPasswordHelp": "Falls festgelegt, müssen die Benutzer dieses Kennwort eingeben, um die freigegebenen Inhalte anzuzeigen.", - "viewMode": "Ansichtsmodus" + "viewMode": "Ansichtsmodus", + "renameMessage": "Geben Sie einen neuen Namen ein:", + "renameMessageInvalid": "Eine Datei kann nicht den Container \"/\" oder \"\\\" enthalten.", + "source": "Quelle{suffix}", + "deleteMessageMultiple": "Sind Sie sicher, dass Sie die Datei(en) {count} löschen möchten?", + "deleteMessageShare": "Sind Sie sicher, dass Sie diese Freigabe löschen möchten: {path} ?", + "lastModified": "Zuletzt modifiziert{suffix}", + "numberDirs": "Anzahl der Verzeichnisse{suffix}", + "numberFiles": "Anzahl der Dateien{suffix}", + "renameMessageConflict": "Ein Artikel mit dem Namen \"{filename}\" existiert bereits!", + "uploadSettingsChunked": "Max. gleichzeitige Uploads: {maxConcurrentUpload}, Chunk-Größe: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Max. gleichzeitige Uploads: {maxConcurrentUpload}, Chunked Upload deaktiviert" }, "search": { "images": "Bilder", "music": "Musik", "pdf": "PDF", - "pressToSearch": "Drücken Sie Enter, um zu suchen...", "search": "Suche...", - "typeToSearch": "Tippen, um zu suchen...", "types": "Typen", "video": "Video", "path": "Pfad:", "smallerThan": "Kleiner als:", "largerThan": "Größer als:", "helpText1": "Die Suche erfolgt bei jedem Zeichen, das Sie eingeben (mindestens 3 Zeichen für Suchbegriffe).", - "helpText2": "Der Index: Die Suche verwendet den Index, der automatisch in dem konfigurierten Intervall aktualisiert wird (Standard: 5 Minuten). Eine Suche direkt nach dem Start des Programms kann zu unvollständigen Ergebnissen führen." + "helpText2": "Der Index: Die Suche verwendet den Index, der automatisch in dem konfigurierten Intervall aktualisiert wird (Standard: 5 Minuten). Eine Suche direkt nach dem Start des Programms kann zu unvollständigen Ergebnissen führen.", + "noResults": "Keine Ergebnisse in der indizierten Suche gefunden.", + "onlyFolders": "Nur Ordner", + "onlyFiles": "Nur Dateien", + "showOptions": "Optionen anzeigen", + "photos": "Fotos", + "audio": "Audio", + "videos": "Videos", + "documents": "Dokumente", + "archives": "Archiv", + "number": "Nummer", + "searchContext": "Suche im Kontext: {context}", + "notEnoughCharacters": "Nicht genügend Zeichen für die Suche (min {minSearchLength})", + "typeToSearch": "Beginnen Sie mit der Eingabe von {minSearchLength} oder mehr Zeichen, um die Suche zu starten." }, "settings": { "admin": "Admin", @@ -214,10 +227,8 @@ "avoidChanges": "(leer lassen, um Änderungen zu vermeiden)", "branding": "Branding", "brandingDirectoryPath": "Branding-Verzeichnispfad", - "brandingHelp": "Sie können das Erscheinungsbild Ihres FileBrowser anpassen, indem Sie den Namen ändern, das Logo austauschen oder eigene Branding definieren bzw. sogar externe Links zu GitHub deaktivieren.\nUm mehr Informationen zum Anpassen des Designs zu bekommen, gehen Sie bitte zu {0}.", "changePassword": "Kennwort ändern", "commandRunner": "Befehlseingabe", - "commandRunnerHelp": "Hier könne Sie Befehle eintragen, welche bei den benannten Aktionen ausgeführt werden. Sie müssen pro Zeile jeweils einen Befehl eingeben. Die Umgebungsvariablen {0} und {1} sind verfügbar, wobei {0} relativ zu {1} ist. Für mehr Informationen über diese Funktion und die verfügbaren Umgebungsvariablen lesen Sie bitte die {2}.", "commandsUpdated": "Befehle aktualisiert!", "createUserDir": "Automatisches Erstellen des Home-Verzeichnisses beim Anlegen neuer Benutzer", "tusUploads": "Gestückeltes Hochladen", @@ -300,7 +311,14 @@ "adminOptions": "Optionen für Verwaltungsbenutzer", "shareDuration": "Abgelaufen", "searchOptions": "Suchoptionen", - "downloads": "Herunterladen" + "downloads": "Herunterladen", + "systemAdmin": "System & Verwaltung", + "configViewer": "Config Viewer", + "configViewerShowFull": "Vollständige Konfiguration anzeigen", + "configViewerShowComments": "Kommentare anzeigen", + "configViewerLoadConfig": "Lastkonfiguration", + "brandingHelp": "Sie können das Aussehen Ihrer Dateibrowser-Instanz anpassen, indem Sie den Namen ändern, das Logo ersetzen, eigene Stile hinzufügen und sogar externe Links zu GitHub deaktivieren.\nWeitere Informationen zum benutzerdefinierten Branding finden Sie unter {0}.", + "commandRunnerHelp": "Hier können Sie Befehle eingeben, die bei den genannten Ereignissen ausgeführt werden. Sie müssen einen pro Zeile schreiben. Die Umgebungsvariablen {0} und {1} werden verfügbar sein, wobei {0} relativ zu {1} ist. Weitere Informationen über diese Funktion und die verfügbaren Umgebungsvariablen finden Sie unter {2}." }, "sidebar": { "help": "Hilfe", @@ -358,12 +376,21 @@ "hideNavButtons": "Navigationsschaltflächen ausblenden", "hideNavButtonsDescription": "Blenden Sie die Navigationsschaltflächen auf der Navigationsleiste in der Freigabe aus, um ein minimalistisches Aussehen zu erzielen.", "viewModeDescription": "Standardlayout für die gemeinsame Seite.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Sie haben eine Aktie zur Ansicht oder zum Download erhalten.", "disableShareCard": "Freigabekarte deaktivieren", "disableShareCardDescription": "Deaktivieren Sie die Freigabekarte auf der freigegebenen Seite in der Seitenleiste oder oben auf der Seite auf dem Handy.", "disableSidebar": "Seitenleiste deaktivieren", - "disableSidebarDescription": "Deaktivieren Sie die Seitenleiste auf der gemeinsamen Seite." + "disableSidebarDescription": "Deaktivieren Sie die Seitenleiste auf der gemeinsamen Seite.", + "enforceDarkLightMode": "Themenmodus erzwingen", + "enforceDarkLightModeDescription": "Erzwingt einen bestimmten Themenmodus (dunkel oder hell) für diese Freigabe und setzt die Benutzereinstellungen außer Kraft.", + "default": "Keinen Modus erzwingen", + "dark": "Dunkelheit", + "light": "Licht", + "enableOnlyOffice": "OnlyOffice viewer aktivieren", + "enableOnlyOfficeDescription": "Erlauben Sie die Anzeige von Office-Dateien mit OnlyOffice in dieser Freigabe.", + "enableOnlyOfficeEditing": "OnlyOffice-Bearbeitung aktivieren", + "enableOnlyOfficeEditingDescription": "Erlauben Sie die Bearbeitung von Office-Dateien mit OnlyOffice in dieser Freigabe.", + "titleDefault": "Gemeinsame Dateien - {title}" }, "api": { "title": "API-Schlüssel:", @@ -447,8 +474,8 @@ "totalDenied": "Verweigert", "totalAllowed": "Erlaubt", "all": "Alle ablehnen", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Dies zeigt, was passiert, wenn ein Benutzer versucht, auf einen Pfad zuzugreifen, für den keine spezifischen Zugriffsregeln bestehen. Zulassen\" bedeutet, dass Benutzer standardmäßig Zugang haben, \"Verweigern\" bedeutet, dass Benutzer standardmäßig blockiert werden, wenn sie nicht ausdrücklich zugelassen sind." + "defaultBehaviorDescription": "Dies zeigt, was passiert, wenn ein Benutzer versucht, auf einen Pfad zuzugreifen, für den keine spezifischen Zugriffsregeln bestehen. Zulassen\" bedeutet, dass Benutzer standardmäßig Zugang haben, \"Verweigern\" bedeutet, dass Benutzer standardmäßig blockiert werden, wenn sie nicht ausdrücklich zugelassen sind.", + "defaultBehavior": "Standard-Aktion{suffix}" }, "fileLoading": { "title": "Uploads & Downloads", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - Das Standardthema", "customTheme": "Wählen Sie Ihr Thema", "showSelectMultiple": "Mehrfachauswahl im Kontextmenü auf dem Desktop anzeigen", - "showSelectMultipleDescription": "Standardmäßig verfügt das Desktop-Kontextmenü nicht über die Option \"Mehrere auswählen\" wie bei mobilen Geräten. Diese Option wird immer im Kontextmenü angezeigt." + "showSelectMultipleDescription": "Standardmäßig verfügt das Desktop-Kontextmenü nicht über die Option \"Mehrere auswählen\" wie bei mobilen Geräten. Diese Option wird immer im Kontextmenü angezeigt.", + "defaultMediaPlayer": "Verwenden Sie den nativen Media Player Ihres Browsers", + "defaultMediaPlayerDescription": "Verwenden Sie für Video- und Audiodateien den nativen Mediaplayer Ihres Browsers und nicht den in FileBrowser enthaltenen 'vue-plyr' Mediaplayer.", + "debugOfficeEditor": "OnlyOffice-Debug-Modus einschalten", + "debugOfficeEditorDescription": "Ein Debug-Popup mit zusätzlichen Informationen im OnlyOffice-Editor anzeigen.", + "fileViewerOptions": "Optionen des Dateibetrachters", + "themeAndLanguage": "Thema und Sprache" }, "editor": { "uninitialized": "Der Editor konnte nicht initialisiert werden. Bitte neu laden.", "saveFailed": "Datei konnte nicht gespeichert werden. Bitte versuchen Sie es erneut.", "saveAborted": "Speichern abgebrochen. Der Dateiname stimmt nicht mit der aktiven Datei überein.", "saveDisabled": "Das Speichern von gemeinsam genutzten Dateien wird nicht unterstützt.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Der Speichervorgang wurde abgebrochen. Die aktive Datei der Anwendung ({activeFile}) stimmt nicht mit der Datei überein, die Sie zu speichern versuchen ({tryingToSave}).", + "syncFailed": "Nach 5 Versuchen ist es nicht gelungen, den Status mit der Route für \"{filename}\" zu synchronisieren. Abbruch der Editor-Einrichtung, um Datenbeschädigung zu vermeiden." + }, + "player": { + "LoopEnabled": "Schleife Aktiviert", + "LoopDisabled": "Schleife Deaktiviert" } } diff --git a/frontend/src/i18n/el.json b/frontend/src/i18n/el.json index 801876156..9df4f1961 100644 --- a/frontend/src/i18n/el.json +++ b/frontend/src/i18n/el.json @@ -49,7 +49,8 @@ "clearCompleted": "Εκκαθάριση ολοκληρώθηκε", "showMore": "Εμφάνιση περισσότερων", "showLess": "Εμφάνιση λιγότερων", - "openParentFolder": "Άνοιγμα γονικού φακέλου" + "openParentFolder": "Άνοιγμα γονικού φακέλου", + "selectedCount": "Αριθμός επιλεγμένων στοιχείων για την εκτέλεση μιας ενέργειας" }, "download": { "downloadFile": "Λήψη αρχείου", @@ -87,15 +88,15 @@ "click": "επιλέξτε αρχείο ή φάκελο", "ctrl": { "click": "επιλογή πολλαπλών αρχείων ή φακέλων", - "f": "ανοίγει την αναζήτηση", - "s": "αποθηκεύει ένα αρχείο ή εκκινεί λήψη του φακέλου στον οποίο βρίσκεστε" + "d": "λήψη επιλεγμένων στοιχείων" }, "del": "διαγραφή επιλεγμένων στοιχείων", - "doubleClick": "ανοίγει ένα αρχείο ή φάκελο", "esc": "καθαρίζει την επιλογή ή/και κλείνει το παράθυρο", - "f1": "αυτή η πληροφορία", "f2": "μετονομασία αρχείου", - "help": "Βοήθεια" + "help": "Βοήθεια", + "f1": "Εμφάνιση προτροπής βοήθειας", + "description": "Μπορείτε να δείτε τις βασικές επιλογές πλοήγησης παρακάτω. Για πρόσθετες πληροφορίες, επισκεφθείτε τη διεύθυνση", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -137,16 +138,13 @@ "prompts": { "copy": "Αντιγραφή", "copyMessage": "Επιλέξτε τοποθεσία για αντιγραφή των αρχείων σας:", - "deleteMessageMultiple": "Είστε σίγουροι ότι θέλετε να διαγράψετε {count} αρχεία;", "deleteMessageSingle": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το αρχείο/φάκελο;", - "deleteMessageShare": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή την κοινοποίηση ({path});", "deleteTitle": "Διαγραφή αρχείων", "displayName": "Εμφάνιση ονόματος:", "download": "Λήψη αρχείων", "downloadMessage": "Επιλέξτε τη μορφή που θέλετε να λάβετε.", "error": "Προέκυψε κάποιο σφάλμα", "fileInfo": "Πληροφορίες αρχείου", - "lastModified": "Τελευταία τροποποίηση{suffix}", "move": "Μετακίνηση", "moveMessage": "Επιλέξτε νέα τοποθεσία για τα αρχεία / τους φακέλους σας:", "newArchetype": "Δημιουργία νέας ανάρτησης με βάση έναν αρχέτυπο. Το αρχείο σας θα δημιουργηθεί στο φάκελο περιεχομένου.", @@ -154,10 +152,7 @@ "newDirMessage": "Γράψτε το όνομα του νέου φακέλου.", "newFile": "Νέο αρχείο", "newFileMessage": "Γράψτε το όνομα του νέου αρχείου.", - "numberDirs": "Αριθμός φακέλων{suffix}", - "numberFiles": "Αριθμός αρχείων{suffix}", "rename": "Μετονομασία", - "renameMessage": "Εισαγάγετε ένα νέο όνομα για το", "replace": "Αντικατάσταση", "replaceMessage": "Ένα από τα αρχεία που προσπαθείτε να μεταφορτώσετε δημιουργεί σύγκρουση με υπάρχον αρχείο λόγω του ονόματός του. Θέλετε να συνεχίσετε τη μεταφόρτωση ή να αντικαταστήσετε το υπάρχον;\n", "schedule": "Προγραμματισμός", @@ -175,10 +170,6 @@ "dragAndDrop": "Σύρετε και αφήστε αρχεία εδώ", "completed": "Ολοκληρωμένο", "conflictsDetected": "Ανιχνεύονται συγκρούσεις", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Μη έγκυρο όνομα: δεν μπορεί να γίνει container \"/\" ή \"\\\"", - "source": "Source{suffix}", "downloadsLimit": "Όριο αριθμού λήψεων", "maxBandwidth": "Μέγιστο εύρος ζώνης λήψης (kbps)", "shareTheme": "Μοιραστείτε το θέμα", @@ -189,22 +180,44 @@ "shareBanner": "Μοιραστείτε το URL του banner", "shareFavicon": "Μοιραστείτε το URL favicon", "optionalPasswordHelp": "Εάν οριστεί, οι χρήστες πρέπει να εισάγουν αυτόν τον κωδικό πρόσβασης για να προβάλουν το κοινόχρηστο περιεχόμενο.", - "viewMode": "Λειτουργία προβολής" + "viewMode": "Λειτουργία προβολής", + "renameMessage": "Εισάγετε ένα νέο όνομα:", + "renameMessageInvalid": "Ένα αρχείο δεν μπορεί να περιέχει \"/\" ή \"\\\"", + "source": "Πηγή{suffix}", + "deleteMessageMultiple": "Είστε σίγουροι ότι θέλετε να διαγράψετε το/τα αρχείο/α {count};", + "deleteMessageShare": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το μερίδιο: {path} ?", + "lastModified": "Τελευταία τροποποίηση{suffix}", + "numberDirs": "Αριθμός καταλόγων{suffix}", + "numberFiles": "Αριθμός αρχείων{suffix}", + "renameMessageConflict": "Ένα στοιχείο με όνομα \"{filename}\" υπάρχει ήδη!", + "uploadSettingsChunked": "Μέγιστες ταυτόχρονες μεταφορτώσεις: {maxConcurrentUpload}, μέγεθος τεμαχίου: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Μέγιστες ταυτόχρονες μεταφορτώσεις: {maxConcurrentUpload}, chunked upload disabled" }, "search": { "images": "Εικόνες", "music": "Μουσική", "pdf": "PDF", - "pressToSearch": "Πατήστε Enter για αναζήτηση…", "search": "Αναζήτηση…", - "typeToSearch": "Πληκτρολογήστε για αναζήτηση…", "types": "Τύποι", "video": "Βίντεο", "path": "Μονοπάτι:", "smallerThan": "Μικρότερο από:", "largerThan": "Μεγαλύτερο από:", "helpText1": "Η αναζήτηση πραγματοποιείται σε κάθε χαρακτήρα που πληκτρολογείτε (ελάχιστο όριο 3 χαρακτήρων για τους όρους αναζήτησης).", - "helpText2": "Ο δείκτης: Η αναζήτηση χρησιμοποιεί το ευρετήριο το οποίο ενημερώνεται αυτόματα στο ρυθμισμένο διάστημα (προεπιλογή: 5 λεπτά). Η αναζήτηση όταν το πρόγραμμα έχει μόλις ξεκινήσει μπορεί να οδηγήσει σε ελλιπή αποτελέσματα." + "helpText2": "Ο δείκτης: Η αναζήτηση χρησιμοποιεί το ευρετήριο το οποίο ενημερώνεται αυτόματα στο ρυθμισμένο διάστημα (προεπιλογή: 5 λεπτά). Η αναζήτηση όταν το πρόγραμμα έχει μόλις ξεκινήσει μπορεί να οδηγήσει σε ελλιπή αποτελέσματα.", + "noResults": "Δεν βρέθηκαν αποτελέσματα στην ευρετηριασμένη αναζήτηση.", + "onlyFolders": "Μόνο φάκελοι", + "onlyFiles": "Μόνο αρχεία", + "showOptions": "Εμφάνιση επιλογών", + "photos": "Φωτογραφίες", + "audio": "Ήχος", + "videos": "Βίντεο", + "documents": "Έγγραφα", + "archives": "Αρχεία", + "number": "Αριθμός", + "searchContext": "Πλαίσιο αναζήτησης: {context}", + "notEnoughCharacters": "Δεν υπάρχουν αρκετοί χαρακτήρες για αναζήτηση (min {minSearchLength})", + "typeToSearch": "Αρχίστε να πληκτρολογείτε {minSearchLength} ή περισσότερους χαρακτήρες για να ξεκινήσετε την αναζήτηση." }, "settings": { "admin": "Διαχειριστής", @@ -217,10 +230,8 @@ "avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)", "branding": "Εξατομίκευση", "brandingDirectoryPath": "Διαδρομή φακέλου εξατομίκευσης", - "brandingHelp": "Μπορείτε να προσαρμόσετε την εμφάνισης της εφαρμογής File Browser αλλάζοντας το όνομά της, αντικαθιστώντας το λογότυπό της, προσθέτοντας προσαρμοσμένα στυλ και ακόμα και απενεργοποιώντας εξωτερικούς συνδέσμους προς το GitHub.\nΓια περισσότερες πληροφορίες σχετικά με αυτές τις προσαρμογές, ελέγξτε το {0}.", "changePassword": "Αλλαγή κωδικού πρόσβασης", "commandRunner": "Εκτέλεση εντολών", - "commandRunnerHelp": "Εδώ μπορείτε να ορίσετε εντολές που εκτελούνται στα ονομασμένα γεγονότα και δραστηριότητες. Πρέπει να γράψετε μία εντολή ανά γραμμή. Οι μεταβλητές περιβάλλοντος {0} και {1} θα είναι διαθέσιμες, και θα είναι {0} σχετικές με το {1}. Για περισσότερες πληροφορίες σχετικά με αυτή τη λειτουργία και τις διαθέσιμες μεταβλητές περιβάλλοντος, παρακαλώ διαβάστε το {2}.", "commandsUpdated": "Οι εντολές ενημερώθηκαν!", "createUserDir": "Αυτόματη δημιουργία φακέλου χρήστη κατά την προσθήκη νέου χρήστη", "tusUploads": "Τμηματικές μεταφορές αρχείων", @@ -303,7 +314,14 @@ "adminOptions": "Επιλογές χρήστη διαχειριστή", "shareDuration": "Λήγει", "searchOptions": "Επιλογές αναζήτησης", - "downloads": "Λήψεις" + "downloads": "Λήψεις", + "systemAdmin": "Σύστημα & Διαχείριση", + "configViewer": "Config Viewer", + "configViewerShowFull": "Εμφάνιση πλήρων ρυθμίσεων", + "configViewerShowComments": "Εμφάνιση σχολίων", + "configViewerLoadConfig": "Φόρτωση Config", + "brandingHelp": "Μπορείτε να προσαρμόσετε την εμφάνιση και την αίσθηση του File Browser αλλάζοντας το όνομά του, αντικαθιστώντας το λογότυπο, προσθέτοντας προσαρμοσμένα στυλ και ακόμη και απενεργοποιώντας τους εξωτερικούς συνδέσμους προς το GitHub.\nΓια περισσότερες πληροφορίες σχετικά με την προσαρμοσμένη επωνυμία, ανατρέξτε στη διεύθυνση {0}.", + "commandRunnerHelp": "Εδώ μπορείτε να ορίσετε τις εντολές που εκτελούνται στα συμβάντα που ονομάζονται. Πρέπει να γράψετε μία ανά γραμμή. Οι περιβαλλοντικές μεταβλητές {0} και {1} θα είναι διαθέσιμες, ενώ η {0} θα είναι σχετική με την {1}. Για περισσότερες πληροφορίες σχετικά με αυτή τη λειτουργία και τις διαθέσιμες μεταβλητές περιβάλλοντος, διαβάστε το {2}." }, "sidebar": { "help": "Βοήθεια", @@ -358,12 +376,21 @@ "hideNavButtons": "Απόκρυψη κουμπιών πλοήγησης", "hideNavButtonsDescription": "Κρύψτε τα κουμπιά πλοήγησης στη γραμμή πλοήγησης στο μερίδιο για να δημιουργήσετε μια μινιμαλιστική εμφάνιση.", "viewModeDescription": "Προεπιλεγμένη διάταξη για την κοινή σελίδα.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Σας έχει αποσταλεί ένα share για να το δείτε ή να το κατεβάσετε.", "disableShareCard": "Απενεργοποίηση κάρτας κοινής χρήσης", "disableShareCardDescription": "Απενεργοποιήστε την κάρτα κοινής χρήσης στην κοινόχρηστη σελίδα στην πλαϊνή μπάρα ή στην κορυφή της σελίδας στο κινητό.", "disableSidebar": "Απενεργοποίηση πλευρικής μπάρας", - "disableSidebarDescription": "Απενεργοποιήστε την πλαϊνή μπάρα στην κοινή σελίδα." + "disableSidebarDescription": "Απενεργοποιήστε την πλαϊνή μπάρα στην κοινή σελίδα.", + "enforceDarkLightMode": "Επιβολή λειτουργίας θέματος", + "enforceDarkLightModeDescription": "Επιβάλλει μια συγκεκριμένη λειτουργία θέματος (σκούρο ή ανοιχτό) για αυτό το κοινόχρηστο, παρακάμπτοντας τις προτιμήσεις του χρήστη.", + "default": "Μην επιβάλλετε μια λειτουργία", + "dark": "Σκούρο", + "light": "Φως", + "enableOnlyOffice": "Ενεργοποίηση της προβολής OnlyOffice", + "enableOnlyOfficeDescription": "Επιτρέψτε την προβολή αρχείων γραφείου χρησιμοποιώντας το OnlyOffice σε αυτό το κοινόχρηστο χώρο.", + "enableOnlyOfficeEditing": "Ενεργοποίηση της επεξεργασίας OnlyOffice", + "enableOnlyOfficeEditingDescription": "Επιτρέψτε την επεξεργασία αρχείων γραφείου χρησιμοποιώντας το OnlyOffice σε αυτό το κοινόχρηστο χώρο.", + "titleDefault": "Κοινόχρηστα αρχεία - {title}" }, "api": { "title": "Κλειδιά API:", @@ -447,8 +474,8 @@ "totalDenied": "Απορρίπτεται", "totalAllowed": "Επιτρεπόμενο", "all": "Άρνηση όλων", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Αυτό δείχνει τι συμβαίνει όταν ένας χρήστης προσπαθεί να αποκτήσει πρόσβαση σε μια διαδρομή όπου δεν υπάρχουν συγκεκριμένοι κανόνες πρόσβασης. 'Allow' σημαίνει ότι οι χρήστες μπορούν να έχουν πρόσβαση από προεπιλογή, 'Deny' σημαίνει ότι οι χρήστες αποκλείονται από προεπιλογή, εκτός αν επιτρέπεται ρητά." + "defaultBehaviorDescription": "Αυτό δείχνει τι συμβαίνει όταν ένας χρήστης προσπαθεί να αποκτήσει πρόσβαση σε μια διαδρομή όπου δεν υπάρχουν συγκεκριμένοι κανόνες πρόσβασης. 'Allow' σημαίνει ότι οι χρήστες μπορούν να έχουν πρόσβαση από προεπιλογή, 'Deny' σημαίνει ότι οι χρήστες αποκλείονται από προεπιλογή, εκτός αν επιτρέπεται ρητά.", + "defaultBehavior": "Προεπιλεγμένη ενέργεια{suffix}" }, "fileLoading": { "title": "Ανεβάσματα & λήψεις", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - Το προεπιλεγμένο θέμα", "customTheme": "Επιλέξτε το θέμα σας", "showSelectMultiple": "Εμφάνιση πολλαπλών επιλογών στο μενού περιβάλλοντος στην επιφάνεια εργασίας", - "showSelectMultipleDescription": "Από προεπιλογή, το μενού περιβάλλοντος της επιφάνειας εργασίας δεν διαθέτει την επιλογή \"επιλογή πολλαπλών\" όπως το κινητό. Αυτή η επιλογή εμφανίζεται πάντα στο μενού περιβάλλοντος." + "showSelectMultipleDescription": "Από προεπιλογή, το μενού περιβάλλοντος της επιφάνειας εργασίας δεν διαθέτει την επιλογή \"επιλογή πολλαπλών\" όπως το κινητό. Αυτή η επιλογή εμφανίζεται πάντα στο μενού περιβάλλοντος.", + "defaultMediaPlayer": "Χρησιμοποιήστε το εγγενές πρόγραμμα αναπαραγωγής πολυμέσων του προγράμματος περιήγησής σας", + "defaultMediaPlayerDescription": "Χρησιμοποιήστε το εγγενές πρόγραμμα αναπαραγωγής πολυμέσων του προγράμματος περιήγησης για αρχεία βίντεο και ήχου αντί για το πρόγραμμα αναπαραγωγής πολυμέσων \"vue-plyr\" που περιλαμβάνεται στο FileBrowser.", + "debugOfficeEditor": "Ενεργοποίηση λειτουργίας εντοπισμού σφαλμάτων OnlyOffice", + "debugOfficeEditorDescription": "Εμφάνιση ενός αναδυόμενου παραθύρου εντοπισμού σφαλμάτων με πρόσθετες πληροφορίες στον επεξεργαστή OnlyOffice.", + "fileViewerOptions": "Επιλογές προβολής αρχείων", + "themeAndLanguage": "Θέμα & Γλώσσα" }, "editor": { "uninitialized": "Απέτυχε η αρχικοποίηση του συντάκτη. Παρακαλούμε επαναφορτώστε.", "saveFailed": "Απέτυχε η αποθήκευση του αρχείου. Προσπαθήστε ξανά.", "saveAborted": "Η αποθήκευση διακόπηκε. Το όνομα του αρχείου δεν ταιριάζει με το ενεργό αρχείο.", "saveDisabled": "Δεν υποστηρίζεται η αποθήκευση κοινόχρηστων αρχείων.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Διακοπή της λειτουργίας αποθήκευσης. Το ενεργό αρχείο της εφαρμογής ({activeFile}) δεν ταιριάζει με το αρχείο που προσπαθείτε να αποθηκεύσετε ({tryingToSave}).", + "syncFailed": "απέτυχε να συγχρονίσει την κατάσταση με τη διαδρομή για το \"{filename}\" μετά από 5 προσπάθειες. Διακοπή της ρύθμισης του συντάκτη για να αποφευχθεί η αλλοίωση των δεδομένων." + }, + "player": { + "LoopEnabled": "Ενεργοποίηση βρόχου", + "LoopDisabled": "Βρόχος απενεργοποιημένος" } } diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 0e882b4d5..22821c3a8 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -49,7 +49,8 @@ "clearCompleted": "Clear completed", "showMore": "Show more", "showLess": "Show less", - "openParentFolder": "Open parent folder" + "openParentFolder": "Open parent folder", + "selectedCount": "Number of items selected to perform an action" }, "download": { "downloadFile": "Download File", @@ -87,15 +88,15 @@ "click": "select file or directory", "ctrl": { "click": "select multiple files or directories", - "f": "opens search", - "s": "save a file or download the directory where you are" + "d": "download selected items" }, "del": "delete selected items", - "doubleClick": "open a file or directory", "esc": "clear selection and/or close the prompt", - "f1": "this information", "f2": "rename file", - "help": "Help" + "f1": "show help prompt", + "help": "Help", + "description": "You can view the basic naviagation options below. For additional information, please visit", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -166,7 +167,16 @@ "disableShareCard": "Disable share card", "disableShareCardDescription": "Disable the share card on the shared page in the sidebar or at the top of the page on mobile.", "disableSidebar": "Disable sidebar", - "disableSidebarDescription": "Disable the sidebar on the shared page." + "disableSidebarDescription": "Disable the sidebar on the shared page.", + "enforceDarkLightMode": "Enforce theme mode", + "enforceDarkLightModeDescription": "Force a specific theme mode (dark or light) for this share, overriding user preferences.", + "default": "Do not enforce a mode", + "dark": "Dark", + "light": "Light", + "enableOnlyOffice": "Enable OnlyOffice viewer", + "enableOnlyOfficeDescription": "Allow viewing office files using OnlyOffice in this share.", + "enableOnlyOfficeEditing": "Enable OnlyOffice editing", + "enableOnlyOfficeEditingDescription": "Allow editing of office files using OnlyOffice in this share." }, "prompts": { "downloadsLimit": "Number of downloads limit", @@ -180,7 +190,6 @@ "shareBanner": "Share banner URL", "shareFavicon": "Share favicon URL", "source": "Source{suffix}", - "invalidName": "Invalid name provided: cannot container \"/\" or \"\\\"", "destinationSource": "Destination Source:", "copy": "Copy", "copyMessage": "Choose the place to copy your files:", @@ -209,7 +218,9 @@ "numberDirs": "Number of directories{suffix}", "numberFiles": "Number of files{suffix}", "rename": "Rename", - "renameMessage": "An item named \"{filename}\" already exists! Enter a new name:", + "renameMessageConflict": "An item named \"{filename}\" already exists!", + "renameMessage": "Enter a new name:", + "renameMessageInvalid": "A file can't container \"/\" or \"\\\"", "replace": "Replace", "replaceMessage": "One of the files you're trying to upload is conflicting because of its name. Do you wish to continue to upload or replace the existing one?\n", "schedule": "Schedule", @@ -226,12 +237,23 @@ "conflictsDetected": "Conflicts detected" }, "search": { + "searchContext": "Search Context: {context}", + "onlyFolders": "Only Folders", + "onlyFiles": "Only Files", + "showOptions": "Show Options", + "photos": "Photos", + "audio": "Audio", + "videos": "Videos", + "documents": "Documents", + "archives": "Archives", + "notEnoughCharacters": "Not enough characters to search (min {minSearchLength})", + "typeToSearch": "Start typing {minSearchLength} or more characters to begin searching.", "images": "Images", "music": "Music", "pdf": "PDF", - "pressToSearch": "No results found in indexed search.", "search": "Search...", - "typeToSearch": "Type to search... (3 character minimum)", + "number": "Number", + "noResults": "No results found in indexed search.", "path": "Path:", "types": "Types", "video": "Video", @@ -241,6 +263,10 @@ "helpText2": "The index: Search utilizes the index which automatically gets updated on the configured interval (default: 5 minutes). Searching when the program has just started may result in incomplete results." }, "profileSettings": { + "debugOfficeEditor": "Enable OnlyOffice Debug Mode", + "debugOfficeEditorDescription": "Show a debug popup with additional information in the OnlyOffice editor.", + "defaultMediaPlayer": "Use your browser's native media player", + "defaultMediaPlayerDescription": "Use your browser's native media player for video and audio files rather than FileBrowser's included 'vue-plyr' media player.", "showSelectMultiple": "Show select multiple in context menu on desktop", "showSelectMultipleDescription": "By default, desktop context menu does not have \"select multiple\" option like mobile. This option always shows it in the context menu.", "customTheme": "Choose your theme", @@ -289,9 +315,16 @@ "disablePreviewExt": "Disable office file previews for certain file extensions", "disablePreviewExtDescription": "A space-separated list of file extensions to disable office file previews for (e.g., '.docx .pptx').", "disableOfficeEditorDescription": "A space-separated list of file extensions to disable the OnlyOffice editor and viewer for. (e.g., '.txt .html .pdf').", - "disableOfficeEditor": "Disable office viewer for certain file extensions" + "disableOfficeEditor": "Disable office viewer for certain file extensions", + "fileViewerOptions": "File Viewer Options", + "themeAndLanguage": "Theme & Language" }, "settings": { + "systemAdmin": "System & Admin", + "configViewer": "Config Viewer", + "configViewerShowFull": "Show full config", + "configViewerShowComments": "Show comments", + "configViewerLoadConfig": "Load Config", "enterPassword": "Enter password", "enterPasswordAgain": "Enter the password again", "passwordsDoNotMatch": "Passwords do not match", @@ -523,5 +556,9 @@ "saveDisabled":"Saving shared files is not supported.", "saveAbortedMessage":"Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", "syncFailed":"failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + }, + "player": { + "LoopEnabled": "Loop Enabled", + "LoopDisabled": "Loop Disabled" } -} \ No newline at end of file +} diff --git a/frontend/src/i18n/es.json b/frontend/src/i18n/es.json index 96ef26577..56b106e6f 100644 --- a/frontend/src/i18n/es.json +++ b/frontend/src/i18n/es.json @@ -31,25 +31,26 @@ "share": "Compartir", "shell": "Presiona Enter para buscar...", "submit": "Enviar", - "switchView": "Cambiar vista", - "toggleSidebar": "Alternar menú", + "switchView": "Cambiar modo de visualización", + "toggleSidebar": "Cambiar visibilidad del panel lateral", "update": "Actualizar", "upload": "Subir", "openFile": "Abrir archivo", "copyDownloadLinkToClipboard": "Copiar enlace de descarga", "continue": "Continuar", "edit": "Editar", - "generateNewOtp": "Generar nuevo código 2FA", + "generateNewOtp": "Generar nuevo código de dos factores (2FA)", "verify": "Verificar", "pause": "Pausar", "resume": "Reanudar", "retry": "Reintentar", "pauseAll": "Pausar todo", "resumeAll": "Reanudar todo", - "clearCompleted": "Limpiar lista", + "clearCompleted": "Limpiar completados", "showMore": "Ver más", "showLess": "Mostrar menos", - "openParentFolder": "Abrir carpeta principal" + "openParentFolder": "Abrir carpeta principal", + "selectedCount": "Número de elementos seleccionados para realizar una acción" }, "download": { "downloadFile": "Descargar archivo", @@ -61,7 +62,7 @@ "internal": "Error interno :(", "notFound": "No se encontró el directorio", "connection": "No se puede conectar al servidor.", - "shareNotFound": "Acción no válida o caducada." + "shareNotFound": "No se encuentra lo que buscas, tal vez el enlace expiró." }, "files": { "body": "Contenido", @@ -75,7 +76,7 @@ "multipleSelectionEnabled": "Selección múltiple activada", "name": "Nombre", "size": "Tamaño", - "sortByLastModified": "Ordenar por modificación", + "sortByLastModified": "Ordenar por fecha de modificación", "sortByName": "Ordenar por nombre", "sortBySize": "Ordenar por tamaño", "noPreview": "Vista previa no disponible" @@ -84,15 +85,15 @@ "click": "Selecciona un archivo o carpeta", "ctrl": { "click": "Selecciona varios archivos o carpetas (Selección multiple)", - "f": "Abrir panel de búsqueda", - "s": "Guardar archivo (solo funciona si estás en el editor)" + "d": "descargar artículos seleccionados" }, "del": "Eliminar archivos/carpetas seleccionados", - "doubleClick": "Abre un archivo o carpeta", "esc": "Cancela la selección o cierra la ventana actual (ej: Para salir rápido cuando estas viendo una imagen o video)", - "f1": "Esta ventana", - "f2": "Cambiar nombre del archivo", - "help": "Ayuda" + "f2": "Cambiar nombre del archivo o carpeta", + "help": "Ayuda", + "f1": "mostrar mensaje de ayuda", + "description": "A continuación puede ver las opciones básicas de navegación. Para más información, visite", + "wiki": "Wiki de FileBrowser Quantum" }, "languages": { "he": "עברית", @@ -134,16 +135,13 @@ "prompts": { "copy": "Copiar", "copyMessage": "Selecciona una ubicación para copiar", - "deleteMessageMultiple": "¿Estás seguro que quieres eliminar {count} archivo(s)?", "deleteMessageSingle": "¿Estás seguro que quieres eliminar este archivo/carpeta?", - "deleteMessageShare": "¿Estás seguro que quieres eliminar este recurso compartido ({path})?", "deleteTitle": "Borrar archivos", "displayName": "Nombre:", "download": "Descargar archivos", "downloadMessage": "Elige el formato de descarga", "error": "Ha ocurrido un error", "fileInfo": "Información del archivo", - "lastModified": "Última modificación{suffix}", "move": "Mover", "moveMessage": "Selecciona un nuevo destino:", "newArchetype": "Crear una nueva publicación en base a una plantilla, tu archivo se creara en la misma carpeta", @@ -151,10 +149,7 @@ "newDirMessage": "Nombre de la carpeta", "newFile": "Nuevo archivo", "newFileMessage": "Nombre del archivo", - "numberDirs": "Carpetas", - "numberFiles": "Archivos", "rename": "Cambiar nombre", - "renameMessage": "Nuevo nombre para:", "replace": "Reemplazar", "replaceMessage": "Un archivo con el mismo nombre ya existe. ¿Quieres cambiar el nombre?\n", "schedule": "Programar", @@ -169,39 +164,57 @@ "selected": "seleccionado", "deleted": "¡Borrado con éxito!", "destinationSource": "Destino:", - "source": "Source{suffix}", "downloadsLimit": "Límite de descargas", - "maxBandwidth": "Ancho de banda máximo de descarga (kbps)", - "shareTheme": "Compartir tema", - "shareThemeColor": "Compartir color del tema (valor CSS)", - "shareTitle": "Compartir título", - "shareDescription": "Compartir descripción", - "shareLogo": "Compartir URL del logotipo", - "shareBanner": "Compartir URL del banner", - "shareFavicon": "Compartir URL de favicon", - "optionalPasswordHelp": "Si se establece, los usuarios deben introducir esta contraseña para ver el contenido compartido.", + "maxBandwidth": "Velocidad máxima de descarga (kbps)", + "shareTheme": "Tema", + "shareThemeColor": "Color del tema (valor CSS)", + "shareTitle": "Título la página", + "shareDescription": "Descripción de la página", + "shareLogo": "Imagen/Logo de la página", + "shareBanner": "Banner de la página", + "shareFavicon": "Mini-icono de la página (favicon)", + "optionalPasswordHelp": "Si eliges una contraseña, los usuarios que accedan deben introducir esa misma contraseña para ver el contenido.", "dragAndDrop": "Arrastra y suelta los archivos aquí", "completed": "Completado", "conflictsDetected": "Conflictos detectados", - "uploadSettingsChunked": "Subidas simultáneas: {maxConcurrentUpload}, velocidad de subida máxima: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Subidas simultáneas: {maxConcurrentUpload}, subida de archivos desactivada", - "invalidName": "Nombre inválido: no puede contener \"/\" o \"\\\".", - "viewMode": "Ver modo" + "viewMode": "Cambiar modo de visualización", + "renameMessage": "Introduce un nuevo nombre:", + "renameMessageInvalid": "Un archivo no puede contener \"/\" o \"\\\"", + "source": "Fuente{suffix}", + "deleteMessageMultiple": "¿Está seguro de que desea eliminar los archivos de {count}?", + "deleteMessageShare": "¿Estás seguro de que quieres borrar esta acción? {path} ?", + "lastModified": "Última modificación{suffix}", + "numberDirs": "Número de directorios{suffix}", + "numberFiles": "Número de ficheros{suffix}", + "renameMessageConflict": "Ya existe un elemento llamado \"{filename}\".", + "uploadSettingsChunked": "Carga máxima simultánea: {maxConcurrentUpload} Tamaño del chunk {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Carga máxima simultánea: {maxConcurrentUpload} Carga por trozos desactivada" }, "search": { "images": "Imágenes", "music": "Música", "pdf": "PDF", - "pressToSearch": "Enter para buscar...", "search": "Buscar...", - "typeToSearch": "Escribe para buscar...", "types": "Tipos", "video": "Vídeo", "path": "Ruta:", "smallerThan": "Menor que:", "largerThan": "Mayor que:", "helpText1": "La búsqueda se realiza por cada carácter (mínimo de 3 caracteres)", - "helpText2": "El índice se actualiza automáticamente (cada 5 min por defecto). Si es interrumpido pueden ocurrir errores" + "helpText2": "El índice se actualiza automáticamente (cada 5 min por defecto). Si es interrumpido pueden ocurrir errores", + "onlyFolders": "Sólo carpetas", + "onlyFiles": "Sólo archivos", + "showOptions": "Mostrar opciones", + "photos": "Fotos", + "audio": "Audio", + "videos": "Vídeos", + "documents": "Documentos", + "archives": "Archivos", + "number": "Número", + "noResults": "No se han encontrado resultados en la búsqueda indexada.", + "searchContext": "Contexto de búsqueda: {context}", + "notEnoughCharacters": "No hay suficientes caracteres para buscar (min {minSearchLength})", + "typeToSearch": "Escriba {minSearchLength} o más caracteres para iniciar la búsqueda." }, "settings": { "scopes": "Establece los directorios y los permisos que el usuario puede acceder", @@ -215,10 +228,8 @@ "avoidChanges": "(dejar en blanco para evitar cambios)", "branding": "Personalización", "brandingDirectoryPath": "Directorio de tus archivos que usarás para personalizar FileBrowser", - "brandingHelp": "Personaliza FileBrowser cambiando el nombre, logo, estilos. Más información en {0}", "changePassword": "Cambiar contraseña", "commandRunner": "Ejecutor de comandos", - "commandRunnerHelp": "Comandos para eventos. Variables disponibles: {0} (ruta relativa), {1} (ruta base). Más info en {2}", "commandsUpdated": "¡Comandos actualizados!", "createUserDir": "Crea automaticamente una carpeta cuando se agrega un nuevo usuario", "userHomeBasePath": "Ruta base para los directorios personales de los usuarios", @@ -249,7 +260,7 @@ "modify": "Editar archivos", "share": "Compartir archivos", "admin": "Administrador", - "api": "Gestionar claves API", + "api": "Gestión de APIs", "realtime": "Actualizaciones en tiempo real" }, "permissions-name": "Permisos", @@ -261,7 +272,7 @@ "rulesHelp": "Puedes definir un conjunto de reglas de permisos para este usuario específico. Los archivos bloqueados no se mostrarán en las listas y no serán accesibles por el usuario. Puedes utilizar regex y rutas relativas a la raíz del usuario.\n", "scope": "Raíz", "settingsUpdated": "¡Ajustes actualizados!", - "shareManagement": "Gestión Compartida", + "shareManagement": "Gestión de recursos compartidos", "shareDeleted": "¡Compartido eliminado!", "singleClick": "Abrir con un solo clic", "themes": { @@ -275,7 +286,7 @@ "userCreated": "¡Usuario creado!", "userDefaults": "Configuración de usuario por defecto", "userDeleted": "¡Usuario eliminado!", - "userManagement": "Gestionar usuarios", + "userManagement": "Gestión de usuarios", "userUpdated": "¡Usuario actualizado!", "username": "Nombre de Usuario", "users": "Usuarios", @@ -287,20 +298,27 @@ "UserManagement": "Gestión de usuarios", "tusUploads": "Subir en partes", "tusUploadsHelp": "File Browser Quantum permite subir archivos en partes, lo que permite que sean subidos de manera eficiente, fiable, que sea reanudable, incluso si tu internet es inestable", - "tusUploadsChunkSize": "Tamaño máximo de subida de un archivo. Puede introducir un número entero que indique una entrada en bytes o una cadena como 10 MB, 1 GB, etc", + "tusUploadsChunkSize": "Tamaño máximo de subida de un archivo. Puede escribir un número entero que indique una entrada en bytes o una cadena como 10 MB, 1 GB, etc", "tusUploadsRetryCount": "Número de reintentos, si falla la subida de un archivo", "api": "Claves API", "loginMethodDescription": "Método de inicio de sesión permitido", "userNotAdmin": "El usuario no es administrador", - "enterPassword": "Indusca la contraseña", - "enterPasswordAgain": "Vuelva a introducir la contraseña", + "enterPassword": "Escriba su contraseña", + "enterPasswordAgain": "Vuelva a escribir su contraseña", "passwordsDoNotMatch": "Las contraseñas no coinciden", "loginMethod": "Método de inicio de sesión", - "shareSettings": "Compartidos", + "shareSettings": "Recursos compartidos", "adminOptions": "Opciones de administrador", "shareDuration": "Expira en", "searchOptions": "Opciones de búsqueda", - "downloads": "Descargas" + "downloads": "Descargas", + "systemAdmin": "Sistema y administración", + "configViewer": "Visor de configuración", + "configViewerShowFull": "Mostrar configuración completa", + "configViewerShowComments": "Mostrar comentarios", + "configViewerLoadConfig": "Configuración de carga", + "brandingHelp": "Puede personalizar el aspecto de su instancia del Navegador de archivos cambiando su nombre, sustituyendo el logotipo, añadiendo estilos personalizados e incluso desactivando los enlaces externos a GitHub.\nPara obtener más información sobre la personalización de la marca, consulta la página {0}.", + "commandRunnerHelp": "Aquí puede establecer los comandos que se ejecutan en los eventos nombrados. Debe escribir uno por línea. Las variables de entorno {0} y {1} estarán disponibles, siendo {0} relativa a {1}. Para más información sobre esta característica y las variables de entorno disponibles, por favor lea el {2}." }, "sidebar": { "help": "Ayuda", @@ -333,61 +351,70 @@ }, "share": { "notice": "Nota: cuando se crea un recurso compartido, queda más expuesto al público y tiene diferentes normas de acceso. Si su recurso compartido no está configurado de forma segura, cualquiera puede acceder a él.", - "disableAnonymousDescription": "Cuando está activada, sólo los usuarios autenticados pueden acceder al recurso compartido. El usuario debe tener acceso al origen del recurso compartido.", + "disableAnonymousDescription": "Cuando está activado, sólo los usuarios con su permiso pueden acceder al recurso compartido. El usuario debe tener acceso al origen recurso compartido.", "disableThumbnailsDescription": "Si está activada, las miniaturas de previsualización no se mostrarán en la acción.", - "enableAllowedUsernamesDescription": "Cuando se activa, sólo los usuarios especificados pueden acceder al recurso compartido. El usuario debe tener acceso al origen del recurso compartido.", + "enableAllowedUsernamesDescription": "Cuando se activa, sólo los usuarios que especifique pueden acceder al recurso compartido. El usuario debe tener acceso al origen del recurso compartido.", "downloadsLimitDescription": "Número máximo de veces que se puede descargar un archivo/carpeta del recurso compartido. Dejar vacío para ilimitado.", "maxBandwidthDescription": "El ancho de banda máximo de descarga en kilobytes por segundo. Dejar vacío para ilimitado.", "shareThemeDescription": "El tema que se utilizará para el enlace compartido.", "disableAnonymous": "Desactivar el acceso anónimo", "disableThumbnails": "Desactivar miniaturas", "enableAllowedUsernames": "Compartir sólo con determinados usuarios", - "allowedUsernamesPlaceholder": "Introduzca los nombres de usuario, separados por comas", - "keepAfterExpiration": "No eliminar la acción una vez caducada", - "keepAfterExpirationDescription": "La acción no se borrará después de que caduque. Esto resulta útil si desea ampliar el plazo de expiración de una acción o editarla después de que expire.", + "allowedUsernamesPlaceholder": "Escriba los nombres de usuario (separado por comas)", + "keepAfterExpiration": "No eliminar el recurso compartido una vez expirado", + "keepAfterExpirationDescription": "Los recursos compartidos no se borraran cuando expiren. Es útil si después quieres ampliar la duración del recurso, o editarlo.", "saveDisabled": "No se pueden guardar los cambios en los archivos compartidos.", - "shareDurationDescription": "Duración antes de que caduque la acción. Dejar en blanco para una acción permanente.", - "passwordDescription": "Contraseña opcional necesaria para acceder a este recurso compartido, incluidos los usuarios autenticados.", + "shareDurationDescription": "Duración del recurso compartido. Dejar en blanco para permanente.", + "passwordDescription": "Contraseña opcional necesaria para acceder al recurso compartido, incluidos los usuarios autenticados.", "allowedUsernamesListDescription": "Nombres de usuario separados por comas que pueden acceder al recurso compartido (si está activado).", "shareThemeColorDescription": "Valor de color CSS aplicado al tema de la acción (por ejemplo, rojo, #0ea5e9, rgb(14,165,233)).", - "shareTitleDescription": "Título de página personalizado que se muestra en la página de compartición.", - "shareDescriptionHelp": "Breve descripción mostrada en la página de compartición (puede utilizarse en las metaetiquetas).", - "shareLogoDescription": "URL o ruta de la imagen del logotipo accesible por el cliente -- se aceptan rutas abosolutas o de índice: por ejemplo, 'https://domain.com/logo.png' o '/ruta/a/logo.png'.", - "shareBannerDescription": "URL de la imagen del banner o ruta accesible por el cliente -- se aceptan rutas abosolutas o de índice: por ejemplo, 'https://domain.com/banner.png' o '/ruta/a/banner.png'.", - "shareFaviconDescription": "URL del favicon o ruta accesible por el cliente -- se aceptan rutas abosolutas o de índice: por ejemplo 'https://domain.com/favicon.png' o '/ruta/a/favicon.png'.", + "shareTitleDescription": "Título de página personalizado de página", + "shareDescriptionHelp": "Breve descripción mostrada en la página (que suele aparecer cuando compartes el enlace)", + "shareLogoDescription": "Enlace (URL), o ruta de la imagen del logo que se mostrará en el enlace -- por ejemplo, 'https://ejemplo.com/logo.png' o '/ruta/a/logo.png'.", + "shareBannerDescription": "Enlace (URL), o ruta de la imagen del banner que se mostrará en el enlace -- por ejemplo, 'https://ejemplo.com/banner.png' o '/ruta/a/banner.png'.", + "shareFaviconDescription": "Enlace (URL) del icono (favicon) que se mostrará en el enlace -- por ejemplo 'https://domain.com/favicon.png' o '/ruta/a/favicon.png'", "hideNavButtons": "Ocultar botones de navegación", "hideNavButtonsDescription": "Oculte los botones de navegación de la barra de navegación en la acción para crear un aspecto minimalista.", "viewModeDescription": "Diseño por defecto de la página compartida.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Se le ha enviado una acción para que la vea o descargue.", "disableShareCard": "Desactivar tarjeta compartida", - "disableShareCardDescription": "Desactivar la tarjeta de compartir en la página compartida en la barra lateral o en la parte superior de la página en móvil.", - "disableSidebar": "Desactivar la barra lateral", - "disableSidebarDescription": "Desactivar la barra lateral en la página compartida." + "disableShareCardDescription": "Desactivar la tarjeta de compartir en la página compartida en el panel lateral o en la parte superior de la página en móvil.", + "disableSidebar": "Desactivar el panel lateral", + "disableSidebarDescription": "Desactivar el panel lateral en la página compartida.", + "enforceDarkLightMode": "Aplicar el modo temático", + "enforceDarkLightModeDescription": "Forzar un modo de tema específico (oscuro o claro) para esta acción, anulando las preferencias del usuario.", + "default": "No imponer un modo", + "dark": "Oscuro", + "light": "Luz", + "enableOnlyOffice": "Activar el visor de OnlyOffice", + "enableOnlyOfficeDescription": "Permitir la visualización de archivos de oficina utilizando OnlyOffice en este recurso compartido.", + "enableOnlyOfficeEditing": "Habilitar la edición de OnlyOffice", + "enableOnlyOfficeEditingDescription": "Permitir la edición de archivos de oficina utilizando OnlyOffice en este recurso compartido.", + "titleDefault": "Archivos compartidos - {title}" }, "api": { - "title": "Crear API:", - "keyName": "Nombre de la clave:", + "title": "Claves API", + "keyName": "Nombre para la API", "createdAt": "Creado en:", "expiresAt": "Expira en:", "permissions": "Permisos:", "createTitle": "Crear clave API", - "keyNamePlaceholder": "Nombre único para clave API:", + "keyNamePlaceholder": "Escriba un nombre único", "tokenDuration": "Duración:", "durationNumberPlaceholder": "Número:", "days": "Días", "months": "Meses", - "swaggerLinkText": "Documentación de Swagger", + "swaggerLinkText": "Documentación de Swagger (API)", "name": "Nombre", "created": "Creado", - "expires": "Expira en", + "expires": "Expira en:", "actions": "Acciones", "enabled": "Habilitado", "disabled": "Deshabilitado", - "permissionNote": "Elija al menos un permiso para la clave. Su usuario también debe tener el permiso.", + "permissionNote": "Elija al menos un permiso para la API. Tu usuario también debe tener ese permiso.", "createKeyFailed": "Error al crear la clave API.", - "createKeySuccess": "Clave API creada correctamente.", - "description": "Las claves API se basan en el usuario que las crea. Véase" + "createKeySuccess": "¡Clave API creada con exito!", + "description": "Las claves API solo podrán tener los permisos que el usuario tenga. Para más info:" }, "index": { "status": "Estado", @@ -399,7 +426,7 @@ "logout": "Cerrar sesión", "toggleClick": "Abrir con un solo clic", "toggleDark": "Activar modo oscuro", - "toggleSticky": "Barra lateral visible", + "toggleSticky": "Cambiar visibilidad del panel lateral", "used": "usado", "noSources": "No hay fuentes disponibles. Añada una o active defaultEnabled: true." }, @@ -439,7 +466,7 @@ "allow": "Permitir", "allowDeny": "Permitir/Denegar", "userGroup": "Usuario/Grupo", - "enterName": "Introduzca el nombre del usuario o del grupo", + "enterName": "Escriba el nombre del usuario o del grupo", "deleted": "Regla de acceso eliminada", "added": "Regla de acceso añadida", "name": "Nombre", @@ -447,8 +474,8 @@ "totalDenied": "Denegado", "totalAllowed": "Permitido", "all": "Denegar todo", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Muestra lo que ocurre cuando un usuario intenta acceder a una ruta en la que no existen reglas de acceso específicas. Permitir\" significa que los usuarios pueden acceder por defecto, \"Denegar\" significa que los usuarios están bloqueados por defecto a menos que se les permita explícitamente." + "defaultBehaviorDescription": "Muestra lo que ocurre cuando un usuario intenta acceder a una ruta en la que no existen reglas de acceso específicas. Permitir\" significa que los usuarios pueden acceder por defecto, \"Denegar\" significa que los usuarios están bloqueados por defecto a menos que se les permita explícitamente.", + "defaultBehavior": "Acción por defecto{suffix}" }, "fileLoading": { "title": "Carga de archivos", @@ -458,17 +485,17 @@ "uploadChunkSizeMbHelp": "Típicamente 5 a 50 MB, más pequeño es mejor para conexiones inestables. Si se pone a 0, se desactiva la subida de archivo en partes" }, "colors": { - "red": "rojo", - "green": "verde", - "blue": "azul", - "yellow": "amarillo", - "violet": "violeta", - "orange": "naranja" + "red": "Rojo", + "green": "Verde", + "blue": "Azul", + "yellow": "Amarillo", + "violet": "Morado", + "orange": "Naranja" }, "profileSettings": { "autoplayMedia": "Reproducción automática de archivos multimedia", "autoplayMediaDescription": "Reproducción automática de archivos multimedia al visualizarlos. Esto sólo funcionará si el archivo es un archivo multimedia", - "sidebarOptions": "Opciones de la barra lateral", + "sidebarOptions": "Opciones del panel lateral", "editorViewerOptions": "Edición y visualización", "editorQuickSave": "Botón de guardado rápido en el editor", "editorQuickSaveDescription": "El editor de texto mostrará siempre el icono de guardar, ocultando cualquier otra acción", @@ -484,9 +511,9 @@ "disableOfficeEditor": "Desactivar el visor y editor de office para las siguientes extensiones:", "defaultThemeDescription": "Predeterminado", "customTheme": "Cambiar tema", - "showSelectMultiple": "Mostrar selección múltiple en el menú contextual del escritorio", + "showSelectMultiple": "Mostrar selección múltiple en el menú contextuaL (en PC)", "showSelectMultipleDescription": "Por defecto, el menú contextual del escritorio no tiene la opción \"seleccionar múltiples\" como el móvil. Esta opción siempre se muestra en el menú contextual.", - "setDateFormat": "Establecer el formato exacto de la fecha", + "setDateFormat": "Usar fecha exacta (DD/MM/AA)", "showHiddenFiles": "Mostrar archivos ocultos", "showHiddenFilesDescription": "Muestra los archivos ocultos. (por ejemplo, .thumbnails, ProgramData), pero siguen siendo accesibles manualmente si están configurados para ser indexados.", "showQuickDownload": "Icono de descarga rápida", @@ -504,24 +531,34 @@ "popupPreview": "Vista previa emergente", "popupPreviewDescription": "Se mostrará una ventana emergente superpuesta sobre la interfaz de usuario al pasar el ratón por encima de los archivos que tengan una miniatura", "filePreviewOptions": "Opciones de previsualización de archivos", - "disableUpdateNotifications": "Desactivar la visualización de notificaciones de actualización", - "disableUpdateNotificationsDescription": "Desactivar el banner de notificación de actualización que aparece para los usuarios administradores en la barra lateral.", - "disableHideSidebar": "Mantener la barra lateral abierta al previsualizar o editar archivos", - "disableHideSidebarDescription": "Si está activada, la barra lateral no se ocultará automáticamente al previsualizar o editar archivos.", - "disableQuickToggles": "Desactivar alternancias rápidas", - "disableQuickTogglesDescription": "Desactiva los toggles rápidos que aparecen en la barra lateral.", + "disableUpdateNotifications": "Desactivar las notificaciones de actualización", + "disableUpdateNotificationsDescription": "Desactivar el banner de notificación de actualización que aparece para los usuarios administradores en el panel lateral.", + "disableHideSidebar": "Mantener el panel lateral abierto al previsualizar o editar archivos", + "disableHideSidebarDescription": "Si está activado, el panel lateral no se ocultará automáticamente al previsualizar o editar archivos.", + "disableQuickToggles": "Desactivar ajustes rápidos", + "disableQuickTogglesDescription": "Desactiva los ajustes rápidos que aparecen en el panel lateral. (cambiar tema, visibilidad del panel y modo de un solo click)", "disableSearchOptions": "Desactivar las opciones de búsqueda", - "disableSearchOptionsDescription": "Desactivar las opciones de búsqueda que aparecen en la barra lateral.", - "hideSidebarFileActions": "Ocultar el botón de acciones de archivo en la barra lateral", + "disableSearchOptionsDescription": "Desactivar las opciones de búsqueda que aparecen en el panel.", + "hideSidebarFileActions": "Ocultar el botón de acciones de archivo en el panel", "deleteWithoutConfirming": "Eliminar archivos sin mensaje de confirmación", - "deleteWithoutConfirmingDescription": "Eliminar archivos sin mensaje de confirmación. Esto sólo funcionará si el archivo es un archivo multimedia." + "deleteWithoutConfirmingDescription": "Eliminar archivos sin mensaje de confirmación. Esto sólo funcionará si el archivo es un archivo multimedia.", + "defaultMediaPlayer": "Usar el reproductor multimedia nativo del navegador", + "defaultMediaPlayerDescription": "Usar el reproductor multimedia nativo de tu navegador para los archivos de vídeo y audio en lugar del reproductor multimedia incluido en FileBrowser.", + "debugOfficeEditor": "Activar el modo de depuración de OnlyOffice", + "debugOfficeEditorDescription": "Mostrar una ventana emergente de depuración con información adicional en el editor OnlyOffice.", + "fileViewerOptions": "Opciones del visor de archivos", + "themeAndLanguage": "Tema y lengua" }, "editor": { - "uninitialized": "Error al inicializar el editor. Por favor, vuelva a cargar.", + "uninitialized": "Error al iniciar el editor, inténtelo de nuevo", "saveFailed": "No se ha podido guardar el archivo. Por favor, inténtelo de nuevo.", - "saveAborted": "Guardado abortado. El nombre del fichero no coincide con el fichero activo.", + "saveAborted": "No se ha podido guardar el archivo. El nombre del archivo no coincide con el que estás editando.", "saveDisabled": "No es posible guardar archivos compartidos.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Operación de guardado abortada. El archivo activo de la aplicación ({activeFile}) no coincide con el archivo que está intentando guardar ({tryingToSave}).", + "syncFailed": "No se ha podido sincronizar el estado con la ruta para \"{filename}\" después de 5 intentos. Abortar la configuración del editor para evitar la corrupción de datos." + }, + "player": { + "LoopEnabled": "Bucle activado", + "LoopDisabled": "Bucle desactivado" } } diff --git a/frontend/src/i18n/fr.json b/frontend/src/i18n/fr.json index a6da533e5..94eb675cd 100644 --- a/frontend/src/i18n/fr.json +++ b/frontend/src/i18n/fr.json @@ -49,7 +49,8 @@ "clearCompleted": "Effacement terminé", "showMore": "Afficher plus", "showLess": "Montrer moins", - "openParentFolder": "Ouvrir le dossier parent" + "openParentFolder": "Ouvrir le dossier parent", + "selectedCount": "Nombre d'éléments sélectionnés pour effectuer une action" }, "download": { "downloadFile": "Télécharger le fichier", @@ -84,15 +85,15 @@ "click": "Sélectionner un élément", "ctrl": { "click": "Sélectionner plusieurs éléments", - "f": "Ouvrir l'invite de recherche", - "s": "Télécharger l'élément actuel" + "d": "télécharger les éléments sélectionnés" }, "del": "Supprimer les éléments sélectionnés", - "doubleClick": "Ouvrir un élément", "esc": "Désélectionner et/ou fermer la boîte de dialogue", - "f1": "Ouvrir l'aide", "f2": "Renommer le fichier", - "help": "Aide" + "help": "Aide", + "f1": "show help prompt", + "description": "Vous pouvez consulter les options de navigation de base ci-dessous. Pour plus d'informations, veuillez consulter le site", + "wiki": "Wiki FileBrowser Quantum" }, "languages": { "he": "עברית", @@ -134,16 +135,13 @@ "prompts": { "copy": "Copier", "copyMessage": "Choisissez l'emplacement où copier la sélection :", - "deleteMessageMultiple": "Êtes-vous sûr de vouloir supprimer ces {count} élément(s) ?", "deleteMessageSingle": "Êtes-vous sûr de vouloir supprimer cet élément ?", - "deleteMessageShare": "Êtes-vous sûr de vouloir supprimer ce partage ({path}) ?", "deleteTitle": "Supprimer", "displayName": "Nom :", "download": "Télécharger", "downloadMessage": "Choisissez le format de téléchargement :", "error": "Quelque chose s'est mal passé", "fileInfo": "Informations", - "lastModified": "Dernière modification{suffix}", "move": "Déplacer", "moveMessage": "Choisissez l'emplacement où déplacer la sélection :", "newArchetype": "Créer un nouveau post basé sur un archétype. Votre fichier sera créé dans le dossier de contenu.", @@ -151,10 +149,7 @@ "newDirMessage": "Nom du nouveau dossier :", "newFile": "Nouveau fichier", "newFileMessage": "Nom du nouveau fichier :", - "numberDirs": "Nombre de dossiers{suffix}", - "numberFiles": "Nombre de fichiers{suffix}", "rename": "Renommer", - "renameMessage": "Nouveau nom pour", "replace": "Remplacer", "replaceMessage": "Un des fichiers que vous êtes en train d'importer a le même nom qu'un autre déjà présent. Voulez-vous remplacer le fichier actuel par le nouveau ?\n", "schedule": "Fixer la date", @@ -172,10 +167,6 @@ "dragAndDrop": "Glisser-déposer des fichiers ici", "completed": "Terminé", "conflictsDetected": "Conflits détectés", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Nom invalide fourni : ne peut contenir \"/\" ou \"\\\".", - "source": "Source{suffix}", "downloadsLimit": "Limite du nombre de téléchargements", "maxBandwidth": "Largeur de bande maximale de téléchargement (kbps)", "shareTheme": "Thème de partage", @@ -186,22 +177,44 @@ "shareBanner": "Partager l'URL de la bannière", "shareFavicon": "Partager l'URL de la favicon", "optionalPasswordHelp": "S'il est défini, les utilisateurs doivent saisir ce mot de passe pour consulter le contenu partagé.", - "viewMode": "Mode d'affichage" + "viewMode": "Mode d'affichage", + "renameMessage": "Saisissez un nouveau nom :", + "renameMessageInvalid": "Un fichier ne peut pas contenir \"/\" ou \"\\\"", + "source": "Source{suffix}", + "deleteMessageMultiple": "Êtes-vous sûr de vouloir supprimer le(s) fichier(s) {count}?", + "deleteMessageShare": "Êtes-vous sûr de vouloir supprimer ce partage : {path} ?", + "lastModified": "Dernière modification{suffix}", + "numberDirs": "Nombre de répertoires{suffix}", + "numberFiles": "Nombre de fichiers{suffix}", + "renameMessageConflict": "Un article nommé \"{filename}\" existe déjà !", + "uploadSettingsChunked": "Nombre maximum de téléchargements simultanés : {maxConcurrentUpload}, taille de chunk : {uploadChunkSizeMb} MO", + "uploadSettingsNoChunk": "Nombre maximum de téléchargements simultanés : {maxConcurrentUpload}, chunked upload désactivé" }, "search": { "images": "Images", "music": "Musique", "pdf": "PDF", - "pressToSearch": "Appuyez sur entrée pour chercher...", "search": "Recherche en cours...", - "typeToSearch": "Écrivez pour chercher...", "types": "Types", "video": "Vidéo", "path": "Chemin :", "smallerThan": "Plus petit que :", "largerThan": "Plus grand que :", "helpText1": "La recherche s'effectue sur chaque caractère saisi (3 caractères minimum pour les termes de recherche).", - "helpText2": "Index : La recherche utilise l'index qui est automatiquement mis à jour à l'intervalle configuré (par défaut : 5 minutes). Une recherche effectuée alors que le programme vient de démarrer peut donner des résultats incomplets." + "helpText2": "Index : La recherche utilise l'index qui est automatiquement mis à jour à l'intervalle configuré (par défaut : 5 minutes). Une recherche effectuée alors que le programme vient de démarrer peut donner des résultats incomplets.", + "noResults": "Aucun résultat n'a été trouvé dans la recherche indexée.", + "onlyFolders": "Dossiers uniquement", + "onlyFiles": "Uniquement les fichiers", + "showOptions": "Afficher les options", + "photos": "Photos", + "audio": "Audio", + "videos": "Vidéos", + "documents": "Documents", + "archives": "Archives", + "number": "Nombre", + "searchContext": "Contexte de la recherche : {context}", + "notEnoughCharacters": "Pas assez de caractères pour effectuer une recherche (min {minSearchLength})", + "typeToSearch": "Commencez à taper {minSearchLength} ou plus de caractères pour lancer la recherche." }, "settings": { "scopes": "Définir les sources autorisées et la portée de l'utilisateur au sein de celles-ci", @@ -215,10 +228,8 @@ "avoidChanges": "(Laisser vide pour conserver l'actuel)", "branding": "Image de marque", "brandingDirectoryPath": "Chemin du dossier d'image de marque", - "brandingHelp": "Vous pouvez personnaliser l'apparence de votre instance de File Browser en changeant son nom, en remplaçant le logo, en ajoutant des styles personnalisés et même en désactivant les liens externes vers GitHub.\nPour plus d'informations sur la personnalisation de l'image de marque, veuillez consulter la {0}.", "changePassword": "Modifier le mot de passe", "commandRunner": "Fichier de commandes", - "commandRunnerHelp": "Ici, vous pouvez définir les commandes qui sont exécutées pour les événements nommés précédemment. Vous devez en écrire une par ligne. Les variables d'environnement {0} et {1} seront disponibles, {0} étant relatif à {1}. Pour plus d'informations sur cette fonctionnalité et les variables d'environnement disponibles, veuillez lire la {2}.", "commandsUpdated": "Commandes mises à jour !", "createUserDir": "Créer automatiquement un dossier pour l'utilisateur", "customStylesheet": "Feuille de style personnalisée", @@ -300,7 +311,14 @@ "adminOptions": "Options de l'utilisateur administrateur", "shareDuration": "Expiration", "searchOptions": "Options de recherche", - "downloads": "Téléchargements" + "downloads": "Téléchargements", + "systemAdmin": "Système et administration", + "configViewer": "Config Viewer", + "configViewerShowFull": "Afficher la configuration complète", + "configViewerShowComments": "Afficher les commentaires", + "configViewerLoadConfig": "Configuration de la charge", + "brandingHelp": "Vous pouvez personnaliser l'apparence de votre instance du navigateur de fichiers en changeant son nom, en remplaçant le logo, en ajoutant des styles personnalisés et même en désactivant les liens externes vers GitHub.\nPour plus d'informations sur la personnalisation de l'image de marque, consultez le site {0}.", + "commandRunnerHelp": "Vous pouvez définir ici les commandes qui seront exécutées dans les événements nommés. Vous devez en écrire une par ligne. Les variables d'environnement {0} et {1} seront disponibles, {0} étant relatif à {1}. Pour plus d'informations sur cette fonctionnalité et les variables d'environnement disponibles, veuillez consulter le site {2}." }, "sidebar": { "help": "Aide", @@ -358,12 +376,21 @@ "hideNavButtons": "Masquer les boutons de navigation", "hideNavButtonsDescription": "Masquez les boutons de navigation de la barre de navigation dans le partage pour créer une apparence minimaliste.", "viewModeDescription": "Mise en page par défaut de la page partagée.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Une action vous a été envoyée pour que vous la consultiez ou la téléchargiez.", "disableShareCard": "Désactiver la carte de partage", "disableShareCardDescription": "Désactiver la carte de partage sur la page partagée dans la barre latérale ou en haut de la page sur mobile.", "disableSidebar": "Désactiver la barre latérale", - "disableSidebarDescription": "Désactiver la barre latérale sur la page partagée." + "disableSidebarDescription": "Désactiver la barre latérale sur la page partagée.", + "enforceDarkLightMode": "Renforcer le mode thématique", + "enforceDarkLightModeDescription": "Forcer un mode de thème spécifique (sombre ou clair) pour ce partage, sans tenir compte des préférences de l'utilisateur.", + "default": "Ne pas imposer de mode", + "dark": "Sombre", + "light": "Lumière", + "enableOnlyOffice": "Activer la visionneuse OnlyOffice", + "enableOnlyOfficeDescription": "Autoriser la visualisation de fichiers bureautiques en utilisant OnlyOffice dans ce partage.", + "enableOnlyOfficeEditing": "Activer l'édition OnlyOffice", + "enableOnlyOfficeEditingDescription": "Autoriser l'édition de fichiers bureautiques en utilisant OnlyOffice dans ce partage.", + "titleDefault": "Fichiers partagés - {title}" }, "api": { "title": "Clés API :", @@ -447,8 +474,8 @@ "totalDenied": "Refusé", "totalAllowed": "Autorisé", "all": "Refuser tout", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Ceci montre ce qui se passe lorsqu'un utilisateur tente d'accéder à un chemin pour lequel il n'existe pas de règles d'accès spécifiques. La mention \"Autoriser\" signifie que les utilisateurs peuvent accéder par défaut, la mention \"Refuser\" signifie que les utilisateurs sont bloqués par défaut, à moins qu'ils n'y soient explicitement autorisés." + "defaultBehaviorDescription": "Ceci montre ce qui se passe lorsqu'un utilisateur tente d'accéder à un chemin pour lequel il n'existe pas de règles d'accès spécifiques. La mention \"Autoriser\" signifie que les utilisateurs peuvent accéder par défaut, la mention \"Refuser\" signifie que les utilisateurs sont bloqués par défaut, à moins qu'ils n'y soient explicitement autorisés.", + "defaultBehavior": "Action par défaut{suffix}" }, "fileLoading": { "title": "Chargements et téléchargements", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - Le thème par défaut", "customTheme": "Choisissez votre thème", "showSelectMultiple": "Afficher la sélection multiple dans le menu contextuel sur le bureau", - "showSelectMultipleDescription": "Par défaut, le menu contextuel de l'ordinateur de bureau n'a pas l'option \"sélectionner plusieurs\" comme celui de l'ordinateur mobile. Cette option apparaît toujours dans le menu contextuel." + "showSelectMultipleDescription": "Par défaut, le menu contextuel de l'ordinateur de bureau n'a pas l'option \"sélectionner plusieurs\" comme celui de l'ordinateur mobile. Cette option apparaît toujours dans le menu contextuel.", + "defaultMediaPlayer": "Utilisez le lecteur multimédia natif de votre navigateur", + "defaultMediaPlayerDescription": "Utilisez le lecteur multimédia natif de votre navigateur pour les fichiers vidéo et audio plutôt que le lecteur multimédia \"vue-plyr\" inclus dans FileBrowser.", + "debugOfficeEditor": "Activer le mode de débogage d'OnlyOffice", + "debugOfficeEditorDescription": "Afficher une fenêtre de débogage avec des informations supplémentaires dans l'éditeur OnlyOffice.", + "fileViewerOptions": "Options de visualisation des fichiers", + "themeAndLanguage": "Thème et langue" }, "editor": { "uninitialized": "Échec de l'initialisation de l'éditeur. Veuillez recharger.", "saveFailed": "Échec de l'enregistrement du fichier. Veuillez réessayer.", "saveAborted": "Sauvegarde interrompue. Le nom du fichier ne correspond pas au fichier actif.", "saveDisabled": "L'enregistrement de fichiers partagés n'est pas possible.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "L'opération d'enregistrement a été interrompue. Le fichier actif de l'application ({activeFile}) ne correspond pas au fichier que vous essayez d'enregistrer ({tryingToSave}).", + "syncFailed": "n'a pas réussi à synchroniser l'état avec l'itinéraire pour \"{filename}\" après 5 tentatives. Abandon de la configuration de l'éditeur pour éviter la corruption des données." + }, + "player": { + "LoopEnabled": "Boucle activée", + "LoopDisabled": "Boucle désactivée" } } diff --git a/frontend/src/i18n/he.json b/frontend/src/i18n/he.json index ebc181e4a..94b6c83ea 100644 --- a/frontend/src/i18n/he.json +++ b/frontend/src/i18n/he.json @@ -49,7 +49,8 @@ "clearCompleted": "השלם", "showMore": "הצג עוד", "showLess": "הצג פחות", - "openParentFolder": "פתח את התיקייה הראשית" + "openParentFolder": "פתח את התיקייה הראשית", + "selectedCount": "מספר הפריטים שנבחרו לביצוע פעולה" }, "download": { "downloadFile": "הורד קובץ", @@ -84,15 +85,15 @@ "click": "בחר קובץ או תקייה", "ctrl": { "click": "בחר מספר קבצים או תקיות", - "f": "פותח את החיפוש", - "s": "לשמור קובץ או להוריד את התקייה שבה אתה נמצא" + "d": "הורדת פריטים נבחרים" }, "del": "מחק את מה שנבחר", - "doubleClick": "פתח קובץ או תקייה", "esc": "נקה את הבחירה ו/או סגור את השדה", - "f1": "המידע הזה", "f2": "שנה שם קובץ", - "help": "עזרה" + "help": "עזרה", + "f1": "הצג עזרה", + "description": "ניתן לצפות באפשרויות הניווט הבסיסיות להלן. למידע נוסף, בקרו בכתובת", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -134,16 +135,13 @@ "prompts": { "copy": "העתק", "copyMessage": "בחר לאן להעתיק את הקבצים:", - "deleteMessageMultiple": "האם אתה בטוח שברצונך למחוק {count} קבצים?", "deleteMessageSingle": "האם אתה בטוח שברצונך למחוק את הקובץ או התקייה?", - "deleteMessageShare": "האם אתה בטוח שברצונך למחוק את השיתוף הזה?({path})?", "deleteTitle": "מחק קבצים", "displayName": "שם:", "download": "הורד קבצים", "downloadMessage": "בחר את הפורמט שברצונך להוריד", "error": "משהו השתבש", "fileInfo": "מידע על הקובץ", - "lastModified": "שונה לאחרונה{suffix}", "move": "העבר", "moveMessage": "בחר מיקום חדש לקובץ / תקייה:", "newArchetype": "צור פוסט חדש. הקובץ יווצר בתקיית התוכן", @@ -151,10 +149,7 @@ "newDirMessage": "כתוב את שם התקייה החדשה", "newFile": "קובץ חדש", "newFileMessage": "כתוב את שם הקובץ החדש", - "numberDirs": "מספר התקיות{suffix}", - "numberFiles": "מספר הקבצים{suffix}", "rename": "שנה שם", - "renameMessage": "הכנס שם חדש עבור", "replace": "החלף", "replaceMessage": "אחד הקבצים בעל שם זהה לקובץ קיים, האם ברצונך להחליף את הקובץ הקיים בחדש? זהירות - הקובץ הישן ימחק\n", "schedule": "תזמון", @@ -172,10 +167,6 @@ "dragAndDrop": "גרור ושחרר קבצים לכאן", "completed": "הושלם", "conflictsDetected": "סכסוכים שזוהו", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "שם לא חוקי: לא ניתן להכיל \"/\" או \"\\\"", - "source": "Source{suffix}", "downloadsLimit": "הגבלת מספר ההורדות", "maxBandwidth": "רוחב פס הורדה מרבי (kbps)", "shareTheme": "שתף נושא", @@ -186,22 +177,44 @@ "shareBanner": "שתף כתובת URL של באנר", "shareFavicon": "שתף כתובת URL של סמל המועדפים", "optionalPasswordHelp": "אם אפשרות זו מוגדרת, על המשתמשים להזין סיסמה זו כדי להציג את התוכן המשותף.", - "viewMode": "מצב תצוגה" + "viewMode": "מצב תצוגה", + "renameMessage": "הזן שם חדש:", + "renameMessageInvalid": "קובץ לא יכול להכיל את התווים \"/\" או \"\\\"", + "source": "מקור{suffix}", + "deleteMessageMultiple": "האם אתה בטוח שברצונך למחוק את הקובץ/ים {count}?", + "deleteMessageShare": "האם אתה בטוח שברצונך למחוק שיתוף זה: {path}?", + "lastModified": "שינוי אחרון{suffix}", + "numberDirs": "מספר הספריות{suffix}", + "numberFiles": "מספר הקבצים{suffix}", + "renameMessageConflict": "פריט בשם \"{filename}\" כבר קיים!", + "uploadSettingsChunked": "מספר העלאות מקסימלי בו-זמנית: {maxConcurrentUpload}, גודל קובץ: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "מספר העלאות מקסימלי בו-זמנית: {maxConcurrentUpload}, העלאה מקוטעת מושבתת" }, "search": { "images": "תמונות", "music": "מוזיקה", "pdf": "PDF", - "pressToSearch": "הקש אנטר לחיפוש...", "search": "חפש...", - "typeToSearch": "הקלד לחיפוש...", "types": "סוג", "video": "וידאו", "path": "נתיב:", "smallerThan": "קטן מ:", "largerThan": "גדול מ:", "helpText1": "החיפוש מתבצע על כל תו שאתה מקליד (מינימום 3 תווים עבור מונחי חיפוש).", - "helpText2": "האינדקס: החיפוש משתמש באינדקס שמתעדכן אוטומטית במרווח הזמן שהוגדר (ברירת מחדל: 5 דקות). חיפוש בעת הפעלת התוכנית עלול להניב תוצאות חלקיות." + "helpText2": "האינדקס: החיפוש משתמש באינדקס שמתעדכן אוטומטית במרווח הזמן שהוגדר (ברירת מחדל: 5 דקות). חיפוש בעת הפעלת התוכנית עלול להניב תוצאות חלקיות.", + "noResults": "לא נמצאו תוצאות בחיפוש באינדקס.", + "onlyFolders": "רק תיקיות", + "onlyFiles": "רק קבצים", + "showOptions": "הצג אפשרויות", + "photos": "תמונות", + "audio": "אודיו", + "videos": "סרטונים", + "documents": "מסמכים", + "archives": "ארכיונים", + "number": "מספר", + "searchContext": "הקשר החיפוש: {context}", + "notEnoughCharacters": "אין מספיק תווים לחיפוש (מינימום {minSearchLength})", + "typeToSearch": "התחל להקליד {minSearchLength} או יותר תווים כדי להתחיל בחיפוש." }, "settings": { "admin": "מנהל", @@ -214,10 +227,8 @@ "avoidChanges": "(השאר ריק כדי למנוע שינויים)", "branding": "מיתוג", "brandingDirectoryPath": "נתיב תקיית מיתוג", - "brandingHelp": "אתה יכול להגדיר את האופן שבו האפליקציה תראה על ידי שינוי שם האפליקציה, החלפת הלוגו, הוספת עיצוב מותאם אישית ואפילו השבתת קישורים חיצוניים לGithub.\nלמידע נוסף עיין ב-{0}.", "changePassword": "שנה סיסמא", "commandRunner": "הרצת פקודות", - "commandRunnerHelp": "אתה יכול להגדיר פקודות שיבוצעו באירועים שונים. עליך לכתוב אחד בכל שורה. משתני הסביבה {0} ו-{1} יהיו זמינים, בהיותם {0} ביחס ל-{1}. למידע נוסף על תכונה זו ועל משתני הסביבה הזמינים, עיין ב {2}.", "commandsUpdated": "הפקודות עודכנו.", "createUserDir": "צור תקיית בית במהלך הוספת משתמש חדש", "userHomeBasePath": "נתיב ראשי לתקיות הבית של משתמשים", @@ -300,7 +311,14 @@ "adminOptions": "אפשרויות משתמש מנהל", "shareDuration": "תוקף", "searchOptions": "אפשרויות חיפוש", - "downloads": "הורדות" + "downloads": "הורדות", + "systemAdmin": "מערכת וניהול", + "configViewer": "מציג תצורה", + "configViewerShowFull": "הצג את התצורה המלאה", + "configViewerShowComments": "הצג תגובות", + "configViewerLoadConfig": "טען תצורה", + "brandingHelp": "ניתן להתאים אישית את המראה והתחושה של דפדפן הקבצים על ידי שינוי שמו, החלפת הלוגו, הוספת סגנונות מותאמים אישית ואפילו השבתת קישורים חיצוניים ל-GitHub.\nלמידע נוסף על מיתוג מותאם אישית, בקרו ב- {0}.", + "commandRunnerHelp": "כאן תוכל להגדיר פקודות שיבוצעו באירועים המוזכרים. עליך לכתוב פקודה אחת בכל שורה. המשתנים הסביבתיים {0} ו- {1} יהיו זמינים, והם יהיו {0} ביחס ל- {1}. למידע נוסף על תכונה זו ועל המשתנים הסביבתיים הזמינים, אנא קרא את {2}." }, "sidebar": { "help": "עזרה", @@ -358,12 +376,21 @@ "hideNavButtons": "הסתר כפתורי ניווט", "hideNavButtonsDescription": "הסתר את כפתורי הניווט בסרגל הניווט בשיתוף כדי ליצור מראה מינימליסטי.", "viewModeDescription": "פריסת ברירת המחדל עבור הדף המשותף.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "נשלחה אליך קובץ לשיתוף או להורדה.", "disableShareCard": "השבת כרטיס שיתוף", "disableShareCardDescription": "השבת את כרטיס השיתוף בדף המשותף בסרגל הצד או בחלק העליון של הדף במכשירים ניידים.", "disableSidebar": "השבת סרגל צד", - "disableSidebarDescription": "השבת את סרגל הצד בדף המשותף." + "disableSidebarDescription": "השבת את סרגל הצד בדף המשותף.", + "enforceDarkLightMode": "אכוף מצב ערכת נושא", + "enforceDarkLightModeDescription": "כפה מצב ערכת נושא ספציפי (כהה או בהיר) עבור שיתוף זה, תוך עקיפת העדפות המשתמש.", + "default": "אל תכפה מצב", + "dark": "אפל", + "light": "אור", + "enableOnlyOffice": "הפעל את תוכנת הצפייה OnlyOffice", + "enableOnlyOfficeDescription": "אפשר צפייה בקבצי Office באמצעות OnlyOffice בשיתוף זה.", + "enableOnlyOfficeEditing": "אפשר עריכה ב-OnlyOffice", + "enableOnlyOfficeEditingDescription": "אפשר עריכת קבצי Office באמצעות OnlyOffice בשיתוף זה.", + "titleDefault": "קבצים משותפים - {title}" }, "api": { "title": "מפתחות API", @@ -447,8 +474,8 @@ "totalDenied": "נדחה", "totalAllowed": "מותר", "all": "דחה הכל", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "זה מראה מה קורה כאשר משתמש מנסה לגשת לנתיב שבו אין כללי גישה ספציפיים. 'אפשר' פירושו שמשתמשים יכולים לגשת כברירת מחדל, 'דחה' פירושו שמשתמשים נחסמים כברירת מחדל, אלא אם כן הותר להם במפורש." + "defaultBehaviorDescription": "זה מראה מה קורה כאשר משתמש מנסה לגשת לנתיב שבו אין כללי גישה ספציפיים. 'אפשר' פירושו שמשתמשים יכולים לגשת כברירת מחדל, 'דחה' פירושו שמשתמשים נחסמים כברירת מחדל, אלא אם כן הותר להם במפורש.", + "defaultBehavior": "פעולה ברירת מחדל{suffix}" }, "fileLoading": { "title": "העלאות והורדות", @@ -514,14 +541,24 @@ "defaultThemeDescription": "ברירת מחדל - ערכת הנושא המוגדרת כברירת מחדל", "customTheme": "בחר את הנושא שלך", "showSelectMultiple": "הצג בחירה מרובה בתפריט ההקשר בשולחן העבודה", - "showSelectMultipleDescription": "כברירת מחדל, בתפריט ההקשר של שולחן העבודה אין אפשרות \"בחר מספר פריטים\" כמו במכשירים ניידים. אפשרות זו תמיד מוצגת בתפריט ההקשר." + "showSelectMultipleDescription": "כברירת מחדל, בתפריט ההקשר של שולחן העבודה אין אפשרות \"בחר מספר פריטים\" כמו במכשירים ניידים. אפשרות זו תמיד מוצגת בתפריט ההקשר.", + "defaultMediaPlayer": "השתמש בנגן המדיה המובנה בדפדפן שלך", + "defaultMediaPlayerDescription": "השתמש בנגן המדיה המובנה בדפדפן שלך עבור קבצי וידאו ואודיו, במקום בנגן המדיה 'vue-plyr' הכלול ב-FileBrowser.", + "debugOfficeEditor": "הפעל מצב ניפוי באגים של OnlyOffice", + "debugOfficeEditorDescription": "הצג חלון קופץ לאיתור באגים עם מידע נוסף בעורך OnlyOffice.", + "fileViewerOptions": "אפשרויות תצוגת קבצים", + "themeAndLanguage": "נושא ושפה" }, "editor": { "uninitialized": "לא ניתן לאתחל את העורך. אנא טען מחדש.", "saveFailed": "לא ניתן לשמור את הקובץ. אנא נסה שוב.", "saveAborted": "שמירה בוטלה. שם הקובץ אינו תואם לקובץ הפעיל.", "saveDisabled": "שמירת קבצים משותפים אינה נתמכת.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "פעולת השמירה בוטלה. הקובץ הפעיל של היישום ({activeFile}) אינו תואם לקובץ שאתה מנסה לשמור ({tryingToSave}).", + "syncFailed": "לא הצליח לסנכרן את המצב עם המסלול עבור \"{filename}\" לאחר 5 ניסיונות. מבטל את הגדרת העורך כדי למנוע פגיעה בנתונים." + }, + "player": { + "LoopEnabled": "לולאה מופעלת", + "LoopDisabled": "לולאה מושבתת" } } diff --git a/frontend/src/i18n/hu.json b/frontend/src/i18n/hu.json index 3eba3c895..e62f46a59 100644 --- a/frontend/src/i18n/hu.json +++ b/frontend/src/i18n/hu.json @@ -49,7 +49,8 @@ "clearCompleted": "Tiszta befejezve", "showMore": "Többet mutasson", "showLess": "Kevesebb mutatása", - "openParentFolder": "Szülői mappa megnyitása" + "openParentFolder": "Szülői mappa megnyitása", + "selectedCount": "A művelet végrehajtásához kiválasztott elemek száma" }, "download": { "downloadFile": "Fájl letöltése", @@ -84,15 +85,15 @@ "click": "mappa vagy fájl kijelölése", "ctrl": { "click": "több mappa vagy fájl kijelölése", - "f": "keresés megnyitása", - "s": "az aktuális fájl vagy mappa letöltése" + "d": "kiválasztott elemek letöltése" }, "del": "kijelölt elemek törlése", - "doubleClick": "fájl vagy mappa megnyitása", "esc": "kijelölés törlése és/vagy parancssor bezárása", - "f1": "ezen információ megjelenítése", "f2": "fájl átnevezése", - "help": "Súgó" + "help": "Súgó", + "f1": "show help prompt", + "description": "Az alábbiakban megtekintheti az alapvető navigációs lehetőségeket. További információkért kérjük, látogasson el a következő weboldalra", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -134,16 +135,13 @@ "prompts": { "copy": "Másolása", "copyMessage": "Válassza ki a másolás célját:", - "deleteMessageMultiple": "Biztosan törölni szeretne {count} fájlt?", "deleteMessageSingle": "Biztosan törölni szeretné ezt a fájl vagy mappát?", - "deleteMessageShare": "Biztosan törölni szeretné ezt a megosztást ({path})?", "deleteTitle": "Fájlok törlése", "displayName": "Megjelenített név:", "download": "Fájlok letöltése", "downloadMessage": "Válassza ki a letöltés formátumát.", "error": "Valami rosszul sült el", "fileInfo": "Fájlinformáció", - "lastModified": "Utolsó módosítás{suffix}", "move": "Mozgatás", "moveMessage": "Válasszon új helyet a fájl(ok)nak/mappá(k)nak:", "newArchetype": "Új bejegyzést hoz létre egy archetípus alapján. A fájl a tartalom mappában jön létre.", @@ -151,10 +149,7 @@ "newDirMessage": "Adja meg az új mappa nevét.", "newFile": "Új fájl", "newFileMessage": "Adja meg az új fájl nevét.", - "numberDirs": "Mappák száma{suffix}", - "numberFiles": "Fájlok száma{suffix}", "rename": "Átnevezés", - "renameMessage": "Adja meg az új nevét:", "replace": "Csere", "replaceMessage": "Az egyik feltölteni kívánt fájl a neve miatt ütközik. Szeretné lecserélni a meglévő fájlt?\n", "schedule": "Ütemezés", @@ -172,10 +167,6 @@ "dragAndDrop": "Húzza ide a fájlokat", "completed": "Befejezett", "conflictsDetected": "Feltárt konfliktusok", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Érvénytelen név: nem lehet \"/\" vagy \"\\\" konténer", - "source": "Source{suffix}", "downloadsLimit": "Letöltések száma limit", "maxBandwidth": "Maximális letöltési sávszélesség (kbps)", "shareTheme": "Megosztás témája", @@ -186,22 +177,44 @@ "shareBanner": "Banner URL megosztása", "shareFavicon": "Favicon URL megosztása", "optionalPasswordHelp": "Ha be van állítva, a felhasználóknak ezt a jelszót kell megadniuk a megosztott tartalom megtekintéséhez.", - "viewMode": "Nézet mód" + "viewMode": "Nézet mód", + "renameMessage": "Adjon meg egy új nevet:", + "renameMessageInvalid": "Egy fájl nem tartalmazhat \"/\" vagy \"\\\" konténert.", + "source": "Forrás{suffix}", + "deleteMessageMultiple": "Biztos, hogy törölni szeretné a {count} fájl(ok)at?", + "deleteMessageShare": "Biztos, hogy törölni szeretné ezt a megosztást: {path} ?", + "lastModified": "Utoljára módosítva{suffix}", + "numberDirs": "A könyvtárak száma{suffix}", + "numberFiles": "Fájlok száma{suffix}", + "renameMessageConflict": "A \"{filename}\" nevű elem már létezik!", + "uploadSettingsChunked": "Maximális egyidejű feltöltés: {maxConcurrentUpload}, chunk méret: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Maximális egyidejű feltöltés: {maxConcurrentUpload}, chunked upload disabled" }, "search": { "images": "Képek", "music": "Zene", "pdf": "PDF", - "pressToSearch": "Keresés indítása Enterrel…", "search": "Keresés…", - "typeToSearch": "Keresés indítása beírással…", "types": "Típusok", "video": "Videó", "path": "Útvonal:", "smallerThan": "Kisebb, mint:", "largerThan": "Nagyobb, mint:", "helpText1": "A keresés minden egyes beírt karakterre történik (minimum 3 karakteres keresőkifejezés).", - "helpText2": "Az index: A keresés az indexet használja, amely automatikusan frissül a beállított időközönként (alapértelmezett: 5 perc). A program indulásakor végzett keresés hiányos eredményeket eredményezhet." + "helpText2": "Az index: A keresés az indexet használja, amely automatikusan frissül a beállított időközönként (alapértelmezett: 5 perc). A program indulásakor végzett keresés hiányos eredményeket eredményezhet.", + "noResults": "Az indexelt keresésben nem találtak találatot.", + "onlyFolders": "Csak mappák", + "onlyFiles": "Csak fájlok", + "showOptions": "Beállítások megjelenítése", + "photos": "Fotók", + "audio": "Audio", + "videos": "Videók", + "documents": "Dokumentumok", + "archives": "Archívum", + "number": "Szám", + "searchContext": "Keresési kontextus: {context}", + "notEnoughCharacters": "Nincs elég karakter a kereséshez (min {minSearchLength})", + "typeToSearch": "A keresés megkezdéséhez írja be a {minSearchLength} vagy több karaktert." }, "settings": { "admin": "Admin", @@ -214,10 +227,8 @@ "avoidChanges": "(üresen hagyva nincs változás)", "branding": "Márkázás", "brandingDirectoryPath": "Márkázás mappaútvonala", - "brandingHelp": "Testre szabhatja a File Browser példányának megjelenését a név megváltoztatásával, a logó cseréjével, egyéni stílusok hozzáadásával és még a GitHubra mutató külső hivatkozások letiltásával is.\nAz egyéni márkázással kapcsolatos további információkért tekintse meg: {0}.", "changePassword": "Jelszó módosítása", "commandRunner": "Parancsfuttató", - "commandRunnerHelp": "Beállíthatja azokat a parancsokat, amelyek a megnevezett események során végrehajtásra kerülnek. Soronként egyet kell megadni. A {0} és a {1} környezeti változók lesznek elérhetőek, ahol a {0} relatív a {1}-hez. A funkcióról és a rendelkezésre álló környezeti változókról további információ: {2}.", "commandsUpdated": "Parancsok frissítve!", "createUserDir": "Felhasználók saját mappáinak automatikus létrehozása új felhasználók hozzáadásakor", "userHomeBasePath": "Alap elérési útvonal a felhasználók saját mappáihoz", @@ -300,7 +311,14 @@ "adminOptions": "Admin felhasználói beállítások", "shareDuration": "Lejárat:", "searchOptions": "Keresési lehetőségek", - "downloads": "Letöltések" + "downloads": "Letöltések", + "systemAdmin": "Rendszer és adminisztráció", + "configViewer": "Konfiguráció megtekintő", + "configViewerShowFull": "Teljes konfiguráció megjelenítése", + "configViewerShowComments": "Hozzászólások megjelenítése", + "configViewerLoadConfig": "Konfiguráció betöltése", + "brandingHelp": "Testreszabhatja a Fájlböngésző példányának megjelenését és érzetét a név megváltoztatásával, a logó cseréjével, egyéni stílusok hozzáadásával és akár a GitHubra mutató külső hivatkozások letiltásával.\nAz egyéni márkaépítéssel kapcsolatos további információkat a {0} oldalon talál.", + "commandRunnerHelp": "Itt állíthatja be azokat a parancsokat, amelyek a megnevezett események során végrehajtásra kerülnek. Soronként egyet kell írni. A {0} és a {1} környezeti változók lesznek elérhetőek, a {0} a {1}-hoz képest . A funkcióról és a rendelkezésre álló környezeti változókról bővebb információt a {2} oldalon talál." }, "sidebar": { "help": "Súgó", @@ -358,12 +376,21 @@ "hideNavButtons": "Navigációs gombok elrejtése", "hideNavButtonsDescription": "Rejtse el a navigációs gombokat a megosztás navigációs sávján a minimalista megjelenés érdekében.", "viewModeDescription": "A megosztott oldal alapértelmezett elrendezése.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Megosztást küldtünk Önnek megtekintésre vagy letöltésre.", "disableShareCard": "Megosztási kártya letiltása", "disableShareCardDescription": "Kapcsolja ki a megosztási kártyát a megosztott oldalon az oldalsávban vagy mobilon az oldal tetején.", "disableSidebar": "Oldalsáv kikapcsolása", - "disableSidebarDescription": "Kapcsolja ki az oldalsávot a megosztott oldalon." + "disableSidebarDescription": "Kapcsolja ki az oldalsávot a megosztott oldalon.", + "enforceDarkLightMode": "Témamód érvényesítése", + "enforceDarkLightModeDescription": "Egy adott témamód (sötét vagy világos) kikényszerítése a megosztás számára, felülírva a felhasználói beállításokat.", + "default": "Ne kényszerítsen ki üzemmódot", + "dark": "Sötét", + "light": "Fény", + "enableOnlyOffice": "OnlyOffice megjelenítő engedélyezése", + "enableOnlyOfficeDescription": "Engedélyezze az irodai fájlok megtekintését az OnlyOffice használatával ezen a megosztáson.", + "enableOnlyOfficeEditing": "Engedélyezze az OnlyOffice szerkesztést", + "enableOnlyOfficeEditingDescription": "Engedélyezze az irodai fájlok szerkesztését az OnlyOffice használatával ezen a megosztáson.", + "titleDefault": "Megosztott fájlok - {title}" }, "api": { "title": "API-kulcsok:", @@ -447,8 +474,8 @@ "totalDenied": "Elutasítva", "totalAllowed": "Engedélyezett", "all": "Mindent megtagadni", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Ez azt mutatja, hogy mi történik, ha egy felhasználó megpróbál hozzáférni egy olyan elérési útvonalhoz, ahol nincsenek speciális hozzáférési szabályok. Az 'Allow' azt jelenti, hogy a felhasználók alapértelmezés szerint hozzáférhetnek, a 'Deny' azt jelenti, hogy a felhasználók alapértelmezés szerint blokkolva vannak, kivéve, ha kifejezetten engedélyezik." + "defaultBehaviorDescription": "Ez azt mutatja, hogy mi történik, ha egy felhasználó megpróbál hozzáférni egy olyan elérési útvonalhoz, ahol nincsenek speciális hozzáférési szabályok. Az 'Allow' azt jelenti, hogy a felhasználók alapértelmezés szerint hozzáférhetnek, a 'Deny' azt jelenti, hogy a felhasználók alapértelmezés szerint blokkolva vannak, kivéve, ha kifejezetten engedélyezik.", + "defaultBehavior": "Alapértelmezett cselekvés{suffix}" }, "fileLoading": { "title": "Feltöltések és letöltések", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - Az alapértelmezett téma", "customTheme": "Válasszon témát", "showSelectMultiple": "Többszörös kijelölés megjelenítése az asztali kontextusmenüben", - "showSelectMultipleDescription": "Alapértelmezés szerint az asztali kontextusmenü nem rendelkezik a \"többszörös kiválasztás\" opcióval, mint a mobilon. Ez az opció mindig megjelenik a kontextusmenüben." + "showSelectMultipleDescription": "Alapértelmezés szerint az asztali kontextusmenü nem rendelkezik a \"többszörös kiválasztás\" opcióval, mint a mobilon. Ez az opció mindig megjelenik a kontextusmenüben.", + "defaultMediaPlayer": "Használja a böngésző natív médialejátszóját", + "defaultMediaPlayerDescription": "Használja a böngésző natív médialejátszóját a videó- és hangfájlokhoz, nem pedig a FileBrowser mellékelt \"vue-plyr\" médialejátszóját.", + "debugOfficeEditor": "OnlyOffice hibakeresési mód engedélyezése", + "debugOfficeEditorDescription": "További információkat tartalmazó hibakeresési felugró ablak megjelenítése az OnlyOffice szerkesztőben.", + "fileViewerOptions": "Fájlnéző beállításai", + "themeAndLanguage": "Téma és nyelv" }, "editor": { "uninitialized": "Nem sikerült inicializálni a szerkesztőt. Kérjük, töltse újra.", "saveFailed": "A fájl mentése sikertelen. Kérjük, próbálja újra.", "saveAborted": "A mentés megszakadt. A fájl neve nem egyezik az aktív fájléval.", "saveDisabled": "A megosztott fájlok mentése nem támogatott.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "A mentési művelet megszakadt. Az alkalmazás aktív fájlja ({activeFile}) nem egyezik meg a menteni kívánt fájllal ({tryingToSave}).", + "syncFailed": "5 próbálkozás után nem sikerült szinkronizálni az állapotot a \"{filename}\" útvonallal. A szerkesztő beállításának megszakítása az adatok sérülésének elkerülése érdekében." + }, + "player": { + "LoopEnabled": "Hurok engedélyezve", + "LoopDisabled": "Hurok letiltva" } } diff --git a/frontend/src/i18n/it.json b/frontend/src/i18n/it.json index d750bbf11..463306a94 100644 --- a/frontend/src/i18n/it.json +++ b/frontend/src/i18n/it.json @@ -49,7 +49,8 @@ "clearCompleted": "Completato il lavoro di pulizia", "showMore": "Mostra di più", "showLess": "Mostra meno", - "openParentFolder": "Aprire la cartella madre" + "openParentFolder": "Aprire la cartella madre", + "selectedCount": "Numero di elementi selezionati per eseguire un'azione" }, "download": { "downloadFile": "Scarica file", @@ -84,15 +85,15 @@ "click": "seleziona un file o una cartella", "ctrl": { "click": "seleziona più file o cartelle", - "f": "apre la barra di ricerca", - "s": "salva un file o scarica la cartella in cui ci si trova" + "d": "scaricare gli articoli selezionati" }, "del": "elimina gli elementi selezionati", - "doubleClick": "apre un file o una cartella", "esc": "annulla la selezione e/o chiude la finestra aperta", - "f1": "questo pannello", "f2": "rinomina un file", - "help": "Aiuto" + "help": "Aiuto", + "f1": "mostra la richiesta di aiuto", + "description": "Le opzioni di navigazione di base sono riportate di seguito. Per ulteriori informazioni, visitate il sito", + "wiki": "Wiki di FileBrowser Quantum" }, "languages": { "he": "עברית", @@ -134,7 +135,6 @@ "prompts": { "copy": "Copia", "copyMessage": "Seleziona la cartella in cui copiare i file:", - "deleteMessageMultiple": "Sei sicuro di voler eliminare {count} file?", "deleteMessageSingle": "Sei sicuro di voler eliminare questo file/cartella?", "deleteTitle": "Elimina", "displayName": "Nome visualizzato:", @@ -142,7 +142,6 @@ "downloadMessage": "Seleziona il formato che vuoi scaricare.", "error": "Qualcosa è andato per il verso storto", "fileInfo": "Informazioni sul file", - "lastModified": "Ultima modifica{suffix}", "move": "Sposta", "moveMessage": "Seleziona la nuova posizione per i tuoi file e/o cartella/e:", "newArchetype": "Crea un nuovo post basato su un modello. Il tuo file verrà creato nella cartella.", @@ -150,10 +149,7 @@ "newDirMessage": "Scrivi il nome della nuova cartella.", "newFile": "Nuovo file", "newFileMessage": "Scrivi il nome del nuovo file.", - "numberDirs": "Numero di cartelle{suffix}", - "numberFiles": "Numero di file{suffix}", "rename": "Rinomina", - "renameMessage": "Inserisci un nuovo nome per", "replace": "Sostituisci", "replaceMessage": "Uno dei file che stai cercando di caricare sta generando un conflitto per via del suo nome. Desideri sostituire il file già esistente?\n", "schedule": "Pianifica", @@ -161,7 +157,6 @@ "show": "Mostra", "size": "Dimensione", "upload": "Carica", - "deleteMessageShare": "Are you sure you want to delete this share: {path} ?", "deleteUserMessage": "Siete sicuri di voler eliminare questo utente?", "filesSelected": "file selezionati", "optionalPassword": "Password opzionale", @@ -172,10 +167,6 @@ "dragAndDrop": "Trascinare e rilasciare i file qui", "completed": "Completato", "conflictsDetected": "Conflitti rilevati", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Nome non valido: non può contenere \"/\" o \"\\\".", - "source": "Source{suffix}", "downloadsLimit": "Limite del numero di download", "maxBandwidth": "Larghezza di banda massima per il download (kbps)", "shareTheme": "Tema della condivisione", @@ -186,22 +177,44 @@ "shareBanner": "Condividi l'URL del banner", "shareFavicon": "Condividere l'URL della favicon", "optionalPasswordHelp": "Se impostata, gli utenti devono inserire questa password per visualizzare il contenuto condiviso.", - "viewMode": "Modalità di visualizzazione" + "viewMode": "Modalità di visualizzazione", + "renameMessage": "Inserire un nuovo nome:", + "renameMessageInvalid": "Un file non può contenere \"/\" o \"\\\".", + "source": "Fonte{suffix}", + "deleteMessageMultiple": "Siete sicuri di voler eliminare i file {count}?", + "deleteMessageShare": "Sei sicuro di voler eliminare questa condivisione: {path} ?", + "lastModified": "Ultima modifica{suffix}", + "numberDirs": "Numero di directory{suffix}", + "numberFiles": "Numero di file{suffix}", + "renameMessageConflict": "Un elemento chiamato \"{filename}\" esiste già!", + "uploadSettingsChunked": "Carichi massimi contemporanei: {maxConcurrentUpload}, dimensione dei pezzi: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Carichi massimi contemporanei: {maxConcurrentUpload}, caricamento chunked disabilitato" }, "search": { "images": "Immagini", "music": "Musica", "pdf": "PDF", - "pressToSearch": "Premi Invio per cercare...", "search": "Cerca...", - "typeToSearch": "Scrivi per cercare...", "types": "Tipi", "video": "Video", "path": "Percorso:", "smallerThan": "Più piccolo di:", "largerThan": "Più grande di:", "helpText1": "La ricerca avviene per ogni carattere digitato (minimo 3 caratteri per i termini di ricerca).", - "helpText2": "L'indice: La ricerca utilizza l'indice che viene aggiornato automaticamente all'intervallo configurato (default: 5 minuti). La ricerca quando il programma è appena stato avviato può dare risultati incompleti." + "helpText2": "L'indice: La ricerca utilizza l'indice che viene aggiornato automaticamente all'intervallo configurato (default: 5 minuti). La ricerca quando il programma è appena stato avviato può dare risultati incompleti.", + "noResults": "Nessun risultato trovato nella ricerca indicizzata.", + "onlyFolders": "Solo cartelle", + "onlyFiles": "Solo file", + "showOptions": "Mostra opzioni", + "photos": "Foto", + "audio": "Audio", + "videos": "Video", + "documents": "Documenti", + "archives": "Archivio", + "number": "Numero", + "searchContext": "Contesto di ricerca: {context}", + "notEnoughCharacters": "Non ci sono abbastanza caratteri per la ricerca (min {minSearchLength})", + "typeToSearch": "Iniziare a digitare {minSearchLength} o più caratteri per avviare la ricerca." }, "settings": { "admin": "Admin", @@ -214,10 +227,8 @@ "avoidChanges": "(lascia vuoto per evitare cambiamenti)", "branding": "Branding", "brandingDirectoryPath": "Directory del branding", - "brandingHelp": "Puoi personalizzare l'aspetto e il comportamento di File Browser cambiando il suo nome, logo, aggiungendo stili personalizzati e anche disabilitando link esterni verso GitHub.\nPer altre informazioni sul branding personalizzato, vai alla {0}.", "changePassword": "Modifica password", "commandRunner": "Esecutore di comandi", - "commandRunnerHelp": "Qui puoi impostare i comandi da eseguire negli eventi nominati. Ne devi scrivere uno per riga. Le variabili d'ambiente {0} e {1} sono disponibili, essendo {0} relativo a {1}. Per altre informazioni su questa funzionalità e sulle variabili d'ambiente utilizzabili, leggi la {2}.", "commandsUpdated": "Comandi aggiornati!", "createUserDir": "Crea automaticamente la home directory dell'utente quando lo aggiungi", "customStylesheet": "Foglio di stile personalizzato", @@ -300,7 +311,14 @@ "adminOptions": "Opzioni dell'utente amministratore", "shareDuration": "Scadenza", "searchOptions": "Opzioni di ricerca", - "downloads": "Scaricamento" + "downloads": "Scaricamento", + "systemAdmin": "Sistema e amministrazione", + "configViewer": "Visualizzatore di configurazione", + "configViewerShowFull": "Mostra la configurazione completa", + "configViewerShowComments": "Mostra i commenti", + "configViewerLoadConfig": "Configurazione del carico", + "brandingHelp": "È possibile personalizzare l'aspetto dell'istanza del File Browser cambiandone il nome, sostituendo il logo, aggiungendo stili personalizzati e persino disabilitando i collegamenti esterni a GitHub.\nPer ulteriori informazioni sul branding personalizzato, consultare il sito {0}.", + "commandRunnerHelp": "Qui è possibile impostare i comandi che vengono eseguiti negli eventi denominati. È necessario scriverne uno per riga. Le variabili d'ambiente {0} e {1} saranno disponibili, essendo {0} relativa a {1}. Per ulteriori informazioni su questa funzione e sulle variabili d'ambiente disponibili, leggere {2}." }, "sidebar": { "help": "Aiuto", @@ -358,12 +376,21 @@ "hideNavButtons": "Nascondere i pulsanti di navigazione", "hideNavButtonsDescription": "Nascondere i pulsanti di navigazione sulla navbar nella condivisione per creare un look minimalista.", "viewModeDescription": "Layout predefinito per la pagina condivisa.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "È stata inviata una condivisione da visualizzare o scaricare.", "disableShareCard": "Disattivare la scheda di condivisione", "disableShareCardDescription": "Disattivare la scheda di condivisione sulla pagina condivisa nella barra laterale o nella parte superiore della pagina su mobile.", "disableSidebar": "Disattivare la barra laterale", - "disableSidebarDescription": "Disattivare la barra laterale della pagina condivisa." + "disableSidebarDescription": "Disattivare la barra laterale della pagina condivisa.", + "enforceDarkLightMode": "Applicare la modalità a tema", + "enforceDarkLightModeDescription": "Forza una modalità tema specifica (scuro o chiaro) per questa condivisione, ignorando le preferenze dell'utente.", + "default": "Non applicare una modalità", + "dark": "Scuro", + "light": "Luce", + "enableOnlyOffice": "Abilitate il visualizzatore OnlyOffice", + "enableOnlyOfficeDescription": "Consente di visualizzare i file di Office utilizzando OnlyOffice in questa condivisione.", + "enableOnlyOfficeEditing": "Abilita la modifica di OnlyOffice", + "enableOnlyOfficeEditingDescription": "Consentire la modifica dei file di Office utilizzando OnlyOffice in questa condivisione.", + "titleDefault": "File condivisi - {title}" }, "api": { "title": "Chiavi API:", @@ -447,8 +474,8 @@ "totalDenied": "Negato", "totalAllowed": "Consentito", "all": "Rifiuta tutto", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Questo mostra cosa succede quando un utente cerca di accedere a un percorso per il quale non esistono regole di accesso specifiche. 'Consenti' significa che gli utenti possono accedere per impostazione predefinita, 'Rifiuta' significa che gli utenti sono bloccati per impostazione predefinita, a meno che non siano esplicitamente autorizzati." + "defaultBehaviorDescription": "Questo mostra cosa succede quando un utente cerca di accedere a un percorso per il quale non esistono regole di accesso specifiche. 'Consenti' significa che gli utenti possono accedere per impostazione predefinita, 'Rifiuta' significa che gli utenti sono bloccati per impostazione predefinita, a meno che non siano esplicitamente autorizzati.", + "defaultBehavior": "Azione predefinita{suffix}" }, "fileLoading": { "title": "Caricamenti e download", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - Il tema predefinito", "customTheme": "Scegliete il vostro tema", "showSelectMultiple": "Mostra selezione multipla nel menu contestuale sul desktop", - "showSelectMultipleDescription": "Per impostazione predefinita, il menu contestuale del desktop non dispone dell'opzione \"seleziona più\" come quello del cellulare. Questa opzione viene sempre visualizzata nel menu contestuale." + "showSelectMultipleDescription": "Per impostazione predefinita, il menu contestuale del desktop non dispone dell'opzione \"seleziona più\" come quello del cellulare. Questa opzione viene sempre visualizzata nel menu contestuale.", + "defaultMediaPlayer": "Utilizzare il lettore multimediale nativo del browser", + "defaultMediaPlayerDescription": "Utilizzate il lettore multimediale nativo del vostro browser per i file video e audio piuttosto che il lettore multimediale \"vue-plyr\" incluso in FileBrowser.", + "debugOfficeEditor": "Abilita la modalità di debug di OnlyOffice", + "debugOfficeEditorDescription": "Mostra un popup di debug con informazioni aggiuntive nell'editor di OnlyOffice.", + "fileViewerOptions": "Opzioni del visualizzatore di file", + "themeAndLanguage": "Tema e linguaggio" }, "editor": { "uninitialized": "Impossibile inizializzare l'editor. Ricaricare.", "saveFailed": "Impossibile salvare il file. Riprovare.", "saveAborted": "Salvataggio interrotto. Il nome del file non corrisponde al file attivo.", "saveDisabled": "Il salvataggio di file condivisi non è supportato.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Operazione di salvataggio interrotta. Il file attivo dell'applicazione ({activeFile}) non corrisponde al file che si sta cercando di salvare ({tryingToSave}).", + "syncFailed": "non è riuscito a sincronizzare lo stato con il percorso per \"{filename}\" dopo 5 tentativi. Interruzione dell'impostazione dell'editor per evitare la corruzione dei dati." + }, + "player": { + "LoopEnabled": "Loop abilitato", + "LoopDisabled": "Loop disabilitato" } } diff --git a/frontend/src/i18n/ja.json b/frontend/src/i18n/ja.json index 08918b0cb..261495520 100644 --- a/frontend/src/i18n/ja.json +++ b/frontend/src/i18n/ja.json @@ -49,7 +49,8 @@ "clearCompleted": "クリア完了", "showMore": "もっと見る", "showLess": "もっと見る", - "openParentFolder": "親フォルダを開く" + "openParentFolder": "親フォルダを開く", + "selectedCount": "アクションを実行するために選択された項目の数" }, "download": { "downloadFile": "Download File", @@ -84,15 +85,15 @@ "click": "ファイルやディレクトリを選択", "ctrl": { "click": "複数のファイルやディレクトリを選択", - "f": "検索を有効にする", - "s": "ファイルを保存またはカレントディレクトリをダウンロード" + "d": "ダウンロード" }, "del": "選択した項目を削除", - "doubleClick": "ファイルやディレクトリをオープン", "esc": "選択をクリアーまたはプロンプトを閉じる", - "f1": "このヘルプを表示", "f2": "ファイルの名前を変更", - "help": "ヘルプ" + "help": "ヘルプ", + "f1": "ヘルププロンプトを表示する", + "description": "基本的なナビオプションは以下からご覧いただけます。その他の情報については", + "wiki": "ファイルブラウザ クォンタム ウィキ" }, "languages": { "he": "עברית", @@ -134,7 +135,6 @@ "prompts": { "copy": "コピー", "copyMessage": "コピーの目標ディレクトリを選択してください:", - "deleteMessageMultiple": "{count} つのファイルを本当に削除してよろしいですか。", "deleteMessageSingle": "このファイル/フォルダを本当に削除してよろしいですか。", "deleteTitle": "ファイルを削除", "displayName": "名前:", @@ -142,7 +142,6 @@ "downloadMessage": "圧縮形式を選択してください。", "error": "あるエラーが発生しました。", "fileInfo": "ファイル情報", - "lastModified": "最終変更{suffix}", "move": "移動", "moveMessage": "移動の目標ディレクトリを選択してください:", "newArchetype": "ある元型に基づいて新しいポストを作成します。ファイルは コンテンツフォルダに作成されます。", @@ -150,10 +149,7 @@ "newDirMessage": "新しいディレクトリの名前を入力してください。", "newFile": "新しいファイルを作成", "newFileMessage": "新しいファイルの名前を入力してください。", - "numberDirs": "ディレクトリ個数{suffix}", - "numberFiles": "ファイル個数{suffix}", "rename": "名前を変更", - "renameMessage": "名前を変更しようファイルは:", "replace": "置き換える", "replaceMessage": "アップロードするファイルの中でかち合う名前が一つあります。 既存のファイルを置き換えりませんか。\n", "schedule": "スケジュール", @@ -161,7 +157,6 @@ "show": "表示", "size": "サイズ", "upload": "アップロード", - "deleteMessageShare": "Are you sure you want to delete this share: {path} ?", "deleteUserMessage": "本当にこのユーザーを削除しますか?", "filesSelected": "選択されたファイル", "optionalPassword": "任意のパスワード", @@ -172,10 +167,6 @@ "dragAndDrop": "ここにファイルをドラッグ&ドロップする", "completed": "完成", "conflictsDetected": "コンフリクト検出", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "無効な名前が指定されました:\"/\"または\"˶\"をコンテナに入れることはできません。", - "source": "Source{suffix}", "downloadsLimit": "ダウンロード数制限", "maxBandwidth": "最大ダウンロード帯域幅 (kbps)", "shareTheme": "シェアテーマ", @@ -186,22 +177,44 @@ "shareBanner": "共有バナーURL", "shareFavicon": "ファビコンのURLを共有する", "optionalPasswordHelp": "設定されている場合、ユーザーは共有コンテンツを見るためにこのパスワードを入力しなければならない。", - "viewMode": "ビューモード" + "viewMode": "ビューモード", + "renameMessage": "新しい名前を入力する:", + "renameMessageInvalid": "ファイルには\"/\"や\"˶\"を入れることはできません。", + "source": "ソース{suffix}", + "deleteMessageMultiple": "本当に{count} ファイルを削除しますか?", + "deleteMessageShare": "この共有を削除してもよろしいですか? {path} ?", + "lastModified": "最終更新日{suffix}", + "numberDirs": "ディレクトリ数{suffix}", + "numberFiles": "ファイル数{suffix}", + "renameMessageConflict": "{filename}\" という名前のアイテムがすでに存在する!", + "uploadSettingsChunked": "最大同時アップロード数{maxConcurrentUpload}チャンクサイズ {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "最大同時アップロード数{maxConcurrentUpload}チャンクアップロード無効" }, "search": { "images": "画像", "music": "音楽", "pdf": "PDF", - "pressToSearch": "Press enter to search...", "search": "検索...", - "typeToSearch": "Type to search...", "types": "種類", "video": "ビデオ", "path": "パス", "smallerThan": "より小さい:", "largerThan": "より大きい:", "helpText1": "検索は入力した文字ごとに行われます(検索語は最低3文字)。", - "helpText2": "インデックス:検索は、設定された間隔(デフォルト:5分)で自動的に更新されるインデックスを利用します。プログラムが開始したばかりの時に検索すると、不完全な結果になることがあります。" + "helpText2": "インデックス:検索は、設定された間隔(デフォルト:5分)で自動的に更新されるインデックスを利用します。プログラムが開始したばかりの時に検索すると、不完全な結果になることがあります。", + "noResults": "インデックス検索で結果が見つかりませんでした。", + "onlyFolders": "フォルダのみ", + "onlyFiles": "ファイルのみ", + "showOptions": "オプションの表示", + "photos": "写真", + "audio": "オーディオ", + "videos": "ビデオ", + "documents": "資料", + "archives": "アーカイブス", + "number": "番号", + "searchContext": "検索コンテキスト:{context}", + "notEnoughCharacters": "検索する文字数が足りない (最小値{minSearchLength})", + "typeToSearch": "検索を開始するには、{minSearchLength} 以上の文字を入力します。" }, "settings": { "admin": "管理者", @@ -214,10 +227,8 @@ "avoidChanges": "(変更を避けるために空白にしてください)", "branding": "Branding", "brandingDirectoryPath": "Branding directory path", - "brandingHelp": "You can customize how your File Browser instance looks and feels by changing its name, replacing the logo, adding custom styles and even disable external links to GitHub.\nFor more information about custom branding, please check out the {0}.", "changePassword": "パスワードを変更", "commandRunner": "Command runner", - "commandRunnerHelp": "Here you can set commands that are executed in the named events. You must write one per line. The environment variables {0} and {1} will be available, being {0} relative to {1}. For more information about this feature and the available environment variables, please read the {2}.", "commandsUpdated": "コマンドは更新されました!", "createUserDir": "Auto create user home dir while adding new user", "customStylesheet": "カスタムスタイルシ ート", @@ -300,7 +311,14 @@ "adminOptions": "管理者ユーザーオプション", "shareDuration": "期限切れ", "searchOptions": "検索オプション", - "downloads": "ダウンロード" + "downloads": "ダウンロード", + "systemAdmin": "システム&管理", + "configViewer": "コンフィグビューア", + "configViewerShowFull": "フルコンフィグを表示", + "configViewerShowComments": "コメントを表示", + "configViewerLoadConfig": "ロードコンフィグ", + "brandingHelp": "ファイルブラウザのインスタンスの見た目や使い勝手は、名前の変更、ロゴの置き換え、カスタムスタイルの追加、GitHub への外部リンクの無効化などでカスタマイズできます。\nカスタムブランディングの詳細については、{0} をご覧ください。", + "commandRunnerHelp": "ここでは、指定したイベントで実行されるコマンドを設定することができる。1行に1つ書く必要がある。環境変数{0} と{1} が使用可能になり、{0} は{1} の相対変数になります。この機能と使用可能な環境変数の詳細については、{2} をお読みください。" }, "sidebar": { "help": "ヘルプ", @@ -358,12 +376,21 @@ "hideNavButtons": "ナビゲーションボタンを隠す", "hideNavButtonsDescription": "シェア内のナビバーにあるナビゲーションボタンを非表示にして、ミニマルな外観を作る。", "viewModeDescription": "共有ページのデフォルトレイアウト。", - "titleDefault": "Shared files - {title}", "descriptionDefault": "シェアが送信されましたので、閲覧またはダウンロードしてください。", "disableShareCard": "シェアカードを無効にする", "disableShareCardDescription": "サイドバーの共有ページ、またはモバイルのページ上部にある共有カードを無効にする。", "disableSidebar": "サイドバーを無効にする", - "disableSidebarDescription": "共有ページのサイドバーを無効にする。" + "disableSidebarDescription": "共有ページのサイドバーを無効にする。", + "enforceDarkLightMode": "テーマモードの強制", + "enforceDarkLightModeDescription": "この共有に特定のテーマモード(ダークまたはライト)を強制します。", + "default": "モードを強制しない", + "dark": "暗い", + "light": "ライト", + "enableOnlyOffice": "OnlyOfficeビューアを有効にする", + "enableOnlyOfficeDescription": "この共有でOnlyOfficeを使用したオフィスファイルの閲覧を許可する。", + "enableOnlyOfficeEditing": "OnlyOffice編集を有効にする", + "enableOnlyOfficeEditingDescription": "この共有でOnlyOfficeを使ったオフィスファイルの編集を許可する。", + "titleDefault": "共有ファイル{title}" }, "api": { "title": "APIキー:", @@ -447,8 +474,8 @@ "totalDenied": "却下", "totalAllowed": "可", "all": "すべて拒否", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "これは、特定のアクセスルールが存在しないパスにユーザがアクセスしようとしたときに何が起こるかを示している。Allow」はユーザーがデフォルトでアクセスできることを意味し、「Deny」は明示的に許可されない限り、ユーザーがデフォルトでブロックされることを意味する。" + "defaultBehaviorDescription": "これは、特定のアクセスルールが存在しないパスにユーザがアクセスしようとしたときに何が起こるかを示している。Allow」はユーザーがデフォルトでアクセスできることを意味し、「Deny」は明示的に許可されない限り、ユーザーがデフォルトでブロックされることを意味する。", + "defaultBehavior": "デフォルト・アクション{suffix}" }, "fileLoading": { "title": "アップロードとダウンロード", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - デフォルトのテーマ", "customTheme": "テーマを選ぶ", "showSelectMultiple": "デスクトップのコンテキストメニューに複数選択を表示する", - "showSelectMultipleDescription": "デフォルトでは、デスクトップのコンテキストメニューには、モバイルのような「複数選択」オプションはありません。このオプションは常にコンテキストメニューに表示されます。" + "showSelectMultipleDescription": "デフォルトでは、デスクトップのコンテキストメニューには、モバイルのような「複数選択」オプションはありません。このオプションは常にコンテキストメニューに表示されます。", + "defaultMediaPlayer": "ブラウザのネイティブ・メディア・プレーヤーを使う", + "defaultMediaPlayerDescription": "ビデオやオーディオファイルには、FileBrowserに含まれる「vue-plyr」メディアプレーヤーではなく、ブラウザのネイティブメディアプレーヤーを使用してください。", + "debugOfficeEditor": "OnlyOfficeデバッグモードを有効にする", + "debugOfficeEditorDescription": "OnlyOfficeエディタに追加情報のデバッグポップアップを表示します。", + "fileViewerOptions": "ファイル・ビューアーのオプション", + "themeAndLanguage": "テーマと言語" }, "editor": { "uninitialized": "エディタの初期化に失敗しました。再読み込みしてください。", "saveFailed": "ファイルの保存に失敗しました。もう一度お試しください。", "saveAborted": "保存が中止された。ファイル名がアクティブなファイルと一致しません。", "saveDisabled": "共有ファイルの保存はサポートされていません。", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "保存操作が中断されました。アプリケーションのアクティブファイル({activeFile} )と保存しようとしているファイル({tryingToSave} )が一致しません。", + "syncFailed": "{filename}\" のルートとステートの同期に失敗しました。データの破損を防ぐためにエディタのセットアップを中止しています。" + }, + "player": { + "LoopEnabled": "ループ有効", + "LoopDisabled": "ループ無効" } } diff --git a/frontend/src/i18n/ko.json b/frontend/src/i18n/ko.json index ace623439..9f47a33de 100644 --- a/frontend/src/i18n/ko.json +++ b/frontend/src/i18n/ko.json @@ -49,7 +49,8 @@ "clearCompleted": "완료 지우기", "showMore": "자세히 보기", "showLess": "덜 보기", - "openParentFolder": "상위 폴더 열기" + "openParentFolder": "상위 폴더 열기", + "selectedCount": "작업을 수행하기 위해 선택한 항목 수" }, "download": { "downloadFile": "파일 다운로드", @@ -84,15 +85,15 @@ "click": "파일이나 디렉토리를 선택해주세요.", "ctrl": { "click": "여러 개의 파일이나 디렉토리를 선택해주세요.", - "f": "검색창 열기", - "s": "파일 또는 디렉토리 다운로드" + "d": "선택한 항목 다운로드" }, "del": "선택된 파일 삭제", - "doubleClick": "파일 또는 디렉토리 열기", "esc": "선택 취소/프롬프트 닫기", - "f1": "정보", "f2": "파일 이름 변경", - "help": "도움말" + "help": "도움말", + "f1": "도움말 프롬프트 표시", + "description": "아래에서 기본 탐색 옵션을 확인할 수 있습니다. 자세한 내용은 다음을 참조하세요.", + "wiki": "파일브라우저 퀀텀 위키" }, "languages": { "he": "עברית", @@ -134,7 +135,6 @@ "prompts": { "copy": "복사", "copyMessage": "복사할 디렉토리:", - "deleteMessageMultiple": "{count} 개의 파일을 삭제하시겠습니까?", "deleteMessageSingle": "파일 혹은 디렉토리를 삭제하시겠습니까?", "deleteTitle": "파일 삭제", "displayName": "게시 이름:", @@ -142,7 +142,6 @@ "downloadMessage": "다운로드 포맷 설정.", "error": "에러 발생!", "fileInfo": "파일 정보", - "lastModified": "최종 수정{suffix}", "move": "이동", "moveMessage": "이동할 화일 또는 디렉토리를 선택하세요:", "newArchetype": "원형을 유지하는 포스트를 생성합니다. 파일은 컨텐트 폴더에 생성됩니다.", @@ -150,10 +149,7 @@ "newDirMessage": "새 디렉토리 이름을 입력해주세요.", "newFile": "새 파일", "newFileMessage": "새 파일 이름을 입력해주세요.", - "numberDirs": "디렉토리 수{suffix}", - "numberFiles": "파일 수{suffix}", "rename": "이름 변경", - "renameMessage": "새로운 이름을 입력하세요.", "replace": "대체하기", "replaceMessage": "동일한 파일 이름이 존재합니다. 현재 파일을 덮어쓸까요?\n", "schedule": "일정", @@ -161,7 +157,6 @@ "show": "보기", "size": "크기", "upload": "업로드", - "deleteMessageShare": "Are you sure you want to delete this share: {path} ?", "deleteUserMessage": "이 사용자를 삭제하시겠습니까?", "filesSelected": "선택한 파일", "optionalPassword": "선택적 비밀번호", @@ -172,10 +167,6 @@ "dragAndDrop": "파일을 여기로 끌어다 놓기", "completed": "완료", "conflictsDetected": "충돌 감지", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "제공된 이름이 잘못되었습니다: \"/\" 또는 \"\\\"를 포함할 수 없습니다.", - "source": "Source{suffix}", "downloadsLimit": "다운로드 횟수 제한", "maxBandwidth": "최대 다운로드 대역폭(kbps)", "shareTheme": "테마 공유", @@ -186,22 +177,44 @@ "shareBanner": "배너 URL 공유", "shareFavicon": "파비콘 URL 공유", "optionalPasswordHelp": "이 비밀번호를 설정하면 사용자가 공유 콘텐츠를 보려면 이 비밀번호를 입력해야 합니다.", - "viewMode": "보기 모드" + "viewMode": "보기 모드", + "renameMessage": "새 이름을 입력합니다:", + "renameMessageInvalid": "파일은 \"/\" 또는 \"\\\"를 포함할 수 없습니다.", + "source": "출처{suffix}", + "deleteMessageMultiple": "{count} 파일을 정말 삭제하시겠습니까?", + "deleteMessageShare": "이 공유를 삭제하시겠습니까? {path} ?", + "lastModified": "마지막 수정{suffix}", + "numberDirs": "디렉터리 수{suffix}", + "numberFiles": "파일 수{suffix}", + "renameMessageConflict": "\"{filename}\"라는 이름의 항목이 이미 존재합니다!", + "uploadSettingsChunked": "최대 동시 업로드: {maxConcurrentUpload}, 청크 크기: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "최대 동시 업로드: {maxConcurrentUpload}, 청크 업로드 비활성화" }, "search": { "images": "이미지", "music": "음악", "pdf": "PDF", - "pressToSearch": "검색하려면 엔터를 입력하세요", "search": "검색...", - "typeToSearch": "검색어 입력...", "types": "Types", "video": "비디오", "path": "경로:", "smallerThan": "보다 작습니다:", "largerThan": "보다 큽니다:", "helpText1": "검색은 입력하는 각 문자(검색어의 경우 최소 3자)에 대해 이루어집니다.", - "helpText2": "색인: 검색은 설정된 간격(기본값: 5분)에 따라 자동으로 업데이트되는 색인을 활용합니다. 프로그램이 막 시작되었을 때 검색하면 불완전한 결과가 나올 수 있습니다." + "helpText2": "색인: 검색은 설정된 간격(기본값: 5분)에 따라 자동으로 업데이트되는 색인을 활용합니다. 프로그램이 막 시작되었을 때 검색하면 불완전한 결과가 나올 수 있습니다.", + "noResults": "색인 검색에서 결과를 찾을 수 없습니다.", + "onlyFolders": "폴더만", + "onlyFiles": "파일만", + "showOptions": "옵션 표시", + "photos": "사진", + "audio": "오디오", + "videos": "동영상", + "documents": "문서", + "archives": "아카이브", + "number": "번호", + "searchContext": "컨텍스트 검색: {context}", + "notEnoughCharacters": "검색할 문자가 부족합니다(최소 {minSearchLength})", + "typeToSearch": "검색을 시작하려면 {minSearchLength} 을 입력하세요." }, "settings": { "admin": "관리자", @@ -214,10 +227,8 @@ "avoidChanges": "(수정하지 않으면 비워두세요)", "branding": "브랜딩", "brandingDirectoryPath": "브랜드 디렉토리 경로", - "brandingHelp": "File Browser 인스턴스는 이름, 로고, 스타일 등을 변경할 수 있습니다. 자세한 사항은 여기{0}에서 확인하세요.", "changePassword": "비밀번호 변경", "commandRunner": "명령 실행기", - "commandRunnerHelp": "이벤트에 해당하는 명령을 설정하세요. 줄당 1개의 명령을 적으세요. 환경 변수{0} 와 {1}이 사용가능하며, {0} 은 {1}에 상대 경로 입니다. 자세한 사항은 {2} 를 참조하세요.", "commandsUpdated": "명령 수정됨!", "createUserDir": "Auto create user home dir while adding new user", "customStylesheet": "커스텀 스타일시트", @@ -300,7 +311,14 @@ "adminOptions": "관리자 사용자 옵션", "shareDuration": "만료", "searchOptions": "검색 옵션", - "downloads": "다운로드" + "downloads": "다운로드", + "systemAdmin": "시스템 및 관리자", + "configViewer": "구성 뷰어", + "configViewerShowFull": "전체 구성 표시", + "configViewerShowComments": "댓글 표시", + "configViewerLoadConfig": "로드 구성", + "brandingHelp": "파일 브라우저 인스턴스의 이름을 변경하고, 로고를 바꾸고, 사용자 지정 스타일을 추가하고, GitHub에 대한 외부 링크를 비활성화하여 모양과 느낌을 사용자 지정할 수 있습니다.\n사용자 지정 브랜딩에 대한 자세한 내용은 {0} 을 참조하세요.", + "commandRunnerHelp": "여기에서 명명된 이벤트에서 실행되는 명령을 설정할 수 있습니다. 한 줄에 하나씩 작성해야 합니다. 환경 변수 {0} 및 {1} 을 사용할 수 있으며, {0} 은 {1} 에 상대적입니다. 이 기능 및 사용 가능한 환경 변수에 대한 자세한 내용은 {2} 을 참조하세요." }, "sidebar": { "help": "도움말", @@ -358,12 +376,21 @@ "hideNavButtons": "탐색 버튼 숨기기", "hideNavButtonsDescription": "공유의 탐색 모음에서 탐색 버튼을 숨겨 미니멀한 디자인을 만듭니다.", "viewModeDescription": "공유 페이지의 기본 레이아웃입니다.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "보거나 다운로드할 수 있는 공유물이 전송되었습니다.", "disableShareCard": "공유 카드 비활성화", "disableShareCardDescription": "사이드바의 공유 페이지 또는 모바일의 페이지 상단에서 공유 카드를 비활성화합니다.", "disableSidebar": "사이드바 비활성화", - "disableSidebarDescription": "공유 페이지에서 사이드바를 비활성화합니다." + "disableSidebarDescription": "공유 페이지에서 사이드바를 비활성화합니다.", + "enforceDarkLightMode": "테마 모드 적용", + "enforceDarkLightModeDescription": "이 공유에 특정 테마 모드(어둡거나 밝음)를 강제로 적용하여 사용자 기본 설정을 재정의합니다.", + "default": "모드 강제 적용 안 함", + "dark": "Dark", + "light": "빛", + "enableOnlyOffice": "Office 뷰어만 사용", + "enableOnlyOfficeDescription": "이 공유에서 OnlyOffice를 사용하여 Office 파일을 볼 수 있도록 허용합니다.", + "enableOnlyOfficeEditing": "Office 편집만 사용", + "enableOnlyOfficeEditingDescription": "이 공유에서 OnlyOffice를 사용하여 Office 파일을 편집할 수 있도록 허용합니다.", + "titleDefault": "공유 파일 - {title}" }, "api": { "title": "API 키:", @@ -447,8 +474,8 @@ "totalDenied": "거부됨", "totalAllowed": "허용됨", "all": "모두 거부", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "사용자가 특정 액세스 규칙이 없는 경로에 액세스하려고 할 때 어떤 일이 발생하는지 보여줍니다. '허용'은 사용자가 기본적으로 액세스할 수 있음을 의미하며, '거부'는 명시적으로 허용되지 않는 한 기본적으로 차단됨을 의미합니다." + "defaultBehaviorDescription": "사용자가 특정 액세스 규칙이 없는 경로에 액세스하려고 할 때 어떤 일이 발생하는지 보여줍니다. '허용'은 사용자가 기본적으로 액세스할 수 있음을 의미하며, '거부'는 명시적으로 허용되지 않는 한 기본적으로 차단됨을 의미합니다.", + "defaultBehavior": "기본 작업{suffix}" }, "fileLoading": { "title": "업로드 및 다운로드", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - 기본 테마", "customTheme": "테마 선택", "showSelectMultiple": "데스크톱의 컨텍스트 메뉴에서 여러 개 선택 표시", - "showSelectMultipleDescription": "기본적으로 데스크톱 컨텍스트 메뉴에는 모바일처럼 '여러 개 선택' 옵션이 없습니다. 이 옵션은 항상 컨텍스트 메뉴에 표시됩니다." + "showSelectMultipleDescription": "기본적으로 데스크톱 컨텍스트 메뉴에는 모바일처럼 '여러 개 선택' 옵션이 없습니다. 이 옵션은 항상 컨텍스트 메뉴에 표시됩니다.", + "defaultMediaPlayer": "브라우저의 기본 미디어 플레이어 사용", + "defaultMediaPlayerDescription": "비디오 및 오디오 파일은 FileBrowser에 포함된 'vue-plyr' 미디어 플레이어가 아닌 브라우저의 기본 미디어 플레이어를 사용하세요.", + "debugOfficeEditor": "Office 디버그 모드만 사용", + "debugOfficeEditorDescription": "OnlyOffice 편집기에서 추가 정보가 포함된 디버그 팝업을 표시합니다.", + "fileViewerOptions": "파일 뷰어 옵션", + "themeAndLanguage": "테마 및 언어" }, "editor": { "uninitialized": "편집기를 초기화하지 못했습니다. 다시 로드하세요.", "saveFailed": "파일을 저장하지 못했습니다. 다시 시도하세요.", "saveAborted": "저장이 중단되었습니다. 파일 이름이 활성 파일과 일치하지 않습니다.", "saveDisabled": "공유 파일 저장은 지원되지 않습니다.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "저장 작업이 중단되었습니다. 애플리케이션의 활성 파일({activeFile})이 저장하려는 파일({tryingToSave})과 일치하지 않습니다.", + "syncFailed": "5번 시도 후 \"{filename}\"의 경로와 상태를 동기화하지 못했습니다. 데이터 손상을 방지하기 위해 편집기 설정을 중단합니다." + }, + "player": { + "LoopEnabled": "루프 사용", + "LoopDisabled": "루프 비활성화" } } diff --git a/frontend/src/i18n/nl-be.json b/frontend/src/i18n/nl-be.json index 7c94b51e9..4ce9ebab4 100644 --- a/frontend/src/i18n/nl-be.json +++ b/frontend/src/i18n/nl-be.json @@ -49,7 +49,8 @@ "clearCompleted": "Gereed", "showMore": "Meer tonen", "showLess": "Minder tonen", - "openParentFolder": "Bovenliggende map openen" + "openParentFolder": "Bovenliggende map openen", + "selectedCount": "Aantal items geselecteerd om een actie uit te voeren" }, "download": { "downloadFile": "Bestand downloaden", @@ -84,15 +85,15 @@ "click": "selecteer bestand of map", "ctrl": { "click": "selecteer meerdere bestanden of mappen", - "f": "opent zoeken", - "s": "sla een bestand op of download de map waar u bent" + "d": "geselecteerde items downloaden" }, "del": "verwijder geselecteerde items", - "doubleClick": "open een bestand of map", "esc": "Wis selectie en/of sluit de prompt", - "f1": "deze informatie", "f2": "bestand herbenoemen", - "help": "Help" + "help": "Help", + "f1": "help prompt tonen", + "description": "Je kunt de basisnavigatieopties hieronder bekijken. Ga voor aanvullende informatie naar", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -134,7 +135,6 @@ "prompts": { "copy": "Kopiëren", "copyMessage": "Kies een locatie om uw bestanden te kopiëren: ", - "deleteMessageMultiple": "Weet u zeker dat u {count} bestand(en) wil verwijderen?", "deleteMessageSingle": "Weet u zeker dat u dit bestand/map wil verwijderen?", "deleteTitle": "Bestanden verwijderen", "displayName": "Weergavenaam: ", @@ -142,7 +142,6 @@ "downloadMessage": "Kies het vermat dat u wilt downloaden. ", "error": "Er ging iets fout", "fileInfo": "Bestandinformatie", - "lastModified": "Laatst Bewerkt{suffix}", "move": "Verplaatsen", "moveMessage": "Kies een nieuwe locatie voor je bestand(en)/map(pen)", "newArchetype": "Maak een nieuw bericht op basis van een archetype. Uw bestand wordt aangemaakt in de inhoudsmap.", @@ -150,10 +149,7 @@ "newDirMessage": "Schrijf de naam van de nieuwe map", "newFile": "Nieuw bestand", "newFileMessage": "Schrijf de naam van het nieuwe bestand", - "numberDirs": "Aantal mappen{suffix}", - "numberFiles": "Aantal bestanden{suffix}", "rename": "Hernoemen", - "renameMessage": "Voer een nieuwe naam in voor", "replace": "Vervangen", "replaceMessage": "Een van de bestanden die u probeert te uploaden, geeft conflicten i.v.m. de naamgeving. Wilt u de bestaande bestanden vervangen?\n", "schedule": "Plannen", @@ -161,7 +157,6 @@ "show": "Tonen", "size": "Grootte", "upload": "Uploaden", - "deleteMessageShare": "Are you sure you want to delete this share: {path} ?", "deleteUserMessage": "Weet je zeker dat je deze gebruiker wilt verwijderen?", "selected": "geselecteerd", "filesSelected": "geselecteerde bestanden", @@ -172,10 +167,6 @@ "dragAndDrop": "Sleep bestanden hier naartoe", "completed": "Voltooid", "conflictsDetected": "Gedetecteerde conflicten", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Ongeldige naam opgegeven: kan geen \"/\" of \"˜\" bevatten.", - "source": "Source{suffix}", "downloadsLimit": "Limiet aantal downloads", "maxBandwidth": "Max. downloadbandbreedte (kbps)", "shareTheme": "Thema delen", @@ -186,22 +177,44 @@ "shareBanner": "URL banner delen", "shareFavicon": "favicon URL delen", "optionalPasswordHelp": "Indien ingesteld, moeten gebruikers dit wachtwoord invoeren om de gedeelde inhoud te bekijken.", - "viewMode": "Weergavemodus" + "viewMode": "Weergavemodus", + "renameMessage": "Voer een nieuwe naam in:", + "renameMessageInvalid": "Een bestand kan geen \"/\" of \"\\\" bevatten.", + "source": "Bron{suffix}", + "deleteMessageMultiple": "Weet je zeker dat je {count} bestand(en) wilt verwijderen?", + "deleteMessageShare": "Weet je zeker dat je deze share wilt verwijderen: {path} ?", + "lastModified": "Laatst gewijzigd{suffix}", + "numberDirs": "Aantal mappen{suffix}", + "numberFiles": "Aantal bestanden{suffix}", + "renameMessageConflict": "Een item met de naam \"{filename}\" bestaat al!", + "uploadSettingsChunked": "Max gelijktijdige uploads: {maxConcurrentUpload}, brokgrootte: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Max gelijktijdige uploads: {maxConcurrentUpload}, chunked upload uitgeschakeld" }, "search": { "images": "Afbeeldingen", "music": "Muziek", "pdf": "PDF", - "pressToSearch": "Druk op enter om te zoeken...", "search": "Zoeken...", - "typeToSearch": "Typ om te zoeken...", "types": "Types", "video": "Video", "path": "Pad:", "smallerThan": "Kleiner dan:", "largerThan": "Groter dan:", "helpText1": "Er wordt gezocht op elk teken dat je typt (minimaal 3 tekens voor zoektermen).", - "helpText2": "De index: Zoeken maakt gebruik van de index die automatisch wordt bijgewerkt op het ingestelde interval (standaard: 5 minuten). Zoeken als het programma net is gestart, kan onvolledige resultaten opleveren." + "helpText2": "De index: Zoeken maakt gebruik van de index die automatisch wordt bijgewerkt op het ingestelde interval (standaard: 5 minuten). Zoeken als het programma net is gestart, kan onvolledige resultaten opleveren.", + "noResults": "Geen resultaten gevonden in geïndexeerde zoekopdrachten.", + "onlyFolders": "Alleen mappen", + "onlyFiles": "Alleen bestanden", + "showOptions": "Opties weergeven", + "photos": "Foto's", + "audio": "Audio", + "videos": "Video's", + "documents": "Documenten", + "archives": "Archief", + "number": "Aantal", + "searchContext": "Zoek context: {context}", + "notEnoughCharacters": "Niet genoeg tekens om te zoeken (min {minSearchLength})", + "typeToSearch": "Begin met het typen van {minSearchLength} of meer tekens om te beginnen met zoeken." }, "settings": { "admin": "Admin", @@ -214,10 +227,8 @@ "avoidChanges": "(laat leeg om wijzigingen te voorkomen)", "branding": "Branding", "brandingDirectoryPath": "Branding directory path", - "brandingHelp": "U kunt de looks en feels hoe File Browser wordt getoond wijzigen door de naam aan te passen, het logo te vervangen, een aangepast stylesheet toe te voegen en/of de externe links naar GitHub uit te schakelen.\nVoor meer informatie over custom branding, bekijk {0}.", "changePassword": "Wijzig Wachtwoord", "commandRunner": "Command runner", - "commandRunnerHelp": "Hier kunt u opdrachten instellen die worden uitgevoerd in de benoemde gebeurtenissen. U moet er één per regel schrijven. De omgevingsvariabelen {0} en {1} zijn beschikbaar, zijnde {0} relatief ten opzichte van {1}. Raadpleeg {2} voor meer informatie over deze functie en de beschikbare omgevingsvariabelen.", "commandsUpdated": "Commando's bijgewerkt!", "createUserDir": "Maak automatisch een thuismap aan wanneer een nieuwe gebruiker wordt aangemaakt", "customStylesheet": "Aangepast Stylesheet", @@ -300,7 +311,14 @@ "adminOptions": "Opties voor beheerders", "shareDuration": "Verloopt op", "searchOptions": "Zoekopties", - "downloads": "Downloads" + "downloads": "Downloads", + "systemAdmin": "Systeem & Beheer", + "configViewer": "Configuratiescherm", + "configViewerShowFull": "Volledige configuratie weergeven", + "configViewerShowComments": "Opmerkingen tonen", + "configViewerLoadConfig": "Configuratie laden", + "brandingHelp": "Je kunt aanpassen hoe je File Browser instance eruit ziet en aanvoelt door de naam te veranderen, het logo te vervangen, aangepaste stijlen toe te voegen en zelfs externe links naar GitHub uit te schakelen.\nVoor meer informatie over het aanpassen van je branding, kun je kijken op {0}.", + "commandRunnerHelp": "Hier kun je commando's instellen die worden uitgevoerd in de genoemde gebeurtenissen. Je moet er één per regel schrijven. De omgevingsvariabelen {0} en {1} zullen beschikbaar zijn, waarbij {0} relatief is ten opzichte van {1}. Lees voor meer informatie over deze functie en de beschikbare omgevingsvariabelen de {2}." }, "sidebar": { "help": "Help", @@ -358,12 +376,21 @@ "hideNavButtons": "Navigatieknoppen verbergen", "hideNavButtonsDescription": "Verberg de navigatieknoppen op de navigatiebalk in de share om een minimalistische look te creëren.", "viewModeDescription": "Standaard lay-out voor de gedeelde pagina.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Er is een share naar je verzonden om te bekijken of te downloaden.", "disableShareCard": "Aandelenkaart uitschakelen", "disableShareCardDescription": "Schakel de deelkaart uit op de gedeelde pagina in de zijbalk of bovenaan de pagina op mobiel.", "disableSidebar": "Zijbalk uitschakelen", - "disableSidebarDescription": "Schakel de zijbalk op de gedeelde pagina uit." + "disableSidebarDescription": "Schakel de zijbalk op de gedeelde pagina uit.", + "enforceDarkLightMode": "Themamodus afdwingen", + "enforceDarkLightModeDescription": "Forceer een specifieke themamodus (donker of licht) voor deze share, waarbij gebruikersvoorkeuren worden overschreven.", + "default": "Een modus niet afdwingen", + "dark": "Donker", + "light": "Licht", + "enableOnlyOffice": "OnlyOffice viewer inschakelen", + "enableOnlyOfficeDescription": "Sta het bekijken van Office-bestanden toe met OnlyOffice in deze share.", + "enableOnlyOfficeEditing": "Bewerken met OnlyOffice inschakelen", + "enableOnlyOfficeEditingDescription": "Laat het bewerken van Office-bestanden met OnlyOffice toe in deze share.", + "titleDefault": "Gedeelde bestanden - {title}" }, "api": { "title": "API-sleutels", @@ -478,7 +505,13 @@ "defaultThemeDescription": "standaard - Het standaardthema", "customTheme": "Kies je thema", "showSelectMultiple": "Meerdere selecteren tonen in contextmenu op bureaublad", - "showSelectMultipleDescription": "Standaard heeft het contextmenu op de desktop geen optie \"meerdere selecteren\" zoals mobiel. Deze optie wordt altijd weergegeven in het contextmenu." + "showSelectMultipleDescription": "Standaard heeft het contextmenu op de desktop geen optie \"meerdere selecteren\" zoals mobiel. Deze optie wordt altijd weergegeven in het contextmenu.", + "defaultMediaPlayer": "Gebruik de ingebouwde mediaspeler van uw browser", + "defaultMediaPlayerDescription": "Gebruik de eigen mediaspeler van uw browser voor video- en audiobestanden in plaats van de meegeleverde 'vue-plyr' mediaspeler van FileBrowser.", + "debugOfficeEditor": "Debugmodus OnlyOffice inschakelen", + "debugOfficeEditorDescription": "Een foutopsporingspop-up weergeven met aanvullende informatie in de OnlyOffice-editor.", + "fileViewerOptions": "Opties voor bestandsviewer", + "themeAndLanguage": "Thema & Taal" }, "general": { "name": "Naam", @@ -513,15 +546,19 @@ "name": "Naam", "accessManagement": "Toegangsbeheer", "all": "Alles weigeren", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Dit laat zien wat er gebeurt als een gebruiker toegang probeert te krijgen tot een pad waarvoor geen specifieke toegangsregels bestaan. Toestaan' betekent dat gebruikers standaard toegang hebben, 'Weigeren' betekent dat gebruikers standaard worden geblokkeerd tenzij ze expliciet toestemming krijgen." + "defaultBehaviorDescription": "Dit laat zien wat er gebeurt als een gebruiker toegang probeert te krijgen tot een pad waarvoor geen specifieke toegangsregels bestaan. Toestaan' betekent dat gebruikers standaard toegang hebben, 'Weigeren' betekent dat gebruikers standaard worden geblokkeerd tenzij ze expliciet toestemming krijgen.", + "defaultBehavior": "Standaardactie{suffix}" }, "editor": { "uninitialized": "Het is niet gelukt de editor te initialiseren. Gelieve opnieuw te laden.", "saveFailed": "Het is niet gelukt om het bestand op te slaan. Probeer het opnieuw.", "saveAborted": "Opslaan afgebroken. De bestandsnaam komt niet overeen met het actieve bestand.", "saveDisabled": "Het opslaan van gedeelde bestanden wordt niet ondersteund.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Opslaan afgebroken. Het actieve bestand van de toepassing ({activeFile}) komt niet overeen met het bestand dat u probeert op te slaan ({tryingToSave}).", + "syncFailed": "Na 5 pogingen is het niet gelukt om de status te synchroniseren met de route voor \"{filename}\". Installatie van editor afgebroken om gegevensbeschadiging te voorkomen." + }, + "player": { + "LoopEnabled": "Lus ingeschakeld", + "LoopDisabled": "Lus uitgeschakeld" } } diff --git a/frontend/src/i18n/pl.json b/frontend/src/i18n/pl.json index f9ea67066..c571f320a 100644 --- a/frontend/src/i18n/pl.json +++ b/frontend/src/i18n/pl.json @@ -49,7 +49,8 @@ "clearCompleted": "Czyszczenie zakończone", "showMore": "Pokaż więcej", "showLess": "Pokaż mniej", - "openParentFolder": "Otwórz folder nadrzędny" + "openParentFolder": "Otwórz folder nadrzędny", + "selectedCount": "Liczba elementów wybranych do wykonania akcji" }, "download": { "downloadFile": "Pobierz plik", @@ -84,15 +85,15 @@ "click": "wybierz plik lub foler", "ctrl": { "click": "wybierz wiele plików lub folderów", - "f": "otwórz wyszukiwarkę", - "s": "pobierz aktywny plik lub folder" + "d": "Pobierz wybrane elementy" }, "del": "usuń zaznaczone", - "doubleClick": "otwórz plik lub folder", "esc": "wyczyść zaznaczenie i/lub zamknij okno z powiadomieniem", - "f1": "ta informacja", "f2": "zmień nazwę pliku", - "help": "Pomoc" + "help": "Pomoc", + "f1": "pokaż monit pomocy", + "description": "Poniżej znajdują się podstawowe opcje nawigacji. Dodatkowe informacje można znaleźć na stronie", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -134,7 +135,6 @@ "prompts": { "copy": "Kopiuj", "copyMessage": "Wybierz lokalizację do której mają być skopiowane wybrane pliki", - "deleteMessageMultiple": "Czy jesteś pewien że chcesz usunąć {count} plik(ów)?", "deleteMessageSingle": "Czy jesteś pewien, że chcesz usunąć ten plik/folder?", "deleteTitle": "Usuń pliki", "displayName": "Wyświetlana Nazwa:", @@ -142,7 +142,6 @@ "downloadMessage": "Wybierz format, jaki chesz pobrać.", "error": "Pojawił się nieznany błąd", "fileInfo": "Informacje o pliku", - "lastModified": "Ostatnio Zmodyfikowane{suffix}", "move": "Przenieś", "moveMessage": "Wybierz nową lokalizację dla swoich plik(ów)/folder(ów):", "newArchetype": "Utwórz nowy wpis na bazie wybranego wzorca. Twój plik będzie utworzony w wybranym folderze.", @@ -150,10 +149,7 @@ "newDirMessage": "Podaj nazwę tworzonego folderu.", "newFile": "Nowy plik", "newFileMessage": "Podaj nazwętworzonego pliku.", - "numberDirs": "Ilość katalogów{suffix}", - "numberFiles": "Ilość plików{suffix}", "rename": "Zmień nazwę", - "renameMessage": "Podaj nową nazwę dla", "replace": "Zamień", "replaceMessage": "Jednen z plików który próbujesz wrzucić próbje nadpisać plik o tej samej nazwie. Czy chcesz nadpisać poprzedni plik?\n", "schedule": "Grafi", @@ -161,7 +157,6 @@ "show": "Pokaż", "size": "Rozmiar", "upload": "Prześlij", - "deleteMessageShare": "Are you sure you want to delete this share: {path} ?", "deleteUserMessage": "Czy na pewno chcesz usunąć tego użytkownika?", "filesSelected": "wybrane pliki", "optionalPassword": "Hasło opcjonalne", @@ -172,10 +167,6 @@ "dragAndDrop": "Przeciągnij i upuść pliki tutaj", "completed": "Zakończono", "conflictsDetected": "Wykryte konflikty", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Nieprawidłowa nazwa: nie można użyć kontenera \"/\" lub \"\\\"", - "source": "Source{suffix}", "downloadsLimit": "Limit liczby pobrań", "maxBandwidth": "Maksymalna przepustowość pobierania (kbps)", "shareTheme": "Udostępnij temat", @@ -186,22 +177,44 @@ "shareBanner": "Udostępnij adres URL banera", "shareFavicon": "Udostępnij adres URL favicon", "optionalPasswordHelp": "Jeśli jest ustawione, użytkownicy muszą wprowadzić to hasło, aby wyświetlić udostępnioną zawartość.", - "viewMode": "Tryb widoku" + "viewMode": "Tryb widoku", + "renameMessage": "Wprowadź nową nazwę:", + "renameMessageInvalid": "Plik nie może być kontenerem \"/\" lub \"\\\"", + "source": "Źródło{suffix}", + "deleteMessageMultiple": "Czy na pewno chcesz usunąć plik(i) {count}?", + "deleteMessageShare": "Czy na pewno chcesz usunąć ten udział: {path} ?", + "lastModified": "Ostatnia modyfikacja{suffix}", + "numberDirs": "Liczba katalogów{suffix}", + "numberFiles": "Liczba plików{suffix}", + "renameMessageConflict": "Element o nazwie \"{filename}\" już istnieje!", + "uploadSettingsChunked": "Maks. jednoczesne przesyłanie: {maxConcurrentUpload}, rozmiar fragmentu: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Maksymalny jednoczesny upload: {maxConcurrentUpload}, przesyłanie fragmentaryczne wyłączone" }, "search": { "images": "Zdjęcia", "music": "Muzyka", "pdf": "PDF", - "pressToSearch": "Wciśnij enter, aby wyszukać...", "search": "Szukaj...", - "typeToSearch": "Zacznij pisać, aby wyszukać...", "types": "Typy", "video": "Wideo", "path": "Ścieżka:", "smallerThan": "Mniejszy niż:", "largerThan": "Większy niż:", "helpText1": "Wyszukiwanie odbywa się po każdym wpisanym znaku (minimum 3 znaki dla wyszukiwanych haseł).", - "helpText2": "Indeks: Wyszukiwanie wykorzystuje indeks, który jest automatycznie aktualizowany w skonfigurowanych odstępach czasu (domyślnie: 5 minut). Wyszukiwanie po uruchomieniu programu może skutkować niekompletnymi wynikami." + "helpText2": "Indeks: Wyszukiwanie wykorzystuje indeks, który jest automatycznie aktualizowany w skonfigurowanych odstępach czasu (domyślnie: 5 minut). Wyszukiwanie po uruchomieniu programu może skutkować niekompletnymi wynikami.", + "noResults": "Nie znaleziono żadnych wyników w indeksowanym wyszukiwaniu.", + "onlyFolders": "Tylko foldery", + "onlyFiles": "Tylko pliki", + "showOptions": "Pokaż opcje", + "photos": "Zdjęcia", + "audio": "Audio", + "videos": "Filmy", + "documents": "Dokumenty", + "archives": "Archiwa", + "number": "Liczba", + "searchContext": "Kontekst wyszukiwania: {context}", + "notEnoughCharacters": "Za mało znaków do wyszukania (min. {minSearchLength})", + "typeToSearch": "Zacznij wpisywać {minSearchLength} lub więcej znaków, aby rozpocząć wyszukiwanie." }, "settings": { "admin": "Admin", @@ -214,10 +227,8 @@ "avoidChanges": "(pozostaw puste aby nie zosatało zmienione)", "branding": "Branding", "brandingDirectoryPath": "Folder brandingowy", - "brandingHelp": "Możesz dostosować wygląd i doznania użytkownika swojej instancji File Browser poprzez zmianę jej nazwy, zmianę logo, dodanie własnych stylów, a nawet wyłączyć linki zewnętrzne do GitHuba.\nW celu pozyskania większej ilości informacji nt. osobistego brandingu, zapoznaj się z {0}.", "changePassword": "Zmień Hasło", "commandRunner": "Narzędzie do wykonywania poleceń", - "commandRunnerHelp": "Tu możesz ustawić komendy, które będą wykonywane przy danych zdarzeniach. Musisz wpisywać po jednej na linjkę. Zmienne środowiskowe {0} i {1} będą dostępne, gdzie {0} jest względne wobec {1}. Więcej informacji o tej funkcji i dostępnych zmiennych środowiskowych znajdziesz tutaj: {2}.", "commandsUpdated": "Polecenie zaktualizowane!", "createUserDir": "Automatycznie utwórz katalog domowy użytkownika podczas dodania nowego użytkownika", "customStylesheet": "Własny arkusz stylów", @@ -300,7 +311,14 @@ "adminOptions": "Opcje użytkownika administratora", "shareDuration": "Wygasa", "searchOptions": "Opcje wyszukiwania", - "downloads": "Pliki do pobrania" + "downloads": "Pliki do pobrania", + "systemAdmin": "System i administrator", + "configViewer": "Przeglądarka konfiguracji", + "configViewerShowFull": "Pokaż pełną konfigurację", + "configViewerShowComments": "Pokaż komentarze", + "configViewerLoadConfig": "Konfiguracja ładowania", + "brandingHelp": "Możesz dostosować wygląd i sposób działania swojej instancji przeglądarki plików, zmieniając jej nazwę, zastępując logo, dodając niestandardowe style, a nawet wyłączając zewnętrzne łącza do GitHub.\nWięcej informacji na temat niestandardowego brandingu można znaleźć na stronie {0}.", + "commandRunnerHelp": "Tutaj można ustawić polecenia, które będą wykonywane w nazwanych zdarzeniach. Należy wpisać jedną komendę w wierszu. Dostępne będą zmienne środowiskowe {0} i {1}, przy czym {0} będzie relatywna do {1}. Więcej informacji na temat tej funkcji i dostępnych zmiennych środowiskowych można znaleźć na stronie {2}." }, "sidebar": { "help": "Pomoc", @@ -358,12 +376,21 @@ "hideNavButtons": "Ukryj przyciski nawigacyjne", "hideNavButtonsDescription": "Ukryj przyciski nawigacyjne na pasku nawigacyjnym w folderze udostępnionym, aby uzyskać minimalistyczny wygląd.", "viewModeDescription": "Domyślny układ strony udostępnionej.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Udział został wysłany do wyświetlenia lub pobrania.", "disableShareCard": "Wyłączanie karty udostępniania", "disableShareCardDescription": "Wyłącz kartę udostępniania na udostępnianej stronie na pasku bocznym lub u góry strony na urządzeniach mobilnych.", "disableSidebar": "Wyłącz pasek boczny", - "disableSidebarDescription": "Wyłącz pasek boczny na udostępnionej stronie." + "disableSidebarDescription": "Wyłącz pasek boczny na udostępnionej stronie.", + "enforceDarkLightMode": "Wymuszanie trybu motywu", + "enforceDarkLightModeDescription": "Wymusza określony tryb motywu (ciemny lub jasny) dla tego udziału, zastępując preferencje użytkownika.", + "default": "Nie wymuszaj trybu", + "dark": "Ciemny", + "light": "Światło", + "enableOnlyOffice": "Włącz przeglądarkę OnlyOffice", + "enableOnlyOfficeDescription": "Zezwalaj na wyświetlanie plików biurowych przy użyciu OnlyOffice w tym udziale.", + "enableOnlyOfficeEditing": "Włącz edycję OnlyOffice", + "enableOnlyOfficeEditingDescription": "Zezwalaj na edycję plików biurowych za pomocą OnlyOffice w tym udziale.", + "titleDefault": "Udostępnione pliki - {title}" }, "api": { "title": "Klucze API:", @@ -447,8 +474,8 @@ "totalDenied": "Odmowa", "totalAllowed": "Dozwolone", "all": "Odmów wszystkim", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "To pokazuje, co się dzieje, gdy użytkownik próbuje uzyskać dostęp do ścieżki, dla której nie istnieją określone reguły dostępu. \"Zezwól\" oznacza, że użytkownicy mogą uzyskać dostęp domyślnie, \"Odmów\" oznacza, że użytkownicy są domyślnie blokowani, chyba że wyraźnie na to zezwolono." + "defaultBehaviorDescription": "To pokazuje, co się dzieje, gdy użytkownik próbuje uzyskać dostęp do ścieżki, dla której nie istnieją określone reguły dostępu. \"Zezwól\" oznacza, że użytkownicy mogą uzyskać dostęp domyślnie, \"Odmów\" oznacza, że użytkownicy są domyślnie blokowani, chyba że wyraźnie na to zezwolono.", + "defaultBehavior": "Działanie domyślne{suffix}" }, "fileLoading": { "title": "Przesyłanie i pobieranie", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - domyślny motyw", "customTheme": "Wybierz motyw", "showSelectMultiple": "Wyświetlanie wyboru wielokrotnego w menu kontekstowym na pulpicie", - "showSelectMultipleDescription": "Domyślnie menu kontekstowe pulpitu nie ma opcji \"wybierz wiele\", tak jak w przypadku urządzeń mobilnych. Opcja ta jest zawsze widoczna w menu kontekstowym." + "showSelectMultipleDescription": "Domyślnie menu kontekstowe pulpitu nie ma opcji \"wybierz wiele\", tak jak w przypadku urządzeń mobilnych. Opcja ta jest zawsze widoczna w menu kontekstowym.", + "defaultMediaPlayer": "Użyj natywnego odtwarzacza multimediów w przeglądarce", + "defaultMediaPlayerDescription": "Do odtwarzania plików wideo i audio należy używać natywnego odtwarzacza multimediów przeglądarki, a nie dołączonego do FileBrowser odtwarzacza multimediów \"vue-plyr\".", + "debugOfficeEditor": "Włącz tryb debugowania OnlyOffice", + "debugOfficeEditorDescription": "Wyświetla wyskakujące okienko debugowania z dodatkowymi informacjami w edytorze OnlyOffice.", + "fileViewerOptions": "Opcje przeglądarki plików", + "themeAndLanguage": "Temat i język" }, "editor": { "uninitialized": "Nie udało się zainicjować edytora. Przeładuj ponownie.", "saveFailed": "Nie udało się zapisać pliku. Spróbuj ponownie.", "saveAborted": "Zapisywanie przerwane. Nazwa pliku nie pasuje do aktywnego pliku.", "saveDisabled": "Zapisywanie udostępnionych plików nie jest obsługiwane.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Operacja zapisywania została przerwana. Aktywny plik aplikacji ({activeFile}) nie jest zgodny z plikiem, który próbujesz zapisać ({tryingToSave}).", + "syncFailed": "Nie udało się zsynchronizować stanu z trasą dla \"{filename}\" po 5 próbach. Przerwano konfigurację edytora, aby zapobiec uszkodzeniu danych." + }, + "player": { + "LoopEnabled": "Włączona pętla", + "LoopDisabled": "Pętla wyłączona" } } diff --git a/frontend/src/i18n/pt-br.json b/frontend/src/i18n/pt-br.json index 2a102a1c8..716b07faa 100644 --- a/frontend/src/i18n/pt-br.json +++ b/frontend/src/i18n/pt-br.json @@ -49,7 +49,8 @@ "clearCompleted": "Limpar concluído", "showMore": "Mostrar mais", "showLess": "Mostrar menos", - "openParentFolder": "Abrir a pasta principal" + "openParentFolder": "Abrir a pasta principal", + "selectedCount": "Número de itens selecionados para executar uma ação" }, "download": { "downloadFile": "Baixar arquivo", @@ -84,15 +85,15 @@ "click": "selecionar pasta ou arquivo", "ctrl": { "click": "selecionar várias pastas e arquivos", - "f": "pesquisar", - "s": "salvar um arquivo ou baixar a pasta que você está" + "d": "baixar itens selecionados" }, "del": "apagar os arquivos selecionados", - "doubleClick": "abrir pasta ou arquivo", "esc": "limpar seleção e/ou fechar menu", - "f1": "esta informação", "f2": "renomear arquivo", - "help": "Ajuda" + "help": "Ajuda", + "f1": "mostrar prompt de ajuda", + "description": "Você pode ver as opções básicas de navegação abaixo. Para obter informações adicionais, visite", + "wiki": "Wiki do FileBrowser Quantum" }, "languages": { "he": "עברית", @@ -134,16 +135,13 @@ "prompts": { "copy": "Copiar", "copyMessage": "Escolha um lugar para copiar os arquivos:", - "deleteMessageMultiple": "Deseja apagar {count} arquivo(s)?", "deleteMessageSingle": "Deseja apagar esta pasta/arquivo?", - "deleteMessageShare": "Deseja apagar este compartilhamento ({path})?", "deleteTitle": "Apagar arquivos", "displayName": "Nome:", "download": "Baixar arquivos", "downloadMessage": "Escolha o formato do arquivo.", "error": "Algo de errado ocorreu", "fileInfo": "Informação do arquivo", - "lastModified": "Última modificação{suffix}", "move": "Mover", "moveMessage": "Escolha uma nova pasta para os seus arquivos:", "newArchetype": "Criar um novo post baseado num \"archetype\". O seu arquivo será criado na pasta \"content\".", @@ -151,10 +149,7 @@ "newDirMessage": "Escreva o nome da nova pasta.", "newFile": "Novo arquivo", "newFileMessage": "Escreva o nome do novo arquivo.", - "numberDirs": "Número de pastas{suffix}", - "numberFiles": "Número de arquivos{suffix}", "rename": "Renomear", - "renameMessage": "Insira um novo nome para", "replace": "Substituir", "replaceMessage": "Já existe um arquivo com nome igual a um dos que está tentando enviar. Deseja substituir?\n", "schedule": "Agendar", @@ -172,10 +167,6 @@ "dragAndDrop": "Arraste e solte os arquivos aqui", "completed": "Concluído", "conflictsDetected": "Conflitos detectados", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Nome inválido fornecido: não pode conter \"/\" ou \"\\\"", - "source": "Source{suffix}", "downloadsLimit": "Limite de número de downloads", "maxBandwidth": "Largura de banda máxima de download (kbps)", "shareTheme": "Compartilhar tema", @@ -186,22 +177,44 @@ "shareBanner": "Compartilhar URL do banner", "shareFavicon": "Compartilhar URL do favicon", "optionalPasswordHelp": "Se definida, os usuários deverão digitar essa senha para visualizar o conteúdo compartilhado.", - "viewMode": "Modo de exibição" + "viewMode": "Modo de exibição", + "renameMessage": "Digite um novo nome:", + "renameMessageInvalid": "Um arquivo não pode ter o contêiner \"/\" ou \"\\\"", + "source": "Fonte{suffix}", + "deleteMessageMultiple": "Tem certeza de que deseja excluir o(s) arquivo(s) {count}?", + "deleteMessageShare": "Tem certeza de que deseja excluir esse compartilhamento: {path} ?", + "lastModified": "Última modificação{suffix}", + "numberDirs": "Número de diretórios{suffix}", + "numberFiles": "Número de arquivos{suffix}", + "renameMessageConflict": "Já existe um item chamado \"{filename}\"!", + "uploadSettingsChunked": "Máximo de uploads simultâneos: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Máximo de uploads simultâneos: {maxConcurrentUpload}, upload em pedaços desativado" }, "search": { "images": "Imagens", "music": "Músicas", "pdf": "PDF", - "pressToSearch": "Pressione Enter para pesquisar...", "search": "Pesquise...", - "typeToSearch": "Digite para pesquisar...", "types": "Tipos", "video": "Vídeos", "path": "Caminho:", "smallerThan": "Menor que:", "largerThan": "Maior que:", "helpText1": "A pesquisa ocorre em cada caractere que você digita (mínimo de 3 caracteres para termos de pesquisa).", - "helpText2": "O índice: A pesquisa utiliza o índice que é atualizado automaticamente no intervalo configurado (padrão: 5 minutos). A pesquisa quando o programa acaba de ser iniciado pode resultar em resultados incompletos." + "helpText2": "O índice: A pesquisa utiliza o índice que é atualizado automaticamente no intervalo configurado (padrão: 5 minutos). A pesquisa quando o programa acaba de ser iniciado pode resultar em resultados incompletos.", + "noResults": "Não foram encontrados resultados na pesquisa indexada.", + "onlyFolders": "Somente pastas", + "onlyFiles": "Somente arquivos", + "showOptions": "Mostrar opções", + "photos": "Fotos", + "audio": "Áudio", + "videos": "Vídeos", + "documents": "Documentos", + "archives": "Arquivos", + "number": "Número", + "searchContext": "Contexto da pesquisa: {context}", + "notEnoughCharacters": "Não há caracteres suficientes para pesquisar (mín. {minSearchLength})", + "typeToSearch": "Comece a digitar {minSearchLength} ou mais caracteres para iniciar a busca." }, "settings": { "admin": "Admin", @@ -214,10 +227,8 @@ "avoidChanges": "(deixe em branco para manter)", "branding": "Customização", "brandingDirectoryPath": "Diretório de customização", - "brandingHelp": "Você pode mudar a aparência e experiência de sua instância do File Browser alterando seu nome, logotipo, adicionando estilos customizados e até desabilitando links externos para o GitHub.\nPara mais informações sobre customizações, confira {0}.", "changePassword": "Alterar senha", "commandRunner": "Execução de comandos", - "commandRunnerHelp": "Aqui você pode definir comandos que serão executados nos eventos descritos. Escreva um por linha. As variáveis de ambiente {0} e {1} estão disponíveis, sendo {0} relativo a {1}. Para mais informações sobre esta função e as variáveis de ambiente disponíveis, leia a {2}.", "commandsUpdated": "Comandos atualizados!", "createUserDir": "Criar diretório Home para novos usuários", "userHomeBasePath": "Caminho base para diretórios de usuários", @@ -300,7 +311,14 @@ "adminOptions": "Opções do usuário administrador", "shareDuration": "Expirações", "searchOptions": "Opções de pesquisa", - "downloads": "Downloads" + "downloads": "Downloads", + "systemAdmin": "Sistema e administração", + "configViewer": "Visualizador de configuração", + "configViewerShowFull": "Mostrar configuração completa", + "configViewerShowComments": "Mostrar comentários", + "configViewerLoadConfig": "Configuração de carga", + "brandingHelp": "Você pode personalizar a aparência da instância do Navegador de arquivos alterando seu nome, substituindo o logotipo, adicionando estilos personalizados e até mesmo desativando links externos para o GitHub.\nPara obter mais informações sobre branding personalizado, consulte {0}.", + "commandRunnerHelp": "Aqui você pode definir comandos que são executados nos eventos nomeados. Você deve escrever um por linha. As variáveis de ambiente {0} e {1} estarão disponíveis, sendo {0} relativo a {1}. Para obter mais informações sobre esse recurso e as variáveis de ambiente disponíveis, leia {2}." }, "sidebar": { "help": "Ajuda", @@ -358,12 +376,21 @@ "hideNavButtons": "Ocultar botões de navegação", "hideNavButtonsDescription": "Oculte os botões de navegação na barra de navegação do compartilhamento para criar uma aparência minimalista.", "viewModeDescription": "Layout padrão para a página compartilhada.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Um compartilhamento foi enviado a você para visualização ou download.", "disableShareCard": "Desativar cartão compartilhado", "disableShareCardDescription": "Desative o cartão de compartilhamento na página compartilhada na barra lateral ou na parte superior da página no celular.", "disableSidebar": "Desativar a barra lateral", - "disableSidebarDescription": "Desative a barra lateral na página compartilhada." + "disableSidebarDescription": "Desative a barra lateral na página compartilhada.", + "enforceDarkLightMode": "Aplicar o modo de tema", + "enforceDarkLightModeDescription": "Força um modo de tema específico (escuro ou claro) para esse compartilhamento, substituindo as preferências do usuário.", + "default": "Não imponha um modo", + "dark": "Escuro", + "light": "Luz", + "enableOnlyOffice": "Ativar o visualizador do OnlyOffice", + "enableOnlyOfficeDescription": "Permitir a visualização de arquivos do Office usando o OnlyOffice nesse compartilhamento.", + "enableOnlyOfficeEditing": "Ativar a edição do OnlyOffice", + "enableOnlyOfficeEditingDescription": "Permitir a edição de arquivos do Office usando o OnlyOffice nesse compartilhamento.", + "titleDefault": "Arquivos compartilhados - {title}" }, "api": { "title": "Chaves de API:", @@ -447,8 +474,8 @@ "totalDenied": "Negado", "totalAllowed": "Permitido", "all": "Negar tudo", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Isso mostra o que acontece quando um usuário tenta acessar um caminho em que não existem regras de acesso específicas. \"Permitir\" significa que os usuários podem acessar por padrão, \"Negar\" significa que os usuários são bloqueados por padrão, a menos que sejam explicitamente permitidos." + "defaultBehaviorDescription": "Isso mostra o que acontece quando um usuário tenta acessar um caminho em que não existem regras de acesso específicas. \"Permitir\" significa que os usuários podem acessar por padrão, \"Negar\" significa que os usuários são bloqueados por padrão, a menos que sejam explicitamente permitidos.", + "defaultBehavior": "Ação padrão{suffix}" }, "fileLoading": { "title": "Uploads e downloads", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - O tema padrão", "customTheme": "Escolha seu tema", "showSelectMultiple": "Mostrar seleção múltipla no menu de contexto na área de trabalho", - "showSelectMultipleDescription": "Por padrão, o menu de contexto do desktop não tem a opção \"selecionar vários\" como no celular. Essa opção sempre a mostra no menu de contexto." + "showSelectMultipleDescription": "Por padrão, o menu de contexto do desktop não tem a opção \"selecionar vários\" como no celular. Essa opção sempre a mostra no menu de contexto.", + "defaultMediaPlayer": "Use o reprodutor de mídia nativo do seu navegador", + "defaultMediaPlayerDescription": "Use o reprodutor de mídia nativo do seu navegador para arquivos de vídeo e áudio em vez do reprodutor de mídia 'vue-plyr' incluído no FileBrowser.", + "debugOfficeEditor": "Ativar o modo de depuração do OnlyOffice", + "debugOfficeEditorDescription": "Mostrar um pop-up de depuração com informações adicionais no editor do OnlyOffice.", + "fileViewerOptions": "Opções do visualizador de arquivos", + "themeAndLanguage": "Tema e linguagem" }, "editor": { "uninitialized": "Falha ao inicializar o editor. Favor recarregar.", "saveFailed": "Falha ao salvar o arquivo. Tente novamente.", "saveAborted": "Salvamento abortado. O nome do arquivo não corresponde ao arquivo ativo.", "saveDisabled": "Não há suporte para o salvamento de arquivos compartilhados.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Operação de salvamento abortada. O arquivo ativo do aplicativo ({activeFile}) não corresponde ao arquivo que você está tentando salvar ({tryingToSave}).", + "syncFailed": "Falha ao sincronizar o estado com a rota para \"{filename}\" após 5 tentativas. Abortar a configuração do editor para evitar a corrupção de dados." + }, + "player": { + "LoopEnabled": "Loop ativado", + "LoopDisabled": "Loop desativado" } } diff --git a/frontend/src/i18n/pt.json b/frontend/src/i18n/pt.json index 8e8c0241e..636e97fc8 100644 --- a/frontend/src/i18n/pt.json +++ b/frontend/src/i18n/pt.json @@ -49,7 +49,8 @@ "clearCompleted": "Limpar concluído", "showMore": "Mostrar mais", "showLess": "Mostrar menos", - "openParentFolder": "Abrir a pasta principal" + "openParentFolder": "Abrir a pasta principal", + "selectedCount": "Número de itens selecionados para executar uma ação" }, "download": { "downloadFile": "Descarregar ficheiro", @@ -84,15 +85,15 @@ "click": "selecionar pasta ou ficheiro", "ctrl": { "click": "selecionar várias pastas e ficheiros", - "f": "pesquisar", - "s": "guardar um ficheiro ou descarrega a pasta em que está a navegar" + "d": "descarregar itens selecionados" }, "del": "eliminar os ficheiros selecionados", - "doubleClick": "abrir pasta ou ficheiro", "esc": "limpar seleção e/ou fechar menu", - "f1": "esta informação", "f2": "alterar nome do ficheiro", - "help": "Ajuda" + "help": "Ajuda", + "f1": "mostrar prompt de ajuda", + "description": "Pode ver as opções básicas de navegação abaixo. Para obter informações adicionais, visite", + "wiki": "Wiki do FileBrowser Quantum" }, "languages": { "he": "עברית", @@ -134,7 +135,6 @@ "prompts": { "copy": "Copiar", "copyMessage": "Escolha um lugar para onde copiar os ficheiros:", - "deleteMessageMultiple": "Quer eliminar {count} ficheiro(s)?", "deleteMessageSingle": "Quer eliminar esta pasta/ficheiro?", "deleteTitle": "Eliminar ficheiros", "displayName": "Nome:", @@ -142,7 +142,6 @@ "downloadMessage": "Escolha o formato do ficheiro que quer descarregar.", "error": "Algo correu mal", "fileInfo": "Informação do ficheiro", - "lastModified": "Última alteração{suffix}", "move": "Mover", "moveMessage": "Escolha uma nova casa para os seus ficheiros/pastas:", "newArchetype": "Criar um novo post baseado num \"archetype\". O seu ficheiro será criado na pasta \"content\".", @@ -150,10 +149,7 @@ "newDirMessage": "Escreva o nome da nova pasta.", "newFile": "Novo ficheiro", "newFileMessage": "Escreva o nome do novo ficheiro.", - "numberDirs": "Número de pastas{suffix}", - "numberFiles": "Número de ficheiros{suffix}", "rename": "Alterar nome", - "renameMessage": "Insira um novo nome para", "replace": "Substituir", "replaceMessage": "Já existe um ficheiro com nome igual a um dos que está a tentar enviar. Quer substituí-lo?\n", "schedule": "Agendar", @@ -161,7 +157,6 @@ "show": "Mostrar", "size": "Tamanho", "upload": "Carregar", - "deleteMessageShare": "Are you sure you want to delete this share: {path} ?", "deleteUserMessage": "Tem a certeza de que pretende apagar este utilizador?", "selected": "selecionado", "filesSelected": "ficheiros selecionados", @@ -172,10 +167,6 @@ "dragAndDrop": "Arrastar e largar ficheiros aqui", "completed": "Concluído", "conflictsDetected": "Conflitos detectados", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Nome inválido fornecido: não pode conter \"/\" ou \"\\\"", - "source": "Source{suffix}", "downloadsLimit": "Limite do número de descarregamentos", "maxBandwidth": "Largura de banda máxima de descarregamento (kbps)", "shareTheme": "Partilhar o tema", @@ -186,22 +177,44 @@ "shareBanner": "Partilhar URL do banner", "shareFavicon": "Partilhar URL do favicon", "optionalPasswordHelp": "Se estiver definida, os utilizadores têm de introduzir esta palavra-passe para ver o conteúdo partilhado.", - "viewMode": "Modo de visualização" + "viewMode": "Modo de visualização", + "renameMessage": "Introduzir um novo nome:", + "renameMessageInvalid": "Um ficheiro não pode conter \"/\" ou \"\\\"", + "source": "Fonte{suffix}", + "deleteMessageMultiple": "Tem a certeza de que pretende apagar o(s) ficheiro(s) {count}?", + "deleteMessageShare": "Tem a certeza de que pretende apagar esta partilha: {path} ?", + "lastModified": "Última modificação{suffix}", + "numberDirs": "Número de diretórios{suffix}", + "numberFiles": "Número de ficheiros{suffix}", + "renameMessageConflict": "Já existe um item com o nome \"{filename}\"!", + "uploadSettingsChunked": "Máximo de carregamentos simultâneos: {maxConcurrentUpload}, tamanho do pedaço: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Máximo de carregamentos simultâneos: {maxConcurrentUpload}, carregamento em pedaços desativado" }, "search": { "images": "Imagens", "music": "Música", "pdf": "PDF", - "pressToSearch": "Tecla Enter para pesquisar...", "search": "Pesquisar...", - "typeToSearch": "Escrever para pesquisar...", "types": "Tipos", "video": "Vídeos", "path": "Caminho:", "smallerThan": "Mais pequeno do que:", "largerThan": "Maior do que:", "helpText1": "A pesquisa ocorre em cada caractere digitado (mínimo de 3 caracteres para termos de pesquisa).", - "helpText2": "O índice: A pesquisa utiliza o índice que é atualizado automaticamente no intervalo configurado (predefinição: 5 minutos). A pesquisa quando o programa acabou de ser iniciado pode resultar em resultados incompletos." + "helpText2": "O índice: A pesquisa utiliza o índice que é atualizado automaticamente no intervalo configurado (predefinição: 5 minutos). A pesquisa quando o programa acabou de ser iniciado pode resultar em resultados incompletos.", + "noResults": "Não foram encontrados resultados na pesquisa indexada.", + "onlyFolders": "Apenas pastas", + "onlyFiles": "Apenas ficheiros", + "showOptions": "Mostrar opções", + "photos": "Fotos", + "audio": "Áudio", + "videos": "Vídeos", + "documents": "Documentos", + "archives": "Arquivos", + "number": "Número", + "searchContext": "Contexto de pesquisa: {context}", + "notEnoughCharacters": "Não há caracteres suficientes para pesquisar (min {minSearchLength})", + "typeToSearch": "Comece a escrever {minSearchLength} ou mais caracteres para iniciar a pesquisa." }, "settings": { "admin": "Admin", @@ -214,10 +227,8 @@ "avoidChanges": "(deixe em branco para manter)", "branding": "Marca", "brandingDirectoryPath": "Caminho da pasta de marca", - "brandingHelp": "Pode personalizar a aparência do seu Navegador de Ficheiros, alterar o nome, substituindo o logótipo, adicionando estilos personalizados e mesmo desativando links externos para o GitHub.\nPara mais informações sobre marca personalizada, por favor veja {0}.", "changePassword": "Alterar palavra-passe", "commandRunner": "Execução de comandos", - "commandRunnerHelp": "Aqui pode definir comandos que são executados nos eventos nomeados. Tem de escrever um por linha. As variáveis de ambiente {0} e {1} estarão disponíveis, sendo {0} relativo a {1}. Para mais informações sobre esta funcionalidade e as variáveis de ambiente, veja {2}.", "commandsUpdated": "Comandos atualizados!", "createUserDir": "Criar automaticamente a pasta de início ao adicionar um novo utilizador", "customStylesheet": "Folha de estilos personalizada", @@ -300,7 +311,14 @@ "adminOptions": "Opções do utilizador administrador", "shareDuration": "Expirações", "searchOptions": "Opções de pesquisa", - "downloads": "Transferências" + "downloads": "Transferências", + "systemAdmin": "Sistema e administração", + "configViewer": "Visualizador de configuração", + "configViewerShowFull": "Mostrar configuração completa", + "configViewerShowComments": "Mostrar comentários", + "configViewerLoadConfig": "Configuração de carga", + "brandingHelp": "É possível personalizar a aparência da instância do Navegador de arquivos alterando seu nome, substituindo o logotipo, adicionando estilos personalizados e até mesmo desativando links externos para o GitHub.\nPara obter mais informações sobre a marca personalizada, consulte {0}.", + "commandRunnerHelp": "Aqui pode definir comandos que são executados nos eventos nomeados. Deve escrever um por linha. As variáveis de ambiente {0} e {1} estarão disponíveis, sendo {0} relativo a {1}. Para obter mais informações sobre este recurso e as variáveis de ambiente disponíveis, leia {2}." }, "sidebar": { "help": "Ajuda", @@ -358,12 +376,21 @@ "hideNavButtons": "Ocultar botões de navegação", "hideNavButtonsDescription": "Oculte os botões de navegação na barra de navegação na partilha para criar um aspeto minimalista.", "viewModeDescription": "Esquema predefinido para a página partilhada.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Foi-lhe enviada uma ação para visualizar ou descarregar.", "disableShareCard": "Desativar o cartão de partilha", "disableShareCardDescription": "Desativar o cartão de partilha na página partilhada na barra lateral ou na parte superior da página no telemóvel.", "disableSidebar": "Desativar a barra lateral", - "disableSidebarDescription": "Desativar a barra lateral na página partilhada." + "disableSidebarDescription": "Desativar a barra lateral na página partilhada.", + "enforceDarkLightMode": "Aplicar o modo de tema", + "enforceDarkLightModeDescription": "Força um modo de tema específico (escuro ou claro) para esta partilha, substituindo as preferências do utilizador.", + "default": "Não impor um modo", + "dark": "Escuro", + "light": "Luz", + "enableOnlyOffice": "Ativar o visualizador OnlyOffice", + "enableOnlyOfficeDescription": "Permitir a visualização de ficheiros de escritório utilizando o OnlyOffice nesta partilha.", + "enableOnlyOfficeEditing": "Ativar a edição OnlyOffice", + "enableOnlyOfficeEditingDescription": "Permitir a edição de ficheiros do Office utilizando o OnlyOffice nesta partilha.", + "titleDefault": "Ficheiros partilhados - {title}" }, "api": { "title": "Chaves da API", @@ -478,7 +505,13 @@ "defaultThemeDescription": "default - O tema predefinido", "customTheme": "Escolha o seu tema", "showSelectMultiple": "Mostrar seleção múltipla no menu de contexto no ambiente de trabalho", - "showSelectMultipleDescription": "Por predefinição, o menu de contexto do ambiente de trabalho não tem a opção \"selecionar vários\" como no telemóvel. Esta opção mostra-a sempre no menu de contexto." + "showSelectMultipleDescription": "Por predefinição, o menu de contexto do ambiente de trabalho não tem a opção \"selecionar vários\" como no telemóvel. Esta opção mostra-a sempre no menu de contexto.", + "defaultMediaPlayer": "Utilize o leitor multimédia nativo do seu navegador", + "defaultMediaPlayerDescription": "Utilize o leitor multimédia nativo do seu navegador para ficheiros de vídeo e áudio em vez do leitor multimédia 'vue-plyr' incluído no FileBrowser.", + "debugOfficeEditor": "Ativar o modo de depuração do OnlyOffice", + "debugOfficeEditorDescription": "Mostrar um popup de depuração com informações adicionais no editor OnlyOffice.", + "fileViewerOptions": "Opções do visualizador de ficheiros", + "themeAndLanguage": "Tema e linguagem" }, "general": { "name": "Nome", @@ -513,15 +546,19 @@ "name": "Nome", "accessManagement": "Gestão do acesso", "all": "Recusar tudo", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Isto mostra o que acontece quando um utilizador tenta aceder a um caminho onde não existem regras de acesso específicas. \"Permitir\" significa que os utilizadores podem aceder por predefinição, \"Negar\" significa que os utilizadores são bloqueados por predefinição, a menos que sejam explicitamente autorizados." + "defaultBehaviorDescription": "Isto mostra o que acontece quando um utilizador tenta aceder a um caminho onde não existem regras de acesso específicas. \"Permitir\" significa que os utilizadores podem aceder por predefinição, \"Negar\" significa que os utilizadores são bloqueados por predefinição, a menos que sejam explicitamente autorizados.", + "defaultBehavior": "Ação por defeito{suffix}" }, "editor": { "uninitialized": "Falha ao inicializar o editor. Por favor, recarregue.", "saveFailed": "Falha ao guardar o ficheiro. Por favor, tente novamente.", "saveAborted": "Gravação abortada. O nome do ficheiro não corresponde ao ficheiro ativo.", "saveDisabled": "A gravação de ficheiros partilhados não é suportada.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Operação de gravação abortada. O ficheiro ativo da aplicação ({activeFile}) não corresponde ao ficheiro que está a tentar guardar ({tryingToSave}).", + "syncFailed": "não conseguiu sincronizar o estado com a rota para \"{filename}\" após 5 tentativas. Abortar a configuração do editor para evitar a corrupção de dados." + }, + "player": { + "LoopEnabled": "Loop ativado", + "LoopDisabled": "Loop desativado" } } diff --git a/frontend/src/i18n/ro.json b/frontend/src/i18n/ro.json index 368462144..de9471407 100644 --- a/frontend/src/i18n/ro.json +++ b/frontend/src/i18n/ro.json @@ -49,7 +49,8 @@ "clearCompleted": "Golire finalizată", "showMore": "Arată mai multe", "showLess": "Arată mai puțin", - "openParentFolder": "Deschideți folderul părinte" + "openParentFolder": "Deschideți folderul părinte", + "selectedCount": "Numărul de elemente selectate pentru a efectua o acțiune" }, "download": { "downloadFile": "Descarcă fișier", @@ -84,15 +85,15 @@ "click": "alege fișier sau director", "ctrl": { "click": "alege multiple fișiere sau directoare", - "f": "deschide căutarea", - "s": "salvează un fișier sau descarcă directorul în care te afli" + "d": "descărcați elementele selectate" }, "del": "șterge elementele selectate", - "doubleClick": "deschide un fișier sau director", "esc": "elibereaza selecția și/sau închide fereastra", - "f1": "această informație", "f2": "redenumește fișierul", - "help": "Ajutor" + "help": "Ajutor", + "f1": "arată ajutor prompt", + "description": "Puteți vizualiza mai jos opțiunile de navigație de bază. Pentru informații suplimentare, vă rugăm să vizitați", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -134,7 +135,6 @@ "prompts": { "copy": "Copiază", "copyMessage": "Alege unde vrei să copiezi fișierele:", - "deleteMessageMultiple": "Ești sigur că vrei să ștergi aceste {count} fișier(e)?", "deleteMessageSingle": "Ești sigur că vrei să ștergi acest fișier/director?", "deleteTitle": "Șterge fișiere", "displayName": "Nume afișat:", @@ -142,7 +142,6 @@ "downloadMessage": "Alege formatul în care vrei să descarci.", "error": "Ceva n-a funcționat cum trebuie", "fileInfo": "Informații fișier", - "lastModified": "Ultima modificare{suffix}", "move": "Mută", "moveMessage": "Alege destinația:", "newArchetype": "Crează o nouă postare pe baza unui șablon. Fișierul va fi creat in directorul conținut.", @@ -150,10 +149,7 @@ "newDirMessage": "Scrie numele noului director.", "newFile": "Fișier nou", "newFileMessage": "Scrie numele noului fișier.", - "numberDirs": "Numărul directoarelor{suffix}", - "numberFiles": "Numărul fișierelor{suffix}", "rename": "Redenumește", - "renameMessage": "Redactează un nou nume pentru", "replace": "Înlocuiește", "replaceMessage": "Unul din fișierele încărcate intră în conflict din cauza denumrii. Vrei să înlocuiești fișierul existent?\n", "schedule": "Programare", @@ -161,7 +157,6 @@ "show": "Arată", "size": "Dimensiune", "upload": "Încărcare", - "deleteMessageShare": "Are you sure you want to delete this share: {path} ?", "deleteUserMessage": "Sunteți sigur că doriți să ștergeți acest utilizator?", "filesSelected": "fișiere selectate", "optionalPassword": "Parolă opțională", @@ -172,10 +167,6 @@ "dragAndDrop": "Drag and drop fișiere aici", "completed": "Completat", "conflictsDetected": "Conflicte detectate", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Nume invalid furnizat: nu poate conține \"/\" sau \"\\\"", - "source": "Source{suffix}", "downloadsLimit": "Limita numărului de descărcări", "maxBandwidth": "Lățime de bandă maximă de descărcare (kbps)", "shareTheme": "Temă de partajare", @@ -186,22 +177,44 @@ "shareBanner": "Partajați URL-ul bannerului", "shareFavicon": "Partajați URL favicon", "optionalPasswordHelp": "Dacă este setată, utilizatorii trebuie să introducă această parolă pentru a vizualiza conținutul partajat.", - "viewMode": "Mod de vizualizare" + "viewMode": "Mod de vizualizare", + "renameMessage": "Introduceți un nume nou:", + "renameMessageInvalid": "Un fișier nu poate conține \"/\" sau \"\\\"", + "source": "Sursa{suffix}", + "deleteMessageMultiple": "Sunteți sigur că doriți să eliminați fișierul (fișierele) {count}?", + "deleteMessageShare": "Sunteți sigur că doriți să ștergeți această partajare: {path} ?", + "lastModified": "Ultima modificare{suffix}", + "numberDirs": "Număr de directoare{suffix}", + "numberFiles": "Număr de fișiere{suffix}", + "renameMessageConflict": "Un element numit \"{filename}\" există deja!", + "uploadSettingsChunked": "Încărcări simultane maxime: {maxConcurrentUpload}, dimensiunea bucăților: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Încărcări simultane maxime: {maxConcurrentUpload}, încărcare fragmentată dezactivată" }, "search": { "images": "Imagini", "music": "Muzică", "pdf": "PDF", - "pressToSearch": "Apasă enter pentru a căuta...", "search": "Caută...", - "typeToSearch": "Scrie pentru a căuta...", "types": "Tipuri", "video": "Video", "path": "Calea:", "smallerThan": "Mai mic decât:", "largerThan": "Mai mare decât:", "helpText1": "Căutarea are loc pe fiecare caracter tastat (minim 3 caractere pentru termenii de căutare).", - "helpText2": "Indexul: Căutarea utilizează indexul care se actualizează automat la intervalul configurat (implicit: 5 minute). Căutarea atunci când programul tocmai a început poate duce la rezultate incomplete." + "helpText2": "Indexul: Căutarea utilizează indexul care se actualizează automat la intervalul configurat (implicit: 5 minute). Căutarea atunci când programul tocmai a început poate duce la rezultate incomplete.", + "noResults": "Nu s-au găsit rezultate în căutarea indexată.", + "onlyFolders": "Numai foldere", + "onlyFiles": "Numai fișiere", + "showOptions": "Afișare opțiuni", + "photos": "Fotografii", + "audio": "Audio", + "videos": "Video", + "documents": "Documente", + "archives": "Arhive", + "number": "Număr", + "searchContext": "Context de căutare: {context}", + "notEnoughCharacters": "Nu sunt suficiente caractere pentru căutare (min {minSearchLength})", + "typeToSearch": "Începeți să tastați {minSearchLength} sau mai multe caractere pentru a începe căutarea." }, "settings": { "admin": "Admin", @@ -214,10 +227,8 @@ "avoidChanges": "(lasă gol pentru a nu schimba)", "branding": "Branding", "brandingDirectoryPath": "Calea către directorul de branding", - "brandingHelp": "Poți personaliza cum arată instanța ta de File Browser modificându-i numele, înlocuindu-i logo-ul, adăugându-i stiluri personalizate si chiar dezactivând linkurile catre GitHub.\nPentru mai multe informații despre branding fă click aici", "changePassword": "Schimbă parola", "commandRunner": "Rulează comenzi", - "commandRunnerHelp": "Aici poti seta comenzile care sunt executate in evenimente. Trebuie să scrii una pe linie. Variabilele de mediu {0} și {1} vor fi disponile, {0} fiind relativă la {1}. Pentru mai multe informații despre acest feature si variabilele de mediu disponibile, cititi {2}.", "commandsUpdated": "Comenzi actualizate!", "createUserDir": "Auto create user home dir while adding new user", "customStylesheet": "CSS personalizat", @@ -300,7 +311,14 @@ "adminOptions": "Opțiuni utilizator administrator", "shareDuration": "Expiră", "searchOptions": "Opțiuni de căutare", - "downloads": "Descărcări" + "downloads": "Descărcări", + "systemAdmin": "Sistem și administrare", + "configViewer": "Vizualizator de configurare", + "configViewerShowFull": "Afișați configurația completă", + "configViewerShowComments": "Afișați comentariile", + "configViewerLoadConfig": "Configurare încărcare", + "brandingHelp": "Puteți personaliza aspectul instanței File Browser prin schimbarea numelui, înlocuirea logo-ului, adăugarea de stiluri personalizate și chiar dezactivarea linkurilor externe către GitHub.\nPentru mai multe informații despre brandingul personalizat, consultați {0}.", + "commandRunnerHelp": "Aici puteți seta comenzi care sunt executate în cadrul evenimentelor numite. Trebuie să scrieți câte una pe linie. Variabilele de mediu {0} și {1} vor fi disponibile, {0} fiind relativ la {1}. Pentru mai multe informații despre această caracteristică și variabilele de mediu disponibile, vă rugăm să citiți {2}." }, "sidebar": { "help": "Ajutor", @@ -358,12 +376,21 @@ "hideNavButtons": "Ascundeți butoanele de navigare", "hideNavButtonsDescription": "Ascundeți butoanele de navigare de pe bara de navigare din share pentru a crea un aspect minimalist.", "viewModeDescription": "Layout implicit pentru pagina partajată.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "V-a fost trimisă o acțiune pentru a o vizualiza sau descărca.", "disableShareCard": "Dezactivați cardul de partajare", "disableShareCardDescription": "Dezactivați cardul de partajare pe pagina partajată în bara laterală sau în partea de sus a paginii pe mobil.", "disableSidebar": "Dezactivați bara laterală", - "disableSidebarDescription": "Dezactivați bara laterală de pe pagina partajată." + "disableSidebarDescription": "Dezactivați bara laterală de pe pagina partajată.", + "enforceDarkLightMode": "Impuneți modul tematic", + "enforceDarkLightModeDescription": "Forțează un anumit mod tematic (întunecat sau deschis) pentru această partajare, trecând peste preferințele utilizatorului.", + "default": "Nu impuneți un mod", + "dark": "Întuneric", + "light": "Lumină", + "enableOnlyOffice": "Activați vizualizatorul OnlyOffice", + "enableOnlyOfficeDescription": "Permiteți vizualizarea fișierelor office utilizând OnlyOffice în această partajare.", + "enableOnlyOfficeEditing": "Activați editarea OnlyOffice", + "enableOnlyOfficeEditingDescription": "Permiteți editarea fișierelor de birou utilizând OnlyOffice în această partajare.", + "titleDefault": "Fișiere partajate - {title}" }, "api": { "title": "Chei API:", @@ -447,8 +474,8 @@ "totalDenied": "Refuzat", "totalAllowed": "Permise", "all": "Refuzați toate", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Aceasta arată ce se întâmplă atunci când un utilizator încearcă să acceseze o cale unde nu există reguli specifice de acces. \"Allow\" înseamnă că utilizatorii pot accesa în mod implicit, \"Deny\" înseamnă că utilizatorii sunt blocați în mod implicit dacă nu li se permite în mod explicit." + "defaultBehaviorDescription": "Aceasta arată ce se întâmplă atunci când un utilizator încearcă să acceseze o cale unde nu există reguli specifice de acces. \"Allow\" înseamnă că utilizatorii pot accesa în mod implicit, \"Deny\" înseamnă că utilizatorii sunt blocați în mod implicit dacă nu li se permite în mod explicit.", + "defaultBehavior": "Acțiune implicită{suffix}" }, "fileLoading": { "title": "Încărcări și descărcări", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - Tema implicită", "customTheme": "Alege-ți tema", "showSelectMultiple": "Afișarea selecției multiple în meniul contextual de pe desktop", - "showSelectMultipleDescription": "În mod implicit, meniul contextual de pe desktop nu are opțiunea \"select multiple\", ca pe mobil. Această opțiune se afișează întotdeauna în meniul contextual." + "showSelectMultipleDescription": "În mod implicit, meniul contextual de pe desktop nu are opțiunea \"select multiple\", ca pe mobil. Această opțiune se afișează întotdeauna în meniul contextual.", + "defaultMediaPlayer": "Utilizați playerul media nativ al browserului dvs.", + "defaultMediaPlayerDescription": "Utilizați playerul media nativ al browserului dvs. pentru fișierele video și audio mai degrabă decât playerul media \"vue-plyr\" inclus în FileBrowser.", + "debugOfficeEditor": "Activați modul de depanare OnlyOffice", + "debugOfficeEditorDescription": "Afișați o fereastră pop-up de depanare cu informații suplimentare în editorul OnlyOffice.", + "fileViewerOptions": "Opțiuni vizualizator de fișiere", + "themeAndLanguage": "Temă și limbaj" }, "editor": { "uninitialized": "Nu s-a reușit inițializarea editorului. Vă rugăm să reîncărcați.", "saveFailed": "Nu ați reușit să salvați fișierul. Vă rugăm să încercați din nou.", "saveAborted": "Salvare întreruptă. Numele fișierului nu corespunde fișierului activ.", "saveDisabled": "Salvarea fișierelor partajate nu este acceptată.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Operațiunea de salvare a fost întreruptă. Fișierul activ al aplicației ({activeFile}) nu corespunde cu fișierul pe care încercați să îl salvați ({tryingToSave}).", + "syncFailed": "nu a reușit să sincronizeze starea cu ruta pentru \"{filename}\" după 5 încercări. Întreruperea configurării editorului pentru a preveni coruperea datelor." + }, + "player": { + "LoopEnabled": "Bucla activată", + "LoopDisabled": "Bucla dezactivată" } } diff --git a/frontend/src/i18n/ru.json b/frontend/src/i18n/ru.json index b428b86ef..c1ebff9ef 100644 --- a/frontend/src/i18n/ru.json +++ b/frontend/src/i18n/ru.json @@ -49,7 +49,8 @@ "clearCompleted": "Очистка завершена", "showMore": "Показать еще", "showLess": "Показать меньше", - "openParentFolder": "Откройте родительскую папку" + "openParentFolder": "Откройте родительскую папку", + "selectedCount": "Количество элементов, выбранных для выполнения действия" }, "download": { "downloadFile": "Скачать файл", @@ -84,15 +85,15 @@ "click": "выбрать файл или каталог", "ctrl": { "click": "выбрать несколько файлов или каталогов", - "f": "открыть поиск", - "s": "скачать файл или текущий каталог" + "d": "скачать выбранные элементы" }, "del": "удалить выбранные элементы", - "doubleClick": "открыть файл или каталог", "esc": "очистить выделение и/или закрыть окно", - "f1": "помощь", "f2": "переименовать файл", - "help": "Помощь" + "help": "Помощь", + "f1": "показать подсказку", + "description": "Ниже представлены основные варианты навигации. Для получения дополнительной информации, пожалуйста, посетите", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -134,16 +135,13 @@ "prompts": { "copy": "Копировать", "copyMessage": "Копировать в:", - "deleteMessageMultiple": "Удалить эти файлы ({count})?", "deleteMessageSingle": "Удалить этот файл/каталог?", - "deleteMessageShare": "Удалить этот общий файл/каталог ({path})?", "deleteTitle": "Удалить файлы", "displayName": "Отображаемое имя:", "download": "Скачать файлы", "downloadMessage": "Выберите формат в котором хотите скачать.", "error": "Ошибка", "fileInfo": "Информация о файле", - "lastModified": "Последнее изменение{suffix}", "move": "Переместить", "moveMessage": "Переместить в:", "newArchetype": "Создайте новую запись на основе архетипа. Файл будет создан в каталоге.", @@ -151,10 +149,7 @@ "newDirMessage": "Имя нового каталога.", "newFile": "Новый файл", "newFileMessage": "Имя нового файла.", - "numberDirs": "Количество каталогов{suffix}", - "numberFiles": "Количество файлов{suffix}", "rename": "Переименовать", - "renameMessage": "Новое имя", "replace": "Заменить", "replaceMessage": "Имя одного из загружаемых файлов совпадает с уже существующим файлом. Вы хотите заменить существующий?\n", "schedule": "Планировка", @@ -172,10 +167,6 @@ "dragAndDrop": "Перетащите файлы сюда", "completed": "Завершено", "conflictsDetected": "Обнаруженные конфликты", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Неверное имя: не может содержать \"/\" или \"\\\".", - "source": "Source{suffix}", "downloadsLimit": "Ограничение на количество загрузок", "maxBandwidth": "Максимальная пропускная способность загрузки (кб/с)", "shareTheme": "Поделиться темой", @@ -186,22 +177,44 @@ "shareBanner": "Поделиться URL-адресом баннера", "shareFavicon": "Поделиться URL-адресом favicon", "optionalPasswordHelp": "Если установлен, пользователи должны ввести этот пароль для просмотра общего содержимого.", - "viewMode": "Режим просмотра" + "viewMode": "Режим просмотра", + "renameMessage": "Введите новое имя:", + "renameMessageInvalid": "Файл не может содержать \"/\" или \"\\\".", + "source": "Источник{suffix}", + "deleteMessageMultiple": "Вы уверены, что хотите удалить файл(ы) {count}?", + "deleteMessageShare": "Вы уверены, что хотите удалить этот ресурс: {path} ?", + "lastModified": "Последнее изменение{suffix}", + "numberDirs": "Количество каталогов{suffix}", + "numberFiles": "Количество файлов{suffix}", + "renameMessageConflict": "Элемент с именем \"{filename}\" уже существует!", + "uploadSettingsChunked": "Максимальное количество одновременных загрузок: {maxConcurrentUpload}, размер чанка: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Максимальное количество одновременных загрузок: {maxConcurrentUpload}, загрузка в куски отключена" }, "search": { "images": "Изображения", "music": "Музыка", "pdf": "PDF", - "pressToSearch": "Нажмите Enter для поиска ...", "search": "Поиск...", - "typeToSearch": "Введите имя файла ...", "types": "Типы", "video": "Видео", "path": "Путь:", "smallerThan": "Меньше, чем:", "largerThan": "Больше, чем:", "helpText1": "Поиск осуществляется по каждому введенному символу (минимум 3 символа для поисковых запросов).", - "helpText2": "Индекс: Для поиска используется индекс, который автоматически обновляется с заданным интервалом (по умолчанию: 5 минут). Поиск, когда программа только что запущена, может привести к неполным результатам." + "helpText2": "Индекс: Для поиска используется индекс, который автоматически обновляется с заданным интервалом (по умолчанию: 5 минут). Поиск, когда программа только что запущена, может привести к неполным результатам.", + "noResults": "В проиндексированном поиске результатов не найдено.", + "onlyFolders": "Только папки", + "onlyFiles": "Только файлы", + "showOptions": "Показать параметры", + "photos": "Фотографии", + "audio": "Аудио", + "videos": "Видео", + "documents": "Документы", + "archives": "Архивы", + "number": "Номер", + "searchContext": "Контекст поиска: {context}", + "notEnoughCharacters": "Недостаточно символов для поиска (мин. {minSearchLength})", + "typeToSearch": "Начните вводить {minSearchLength} или более символов, чтобы начать поиск." }, "settings": { "admin": "Админ", @@ -214,10 +227,8 @@ "avoidChanges": "(оставьте поле пустым, чтобы избежать изменений)", "branding": "Брендинг", "brandingDirectoryPath": "Путь к каталогу брендов", - "brandingHelp": "Вы можете настроить внешний вид файлового браузера, изменив его имя, заменив логотип, добавив собственные стили и даже отключив внешние ссылки на GitHub.\nДополнительную информацию о персонализированном брендинге можно найти на странице {0}.", "changePassword": "Изменение пароля", "commandRunner": "Запуск команд", - "commandRunnerHelp": "Здесь вы можете установить команды, которые будут выполняться в указанных событиях. Вы должны указать по одной команде в каждой строке. Переменные среды {0} и {1} будут доступны, будучи {0} относительно {1}. Дополнительные сведения об этой функции и доступных переменных среды см. В {2}.", "commandsUpdated": "Команды обновлены!", "createUserDir": "Автоматическое создание домашнего каталога пользователя при добавлении нового пользователя", "customStylesheet": "Свой стиль", @@ -300,7 +311,14 @@ "adminOptions": "Параметры пользователя администратора", "shareDuration": "Срок действия:", "searchOptions": "Параметры поиска", - "downloads": "Скачать" + "downloads": "Скачать", + "systemAdmin": "Система и администрирование", + "configViewer": "Программа просмотра конфигурации", + "configViewerShowFull": "Показать полную конфигурацию", + "configViewerShowComments": "Показать комментарии", + "configViewerLoadConfig": "Конфигурация нагрузки", + "brandingHelp": "Вы можете настроить внешний вид экземпляра File Browser, изменив его название, заменив логотип, добавив пользовательские стили и даже отключив внешние ссылки на GitHub.\nБолее подробную информацию о пользовательском брендинге можно найти на сайте {0}.", + "commandRunnerHelp": "Здесь вы можете задать команды, которые будут выполняться в названных событиях. Вы должны написать по одной команде в строке. Будут доступны переменные окружения {0} и {1}, причем {0} будет относиться к {1}. Для получения дополнительной информации об этой функции и доступных переменных окружения, пожалуйста, прочтите {2}." }, "sidebar": { "help": "Помощь", @@ -358,12 +376,21 @@ "hideNavButtons": "Скрыть кнопки навигации", "hideNavButtonsDescription": "Спрячьте кнопки навигации на навигационной панели в ресурсе, чтобы создать минималистичный вид.", "viewModeDescription": "Макет по умолчанию для общей страницы.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Вам отправлена акция для просмотра или загрузки.", "disableShareCard": "Отключение карты доступа", "disableShareCardDescription": "Отключите карточку общего доступа на странице общего доступа в боковой панели или в верхней части страницы на мобильных устройствах.", "disableSidebar": "Отключить боковую панель", - "disableSidebarDescription": "Отключите боковую панель на общей странице." + "disableSidebarDescription": "Отключите боковую панель на общей странице.", + "enforceDarkLightMode": "Включение режима темы", + "enforceDarkLightModeDescription": "Принудительно устанавливает определенный режим темы (темный или светлый) для этого ресурса, отменяя предпочтения пользователя.", + "default": "Не вводите режим", + "dark": "Темнота", + "light": "Свет", + "enableOnlyOffice": "Включите программу просмотра OnlyOffice", + "enableOnlyOfficeDescription": "Разрешите просмотр офисных файлов с помощью OnlyOffice в этом ресурсе.", + "enableOnlyOfficeEditing": "Включить редактирование OnlyOffice", + "enableOnlyOfficeEditingDescription": "Разрешите редактирование офисных файлов с помощью OnlyOffice в этом ресурсе.", + "titleDefault": "Общие файлы - {title}" }, "api": { "title": "Ключи API:", @@ -447,8 +474,8 @@ "totalDenied": "Отказано", "totalAllowed": "Разрешено", "all": "Запретить все", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Здесь показано, что происходит, когда пользователь пытается получить доступ к пути, для которого не существует специальных правил доступа. 'Allow' означает, что пользователи могут получить доступ по умолчанию, 'Deny' означает, что пользователи блокируются по умолчанию, если только они не разрешены явным образом." + "defaultBehaviorDescription": "Здесь показано, что происходит, когда пользователь пытается получить доступ к пути, для которого не существует специальных правил доступа. 'Allow' означает, что пользователи могут получить доступ по умолчанию, 'Deny' означает, что пользователи блокируются по умолчанию, если только они не разрешены явным образом.", + "defaultBehavior": "Действие по умолчанию{suffix}" }, "fileLoading": { "title": "Загрузка и скачивание", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - Тема по умолчанию", "customTheme": "Выберите тему", "showSelectMultiple": "Отображение выбора нескольких в контекстном меню на рабочем столе", - "showSelectMultipleDescription": "По умолчанию в контекстном меню рабочего стола нет опции \"выбрать несколько\", как в мобильном. Эта опция всегда отображается в контекстном меню." + "showSelectMultipleDescription": "По умолчанию в контекстном меню рабочего стола нет опции \"выбрать несколько\", как в мобильном. Эта опция всегда отображается в контекстном меню.", + "defaultMediaPlayer": "Используйте встроенный медиаплеер вашего браузера", + "defaultMediaPlayerDescription": "Для воспроизведения видео- и аудиофайлов используйте штатный медиаплеер браузера, а не медиаплеер 'vue-plyr', входящий в комплект поставки FileBrowser.", + "debugOfficeEditor": "Включить режим отладки OnlyOffice", + "debugOfficeEditorDescription": "Отображение всплывающего окна отладки с дополнительной информацией в редакторе OnlyOffice.", + "fileViewerOptions": "Параметры просмотра файлов", + "themeAndLanguage": "Тема и язык" }, "editor": { "uninitialized": "Не удалось инициализировать редактор. Пожалуйста, перезагрузитесь.", "saveFailed": "Не удалось сохранить файл. Пожалуйста, попробуйте еще раз.", "saveAborted": "Сохранение прервано. Имя файла не совпадает с именем активного файла.", "saveDisabled": "Сохранение общих файлов не поддерживается.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Операция сохранения прервана. Активный файл приложения ({activeFile}) не совпадает с файлом, который вы пытаетесь сохранить ({tryingToSave}).", + "syncFailed": "не удалось синхронизировать состояние с маршрутом для \"{filename}\" после 5 попыток. Прерываю настройку редактора, чтобы предотвратить повреждение данных." + }, + "player": { + "LoopEnabled": "Петля включена", + "LoopDisabled": "Петля отключена" } } diff --git a/frontend/src/i18n/sk.json b/frontend/src/i18n/sk.json index 9d4889ef4..b241b4d70 100644 --- a/frontend/src/i18n/sk.json +++ b/frontend/src/i18n/sk.json @@ -49,7 +49,8 @@ "clearCompleted": "Vyčistiť dokončené", "showMore": "Zobraziť viac", "showLess": "Zobraziť menej", - "openParentFolder": "Otvorenie nadradeného priečinka" + "openParentFolder": "Otvorenie nadradeného priečinka", + "selectedCount": "Počet položiek vybraných na vykonanie akcie" }, "download": { "downloadFile": "Stiahnuť súbor", @@ -84,15 +85,15 @@ "click": "vyberie súbor alebo priečinok", "ctrl": { "click": "vyberie viac súborov alebo priečinkov", - "f": "otvorí vyhľadávanie", - "s": "uloží súbor alebo stiahne priečinok tam kde ste" + "d": "stiahnuť vybrané položky" }, "del": "odstráni vybraté položky", - "doubleClick": "otvorí súbor alebo priečinok", "esc": "zruší výber a/alebo zavrie okno", - "f1": "tieto informácie", "f2": "premenuje súbor", - "help": "Pomoc" + "help": "Pomoc", + "f1": "zobraziť výzvu na pomoc", + "description": "Základné možnosti navigácie si môžete pozrieť nižšie. Ďalšie informácie nájdete na stránke", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -134,16 +135,13 @@ "prompts": { "copy": "Kopírovať", "copyMessage": "Zvoľte miesto, kde chcete kopírovať súbory:", - "deleteMessageMultiple": "Naozaj chcete odstrániť {count} súbor(ov)?", "deleteMessageSingle": "Naozaj chcete odstrániť tento súbor/priečinok?", - "deleteMessageShare": "Naozaj chcete odstrániť toto zdieľanie({path})?", "deleteTitle": "Odstránenie súborov", "displayName": "Zobrazený názov:", "download": "Stiahnuť súbory", "downloadMessage": "Vyberte formát, ktorý chcete stiahnuť.", "error": "Niečo sa pokazilo", "fileInfo": "Informácie o súbore", - "lastModified": "Dátum zmeny{suffix}", "move": "Presunúť", "moveMessage": "Zvoľte nový domov pre vaše súbory/priečinky:", "newArchetype": "Vytvorí nový príspevok z archetypu. Nový súbor sa vytvorí v priečinku s obsahom.", @@ -151,10 +149,7 @@ "newDirMessage": "Napíšte názov nového priečinka.", "newFile": "Nový súbor", "newFileMessage": "Napíšte názov nového súboru.", - "numberDirs": "Počet priečinkov{suffix}", - "numberFiles": "Počet súborov{suffix}", "rename": "Premenovať", - "renameMessage": "Zadajte nový názov pre", "replace": "Nahradiť", "replaceMessage": "Niektorý nahrávaný súbor je v konflikte názvov. Chcete nahradiť existujúci súbor?\n", "schedule": "Naplánovať", @@ -172,10 +167,6 @@ "dragAndDrop": "Presuňte sem súbory", "completed": "Dokončené", "conflictsDetected": "Zistené konflikty", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Zadaný neplatný názov: nemôže byť kontajner \"/\" alebo \"\\\"", - "source": "Source{suffix}", "downloadsLimit": "Limit počtu stiahnutí", "maxBandwidth": "Maximálna šírka pásma sťahovania (kbps)", "shareTheme": "Zdieľať tému", @@ -186,22 +177,44 @@ "shareBanner": "Zdieľanie adresy URL banneru", "shareFavicon": "Zdieľanie adresy URL favicon", "optionalPasswordHelp": "Ak je nastavené, používatelia musia zadať toto heslo, aby si mohli prezerať zdieľaný obsah.", - "viewMode": "Režim zobrazenia" + "viewMode": "Režim zobrazenia", + "renameMessage": "Zadajte nový názov:", + "renameMessageInvalid": "Súbor nemôže obsahovať \"/\" alebo \"\\\"", + "source": "Zdroj{suffix}", + "deleteMessageMultiple": "Ste si istí, že chcete odstrániť súbor(y) {count}?", + "deleteMessageShare": "Ste si istí, že chcete odstrániť toto zdieľanie: {path} ?", + "lastModified": "Posledná úprava{suffix}", + "numberDirs": "Počet adresárov{suffix}", + "numberFiles": "Počet súborov{suffix}", + "renameMessageConflict": "Položka s názvom \"{filename}\" už existuje!", + "uploadSettingsChunked": "Maximálne súbežné nahrávanie: {maxConcurrentUpload}, veľkosť chunk: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Maximálne súbežné nahrávanie: {maxConcurrentUpload}, chunked upload vypnuté" }, "search": { "images": "Obrázky", "music": "Hudba", "pdf": "PDF", - "pressToSearch": "Vyhľadáte stlačením Enter...", "search": "Hľadať...", - "typeToSearch": "Vyhľadáte písaním...", "types": "Typy", "video": "Video", "path": "Cesta:", "smallerThan": "Menšie ako:", "largerThan": "Väčšie ako:", "helpText1": "Vyhľadávanie prebieha na základe každého zadaného znaku (minimálne 3 znaky pre hľadané výrazy).", - "helpText2": "Index: Vyhľadávanie využíva index, ktorý sa automaticky aktualizuje v nastavenom intervale (predvolené: 5 minút). Vyhľadávanie, keď sa program práve spustil, môže viesť k neúplným výsledkom." + "helpText2": "Index: Vyhľadávanie využíva index, ktorý sa automaticky aktualizuje v nastavenom intervale (predvolené: 5 minút). Vyhľadávanie, keď sa program práve spustil, môže viesť k neúplným výsledkom.", + "noResults": "V indexovanom vyhľadávaní sa nenašli žiadne výsledky.", + "onlyFolders": "Iba priečinky", + "onlyFiles": "Iba súbory", + "showOptions": "Zobraziť možnosti", + "photos": "Fotografie", + "audio": "Audio", + "videos": "Videá", + "documents": "Dokumenty", + "archives": "Archív", + "number": "Číslo", + "searchContext": "Kontext vyhľadávania: {context}", + "notEnoughCharacters": "Nedostatok znakov na vyhľadávanie (min. {minSearchLength})", + "typeToSearch": "Začnite písať {minSearchLength} alebo viac znakov, aby ste začali vyhľadávať." }, "settings": { "admin": "Admin", @@ -214,10 +227,8 @@ "avoidChanges": "(nechajte prázdne, aby sa nezmenilo)", "branding": "Vlastný vzhľad", "brandingDirectoryPath": "Cesta k priečinku s vlastným vzhľadom", - "brandingHelp": "Môžete si prispôsobiť ako bude vyzerá váš File Browser instance zmenou jeho názvu, výmenou loga a pridaním vlastný štýlov alebo vypnutím externých odkazov na GitHub.\nViac informácií o vlastnom vzhľade nájdete na {0}.", "changePassword": "Zmeniť heslo", "commandRunner": "Spúšťač príkazov", - "commandRunnerHelp": "Sem môžete nastaviť príkazy, ktoré sa vykonajú pri určitých udalostiach. Musíte písať jeden na riadok. Premenné prostredia {0} a {1} sú k dispozícii, s tým že {0} relatívne k {1}. Viac informácií o tejto funkcionalite a dostupných premenných prostredia nájdete na {2}.", "commandsUpdated": "Príkazy upravené!", "createUserDir": "Automaticky vytvoriť domovský priečinok pri pridaní používateľa", "customStylesheet": "Vlastný Stylesheet", @@ -300,7 +311,14 @@ "adminOptions": "Možnosti používateľa správcu", "shareDuration": "Platnosť končí na", "searchOptions": "Možnosti vyhľadávania", - "downloads": "Na stiahnutie" + "downloads": "Na stiahnutie", + "systemAdmin": "Systém a administrátor", + "configViewer": "Prehliadač konfigurácie", + "configViewerShowFull": "Zobraziť úplnú konfiguráciu", + "configViewerShowComments": "Zobraziť komentáre", + "configViewerLoadConfig": "Konfigurácia načítania", + "brandingHelp": "Vzhľad a pôsobenie inštancie Prieskumníka súborov môžete prispôsobiť zmenou názvu, výmenou loga, pridaním vlastných štýlov a dokonca aj zakázaním externých prepojení na GitHub.\nViac informácií o používaní vlastnej značky nájdete na stránke {0}.", + "commandRunnerHelp": "Tu môžete nastaviť príkazy, ktoré sa vykonajú v pomenovaných udalostiach. Na každý riadok musíte napísať jeden. K dispozícii budú premenné prostredia {0} a {1}, pričom {0} bude relatívne k {1}. Viac informácií o tejto funkcii a dostupných premenných prostredia nájdete na stránke {2}." }, "sidebar": { "help": "Pomoc", @@ -358,12 +376,21 @@ "hideNavButtons": "Skrytie navigačných tlačidiel", "hideNavButtonsDescription": "Skryte navigačné tlačidlá na paneli navigácie v zdieľaní, aby ste vytvorili minimalistický vzhľad.", "viewModeDescription": "Predvolené rozloženie zdieľanej stránky.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Bola vám odoslaná akcia na zobrazenie alebo stiahnutie.", "disableShareCard": "Zakázanie zdieľanej karty", "disableShareCardDescription": "Zakážte kartu zdieľania na zdieľanej stránke v bočnom paneli alebo v hornej časti stránky na mobilnom zariadení.", "disableSidebar": "Zakázanie bočného panela", - "disableSidebarDescription": "Zakážte bočný panel na zdieľanej stránke." + "disableSidebarDescription": "Zakážte bočný panel na zdieľanej stránke.", + "enforceDarkLightMode": "Vynútiť režim témy", + "enforceDarkLightModeDescription": "Vynútenie konkrétneho režimu motívu (tmavý alebo svetlý) pre túto akciu, ktorý je nadradený používateľským nastaveniam.", + "default": "Nevynucujte režim", + "dark": "Dark", + "light": "Svetlo", + "enableOnlyOffice": "Povolenie prehliadača OnlyOffice", + "enableOnlyOfficeDescription": "Povoľte zobrazovanie kancelárskych súborov pomocou OnlyOffice v tejto zdieľanej zložke.", + "enableOnlyOfficeEditing": "Povolenie úprav OnlyOffice", + "enableOnlyOfficeEditingDescription": "Povoľte úpravu kancelárskych súborov pomocou aplikácie OnlyOffice v tejto zdieľanej zložke.", + "titleDefault": "Zdieľané súbory - {title}" }, "api": { "title": "Kľúče API:", @@ -447,8 +474,8 @@ "totalDenied": "Odmietnuté", "totalAllowed": "Povolené", "all": "Odmietnuť všetko", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Toto ukazuje, čo sa stane, keď sa používateľ pokúsi získať prístup k ceste, kde neexistujú žiadne špecifické pravidlá prístupu. \"Allow\" (Povoliť) znamená, že používatelia majú predvolený prístup, \"Deny\" (Odmietnuť) znamená, že používatelia sú predvolene blokovaní, pokiaľ to nie je výslovne povolené." + "defaultBehaviorDescription": "Toto ukazuje, čo sa stane, keď sa používateľ pokúsi získať prístup k ceste, kde neexistujú žiadne špecifické pravidlá prístupu. \"Allow\" (Povoliť) znamená, že používatelia majú predvolený prístup, \"Deny\" (Odmietnuť) znamená, že používatelia sú predvolene blokovaní, pokiaľ to nie je výslovne povolené.", + "defaultBehavior": "Predvolená akcia{suffix}" }, "fileLoading": { "title": "Nahrávanie a sťahovanie", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - Predvolená téma", "customTheme": "Vyberte si tému", "showSelectMultiple": "Zobrazenie výberu viacerých v kontextovom menu na ploche", - "showSelectMultipleDescription": "V predvolenom nastavení kontextová ponuka na ploche nemá možnosť \"vybrať viacero\" ako v mobilných zariadeniach. Táto možnosť sa v kontextovej ponuke zobrazuje vždy." + "showSelectMultipleDescription": "V predvolenom nastavení kontextová ponuka na ploche nemá možnosť \"vybrať viacero\" ako v mobilných zariadeniach. Táto možnosť sa v kontextovej ponuke zobrazuje vždy.", + "defaultMediaPlayer": "Používanie natívneho prehrávača médií prehliadača", + "defaultMediaPlayerDescription": "Na prehrávanie video a zvukových súborov použite natívny prehrávač médií prehliadača, a nie mediálny prehrávač vue-plyr, ktorý je súčasťou aplikácie FileBrowser.", + "debugOfficeEditor": "Povolenie režimu ladenia OnlyOffice", + "debugOfficeEditorDescription": "Zobrazenie vyskakovacieho okna ladenia s ďalšími informáciami v editore OnlyOffice.", + "fileViewerOptions": "Možnosti prehliadača súborov", + "themeAndLanguage": "Téma a jazyk" }, "editor": { "uninitialized": "Nepodarilo sa inicializovať editor. Prosím, načítajte ho znova.", "saveFailed": "Nepodarilo sa uložiť súbor. Skúste to prosím znova.", "saveAborted": "Uloženie prerušené. Názov súboru sa nezhoduje s aktívnym súborom.", "saveDisabled": "Ukladanie zdieľaných súborov nie je podporované.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Operácia uloženia sa prerušila. Aktívny súbor aplikácie ({activeFile}) sa nezhoduje so súborom, ktorý sa pokúšate uložiť ({tryingToSave}).", + "syncFailed": "sa nepodarilo synchronizovať stav s trasou pre \"{filename}\" po 5 pokusoch. Prerušenie nastavenia editora, aby sa zabránilo poškodeniu údajov." + }, + "player": { + "LoopEnabled": "Povolená slučka", + "LoopDisabled": "Vypnutá slučka" } } diff --git a/frontend/src/i18n/sv-se.json b/frontend/src/i18n/sv-se.json index 3aebfdb38..3f8b3555d 100644 --- a/frontend/src/i18n/sv-se.json +++ b/frontend/src/i18n/sv-se.json @@ -49,7 +49,8 @@ "clearCompleted": "Klart avslutat", "showMore": "Visa mer", "showLess": "Visa mindre", - "openParentFolder": "Öppna överordnad mapp" + "openParentFolder": "Öppna överordnad mapp", + "selectedCount": "Antal objekt som valts för att utföra en åtgärd" }, "download": { "downloadFile": "Ladda ner fil", @@ -84,15 +85,15 @@ "click": "välj fil eller mapp", "ctrl": { "click": "välj flera filer eller mappar", - "f": "öppnar sök", - "s": "Spara en fil eller ladda ner den katalog där du är" + "d": "ladda ner valda artiklar" }, "del": "ta bort valda objekt", - "doubleClick": "öppna en fil eller mapp", "esc": "Rensa markeringen och/eller stänga prompten", - "f1": "denna information", "f2": "ändra namnet på filen", - "help": "Hjälp" + "help": "Hjälp", + "f1": "visa hjälp prompt", + "description": "Du kan se de grundläggande navigeringsalternativen nedan. För ytterligare information, vänligen besök", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -134,7 +135,6 @@ "prompts": { "copy": "Kopiera", "copyMessage": "Välj var dina filer skall sparas:", - "deleteMessageMultiple": "Är du säker på att du vill radera {count} filer(na)?", "deleteMessageSingle": "Är du säker på att du vill radera denna fil/mapp", "deleteTitle": "Ta bort filer", "displayName": "Visningsnamn:", @@ -142,7 +142,6 @@ "downloadMessage": "Välj format på det du vill lada ner.", "error": "Något gick fel", "fileInfo": "Fil information", - "lastModified": "Senast ändrad{suffix}", "move": "Flytta", "moveMessage": "Välj ny plats för din fil (er)/mapp (ar):", "newArchetype": "Skapa ett nytt inlägg baserat på en arketyp. Din fil kommer att skapas på innehållsmapp.", @@ -150,10 +149,7 @@ "newDirMessage": "Ange namn på din nya mapp.", "newFile": "Ny fil", "newFileMessage": "Ange namn på din nya fil.", - "numberDirs": "Antal kataloger{suffix}", - "numberFiles": "Antal filer{suffix}", "rename": "Ändra namn", - "renameMessage": "Infoga ett nytt namn för", "replace": "Ersätt", "replaceMessage": "En av filerna som du försöker överföra är i konflikt på grund av dess namn. Vill du ersätta den befintliga?\n", "schedule": "Schema", @@ -161,7 +157,6 @@ "show": "Visa", "size": "Storlek", "upload": "Ladda upp", - "deleteMessageShare": "Are you sure you want to delete this share: {path} ?", "deleteUserMessage": "Är du säker på att du vill ta bort den här användaren?", "selected": "utvalda", "filesSelected": "valda filer", @@ -172,10 +167,6 @@ "dragAndDrop": "Dra och släpp filer här", "completed": "Färdigställd", "conflictsDetected": "Konflikter upptäckta", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Ogiltigt namn anges: kan inte innehålla \"/\" eller \"\\\"", - "source": "Source{suffix}", "downloadsLimit": "Begränsning av antal nedladdningar", "maxBandwidth": "Max bandbredd för nedladdning (kbps)", "shareTheme": "Dela tema", @@ -186,22 +177,44 @@ "shareBanner": "Dela bannerns URL", "shareFavicon": "Dela URL för favicon", "optionalPasswordHelp": "Om det är inställt måste användarna ange lösenordet för att visa det delade innehållet.", - "viewMode": "Visa läge" + "viewMode": "Visa läge", + "renameMessage": "Ange ett nytt namn:", + "renameMessageInvalid": "En fil kan inte innehålla \"/\" eller \"\\\"", + "source": "Källa{suffix}", + "deleteMessageMultiple": "Är du säker att du vill ta bort {count} fil(er)?", + "deleteMessageShare": "Är du säker på att du vill ta bort den här aktien: {path} ?", + "lastModified": "Senast ändrad{suffix}", + "numberDirs": "Antal kataloger{suffix}", + "numberFiles": "Antal filer{suffix}", + "renameMessageConflict": "Ett objekt med namnet \"{filename}\" finns redan!", + "uploadSettingsChunked": "Max samtidiga uppladdningar: {maxConcurrentUpload}, chunkstorlek: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Max samtidiga uppladdningar: {maxConcurrentUpload}, chunked uppladdning inaktiverad" }, "search": { "images": "Bilder", "music": "Musik", "pdf": "PDF", - "pressToSearch": "Tryck på enter för att söka...", "search": "Sök...", - "typeToSearch": "Skriv för att söka....", "types": "Typ", "video": "Video", "path": "Väg:", "smallerThan": "Mindre än:", "largerThan": "Större än:", "helpText1": "Sökningen sker på varje tecken du skriver (minst 3 tecken för sökbegrepp).", - "helpText2": "Indexet: Sökningen använder indexet som automatiskt uppdateras med det inställda intervallet (standard: 5 minuter). Om du söker när programmet precis har startat kan det leda till ofullständiga resultat." + "helpText2": "Indexet: Sökningen använder indexet som automatiskt uppdateras med det inställda intervallet (standard: 5 minuter). Om du söker när programmet precis har startat kan det leda till ofullständiga resultat.", + "noResults": "Inga resultat hittades i indexerad sökning.", + "onlyFolders": "Endast mappar", + "onlyFiles": "Endast filer", + "showOptions": "Visa alternativ", + "photos": "Bilder", + "audio": "Ljud", + "videos": "Videor", + "documents": "Dokument", + "archives": "Arkiv", + "number": "Antal", + "searchContext": "Sök kontext: {context}", + "notEnoughCharacters": "Inte tillräckligt med tecken för att söka (min {minSearchLength})", + "typeToSearch": "Börja skriva {minSearchLength} eller fler tecken för att påbörja sökningen." }, "settings": { "admin": "Admin", @@ -214,10 +227,8 @@ "avoidChanges": "(lämna blankt för att undvika ändringar)", "branding": "Varumärke", "brandingDirectoryPath": "Sökväg till varumärkes katalog", - "brandingHelp": "Du kan skräddarsyr hur din fil hanterar instansen ser ut och känns genom att ändra dess namn, ersätter logo typen, lägga till egna stilar och även inaktivera externa länkar till GitHub.\nMer information om anpassad varumärkes profilering finns i {0}.", "changePassword": "Ändra lösenord", "commandRunner": "Kommando körare", - "commandRunnerHelp": "Här kan du ange kommandon som körs i de namngivna händelserna. Du måste skriva en per rad. Miljövariablerna {0} och {1} kommer att vara tillgängliga, och vara {0} i förhållande till {1}. För mer information om den här funktionen och de tillgängliga miljövariablerna, vänligen läs {2}.", "commandsUpdated": "Kommandon uppdaterade!", "createUserDir": "Auto skapa användarens hemkatalog när du lägger till nya användare", "customStylesheet": "Anpassad formatmall", @@ -300,7 +311,14 @@ "adminOptions": "Alternativ för administratörsanvändare", "shareDuration": "Upphör att gälla", "searchOptions": "Sökalternativ", - "downloads": "Nedladdningar" + "downloads": "Nedladdningar", + "systemAdmin": "System & Administration", + "configViewer": "Konfig-visare", + "configViewerShowFull": "Visa fullständig konfiguration", + "configViewerShowComments": "Visa kommentarer", + "configViewerLoadConfig": "Ladda konfig", + "brandingHelp": "Du kan anpassa hur din File Browser-instans ser ut och känns genom att ändra dess namn, byta ut logotypen, lägga till anpassade stilar och till och med inaktivera externa länkar till GitHub.\nFör mer information om anpassad branding, se {0}.", + "commandRunnerHelp": "Här kan du ställa in kommandon som utförs i de namngivna händelserna. Du måste skriva ett per rad. Miljövariablerna {0} och {1} kommer att vara tillgängliga, {0} i förhållande till {1}. För mer information om denna funktion och de tillgängliga miljövariablerna, läs {2}." }, "sidebar": { "help": "Hjälp", @@ -358,12 +376,21 @@ "hideNavButtons": "Dölj navigeringsknappar", "hideNavButtonsDescription": "Dölj navigeringsknapparna i navigeringsfältet i aktien för att skapa ett minimalistiskt utseende.", "viewModeDescription": "Standardlayout för den delade sidan.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "En aktie har skickats till dig för visning eller nedladdning.", "disableShareCard": "Inaktivera aktiekort", "disableShareCardDescription": "Inaktivera delningskortet på den delade sidan i sidofältet eller högst upp på sidan på mobilen.", "disableSidebar": "Inaktivera sidofält", - "disableSidebarDescription": "Inaktivera sidofältet på den delade sidan." + "disableSidebarDescription": "Inaktivera sidofältet på den delade sidan.", + "enforceDarkLightMode": "Genomför temaläge", + "enforceDarkLightModeDescription": "Tvinga fram ett specifikt temaläge (mörkt eller ljust) för den här aktien, utan hänsyn till användarens preferenser.", + "default": "Inte tvinga fram ett läge", + "dark": "Mörk", + "light": "Ljus", + "enableOnlyOffice": "Aktivera OnlyOffice-visningsprogram", + "enableOnlyOfficeDescription": "Tillåt visning av kontorsfiler med OnlyOffice i denna delning.", + "enableOnlyOfficeEditing": "Aktivera redigering av OnlyOffice", + "enableOnlyOfficeEditingDescription": "Tillåt redigering av kontorsfiler med hjälp av OnlyOffice i denna andel.", + "titleDefault": "Delade filer {title}" }, "api": { "title": "API-nycklar", @@ -478,7 +505,13 @@ "defaultThemeDescription": "default - Standardtema", "customTheme": "Välj ditt tema", "showSelectMultiple": "Visa markera flera i snabbmenyn på skrivbordet", - "showSelectMultipleDescription": "Som standard har snabbmenyn på skrivbordet inte alternativet \"välj flera\" som på mobilen. Det här alternativet visas alltid i snabbmenyn." + "showSelectMultipleDescription": "Som standard har snabbmenyn på skrivbordet inte alternativet \"välj flera\" som på mobilen. Det här alternativet visas alltid i snabbmenyn.", + "defaultMediaPlayer": "Använd den inbyggda mediaspelaren i din webbläsare", + "defaultMediaPlayerDescription": "Använd webbläsarens inbyggda mediaspelare för video- och ljudfiler i stället för FileBrowsers medföljande \"vue-plyr\"-mediaspelare.", + "debugOfficeEditor": "Aktivera OnlyOffice felsökningsläge", + "debugOfficeEditorDescription": "Visa en debug-popup med ytterligare information i OnlyOffice-redigeraren.", + "fileViewerOptions": "Alternativ för filvisare", + "themeAndLanguage": "Tema & språk" }, "general": { "name": "Namn", @@ -513,15 +546,19 @@ "name": "Namn", "accessManagement": "Hantering av åtkomst", "all": "Avvisa alla", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Detta visar vad som händer när en användare försöker komma åt en sökväg där det inte finns några specifika åtkomstregler. \"Allow\" innebär att användare kan komma åt som standard, \"Deny\" innebär att användare blockeras som standard om de inte uttryckligen tillåts." + "defaultBehaviorDescription": "Detta visar vad som händer när en användare försöker komma åt en sökväg där det inte finns några specifika åtkomstregler. \"Allow\" innebär att användare kan komma åt som standard, \"Deny\" innebär att användare blockeras som standard om de inte uttryckligen tillåts.", + "defaultBehavior": "Standardåtgärd{suffix}" }, "editor": { "uninitialized": "Redigeraren kunde inte initieras. Vänligen ladda om.", "saveFailed": "Misslyckades med att spara filen. Vänligen försök igen.", "saveAborted": "Spara avbröts. Filnamnet stämmer inte överens med den aktiva filen.", "saveDisabled": "Sparande av delade filer stöds inte.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Spara åtgärden avbröts. Programmets aktiva fil ({activeFile}) stämmer inte överens med den fil du försöker spara ({tryingToSave}).", + "syncFailed": "lyckades inte synkronisera tillstånd med rutten för \"{filename}\" efter 5 försök. Avbryter redigeringsinställningen för att förhindra datakorruption." + }, + "player": { + "LoopEnabled": "Slinga aktiverad", + "LoopDisabled": "Slinga avaktiverad" } } diff --git a/frontend/src/i18n/tr.json b/frontend/src/i18n/tr.json index ff9fe6746..30336eb82 100644 --- a/frontend/src/i18n/tr.json +++ b/frontend/src/i18n/tr.json @@ -49,7 +49,8 @@ "clearCompleted": "Temizleme tamamlandı", "showMore": "Daha fazla göster", "showLess": "Daha az göster", - "openParentFolder": "Üst klasörü açın" + "openParentFolder": "Üst klasörü açın", + "selectedCount": "Bir eylemi gerçekleştirmek için seçilen öğe sayısı" }, "download": { "downloadFile": "Dosyayı indir", @@ -84,15 +85,15 @@ "click": "dosya veya klasör seçin", "ctrl": { "click": "çoklu dosya ve klasör seçin", - "f": "Aramayı aç", - "s": "bir dosyayı kaydedin veya bulunduğunuz dizini indirin" + "d": "seçili öğeleri indirin" }, "del": "seçilileri sil", - "doubleClick": "dosya veya dizini açın", "esc": "seçimi temizle veya kapatın", - "f1": "bu bilgi", "f2": "dosyayı yeniden adlandır", - "help": "Yardım" + "help": "Yardım", + "f1": "yardım istemini göster", + "description": "Temel navigasyon seçeneklerini aşağıda görüntüleyebilirsiniz. Ek bilgi için lütfen şu adresi ziyaret edin", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -134,16 +135,13 @@ "prompts": { "copy": "Kopyala", "copyMessage": "Dosyalarınızı kopyalayacağınız yeri seçin:", - "deleteMessageMultiple": "{count} dosyayı/dosyaları silmek istediğinizden emin misiniz?", "deleteMessageSingle": "Bu dosyayı/klasörü silmek istediğinizden emin misiniz?", - "deleteMessageShare": "Bu paylaşımı({path}) silmek istediğinizden emin misiniz?", "deleteTitle": "Dosyaları sil", "displayName": "Görünen Ad:", "download": "Dosyaları indirŞ", "downloadMessage": "İndirmek istediğiniz formatı seçin.", "error": "Bir şeyler yanlış gitti", "fileInfo": "Dosya bilgisi", - "lastModified": "Son güncellenme{suffix}", "move": "Taşı", "moveMessage": "Dosya(lar)ınız/klasör(ler)iniz için yeni ana dizin seçin:", "newArchetype": "Bir prototip temelinde yeni bir gönderi oluşturun. Dosyanız içerik klasöründe oluşturulacaktır.", @@ -151,10 +149,7 @@ "newDirMessage": "Yeni dizinin adını yazın.", "newFile": "Yeni dosya", "newFileMessage": "Yeni dosyanın adını yazın.", - "numberDirs": "Dizin sayısı{suffix}", - "numberFiles": "Dosya sayısı{suffix}", "rename": "Yeniden adlandır", - "renameMessage": "için yeni bir ad girin", "replace": "Değiştir", "replaceMessage": "Yüklemeye çalıştığınız dosyalardan biri, adı nedeniyle çakışıyor. Mevcut olanı değiştirmek istiyor musunuz?\n", "schedule": "Planla", @@ -172,10 +167,6 @@ "dragAndDrop": "Dosyaları buraya sürükleyip bırakın", "completed": "Tamamlandı", "conflictsDetected": "Tespit edilen çatışmalar", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Geçersiz ad sağlandı: \"/\" veya \"\\\" kapsayıcı olamaz", - "source": "Source{suffix}", "downloadsLimit": "İndirme sayısı sınırı", "maxBandwidth": "Maksimum indirme bant genişliği (kbps)", "shareTheme": "Temayı paylaş", @@ -186,22 +177,44 @@ "shareBanner": "Banner URL'sini paylaş", "shareFavicon": "Favicon URL'sini paylaş", "optionalPasswordHelp": "Ayarlanırsa, kullanıcılar paylaşılan içeriği görüntülemek için bu parolayı girmelidir.", - "viewMode": "Görünüm modu" + "viewMode": "Görünüm modu", + "renameMessage": "Yeni bir ad girin:", + "renameMessageInvalid": "Bir dosya \"/\" veya \"\\\" içeremez", + "source": "Kaynak{suffix}", + "deleteMessageMultiple": "{count} dosya(lar)ını silmek istediğinizden emin misiniz?", + "deleteMessageShare": "Bu paylaşımı silmek istediğinizden emin misiniz? {path} ?", + "lastModified": "Son Değişiklik{suffix}", + "numberDirs": "Dizin sayısı{suffix}", + "numberFiles": "Dosya sayısı{suffix}", + "renameMessageConflict": "\"{filename}\" adında bir öğe zaten var!", + "uploadSettingsChunked": "Maksimum eşzamanlı yükleme: {maxConcurrentUpload}, yığın boyutu: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "Maksimum eşzamanlı yükleme: {maxConcurrentUpload}, yığınlanmış yükleme devre dışı" }, "search": { "images": "Görseller", "music": "Müzik", "pdf": "PDF", - "pressToSearch": "Aramak için enter'a basın...", "search": "Ara...", - "typeToSearch": "Aramak için yazın...", "types": "Türler", "video": "Video", "path": "Yol:", "smallerThan": "Daha küçük:", "largerThan": "Daha büyük:", "helpText1": "Arama, yazdığınız her karakterde gerçekleşir (arama terimleri için minimum 3 karakter).", - "helpText2": "Dizin: Arama, yapılandırılan aralıkta (varsayılan: 5 dakika) otomatik olarak güncellenen dizini kullanır. Program yeni başladığında arama yapmak eksik sonuçlara neden olabilir." + "helpText2": "Dizin: Arama, yapılandırılan aralıkta (varsayılan: 5 dakika) otomatik olarak güncellenen dizini kullanır. Program yeni başladığında arama yapmak eksik sonuçlara neden olabilir.", + "noResults": "Dizinlenmiş aramada sonuç bulunamadı.", + "onlyFolders": "Yalnızca Klasörler", + "onlyFiles": "Sadece Dosyalar", + "showOptions": "Seçenekleri Göster", + "photos": "Fotoğraflar", + "audio": "Ses", + "videos": "Videolar", + "documents": "Belgeler", + "archives": "Arşivler", + "number": "Sayı", + "searchContext": "Arama Bağlamı: {context}", + "notEnoughCharacters": "Arama yapmak için yeterli karakter yok (min {minSearchLength})", + "typeToSearch": "Aramaya başlamak için {minSearchLength} veya daha fazla karakter yazmaya başlayın." }, "settings": { "admin": "Yönetim", @@ -214,10 +227,8 @@ "avoidChanges": "(değişiklikleri önlemek için boş bırakın)", "branding": "Marka", "brandingDirectoryPath": "Marka dizin yolu", - "brandingHelp": "Adını değiştirerek, logoyu değiştirerek, özel stiller ekleyerek ve hatta GitHub'a harici bağlantıları devre dışı bırakarak FileBrowser örneğinizin görünüşünü ve hissini özelleştirebilirsiniz.\nÖzel marka bilinci oluşturma hakkında daha fazla bilgi için lütfen {0} sayfasına göz atın.", "changePassword": "Şifre Değiştir", "commandRunner": "Komut satırı", - "commandRunnerHelp": "Burada, adlandırılmış olaylarda yürütülen komutları ayarlayabilirsiniz. Her satıra bir tane yazmalısınız. {0} ve {1} ortam değişkenleri, {1}'ye göre {0} olacak şekilde kullanılabilir olacaktır. Bu özellik ve mevcut ortam değişkenleri hakkında daha fazla bilgi için lütfen {2}'yi okuyun.", "commandsUpdated": "Komutlar güncellendi!", "createUserDir": "Kullanıcı eklerken, kullanıcı ana dizinini otomatik oluştur", "customStylesheet": "Özel CSS", @@ -300,7 +311,14 @@ "adminOptions": "Yönetici kullanıcı seçenekleri", "shareDuration": "Sona erdi", "searchOptions": "Arama seçenekleri", - "downloads": "İndirmeler" + "downloads": "İndirmeler", + "systemAdmin": "Sistem ve Yönetici", + "configViewer": "Konfigürasyon Görüntüleyici", + "configViewerShowFull": "Tam yapılandırmayı göster", + "configViewerShowComments": "Yorumları göster", + "configViewerLoadConfig": "Yük Konfigürasyonu", + "brandingHelp": "Adını değiştirerek, logoyu değiştirerek, özel stiller ekleyerek ve hatta GitHub'a harici bağlantıları devre dışı bırakarak Dosya Tarayıcı örneğinizin nasıl göründüğünü ve hissettirdiğini özelleştirebilirsiniz.\nÖzel markalama hakkında daha fazla bilgi için lütfen {0} adresine göz atın.", + "commandRunnerHelp": "Burada, adlandırılmış olaylarda yürütülecek komutları ayarlayabilirsiniz. Her satıra bir tane yazmalısınız. {0} ve {1} ortam değişkenleri kullanılabilir olacaktır, {0} ise {1} ile ilişkilidir. Bu özellik ve mevcut ortam değişkenleri hakkında daha fazla bilgi için lütfen {2} adresini okuyun." }, "sidebar": { "help": "Yardım", @@ -358,12 +376,21 @@ "hideNavButtons": "Gezinme düğmelerini gizle", "hideNavButtonsDescription": "Minimalist bir görünüm oluşturmak için paylaşımdaki gezinme çubuğundaki gezinme düğmelerini gizleyin.", "viewModeDescription": "Paylaşılan sayfa için varsayılan düzen.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Görüntülemeniz veya indirmeniz için size bir paylaşım gönderildi.", "disableShareCard": "Paylaşım kartını devre dışı bırak", "disableShareCardDescription": "Kenar çubuğunda paylaşılan sayfadaki veya mobil cihazlarda sayfanın üst kısmındaki paylaşım kartını devre dışı bırakın.", "disableSidebar": "Kenar çubuğunu devre dışı bırak", - "disableSidebarDescription": "Paylaşılan sayfadaki kenar çubuğunu devre dışı bırakın." + "disableSidebarDescription": "Paylaşılan sayfadaki kenar çubuğunu devre dışı bırakın.", + "enforceDarkLightMode": "Tema modunu zorla", + "enforceDarkLightModeDescription": "Kullanıcı tercihlerini geçersiz kılarak bu paylaşım için belirli bir tema modunu (koyu veya açık) zorlayın.", + "default": "Bir modu zorlamayın", + "dark": "Karanlık", + "light": "Işık", + "enableOnlyOffice": "OnlyOffice görüntüleyiciyi etkinleştirin", + "enableOnlyOfficeDescription": "Bu paylaşımda OnlyOffice kullanarak ofis dosyalarının görüntülenmesine izin verin.", + "enableOnlyOfficeEditing": "OnlyOffice düzenlemesini etkinleştirin", + "enableOnlyOfficeEditingDescription": "Bu paylaşımda OnlyOffice kullanarak ofis dosyalarının düzenlenmesine izin verin.", + "titleDefault": "Paylaşılan dosyalar - {title}" }, "api": { "title": "API Anahtarları:", @@ -447,8 +474,8 @@ "totalDenied": "Reddedildi", "totalAllowed": "İzin verildi", "all": "Tümünü Reddet", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Bu, bir kullanıcı belirli erişim kurallarının bulunmadığı bir yola erişmeye çalıştığında ne olacağını gösterir. 'Allow' kullanıcıların varsayılan olarak erişebileceği anlamına gelir, 'Deny' ise açıkça izin verilmediği sürece kullanıcıların varsayılan olarak engellendiği anlamına gelir." + "defaultBehaviorDescription": "Bu, bir kullanıcı belirli erişim kurallarının bulunmadığı bir yola erişmeye çalıştığında ne olacağını gösterir. 'Allow' kullanıcıların varsayılan olarak erişebileceği anlamına gelir, 'Deny' ise açıkça izin verilmediği sürece kullanıcıların varsayılan olarak engellendiği anlamına gelir.", + "defaultBehavior": "Varsayılan eylem{suffix}" }, "fileLoading": { "title": "Yüklemeler & İndirmeler", @@ -514,14 +541,24 @@ "defaultThemeDescription": "default - Varsayılan tema", "customTheme": "Temanızı seçin", "showSelectMultiple": "Masaüstündeki bağlam menüsünde çoklu seçmeyi göster", - "showSelectMultipleDescription": "Varsayılan olarak, masaüstü içerik menüsünde mobil cihazlardaki gibi \"çoklu seç\" seçeneği yoktur. Bu seçenek her zaman içerik menüsünde gösterilir." + "showSelectMultipleDescription": "Varsayılan olarak, masaüstü içerik menüsünde mobil cihazlardaki gibi \"çoklu seç\" seçeneği yoktur. Bu seçenek her zaman içerik menüsünde gösterilir.", + "defaultMediaPlayer": "Tarayıcınızın yerel medya oynatıcısını kullanın", + "defaultMediaPlayerDescription": "Video ve ses dosyaları için FileBrowser'ın dahil ettiği 'vue-plyr' medya oynatıcısı yerine tarayıcınızın yerel medya oynatıcısını kullanın.", + "debugOfficeEditor": "OnlyOffice Hata Ayıklama Modunu Etkinleştir", + "debugOfficeEditorDescription": "OnlyOffice düzenleyicisinde ek bilgiler içeren bir hata ayıklama açılır penceresi gösterin.", + "fileViewerOptions": "Dosya Görüntüleyici Seçenekleri", + "themeAndLanguage": "Tema ve Dil" }, "editor": { "uninitialized": "Düzenleyici başlatılamadı. Lütfen yeniden yükleyin.", "saveFailed": "Dosya kaydedilemedi. Lütfen tekrar deneyin.", "saveAborted": "Kaydetme işlemi iptal edildi. Dosya adı etkin dosyayla eşleşmiyor.", "saveDisabled": "Paylaşılan dosyaların kaydedilmesi desteklenmez.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Kaydetme işlemi iptal edildi. Uygulamanın etkin dosyası ({activeFile}) kaydetmeye çalıştığınız dosyayla ({tryingToSave}) eşleşmiyor.", + "syncFailed": "\"{filename}\" için 5 denemeden sonra rota ile durum senkronize edilemedi. Veri bozulmasını önlemek için düzenleyici kurulumu iptal ediliyor." + }, + "player": { + "LoopEnabled": "Döngü Etkin", + "LoopDisabled": "Döngü Devre Dışı" } } diff --git a/frontend/src/i18n/ua.json b/frontend/src/i18n/ua.json index a7bad549f..9338f8fa8 100644 --- a/frontend/src/i18n/ua.json +++ b/frontend/src/i18n/ua.json @@ -49,7 +49,8 @@ "clearCompleted": "Очищення завершено", "showMore": "Показати більше", "showLess": "Показати менше", - "openParentFolder": "Відкрийте батьківську папку" + "openParentFolder": "Відкрийте батьківську папку", + "selectedCount": "Кількість елементів, вибраних для виконання дії" }, "download": { "downloadFile": "Завантажити файл", @@ -84,15 +85,15 @@ "click": "вибрати файл чи каталог", "ctrl": { "click": "вибрати кілька файлів чи каталогів", - "f": "відкрити пошук", - "s": "скачати файл або поточний каталог" + "d": "завантажити вибрані елементи" }, "del": "видалити вибрані елементи", - "doubleClick": "відкрити файл чи каталог", "esc": "очистити виділення та/або закрити вікно", - "f1": "допомога", "f2": "перейменувати файл", - "help": "Допомога" + "help": "Допомога", + "f1": "показати запит на допомогу", + "description": "Нижче ви можете переглянути основні параметри навігації. Для отримання додаткової інформації, будь ласка, відвідайте", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -134,16 +135,13 @@ "prompts": { "copy": "Копіювати", "copyMessage": "Копіювати в:", - "deleteMessageMultiple": "Видалити ці файли ({count})?", "deleteMessageSingle": "Видалити цей файл/каталог?", - "deleteMessageShare": "Видалити цей спільний файл/каталог ({path})?", "deleteTitle": "Видалити файлы", "displayName": "Відображене ім'я:", "download": "Завантажити файлы", "downloadMessage": "Виберіть формат, в якому хочете завантажити.", "error": "Помилка", "fileInfo": "Інформація про файл", - "lastModified": "Останній раз змінено{suffix}", "move": "Перемістити", "moveMessage": "Перемістити в:", "newArchetype": "Створіть новий запис на основі архетипу. Файл буде створено у каталозі.", @@ -151,10 +149,7 @@ "newDirMessage": "Ім'я нового каталогу.", "newFile": "Новий файл", "newFileMessage": "Ім'я нового файлу.", - "numberDirs": "Кількість каталогів{suffix}", - "numberFiles": "Кількість файлів{suffix}", "rename": "Перейменувати", - "renameMessage": "Нове ім'я", "replace": "Замінити", "replaceMessage": "Ім'я одного з файлів, що завантажуються, збігається з вже існуючим файлом. Ви бажаєте замінити існуючий?\n", "schedule": "Планування", @@ -172,10 +167,6 @@ "dragAndDrop": "Перетягніть файли сюди", "completed": "Завершено", "conflictsDetected": "Виявлені конфлікти", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "Вказано невірне ім'я: не можна використовувати символи \"/\" або \"\\\"", - "source": "Source{suffix}", "downloadsLimit": "Обмеження на кількість завантажень", "maxBandwidth": "Максимальна швидкість завантаження (кбіт/с)", "shareTheme": "Поділіться темою", @@ -186,22 +177,44 @@ "shareBanner": "Поділіться URL банера", "shareFavicon": "Поділитися URL-адресою іконки", "optionalPasswordHelp": "Якщо його встановлено, користувачі повинні ввести цей пароль, щоб переглянути спільний вміст.", - "viewMode": "Режим перегляду" + "viewMode": "Режим перегляду", + "renameMessage": "Введіть нове ім'я:", + "renameMessageInvalid": "Файл не може містити символи \"/\" або \"\\\"", + "source": "Джерело{suffix}", + "deleteMessageMultiple": "Ви дійсно хочете видалити файл(и) {count}?", + "deleteMessageShare": "Ви впевнені, що хочете видалити цей ресурс? {path} ?", + "lastModified": "Останній раз змінено{suffix}", + "numberDirs": "Кількість каталогів{suffix}", + "numberFiles": "Кількість файлів{suffix}", + "renameMessageConflict": "Елемент з назвою \"{filename}\" вже існує!", + "uploadSettingsChunked": "Максимальна кількість одночасних завантажень: {maxConcurrentUpload}, розмір фрагмента: {uploadChunkSizeMb} МБ", + "uploadSettingsNoChunk": "Максимальне одночасне завантаження: {maxConcurrentUpload}, chunked upload вимкнено" }, "search": { "images": "Зображення", "music": "Музика", "pdf": "PDF", - "pressToSearch": "Натисніть ENTER для пошуку", "search": "Пошук...", - "typeToSearch": "Введіть ім'я файлу...", "types": "Типи", "video": "Відео", "path": "Шлях:", "smallerThan": "Менше, ніж:", "largerThan": "Більше, ніж:", "helpText1": "Пошук відбувається за кожним введеним символом (мінімум 3 символи для пошукових термінів).", - "helpText2": "Індекс: Пошук використовує індекс, який автоматично оновлюється з заданим інтервалом (за замовчуванням: 5 хвилин). Пошук, коли програма щойно запущена, може призвести до неповних результатів." + "helpText2": "Індекс: Пошук використовує індекс, який автоматично оновлюється з заданим інтервалом (за замовчуванням: 5 хвилин). Пошук, коли програма щойно запущена, може призвести до неповних результатів.", + "noResults": "В індексованому пошуку результатів не знайдено.", + "onlyFolders": "Тільки папки", + "onlyFiles": "Тільки файли", + "showOptions": "Показати параметри", + "photos": "Фотографії", + "audio": "Аудіо", + "videos": "Відео", + "documents": "Документи", + "archives": "Архіви", + "number": "Номер", + "searchContext": "Пошуковий контекст: {context}", + "notEnoughCharacters": "Недостатньо символів для пошуку (мінімум {minSearchLength})", + "typeToSearch": "Почніть вводити {minSearchLength} або більше символів, щоб почати пошук." }, "settings": { "admin": "Адмін", @@ -214,10 +227,8 @@ "avoidChanges": "(залишіть поле порожнім, щоб уникнути змін)", "branding": "Брендинг", "brandingDirectoryPath": "Шлях до каталогу брендів", - "brandingHelp": "Ви можете налаштувати зовнішній вигляд файлового браузера, змінивши його ім'я, замінивши логотип, додавши власні стилі та навіть відключивши зовнішні посилання на GitHub.\nДодаткову інформацію про персоналізований брендинг можна знайти на сторінці {0}.", "changePassword": "Зміна пароля", "commandRunner": "Запуск команд", - "commandRunnerHelp": "Тут ви можете встановити команди, які будуть виконуватися у зазначених подіях. Ви повинні вказати по одній команді в кожному рядку. Змінні середовища {0} та {1} будуть доступні, будучи {0} щодо {1}. Додаткові відомості про цю функцію та доступні змінні середовища див. у {2}.", "commandsUpdated": "Команди оновлені!", "createUserDir": "Автоматичне створення домашнього каталогу користувача при додаванні нового користувача", "customStylesheet": "Свій стиль", @@ -300,7 +311,14 @@ "adminOptions": "Параметри користувача адміністрування", "shareDuration": "Закінчується", "searchOptions": "Параметри пошуку", - "downloads": "Завантаження" + "downloads": "Завантаження", + "systemAdmin": "Система та адміністрування", + "configViewer": "Переглядач конфігурацій", + "configViewerShowFull": "Показати повну конфігурацію", + "configViewerShowComments": "Показати коментарі", + "configViewerLoadConfig": "Завантажити конфігурацію", + "brandingHelp": "Ви можете налаштувати зовнішній вигляд вашого екземпляра File Browser, змінивши його назву, замінивши логотип, додавши власні стилі і навіть вимкнувши зовнішні посилання на GitHub.\nЩоб дізнатися більше про кастомне брендування, будь ласка, відвідайте сторінку {0}.", + "commandRunnerHelp": "Тут ви можете задати команди, які будуть виконуватися у названих подіях. Ви повинні писати по одній у рядку. Змінні оточення {0} і {1} будуть доступні, будучи {0} відносно {1}. Для отримання додаткової інформації про цю можливість і доступні змінні оточення, будь ласка, прочитайте {2}." }, "sidebar": { "help": "Допомога", @@ -358,12 +376,21 @@ "hideNavButtons": "Приховати кнопки навігації", "hideNavButtonsDescription": "Приховайте кнопки навігації на панелі навігації у спільному доступі, щоб створити мінімалістичний вигляд.", "viewModeDescription": "Макет за замовчуванням для спільної сторінки.", - "titleDefault": "Shared files - {title}", "descriptionDefault": "Вам надіслано файл для перегляду або завантаження.", "disableShareCard": "Вимкнути картку спільного доступу", "disableShareCardDescription": "Вимкніть картку спільного доступу на сторінці, до якої надається спільний доступ, на бічній панелі або вгорі сторінки на мобільних пристроях.", "disableSidebar": "Вимкнути бічну панель", - "disableSidebarDescription": "Вимкнути бічну панель на спільній сторінці." + "disableSidebarDescription": "Вимкнути бічну панель на спільній сторінці.", + "enforceDarkLightMode": "Увімкнути режим теми", + "enforceDarkLightModeDescription": "Примусово встановлює певний режим теми (темний або світлий) для цього ресурсу, перекриваючи налаштування користувача.", + "default": "Не застосовуйте примусовий режим", + "dark": "Темнота.", + "light": "Світло", + "enableOnlyOffice": "Увімкнути переглядач OnlyOffice", + "enableOnlyOfficeDescription": "Дозволити перегляд офісних файлів за допомогою OnlyOffice у цьому ресурсі.", + "enableOnlyOfficeEditing": "Увімкнути редагування OnlyOffice", + "enableOnlyOfficeEditingDescription": "Дозволити редагування офісних файлів за допомогою OnlyOffice у цьому ресурсі.", + "titleDefault": "Спільні файли - {title}" }, "api": { "title": "Ключі API", @@ -478,7 +505,13 @@ "defaultThemeDescription": "default - тема за замовчуванням", "customTheme": "Виберіть свою тему", "showSelectMultiple": "Показувати вибране кілька в контекстному меню на робочому столі", - "showSelectMultipleDescription": "За замовчуванням, контекстне меню десктопа не має опції \"вибрати декілька\", як у мобільному. Ця опція завжди відображається у контекстному меню." + "showSelectMultipleDescription": "За замовчуванням, контекстне меню десктопа не має опції \"вибрати декілька\", як у мобільному. Ця опція завжди відображається у контекстному меню.", + "defaultMediaPlayer": "Використовуйте вбудований медіаплеєр вашого браузера", + "defaultMediaPlayerDescription": "Використовуйте власний медіаплеєр вашого браузера для відео- та аудіофайлів, а не медіаплеєр \"vue-plyr\", що входить до складу FileBrowser.", + "debugOfficeEditor": "Увімкнути режим налагодження OnlyOffice", + "debugOfficeEditorDescription": "Показати налагоджувальне спливаюче вікно з додатковою інформацією в редакторі OnlyOffice.", + "fileViewerOptions": "Параметри перегляду файлів", + "themeAndLanguage": "Тема та мова" }, "general": { "name": "Ім'я", @@ -513,15 +546,19 @@ "name": "Ім'я", "accessManagement": "Керування доступом", "all": "Заперечувати все.", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "Тут показано, що відбувається, коли користувач намагається отримати доступ до шляху, для якого не існує певних правил доступу. \"Дозволити\" означає, що користувачі можуть отримати доступ за замовчуванням, \"Заборонити\" означає, що користувачі за замовчуванням заблоковані, якщо тільки це не дозволено явно." + "defaultBehaviorDescription": "Тут показано, що відбувається, коли користувач намагається отримати доступ до шляху, для якого не існує певних правил доступу. \"Дозволити\" означає, що користувачі можуть отримати доступ за замовчуванням, \"Заборонити\" означає, що користувачі за замовчуванням заблоковані, якщо тільки це не дозволено явно.", + "defaultBehavior": "Дія за замовчуванням{suffix}" }, "editor": { "uninitialized": "Не вдалося ініціалізувати редактор. Будь ласка, перезавантажте.", "saveFailed": "Не вдалося зберегти файл. Спробуйте ще раз.", "saveAborted": "Збереження перервано. Ім'я файлу не збігається з активним файлом.", "saveDisabled": "Збереження спільних файлів не підтримується.", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "Операцію збереження перервано. Активний файл програми ({activeFile}) не відповідає файлу, який ви намагаєтеся зберегти ({tryingToSave}).", + "syncFailed": "не вдалося синхронізувати стан з маршрутом для \"{filename}\" після 5 спроб. Переривання налаштування редактора для запобігання пошкодженню даних." + }, + "player": { + "LoopEnabled": "Цикл увімкнено", + "LoopDisabled": "Цикл вимкнено" } } diff --git a/frontend/src/i18n/zh-cn.json b/frontend/src/i18n/zh-cn.json index 592f5fbcb..9d302a248 100644 --- a/frontend/src/i18n/zh-cn.json +++ b/frontend/src/i18n/zh-cn.json @@ -49,7 +49,8 @@ "clearCompleted": "清除完毕", "showMore": "显示更多", "showLess": "显示更少", - "openParentFolder": "打开父文件夹" + "openParentFolder": "打开父文件夹", + "selectedCount": "选择执行操作的项目数" }, "download": { "downloadFile": "下载文件", @@ -84,15 +85,15 @@ "click": "选择文件或目录", "ctrl": { "click": "选择多个文件或目录", - "f": "打开搜索框", - "s": "保存文件或下载当前文件夹" + "d": "下载所选项目" }, "del": "删除所选的文件/文件夹", - "doubleClick": "打开文件/文件夹", "esc": "清除已选项或关闭提示信息", - "f1": "显示该帮助信息", "f2": "重命名文件/文件夹", - "help": "帮助" + "help": "帮助", + "f1": "显示帮助提示", + "description": "您可以查看下面的基本导航选项。有关其他信息,请访问", + "wiki": "文件浏览器量子维基" }, "languages": { "he": "עברית", @@ -134,16 +135,13 @@ "prompts": { "copy": "复制", "copyMessage": "请选择欲复制至的目录:", - "deleteMessageMultiple": "你确定要删除这 {count} 个文件吗?", "deleteMessageSingle": "你确定要删除这个文件/文件夹吗?", - "deleteMessageShare": "你确定要删除这个分享({path})吗?", "deleteTitle": "删除文件", "displayName": "名称:", "download": "下载文件", "downloadMessage": "请选择要下载的压缩格式。", "error": "出了一点问题...", "fileInfo": "文件信息", - "lastModified": "最后修改{suffix}", "move": "移动", "moveMessage": "请选择欲移动至的目录:", "newArchetype": "创建一个基于原型的新帖子。您的文件将会创建在内容文件夹中。", @@ -151,10 +149,7 @@ "newDirMessage": "请输入新目录的名称。", "newFile": "新建文件", "newFileMessage": "请输入新文件的名称。", - "numberDirs": "目录数{suffix}", - "numberFiles": "文件数{suffix}", "rename": "重命名", - "renameMessage": "请输入新名称,旧名称为:", "replace": "替换", "replaceMessage": "您尝试上传的文件中有一个与现有文件的名称存在冲突。是否替换现有的同名文件?\n", "schedule": "计划", @@ -172,10 +167,6 @@ "dragAndDrop": "在此处拖放文件", "completed": "已完成", "conflictsDetected": "检测到冲突", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "提供的名称无效:不能包含\"/\"或\"\\\"。", - "source": "Source{suffix}", "downloadsLimit": "下载次数限制", "maxBandwidth": "最大下载带宽(千字节/秒)", "shareTheme": "分享主题", @@ -186,22 +177,44 @@ "shareBanner": "共享横幅 URL", "shareFavicon": "共享图标 URL", "optionalPasswordHelp": "如果设置了该密码,用户必须输入该密码才能查看共享内容。", - "viewMode": "查看模式" + "viewMode": "查看模式", + "renameMessage": "输入新名称:", + "renameMessageInvalid": "文件不能包含\"/\"或\"\\\"。", + "source": "资料来源{suffix}", + "deleteMessageMultiple": "您确定要删除{count} 文件吗?", + "deleteMessageShare": "您确定要删除此共享? {path} ?", + "lastModified": "最后修改{suffix}", + "numberDirs": "目录数量{suffix}", + "numberFiles": "文件数量{suffix}", + "renameMessageConflict": "名为 \"{filename}\" 的项目已经存在!", + "uploadSettingsChunked": "最大并发上传:{maxConcurrentUpload}块大小 {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "最大并发上传:{maxConcurrentUpload}已禁用分块上传" }, "search": { "images": "图像", "music": "音乐", "pdf": "PDF", - "pressToSearch": "输入回车以搜索...", "search": "搜索...", - "typeToSearch": "输入以搜索...", "types": "类型", "video": "视频", "path": "路径", "smallerThan": "小于", "largerThan": "大于:", "helpText1": "您键入的每个字符都会进行搜索(搜索词最少 3 个字符)。", - "helpText2": "索引:利用索引进行搜索,索引会根据配置的时间间隔(默认为 5 分钟)自动更新。程序刚启动时进行搜索可能会导致搜索结果不完整。" + "helpText2": "索引:利用索引进行搜索,索引会根据配置的时间间隔(默认为 5 分钟)自动更新。程序刚启动时进行搜索可能会导致搜索结果不完整。", + "noResults": "索引搜索未找到结果。", + "onlyFolders": "仅文件夹", + "onlyFiles": "仅文件", + "showOptions": "显示选项", + "photos": "照片", + "audio": "音频", + "videos": "视频", + "documents": "文件", + "archives": "档案", + "number": "数量", + "searchContext": "搜索上下文:{context}", + "notEnoughCharacters": "搜索字符数不足(最少{minSearchLength})", + "typeToSearch": "开始键入{minSearchLength} 或更多字符开始搜索。" }, "settings": { "scopes": "定义允许的来源及其中的用户范围", @@ -215,10 +228,8 @@ "avoidChanges": "(留空以避免更改)", "branding": "品牌", "brandingDirectoryPath": "品牌信息文件夹路径", - "brandingHelp": "您可以通过改变实例名称,更换 Logo,加入自定义样式,甚至禁用到 Github 的外部链接来自定义 File Browser 的外观和感觉。\n想获得更多信息,请查看 {0}。", "changePassword": "更改密码", "commandRunner": "命令执行器", - "commandRunnerHelp": "你可以在此设置在下列事件中执行的命令。每行必须写一条命令。可以在命令中使用环境变量 {0} 和 {1},使 {0} 与 {1} 相关联。关于此功能和可用环境变量的更多信息,请阅读 {2}。", "commandsUpdated": "命令已更新!", "createUserDir": "在添加新用户的同时自动创建用户的个人目录", "customStylesheet": "自定义样式表(CSS)", @@ -300,7 +311,14 @@ "adminOptions": "管理员用户选项", "shareDuration": "到期", "searchOptions": "搜索选项", - "downloads": "下载" + "downloads": "下载", + "systemAdmin": "系统与管理", + "configViewer": "配置查看器", + "configViewerShowFull": "显示完整配置", + "configViewerShowComments": "显示评论", + "configViewerLoadConfig": "加载配置", + "brandingHelp": "您可以更改文件浏览器实例的名称、更换徽标、添加自定义样式,甚至禁用指向 GitHub 的外部链接,从而自定义文件浏览器实例的外观和感觉。\n有关自定义品牌的更多信息,请查阅{0} 。", + "commandRunnerHelp": "您可以在此设置在命名事件中执行的命令。每行必须写一条。环境变量{0} 和{1} 将可用,{0} 与{1} 相对。有关此功能和可用环境变量的更多信息,请阅读{2} 。" }, "sidebar": { "help": "帮助", @@ -358,12 +376,21 @@ "hideNavButtons": "隐藏导航按钮", "hideNavButtonsDescription": "隐藏共享导航栏上的导航按钮,打造简约外观。", "viewModeDescription": "共享页面的默认布局。", - "titleDefault": "Shared files - {title}", "descriptionDefault": "您已收到一份共享文件,供您查看或下载。", "disableShareCard": "禁用共享卡", "disableShareCardDescription": "禁用共享页面侧边栏或手机页面顶部的共享卡。", "disableSidebar": "禁用侧边栏", - "disableSidebarDescription": "禁用共享页面上的边栏。" + "disableSidebarDescription": "禁用共享页面上的边栏。", + "enforceDarkLightMode": "执行主题模式", + "enforceDarkLightModeDescription": "为该共享强制设置特定主题模式(深色或浅色),覆盖用户偏好。", + "default": "不执行模式", + "dark": "黑暗", + "light": "灯光", + "enableOnlyOffice": "启用 OnlyOffice 查看器", + "enableOnlyOfficeDescription": "允许在此共享中使用 OnlyOffice 查看办公文件。", + "enableOnlyOfficeEditing": "启用 OnlyOffice 编辑功能", + "enableOnlyOfficeEditingDescription": "允许在此共享中使用 OnlyOffice 编辑办公文件。", + "titleDefault": "共享文件{title}" }, "api": { "title": "应用程序接口密钥", @@ -478,7 +505,13 @@ "defaultThemeDescription": "default - 默认主题", "customTheme": "选择主题", "showSelectMultiple": "在桌面右键菜单中显示多个选择", - "showSelectMultipleDescription": "默认情况下,桌面右键菜单不像手机那样有 \"选择多个 \"选项。该选项始终显示在右键菜单中。" + "showSelectMultipleDescription": "默认情况下,桌面右键菜单不像手机那样有 \"选择多个 \"选项。该选项始终显示在右键菜单中。", + "defaultMediaPlayer": "使用浏览器的本地媒体播放器", + "defaultMediaPlayerDescription": "使用浏览器的本地媒体播放器播放视频和音频文件,而不是 FileBrowser 附带的 \"vue-plyr \"媒体播放器。", + "debugOfficeEditor": "启用 OnlyOffice 调试模式", + "debugOfficeEditorDescription": "在 OnlyOffice 编辑器中显示包含附加信息的调试弹出窗口。", + "fileViewerOptions": "文件查看器选项", + "themeAndLanguage": "主题与语言" }, "general": { "name": "名称", @@ -513,15 +546,19 @@ "name": "名称", "accessManagement": "访问管理", "all": "全部拒绝", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "这显示了当用户试图访问没有特定访问规则的路径时会发生什么。允许 \"表示默认情况下用户可以访问,\"拒绝 \"表示除非明确允许,否则默认情况下用户将被阻止。" + "defaultBehaviorDescription": "这显示了当用户试图访问没有特定访问规则的路径时会发生什么。允许 \"表示默认情况下用户可以访问,\"拒绝 \"表示除非明确允许,否则默认情况下用户将被阻止。", + "defaultBehavior": "默认操作{suffix}" }, "editor": { "uninitialized": "编辑器初始化失败。请重新加载。", "saveFailed": "保存文件失败。请重试。", "saveAborted": "保存失败。文件名与活动文件不匹配。", "saveDisabled": "不支持保存共享文件。", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "保存操作中止。应用程序的活动文件 ({activeFile}) 与您要保存的文件 ({tryingToSave}) 不匹配。", + "syncFailed": "经过 5 次尝试后,未能将 \"{filename}\" 的状态与路由同步。终止编辑器设置,以防数据损坏。" + }, + "player": { + "LoopEnabled": "启用环路", + "LoopDisabled": "环路禁用" } } diff --git a/frontend/src/i18n/zh-tw.json b/frontend/src/i18n/zh-tw.json index 664c77167..fd0cea2a9 100644 --- a/frontend/src/i18n/zh-tw.json +++ b/frontend/src/i18n/zh-tw.json @@ -49,7 +49,8 @@ "clearCompleted": "清除完成", "showMore": "顯示更多", "showLess": "顯示更少", - "openParentFolder": "開啟父資料夾" + "openParentFolder": "開啟父資料夾", + "selectedCount": "選取執行動作的項目數量" }, "download": { "downloadFile": "下載檔案", @@ -84,15 +85,15 @@ "click": "選擇檔案或目錄", "ctrl": { "click": "選擇多個檔案或目錄", - "f": "打開搜尋列", - "s": "儲存檔案或下載目前資料夾" + "d": "下載所選項目" }, "del": "刪除所選的檔案/資料夾", - "doubleClick": "打開檔案/資料夾", "esc": "清除已選項或關閉提示資訊", - "f1": "顯示該幫助資訊", "f2": "重新命名檔案/資料夾", - "help": "幫助" + "help": "幫助", + "f1": "顯示幫助提示", + "description": "您可以查看以下的基本導航選項。如需其他資訊,請造訪", + "wiki": "FileBrowser Quantum Wiki" }, "languages": { "he": "עברית", @@ -134,7 +135,6 @@ "prompts": { "copy": "複製", "copyMessage": "請選擇欲複製至的目錄:", - "deleteMessageMultiple": "你確定要刪除這 {count} 個檔案嗎?", "deleteMessageSingle": "你確定要刪除這個檔案/資料夾嗎?", "deleteTitle": "刪除檔案", "displayName": "名稱:", @@ -142,7 +142,6 @@ "downloadMessage": "請選擇要下載的壓縮格式。", "error": "發出了一點錯誤...", "fileInfo": "檔案資訊", - "lastModified": "最後修改{suffix}", "move": "移動", "moveMessage": "請選擇欲移動至的目錄:", "newArchetype": "建立一個基於原型的新貼文。您的檔案將會建立在內容資料夾中。", @@ -150,10 +149,7 @@ "newDirMessage": "請輸入新目錄的名稱。", "newFile": "建立檔案", "newFileMessage": "請輸入新檔案的名稱。", - "numberDirs": "目錄數{suffix}", - "numberFiles": "檔案數{suffix}", "rename": "重新命名", - "renameMessage": "請輸入新名稱,舊名稱為:", "replace": "替換", "replaceMessage": "您嘗試上傳的檔案中有一個與現有檔案的名稱存在衝突。是否取代現有的同名檔案?", "schedule": "計畫", @@ -161,7 +157,6 @@ "show": "顯示", "size": "大小", "upload": "上傳", - "deleteMessageShare": "Are you sure you want to delete this share: {path} ?", "destinationSource": "目的地來源:", "deleteUserMessage": "您確定要刪除此使用者嗎?", "dragAndDrop": "將檔案拖放至此", @@ -172,10 +167,6 @@ "typeName": "類型:", "deleted": "已成功刪除!", "conflictsDetected": "偵測到衝突", - "uploadSettingsChunked": "Max concurrent uploads: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB", - "uploadSettingsNoChunk": "Max concurrent uploads: {maxConcurrentUpload}, chunked upload disabled", - "invalidName": "提供的名稱無效:不能包含 \"/「 或 」\\\" 的容器", - "source": "Source{suffix}", "downloadsLimit": "下載次數限制", "maxBandwidth": "最大下載頻寬 (kbps)", "shareTheme": "分享主題", @@ -186,22 +177,44 @@ "shareBanner": "分享橫幅網址", "shareFavicon": "分享圖示 URL", "optionalPasswordHelp": "如果設定,使用者必須輸入此密碼才能檢視共用內容。", - "viewMode": "檢視模式" + "viewMode": "檢視模式", + "renameMessage": "輸入新名稱:", + "renameMessageInvalid": "檔案不能包含\"/「或」\\\"", + "source": "來源{suffix}", + "deleteMessageMultiple": "您確定要刪除{count} 檔案嗎?", + "deleteMessageShare": "您確定要刪除此共用嗎? {path} ?", + "lastModified": "最後修改{suffix}", + "numberDirs": "目錄數量{suffix}", + "numberFiles": "檔案數量{suffix}", + "renameMessageConflict": "名為 \"{filename}\" 的項目已經存在!", + "uploadSettingsChunked": "最大同時上傳數量:{maxConcurrentUpload}, 分塊大小: {uploadChunkSizeMb} MB", + "uploadSettingsNoChunk": "最大同時上傳數量:{maxConcurrentUpload}, 已停用分塊上傳" }, "search": { "images": "影像", "music": "音樂", "pdf": "PDF", - "pressToSearch": "按確認鍵搜尋...", "search": "搜尋...", - "typeToSearch": "輸入以搜尋...", "types": "類型", "video": "影片", "path": "路徑:", "smallerThan": "小於:", "largerThan": "大於:", "helpText1": "您輸入的每個字元都會進行搜尋(搜尋字詞最少 3 個字元)。", - "helpText2": "索引:搜尋會使用索引,該索引會在設定的間隔 (預設值:5 分鐘) 自動更新。當程式剛開始搜尋時,可能會導致結果不完整。" + "helpText2": "索引:搜尋會使用索引,該索引會在設定的間隔 (預設值:5 分鐘) 自動更新。當程式剛開始搜尋時,可能會導致結果不完整。", + "noResults": "在索引搜尋中未找到任何結果。", + "onlyFolders": "僅文件夾", + "onlyFiles": "僅檔案", + "showOptions": "顯示選項", + "photos": "圖片", + "audio": "音訊", + "videos": "影片", + "documents": "文件", + "archives": "檔案", + "number": "數量", + "searchContext": "搜尋範圍:{context}", + "notEnoughCharacters": "搜尋字元不足 (最少{minSearchLength})", + "typeToSearch": "開始輸入{minSearchLength} 或更多字元以開始搜尋。" }, "settings": { "admin": "管理員", @@ -214,10 +227,8 @@ "avoidChanges": "(留空以避免更改)", "branding": "品牌", "brandingDirectoryPath": "品牌資訊資料夾路徑", - "brandingHelp": "您可以通過改變例項名稱,更換Logo,加入自定義樣式,甚至禁用到Github的外部連結來自定義File Browser的外觀和給人的感覺。\n想獲得更多資訊,請檢視 {0} 。", "changePassword": "更改密碼", "commandRunner": "命令執行器", - "commandRunnerHelp": "在這裡你可以設定在下面的事件中執行的命令。每行必須寫一條命令。可以在命令中使用環境變數 {0} 和 {1}。關於此功能和可用環境變數的更多資訊,請閱讀{2}.", "commandsUpdated": "命令已更新!", "createUserDir": "在新增新使用者的同時自動建立使用者的個人目錄", "customStylesheet": "自定義樣式表", @@ -300,7 +311,14 @@ "shareDuration": "到期", "shareDeleted": "分享已刪除!", "adminOptions": "管理員使用者選項", - "downloads": "下載" + "downloads": "下載", + "systemAdmin": "系統與管理", + "configViewer": "組態檢視器", + "configViewerShowFull": "顯示完整設定", + "configViewerShowComments": "顯示評論", + "configViewerLoadConfig": "載入組態", + "brandingHelp": "您可以透過更改名稱、更換標誌、新增自訂樣式,甚至停用 GitHub 的外部連結,自訂檔案瀏覽器實例的外觀和感覺。\n有關自訂品牌的詳細資訊,請參閱{0} 。", + "commandRunnerHelp": "您可以在此設定在命名事件中執行的指令。您必須每行寫一個。環境變數{0} 和{1} 將可用,是相對於{1} 的{0} 。如需更多關於此功能和可用環境變數的資訊,請閱讀{2} 。" }, "sidebar": { "help": "幫助", @@ -358,12 +376,21 @@ "hideNavButtons": "隱藏導覽按鈕", "hideNavButtonsDescription": "隱藏分享中導覽列上的導覽按鈕,以創造簡約的外觀。", "viewModeDescription": "共用頁面的預設版面。", - "titleDefault": "Shared files - {title}", "descriptionDefault": "已向您發送一份共用檔案,供您檢視或下載。", "disableShareCard": "停用分享卡", "disableShareCardDescription": "停用分享頁面側邊欄或行動版頁面頂端的分享卡。", "disableSidebar": "停用側邊欄", - "disableSidebarDescription": "停用共用頁面上的側邊欄。" + "disableSidebarDescription": "停用共用頁面上的側邊欄。", + "enforceDarkLightMode": "強制執行主題模式", + "enforceDarkLightModeDescription": "強制此共用的特定主題模式 (深色或淺色),凌駕使用者偏好設定。", + "default": "不要強制執行模式", + "dark": "黑暗", + "light": "燈光", + "enableOnlyOffice": "啟用 OnlyOffice 檢視器", + "enableOnlyOfficeDescription": "允許在此共用中使用 OnlyOffice 檢視辦公室檔案。", + "enableOnlyOfficeEditing": "啟用 OnlyOffice 編輯", + "enableOnlyOfficeEditingDescription": "允許在此共用中使用 OnlyOffice 編輯辦公室檔案。", + "titleDefault": "共用檔案 -{title}" }, "api": { "title": "API 金鑰", @@ -452,7 +479,13 @@ "defaultThemeDescription": "default - 預設主題", "customTheme": "選擇您的主題", "showSelectMultiple": "在桌面的上下文功能表中顯示多重選擇", - "showSelectMultipleDescription": "預設情況下,桌上型電腦的上下文功能表不像行動裝置一樣有「選取多個」選項。此選項總是在上下文功能表中顯示。" + "showSelectMultipleDescription": "預設情況下,桌上型電腦的上下文功能表不像行動裝置一樣有「選取多個」選項。此選項總是在上下文功能表中顯示。", + "defaultMediaPlayer": "使用瀏覽器的本機媒體播放器", + "defaultMediaPlayerDescription": "使用瀏覽器的本機媒體播放器來播放視訊和音訊檔案,而非 FileBrowser 隨附的「vue-plyr」媒體播放器。", + "debugOfficeEditor": "啟用 OnlyOffice 除錯模式", + "debugOfficeEditorDescription": "在 OnlyOffice 編輯器中顯示包含其他資訊的除錯彈出式視窗。", + "fileViewerOptions": "檔案檢視器選項", + "themeAndLanguage": "主題與語言" }, "otp": { "name": "雙因素驗證", @@ -513,15 +546,19 @@ "name": "名稱", "accessManagement": "存取管理", "all": "全部拒絕", - "defaultBehavior": "Default action{suffix}", - "defaultBehaviorDescription": "這顯示使用者嘗試存取沒有特定存取規則的路徑時會發生的情況。允許」表示使用者預設可以存取,「拒絕」表示使用者預設會被阻擋,除非明確允許。" + "defaultBehaviorDescription": "這顯示使用者嘗試存取沒有特定存取規則的路徑時會發生的情況。允許」表示使用者預設可以存取,「拒絕」表示使用者預設會被阻擋,除非明確允許。", + "defaultBehavior": "預設動作{suffix}" }, "editor": { "uninitialized": "編輯器初始化失敗。請重新載入。", "saveFailed": "儲存檔案失敗。請重試。", "saveAborted": "儲存中止。檔案名稱與使用中的檔案不符。", "saveDisabled": "不支援儲存共用檔案。", - "saveAbortedMessage": "Save operation aborted. The application's active file ({activeFile}) does not match the file you are trying to save ({tryingToSave}).", - "syncFailed": "failed to sync state with the route for \"{filename}\" after 5 attempts. Aborting editor setup to prevent data corruption." + "saveAbortedMessage": "儲存作業中止。應用程式的作用中檔案 ({activeFile}) 與您試圖儲存的檔案 ({tryingToSave}) 不匹配。", + "syncFailed": "經過 5 次嘗試後,未能與 \"{filename}\" 的路由同步狀態。中止編輯器設定以防止資料損毀。" + }, + "player": { + "LoopEnabled": "啟用循環", + "LoopDisabled": "迴路停用" } } diff --git a/frontend/src/main.ts b/frontend/src/main.ts index bc800d47c..26f36ed0f 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -1,11 +1,12 @@ -import { createApp } from 'vue'; -import router from './router'; // Adjust the path as per your setup -import App from './App.vue'; // Adjust the path as per your setup -import { state } from '@/store'; // Adjust the path as per your setup +import { createApp } from "vue"; +import router from "./router"; // Adjust the path as per your setup +import App from "./App.vue"; // Adjust the path as per your setup +import { state } from "@/store"; // Adjust the path as per your setup import i18n from "@/i18n"; import VueLazyload from "vue-lazyload"; +import VuePlyr from "@skjnldsv/vue-plyr"; // Custom media player -import './css/styles.css'; +import "./css/styles.css"; const app = createApp(App); @@ -13,9 +14,10 @@ const app = createApp(App); app.use(VueLazyload); app.use(i18n); app.use(router); +app.use(VuePlyr); // Provide state to the entire application -app.provide('state', state); +app.provide("state", state); // provide v-focus for components app.directive("focus", { @@ -35,4 +37,4 @@ app.mixin({ }, }); -router.isReady().then(() => app.mount("#app")); \ No newline at end of file +router.isReady().then(() => app.mount("#app")); diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 13c76a025..7a344c748 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -138,7 +138,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; + return to.path === from.path && to.hash === from.hash; } router.beforeResolve(async (to, from, next) => { diff --git a/frontend/src/store/getters.js b/frontend/src/store/getters.js index b31ddfe2f..b11558eb0 100644 --- a/frontend/src/store/getters.js +++ b/frontend/src/store/getters.js @@ -44,6 +44,12 @@ export const getters = { return shareInfo.isShare }, isDarkMode: () => { + if (shareInfo.enforceDarkLightMode == "dark") { + return true + } + if (shareInfo.enforceDarkLightMode == "light") { + return false + } if (state.user == null) { return true } @@ -342,12 +348,11 @@ export const getters = { return false }, officeViewingDisabled: filename => { - if (shareInfo.isShare) { - return true - } const ext = ' ' + getFileExtension(filename) - if (state.user.disableOfficePreviewExt) { - const disabledExts = ' ' + state.user.disableOfficePreviewExt.toLowerCase() + const hasDisabled = shareInfo.disableOfficePreviewExt || state.user.disableOfficePreviewExt + if (hasDisabled) { + const disabledList = shareInfo.disableOfficePreviewExt + ' ' + state.user.disableOfficePreviewExt + const disabledExts = ' ' + disabledList.toLowerCase() if (disabledExts.includes(ext.toLowerCase())) { return true } diff --git a/frontend/src/store/mutations.js b/frontend/src/store/mutations.js index ac511e546..8dfb1ce61 100644 --- a/frontend/src/store/mutations.js +++ b/frontend/src/store/mutations.js @@ -7,6 +7,13 @@ import { sortedItems } from "@/utils/sort.js"; import { serverHasMultipleSources } from "@/utils/constants.js"; export const mutations = { + setPreviousHistoryItem: (value) => { + if (value == state.previousHistoryItem) { + return; + } + state.previousHistoryItem = value; + emitStateChanged(); + }, setContextMenuHasItems: (value) => { if (value == state.contextMenuHasItems) { return; diff --git a/frontend/src/store/state.js b/frontend/src/store/state.js index cb043ae4f..3e7b4098f 100644 --- a/frontend/src/store/state.js +++ b/frontend/src/store/state.js @@ -7,10 +7,16 @@ export const state = reactive({ content: "", x: 0, y: 0, + pointerEvents: false, + width: null, + }, + previousHistoryItem: { + name: "", + source: "", + path: "", }, contextMenuHasItems: false, deletedItem: false, - previousHash: "", showOverflowMenu: false, sessionId: "", isSafari: /^((?!chrome|android).)*safari/i.test(navigator.userAgent), @@ -55,6 +61,7 @@ export const state = reactive({ permissions: {}, // Default to an empty object for permissions darkMode: true, // Default to false, assuming this is a boolean disableSettings: false, + debugOffice: false, // Debug mode for OnlyOffice integration profile: { // Example of additional user properties username: '', // Default to an empty string email: '', // Default to an empty string diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index dcc7f250c..0b43c0348 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -23,6 +23,7 @@ const muPdfAvailable = window.filebrowser.muPdfAvailable; const updateAvailable = window.filebrowser.updateAvailable; const disableNavButtons = window.filebrowser.disableNavButtons; const userSelectableThemes = window.filebrowser.userSelectableThemes; +const minSearchLength = window.filebrowser.minSearchLength; const shareInfo = window.filebrowser.share; const origin = window.location.origin; @@ -31,12 +32,13 @@ const settings = [ { 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 } }, { id: 'users', label: 'settings.userManagement', component: 'UserManagement' }, { id: 'access', label: 'access.accessManagement', component: 'AccessSettings', permissions: { admin: true } }, + { id: 'systemAdmin', label: 'settings.systemAdmin', component: 'SystemAdmin', permissions: { admin: true } }, ]; export { + minSearchLength, shareInfo, userSelectableThemes, disableNavButtons, diff --git a/frontend/src/utils/url.js b/frontend/src/utils/url.js index 0218ad4f1..1cc09453b 100644 --- a/frontend/src/utils/url.js +++ b/frontend/src/utils/url.js @@ -1,5 +1,5 @@ import { baseURL } from "@/utils/constants.js"; -import { state, mutations } from "@/store"; +import { state, mutations, getters } from "@/store"; import { router } from "@/router"; import { shareInfo } from "@/utils/constants.js"; @@ -139,6 +139,9 @@ export function extractSourceFromPath(url) { } export function buildItemUrl(source, path) { + if (getters.isShare()) { + return `/public/share/${shareInfo.hash}${path}`; + } if (state.serverHasMultipleSources) { return `/files/${source}${path}`; } else { @@ -157,11 +160,9 @@ export function encodedPath(path) { } // assume non-encoded input path and source -export function goToItem(source, path, previousHash) { +export function goToItem(source, path, previousHistoryItem) { mutations.resetAll() - if (previousHash) { - location.hash = previousHash; - } + mutations.setPreviousHistoryItem(previousHistoryItem); let newPath = encodedPath(path); let fullPath; if (shareInfo.isShare) { diff --git a/frontend/src/views/Files.vue b/frontend/src/views/Files.vue index 167f2e5f3..dadbdc86e 100644 --- a/frontend/src/views/Files.vue +++ b/frontend/src/views/Files.vue @@ -132,7 +132,11 @@ export default { }, methods: { scrollToHash() { - if (window.location.hash === this.lastHash) return; + let scrollToId = ""; + // scroll to previous item either from location hash or from previousItemHashId state + // prefers location hash + const noHashChange = window.location.hash === this.lastHash + if (noHashChange && state.previousItemHashId === "") return; this.lastHash = window.location.hash; if (window.location.hash) { const rawHash = window.location.hash.slice(1); @@ -143,8 +147,12 @@ export default { // If the hash contains malformed escape sequences, fall back to raw decodedName = rawHash; } - const id = url.base64Encode(encodeURIComponent(decodedName)); - const element = document.getElementById(id); + scrollToId = url.base64Encode(encodeURIComponent(decodedName)); + + } else if (state.previousItemHashId) { + scrollToId = url.base64Encode(encodeURIComponent(state.previousItemHashId)); + } + const element = document.getElementById(scrollToId); if (element) { element.scrollIntoView({ behavior: "instant", @@ -157,7 +165,6 @@ export default { element.classList.remove('scroll-glow'); }, 1000); } - } }, async fetchData() { if (state.deletedItem || getters.isInvalidShare()) { diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index a782e7846..c11a8d19f 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -36,6 +36,7 @@ import AccessSettings from "@/views/settings/Access.vue"; import UserSettings from "@/views/settings/Users.vue"; import FileLoading from "@/views/settings/FileLoading.vue"; import ApiKeys from "@/views/settings/Api.vue"; +import SystemAdmin from "@/views/settings/SystemAdmin.vue"; export default { name: "settings", components: { @@ -47,6 +48,7 @@ export default { AccessSettings, FileLoading, UserSettings, + SystemAdmin, }, data() { return { diff --git a/frontend/src/views/bars/Default.vue b/frontend/src/views/bars/Default.vue index dbc0d2341..289e8c0ae 100644 --- a/frontend/src/views/bars/Default.vue +++ b/frontend/src/views/bars/Default.vue @@ -45,6 +45,7 @@ import { getters, state, mutations } from "@/store"; import Action from "@/components/Action.vue"; import Search from "@/components/Search.vue"; import { disableNavButtons, shareInfo } from "@/utils/constants"; +import { url } from "@/utils"; export default { name: "UnifiedHeader", @@ -169,10 +170,15 @@ export default { } else { mutations.closeHovers(); if (listingView === "settings") { + if (state.previousHistoryItem.name) { + url.goToItem(state.previousHistoryItem.source, state.previousHistoryItem.path, {}); + return; + } router.push({ path: "/files" }); return; } mutations.replaceRequest({}); + router.go(-1); } }, diff --git a/frontend/src/views/files/Editor.vue b/frontend/src/views/files/Editor.vue index 2d161d8f7..8b932bda9 100644 --- a/frontend/src/views/files/Editor.vue +++ b/frontend/src/views/files/Editor.vue @@ -4,12 +4,6 @@
- - + + \ No newline at end of file diff --git a/frontend/src/views/files/ListingView.vue b/frontend/src/views/files/ListingView.vue index c847f0a7b..8014c09f7 100644 --- a/frontend/src/views/files/ListingView.vue +++ b/frontend/src/views/files/ListingView.vue @@ -275,7 +275,7 @@ export default { return state.user.gallerySize; }, isDarkMode() { - return state.user?.darkMode; + return getters.isDarkMode(); }, getMultiple() { return state.multiple; @@ -606,7 +606,7 @@ export default { return; } // Handle the space bar key - if (key === " ") { + if (key === " " && !modifierKeys) { event.preventDefault(); if (state.isSearchActive) { mutations.setSearch(false); @@ -623,6 +623,25 @@ export default { if (modifierKeys) { this.ctrKeyPressed = true; + const charKey = String.fromCharCode(which).toLowerCase(); + + switch (charKey) { + case "c": + case "x": + this.copyCut(event, charKey); + break; + case "v": + this.paste(event); + break; + case "a": + event.preventDefault(); + this.selectAll(); + break; + case "d": + event.preventDefault(); + downloadFiles(state.selected); + break; + } return; } @@ -631,7 +650,7 @@ export default { case "Enter": if (this.selectedCount === 1) { const selected = getters.getFirstSelected(); - const selectedUrl = url.buildItemUrl(selected.source, selected.path) + const selectedUrl = url.buildItemUrl(selected.source, selected.path); router.push({ path: selectedUrl }); } break; @@ -655,7 +674,12 @@ export default { case "F2": if (!state.user.permissions.modify || state.selected.length !== 1) return; - mutations.showHover("rename"); + mutations.showHover({ + name: "rename", + props: { + item: getters.getFirstSelected(), + }, + }); break; case "ArrowUp": @@ -670,26 +694,6 @@ export default { this.navigateKeboardArrows(key); break; } - - const charKey = String.fromCharCode(which).toLowerCase(); - - switch (charKey) { - case "c": - case "x": - this.copyCut(event, charKey); - break; - case "v": - this.paste(event); - break; - case "a": - event.preventDefault(); - this.selectAll(); - break; - case "s": - event.preventDefault(); - downloadFiles(state.selected); - break; - } }, selectAll() { for (let file of this.items.files) { diff --git a/frontend/src/views/files/MarkdownViewer.vue b/frontend/src/views/files/MarkdownViewer.vue index 9f4d39dd6..22520c093 100644 --- a/frontend/src/views/files/MarkdownViewer.vue +++ b/frontend/src/views/files/MarkdownViewer.vue @@ -5,7 +5,7 @@ - diff --git a/frontend/src/views/settings/FileLoading.vue b/frontend/src/views/settings/FileLoading.vue index 62aed21f7..c18117f06 100644 --- a/frontend/src/views/settings/FileLoading.vue +++ b/frontend/src/views/settings/FileLoading.vue @@ -14,7 +14,7 @@
+ :placeholder="$t('search.number')" /> {{ localuser.fileLoading.maxConcurrentUpload }}
@@ -28,7 +28,7 @@
+ :placeholder="$t('search.number')" />
diff --git a/frontend/src/views/settings/Profile.vue b/frontend/src/views/settings/Profile.vue index a53a92ca0..e3912e654 100644 --- a/frontend/src/views/settings/Profile.vue +++ b/frontend/src/views/settings/Profile.vue @@ -4,261 +4,155 @@
-
-

{{ $t("profileSettings.sidebarOptions") }}

+
+
- - - - -
-
-
-

{{ $t("settings.listingOptions") }}

-
- - - + + - + - - - - + + + + - - - + + + + :description="$t('profileSettings.showSelectMultipleDescription')" />
-

{{ $t("profileSettings.editorViewerOptions") }}

+ +
- - + + +
-

{{ $t("settings.searchOptions") }}

+
+
- -
-

{{ $t("settings.adminOptions") }}

-
- + :description="$t('profileSettings.disableSearchOptionsDescription')" />
-
-
-

{{ $t("profileSettings.disableThumbnailPreviews") }}

- - help - -
-
- - -
+ + +
+ + +
-
-

{{ $t("profileSettings.disableViewingFiles") }}

- - help - -
-
- - -
+
+

{{ $t("profileSettings.disableThumbnailPreviews") }}

+ + help + +
+
+ + +
+
+
+
+

{{ $t("profileSettings.disableViewingFiles") }}

+ + help + +
+
+ +
-
-
-

{{ $t("profileSettings.disableOfficeEditor") }}

- - help - -
-
- - -
+
+
+
+

{{ $t("profileSettings.disableOfficeEditor") }}

+ + help + +
+
+ + +
+
+
+
-

{{ $t("settings.themeColor") }}

- -

{{ $t("profileSettings.customTheme") }}

+ +
+ + +

{{ $t('settings.themeColor') }}

+ + +

{{ $t('profileSettings.customTheme') }}

- +
-

{{ $t("settings.language") }}

- -
- -
+ +

{{ $t('settings.language') }}

+ +
+
+ +
@@ -270,6 +164,7 @@ import { usersApi } from "@/api"; import Languages from "@/components/settings/Languages.vue"; import ButtonGroup from "@/components/ButtonGroup.vue"; import ToggleSwitch from "@/components/settings/ToggleSwitch.vue"; +import SettingsItem from "@/components/settings/SettingsItem.vue"; export default { name: "settings", @@ -277,6 +172,7 @@ export default { Languages, ButtonGroup, ToggleSwitch, + SettingsItem, }, data() { return { @@ -284,17 +180,20 @@ export default { formDisablePreviews: "", // holds temporary input before saving formDisabledViewing: "", // holds temporary input before saving formDisableOfficePreview: "", // holds temporary input before saving - colorChoices: [ + expandedSection: 'listingOptions', // Track which section is currently expanded for accordion behavior + }; + }, + computed: { + colorChoices() { + return [ { label: this.$t("colors.blue"), value: "var(--blue)" }, { label: this.$t("colors.red"), value: "var(--red)" }, { label: this.$t("colors.green"), value: "var(--icon-green)" }, { label: this.$t("colors.violet"), value: "var(--icon-violet)" }, { label: this.$t("colors.yellow"), value: "var(--icon-yellow)" }, { label: this.$t("colors.orange"), value: "var(--icon-orange)" }, - ], - }; - }, - computed: { + ]; + }, availableThemes() { return userSelectableThemes || {}; }, @@ -408,6 +307,7 @@ export default { "hideSidebarFileActions", "editorQuickSave", "showSelectMultiple", + "debugOffice", ]); if (themeChanged) { window.location.reload(); @@ -422,12 +322,22 @@ export default { this.localuser.locale = updatedLocale; this.updateSettings(); }, + handleSectionToggle(sectionTitle) { + // Accordion logic: if clicking the same section, collapse it, otherwise expand the new one + if (this.expandedSection === sectionTitle) { + this.expandedSection = null; // Collapse current section + } else { + this.expandedSection = sectionTitle; // Expand new section + } + }, + isSectionCollapsed(sectionTitle) { + return this.expandedSection !== sectionTitle; + }, }, }; \ No newline at end of file diff --git a/frontend/src/views/settings/Shares.vue b/frontend/src/views/settings/Shares.vue index a7723fe14..7635a942c 100644 --- a/frontend/src/views/settings/Shares.vue +++ b/frontend/src/views/settings/Shares.vue @@ -70,7 +70,7 @@ + + diff --git a/frontend/tests/playwright/general/file-actions.spec.ts b/frontend/tests/playwright/general/file-actions.spec.ts index b55c452f0..c8787869f 100644 --- a/frontend/tests/playwright/general/file-actions.spec.ts +++ b/frontend/tests/playwright/general/file-actions.spec.ts @@ -7,7 +7,7 @@ test("info from listing", async({ page, checkForErrors, context }) => { await page.locator('a[aria-label="file.tar.gz"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="file.tar.gz"]').click( { button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Info"]').click(); await expect(page.locator('.break-word')).toHaveText('Display Name: file.tar.gz'); checkForErrors(); @@ -21,7 +21,7 @@ test("info from search", async({ page, checkForErrors, context }) => { await expect(page.locator('#result-list')).toHaveCount(1); await page.locator('li[aria-label="file.tar.gz"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Info"]').click(); await expect(page.locator('.break-word')).toHaveText('Display Name: file.tar.gz'); checkForErrors(); @@ -69,7 +69,7 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="copyme.txt"]').click( { button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Copy file"]').click(); await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /'); await expect(page.locator('li[aria-selected="true"]')).toHaveCount(0); @@ -97,7 +97,7 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context await page.locator('a[aria-label="copyme.txt"]').click( { button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Copy file"]').click(); await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /myfolder'); await page.locator('li[aria-label="newfolder"]').click(); @@ -114,7 +114,7 @@ test("delete file", async({ page, checkForErrors, context }) => { await page.locator('a[aria-label="deleteme.txt"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="deleteme.txt"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Delete"]').click(); await expect( page.locator('.card-content')).toHaveText('Are you sure you want to delete this file/folder?/deleteme.txt'); await expect(page.locator('div[aria-label="delete-path"]')).toHaveText('/deleteme.txt'); @@ -131,7 +131,7 @@ test("delete nested file prompt", async({ page, checkForErrors, context }) => { await page.locator('a[aria-label="file#.sh"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="file#.sh"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Delete"]').click(); await expect(page.locator('.card-content')).toHaveText('Are you sure you want to delete this file/folder?/folder#hash/file#.sh'); await expect(page.locator('div[aria-label="delete-path"]')).toHaveText('/folder#hash/file#.sh'); diff --git a/frontend/tests/playwright/general/settings.spec.ts b/frontend/tests/playwright/general/settings.spec.ts index 8a601d849..bc1eed9b5 100644 --- a/frontend/tests/playwright/general/settings.spec.ts +++ b/frontend/tests/playwright/general/settings.spec.ts @@ -11,6 +11,7 @@ test("adjusting theme colors", async({ page, checkForErrors, context }) => { await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files"); await page.locator('i[aria-label="settings"]').click(); await expect(page).toHaveTitle("Graham's Filebrowser - Settings"); + await page.locator('div[aria-label="themeLanguage"]').click(); await page.locator('button', { hasText: 'violet' }).click(); const popup = page.locator('#popup-notification-content'); await popup.waitFor({ state: 'visible' }); diff --git a/frontend/tests/playwright/global-setup.ts b/frontend/tests/playwright/global-setup.ts index c44ecee3e..f41017401 100644 --- a/frontend/tests/playwright/global-setup.ts +++ b/frontend/tests/playwright/global-setup.ts @@ -22,7 +22,7 @@ async function globalSetup() { await page.locator('a[aria-label="myfolder"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="myfolder"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Share"]').click(); await expect(page.locator('div[aria-label="share-path"]')).toHaveText('Path: /myfolder'); await page.locator('button[aria-label="Share-Confirm"]').click(); @@ -41,7 +41,7 @@ async function globalSetup() { await page.locator('a[aria-label="1file1.txt"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="1file1.txt"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Share"]').click(); await expect(page.locator('div[aria-label="share-path"]')).toHaveText('Path: /1file1.txt'); await page.locator('button[aria-label="Share-Confirm"]').click(); @@ -60,7 +60,7 @@ async function globalSetup() { await page.locator('a[aria-label="share"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="share"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Share"]').click(); await expect(page.locator('div[aria-label="share-path"]')).toHaveText('Path: /share'); await page.locator('button[aria-label="Share-Confirm"]').click(); diff --git a/frontend/tests/playwright/noauth-setup.ts b/frontend/tests/playwright/noauth-setup.ts index e6b83a286..741e17e83 100644 --- a/frontend/tests/playwright/noauth-setup.ts +++ b/frontend/tests/playwright/noauth-setup.ts @@ -13,7 +13,7 @@ async function globalSetup() { await page.locator('a[aria-label="myfolder"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="myfolder"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Share"]').click(); await expect(page.locator('div[aria-label="share-path"]')).toHaveText('Path: /myfolder'); await page.locator('button[aria-label="Share-Confirm"]').click(); @@ -32,7 +32,7 @@ async function globalSetup() { await page.locator('a[aria-label="1file1.txt"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="1file1.txt"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Share"]').click(); await expect(page.locator('div[aria-label="share-path"]')).toHaveText('Path: /1file1.txt'); await page.locator('button[aria-label="Share-Confirm"]').click(); diff --git a/frontend/tests/playwright/noauth/file-actions.spec.ts b/frontend/tests/playwright/noauth/file-actions.spec.ts index 12baeae60..caffa7bec 100644 --- a/frontend/tests/playwright/noauth/file-actions.spec.ts +++ b/frontend/tests/playwright/noauth/file-actions.spec.ts @@ -7,7 +7,7 @@ test("info from listing", async({ page, checkForErrors, context }) => { await page.locator('a[aria-label="file.tar.gz"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="file.tar.gz"]').click( { button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Info"]').click(); await expect(page.locator('.break-word')).toHaveText('Display Name: file.tar.gz'); checkForErrors(); @@ -21,7 +21,7 @@ test("info from search", async({ page, checkForErrors, context }) => { await expect(page.locator('#result-list')).toHaveCount(1); await page.locator('li[aria-label="file.tar.gz"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Info"]').click(); await expect(page.locator('.break-word')).toHaveText('Display Name: file.tar.gz'); checkForErrors(); @@ -45,7 +45,7 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context await page.locator('a[aria-label="copyme.txt"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="copyme.txt"]').click( { button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Copy file"]').click(); await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /'); await expect(page.locator('li[aria-selected="true"]')).toHaveCount(0); @@ -73,7 +73,7 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context await page.locator('a[aria-label="copyme.txt"]').click( { button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Copy file"]').click(); await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /myfolder'); await page.locator('li[aria-label="newfolder"]').click(); @@ -90,7 +90,7 @@ test("delete file", async({ page, checkForErrors, context }) => { await page.locator('a[aria-label="deleteme.txt"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="deleteme.txt"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Delete"]').click(); await expect( page.locator('.card-content')).toHaveText('Are you sure you want to delete this file/folder?/deleteme.txt'); await expect(page.locator('div[aria-label="delete-path"]')).toHaveText('/deleteme.txt'); @@ -108,7 +108,7 @@ test("delete nested file prompt", async({ page, checkForErrors, context }) => { await page.locator('a[aria-label="file#.sh"]').waitFor({ state: 'visible' }); await page.locator('a[aria-label="file#.sh"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('1 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('1'); await page.locator('button[aria-label="Delete"]').click(); await expect(page.locator('.card-content')).toHaveText('Are you sure you want to delete this file/folder?/folder#hash/file#.sh'); await expect(page.locator('div[aria-label="delete-path"]')).toHaveText('/folder#hash/file#.sh'); diff --git a/frontend/tests/playwright/noauth/share.mobile.spec.ts b/frontend/tests/playwright/noauth/share.mobile.spec.ts index bfa7d66c0..5a10ced3d 100644 --- a/frontend/tests/playwright/noauth/share.mobile.spec.ts +++ b/frontend/tests/playwright/noauth/share.mobile.spec.ts @@ -23,7 +23,7 @@ test("share download multiple files", async ({ page, checkForErrors, context }) await page.locator('a[aria-label="gray-sample.jpg"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('3 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('3'); await page.locator('button[aria-label="Download"]').waitFor({ state: 'visible' }); await page.locator('button[aria-label="Download"]').click(); diff --git a/frontend/tests/playwright/noauth/theme-branding.spec.ts b/frontend/tests/playwright/noauth/theme-branding.spec.ts index 1eef9f02b..1f1324ed5 100644 --- a/frontend/tests/playwright/noauth/theme-branding.spec.ts +++ b/frontend/tests/playwright/noauth/theme-branding.spec.ts @@ -32,6 +32,7 @@ test("adjusting theme colors", async({ page, checkForErrors, context }) => { await expect(page).toHaveTitle("Graham's Filebrowser - Files - playwright-files"); await page.locator('i[aria-label="settings"]').click(); await expect(page).toHaveTitle("Graham's Filebrowser - Settings"); + await page.locator('div[aria-label="themeLanguage"]').click(); await page.locator('button', { hasText: 'violet' }).click(); const popup = page.locator('#popup-notification-content'); await popup.waitFor({ state: 'visible' }); diff --git a/frontend/tests/playwright/proxy-setup.ts b/frontend/tests/playwright/proxy-setup.ts index d17d2a274..5e333d547 100644 --- a/frontend/tests/playwright/proxy-setup.ts +++ b/frontend/tests/playwright/proxy-setup.ts @@ -19,6 +19,7 @@ async function globalSetup() { await page.locator('button[aria-label="File-Actions"]').click(); await page.locator('button[aria-label="Share"]').click(); await page.locator('button[aria-label="Share-Confirm"]').click(); + await page.waitForResponse(response => response.url().includes('/api/share') && response.status() === 200); await expect(page.locator("div[aria-label='share-prompt'] .card-content table tbody tr:not(:has(th))")).toHaveCount(1); const shareHash = await page.locator("div[aria-label='share-prompt'] .card-content table tbody tr:not(:has(th)) td").first().textContent(); if (!shareHash) { diff --git a/frontend/tests/playwright/sharing/share.mobile.spec.ts b/frontend/tests/playwright/sharing/share.mobile.spec.ts index f84d9d5cd..e9e89d8d6 100644 --- a/frontend/tests/playwright/sharing/share.mobile.spec.ts +++ b/frontend/tests/playwright/sharing/share.mobile.spec.ts @@ -39,7 +39,7 @@ test("share download multiple files", async ({ page, checkForErrors, context }) await page.locator('a[aria-label="gray-sample.jpg"]').click({ button: "right" }); await page.locator('.selected-count-header').waitFor({ state: 'visible' }); - await expect(page.locator('.selected-count-header')).toHaveText('3 selected'); + await expect(page.locator('.selected-count-header')).toHaveText('3'); await page.locator('button[aria-label="Download"]').waitFor({ state: 'visible' }); await page.locator('button[aria-label="Download"]').click(); diff --git a/makefile b/makefile index aadd80a00..6e69757f0 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,5 @@ SHELL := /bin/bash - +FILEBROWSER_DEVMODE=true .SILENT: setup: echo "creating ./backend/test_config.yaml for local testing..." @@ -48,13 +48,10 @@ run: build-frontend else \ sed -i '/func init/,+3d' ./swagger/docs/docs.go; \ fi && \ - FILEBROWSER_DEVMODE=true CGO_ENABLED=1 go run --tags=mupdf \ + CGO_ENABLED=1 go run --tags=mupdf \ --ldflags="-w -s -X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=testing'" . -c test_config.yaml build-frontend: - cd backend && rm -rf http/dist http/embed/* && \ - FILEBROWSER_GENERATE_CONFIG=true go run . && cp generated.yaml ../frontend/public/config.generated.yaml - cd backend/http/ && ln -s ../../frontend/dist if [ "$(OS)" = "Windows_NT" ]; then \ cd frontend && npm run build-windows; \ else \