mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-04 16:50:08 -05:00
Compare commits
9
Commits
main
...
v1.2.4-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c2e2f6af5 | ||
|
|
dc8d71ddb0 | ||
|
|
86c41db9c5 | ||
|
|
89d99090ec | ||
|
|
dfe217d020 | ||
|
|
1d2ac2ab06 | ||
|
|
87923add8a | ||
|
|
be7df9000a | ||
|
|
1126fe7d95 |
No files matched your search
@@ -2,6 +2,22 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
## v1.2.4-beta
|
||||
|
||||
**Notes**:
|
||||
- Changed `/api/media/subtitles` api endpoint to better support subtitles.
|
||||
- Correcting some errors in French language (#1947)
|
||||
- add Dutch `nl.json` for frontend i18n -- thanks @Stephan-P (#1957)
|
||||
- share banner and icon images automatically serve a scaled down 1024x1024
|
||||
|
||||
**BugFixes**:
|
||||
- Can't access directories with space (and possibly other special characters) when browsing a public link share (#1956)
|
||||
- External ASS subtitles are not usable in web preview (#1933)
|
||||
- Filename blocking rename/move (#1950)
|
||||
- enforcedOtp: true blocks all user settings updates with HTTP 400 (#1962)
|
||||
- Share favicon URL not showing up and Share description causing endless loading (#1911)
|
||||
- folder sizes not updated for changes to filesystem outside of filebrowser (#1974)
|
||||
|
||||
## v1.2.3-beta
|
||||
|
||||
**Notes**:
|
||||
|
||||
@@ -36,6 +36,18 @@ func processDirectoryMetadata(response *iteminfo.ExtendedFileInfo, idx *indexing
|
||||
metadataCount++
|
||||
}
|
||||
}
|
||||
// Process files concurrently using goroutines
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex // Protects processedCount
|
||||
|
||||
// Refresh directory to update folder sizes
|
||||
wg.Go(func() {
|
||||
indexPath := idx.MakeIndexPath(response.Path, true)
|
||||
err := idx.RefreshDirectory(indexPath, false)
|
||||
if err != nil {
|
||||
logger.Debugf("Failed to refresh directory for folder size update: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Set hasMetadata flag if there are files with potential metadata
|
||||
if metadataCount > 0 {
|
||||
@@ -49,9 +61,6 @@ func processDirectoryMetadata(response *iteminfo.ExtendedFileInfo, idx *indexing
|
||||
// Create a single shared FFmpegService instance for all files to coordinate concurrency
|
||||
sharedFFmpegService := ffmpeg.NewFFmpegService(10, false, "")
|
||||
if sharedFFmpegService != nil {
|
||||
// Process files concurrently using goroutines
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex // Protects processedCount
|
||||
|
||||
for i := range response.Files {
|
||||
fileItem := &response.Files[i]
|
||||
@@ -85,10 +94,10 @@ func processDirectoryMetadata(response *iteminfo.ExtendedFileInfo, idx *indexing
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
// Wait for all goroutines to complete
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// finalizeResponse handles final response adjustments (OnlyOffice ID, scope stripping)
|
||||
@@ -210,17 +219,16 @@ func processContent(info *iteminfo.ExtendedFileInfo, idx *indexing.Index, opts u
|
||||
}
|
||||
|
||||
// Handle subtitles if requested
|
||||
if opts.ExtractEmbeddedSubtitles {
|
||||
parentPath := filepath.Dir(info.Path)
|
||||
parentInfo, exists := idx.GetReducedMetadata(parentPath, true)
|
||||
if exists {
|
||||
info.DetectSubtitles(parentInfo)
|
||||
err := info.LoadSubtitleContent()
|
||||
if err != nil {
|
||||
logger.Debug("failed to load subtitle content: " + err.Error())
|
||||
}
|
||||
}
|
||||
parentPath := filepath.Dir(info.Path)
|
||||
parentInfo, exists := idx.GetMetadataInfo(parentPath, true, false)
|
||||
if exists {
|
||||
info.GetSubtitles(parentInfo)
|
||||
}
|
||||
if opts.ExtractEmbeddedSubtitles {
|
||||
subtitles := ffmpeg.DetectEmbeddedSubtitles(info.RealPath, info.ModTime)
|
||||
info.Subtitles = append(info.Subtitles, subtitles...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ func VerifyTotpCode(user *users.User, code string, userStore *users.Storage) err
|
||||
logger.Warningf("Invalid TOTP code for user %s", user.Username)
|
||||
return fmt.Errorf("invalid OTP token")
|
||||
}
|
||||
if totpSecret != "" {
|
||||
if totpSecret != "" || !user.OtpEnabled {
|
||||
user.TOTPSecret = totpSecret // The encrypted or plaintext secret
|
||||
user.TOTPNonce = totpNonce // The nonce if encrypted, or empty if plaintext
|
||||
user.OtpEnabled = true // Enable OTP for the user
|
||||
|
||||
@@ -63,12 +63,18 @@ type FileOptions struct {
|
||||
// SanitizeUserPath prevents path traversal attacks by cleaning and validating user input.
|
||||
// Rule 1: Do Not Use User Input in File Paths (without validation)
|
||||
func SanitizeUserPath(userPath string) (string, error) {
|
||||
// Clean resolves . and .. segments
|
||||
clean := filepath.Clean(userPath)
|
||||
|
||||
// Reject absolute paths or attempts to escape scope
|
||||
if strings.HasPrefix(clean, "..") {
|
||||
return "", fmt.Errorf("invalid path")
|
||||
// Split the path into segments to check for path traversal attempts
|
||||
// We check if ".." appears as a complete path segment (not just in a filename)
|
||||
segments := strings.Split(clean, string(filepath.Separator))
|
||||
|
||||
for _, segment := range segments {
|
||||
// Check if any segment is exactly ".." (path traversal attempt)
|
||||
// This catches any ".." that filepath.Clean couldn't resolve (i.e., escape attempts)
|
||||
if segment == ".." {
|
||||
return "", fmt.Errorf("invalid path: path traversal detected")
|
||||
}
|
||||
}
|
||||
|
||||
return clean, nil
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
type SubtitleTrack struct {
|
||||
Name string `json:"name"` // filename for external, or descriptive name for embedded
|
||||
Language string `json:"language"` // 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
|
||||
Embedded bool `json:"embedded"` // true for embedded subtitles, false for external files
|
||||
}
|
||||
|
||||
func GetSubtitleSidecarContent(subtitlePath string) (string, error) {
|
||||
// validate the size is not too large
|
||||
info, err := os.Stat(subtitlePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if info.Size() > 1024*1024*50 { // 50MB
|
||||
return "", errors.New("subtitle file is too large")
|
||||
}
|
||||
|
||||
// First check if it's a text file using the consolidated validation logic
|
||||
isText, err := IsTextFile(subtitlePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !isText {
|
||||
// Not a text file, return empty string (no error, just not text)
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// read the file content
|
||||
content, err := os.ReadFile(subtitlePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(content), nil
|
||||
}
|
||||
@@ -77,7 +77,7 @@ func (st usersBackend) Update(user *users.User, actorIsAdmin bool, fields ...str
|
||||
|
||||
passwordUser := existingUser.LoginMethod == users.LoginMethodPassword
|
||||
enforcedOtp := settings.Config.Auth.Methods.PasswordAuth.EnforcedOtp
|
||||
if passwordUser && enforcedOtp && !user.OtpEnabled {
|
||||
if passwordUser && enforcedOtp && !existingUser.OtpEnabled {
|
||||
return errors.ErrNoTotpConfigured
|
||||
}
|
||||
if user.LoginMethod == "" {
|
||||
|
||||
@@ -3,11 +3,12 @@ package ffmpeg
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/go-cache/cache"
|
||||
)
|
||||
|
||||
var (
|
||||
MediaCache = cache.NewCache[[]SubtitleTrack](24 * time.Hour) // subtitles get cached for 24 hours
|
||||
SubtitleContentCache = cache.NewCache[string](24 * time.Hour) // subtitle content gets cached for 24 hours
|
||||
MetadataCache = cache.NewCache[float64](24 * time.Hour) // media duration gets cached for 24 hours
|
||||
MediaCache = cache.NewCache[[]utils.SubtitleTrack](24 * time.Hour) // subtitles get cached for 24 hours
|
||||
SubtitleContentCache = cache.NewCache[string](24 * time.Hour) // subtitle content gets cached for 24 hours
|
||||
MetadataCache = cache.NewCache[float64](24 * time.Hour) // media duration gets cached for 24 hours
|
||||
)
|
||||
+33
-136
@@ -10,20 +10,10 @@ import (
|
||||
"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 {
|
||||
@@ -36,7 +26,7 @@ type FFProbeOutput struct {
|
||||
}
|
||||
|
||||
// DetectAllSubtitles finds both embedded and external subtitle tracks
|
||||
func DetectAllSubtitles(videoPath string, parentDir string, modtime time.Time) []SubtitleTrack {
|
||||
func DetectAllSubtitles(videoPath string, parentDir string, modtime time.Time) []utils.SubtitleTrack {
|
||||
key := "all_subtitles:" + videoPath + ":" + modtime.Format(time.RFC3339)
|
||||
|
||||
// Check cache first
|
||||
@@ -44,7 +34,7 @@ func DetectAllSubtitles(videoPath string, parentDir string, modtime time.Time) [
|
||||
return cached
|
||||
}
|
||||
|
||||
var allSubtitles []SubtitleTrack
|
||||
var allSubtitles []utils.SubtitleTrack
|
||||
|
||||
// First, get embedded subtitles
|
||||
embeddedSubs := detectEmbeddedSubtitles(videoPath)
|
||||
@@ -60,9 +50,25 @@ func DetectAllSubtitles(videoPath string, parentDir string, modtime time.Time) [
|
||||
return allSubtitles
|
||||
}
|
||||
|
||||
// DetectEmbeddedSubtitles detects embedded subtitle streams using ffprobe.
|
||||
// This is the public API that can be called from other packages.
|
||||
// Returns empty array if ffprobe is not available or fails.
|
||||
func DetectEmbeddedSubtitles(videoPath string, modtime time.Time) []utils.SubtitleTrack {
|
||||
// Check cache first
|
||||
key := "embedded_subtitles:" + videoPath + ":" + modtime.Format(time.RFC3339)
|
||||
if cached, ok := MediaCache.Get(key); ok {
|
||||
return cached
|
||||
}
|
||||
// Detect embedded subtitles
|
||||
subtitles := detectEmbeddedSubtitles(videoPath)
|
||||
// Cache the results
|
||||
MediaCache.Set(key, subtitles)
|
||||
return subtitles
|
||||
}
|
||||
|
||||
// detectEmbeddedSubtitles uses ffprobe to find embedded subtitle tracks
|
||||
// Always runs ffprobe - results are cached for performance
|
||||
func detectEmbeddedSubtitles(realPath string) []SubtitleTrack {
|
||||
func detectEmbeddedSubtitles(realPath string) []utils.SubtitleTrack {
|
||||
cmd := exec.Command("ffprobe",
|
||||
"-v", "quiet",
|
||||
"-print_format", "json",
|
||||
@@ -82,13 +88,13 @@ func detectEmbeddedSubtitles(realPath string) []SubtitleTrack {
|
||||
return nil
|
||||
}
|
||||
|
||||
var subtitles []SubtitleTrack
|
||||
var subtitles []utils.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
|
||||
track := utils.SubtitleTrack{
|
||||
Index: &stream.Index,
|
||||
Codec: stream.CodecName,
|
||||
Embedded: true, // This is an embedded subtitle
|
||||
}
|
||||
|
||||
// Extract language and title from tags
|
||||
@@ -117,8 +123,8 @@ func detectEmbeddedSubtitles(realPath string) []SubtitleTrack {
|
||||
}
|
||||
|
||||
// detectExternalSubtitles finds external subtitle files in the same directory
|
||||
func detectExternalSubtitles(videoPath string, parentDir string) []SubtitleTrack {
|
||||
var subtitles []SubtitleTrack
|
||||
func detectExternalSubtitles(videoPath string, parentDir string) []utils.SubtitleTrack {
|
||||
var subtitles []utils.SubtitleTrack
|
||||
|
||||
// Get the base name of the video (without extension)
|
||||
videoBaseName := strings.TrimSuffix(filepath.Base(videoPath), filepath.Ext(videoPath))
|
||||
@@ -131,9 +137,9 @@ func detectExternalSubtitles(videoPath string, parentDir string) []SubtitleTrack
|
||||
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
|
||||
track := utils.SubtitleTrack{
|
||||
Name: filepath.Base(subtitlePath),
|
||||
Embedded: false, // This is an external file
|
||||
}
|
||||
|
||||
// Try to infer language from filename patterns like "video.en.srt"
|
||||
@@ -170,120 +176,11 @@ func ExtractSubtitleContent(videoPath string, streamIndex int) (string, error) {
|
||||
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
|
||||
// LoadSubtitleFile loads a subtitle file and returns its raw content
|
||||
func LoadSubtitleFile(subtitlePath string) (string, error) {
|
||||
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 := SubtitleContentCache.Get(contentKey); 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
|
||||
SubtitleContentCache.Set(contentKey, content)
|
||||
}
|
||||
return nil
|
||||
return string(content), nil
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
libError "errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -398,10 +397,6 @@ func authenticateShareRequest(r *http.Request, l *share.Link) (int, error) {
|
||||
}
|
||||
|
||||
password := r.Header.Get("X-SHARE-PASSWORD")
|
||||
password, err := url.QueryUnescape(password)
|
||||
if err != nil {
|
||||
return http.StatusUnauthorized, err
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(l.PasswordHash), []byte(password)); err != nil {
|
||||
if libError.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
|
||||
return http.StatusUnauthorized, nil
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -556,7 +555,10 @@ func mergeGroupsByChecksum(groups []duplicateGroupWithChecksums) []duplicateGrou
|
||||
|
||||
func prepDuplicatesOptions(r *http.Request, d *requestContext) (*duplicatesOptions, error) {
|
||||
source := r.URL.Query().Get("source")
|
||||
scope := r.URL.Query().Get("scope")
|
||||
scope, err := utils.SanitizeUserPath(r.URL.Query().Get("scope"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid scope: %v", err)
|
||||
}
|
||||
|
||||
minSizeMbStr := r.URL.Query().Get("minSizeMb")
|
||||
// First pass uses filename matching for speed, then checksums verify final groups
|
||||
@@ -565,19 +567,14 @@ func prepDuplicatesOptions(r *http.Request, d *requestContext) (*duplicatesOptio
|
||||
// Default minimum size: 1MB
|
||||
minSizeMb := int64(1)
|
||||
if minSizeMbStr != "" {
|
||||
if _, err := fmt.Sscanf(minSizeMbStr, "%d", &minSizeMb); err != nil {
|
||||
if _, err = fmt.Sscanf(minSizeMbStr, "%d", &minSizeMb); err != nil {
|
||||
return nil, fmt.Errorf("invalid minSizeMb parameter: %w", err)
|
||||
}
|
||||
}
|
||||
// Convert MB to bytes
|
||||
minSize := minSizeMb * 1024 * 1024
|
||||
|
||||
unencodedScope, err := url.PathUnescape(scope)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
|
||||
searchScope := strings.TrimPrefix(unencodedScope, ".")
|
||||
searchScope := strings.TrimPrefix(scope, ".")
|
||||
|
||||
index := indexing.GetIndex(source)
|
||||
if index == nil {
|
||||
|
||||
@@ -137,8 +137,12 @@ func fileWatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if path == "" || source == "" {
|
||||
return http.StatusBadRequest, fmt.Errorf("path and source are required")
|
||||
}
|
||||
|
||||
var err error
|
||||
path, err = utils.SanitizeUserPath(path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
|
||||
// Parse lines parameter
|
||||
lines := 10 // default
|
||||
if linesStr != "" {
|
||||
@@ -282,6 +286,13 @@ func fileWatchSSEHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
return http.StatusBadRequest, fmt.Errorf("path and source are required")
|
||||
}
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
path = cleanPath
|
||||
|
||||
// Parse lines parameter
|
||||
lines := 10 // default
|
||||
if linesStr != "" {
|
||||
|
||||
+54
-25
@@ -4,24 +4,26 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"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
|
||||
// subtitlesHandler handles subtitle requests for both external files and embedded streams
|
||||
// @Summary Get subtitle content
|
||||
// @Description Returns raw subtitle content from external files or embedded streams
|
||||
// @Tags Subtitles
|
||||
// @Accept json
|
||||
// @Produce text/vtt
|
||||
// @Produce text/plain
|
||||
// @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"
|
||||
// @Param name query string true "Subtitle track name (filename for external, descriptive name for embedded)"
|
||||
// @Param embedded query bool false "Whether this is an embedded stream (true) or external file (false), defaults to false"
|
||||
// @Success 200 {string} string "Raw subtitle content in original format"
|
||||
// @Failure 400 {object} map[string]string "Bad request"
|
||||
// @Failure 403 {object} map[string]string "Forbidden"
|
||||
// @Failure 404 {object} map[string]string "Resource not found"
|
||||
@@ -30,10 +32,11 @@ import (
|
||||
func subtitlesHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
path := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
indexParam := r.URL.Query().Get("index")
|
||||
name := r.URL.Query().Get("name")
|
||||
embedded := r.URL.Query().Get("embedded") == "true"
|
||||
|
||||
if indexParam == "" {
|
||||
indexParam = "0" // default to first subtitle stream
|
||||
if name == "" {
|
||||
return http.StatusBadRequest, fmt.Errorf("name parameter is required")
|
||||
}
|
||||
|
||||
userscope, err := d.user.GetScopeForSourceName(source)
|
||||
@@ -45,28 +48,54 @@ func subtitlesHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
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, false)
|
||||
if !exists {
|
||||
return http.StatusNotFound, fmt.Errorf("file not found: %v", err)
|
||||
|
||||
parentDir := filepath.Dir(realPath)
|
||||
var content string
|
||||
|
||||
if !embedded {
|
||||
content, err = utils.GetSubtitleSidecarContent(filepath.Join(parentDir, name))
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to get subtitle sidecar content: %v", err)
|
||||
}
|
||||
} else {
|
||||
// For embedded subtitles, we need to find the stream index by name
|
||||
// Get file modification time for caching
|
||||
fileInfo, err := os.Stat(realPath)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to stat file: %v", err)
|
||||
}
|
||||
|
||||
// Detect embedded subtitles
|
||||
embeddedSubs := ffmpeg.DetectEmbeddedSubtitles(realPath, fileInfo.ModTime())
|
||||
|
||||
// Find the subtitle track by name
|
||||
var streamIndex *int
|
||||
for _, sub := range embeddedSubs {
|
||||
if sub.Name == name {
|
||||
streamIndex = sub.Index
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if streamIndex == nil {
|
||||
return http.StatusNotFound, fmt.Errorf("embedded subtitle track '%s' not found", name)
|
||||
}
|
||||
|
||||
content, err = ffmpeg.ExtractSubtitleContent(realPath, *streamIndex)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("failed to extract embedded subtitle: %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")
|
||||
// Return raw content with appropriate content type
|
||||
w.Header().Set("Content-Type", "text/plain; 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)))
|
||||
http.ServeContent(w, r, name, time.Now(), bytes.NewReader([]byte(content)))
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
@@ -80,6 +80,14 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, providedPath)
|
||||
return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required")
|
||||
}
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(providedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
providedPath = cleanPath
|
||||
|
||||
themeMode := utils.Ternary(d.user.DarkMode, "dark", "light")
|
||||
var sourceInfo *settings.Source
|
||||
var ok bool
|
||||
@@ -102,7 +110,8 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
// Build file info based on whether this is a share or regular request
|
||||
// Regular user request
|
||||
logger.Debugf("OnlyOffice user request: request path=%s", path)
|
||||
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
|
||||
var fileInfo *iteminfo.ExtendedFileInfo
|
||||
fileInfo, err = files.FileInfoFaster(utils.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
@@ -336,6 +345,15 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
|
||||
logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, path)
|
||||
return returnOnlyOfficeError(w, r, 400, "missing required parameters: path + source/hash are required")
|
||||
}
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(path)
|
||||
if err != nil {
|
||||
logger.Errorf("OnlyOffice callback: invalid path: %v", err)
|
||||
return returnOnlyOfficeError(w, r, 400, "invalid path")
|
||||
}
|
||||
path = cleanPath
|
||||
|
||||
var sourceInfo *settings.Source
|
||||
var ok bool
|
||||
if d.fileInfo.Hash != "" {
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/preview"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
@@ -87,10 +86,6 @@ func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
}
|
||||
|
||||
func rawFileHandler(w http.ResponseWriter, r *http.Request, file iteminfo.ExtendedFileInfo) (int, error) {
|
||||
idx := indexing.GetIndex(file.Source)
|
||||
if idx == nil {
|
||||
return http.StatusNotFound, fmt.Errorf("source not found: %s", file.Source)
|
||||
}
|
||||
fd, err := os.Open(file.RealPath)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
@@ -162,7 +157,7 @@ func getDirectoryPreview(r *http.Request, d *requestContext) (*iteminfo.Extended
|
||||
|
||||
func previewHelperFunc(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
previewSize := r.URL.Query().Get("size")
|
||||
if !(previewSize == "large" || previewSize == "original") {
|
||||
if !(previewSize == "large" || previewSize == "original" || previewSize == "xlarge") {
|
||||
previewSize = "small"
|
||||
}
|
||||
if !d.fileInfo.HasPreview {
|
||||
@@ -183,9 +178,13 @@ func previewHelperFunc(w http.ResponseWriter, r *http.Request, d *requestContext
|
||||
isImage := strings.HasPrefix(d.fileInfo.Type, "image")
|
||||
ext := strings.ToLower(filepath.Ext(d.fileInfo.Name))
|
||||
resizable := iteminfo.ResizableImageTypes[ext]
|
||||
if (!resizable || config.Server.DisableResize) && isImage {
|
||||
|
||||
// If image is under 500KB, serve the original instead of generating a preview
|
||||
const maxSizeForOriginal = 500 * 1024 // 500KB
|
||||
if (!resizable || config.Server.DisableResize || d.fileInfo.Size < maxSizeForOriginal) && isImage {
|
||||
return rawFileHandler(w, r, d.fileInfo)
|
||||
}
|
||||
|
||||
seekPercentage := 0
|
||||
percentage := r.URL.Query().Get("atPercentage")
|
||||
if percentage != "" {
|
||||
|
||||
+43
-20
@@ -76,8 +76,8 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
}
|
||||
|
||||
// Get all "file" parameter values (supports repeated params)
|
||||
encodedFiles := r.URL.Query()["file"]
|
||||
if len(encodedFiles) == 0 {
|
||||
files := r.URL.Query()["file"]
|
||||
if len(files) == 0 {
|
||||
return http.StatusBadRequest, fmt.Errorf("no files specified")
|
||||
}
|
||||
|
||||
@@ -88,16 +88,17 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
}
|
||||
actualSourceName := sourceInfo.Name
|
||||
|
||||
// Decode and process each file path
|
||||
// Process each file path
|
||||
fileList := []string{}
|
||||
for _, encodedFile := range encodedFiles {
|
||||
// Decode the URL-encoded path - use PathUnescape to preserve + as literal character
|
||||
file, decodeErr := url.PathUnescape(encodedFile)
|
||||
if decodeErr != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", decodeErr)
|
||||
for _, file := range files {
|
||||
// Rule 1: Validate each file path to prevent path traversal
|
||||
cleanFile, err := utils.SanitizeUserPath(file)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid file path: %v", err)
|
||||
}
|
||||
|
||||
// Join the share path with the requested path
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, file)
|
||||
filePath := utils.JoinPathAsUnix(d.share.Path, cleanFile)
|
||||
fileList = append(fileList, filePath)
|
||||
}
|
||||
|
||||
@@ -106,8 +107,8 @@ func publicRawHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
if err == errors.ErrDownloadNotAllowed {
|
||||
return http.StatusForbidden, errors.ErrDownloadNotAllowed
|
||||
}
|
||||
logger.Errorf("public share handler: error processing filelist: %v with error %v", encodedFiles, err)
|
||||
return status, fmt.Errorf("error processing filelist: %v", encodedFiles)
|
||||
logger.Errorf("public share handler: error processing filelist: %v with error %v", files, err)
|
||||
return status, fmt.Errorf("error processing filelist: %v", files)
|
||||
}
|
||||
return status, nil
|
||||
}
|
||||
@@ -255,7 +256,14 @@ func publicPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
}
|
||||
// Go automatically decodes query params
|
||||
path := r.URL.Query().Get("path")
|
||||
resolvedPath := utils.JoinPathAsUnix(d.share.Path, path)
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
|
||||
resolvedPath := utils.JoinPathAsUnix(d.share.Path, cleanPath)
|
||||
err = files.WriteFile(source, resolvedPath, r.Body)
|
||||
// hide the error
|
||||
if err != nil {
|
||||
@@ -384,15 +392,15 @@ func publicPatchHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
return status, err
|
||||
}
|
||||
|
||||
// getShareImage serves banner or favicon files for shares
|
||||
// @Summary Get share image (banner or favicon)
|
||||
// @Description Serves the banner or favicon file for a share
|
||||
// getShareImage serves banner or favicon files for shares as resizable previews
|
||||
// @Summary Get share image (banner or favicon) as preview
|
||||
// @Description Returns a resizable preview (large size) for the banner or favicon file of a share
|
||||
// @Tags Public Shares
|
||||
// @Produce octet-stream
|
||||
// @Produce image/jpeg
|
||||
// @Param hash query string true "Share hash"
|
||||
// @Param banner query bool false "Request banner file"
|
||||
// @Param favicon query bool false "Request favicon file"
|
||||
// @Success 200 {file} binary "Image file content"
|
||||
// @Success 200 {file} file "Preview image content (JPEG)"
|
||||
// @Failure 400 {object} map[string]string "Invalid request"
|
||||
// @Failure 403 {object} map[string]string "Permission denied"
|
||||
// @Failure 404 {object} map[string]string "Asset not found"
|
||||
@@ -437,9 +445,24 @@ func getShareImage(w http.ResponseWriter, r *http.Request, d *requestContext) (i
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid file type, must be image")
|
||||
}
|
||||
|
||||
// Serve the file
|
||||
http.ServeFile(w, r, fileInfo.RealPath)
|
||||
return 0, nil
|
||||
// Set file info in request context for preview generation
|
||||
d.fileInfo = *fileInfo
|
||||
q := r.URL.Query()
|
||||
if isBanner {
|
||||
q.Set("size", "xlarge")
|
||||
} else {
|
||||
q.Set("size", "small")
|
||||
}
|
||||
r.URL.RawQuery = q.Encode()
|
||||
|
||||
// Use the preview helper to generate and serve a resized preview
|
||||
status, err := previewHelperFunc(w, r, d)
|
||||
if err != nil {
|
||||
logger.Errorf("error generating preview for share asset: source=%v path=%v error=%v", sourceName, assetPath, err)
|
||||
return http.StatusNotFound, fmt.Errorf("preview not available for this asset")
|
||||
}
|
||||
|
||||
return status, err
|
||||
}
|
||||
|
||||
func getShareImagePartsHelper(share *share.Link, isBanner bool) (string, string, error) {
|
||||
|
||||
@@ -113,6 +113,16 @@ func setContentDisposition(w http.ResponseWriter, r *http.Request, fileName stri
|
||||
func rawHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
source := r.URL.Query().Get("source")
|
||||
fileList := r.URL.Query()["file"]
|
||||
|
||||
// Rule 1: Validate all user-provided file paths to prevent path traversal
|
||||
for i, filePath := range fileList {
|
||||
cleanPath, err := utils.SanitizeUserPath(filePath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid file path: %v", err)
|
||||
}
|
||||
fileList[i] = cleanPath
|
||||
}
|
||||
|
||||
return rawFilesHandler(w, r, d, source, fileList)
|
||||
}
|
||||
|
||||
|
||||
+32
-11
@@ -382,6 +382,14 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
path := r.URL.Query().Get("path")
|
||||
source := r.URL.Query().Get("source")
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
path = cleanPath
|
||||
|
||||
if d.share == nil && !d.user.Permissions.Create {
|
||||
return http.StatusForbidden, fmt.Errorf("user is not allowed to create or modify")
|
||||
}
|
||||
@@ -574,6 +582,13 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
source := r.URL.Query().Get("source")
|
||||
path := r.URL.Query().Get("path")
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(path)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
path = cleanPath
|
||||
|
||||
// Only allow PUT for files.
|
||||
if strings.HasSuffix(path, "/") {
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
@@ -641,6 +656,7 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
|
||||
// Process each item
|
||||
for _, item := range req.Items {
|
||||
|
||||
// Validate all fields are provided
|
||||
if item.FromSource == "" || item.FromPath == "" || item.ToSource == "" || item.ToPath == "" {
|
||||
item.Message = "fromSource, fromPath, toSource, and toPath are required"
|
||||
@@ -653,26 +669,26 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
response.Failed = append(response.Failed, item)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for root path modifications
|
||||
if item.ToPath == "/" || item.FromPath == "/" {
|
||||
item.Message = "cannot modify root directory"
|
||||
if d.share != nil {
|
||||
response.Failed = append(response.Failed, MoveCopyItem{
|
||||
Message: item.Message,
|
||||
})
|
||||
continue
|
||||
}
|
||||
cleanFromPath, err := utils.SanitizeUserPath(item.FromPath)
|
||||
if err != nil {
|
||||
item.Message = fmt.Sprintf("invalid fromPath: %v", err)
|
||||
response.Failed = append(response.Failed, item)
|
||||
continue
|
||||
}
|
||||
cleanToPath, err := utils.SanitizeUserPath(item.ToPath)
|
||||
if err != nil {
|
||||
item.Message = fmt.Sprintf("invalid toPath: %v", err)
|
||||
response.Failed = append(response.Failed, item)
|
||||
continue
|
||||
}
|
||||
item.FromPath = cleanFromPath
|
||||
item.ToPath = cleanToPath
|
||||
|
||||
// Get user scopes for both sources
|
||||
// For shares, paths are already absolute, so use empty scope
|
||||
userscopeSrc := ""
|
||||
userscopeDst := ""
|
||||
if d.share == nil {
|
||||
var err error
|
||||
userscopeSrc, err = d.user.GetScopeForSourceName(item.FromSource)
|
||||
if err != nil {
|
||||
item.Message = "source not available"
|
||||
@@ -718,6 +734,11 @@ func resourcePatchHandler(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
// Build full index paths for access control
|
||||
fullSrcIndexPath := utils.JoinPathAsUnix(userscopeSrc, item.FromPath)
|
||||
fullDstIndexPath := utils.JoinPathAsUnix(userscopeDst, item.ToPath)
|
||||
if fullDstIndexPath == "/" || fullSrcIndexPath == "/" {
|
||||
item.Message = "source or destination is the root or unautharized directory"
|
||||
response.Failed = append(response.Failed, item)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check access control for both source and destination paths
|
||||
if !store.Access.Permitted(srcIdx.Path, fullSrcIndexPath, d.user.Username) {
|
||||
|
||||
@@ -122,6 +122,11 @@ func prepSearchOptions(r *http.Request, d *requestContext) (*searchOptions, erro
|
||||
sourceParam := r.URL.Query().Get("source") // deprecated, but still supported
|
||||
scope := r.URL.Query().Get("scope") // Go automatically decodes query params
|
||||
largest := r.URL.Query().Get("largest") == "true"
|
||||
cleanScope, err := utils.SanitizeUserPath(scope)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid scope: %v", err)
|
||||
}
|
||||
scope = cleanScope
|
||||
|
||||
var sources []string
|
||||
if sourcesParam != "" {
|
||||
|
||||
@@ -116,13 +116,8 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
// @Failure 500 {object} map[string]string "Internal server error"
|
||||
// @Router /api/share [get]
|
||||
func shareGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
path := r.URL.Query().Get("path")
|
||||
sourceName := r.URL.Query().Get("source")
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.PathUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
sourceInfo, ok := config.Server.NameToSource[sourceName] // backend source is path
|
||||
if !ok {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid source name: %s", sourceName)
|
||||
@@ -363,7 +358,14 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
providedPath := body.Path
|
||||
body.Path = utils.JoinPathAsUnix(userscope, providedPath)
|
||||
|
||||
// Rule 1: Validate user-provided path to prevent path traversal
|
||||
cleanPath, err := utils.SanitizeUserPath(providedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path: %v", err)
|
||||
}
|
||||
|
||||
body.Path = utils.JoinPathAsUnix(userscope, cleanPath)
|
||||
body.Path = utils.AddTrailingSlashIfNotExists(body.Path)
|
||||
// validate path exists as file or folder
|
||||
_, _, err = idx.GetRealPath(body.Path)
|
||||
|
||||
@@ -111,7 +111,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
if d.share.DisableSidebar {
|
||||
disableSidebar = true
|
||||
}
|
||||
if strings.HasPrefix(d.share.Banner, "http") {
|
||||
if strings.HasPrefix(strings.ToLower(d.share.Banner), "http") {
|
||||
banner = d.share.Banner
|
||||
} else {
|
||||
_, _, err := getShareImagePartsHelper(d.share, true)
|
||||
@@ -119,7 +119,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
banner = fmt.Sprintf("%s%spublic/api/share/image?banner=true&hash=%s", config.Server.ExternalUrl, config.Server.BaseURL, d.share.Hash)
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(d.share.Favicon, "http") {
|
||||
if strings.HasPrefix(strings.ToLower(d.share.Favicon), "http") {
|
||||
favicon = d.share.Favicon
|
||||
} else {
|
||||
_, _, err := getShareImagePartsHelper(d.share, false)
|
||||
|
||||
@@ -3,7 +3,7 @@ package iteminfo
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
)
|
||||
|
||||
type ItemInfo struct {
|
||||
@@ -47,14 +47,14 @@ type MediaMetadata 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 []ffmpeg.SubtitleTrack `json:"subtitles,omitempty"` // subtitles for video files
|
||||
Metadata *MediaMetadata `json:"metadata,omitempty"` // media metadata for audio/video files (includes duration)
|
||||
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
|
||||
HasMetadata bool `json:"hasMetadata"` // whether the file or folder has metadata
|
||||
RealPath string `json:"-"`
|
||||
Content string `json:"content,omitempty"` // text content of a file, if requested
|
||||
Subtitles []utils.SubtitleTrack `json:"subtitles,omitempty"` // subtitles for video files
|
||||
Metadata *MediaMetadata `json:"metadata,omitempty"` // media metadata for audio/video files (includes duration)
|
||||
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
|
||||
HasMetadata bool `json:"hasMetadata"` // whether the file or folder has metadata
|
||||
RealPath string `json:"-"`
|
||||
}
|
||||
@@ -8,19 +8,9 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
)
|
||||
|
||||
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"`
|
||||
@@ -31,20 +21,55 @@ type FFProbeOutput struct {
|
||||
} `json:"streams"`
|
||||
}
|
||||
|
||||
// detects subtitles for video files.
|
||||
func (i *ExtendedFileInfo) DetectSubtitles(parentInfo *FileInfo) {
|
||||
// GetSubtitles detects external subtitle files for video files.
|
||||
// Embedded subtitles should be detected by ffmpeg and passed as embeddedSubs parameter.
|
||||
func (i *ExtendedFileInfo) GetSubtitles(parentInfo *FileInfo) {
|
||||
if !strings.HasPrefix(i.Type, "video") {
|
||||
logger.Debug("subtitles are not supported for this file : " + i.Name)
|
||||
return
|
||||
}
|
||||
// Use unified subtitle detection that finds both embedded and external
|
||||
parentDir := filepath.Dir(i.RealPath)
|
||||
i.Subtitles = ffmpeg.DetectAllSubtitles(i.RealPath, parentDir, i.ModTime)
|
||||
}
|
||||
ext := filepath.Ext(i.Name)
|
||||
baseWithoutExt := strings.TrimSuffix(i.Name, ext)
|
||||
|
||||
// LoadSubtitleContent loads the actual content for all detected subtitle tracks
|
||||
func (i *ExtendedFileInfo) LoadSubtitleContent() error {
|
||||
return ffmpeg.LoadAllSubtitleContent(i.RealPath, i.Subtitles, i.ModTime)
|
||||
// Collect external subtitle files
|
||||
var externalSubs []utils.SubtitleTrack
|
||||
if parentInfo != nil && parentInfo.Files != nil {
|
||||
for _, f := range parentInfo.Files {
|
||||
fileExt := filepath.Ext(f.Name)
|
||||
fileBase := strings.TrimSuffix(f.Name, fileExt)
|
||||
|
||||
// Check if this file has the same base name and a subtitle extension
|
||||
if fileBase == baseWithoutExt && SubtitleExts[strings.ToLower(fileExt)] {
|
||||
track := utils.SubtitleTrack{
|
||||
Name: f.Name,
|
||||
Embedded: false,
|
||||
}
|
||||
|
||||
// Try to infer language from filename patterns like "video.en.srt"
|
||||
parts := strings.Split(fileBase, ".")
|
||||
if len(parts) > 1 {
|
||||
lastPart := parts[len(parts)-1]
|
||||
if len(lastPart) == 2 || len(lastPart) == 3 {
|
||||
track.Language = lastPart
|
||||
}
|
||||
}
|
||||
|
||||
externalSubs = append(externalSubs, track)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort external subtitles alphabetically for consistent ordering
|
||||
sort.Slice(externalSubs, func(i, j int) bool {
|
||||
return externalSubs[i].Name < externalSubs[j].Name
|
||||
})
|
||||
|
||||
i.Subtitles = append(i.Subtitles, externalSubs...)
|
||||
|
||||
// Sort all subtitles for consistent ordering
|
||||
sort.Slice(i.Subtitles, func(a, b int) bool {
|
||||
return strings.ToLower(i.Subtitles[a].Name) < strings.ToLower(i.Subtitles[b].Name)
|
||||
})
|
||||
// Note: Content is NOT loaded here. Frontend will call /api/media/subtitles for each track
|
||||
}
|
||||
|
||||
func (info *FileInfo) SortItems() {
|
||||
|
||||
@@ -520,6 +520,14 @@ func GeneratePreview(ctx context.Context, file iteminfo.ExtendedFileInfo, previe
|
||||
// getPreviewOptions returns resize options for the given preview size
|
||||
func getPreviewOptions(previewSize string) (ResizeOptions, error) {
|
||||
switch previewSize {
|
||||
case "xlarge":
|
||||
return ResizeOptions{
|
||||
Width: 1024,
|
||||
Height: 1024,
|
||||
ResizeMode: ResizeModeFit,
|
||||
Quality: QualityHigh,
|
||||
Format: FormatJpeg,
|
||||
}, nil
|
||||
case "large":
|
||||
return ResizeOptions{
|
||||
Width: 640,
|
||||
|
||||
@@ -1105,17 +1105,17 @@ const docTemplate = `{
|
||||
},
|
||||
"/api/media/subtitles": {
|
||||
"get": {
|
||||
"description": "Extracts embedded subtitle content from video files by stream index and returns raw WebVTT content",
|
||||
"description": "Returns raw subtitle content from external files or embedded streams",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"text/vtt"
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"Subtitles"
|
||||
],
|
||||
"summary": "Extract embedded subtitles",
|
||||
"summary": "Get subtitle content",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -1132,15 +1132,22 @@ const docTemplate = `{
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Stream index for embedded subtitle extraction, defaults to 0",
|
||||
"name": "index",
|
||||
"type": "string",
|
||||
"description": "Subtitle track name (filename for external, descriptive name for embedded)",
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Whether this is an embedded stream (true) or external file (false), defaults to false",
|
||||
"name": "embedded",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Raw WebVTT subtitle content",
|
||||
"description": "Raw subtitle content in original format",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -3312,14 +3319,14 @@ const docTemplate = `{
|
||||
},
|
||||
"/public/api/share/image": {
|
||||
"get": {
|
||||
"description": "Serves the banner or favicon file for a share",
|
||||
"description": "Returns a resizable preview (large size) for the banner or favicon file of a share",
|
||||
"produces": [
|
||||
"application/octet-stream"
|
||||
"image/jpeg"
|
||||
],
|
||||
"tags": [
|
||||
"Public Shares"
|
||||
],
|
||||
"summary": "Get share image (banner or favicon)",
|
||||
"summary": "Get share image (banner or favicon) as preview",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -3343,7 +3350,7 @@ const docTemplate = `{
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Image file content",
|
||||
"description": "Preview image content (JPEG)",
|
||||
"schema": {
|
||||
"type": "file"
|
||||
}
|
||||
|
||||
@@ -1094,17 +1094,17 @@
|
||||
},
|
||||
"/api/media/subtitles": {
|
||||
"get": {
|
||||
"description": "Extracts embedded subtitle content from video files by stream index and returns raw WebVTT content",
|
||||
"description": "Returns raw subtitle content from external files or embedded streams",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"text/vtt"
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"Subtitles"
|
||||
],
|
||||
"summary": "Extract embedded subtitles",
|
||||
"summary": "Get subtitle content",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -1121,15 +1121,22 @@
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Stream index for embedded subtitle extraction, defaults to 0",
|
||||
"name": "index",
|
||||
"type": "string",
|
||||
"description": "Subtitle track name (filename for external, descriptive name for embedded)",
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"description": "Whether this is an embedded stream (true) or external file (false), defaults to false",
|
||||
"name": "embedded",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Raw WebVTT subtitle content",
|
||||
"description": "Raw subtitle content in original format",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -3301,14 +3308,14 @@
|
||||
},
|
||||
"/public/api/share/image": {
|
||||
"get": {
|
||||
"description": "Serves the banner or favicon file for a share",
|
||||
"description": "Returns a resizable preview (large size) for the banner or favicon file of a share",
|
||||
"produces": [
|
||||
"application/octet-stream"
|
||||
"image/jpeg"
|
||||
],
|
||||
"tags": [
|
||||
"Public Shares"
|
||||
],
|
||||
"summary": "Get share image (banner or favicon)",
|
||||
"summary": "Get share image (banner or favicon) as preview",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
@@ -3332,7 +3339,7 @@
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Image file content",
|
||||
"description": "Preview image content (JPEG)",
|
||||
"schema": {
|
||||
"type": "file"
|
||||
}
|
||||
|
||||
@@ -2499,8 +2499,7 @@ paths:
|
||||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Extracts embedded subtitle content from video files by stream index
|
||||
and returns raw WebVTT content
|
||||
description: Returns raw subtitle content from external files or embedded streams
|
||||
parameters:
|
||||
- description: Index path to the video file
|
||||
in: query
|
||||
@@ -2512,15 +2511,22 @@ paths:
|
||||
name: source
|
||||
required: true
|
||||
type: string
|
||||
- description: Stream index for embedded subtitle extraction, defaults to 0
|
||||
- description: Subtitle track name (filename for external, descriptive name
|
||||
for embedded)
|
||||
in: query
|
||||
name: index
|
||||
type: integer
|
||||
name: name
|
||||
required: true
|
||||
type: string
|
||||
- description: Whether this is an embedded stream (true) or external file (false),
|
||||
defaults to false
|
||||
in: query
|
||||
name: embedded
|
||||
type: boolean
|
||||
produces:
|
||||
- text/vtt
|
||||
- text/plain
|
||||
responses:
|
||||
"200":
|
||||
description: Raw WebVTT subtitle content
|
||||
description: Raw subtitle content in original format
|
||||
schema:
|
||||
type: string
|
||||
"400":
|
||||
@@ -2547,7 +2553,7 @@ paths:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Extract embedded subtitles
|
||||
summary: Get subtitle content
|
||||
tags:
|
||||
- Subtitles
|
||||
/api/preview:
|
||||
@@ -4020,7 +4026,8 @@ paths:
|
||||
- Public Shares
|
||||
/public/api/share/image:
|
||||
get:
|
||||
description: Serves the banner or favicon file for a share
|
||||
description: Returns a resizable preview (large size) for the banner or favicon
|
||||
file of a share
|
||||
parameters:
|
||||
- description: Share hash
|
||||
in: query
|
||||
@@ -4036,10 +4043,10 @@ paths:
|
||||
name: favicon
|
||||
type: boolean
|
||||
produces:
|
||||
- application/octet-stream
|
||||
- image/jpeg
|
||||
responses:
|
||||
"200":
|
||||
description: Image file content
|
||||
description: Preview image content (JPEG)
|
||||
schema:
|
||||
type: file
|
||||
"400":
|
||||
@@ -4060,7 +4067,7 @@ paths:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
summary: Get share image (banner or favicon)
|
||||
summary: Get share image (banner or favicon) as preview
|
||||
tags:
|
||||
- Public Shares
|
||||
/public/api/shareinfo:
|
||||
|
||||
+64
-52
@@ -12,8 +12,8 @@ export async function fetchFiles(source, path, content = false, metadata = false
|
||||
}
|
||||
try {
|
||||
const apiPath = getApiPath('api/resources', {
|
||||
path: encodeURIComponent(path),
|
||||
source: encodeURIComponent(source),
|
||||
path: path,
|
||||
source: source,
|
||||
...(content && { content: 'true' }),
|
||||
...(metadata && { metadata: 'true' })
|
||||
})
|
||||
@@ -32,8 +32,6 @@ async function resourceAction(source, path, method, content) {
|
||||
throw new Error('no source provided')
|
||||
}
|
||||
try {
|
||||
source = encodeURIComponent(source)
|
||||
path = encodeURIComponent(path)
|
||||
const apiPath = getApiPath('api/resources', { path, source })
|
||||
let opts = { method }
|
||||
if (content) {
|
||||
@@ -76,15 +74,12 @@ export async function bulkDelete(items) {
|
||||
},
|
||||
body: JSON.stringify(items),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// 200 = all succeeded, 207 = partial success (some succeeded, some failed)
|
||||
// Both are valid responses that should be returned, not thrown as errors
|
||||
if (response.status === 200 || response.status === 207) {
|
||||
return data
|
||||
}
|
||||
|
||||
// For other error status codes, throw an error
|
||||
const error = new Error(data.message || response.statusText)
|
||||
error.status = response.status
|
||||
@@ -110,12 +105,12 @@ export async function download(format, files, shareHash = "") {
|
||||
// Check if chunked download should be used (single file only)
|
||||
const downloadChunkSizeMb = state.user?.fileLoading?.downloadChunkSizeMb || 0
|
||||
const sizeThreshold = downloadChunkSizeMb * 1024 * 1024;
|
||||
|
||||
const useChunkedDownload =
|
||||
downloadChunkSizeMb > 0 &&
|
||||
files.length === 1 &&
|
||||
!files[0].isDir &&
|
||||
files[0].size &&
|
||||
|
||||
const useChunkedDownload =
|
||||
downloadChunkSizeMb > 0 &&
|
||||
files.length === 1 &&
|
||||
!files[0].isDir &&
|
||||
files[0].size &&
|
||||
files[0].size >= sizeThreshold
|
||||
|
||||
if (useChunkedDownload) {
|
||||
@@ -127,14 +122,14 @@ export async function download(format, files, shareHash = "") {
|
||||
if (format !== 'zip') {
|
||||
format = 'tar.gz'
|
||||
}
|
||||
|
||||
|
||||
// For non-share downloads, validate single source and build file list
|
||||
let source = null
|
||||
let filePaths = []
|
||||
|
||||
|
||||
if (shareHash) {
|
||||
// For shares, no source parameter needed, just paths
|
||||
filePaths = files.map(file => encodeURIComponent(file.path))
|
||||
filePaths = files.map(file => file.path)
|
||||
} else {
|
||||
// Validate all files are from the same source
|
||||
for (let file of files) {
|
||||
@@ -146,19 +141,19 @@ export async function download(format, files, shareHash = "") {
|
||||
} else if (source !== file.source) {
|
||||
throw new Error('All files must be from the same source for downloads')
|
||||
}
|
||||
filePaths.push(encodeURIComponent(file.path))
|
||||
filePaths.push(file.path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const params = {
|
||||
file: filePaths, // Array of file paths - getApiPath will create repeated parameters
|
||||
algo: format,
|
||||
...(shareHash && { hash: shareHash }),
|
||||
...(!shareHash && source && { source: encodeURIComponent(source) }),
|
||||
...(!shareHash && source && { source: source }),
|
||||
...(state.shareInfo.token && { token: state.shareInfo.token }),
|
||||
sessionId: state.sessionId
|
||||
}
|
||||
|
||||
|
||||
const apiPath = getApiPath(shareHash == "" ? 'api/raw' : 'public/api/raw', params)
|
||||
const url = window.origin + apiPath
|
||||
|
||||
@@ -172,7 +167,7 @@ export async function download(format, files, shareHash = "") {
|
||||
link.style.display = 'none'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
|
||||
|
||||
// Clean up after a short delay
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(link)
|
||||
@@ -181,36 +176,36 @@ export async function download(format, files, shareHash = "") {
|
||||
|
||||
async function downloadChunked(file, shareHash = "") {
|
||||
const chunkSizeMb = state.user?.fileLoading?.downloadChunkSizeMb || 0
|
||||
|
||||
|
||||
if (chunkSizeMb === 0) {
|
||||
throw new Error("Chunked download is disabled (chunk size is 0)")
|
||||
}
|
||||
const chunkSize = chunkSizeMb * 1024 * 1024 // Convert MB to bytes
|
||||
const fileSize = file.size
|
||||
|
||||
|
||||
// Extract filename from path if name is not available
|
||||
const fileName = file.name || (file.path ? file.path.split('/').pop() : 'download')
|
||||
|
||||
// Add to download manager
|
||||
const downloadId = downloadManager.add(file, shareHash)
|
||||
|
||||
|
||||
downloadManager.setStatus(downloadId, "downloading")
|
||||
|
||||
|
||||
// Show download prompt if not already shown (it should already be shown by downloadFiles, but check to be safe)
|
||||
const hasDownloadPrompt = state.hovers && state.hovers.some(h => h.name === 'download');
|
||||
|
||||
|
||||
if (!hasDownloadPrompt) {
|
||||
mutations.showHover({ name: 'download' })
|
||||
}
|
||||
|
||||
const params = {
|
||||
file: encodeURIComponent(file.path),
|
||||
file: file.path,
|
||||
...(shareHash && { hash: shareHash }),
|
||||
...(!shareHash && file.source && { source: encodeURIComponent(file.source) }),
|
||||
...(!shareHash && file.source && { source: file.source }),
|
||||
...(state.shareInfo.token && { token: state.shareInfo.token }),
|
||||
sessionId: state.sessionId
|
||||
}
|
||||
|
||||
|
||||
const apiPath = getApiPath(shareHash == "" ? 'api/raw' : 'public/api/raw', params)
|
||||
const baseUrl = window.origin + apiPath
|
||||
|
||||
@@ -225,7 +220,7 @@ async function downloadChunked(file, shareHash = "") {
|
||||
let loaded = 0
|
||||
|
||||
while (offset < fileSize) {
|
||||
|
||||
|
||||
const download = downloadManager.findById(downloadId);
|
||||
if (download && download.status === "cancelled") {
|
||||
// Silently handle cancellation - don't throw error
|
||||
@@ -249,7 +244,7 @@ async function downloadChunked(file, shareHash = "") {
|
||||
|
||||
// Track progress within the chunk using ReadableStream
|
||||
const expectedChunkSize = end - offset + 1;
|
||||
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const chunkParts = [];
|
||||
let chunkLoaded = 0;
|
||||
@@ -264,14 +259,14 @@ async function downloadChunked(file, shareHash = "") {
|
||||
reading = false;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
chunkParts.push(value);
|
||||
chunkLoaded += value.length;
|
||||
|
||||
|
||||
// Calculate progress: only count up to expected chunk size to avoid over-counting
|
||||
const chunkProgress = Math.min(chunkLoaded, expectedChunkSize);
|
||||
const totalLoaded = offset + chunkProgress;
|
||||
|
||||
|
||||
// Update progress in real-time, but throttle updates for performance
|
||||
if (chunkLoaded - lastProgressUpdate >= progressUpdateInterval || chunkLoaded >= expectedChunkSize) {
|
||||
downloadManager.updateProgress(downloadId, totalLoaded, fileSize);
|
||||
@@ -295,19 +290,19 @@ async function downloadChunked(file, shareHash = "") {
|
||||
chunk.set(part, position);
|
||||
position += part.length;
|
||||
}
|
||||
|
||||
|
||||
// Only use the expected chunk size portion if server returned more (handles Range header issues)
|
||||
const chunkToUse = chunk.byteLength > expectedChunkSize
|
||||
? chunk.slice(0, expectedChunkSize).buffer
|
||||
const chunkToUse = chunk.byteLength > expectedChunkSize
|
||||
? chunk.slice(0, expectedChunkSize).buffer
|
||||
: chunk.buffer;
|
||||
|
||||
|
||||
chunks.push(chunkToUse)
|
||||
// Always use expected chunk size for progress to avoid double-counting
|
||||
loaded += expectedChunkSize
|
||||
|
||||
|
||||
// Final progress update for this chunk
|
||||
downloadManager.updateProgress(downloadId, loaded, fileSize)
|
||||
|
||||
|
||||
offset = end + 1
|
||||
}
|
||||
|
||||
@@ -360,8 +355,8 @@ export function post(
|
||||
}
|
||||
try {
|
||||
const apiPath = getApiPath("api/resources", {
|
||||
path: encodeURIComponent(path),
|
||||
source: encodeURIComponent(source),
|
||||
path: path,
|
||||
source: source,
|
||||
override: overwrite,
|
||||
...(isDir && { isDir: 'true' })
|
||||
});
|
||||
@@ -453,7 +448,7 @@ export async function moveCopy(
|
||||
'Content-Type': 'application/json',
|
||||
'sessionId': state.sessionId
|
||||
}
|
||||
|
||||
|
||||
const response = await fetch(apiPath, {
|
||||
method: 'PATCH',
|
||||
headers: headers,
|
||||
@@ -496,8 +491,8 @@ export async function checksum(source, path, algo) {
|
||||
}
|
||||
try {
|
||||
const params = {
|
||||
path: encodeURIComponent(path),
|
||||
source: encodeURIComponent(source),
|
||||
path: path,
|
||||
source: source,
|
||||
checksum: algo
|
||||
}
|
||||
const apiPath = getApiPath('api/resources', params)
|
||||
@@ -516,8 +511,8 @@ export function getDownloadURL(source, path, inline, useExternal) {
|
||||
}
|
||||
try {
|
||||
const params = {
|
||||
source: encodeURIComponent(source),
|
||||
file: encodeURIComponent(path),
|
||||
source: source,
|
||||
file: path,
|
||||
...(inline && { inline: 'true' })
|
||||
}
|
||||
const apiPath = getApiPath('api/raw', params)
|
||||
@@ -537,9 +532,9 @@ export function getPreviewURL(source, path, modified) {
|
||||
}
|
||||
try {
|
||||
const params = {
|
||||
path: encodeURIComponent(path),
|
||||
path: path,
|
||||
key: Date.parse(modified), // Use modified date as cache key
|
||||
source: encodeURIComponent(source),
|
||||
source: source,
|
||||
inline: 'true'
|
||||
}
|
||||
const apiPath = getApiPath('api/preview', params)
|
||||
@@ -568,9 +563,9 @@ export async function sources() {
|
||||
|
||||
export async function GetOfficeConfig(req) {
|
||||
const params = {
|
||||
path: encodeURIComponent(req.path),
|
||||
...(req.hash && { hash: encodeURIComponent(req.hash) }),
|
||||
...(req.source && { source: encodeURIComponent(req.source) })
|
||||
path: req.path,
|
||||
...(req.hash && { hash: req.hash }),
|
||||
...(req.source && { source: req.source })
|
||||
}
|
||||
let apiPath = getApiPath('api/onlyoffice/config', params)
|
||||
if (req.hash) {
|
||||
@@ -578,4 +573,21 @@ export async function GetOfficeConfig(req) {
|
||||
}
|
||||
const res = await fetchURL(apiPath)
|
||||
return await res.json()
|
||||
}
|
||||
|
||||
export async function getSubtitleContent(source, path, subtitleName, embedded = false) {
|
||||
try {
|
||||
const apiPath = getApiPath('api/media/subtitles', {
|
||||
source: source,
|
||||
path: path,
|
||||
name: subtitleName,
|
||||
embedded: embedded.toString()
|
||||
})
|
||||
const res = await fetchURL(apiPath)
|
||||
const content = await res.text()
|
||||
return content
|
||||
} catch (err) {
|
||||
notify.showError(err.message || `Error fetching subtitle ${subtitleName}`)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
+10
-14
@@ -1,6 +1,6 @@
|
||||
import { adjustedData } from "./utils";
|
||||
import { notify } from "@/notify";
|
||||
import { getPublicApiPath, encodedPath } from "@/utils/url.js";
|
||||
import { getPublicApiPath } from "@/utils/url.js";
|
||||
import { state } from "@/store";
|
||||
|
||||
// ============================================================================
|
||||
@@ -17,7 +17,6 @@ import { state } from "@/store";
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function fetchPub(path, hash, password = "", content = false, metadata = false) {
|
||||
path = encodedPath(path);
|
||||
const params = {
|
||||
path: path,
|
||||
hash,
|
||||
@@ -28,7 +27,7 @@ export async function fetchPub(path, hash, password = "", content = false, metad
|
||||
const apiPath = getPublicApiPath("resources", params);
|
||||
const response = await fetch(apiPath, {
|
||||
headers: {
|
||||
"X-SHARE-PASSWORD": password ? encodeURIComponent(password) : "",
|
||||
"X-SHARE-PASSWORD": password || "",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -61,10 +60,8 @@ export async function fetchPub(path, hash, password = "", content = false, metad
|
||||
export function getDownloadURL(share, files, inline=false) {
|
||||
// Handle array of files for repeated 'file' parameters
|
||||
const fileArray = Array.isArray(files) ? files : [files]
|
||||
const filePaths = fileArray.map(file => encodeURIComponent(file))
|
||||
|
||||
const params = {
|
||||
file: filePaths, // Array of file paths - getPublicApiPath will create repeated parameters
|
||||
file: fileArray, // Array of file paths - getPublicApiPath will create repeated parameters
|
||||
hash: share.hash,
|
||||
token: share.token,
|
||||
...(inline && { inline: 'true' })
|
||||
@@ -82,7 +79,7 @@ export function getDownloadURL(share, files, inline=false) {
|
||||
export function getPreviewURL(path, size) {
|
||||
try {
|
||||
const params = {
|
||||
path: encodeURIComponent(path),
|
||||
path: path,
|
||||
hash: state.shareInfo.hash,
|
||||
inline: 'true',
|
||||
// Only add size parameter if specified and not the default
|
||||
@@ -156,13 +153,13 @@ export function post(
|
||||
// If parsing fails, use responseText or default message
|
||||
errorMessage = request.responseText || errorMessage;
|
||||
}
|
||||
|
||||
|
||||
const error = new Error(errorMessage);
|
||||
error.status = request.status;
|
||||
|
||||
|
||||
// Show notification for upload errors
|
||||
notify.showError(errorMessage);
|
||||
|
||||
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
@@ -172,7 +169,7 @@ export function post(
|
||||
notify.showError("Network error during upload");
|
||||
reject(error);
|
||||
};
|
||||
|
||||
|
||||
request.onabort = () => {
|
||||
const error = new Error("Upload aborted");
|
||||
notify.showError("Upload was aborted");
|
||||
@@ -207,7 +204,6 @@ async function resourceAction(hash, path, method, content, token = "") {
|
||||
if (sharePassword) {
|
||||
headers["X-SHARE-PASSWORD"] = sharePassword;
|
||||
}
|
||||
path = encodeURIComponent(path)
|
||||
const apiPath = getPublicApiPath('resources', { path, hash: hash, token: token })
|
||||
const response = await fetch(apiPath, {
|
||||
method,
|
||||
@@ -240,12 +236,12 @@ export async function bulkDelete(items) {
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
throw new Error('items array is required and must not be empty')
|
||||
}
|
||||
|
||||
|
||||
const hash = state.shareInfo?.hash;
|
||||
if (!hash) {
|
||||
throw new Error('share hash is required')
|
||||
}
|
||||
|
||||
|
||||
const params = {
|
||||
hash: hash,
|
||||
...(state.shareInfo.token && { token: state.shareInfo.token }),
|
||||
|
||||
@@ -4,14 +4,12 @@ import { getApiPath } from "@/utils/url.js";
|
||||
|
||||
export default async function search(base, sources, query, largest = false) {
|
||||
try {
|
||||
query = encodeURIComponent(query);
|
||||
|
||||
// Ensure sources is an array
|
||||
const sourcesArray = Array.isArray(sources) ? sources : [sources];
|
||||
|
||||
const params = {
|
||||
query: query,
|
||||
sources: encodeURIComponent(sourcesArray.join(","))
|
||||
sources: sourcesArray.join(",")
|
||||
};
|
||||
|
||||
// Only include scope if searching a single source
|
||||
@@ -20,7 +18,7 @@ export default async function search(base, sources, query, largest = false) {
|
||||
if (!base.endsWith("/")) {
|
||||
base += "/";
|
||||
}
|
||||
params.scope = encodeURIComponent(base);
|
||||
params.scope = base;
|
||||
}
|
||||
|
||||
if (largest) {
|
||||
@@ -44,8 +42,8 @@ export async function findDuplicates(base, source, minSizeMb, useChecksum = fals
|
||||
base += "/";
|
||||
}
|
||||
const params = {
|
||||
scope: encodeURIComponent(base),
|
||||
source: encodeURIComponent(source),
|
||||
scope: base,
|
||||
source: source,
|
||||
minSizeMb: minSizeMb.toString()
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function list() {
|
||||
*/
|
||||
export async function get(path, source) {
|
||||
try {
|
||||
const params = { path: encodeURIComponent(path), source: encodeURIComponent(source) };
|
||||
const params = { path, source };
|
||||
const apiPath = getApiPath("api/share", params);
|
||||
let data = await fetchJSON(apiPath);
|
||||
return adjustedData(data);
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
:modified="item.originalItem?.modified || new Date().toISOString()"
|
||||
:index="index"
|
||||
:path="item.path"
|
||||
:hasPreview="item.originalItem?.hasPreview || false"
|
||||
:hasPreview="item.originalItem?.hasPreview && item.type !== 'directory' || false"
|
||||
:metadata="item.originalItem?.metadata"
|
||||
:hasDuration="item.originalItem?.hasDuration || false"
|
||||
:updateGlobalState="false"
|
||||
|
||||
@@ -27,26 +27,10 @@
|
||||
</div>
|
||||
|
||||
<div v-if="isPickingFile">
|
||||
<file-list
|
||||
@update:selected="updateTempFilePath"
|
||||
:browse-source="displaySource"
|
||||
:browse-path="displayPath"
|
||||
:show-files="true"
|
||||
:show-folders="true"
|
||||
:allowed-file-types="filePickerAllowedTypes"
|
||||
:require-file-selection="true"
|
||||
:title="filePickerTitle">
|
||||
<file-list @update:selected="updateTempFilePath" :browse-source="displaySource" :browse-path="displayPath"
|
||||
:show-files="true" :show-folders="true" :allowed-file-types="filePickerAllowedTypes"
|
||||
:require-file-selection="true" :title="filePickerTitle">
|
||||
</file-list>
|
||||
<div class="card-action">
|
||||
<button class="button button--flat" @click="cancelFilePicker" :aria-label="$t('general.cancel')"
|
||||
:title="$t('general.cancel')">
|
||||
{{ $t("general.cancel") }}
|
||||
</button>
|
||||
<button class="button button--flat button--blue" @click="confirmFilePicker" :disabled="!filePickerSelectionValid" :aria-label="$t('general.ok')"
|
||||
:title="$t('general.ok')">
|
||||
{{ $t("general.ok") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!isPickingFile">
|
||||
@@ -110,8 +94,8 @@
|
||||
</i>
|
||||
</p>
|
||||
<div class="form-flex-group">
|
||||
<input class="form-grow input flat-right" v-focus type="number" max="2147483647" min="0" @keyup.enter="submit"
|
||||
v-model.trim="time" />
|
||||
<input class="form-grow input flat-right" v-focus type="number" max="2147483647" min="0"
|
||||
@keyup.enter="submit" v-model.trim="time" />
|
||||
<select class="flat-left input form-dropdown" v-model="unit" :aria-label="$t('time.unit')">
|
||||
<option value="minutes">{{ $t("time.minutes") }}</option>
|
||||
<option value="hours">{{ $t("time.hours") }}</option>
|
||||
@@ -148,16 +132,19 @@
|
||||
{{ $t('share.customizeSidebarLinksButton') }}
|
||||
</button>
|
||||
<div class="settings-items" style="margin-top: 0.5em;">
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowModify" :name="$t('share.allowModify')"
|
||||
:description="$t('share.allowModifyDescription')" aria-label="allow editing files toggle" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowCreate" :name="$t('share.allowCreate')"
|
||||
:description="$t('share.allowCreateDescription')"
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowModify"
|
||||
:name="$t('share.allowModify')" :description="$t('share.allowModifyDescription')"
|
||||
aria-label="allow editing files toggle" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowCreate"
|
||||
:name="$t('share.allowCreate')" :description="$t('share.allowCreateDescription')"
|
||||
aria-label="allow creating and uploading files and folders toggle" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowDelete" :name="$t('share.allowDelete')"
|
||||
:description="$t('share.allowDeleteDescription')" aria-label="allow deleting files toggle" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="allowDelete"
|
||||
:name="$t('share.allowDelete')" :description="$t('share.allowDeleteDescription')"
|
||||
aria-label="allow deleting files toggle" />
|
||||
</div>
|
||||
</div>
|
||||
<SettingsItem :title="showMoreExpanded ? $t('buttons.showLess') : $t('buttons.showMore')" :collapsable="true" :start-collapsed="!showMoreExpanded" @toggle="showMoreExpanded = $event">
|
||||
<SettingsItem :title="showMoreExpanded ? $t('buttons.showLess') : $t('buttons.showMore')" :collapsable="true"
|
||||
:start-collapsed="!showMoreExpanded" @toggle="showMoreExpanded = $event">
|
||||
<div class="settings-items">
|
||||
<p>
|
||||
{{ $t("prompts.shareTheme") }}
|
||||
@@ -213,7 +200,8 @@
|
||||
<p>
|
||||
{{ $t("share.enforceDarkLightMode") }}
|
||||
<i class="no-select material-symbols-outlined tooltip-info-icon"
|
||||
@mouseenter="showTooltip($event, $t('share.enforceDarkLightModeDescription'))" @mouseleave="hideTooltip">
|
||||
@mouseenter="showTooltip($event, $t('share.enforceDarkLightModeDescription'))"
|
||||
@mouseleave="hideTooltip">
|
||||
help
|
||||
</i>
|
||||
</p>
|
||||
@@ -227,22 +215,22 @@
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="disableThumbnails"
|
||||
:name="$t('share.disableThumbnails')" :description="$t('share.disableThumbnailsDescription')" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="showHidden"
|
||||
:name="$t('profileSettings.showHiddenFiles')" :description="$t('profileSettings.showHiddenFilesDescription')" />
|
||||
<ToggleSwitch v-if="shareType !== 'upload'" class="item" v-model="disableNavButtons" :name="$t('share.hideNavButtons')"
|
||||
:description="$t('share.hideNavButtonsDescription')" />
|
||||
:name="$t('profileSettings.showHiddenFiles')"
|
||||
:description="$t('profileSettings.showHiddenFilesDescription')" />
|
||||
<ToggleSwitch v-if="shareType !== 'upload'" class="item" v-model="disableNavButtons"
|
||||
:name="$t('share.hideNavButtons')" :description="$t('share.hideNavButtonsDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableShareCard" :name="$t('share.disableShareCard')"
|
||||
:description="$t('share.disableShareCardDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableSidebar"
|
||||
:name="$t('share.disableSidebar')" :description="$t('share.disableSidebarDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableSidebar" :name="$t('share.disableSidebar')"
|
||||
:description="$t('share.disableSidebarDescription')" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="perUserDownloadLimit"
|
||||
:name="$t('share.perUserDownloadLimit')" :description="$t('share.perUserDownloadLimitDescription')" />
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="extractEmbeddedSubtitles"
|
||||
:name="$t('share.extractEmbeddedSubtitles')"
|
||||
:description="$t('share.extractEmbeddedSubtitlesDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableLoginOption"
|
||||
:name="$t('share.disableLoginOption')"
|
||||
:description="$t('share.disableLoginOptionDescription')" />
|
||||
</div>
|
||||
<ToggleSwitch v-if="shareType === 'normal'" class="item" v-model="extractEmbeddedSubtitles"
|
||||
:name="$t('share.extractEmbeddedSubtitles')"
|
||||
:description="$t('share.extractEmbeddedSubtitlesDescription')" />
|
||||
<ToggleSwitch class="item" v-model="disableLoginOption" :name="$t('share.disableLoginOption')"
|
||||
:description="$t('share.disableLoginOptionDescription')" />
|
||||
</div>
|
||||
|
||||
<div v-if="shareType === 'normal'">
|
||||
<p>
|
||||
@@ -342,6 +330,16 @@
|
||||
{{ $t("general.share") }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="isPickingFile" class="card-action">
|
||||
<button class="button button--flat" @click="cancelFilePicker" :aria-label="$t('general.cancel')"
|
||||
:title="$t('general.cancel')">
|
||||
{{ $t("general.cancel") }}
|
||||
</button>
|
||||
<button class="button button--flat button--blue" @click="confirmFilePicker" :disabled="!filePickerSelectionValid"
|
||||
:aria-label="$t('general.ok')" :title="$t('general.ok')">
|
||||
{{ $t("general.ok") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { notify } from "@/notify";
|
||||
@@ -864,7 +862,7 @@ export default {
|
||||
icon: "qr_code"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
// Only add Download link for normal shares, not upload shares
|
||||
if (this.shareType !== 'upload') {
|
||||
this.sidebarLinks.push({
|
||||
@@ -964,7 +962,7 @@ export default {
|
||||
if (this.tempFilePath && this.tempFileSource) {
|
||||
// Build query params format: source=sourcename&path=indexpath
|
||||
const queryParams = `source=${encodeURIComponent(this.tempFileSource)}&path=${encodeURIComponent(this.tempFilePath)}`;
|
||||
|
||||
|
||||
if (this.filePickerField === 'banner') {
|
||||
this.banner = queryParams;
|
||||
} else if (this.filePickerField === 'favicon') {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import { availableLocales } from "@/i18n/index.ts";
|
||||
|
||||
export default defineComponent({
|
||||
name: "Languages",
|
||||
@@ -19,32 +20,7 @@ export default defineComponent({
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
locales: {
|
||||
he: "he",
|
||||
hu: "hu",
|
||||
ar: "ar",
|
||||
de: "de",
|
||||
en: "en",
|
||||
es: "es",
|
||||
fr: "fr",
|
||||
is: "is",
|
||||
it: "it",
|
||||
ja: "ja",
|
||||
ko: "ko",
|
||||
nlBE: "nl-be",
|
||||
pl: "pl",
|
||||
ptBR: "pt-br",
|
||||
pt: "pt",
|
||||
ro: "ro",
|
||||
ru: "ru",
|
||||
sk: "sk",
|
||||
svSE: "sv-se",
|
||||
tr: "tr",
|
||||
ua: "ua",
|
||||
zhCN: "zh-cn",
|
||||
zhTW: "zh-tw",
|
||||
cz: "cz",
|
||||
},
|
||||
locales: availableLocales,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Create an account",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "خيارات المستخدم المسؤول",
|
||||
"searchOptions": "خيارات البحث",
|
||||
"systemAdmin": "النظام والإدارة",
|
||||
"configViewer": "عارض التكوين",
|
||||
"configViewerShowFull": "إظهار التكوين الكامل",
|
||||
"configViewerShowComments": "إظهار التعليقات",
|
||||
"configViewerLoadConfig": "تكوين التحميل",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "الوكيل"
|
||||
},
|
||||
"newUserHintSubFolder": "على سبيل المثال '/مجلد فرعي' - فارغ للافتراضي",
|
||||
"permissionsHelp": "تتحكم أذونات المستخدم في الوصول إلى نوع الإجراءات التي يمكن للمستخدم تنفيذها. يمنح الوصول إلى المسؤول التحكم الكامل لتحديث وتغيير إعدادات أي مستخدم بالإضافة إلى العديد من إعدادات النظام."
|
||||
"permissionsHelp": "تتحكم أذونات المستخدم في الوصول إلى نوع الإجراءات التي يمكن للمستخدم تنفيذها. يمنح الوصول إلى المسؤول التحكم الكامل لتحديث وتغيير إعدادات أي مستخدم بالإضافة إلى العديد من إعدادات النظام.",
|
||||
"configViewer": "عارض التكوين"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "إعدادات الموقع",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "الخطوط{suffix}",
|
||||
"lastUpdate": "آخر تحديث{suffix}",
|
||||
"latency": "الكمون{suffix}",
|
||||
"all": "الكل{suffix}"
|
||||
"all": "الكل{suffix}",
|
||||
"expiration": "انتهاء الصلاحية{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "قواعد الوصول",
|
||||
|
||||
@@ -117,7 +117,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Vytvořit účet",
|
||||
@@ -457,7 +458,6 @@
|
||||
"userUpdated": "Uživatel aktualizován!",
|
||||
"adminOptions": "Možnosti administrátora",
|
||||
"systemAdmin": "Systém a správce",
|
||||
"configViewer": "Prohlížeč konfigurace",
|
||||
"configViewerShowFull": "Zobrazit úplnou konfiguraci",
|
||||
"configViewerShowComments": "Zobrazit komentáře",
|
||||
"configViewerLoadConfig": "Konfigurace načítání",
|
||||
@@ -469,7 +469,8 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "např. '/subfolder' - prázdný pro výchozí nastavení",
|
||||
"permissionsHelp": "Oprávnění uživatele řídí přístup k typům akcí, které může uživatel provádět. Přístup administrátora poskytuje plnou kontrolu nad aktualizací a změnou nastavení libovolného uživatele i nad mnoha nastaveními systému."
|
||||
"permissionsHelp": "Oprávnění uživatele řídí přístup k typům akcí, které může uživatel provádět. Přístup administrátora poskytuje plnou kontrolu nad aktualizací a změnou nastavení libovolného uživatele i nad mnoha nastaveními systému.",
|
||||
"configViewer": "Prohlížeč konfigurace"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Nastavení webu",
|
||||
@@ -705,7 +706,8 @@
|
||||
"lines": "Řádky{suffix}",
|
||||
"lastUpdate": "Poslední aktualizace{suffix}",
|
||||
"latency": "Zpoždění{suffix}",
|
||||
"all": "Všechny{suffix}"
|
||||
"all": "Všechny{suffix}",
|
||||
"expiration": "Expirace{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Pravidla přístupu",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Konto erstellen",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Optionen für Verwaltungsbenutzer",
|
||||
"searchOptions": "Suchoptionen",
|
||||
"systemAdmin": "System & Verwaltung",
|
||||
"configViewer": "Config Viewer",
|
||||
"configViewerShowFull": "Vollständige Konfiguration anzeigen",
|
||||
"configViewerShowComments": "Kommentare anzeigen",
|
||||
"configViewerLoadConfig": "Konfiguration laden",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "z.B. '/subfolder' - leer für Standard",
|
||||
"permissionsHelp": "Die Benutzerrechte steuern den Zugriff auf die Art der Aktionen, die der Benutzer durchführen kann. Der Administrator-Zugang gibt Ihnen die volle Kontrolle über die Aktualisierung und Änderung der Benutzereinstellungen sowie vieler Systemeinstellungen."
|
||||
"permissionsHelp": "Die Benutzerrechte steuern den Zugriff auf die Art der Aktionen, die der Benutzer durchführen kann. Der Administrator-Zugang gibt Ihnen die volle Kontrolle über die Aktualisierung und Änderung der Benutzereinstellungen sowie vieler Systemeinstellungen.",
|
||||
"configViewer": "Config Viewer"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Seiteneinstellungen",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "Zeilen{suffix}",
|
||||
"lastUpdate": "Letzte Aktualisierung{suffix}",
|
||||
"latency": "Latenzzeit{suffix}",
|
||||
"all": "Alle{suffix}"
|
||||
"all": "Alle{suffix}",
|
||||
"expiration": "Verfallsdatum{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Zugangsregeln",
|
||||
|
||||
@@ -117,7 +117,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Δημιουργία λογαριασμού",
|
||||
@@ -323,7 +324,6 @@
|
||||
"adminOptions": "Επιλογές χρήστη διαχειριστή",
|
||||
"searchOptions": "Επιλογές αναζήτησης",
|
||||
"systemAdmin": "Σύστημα & Διαχείριση",
|
||||
"configViewer": "Config Viewer",
|
||||
"configViewerShowFull": "Εμφάνιση πλήρων ρυθμίσεων",
|
||||
"configViewerShowComments": "Εμφάνιση σχολίων",
|
||||
"configViewerLoadConfig": "Φόρτωση Config",
|
||||
@@ -335,7 +335,8 @@
|
||||
"proxy": "Πληρεξούσιος"
|
||||
},
|
||||
"newUserHintSubFolder": "π.χ. '/subfolder' - κενό για προεπιλογή",
|
||||
"permissionsHelp": "Τα δικαιώματα χρήστη ελέγχουν την πρόσβαση σε είδος ενεργειών που μπορεί να εκτελέσει ο χρήστης. Η πρόσβαση διαχειριστή παρέχει πλήρη έλεγχο για την ενημέρωση και την αλλαγή των ρυθμίσεων οποιουδήποτε χρήστη, καθώς και πολλών ρυθμίσεων του συστήματος."
|
||||
"permissionsHelp": "Τα δικαιώματα χρήστη ελέγχουν την πρόσβαση σε είδος ενεργειών που μπορεί να εκτελέσει ο χρήστης. Η πρόσβαση διαχειριστή παρέχει πλήρη έλεγχο για την ενημέρωση και την αλλαγή των ρυθμίσεων οποιουδήποτε χρήστη, καθώς και πολλών ρυθμίσεων του συστήματος.",
|
||||
"configViewer": "Config Viewer"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Ρυθμίσεις ιστότοπου",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "Γραμμές{suffix}",
|
||||
"lastUpdate": "Τελευταία ενημέρωση{suffix}",
|
||||
"latency": "Καθυστέρηση{suffix}",
|
||||
"all": "Όλα{suffix}"
|
||||
"all": "Όλα{suffix}",
|
||||
"expiration": "Λήξη{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Κανόνες πρόσβασης",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"incompleteResultsDetails": "Showing max results ({max}). Try narrowing your search to get more results under {max}."
|
||||
},
|
||||
"general": {
|
||||
"expiration": "Expiration{suffix}",
|
||||
"all": "All{suffix}",
|
||||
"name": "Name{suffix}",
|
||||
"path": "Path{suffix}",
|
||||
@@ -235,7 +236,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Create an account",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Crear cuenta",
|
||||
@@ -326,13 +327,13 @@
|
||||
"adminOptions": "Opciones de administrador",
|
||||
"searchOptions": "Opciones de busqueda",
|
||||
"systemAdmin": "Sistema y administración",
|
||||
"configViewer": "Visor de configuración",
|
||||
"configViewerShowFull": "Mostrar configuración completa",
|
||||
"configViewerShowComments": "Mostrar comentarios",
|
||||
"configViewerLoadConfig": "Cargar configuración",
|
||||
"brandingHelp": "Puedes personalizar la apariencia de FileBrowser Quantum cambiando el nombre, el logo, puedes añadir tus propios estilos personalizados, desactivar los enlaces externos a GitHub, etc. \nSi quieres saber más, visita {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}.",
|
||||
"permissionsHelp": "Los permisos de usuario controlan el acceso al tipo de acciones que el usuario puede realizar. El acceso de administrador proporciona un control total para actualizar y cambiar la configuración de cualquier usuario, así como muchos ajustes del sistema."
|
||||
"permissionsHelp": "Los permisos de usuario controlan el acceso al tipo de acciones que el usuario puede realizar. El acceso de administrador proporciona un control total para actualizar y cambiar la configuración de cualquier usuario, así como muchos ajustes del sistema.",
|
||||
"configViewer": "Visor de configuración"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Ajustes del sitio",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "Líneas{suffix}",
|
||||
"lastUpdate": "Última actualización{suffix}",
|
||||
"latency": "Latencia{suffix}",
|
||||
"all": "Todos{suffix}"
|
||||
"all": "Todos{suffix}",
|
||||
"expiration": "Caducidad{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Reglas de acceso",
|
||||
|
||||
+19
-17
@@ -58,9 +58,9 @@
|
||||
"itemSelected": "élément sélectionné",
|
||||
"itemsSelected": "éléments sélectionnés",
|
||||
"myFiles": "Mes dossiers{suffix}",
|
||||
"newFile": "Nouveau Fichier{suffix}",
|
||||
"newFile": "Nouveau fichier{suffix}",
|
||||
"newFolder": "Nouveau dossier{suffix}",
|
||||
"duration": "Durée de l'accord{suffix}",
|
||||
"duration": "Durée{suffix}",
|
||||
"sortByDuration": "Trier par durée",
|
||||
"sameFolder": "Impossible de déplacer vers le même dossier",
|
||||
"cannotAccesDir": "Impossible d'accéder au répertoire",
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Créer un compte",
|
||||
@@ -309,7 +310,7 @@
|
||||
"userHomeBasePath": "Chemin de base pour les répertoires personnels des utilisateurs",
|
||||
"userScopeGenerationPlaceholder": "Le champ d'application sera généré automatiquement",
|
||||
"createUserHomeDirectory": "Créer le répertoire personnel de l'utilisateur",
|
||||
"api": "Clés API",
|
||||
"api": "API",
|
||||
"loginMethodDescription": "méthode de connexion autorisée pour l'utilisateur.",
|
||||
"userNotAdmin": "L'utilisateur n'est pas un administrateur",
|
||||
"enterPassword": "Saisir le mot de passe",
|
||||
@@ -320,7 +321,7 @@
|
||||
"adminOptions": "Options de l'utilisateur administrateur",
|
||||
"searchOptions": "Options de recherche",
|
||||
"systemAdmin": "Système et administration",
|
||||
"configViewer": "Config Viewer",
|
||||
"configViewer": "Visualiseur de configuration",
|
||||
"configViewerShowFull": "Afficher la configuration complète",
|
||||
"configViewerShowComments": "Afficher les commentaires",
|
||||
"configViewerLoadConfig": "Configuration de la charge",
|
||||
@@ -476,7 +477,6 @@
|
||||
"description": "Les clés API sont basées sur l'utilisateur qui les a créées. Voir les",
|
||||
"swaggerLinkText": "page swagger",
|
||||
"created": "Créée",
|
||||
"expires": "Expiration",
|
||||
"actions": "Actions",
|
||||
"enabled": "Activée",
|
||||
"disabled": "Désactivée",
|
||||
@@ -484,7 +484,8 @@
|
||||
"tokenType": "Type de jeton :",
|
||||
"clickToCopyKey": "Cliquez pour copier la clé (cachée)",
|
||||
"minimalDescription": "Jeton minimal",
|
||||
"minimalInfo": "Exclut les personnalisations du jeton afin de réduire la longueur du jeton. Les autorisations correspondent exactement à celles de l'utilisateur associé."
|
||||
"minimalInfo": "Exclut les personnalisations du jeton afin de réduire la longueur du jeton. Les autorisations correspondent exactement à celles de l'utilisateur associé.",
|
||||
"expires": "Expiration"
|
||||
},
|
||||
"index": {
|
||||
"assessment": "Évaluation",
|
||||
@@ -494,7 +495,7 @@
|
||||
"settingsHover": "Paramètres pour l'utilisateur",
|
||||
"toggleClick": "Activation du mode simple clic",
|
||||
"toggleDark": "Mode sombre/clair",
|
||||
"toggleSticky": "Activer/Désactiver le mode persistent",
|
||||
"toggleSticky": "Activer/Désactiver le mode persistant",
|
||||
"noSources": "Aucune source n'est disponible pour cet utilisateur. Ajoutez une source pour cet utilisateur ou configurez une source avec defaultEnabled : true",
|
||||
"unknown": "Inconnu",
|
||||
"simple": "Simple",
|
||||
@@ -525,11 +526,11 @@
|
||||
"signup": "S'inscrire",
|
||||
"password": "Mot de passe",
|
||||
"username": "Nom d'utilisateur",
|
||||
"files": "Dossiers",
|
||||
"files": "Fichiers",
|
||||
"folders": "Dossiers",
|
||||
"exit": "Sortie",
|
||||
"permanent": "Permanent",
|
||||
"hash": "Hachures",
|
||||
"hash": "Hachage",
|
||||
"of": "de",
|
||||
"source": "Source{suffix}",
|
||||
"percentage": "{percentage}%",
|
||||
@@ -569,7 +570,7 @@
|
||||
"documentation": "la documentation{suffix}",
|
||||
"examples": "Exemples{suffix}",
|
||||
"dark": "Sombre{suffix}",
|
||||
"light": "Lumière{suffix}",
|
||||
"light": "Clair{suffix}",
|
||||
"title": "Thème{suffix}",
|
||||
"expires": "Expiration{suffix}",
|
||||
"scope": "Champ d'application{suffix}",
|
||||
@@ -604,12 +605,12 @@
|
||||
"select": "Sélectionner{suffix}",
|
||||
"share": "Partager{suffix}",
|
||||
"update": "Mise à jour{suffix}",
|
||||
"upload": "Télécharger{suffix}",
|
||||
"upload": "Importer{suffix}",
|
||||
"openFile": "Ouvrir un fichier{suffix}",
|
||||
"continue": "Continuer{suffix}",
|
||||
"edit": "Editer{suffix}",
|
||||
"edit": "Éditer{suffix}",
|
||||
"pause": "Pause{suffix}",
|
||||
"resume": "Curriculum vitae{suffix}",
|
||||
"resume": "Reprendre{suffix}",
|
||||
"retry": "Réessayer{suffix}",
|
||||
"disable": "Désactiver{suffix}",
|
||||
"confirm": "Confirmer{suffix}",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "Lignes{suffix}",
|
||||
"lastUpdate": "Dernière mise à jour{suffix}",
|
||||
"latency": "Temps de latence{suffix}",
|
||||
"all": "Tous{suffix}"
|
||||
"all": "Tous{suffix}",
|
||||
"expiration": "Expiration{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Règles d'accès",
|
||||
@@ -770,11 +772,11 @@
|
||||
"video": "Vidéo",
|
||||
"image": "Image",
|
||||
"audio": "Audio",
|
||||
"archive": "Archives",
|
||||
"archive": "Archive",
|
||||
"document": "Document",
|
||||
"binary": "Binaire",
|
||||
"directory": "Annuaire",
|
||||
"other": "Autres"
|
||||
"other": "Autre"
|
||||
},
|
||||
"tools": {
|
||||
"description": "Utilitaires et outils d'analyse pour vos fichiers",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "צור חשבון",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "אפשרויות משתמש מנהל",
|
||||
"searchOptions": "אפשרויות חיפוש",
|
||||
"systemAdmin": "מערכת וניהול",
|
||||
"configViewer": "מציג תצורה",
|
||||
"configViewerShowFull": "הצג את התצורה המלאה",
|
||||
"configViewerShowComments": "הצג תגובות",
|
||||
"configViewerLoadConfig": "טען תצורה",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "פרוקסי"
|
||||
},
|
||||
"newUserHintSubFolder": "לדוגמה, '/subfolder' - ריק עבור ברירת המחדל",
|
||||
"permissionsHelp": "הרשאות המשתמש שולטות בגישה לסוגי הפעולות שהמשתמש יכול לבצע. גישה לניהול מעניקה שליטה מלאה לעדכון ולשינוי הגדרות המשתמש, כמו גם הגדרות רבות של המערכת."
|
||||
"permissionsHelp": "הרשאות המשתמש שולטות בגישה לסוגי הפעולות שהמשתמש יכול לבצע. גישה לניהול מעניקה שליטה מלאה לעדכון ולשינוי הגדרות המשתמש, כמו גם הגדרות רבות של המערכת.",
|
||||
"configViewer": "מציג תצורה"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "הגדרות",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "קווים{suffix}",
|
||||
"lastUpdate": "עדכון אחרון{suffix}",
|
||||
"latency": "חביון{suffix}",
|
||||
"all": "כל{suffix}"
|
||||
"all": "כל{suffix}",
|
||||
"expiration": "תוקף{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "כללי גישה",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Fiók létrehozása",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Admin felhasználói beállítások",
|
||||
"searchOptions": "Keresési lehetőségek",
|
||||
"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",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "pl. '/subfolder' - üres az alapértelmezéshez",
|
||||
"permissionsHelp": "A felhasználói engedélyek szabályozzák, hogy a felhasználó milyen típusú műveleteket hajthat végre. A rendszergazdai hozzáférés teljes körű ellenőrzést biztosít bármely felhasználó beállításainak, valamint számos rendszerbeállításnak a frissítéséhez és módosításához."
|
||||
"permissionsHelp": "A felhasználói engedélyek szabályozzák, hogy a felhasználó milyen típusú műveleteket hajthat végre. A rendszergazdai hozzáférés teljes körű ellenőrzést biztosít bármely felhasználó beállításainak, valamint számos rendszerbeállításnak a frissítéséhez és módosításához.",
|
||||
"configViewer": "Konfiguráció megtekintő"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Oldalbeállítások",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "Vonalak{suffix}",
|
||||
"lastUpdate": "Utolsó frissítés{suffix}",
|
||||
"latency": "Késleltetés{suffix}",
|
||||
"all": "Minden{suffix}"
|
||||
"all": "Minden{suffix}",
|
||||
"expiration": "Lejárat{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Hozzáférési szabályok",
|
||||
|
||||
+52
-59
@@ -1,19 +1,21 @@
|
||||
// i18n.js
|
||||
import { createI18n } from 'vue-i18n';
|
||||
|
||||
// Import translations
|
||||
import he from './he.json';
|
||||
import hu from './hu.json';
|
||||
// Import translations (alphabetical order)
|
||||
import ar from './ar.json';
|
||||
import cz from './cz.json';
|
||||
import de from './de.json';
|
||||
import el from './el.json';
|
||||
import en from './en.json';
|
||||
import es from './es.json';
|
||||
import fr from './fr.json';
|
||||
import he from './he.json';
|
||||
import hu from './hu.json';
|
||||
import is from './is.json';
|
||||
import it from './it.json';
|
||||
import ja from './ja.json';
|
||||
import ko from './ko.json';
|
||||
import nl from './nl.json';
|
||||
import nlBE from './nl-be.json';
|
||||
import pl from './pl.json';
|
||||
import pt from './pt.json';
|
||||
@@ -21,14 +23,35 @@ import ptBR from './pt-br.json';
|
||||
import ro from './ro.json';
|
||||
import ru from './ru.json';
|
||||
import sk from './sk.json';
|
||||
import ua from './ua.json';
|
||||
import svSE from './sv-se.json';
|
||||
import ua from './ua.json';
|
||||
import zhCN from './zh-cn.json';
|
||||
import zhTW from './zh-tw.json';
|
||||
import cz from './cz.json';
|
||||
|
||||
type LocaleMap = { [key: string]: string };
|
||||
|
||||
// Map of all imported translation modules (alphabetical order)
|
||||
// This is the single source for all translations
|
||||
const translationModules: { [key: string]: any } = {
|
||||
ar, cz, de, el, en, es, fr, he, hu, is, it, ja, ko, nl, nlBE, pl, pt, ptBR, ro, ru, sk, svSE, ua, zhCN, zhTW
|
||||
};
|
||||
|
||||
// Shared list of available locales for the application
|
||||
// Auto-generated from translation modules
|
||||
export const availableLocales: LocaleMap = Object.keys(translationModules).reduce((acc, key) => {
|
||||
// Convert internal keys to their display format
|
||||
const displayMap: { [key: string]: string } = {
|
||||
nlBE: 'nl-be',
|
||||
ptBR: 'pt-br',
|
||||
svSE: 'sv-se',
|
||||
zhCN: 'zh-cn',
|
||||
zhTW: 'zh-tw',
|
||||
};
|
||||
acc[key] = displayMap[key] || key;
|
||||
return acc;
|
||||
}, {} as LocaleMap);
|
||||
|
||||
// Maps internal locale names to standard BCP 47 codes (for navigator.language)
|
||||
export const internalToStandardLocaleMap: { [key: string]: string } = {
|
||||
nlBE: 'nl-be',
|
||||
ptBR: 'pt-br',
|
||||
@@ -44,43 +67,38 @@ export function toStandardLocale(locale: string): string {
|
||||
}
|
||||
|
||||
export function detectLocale(): string {
|
||||
const locale = navigator.language.toLowerCase();
|
||||
const localeMap: LocaleMap = {
|
||||
'he': 'he',
|
||||
'hu': 'hu',
|
||||
'ar': 'ar',
|
||||
'el': 'el',
|
||||
'es': 'es',
|
||||
'en': 'en',
|
||||
'is': 'is',
|
||||
'it': 'it',
|
||||
'fr': 'fr',
|
||||
const browserLocale = navigator.language.toLowerCase();
|
||||
|
||||
// Map of browser locale codes to internal locale keys
|
||||
const browserToInternalMap: LocaleMap = {
|
||||
'pt-br': 'ptBR',
|
||||
'pt': 'pt',
|
||||
'ja': 'ja',
|
||||
'zh-tw': 'zhTW',
|
||||
'zh-cn': 'zhCN',
|
||||
'zh': 'zhCN',
|
||||
'de': 'de',
|
||||
'ro': 'ro',
|
||||
'ru': 'ru',
|
||||
'pl': 'pl',
|
||||
'ko': 'ko',
|
||||
'cz': 'cz',
|
||||
'sk': 'sk',
|
||||
'tr': 'tr',
|
||||
'uk': 'uk',
|
||||
'sv-se': 'svSE',
|
||||
'sv': 'svSE',
|
||||
'nl-be': 'nlBE',
|
||||
};
|
||||
|
||||
for (const key in localeMap) {
|
||||
if (locale.startsWith(key)) {
|
||||
return localeMap[key];
|
||||
}
|
||||
// Check for exact matches first (including variants like pt-br)
|
||||
if (browserToInternalMap[browserLocale]) {
|
||||
return browserToInternalMap[browserLocale];
|
||||
}
|
||||
return 'en-us'; // Default fallback
|
||||
|
||||
// Check for language prefix matches (e.g., 'en-US' -> 'en')
|
||||
const languagePrefix = browserLocale.split('-')[0];
|
||||
|
||||
// If the language prefix exists in our translations, use it
|
||||
if (translationModules[languagePrefix]) {
|
||||
return languagePrefix;
|
||||
}
|
||||
|
||||
// Try browser-specific mappings for the prefix
|
||||
if (browserToInternalMap[languagePrefix]) {
|
||||
return browserToInternalMap[languagePrefix];
|
||||
}
|
||||
|
||||
return 'en'; // Default fallback
|
||||
}
|
||||
|
||||
// List of RTL languages
|
||||
@@ -100,38 +118,13 @@ export function setLocale(locale: string) {
|
||||
}
|
||||
|
||||
|
||||
// Create i18n instance
|
||||
// Create i18n instance with auto-generated messages from imported modules
|
||||
const i18n = createI18n({
|
||||
locale: detectLocale(),
|
||||
fallbackLocale: 'en',
|
||||
// expose i18n.global for outside components
|
||||
legacy: true,
|
||||
messages: {
|
||||
he,
|
||||
hu,
|
||||
ar,
|
||||
de,
|
||||
el,
|
||||
en,
|
||||
es,
|
||||
fr,
|
||||
is,
|
||||
it,
|
||||
ja,
|
||||
ko,
|
||||
nlBE,
|
||||
pl,
|
||||
ptBR,
|
||||
pt,
|
||||
ru,
|
||||
ro,
|
||||
sk,
|
||||
svSE,
|
||||
ua,
|
||||
zhCN,
|
||||
zhTW,
|
||||
cz,
|
||||
},
|
||||
messages: translationModules,
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Crea un account",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Opzioni dell'utente amministratore",
|
||||
"searchOptions": "Opzioni di ricerca",
|
||||
"systemAdmin": "Sistema e amministrazione",
|
||||
"configViewer": "Visualizzatore di configurazione",
|
||||
"configViewerShowFull": "Mostra la configurazione completa",
|
||||
"configViewerShowComments": "Mostra i commenti",
|
||||
"configViewerLoadConfig": "Configurazione del carico",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "es. '/sottocartella' - vuoto per l'impostazione predefinita",
|
||||
"permissionsHelp": "I permessi utente controllano l'accesso al tipo di azioni che l'utente può eseguire. L'accesso dell'amministratore consente di aggiornare e modificare le impostazioni di qualsiasi utente e molte impostazioni del sistema."
|
||||
"permissionsHelp": "I permessi utente controllano l'accesso al tipo di azioni che l'utente può eseguire. L'accesso dell'amministratore consente di aggiornare e modificare le impostazioni di qualsiasi utente e molte impostazioni del sistema.",
|
||||
"configViewer": "Visualizzatore di configurazione"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Impostazioni del sito",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "Linee{suffix}",
|
||||
"lastUpdate": "Ultimo aggiornamento{suffix}",
|
||||
"latency": "Latenza{suffix}",
|
||||
"all": "Tutti{suffix}"
|
||||
"all": "Tutti{suffix}",
|
||||
"expiration": "Scadenza{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regole di accesso",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "アカウントを作成",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "管理者ユーザーオプション",
|
||||
"searchOptions": "検索オプション",
|
||||
"systemAdmin": "システム&管理",
|
||||
"configViewer": "設定ビューア",
|
||||
"configViewerShowFull": "全設定を表示",
|
||||
"configViewerShowComments": "コメントを表示",
|
||||
"configViewerLoadConfig": "設定を読み込み",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "プロキシ"
|
||||
},
|
||||
"newUserHintSubFolder": "例 '/subfolder' - デフォルトは空白",
|
||||
"permissionsHelp": "ユーザー権限は、ユーザーが実行できるアクションの種類へのアクセスを制御します。管理者アクセスは、多くのシステム設定だけでなく、任意のユーザーの設定を更新および変更するための完全なコントロールを提供します。"
|
||||
"permissionsHelp": "ユーザー権限は、ユーザーが実行できるアクションの種類へのアクセスを制御します。管理者アクセスは、多くのシステム設定だけでなく、任意のユーザーの設定を更新および変更するための完全なコントロールを提供します。",
|
||||
"configViewer": "コンフィグビューア"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "サイト設定",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "ライン{suffix}",
|
||||
"lastUpdate": "最終更新{suffix}",
|
||||
"latency": "レイテンシー{suffix}",
|
||||
"all": "すべて{suffix}"
|
||||
"all": "すべて{suffix}",
|
||||
"expiration": "有効期限{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "アクセスルール",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "계정 생성",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "관리자 사용자 옵션",
|
||||
"searchOptions": "검색 옵션",
|
||||
"systemAdmin": "시스템 및 관리자",
|
||||
"configViewer": "구성 뷰어",
|
||||
"configViewerShowFull": "전체 구성 표시",
|
||||
"configViewerShowComments": "댓글 표시",
|
||||
"configViewerLoadConfig": "로드 구성",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "프록시"
|
||||
},
|
||||
"newUserHintSubFolder": "예: '/subfolder' - 기본값은 비어 있습니다.",
|
||||
"permissionsHelp": "사용자 권한은 사용자가 수행할 수 있는 작업 유형에 대한 액세스를 제어합니다. 관리자 권한은 모든 사용자 설정은 물론 여러 시스템 설정을 업데이트하고 변경할 수 있는 모든 권한을 부여합니다."
|
||||
"permissionsHelp": "사용자 권한은 사용자가 수행할 수 있는 작업 유형에 대한 액세스를 제어합니다. 관리자 권한은 모든 사용자 설정은 물론 여러 시스템 설정을 업데이트하고 변경할 수 있는 모든 권한을 부여합니다.",
|
||||
"configViewer": "구성 뷰어"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "사이트 설정",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "라인{suffix}",
|
||||
"lastUpdate": "최근 업데이트{suffix}",
|
||||
"latency": "지연 시간{suffix}",
|
||||
"all": "모두{suffix}"
|
||||
"all": "모두{suffix}",
|
||||
"expiration": "만료{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "액세스 규칙",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Account aanmaken",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Opties voor beheerders",
|
||||
"searchOptions": "Zoekopties",
|
||||
"systemAdmin": "Systeem & Beheer",
|
||||
"configViewer": "Configuratiescherm",
|
||||
"configViewerShowFull": "Volledige configuratie weergeven",
|
||||
"configViewerShowComments": "Opmerkingen tonen",
|
||||
"configViewerLoadConfig": "Configuratie laden",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "bijv. '/submap' - leeg voor standaard",
|
||||
"permissionsHelp": "Gebruikersrechten bepalen welke acties de gebruiker kan uitvoeren. Administrar toegang geeft volledige controle om de instellingen van elke gebruiker bij te werken en te wijzigen, evenals vele systeeminstellingen."
|
||||
"permissionsHelp": "Gebruikersrechten bepalen welke acties de gebruiker kan uitvoeren. Administrar toegang geeft volledige controle om de instellingen van elke gebruiker bij te werken en te wijzigen, evenals vele systeeminstellingen.",
|
||||
"configViewer": "Configuratiescherm"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Site-instellingen",
|
||||
@@ -705,7 +706,8 @@
|
||||
"lines": "Lijnen{suffix}",
|
||||
"lastUpdate": "Laatste update{suffix}",
|
||||
"latency": "Latency{suffix}",
|
||||
"all": "Alle{suffix}"
|
||||
"all": "Alle{suffix}",
|
||||
"expiration": "Vervaldatum{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Toegangsregels",
|
||||
|
||||
@@ -0,0 +1,853 @@
|
||||
{
|
||||
"messages": {
|
||||
"pathNotFound": "Pad bestaat niet op bestandssysteem",
|
||||
"pathNotFoundMessage": "Het pad hiervoor bestaat niet meer op het bestandssysteem. Het kan verplaatst of verwijderd zijn.",
|
||||
"reassignPath": "Pad opnieuw toewijzen",
|
||||
"pathReassigned": "Pad succesvol bijgewerkt!",
|
||||
"pathReassignFailed": "Het bijwerken van het pad is mislukt. Probeer het opnieuw.",
|
||||
"incompleteResultsDetails": "Max resultaten ({max}). Probeer de zoekopdracht aan te passen voor meer resultaten onder {max}."
|
||||
},
|
||||
"general": {
|
||||
"all": "Alle{suffix}",
|
||||
"name": "Naam{suffix}",
|
||||
"path": "Pad{suffix}",
|
||||
"user": "Gebruiker{suffix}",
|
||||
"group": "Groep{suffix}",
|
||||
"signup": "Aanmelden{suffix}",
|
||||
"password": "Wachtwoord{suffix}",
|
||||
"username": "Gebruikersnaam{suffix}",
|
||||
"files": "Bestanden{suffix}",
|
||||
"folders": "Mappen{suffix}",
|
||||
"exit": "Sluiten{suffix}",
|
||||
"permanent": "Permanent{suffix}",
|
||||
"hash": "Hash{suffix}",
|
||||
"of": "van{suffix}",
|
||||
"source": "Bron{suffix}",
|
||||
"percentage": "{percentage}%",
|
||||
"type": "Type{suffix}",
|
||||
"size": "Grootte{suffix}",
|
||||
"body": "Inhoud{suffix}",
|
||||
"home": "Thuis{suffix}",
|
||||
"clear": "Wissen{suffix}",
|
||||
"help": "Hulp{suffix}",
|
||||
"login": "Inloggen{suffix}",
|
||||
"logout": "Afmelden{suffix}",
|
||||
"preview": "Voorbeeld{suffix}",
|
||||
"settings": "Instellingen{suffix}",
|
||||
"sources": "Bronnen{suffix}",
|
||||
"links": "Links{suffix}",
|
||||
"tool": "Gereedschap{suffix}",
|
||||
"used": "gebruikt{suffix}",
|
||||
"status": "Status{suffix}",
|
||||
"analyze": "Analyseren{suffix}",
|
||||
"results": "Resultaten{suffix}",
|
||||
"users": "Gebruikers{suffix}",
|
||||
"scopes": "Bereiken{suffix}",
|
||||
"commands": "Opdrachten{suffix}",
|
||||
"photos": "Foto's",
|
||||
"audio": "Audio{suffix}",
|
||||
"videos": "Video's{suffix}",
|
||||
"documents": "Documenten{suffix}",
|
||||
"archives": "Archieven{suffix}",
|
||||
"images": "Afbeeldingen{suffix}",
|
||||
"music": "Muziek{suffix}",
|
||||
"pdf": "PDF{suffix}",
|
||||
"search": "Zoeken{suffix}",
|
||||
"number": "Aantal{suffix}",
|
||||
"types": "Typen{suffix}",
|
||||
"video": "Video{suffix}",
|
||||
"admin": "Admin{suffix}",
|
||||
"branding": "Branding{suffix}",
|
||||
"language": "Taal{suffix}",
|
||||
"documentation": "documentatie{suffix}",
|
||||
"examples": "Voorbeelden{suffix}",
|
||||
"dark": "Donker{suffix}",
|
||||
"light": "Licht{suffix}",
|
||||
"title": "Thema{suffix}",
|
||||
"expires": "Verloopt op{suffix}",
|
||||
"scope": "Bereik{suffix}",
|
||||
"rules": "Regels{suffix}",
|
||||
"downloads": "Downloads{suffix}",
|
||||
"submit": "Indienen{suffix}",
|
||||
"navigation": "Navigatie{suffix}",
|
||||
"metadata": "Metagegevens{suffix}",
|
||||
"loading": "Laden{suffix}",
|
||||
"add": "Toevoegen{suffix}",
|
||||
"stay": "Blijven{suffix}",
|
||||
"cancel": "Annuleren{suffix}",
|
||||
"change": "Wijzigen{suffix}",
|
||||
"verify": "Controleren{suffix}",
|
||||
"close": "Sluiten{suffix}",
|
||||
"copy": "Kopiëren{suffix}",
|
||||
"create": "Aanmaken{suffix}",
|
||||
"delete": "Verwijderen{suffix}",
|
||||
"download": "Downloaden{suffix}",
|
||||
"file": "Bestand{suffix}",
|
||||
"folder": "Map{suffix}",
|
||||
"info": "Info{suffix}",
|
||||
"more": "Meer{suffix}",
|
||||
"move": "Verplaatsen{suffix}",
|
||||
"new": "Nieuw{suffix}",
|
||||
"next": "Volgende{suffix}",
|
||||
"ok": "OK{suffix}",
|
||||
"previous": "Vorige{suffix}",
|
||||
"publish": "Publiceren{suffix}",
|
||||
"rename": "Hernoemen{suffix}",
|
||||
"replace": "Vervangen{suffix}",
|
||||
"save": "Opslaan{suffix}",
|
||||
"schedule": "Schema{suffix}",
|
||||
"select": "Selecteren{suffix}",
|
||||
"share": "Delen{suffix}",
|
||||
"update": "Bijwerken{suffix}",
|
||||
"upload": "Uploaden{suffix}",
|
||||
"openFile": "Bestand openen{suffix}",
|
||||
"continue": "Doorgaan{suffix}",
|
||||
"edit": "Bewerken{suffix}",
|
||||
"pause": "Pauzeren{suffix}",
|
||||
"resume": "Hervatten{suffix}",
|
||||
"retry": "Opnieuw proberen{suffix}",
|
||||
"disable": "Uitschakelen{suffix}",
|
||||
"confirm": "Bevestigen{suffix}",
|
||||
"uploading": "Uploaden{suffix}",
|
||||
"paused": "Gepauzeerd{suffix}",
|
||||
"conflict": "Conflict{suffix}",
|
||||
"output": "Uitvoer{suffix}",
|
||||
"watching": "Bekijken{suffix}",
|
||||
"lines": "Regels{suffix}",
|
||||
"lastUpdate": "Laatste update{suffix}",
|
||||
"latency": "Vertraging{suffix}",
|
||||
"expiration": "Vervaldatum{suffix}"
|
||||
},
|
||||
"buttons": {
|
||||
"generateNewOtp": "Herstellen en nieuwe twee-factorcode genereren",
|
||||
"copyFile": "Bestand kopiëren",
|
||||
"copyToClipboard": "Kopiëren naar klembord",
|
||||
"copyDownloadLinkToClipboard": "Downloadlink kopiëren naar klembord",
|
||||
"moveFile": "Bestand verplaatsen",
|
||||
"permalink": "Permanente link aanmaken",
|
||||
"reportIssue": "Probleem melden",
|
||||
"selectMultiple": "Meerdervoudige selectie",
|
||||
"shell": "Opdrachtvenster schakelen",
|
||||
"switchView": "Weergave wisselen",
|
||||
"toggleSidebar": "Zijbalk schakelen",
|
||||
"pauseAll": "Alles onderbreken",
|
||||
"resumeAll": "Alles hervatten",
|
||||
"clearCompleted": "Wissen voltooid",
|
||||
"showMore": "Geavanceerde opties weergeven",
|
||||
"showLess": "Geavanceerde opties verbergen",
|
||||
"openParentFolder": "Bovenliggende map openen",
|
||||
"watchFile": "Bestand bekijken",
|
||||
"listView": "Lijstweergave",
|
||||
"compactView": "Compacte weergave",
|
||||
"normalView": "Normale weergave",
|
||||
"galleryView": "Galerie",
|
||||
"selectedCount": "Aantal items geselecteerd om een actie uit te voeren",
|
||||
"selectAll": "Alles selecteren",
|
||||
"uploadFiles": "Bestanden uploaden",
|
||||
"uploadFolder": "Map uploaden",
|
||||
"openUploadViewer": "Uploadmanager openen",
|
||||
"discardAndExit": "Weggooien en afsluiten",
|
||||
"saveAndExit": "Opslaan en afsluiten",
|
||||
"keepEditing": "Blijven bewerken",
|
||||
"closeAnyway": "Sluiten",
|
||||
"goToItem": "Ga naar item"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Bestand downloaden",
|
||||
"downloadFolder": "Map downloaden",
|
||||
"downloadSelected": "Selectie downloaden"
|
||||
},
|
||||
"upload": {
|
||||
"abortUpload": "Weet u zeker dat u wilt afbreken?"
|
||||
},
|
||||
"errors": {
|
||||
"forbidden": "U hebt hier geen toegangsrechten.",
|
||||
"internal": "Er ging iets niet goed.",
|
||||
"notFound": "Geen toegang tot deze locatie.",
|
||||
"connection": "Geen toegang tot de server.",
|
||||
"shareNotFound": "Ongeldige of verlopen gedeelde map.",
|
||||
"failedToConnectToServer": "Kan geen verbinding maken met de server, draait deze nog?"
|
||||
},
|
||||
"files": {
|
||||
"closePreview": "Voorbeeld sluiten",
|
||||
"lastModified": "Laatst bewerkt{suffix}",
|
||||
"duration": "Duur{suffix}",
|
||||
"lonely": "Hier is niets te zien...",
|
||||
"multipleSelectionEnabled": "Meervoudige selectie ingeschakeld",
|
||||
"sortByLastModified": "Sorteren op laatst bewerkt",
|
||||
"sortByName": "Sorteren op naam",
|
||||
"sortBySize": "Sorteren op grootte",
|
||||
"sortByDuration": "Sorteren op duur",
|
||||
"noPreview": "Voorbeeld is niet beschikbaar voor dit bestand.",
|
||||
"noDownloadAccess": "Download is niet beschikbaar.",
|
||||
"itemSelected": "item geselecteerd",
|
||||
"itemsSelected": "items geselecteerd",
|
||||
"myFiles": "Mijn bestanden{suffix}",
|
||||
"newFile": "Nieuw bestand{suffix}",
|
||||
"newFolder": "Nieuwe map{suffix}",
|
||||
"sameFolder": "Kan niet verplaatsen naar dezelfde map",
|
||||
"cannotAccesDir": "Geen toegang tot map"
|
||||
},
|
||||
"help": {
|
||||
"functionKeys": "Functietoetsen",
|
||||
"mouseActions": "Muisacties",
|
||||
"fileOperations": "Bestandsbewerkingen",
|
||||
"click": "selecteer bestand of map",
|
||||
"marqueeSelect": "selecteer meerdere bestanden of mappen door over lege ruimte te slepen",
|
||||
"ctrl": {
|
||||
"click": "meerdere bestanden of mappen selecteren",
|
||||
"c": "geselecteerde items kopiëren",
|
||||
"v": "gekopieerde/geknipte items plakken",
|
||||
"x": "geselecteerde items knippen",
|
||||
"d": "geselecteerde items downloaden",
|
||||
"b": "zijbalk schakelen",
|
||||
"comma": "ga naar instellingen"
|
||||
},
|
||||
"del": "geselecteerde items verwijderen",
|
||||
"esc": "selectie wissen en/of opdrachtvenster sluiten",
|
||||
"f2": "bestand herbenoemen",
|
||||
"f1": "hulp-venster tonen",
|
||||
"slash": "Zoeken inschakelen",
|
||||
"description": "U kunt de basisnavigatieopties hieronder bekijken. Ga voor aanvullende informatie naar",
|
||||
"wiki": "FileBrowser Quantum Officiële documenten"
|
||||
},
|
||||
"languages": {
|
||||
"he": "עברית",
|
||||
"hu": "Magyar",
|
||||
"ar": "العربية",
|
||||
"de": "Deutsch",
|
||||
"el": "Ελληνικά",
|
||||
"en": "English",
|
||||
"es": "Español",
|
||||
"fr": "Français",
|
||||
"is": "Icelandic",
|
||||
"it": "Italiano",
|
||||
"ja": "日本語",
|
||||
"ko": "한국어",
|
||||
"nlBE": "Dutch (Belgium)",
|
||||
"pl": "Polski",
|
||||
"pt": "Português",
|
||||
"ptBR": "Português (Brasil)",
|
||||
"ro": "Romanian",
|
||||
"ru": "Русский",
|
||||
"sk": "Slovenčina",
|
||||
"svSE": "Swedish (Sweden)",
|
||||
"tr": "Türkçe",
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Account aanmaken",
|
||||
"loginInstead": "Ik heb al een account",
|
||||
"passwordConfirm": "Wachtwoord bevestigen",
|
||||
"passwordsDontMatch": "Wachtwoorden komen niet overeen",
|
||||
"usernameTaken": "Gebruikersnaam al in gebruik",
|
||||
"wrongCredentials": "Verkeerde inloggegevens",
|
||||
"or": "OF",
|
||||
"inProgress": "Inloggen...",
|
||||
"failedLogin": "Inloggen is mislukt",
|
||||
"invalidCredentials": "Ongeldige inloggegevens verstrekt"
|
||||
},
|
||||
"share": {
|
||||
"notice": "Opmerking: wanneer een gedeelde map wordt aangemaakt, wordt deze meer blootgesteld aan het publiek en gelden er andere toegangsregels. Als uw gedeelde map niet veilig is geconfigureerd, kan iedereen er toegang tot krijgen.",
|
||||
"disableAnonymous": "Anonieme toegang uitschakelen",
|
||||
"disableThumbnails": "Miniaturen uitschakelen",
|
||||
"enableAllowedUsernames": "Alleen delen met bepaalde gebruikers",
|
||||
"allowedUsernamesPlaceholder": "Voer gebruikersnamen in, door komma's gescheiden",
|
||||
"disableAnonymousDescription": "Alleen geauthenticeerde gebruikers krijgen toegang tot de gedeelde map. De gebruiker moet toegang hebben tot de bron van de gedeelde map.",
|
||||
"disableThumbnailsDescription": "Er worden geen voorbeeldminiaturen weergegeven in de gedeelde map.",
|
||||
"enableAllowedUsernamesDescription": "Alleen de opgegeven gebruikers krijgen toegang tot de gedeelde map. De gebruiker moet toegang hebben tot de bron van de gedeelde map.",
|
||||
"downloadsLimitDescription": "Het maximum aantal keren dat een bestand/map van de gedeelde map kan worden gedownload. Leeg laten voor onbeperkt.",
|
||||
"maxBandwidthDescription": "De maximale downloadbandbreedte in kilobytes per seconde. Leeg laten voor onbeperkt.",
|
||||
"shareThemeDescription": "Het thema dat moet worden gebruikt voor de link delen.",
|
||||
"keepAfterExpiration": "Gedeelde map niet verwijderen nadat deze is verlopen",
|
||||
"keepAfterExpirationDescription": "De gedeelde map wordt niet verwijderd nadat deze verlopen is. Dit is handig als u de vervaldatum van een gedeelde map wilt verlengen of het verder wilt bewerken nadat het verlopen is.",
|
||||
"saveDisabled": "Kan wijzigingen in gedeelde bestanden niet opslaan.",
|
||||
"shareDurationDescription": "Duur voordat de gedeelde map verloopt. Leeg laten voor een permanente gedeelde map.",
|
||||
"passwordDescription": "Optioneel wachtwoord dat vereist is om toegang te krijgen tot deze gedeelde map, inclusief geauthenticeerde gebruikers.",
|
||||
"allowedUsernamesListDescription": "Door komma's gescheiden gebruikersnamen die toegang hebben tot de gedeelde map (indien ingeschakeld).",
|
||||
"shareThemeColorDescription": "CSS kleurwaarde toegepast op het thema van de gedeelde map (bijv. rood, #0ea5e9, rgb(14,165,233)).",
|
||||
"shareTitleDescription": "Aangepaste paginatitel die wordt weergegeven op de deelpagina.",
|
||||
"shareDescriptionHelp": "Korte beschrijving die wordt weergegeven op de pagina van de gedeelde map (kan worden gebruikt in metatags).",
|
||||
"shareLogoDescription": "URL van logoafbeelding of pad dat toegankelijk is voor de client -- zowel abosolute als index path worden geaccepteerd: bijv. 'https://domain.com/logo.png' of '/path/to/logo.png'.",
|
||||
"shareBannerDescription": "URL van bannerafbeelding of pad dat toegankelijk is voor de client -- zowel abosolute als indexpath worden geaccepteerd: bijv. 'https://domain.com/banner.png' of '/path/to/banner.png'.",
|
||||
"shareFaviconDescription": "Favicon URL of pad toegankelijk door de client -- zowel abosolute als index path worden geaccepteerd: bijv. 'https://domain.com/favicon.png' of '/path/to/favicon.png'.",
|
||||
"hideNavButtons": "Navigatieknoppen verbergen",
|
||||
"hideNavButtonsDescription": "Verberg de navigatieknoppen op de navigatiebalk in de gedeelde map om een minimalistische look te creëren.",
|
||||
"viewModeDescription": "Standaard lay-out voor de gedeelde pagina.",
|
||||
"titleDefault": "Gedeelde bestanden - {title}",
|
||||
"descriptionDefault": "Er is een gedeelde map naar u verzonden om te bekijken of te downloaden.",
|
||||
"descriptionUploadDefault": "U hebt een gedeelde map ontvangen om bestanden te uploaden.",
|
||||
"disableShareCard": "Gedeelde map-kaart uitschakelen",
|
||||
"disableShareCardDescription": "De gedeelde map-kaart uitschakelen op de gedeelde pagina in de zijbalk of bovenaan de pagina op mobiel.",
|
||||
"disableSidebar": "Zijbalk uitschakelen",
|
||||
"disableFileViewer": "Bestandsverkenner uitschakelen",
|
||||
"disableSidebarDescription": "De zijbalk op de gedeelde pagina uitschakelen.",
|
||||
"enforceDarkLightMode": "Themamodus afdwingen",
|
||||
"enforceDarkLightModeDescription": "Een specifieke themamodus (donker of licht) afdwingen voor deze gedeelde map, waarbij gebruikersvoorkeuren worden overschreven.",
|
||||
"defaultViewMode": "Standaard weergavemodus",
|
||||
"defaultViewModeDescription": "Stel de standaard weergavemodus in voor anonieme gebruikers die deze gedeelde map bekijken. Aangemelde gebruikers kunnen nog steeds hun weergavevoorkeur wijzigen.",
|
||||
"default": "Gebruikersvoorkeuren toepassen",
|
||||
"dark": "Donker",
|
||||
"light": "Licht",
|
||||
"enableOnlyOffice": "OnlyOffice Viewer inschakelen",
|
||||
"enableOnlyOfficeDescription": "Het bekijken van Office-bestanden toestaan met OnlyOffice in deze gedeelde map.",
|
||||
"shareType": "Type gedeelde map",
|
||||
"shareTypeDescription": "Het type gedeelde map dat moet worden aangemaakt. Normale gedeelde mappen staan het bekijken en downloaden van bestanden toe. Upload gedeelde mappen staan het uploaden van bestanden naar de gedeelde map toe.",
|
||||
"normalShare": "Normaal",
|
||||
"uploadShare": "Alleen uploaden",
|
||||
"perUserDownloadLimit": "Downloadlimiet op elke gebruiker toepassen",
|
||||
"perUserDownloadLimitDescription": "Indien ingeschakeld, wordt de downloadlimiet toegepast op elke gebruiker. Anonieme gebruikers kunnen geen bestanden downloaden van deze gedeelde map.",
|
||||
"extractEmbeddedSubtitles": "Ingesloten ondertitels uitpakken",
|
||||
"extractEmbeddedSubtitlesDescription": "Ingesloten ondertitels uit mediabestanden halen tijdens het laden -- dit kan traag zijn voor grote bestanden.",
|
||||
"disableLoginOption": "Aanmeldingsoptie verbergen",
|
||||
"disableLoginOptionDescription": "De aanmeldknop in de zijbalk verbergen voor anonieme gebruikers. Als deze optie is ingeschakeld, zien anonieme gebruikers geen optie om in te loggen.",
|
||||
"customizeSidebarLinks": "Koppelingen in de zijbalk aanpassen",
|
||||
"customizeSidebarLinksButton": "Koppelingen aanpassen",
|
||||
"browseFiles": "Bladeren door bestanden",
|
||||
"selectBannerImage": "Banner-afbeelding selecteren",
|
||||
"selectFaviconImage": "Favicon-afbeelding selecteren",
|
||||
"selectImage": "Afbeelding selecteren",
|
||||
"disableDownload": "Het downloaden van bestanden uitschakelen",
|
||||
"disableDownloadDescription": "Niemand kan bestanden downloaden of de inhoud van een bestand bekijken.",
|
||||
"allowModify": "Bewerken van bestanden toestaan",
|
||||
"allowModifyDescription": "Iedereen kan bestanden bewerken in deze gedeelde map.",
|
||||
"allowDelete": "Bestanden verwijderen toestaan",
|
||||
"allowDeleteDescription": "Iedereen kan bestanden en mappen van deze gedeelde map verwijderen.",
|
||||
"allowCreate": "Bestanden en mappen aanmaken en uploaden",
|
||||
"allowCreateDescription": "Iedereen kan nieuwe bestanden en mappen aanmaken en uploaden in deze gedeelde map.",
|
||||
"allowReplacements": "Vervanging van bestanden toestaan",
|
||||
"allowReplacementsDescription": "Iedereen kan bestaande bestanden vervangen door bestanden met dezelfde naam te uploaden.",
|
||||
"shareInfo": "QR-code en info delen",
|
||||
"shareInfoDescription": "Deze koppeling toont de informatie van de gedeelde map, inclusief titel, beschrijving, banner en QR-code in een popup.",
|
||||
"downloadDescription": "Met deze koppeling kunt u de volledige inhoud van de gedeelde map downloaden als een gecomprimeerd bestand."
|
||||
},
|
||||
"prompts": {
|
||||
"operationInProgress": "Werk in uitvoering, even geduld...",
|
||||
"quickJump": "Snel navigeren naar andere bestanden",
|
||||
"downloadsLimit": "Max. aantal downloads",
|
||||
"maxBandwidth": "Max. downloadbandbreedte (kbps)",
|
||||
"shareTheme": "Thema delen",
|
||||
"viewMode": "Weergavemodus",
|
||||
"shareThemeColor": "Themakleur delen (CSS-waarde)",
|
||||
"shareTitle": "Titel delen",
|
||||
"shareDescription": "Beschrijving delen",
|
||||
"shareLogo": "URL van logo delen",
|
||||
"shareBanner": "URL van banner delen",
|
||||
"shareFavicon": "URL van favicon delen",
|
||||
"destinationSource": "Bestemmingsbron:",
|
||||
"selectPath": "Pad kiezen",
|
||||
"copy": "Kopiëren",
|
||||
"copyMessage": "Kies een locatie om uw bestanden te kopiëren: ",
|
||||
"deleteMessageMultiple": "Weet u zeker dat u {count} bestand(en) wilt verwijderen?",
|
||||
"deleteMessageSingle": "Weet u zeker dat u dit bestand/deze map wil verwijderen?",
|
||||
"deleteMessageShare": "Weet u zeker dat u deze gedeelde map wilt verwijderen: {path} ?",
|
||||
"deleteUserMessage": "Weet u zeker dat u deze gebruiker wilt verwijderen?",
|
||||
"deleteTitle": "Bestanden verwijderen",
|
||||
"displayName": "Weergavenaam: ",
|
||||
"download": "Bestanden downloaden",
|
||||
"downloadMessage": "Kies het formaat dat u wilt downloaden. ",
|
||||
"dragAndDrop": "Sleep bestanden hier naartoe",
|
||||
"error": "Er ging iets mis",
|
||||
"completed": "Voltooid",
|
||||
"fileInfo": "Bestandsinformatie",
|
||||
"selected": "geselecteerd",
|
||||
"filesSelected": "bestanden geselecteerd",
|
||||
"lastModified": "Laatst gewijzigd{suffix}",
|
||||
"move": "Verplaatsen",
|
||||
"moveSuccess": "Bestanden succesvol verplaatst!",
|
||||
"newFileSuccess": "Bestand succesvol aangemaakt!",
|
||||
"newDirSuccess": "Map succesvol aangemaakt!",
|
||||
"copySuccess": "Bestanden succesvol gekopieerd!",
|
||||
"moveMessage": "Kies een nieuwe locatie voor de bestand(en)/map(pen)",
|
||||
"newArchetype": "Maak een nieuw bericht op basis van een archetype. Uw bestand wordt aangemaakt in de inhoudsmap.",
|
||||
"newDir": "Nieuwe map",
|
||||
"newDirMessage": "Voer de naam van de nieuwe map in",
|
||||
"newFile": "Nieuw bestand",
|
||||
"newFileMessage": "Voer de naam van het nieuwe bestand in",
|
||||
"numberDirs": "Aantal mappen{suffix}",
|
||||
"numberFiles": "Aantal bestanden{suffix}",
|
||||
"rename": "Hernoemen",
|
||||
"renameSuccess": "Item met succes hernoemd!",
|
||||
"renameMessageConflict": "Een item met de naam \"{filename}\" bestaat al!",
|
||||
"renameMessage": "Voer een nieuwe naam in:",
|
||||
"renameMessageInvalid": "Een bestand kan geen \"/\" of \"\\\" bevatten.",
|
||||
"renameFailed": "Hernoemen van item mislukt",
|
||||
"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",
|
||||
"saveBeforeExit": "Niet-opgeslagen wijzigingen",
|
||||
"saveBeforeExitMessage": "U hebt niet-opgeslagen wijzigingen in de editor. Wat wilt u doen?",
|
||||
"schedule": "Plannen",
|
||||
"scheduleMessage": "Kies een datum en tijd om de publicatie van dit bericht in te plannen.",
|
||||
"show": "Tonen",
|
||||
"size": "Grootte{suffix}",
|
||||
"upload": "Uploaden",
|
||||
"uploadSettingsChunked": "Max. aantal gelijktijdige uploads: {maxConcurrentUpload}, brokgrootte: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Max. aantal gelijktijdige uploads: {maxConcurrentUpload}, upload in brokken uitgeschakeld",
|
||||
"optionalPassword": "Optioneel wachtwoord",
|
||||
"optionalPasswordHelp": "Indien ingesteld, moeten gebruikers dit wachtwoord invoeren om de gedeelde inhoud te bekijken.",
|
||||
"typeName": "Type:",
|
||||
"deleted": "Met succes verwijderd!",
|
||||
"conflictsDetected": "Conflicten gedetecteerd",
|
||||
"copyPasteConfirm": "Bevestiging kopiëren/plakken",
|
||||
"copyItemQuestion": "Dit item kopiëren naar de nieuwe locatie?",
|
||||
"moveItemQuestion": "Dit item verplaatsen naar de nieuwe locatie?",
|
||||
"sourcePath": "Bron:",
|
||||
"destinationPath": "Bestemming:",
|
||||
"closeWithActiveUploads": "Actieve uploads",
|
||||
"closeWithActiveUploadsMessage": "Waarschuwing: U hebt actieve uploads en het sluiten van het uploadvenster kan resulteren in onvoltooide uploads. Wat wilt u doen?",
|
||||
"basicInfo": "Basisinformatie",
|
||||
"directoryInfo": "Mapinformatie",
|
||||
"additionalInfo": "Aanvullende informatie",
|
||||
"hidden": "Verborgen",
|
||||
"hasPreview": "Voorbeeld",
|
||||
"checksums": "Controlesommen",
|
||||
"hashAlgorithm": "Hash-algoritme",
|
||||
"hashValue": "Hashwaarde",
|
||||
"selectHashAlgorithm": "Selecteer een hash-algoritme",
|
||||
"generate": "Genereren",
|
||||
"errorGeneratingHash": "Fout bij het genereren van hash",
|
||||
"hashCopied": "Hash gekopieerd naar klembord",
|
||||
"copyToClipboardFailed": "Kan niet naar klembord kopiëren",
|
||||
"errorCopyingHash": "Fout bij het kopiëren van hash",
|
||||
"token": "Token",
|
||||
"onlyOfficeId": "OnlyOffice-ID",
|
||||
"firstLoadTitle": "Welkom bij FileBrowser Quantum!",
|
||||
"firstLoadBody": "Er is een nieuwe database aangemaakt bij het opstarten. Als dit niet is wat u verwachtte, controleer dan het configuratiebestand voor het `server.database` pad en zorg ervoor dat het bestaat. Op docker kan het databasepad `./data/database.db` of `./database.db` zijn.",
|
||||
"halloweenTitle": "Happy Halloween!",
|
||||
"halloweenBody": "Het Halloween-thema is toegepast. U kunt het uitschakelen als u dit niet prettig vindt, of permanent uitschakelen in de profielinstellingen door een ander thema te selecteren.",
|
||||
"moveFailed": "Verplaatsen mislukt",
|
||||
"operationFailed": "Bewerking is mislukt",
|
||||
"partialSuccess": "Enkele bewerkingen zijn mislukt: {succeeded} gelukt, {failed} mislukt."
|
||||
},
|
||||
"search": {
|
||||
"searchContext": "Zoekcontext: {context}",
|
||||
"onlyFolders": "Alleen mappen",
|
||||
"onlyFiles": "Alleen bestanden",
|
||||
"showOptions": "Opties weergeven",
|
||||
"notEnoughCharacters": "Niet genoeg tekens om te zoeken (min {minSearchLength})",
|
||||
"typeToSearch": "Begin met het typen van {minSearchLength} of meer tekens om te beginnen met zoeken.",
|
||||
"noResults": "Geen resultaten gevonden in geïndexeerde zoekopdrachten.",
|
||||
"smallerThan": "Kleiner dan:",
|
||||
"largerThan": "Groter dan:",
|
||||
"showPreviewImages": "Voorbeeldafbeeldingen tonen",
|
||||
"showPreviewImagesDescription": "Toon, indien beschikbaar, miniatuurvoorbeelden voor zoekresultaten en toon grotere pictogrammen voor alle resultaten.",
|
||||
"helpText1": "Er wordt gezocht op elk teken dat u 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."
|
||||
},
|
||||
"profileSettings": {
|
||||
"previewFolder": "Toon miniaturen voor mappen indien beschikbaar",
|
||||
"previewFolderDescription": "Toon miniatuurafbeeldingen voor mappen met het eerste item met miniatuurafbeelding als omslag.",
|
||||
"debugOfficeEditor": "OnlyOffice foutopsporingsmodus inschakelen",
|
||||
"debugOfficeEditorDescription": "Een foutopsporingspop-up weergeven met aanvullende informatie in de OnlyOffice-editor.",
|
||||
"defaultMediaPlayer": "De ingebouwde mediaspeler van uw browser gebruiken",
|
||||
"defaultMediaPlayerDescription": "Gebruik de eigen mediaspeler van uw browser voor video- en audiobestanden in plaats van de meegeleverde 'vue-plyr' mediaspeler van FileBrowser.",
|
||||
"showSelectMultiple": "Meervoudige selectie 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.",
|
||||
"customTheme": "Kies uw thema",
|
||||
"defaultThemeDescription": "standaard - Het standaardthema",
|
||||
"setDateFormat": "Exact datumformaat instellen",
|
||||
"showHiddenFiles": "Verborgen bestanden weergeven",
|
||||
"showHiddenFilesDescription": "Toon verborgen bestanden in de lijst (bijv. .thumbnails, ProgramData), maar ze zijn nog steeds handmatig toegankelijk als ze zijn geconfigureerd om te worden geïndexeerd.",
|
||||
"showQuickDownload": "Downloadpictogram weergeven voor snelle toegang",
|
||||
"showQuickDownloadDescription": "Toon een downloadpictogram voor snelle toegang tot elk bestandsitem waarop kan worden geklikt om het meteen te downloaden.",
|
||||
"previewImages": "Miniaturen van afbeeldingen weergeven",
|
||||
"previewImagesDescription": "Toon miniatuurvoorbeelden voor afbeeldingen, toont generiek pictogram indien uitgeschakeld.",
|
||||
"previewVideos": "Miniaturen van video's weergeven",
|
||||
"previewVideosDescription": "Toon miniatuurvoorbeelden voor video's, toont generiek pictogram indien uitgeschakeld.",
|
||||
"previewMotionVideos": "Bewegingsvoorbeeld voor video's inschakelen",
|
||||
"previewMotionVideosDescription": "Bewegingsvoorbeeld is het geanimeerde popup-voorbeeld dat door de frames van de video loopt.",
|
||||
"highQualityPreview": "Miniaturen van hoge kwaliteit",
|
||||
"highQualityPreviewDescription": "Heeft alleen invloed op de galerieweergave. Het laden kan langer duren bij een langzame verbinding.",
|
||||
"editOffice": "Office-bestanden bekijken en bewerken met OnlyOffice",
|
||||
"editOfficeDescription": "Als alleen office is ingeschakeld, is de bestandsviewer/editor uitgeschakeld voor alle bestandsextensies die gerelateerd zijn aan Office-bestanden.",
|
||||
"popupPreview": "Pop-upvoorbeeld inschakelen wanneer u met de muis over bestanden beweegt",
|
||||
"popupPreviewDescription": "Er verschijnt een overlay pop-upvenster over de UI wanneer u met de muis over bestanden beweegt die een groot voorbeeld ondersteunen.",
|
||||
"filePreviewOptions": "Opties voor bestandsvoorbeeld",
|
||||
"disableUpdateNotifications": "Het tonen van meldingen update uitschakelen",
|
||||
"disableUpdateNotificationsDescription": "Schakel de banner met updatemeldingen uit die wordt weergegeven voor beheerders in de zijbalk.",
|
||||
"disableHideSidebar": "Houd de zijbalk open bij het bekijken of bewerken van bestanden",
|
||||
"disableHideSidebarDescription": "Als deze optie is ingeschakeld, wordt de zijbalk niet automatisch verborgen bij het bekijken of bewerken van bestanden.",
|
||||
"autoplayMedia": "Mediabestanden automatisch afspelen bij het bekijken",
|
||||
"autoplayMediaDescription": "Mediabestanden automatisch afspelen bij het bekijken. Dit werkt alleen als het bestand een mediabestand is.",
|
||||
"sidebarOptions": "Zijbalk-opties",
|
||||
"disableQuickToggles": "Snel wisselen uitschakelen",
|
||||
"disableFileExtensions": "Voer bestandsextensies in om uit te schakelen",
|
||||
"disableQuickTogglesDescription": "Schakel de sneltoetsen uit die op de zijbalk worden weergegeven.",
|
||||
"disableSearchOptions": "Zoekopties uitschakelen",
|
||||
"disableSearchOptionsDescription": "Schakel de zoekopties in de zijbalk uit.",
|
||||
"hideSidebarFileActions": "Verberg de knop Bestandsacties in de zijbalk",
|
||||
"deleteWithoutConfirming": "Bestanden verwijderen zonder bevestigingsvenster",
|
||||
"deleteWithoutConfirmingDescription": "Bestanden verwijderen zonder bevestigingsvenster. Dit werkt alleen als het bestand een mediabestand is.",
|
||||
"editorViewerOptions": "Opties voor editor en viewer",
|
||||
"editorQuickSave": "Snel opslaan in editor",
|
||||
"editorQuickSaveDescription": "De teksteditor toont altijd het pictogram voor opslaan en verbergt alle andere acties",
|
||||
"previewOffice": "Miniaturen weergeven voor Office-bestanden",
|
||||
"previewOfficeDescription": "Toon miniaturen voor kantoorbestandstypen in de listingweergave",
|
||||
"disableThumbnailPreviews": "Miniaturen uitschakelen voor bepaalde bestandsextensies",
|
||||
"disableThumbnailPreviewsDescription": "Een door spaties gescheiden lijst met bestandsextensies waarvoor miniatuurafbeeldingen uitgeschakeld moeten worden (bijv. '.txt .html').",
|
||||
"disableViewingFiles": "Bestandsviewer/editor uitschakelen voor bepaalde bestandsextensies",
|
||||
"disableViewingFilesDescription": "Een door spaties gescheiden lijst met bestandsextensies waarvoor de bestandsviewer/editor uitgeschakeld moet worden (bijv. '.txt .pdf').",
|
||||
"disablePreviewExt": "Voorbeelden van Office-bestanden uitschakelen voor bepaalde bestandsextensies",
|
||||
"disablePreviewExtDescription": "Een door spaties gescheiden lijst met bestandsextensies waarvoor Office-bestandsvoorvertoningen moeten worden uitgeschakeld (bijv. '.docx .pptx').",
|
||||
"disableOfficeEditorDescription": "Een door spaties gescheiden lijst van bestandsextensies waarvoor de OnlyOffice editor en viewer uitgeschakeld moeten worden - of '*' om alle extensies uit te schakelen. (bijv. '.txt .html .pdf').",
|
||||
"disableOfficeEditor": "Office Viewer uitschakelen voor bepaalde bestandsextensies",
|
||||
"fileViewerOptions": "Opties voor bestandsviewer",
|
||||
"themeAndLanguage": "Thema & Taal"
|
||||
},
|
||||
"settings": {
|
||||
"systemAdmin": "Systeem & Beheer",
|
||||
"configViewer": "Configuratiescherm",
|
||||
"configViewerShowFull": "Volledige configuratie weergeven",
|
||||
"configViewerShowComments": "Opmerkingen tonen",
|
||||
"configViewerLoadConfig": "Configuratie laden",
|
||||
"enterPassword": "Wachtwoord invoeren",
|
||||
"enterPasswordAgain": "Wachtwoord opnieuw invoeren",
|
||||
"passwordsDoNotMatch": "Wachtwoorden komen niet overeen",
|
||||
"loginMethod": "Aanmeldingsmethode",
|
||||
"loginMethods": {
|
||||
"password": "Wachtwoord",
|
||||
"oidc": "OIDC",
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"loginMethodDescription": "toegestane aanmeldingsmethode voor de gebruiker.",
|
||||
"modifyOtherUser": "Gebruiker wijzigen:",
|
||||
"modifyCurrentUser": "Huidige gebruiker wijzigen",
|
||||
"themeColor": "Themakleur",
|
||||
"listingOptions": "Lijstopties",
|
||||
"searchOptions": "Zoekopties",
|
||||
"emptyGlobal": "Op dit moment zijn er geen globale configuratieopties. U kunt de configuratie yaml bewerken om systeeminstellingen aan te passen.",
|
||||
"scopes": "Toegestane bronnen en het gebruikersbereik daarbinnen definiëren",
|
||||
"UserManagement": "Gebruikersbeheer",
|
||||
"userNotAdmin": "Gebruiker is geen beheerder",
|
||||
"administrator": "Beheerder",
|
||||
"allowCommands": "Opdrachten uitvoeren",
|
||||
"allowEdit": "Bestanden of mappen bewerken, hernoemen of verwijderen",
|
||||
"allowNew": "Nieuwe bestanden of mappen aanmaken",
|
||||
"allowPublish": "Nieuwe berichten en pagina's publiceren",
|
||||
"allowSignup": "Gebruikers toestaan om zich te registreren",
|
||||
"avoidChanges": "(Leeg laten om wijzigingen te voorkomen)",
|
||||
"brandingDirectoryPath": "Pad naar map met Branding",
|
||||
"brandingHelp": "U kunt aanpassen hoe deze FileBrowser 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.\nMeer informatie over het aanpassen van de branding, kunt u lezen op {0}.",
|
||||
"changePassword": "Wachtwoord wijzigen",
|
||||
"commandRunner": "Opdrachten uitvoeren",
|
||||
"commandRunnerHelp": "Hier kunt u opdrachten instellen die worden uitgevoerd in de genoemde gebeurtenissen. U moet er één per regel schrijven. De omgevingsvariabelen {0} en {1} zijn beschikbaar, waarbij {0} relatief is ten opzichte van {1}. Lees de {2} voor meer informatie over deze functie en de beschikbare omgevingsvariabelen.",
|
||||
"commandsUpdated": "Opdrachten bijgewerkt!",
|
||||
"createUserDir": "Maak automatisch een thuismap aan wanneer een nieuwe gebruiker wordt aangemaakt",
|
||||
"tusUploads": "Verzamelde uploads",
|
||||
"tusUploadsHelp": "FileBrowser ondersteunt bestandsuploads in brokken, waardoor efficiënte, betrouwbare, hervattbare bestandsuploads mogelijk zijn, zelfs op onbetrouwbare netwerken.",
|
||||
"tusUploadsChunkSize": "Geeft de maximale grootte van een verzoek aan (directe uploads worden gebruikt voor kleinere uploads). U kunt een gewoon geheel getal invoeren dat staat voor een bytes-invoer of een tekenreeks zoals 10MB, 1GB enz.",
|
||||
"tusUploadsRetryCount": "Aantal pogingen dat moet worden uitgevoerd als het uploaden van een brok mislukt.",
|
||||
"userHomeBasePath": "Basispad voor thuismappen van gebruikers",
|
||||
"userScopeGenerationPlaceholder": "Het bereik wordt automatisch gegenereerd",
|
||||
"createUserHomeDirectory": "Thuismap voor gebruiker aanmaken",
|
||||
"customStylesheet": "Aangepast Stylesheet",
|
||||
"defaultUserDescription": "Dit zijn de standaardinstellingen voor nieuwe gebruikers.",
|
||||
"disableExternalLinks": "Externe links uitschakelen (behalve documentatie)",
|
||||
"disableUsedDiskPercentage": "Grafiek voor gebruikt schijfpercentage uitschakelen",
|
||||
"executeOnShell": "Uitvoeren in het opdrachtvenster",
|
||||
"executeOnShellDescription": "FileBrowser voert de opdrachten standaard uit door hun binaire bestanden rechtstreeks op te roepen. Als u ze in plaats daarvan wilt uitvoeren op een opdrachtvenster (zoals Bash of PowerShell), kunt u dit hier definiëren met de vereiste argumenten en vlaggen. Indien ingesteld, wordt de opdracht die u uitvoert, toegevoegd als een argument. Dit is van toepassing op zowel gebruikersopdrachten als event hooks.",
|
||||
"globalRules": "Dit is een algemene reeks regels die al dan niet zijn toegestaan. Ze zijn van toepassing op elke gebruiker. U kunt specifieke regels voor de instellingen van elke gebruiker definiëren om deze te overschrijven.",
|
||||
"globalSettings": "Algemene instellingen",
|
||||
"shareSettings": "Instellingen voor Delen",
|
||||
"api": "API-sleutels",
|
||||
"insertPath": "Een pad invoeren",
|
||||
"insertRegex": "Regex expressie invoeren",
|
||||
"instanceName": "Naam van de instantie",
|
||||
"lockPassword": "Voorkom dat de gebruiker het wachtwoord wijzigt",
|
||||
"newPassword": "Uw nieuwe wachtwoord",
|
||||
"newPasswordConfirm": "Uw nieuwe wachtwoord bevestigen",
|
||||
"newUser": "Nieuwe gebruiker",
|
||||
"newUserHintSubFolder": "bijv. '/submap' - leeg voor standaard",
|
||||
"passwordUpdated": "Wachtwoord bijgewerkt!",
|
||||
"permissions": {
|
||||
"admin": "Beheerder",
|
||||
"modify": "Bestanden bewerken",
|
||||
"share": "Bestanden delen",
|
||||
"api": "Langlevende API-sleutels aanmaken en beheren",
|
||||
"realtime": "Real-time verbindingen en updates mogelijk maken",
|
||||
"create": "Bestanden aanmaken",
|
||||
"delete": "Bestanden verwijderen",
|
||||
"download": "Bestanden downloaden"
|
||||
},
|
||||
"permissions-name": "Machtigingen",
|
||||
"permissionsHelp": "Gebruikersrechten bepalen welke acties de gebruiker kan uitvoeren. Toegang als beheerder geeft volledige controle om de instellingen van elke gebruiker bij te werken en te wijzigen, evenals vele systeeminstellingen.",
|
||||
"profileSettings": "Profielinstellingen",
|
||||
"ruleExample1": "voorkomt de toegang tot elk puntbestand (zoals .git, .gitignore) in elke map.\n",
|
||||
"ruleExample2": "blokkeert de toegang tot het bestand met de naam Caddyfile in de hoofdmap van het bereik.",
|
||||
"rulesHelp": "Hier kunt u een reeks regels voor toestaan en niet toestaan voor deze specifieke gebruiker definiëren. De geblokkeerde bestanden verschijnen niet in de lijsten en zijn niet toegankelijk voor de gebruiker. We ondersteunen regex en paden relatief ten opzichte van het bereik van gebruikers. \n",
|
||||
"settingsUpdated": "Instellingen bijgewerkt!",
|
||||
"shareManagement": "Beheer van Gedeelde mappen",
|
||||
"shareDeleted": "Gedeelde map verwijderd!",
|
||||
"singleClick": "Gebruik één klik om bestanden en mappen te openen",
|
||||
"userCommandsHelp": "Een lijst met beschikbare opdrachten voor de gebruiker gescheiden door spaties. Voorbeeld: \n",
|
||||
"userCreated": "Gebruiker aangemaakt!",
|
||||
"userDefaults": "Standaardinstellingen gebruiker",
|
||||
"userDeleted": "Gebruiker verwijderd!",
|
||||
"userManagement": "Gebruikersbeheer",
|
||||
"userUpdated": "Gebruiker bijgewerkt!",
|
||||
"adminOptions": "Opties voor beheerders"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Site-instellingen",
|
||||
"updateIsAvailable": "Er is een update beschikbaar!",
|
||||
"fileActions": "Bestandsacties",
|
||||
"customizeLinks": "Zijbalk koppelingen aanpassen",
|
||||
"customizeLinksDescription": "Pas de koppelingen in de zijbalk aan. Gebruik slepen en neerzetten om te ordenen.",
|
||||
"customizeShareLinks": "Koppelingen in Zijbalk Delen aanpassen",
|
||||
"customizeShareLinksDescription": "Pas de koppelingen aan die worden weergegeven in de zijbalk voor deze gedeelde map. Slepen en neerzetten om te herschikken.",
|
||||
"currentLinks": "Huidige koppelingen",
|
||||
"addNewLink": "Nieuwe koppeling toevoegen",
|
||||
"editLink": "Koppeling bewerken",
|
||||
"linkType": "Type koppeling",
|
||||
"selectLinkType": "Selecteer een type koppeling",
|
||||
"customLink": "Aangepaste koppeling",
|
||||
"selectSource": "Bron selecteren",
|
||||
"chooseSource": "Kies een bron",
|
||||
"selectShare": "Selecteer gedeelde map",
|
||||
"chooseShare": "Kies een gedeelde map",
|
||||
"selectSharePath": "Pad binnen gedeelde map (optioneel)",
|
||||
"sharePathPlaceholder": "bijv. /submap of leeg laten voor root",
|
||||
"selectTool": "Selecteer gereedschap",
|
||||
"chooseTool": "Kies een hulpmiddel",
|
||||
"linkName": "Link Naam",
|
||||
"linkNamePlaceholder": "Naam koppeling invoeren",
|
||||
"linkUrl": "URL van koppeling",
|
||||
"linkUrlPlaceholder": "Voer een URL of pad in (bijv. /files/path of https://example.com)",
|
||||
"linkIcon": "Pictogram van koppeling",
|
||||
"linkIconPlaceholder": "Geef optioneel een materiaalpictogramnaam op",
|
||||
"browseIcons": "Bladeren door pictogrammen",
|
||||
"pickIcon": "Kies een pictogram",
|
||||
"pickIconDescription": "Zoek en selecteer een pictogram uit de bibliotheek met materiaalpictogrammen",
|
||||
"linksUpdatedSuccess": "Koppelingen in zijbalk succesvol bijgewerkt!",
|
||||
"shareLinksUpdatedSuccess": "Koppelingen in zijbalk Delen succesvol bijgewerkt!",
|
||||
"linksUpdateFailed": "Bijwerken van koppelingen in zijbalk is mislukt",
|
||||
"sourceNotAccessible": "Deze bron is niet langer toegankelijk voor jou",
|
||||
"noLinksYet": "Er zijn nog geen links geconfigureerd. Voeg hieronder uw eerste link toe.",
|
||||
"shareHome": "Thuis delen"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "Koppeling gekopieerd!"
|
||||
},
|
||||
"time": {
|
||||
"days": "Dagen",
|
||||
"hours": "Uren",
|
||||
"minutes": "Minuten",
|
||||
"seconds": "Seconden",
|
||||
"unit": "Tijdseenheid"
|
||||
},
|
||||
"api": {
|
||||
"title": "API-sleutels",
|
||||
"createdAt": "Aangemaakt op:",
|
||||
"expiresAt": "Verloopt op:",
|
||||
"permissions": "Machtigingen:",
|
||||
"createTitle": "API-sleutel aanmaken",
|
||||
"keyNamePlaceholder": "Voer een unieke naam voor de API-sleutel in:",
|
||||
"tokenDuration": "Duur van token:",
|
||||
"durationNumberPlaceholder": "Aantal",
|
||||
"days": "Dagen",
|
||||
"months": "Maanden",
|
||||
"permissionNote": "Kies minstens één machtiging voor de sleutel. Uw gebruiker moet deze rechten ook hebben.",
|
||||
"createKeyFailed": "API-sleutel is niet aangemaakt.",
|
||||
"createKeySuccess": "API-sleutel met succes aangemaakt.",
|
||||
"apiKeyDeleted": "API-sleutel verwijderd!",
|
||||
"description": "API-sleutels zijn gebaseerd op de gebruiker die ze aanmaakt. Zie",
|
||||
"swaggerLinkText": "branie-pagina",
|
||||
"created": "Aangemaakt op",
|
||||
"expires": "Verloopt op",
|
||||
"actions": "Acties",
|
||||
"enabled": "Ingeschakeld",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"tokenType": "Type token:",
|
||||
"minimalDescription": "Minimaal token",
|
||||
"minimalInfo": "Sluit tokenaanpassingen uit om een kleinere tokenlengte te creëren. Exacte overeenkomst met de rechten van de bijbehorende gebruiker.",
|
||||
"clickToCopyKey": "Klik om sleutel te kopiëren (verborgen)"
|
||||
},
|
||||
"index": {
|
||||
"unknown": "Onbekend",
|
||||
"simple": "Eenvoudig",
|
||||
"normal": "Normaal",
|
||||
"complex": "Complex",
|
||||
"highlyComplex": "Zeer complex",
|
||||
"assessment": "Beoordeling",
|
||||
"ready": "Klaar",
|
||||
"indexing": "Indexering",
|
||||
"unavailable": "Niet beschikbaar",
|
||||
"error": "Fout",
|
||||
"lastScanned": "Laatst gescand",
|
||||
"quickScan": "Seconden voor Snelle scan",
|
||||
"fullScan": "Seconden voor Volledige scan",
|
||||
"settingsHover": "Instellingen voor gebruiker",
|
||||
"toggleClick": "Enkele klik schakelen",
|
||||
"toggleDark": "Donkere modus schakelen",
|
||||
"toggleSticky": "Kleefmodus schakelen",
|
||||
"noSources": "Geen bronnen beschikbaar voor deze gebruiker. Voeg een bron toe voor deze gebruiker of configureer een bron met defaultEnabled: true"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Twee-factorenauthenticatie",
|
||||
"verifyInstructions": "Voer de code van uw authenticator-app in",
|
||||
"generate": "Scan de QR-code met uw authenticator-app",
|
||||
"codeInputPlaceholder": "6-cijferige code",
|
||||
"invalidCodeType": "De verificatiecode moet uit 6 engelse cijfers bestaan",
|
||||
"enabledSuccessfully": "OTP succesvol ingeschakeld",
|
||||
"verificationFailed": "Verificatie is mislukt",
|
||||
"verificationSucceed": "Verificatie is geslaagd",
|
||||
"generationFailed": "Aanmaken is mislukt"
|
||||
},
|
||||
"access": {
|
||||
"all": "Alles weigeren",
|
||||
"rules": "Toegangsregels",
|
||||
"deny": "Weigeren",
|
||||
"deniedUsers": "Geweigerde gebruikers",
|
||||
"deniedGroups": "Geweigerde groepen",
|
||||
"allowedUsers": "Toegestane gebruikers",
|
||||
"allowedGroups": "Toegestane groepen",
|
||||
"totalDenied": "Geweigerd",
|
||||
"totalAllowed": "Toegestaan",
|
||||
"allow": "Toestaan",
|
||||
"allowDeny": "Toestaan/weigeren",
|
||||
"userGroup": "Gebruiker/Groep",
|
||||
"enterName": "Voer de naam van de gebruiker of groep in",
|
||||
"deleted": "Toegangsregel verwijderd!",
|
||||
"deletedCascade": "Toegangsregels verwijderd van pad en alle subpaden!",
|
||||
"added": "Toegangsregel toegevoegd!",
|
||||
"accessManagement": "Toegangsbeheer",
|
||||
"defaultBehavior": "Standaardactie{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.",
|
||||
"cascadeDelete": "Recursief verwijderen",
|
||||
"cascadeDeleteDescription": "Indien ingeschakeld, zal het verwijderen van een toegangsregel deze ook verwijderen uit alle submappen. Hierdoor kunt u een gebruiker of groep volledig verwijderen uit de hele mappenstructuur."
|
||||
},
|
||||
"fileLoading": {
|
||||
"title": "Uploaden & Downloads",
|
||||
"maxConcurrentUpload": "Max. gelijktijdige uploads",
|
||||
"maxConcurrentUploadHelp": "Het maximum aantal gelijktijdige uploads. Hogere waarden presteren meestal beter, maar dit hangt van veel factoren af.",
|
||||
"uploadChunkSizeMb": "Upload brokgrootte in MB",
|
||||
"uploadChunkSizeMbHelp": "Gewoonlijk 5 tot 50 MB, kleiner is beter voor onstabiele of beperkte verbindingen. Instellen op 0 schakelt uploads in brokken uit.",
|
||||
"downloadChunkSizeMb": "Grootte downloadbrok in MB",
|
||||
"downloadChunkSizeMbHelp": "Brok-grootte voor downloads in brokken. Bestanden die groter zijn dan deze grootte worden in brokken gedownload om time-outproblemen te voorkomen (bijv. Cloudflare 524 fouten). Instellen op 0 schakelt downloads in brokken uit.",
|
||||
"clearAll": "Wissen voltooid moet fout, pauze en conflict verhelpen",
|
||||
"clearAllDescription": "De knop Wissen voltooid wist alle uploads in een voltooide status, inclusief fout-, gepauzeerde en conflictstatussen.",
|
||||
"uploadSettings": "Instellingen uploaden"
|
||||
},
|
||||
"colors": {
|
||||
"red": "rood",
|
||||
"green": "groen",
|
||||
"blue": "blauw",
|
||||
"yellow": "geel",
|
||||
"violet": "violet",
|
||||
"orange": "oranje"
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Het is niet gelukt de editor te initialiseren. Opnieuw 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.",
|
||||
"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.",
|
||||
"fileSaved": "Bestand opgeslagen!",
|
||||
"errorSavingFile": "Fout bij het opslaan van bestand: {error}"
|
||||
},
|
||||
"player": {
|
||||
"LoopEnabled": "Herhaling ingeschakeld",
|
||||
"LoopDisabled": "Herhaling uitgeschakeld",
|
||||
"PlayAllOncePlayback": "Alles eenmaal afspelen",
|
||||
"ShuffleAllPlayback": "Alles willekeurig afspelen",
|
||||
"PlayAllLoopedPlayback": "Alles herhaald afspelen",
|
||||
"QueueButtonHint": "Afspeellijst weergeven",
|
||||
"QueuePlayback": "Afspeellijst",
|
||||
"changePlaybackMode": "Afspeelmodus wijzigen",
|
||||
"changePlaybackModeHint": "Wijzig afspeelmodus om meer bestanden te zien",
|
||||
"emptyQueue": "Wachtrij is leeg"
|
||||
},
|
||||
"fileSizeAnalyzer": {
|
||||
"title": "Bestandsgrootte analyseren",
|
||||
"searchLocation": "Zoeklocatie",
|
||||
"selectPath": "Zoeklocatie selecteren",
|
||||
"largerThan": "Groter dan (MB)",
|
||||
"includeFolders": "Mappen opnemen",
|
||||
"includeFoldersDescription": "mappen meenemen in de analyse",
|
||||
"totalFiles": "Totaal aantal bestanden{suffix}",
|
||||
"totalSize": "Totale grootte{suffix}",
|
||||
"emptyState": "Voer zoekcriteria in en klik op Analyseren om bestandsgroottes te visualiseren",
|
||||
"completeResults": "Volledige resultaten getoond!",
|
||||
"incompleteResults": "Resultaten kunnen onvolledig zijn",
|
||||
"relativePercentage": "Relatief percentage{suffix}",
|
||||
"percentageOfResults": "{percentage}% resultaten"
|
||||
},
|
||||
"fileTypes": {
|
||||
"video": "Video",
|
||||
"image": "Afbeelding",
|
||||
"audio": "Audio",
|
||||
"archive": "Archief",
|
||||
"document": "Document",
|
||||
"binary": "Binair",
|
||||
"directory": "Map",
|
||||
"other": "Overig"
|
||||
},
|
||||
"tools": {
|
||||
"title": "Gereedschap",
|
||||
"description": "Hulpprogramma's en analysetools voor uw bestanden",
|
||||
"sizeAnalyzer": {
|
||||
"name": "Bestandsgrootte analyseren",
|
||||
"description": "Visualiseer grote bestanden en mappen met een interactieve treemap"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"name": "Duplicaatbestanden zoeken",
|
||||
"description": "Dubbele bestanden vinden en beheren om opslagruimte terug te winnen"
|
||||
},
|
||||
"materialIconPicker": {
|
||||
"name": "Material pictogrammenkiezer",
|
||||
"description": "Bladeren en zoeken in Material pictogrammen met live voorbeeld",
|
||||
"searchPlaceholder": "Pictogrammen zoeken...",
|
||||
"browseFullLibrary": "Blader door de volledige pictogrammenbibliotheek",
|
||||
"iconLibraryUrl": "fonts.google.com/icons",
|
||||
"popularIcons": "{count} populaire pictogrammen weergeven",
|
||||
"showingResults": "{count} resultaten weergeven",
|
||||
"noResults": "Geen pictogrammen gevonden",
|
||||
"tryDifferentSearch": "Probeer een andere zoekterm",
|
||||
"copiedToClipboard": "'{name}' is gekopieerd naar klembord!",
|
||||
"copyFailed": "Kopiëren naar klembord mislukt",
|
||||
"custom": "Aangepast",
|
||||
"useCustomIcon": "Gebruik een aangepast pictogram: {name}"
|
||||
},
|
||||
"fileWatcher": {
|
||||
"name": "Bestandenbewaker",
|
||||
"description": "Bestanden in realtime bewaken en volgen met latentiebewaking",
|
||||
"file": "Bestand{suffix}",
|
||||
"interval": "Update-interval",
|
||||
"stopWatching": "Stoppen met bewaken",
|
||||
"emptyState": "Kies een bestand en klik op afspelen om te beginnen met het controleren van bestandswijzigingen",
|
||||
"chooseFile": "Kies een bestand"
|
||||
},
|
||||
"toolNotFound": "Gereedschap niet gevonden",
|
||||
"backToTools": "Terug naar Gereedschap"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Meldingen",
|
||||
"description": "Alle meldingen van uw huidige sessie bekijken",
|
||||
"empty": "Nog geen meldingen",
|
||||
"clearAll": "Alles wissen",
|
||||
"clearConfirm": "Weet u zeker dat u de gehele meldingsgeschiedenis wilt wissen?",
|
||||
"total": "{count} meldingen"
|
||||
},
|
||||
"duplicateFinder": {
|
||||
"title": "Duplicaatbestanden zoeken",
|
||||
"minSize": "Minimale grootte (MB){suffix}",
|
||||
"minSizeHint": "Alleen bestanden analyseren die groter zijn dan deze grootte (in megabytes)",
|
||||
"useChecksums": "Bestandsinhoud vergelijken",
|
||||
"useChecksumsDescription": "Neem ook de inhoud van bestanden op in de vergelijking. Zonder dit worden bestanden vergeleken op basis van grootte en bestandsnaam overeenkomst.",
|
||||
"findDuplicates": "Duplicaten zoeken",
|
||||
"groupsFound": "Dubbele groepen gevonden{suffix}",
|
||||
"totalWastedSpace": "Totale verspilde ruimte{suffix}",
|
||||
"wasted": "Verspild{suffix}",
|
||||
"emptyState": "Voer zoekcriteria in en klik op Duplicaten zoeken om dubbele bestanden te identificeren"
|
||||
},
|
||||
"events": {
|
||||
"serverShutdown": "De server is uitgeschakeld.",
|
||||
"authenticationFailed": "Authenticatie is mislukt. Ververs de pagina om opnieuw in te loggen.",
|
||||
"reconnected": "Opnieuw verbinding gemaakt met server.",
|
||||
"connectionLost": "De verbinding met de server is verbroken. Probeer opnieuw verbinding te maken..."
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Utwórz konto",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Opcje użytkownika administratora",
|
||||
"searchOptions": "Opcje wyszukiwania",
|
||||
"systemAdmin": "System i administrator",
|
||||
"configViewer": "Przeglądarka konfiguracji",
|
||||
"configViewerShowFull": "Pokaż pełną konfigurację",
|
||||
"configViewerShowComments": "Pokaż komentarze",
|
||||
"configViewerLoadConfig": "Konfiguracja ładowania",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Pełnomocnik"
|
||||
},
|
||||
"newUserHintSubFolder": "np. '/subfolder' - domyślnie puste",
|
||||
"permissionsHelp": "Uprawnienia użytkownika kontrolują dostęp do rodzaju działań, które użytkownik może wykonywać. Dostęp administratora daje pełną kontrolę nad aktualizacją i zmianą ustawień dowolnego użytkownika, a także wielu ustawień systemowych."
|
||||
"permissionsHelp": "Uprawnienia użytkownika kontrolują dostęp do rodzaju działań, które użytkownik może wykonywać. Dostęp administratora daje pełną kontrolę nad aktualizacją i zmianą ustawień dowolnego użytkownika, a także wielu ustawień systemowych.",
|
||||
"configViewer": "Przeglądarka konfiguracji"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Ustawienia Strony",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "Linie{suffix}",
|
||||
"lastUpdate": "Ostatnia aktualizacja{suffix}",
|
||||
"latency": "Opóźnienie{suffix}",
|
||||
"all": "Wszystkie{suffix}"
|
||||
"all": "Wszystkie{suffix}",
|
||||
"expiration": "Wygaśnięcie{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Zasady dostępu",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Criar uma conta",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Opções do usuário administrador",
|
||||
"searchOptions": "Opções de pesquisa",
|
||||
"systemAdmin": "Sistema e administração",
|
||||
"configViewer": "Visualizador de configuração",
|
||||
"configViewerShowFull": "Mostrar configuração completa",
|
||||
"configViewerShowComments": "Mostrar comentários",
|
||||
"configViewerLoadConfig": "Configuração de carga",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "Por exemplo, '/subfolder' - em branco para o padrão",
|
||||
"permissionsHelp": "As permissões de usuário controlam o acesso aos tipos de ações que o usuário pode executar. O acesso de administrador dá controle total para atualizar e alterar as configurações de qualquer usuário, bem como muitas configurações do sistema."
|
||||
"permissionsHelp": "As permissões de usuário controlam o acesso aos tipos de ações que o usuário pode executar. O acesso de administrador dá controle total para atualizar e alterar as configurações de qualquer usuário, bem como muitas configurações do sistema.",
|
||||
"configViewer": "Visualizador de configuração"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Configurações do site",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "Linhas{suffix}",
|
||||
"lastUpdate": "Última atualização{suffix}",
|
||||
"latency": "Latência{suffix}",
|
||||
"all": "Todos{suffix}"
|
||||
"all": "Todos{suffix}",
|
||||
"expiration": "Vencimento{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regras de acesso",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Criar uma conta",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Opções do utilizador administrador",
|
||||
"searchOptions": "Opções de pesquisa",
|
||||
"systemAdmin": "Sistema e administração",
|
||||
"configViewer": "Visualizador de configuração",
|
||||
"configViewerShowFull": "Mostrar configuração completa",
|
||||
"configViewerShowComments": "Mostrar comentários",
|
||||
"configViewerLoadConfig": "Configuração de carga",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "por exemplo, '/subpasta' - em branco por defeito",
|
||||
"permissionsHelp": "As permissões de utilizador controlam o acesso ao tipo de acções que o utilizador pode executar. O acesso de administrador dá controlo total para atualizar e alterar as definições de qualquer utilizador, bem como muitas definições do sistema."
|
||||
"permissionsHelp": "As permissões de utilizador controlam o acesso ao tipo de acções que o utilizador pode executar. O acesso de administrador dá controlo total para atualizar e alterar as definições de qualquer utilizador, bem como muitas definições do sistema.",
|
||||
"configViewer": "Visualizador de configuração"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Configurações do site",
|
||||
@@ -705,7 +706,8 @@
|
||||
"lines": "Linhas{suffix}",
|
||||
"lastUpdate": "Última atualização{suffix}",
|
||||
"latency": "Latência{suffix}",
|
||||
"all": "Todos{suffix}"
|
||||
"all": "Todos{suffix}",
|
||||
"expiration": "Prazo de validade{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regras de acesso",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Crează cont",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Opțiuni utilizator administrator",
|
||||
"searchOptions": "Opțiuni de căutare",
|
||||
"systemAdmin": "Sistem și administrare",
|
||||
"configViewer": "Vizualizator de configurare",
|
||||
"configViewerShowFull": "Afișați configurația completă",
|
||||
"configViewerShowComments": "Afișați comentariile",
|
||||
"configViewerLoadConfig": "Configurare încărcare",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "de exemplu \"/subfolder\" - gol pentru implicit",
|
||||
"permissionsHelp": "Permisiunile utilizatorului controlează accesul la ce tip de acțiuni poate efectua utilizatorul. Accesul de administrator oferă control deplin pentru actualizarea și modificarea setărilor oricărui utilizator, precum și a multor setări de sistem."
|
||||
"permissionsHelp": "Permisiunile utilizatorului controlează accesul la ce tip de acțiuni poate efectua utilizatorul. Accesul de administrator oferă control deplin pentru actualizarea și modificarea setărilor oricărui utilizator, precum și a multor setări de sistem.",
|
||||
"configViewer": "Vizualizator de configurare"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Setări site",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "Linii{suffix}",
|
||||
"lastUpdate": "Ultima actualizare{suffix}",
|
||||
"latency": "Latență{suffix}",
|
||||
"all": "Toate{suffix}"
|
||||
"all": "Toate{suffix}",
|
||||
"expiration": "Expirare{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Reguli de acces",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Создать аккаунт",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Параметры пользователя администратора",
|
||||
"searchOptions": "Параметры поиска",
|
||||
"systemAdmin": "Система и администрирование",
|
||||
"configViewer": "Программа просмотра конфигурации",
|
||||
"configViewerShowFull": "Показать полную конфигурацию",
|
||||
"configViewerShowComments": "Показать комментарии",
|
||||
"configViewerLoadConfig": "Загрузить конфигурацию",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Прокси-сервер"
|
||||
},
|
||||
"newUserHintSubFolder": "например, '/subfolder' - пустое место по умолчанию",
|
||||
"permissionsHelp": "Разрешения пользователя контролируют, какие действия он может выполнять. Доступ администратора дает полный контроль над обновлением и изменением настроек любого пользователя, а также многих системных настроек."
|
||||
"permissionsHelp": "Разрешения пользователя контролируют, какие действия он может выполнять. Доступ администратора дает полный контроль над обновлением и изменением настроек любого пользователя, а также многих системных настроек.",
|
||||
"configViewer": "Программа просмотра конфигурации"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Настройки сайта",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "Линии{suffix}",
|
||||
"lastUpdate": "Последнее обновление{suffix}",
|
||||
"latency": "Латентность{suffix}",
|
||||
"all": "Все{suffix}"
|
||||
"all": "Все{suffix}",
|
||||
"expiration": "Срок действия{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Правила доступа",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Vytvoriť účet",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Možnosti používateľa správcu",
|
||||
"searchOptions": "Možnosti vyhľadávania",
|
||||
"systemAdmin": "Systém a administrátor",
|
||||
"configViewer": "Prehliadač konfigurácie",
|
||||
"configViewerShowFull": "Zobraziť úplnú konfiguráciu",
|
||||
"configViewerShowComments": "Zobraziť komentáre",
|
||||
"configViewerLoadConfig": "Konfigurácia načítania",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "napr. '/subfolder' - prázdne pre predvolené nastavenie",
|
||||
"permissionsHelp": "Oprávnenia používateľa riadia prístup k typom akcií, ktoré môže používateľ vykonávať. Prístup administrátora poskytuje úplnú kontrolu nad aktualizáciou a zmenou nastavení ktoréhokoľvek používateľa, ako aj nad mnohými systémovými nastaveniami."
|
||||
"permissionsHelp": "Oprávnenia používateľa riadia prístup k typom akcií, ktoré môže používateľ vykonávať. Prístup administrátora poskytuje úplnú kontrolu nad aktualizáciou a zmenou nastavení ktoréhokoľvek používateľa, ako aj nad mnohými systémovými nastaveniami.",
|
||||
"configViewer": "Prehliadač konfigurácie"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Nastavenia stránky",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "Riadky{suffix}",
|
||||
"lastUpdate": "Posledná aktualizácia{suffix}",
|
||||
"latency": "Latencia{suffix}",
|
||||
"all": "Všetky{suffix}"
|
||||
"all": "Všetky{suffix}",
|
||||
"expiration": "Uplynutie platnosti{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Pravidlá prístupu",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Skapa ett konto",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Alternativ för administratörsanvändare",
|
||||
"searchOptions": "Sökalternativ",
|
||||
"systemAdmin": "System & Administration",
|
||||
"configViewer": "Konfig-visare",
|
||||
"configViewerShowFull": "Visa fullständig konfiguration",
|
||||
"configViewerShowComments": "Visa kommentarer",
|
||||
"configViewerLoadConfig": "Ladda konfig",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "t.ex. \"/undermapp\" - tom för standard",
|
||||
"permissionsHelp": "Användarbehörigheter styr vilka typer av åtgärder som användaren kan utföra. Administratörsbehörighet ger full kontroll över uppdatering och ändring av alla användarinställningar samt många systeminställningar."
|
||||
"permissionsHelp": "Användarbehörigheter styr vilka typer av åtgärder som användaren kan utföra. Administratörsbehörighet ger full kontroll över uppdatering och ändring av alla användarinställningar samt många systeminställningar.",
|
||||
"configViewer": "Konfig-visare"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "System inställningar",
|
||||
@@ -705,7 +706,8 @@
|
||||
"lines": "Linjer{suffix}",
|
||||
"lastUpdate": "Senaste uppdatering{suffix}",
|
||||
"latency": "Fördröjning{suffix}",
|
||||
"all": "Alla{suffix}"
|
||||
"all": "Alla{suffix}",
|
||||
"expiration": "Förfallodag{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Regler för åtkomst",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Bir hesap oluşturun",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Yönetici kullanıcı seçenekleri",
|
||||
"searchOptions": "Arama seçenekleri",
|
||||
"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",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Proxy"
|
||||
},
|
||||
"newUserHintSubFolder": "örneğin '/subfolder' - varsayılan için boş",
|
||||
"permissionsHelp": "Kullanıcı izinleri, kullanıcının ne tür eylemler gerçekleştirebileceğine erişimi kontrol eder. Yönetici erişimi, herhangi bir kullanıcının ayarlarının yanı sıra birçok sistem ayarını güncellemek ve değiştirmek için tam kontrol sağlar."
|
||||
"permissionsHelp": "Kullanıcı izinleri, kullanıcının ne tür eylemler gerçekleştirebileceğine erişimi kontrol eder. Yönetici erişimi, herhangi bir kullanıcının ayarlarının yanı sıra birçok sistem ayarını güncellemek ve değiştirmek için tam kontrol sağlar.",
|
||||
"configViewer": "Yapılandırma Görüntüleyici"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Site ayarları!",
|
||||
@@ -625,7 +626,8 @@
|
||||
"lines": "Çizgiler{suffix}",
|
||||
"lastUpdate": "Son Güncelleme{suffix}",
|
||||
"latency": "Gecikme{suffix}",
|
||||
"all": "Tümü{suffix}"
|
||||
"all": "Tümü{suffix}",
|
||||
"expiration": "Son kullanma tarihi{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Erişim kuralları",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "Створити обліковий запис",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "Параметри користувача адміністрування",
|
||||
"searchOptions": "Параметри пошуку",
|
||||
"systemAdmin": "Система та адміністрування",
|
||||
"configViewer": "Переглядач конфігурацій",
|
||||
"configViewerShowFull": "Показати повну конфігурацію",
|
||||
"configViewerShowComments": "Показати коментарі",
|
||||
"configViewerLoadConfig": "Завантажити конфігурацію",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "Довірена особа"
|
||||
},
|
||||
"newUserHintSubFolder": "наприклад, '/subfolder' - порожній за замовчуванням",
|
||||
"permissionsHelp": "Дозволи користувача контролюють доступ до того, які дії може виконувати користувач. Доступ адміністратора дає повний контроль над оновленням і зміною будь-яких налаштувань користувача, а також багатьох системних налаштувань."
|
||||
"permissionsHelp": "Дозволи користувача контролюють доступ до того, які дії може виконувати користувач. Доступ адміністратора дає повний контроль над оновленням і зміною будь-яких налаштувань користувача, а також багатьох системних налаштувань.",
|
||||
"configViewer": "Переглядач конфігурацій"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "Налаштування сайту",
|
||||
@@ -705,7 +706,8 @@
|
||||
"lines": "Лінії{suffix}",
|
||||
"lastUpdate": "Останнє оновлення{suffix}",
|
||||
"latency": "Затримка{suffix}",
|
||||
"all": "Усе.{suffix}"
|
||||
"all": "Усе.{suffix}",
|
||||
"expiration": "Закінчення терміну дії{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "Правила доступу",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "创建用户",
|
||||
@@ -320,7 +321,6 @@
|
||||
"adminOptions": "管理员用户选项",
|
||||
"searchOptions": "搜索选项",
|
||||
"systemAdmin": "系统与管理",
|
||||
"configViewer": "配置查看器",
|
||||
"configViewerShowFull": "显示完整配置",
|
||||
"configViewerShowComments": "显示评论",
|
||||
"configViewerLoadConfig": "加载配置",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "代理"
|
||||
},
|
||||
"newUserHintSubFolder": "例如\"/子文件夹\"--默认为空",
|
||||
"permissionsHelp": "用户权限控制用户可以执行哪些类型的操作。管理员权限可完全控制更新和更改任何用户的设置以及许多系统设置。"
|
||||
"permissionsHelp": "用户权限控制用户可以执行哪些类型的操作。管理员权限可完全控制更新和更改任何用户的设置以及许多系统设置。",
|
||||
"configViewer": "配置查看器"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "网站设置",
|
||||
@@ -705,7 +706,8 @@
|
||||
"lines": "线路{suffix}",
|
||||
"lastUpdate": "最新更新{suffix}",
|
||||
"latency": "延迟{suffix}",
|
||||
"all": "全部{suffix}"
|
||||
"all": "全部{suffix}",
|
||||
"expiration": "到期{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "访问规则",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"ua": "Українська",
|
||||
"zhCN": "中文 (简体)",
|
||||
"zhTW": "中文 (繁體)",
|
||||
"cz": "Čeština"
|
||||
"cz": "Čeština",
|
||||
"nl": "Dutch"
|
||||
},
|
||||
"login": {
|
||||
"createAnAccount": "新建賬戶",
|
||||
@@ -320,7 +321,6 @@
|
||||
"shareDeleted": "分享已刪除!",
|
||||
"adminOptions": "管理員使用者選項",
|
||||
"systemAdmin": "系統與管理",
|
||||
"configViewer": "組態檢視器",
|
||||
"configViewerShowFull": "顯示完整設定",
|
||||
"configViewerShowComments": "顯示評論",
|
||||
"configViewerLoadConfig": "載入組態",
|
||||
@@ -332,7 +332,8 @@
|
||||
"proxy": "代理"
|
||||
},
|
||||
"newUserHintSubFolder": "例如 '/subfolder' - 預設為空白",
|
||||
"permissionsHelp": "使用者權限可控制使用者可執行的動作類型。管理員存取權可完全控制更新和變更任何使用者的設定以及許多系統設定。"
|
||||
"permissionsHelp": "使用者權限可控制使用者可執行的動作類型。管理員存取權可完全控制更新和變更任何使用者的設定以及許多系統設定。",
|
||||
"configViewer": "組態檢視器"
|
||||
},
|
||||
"sidebar": {
|
||||
"siteSettings": "網站設定",
|
||||
@@ -705,7 +706,8 @@
|
||||
"lines": "線路{suffix}",
|
||||
"lastUpdate": "最後更新{suffix}",
|
||||
"latency": "延遲{suffix}",
|
||||
"all": "全部{suffix}"
|
||||
"all": "全部{suffix}",
|
||||
"expiration": "到期日{suffix}"
|
||||
},
|
||||
"access": {
|
||||
"rules": "存取規則",
|
||||
|
||||
@@ -270,8 +270,9 @@ export const getters = {
|
||||
if (urlPath == "/" || urlPath == "") {
|
||||
return "";
|
||||
}
|
||||
// remove hash from path
|
||||
urlPath = urlPath.split('/').slice(2).join('/')
|
||||
// remove hash from path and decode each part
|
||||
let parts = urlPath.split('/').slice(2);
|
||||
urlPath = parts.map(part => decodeURIComponent(part)).join('/')
|
||||
if (subPath != "") {
|
||||
urlPath = url.joinPath(urlPath, removeLeadingSlash(subPath))
|
||||
}
|
||||
|
||||
@@ -79,12 +79,12 @@ export function removePrefix(path, prefix = "") {
|
||||
|
||||
// get path with parameters
|
||||
// Supports array values for repeated parameters (e.g., file=a&file=b&file=c)
|
||||
export function getApiPath(path, params = {}, encode = false) {
|
||||
export function getApiPath(path, params = {}, encode = true) {
|
||||
if (path.startsWith("/")) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
path = `${globalVars.baseURL}${path}`;
|
||||
|
||||
|
||||
const paramKeys = Object.keys(params);
|
||||
if (paramKeys.length > 0) {
|
||||
if (encode) {
|
||||
@@ -92,7 +92,7 @@ export function getApiPath(path, params = {}, encode = false) {
|
||||
for (const key of paramKeys) {
|
||||
const value = params[key];
|
||||
if (value === undefined) continue;
|
||||
|
||||
|
||||
// Handle array values for repeated parameters
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => {
|
||||
@@ -110,7 +110,7 @@ export function getApiPath(path, params = {}, encode = false) {
|
||||
for (const key in params) {
|
||||
const value = params[key];
|
||||
if (value === undefined) continue;
|
||||
|
||||
|
||||
// Handle array values for repeated parameters
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => {
|
||||
@@ -125,14 +125,13 @@ export function getApiPath(path, params = {}, encode = false) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// get path with parameters
|
||||
// relative path so it can be used behind proxy
|
||||
export function getPublicApiPath(path, params = {}) {
|
||||
return getApiPath(`/public/api/${path}`, params);
|
||||
export function getPublicApiPath(path, params = {}, encode = true) {
|
||||
return getApiPath(`/public/api/${path}`, params, encode);
|
||||
}
|
||||
|
||||
export function removeTrailingSlash(str) {
|
||||
@@ -229,7 +228,7 @@ export function goToItem(source, path, previousHistoryItem, newTab = false) {
|
||||
router.push({ path: fullPath });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (previousHistoryItem === undefined) {
|
||||
// When undefined will not create browser history
|
||||
router.replace({ path: fullPath });
|
||||
|
||||
@@ -191,7 +191,9 @@ export default {
|
||||
// Parse share route to get subPath
|
||||
let urlPath = getters.routePath('public/share')
|
||||
let parts = urlPath.split("/");
|
||||
shareInfo.subPath = "/" + parts.slice(2).join("/");
|
||||
// Decode each part since URL paths are encoded
|
||||
let decodedParts = parts.slice(2).map(part => decodeURIComponent(part));
|
||||
shareInfo.subPath = "/" + decodedParts.join("/");
|
||||
|
||||
// Set shareInfo in state
|
||||
mutations.setShareInfo(shareInfo);
|
||||
|
||||
@@ -231,12 +231,14 @@ export default {
|
||||
}
|
||||
mutations.setDeletedItem(false);
|
||||
},
|
||||
req() {
|
||||
async req() {
|
||||
if (!getters.isLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isDeleted = false;
|
||||
// Reload subtitles when navigating to a new video
|
||||
this.subtitlesList = await this.subtitles();
|
||||
this.updatePreview();
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected({
|
||||
@@ -279,38 +281,45 @@ export default {
|
||||
return [];
|
||||
}
|
||||
let subs = [];
|
||||
for (const subtitleTrack of state.req.subtitles) {
|
||||
// All subtitle content is now pre-loaded when content=true
|
||||
// Simply use the content that's already available
|
||||
if (
|
||||
!subtitleTrack.content ||
|
||||
subtitleTrack.content.length === 0
|
||||
) {
|
||||
console.warn(
|
||||
"Subtitle track has no content:",
|
||||
subtitleTrack.name,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let vttContent = subtitleTrack.content;
|
||||
if (!subtitleTrack.content.startsWith("WEBVTT")) {
|
||||
const ext = getFileExtension(subtitleTrack.name);
|
||||
vttContent = convertToVTT(ext, subtitleTrack.content);
|
||||
}
|
||||
if (vttContent.startsWith("WEBVTT")) {
|
||||
// Create a virtual file (Blob) and get a URL for it
|
||||
const blob = new Blob([vttContent], { type: "text/vtt" });
|
||||
const vttURL = URL.createObjectURL(blob);
|
||||
subs.push({
|
||||
name: subtitleTrack.name,
|
||||
src: vttURL,
|
||||
language: subtitleTrack.language || ''
|
||||
});
|
||||
} else {
|
||||
console.warn(
|
||||
"Skipping subtitle track because it has no WEBVTT header:",
|
||||
// Fetch subtitle content for each track using the media API
|
||||
for (let index = 0; index < state.req.subtitles.length; index++) {
|
||||
const subtitleTrack = state.req.subtitles[index];
|
||||
try {
|
||||
// Fetch subtitle content from API using name and embedded
|
||||
const content = await filesApi.getSubtitleContent(
|
||||
state.req.source,
|
||||
state.req.path,
|
||||
subtitleTrack.name,
|
||||
subtitleTrack.embedded
|
||||
);
|
||||
if (!content || content.length === 0) {
|
||||
console.warn("Subtitle track has no content:", subtitleTrack.name);
|
||||
continue;
|
||||
}
|
||||
// Convert to VTT if needed
|
||||
let vttContent = content;
|
||||
if (!content.startsWith("WEBVTT")) {
|
||||
const ext = getFileExtension(subtitleTrack.name);
|
||||
vttContent = convertToVTT(ext, content);
|
||||
}
|
||||
if (vttContent.startsWith("WEBVTT")) {
|
||||
// Create a virtual file (Blob) and get a URL for it
|
||||
const blob = new Blob([vttContent], { type: "text/vtt" });
|
||||
const vttURL = URL.createObjectURL(blob);
|
||||
|
||||
subs.push({
|
||||
name: subtitleTrack.name,
|
||||
src: vttURL,
|
||||
language: subtitleTrack.language
|
||||
});
|
||||
} else {
|
||||
console.warn(
|
||||
"Skipping subtitle track - no WEBVTT header after conversion:",
|
||||
subtitleTrack.name
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load subtitle:", subtitleTrack.name, err);
|
||||
}
|
||||
}
|
||||
return subs;
|
||||
@@ -413,7 +422,7 @@ export default {
|
||||
},
|
||||
showDeletePrompt() {
|
||||
const item = state.req;
|
||||
const previewUrl = item.hasPreview
|
||||
const previewUrl = item.hasPreview
|
||||
? filesApi.getPreviewURL(item.source, item.path, item.modified)
|
||||
: null;
|
||||
mutations.showHover({
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<div class="plyr-video-container" ref="plyrVideoContainer">
|
||||
<video :src="raw" :type="req.type" :autoplay="shouldAutoplay" @play="handlePlay" playsinline ref="videoElement">
|
||||
<track kind="captions" v-for="(sub, index) in subtitlesList" :key="index" :src="sub.src"
|
||||
:label="'Subtitle ' + sub.name" :default="false" />
|
||||
:label="sub.name" :srclang="sub.language" :default="index === 0" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@
|
||||
<video ref="defaultVideoPlayer" :src="raw"
|
||||
controls :autoplay="shouldAutoplay" @play="handlePlay" playsinline >
|
||||
<track kind="captions" v-for="(sub, index) in subtitlesList" :key="index" :src="sub.src"
|
||||
:label="'Subtitle ' + sub.name" :default="index === 0" />
|
||||
:label="sub.name" :srclang="sub.language" :default="index === 0" />
|
||||
</video>
|
||||
</div>
|
||||
|
||||
@@ -188,7 +188,7 @@ export default {
|
||||
"settings",
|
||||
"fullscreen",
|
||||
],
|
||||
settings: ["quality", "speed", "playback"],
|
||||
settings: ["captions", "quality", "speed", "playback"],
|
||||
speed: {
|
||||
selected: 1,
|
||||
options: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2],
|
||||
@@ -233,6 +233,16 @@ export default {
|
||||
this.setupPlaybackQueue(true);
|
||||
}
|
||||
},
|
||||
subtitlesList(newSubs, oldSubs) {
|
||||
// Initialize Plyr when subtitles are loaded (if we were waiting for them)
|
||||
if (newSubs && newSubs.length > 0 && (!oldSubs || oldSubs.length === 0)) {
|
||||
if (!this.useDefaultMediaPlayer && !this.player && this.previewType === 'video') {
|
||||
this.$nextTick(() => {
|
||||
this.initializePlyr();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
darkMode() {
|
||||
@@ -592,6 +602,17 @@ export default {
|
||||
this.setupDefaultPlayerEvents(this.mediaElement);
|
||||
return;
|
||||
}
|
||||
|
||||
// For videos with subtitle metadata, wait for subtitles to load before initializing Plyr
|
||||
// This prevents Plyr from trying to access tracks before they have valid blob URLs
|
||||
const hasSubtitleMetadata = this.req?.subtitles && this.req.subtitles.length > 0;
|
||||
const subtitlesNotLoaded = !this.subtitlesList || this.subtitlesList.length === 0;
|
||||
|
||||
if (this.previewType === 'video' && hasSubtitleMetadata && subtitlesNotLoaded) {
|
||||
// Wait for subtitles to be loaded (watcher will call initializePlyr)
|
||||
return;
|
||||
}
|
||||
|
||||
this.initializePlyr();
|
||||
},
|
||||
initializePlyr() {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<tr>
|
||||
<th>{{ $t("general.hash") }}</th>
|
||||
<th>{{ $t("general.path") }}</th>
|
||||
<th>{{ $t("time.unit") }}</th>
|
||||
<th>{{ $t("general.expiration") }}</th>
|
||||
<th>{{ $t("general.downloads") }}</th>
|
||||
<th>{{ $t("general.username") }}</th>
|
||||
<th></th>
|
||||
|
||||
Reference in new issue
Block a user