Compare commits
12
Commits
main
...
v0.8.5-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60615a7c4d | ||
|
|
7a329ba160 | ||
|
|
1cbe826346 | ||
|
|
8125211675 | ||
|
|
e36bca2eb2 | ||
|
|
f8dbb2da67 | ||
|
|
deab8dce4b | ||
|
|
b8a8ae3c5d | ||
|
|
b2c2a373dd | ||
|
|
ce177a4f59 | ||
|
|
2d2a3d7707 | ||
|
|
7baf2653e0 |
No files matched your search
@@ -2,6 +2,42 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
## v0.8.5-beta
|
||||
|
||||
**New Features**:
|
||||
- backend media metadata processing
|
||||
- significantly improved performance
|
||||
- album artwork shows in listing view as preview thumbnail Album art thumbnail: display embedded covers of mp3/flac files #925
|
||||
- folders can show album art if enabled with indexing `server.sources.config.indexAlbumArt:true`
|
||||
- removed need for frontend dependancies.
|
||||
- optional thumbnail support for folders -- uses first child item with thumbnail as cover. Useful in combination with album metadata support to show album covers for folders with music.
|
||||
- Remember folder view type (and sorting) for each folder #966
|
||||
- heic is supported in viewer (via ffmpeg conversion) when on non-safari browser. This can be enabled via `integrations.ffmpeg.convert.impagePreview.heic: true` #1191
|
||||
- OnlyOffice: Add option to open documents in viewer mode (`integrations.office.viewOnly`) #1193
|
||||
- Exclude folders and file names 'starting with' or wildcard option #1054
|
||||
- added better info for failed uploads #1050
|
||||
- upload prompt "clear completed" can be changed to "clear all" in uploads & downloads settings to clear error and conflict states as well. #1128
|
||||
- share 'default view mode' option #1212
|
||||
- Universal Next/Previous buttons
|
||||
- available for all listing items (including office/markdown etc)
|
||||
- remembers sort order from parent directory
|
||||
- new "Quick Jump" prompt if you drag the next/previous icons. This shows other files available to "jump" directly to quickly.
|
||||
|
||||
**Notes**:
|
||||
- Added more tests to ensure new features work in future.
|
||||
- caching for preview images is md5 based. Moved, renamed, or duplicate images don't get re-generated. Same album artwork shares cache.
|
||||
- Hide @eaDir folder by default #1212
|
||||
- defaults to hide "@eadir" folders (common for synology) #1212
|
||||
- After move/copy, ability to move to the destination folder #999
|
||||
|
||||
**BugFixes**:
|
||||
- Access Management: issue with access settings #1195
|
||||
- fix shutdown panic related to sse connection
|
||||
- Custom theming not working in 0.8.4 beta #1204
|
||||
- Config Viewer not working #1189
|
||||
- "Path not found" when trying to share a file or folder inside a sub-directory #1139
|
||||
- files containing "+" in share issue #1089
|
||||
|
||||
## v0.8.4-beta
|
||||
|
||||
**New Features**:
|
||||
|
||||
+3
-3
@@ -9,14 +9,14 @@ Thank you for your interest in contributing to FileBrowser Quantum! This guide w
|
||||
- **Docker** (optional, for containerized development)
|
||||
- **Git**
|
||||
|
||||
### Required Tools
|
||||
### Optional Tools
|
||||
|
||||
- **ffmpeg**: Required for video thumbnail generation
|
||||
- **ffmpeg**: For media features (subtitles, thumbnails, heic)
|
||||
- Ubuntu/Debian: `sudo apt-get install ffmpeg`
|
||||
- macOS: `brew install ffmpeg`
|
||||
- Windows: Download from [ffmpeg.org](https://ffmpeg.org/download.html)
|
||||
|
||||
- **mupdf-tools**: Required for PDF preview generation
|
||||
- **mupdf-tools**: For PDF preview generation
|
||||
- Ubuntu/Debian: `sudo apt-get install mupdf-tools`
|
||||
- macOS: `brew install mupdf-tools`
|
||||
- Windows: Download from [mupdf.com](https://mupdf.com/downloads/)
|
||||
|
||||
@@ -10,6 +10,7 @@ COPY [ "./frontend/tests/", "./tests" ]
|
||||
|
||||
WORKDIR /app/backend/
|
||||
COPY [ "./backend/filebrowser*", "./"]
|
||||
COPY [ "./backend/reduce-rounded-corners.css", "./no-rounded.css" ]
|
||||
RUN mkdir -p ./http/dist
|
||||
COPY [ "./backend/http/embed/", "./http/dist/"]
|
||||
|
||||
|
||||
@@ -15,6 +15,11 @@ server:
|
||||
frontend:
|
||||
name: "Graham's Filebrowser"
|
||||
disableDefaultLinks: true
|
||||
styling:
|
||||
customThemes:
|
||||
no-rounded:
|
||||
description: "No rounded corners" # The description of the theme to display in the UI.
|
||||
css: "no-rounded.css" # The css file path and filename to use for the theme.
|
||||
externalLinks:
|
||||
- text: "A playwright test"
|
||||
url: "https://playwright.dev/"
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package files
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -18,6 +13,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dhowden/tag"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
@@ -34,7 +30,6 @@ func FileInfoFaster(opts iteminfo.FileOptions) (*iteminfo.ExtendedFileInfo, erro
|
||||
if index == nil {
|
||||
return response, fmt.Errorf("could not get index: %v ", opts.Source)
|
||||
}
|
||||
|
||||
realPath, isDir, err := index.GetRealPath(opts.Path)
|
||||
if err != nil {
|
||||
return response, fmt.Errorf("could not get real path for requested path: %v", opts.Path)
|
||||
@@ -42,26 +37,39 @@ func FileInfoFaster(opts iteminfo.FileOptions) (*iteminfo.ExtendedFileInfo, erro
|
||||
opts.IsDir = isDir
|
||||
var info *iteminfo.FileInfo
|
||||
var exists bool
|
||||
err = index.RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
if err == errors.ErrNotIndexed && index.Config.DisableIndexing {
|
||||
info, err = index.GetFsDirInfo(opts.Path)
|
||||
if err != nil {
|
||||
return response, err
|
||||
var useFsDirInfo bool
|
||||
if isDir {
|
||||
err = index.RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
if err == errors.ErrNotIndexed && index.Config.DisableIndexing {
|
||||
useFsDirInfo = true
|
||||
} else if err == errors.ErrNotIndexed {
|
||||
return response, fmt.Errorf("could not refresh file info: %v", err)
|
||||
}
|
||||
} else if err == errors.ErrNotIndexed {
|
||||
return response, fmt.Errorf("could not refresh file info: %v", err)
|
||||
}
|
||||
}
|
||||
if useFsDirInfo {
|
||||
info, err = index.GetFsDirInfo(opts.Path)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
} else {
|
||||
info, exists = index.GetReducedMetadata(opts.Path, opts.IsDir)
|
||||
if !exists {
|
||||
return response, fmt.Errorf("could not get metadata for path: %v", opts.Path)
|
||||
err = index.RefreshFileInfo(opts)
|
||||
if err != nil {
|
||||
return response, fmt.Errorf("could not refresh file info: %v", err)
|
||||
}
|
||||
info, exists = index.GetReducedMetadata(opts.Path, opts.IsDir)
|
||||
if !exists {
|
||||
return response, fmt.Errorf("could not get metadata for path: %v", opts.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.FileInfo = *info
|
||||
response.RealPath = realPath
|
||||
response.Source = opts.Source
|
||||
|
||||
if opts.Access != nil && !opts.Access.Permitted(index.Path, opts.Path, opts.Username) {
|
||||
// check if any subpath is permitted
|
||||
// keep track of permitted paths and only show them at the end
|
||||
@@ -100,6 +108,12 @@ func FileInfoFaster(opts iteminfo.FileOptions) (*iteminfo.ExtendedFileInfo, erro
|
||||
|
||||
func processContent(info *iteminfo.ExtendedFileInfo, idx *indexing.Index) {
|
||||
isVideo := strings.HasPrefix(info.Type, "video")
|
||||
isAudio := strings.HasPrefix(info.Type, "audio")
|
||||
isFolder := info.Type == "directory"
|
||||
if isFolder {
|
||||
return
|
||||
}
|
||||
|
||||
if isVideo {
|
||||
parentInfo, exists := idx.GetReducedMetadata(filepath.Dir(info.Path), true)
|
||||
if exists {
|
||||
@@ -111,6 +125,18 @@ func processContent(info *iteminfo.ExtendedFileInfo, idx *indexing.Index) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if isAudio {
|
||||
err := extractAudioMetadata(info)
|
||||
if err != nil {
|
||||
logger.Debugf("failed to extract audio metadata for file: "+info.RealPath, info.Name, err)
|
||||
} else {
|
||||
info.HasPreview = info.AudioMeta.AlbumArt != ""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Process text content for non-video, non-audio files
|
||||
if info.Size < 20*1024*1024 { // 20 megabytes in bytes
|
||||
content, err := getContent(info.RealPath)
|
||||
if err != nil {
|
||||
@@ -134,34 +160,42 @@ func generateOfficeId(realPath string) string {
|
||||
return key
|
||||
}
|
||||
|
||||
// Checksum checksums a given File for a given User, using a specific
|
||||
// algorithm. The checksums data is saved on File object.
|
||||
func GetChecksum(fullPath, algo string) (map[string]string, error) {
|
||||
subs := map[string]string{}
|
||||
reader, err := os.Open(fullPath)
|
||||
// extractAudioMetadata extracts metadata from an audio file using dhowden/tag
|
||||
func extractAudioMetadata(item *iteminfo.ExtendedFileInfo) error {
|
||||
file, err := os.Open(item.RealPath)
|
||||
if err != nil {
|
||||
return subs, err
|
||||
return err
|
||||
}
|
||||
defer reader.Close()
|
||||
defer file.Close()
|
||||
|
||||
hashFuncs := map[string]hash.Hash{
|
||||
"md5": md5.New(),
|
||||
"sha1": sha1.New(),
|
||||
"sha256": sha256.New(),
|
||||
"sha512": sha512.New(),
|
||||
}
|
||||
|
||||
h, ok := hashFuncs[algo]
|
||||
if !ok {
|
||||
return subs, errors.ErrInvalidOption
|
||||
}
|
||||
|
||||
_, err = io.Copy(h, reader)
|
||||
m, err := tag.ReadFrom(file)
|
||||
if err != nil {
|
||||
return subs, err
|
||||
return err
|
||||
}
|
||||
subs[algo] = hex.EncodeToString(h.Sum(nil))
|
||||
return subs, nil
|
||||
|
||||
item.AudioMeta = &iteminfo.AudioMetadata{
|
||||
Title: m.Title(),
|
||||
Artist: m.Artist(),
|
||||
Album: m.Album(),
|
||||
Year: m.Year(),
|
||||
Genre: m.Genre(),
|
||||
}
|
||||
|
||||
// Extract track number
|
||||
track, _ := m.Track()
|
||||
item.AudioMeta.Track = track
|
||||
|
||||
// Extract album art and encode as base64
|
||||
if picture := m.Picture(); picture != nil && picture.Data != nil {
|
||||
// Limit album art size to prevent excessive response sizes (max 1MB)
|
||||
if len(picture.Data) <= 1024*1024 {
|
||||
item.AudioMeta.AlbumArt = base64.StdEncoding.EncodeToString(picture.Data)
|
||||
} else {
|
||||
logger.Debugf("Skipping album art for %s: too large (%d bytes)", item.RealPath, len(picture.Data))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteFiles(source, absPath string, absDirPath string) error {
|
||||
|
||||
+7
-1
@@ -349,11 +349,17 @@ func createConfig(configpath string) {
|
||||
}
|
||||
|
||||
func generateYaml() {
|
||||
if os.Getenv("FILEBROWSER_GENERATE_CONFIG") != "" {
|
||||
generateConfig := os.Getenv("FILEBROWSER_GENERATE_CONFIG") == "true"
|
||||
devMode := os.Getenv("FILEBROWSER_DEVMODE") == "true"
|
||||
if generateConfig || devMode {
|
||||
logger.Info("Generating config.yaml")
|
||||
settings.GenerateYaml()
|
||||
}
|
||||
|
||||
if generateConfig {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func SplitByMultiple(str string) []string {
|
||||
|
||||
@@ -77,12 +77,22 @@ func StartFilebrowser() {
|
||||
if !dbExists {
|
||||
database = fmt.Sprintf("Creating new database : %v", settings.Config.Server.Database)
|
||||
}
|
||||
|
||||
// Dev mode enables development features like template hot-reloading
|
||||
devMode := os.Getenv("FILEBROWSER_DEVMODE") == "true"
|
||||
_, err := os.Stat("http/dist")
|
||||
// In dev mode, always use filesystem assets. Otherwise, check if http/dist exists
|
||||
if !devMode {
|
||||
settings.Config.Server.EmbeddedFs = os.IsNotExist(err)
|
||||
}
|
||||
|
||||
sourceList := []string{}
|
||||
for path, source := range settings.Config.Server.SourceMap {
|
||||
sourceList = append(sourceList, fmt.Sprintf("%v: %v", source.Name, path))
|
||||
}
|
||||
logger.Infof("Initializing FileBrowser Quantum (%v)", version.Version)
|
||||
logger.Infof("Using Config file : %v", configPath)
|
||||
logger.Infof("Using Embedded FS : %v", settings.Config.Server.EmbeddedFs)
|
||||
logger.Infof("Auth Methods : %v", settings.Config.Auth.AuthMethods)
|
||||
logger.Info(database)
|
||||
logger.Infof("Sources : %v", sourceList)
|
||||
|
||||
@@ -69,7 +69,9 @@ func setupFrontend(generate bool) {
|
||||
Config.Frontend.Styling.DarkBackground = FallbackColor(Config.Frontend.Styling.DarkBackground, "#141D24")
|
||||
Config.Frontend.Styling.CustomCSSRaw = readCustomCSS(Config.Frontend.Styling.CustomCSS)
|
||||
Config.Frontend.Styling.CustomThemeOptions = map[string]CustomTheme{}
|
||||
Config.Frontend.Styling.CustomThemes = map[string]CustomTheme{}
|
||||
if Config.Frontend.Styling.CustomThemes == nil {
|
||||
Config.Frontend.Styling.CustomThemes = map[string]CustomTheme{}
|
||||
}
|
||||
for name, theme := range Config.Frontend.Styling.CustomThemes {
|
||||
addCustomTheme(name, theme.Description, theme.CSS)
|
||||
}
|
||||
@@ -362,7 +364,7 @@ func setDefaults(generate bool) Settings {
|
||||
if database == "" {
|
||||
database = "database.db"
|
||||
}
|
||||
return Settings{
|
||||
s := Settings{
|
||||
Server: Server{
|
||||
Port: 80,
|
||||
NumImageProcessors: numCpus,
|
||||
@@ -410,6 +412,12 @@ func setDefaults(generate bool) Settings {
|
||||
},
|
||||
},
|
||||
}
|
||||
// set default image preview types
|
||||
s.Integrations.Media.Convert.ImagePreview = make(map[ImagePreviewType]bool)
|
||||
for _, t := range AllImagePreviewTypes {
|
||||
s.Integrations.Media.Convert.ImagePreview[t] = (t == HEICImagePreview) // default is heic
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func ConvertToBackendScopes(scopes []users.SourceScope) ([]users.SourceScope, error) {
|
||||
|
||||
@@ -116,6 +116,267 @@ func CollectCommentsAndSecrets(srcPath string) (CommentsMap, SecretFieldsMap, De
|
||||
return comments, secrets, deprecated, nil
|
||||
}
|
||||
|
||||
// PathToTypeField represents a mapping from YAML path to Go type and field info
|
||||
type PathToTypeField struct {
|
||||
TypeName string
|
||||
FieldName string
|
||||
}
|
||||
|
||||
// buildStructPathMap builds a dynamic mapping from YAML paths to Go struct types/fields using reflection
|
||||
func buildStructPathMap(config *Settings) map[string]PathToTypeField {
|
||||
pathMap := make(map[string]PathToTypeField)
|
||||
|
||||
// Start with the root Settings struct
|
||||
buildPathMapRecursive(reflect.TypeOf(config).Elem(), reflect.ValueOf(config).Elem(), "", "Settings", pathMap)
|
||||
|
||||
return pathMap
|
||||
}
|
||||
|
||||
// buildPathMapRecursive recursively builds the path mapping using reflection
|
||||
func buildPathMapRecursive(structType reflect.Type, structValue reflect.Value, currentPath string, typeName string, pathMap map[string]PathToTypeField) {
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
field := structType.Field(i)
|
||||
|
||||
// Skip unexported fields
|
||||
if field.PkgPath != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip fields marked to ignore
|
||||
if yamlTag := field.Tag.Get("yaml"); yamlTag == "-" {
|
||||
continue
|
||||
}
|
||||
if jsonTag := field.Tag.Get("json"); jsonTag == "-" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine the YAML field name from struct tags
|
||||
yamlFieldName := getYamlFieldName(field)
|
||||
|
||||
// Build the full path
|
||||
var fullPath string
|
||||
if currentPath == "" {
|
||||
fullPath = yamlFieldName
|
||||
} else {
|
||||
fullPath = currentPath + "." + yamlFieldName
|
||||
}
|
||||
|
||||
// Add this field to the path map
|
||||
pathMap[fullPath] = PathToTypeField{
|
||||
TypeName: typeName,
|
||||
FieldName: field.Name,
|
||||
}
|
||||
|
||||
// If this field is a struct, recurse into it
|
||||
fieldValue := structValue.Field(i)
|
||||
fieldType := field.Type
|
||||
|
||||
// Handle pointers
|
||||
if fieldType.Kind() == reflect.Ptr {
|
||||
if fieldValue.IsNil() {
|
||||
// Skip nil pointers
|
||||
continue
|
||||
}
|
||||
fieldValue = fieldValue.Elem()
|
||||
fieldType = fieldType.Elem()
|
||||
}
|
||||
|
||||
// Recurse into structs (but not slices of structs for now - those are handled differently)
|
||||
if fieldType.Kind() == reflect.Struct {
|
||||
// Skip certain types that shouldn't be recursed into
|
||||
if isSkippableStructType(fieldType) {
|
||||
continue
|
||||
}
|
||||
|
||||
buildPathMapRecursive(fieldType, fieldValue, fullPath, fieldType.Name(), pathMap)
|
||||
} else if fieldType.Kind() == reflect.Slice && fieldType.Elem().Kind() == reflect.Struct {
|
||||
// For slices of structs, we map to the element type
|
||||
elemType := fieldType.Elem()
|
||||
if !isSkippableStructType(elemType) {
|
||||
// Create a zero value of the element type to recurse into
|
||||
zeroValue := reflect.Zero(elemType)
|
||||
buildPathMapRecursive(elemType, zeroValue, fullPath, elemType.Name(), pathMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getYamlFieldName determines the YAML field name from struct tags
|
||||
func getYamlFieldName(field reflect.StructField) string {
|
||||
// Check yaml tag first
|
||||
if yamlTag := field.Tag.Get("yaml"); yamlTag != "" && yamlTag != "-" {
|
||||
return strings.Split(yamlTag, ",")[0]
|
||||
}
|
||||
|
||||
// Check json tag second
|
||||
if jsonTag := field.Tag.Get("json"); jsonTag != "" && jsonTag != "-" {
|
||||
return strings.Split(jsonTag, ",")[0]
|
||||
}
|
||||
|
||||
// Default to field name
|
||||
return field.Name
|
||||
}
|
||||
|
||||
// isSkippableStructType checks if a struct type should be skipped during recursion
|
||||
func isSkippableStructType(t reflect.Type) bool {
|
||||
// Skip types from other packages that we don't want to recurse into
|
||||
pkgPath := t.PkgPath()
|
||||
|
||||
// Skip external packages
|
||||
if pkgPath != "" && !strings.Contains(pkgPath, "filebrowser/backend") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// CollectCommentsFromEmbeddedYaml parses comments from embedded YAML content and returns CommentsMap, SecretFieldsMap, and DeprecatedFieldsMap
|
||||
func CollectCommentsFromEmbeddedYaml(yamlContent string) (CommentsMap, SecretFieldsMap, DeprecatedFieldsMap, error) {
|
||||
comments := make(CommentsMap)
|
||||
secrets := make(SecretFieldsMap)
|
||||
deprecated := make(DeprecatedFieldsMap)
|
||||
|
||||
// Build the dynamic path mapping from the actual Config struct
|
||||
pathMap := buildStructPathMap(&Config)
|
||||
|
||||
lines := strings.Split(yamlContent, "\n")
|
||||
|
||||
// Track the current path in the YAML structure
|
||||
var currentPath []string
|
||||
|
||||
for _, line := range lines {
|
||||
// Skip empty lines and lines that are just comments
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the key and comment in the line
|
||||
if !strings.Contains(line, ":") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Split on the first colon to get key part
|
||||
colonIndex := strings.Index(line, ":")
|
||||
keyPart := line[:colonIndex]
|
||||
restPart := line[colonIndex+1:]
|
||||
|
||||
// Calculate indentation
|
||||
indent := 0
|
||||
for i, char := range keyPart {
|
||||
if char == ' ' {
|
||||
indent++
|
||||
} else {
|
||||
keyPart = keyPart[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
depth := indent / 2
|
||||
|
||||
// Get the key name
|
||||
key := strings.TrimSpace(keyPart)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Adjust the current path based on depth
|
||||
if depth < len(currentPath) {
|
||||
currentPath = currentPath[:depth]
|
||||
}
|
||||
|
||||
// Add current key to path
|
||||
if depth < len(currentPath) {
|
||||
currentPath[depth] = key
|
||||
currentPath = currentPath[:depth+1]
|
||||
} else {
|
||||
currentPath = append(currentPath, key)
|
||||
}
|
||||
|
||||
// Look for comment in the rest of the line
|
||||
var comment string
|
||||
if strings.Contains(restPart, "#") {
|
||||
// Find the comment part (not inside quotes)
|
||||
inQuotes := false
|
||||
for i, char := range restPart {
|
||||
if char == '"' && (i == 0 || restPart[i-1] != '\\') {
|
||||
inQuotes = !inQuotes
|
||||
} else if char == '#' && !inQuotes {
|
||||
comment = strings.TrimSpace(restPart[i+1:])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store comment if it exists
|
||||
if comment != "" {
|
||||
// Build the full path string
|
||||
fullPath := strings.Join(currentPath, ".")
|
||||
|
||||
// Look up the type and field name from our dynamic mapping
|
||||
if pathInfo, exists := pathMap[fullPath]; exists {
|
||||
typeName := pathInfo.TypeName
|
||||
fieldName := pathInfo.FieldName
|
||||
|
||||
// Initialize maps if they don't exist
|
||||
if comments[typeName] == nil {
|
||||
comments[typeName] = make(map[string]string)
|
||||
}
|
||||
if secrets[typeName] == nil {
|
||||
secrets[typeName] = make(map[string]bool)
|
||||
}
|
||||
if deprecated[typeName] == nil {
|
||||
deprecated[typeName] = make(map[string]bool)
|
||||
}
|
||||
|
||||
comments[typeName][fieldName] = comment
|
||||
|
||||
// Check for secret and deprecated markers
|
||||
commentLower := strings.ToLower(comment)
|
||||
if strings.Contains(commentLower, "secret:") {
|
||||
secrets[typeName][fieldName] = true
|
||||
}
|
||||
if strings.Contains(commentLower, "deprecated:") {
|
||||
deprecated[typeName][fieldName] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return comments, secrets, deprecated, nil
|
||||
}
|
||||
|
||||
// parseDefaultsFromEmbeddedYaml creates a default config by parsing the embedded YAML template
|
||||
func parseDefaultsFromEmbeddedYaml(embeddedYaml string) (*Settings, error) {
|
||||
// Start with baseline defaults from setDefaults to ensure all fields are initialized
|
||||
defaultConfig := setDefaults(true)
|
||||
defaultConfig.Server.Sources = []Source{{Path: "."}}
|
||||
|
||||
// Parse the embedded YAML to overlay the documented defaults
|
||||
if err := yaml.Unmarshal([]byte(embeddedYaml), &defaultConfig); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal embedded YAML into config: %v", err)
|
||||
}
|
||||
|
||||
return &defaultConfig, nil
|
||||
}
|
||||
|
||||
// readEmbeddedYaml attempts to read the embedded config.generated.yaml file
|
||||
func readEmbeddedYaml() (string, error) {
|
||||
// Try to read from the relative path that should work when the binary includes embedded files
|
||||
embeddedContent, err := os.ReadFile("http/dist/config.generated.yaml")
|
||||
if err != nil {
|
||||
// Try alternative path
|
||||
embeddedContent, err = os.ReadFile("backend/http/dist/config.generated.yaml")
|
||||
if err != nil {
|
||||
// Try current directory path
|
||||
embeddedContent, err = os.ReadFile("dist/config.generated.yaml")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read embedded YAML from any path: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return string(embeddedContent), nil
|
||||
}
|
||||
|
||||
// BuildNode constructs a yaml.Node for any Go value, injecting comments on struct fields.
|
||||
func BuildNode(v reflect.Value, comm CommentsMap) (*yaml.Node, error) {
|
||||
return buildNodeWithDefaults(v, comm, reflect.Value{}, SecretFieldsMap{}, DeprecatedFieldsMap{})
|
||||
@@ -255,7 +516,10 @@ func buildNodeWithDefaults(v reflect.Value, comm CommentsMap, defaults reflect.V
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seq.Content = append(seq.Content, n)
|
||||
// Only add placeholder if it's not empty
|
||||
if n.Kind == yaml.MappingNode && len(n.Content) > 0 {
|
||||
seq.Content = append(seq.Content, n)
|
||||
}
|
||||
return seq, nil
|
||||
}
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
@@ -272,6 +536,10 @@ func buildNodeWithDefaults(v reflect.Value, comm CommentsMap, defaults reflect.V
|
||||
return seq, nil
|
||||
|
||||
case reflect.Map:
|
||||
if v.Len() == 0 {
|
||||
// Return an empty string node for empty maps, which renders as a blank value.
|
||||
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: ""}, nil
|
||||
}
|
||||
mapNode := &yaml.Node{Kind: yaml.MappingNode}
|
||||
|
||||
for _, key := range v.MapKeys() {
|
||||
@@ -361,6 +629,71 @@ func GenerateConfigYaml(config *Settings, showComments bool, showFull bool, filt
|
||||
return GenerateConfigYamlWithEmptyMaps(config, showFull)
|
||||
}
|
||||
|
||||
// GenerateConfigYamlWithEmbedded generates YAML from a given config using embedded YAML content as comment source
|
||||
func GenerateConfigYamlWithEmbedded(config *Settings, showComments bool, showFull bool, filterDeprecated bool, embeddedYaml string) (string, error) {
|
||||
var comm CommentsMap
|
||||
var secrets SecretFieldsMap
|
||||
var deprecated DeprecatedFieldsMap
|
||||
var err error
|
||||
|
||||
if showComments && embeddedYaml != "" {
|
||||
// Parse comments from the embedded YAML content
|
||||
comm, secrets, deprecated, err = CollectCommentsFromEmbeddedYaml(embeddedYaml)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error parsing embedded YAML comments: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Create empty maps
|
||||
comm = make(CommentsMap)
|
||||
secrets = make(SecretFieldsMap)
|
||||
deprecated = make(DeprecatedFieldsMap)
|
||||
}
|
||||
|
||||
// If not filtering deprecated fields, clear the deprecated map
|
||||
if !filterDeprecated {
|
||||
deprecated = make(DeprecatedFieldsMap)
|
||||
}
|
||||
|
||||
var node *yaml.Node
|
||||
|
||||
if showFull {
|
||||
// Show the full current config
|
||||
node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.Value{}, secrets, deprecated)
|
||||
} else {
|
||||
// Show only non-default values by comparing with defaults from embedded YAML
|
||||
defaultConfig, parseErr := parseDefaultsFromEmbeddedYaml(embeddedYaml)
|
||||
if parseErr != nil {
|
||||
return "", fmt.Errorf("failed to parse defaults from embedded YAML: %v", parseErr)
|
||||
}
|
||||
|
||||
node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.ValueOf(defaultConfig), secrets, deprecated)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create document
|
||||
doc := &yaml.Node{Kind: yaml.DocumentNode}
|
||||
doc.Content = []*yaml.Node{node}
|
||||
|
||||
// Encode to YAML
|
||||
var rawBuf bytes.Buffer
|
||||
enc := yaml.NewEncoder(&rawBuf)
|
||||
enc.SetIndent(2)
|
||||
if err := enc.Encode(doc); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Apply comment alignment if comments are enabled
|
||||
yamlOutput := rawBuf.String()
|
||||
if showComments {
|
||||
yamlOutput = AlignComments(yamlOutput)
|
||||
}
|
||||
|
||||
return yamlOutput, nil
|
||||
}
|
||||
|
||||
// GenerateConfigYamlWithEmptyMaps generates YAML without comment parsing when source files are unavailable
|
||||
func GenerateConfigYamlWithEmptyMaps(config *Settings, showFull bool) (string, error) {
|
||||
// Create empty maps
|
||||
@@ -376,10 +709,20 @@ func GenerateConfigYamlWithEmptyMaps(config *Settings, showFull bool) (string, e
|
||||
node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.Value{}, secrets, deprecated)
|
||||
} else {
|
||||
// Show only non-default values by comparing with defaults during node building
|
||||
defaultConfig := setDefaults(true)
|
||||
defaultConfig.Server.Sources = []Source{{Path: "."}}
|
||||
// Try to use embedded YAML defaults for consistency, fallback to setDefaults
|
||||
var defaultConfig *Settings
|
||||
embeddedYaml, readErr := readEmbeddedYaml()
|
||||
if readErr == nil {
|
||||
defaultConfig, err = parseDefaultsFromEmbeddedYaml(embeddedYaml)
|
||||
}
|
||||
if err != nil || readErr != nil {
|
||||
// Fallback to setDefaults
|
||||
defaultConfigValue := setDefaults(true)
|
||||
defaultConfigValue.Server.Sources = []Source{{Path: "."}}
|
||||
defaultConfig = &defaultConfigValue
|
||||
}
|
||||
|
||||
node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.ValueOf(&defaultConfig), secrets, deprecated)
|
||||
node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.ValueOf(defaultConfig), secrets, deprecated)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -435,12 +778,20 @@ func GenerateConfigYamlWithSource(config *Settings, showComments bool, showFull
|
||||
node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.Value{}, secrets, deprecated)
|
||||
} else {
|
||||
// Show only non-default values by comparing with defaults during node building
|
||||
// Create a clean default config (no file loading, just pure defaults)
|
||||
defaultConfig := setDefaults(true)
|
||||
// Apply same setup as a fresh instance would have
|
||||
defaultConfig.Server.Sources = []Source{{Path: "."}}
|
||||
// Try to use embedded YAML defaults for consistency, fallback to setDefaults
|
||||
var defaultConfig *Settings
|
||||
embeddedYaml, readErr := readEmbeddedYaml()
|
||||
if readErr == nil {
|
||||
defaultConfig, err = parseDefaultsFromEmbeddedYaml(embeddedYaml)
|
||||
}
|
||||
if err != nil || readErr != nil {
|
||||
// Fallback to setDefaults
|
||||
defaultConfigValue := setDefaults(true)
|
||||
defaultConfigValue.Server.Sources = []Source{{Path: "."}}
|
||||
defaultConfig = &defaultConfigValue
|
||||
}
|
||||
|
||||
node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.ValueOf(&defaultConfig), secrets, deprecated)
|
||||
node, err = buildNodeWithDefaults(reflect.ValueOf(config), comm, reflect.ValueOf(defaultConfig), secrets, deprecated)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -21,18 +21,18 @@ type Settings struct {
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
MinSearchLength int `json:"minSearchLength"` // minimum length of search query to begin searching (default: 3)
|
||||
DisableUpdateCheck bool `json:"disableUpdateCheck"` // disables backend update check service
|
||||
NumImageProcessors int `json:"numImageProcessors"` // number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
|
||||
Socket string `json:"socket"` // socket to listen on
|
||||
TLSKey string `json:"tlsKey"` // path to TLS key
|
||||
TLSCert string `json:"tlsCert"` // path to TLS cert
|
||||
DisablePreviews bool `json:"disablePreviews"` // disable all previews thumbnails, simple icons will be used
|
||||
DisableResize bool `json:"disablePreviewResize"` // disable resizing of previews for faster loading over slow connections
|
||||
DisableTypeDetectionByHeader bool `json:"disableTypeDetectionByHeader"` // disable type detection by header, useful if filesystem is slow.
|
||||
Port int `json:"port"` // port to listen on
|
||||
BaseURL string `json:"baseURL"` // base URL for the server, the subpath that the server is running on.
|
||||
Logging []LogConfig `json:"logging"`
|
||||
MinSearchLength int `json:"minSearchLength" yaml:"minSearchLength"` // minimum length of search query to begin searching (default: 3)
|
||||
DisableUpdateCheck bool `json:"disableUpdateCheck"` // disables backend update check service
|
||||
NumImageProcessors int `json:"numImageProcessors"` // number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
|
||||
Socket string `json:"socket"` // socket to listen on
|
||||
TLSKey string `json:"tlsKey"` // path to TLS key
|
||||
TLSCert string `json:"tlsCert"` // path to TLS cert
|
||||
DisablePreviews bool `json:"disablePreviews"` // disable all previews thumbnails, simple icons will be used
|
||||
DisableResize bool `json:"disablePreviewResize"` // disable resizing of previews for faster loading over slow connections
|
||||
DisableTypeDetectionByHeader bool `json:"disableTypeDetectionByHeader"` // disable type detection by header, useful if filesystem is slow.
|
||||
Port int `json:"port"` // port to listen on
|
||||
BaseURL string `json:"baseURL"` // base URL for the server, the subpath that the server is running on.
|
||||
Logging []LogConfig `json:"logging" yaml:"logging"`
|
||||
DebugMedia bool `json:"debugMedia"` // output ffmpeg stdout for media integration -- careful can produces lots of output!
|
||||
Database string `json:"database"` // path to the database file
|
||||
Sources []Source `json:"sources" validate:"required,dive"`
|
||||
@@ -44,6 +44,7 @@ type Server struct {
|
||||
SourceMap map[string]Source `json:"-" validate:"omitempty"` // uses realpath as key
|
||||
NameToSource map[string]Source `json:"-" validate:"omitempty"` // uses name as key
|
||||
MuPdfAvailable bool `json:"-"` // used internally if compiled with mupdf support
|
||||
EmbeddedFs bool `json:"-"` // used internally if compiled with embedded fs support
|
||||
}
|
||||
|
||||
type Integrations struct {
|
||||
@@ -57,19 +58,40 @@ type OnlyOffice struct {
|
||||
Url string `json:"url" validate:"required"` // The URL to the OnlyOffice Document Server, needs to be accessible to the user.
|
||||
InternalUrl string `json:"internalUrl"` // An optional internal address that the filebrowser server can use to communicate with the OnlyOffice Document Server, could be useful to bypass proxy.
|
||||
Secret string `json:"secret" validate:"required"` // secret: authentication key for OnlyOffice integration
|
||||
ViewOnly bool `json:"viewOnly"` // view only mode for OnlyOffice
|
||||
}
|
||||
|
||||
type Media struct {
|
||||
FfmpegPath string `json:"ffmpegPath"` // path to ffmpeg directory with ffmpeg and ffprobe (eg. /usr/local/bin)
|
||||
FfmpegPath string `json:"ffmpegPath"` // path to ffmpeg directory with ffmpeg and ffprobe (eg. /usr/local/bin)
|
||||
Convert FfmpegConvert `json:"convert"` // config for ffmpeg conversion settings
|
||||
}
|
||||
|
||||
type FfmpegConvert struct {
|
||||
ImagePreview map[ImagePreviewType]bool `json:"imagePreview"` // supported image preview formats. default is heic
|
||||
}
|
||||
|
||||
type ImagePreviewType string
|
||||
|
||||
const (
|
||||
HEICImagePreview ImagePreviewType = "heic"
|
||||
)
|
||||
|
||||
func (i ImagePreviewType) String() string {
|
||||
return string(i)
|
||||
}
|
||||
|
||||
// AllImagePreviewTypes contains all supported image preview types.
|
||||
var AllImagePreviewTypes = []ImagePreviewType{
|
||||
HEICImagePreview,
|
||||
}
|
||||
|
||||
type LogConfig struct {
|
||||
Levels string `json:"levels"` // separated list of log levels to enable. (eg. "info|warning|error|debug")
|
||||
ApiLevels string `json:"apiLevels"` // separated list of log levels to enable for the API. (eg. "info|warning|error")
|
||||
Output string `json:"output"` // output location. (eg. "stdout" or "path/to/file.log")
|
||||
NoColors bool `json:"noColors"` // disable colors in the output
|
||||
Json bool `json:"json"` // output in json format, currently not supported
|
||||
Utc bool `json:"utc"` // use UTC time in the output instead of local time
|
||||
Levels string `json:"levels" yaml:"levels"` // separated list of log levels to enable. (eg. "info|warning|error|debug")
|
||||
ApiLevels string `json:"apiLevels" yaml:"apiLevels"` // separated list of log levels to enable for the API. (eg. "info|warning|error")
|
||||
Output string `json:"output" yaml:"output"` // output location. (eg. "stdout" or "path/to/file.log")
|
||||
NoColors bool `json:"noColors" yaml:"noColors"` // disable colors in the output
|
||||
Json bool `json:"json" yaml:"json"` // output in json format, currently not supported
|
||||
Utc bool `json:"utc" yaml:"utc"` // use UTC time in the output instead of local time
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
@@ -91,6 +113,7 @@ type SourceConfig struct {
|
||||
DefaultUserScope string `json:"defaultUserScope"` // default "/" should match folders under path
|
||||
DefaultEnabled bool `json:"defaultEnabled"` // should be added as a default source for new users?
|
||||
CreateUserDir bool `json:"createUserDir"` // create a user directory for each user
|
||||
IndexAlbumArt bool `json:"indexAlbumArt"` // check for album art in audio files and remember it for folder previews. Can slow down indexing.
|
||||
}
|
||||
|
||||
type IncludeIndexFilter struct {
|
||||
@@ -99,14 +122,16 @@ type IncludeIndexFilter struct {
|
||||
}
|
||||
|
||||
type ExcludeIndexFilter struct {
|
||||
Hidden bool `json:"hidden"` // exclude hidden files and folders.
|
||||
ZeroSizeFolders bool `json:"ignoreZeroSizeFolders"` // ignore folders with 0 size
|
||||
FilePaths []string `json:"filePaths"` // list of filepaths Eg. "folder1" or "file1.txt" or "folder1/file1.txt" (without source path prefix)
|
||||
FolderPaths []string `json:"folderPaths"` // (filepath) list of folder names to include/exclude. Eg. "folder1" or "folder1/subfolder" (do not include source path, just the subpaths from the source path)
|
||||
FileNames []string `json:"fileNames"` // (global) list of file names to include/exclude. Eg. "a.jpg"
|
||||
FolderNames []string `json:"folderNames"` // (global) list of folder names to include/exclude. Eg. "@eadir" or ".thumbnails"
|
||||
FileEndsWith []string `json:"fileEndsWith"` // (global) exclude files that end with these suffixes. Eg. ".jpg" or ".txt"
|
||||
FolderEndsWith []string `json:"folderEndsWith"` // (global) exclude folders that end with these suffixes. Eg. ".thumbnails" or ".git"
|
||||
Hidden bool `json:"hidden"` // exclude hidden files and folders.
|
||||
ZeroSizeFolders bool `json:"ignoreZeroSizeFolders"` // ignore folders with 0 size
|
||||
FilePaths []string `json:"filePaths"` // list of filepaths Eg. "folder1" or "file1.txt" or "folder1/file1.txt" (without source path prefix)
|
||||
FolderPaths []string `json:"folderPaths"` // (filepath) list of folder names to include/exclude. Eg. "folder1" or "folder1/subfolder" (do not include source path, just the subpaths from the source path)
|
||||
FileNames []string `json:"fileNames"` // (global) list of file names to include/exclude. Eg. "a.jpg"
|
||||
FolderNames []string `json:"folderNames"` // (global) list of folder names to include/exclude. Eg. "@eadir" or ".thumbnails"
|
||||
FileEndsWith []string `json:"fileEndsWith"` // (global) exclude files that end with these suffixes. Eg. ".jpg" or ".txt"
|
||||
FolderEndsWith []string `json:"folderEndsWith"` // (global) exclude folders that end with these suffixes. Eg. ".thumbnails" or ".git"
|
||||
FileStartsWith []string `json:"fileStartsWith"` // (global) exclude files that start with these prefixes. Eg. "archive-" or "backup-"
|
||||
FolderStartsWith []string `json:"folderStartsWith"` // (global) exclude folders that start with these prefixes. Eg. "archive-" or "backup-"
|
||||
}
|
||||
|
||||
type Frontend struct {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"hash"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/errors"
|
||||
)
|
||||
|
||||
// GetChecksum calculates the checksum of a file using the specified algorithm.
|
||||
// Returns the checksum as a hex-encoded string.
|
||||
func GetChecksum(fullPath, algo string) (string, error) {
|
||||
reader, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
hashFuncs := map[string]hash.Hash{
|
||||
"md5": md5.New(),
|
||||
"sha1": sha1.New(),
|
||||
"sha256": sha256.New(),
|
||||
"sha512": sha512.New(),
|
||||
}
|
||||
|
||||
h, ok := hashFuncs[algo]
|
||||
if !ok {
|
||||
return "", errors.ErrInvalidOption
|
||||
}
|
||||
|
||||
_, err = io.Copy(h, reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
@@ -273,32 +273,91 @@ func (s *Storage) Permitted(sourcePath, indexPath, username string) bool {
|
||||
}
|
||||
|
||||
func (s *Storage) computePermitted(sourcePath, indexPath, username string) bool {
|
||||
var rulesFound []*AccessRule
|
||||
|
||||
// Walk up the path hierarchy and collect all relevant rules
|
||||
currentPath := indexPath
|
||||
for {
|
||||
permitted, found := s.permittedAtExactPath(sourcePath, indexPath, username)
|
||||
rule, found := s.getRuleAtExactPath(sourcePath, currentPath)
|
||||
if found {
|
||||
return permitted
|
||||
rulesFound = append(rulesFound, rule)
|
||||
}
|
||||
indexPath = utils.GetParentDirectoryPath(indexPath)
|
||||
if indexPath == "" {
|
||||
if currentPath == "/" || currentPath == "." || currentPath == "" {
|
||||
break
|
||||
}
|
||||
currentPath = utils.GetParentDirectoryPath(currentPath)
|
||||
}
|
||||
|
||||
// Now evaluate the rules, starting from the most specific (indexPath) to the least specific (root)
|
||||
for _, rule := range rulesFound {
|
||||
permitted, hasSpecificRule := s.evaluateRuleForUser(rule, username)
|
||||
if hasSpecificRule {
|
||||
return permitted
|
||||
}
|
||||
}
|
||||
|
||||
// No rules found anywhere in the path hierarchy
|
||||
// Check if the source has DenyByDefault configured (acts like a root-level denyAll rule)
|
||||
// No specific user or group rule found in the hierarchy.
|
||||
// Check for any DenyAll rule in the path.
|
||||
for _, rule := range rulesFound {
|
||||
if rule.DenyAll {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// No specific rules found anywhere in the path hierarchy.
|
||||
// Fallback to the source's DenyByDefault setting.
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[sourcePath]
|
||||
if !ok {
|
||||
logger.Errorf("source %s not found in config during access check", sourcePath)
|
||||
return false
|
||||
}
|
||||
|
||||
if sourceInfo.Config.DenyByDefault {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return !sourceInfo.Config.DenyByDefault
|
||||
}
|
||||
|
||||
// getRuleAtExactPath is a helper to get a rule without the recursive logic.
|
||||
func (s *Storage) getRuleAtExactPath(sourcePath, indexPath string) (*AccessRule, bool) {
|
||||
s.mux.RLock()
|
||||
defer s.mux.RUnlock()
|
||||
rulesBySource, ok := s.AllRules[sourcePath]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
rule, ok := rulesBySource[indexPath]
|
||||
return rule, ok
|
||||
}
|
||||
|
||||
// evaluateRuleForUser evaluates a single rule for a user and returns if a specific rule was found.
|
||||
func (s *Storage) evaluateRuleForUser(rule *AccessRule, username string) (permitted bool, hasSpecificRule bool) {
|
||||
// Check user deny first
|
||||
if _, found := rule.Deny.Users[username]; found {
|
||||
return false, true
|
||||
}
|
||||
|
||||
// Check group deny
|
||||
for group := range rule.Deny.Groups {
|
||||
if s.isUserInGroup(username, group) {
|
||||
return false, true
|
||||
}
|
||||
}
|
||||
|
||||
// Check user allow
|
||||
if _, found := rule.Allow.Users[username]; found {
|
||||
return true, true
|
||||
}
|
||||
|
||||
// Check group allow
|
||||
for group := range rule.Allow.Groups {
|
||||
if s.isUserInGroup(username, group) {
|
||||
return true, true
|
||||
}
|
||||
}
|
||||
|
||||
// No specific rule for this user in this rule set.
|
||||
return false, false
|
||||
}
|
||||
|
||||
/*
|
||||
// permittedAtExactPath checks if a rule exists at the given path and evaluates it if so.
|
||||
func (s *Storage) permittedAtExactPath(sourcePath, indexPath, username string) (bool, bool) {
|
||||
s.mux.RLock()
|
||||
@@ -371,6 +430,7 @@ func (s *Storage) permittedRule(rule *AccessRule, username string, sourcePath st
|
||||
|
||||
return true
|
||||
}
|
||||
*/
|
||||
|
||||
// isUserInGroup checks if a username is in a group.
|
||||
func (s *Storage) isUserInGroup(username, group string) bool {
|
||||
@@ -421,8 +481,6 @@ func (s *Storage) GetFrontendRules(sourcePath, indexPath string) (FrontendAccess
|
||||
frontendRules.Deny.Groups = utils.NonNilSlice(slices.Collect(maps.Keys(rule.Deny.Groups)))
|
||||
frontendRules.Allow.Users = utils.NonNilSlice(slices.Collect(maps.Keys(rule.Allow.Users)))
|
||||
frontendRules.Allow.Groups = utils.NonNilSlice(slices.Collect(maps.Keys(rule.Allow.Groups)))
|
||||
|
||||
logger.Debugf("Returning frontend rules for source: %s and index: %s", sourcePath, indexPath)
|
||||
return frontendRules, ok
|
||||
}
|
||||
|
||||
|
||||
@@ -405,3 +405,77 @@ func TestPermitted_DenyByDefaultWithDenyAll(t *testing.T) {
|
||||
t.Error("bob should not be permitted (DenyByDefault is true and no allow rule)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermitted_DenyByDefault_AdminRootAccess(t *testing.T) {
|
||||
// 1. Setup
|
||||
access.ClearCache()
|
||||
s, userStore := createTestStorage(t)
|
||||
createTestUser(t, userStore, "admin")
|
||||
createTestUser(t, userStore, "graham")
|
||||
createTestUser(t, userStore, "bob")
|
||||
err := s.LoadFromDB()
|
||||
if err != nil && err != storm.ErrNotFound {
|
||||
t.Errorf("unexpected error loading from DB: %v", err)
|
||||
}
|
||||
|
||||
// 2. Configure source with DenyByDefault: true
|
||||
originalSourceMap := settings.Config.Server.SourceMap
|
||||
defer func() {
|
||||
settings.Config.Server.SourceMap = originalSourceMap
|
||||
access.ClearCache()
|
||||
}()
|
||||
settings.Config.Server.SourceMap = map[string]settings.Source{
|
||||
"mnt/secure": {
|
||||
Path: "mnt/secure",
|
||||
Name: "secure",
|
||||
Config: settings.SourceConfig{
|
||||
DenyByDefault: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// 3. Set up access rules
|
||||
if err := s.AllowUser("mnt/secure", "/", "admin"); err != nil {
|
||||
t.Fatalf("AllowUser for admin failed: %v", err)
|
||||
}
|
||||
if err := s.AllowUser("mnt/secure", "/test", "graham"); err != nil {
|
||||
t.Fatalf("AllowUser for graham failed: %v", err)
|
||||
}
|
||||
|
||||
// 4. Assertions
|
||||
// Admin checks
|
||||
if !s.Permitted("mnt/secure", "/", "admin") {
|
||||
t.Error("admin should be permitted for /")
|
||||
}
|
||||
if !s.Permitted("mnt/secure", "/test", "admin") {
|
||||
t.Error("admin should be permitted for /test")
|
||||
}
|
||||
if !s.Permitted("mnt/secure", "/test/sub", "admin") {
|
||||
t.Error("admin should be permitted for /test/sub (this is the bug)")
|
||||
}
|
||||
|
||||
// Graham checks
|
||||
if !s.Permitted("mnt/secure", "/test", "graham") {
|
||||
t.Error("graham should be permitted for /test")
|
||||
}
|
||||
if !s.Permitted("mnt/secure", "/test/sub", "graham") {
|
||||
t.Error("graham should be permitted for /test/sub")
|
||||
}
|
||||
if s.Permitted("mnt/secure", "/", "graham") {
|
||||
t.Error("graham should NOT be permitted for /")
|
||||
}
|
||||
if s.Permitted("mnt/secure", "/anotherfolder", "graham") {
|
||||
t.Error("graham should NOT be permitted for /anotherfolder")
|
||||
}
|
||||
|
||||
// Bob checks (should have no access)
|
||||
if s.Permitted("mnt/secure", "/", "bob") {
|
||||
t.Error("bob should NOT be permitted for /")
|
||||
}
|
||||
if s.Permitted("mnt/secure", "/test", "bob") {
|
||||
t.Error("bob should NOT be permitted for /test")
|
||||
}
|
||||
if s.Permitted("mnt/secure", "/test/sub", "bob") {
|
||||
t.Error("bob should NOT be permitted for /test/sub")
|
||||
}
|
||||
}
|
||||
@@ -21,12 +21,12 @@ type CommonShare struct {
|
||||
QuickDownload bool `json:"quickDownload,omitempty"`
|
||||
HideNavButtons bool `json:"hideNavButtons,omitempty"`
|
||||
DisableSidebar bool `json:"disableSidebar"`
|
||||
ViewMode string `json:"viewMode,omitempty"`
|
||||
Source string `json:"source,omitempty"` // backend source is path to maintain between name changes
|
||||
Path string `json:"path,omitempty"`
|
||||
DownloadURL string `json:"downloadURL,omitempty"`
|
||||
DisableShareCard bool `json:"disableShareCard,omitempty"`
|
||||
EnforceDarkLightMode string `json:"enforceDarkLightMode,omitempty"` // "dark" or "light"
|
||||
ViewMode string `json:"viewMode,omitempty"` // default view mode for anonymous users: "list", "compact", "normal", "gallery"
|
||||
EnableOnlyOffice bool `json:"enableOnlyOffice,omitempty"`
|
||||
EnableOnlyOfficeEditing bool `json:"enableOnlyOfficeEditing,omitempty"`
|
||||
}
|
||||
|
||||
@@ -42,14 +42,15 @@ type Sorting struct {
|
||||
|
||||
type Preview struct {
|
||||
DisableHideSidebar bool `json:"disableHideSidebar"` // disable the hide sidebar preview for previews and editors
|
||||
HighQuality bool `json:"highQuality"` // generate high quality preview images
|
||||
Image bool `json:"image"` // show real image as icon instead of generic photo icon
|
||||
Video bool `json:"video"` // show preview image for video files
|
||||
MotionVideoPreview bool `json:"motionVideoPreview"` // show multiple frames for videos in preview when hovering
|
||||
Office bool `json:"office"` // show preview image for office files
|
||||
PopUp bool `json:"popup"` // show larger popup preview when hovering
|
||||
HighQuality bool `json:"highQuality"` // generate high quality thumbnail preview images
|
||||
Image bool `json:"image"` // show thumbnail preview image for image files
|
||||
Video bool `json:"video"` // show thumbnail preview image for video files
|
||||
MotionVideoPreview bool `json:"motionVideoPreview"` // show multiple frames for videos in thumbnail preview when hovering
|
||||
Office bool `json:"office"` // show thumbnail preview image for office files
|
||||
PopUp bool `json:"popup"` // show larger popup preview when hovering over thumbnail
|
||||
AutoplayMedia bool `json:"autoplayMedia"` // autoplay media files in preview
|
||||
DefaultMediaPlayer bool `json:"defaultMediaPlayer"` // disable html5 media player and use the default media player
|
||||
Folder bool `json:"folder"` // show thumbnail preview image for folder files
|
||||
}
|
||||
|
||||
// User describes a user.
|
||||
@@ -108,8 +109,9 @@ type NonAdminEditable struct {
|
||||
}
|
||||
|
||||
type FileLoading struct {
|
||||
MaxConcurrent int `json:"maxConcurrentUpload"`
|
||||
ChunkSize int `json:"uploadChunkSizeMb"`
|
||||
MaxConcurrent int `json:"maxConcurrentUpload"`
|
||||
ChunkSize int `json:"uploadChunkSizeMb"`
|
||||
ClearAll bool `json:"clearAll"`
|
||||
}
|
||||
|
||||
func CleanUsername(s string) string {
|
||||
|
||||
@@ -75,7 +75,12 @@ func Register(username string, sources []string) chan EventMessage {
|
||||
|
||||
func Unregister(username string, ch chan EventMessage) {
|
||||
userClientsMu.Lock()
|
||||
conns := userClients[username]
|
||||
defer userClientsMu.Unlock()
|
||||
conns, ok := userClients[username]
|
||||
if !ok {
|
||||
// User already cleaned up by Shutdown, nothing to do.
|
||||
return
|
||||
}
|
||||
for i, c := range conns {
|
||||
if c == ch {
|
||||
userClients[username] = append(conns[:i], conns[i+1:]...)
|
||||
@@ -85,18 +90,15 @@ func Unregister(username string, ch chan EventMessage) {
|
||||
if len(userClients[username]) == 0 {
|
||||
delete(userClients, username)
|
||||
}
|
||||
userClientsMu.Unlock()
|
||||
|
||||
sourceClientsMu.Lock()
|
||||
defer sourceClientsMu.Unlock()
|
||||
for source, clients := range sourceClients {
|
||||
delete(clients, ch)
|
||||
if len(clients) == 0 {
|
||||
delete(sourceClients, source)
|
||||
}
|
||||
}
|
||||
sourceClientsMu.Unlock()
|
||||
|
||||
close(ch)
|
||||
}
|
||||
|
||||
func SendToUsers(eventType, message string, users []string) {
|
||||
@@ -142,10 +144,20 @@ func handleSourceUpdates() {
|
||||
func Shutdown() {
|
||||
userClientsMu.Lock()
|
||||
defer userClientsMu.Unlock()
|
||||
sourceClientsMu.Lock()
|
||||
defer sourceClientsMu.Unlock()
|
||||
|
||||
for _, clientChannels := range userClients {
|
||||
for username, clientChannels := range userClients {
|
||||
for _, ch := range clientChannels {
|
||||
// Clean up source clients
|
||||
for source, clients := range sourceClients {
|
||||
delete(clients, ch)
|
||||
if len(clients) == 0 {
|
||||
delete(sourceClients, source)
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
}
|
||||
delete(userClients, username)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ImageService handles image operations with ffmpeg
|
||||
type ImageService struct {
|
||||
ffmpegPath string
|
||||
ffprobePath string
|
||||
debug bool
|
||||
cacheDir string
|
||||
}
|
||||
|
||||
// NewImageService creates a new image service instance
|
||||
func NewImageService(ffmpegPath, ffprobePath string, debug bool, cacheDir string) *ImageService {
|
||||
return &ImageService{
|
||||
ffmpegPath: ffmpegPath,
|
||||
ffprobePath: ffprobePath,
|
||||
debug: debug,
|
||||
cacheDir: cacheDir,
|
||||
}
|
||||
}
|
||||
|
||||
// GetImageOrientation extracts the EXIF orientation from an image file using exiftool
|
||||
func (s *ImageService) GetImageOrientation(imagePath string) (string, error) {
|
||||
// Use exiftool to get orientation information
|
||||
cmd := exec.Command("exiftool", "-Orientation", "-s3", imagePath)
|
||||
var out bytes.Buffer
|
||||
var errOut bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &errOut
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return "Horizontal (normal)", nil // Default to normal orientation
|
||||
}
|
||||
|
||||
orientation := strings.TrimSpace(out.String())
|
||||
if orientation == "" {
|
||||
orientation = "Horizontal (normal)" // Default if no orientation found
|
||||
}
|
||||
return orientation, nil
|
||||
}
|
||||
|
||||
// GetOrientationFilter converts EXIF orientation to FFmpeg filter
|
||||
func (s *ImageService) GetOrientationFilter(orientation string) string {
|
||||
switch orientation {
|
||||
// Text-based EXIF orientation values (from exiftool)
|
||||
case "Rotate 90 CW", "Right-top":
|
||||
return ",transpose=1" // 90° clockwise
|
||||
case "Rotate 180", "Bottom-right":
|
||||
return ",transpose=1,transpose=1" // 180° (two 90° rotations)
|
||||
case "Rotate 270 CW", "Left-bottom":
|
||||
return ",transpose=2" // 90° counter-clockwise
|
||||
case "Mirror horizontal", "Top-right":
|
||||
return ",hflip" // Horizontal flip
|
||||
case "Mirror vertical", "Bottom-left":
|
||||
return ",vflip" // Vertical flip (upside down)
|
||||
case "Mirror horizontal and rotate 270 CW", "Right-bottom":
|
||||
return ",transpose=0" // 90° counter-clockwise + horizontal flip
|
||||
case "Mirror horizontal and rotate 90 CW", "Left-top":
|
||||
return ",transpose=3" // 90° clockwise + horizontal flip
|
||||
case "Horizontal (normal)", "Top-left":
|
||||
return "" // No rotation needed
|
||||
|
||||
// Numeric EXIF orientation values (standard EXIF specification)
|
||||
case "1":
|
||||
return "" // Top-left (normal)
|
||||
case "2":
|
||||
return ",hflip" // Top-right (flip horizontal)
|
||||
case "3":
|
||||
return ",transpose=1,transpose=1" // Bottom-right (rotate 180)
|
||||
case "4":
|
||||
return ",vflip" // Bottom-left (flip vertical)
|
||||
case "5":
|
||||
return ",transpose=3" // Left-top (transpose: 90° CW + horizontal flip)
|
||||
case "6":
|
||||
return ",transpose=1" // Right-top (rotate 90 CW)
|
||||
case "7":
|
||||
return ",transpose=0" // Right-bottom (transverse: 90° CCW + horizontal flip)
|
||||
case "8":
|
||||
return ",transpose=2" // Left-bottom (rotate 270 CW / 90° CCW)
|
||||
|
||||
default:
|
||||
return "" // Default to no rotation for unknown orientations
|
||||
}
|
||||
}
|
||||
|
||||
// GetImageDimensions extracts the dimensions of an image file using ffprobe
|
||||
func (s *ImageService) GetImageDimensions(imagePath string) (width, height int, err error) {
|
||||
|
||||
// Get HEIC dimensions (fallback to individual stream dimensions for nowe re
|
||||
probeCmd := exec.Command(
|
||||
s.ffprobePath,
|
||||
"-v", "error",
|
||||
"-select_streams", "v:0",
|
||||
"-show_entries", "stream=width,height",
|
||||
"-of", "csv=p=0:s=x",
|
||||
imagePath,
|
||||
)
|
||||
|
||||
var probeOut bytes.Buffer
|
||||
var probeErr bytes.Buffer
|
||||
probeCmd.Stdout = &probeOut
|
||||
probeCmd.Stderr = &probeErr
|
||||
|
||||
if err = probeCmd.Run(); err != nil {
|
||||
return 0, 0, fmt.Errorf("ffprobe command failed on image file '%s': %w", imagePath, err)
|
||||
}
|
||||
|
||||
dimensions := strings.TrimSpace(probeOut.String())
|
||||
|
||||
// Handle cases where ffprobe returns multiple lines or extra characters
|
||||
// Take the first line and clean it up
|
||||
lines := strings.Split(dimensions, "\n")
|
||||
cleanDimensions := strings.TrimSpace(lines[0])
|
||||
|
||||
// Remove any trailing 'x' characters that might appear
|
||||
cleanDimensions = strings.TrimSuffix(cleanDimensions, "x")
|
||||
|
||||
parts := strings.Split(cleanDimensions, "x")
|
||||
if len(parts) != 2 {
|
||||
return 0, 0, fmt.Errorf("invalid dimensions format: %s", cleanDimensions)
|
||||
}
|
||||
|
||||
width, err = strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid width: %w", err)
|
||||
}
|
||||
|
||||
height, err = strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid height: %w", err)
|
||||
}
|
||||
|
||||
return width, height, nil
|
||||
}
|
||||
|
||||
// ConvertHEICToJPEGDirect converts a HEIC file to JPEG using direct FFmpeg conversion (fast method)
|
||||
func (s *ImageService) ConvertHEICToJPEGDirect(heicPath string, targetWidth, targetHeight int, quality string) ([]byte, error) {
|
||||
|
||||
// Get EXIF orientation and create appropriate filter
|
||||
orientation, err := s.GetImageOrientation(heicPath)
|
||||
if err != nil {
|
||||
orientation = "Horizontal (normal)"
|
||||
}
|
||||
orientationFilter := s.GetOrientationFilter(orientation)
|
||||
|
||||
// Create temporary output file
|
||||
outputDir := s.cacheDir
|
||||
err = os.MkdirAll(outputDir, 0755)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
outputFile := filepath.Join(outputDir, fmt.Sprintf("heic_direct_%d.jpg", os.Getpid()))
|
||||
defer os.Remove(outputFile)
|
||||
|
||||
// Build FFmpeg command for direct conversion
|
||||
args := []string{"-i", heicPath}
|
||||
|
||||
// Build video filter chain with scaling and orientation
|
||||
var filterChain string
|
||||
if targetWidth > 0 && targetHeight > 0 {
|
||||
filterChain = fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease%s", targetWidth, targetHeight, orientationFilter)
|
||||
} else {
|
||||
// For original size, only apply orientation if needed
|
||||
if orientationFilter != "" {
|
||||
filterChain = orientationFilter[1:] // Remove leading comma
|
||||
}
|
||||
}
|
||||
|
||||
// Add video filter if we have one
|
||||
if filterChain != "" {
|
||||
args = append(args, "-vf", filterChain)
|
||||
}
|
||||
|
||||
// Add quality and output settings
|
||||
args = append(args, "-q:v", quality, "-pix_fmt", "yuvj420p", "-y", outputFile)
|
||||
|
||||
cmd := exec.Command(s.ffmpegPath, args...)
|
||||
var cmdOut bytes.Buffer
|
||||
var cmdErr bytes.Buffer
|
||||
cmd.Stdout = &cmdOut
|
||||
cmd.Stderr = &cmdErr
|
||||
|
||||
if err = cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("direct HEIC conversion failed: %w", err)
|
||||
}
|
||||
|
||||
// Read the converted file
|
||||
jpegBytes, err := os.ReadFile(outputFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read converted JPEG: %w", err)
|
||||
}
|
||||
|
||||
return jpegBytes, nil
|
||||
}
|
||||
|
||||
// ConvertHEICToJPEG converts a HEIC file to JPEG with specified dimensions and quality using proper tile extraction
|
||||
func (s *ImageService) ConvertHEICToJPEG(heicPath string, targetWidth, targetHeight int, quality string) ([]byte, error) {
|
||||
// Create temporary directory for tile processing
|
||||
outputDir := s.cacheDir
|
||||
tempDir := filepath.Join(outputDir, fmt.Sprintf("heic_tiles_%d", os.Getpid()))
|
||||
err := os.MkdirAll(tempDir, 0755)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Step 1: Get grid info using trace output
|
||||
|
||||
// Use ffprobe with trace level to get grid information (with time limit for speed)
|
||||
gridCmd := exec.Command(s.ffmpegPath, "-loglevel", "trace", "-i", heicPath, "-f", "null", "-", "-t", "0.1")
|
||||
var gridOut bytes.Buffer
|
||||
var gridErr bytes.Buffer
|
||||
gridCmd.Stdout = &gridOut
|
||||
gridCmd.Stderr = &gridErr
|
||||
_ = gridCmd.Run() // Don't check error, we're just extracting info
|
||||
|
||||
traceOutput := gridErr.String()
|
||||
|
||||
// Parse grid dimensions from trace output
|
||||
var gridCols, gridRows = 8, 6 // Default fallback
|
||||
var val int
|
||||
|
||||
// Look for grid_row and grid_col in trace output
|
||||
lines := strings.Split(traceOutput, "\n")
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "grid_row") {
|
||||
// Try to extract grid_row value
|
||||
if parts := strings.Split(line, "grid_row"); len(parts) > 1 {
|
||||
if matches := strings.Fields(parts[1]); len(matches) > 0 {
|
||||
if val, err = strconv.Atoi(strings.Trim(matches[0], " :=")); err == nil {
|
||||
gridRows = val + 1 // HEIC grid_row is 0-based
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.Contains(line, "grid_col") {
|
||||
// Try to extract grid_col value
|
||||
if parts := strings.Split(line, "grid_col"); len(parts) > 1 {
|
||||
if matches := strings.Fields(parts[1]); len(matches) > 0 {
|
||||
if val, err = strconv.Atoi(strings.Trim(matches[0], " :=")); err == nil {
|
||||
gridCols = val + 1 // HEIC grid_col is 0-based
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get EXIF orientation and create appropriate filter
|
||||
orientation, err := s.GetImageOrientation(heicPath)
|
||||
if err != nil {
|
||||
orientation = "Horizontal (normal)"
|
||||
}
|
||||
orientationFilter := s.GetOrientationFilter(orientation)
|
||||
|
||||
// Step 2: Extract tile streams (skip stream 0 which is compatibility image)
|
||||
|
||||
// Extract using JPEG for faster I/O (PNG was unnecessarily slow and large)
|
||||
// Skip the first output (stream 0) since it's the compatibility image
|
||||
tilesPattern := filepath.Join(tempDir, "output_%d.jpg")
|
||||
|
||||
extractCmd := exec.Command(
|
||||
s.ffmpegPath,
|
||||
"-i", heicPath,
|
||||
"-map", "0", // Map all streams
|
||||
"-q:v", "3", // Medium quality for intermediate tiles
|
||||
"-y",
|
||||
tilesPattern,
|
||||
)
|
||||
|
||||
var extractOut bytes.Buffer
|
||||
var extractErr bytes.Buffer
|
||||
extractCmd.Stdout = &extractOut
|
||||
extractCmd.Stderr = &extractErr
|
||||
|
||||
if err = extractCmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("tile extraction failed: %w", err)
|
||||
}
|
||||
|
||||
// Step 3: Filter tiles (skip output_0.png which is compatibility image)
|
||||
|
||||
files, err := filepath.Glob(filepath.Join(tempDir, "output_*.jpg"))
|
||||
if err != nil || len(files) == 0 {
|
||||
return nil, fmt.Errorf("no tiles extracted, found %d files", len(files))
|
||||
}
|
||||
|
||||
// Create filtered tiles, skipping output_0.jpg (compatibility image)
|
||||
// and rename them to sequential tile_001.jpg, tile_002.jpg, etc.
|
||||
tileIndex := 1
|
||||
expectedTiles := gridCols * gridRows
|
||||
|
||||
for i := 1; i <= len(files); i++ { // Start from 1 to skip output_0.jpg
|
||||
sourceFile := filepath.Join(tempDir, fmt.Sprintf("output_%d.jpg", i))
|
||||
var info os.FileInfo
|
||||
if info, err = os.Stat(sourceFile); err == nil && info.Size() > 500 {
|
||||
// This is a valid tile, rename it to sequential format
|
||||
targetFile := filepath.Join(tempDir, fmt.Sprintf("tile_%03d.jpg", tileIndex))
|
||||
if err = os.Rename(sourceFile, targetFile); err != nil {
|
||||
continue
|
||||
}
|
||||
tileIndex++
|
||||
|
||||
// Stop if we have enough tiles
|
||||
if tileIndex > expectedTiles {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actualTiles := tileIndex - 1
|
||||
|
||||
// Step 4: Merge tiles back into final image with proper quality and rotation
|
||||
outputFile := filepath.Join(tempDir, "final_output.jpg")
|
||||
|
||||
// Build the video filter chain
|
||||
inputPattern := filepath.Join(tempDir, "tile_%03d.jpg")
|
||||
|
||||
// Create filter chain: tile assembly + proper scaling
|
||||
var filterChain string
|
||||
|
||||
// For original size (targetWidth=0, targetHeight=0), don't scale
|
||||
if targetWidth == 0 && targetHeight == 0 {
|
||||
// This is original size - no scaling, maximum quality
|
||||
// Use padding=0:margin=0 to eliminate black borders between tiles
|
||||
filterChain = fmt.Sprintf("tile=%dx%d:nb_frames=%d:padding=0:margin=0%s",
|
||||
gridCols, gridRows, actualTiles, orientationFilter)
|
||||
} else {
|
||||
// This is a resize request - apply scaling with proper aspect handling
|
||||
filterChain = fmt.Sprintf("tile=%dx%d:nb_frames=%d:padding=0:margin=0%s,scale=%d:%d:force_original_aspect_ratio=decrease:eval=frame",
|
||||
gridCols, gridRows, actualTiles, orientationFilter, targetWidth, targetHeight)
|
||||
}
|
||||
|
||||
mergeCmd := exec.Command(
|
||||
s.ffmpegPath,
|
||||
"-i", inputPattern,
|
||||
"-vf", filterChain,
|
||||
"-frames:v", "1",
|
||||
"-q:v", quality, // Use the quality setting passed in
|
||||
"-pix_fmt", "yuvj420p", // Use full-range YUV for better JPEG compatibility
|
||||
"-y",
|
||||
outputFile,
|
||||
)
|
||||
|
||||
var mergeOut bytes.Buffer
|
||||
var mergeErr bytes.Buffer
|
||||
mergeCmd.Stdout = &mergeOut
|
||||
mergeCmd.Stderr = &mergeErr
|
||||
|
||||
if err = mergeCmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("tile merge failed: %w", err)
|
||||
}
|
||||
|
||||
// Step 5: Read final output
|
||||
jpegBytes, err := os.ReadFile(outputFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read merged JPEG: %w", err)
|
||||
}
|
||||
|
||||
return jpegBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package ffmpeg
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetOrientationFilter(t *testing.T) {
|
||||
service := &ImageService{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
orientation string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Normal orientation",
|
||||
orientation: "Horizontal (normal)",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Normal orientation alternative",
|
||||
orientation: "Top-left",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Rotate 90 CW",
|
||||
orientation: "Rotate 90 CW",
|
||||
expected: ",transpose=1",
|
||||
},
|
||||
{
|
||||
name: "Rotate 90 CW alternative",
|
||||
orientation: "Right-top",
|
||||
expected: ",transpose=1",
|
||||
},
|
||||
{
|
||||
name: "Rotate 180",
|
||||
orientation: "Rotate 180",
|
||||
expected: ",transpose=1,transpose=1",
|
||||
},
|
||||
{
|
||||
name: "Rotate 180 alternative",
|
||||
orientation: "Bottom-right",
|
||||
expected: ",transpose=1,transpose=1",
|
||||
},
|
||||
{
|
||||
name: "Rotate 270 CW",
|
||||
orientation: "Rotate 270 CW",
|
||||
expected: ",transpose=2",
|
||||
},
|
||||
{
|
||||
name: "Rotate 270 CW alternative",
|
||||
orientation: "Left-bottom",
|
||||
expected: ",transpose=2",
|
||||
},
|
||||
{
|
||||
name: "Mirror horizontal",
|
||||
orientation: "Mirror horizontal",
|
||||
expected: ",hflip",
|
||||
},
|
||||
{
|
||||
name: "Mirror horizontal alternative",
|
||||
orientation: "Top-right",
|
||||
expected: ",hflip",
|
||||
},
|
||||
{
|
||||
name: "Mirror vertical",
|
||||
orientation: "Mirror vertical",
|
||||
expected: ",vflip",
|
||||
},
|
||||
{
|
||||
name: "Mirror vertical alternative",
|
||||
orientation: "Bottom-left",
|
||||
expected: ",vflip",
|
||||
},
|
||||
{
|
||||
name: "Mirror horizontal and rotate 270 CW",
|
||||
orientation: "Mirror horizontal and rotate 270 CW",
|
||||
expected: ",transpose=0",
|
||||
},
|
||||
{
|
||||
name: "Mirror horizontal and rotate 270 CW alternative",
|
||||
orientation: "Right-bottom",
|
||||
expected: ",transpose=0",
|
||||
},
|
||||
{
|
||||
name: "Mirror horizontal and rotate 90 CW",
|
||||
orientation: "Mirror horizontal and rotate 90 CW",
|
||||
expected: ",transpose=3",
|
||||
},
|
||||
{
|
||||
name: "Mirror horizontal and rotate 90 CW alternative",
|
||||
orientation: "Left-top",
|
||||
expected: ",transpose=3",
|
||||
},
|
||||
{
|
||||
name: "Unknown orientation",
|
||||
orientation: "Unknown Orientation",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Empty orientation",
|
||||
orientation: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := service.GetOrientationFilter(tt.orientation)
|
||||
if result != tt.expected {
|
||||
t.Errorf("GetOrientationFilter(%q) = %q, want %q", tt.orientation, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetImageOrientation(t *testing.T) {
|
||||
// Create a temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "ffmpeg_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
service := NewImageService("ffmpeg", "ffprobe", false, tempDir)
|
||||
|
||||
t.Run("Non-existent file", func(t *testing.T) {
|
||||
nonExistentFile := filepath.Join(tempDir, "non_existent.heic")
|
||||
orientation, err := service.GetImageOrientation(nonExistentFile)
|
||||
|
||||
// Should return default orientation when exiftool fails
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for non-existent file, got: %v", err)
|
||||
}
|
||||
if orientation != "Horizontal (normal)" {
|
||||
t.Errorf("Expected default orientation 'Horizontal (normal)', got: %q", orientation)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Empty file path", func(t *testing.T) {
|
||||
orientation, err := service.GetImageOrientation("")
|
||||
|
||||
// Should return default orientation when exiftool fails
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for empty file path, got: %v", err)
|
||||
}
|
||||
if orientation != "Horizontal (normal)" {
|
||||
t.Errorf("Expected default orientation 'Horizontal (normal)', got: %q", orientation)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewImageService(t *testing.T) {
|
||||
ffmpegPath := "/usr/bin/ffmpeg"
|
||||
ffprobePath := "/usr/bin/ffprobe"
|
||||
debug := true
|
||||
cacheDir := "/tmp/test_cache"
|
||||
|
||||
service := NewImageService(ffmpegPath, ffprobePath, debug, cacheDir)
|
||||
|
||||
if service.ffmpegPath != ffmpegPath {
|
||||
t.Errorf("Expected ffmpegPath %q, got %q", ffmpegPath, service.ffmpegPath)
|
||||
}
|
||||
if service.ffprobePath != ffprobePath {
|
||||
t.Errorf("Expected ffprobePath %q, got %q", ffprobePath, service.ffprobePath)
|
||||
}
|
||||
if service.debug != debug {
|
||||
t.Errorf("Expected debug %v, got %v", debug, service.debug)
|
||||
}
|
||||
if service.cacheDir != cacheDir {
|
||||
t.Errorf("Expected cacheDir %q, got %q", cacheDir, service.cacheDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Integration test for orientation handling in direct conversion
|
||||
func TestConvertHEICToJPEGDirect_OrientationHandling(t *testing.T) {
|
||||
// Skip this test if ffmpeg is not available
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Create a temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "ffmpeg_integration_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
service := NewImageService("ffmpeg", "ffprobe", true, tempDir)
|
||||
|
||||
t.Run("Direct conversion with orientation detection", func(t *testing.T) {
|
||||
// This test requires actual HEIC files to work properly
|
||||
// For CI/CD environments, we can mock this or skip if files don't exist
|
||||
|
||||
heicTestFiles := []string{
|
||||
"/Users/steffag/Downloads/heic/IMG_6660.heic",
|
||||
"/Users/steffag/Downloads/heic/IMG_2919.HEIC",
|
||||
"/Users/steffag/Downloads/heic/shelf-christmas-decoration.heic",
|
||||
}
|
||||
|
||||
for _, testFile := range heicTestFiles {
|
||||
// Check if test file exists
|
||||
if _, err := os.Stat(testFile); os.IsNotExist(err) {
|
||||
t.Logf("Skipping test for %s (file not found)", filepath.Base(testFile))
|
||||
continue
|
||||
}
|
||||
|
||||
t.Logf("Testing orientation handling for %s", filepath.Base(testFile))
|
||||
|
||||
// Test orientation detection
|
||||
orientation, err := service.GetImageOrientation(testFile)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get orientation for %s: %v", filepath.Base(testFile), err)
|
||||
continue
|
||||
}
|
||||
t.Logf("Detected orientation: %s", orientation)
|
||||
|
||||
// Test filter generation
|
||||
filter := service.GetOrientationFilter(orientation)
|
||||
t.Logf("Generated filter: %s", filter)
|
||||
|
||||
// Test actual conversion (small size for speed)
|
||||
jpegBytes, err := service.ConvertHEICToJPEGDirect(testFile, 100, 100, "5")
|
||||
if err != nil {
|
||||
t.Errorf("Failed to convert %s: %v", filepath.Base(testFile), err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(jpegBytes) < 400 {
|
||||
t.Errorf("Converted image too small (%d bytes), likely conversion failed", len(jpegBytes))
|
||||
}
|
||||
|
||||
t.Logf("Successfully converted %s to %d bytes", filepath.Base(testFile), len(jpegBytes))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark the orientation filter generation
|
||||
func BenchmarkGetOrientationFilter(b *testing.B) {
|
||||
service := &ImageService{}
|
||||
orientations := []string{
|
||||
"Horizontal (normal)",
|
||||
"Rotate 90 CW",
|
||||
"Rotate 180",
|
||||
"Mirror vertical",
|
||||
"Unknown Orientation",
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
orientation := orientations[i%len(orientations)]
|
||||
_ = service.GetOrientationFilter(orientation)
|
||||
}
|
||||
}
|
||||
|
||||
// Test table for all EXIF orientation values
|
||||
func TestGetOrientationFilter_AllEXIFValues(t *testing.T) {
|
||||
service := &ImageService{}
|
||||
|
||||
// Test all possible EXIF orientation values according to EXIF spec
|
||||
exifTests := []struct {
|
||||
exifValue string
|
||||
description string
|
||||
expectsTransformation bool
|
||||
}{
|
||||
{"1", "Top-left (normal)", false},
|
||||
{"2", "Top-right (flip horizontal)", true},
|
||||
{"3", "Bottom-right (rotate 180)", true},
|
||||
{"4", "Bottom-left (flip vertical)", true},
|
||||
{"5", "Left-top (transpose)", true},
|
||||
{"6", "Right-top (rotate 90 CW)", true},
|
||||
{"7", "Right-bottom (transverse)", true},
|
||||
{"8", "Left-bottom (rotate 270 CW)", true},
|
||||
}
|
||||
|
||||
for _, tt := range exifTests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
filter := service.GetOrientationFilter(tt.exifValue)
|
||||
|
||||
if tt.expectsTransformation && filter == "" {
|
||||
t.Errorf("Expected transformation for EXIF value %s (%s), but got empty filter", tt.exifValue, tt.description)
|
||||
}
|
||||
if !tt.expectsTransformation && filter != "" {
|
||||
t.Errorf("Expected no transformation for EXIF value %s (%s), but got filter: %s", tt.exifValue, tt.description, filter)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+33
-21
@@ -5,6 +5,7 @@ go 1.25.0
|
||||
require (
|
||||
github.com/asdine/storm/v3 v3.2.1
|
||||
github.com/coreos/go-oidc/v3 v3.15.0
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
|
||||
github.com/dsoprea/go-exif/v3 v3.0.1
|
||||
github.com/gen2brain/go-fitz v1.24.15
|
||||
github.com/go-playground/validator/v10 v10.27.0
|
||||
@@ -15,16 +16,16 @@ require (
|
||||
github.com/gtsteffaniak/go-logger v0.1.2
|
||||
github.com/kovidgoyal/imaging v1.6.4
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/spf13/afero v1.14.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/swaggo/http-swagger v1.3.4
|
||||
github.com/swaggo/swag v1.16.6
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/image v0.30.0
|
||||
golang.org/x/mod v0.27.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/time v0.12.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/image v0.31.0
|
||||
golang.org/x/mod v0.28.0
|
||||
golang.org/x/oauth2 v0.31.0
|
||||
golang.org/x/sys v0.36.0
|
||||
golang.org/x/time v0.13.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@@ -46,7 +47,7 @@ require (
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.3.1 // indirect
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
|
||||
github.com/air-verse/air v1.62.0 // indirect
|
||||
github.com/air-verse/air v1.63.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
|
||||
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
|
||||
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
|
||||
@@ -101,10 +102,21 @@ require (
|
||||
github.com/go-critic/go-critic v0.13.0 // indirect
|
||||
github.com/go-errors/errors v1.5.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.2 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.22.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.1 // indirect
|
||||
github.com/go-openapi/spec v0.21.0 // indirect
|
||||
github.com/go-openapi/swag v0.23.1 // indirect
|
||||
github.com/go-openapi/swag v0.24.1 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.24.0 // indirect
|
||||
github.com/go-openapi/swag/conv v0.24.0 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.24.0 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.24.0 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.24.0 // indirect
|
||||
github.com/go-openapi/swag/loading v0.24.0 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.24.0 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.24.0 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.24.0 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.24.0 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.24.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||
@@ -118,8 +130,8 @@ require (
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/gohugoio/hugo v0.147.6 // indirect
|
||||
github.com/golang/geo v0.0.0-20250825151631-54d70cc7cb31 // indirect
|
||||
github.com/gohugoio/hugo v0.149.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20250912065020-b504328d3ef3 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
|
||||
github.com/golangci/go-printf-func-name v0.1.0 // indirect
|
||||
@@ -211,7 +223,7 @@ require (
|
||||
github.com/sivchari/containedctx v1.0.3 // indirect
|
||||
github.com/sonatard/noctx v0.4.0 // indirect
|
||||
github.com/sourcegraph/go-diff v0.7.0 // indirect
|
||||
github.com/spf13/cast v1.8.0 // indirect
|
||||
github.com/spf13/cast v1.9.2 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
@@ -222,7 +234,7 @@ require (
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
github.com/swaggo/files v1.0.1 // indirect
|
||||
github.com/tdakkota/asciicheck v0.4.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.1 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.3 // indirect
|
||||
github.com/tetafro/godot v1.5.1 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
|
||||
github.com/timonwong/loggercheck v0.11.0 // indirect
|
||||
@@ -248,11 +260,11 @@ require (
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
honnef.co/go/tools v0.6.1 // indirect
|
||||
|
||||
+98
-60
@@ -73,8 +73,8 @@ github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsu
|
||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
|
||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
|
||||
github.com/air-verse/air v1.62.0 h1:6CoXL4MAX9dc4xAzLfjMcDfbBoGmW5VjuuTV/1+bI+M=
|
||||
github.com/air-verse/air v1.62.0/go.mod h1:EO+jWuetL10tS9raffwg8WEV0t0KUeucRRaf9ii86dA=
|
||||
github.com/air-verse/air v1.63.0 h1:fwcdHpwaUe4/+q349PxptzAIn8gVE6Yke8TgW0LsDxQ=
|
||||
github.com/air-verse/air v1.63.0/go.mod h1:RyCQVx2+3Zz2BzoqkukYiGmWkWXNKMf0x5ubIFcUB8Q=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||
@@ -108,6 +108,8 @@ github.com/ashanbrown/makezero/v2 v2.0.1 h1:r8GtKetWOgoJ4sLyUx97UTwyt2dO7WkGFHiz
|
||||
github.com/ashanbrown/makezero/v2 v2.0.1/go.mod h1:kKU4IMxmYW1M4fiEHMb2vc5SFoPzXvgbMR9gIp5pjSw=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -116,8 +118,8 @@ github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
|
||||
github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/bep/gitmap v1.6.0 h1:sDuQMm9HoTL0LtlrfxjbjgAg2wHQd4nkMup2FInYzhA=
|
||||
github.com/bep/gitmap v1.6.0/go.mod h1:n+3W1f/rot2hynsqEGxGMErPRgT41n9CkGuzPvz9cIw=
|
||||
github.com/bep/gitmap v1.9.0 h1:2pyb1ex+cdwF6c4tsrhEgEKfyNfxE34d5K+s2sa9byc=
|
||||
github.com/bep/gitmap v1.9.0/go.mod h1:Juq6e1qqCRvc1W7nzgadPGI9IGV13ZncEebg5atj4Vo=
|
||||
github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
|
||||
github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
|
||||
github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg=
|
||||
@@ -128,6 +130,8 @@ github.com/bep/goportabletext v0.1.0 h1:8dqym2So1cEqVZiBa4ZnMM1R9l/DnC1h4ONg4J5k
|
||||
github.com/bep/goportabletext v0.1.0/go.mod h1:6lzSTsSue75bbcyvVc0zqd1CdApuT+xkZQ6Re5DzZFg=
|
||||
github.com/bep/gowebp v0.4.0 h1:QihuVnvIKbRoeBNQkN0JPMM8ClLmD6V2jMftTFwSK3Q=
|
||||
github.com/bep/gowebp v0.4.0/go.mod h1:95gtYkAA8iIn1t3HkAPurRCVGV/6NhgaHJ1urz0iIwc=
|
||||
github.com/bep/helpers v0.6.0 h1:qtqMCK8XPFNM9hp5Ztu9piPjxNNkk8PIyUVjg6v8Bsw=
|
||||
github.com/bep/helpers v0.6.0/go.mod h1:IOZlgx5PM/R/2wgyCatfsgg5qQ6rNZJNDpWGXqDR044=
|
||||
github.com/bep/imagemeta v0.12.0 h1:ARf+igs5B7pf079LrqRnwzQ/wEB8Q9v4NSDRZO1/F5k=
|
||||
github.com/bep/imagemeta v0.12.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8=
|
||||
github.com/bep/lazycache v0.8.0 h1:lE5frnRjxaOFbkPZ1YL6nijzOPPz6zeXasJq8WpG4L8=
|
||||
@@ -207,6 +211,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
|
||||
github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
|
||||
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
|
||||
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
|
||||
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
@@ -236,8 +242,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
|
||||
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
|
||||
github.com/evanw/esbuild v0.25.3 h1:4JKyUsm/nHDhpxis4IyWXAi8GiyTwG1WdEp6OhGVE8U=
|
||||
github.com/evanw/esbuild v0.25.3/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/evanw/esbuild v0.25.9 h1:aU7GVC4lxJGC1AyaPwySWjSIaNLAdVEEuq3chD0Khxs=
|
||||
github.com/evanw/esbuild v0.25.9/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
|
||||
@@ -255,8 +261,8 @@ github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIp
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gen2brain/go-fitz v1.24.15 h1:sJNB1MOWkqnzzENPHggFpgxTwW0+S5WF/rM5wUBpJWo=
|
||||
github.com/gen2brain/go-fitz v1.24.15/go.mod h1:SftkiVbTHqF141DuiLwBBM65zP7ig6AVDQpf2WlHamo=
|
||||
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
|
||||
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
|
||||
github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
||||
github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghostiam/protogetter v0.3.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY=
|
||||
@@ -282,14 +288,36 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
|
||||
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||
github.com/go-openapi/jsonpointer v0.22.0 h1:TmMhghgNef9YXxTu1tOopo+0BGEytxA+okbry0HjZsM=
|
||||
github.com/go-openapi/jsonpointer v0.22.0/go.mod h1:xt3jV88UtExdIkkL7NloURjRQjbeUgcxFblMjq2iaiU=
|
||||
github.com/go-openapi/jsonreference v0.21.1 h1:bSKrcl8819zKiOgxkbVNRUBIr6Wwj9KYrDbMjRs0cDA=
|
||||
github.com/go-openapi/jsonreference v0.21.1/go.mod h1:PWs8rO4xxTUqKGu+lEvvCxD5k2X7QYkKAepJyCmSTT8=
|
||||
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||
github.com/go-openapi/swag v0.24.1 h1:DPdYTZKo6AQCRqzwr/kGkxJzHhpKxZ9i/oX0zag+MF8=
|
||||
github.com/go-openapi/swag v0.24.1/go.mod h1:sm8I3lCPlspsBBwUm1t5oZeWZS0s7m/A+Psg0ooRU0A=
|
||||
github.com/go-openapi/swag/cmdutils v0.24.0 h1:KlRCffHwXFI6E5MV9n8o8zBRElpY4uK4yWyAMWETo9I=
|
||||
github.com/go-openapi/swag/cmdutils v0.24.0/go.mod h1:uxib2FAeQMByyHomTlsP8h1TtPd54Msu2ZDU/H5Vuf8=
|
||||
github.com/go-openapi/swag/conv v0.24.0 h1:ejB9+7yogkWly6pnruRX45D1/6J+ZxRu92YFivx54ik=
|
||||
github.com/go-openapi/swag/conv v0.24.0/go.mod h1:jbn140mZd7EW2g8a8Y5bwm8/Wy1slLySQQ0ND6DPc2c=
|
||||
github.com/go-openapi/swag/fileutils v0.24.0 h1:U9pCpqp4RUytnD689Ek/N1d2N/a//XCeqoH508H5oak=
|
||||
github.com/go-openapi/swag/fileutils v0.24.0/go.mod h1:3SCrCSBHyP1/N+3oErQ1gP+OX1GV2QYFSnrTbzwli90=
|
||||
github.com/go-openapi/swag/jsonname v0.24.0 h1:2wKS9bgRV/xB8c62Qg16w4AUiIrqqiniJFtZGi3dg5k=
|
||||
github.com/go-openapi/swag/jsonname v0.24.0/go.mod h1:GXqrPzGJe611P7LG4QB9JKPtUZ7flE4DOVechNaDd7Q=
|
||||
github.com/go-openapi/swag/jsonutils v0.24.0 h1:F1vE1q4pg1xtO3HTyJYRmEuJ4jmIp2iZ30bzW5XgZts=
|
||||
github.com/go-openapi/swag/jsonutils v0.24.0/go.mod h1:vBowZtF5Z4DDApIoxcIVfR8v0l9oq5PpYRUuteVu6f0=
|
||||
github.com/go-openapi/swag/loading v0.24.0 h1:ln/fWTwJp2Zkj5DdaX4JPiddFC5CHQpvaBKycOlceYc=
|
||||
github.com/go-openapi/swag/loading v0.24.0/go.mod h1:gShCN4woKZYIxPxbfbyHgjXAhO61m88tmjy0lp/LkJk=
|
||||
github.com/go-openapi/swag/mangling v0.24.0 h1:PGOQpViCOUroIeak/Uj/sjGAq9LADS3mOyjznmHy2pk=
|
||||
github.com/go-openapi/swag/mangling v0.24.0/go.mod h1:Jm5Go9LHkycsz0wfoaBDkdc4CkpuSnIEf62brzyCbhc=
|
||||
github.com/go-openapi/swag/netutils v0.24.0 h1:Bz02HRjYv8046Ycg/w80q3g9QCWeIqTvlyOjQPDjD8w=
|
||||
github.com/go-openapi/swag/netutils v0.24.0/go.mod h1:WRgiHcYTnx+IqfMCtu0hy9oOaPR0HnPbmArSRN1SkZM=
|
||||
github.com/go-openapi/swag/stringutils v0.24.0 h1:i4Z/Jawf9EvXOLUbT97O0HbPUja18VdBxeadyAqS1FM=
|
||||
github.com/go-openapi/swag/stringutils v0.24.0/go.mod h1:5nUXB4xA0kw2df5PRipZDslPJgJut+NjL7D25zPZ/4w=
|
||||
github.com/go-openapi/swag/typeutils v0.24.0 h1:d3szEGzGDf4L2y1gYOSSLeK6h46F+zibnEas2Jm/wIw=
|
||||
github.com/go-openapi/swag/typeutils v0.24.0/go.mod h1:q8C3Kmk/vh2VhpCLaoR2MVWOGP8y7Jc8l82qCTd1DYI=
|
||||
github.com/go-openapi/swag/yamlutils v0.24.0 h1:bhw4894A7Iw6ne+639hsBNRHg9iZg/ISrOVr+sJGp4c=
|
||||
github.com/go-openapi/swag/yamlutils v0.24.0/go.mod h1:DpKv5aYuaGm/sULePoeiG8uwMpZSfReo1HR3Ik0yaG8=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
@@ -341,10 +369,10 @@ github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp4
|
||||
github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec=
|
||||
github.com/gohugoio/httpcache v0.7.0 h1:ukPnn04Rgvx48JIinZvZetBfHaWE7I01JR2Q2RrQ3Vs=
|
||||
github.com/gohugoio/httpcache v0.7.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI=
|
||||
github.com/gohugoio/hugo v0.147.6 h1:rL4rnus/5qzj4+FoA+JMzsVvFJ2YZdVIH6pbuCB2P84=
|
||||
github.com/gohugoio/hugo v0.147.6/go.mod h1:Sb2COQPDPYG+tRSpePtzKytiuVDqkBivEhgIew1QbNo=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.3.0 h1:gj49kTR5Z4Hnm0ZaQrgPVazL3DUkppw+x6XhHCmh+Wk=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.3.0/go.mod h1:IMMj7xiUbLt1YNJ6m7AM4cnsX4cFnnfkleO/lBHGzUg=
|
||||
github.com/gohugoio/hugo v0.149.1 h1:uWOc8Ve4h4e48FyYhBquRoHCJviyxA5yGrFJLT48yio=
|
||||
github.com/gohugoio/hugo v0.149.1/go.mod h1:HS6BP6e8FGxungP4CHC3zeLDvhBLnTJIjHJZWTZjs7o=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.5.0 h1:dco+7YiOryRoPOMXwwaf+kktZSCtlFtreNdiJbETvYE=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/extras v0.5.0/go.mod h1:CRrxQTKeM3imw+UoS4EHKyrqB7Zp6sAJiqHit+aMGTE=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1 h1:nUzXfRTszLliZuN0JTKeunXTRaiFX6ksaWP0puLLYAY=
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1/go.mod h1:Wy8ThAA8p2/w1DY05vEzq6EIeI2mzDjvHsu7ULBVwog=
|
||||
github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
|
||||
@@ -356,8 +384,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
|
||||
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20250825151631-54d70cc7cb31 h1:226lwSa0uZO7sd7QU88n43nDIqGoc+bOh0vSO3Q/byU=
|
||||
github.com/golang/geo v0.0.0-20250825151631-54d70cc7cb31/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
|
||||
github.com/golang/geo v0.0.0-20250912065020-b504328d3ef3 h1:vLpszxHK60Falbg9wHLAFI5LPqXlxnbvQRGxCJJHvLY=
|
||||
github.com/golang/geo v0.0.0-20250912065020-b504328d3ef3/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@@ -442,6 +470,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s=
|
||||
github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk=
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc=
|
||||
github.com/gostaticanalysis/comment v1.4.1/go.mod h1:ih6ZxzTHLdadaiSnF5WY3dxUoXfXAlTaRzuaNDlSado=
|
||||
@@ -579,6 +609,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0j
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mgechev/revive v1.11.0 h1:b/gLLpBE427o+Xmd8G58gSA+KtBwxWinH/A565Awh0w=
|
||||
github.com/mgechev/revive v1.11.0/go.mod h1:tI0oLF/2uj+InHCBLrrqfTKfjtFTBCFFfG05auyzgdw=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
|
||||
@@ -600,8 +632,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U=
|
||||
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
|
||||
github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek=
|
||||
github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o=
|
||||
github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
|
||||
github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
|
||||
github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg=
|
||||
github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs=
|
||||
github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk=
|
||||
@@ -612,8 +644,12 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J
|
||||
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
|
||||
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
|
||||
github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y=
|
||||
@@ -718,10 +754,10 @@ github.com/sonatard/noctx v0.4.0 h1:7MC/5Gg4SQ4lhLYR6mvOP6mQVSxCrdyiExo7atBs27o=
|
||||
github.com/sonatard/noctx v0.4.0/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas=
|
||||
github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0=
|
||||
github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk=
|
||||
github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
|
||||
@@ -744,8 +780,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
|
||||
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
@@ -756,10 +792,10 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/tdakkota/asciicheck v0.4.1 h1:bm0tbcmi0jezRA2b5kg4ozmMuGAFotKI3RZfrhfovg8=
|
||||
github.com/tdakkota/asciicheck v0.4.1/go.mod h1:0k7M3rCfRXb0Z6bwgvkEIMleKH3kXNz9UqJ9Xuqopr8=
|
||||
github.com/tdewolff/minify/v2 v2.23.5 h1:/P548KcpTkIOUvNg22zN83/GiaYSOIrbqtoue4I7kYM=
|
||||
github.com/tdewolff/minify/v2 v2.23.5/go.mod h1:2RI9tiIrzJU1Z5EasXEPaI1MqobRyxKHOOgrRkq5oEw=
|
||||
github.com/tdewolff/parse/v2 v2.8.1 h1:J5GSHru6o3jF1uLlEKVXkDxxcVx6yzOlIVIotK4w2po=
|
||||
github.com/tdewolff/parse/v2 v2.8.1/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/minify/v2 v2.24.2 h1:vnY3nTulEAbCAAlxTxPPDkzG24rsq31SOzp63yT+7mo=
|
||||
github.com/tdewolff/minify/v2 v2.24.2/go.mod h1:1JrCtoZXaDbqioQZfk3Jdmr0GPJKiU7c1Apmb+7tCeE=
|
||||
github.com/tdewolff/parse/v2 v2.8.3 h1:5VbvtJ83cfb289A1HzRA9sf02iT8YyUwN84ezjkdY1I=
|
||||
github.com/tdewolff/parse/v2 v2.8.3/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
|
||||
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/tenntenn/modver v1.0.1 h1:2klLppGhDgzJrScMpkj9Ujy3rXPUspSjAcev9tSEBgA=
|
||||
@@ -790,6 +826,8 @@ github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU=
|
||||
github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg=
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
|
||||
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
|
||||
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
|
||||
github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM=
|
||||
github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
@@ -807,8 +845,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo=
|
||||
github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||
gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo=
|
||||
@@ -848,8 +886,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -860,16 +898,16 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA=
|
||||
golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
|
||||
golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b h1:KdrhdYPDUvJTvrDK9gdjfFd6JTk8vA1WJoldYSi0kHo=
|
||||
golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b/go.mod h1:LKZHyeOpPuZcMgxeHjJp4p5yvxrCX1xDvH10zYHhjjQ=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
|
||||
golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
|
||||
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
|
||||
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -895,8 +933,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -939,16 +977,16 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -964,8 +1002,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1019,8 +1057,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -1037,13 +1075,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -1098,8 +1136,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
|
||||
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
|
||||
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
|
||||
@@ -1185,8 +1223,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -44,20 +44,9 @@ var (
|
||||
func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete chan struct{}) {
|
||||
store = storage
|
||||
config = &settings.Config
|
||||
|
||||
// Dev mode enables development features like template hot-reloading
|
||||
devMode := os.Getenv("FILEBROWSER_DEVMODE") == "true"
|
||||
_, err := os.Stat("http/dist")
|
||||
// In dev mode, always use filesystem assets. Otherwise, check if http/dist exists
|
||||
var embeddedFS bool
|
||||
if devMode {
|
||||
embeddedFS = false
|
||||
} else {
|
||||
embeddedFS = os.IsNotExist(err)
|
||||
}
|
||||
|
||||
var err error
|
||||
// --- START: ADD THIS DECRYPTION LOGIC ---
|
||||
if embeddedFS {
|
||||
if settings.Config.Server.EmbeddedFs {
|
||||
// Embedded mode: Serve files from the embedded assets
|
||||
assetFs, err = fs.Sub(assets, "embed")
|
||||
if err != nil {
|
||||
@@ -75,6 +64,8 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
|
||||
return string(a), err
|
||||
},
|
||||
})
|
||||
|
||||
devMode := os.Getenv("FILEBROWSER_DEVMODE") == "true"
|
||||
if !devMode {
|
||||
templates = template.Must(templates.ParseFS(assetFs, "public/index.html"))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -48,7 +49,13 @@ type handleFunc func(w http.ResponseWriter, r *http.Request, data *requestContex
|
||||
func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
return withOrWithoutUserHelper(func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
|
||||
hash := r.URL.Query().Get("hash")
|
||||
path := r.URL.Query().Get("path")
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
// Decode the URL-encoded path - use PathUnescape to preserve + as literal character
|
||||
path, err := url.PathUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
|
||||
// Get the file link by hash
|
||||
link, err := store.Share.GetByHash(hash)
|
||||
if err != nil {
|
||||
@@ -81,7 +88,8 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
}
|
||||
// Get file information with options
|
||||
getContent := r.URL.Query().Get("content") == "true"
|
||||
if link.DisableFileViewer || link.Downloads >= link.DownloadsLimit {
|
||||
reachedDownloadsLimit := link.Downloads >= link.DownloadsLimit && link.DownloadsLimit > 0
|
||||
if link.DisableFileViewer || reachedDownloadsLimit {
|
||||
getContent = false
|
||||
}
|
||||
file, err := FileInfoFasterFunc(iteminfo.FileOptions{
|
||||
@@ -98,7 +106,7 @@ func withHashFileHelper(fn handleFunc) handleFunc {
|
||||
file.Token = link.Token
|
||||
file.Source = ""
|
||||
file.Hash = link.Hash
|
||||
if !link.EnableOnlyOffice || !link.DisableFileViewer {
|
||||
if !link.EnableOnlyOffice || !link.DisableFileViewer || reachedDownloadsLimit {
|
||||
file.OnlyOfficeId = ""
|
||||
}
|
||||
if getContent && file.Content != "" {
|
||||
|
||||
+40
-37
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -49,7 +50,21 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required")
|
||||
}
|
||||
themeMode := utils.Ternary(d.user.DarkMode, "dark", "light")
|
||||
|
||||
var sourceInfo settings.Source
|
||||
var ok bool
|
||||
if d.fileInfo.Hash != "" {
|
||||
sourceInfo, ok = settings.Config.Server.SourceMap[source]
|
||||
if !ok {
|
||||
logger.Error("OnlyOffice: source not found")
|
||||
return http.StatusInternalServerError, fmt.Errorf("source not found")
|
||||
}
|
||||
} else {
|
||||
sourceInfo, ok = settings.Config.Server.NameToSource[source]
|
||||
if !ok {
|
||||
logger.Error("OnlyOffice: source not found")
|
||||
return http.StatusInternalServerError, fmt.Errorf("source not found")
|
||||
}
|
||||
}
|
||||
if d.fileInfo.Hash == "" {
|
||||
// Build file info based on whether this is a share or regular request
|
||||
// Regular user request - need to resolve scope
|
||||
@@ -58,27 +73,20 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
logger.Errorf("OnlyOffice: source %s not available for user %s: %v", source, d.user.Username, scopeErr)
|
||||
return http.StatusForbidden, fmt.Errorf("source %s is not available", source)
|
||||
}
|
||||
resolvedPath := utils.JoinPathAsUnix(userScope, path)
|
||||
logger.Debugf("OnlyOffice user request: resolved path=%s", resolvedPath)
|
||||
|
||||
indexPath := utils.JoinPathAsUnix(userScope, path)
|
||||
logger.Debugf("OnlyOffice user request: resolved path=%s", indexPath)
|
||||
fileInfo, err := files.FileInfoFaster(iteminfo.FileOptions{
|
||||
Path: resolvedPath,
|
||||
Path: indexPath,
|
||||
Modify: d.user.Permissions.Modify,
|
||||
Source: source,
|
||||
Expand: false,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Errorf("OnlyOffice: failed to get file info for source=%s, path=%s: %v", source, resolvedPath, err)
|
||||
logger.Errorf("OnlyOffice: failed to get file info for source=%s, path=%s: %v", source, indexPath, err)
|
||||
return errToStatus(err), err
|
||||
}
|
||||
d.fileInfo = *fileInfo
|
||||
} else {
|
||||
// is a share, use the file info from the share middleware
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source]
|
||||
if !ok {
|
||||
logger.Error("OnlyOffice: source from share not found")
|
||||
return http.StatusInternalServerError, fmt.Errorf("source not found for share")
|
||||
}
|
||||
source = sourceInfo.Name
|
||||
// path is index path, so we build from share path
|
||||
path = utils.JoinPathAsUnix(d.share.Path, path)
|
||||
@@ -92,7 +100,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
}
|
||||
|
||||
// Determine file type and editing permissions
|
||||
fileType := getFileExtension(d.fileInfo.Name)
|
||||
fileType := strings.TrimPrefix(filepath.Ext(d.fileInfo.Name), ".")
|
||||
canEdit := iteminfo.CanEditOnlyOffice(d.user.Permissions.Modify, fileType)
|
||||
canEditMode := utils.Ternary(canEdit, "edit", "view")
|
||||
if d.fileInfo.Hash != "" {
|
||||
@@ -111,7 +119,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
}
|
||||
|
||||
// Generate document ID for OnlyOffice
|
||||
documentId, err := getOnlyOfficeId(source, path)
|
||||
documentId, err := getOnlyOfficeId(d.fileInfo.RealPath)
|
||||
if err != nil {
|
||||
logger.Errorf("OnlyOffice: failed to generate document ID for source=%s, path=%s: %v", source, path, err)
|
||||
return http.StatusNotFound, fmt.Errorf("failed to generate document ID: %v", err)
|
||||
@@ -131,7 +139,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
"title": d.fileInfo.Name,
|
||||
"url": downloadURL,
|
||||
"permissions": map[string]interface{}{
|
||||
"edit": canEditMode,
|
||||
"edit": utils.Ternary(settings.Config.Integrations.OnlyOffice.ViewOnly, "view", canEditMode),
|
||||
"download": true,
|
||||
"print": true,
|
||||
},
|
||||
@@ -148,7 +156,7 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
"uiTheme": themeMode,
|
||||
},
|
||||
"lang": d.user.Locale,
|
||||
"mode": canEditMode,
|
||||
"mode": utils.Ternary(settings.Config.Integrations.OnlyOffice.ViewOnly, "view", canEditMode),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -166,15 +174,6 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
|
||||
return renderJSON(w, r, clientConfig)
|
||||
}
|
||||
|
||||
// getFileExtension extracts file extension from filename
|
||||
func getFileExtension(filename string) string {
|
||||
parts := strings.Split(filename, ".")
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
|
||||
// buildOnlyOfficeDownloadURL constructs the download URL that OnlyOffice server will use to fetch the file
|
||||
func buildOnlyOfficeDownloadURL(source, path, hash, token string) string {
|
||||
// Determine base URL (internal URL takes priority for OnlyOffice server communication)
|
||||
@@ -254,6 +253,21 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, path)
|
||||
return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required")
|
||||
}
|
||||
var sourceInfo settings.Source
|
||||
var ok bool
|
||||
if d.fileInfo.Hash != "" {
|
||||
sourceInfo, ok = settings.Config.Server.SourceMap[source]
|
||||
if !ok {
|
||||
logger.Error("OnlyOffice: source not found")
|
||||
return http.StatusInternalServerError, fmt.Errorf("source not found")
|
||||
}
|
||||
} else {
|
||||
sourceInfo, ok = settings.Config.Server.NameToSource[source]
|
||||
if !ok {
|
||||
logger.Error("OnlyOffice: source not found")
|
||||
return http.StatusInternalServerError, fmt.Errorf("source not found")
|
||||
}
|
||||
}
|
||||
|
||||
if d.fileInfo.Hash == "" {
|
||||
// Regular user request - need to resolve scope
|
||||
@@ -264,12 +278,6 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
}
|
||||
path = utils.JoinPathAsUnix(userScope, path)
|
||||
} else {
|
||||
// is a share, use the file info from the share middleware
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source]
|
||||
if !ok {
|
||||
logger.Error("OnlyOffice: source from share not found")
|
||||
return http.StatusInternalServerError, fmt.Errorf("source not found for share")
|
||||
}
|
||||
source = sourceInfo.Name
|
||||
// path is index path, so we build from share path
|
||||
path = utils.JoinPathAsUnix(d.share.Path, path)
|
||||
@@ -339,12 +347,7 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
|
||||
return renderJSON(w, r, resp)
|
||||
}
|
||||
|
||||
func getOnlyOfficeId(source, path string) (string, error) {
|
||||
idx := indexing.GetIndex(source)
|
||||
if idx == nil {
|
||||
return "", fmt.Errorf("source not found")
|
||||
}
|
||||
realpath, _, _ := idx.GetRealPath(path)
|
||||
func getOnlyOfficeId(realpath string) (string, error) {
|
||||
// error is intentionally ignored in order treat errors
|
||||
// the same as a cache-miss
|
||||
cachedDocumentKey, ok := utils.OnlyOfficeCache.Get(realpath).(string)
|
||||
|
||||
+36
-6
@@ -48,7 +48,7 @@ func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
source := r.URL.Query().Get("source")
|
||||
var err error
|
||||
// decode url encoded source name
|
||||
source, err = url.QueryUnescape(source)
|
||||
source, err = url.PathUnescape(source)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid source encoding: %v", err)
|
||||
}
|
||||
@@ -63,9 +63,8 @@ func previewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (
|
||||
Access: store.Access,
|
||||
Username: d.user.Username,
|
||||
Path: utils.JoinPathAsUnix(userscope, path),
|
||||
Modify: d.user.Permissions.Modify,
|
||||
Source: source,
|
||||
Expand: true,
|
||||
Content: true,
|
||||
})
|
||||
if err != nil {
|
||||
return errToStatus(err), err
|
||||
@@ -98,15 +97,46 @@ func previewHelperFunc(w http.ResponseWriter, r *http.Request, d *requestContext
|
||||
if !(previewSize == "large" || previewSize == "original") {
|
||||
previewSize = "small"
|
||||
}
|
||||
if d.fileInfo.Type == "directory" {
|
||||
return http.StatusBadRequest, fmt.Errorf("can't create preview for directory")
|
||||
if d.fileInfo.Type == "directory" && !d.fileInfo.HasPreview {
|
||||
return http.StatusBadRequest, fmt.Errorf("this folder does not have a preview")
|
||||
}
|
||||
if d.fileInfo.Type == "directory" {
|
||||
// get extended file info of first previewable item in directory
|
||||
for _, item := range d.fileInfo.Files {
|
||||
if item.HasPreview {
|
||||
source := d.fileInfo.Source
|
||||
path := utils.JoinPathAsUnix(d.fileInfo.Path, item.Name)
|
||||
if d.share != nil {
|
||||
// Get the actual source name from the share's source mapping
|
||||
sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source]
|
||||
if !ok {
|
||||
return http.StatusInternalServerError, fmt.Errorf("source not found for share")
|
||||
}
|
||||
source = sourceInfo.Name
|
||||
path = utils.JoinPathAsUnix(d.share.Path, path)
|
||||
}
|
||||
fileInfo, err := files.FileInfoFaster(
|
||||
iteminfo.FileOptions{
|
||||
Path: path,
|
||||
Source: source,
|
||||
Content: true,
|
||||
})
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
d.fileInfo = *fileInfo
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setContentDisposition(w, r, d.fileInfo.Name)
|
||||
isImage := strings.HasPrefix(d.fileInfo.Type, "image")
|
||||
if config.Server.DisableResize && isImage {
|
||||
return rawFileHandler(w, r, d.fileInfo)
|
||||
}
|
||||
if !preview.AvailablePreview(d.fileInfo) {
|
||||
|
||||
if !d.fileInfo.HasPreview {
|
||||
if isImage {
|
||||
return rawFileHandler(w, r, d.fileInfo)
|
||||
}
|
||||
|
||||
@@ -116,13 +116,13 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
return http.StatusNotFound, fmt.Errorf("source %s not found", source)
|
||||
}
|
||||
realPath, _, _ := idx.GetRealPath(userscope, path)
|
||||
checksums, err := files.GetChecksum(realPath, algo)
|
||||
checksum, err := utils.GetChecksum(realPath, algo)
|
||||
if err == errors.ErrInvalidOption {
|
||||
return http.StatusBadRequest, nil
|
||||
} else if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
fileInfo.Checksums = checksums
|
||||
fileInfo.Checksums[algo] = checksum
|
||||
}
|
||||
return renderJSON(w, r, fileInfo)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
@@ -54,16 +55,27 @@ func settingsConfigHandler(w http.ResponseWriter, r *http.Request, d *requestCon
|
||||
|
||||
showFull := fullParam == "true"
|
||||
showComments := commentsParam == "true"
|
||||
|
||||
// Generate YAML using the existing generator function (don't filter deprecated fields in API)
|
||||
yamlOutput, err := settings.GenerateConfigYaml(config, showComments, showFull, false)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
var err error
|
||||
var yamlConfig string
|
||||
if config.Server.EmbeddedFs {
|
||||
embeddedYaml, readErr := assets.ReadFile("embed/config.generated.yaml")
|
||||
if readErr != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("error reading embedded YAML: %v", readErr)
|
||||
}
|
||||
yamlConfig, err = settings.GenerateConfigYamlWithEmbedded(config, showComments, showFull, false, string(embeddedYaml))
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("error generating YAML: %v", err)
|
||||
}
|
||||
} else {
|
||||
yamlConfig, err = settings.GenerateConfigYaml(config, showComments, showFull, false)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("error generating YAML: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set content type and write response
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
if _, err := w.Write([]byte(yamlOutput)); err != nil {
|
||||
if _, err := w.Write([]byte(yamlConfig)); err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ func shareGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
|
||||
encodedPath := r.URL.Query().Get("path")
|
||||
sourceName := r.URL.Query().Get("source")
|
||||
// Decode the URL-encoded path
|
||||
path, err := url.QueryUnescape(encodedPath)
|
||||
path, err := url.PathUnescape(encodedPath)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
|
||||
}
|
||||
|
||||
+38
-25
@@ -56,7 +56,9 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
|
||||
defaultThemeColor := "#455a64"
|
||||
staticURL := config.Server.BaseURL + "static"
|
||||
publicStaticURL := config.Server.BaseURL + "public/static"
|
||||
if d.share != nil {
|
||||
staticURL = config.Server.BaseURL + "public/static"
|
||||
}
|
||||
|
||||
// Use custom favicon if configured and validated, otherwise fall back to default
|
||||
var favicon string
|
||||
@@ -66,20 +68,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
// Default favicon
|
||||
favicon = staticURL + "/img/icons/favicon-256x256.png"
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"title": config.Frontend.Name,
|
||||
"customCSS": config.Frontend.Styling.CustomCSSRaw,
|
||||
"userSelectedTheme": userSelectedTheme,
|
||||
"lightBackground": config.Frontend.Styling.LightBackground,
|
||||
"darkBackground": config.Frontend.Styling.DarkBackground,
|
||||
"staticURL": staticURL,
|
||||
"baseURL": config.Server.BaseURL,
|
||||
"favicon": favicon,
|
||||
"color": defaultThemeColor,
|
||||
"winIcon": staticURL + "/img/icons/mstile-144x144.png",
|
||||
"appIcon": staticURL + "/img/icons/android-chrome-256x256.png",
|
||||
"description": config.Frontend.Description,
|
||||
}
|
||||
data := make(map[string]interface{})
|
||||
shareProps := map[string]interface{}{
|
||||
"isShare": false,
|
||||
"isValid": false,
|
||||
@@ -117,13 +106,14 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
shareProps["isPasswordProtected"] = d.share.PasswordHash != ""
|
||||
shareProps["downloadURL"] = getDownloadURL(r, d.share.Hash)
|
||||
shareProps["enforceDarkLightMode"] = d.share.EnforceDarkLightMode
|
||||
shareProps["viewMode"] = d.share.ViewMode
|
||||
shareProps["enableOnlyOffice"] = d.share.EnableOnlyOffice
|
||||
shareProps["enableOnlyOfficeEditing"] = d.share.EnableOnlyOfficeEditing
|
||||
if d.share.Favicon != "" {
|
||||
if strings.HasPrefix(d.share.Favicon, "http") {
|
||||
data["favicon"] = d.share.Favicon
|
||||
} else {
|
||||
data["favicon"] = publicStaticURL + "/" + d.share.Favicon
|
||||
data["favicon"] = staticURL + "/" + d.share.Favicon
|
||||
}
|
||||
}
|
||||
if d.share.Description != "" {
|
||||
@@ -135,15 +125,29 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
}
|
||||
|
||||
// base url could be different for routes behind proxy
|
||||
data["staticURL"] = publicStaticURL
|
||||
data["staticURL"] = staticURL
|
||||
// Use custom favicon for shares too if configured
|
||||
if config.Frontend.Favicon != "" {
|
||||
data["favicon"] = publicStaticURL + "/favicon"
|
||||
data["favicon"] = staticURL + "/favicon"
|
||||
} else {
|
||||
data["favicon"] = publicStaticURL + "/img/icons/favicon-256x256.png"
|
||||
data["favicon"] = staticURL + "/img/icons/favicon-256x256.png"
|
||||
}
|
||||
data["winIcon"] = publicStaticURL + "/img/icons/mstile-144x144.png"
|
||||
data["appIcon"] = publicStaticURL + "/img/icons/android-chrome-256x256.png"
|
||||
data["winIcon"] = staticURL + "/img/icons/mstile-144x144.png"
|
||||
data["appIcon"] = staticURL + "/img/icons/android-chrome-256x256.png"
|
||||
}
|
||||
data["htmlVars"] = map[string]interface{}{
|
||||
"title": config.Frontend.Name,
|
||||
"customCSS": config.Frontend.Styling.CustomCSSRaw,
|
||||
"userSelectedTheme": userSelectedTheme,
|
||||
"lightBackground": config.Frontend.Styling.LightBackground,
|
||||
"darkBackground": config.Frontend.Styling.DarkBackground,
|
||||
"staticURL": staticURL,
|
||||
"baseURL": config.Server.BaseURL,
|
||||
"favicon": favicon,
|
||||
"color": defaultThemeColor,
|
||||
"winIcon": staticURL + "/img/icons/mstile-144x144.png",
|
||||
"appIcon": staticURL + "/img/icons/android-chrome-256x256.png",
|
||||
"description": config.Frontend.Description,
|
||||
}
|
||||
// variables consumed by frontend as json
|
||||
data["globalVars"] = map[string]interface{}{
|
||||
@@ -169,13 +173,22 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
|
||||
"updateAvailable": utils.GetUpdateAvailableUrl(),
|
||||
"disableNavButtons": disableNavButtons,
|
||||
"userSelectableThemes": config.Frontend.Styling.CustomThemeOptions,
|
||||
"share": shareProps,
|
||||
"enableHeicConversion": config.Integrations.Media.Convert.ImagePreview[settings.HEICImagePreview],
|
||||
}
|
||||
jsonVars, err := json.Marshal(data["globalVars"])
|
||||
|
||||
// Marshal each variable to JSON strings for direct template usage
|
||||
globalVarsJSON, err := json.Marshal(data["globalVars"])
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
return http.StatusInternalServerError, fmt.Errorf("error marshaling globalVars: %w", err)
|
||||
}
|
||||
data["globalVars"] = strings.ReplaceAll(string(jsonVars), `'`, `\'`)
|
||||
shareVarsJSON, err := json.Marshal(shareProps)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, fmt.Errorf("error marshaling shareVars: %w", err)
|
||||
}
|
||||
|
||||
// Replace with JSON strings for direct template usage
|
||||
data["globalVars"] = string(globalVarsJSON)
|
||||
data["shareVars"] = string(shareVarsJSON)
|
||||
|
||||
// Render the template with global variables
|
||||
if err := templateRenderer.Render(w, file, data); err != nil {
|
||||
|
||||
@@ -65,6 +65,9 @@ const (
|
||||
UNAVAILABLE IndexStatus = "unavailable"
|
||||
)
|
||||
|
||||
// omitList contains directory names to skip during indexing
|
||||
var omitList = []string{"$RECYCLE.BIN", "System Volume Information", "@eaDir"}
|
||||
|
||||
func init() {
|
||||
indexes = make(map[string]*Index)
|
||||
}
|
||||
@@ -194,7 +197,7 @@ func (idx *Index) GetFsDirInfo(adjustedPath string) (*iteminfo.FileInfo, error)
|
||||
ModTime: dirInfo.ModTime(),
|
||||
},
|
||||
}
|
||||
fileInfo.DetectType(adjustedPath, false)
|
||||
fileInfo.DetectType(realPath, false)
|
||||
return &fileInfo, nil
|
||||
}
|
||||
combinedPath := adjustedPath + "/"
|
||||
@@ -238,6 +241,13 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
var totalSize int64
|
||||
fileInfos := []iteminfo.ItemInfo{}
|
||||
dirInfos := []iteminfo.ItemInfo{}
|
||||
hasPreview := false
|
||||
if !recursive {
|
||||
realDirInfo, exists := idx.GetMetadataInfo(adjustedPath, true)
|
||||
if exists {
|
||||
hasPreview = realDirInfo.HasPreview
|
||||
}
|
||||
}
|
||||
|
||||
// Process each file and directory in the current directory
|
||||
for _, file := range files {
|
||||
@@ -271,7 +281,7 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
}
|
||||
}
|
||||
// skip non-indexable dirs.
|
||||
if file.Name() == "$RECYCLE.BIN" || file.Name() == "System Volume Information" {
|
||||
if slices.Contains(omitList, file.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -288,6 +298,7 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
realDirInfo, exists := idx.GetMetadataInfo(dirPath, true)
|
||||
if exists {
|
||||
itemInfo.Size = realDirInfo.Size
|
||||
itemInfo.HasPreview = realDirInfo.HasPreview
|
||||
}
|
||||
totalSize += itemInfo.Size
|
||||
itemInfo.Type = "directory"
|
||||
@@ -297,7 +308,35 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
}
|
||||
} else {
|
||||
size, shouldCountSize := idx.handleFile(file, fullCombined)
|
||||
itemInfo.DetectType(fullCombined, false)
|
||||
itemInfo.DetectType(realPath+"/"+file.Name(), false)
|
||||
simpleType := strings.Split(itemInfo.Type, "/")[0]
|
||||
if simpleType == "audio" {
|
||||
if recursive && !quick && idx.Config.IndexAlbumArt {
|
||||
itemInfo.HasPreview = iteminfo.HasAlbumArt(realPath+"/"+file.Name(), filepath.Ext(file.Name()))
|
||||
}
|
||||
if !recursive && !itemInfo.HasPreview {
|
||||
info, exists := idx.GetReducedMetadata(fullCombined, false)
|
||||
if exists {
|
||||
itemInfo.HasPreview = info.HasPreview
|
||||
}
|
||||
}
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(file.Name()))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg", ".png", ".bmp", ".tiff":
|
||||
itemInfo.HasPreview = true
|
||||
case ".heic", ".heif":
|
||||
if settings.Config.Integrations.Media.FfmpegPath != "" {
|
||||
itemInfo.HasPreview = true
|
||||
}
|
||||
}
|
||||
if simpleType == "image" {
|
||||
itemInfo.HasPreview = true
|
||||
}
|
||||
if settings.Config.Integrations.Media.FfmpegPath != "" && simpleType == "video" {
|
||||
itemInfo.HasPreview = true
|
||||
}
|
||||
|
||||
itemInfo.Size = int64(size)
|
||||
fileInfos = append(fileInfos, *itemInfo)
|
||||
if shouldCountSize {
|
||||
@@ -306,6 +345,16 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
if recursive {
|
||||
idx.NumFiles++
|
||||
}
|
||||
if itemInfo.HasPreview {
|
||||
hasPreview = true
|
||||
}
|
||||
// these don't create preview for parent folders
|
||||
if settings.Config.Integrations.OnlyOffice.Secret != "" && iteminfo.IsOnlyOffice(file.Name()) {
|
||||
itemInfo.HasPreview = true
|
||||
}
|
||||
if iteminfo.HasDocConvertableExtension(itemInfo.Name, itemInfo.Type) {
|
||||
itemInfo.HasPreview = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,10 +375,11 @@ func (idx *Index) GetDirInfo(dirInfo *os.File, stat os.FileInfo, realPath, adjus
|
||||
Folders: dirInfos,
|
||||
}
|
||||
dirFileInfo.ItemInfo = iteminfo.ItemInfo{
|
||||
Name: filepath.Base(dirInfo.Name()),
|
||||
Type: "directory",
|
||||
Size: totalSize,
|
||||
ModTime: stat.ModTime(),
|
||||
Name: filepath.Base(dirInfo.Name()),
|
||||
Type: "directory",
|
||||
Size: totalSize,
|
||||
ModTime: stat.ModTime(),
|
||||
HasPreview: hasPreview,
|
||||
}
|
||||
dirFileInfo.SortItems()
|
||||
return dirFileInfo, nil
|
||||
@@ -488,6 +538,13 @@ func (idx *Index) shouldSkip(isDir bool, isHidden bool, fullCombined, baseName s
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rules.FolderStartsWith) > 0 {
|
||||
for _, start := range rules.FolderStartsWith {
|
||||
if strings.HasPrefix(baseName, start) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(rules.FilePaths) > 0 {
|
||||
for _, p := range rules.FilePaths {
|
||||
@@ -506,6 +563,13 @@ func (idx *Index) shouldSkip(isDir bool, isHidden bool, fullCombined, baseName s
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rules.FileStartsWith) > 0 {
|
||||
for _, start := range rules.FileStartsWith {
|
||||
if strings.HasPrefix(baseName, start) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Exclude if parent directory matches FolderPaths
|
||||
if len(rules.FolderPaths) > 0 {
|
||||
parent := filepath.Dir(fullCombined)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/dhowden/tag"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
)
|
||||
|
||||
@@ -32,21 +33,21 @@ var SubtitleExts = []string{
|
||||
".smi",
|
||||
}
|
||||
|
||||
var MuPdfConvertable = []string{
|
||||
".pdf", // PDF
|
||||
".xps", // XPS
|
||||
".epub", // EPUB
|
||||
".mobi", // MOBI
|
||||
".fb2", // FB2
|
||||
".cbz", // CBZ
|
||||
".svg", // SVG
|
||||
".txt", // TXT
|
||||
".docx", // DOCX
|
||||
".pptx", // PPTX
|
||||
".xlsx", // XLSX
|
||||
".hwp", // HWP
|
||||
".hwp", // HWPX
|
||||
".md", // Markdown
|
||||
var MuPdfConvertable = map[string]bool{
|
||||
".pdf": true, // PDF
|
||||
".xps": true, // XPS
|
||||
".epub": true, // EPUB
|
||||
".mobi": true, // MOBI
|
||||
".fb2": true, // FB2
|
||||
".cbz": true, // CBZ
|
||||
".svg": true, // SVG
|
||||
".txt": true, // TXT
|
||||
".docx": true, // DOCX
|
||||
".pptx": true, // PPTX
|
||||
".xlsx": true, // XLSX
|
||||
".hwp": true, // HWP
|
||||
".hwpx": true, // HWPX
|
||||
".md": true, // Markdown
|
||||
}
|
||||
|
||||
// Known bundle-style extensions that are technically directories but treated as files
|
||||
@@ -184,6 +185,26 @@ var compressedFile = []string{
|
||||
".zstd",
|
||||
}
|
||||
|
||||
var audioMetadataTypes = []string{
|
||||
".mp3",
|
||||
".flac",
|
||||
".ogg",
|
||||
".m4a",
|
||||
".mp4",
|
||||
".wav",
|
||||
".ape",
|
||||
".wv",
|
||||
}
|
||||
|
||||
func CouldHaveAlbumArt(extension string) bool {
|
||||
for _, typefile := range audioMetadataTypes {
|
||||
if extension == typefile {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func ExtendedMimeTypeCheck(extension string) string {
|
||||
if IsDoc(extension) {
|
||||
return "application/document"
|
||||
@@ -267,14 +288,19 @@ func IsMatchingType(extension string, matchType string) bool {
|
||||
// DetectType detects the MIME type of a file and updates the ItemInfo struct.
|
||||
func (i *ItemInfo) DetectType(realPath string, saveContent bool) {
|
||||
name := i.Name
|
||||
ext := filepath.Ext(name)
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
|
||||
// Attempt MIME detection by file extension
|
||||
if ext == ".md" {
|
||||
switch ext {
|
||||
case ".md":
|
||||
i.Type = "text/markdown"
|
||||
return
|
||||
case ".heic", ".heif":
|
||||
i.Type = "image/heic"
|
||||
return
|
||||
}
|
||||
i.Type = strings.Split(mime.TypeByExtension(ext), ";")[0]
|
||||
|
||||
if i.Type == "" {
|
||||
i.Type = ExtendedMimeTypeCheck(ext)
|
||||
}
|
||||
@@ -285,10 +311,40 @@ func (i *ItemInfo) DetectType(realPath string, saveContent bool) {
|
||||
i.Type = DetectTypeByHeader(realPath)
|
||||
return
|
||||
}
|
||||
if i.Type == "blob" {
|
||||
if i.Type == "blob" || i.Type == "" {
|
||||
i.Type = DetectTypeByHeader(realPath)
|
||||
}
|
||||
}
|
||||
if i.Type == "" || i.Type == "application/octet-stream" {
|
||||
i.Type = "blob"
|
||||
}
|
||||
}
|
||||
|
||||
// hasAlbumArt efficiently checks if an audio file contains embedded album art.
|
||||
// It returns true if album art is found, and false otherwise.
|
||||
func HasAlbumArt(filePath string, extension string) bool {
|
||||
if !CouldHaveAlbumArt(extension) {
|
||||
return false
|
||||
}
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read metadata from the file.
|
||||
m, err := tag.ReadFrom(file)
|
||||
if err != nil {
|
||||
// If the error is simply that no tags were found, it's not a processing error.
|
||||
// It just means there's no album art.
|
||||
if err == tag.ErrNoTagsFound {
|
||||
return false
|
||||
}
|
||||
// For any other error (e.g., corrupted file), return the error.
|
||||
return false
|
||||
}
|
||||
// m.Picture() returns the first picture found. If it's not nil, album art exists.
|
||||
return m.Picture() != nil
|
||||
}
|
||||
|
||||
// DetectTypeByHeader detects the MIME type of a file based on its header.
|
||||
@@ -406,10 +462,9 @@ func HasDocConvertableExtension(name, mimetype string) bool {
|
||||
return true
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
for _, e := range MuPdfConvertable {
|
||||
if ext == e {
|
||||
return true
|
||||
}
|
||||
val, ok := MuPdfConvertable[ext]
|
||||
if ok {
|
||||
return val
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
)
|
||||
|
||||
type ItemInfo struct {
|
||||
Name string `json:"name"` // name of the file
|
||||
Size int64 `json:"size"` // length in bytes for regular files
|
||||
ModTime time.Time `json:"modified"` // modification time
|
||||
Type string `json:"type"` // type of the file, either "directory" or a file mimetype
|
||||
Hidden bool `json:"hidden"` // whether the file is hidden
|
||||
Name string `json:"name"` // name of the file
|
||||
Size int64 `json:"size"` // length in bytes for regular files
|
||||
ModTime time.Time `json:"modified"` // modification time
|
||||
Type string `json:"type"` // type of the file, either "directory" or a file mimetype
|
||||
Hidden bool `json:"hidden"` // whether the file is hidden
|
||||
HasPreview bool `json:"hasPreview"` // whether the file has a thumbnail preview
|
||||
}
|
||||
|
||||
// FileInfo describes a file.
|
||||
@@ -25,12 +26,25 @@ type FileInfo struct {
|
||||
Path string `json:"path"` // path scoped to the associated index
|
||||
}
|
||||
|
||||
// AudioMetadata contains metadata extracted from audio files
|
||||
type AudioMetadata struct {
|
||||
Title string `json:"title,omitempty"` // track title
|
||||
Artist string `json:"artist,omitempty"` // track artist
|
||||
Album string `json:"album,omitempty"` // album name
|
||||
Year int `json:"year,omitempty"` // release year
|
||||
Genre string `json:"genre,omitempty"` // music genre
|
||||
Track int `json:"track,omitempty"` // track number
|
||||
Duration int `json:"duration,omitempty"` // duration in seconds
|
||||
AlbumArt string `json:"albumArt,omitempty"` // base64 encoded album art
|
||||
}
|
||||
|
||||
// for efficiency, a response will be a pointer to the data
|
||||
// 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
|
||||
AudioMeta *AudioMetadata `json:"audioMeta,omitempty"` // audio metadata for audio files
|
||||
Checksums map[string]string `json:"checksums,omitempty"` // checksums for the file
|
||||
Token string `json:"token,omitempty"` // token for the file -- used for sharing
|
||||
OnlyOfficeId string `json:"onlyOfficeId,omitempty"` // id for onlyoffice files
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package preview
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/ffmpeg"
|
||||
)
|
||||
|
||||
// convertHEICToJPEGWithFFmpeg converts a HEIC file to JPEG format using FFmpeg
|
||||
// This function handles all FFmpeg-related logic and parameters
|
||||
func (s *Service) convertHEICToJPEGWithFFmpeg(filePath string, previewSize string) ([]byte, error) {
|
||||
// Create FFmpeg image service
|
||||
imageService := ffmpeg.NewImageService(s.ffmpegPath, s.ffprobePath, s.debug, filepath.Join(settings.Config.Server.CacheDir, "heic", filepath.Base(filePath)))
|
||||
// Determine target dimensions and quality based on preview size
|
||||
var width, height int
|
||||
var quality string
|
||||
switch previewSize {
|
||||
case "large":
|
||||
width, height = 640, 640
|
||||
quality = "2" // High quality for FFmpeg -q:v
|
||||
case "original":
|
||||
// For original size - no scaling, maximum quality
|
||||
width, height = 0, 0 // Signal to not apply scaling
|
||||
quality = "1" // Maximum quality for original
|
||||
default:
|
||||
width, height = 256, 256
|
||||
quality = "5" // Medium quality
|
||||
}
|
||||
// Use tile-based conversion for correct full-resolution image reconstruction
|
||||
result, err := imageService.ConvertHEICToJPEG(filePath, width, height, quality)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
//go:generate go-enum --sql --marshal --file $GOFILE
|
||||
package preview
|
||||
|
||||
import (
|
||||
@@ -8,9 +7,9 @@ import (
|
||||
"image"
|
||||
"io"
|
||||
|
||||
"github.com/kovidgoyal/imaging"
|
||||
exif "github.com/dsoprea/go-exif/v3"
|
||||
exifcommon "github.com/dsoprea/go-exif/v3/common"
|
||||
"github.com/kovidgoyal/imaging"
|
||||
)
|
||||
|
||||
// Format is an image file format.
|
||||
@@ -21,6 +20,7 @@ png
|
||||
gif
|
||||
tiff
|
||||
bmp
|
||||
heic
|
||||
)
|
||||
*/
|
||||
type Format int
|
||||
@@ -37,6 +37,8 @@ func (x Format) toImaging() imaging.Format {
|
||||
return imaging.TIFF
|
||||
case FormatBmp:
|
||||
return imaging.BMP
|
||||
case FormatHeic:
|
||||
return imaging.JPEG
|
||||
default:
|
||||
return imaging.JPEG
|
||||
}
|
||||
@@ -73,6 +75,12 @@ fill
|
||||
type ResizeMode int
|
||||
|
||||
func (s *Service) FormatFromExtension(ext string) (Format, error) {
|
||||
// heic is not supported by imaging, so we return FormatHeic
|
||||
switch ext {
|
||||
case ".heic", ".heif":
|
||||
return FormatHeic, nil
|
||||
}
|
||||
|
||||
format, err := imaging.FormatFromExtension(ext)
|
||||
if err != nil {
|
||||
return -1, ErrUnsupportedFormat
|
||||
@@ -139,6 +147,10 @@ func (s *Service) Resize(in io.Reader, width, height int, out io.Writer, options
|
||||
option(&config)
|
||||
}
|
||||
|
||||
if format == FormatHeic {
|
||||
config.format = FormatJpeg
|
||||
}
|
||||
|
||||
if config.quality == QualityLow && format == FormatJpeg {
|
||||
thm, newWrappedReader, errThm := getEmbeddedThumbnail(wrappedReader)
|
||||
wrappedReader = newWrappedReader
|
||||
@@ -150,16 +162,19 @@ func (s *Service) Resize(in io.Reader, width, height int, out io.Writer, options
|
||||
}
|
||||
}
|
||||
|
||||
img, err := imaging.Decode(wrappedReader, imaging.AutoOrientation(true))
|
||||
// For HEIC files, try without AutoOrientation first since it might not work properly
|
||||
img, err := imaging.Decode(wrappedReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Note: For HEIC files processed via FFmpeg, orientation is handled automatically
|
||||
|
||||
switch config.resizeMode {
|
||||
case ResizeModeFill:
|
||||
img = imaging.Fill(img, width, height, imaging.Center, config.quality.resampleFilter())
|
||||
case ResizeModeFit:
|
||||
fallthrough
|
||||
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
|
||||
default:
|
||||
img = imaging.Fit(img, width, height, config.quality.resampleFilter())
|
||||
}
|
||||
@@ -176,6 +191,10 @@ func (s *Service) detectFormat(in io.Reader) (Format, io.Reader, error) {
|
||||
return 0, nil, fmt.Errorf("%s: %w", err.Error(), ErrUnsupportedFormat)
|
||||
}
|
||||
|
||||
if imgFormat == "heif" {
|
||||
imgFormat = "heic"
|
||||
}
|
||||
|
||||
format, err := ParseFormat(imgFormat)
|
||||
if err != nil {
|
||||
return 0, nil, ErrUnsupportedFormat
|
||||
|
||||
@@ -19,16 +19,19 @@ const (
|
||||
FormatTiff
|
||||
// FormatBmp is a Format of type Bmp
|
||||
FormatBmp
|
||||
// FormatHeic is a Format of type Heic
|
||||
FormatHeic
|
||||
)
|
||||
|
||||
const _FormatName = "jpegpnggiftiffbmp"
|
||||
const _FormatName = "jpegpnggiftiffbmpheic"
|
||||
|
||||
var _FormatMap = map[Format]string{
|
||||
0: _FormatName[0:4],
|
||||
1: _FormatName[4:7],
|
||||
2: _FormatName[7:10],
|
||||
3: _FormatName[10:14],
|
||||
4: _FormatName[14:17],
|
||||
0: _FormatName[0:4], // jpeg
|
||||
1: _FormatName[4:7], // png
|
||||
2: _FormatName[7:10], // gif
|
||||
3: _FormatName[10:14], // tiff
|
||||
4: _FormatName[14:17], // bmp
|
||||
5: _FormatName[17:21], // heic
|
||||
}
|
||||
|
||||
// String implements the Stringer interface.
|
||||
@@ -40,11 +43,12 @@ func (x Format) String() string {
|
||||
}
|
||||
|
||||
var _FormatValue = map[string]Format{
|
||||
_FormatName[0:4]: 0,
|
||||
_FormatName[4:7]: 1,
|
||||
_FormatName[7:10]: 2,
|
||||
_FormatName[10:14]: 3,
|
||||
_FormatName[14:17]: 4,
|
||||
_FormatName[0:4]: 0, // jpeg
|
||||
_FormatName[4:7]: 1, // png
|
||||
_FormatName[7:10]: 2, // gif
|
||||
_FormatName[10:14]: 3, // tiff
|
||||
_FormatName[14:17]: 4, // bmp
|
||||
_FormatName[17:21]: 5, // heic
|
||||
}
|
||||
|
||||
// ParseFormat attempts to convert a string to a Format
|
||||
|
||||
+56
-29
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -13,10 +14,10 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/diskcache"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
"github.com/gtsteffaniak/go-logger/logger"
|
||||
)
|
||||
@@ -61,6 +62,10 @@ func NewPreviewGenerator(concurrencyLimit int, ffmpegPath string, cacheDir strin
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
err = os.MkdirAll(filepath.Join(settings.Config.Server.CacheDir, "heic"), 0755)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
ffmpegMainPath, err := CheckValidFFmpeg(ffmpegPath)
|
||||
if err != nil && ffmpegPath != "" {
|
||||
logger.Fatalf("the configured ffmpeg path does not contain a valid ffmpeg binary %s, err: %v", ffmpegPath, err)
|
||||
@@ -70,11 +75,11 @@ func NewPreviewGenerator(concurrencyLimit int, ffmpegPath string, cacheDir strin
|
||||
logger.Fatalf("the configured ffmpeg path is not a valid ffprobe binary %s, err: %v", ffmpegPath, err)
|
||||
}
|
||||
if errprobe == nil && err == nil {
|
||||
logger.Infof("Media Enabled : %v", errprobe == nil)
|
||||
settings.Config.Integrations.Media.FfmpegPath = filepath.Base(ffmpegMainPath)
|
||||
}
|
||||
logger.Debugf("Media Enabled : %v", ffmpegMainPath != "" && ffprobePath != "")
|
||||
settings.Config.Server.MuPdfAvailable = docEnabled()
|
||||
logger.Infof("MuPDF Enabled : %v", settings.Config.Server.MuPdfAvailable)
|
||||
logger.Debugf("MuPDF Enabled : %v", settings.Config.Server.MuPdfAvailable)
|
||||
return &Service{
|
||||
sem: make(chan struct{}, concurrencyLimit),
|
||||
ffmpegPath: ffmpegMainPath,
|
||||
@@ -90,16 +95,31 @@ func StartPreviewGenerator(concurrencyLimit int, ffmpegPath, cacheDir string) er
|
||||
}
|
||||
|
||||
func GetPreviewForFile(file iteminfo.ExtendedFileInfo, previewSize, url string, seekPercentage int) ([]byte, error) {
|
||||
if !AvailablePreview(file) {
|
||||
if !file.HasPreview {
|
||||
return nil, ErrUnsupportedMedia
|
||||
}
|
||||
cacheKey := CacheKey(file.RealPath, previewSize, file.ModTime, seekPercentage)
|
||||
var thisMd5 string
|
||||
if file.AudioMeta != nil && file.AudioMeta.AlbumArt != "" {
|
||||
// md5 is based on album art
|
||||
// md5 file.AlbumArt
|
||||
hasher := md5.New() //nolint:gosec
|
||||
_, _ = hasher.Write([]byte(file.AudioMeta.AlbumArt))
|
||||
thisMd5 = hex.EncodeToString(hasher.Sum(nil))
|
||||
file.Checksums = make(map[string]string)
|
||||
file.Checksums["md5"] = thisMd5
|
||||
} else {
|
||||
var err error
|
||||
thisMd5, err = utils.GetChecksum(file.RealPath, "md5")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get checksum: %w", err)
|
||||
}
|
||||
}
|
||||
cacheKey := CacheKey(thisMd5, previewSize, seekPercentage)
|
||||
if data, found, err := service.fileCache.Load(context.Background(), cacheKey); err != nil {
|
||||
return nil, fmt.Errorf("failed to load from cache: %w", err)
|
||||
} else if found {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
return GeneratePreview(file, previewSize, url, seekPercentage)
|
||||
}
|
||||
|
||||
@@ -109,9 +129,10 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
|
||||
err error
|
||||
imageBytes []byte
|
||||
)
|
||||
|
||||
// Generate thumbnail image from video
|
||||
hasher := md5.New() //nolint:gosec
|
||||
_, _ = hasher.Write([]byte(CacheKey(file.RealPath, previewSize, file.ModTime, seekPercentage)))
|
||||
_, _ = hasher.Write([]byte(CacheKey(file.Checksums["md5"], previewSize, seekPercentage)))
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
// Generate an image from office document
|
||||
if iteminfo.HasDocConvertableExtension(file.Name, file.Type) {
|
||||
@@ -125,8 +146,19 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create image for office file: %w", err)
|
||||
}
|
||||
} else if strings.HasPrefix(file.Type, "image/heic") {
|
||||
// HEIC files need FFmpeg conversion to JPEG with proper size/quality handling
|
||||
imageBytes, err = service.convertHEICToJPEGWithFFmpeg(file.RealPath, previewSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to process HEIC image file: %w", err)
|
||||
}
|
||||
// For HEIC files, we've already done the resize/conversion, so cache and return directly
|
||||
cacheKey := CacheKey(file.Checksums["md5"], previewSize, seekPercentage)
|
||||
if err = service.fileCache.Store(context.Background(), cacheKey, imageBytes); err != nil {
|
||||
logger.Errorf("failed to cache HEIC image: %v", err)
|
||||
}
|
||||
return imageBytes, nil
|
||||
} else if strings.HasPrefix(file.Type, "image") {
|
||||
// get image bytes from file
|
||||
imageBytes, err = os.ReadFile(file.RealPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read image file: %w", err)
|
||||
@@ -141,6 +173,16 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create image for video file: %w", err)
|
||||
}
|
||||
} else if strings.HasPrefix(file.Type, "audio") {
|
||||
// Extract album artwork from audio files
|
||||
if file.AudioMeta != nil && file.AudioMeta.AlbumArt != "" {
|
||||
imageBytes, err = base64.StdEncoding.DecodeString(file.AudioMeta.AlbumArt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode album artwork: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("no album artwork available for audio file: %s", file.Name)
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported media type: %s", ext)
|
||||
}
|
||||
@@ -155,13 +197,13 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
|
||||
return nil, fmt.Errorf("failed to resize preview image: %w", err)
|
||||
}
|
||||
// Cache and return
|
||||
cacheKey := CacheKey(file.RealPath, previewSize, file.ModTime, seekPercentage)
|
||||
cacheKey := CacheKey(file.Checksums["md5"], previewSize, seekPercentage)
|
||||
if err := service.fileCache.Store(context.Background(), cacheKey, resizedBytes); err != nil {
|
||||
logger.Errorf("failed to cache resized image: %v", err)
|
||||
}
|
||||
return resizedBytes, nil
|
||||
} else {
|
||||
cacheKey := CacheKey(file.RealPath, previewSize, file.ModTime, seekPercentage)
|
||||
cacheKey := CacheKey(file.Checksums["md5"], previewSize, seekPercentage)
|
||||
if err := service.fileCache.Store(context.Background(), cacheKey, imageBytes); err != nil {
|
||||
logger.Errorf("failed to cache resized image: %v", err)
|
||||
}
|
||||
@@ -198,35 +240,20 @@ func (s *Service) CreatePreview(data []byte, previewSize string) ([]byte, error)
|
||||
return output.Bytes(), nil
|
||||
}
|
||||
|
||||
func CacheKey(realPath, previewSize string, modTime time.Time, percentage int) string {
|
||||
return fmt.Sprintf("%x%x%x%x", realPath, modTime.Unix(), previewSize, percentage)
|
||||
func CacheKey(md5, previewSize string, percentage int) string {
|
||||
return fmt.Sprintf("%x%x%x", md5, previewSize, percentage)
|
||||
}
|
||||
|
||||
func DelThumbs(ctx context.Context, file iteminfo.ExtendedFileInfo) {
|
||||
errSmall := service.fileCache.Delete(ctx, CacheKey(file.RealPath, "small", file.ModTime, 0))
|
||||
errSmall := service.fileCache.Delete(ctx, CacheKey(file.Checksums["md5"], "small", 0))
|
||||
if errSmall != nil {
|
||||
errLarge := service.fileCache.Delete(ctx, CacheKey(file.RealPath, "large", file.ModTime, 0))
|
||||
errLarge := service.fileCache.Delete(ctx, CacheKey(file.Checksums["md5"], "large", 0))
|
||||
if errLarge != nil {
|
||||
logger.Debugf("Could not delete thumbnail: %v", file.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AvailablePreview(file iteminfo.ExtendedFileInfo) bool {
|
||||
if strings.HasPrefix(file.Type, "video") && service.ffmpegPath != "" {
|
||||
return true
|
||||
}
|
||||
if iteminfo.HasDocConvertableExtension(file.Name, file.Type) {
|
||||
return true
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(file.Name))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg", ".png", ".bmp", ".tiff":
|
||||
return true
|
||||
}
|
||||
return file.OnlyOfficeId != ""
|
||||
}
|
||||
|
||||
// CheckValidFFmpeg checks for a valid ffmpeg executable.
|
||||
// If a path is provided, it looks there. Otherwise, it searches the system's PATH.
|
||||
func CheckValidFFmpeg(path string) (string, error) {
|
||||
|
||||
@@ -2549,6 +2549,10 @@ const docTemplate = `{
|
||||
"$ref": "#/definitions/iteminfo.ItemInfo"
|
||||
}
|
||||
},
|
||||
"hasPreview": {
|
||||
"description": "whether the file has a thumbnail preview",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"description": "whether the file is hidden",
|
||||
"type": "boolean"
|
||||
@@ -2578,6 +2582,10 @@ const docTemplate = `{
|
||||
"iteminfo.ItemInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hasPreview": {
|
||||
"description": "whether the file has a thumbnail preview",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"description": "whether the file is hidden",
|
||||
"type": "boolean"
|
||||
@@ -2665,6 +2673,13 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"fileStartsWith": {
|
||||
"description": "(global) exclude files that start with these prefixes. Eg. \"archive-\" or \"backup-\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"folderEndsWith": {
|
||||
"description": "(global) exclude folders that end with these suffixes. Eg. \".thumbnails\" or \".git\"",
|
||||
"type": "array",
|
||||
@@ -2686,6 +2701,13 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"folderStartsWith": {
|
||||
"description": "(global) exclude folders that start with these prefixes. Eg. \"archive-\" or \"backup-\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hidden": {
|
||||
"description": "exclude hidden files and folders.",
|
||||
"type": "boolean"
|
||||
@@ -2717,6 +2739,18 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.FfmpegConvert": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"imagePreview": {
|
||||
"description": "supported image preview formats. default is heic",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.Frontend": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2835,6 +2869,14 @@ const docTemplate = `{
|
||||
"settings.Media": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"convert": {
|
||||
"description": "config for ffmpeg conversion settings",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/settings.FfmpegConvert"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ffmpegPath": {
|
||||
"description": "path to ffmpeg directory with ffmpeg and ffprobe (eg. /usr/local/bin)",
|
||||
"type": "string"
|
||||
@@ -2908,6 +2950,10 @@ const docTemplate = `{
|
||||
"url": {
|
||||
"description": "The URL to the OnlyOffice Document Server, needs to be accessible to the user.",
|
||||
"type": "string"
|
||||
},
|
||||
"viewOnly": {
|
||||
"description": "view only mode for OnlyOffice",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3148,6 +3194,10 @@ const docTemplate = `{
|
||||
}
|
||||
]
|
||||
},
|
||||
"indexAlbumArt": {
|
||||
"description": "check for album art in audio files and remember it for folder previews. Can slow down indexing.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"indexingIntervalMinutes": {
|
||||
"description": "optional manual overide interval in minutes to re-index the source",
|
||||
"type": "integer"
|
||||
@@ -3410,6 +3460,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"viewMode": {
|
||||
"description": "default view mode for anonymous users: \"list\", \"compact\", \"normal\", \"gallery\"",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -3515,6 +3566,7 @@ const docTemplate = `{
|
||||
"type": "integer"
|
||||
},
|
||||
"viewMode": {
|
||||
"description": "default view mode for anonymous users: \"list\", \"compact\", \"normal\", \"gallery\"",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -3545,6 +3597,9 @@ const docTemplate = `{
|
||||
"users.FileLoading": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"clearAll": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"maxConcurrentUpload": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -3601,28 +3656,32 @@ const docTemplate = `{
|
||||
"description": "disable the hide sidebar preview for previews and editors",
|
||||
"type": "boolean"
|
||||
},
|
||||
"folder": {
|
||||
"description": "show thumbnail preview image for folder files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"highQuality": {
|
||||
"description": "generate high quality preview images",
|
||||
"description": "generate high quality thumbnail preview images",
|
||||
"type": "boolean"
|
||||
},
|
||||
"image": {
|
||||
"description": "show real image as icon instead of generic photo icon",
|
||||
"description": "show thumbnail preview image for image files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"motionVideoPreview": {
|
||||
"description": "show multiple frames for videos in preview when hovering",
|
||||
"description": "show multiple frames for videos in thumbnail preview when hovering",
|
||||
"type": "boolean"
|
||||
},
|
||||
"office": {
|
||||
"description": "show preview image for office files",
|
||||
"description": "show thumbnail preview image for office files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"popup": {
|
||||
"description": "show larger popup preview when hovering",
|
||||
"description": "show larger popup preview when hovering over thumbnail",
|
||||
"type": "boolean"
|
||||
},
|
||||
"video": {
|
||||
"description": "show preview image for video files",
|
||||
"description": "show thumbnail preview image for video files",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2538,6 +2538,10 @@
|
||||
"$ref": "#/definitions/iteminfo.ItemInfo"
|
||||
}
|
||||
},
|
||||
"hasPreview": {
|
||||
"description": "whether the file has a thumbnail preview",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"description": "whether the file is hidden",
|
||||
"type": "boolean"
|
||||
@@ -2567,6 +2571,10 @@
|
||||
"iteminfo.ItemInfo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hasPreview": {
|
||||
"description": "whether the file has a thumbnail preview",
|
||||
"type": "boolean"
|
||||
},
|
||||
"hidden": {
|
||||
"description": "whether the file is hidden",
|
||||
"type": "boolean"
|
||||
@@ -2654,6 +2662,13 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"fileStartsWith": {
|
||||
"description": "(global) exclude files that start with these prefixes. Eg. \"archive-\" or \"backup-\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"folderEndsWith": {
|
||||
"description": "(global) exclude folders that end with these suffixes. Eg. \".thumbnails\" or \".git\"",
|
||||
"type": "array",
|
||||
@@ -2675,6 +2690,13 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"folderStartsWith": {
|
||||
"description": "(global) exclude folders that start with these prefixes. Eg. \"archive-\" or \"backup-\"",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"hidden": {
|
||||
"description": "exclude hidden files and folders.",
|
||||
"type": "boolean"
|
||||
@@ -2706,6 +2728,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.FfmpegConvert": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"imagePreview": {
|
||||
"description": "supported image preview formats. default is heic",
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.Frontend": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -2824,6 +2858,14 @@
|
||||
"settings.Media": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"convert": {
|
||||
"description": "config for ffmpeg conversion settings",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/settings.FfmpegConvert"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ffmpegPath": {
|
||||
"description": "path to ffmpeg directory with ffmpeg and ffprobe (eg. /usr/local/bin)",
|
||||
"type": "string"
|
||||
@@ -2897,6 +2939,10 @@
|
||||
"url": {
|
||||
"description": "The URL to the OnlyOffice Document Server, needs to be accessible to the user.",
|
||||
"type": "string"
|
||||
},
|
||||
"viewOnly": {
|
||||
"description": "view only mode for OnlyOffice",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3137,6 +3183,10 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"indexAlbumArt": {
|
||||
"description": "check for album art in audio files and remember it for folder previews. Can slow down indexing.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"indexingIntervalMinutes": {
|
||||
"description": "optional manual overide interval in minutes to re-index the source",
|
||||
"type": "integer"
|
||||
@@ -3399,6 +3449,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"viewMode": {
|
||||
"description": "default view mode for anonymous users: \"list\", \"compact\", \"normal\", \"gallery\"",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -3504,6 +3555,7 @@
|
||||
"type": "integer"
|
||||
},
|
||||
"viewMode": {
|
||||
"description": "default view mode for anonymous users: \"list\", \"compact\", \"normal\", \"gallery\"",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
@@ -3534,6 +3586,9 @@
|
||||
"users.FileLoading": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"clearAll": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"maxConcurrentUpload": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -3590,28 +3645,32 @@
|
||||
"description": "disable the hide sidebar preview for previews and editors",
|
||||
"type": "boolean"
|
||||
},
|
||||
"folder": {
|
||||
"description": "show thumbnail preview image for folder files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"highQuality": {
|
||||
"description": "generate high quality preview images",
|
||||
"description": "generate high quality thumbnail preview images",
|
||||
"type": "boolean"
|
||||
},
|
||||
"image": {
|
||||
"description": "show real image as icon instead of generic photo icon",
|
||||
"description": "show thumbnail preview image for image files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"motionVideoPreview": {
|
||||
"description": "show multiple frames for videos in preview when hovering",
|
||||
"description": "show multiple frames for videos in thumbnail preview when hovering",
|
||||
"type": "boolean"
|
||||
},
|
||||
"office": {
|
||||
"description": "show preview image for office files",
|
||||
"description": "show thumbnail preview image for office files",
|
||||
"type": "boolean"
|
||||
},
|
||||
"popup": {
|
||||
"description": "show larger popup preview when hovering",
|
||||
"description": "show larger popup preview when hovering over thumbnail",
|
||||
"type": "boolean"
|
||||
},
|
||||
"video": {
|
||||
"description": "show preview image for video files",
|
||||
"description": "show thumbnail preview image for video files",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,9 @@ definitions:
|
||||
items:
|
||||
$ref: '#/definitions/iteminfo.ItemInfo'
|
||||
type: array
|
||||
hasPreview:
|
||||
description: whether the file has a thumbnail preview
|
||||
type: boolean
|
||||
hidden:
|
||||
description: whether the file is hidden
|
||||
type: boolean
|
||||
@@ -121,6 +124,9 @@ definitions:
|
||||
type: object
|
||||
iteminfo.ItemInfo:
|
||||
properties:
|
||||
hasPreview:
|
||||
description: whether the file has a thumbnail preview
|
||||
type: boolean
|
||||
hidden:
|
||||
description: whether the file is hidden
|
||||
type: boolean
|
||||
@@ -189,6 +195,12 @@ definitions:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
fileStartsWith:
|
||||
description: (global) exclude files that start with these prefixes. Eg. "archive-"
|
||||
or "backup-"
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
folderEndsWith:
|
||||
description: (global) exclude folders that end with these suffixes. Eg. ".thumbnails"
|
||||
or ".git"
|
||||
@@ -208,6 +220,12 @@ definitions:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
folderStartsWith:
|
||||
description: (global) exclude folders that start with these prefixes. Eg.
|
||||
"archive-" or "backup-"
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
hidden:
|
||||
description: exclude hidden files and folders.
|
||||
type: boolean
|
||||
@@ -230,6 +248,14 @@ definitions:
|
||||
- text
|
||||
- url
|
||||
type: object
|
||||
settings.FfmpegConvert:
|
||||
properties:
|
||||
imagePreview:
|
||||
additionalProperties:
|
||||
type: boolean
|
||||
description: supported image preview formats. default is heic
|
||||
type: object
|
||||
type: object
|
||||
settings.Frontend:
|
||||
properties:
|
||||
description:
|
||||
@@ -315,6 +341,10 @@ definitions:
|
||||
type: object
|
||||
settings.Media:
|
||||
properties:
|
||||
convert:
|
||||
allOf:
|
||||
- $ref: '#/definitions/settings.FfmpegConvert'
|
||||
description: config for ffmpeg conversion settings
|
||||
ffmpegPath:
|
||||
description: path to ffmpeg directory with ffmpeg and ffprobe (eg. /usr/local/bin)
|
||||
type: string
|
||||
@@ -372,6 +402,9 @@ definitions:
|
||||
description: The URL to the OnlyOffice Document Server, needs to be accessible
|
||||
to the user.
|
||||
type: string
|
||||
viewOnly:
|
||||
description: view only mode for OnlyOffice
|
||||
type: boolean
|
||||
required:
|
||||
- secret
|
||||
- url
|
||||
@@ -550,6 +583,10 @@ definitions:
|
||||
allOf:
|
||||
- $ref: '#/definitions/settings.IncludeIndexFilter'
|
||||
description: include files and folders from indexing, if exclude is not set
|
||||
indexAlbumArt:
|
||||
description: check for album art in audio files and remember it for folder
|
||||
previews. Can slow down indexing.
|
||||
type: boolean
|
||||
indexingIntervalMinutes:
|
||||
description: optional manual overide interval in minutes to re-index the source
|
||||
type: integer
|
||||
@@ -747,6 +784,8 @@ definitions:
|
||||
unit:
|
||||
type: string
|
||||
viewMode:
|
||||
description: 'default view mode for anonymous users: "list", "compact", "normal",
|
||||
"gallery"'
|
||||
type: string
|
||||
type: object
|
||||
share.Link:
|
||||
@@ -821,6 +860,8 @@ definitions:
|
||||
userID:
|
||||
type: integer
|
||||
viewMode:
|
||||
description: 'default view mode for anonymous users: "list", "compact", "normal",
|
||||
"gallery"'
|
||||
type: string
|
||||
type: object
|
||||
users.AuthToken:
|
||||
@@ -840,6 +881,8 @@ definitions:
|
||||
type: object
|
||||
users.FileLoading:
|
||||
properties:
|
||||
clearAll:
|
||||
type: boolean
|
||||
maxConcurrentUpload:
|
||||
type: integer
|
||||
uploadChunkSizeMb:
|
||||
@@ -879,23 +922,26 @@ definitions:
|
||||
disableHideSidebar:
|
||||
description: disable the hide sidebar preview for previews and editors
|
||||
type: boolean
|
||||
folder:
|
||||
description: show thumbnail preview image for folder files
|
||||
type: boolean
|
||||
highQuality:
|
||||
description: generate high quality preview images
|
||||
description: generate high quality thumbnail preview images
|
||||
type: boolean
|
||||
image:
|
||||
description: show real image as icon instead of generic photo icon
|
||||
description: show thumbnail preview image for image files
|
||||
type: boolean
|
||||
motionVideoPreview:
|
||||
description: show multiple frames for videos in preview when hovering
|
||||
description: show multiple frames for videos in thumbnail preview when hovering
|
||||
type: boolean
|
||||
office:
|
||||
description: show preview image for office files
|
||||
description: show thumbnail preview image for office files
|
||||
type: boolean
|
||||
popup:
|
||||
description: show larger popup preview when hovering
|
||||
description: show larger popup preview when hovering over thumbnail
|
||||
type: boolean
|
||||
video:
|
||||
description: show preview image for video files
|
||||
description: show thumbnail preview image for video files
|
||||
type: boolean
|
||||
type: object
|
||||
users.Sorting:
|
||||
|
||||
@@ -55,8 +55,6 @@
|
||||
"shallowOnly": true
|
||||
}
|
||||
]
|
||||
// no-undef is already included in
|
||||
// @vue/eslint-config-typescript
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"file-loader": "^6.2.0",
|
||||
"glob": "^9.3.5",
|
||||
"highlight.js": "^11.11.1",
|
||||
"jsmediatags": "github:Kurami32/jsmediatags",
|
||||
"mammoth": "^1.9.1",
|
||||
"marked": "^15.0.6",
|
||||
"normalize.css": "^8.0.1",
|
||||
@@ -56,6 +55,7 @@
|
||||
"fs-extra": "^11.3.0",
|
||||
"jsdom": "^25.0.1",
|
||||
"vite": "^6.2.0",
|
||||
"vite-plugin-checker": "^0.10.3",
|
||||
"vite-plugin-compression2": "^1.0.0",
|
||||
"vitest": "^3.0.7",
|
||||
"vue-tsc": "^2.0.7"
|
||||
|
||||
@@ -39,12 +39,15 @@ server:
|
||||
folderNames: []
|
||||
fileEndsWith: []
|
||||
folderEndsWith: []
|
||||
fileStartsWith: []
|
||||
folderStartsWith: []
|
||||
include: # include files and folders from indexing, if exclude is not set
|
||||
rootFolders: []
|
||||
rootFiles: []
|
||||
defaultUserScope: "/" # default "/" should match folders under path
|
||||
defaultEnabled: false # should be added as a default source for new users?
|
||||
createUserDir: false # create a user directory for each user
|
||||
indexAlbumArt: false # check for album art in audio files and remember it for folder previews. Can slow down indexing.
|
||||
externalUrl: "" # used by share links if set (eg. http://mydomain.com)
|
||||
internalUrl: "" # used by integrations if set, this is the base domain that an integration service will use to communicate with filebrowser (eg. http://localhost:8080)
|
||||
cacheDir: "tmp" # path to the cache directory, used for thumbnails and other cached files
|
||||
@@ -137,6 +140,7 @@ userDefaults:
|
||||
popup: false
|
||||
autoplayMedia: false
|
||||
defaultMediaPlayer: false
|
||||
folder: false
|
||||
permissions:
|
||||
api: false
|
||||
admin: false
|
||||
@@ -149,6 +153,7 @@ userDefaults:
|
||||
fileLoading: # upload and download settings
|
||||
maxConcurrentUpload: 10
|
||||
uploadChunkSizeMb: 10
|
||||
clearAll: false
|
||||
disableOnlyOfficeExt: "" # list of file extensions to disable onlyoffice editor for
|
||||
customTheme: "" # Name of theme to use chosen from custom themes config.
|
||||
showSelectMultiple: false # show select multiple files on desktop
|
||||
@@ -158,5 +163,9 @@ integrations:
|
||||
url: "" # The URL to the OnlyOffice Document Server, needs to be accessible to the user. validate:required
|
||||
internalUrl: "" # An optional internal address that the filebrowser server can use to communicate with the OnlyOffice Document Server, could be useful to bypass proxy.
|
||||
secret: "**hidden**" # secret: authentication key for OnlyOffice integration validate:required
|
||||
viewOnly: false # view only mode for OnlyOffice
|
||||
media: # validate:omitempty
|
||||
ffmpegPath: "" # path to ffmpeg directory with ffmpeg and ffprobe (eg. /usr/local/bin)
|
||||
convert: # config for ffmpeg conversion settings
|
||||
imagePreview: # supported image preview formats. default is heic
|
||||
heic: true
|
||||
+18
-17
@@ -7,49 +7,50 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<title>{{ .title }}</title>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="256x256" href="{{ .favicon }}">
|
||||
<link rel="icon" type="image/png" sizes="256x256" href="{{ .htmlVars.favicon }}">
|
||||
|
||||
<!--<link href="https://fonts.googleapis.com/css?family=Material+Icons|Material+Symbols+Outlined" rel="stylesheet">-->
|
||||
|
||||
<!-- Add to home screen for Android and modern mobile browsers -->
|
||||
<link rel="manifest" id="manifestPlaceholder" crossorigin="use-credentials">
|
||||
<meta name="theme-color" content="{{ .color }}">
|
||||
<meta name="theme-color" content="{{ .htmlVars.color }}">
|
||||
|
||||
<!-- Add to home screen for Safari on iOS/iPadOS -->
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="assets">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ .appIcon }}">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ .htmlVars.appIcon }}">
|
||||
|
||||
<!-- Add to home screen for Windows -->
|
||||
<meta name="msapplication-TileImage" content="{{ .winIcon }}">
|
||||
<meta name="msapplication-TileColor" content="{{ .color }}">
|
||||
<meta name="msapplication-TileImage" content="{{ .htmlVars.winIcon }}">
|
||||
<meta name="msapplication-TileColor" content="{{ .htmlVars.color }}">
|
||||
|
||||
<meta name="description" content="{{ .description }}">
|
||||
<meta name="description" content="{{ .htmlVars.description }}">
|
||||
<link rel="canonical" href="https://github.com/gtsteffaniak/filebrowser">
|
||||
<link rel="canonical" href="https://github.com/gtsteffaniak">
|
||||
<link rel="canonical" href="https://github.com/quantumx-apps">
|
||||
|
||||
<!-- Inject Some Variables and generate the manifest json -->
|
||||
<script>
|
||||
window.filebrowser = JSON.parse('{{ .globalVars }}');
|
||||
window.globalVars = {{ .globalVars }};
|
||||
window.shareVars = {{ .shareVars }};
|
||||
var dynamicManifest = {
|
||||
"name": window.filebrowser.name || 'FileBrowser Quantum',
|
||||
"name": window.globalVars.name || 'FileBrowser Quantum',
|
||||
"icons": [
|
||||
{
|
||||
"src": window.location.origin + "{{ .staticURL }}/img/icons/android-chrome-256x256.png",
|
||||
"src": window.location.origin + "{{ .htmlVars.staticURL }}/img/icons/android-chrome-256x256.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": window.location.origin + "{{ .staticURL }}/img/icons/android-chrome-192x192.png",
|
||||
"src": window.location.origin + "{{ .htmlVars.staticURL }}/img/icons/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
],
|
||||
"start_url": window.location.origin + "{{ .baseURL }}",
|
||||
"start_url": window.location.origin + "{{ .htmlVars.baseURL }}",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "{{ .color }}"
|
||||
"theme_color": "{{ .htmlVars.color }}"
|
||||
}
|
||||
|
||||
const stringManifest = JSON.stringify(dynamicManifest);
|
||||
@@ -61,7 +62,7 @@
|
||||
<style>
|
||||
/* Basic Styles */
|
||||
:root {
|
||||
--background: {{ .lightBackground }};
|
||||
--background: {{ .htmlVars.lightBackground }};
|
||||
--alt-background: #ddd;
|
||||
--surfacePrimary: gray;
|
||||
--surfaceSecondary: lightgray;
|
||||
@@ -71,7 +72,7 @@
|
||||
--activeWhiteIcon: gray;
|
||||
}
|
||||
.dark-mode {
|
||||
--background: {{ .darkBackground }};
|
||||
--background: {{ .htmlVars.darkBackground }};
|
||||
--alt-background: #283136;
|
||||
--surfacePrimary: #20292F;
|
||||
--surfaceSecondary: #3A4147;
|
||||
@@ -155,15 +156,15 @@
|
||||
transform: scale(1.0);
|
||||
}
|
||||
}
|
||||
{{ .customCSS }}
|
||||
{{ .userSelectedTheme }}
|
||||
{{ .htmlVars.customCSS }}
|
||||
{{ .htmlVars.userSelectedTheme }}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
{{ if .darkMode -}}
|
||||
{{ if .htmlVars.darkMode -}}
|
||||
<div id="loading" class="dark-mode">
|
||||
<div class="spinner">
|
||||
<div class="bounce1"></div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { baseURL } from "@/utils/constants";
|
||||
import { globalVars } from "@/utils/constants";
|
||||
|
||||
const ssl = window.location.protocol === "https:";
|
||||
const protocol = ssl ? "wss:" : "ws:";
|
||||
|
||||
export default function command(url, command, onmessage, onclose) {
|
||||
url = `${protocol}//${window.location.host}${baseURL}api/command${url}`;
|
||||
url = `${protocol}//${window.location.host}${globalVars.baseURL}api/command${url}`;
|
||||
let conn = new window.WebSocket(url);
|
||||
conn.onopen = () => conn.send(command);
|
||||
conn.onmessage = onmessage;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { fetchURL, adjustedData } from './utils'
|
||||
import { getApiPath, doubleEncode, getPublicApiPath } from '@/utils/url.js'
|
||||
import { mutations } from '@/store'
|
||||
import { notify } from '@/notify'
|
||||
import { externalUrl } from '@/utils/constants'
|
||||
import { globalVars } from '@/utils/constants'
|
||||
|
||||
// Notify if errors occur
|
||||
export async function fetchFiles(source, path, content = false) {
|
||||
@@ -229,8 +229,8 @@ export function getDownloadURL(source, path, inline, useExternal) {
|
||||
...(inline && { inline: 'true' })
|
||||
}
|
||||
const apiPath = getApiPath('api/raw', params)
|
||||
if (externalUrl && useExternal) {
|
||||
return externalUrl + apiPath
|
||||
if (globalVars.externalUrl && useExternal) {
|
||||
return globalVars.externalUrl + apiPath
|
||||
}
|
||||
return window.origin + apiPath
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { adjustedData } from "./utils";
|
||||
import { notify } from "@/notify";
|
||||
import { getApiPath, getPublicApiPath } from "@/utils/url.js";
|
||||
import { externalUrl } from "@/utils/constants";
|
||||
import { getApiPath, getPublicApiPath, encodedPath } from "@/utils/url.js";
|
||||
import { globalVars } from "@/utils/constants";
|
||||
import { state } from "@/store";
|
||||
|
||||
// ============================================================================
|
||||
@@ -17,8 +17,9 @@ import { state } from "@/store";
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
export async function fetchPub(path, hash, password = "", content = false) {
|
||||
path = encodedPath(path);
|
||||
const params = {
|
||||
path,
|
||||
path: path,
|
||||
hash,
|
||||
...(content && { content: 'true' }),
|
||||
...(state.share.token && { token: state.share.token })
|
||||
@@ -97,9 +98,9 @@ export function getPreviewURL(path,size="small") {
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getShareURL(share) {
|
||||
if (externalUrl) {
|
||||
if (globalVars.externalUrl) {
|
||||
const apiPath = getApiPath(`public/share/${share.hash}`)
|
||||
return externalUrl + apiPath;
|
||||
return globalVars.externalUrl + apiPath;
|
||||
}
|
||||
return window.origin + getApiPath(`public/share/${share.hash}`);
|
||||
}
|
||||
@@ -1,12 +1,50 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
vi.mock('@/utils/constants', () => {
|
||||
return {
|
||||
globalVars: {
|
||||
baseURL: "unit-testing",
|
||||
name: "Test App",
|
||||
mediaAvailable: true,
|
||||
muPdfAvailable: true,
|
||||
onlyOfficeUrl: "",
|
||||
recaptcha: false,
|
||||
recaptchaKey: "",
|
||||
darkMode: false,
|
||||
oidcAvailable: false,
|
||||
passwordAvailable: true,
|
||||
externalUrl: "",
|
||||
minSearchLength: 1,
|
||||
disableNavButtons: false,
|
||||
userSelectableThemes: {},
|
||||
enableThumbs: true,
|
||||
noAuth: false,
|
||||
loginPage: true,
|
||||
signup: false,
|
||||
version: "test",
|
||||
commitSHA: "test",
|
||||
disableExternal: false,
|
||||
externalLinks: [],
|
||||
updateAvailable: "",
|
||||
},
|
||||
shareInfo: {
|
||||
isShare: false,
|
||||
disableThumbnails: false,
|
||||
hash: "",
|
||||
enforceDarkLightMode: "",
|
||||
disableSidebar: false,
|
||||
isValid: true,
|
||||
},
|
||||
serverHasMultipleSources: false,
|
||||
logoURL: "test-logo.png",
|
||||
origin: "http://localhost",
|
||||
settings: [],
|
||||
};
|
||||
});
|
||||
|
||||
import { adjustedData } from './utils.js';
|
||||
|
||||
describe('adjustedData', () => {
|
||||
vi.doMock('@/utils/constants', () => {
|
||||
return {
|
||||
baseURL: "unit-testing",
|
||||
};
|
||||
});
|
||||
it('should append the URL and process directory data correctly', () => {
|
||||
const input = {
|
||||
type: "directory",
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
import downloadFiles from "@/utils/download";
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import Action from "@/components/Action.vue";
|
||||
import { onlyOfficeUrl } from "@/utils/constants.js";
|
||||
import { globalVars } from "@/utils/constants.js";
|
||||
import buttons from "@/utils/buttons";
|
||||
import { notify } from "@/notify";
|
||||
import { eventBus } from "@/store/eventBus";
|
||||
@@ -246,7 +246,7 @@ export default {
|
||||
return getters.currentPromptName() == "ContextMenu";
|
||||
},
|
||||
onlyofficeEnabled() {
|
||||
return onlyOfficeUrl !== "";
|
||||
return globalVars.onlyOfficeUrl !== "";
|
||||
},
|
||||
isSearchActive() {
|
||||
return state.isSearchActive;
|
||||
@@ -281,14 +281,6 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentPrompt: {
|
||||
handler(prompt) {
|
||||
if (prompt && prompt.name === "ContextMenu") {
|
||||
this.setPositions();
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
hasOverflowItems: {
|
||||
handler(hasItems) {
|
||||
mutations.setContextMenuHasItems(hasItems);
|
||||
|
||||
@@ -18,12 +18,21 @@ https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/compon
|
||||
v-if="textPosition == 'inside'"
|
||||
>
|
||||
{{ displayed_text }}
|
||||
<i
|
||||
v-if="helpText && status === 'error'"
|
||||
class="no-select material-symbols-outlined tooltip-info-icon"
|
||||
@mouseenter="showTooltip"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
help
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes.js";
|
||||
import { mutations } from "@/store";
|
||||
|
||||
// We're leaving this untouched as you can read in the beginning
|
||||
var isNumber = function (n) {
|
||||
@@ -92,6 +101,10 @@ export default {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
"help-text": {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isValNumeric() {
|
||||
@@ -244,6 +257,20 @@ export default {
|
||||
return style;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showTooltip(event) {
|
||||
if (this.helpText) {
|
||||
mutations.showTooltip({
|
||||
content: this.helpText,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
}
|
||||
},
|
||||
hideTooltip() {
|
||||
mutations.hideTooltip();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -263,4 +290,16 @@ export default {
|
||||
.vue-simple-progress-text {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.tooltip-info-icon {
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
margin-left: 0.3em;
|
||||
vertical-align: middle;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.tooltip-info-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -147,7 +147,7 @@ import { getters, mutations, state } from "@/store";
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||
import { url } from "@/utils/";
|
||||
import Icon from "@/components/files/Icon.vue";
|
||||
import { serverHasMultipleSources, baseURL, minSearchLength } from "@/utils/constants";
|
||||
import { globalVars, serverHasMultipleSources } from "@/utils/constants";
|
||||
|
||||
var boxes = {
|
||||
folder: { label: "folders", icon: "folder" },
|
||||
@@ -169,7 +169,7 @@ export default {
|
||||
return {
|
||||
largerThan: "",
|
||||
smallerThan: "",
|
||||
noneMessage: this.$t("search.typeToSearch", { minSearchLength: minSearchLength }),
|
||||
noneMessage: this.$t("search.typeToSearch", { minSearchLength: globalVars.minSearchLength }),
|
||||
searchTypes: "",
|
||||
isTypeSelectDisabled: false,
|
||||
showHelp: false,
|
||||
@@ -294,7 +294,7 @@ export default {
|
||||
if (this.ongoing) {
|
||||
return "";
|
||||
}
|
||||
return this.$t("search.typeToSearch", { minSearchLength: minSearchLength })
|
||||
return this.$t("search.typeToSearch", { minSearchLength: globalVars.minSearchLength })
|
||||
},
|
||||
isRunning() {
|
||||
return this.ongoing;
|
||||
@@ -395,9 +395,9 @@ export default {
|
||||
const encodedPath = encodeURIComponent(context + "/" + path).replaceAll("%2F", "/");
|
||||
let fullpath = encodedPath;
|
||||
if (serverHasMultipleSources) {
|
||||
fullpath = baseURL+"files/" + this.selectedSource + encodedPath;
|
||||
fullpath = globalVars.baseURL+"files/" + this.selectedSource + encodedPath;
|
||||
} else {
|
||||
fullpath = baseURL+"files" + encodedPath;
|
||||
fullpath = globalVars.baseURL+"files" + encodedPath;
|
||||
}
|
||||
return fullpath;
|
||||
},
|
||||
@@ -483,9 +483,8 @@ export default {
|
||||
if (event != undefined) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (this.value === "" || this.value.length < minSearchLength) {
|
||||
this.ongoing = false;
|
||||
this.noneMessage = this.$t("search.notEnoughCharacters", { minSearchLength: minSearchLength });
|
||||
if (this.value === "" || this.value.length < globalVars.minSearchLength) {
|
||||
this.ongoing = false; this.noneMessage = this.$t("search.notEnoughCharacters", { minSearchLength: globalVars.minSearchLength });
|
||||
return;
|
||||
}
|
||||
let searchTypesFull = this.searchTypes;
|
||||
|
||||
@@ -24,19 +24,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
onlyOfficeUrl,
|
||||
mediaAvailable,
|
||||
muPdfAvailable,
|
||||
baseURL,
|
||||
} from "@/utils/constants";
|
||||
import { globalVars, shareInfo } from "@/utils/constants";
|
||||
import { getTypeInfo } from "@/utils/mimetype";
|
||||
import { mutations, state } from "@/store";
|
||||
import { shareInfo } from "@/utils/constants";
|
||||
import { mutations, state, getters } from "@/store";
|
||||
|
||||
// NEW: Define placeholder and error image URLs for easy configuration
|
||||
const PLACEHOLDER_URL = baseURL + "static/img/placeholder.png"; // A generic loading placeholder
|
||||
const ERROR_URL = baseURL + "static/img/placeholder.png";
|
||||
const PLACEHOLDER_URL = globalVars.baseURL + "static/img/placeholder.png"; // A generic loading placeholder
|
||||
const ERROR_URL = globalVars.baseURL + "static/img/placeholder.png";
|
||||
|
||||
export default {
|
||||
name: "Icon",
|
||||
@@ -49,6 +43,10 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
hasPreview: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
},
|
||||
@@ -79,7 +77,16 @@ export default {
|
||||
if (shareInfo.disableThumbnails) {
|
||||
return false;
|
||||
}
|
||||
return this.isPreviewImg && this.imageState !== 'error' && !this.disablePreviewExt && !this.officeFileDisabled
|
||||
if (this.thumbnailUrl == "") {
|
||||
return false;
|
||||
}
|
||||
if (!this.hasPreview) {
|
||||
return false;
|
||||
}
|
||||
if (!state.user.preview.folder && this.mimetype == "directory") {
|
||||
return false;
|
||||
}
|
||||
return this.imageState !== 'error' && !this.disablePreviewExt && !this.officeFileDisabled
|
||||
},
|
||||
disablePreviewExt() {
|
||||
const ext = "." + (this.filename.split(".").pop() || "").toLowerCase(); // Ensure lowercase and dot
|
||||
@@ -92,7 +99,7 @@ export default {
|
||||
return state.user?.disablePreviewExt?.includes(ext);
|
||||
},
|
||||
pdfConvertable() {
|
||||
if (!muPdfAvailable) {
|
||||
if (!globalVars.muPdfAvailable) {
|
||||
return false; // If muPDF is not available
|
||||
}
|
||||
const ext = "." + (this.filename.split(".").pop() || "").toLowerCase(); // Ensure lowercase and dot
|
||||
@@ -126,16 +133,16 @@ export default {
|
||||
return this.imageTargetSrc;
|
||||
},
|
||||
showLargeIcon() {
|
||||
return state.user.viewMode === "gallery" && state.user.preview.highQuality;
|
||||
return getters.viewMode() === "gallery" && state.user.preview.highQuality;
|
||||
},
|
||||
showLarger() {
|
||||
return state.user.viewMode === "gallery" || state.user.viewMode === "normal";
|
||||
return getters.viewMode() === "gallery" || getters.viewMode() === "normal";
|
||||
},
|
||||
hasMotion() {
|
||||
return (
|
||||
this.getIconForType().simpleType === "video" &&
|
||||
state.user.preview?.video &&
|
||||
mediaAvailable &&
|
||||
globalVars.mediaAvailable &&
|
||||
// @ts-ignore
|
||||
state.user.preview.motionVideoPreview
|
||||
);
|
||||
@@ -143,37 +150,6 @@ export default {
|
||||
isMaterialIcon() {
|
||||
return this.materialIcon !== "";
|
||||
},
|
||||
isPreviewImg() {
|
||||
if (this.thumbnailUrl == "") {
|
||||
return false;
|
||||
}
|
||||
if (this.mimetype == "text/csv") {
|
||||
return false;
|
||||
}
|
||||
// @ts-ignore
|
||||
if (this.pdfConvertable && state.user.preview?.office) {
|
||||
return true;
|
||||
}
|
||||
if (this.getIconForType().simpleType === "image" && state.user.preview?.image) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.getIconForType().simpleType === "video" &&
|
||||
state.user.preview?.video &&
|
||||
mediaAvailable
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
this.getIconForType().simpleType === "document" &&
|
||||
// @ts-ignore
|
||||
state.user.preview?.office &&
|
||||
onlyOfficeUrl != ""
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// NEW: Centralized method to load any image and handle its state
|
||||
@@ -276,11 +252,11 @@ export default {
|
||||
showLargeIcon() {
|
||||
this.updateImageTargetSrc();
|
||||
},
|
||||
// UPDATED: Added a check for isPreviewImg
|
||||
// UPDATED: Added a check for hasPreviewImage
|
||||
imageTargetSrc: {
|
||||
handler(newSrc) {
|
||||
// ONLY trigger the image loader if the component is meant to show a preview.
|
||||
if (this.isPreviewImg) {
|
||||
if (this.hasPreviewImage) {
|
||||
this.loadImage(newSrc);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
:active="isSelected"
|
||||
:thumbnailUrl="isThumbnailInView ? thumbnailUrl : ''"
|
||||
:filename="name"
|
||||
:hasPreview="hasPreview"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -54,6 +55,7 @@
|
||||
@click.stop="downloadFile"
|
||||
v-if="quickDownloadEnabled"
|
||||
:filename="name"
|
||||
:hasPreview="hasPreview"
|
||||
mimetype="file_download"
|
||||
style="padding-right: 0.5em"
|
||||
class="download-icon"
|
||||
@@ -66,7 +68,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { enableThumbs } from "@/utils/constants";
|
||||
import { globalVars, serverHasMultipleSources, shareInfo } from "@/utils/constants";
|
||||
import downloadFiles from "@/utils/download";
|
||||
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
|
||||
@@ -75,7 +77,6 @@ import * as upload from "@/utils/upload";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { url } from "@/utils";
|
||||
import Icon from "@/components/files/Icon.vue";
|
||||
import { baseURL, serverHasMultipleSources, shareInfo } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "item",
|
||||
@@ -106,10 +107,11 @@ export default {
|
||||
"path",
|
||||
"reducedOpacity",
|
||||
"hash",
|
||||
"hasPreview",
|
||||
],
|
||||
computed: {
|
||||
galleryView() {
|
||||
return state.user.viewMode === "gallery";
|
||||
return getters.viewMode() === "gallery";
|
||||
},
|
||||
quickDownloadEnabled() {
|
||||
// @ts-ignore
|
||||
@@ -164,7 +166,7 @@ export default {
|
||||
return true;
|
||||
},
|
||||
thumbnailUrl() {
|
||||
if (!enableThumbs) {
|
||||
if (!globalVars.enableThumbs) {
|
||||
return "";
|
||||
}
|
||||
const previewPath = url.removeTrailingSlash(state.req.path) + "/" + this.name;
|
||||
@@ -175,7 +177,7 @@ export default {
|
||||
return filesApi.getPreviewURL(state.req.source, previewPath, this.modified);
|
||||
},
|
||||
isThumbsEnabled() {
|
||||
return enableThumbs;
|
||||
return globalVars.enableThumbs;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -232,12 +234,12 @@ export default {
|
||||
},
|
||||
getUrl() {
|
||||
if (this.hash) {
|
||||
return baseURL + "public/share/" + this.hash + "/" + url.encodedPath(this.path);
|
||||
return globalVars.baseURL + "public/share/" + this.hash + "/" + url.encodedPath(this.path);
|
||||
}
|
||||
if (serverHasMultipleSources) {
|
||||
return baseURL + "files/" + encodeURIComponent(this.source) + url.encodedPath(this.path);
|
||||
return globalVars.baseURL + "files/" + encodeURIComponent(this.source) + url.encodedPath(this.path);
|
||||
}
|
||||
return baseURL + "files" + url.encodedPath(this.path);
|
||||
return globalVars.baseURL + "files" + url.encodedPath(this.path);
|
||||
},
|
||||
/** @param {MouseEvent} event */
|
||||
onRightClick(event) {
|
||||
|
||||
@@ -0,0 +1,882 @@
|
||||
<template>
|
||||
<!-- Left edge detection zone -->
|
||||
<div
|
||||
v-if="enabled && hasPrevious"
|
||||
class="nav-zone nav-zone-left"
|
||||
:class="{ moveWithSidebar: moveWithSidebar }"
|
||||
@mousemove="toggleNavigation"
|
||||
@touchstart="(e) => { handleTouchStart(e); toggleNavigation(e); }"
|
||||
@touchmove="handleTouchMove"
|
||||
@click="handleClick"
|
||||
></div>
|
||||
|
||||
<!-- Right edge detection zone -->
|
||||
<div
|
||||
v-if="enabled && hasNext"
|
||||
class="nav-zone nav-zone-right"
|
||||
@mousemove="toggleNavigation"
|
||||
@touchstart="(e) => { handleTouchStart(e); toggleNavigation(e); }"
|
||||
@touchmove="handleTouchMove"
|
||||
@click="handleClick"
|
||||
></div>
|
||||
|
||||
<!-- Previous button -->
|
||||
<button
|
||||
v-if="enabled && hasPrevious"
|
||||
@click.stop="handlePrevClick"
|
||||
@mousedown="startDrag($event, 'previous')"
|
||||
@touchstart="handleTouchStart($event, 'previous')"
|
||||
@touchmove="handleButtonTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
@mouseover="setHoverNav(true)"
|
||||
@mouseleave="setHoverNav(false)"
|
||||
class="nav-button nav-previous"
|
||||
:class="{
|
||||
moveWithSidebar: moveWithSidebar,
|
||||
hidden: !showNav,
|
||||
disabled: !hasPrevious,
|
||||
dragging: dragState.type === 'previous',
|
||||
active: dragState.atFullExtent && dragState.type === 'previous',
|
||||
'dark-mode': isDarkMode,
|
||||
}"
|
||||
:style="dragState.type === 'previous' ? { transform: `translateY(-50%) translate(${dragState.deltaX}px, 0)` } : {}"
|
||||
:aria-label="$t('buttons.previous')"
|
||||
:title="$t('buttons.previous')"
|
||||
>
|
||||
<i class="material-icons">
|
||||
{{ dragState.type === 'previous' && dragState.atFullExtent ? 'list_alt' : 'chevron_left' }} <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
</i>
|
||||
</button>
|
||||
|
||||
<!-- Next button -->
|
||||
<button
|
||||
v-if="enabled && hasNext"
|
||||
@click.stop="handleNextClick"
|
||||
@mousedown="startDrag($event, 'next')"
|
||||
@touchstart="handleTouchStart($event, 'next')"
|
||||
@touchmove="handleButtonTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
@mouseover="setHoverNav(true)"
|
||||
@mouseleave="setHoverNav(false)"
|
||||
class="nav-button nav-next"
|
||||
:class="{ hidden: !showNav, dragging: dragState.type === 'next', active: dragState.atFullExtent && dragState.type === 'next','dark-mode': isDarkMode}"
|
||||
:style="dragState.type === 'next' ? { transform: `translateY(-50%) translate(${dragState.deltaX}px, 0)` } : {}"
|
||||
:aria-label="$t('buttons.next')"
|
||||
:title="$t('buttons.next')"
|
||||
>
|
||||
<i class="material-icons">
|
||||
{{ dragState.type === 'next' && dragState.atFullExtent ? 'list_alt' : 'chevron_right' }} <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
</i>
|
||||
</button>
|
||||
|
||||
<!-- Prefetch links for better performance -->
|
||||
<link v-if="previousRaw" rel="prefetch" :href="previousRaw" />
|
||||
<link v-if="nextRaw" rel="prefetch" :href="nextRaw" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import throttle from "@/utils/throttle";
|
||||
import { previewViews } from "@/utils/constants";
|
||||
import { url } from "@/utils";
|
||||
import { filesApi, publicApi } from "@/api";
|
||||
|
||||
export default {
|
||||
name: "NextPrevious",
|
||||
data() {
|
||||
return {
|
||||
hoverNav: false,
|
||||
dragState: {
|
||||
isDragging: false,
|
||||
type: null, // 'previous' or 'next'
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
deltaX: 0,
|
||||
deltaY: 0,
|
||||
threshold: 0, // Will be calculated as 10em in pixels
|
||||
atFullExtent: false,
|
||||
triggered: false,
|
||||
},
|
||||
// State tracking
|
||||
navigationTimeout: null,
|
||||
isSwipe: false,
|
||||
touchStartX: 0,
|
||||
touchStartY: 0,
|
||||
// Button touch handling
|
||||
touchState: {
|
||||
isButtonTouch: false,
|
||||
buttonType: null,
|
||||
startTime: 0,
|
||||
hasMoved: false,
|
||||
tapTimeout: null,
|
||||
triggered: false
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDarkMode() { return getters.isDarkMode(); },
|
||||
moveWithSidebar() {
|
||||
return getters.isSidebarVisible() && getters.isStickySidebar();
|
||||
},
|
||||
enabled() {
|
||||
return state.navigation.enabled;
|
||||
},
|
||||
showNav() {
|
||||
const shouldShow = state.navigation.show || this.hoverNav;
|
||||
return shouldShow;
|
||||
},
|
||||
hasPrevious() {
|
||||
const has = state.navigation.previousLink !== "";
|
||||
return has;
|
||||
},
|
||||
hasNext() {
|
||||
const has = state.navigation.nextLink !== "";
|
||||
return has;
|
||||
},
|
||||
previousRaw() {
|
||||
return state.navigation.previousRaw;
|
||||
},
|
||||
nextRaw() {
|
||||
return state.navigation.nextRaw;
|
||||
},
|
||||
currentView() {
|
||||
const view = getters.currentView();
|
||||
return view;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentView() {
|
||||
this.updateNavigationEnabled();
|
||||
|
||||
// Also trigger navigation setup if we're now in a preview view
|
||||
this.$nextTick(() => {
|
||||
if (this.enabled && state.req) {
|
||||
this.setupNavigationForCurrentItem();
|
||||
}
|
||||
});
|
||||
},
|
||||
'state.req': {
|
||||
handler() {
|
||||
this.updateNavigationEnabled();
|
||||
// Auto-setup navigation when request changes and we're enabled
|
||||
if (this.enabled) {
|
||||
this.$nextTick(() => {
|
||||
this.setupNavigationForCurrentItem();
|
||||
});
|
||||
}
|
||||
},
|
||||
deep: true,
|
||||
immediate: false
|
||||
},
|
||||
enabled(newEnabled) {
|
||||
if (newEnabled && state.req) {
|
||||
this.$nextTick(() => {
|
||||
this.setupNavigationForCurrentItem();
|
||||
});
|
||||
}
|
||||
},
|
||||
'$route'() {
|
||||
// Give time for state.req to be updated, then setup navigation
|
||||
setTimeout(() => {
|
||||
this.$nextTick(() => {
|
||||
if (this.enabled && state.req) {
|
||||
this.setupNavigationForCurrentItem();
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
},
|
||||
// Watch for when navigation links are set up
|
||||
'state.navigation.previousLink'() {
|
||||
this.showInitialNavigation();
|
||||
},
|
||||
'state.navigation.nextLink'() {
|
||||
this.showInitialNavigation();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
window.addEventListener("mousemove", this.handleDrag);
|
||||
window.addEventListener("mouseup", this.endDrag);
|
||||
window.addEventListener("touchmove", this.handleDrag, { passive: false });
|
||||
window.addEventListener("touchend", this.endDrag);
|
||||
|
||||
// Calculate 10em threshold in pixels
|
||||
const emSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
this.dragState.threshold = 10 * emSize;
|
||||
|
||||
this.updateNavigationEnabled();
|
||||
|
||||
// Setup navigation if enabled and we have a current item
|
||||
if (this.enabled && state.req) {
|
||||
this.$nextTick(() => {
|
||||
this.setupNavigationForCurrentItem();
|
||||
});
|
||||
} else {
|
||||
this.$nextTick(() => {
|
||||
this.showInitialNavigation();
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
window.removeEventListener("mousemove", this.handleDrag);
|
||||
window.removeEventListener("mouseup", this.endDrag);
|
||||
window.removeEventListener("touchmove", this.handleDrag);
|
||||
window.removeEventListener("touchend", this.endDrag);
|
||||
|
||||
// Clear our local timeout
|
||||
if (this.navigationTimeout) {
|
||||
clearTimeout(this.navigationTimeout);
|
||||
this.navigationTimeout = null;
|
||||
}
|
||||
|
||||
// Clean up touch state
|
||||
this.resetTouchState();
|
||||
|
||||
mutations.clearNavigation();
|
||||
},
|
||||
methods: {
|
||||
updateNavigationEnabled() {
|
||||
const shouldEnable = previewViews.includes(this.currentView);
|
||||
mutations.setNavigationEnabled(shouldEnable);
|
||||
},
|
||||
async setupNavigationForCurrentItem() {
|
||||
if (!this.enabled || !state.req || state.req.type === 'directory') {
|
||||
// Clear navigation when not applicable
|
||||
mutations.clearNavigation();
|
||||
return;
|
||||
}
|
||||
|
||||
const directoryPath = url.removeLastDir(state.req.path);
|
||||
let listing = null;
|
||||
|
||||
// Try to get listing from current request first
|
||||
if (state.req.items) {
|
||||
listing = state.req.items;
|
||||
} else {
|
||||
// Fetch the directory listing
|
||||
try {
|
||||
let res;
|
||||
if (getters.isShare()) {
|
||||
res = await publicApi.fetchPub(directoryPath, state.share.hash);
|
||||
} else {
|
||||
res = await filesApi.fetchFiles(state.req.source, directoryPath);
|
||||
}
|
||||
listing = res.items;
|
||||
} catch (error) {
|
||||
listing = [state.req]; // Fallback to current item only
|
||||
}
|
||||
}
|
||||
|
||||
mutations.setupNavigation({
|
||||
listing: listing,
|
||||
currentItem: state.req,
|
||||
directoryPath: directoryPath
|
||||
});
|
||||
},
|
||||
showInitialNavigation() {
|
||||
// Show navigation initially for 3 seconds when navigation is set up
|
||||
if (this.enabled && (this.hasPrevious || this.hasNext)) {
|
||||
mutations.setNavigationShow(true);
|
||||
|
||||
// Clear any existing timeout
|
||||
if (this.navigationTimeout) {
|
||||
clearTimeout(this.navigationTimeout);
|
||||
this.navigationTimeout = null;
|
||||
}
|
||||
|
||||
this.navigationTimeout = setTimeout(() => {
|
||||
if (!this.hoverNav) {
|
||||
mutations.setNavigationShow(false);
|
||||
}
|
||||
this.navigationTimeout = null;
|
||||
}, 3000);
|
||||
}
|
||||
},
|
||||
prev() {
|
||||
if (this.hasPrevious) {
|
||||
this.hoverNav = false;
|
||||
this.$router.replace({ path: state.navigation.previousLink });
|
||||
}
|
||||
},
|
||||
next() {
|
||||
if (this.hasNext) {
|
||||
this.hoverNav = false;
|
||||
this.$router.replace({ path: state.navigation.nextLink });
|
||||
}
|
||||
},
|
||||
keyEvent(event) {
|
||||
// Only handle navigation if enabled and no prompt is active
|
||||
if (!this.enabled || state.prompts.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { key } = event;
|
||||
|
||||
switch (key) {
|
||||
case "ArrowRight":
|
||||
if (this.hasNext) {
|
||||
event.preventDefault();
|
||||
this.next();
|
||||
}
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
if (this.hasPrevious) {
|
||||
event.preventDefault();
|
||||
this.prev();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
handleClick() {
|
||||
// Don't show navigation if this is part of a swipe gesture
|
||||
if (this.isSwipe) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Simplified: clicking anywhere in the CSS zones shows navigation
|
||||
if (!this.enabled || (!this.hasPrevious && !this.hasNext)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.showNavigation();
|
||||
},
|
||||
showNavigation() {
|
||||
mutations.setNavigationShow(true);
|
||||
mutations.clearNavigationTimeout();
|
||||
|
||||
// Clear our local timeout too
|
||||
if (this.navigationTimeout) {
|
||||
clearTimeout(this.navigationTimeout);
|
||||
this.navigationTimeout = null;
|
||||
}
|
||||
|
||||
this.navigationTimeout = setTimeout(() => {
|
||||
if (!this.hoverNav) {
|
||||
mutations.setNavigationShow(false);
|
||||
}
|
||||
mutations.clearNavigationTimeout();
|
||||
this.navigationTimeout = null;
|
||||
}, 3000); // Show for 3 seconds
|
||||
|
||||
mutations.setNavigationTimeout(this.navigationTimeout);
|
||||
},
|
||||
toggleNavigation: throttle(function () {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
if (this.isSwipe) {
|
||||
return;
|
||||
}
|
||||
this.showNavigation();
|
||||
}, 100),
|
||||
setHoverNav(value) {
|
||||
this.hoverNav = value;
|
||||
mutations.setNavigationHover(value);
|
||||
},
|
||||
|
||||
// Touch handling and swipe detection (similar to ListingItem.vue)
|
||||
handleTouchStart(event, buttonType = null) {
|
||||
if (event.touches && event.touches.length > 0) {
|
||||
const touch = event.touches[0];
|
||||
this.touchStartX = touch.clientX;
|
||||
this.touchStartY = touch.clientY;
|
||||
this.isSwipe = false;
|
||||
|
||||
// Handle button-specific touch
|
||||
if (buttonType) {
|
||||
this.touchState = {
|
||||
isButtonTouch: true,
|
||||
buttonType: buttonType,
|
||||
startTime: Date.now(),
|
||||
hasMoved: false,
|
||||
tapTimeout: null,
|
||||
triggered: false
|
||||
};
|
||||
|
||||
// Set a longer timeout to allow for drag intent detection
|
||||
this.touchState.tapTimeout = setTimeout(() => {
|
||||
// If we haven't moved significantly and not dragging, treat as tap
|
||||
if (!this.touchState.hasMoved && !this.dragState.isDragging) {
|
||||
this.handleButtonTap(buttonType);
|
||||
}
|
||||
}, 300); // Increased to 300ms to allow for drag detection
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleTouchMove(event) {
|
||||
if (!event.touches || event.touches.length === 0) return;
|
||||
|
||||
const touch = event.touches[0];
|
||||
const deltaX = Math.abs(touch.clientX - this.touchStartX);
|
||||
const deltaY = Math.abs(touch.clientY - this.touchStartY);
|
||||
const movementThreshold = 10;
|
||||
|
||||
if (deltaX > movementThreshold || deltaY > movementThreshold) {
|
||||
this.isSwipe = true;
|
||||
this.cancelNavigationTimeout();
|
||||
}
|
||||
},
|
||||
|
||||
// Handle touch movement specifically for navigation buttons
|
||||
handleButtonTouchMove(event) {
|
||||
if (!event.touches || event.touches.length === 0) return;
|
||||
if (!this.touchState.isButtonTouch) return;
|
||||
|
||||
event.preventDefault(); // Prevent scrolling while dragging
|
||||
|
||||
const touch = event.touches[0];
|
||||
const deltaX = Math.abs(touch.clientX - this.touchStartX);
|
||||
const deltaY = Math.abs(touch.clientY - this.touchStartY);
|
||||
const movementThreshold = 10;
|
||||
|
||||
// Check if user has moved enough to start dragging
|
||||
if (deltaX > movementThreshold || deltaY > movementThreshold) {
|
||||
this.touchState.hasMoved = true;
|
||||
|
||||
// Cancel tap timeout since user is dragging
|
||||
if (this.touchState.tapTimeout) {
|
||||
clearTimeout(this.touchState.tapTimeout);
|
||||
this.touchState.tapTimeout = null;
|
||||
}
|
||||
|
||||
// Initialize drag state if not already dragging
|
||||
if (!this.dragState.isDragging) {
|
||||
// Calculate 10em threshold in pixels if not set
|
||||
if (!this.dragState.threshold) {
|
||||
const emSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
this.dragState.threshold = 10 * emSize;
|
||||
}
|
||||
|
||||
this.dragState = {
|
||||
isDragging: true,
|
||||
type: this.touchState.buttonType,
|
||||
startX: this.touchStartX,
|
||||
startY: this.touchStartY,
|
||||
deltaX: 0,
|
||||
deltaY: 0,
|
||||
threshold: this.dragState.threshold,
|
||||
atFullExtent: false,
|
||||
triggered: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Update drag position - implement drag logic directly
|
||||
if (this.dragState.isDragging) {
|
||||
let dragDeltaX = touch.clientX - this.dragState.startX;
|
||||
const maxDrag = this.dragState.threshold; // 10em
|
||||
|
||||
// Constrain drag to correct direction and max distance
|
||||
if (this.dragState.type === 'previous') {
|
||||
// Left button: only allow rightward drag (positive deltaX)
|
||||
dragDeltaX = Math.max(0, Math.min(maxDrag, dragDeltaX));
|
||||
} else if (this.dragState.type === 'next') {
|
||||
// Right button: only allow leftward drag (negative deltaX)
|
||||
dragDeltaX = Math.min(0, Math.max(-maxDrag, dragDeltaX));
|
||||
}
|
||||
|
||||
this.dragState.deltaX = dragDeltaX;
|
||||
|
||||
// Check if we've reached the full extent
|
||||
const atFullExtent = Math.abs(dragDeltaX) >= maxDrag;
|
||||
this.dragState.atFullExtent = atFullExtent;
|
||||
}
|
||||
}
|
||||
},
|
||||
handleTouchEnd() {
|
||||
// Handle touch end for buttons
|
||||
if (this.touchState.isButtonTouch) {
|
||||
const touchDuration = Date.now() - this.touchState.startTime;
|
||||
|
||||
// If it was a short touch without movement, and we haven't already navigated, treat as tap
|
||||
if (!this.touchState.hasMoved && touchDuration < 300 && this.touchState.tapTimeout) {
|
||||
clearTimeout(this.touchState.tapTimeout);
|
||||
this.touchState.tapTimeout = null;
|
||||
this.handleButtonTap(this.touchState.buttonType);
|
||||
}
|
||||
|
||||
// Reset touch state
|
||||
this.resetTouchState();
|
||||
}
|
||||
|
||||
// Reset navigation swipe state
|
||||
this.isSwipe = false;
|
||||
|
||||
// Let endDrag handle the drag cleanup
|
||||
if (this.dragState.isDragging) {
|
||||
this.endDrag();
|
||||
}
|
||||
},
|
||||
|
||||
cancelNavigationTimeout() {
|
||||
if (this.navigationTimeout) {
|
||||
clearTimeout(this.navigationTimeout);
|
||||
this.navigationTimeout = null;
|
||||
}
|
||||
},
|
||||
|
||||
// Handle immediate button tap (mobile-friendly)
|
||||
handleButtonTap(buttonType) {
|
||||
// Prevent double navigation if already triggered
|
||||
if (this.touchState.triggered) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear any pending timeouts
|
||||
if (this.touchState.tapTimeout) {
|
||||
clearTimeout(this.touchState.tapTimeout);
|
||||
this.touchState.tapTimeout = null;
|
||||
}
|
||||
|
||||
// Mark as triggered to prevent double navigation
|
||||
this.touchState.triggered = true;
|
||||
|
||||
// Navigate immediately on tap
|
||||
if (buttonType === 'previous' && this.hasPrevious) {
|
||||
this.prev();
|
||||
} else if (buttonType === 'next' && this.hasNext) {
|
||||
this.next();
|
||||
}
|
||||
|
||||
// Reset touch state
|
||||
this.resetTouchState();
|
||||
},
|
||||
|
||||
resetTouchState() {
|
||||
if (this.touchState.tapTimeout) {
|
||||
clearTimeout(this.touchState.tapTimeout);
|
||||
}
|
||||
this.touchState = {
|
||||
isButtonTouch: false,
|
||||
buttonType: null,
|
||||
startTime: 0,
|
||||
hasMoved: false,
|
||||
tapTimeout: null,
|
||||
triggered: false
|
||||
};
|
||||
},
|
||||
|
||||
// Drag functionality for navigation buttons
|
||||
startDrag(event, type) {
|
||||
event.preventDefault();
|
||||
|
||||
const clientX = event.touches ? event.touches[0].clientX : event.clientX;
|
||||
const clientY = event.touches ? event.touches[0].clientY : event.clientY;
|
||||
|
||||
this.dragState = {
|
||||
isDragging: true,
|
||||
type: type,
|
||||
startX: clientX,
|
||||
startY: clientY,
|
||||
deltaX: 0,
|
||||
deltaY: 0,
|
||||
threshold: this.dragState.threshold,
|
||||
atFullExtent: false,
|
||||
triggered: false,
|
||||
};
|
||||
|
||||
},
|
||||
|
||||
handleDrag(event) {
|
||||
if (!this.dragState.isDragging) return;
|
||||
|
||||
const clientX = event.touches ? event.touches[0].clientX : event.clientX;
|
||||
|
||||
let deltaX = clientX - this.dragState.startX;
|
||||
|
||||
// Constrain drag to correct direction and max distance
|
||||
const maxDrag = this.dragState.threshold; // 10em
|
||||
if (this.dragState.type === 'previous') {
|
||||
// Left button: only allow rightward drag (positive deltaX)
|
||||
deltaX = Math.max(0, Math.min(maxDrag, deltaX));
|
||||
} else if (this.dragState.type === 'next') {
|
||||
// Right button: only allow leftward drag (negative deltaX)
|
||||
deltaX = Math.min(0, Math.max(-maxDrag, deltaX));
|
||||
}
|
||||
|
||||
this.dragState.deltaX = deltaX;
|
||||
|
||||
// Check if we've reached the full extent
|
||||
const atFullExtent = Math.abs(deltaX) >= maxDrag;
|
||||
this.dragState.atFullExtent = atFullExtent;
|
||||
|
||||
// Prevent default to avoid text selection during drag
|
||||
event.preventDefault();
|
||||
},
|
||||
|
||||
endDrag() {
|
||||
if (!this.dragState.isDragging && !this.touchState.isButtonTouch) return;
|
||||
|
||||
// Only show file list if user released at full extent
|
||||
if (this.dragState.atFullExtent) {
|
||||
this.showFileList(this.dragState.type);
|
||||
}
|
||||
|
||||
this.resetDragState();
|
||||
this.resetTouchState();
|
||||
},
|
||||
|
||||
resetDragState() {
|
||||
this.dragState = {
|
||||
isDragging: false,
|
||||
type: null,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
deltaX: 0,
|
||||
deltaY: 0,
|
||||
threshold: this.dragState.threshold,
|
||||
atFullExtent: false,
|
||||
triggered: false,
|
||||
};
|
||||
},
|
||||
|
||||
handlePrevClick() {
|
||||
// Only navigate if this wasn't a drag
|
||||
if (!this.dragState.triggered) {
|
||||
this.prev();
|
||||
}
|
||||
this.resetDragState();
|
||||
},
|
||||
|
||||
handleNextClick() {
|
||||
// Only navigate if this wasn't a drag
|
||||
if (!this.dragState.triggered) {
|
||||
this.next();
|
||||
}
|
||||
this.resetDragState();
|
||||
},
|
||||
|
||||
showFileList(type) {
|
||||
// Hide navigation buttons when showing file list
|
||||
mutations.setNavigationShow(false);
|
||||
|
||||
// Determine what list to show based on drag type
|
||||
if (type === 'previous') {
|
||||
// Show parent directories for navigating up
|
||||
this.showParentDirectories();
|
||||
} else if (type === 'next') {
|
||||
// Show current listing items for quick jumping
|
||||
this.showCurrentListing();
|
||||
}
|
||||
},
|
||||
|
||||
showParentDirectories() {
|
||||
// Show files in the current directory (same directory as the previewed file)
|
||||
const currentItems = this.getCurrentListingItems();
|
||||
mutations.showHover({
|
||||
name: "file-list",
|
||||
props: {
|
||||
fileList: currentItems,
|
||||
mode: "navigate-siblings",
|
||||
title: this.$t("prompts.quickJump")
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showCurrentListing() {
|
||||
const currentItems = this.getCurrentListingItems();
|
||||
mutations.showHover({
|
||||
name: "file-list",
|
||||
props: {
|
||||
fileList: currentItems,
|
||||
mode: "quick-jump",
|
||||
title: this.$t("prompts.quickJump")
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getParentDirectories() {
|
||||
// Build array of parent directories from current path
|
||||
const currentPath = state.req.path || "/";
|
||||
const pathParts = currentPath.split("/").filter(part => part);
|
||||
const parentDirs = [];
|
||||
|
||||
// Add root
|
||||
parentDirs.push({
|
||||
name: "/",
|
||||
path: "/",
|
||||
source: state.req.source,
|
||||
isDirectory: true
|
||||
});
|
||||
|
||||
// Add each level up to current
|
||||
let buildPath = "";
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
buildPath += "/" + pathParts[i];
|
||||
parentDirs.push({
|
||||
name: pathParts[i],
|
||||
path: buildPath,
|
||||
source: state.req.source,
|
||||
isDirectory: true
|
||||
});
|
||||
}
|
||||
|
||||
return parentDirs.reverse(); // Show deepest first
|
||||
},
|
||||
|
||||
getCurrentListingItems() {
|
||||
// Get items from the current navigation listing (files in same directory)
|
||||
const listing = state.navigation.listing || [];
|
||||
return listing.map(item => ({
|
||||
name: item.name, // Keep original names without emojis
|
||||
path: item.path,
|
||||
source: item.source || state.req.source,
|
||||
type: item.type,
|
||||
isDirectory: item.type === 'directory',
|
||||
originalItem: item
|
||||
}));
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Thin edge detection zones - minimal interference with content */
|
||||
.nav-zone {
|
||||
position: fixed;
|
||||
top: 25%; /* Start at 25% from top */
|
||||
bottom: 25%; /* End at 25% from bottom (so middle 50%) */
|
||||
width: 3em; /* Reduced width to minimize content interference */
|
||||
pointer-events: auto;
|
||||
z-index: 5; /* Lower z-index so content can appear above */
|
||||
background: transparent; /* Invisible zones for mouse/touch detection */
|
||||
}
|
||||
|
||||
|
||||
.nav-zone-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.nav-zone-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Removed navigation-buttons container to prevent content interaction blocking */
|
||||
|
||||
.nav-button {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--background);
|
||||
color: var(--textPrimary);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.4s ease, transform 0.3s ease, background-color 0.3s ease, box-shadow 0.3s ease;
|
||||
pointer-events: auto;
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-button.dark-mode {
|
||||
background: var(--surfacePrimary);
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
.nav-button:hover,
|
||||
.nav-button.active {
|
||||
background: var(--primaryColor);
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
box-shadow:
|
||||
inset 0 -3em 3em rgba(217, 217, 217, 0.211),
|
||||
0 0 0 2px var(--alt-background),
|
||||
0 4px 20px rgba(0, 0, 0, 0.4);
|
||||
color: white;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
.nav-previous {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.nav-next {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.nav-button.hidden {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%) scale(0.9);
|
||||
pointer-events: none !important; /* Ensure no interaction when hidden */
|
||||
z-index: -1; /* Move behind content when hidden */
|
||||
}
|
||||
|
||||
/* Smooth show animation for better UX */
|
||||
.nav-button:not(.hidden) {
|
||||
animation: nav-button-show 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes nav-button-show {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%) scale(0.8);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-button.dragging {
|
||||
z-index: 1002;
|
||||
cursor: grabbing;
|
||||
transition: none; /* Disable transitions during drag for immediate response */
|
||||
}
|
||||
|
||||
.nav-button i.material-icons {
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-button:hover i.material-icons,
|
||||
.nav-button.active i.material-icons {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Mobile styles */
|
||||
@media (max-width: 768px) {
|
||||
.nav-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.nav-previous {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.nav-next {
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.nav-button i.material-icons {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Reduce animation intensity on mobile for better performance */
|
||||
.nav-button:not(.hidden) {
|
||||
animation-duration: 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
/* Ensure buttons don't interfere with scrollbars */
|
||||
@media (max-width: 480px) {
|
||||
.nav-previous {
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.nav-next {
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
.moveWithSidebar {
|
||||
margin-left: 20em;
|
||||
}
|
||||
</style>
|
||||
@@ -5,14 +5,6 @@
|
||||
<div class="card-content">
|
||||
<div v-if="isEditingPath">
|
||||
<file-list @update:selected="updateTempPath" :browse-source="sourceName"></file-list>
|
||||
<div style="margin-top: 1em; text-align: right;">
|
||||
<button class="button button--flat" @click="cancelPathChange">
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button class="button button--flat" @click="confirmPathChange">
|
||||
{{ $t("buttons.ok") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p>{{ $t("prompts.source", { suffix: ":" }) }} {{ currentSource }}</p>
|
||||
@@ -77,10 +69,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-action">
|
||||
<button v-if="!isEditingPath" @click="closeHovers" class="button button--flat button--grey"
|
||||
:aria-label="$t('buttons.close')" :title="$t('buttons.close')">
|
||||
{{ $t("buttons.close") }}
|
||||
</button>
|
||||
<template v-if="isEditingPath">
|
||||
<button class="button button--flat" @click="cancelPathChange" :aria-label="$t('buttons.cancel')" :title="$t('buttons.cancel')">
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button class="button button--flat" @click="confirmPathChange" :aria-label="$t('buttons.ok')" :title="$t('buttons.ok')">
|
||||
{{ $t("buttons.ok") }}
|
||||
</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button @click="closeHovers" class="button button--flat button--grey" :aria-label="$t('buttons.close')" :title="$t('buttons.close')">
|
||||
{{ $t("buttons.close") }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,51 +1,74 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Source Selection Dropdown (only show if multiple sources available) -->
|
||||
<div v-if="availableSources.length > 1" class="source-selector" style="margin-bottom: 1rem;">
|
||||
|
||||
<div v-if="isDisplayMode" class="card-title">
|
||||
<h2>{{ effectiveTitle }}</h2>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<!-- Source Selection Dropdown (only show if multiple sources available and not in display mode) -->
|
||||
<div v-if="availableSources.length > 1 && !isDisplayMode" class="source-selector" style="margin-bottom: 1rem;">
|
||||
<label for="destinationSource" style="display: block; margin-bottom: 0.5rem; font-weight: bold;">
|
||||
{{ $t("prompts.destinationSource") }}
|
||||
</label>
|
||||
<select id="destinationSource" v-model="currentSource" @change="onSourceChange"
|
||||
class="input">
|
||||
<select id="destinationSource" v-model="currentSource" @change="onSourceChange" class="input">
|
||||
<option v-for="source in availableSources" :key="source" :value="source">
|
||||
{{ source }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>Source: {{ sourcePath.source }} </div> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<div aria-label="filelist-path" class="searchContext button clickable">{{$t('search.path')}} {{ sourcePath.path }}</div>
|
||||
<div v-if="!isDisplayMode" aria-label="filelist-path" class="searchContext button clickable">{{ $t('search.path') }}
|
||||
{{ sourcePath.path }}</div>
|
||||
|
||||
<ul class="file-list">
|
||||
<li
|
||||
@click="itemClick"
|
||||
@touchstart="touchstart"
|
||||
@dblclick="next"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-label="item.name"
|
||||
:aria-selected="selected == item.path"
|
||||
:key="item.name"
|
||||
v-for="item in items"
|
||||
:data-path="item.path"
|
||||
>
|
||||
{{ item.name }}
|
||||
<li @click="itemClick" @touchstart="touchstart" @dblclick="next" role="button" tabindex="0"
|
||||
:aria-label="item.name" :aria-selected="selected == item.path" :key="item.name" v-for="item in items"
|
||||
:data-path="item.path" class="file-item">
|
||||
<Icon v-if="isDisplayMode" :filename="item.name"
|
||||
:mimetype="item.originalItem?.type || 'application/octet-stream'" :active="selected == item.path"
|
||||
class="file-icon" />
|
||||
<span class="file-name">{{ item.name }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Cancel/Close button for display mode -->
|
||||
<div v-if="isDisplayMode" class="card-action">
|
||||
<button @click="closeModal" class="button button--flat" :aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')">
|
||||
{{ $t('buttons.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { state, mutations } from "@/store";
|
||||
import { url } from "@/utils";
|
||||
import { filesApi } from "@/api";
|
||||
import Icon from "@/components/files/Icon.vue";
|
||||
|
||||
export default {
|
||||
name: "file-list",
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
browseSource: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
fileList: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: "browse", // 'browse', 'navigate-up', 'quick-jump'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data: function () {
|
||||
const initialSource = this.browseSource || state.req.source;
|
||||
@@ -66,6 +89,9 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
effectiveTitle() {
|
||||
return this.title || this.$t("files.files");
|
||||
},
|
||||
sourcePath() {
|
||||
return { source: this.source, path: this.path };
|
||||
},
|
||||
@@ -76,6 +102,10 @@ export default {
|
||||
// Get all available sources from state.sources.info
|
||||
return state.sources && state.sources.info ? Object.keys(state.sources.info) : [state.req.source];
|
||||
},
|
||||
isDisplayMode() {
|
||||
// Display mode when fileList prop is provided (drag-triggered navigation)
|
||||
return this.fileList !== null;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
browseSource(newSource) {
|
||||
@@ -91,19 +121,24 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
// Use currentSource if provided, otherwise use state.req
|
||||
const sourceToUse = this.currentSource;
|
||||
const pathToUse = this.currentSource !== state.req.source ? "/" : state.req.path;
|
||||
const initialReq = {
|
||||
...state.req,
|
||||
source: sourceToUse,
|
||||
path: pathToUse,
|
||||
};
|
||||
// Fetch the initial data for the source
|
||||
if (this.currentSource !== state.req.source) {
|
||||
filesApi.fetchFiles(sourceToUse, pathToUse).then(this.fillOptions);
|
||||
if (this.isDisplayMode) {
|
||||
// Display mode: use provided fileList
|
||||
this.fillOptionsFromList();
|
||||
} else {
|
||||
this.fillOptions(initialReq);
|
||||
// Normal browse mode: fetch files
|
||||
const sourceToUse = this.currentSource;
|
||||
const pathToUse = this.currentSource !== state.req.source ? "/" : state.req.path;
|
||||
const initialReq = {
|
||||
...state.req,
|
||||
source: sourceToUse,
|
||||
path: pathToUse,
|
||||
};
|
||||
// Fetch the initial data for the source
|
||||
if (this.currentSource !== state.req.source) {
|
||||
filesApi.fetchFiles(sourceToUse, pathToUse).then(this.fillOptions);
|
||||
} else {
|
||||
this.fillOptions(initialReq);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -193,8 +228,14 @@ export default {
|
||||
}
|
||||
},
|
||||
itemClick: function (event) {
|
||||
if (state.user.singleClick) this.next(event);
|
||||
else this.select(event);
|
||||
if (this.isDisplayMode) {
|
||||
// In display mode, navigate directly to the item
|
||||
this.navigateToItem(event);
|
||||
} else if (state.user.singleClick) {
|
||||
this.next(event);
|
||||
} else {
|
||||
this.select(event);
|
||||
}
|
||||
},
|
||||
select: function (event) {
|
||||
let path = event.currentTarget.dataset.path;
|
||||
@@ -232,6 +273,80 @@ export default {
|
||||
onSourceChange() {
|
||||
this.resetToSource(this.currentSource);
|
||||
},
|
||||
|
||||
// Display mode methods (for drag-triggered navigation)
|
||||
fillOptionsFromList() {
|
||||
// Use the provided fileList, filtering out directories to show only files
|
||||
const allItems = this.fileList || [];
|
||||
this.items = allItems.filter(item => !item.isDirectory && item.type !== 'directory');
|
||||
this.current = this.title || "Navigation";
|
||||
this.source = state.req.source;
|
||||
|
||||
// Emit the current info
|
||||
this.$emit("update:selected", {
|
||||
path: this.current,
|
||||
source: this.source
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
navigateToItem(event) {
|
||||
const path = event.currentTarget.dataset.path;
|
||||
const item = this.items.find(item => item.path === path);
|
||||
|
||||
if (!item) return;
|
||||
|
||||
// Close the file list modal
|
||||
mutations.closeHovers();
|
||||
|
||||
// Navigate to the item's URL
|
||||
const itemUrl = url.buildItemUrl(item.source || state.req.source, item.path);
|
||||
|
||||
// Use router to navigate
|
||||
this.$router.replace({ path: itemUrl });
|
||||
},
|
||||
|
||||
closeModal() {
|
||||
// Close the file list modal
|
||||
mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background-color: var(--surfaceSecondary, rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
margin-right: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.file-list li[aria-selected=true] {
|
||||
background: var(--primaryColor) !important;
|
||||
color: #fff !important;
|
||||
transition: .1s ease all;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -56,9 +56,9 @@
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button type="submit" @click="closeHovers" class="button button--flat" :aria-label="$t('buttons.ok')"
|
||||
:title="$t('buttons.ok')">
|
||||
{{ $t("buttons.ok") }}
|
||||
<button type="submit" @click="closeHovers" class="button button--flat" :aria-label="$t('buttons.close')"
|
||||
:title="$t('buttons.close')">
|
||||
{{ $t("buttons.close") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -38,7 +38,7 @@ import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { url } from "@/utils";
|
||||
import { notify } from "@/notify";
|
||||
import { removeLastDir } from "@/utils/url";
|
||||
import { removeLastDir, goToItem } from "@/utils/url";
|
||||
|
||||
export default {
|
||||
name: "move-copy",
|
||||
@@ -171,7 +171,15 @@ export default {
|
||||
}
|
||||
mutations.closeHovers();
|
||||
mutations.setSearch(false);
|
||||
notify.showSuccess(`Server is ${this.operation === "copy" ? "copying" : "moving"} files...`);
|
||||
if (this.operation === "move") {
|
||||
notify.showSuccess(this.$t(`prompts.moveSuccess`));
|
||||
} else {
|
||||
notify.showSuccess(this.$t(`prompts.copySuccess`));
|
||||
}
|
||||
// Navigate to the destination folder after successful operation
|
||||
if (this.destSource && this.destPath) {
|
||||
goToItem(this.destSource, this.destPath);
|
||||
}
|
||||
} catch (error) {
|
||||
notify.showError(error);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import buttons from "@/utils/buttons";
|
||||
import Totp from "./Totp.vue";
|
||||
import Access from "./Access.vue";
|
||||
import Password from "./Password.vue";
|
||||
import FileList from "./FileList.vue";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
|
||||
export default {
|
||||
@@ -58,6 +59,7 @@ export default {
|
||||
ActionApi,
|
||||
Access,
|
||||
Password,
|
||||
"file-list": FileList,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -130,6 +130,22 @@
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<p>
|
||||
{{ $t("share.defaultViewMode") }}
|
||||
<i
|
||||
class="no-select material-symbols-outlined tooltip-info-icon"
|
||||
@mouseenter="showTooltip($event, $t('share.defaultViewModeDescription'))"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
help
|
||||
</i>
|
||||
</p>
|
||||
<select class="input" v-model="viewMode">
|
||||
<option value="normal">{{ $t("buttons.normalView") }}</option>
|
||||
<option value="list">{{ $t("buttons.listView") }}</option>
|
||||
<option value="compact">{{ $t("buttons.compactView") }}</option>
|
||||
<option value="gallery">{{ $t("buttons.galleryView") }}</option>
|
||||
</select>
|
||||
<SettingsItem :title="$t('buttons.showMore')" :collapsable="true" :start-collapsed="true">
|
||||
<div class="settings-items">
|
||||
<ToggleSwitch class="item" v-model="keepAfterExpiration" :name="$t('share.keepAfterExpiration')" :description="$t('share.keepAfterExpirationDescription')" />
|
||||
@@ -255,7 +271,7 @@ import { fromNow } from "@/utils/moment";
|
||||
import { buildItemUrl, fixDownloadURL } from "@/utils/url";
|
||||
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
|
||||
import SettingsItem from "@/components/settings/SettingsItem.vue";
|
||||
import { userSelectableThemes, externalUrl, onlyOfficeUrl } from "@/utils/constants";
|
||||
import { globalVars } from "@/utils/constants";
|
||||
import { eventBus } from "@/store/eventBus";
|
||||
//import ViewMode from "@/components/settings/ViewMode.vue";
|
||||
|
||||
@@ -316,6 +332,7 @@ export default {
|
||||
disableShareCard: false,
|
||||
disableSidebar: false,
|
||||
enforceDarkLightMode: "default",
|
||||
viewMode: "normal",
|
||||
enableOnlyOffice: false,
|
||||
enableOnlyOfficeEditing: false,
|
||||
//viewMode: "normal",
|
||||
@@ -323,10 +340,10 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
onlyOfficeAvailable() {
|
||||
return onlyOfficeUrl !== "";
|
||||
return globalVars.onlyOfficeUrl !== "";
|
||||
},
|
||||
availableThemes() {
|
||||
return userSelectableThemes || {};
|
||||
return globalVars.userSelectableThemes || {};
|
||||
},
|
||||
closeHovers() {
|
||||
return mutations.closeHovers;
|
||||
@@ -397,6 +414,7 @@ export default {
|
||||
this.disableShareCard = this.link.disableShareCard || false;
|
||||
this.disableSidebar = this.link.disableSidebar || false;
|
||||
this.enforceDarkLightMode = this.link.enforceDarkLightMode || "default";
|
||||
this.viewMode = this.link.viewMode || "normal";
|
||||
this.enableOnlyOffice = this.link.enableOnlyOffice || false;
|
||||
this.enableOnlyOfficeEditing = this.link.enableOnlyOfficeEditing || false;
|
||||
//this.viewMode = this.link.viewMode || "normal";
|
||||
@@ -478,6 +496,7 @@ export default {
|
||||
disableShareCard: this.disableShareCard,
|
||||
disableSidebar: this.disableSidebar,
|
||||
enforceDarkLightMode: this.enforceDarkLightMode,
|
||||
viewMode: this.viewMode,
|
||||
enableOnlyOffice: this.enableOnlyOffice,
|
||||
enableOnlyOfficeEditing: this.enableOnlyOfficeEditing,
|
||||
};
|
||||
@@ -543,7 +562,7 @@ export default {
|
||||
* @param {Share} share
|
||||
*/
|
||||
buildDownloadLink(share) {
|
||||
if (share.downloadURL && externalUrl == "") {
|
||||
if (share.downloadURL && globalVars.externalUrl == "") {
|
||||
return this.fixDownloadURL(share.downloadURL);
|
||||
}
|
||||
return publicApi.getDownloadURL(share, [this.item.name]);
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
? $t('prompts.conflictsDetected')
|
||||
: (file.progress / 100) * file.size
|
||||
" :unit="file.status === 'completed' || file.status === 'error' ? '' : 'bytes'" :max="file.size"
|
||||
:status="file.status" text-position="inside" size="20">
|
||||
:status="file.status" text-position="inside" size="20"
|
||||
:help-text="file.status === 'error' && file.errorDetails ? file.errorDetails : ''">
|
||||
</progress-bar>
|
||||
<div v-else class="status-label">
|
||||
<span>{{ $t(`prompts.${file.status}`) }}</span>
|
||||
@@ -94,7 +95,7 @@
|
||||
:aria-label="$t('buttons.resumeAll')" :title="$t('buttons.resumeAll')">
|
||||
{{ $t("buttons.resumeAll") }}
|
||||
</button>
|
||||
<button @click="clearCompleted" class="button button--flat" :disabled="!hasCompleted"
|
||||
<button @click="clearCompleted" class="button button--flat" :disabled="!hasClearable"
|
||||
:aria-label="$t('buttons.clearCompleted')" :title="$t('buttons.clearCompleted')">
|
||||
{{ $t("buttons.clearCompleted") }}
|
||||
</button>
|
||||
@@ -184,7 +185,7 @@ export default {
|
||||
console.error("No conflicting folder found for rename");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
mutations.showHover({
|
||||
name: "rename",
|
||||
confirm: (newName) => {
|
||||
@@ -205,7 +206,7 @@ export default {
|
||||
|
||||
// Update upload manager with the new folder name
|
||||
await uploadManager.renameFolder(oldName, newName);
|
||||
|
||||
|
||||
// Resolve the conflict and continue upload
|
||||
if (conflictResolver) {
|
||||
conflictResolver({ rename: newName });
|
||||
@@ -256,6 +257,21 @@ export default {
|
||||
files.value.some((file) => file.status === "completed")
|
||||
);
|
||||
|
||||
const hasClearable = computed(() => {
|
||||
if (state.user.fileLoading?.clearAll) {
|
||||
// For "clear all" mode: check for completed, error, conflict, or paused uploads
|
||||
return files.value.some((file) =>
|
||||
file.status === "completed" ||
|
||||
file.status === "error" ||
|
||||
file.status === "conflict" ||
|
||||
file.status === "paused"
|
||||
);
|
||||
} else {
|
||||
// For "clear completed" mode: only check for completed uploads
|
||||
return files.value.some((file) => file.status === "completed");
|
||||
}
|
||||
});
|
||||
|
||||
const canPauseAll = computed(() =>
|
||||
files.value.some((file) => file.status === "uploading")
|
||||
);
|
||||
@@ -447,6 +463,7 @@ export default {
|
||||
close,
|
||||
clearCompleted,
|
||||
hasCompleted,
|
||||
hasClearable,
|
||||
showConflictPrompt,
|
||||
resolveConflict,
|
||||
showRenamePrompt,
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<i class="material-icons">sentiment_dissatisfied</i>
|
||||
<span>{{ $t("files.lonely") }}</span>
|
||||
</h2>
|
||||
<div v-if="user.loginMethod == 'password' && passwordAvailable && !isNew">
|
||||
<div v-if="user.loginMethod == 'password' && globalVars.passwordAvailable && !isNew">
|
||||
<label for="password">{{ $t("settings.password") }}</label>
|
||||
<div class="form-flex-group">
|
||||
<input
|
||||
@@ -63,7 +63,7 @@
|
||||
/>
|
||||
</p>
|
||||
|
||||
<div v-if="user.loginMethod == 'password' && passwordAvailable && isNew">
|
||||
<div v-if="user.loginMethod == 'password' && globalVars.passwordAvailable && isNew">
|
||||
<label for="password">{{ $t("settings.password") }}</label>
|
||||
<div class="form-flex-group">
|
||||
<input
|
||||
@@ -98,7 +98,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="user.loginMethod == 'password' && passwordAvailable"
|
||||
v-if="user.loginMethod == 'password' && globalVars.passwordAvailable"
|
||||
class="settings-items"
|
||||
>
|
||||
<ToggleSwitch
|
||||
@@ -186,7 +186,7 @@ import { mutations, state } from "@/store";
|
||||
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
|
||||
import { notify } from "@/notify";
|
||||
import { usersApi, settingsApi } from "@/api";
|
||||
import { passwordAvailable } from "@/utils/constants";
|
||||
import { globalVars } from "@/utils/constants";
|
||||
|
||||
export default {
|
||||
name: "UserForm",
|
||||
@@ -246,7 +246,8 @@ export default {
|
||||
this.user.password != this.passwordRef && this.user.password.length > 0;
|
||||
return matching;
|
||||
},
|
||||
passwordAvailable: () => passwordAvailable,
|
||||
passwordAvailable: () => globalVars.passwordAvailable,
|
||||
globalVars: () => globalVars,
|
||||
duplicateSources() {
|
||||
const names = this.selectedSources.map((s) => s.name);
|
||||
return names.filter((name, idx) => names.indexOf(name) !== idx);
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
|
||||
<script>
|
||||
import * as auth from "@/utils/auth";
|
||||
import { signup, disableExternal, noAuth, loginPage, shareInfo } from "@/utils/constants";
|
||||
import { globalVars, shareInfo } from "@/utils/constants";
|
||||
import ProgressBar from "@/components/ProgressBar.vue";
|
||||
import { state, getters, mutations } from "@/store"; // Import your custom store
|
||||
import { getHumanReadableFilesize } from "@/utils/filesizes.js";
|
||||
@@ -170,11 +170,11 @@ export default {
|
||||
showSources: () => !getters.isShare(),
|
||||
currentPrompt: () => getters.currentPrompt(),
|
||||
active: () => getters.isSidebarVisible(),
|
||||
signup: () => signup,
|
||||
version: () => version,
|
||||
commitSHA: () => commitSHA,
|
||||
disableExternal: () => disableExternal,
|
||||
canLogout: () => !noAuth && loginPage,
|
||||
signup: () => globalVars.signup,
|
||||
version: () => globalVars.version,
|
||||
commitSHA: () => globalVars.commitSHA,
|
||||
disableExternal: () => globalVars.disableExternal,
|
||||
canLogout: () => !globalVars.noAuth && globalVars.loginPage,
|
||||
route: () => state.route,
|
||||
sourceInfo: () => state.sources.info,
|
||||
activeSource: () => state.sources.current,
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { externalLinks, name, updateAvailable } from "@/utils/constants";
|
||||
import { globalVars } from "@/utils/constants";
|
||||
import { getters, mutations, state } from "@/store"; // Import your custom store
|
||||
import SidebarGeneral from "./General.vue";
|
||||
import SidebarSettings from "./Settings.vue";
|
||||
@@ -46,19 +46,16 @@ export default {
|
||||
SidebarSettings,
|
||||
SidebarShare,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
externalLinks,
|
||||
name,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Ensure the sidebar is initialized correctly
|
||||
mutations.setSeenUpdate(localStorage.getItem("seenUpdate"));
|
||||
},
|
||||
computed: {
|
||||
externalLinks: () => globalVars.externalLinks,
|
||||
name: () => globalVars.name,
|
||||
isValidShare: () => getters.isValidShare(),
|
||||
releaseUrl: () => updateAvailable,
|
||||
releaseUrl: () => globalVars.updateAvailable,
|
||||
isDarkMode: () => getters.isDarkMode(),
|
||||
isLoggedIn: () => getters.isLoggedIn(),
|
||||
isSettings: () => getters.isSettings(),
|
||||
@@ -66,9 +63,9 @@ export default {
|
||||
behindOverlay: () => state.isSearchActive,
|
||||
shouldShow() {
|
||||
return (
|
||||
updateAvailable != "" &&
|
||||
globalVars.updateAvailable != "" &&
|
||||
state.user.permissions.admin &&
|
||||
state.seenUpdate != updateAvailable &&
|
||||
state.seenUpdate != globalVars.updateAvailable &&
|
||||
!state.user.disableUpdateNotifications
|
||||
);
|
||||
},
|
||||
@@ -79,7 +76,7 @@ export default {
|
||||
mutations.showHover("help");
|
||||
},
|
||||
setSeenUpdate() {
|
||||
mutations.setSeenUpdate(updateAvailable);
|
||||
mutations.setSeenUpdate(globalVars.updateAvailable);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
body {
|
||||
body {
|
||||
font-family: "Roboto", sans-serif;
|
||||
padding-top: 4em;
|
||||
background-color: var(--background);
|
||||
|
||||
@@ -155,15 +155,6 @@
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
/* File list item */
|
||||
.dark-mode .file-list li:hover {
|
||||
background: var(--surfaceSecondary);
|
||||
}
|
||||
|
||||
.dark-mode .file-list li:before {
|
||||
color: var(--textSecondary);
|
||||
}
|
||||
|
||||
/* Shell */
|
||||
.dark-mode .shell {
|
||||
background: var(--surfacePrimary);
|
||||
|
||||
@@ -164,7 +164,6 @@ table tr>*:last-child {
|
||||
background-color: var(--background);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);
|
||||
overflow: auto;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
@@ -176,8 +175,11 @@ table tr>*:last-child {
|
||||
z-index: 5;
|
||||
max-width: 40em;
|
||||
width: 90%;
|
||||
max-height: 95%;
|
||||
max-height: 80vh; /* Constrain modal height for scrolling */
|
||||
animation: .1s show forwards;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card>*>*:first-child {
|
||||
@@ -188,6 +190,12 @@ table tr>*:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Apply flex behavior only to direct children of floating cards */
|
||||
.card.floating > .card-title {
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -220,6 +228,19 @@ body.rtl .card-title>*:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Apply flex behavior only to direct children of floating cards */
|
||||
.card.floating > .card-action {
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.card.floating > .card-content {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
min-height: 0; /* Allows flex child to shrink below content size */
|
||||
}
|
||||
|
||||
/* Regular card styles (non-floating) */
|
||||
.card .card-action {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -251,46 +272,6 @@ body.rtl .card-title>*:first-child {
|
||||
max-width: 15em;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-list li {
|
||||
width: 100%;
|
||||
user-select: none;
|
||||
border-radius: .2em;
|
||||
padding: .3em;
|
||||
}
|
||||
|
||||
.file-list li[aria-selected=true] {
|
||||
background: var(--primaryColor) !important;
|
||||
color: #fff !important;
|
||||
transition: .1s ease all;
|
||||
}
|
||||
|
||||
.file-list li:hover {
|
||||
background-color: #e9eaeb;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-list li:before {
|
||||
content: "folder";
|
||||
color: #6f6f6f;
|
||||
vertical-align: middle;
|
||||
line-height: 1.4;
|
||||
font-family: 'Material Icons';
|
||||
font-size: 1.75em;
|
||||
margin-right: .25em;
|
||||
}
|
||||
|
||||
.file-list li[aria-selected=true]:before {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.help {
|
||||
max-width: 24em;
|
||||
@@ -421,4 +402,4 @@ body.rtl .credits {
|
||||
z-index: 4;
|
||||
top: 0;
|
||||
animation: .3s show ease-in;
|
||||
}
|
||||
}
|
||||
@@ -335,10 +335,6 @@ body.rtl .card-content .small + input {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
body.rtl .card.floating .card-content .file-list {
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.fb-shadow {
|
||||
box-shadow: 0px 0px 15px 0px rgb(100 100 100 / 75%);
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "اعرض المزيد",
|
||||
"showLess": "اعرض أقل",
|
||||
"openParentFolder": "فتح المجلد الأصل",
|
||||
"selectedCount": "عدد العناصر المحددة لتنفيذ إجراء ما"
|
||||
"selectedCount": "عدد العناصر المحددة لتنفيذ إجراء ما",
|
||||
"listView": "عرض القائمة",
|
||||
"compactView": "عرض مدمج",
|
||||
"normalView": "عرض عادي",
|
||||
"galleryView": "عرض المعرض"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Download File",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "عدد الملفات{suffix}",
|
||||
"renameMessageConflict": "عنصر باسم \"{filename}\" موجود بالفعل!",
|
||||
"uploadSettingsChunked": "الحد الأقصى للتحميلات المتزامنة: {maxConcurrentUpload} ، حجم القطعة: {uploadChunkSizeMb} ميغابايت",
|
||||
"uploadSettingsNoChunk": "الحد الأقصى للتحميلات المتزامنة: {maxConcurrentUpload} ، تم تعطيل التحميل المقطّع"
|
||||
"uploadSettingsNoChunk": "الحد الأقصى للتحميلات المتزامنة: {maxConcurrentUpload} ، تم تعطيل التحميل المقطّع",
|
||||
"quickJump": "الانتقال بسرعة إلى ملفات أخرى",
|
||||
"moveSuccess": "تم نقل الملفات بنجاح!",
|
||||
"copySuccess": "تم نسخ الملفات بنجاح!"
|
||||
},
|
||||
"search": {
|
||||
"images": "الصور",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "السماح بعرض ملفات المكتب باستخدام OnlyOffice في هذه المشاركة.",
|
||||
"enableOnlyOfficeEditing": "تمكين التحرير في المكتب فقط",
|
||||
"enableOnlyOfficeEditingDescription": "السماح بتحرير الملفات المكتبية باستخدام OnlyOffice في هذه الحصة.",
|
||||
"titleDefault": "الملفات المشتركة - {title}"
|
||||
"titleDefault": "الملفات المشتركة - {title}",
|
||||
"defaultViewMode": "وضع العرض الافتراضي",
|
||||
"defaultViewModeDescription": "تعيين وضع العرض الافتراضي للمستخدمين المجهولين الذين يشاهدون هذه المشاركة. لا يزال بإمكان المستخدمين المسجلين الدخول تغيير تفضيلات العرض الخاصة بهم."
|
||||
},
|
||||
"api": {
|
||||
"title": "مفاتيح واجهة برمجة التطبيقات (API):",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "الحد الأقصى للتحميلات المتزامنة",
|
||||
"maxConcurrentUploadHelp": "الحد الأقصى لعدد التحميلات المتزامنة. عادةً ما يكون أداء القيم الأعلى أفضل، ولكن هذا يعتمد على العديد من العوامل.",
|
||||
"uploadChunkSizeMb": "حجم قطعة التحميل بالميجابايت",
|
||||
"uploadChunkSizeMbHelp": "عادةً من 5 إلى 50 ميغابايت، والأصغر هو الأفضل للاتصالات غير المستقرة أو المقيدة. سيؤدي التعيين إلى 0 إلى تعطيل تقطيع التحميل."
|
||||
"uploadChunkSizeMbHelp": "عادةً من 5 إلى 50 ميغابايت، والأصغر هو الأفضل للاتصالات غير المستقرة أو المقيدة. سيؤدي التعيين إلى 0 إلى تعطيل تقطيع التحميل.",
|
||||
"clearAll": "يجب أن يزيل المسح المكتمل الخطأ والإيقاف المؤقت والتعارض",
|
||||
"clearAllDescription": "سيؤدي زر مسح المكتمل إلى مسح جميع التحميلات في حالة الانتهاء، بما في ذلك حالات الخطأ والإيقاف المؤقت والتعارض."
|
||||
},
|
||||
"profileSettings": {
|
||||
"setDateFormat": "تعيين تنسيق التاريخ الدقيق",
|
||||
@@ -539,7 +550,9 @@
|
||||
"debugOfficeEditor": "تمكين وضع التصحيح فقط",
|
||||
"debugOfficeEditorDescription": "إظهار نافذة منبثقة للتصحيح مع معلومات إضافية في محرر OnlyOffice.",
|
||||
"fileViewerOptions": "خيارات عارض الملفات",
|
||||
"themeAndLanguage": "الموضوع واللغة"
|
||||
"themeAndLanguage": "الموضوع واللغة",
|
||||
"previewFolder": "إظهار الصور المصغرة للمجلدات إذا كانت متوفرة",
|
||||
"previewFolderDescription": "إظهار معاينات الصور المصغرة للمجلدات التي تحتوي على العنصر الفرعي الأول مع صورة مصغرة كغلاف."
|
||||
},
|
||||
"colors": {
|
||||
"red": "أحمر",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Zobrazit více",
|
||||
"showLess": "Zobrazit méně",
|
||||
"openParentFolder": "Otevření nadřazené složky",
|
||||
"selectedCount": "Počet položek vybraných k provedení akce"
|
||||
"selectedCount": "Počet položek vybraných k provedení akce",
|
||||
"listView": "Zobrazení seznamu",
|
||||
"compactView": "Kompaktní zobrazení",
|
||||
"normalView": "Normální zobrazení",
|
||||
"galleryView": "Pohled do galerie"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Stáhnout soubor",
|
||||
@@ -176,7 +180,9 @@
|
||||
"enableOnlyOfficeDescription": "Povolit prohlížení kancelářských souborů pomocí OnlyOffice v této sdílené složce.",
|
||||
"enableOnlyOfficeEditing": "Povolení úprav OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Povolit úpravy kancelářských souborů pomocí OnlyOffice v této sdílené složce.",
|
||||
"titleDefault": "Sdílené soubory - {title}"
|
||||
"titleDefault": "Sdílené soubory - {title}",
|
||||
"defaultViewMode": "Výchozí režim zobrazení",
|
||||
"defaultViewModeDescription": "Nastavení výchozího režimu zobrazení pro anonymní uživatele prohlížející tuto sdílenou složku. Přihlášení uživatelé mohou stále měnit předvolby zobrazení."
|
||||
},
|
||||
"prompts": {
|
||||
"copy": "Kopírovat",
|
||||
@@ -234,7 +240,10 @@
|
||||
"numberFiles": "Počet souborů{suffix}",
|
||||
"renameMessageConflict": "Položka s názvem \"{filename}\" již existuje!",
|
||||
"uploadSettingsChunked": "Maximální souběžné nahrávání: {maxConcurrentUpload}, velikost chunků: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Maximální souběžné nahrávání: {maxConcurrentUpload}, vypnuto hromadné nahrávání"
|
||||
"uploadSettingsNoChunk": "Maximální souběžné nahrávání: {maxConcurrentUpload}, vypnuto hromadné nahrávání",
|
||||
"quickJump": "Rychlý přechod na jiné soubory",
|
||||
"moveSuccess": "Soubory byly úspěšně přesunuty!",
|
||||
"copySuccess": "Soubory byly úspěšně zkopírovány!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Obrázky",
|
||||
@@ -317,7 +326,9 @@
|
||||
"debugOfficeEditor": "Povolení režimu ladění OnlyOffice",
|
||||
"debugOfficeEditorDescription": "Zobrazení vyskakovacího okna pro ladění s dalšími informacemi v editoru OnlyOffice.",
|
||||
"fileViewerOptions": "Možnosti prohlížeče souborů",
|
||||
"themeAndLanguage": "Téma a jazyk"
|
||||
"themeAndLanguage": "Téma a jazyk",
|
||||
"previewFolder": "Zobrazení miniatur složek, pokud jsou k dispozici",
|
||||
"previewFolderDescription": "Zobrazení náhledů miniatur pro složky s první podřízenou položkou s miniaturou jako obálkou."
|
||||
},
|
||||
"settings": {
|
||||
"enterPassword": "Zadejte heslo",
|
||||
@@ -503,7 +514,9 @@
|
||||
"maxConcurrentUpload": "Maximální souběžné nahrávání",
|
||||
"maxConcurrentUploadHelp": "Maximální počet souběžných odesílání. Vyšší hodnoty jsou obvykle výkonnější, ale záleží na mnoha faktorech.",
|
||||
"uploadChunkSizeMb": "Velikost nahraného souboru v MB",
|
||||
"uploadChunkSizeMbHelp": "Obvykle 5 až 50 MB, menší velikost je lepší pro nestabilní nebo omezené připojení. Nastavením na hodnotu 0 se vypne rozdělování nahrávání."
|
||||
"uploadChunkSizeMbHelp": "Obvykle 5 až 50 MB, menší velikost je lepší pro nestabilní nebo omezené připojení. Nastavením na hodnotu 0 se vypne rozdělování nahrávání.",
|
||||
"clearAll": "Vymazání dokončeno by mělo odstranit chybu, pauzu a konflikt.",
|
||||
"clearAllDescription": "Tlačítko Vymazat dokončené vymaže všechna odesílání v dokončeném stavu, včetně chybových, pozastavených a konfliktních stavů."
|
||||
},
|
||||
"colors": {
|
||||
"red": "červená",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Mehr anzeigen",
|
||||
"showLess": "Weniger anzeigen",
|
||||
"openParentFolder": "Übergeordneten Ordner öffnen",
|
||||
"selectedCount": "Anzahl der zur Durchführung einer Aktion ausgewählten Elemente"
|
||||
"selectedCount": "Anzahl der zur Durchführung einer Aktion ausgewählten Elemente",
|
||||
"listView": "Listenansicht",
|
||||
"compactView": "Kompakte Ansicht",
|
||||
"normalView": "Normale Ansicht",
|
||||
"galleryView": "Galerie-Ansicht"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Datei herunterladen",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Anzahl der Dateien{suffix}",
|
||||
"renameMessageConflict": "Ein Artikel mit dem Namen \"{filename}\" existiert bereits!",
|
||||
"uploadSettingsChunked": "Max. gleichzeitige Uploads: {maxConcurrentUpload}, Chunk-Größe: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Max. gleichzeitige Uploads: {maxConcurrentUpload}, Chunked Upload deaktiviert"
|
||||
"uploadSettingsNoChunk": "Max. gleichzeitige Uploads: {maxConcurrentUpload}, Chunked Upload deaktiviert",
|
||||
"quickJump": "Schnelles Springen zu anderen Dateien",
|
||||
"moveSuccess": "Dateien erfolgreich verschoben!",
|
||||
"copySuccess": "Dateien erfolgreich kopiert!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Bilder",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Erlauben Sie die Anzeige von Office-Dateien mit OnlyOffice in dieser Freigabe.",
|
||||
"enableOnlyOfficeEditing": "OnlyOffice-Bearbeitung aktivieren",
|
||||
"enableOnlyOfficeEditingDescription": "Erlauben Sie die Bearbeitung von Office-Dateien mit OnlyOffice in dieser Freigabe.",
|
||||
"titleDefault": "Gemeinsame Dateien - {title}"
|
||||
"titleDefault": "Gemeinsame Dateien - {title}",
|
||||
"defaultViewMode": "Standard-Ansichtsmodus",
|
||||
"defaultViewModeDescription": "Legen Sie den Standardanzeigemodus für anonyme Benutzer fest, die diese Freigabe anzeigen. Angemeldete Benutzer können ihre Ansichtseinstellungen immer noch ändern."
|
||||
},
|
||||
"api": {
|
||||
"title": "API-Schlüssel:",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "Max. gleichzeitige Uploads",
|
||||
"maxConcurrentUploadHelp": "Die maximale Anzahl der gleichzeitigen Uploads. Höhere Werte führen in der Regel zu einer besseren Leistung, aber dies hängt von vielen Faktoren ab.",
|
||||
"uploadChunkSizeMb": "Größe des hochzuladenden Pakets in MB",
|
||||
"uploadChunkSizeMbHelp": "Normalerweise 5 bis 50 MB, kleiner ist besser für instabile oder eingeschränkte Verbindungen. Die Einstellung 0 deaktiviert das Chunking beim Hochladen."
|
||||
"uploadChunkSizeMbHelp": "Normalerweise 5 bis 50 MB, kleiner ist besser für instabile oder eingeschränkte Verbindungen. Die Einstellung 0 deaktiviert das Chunking beim Hochladen.",
|
||||
"clearAll": "Clear completed sollte Fehler, Pausen und Konflikte beseitigen",
|
||||
"clearAllDescription": "Mit der Schaltfläche Abgeschlossen löschen werden alle Uploads im abgeschlossenen Zustand gelöscht, einschließlich Fehler-, Pausen- und Konfliktstatus."
|
||||
},
|
||||
"colors": {
|
||||
"red": "rot",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "OnlyOffice-Debug-Modus einschalten",
|
||||
"debugOfficeEditorDescription": "Ein Debug-Popup mit zusätzlichen Informationen im OnlyOffice-Editor anzeigen.",
|
||||
"fileViewerOptions": "Optionen des Dateibetrachters",
|
||||
"themeAndLanguage": "Thema und Sprache"
|
||||
"themeAndLanguage": "Thema und Sprache",
|
||||
"previewFolder": "Miniaturansichten für Ordner anzeigen, falls vorhanden",
|
||||
"previewFolderDescription": "Zeigen Sie Miniaturvorschauen für Ordner an, deren erstes untergeordnetes Element ein Miniaturbild als Cover hat."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Der Editor konnte nicht initialisiert werden. Bitte neu laden.",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Εμφάνιση περισσότερων",
|
||||
"showLess": "Εμφάνιση λιγότερων",
|
||||
"openParentFolder": "Άνοιγμα γονικού φακέλου",
|
||||
"selectedCount": "Αριθμός επιλεγμένων στοιχείων για την εκτέλεση μιας ενέργειας"
|
||||
"selectedCount": "Αριθμός επιλεγμένων στοιχείων για την εκτέλεση μιας ενέργειας",
|
||||
"listView": "Προβολή λίστας",
|
||||
"compactView": "Συμπαγής προβολή",
|
||||
"normalView": "Κανονική προβολή",
|
||||
"galleryView": "Προβολή γκαλερί"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Λήψη αρχείου",
|
||||
@@ -191,7 +195,10 @@
|
||||
"numberFiles": "Αριθμός αρχείων{suffix}",
|
||||
"renameMessageConflict": "Ένα στοιχείο με όνομα \"{filename}\" υπάρχει ήδη!",
|
||||
"uploadSettingsChunked": "Μέγιστες ταυτόχρονες μεταφορτώσεις: {maxConcurrentUpload}, μέγεθος τεμαχίου: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Μέγιστες ταυτόχρονες μεταφορτώσεις: {maxConcurrentUpload}, chunked upload disabled"
|
||||
"uploadSettingsNoChunk": "Μέγιστες ταυτόχρονες μεταφορτώσεις: {maxConcurrentUpload}, chunked upload disabled",
|
||||
"quickJump": "Γρήγορη μετάβαση σε άλλα αρχεία",
|
||||
"moveSuccess": "Τα αρχεία μετακινήθηκαν με επιτυχία!",
|
||||
"copySuccess": "Τα αρχεία αντιγράφηκαν με επιτυχία!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Εικόνες",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Επιτρέψτε την προβολή αρχείων γραφείου χρησιμοποιώντας το OnlyOffice σε αυτό το κοινόχρηστο χώρο.",
|
||||
"enableOnlyOfficeEditing": "Ενεργοποίηση της επεξεργασίας OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Επιτρέψτε την επεξεργασία αρχείων γραφείου χρησιμοποιώντας το OnlyOffice σε αυτό το κοινόχρηστο χώρο.",
|
||||
"titleDefault": "Κοινόχρηστα αρχεία - {title}"
|
||||
"titleDefault": "Κοινόχρηστα αρχεία - {title}",
|
||||
"defaultViewMode": "Προεπιλεγμένη λειτουργία προβολής",
|
||||
"defaultViewModeDescription": "Ορίστε την προεπιλεγμένη λειτουργία προβολής για τους ανώνυμους χρήστες που προβάλλουν αυτή την κοινή χρήση. Οι συνδεδεμένοι χρήστες μπορούν ακόμα να αλλάξουν τις προτιμήσεις προβολής τους."
|
||||
},
|
||||
"api": {
|
||||
"title": "Κλειδιά API:",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "Μέγιστες ταυτόχρονες μεταφορτώσεις",
|
||||
"maxConcurrentUploadHelp": "Ο μέγιστος αριθμός ταυτόχρονων μεταφορτώσεων. Υψηλότερες τιμές συνήθως έχουν καλύτερες επιδόσεις, αλλά αυτό εξαρτάται από πολλούς παράγοντες.",
|
||||
"uploadChunkSizeMb": "Μέγεθος τμήματος φόρτωσης σε MB",
|
||||
"uploadChunkSizeMbHelp": "Συνήθως 5 έως 50 MB, τα μικρότερα είναι καλύτερα για ασταθείς ή περιορισμένες συνδέσεις. Η ρύθμιση σε 0 θα απενεργοποιήσει την ομαδοποίηση μεταφόρτωσης."
|
||||
"uploadChunkSizeMbHelp": "Συνήθως 5 έως 50 MB, τα μικρότερα είναι καλύτερα για ασταθείς ή περιορισμένες συνδέσεις. Η ρύθμιση σε 0 θα απενεργοποιήσει την ομαδοποίηση μεταφόρτωσης.",
|
||||
"clearAll": "Η εκκαθάριση που ολοκληρώθηκε θα πρέπει να καταργήσει το σφάλμα, την παύση και τη σύγκρουση",
|
||||
"clearAllDescription": "Το κουμπί Clear completed θα διαγράψει όλες τις μεταφορτώσεις σε κατάσταση ολοκλήρωσης, συμπεριλαμβανομένων των καταστάσεων σφάλματος, παύσης και σύγκρουσης."
|
||||
},
|
||||
"colors": {
|
||||
"red": "κόκκινο",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "Ενεργοποίηση λειτουργίας εντοπισμού σφαλμάτων OnlyOffice",
|
||||
"debugOfficeEditorDescription": "Εμφάνιση ενός αναδυόμενου παραθύρου εντοπισμού σφαλμάτων με πρόσθετες πληροφορίες στον επεξεργαστή OnlyOffice.",
|
||||
"fileViewerOptions": "Επιλογές προβολής αρχείων",
|
||||
"themeAndLanguage": "Θέμα & Γλώσσα"
|
||||
"themeAndLanguage": "Θέμα & Γλώσσα",
|
||||
"previewFolder": "Εμφάνιση μικρογραφιών για φακέλους, εάν είναι διαθέσιμες",
|
||||
"previewFolderDescription": "Εμφάνιση προεπισκοπήσεων μικρογραφιών για φακέλους με το πρώτο στοιχείο-παιδί με μικρογραφία ως εξώφυλλο."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Απέτυχε η αρχικοποίηση του συντάκτη. Παρακαλούμε επαναφορτώστε.",
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
"showMore": "Show more",
|
||||
"showLess": "Show less",
|
||||
"openParentFolder": "Open parent folder",
|
||||
"listView": "List View",
|
||||
"compactView": "Compact View",
|
||||
"normalView": "Normal View",
|
||||
"galleryView": "Gallery View",
|
||||
"selectedCount": "Number of items selected to perform an action"
|
||||
},
|
||||
"download": {
|
||||
@@ -170,7 +174,9 @@
|
||||
"disableSidebarDescription": "Disable the sidebar on the shared page.",
|
||||
"enforceDarkLightMode": "Enforce theme mode",
|
||||
"enforceDarkLightModeDescription": "Force a specific theme mode (dark or light) for this share, overriding user preferences.",
|
||||
"default": "Do not enforce a mode",
|
||||
"defaultViewMode": "Default view mode",
|
||||
"defaultViewModeDescription": "Set the default view mode for anonymous users viewing this share. Logged-in users can still change their view preference.",
|
||||
"default": "Use user preference",
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"enableOnlyOffice": "Enable OnlyOffice viewer",
|
||||
@@ -179,6 +185,7 @@
|
||||
"enableOnlyOfficeEditingDescription": "Allow editing of office files using OnlyOffice in this share."
|
||||
},
|
||||
"prompts": {
|
||||
"quickJump": "Quickly jump to other files",
|
||||
"downloadsLimit": "Number of downloads limit",
|
||||
"maxBandwidth": "Max download bandwidth (kbps)",
|
||||
"shareTheme": "Share theme",
|
||||
@@ -209,6 +216,8 @@
|
||||
"filesSelected": "files selected",
|
||||
"lastModified": "Last Modified{suffix}",
|
||||
"move": "Move",
|
||||
"moveSuccess": "Files moved successfully!",
|
||||
"copySuccess": "Files copied successfully!",
|
||||
"moveMessage": "Choose new house for your file(s)/folder(s):",
|
||||
"newArchetype": "Create a new post based on an archetype. Your file will be created on content folder.",
|
||||
"newDir": "New directory",
|
||||
@@ -263,6 +272,8 @@
|
||||
"helpText2": "The index: Search utilizes the index which automatically gets updated on the configured interval (default: 5 minutes). Searching when the program has just started may result in incomplete results."
|
||||
},
|
||||
"profileSettings": {
|
||||
"previewFolder": "Show thumbnails for folders if available",
|
||||
"previewFolderDescription": "Show thumbnail previews for folders with first child item with thumbnail as cover.",
|
||||
"debugOfficeEditor": "Enable OnlyOffice Debug Mode",
|
||||
"debugOfficeEditorDescription": "Show a debug popup with additional information in the OnlyOffice editor.",
|
||||
"defaultMediaPlayer": "Use your browser's native media player",
|
||||
@@ -539,7 +550,9 @@
|
||||
"maxConcurrentUpload": "Max concurrent uploads",
|
||||
"maxConcurrentUploadHelp": "The maximum number of concurrent uploads. Higher values typically perform better, but this depends on many factors.",
|
||||
"uploadChunkSizeMb": "Upload chunk size in MB",
|
||||
"uploadChunkSizeMbHelp": "Typically 5 to 50 MB, smaller is better for unstable or restricted connections. Setting to 0 will disable upload chunking."
|
||||
"uploadChunkSizeMbHelp": "Typically 5 to 50 MB, smaller is better for unstable or restricted connections. Setting to 0 will disable upload chunking.",
|
||||
"clearAll": "Clear completed should remove error, pause, and conflict",
|
||||
"clearAllDescription": "The clear completed button will clear all uploads in a finished state, including error, paused, and conflict states."
|
||||
},
|
||||
"colors": {
|
||||
"red": "red",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Ver más",
|
||||
"showLess": "Mostrar menos",
|
||||
"openParentFolder": "Abrir carpeta principal",
|
||||
"selectedCount": "Número de elementos seleccionados para realizar una acción"
|
||||
"selectedCount": "Número de elementos seleccionados para realizar una acción",
|
||||
"listView": "Ver lista",
|
||||
"compactView": "Vista compacta",
|
||||
"normalView": "Vista normal",
|
||||
"galleryView": "Vista de la galería"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descargar archivo",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Número de ficheros{suffix}",
|
||||
"renameMessageConflict": "Ya existe un elemento llamado \"{filename}\".",
|
||||
"uploadSettingsChunked": "Carga máxima simultánea: {maxConcurrentUpload} Tamaño del chunk {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Carga máxima simultánea: {maxConcurrentUpload} Carga por trozos desactivada"
|
||||
"uploadSettingsNoChunk": "Carga máxima simultánea: {maxConcurrentUpload} Carga por trozos desactivada",
|
||||
"quickJump": "Saltar rápidamente a otros archivos",
|
||||
"moveSuccess": "¡Archivos movidos con éxito!",
|
||||
"copySuccess": "¡Archivos copiados correctamente!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Imágenes",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Permitir la visualización de archivos de oficina utilizando OnlyOffice en este recurso compartido.",
|
||||
"enableOnlyOfficeEditing": "Habilitar la edición de OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Permitir la edición de archivos de oficina utilizando OnlyOffice en este recurso compartido.",
|
||||
"titleDefault": "Archivos compartidos - {title}"
|
||||
"titleDefault": "Archivos compartidos - {title}",
|
||||
"defaultViewMode": "Modo de vista por defecto",
|
||||
"defaultViewModeDescription": "Establece el modo de visualización predeterminado para los usuarios anónimos que vean esta acción. Los usuarios registrados pueden cambiar su preferencia de visualización."
|
||||
},
|
||||
"api": {
|
||||
"title": "Claves API",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "Cargas máximas simultáneas",
|
||||
"maxConcurrentUploadHelp": "El número máximo de archivos a subir a la vez. Los valores más altos suelen dar mejores resultados, pero esto depende de muchos factores",
|
||||
"uploadChunkSizeMb": "Velocidad de subida (en MB)",
|
||||
"uploadChunkSizeMbHelp": "Típicamente 5 a 50 MB, más pequeño es mejor para conexiones inestables. Si se pone a 0, se desactiva la subida de archivo en partes"
|
||||
"uploadChunkSizeMbHelp": "Típicamente 5 a 50 MB, más pequeño es mejor para conexiones inestables. Si se pone a 0, se desactiva la subida de archivo en partes",
|
||||
"clearAll": "El borrado completado debería eliminar el error, la pausa y el conflicto",
|
||||
"clearAllDescription": "El botón de borrado de cargas completadas borrará todas las cargas en estado finalizado, incluidos los estados de error, pausa y conflicto."
|
||||
},
|
||||
"colors": {
|
||||
"red": "Rojo",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "Activar el modo de depuración de OnlyOffice",
|
||||
"debugOfficeEditorDescription": "Mostrar una ventana emergente de depuración con información adicional en el editor OnlyOffice.",
|
||||
"fileViewerOptions": "Opciones del visor de archivos",
|
||||
"themeAndLanguage": "Tema y lengua"
|
||||
"themeAndLanguage": "Tema y lengua",
|
||||
"previewFolder": "Mostrar miniaturas de las carpetas si están disponibles",
|
||||
"previewFolderDescription": "Mostrar vistas previas en miniatura para carpetas con el primer elemento hijo con miniatura como portada."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Error al iniciar el editor, inténtelo de nuevo",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Afficher plus",
|
||||
"showLess": "Montrer moins",
|
||||
"openParentFolder": "Ouvrir le dossier parent",
|
||||
"selectedCount": "Nombre d'éléments sélectionnés pour effectuer une action"
|
||||
"selectedCount": "Nombre d'éléments sélectionnés pour effectuer une action",
|
||||
"listView": "Vue de la liste",
|
||||
"compactView": "Vue compacte",
|
||||
"normalView": "Vue normale",
|
||||
"galleryView": "Vue de la galerie"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Télécharger le fichier",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Nombre de fichiers{suffix}",
|
||||
"renameMessageConflict": "Un article nommé \"{filename}\" existe déjà !",
|
||||
"uploadSettingsChunked": "Nombre maximum de téléchargements simultanés : {maxConcurrentUpload}, taille de chunk : {uploadChunkSizeMb} MO",
|
||||
"uploadSettingsNoChunk": "Nombre maximum de téléchargements simultanés : {maxConcurrentUpload}, chunked upload désactivé"
|
||||
"uploadSettingsNoChunk": "Nombre maximum de téléchargements simultanés : {maxConcurrentUpload}, chunked upload désactivé",
|
||||
"quickJump": "Passer rapidement à d'autres fichiers",
|
||||
"moveSuccess": "Les fichiers ont été déplacés avec succès !",
|
||||
"copySuccess": "Les fichiers ont été copiés avec succès !"
|
||||
},
|
||||
"search": {
|
||||
"images": "Images",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Autoriser la visualisation de fichiers bureautiques en utilisant OnlyOffice dans ce partage.",
|
||||
"enableOnlyOfficeEditing": "Activer l'édition OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Autoriser l'édition de fichiers bureautiques en utilisant OnlyOffice dans ce partage.",
|
||||
"titleDefault": "Fichiers partagés - {title}"
|
||||
"titleDefault": "Fichiers partagés - {title}",
|
||||
"defaultViewMode": "Mode d'affichage par défaut",
|
||||
"defaultViewModeDescription": "Définit le mode d'affichage par défaut pour les utilisateurs anonymes qui consultent ce partage. Les utilisateurs connectés peuvent toujours modifier leurs préférences d'affichage."
|
||||
},
|
||||
"api": {
|
||||
"title": "Clés API :",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "Nombre maximum de téléchargements simultanés",
|
||||
"maxConcurrentUploadHelp": "Nombre maximal de téléchargements simultanés. Des valeurs plus élevées sont généralement plus performantes, mais cela dépend de nombreux facteurs.",
|
||||
"uploadChunkSizeMb": "Taille de la zone de téléchargement en Mo",
|
||||
"uploadChunkSizeMbHelp": "Généralement de 5 à 50 Mo, une taille inférieure est préférable pour les connexions instables ou restreintes. La valeur 0 désactive le fractionnement de l'upload."
|
||||
"uploadChunkSizeMbHelp": "Généralement de 5 à 50 Mo, une taille inférieure est préférable pour les connexions instables ou restreintes. La valeur 0 désactive le fractionnement de l'upload.",
|
||||
"clearAll": "Clear completed devrait supprimer les erreurs, les pauses et les conflits.",
|
||||
"clearAllDescription": "Le bouton \"Effacer les téléchargements terminés\" efface tous les téléchargements terminés, y compris les erreurs, les pauses et les conflits."
|
||||
},
|
||||
"colors": {
|
||||
"red": "rouge",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "Activer le mode de débogage d'OnlyOffice",
|
||||
"debugOfficeEditorDescription": "Afficher une fenêtre de débogage avec des informations supplémentaires dans l'éditeur OnlyOffice.",
|
||||
"fileViewerOptions": "Options de visualisation des fichiers",
|
||||
"themeAndLanguage": "Thème et langue"
|
||||
"themeAndLanguage": "Thème et langue",
|
||||
"previewFolder": "Afficher les vignettes des dossiers si elles sont disponibles",
|
||||
"previewFolderDescription": "Afficher des aperçus de vignettes pour les dossiers dont le premier élément enfant a une vignette comme couverture."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Échec de l'initialisation de l'éditeur. Veuillez recharger.",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "הצג עוד",
|
||||
"showLess": "הצג פחות",
|
||||
"openParentFolder": "פתח את התיקייה הראשית",
|
||||
"selectedCount": "מספר הפריטים שנבחרו לביצוע פעולה"
|
||||
"selectedCount": "מספר הפריטים שנבחרו לביצוע פעולה",
|
||||
"listView": "תצוגת רשימה",
|
||||
"compactView": "תצוגה קומפקטית",
|
||||
"normalView": "תצוגה רגילה",
|
||||
"galleryView": "תצוגת גלריה"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "הורד קובץ",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "מספר הקבצים{suffix}",
|
||||
"renameMessageConflict": "פריט בשם \"{filename}\" כבר קיים!",
|
||||
"uploadSettingsChunked": "מספר העלאות מקסימלי בו-זמנית: {maxConcurrentUpload}, גודל קובץ: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "מספר העלאות מקסימלי בו-זמנית: {maxConcurrentUpload}, העלאה מקוטעת מושבתת"
|
||||
"uploadSettingsNoChunk": "מספר העלאות מקסימלי בו-זמנית: {maxConcurrentUpload}, העלאה מקוטעת מושבתת",
|
||||
"quickJump": "קפיצה מהירה לקבצים אחרים",
|
||||
"moveSuccess": "הקבצים הועברו בהצלחה!",
|
||||
"copySuccess": "הקבצים הועתקו בהצלחה!"
|
||||
},
|
||||
"search": {
|
||||
"images": "תמונות",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "אפשר צפייה בקבצי Office באמצעות OnlyOffice בשיתוף זה.",
|
||||
"enableOnlyOfficeEditing": "אפשר עריכה ב-OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "אפשר עריכת קבצי Office באמצעות OnlyOffice בשיתוף זה.",
|
||||
"titleDefault": "קבצים משותפים - {title}"
|
||||
"titleDefault": "קבצים משותפים - {title}",
|
||||
"defaultViewMode": "מצב תצוגה ברירת מחדל",
|
||||
"defaultViewModeDescription": "הגדר את מצב התצוגה המוגדר כברירת מחדל עבור משתמשים אנונימיים הצופים בשיתוף זה. משתמשים מחוברים עדיין יכולים לשנות את העדפות התצוגה שלהם."
|
||||
},
|
||||
"api": {
|
||||
"title": "מפתחות API",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "העלאות מקסימליות בו-זמנית",
|
||||
"maxConcurrentUploadHelp": "המספר המרבי של העלאות בו-זמנית. ערכים גבוהים יותר בדרך כלל מספקים ביצועים טובים יותר, אך הדבר תלוי בגורמים רבים.",
|
||||
"uploadChunkSizeMb": "גודל הקובץ להעלאה במגה-בייט",
|
||||
"uploadChunkSizeMbHelp": "בדרך כלל 5 עד 50 MB, קטן יותר עדיף עבור חיבורים לא יציבים או מוגבלים. הגדרה ל-0 תשבית את חלוקת הקבצים להעלאה."
|
||||
"uploadChunkSizeMbHelp": "בדרך כלל 5 עד 50 MB, קטן יותר עדיף עבור חיבורים לא יציבים או מוגבלים. הגדרה ל-0 תשבית את חלוקת הקבצים להעלאה.",
|
||||
"clearAll": "השלמת הניקוי אמורה להסיר את השגיאה, ההשהיה והקונפליקט.",
|
||||
"clearAllDescription": "הכפתור 'נקה את ההשלמות' ינקה את כל ההעלאות במצב 'הושלם', כולל מצבי שגיאה, השהיה וקונפליקט."
|
||||
},
|
||||
"colors": {
|
||||
"red": "אדום",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "הפעל מצב ניפוי באגים של OnlyOffice",
|
||||
"debugOfficeEditorDescription": "הצג חלון קופץ לאיתור באגים עם מידע נוסף בעורך OnlyOffice.",
|
||||
"fileViewerOptions": "אפשרויות תצוגת קבצים",
|
||||
"themeAndLanguage": "נושא ושפה"
|
||||
"themeAndLanguage": "נושא ושפה",
|
||||
"previewFolder": "הצג תמונות ממוזערות של תיקיות, אם זמינות",
|
||||
"previewFolderDescription": "הצג תצוגה מקדימה של תמונות ממוזערות עבור תיקיות עם פריט משנה ראשון עם תמונה ממוזערת ככריכה."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "לא ניתן לאתחל את העורך. אנא טען מחדש.",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Többet mutasson",
|
||||
"showLess": "Kevesebb mutatása",
|
||||
"openParentFolder": "Szülői mappa megnyitása",
|
||||
"selectedCount": "A művelet végrehajtásához kiválasztott elemek száma"
|
||||
"selectedCount": "A művelet végrehajtásához kiválasztott elemek száma",
|
||||
"listView": "Lista nézet",
|
||||
"compactView": "Kompakt nézet",
|
||||
"normalView": "Normál nézet",
|
||||
"galleryView": "Galéria nézet"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Fájl letöltése",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Fájlok száma{suffix}",
|
||||
"renameMessageConflict": "A \"{filename}\" nevű elem már létezik!",
|
||||
"uploadSettingsChunked": "Maximális egyidejű feltöltés: {maxConcurrentUpload}, chunk méret: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Maximális egyidejű feltöltés: {maxConcurrentUpload}, chunked upload disabled"
|
||||
"uploadSettingsNoChunk": "Maximális egyidejű feltöltés: {maxConcurrentUpload}, chunked upload disabled",
|
||||
"quickJump": "Gyors ugrás más fájlokra",
|
||||
"moveSuccess": "Fájlok sikeresen áthelyezve!",
|
||||
"copySuccess": "A fájlok másolása sikeres volt!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Képek",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Engedélyezze az irodai fájlok megtekintését az OnlyOffice használatával ezen a megosztáson.",
|
||||
"enableOnlyOfficeEditing": "Engedélyezze az OnlyOffice szerkesztést",
|
||||
"enableOnlyOfficeEditingDescription": "Engedélyezze az irodai fájlok szerkesztését az OnlyOffice használatával ezen a megosztáson.",
|
||||
"titleDefault": "Megosztott fájlok - {title}"
|
||||
"titleDefault": "Megosztott fájlok - {title}",
|
||||
"defaultViewMode": "Alapértelmezett nézetmód",
|
||||
"defaultViewModeDescription": "A megosztást megtekintő névtelen felhasználók alapértelmezett megtekintési módjának beállítása. A bejelentkezett felhasználók továbbra is módosíthatják a nézeti beállításaikat."
|
||||
},
|
||||
"api": {
|
||||
"title": "API-kulcsok:",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "Maximális egyidejű feltöltés",
|
||||
"maxConcurrentUploadHelp": "Az egyidejű feltöltések maximális száma. A magasabb értékek általában jobban teljesítenek, de ez sok tényezőtől függ.",
|
||||
"uploadChunkSizeMb": "Feltöltési csomag mérete MB-ban",
|
||||
"uploadChunkSizeMbHelp": "Általában 5 és 50 MB között, instabil vagy korlátozott kapcsolatok esetén a kisebb méret jobb. A 0 értékre állítás kikapcsolja a feltöltési darabolás funkciót."
|
||||
"uploadChunkSizeMbHelp": "Általában 5 és 50 MB között, instabil vagy korlátozott kapcsolatok esetén a kisebb méret jobb. A 0 értékre állítás kikapcsolja a feltöltési darabolás funkciót.",
|
||||
"clearAll": "A befejezett törlésnek el kell távolítania a hibát, a szünetet és a konfliktust.",
|
||||
"clearAllDescription": "A befejezett állapot törlése gomb az összes befejezett állapotban lévő feltöltést törli, beleértve a hiba, a szüneteltetés és a konfliktus állapotokat is."
|
||||
},
|
||||
"colors": {
|
||||
"red": "piros",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "OnlyOffice hibakeresési mód engedélyezése",
|
||||
"debugOfficeEditorDescription": "További információkat tartalmazó hibakeresési felugró ablak megjelenítése az OnlyOffice szerkesztőben.",
|
||||
"fileViewerOptions": "Fájlnéző beállításai",
|
||||
"themeAndLanguage": "Téma és nyelv"
|
||||
"themeAndLanguage": "Téma és nyelv",
|
||||
"previewFolder": "A mappák miniatűrjeinek megjelenítése, ha rendelkezésre állnak",
|
||||
"previewFolderDescription": "Miniatűr előnézetek megjelenítése olyan mappákhoz, amelyekben az első gyermekelem miniatűrrel borítóképként szerepel."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Nem sikerült inicializálni a szerkesztőt. Kérjük, töltse újra.",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Mostra di più",
|
||||
"showLess": "Mostra meno",
|
||||
"openParentFolder": "Aprire la cartella madre",
|
||||
"selectedCount": "Numero di elementi selezionati per eseguire un'azione"
|
||||
"selectedCount": "Numero di elementi selezionati per eseguire un'azione",
|
||||
"listView": "Vista elenco",
|
||||
"compactView": "Vista compatta",
|
||||
"normalView": "Vista normale",
|
||||
"galleryView": "Vista della galleria"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Scarica file",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Numero di file{suffix}",
|
||||
"renameMessageConflict": "Un elemento chiamato \"{filename}\" esiste già!",
|
||||
"uploadSettingsChunked": "Carichi massimi contemporanei: {maxConcurrentUpload}, dimensione dei pezzi: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Carichi massimi contemporanei: {maxConcurrentUpload}, caricamento chunked disabilitato"
|
||||
"uploadSettingsNoChunk": "Carichi massimi contemporanei: {maxConcurrentUpload}, caricamento chunked disabilitato",
|
||||
"quickJump": "Salto rapido ad altri file",
|
||||
"moveSuccess": "I file sono stati spostati con successo!",
|
||||
"copySuccess": "I file sono stati copiati con successo!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Immagini",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Consente di visualizzare i file di Office utilizzando OnlyOffice in questa condivisione.",
|
||||
"enableOnlyOfficeEditing": "Abilita la modifica di OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Consentire la modifica dei file di Office utilizzando OnlyOffice in questa condivisione.",
|
||||
"titleDefault": "File condivisi - {title}"
|
||||
"titleDefault": "File condivisi - {title}",
|
||||
"defaultViewMode": "Modalità di visualizzazione predefinita",
|
||||
"defaultViewModeDescription": "Imposta la modalità di visualizzazione predefinita per gli utenti anonimi che visualizzano questa condivisione. Gli utenti registrati possono comunque modificare le loro preferenze di visualizzazione."
|
||||
},
|
||||
"api": {
|
||||
"title": "Chiavi API:",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "Carichi massimi contemporanei",
|
||||
"maxConcurrentUploadHelp": "Il numero massimo di upload contemporanei. I valori più alti sono in genere più performanti, ma ciò dipende da molti fattori.",
|
||||
"uploadChunkSizeMb": "Dimensione dei pezzi caricati in MB",
|
||||
"uploadChunkSizeMbHelp": "In genere da 5 a 50 MB, ma per connessioni instabili o limitate è meglio che sia più piccolo. Impostando 0 si disattiva il chunking del caricamento."
|
||||
"uploadChunkSizeMbHelp": "In genere da 5 a 50 MB, ma per connessioni instabili o limitate è meglio che sia più piccolo. Impostando 0 si disattiva il chunking del caricamento.",
|
||||
"clearAll": "L'opzione Clear completed dovrebbe rimuovere gli errori, le pause e i conflitti.",
|
||||
"clearAllDescription": "Il pulsante Cancella completato cancella tutti i caricamenti in stato completato, compresi gli stati di errore, pausa e conflitto."
|
||||
},
|
||||
"colors": {
|
||||
"red": "rosso",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "Abilita la modalità di debug di OnlyOffice",
|
||||
"debugOfficeEditorDescription": "Mostra un popup di debug con informazioni aggiuntive nell'editor di OnlyOffice.",
|
||||
"fileViewerOptions": "Opzioni del visualizzatore di file",
|
||||
"themeAndLanguage": "Tema e linguaggio"
|
||||
"themeAndLanguage": "Tema e linguaggio",
|
||||
"previewFolder": "Mostra le miniature delle cartelle, se disponibili.",
|
||||
"previewFolderDescription": "Mostra le anteprime per le cartelle con il primo elemento figlio con miniatura come copertina."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Impossibile inizializzare l'editor. Ricaricare.",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "もっと見る",
|
||||
"showLess": "表示を減らす",
|
||||
"openParentFolder": "親フォルダを開く",
|
||||
"selectedCount": "アクションを実行するために選択された項目の数"
|
||||
"selectedCount": "アクションを実行するために選択された項目の数",
|
||||
"listView": "リスト表示",
|
||||
"compactView": "コンパクトビュー",
|
||||
"normalView": "通常表示",
|
||||
"galleryView": "ギャラリービュー"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "ファイルをダウンロード",
|
||||
@@ -186,6 +190,9 @@
|
||||
"lastModified": "最終更新日時{suffix}",
|
||||
"numberDirs": "ディレクトリ数{suffix}",
|
||||
"numberFiles": "ファイル数{suffix}",
|
||||
"quickJump": "他のファイルに素早くジャンプ",
|
||||
"moveSuccess": "ファイルは正常に移動しました!",
|
||||
"copySuccess": "ファイルは正常にコピーされました!",
|
||||
"renameMessageConflict": "\"{filename}\"という名前のアイテムがすでに存在します!",
|
||||
"uploadSettingsChunked": "最大同時アップロード数:{maxConcurrentUpload}、チャンクサイズ:{uploadChunkSizeMb}MB",
|
||||
"uploadSettingsNoChunk": "最大同時アップロード数:{maxConcurrentUpload}、チャンクアップロード無効"
|
||||
@@ -389,6 +396,8 @@
|
||||
"enableOnlyOffice": "OnlyOfficeビューアを有効にする",
|
||||
"enableOnlyOfficeDescription": "この共有でOnlyOfficeを使用したオフィスファイルの閲覧を許可する",
|
||||
"enableOnlyOfficeEditing": "OnlyOffice編集を有効にする",
|
||||
"defaultViewMode": "デフォルトの表示モード",
|
||||
"defaultViewModeDescription": "匿名ユーザーがこの共有を閲覧する際のデフォルトの表示モードを設定します。ログインしているユーザーは、表示モードを変更することができます。",
|
||||
"enableOnlyOfficeEditingDescription": "この共有でOnlyOfficeを使ったオフィスファイルの編集を許可する",
|
||||
"titleDefault": "共有ファイル{title}"
|
||||
},
|
||||
@@ -481,6 +490,8 @@
|
||||
"title": "アップロードとダウンロード",
|
||||
"maxConcurrentUpload": "最大同時アップロード数",
|
||||
"maxConcurrentUploadHelp": "同時アップロードの最大数。一般的に高い値の方がパフォーマンスが良いですが、これは多くの要因に依存します。",
|
||||
"clearAll": "クリアが完了すると、エラー、一時停止、コンフリクトが取り除かれる。",
|
||||
"clearAllDescription": "clear completedボタンは、エラー、一時停止、競合状態を含む、完了状態のすべてのアップロードをクリアします。",
|
||||
"uploadChunkSizeMb": "アップロードチャンクサイズ(MB)",
|
||||
"uploadChunkSizeMbHelp": "通常5~50MBですが、不安定な接続や制限のある接続の場合は小さい方が良いでしょう。0に設定するとアップロードチャンキングが無効になります。"
|
||||
},
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "OnlyOfficeデバッグモードを有効にする",
|
||||
"debugOfficeEditorDescription": "OnlyOfficeエディタに追加情報のデバッグポップアップを表示します。",
|
||||
"fileViewerOptions": "ファイルビューアオプション",
|
||||
"themeAndLanguage": "テーマと言語"
|
||||
"themeAndLanguage": "テーマと言語",
|
||||
"previewFolder": "利用可能であれば、フォルダのサムネイルを表示する",
|
||||
"previewFolderDescription": "サムネイルを持つ最初の子アイテムをカバーとするフォルダのサムネイルプレビューを表示します。"
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "エディタの初期化に失敗しました。再読み込みしてください。",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "자세히 보기",
|
||||
"showLess": "덜 보기",
|
||||
"openParentFolder": "상위 폴더 열기",
|
||||
"selectedCount": "작업을 수행하기 위해 선택한 항목 수"
|
||||
"selectedCount": "작업을 수행하기 위해 선택한 항목 수",
|
||||
"listView": "목록 보기",
|
||||
"compactView": "컴팩트 보기",
|
||||
"normalView": "일반 보기",
|
||||
"galleryView": "갤러리 보기"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "파일 다운로드",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "파일 수{suffix}",
|
||||
"renameMessageConflict": "\"{filename}\"라는 이름의 항목이 이미 존재합니다!",
|
||||
"uploadSettingsChunked": "최대 동시 업로드: {maxConcurrentUpload}, 청크 크기: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "최대 동시 업로드: {maxConcurrentUpload}, 청크 업로드 비활성화"
|
||||
"uploadSettingsNoChunk": "최대 동시 업로드: {maxConcurrentUpload}, 청크 업로드 비활성화",
|
||||
"quickJump": "다른 파일로 빠르게 이동",
|
||||
"moveSuccess": "파일이 성공적으로 이동했습니다!",
|
||||
"copySuccess": "파일이 성공적으로 복사되었습니다!"
|
||||
},
|
||||
"search": {
|
||||
"images": "이미지",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "이 공유에서 OnlyOffice를 사용하여 Office 파일을 볼 수 있도록 허용합니다.",
|
||||
"enableOnlyOfficeEditing": "Office 편집만 사용",
|
||||
"enableOnlyOfficeEditingDescription": "이 공유에서 OnlyOffice를 사용하여 Office 파일을 편집할 수 있도록 허용합니다.",
|
||||
"titleDefault": "공유 파일 - {title}"
|
||||
"titleDefault": "공유 파일 - {title}",
|
||||
"defaultViewMode": "기본 보기 모드",
|
||||
"defaultViewModeDescription": "이 공유를 보는 익명 사용자의 기본 보기 모드를 설정합니다. 로그인한 사용자는 여전히 보기 기본 설정을 변경할 수 있습니다."
|
||||
},
|
||||
"api": {
|
||||
"title": "API 키:",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "최대 동시 업로드 수",
|
||||
"maxConcurrentUploadHelp": "최대 동시 업로드 수입니다. 일반적으로 값이 높을수록 성능이 향상되지만, 이는 여러 요인에 따라 달라집니다.",
|
||||
"uploadChunkSizeMb": "업로드 청크 크기(MB)",
|
||||
"uploadChunkSizeMbHelp": "일반적으로 5~50MB이며, 연결이 불안정하거나 제한적인 경우에는 이보다 작을수록 좋습니다. 0으로 설정하면 업로드 청킹이 비활성화됩니다."
|
||||
"uploadChunkSizeMbHelp": "일반적으로 5~50MB이며, 연결이 불안정하거나 제한적인 경우에는 이보다 작을수록 좋습니다. 0으로 설정하면 업로드 청킹이 비활성화됩니다.",
|
||||
"clearAll": "완료를 지우면 오류, 일시 중지 및 충돌이 제거됩니다.",
|
||||
"clearAllDescription": "완료 지우기 버튼은 오류, 일시 중지, 충돌 상태를 포함하여 완료된 상태의 모든 업로드를 지웁니다."
|
||||
},
|
||||
"colors": {
|
||||
"red": "빨간색",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "Office 디버그 모드만 사용",
|
||||
"debugOfficeEditorDescription": "OnlyOffice 편집기에서 추가 정보가 포함된 디버그 팝업을 표시합니다.",
|
||||
"fileViewerOptions": "파일 뷰어 옵션",
|
||||
"themeAndLanguage": "테마 및 언어"
|
||||
"themeAndLanguage": "테마 및 언어",
|
||||
"previewFolder": "가능한 경우 폴더의 썸네일 표시",
|
||||
"previewFolderDescription": "썸네일을 표지로 사용하는 첫 번째 하위 항목이 있는 폴더의 썸네일 미리 보기를 표시합니다."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "편집기를 초기화하지 못했습니다. 다시 로드하세요.",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Meer tonen",
|
||||
"showLess": "Minder tonen",
|
||||
"openParentFolder": "Bovenliggende map openen",
|
||||
"selectedCount": "Aantal items geselecteerd om een actie uit te voeren"
|
||||
"selectedCount": "Aantal items geselecteerd om een actie uit te voeren",
|
||||
"listView": "Lijstweergave",
|
||||
"compactView": "Compacte weergave",
|
||||
"normalView": "Normale weergave",
|
||||
"galleryView": "Galerij"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Bestand downloaden",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Aantal bestanden{suffix}",
|
||||
"renameMessageConflict": "Een item met de naam \"{filename}\" bestaat al!",
|
||||
"uploadSettingsChunked": "Max gelijktijdige uploads: {maxConcurrentUpload}, brokgrootte: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Max gelijktijdige uploads: {maxConcurrentUpload}, chunked upload uitgeschakeld"
|
||||
"uploadSettingsNoChunk": "Max gelijktijdige uploads: {maxConcurrentUpload}, chunked upload uitgeschakeld",
|
||||
"quickJump": "Snel naar andere bestanden springen",
|
||||
"moveSuccess": "Bestanden succesvol verplaatst!",
|
||||
"copySuccess": "Bestanden succesvol gekopieerd!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Afbeeldingen",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Sta het bekijken van Office-bestanden toe met OnlyOffice in deze share.",
|
||||
"enableOnlyOfficeEditing": "Bewerken met OnlyOffice inschakelen",
|
||||
"enableOnlyOfficeEditingDescription": "Laat het bewerken van Office-bestanden met OnlyOffice toe in deze share.",
|
||||
"titleDefault": "Gedeelde bestanden - {title}"
|
||||
"titleDefault": "Gedeelde bestanden - {title}",
|
||||
"defaultViewMode": "Standaard weergavemodus",
|
||||
"defaultViewModeDescription": "Stel de standaard weergavemodus in voor anonieme gebruikers die deze share bekijken. Aangemelde gebruikers kunnen nog steeds hun weergavevoorkeur wijzigen."
|
||||
},
|
||||
"api": {
|
||||
"title": "API-sleutels",
|
||||
@@ -446,7 +455,9 @@
|
||||
"maxConcurrentUpload": "Max. gelijktijdige uploads",
|
||||
"maxConcurrentUploadHelp": "Het maximum aantal gelijktijdige uploads. Hogere waarden presteren meestal beter, maar dit hangt van veel factoren af.",
|
||||
"uploadChunkSizeMb": "Upload brokgrootte in MB",
|
||||
"uploadChunkSizeMbHelp": "Gewoonlijk 5 tot 50 MB, kleiner is beter voor onstabiele of beperkte verbindingen. Instellen op 0 schakelt chunking uit."
|
||||
"uploadChunkSizeMbHelp": "Gewoonlijk 5 tot 50 MB, kleiner is beter voor onstabiele of beperkte verbindingen. Instellen op 0 schakelt chunking uit.",
|
||||
"clearAll": "Wissen voltooid moet fout, pauze en conflict verwijderen",
|
||||
"clearAllDescription": "De knop Voltooid wissen wist alle uploads in een voltooide status, inclusief fout-, gepauzeerde en conflictstatussen."
|
||||
},
|
||||
"colors": {
|
||||
"red": "rood",
|
||||
@@ -511,7 +522,9 @@
|
||||
"debugOfficeEditor": "Debugmodus OnlyOffice inschakelen",
|
||||
"debugOfficeEditorDescription": "Een foutopsporingspop-up weergeven met aanvullende informatie in de OnlyOffice-editor.",
|
||||
"fileViewerOptions": "Opties voor bestandsviewer",
|
||||
"themeAndLanguage": "Thema & Taal"
|
||||
"themeAndLanguage": "Thema & Taal",
|
||||
"previewFolder": "Toon miniaturen voor mappen indien beschikbaar",
|
||||
"previewFolderDescription": "Toon miniatuurafbeeldingen voor mappen met het eerste kinditem met miniatuurafbeelding als omslag."
|
||||
},
|
||||
"general": {
|
||||
"name": "Naam",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Pokaż więcej",
|
||||
"showLess": "Pokaż mniej",
|
||||
"openParentFolder": "Otwórz folder nadrzędny",
|
||||
"selectedCount": "Liczba elementów wybranych do wykonania akcji"
|
||||
"selectedCount": "Liczba elementów wybranych do wykonania akcji",
|
||||
"listView": "Widok listy",
|
||||
"compactView": "Widok kompaktowy",
|
||||
"normalView": "Widok normalny",
|
||||
"galleryView": "Widok galerii"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Pobierz plik",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Liczba plików{suffix}",
|
||||
"renameMessageConflict": "Element o nazwie \"{filename}\" już istnieje!",
|
||||
"uploadSettingsChunked": "Maks. jednoczesne przesyłanie: {maxConcurrentUpload}, rozmiar fragmentu: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Maksymalny jednoczesny upload: {maxConcurrentUpload}, przesyłanie fragmentaryczne wyłączone"
|
||||
"uploadSettingsNoChunk": "Maksymalny jednoczesny upload: {maxConcurrentUpload}, przesyłanie fragmentaryczne wyłączone",
|
||||
"quickJump": "Szybkie przechodzenie do innych plików",
|
||||
"moveSuccess": "Pliki zostały przeniesione pomyślnie!",
|
||||
"copySuccess": "Pliki zostały skopiowane pomyślnie!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Zdjęcia",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Zezwalaj na wyświetlanie plików biurowych przy użyciu OnlyOffice w tym udziale.",
|
||||
"enableOnlyOfficeEditing": "Włącz edycję OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Zezwalaj na edycję plików biurowych za pomocą OnlyOffice w tym udziale.",
|
||||
"titleDefault": "Udostępnione pliki - {title}"
|
||||
"titleDefault": "Udostępnione pliki - {title}",
|
||||
"defaultViewMode": "Domyślny tryb widoku",
|
||||
"defaultViewModeDescription": "Ustawia domyślny tryb widoku dla anonimowych użytkowników przeglądających ten folder udostępniony. Zalogowani użytkownicy mogą nadal zmieniać swoje preferencje widoku."
|
||||
},
|
||||
"api": {
|
||||
"title": "Klucze API:",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "Maksymalne jednoczesne przesyłanie",
|
||||
"maxConcurrentUploadHelp": "Maksymalna liczba jednocześnie przesyłanych plików. Wyższe wartości zazwyczaj działają lepiej, ale zależy to od wielu czynników.",
|
||||
"uploadChunkSizeMb": "Rozmiar przesyłanego fragmentu w MB",
|
||||
"uploadChunkSizeMbHelp": "Zazwyczaj od 5 do 50 MB, mniejsze są lepsze dla niestabilnych lub ograniczonych połączeń. Ustawienie wartości 0 spowoduje wyłączenie podziału na fragmenty."
|
||||
"uploadChunkSizeMbHelp": "Zazwyczaj od 5 do 50 MB, mniejsze są lepsze dla niestabilnych lub ograniczonych połączeń. Ustawienie wartości 0 spowoduje wyłączenie podziału na fragmenty.",
|
||||
"clearAll": "Wyczyszczenie zakończone powinno usunąć błąd, pauzę i konflikt",
|
||||
"clearAllDescription": "Przycisk wyczyść ukończone spowoduje wyczyszczenie wszystkich przesłanych plików w stanie ukończenia, w tym w stanie błędu, wstrzymania i konfliktu."
|
||||
},
|
||||
"colors": {
|
||||
"red": "czerwony",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "Włącz tryb debugowania OnlyOffice",
|
||||
"debugOfficeEditorDescription": "Wyświetla wyskakujące okienko debugowania z dodatkowymi informacjami w edytorze OnlyOffice.",
|
||||
"fileViewerOptions": "Opcje przeglądarki plików",
|
||||
"themeAndLanguage": "Temat i język"
|
||||
"themeAndLanguage": "Temat i język",
|
||||
"previewFolder": "Wyświetlanie miniatur folderów, jeśli są dostępne.",
|
||||
"previewFolderDescription": "Wyświetlanie podglądu miniatur dla folderów z pierwszym elementem podrzędnym z miniaturą jako okładką."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Nie udało się zainicjować edytora. Przeładuj ponownie.",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Mostrar mais",
|
||||
"showLess": "Mostrar menos",
|
||||
"openParentFolder": "Abrir a pasta principal",
|
||||
"selectedCount": "Número de itens selecionados para executar uma ação"
|
||||
"selectedCount": "Número de itens selecionados para executar uma ação",
|
||||
"listView": "Exibição de lista",
|
||||
"compactView": "Visão compacta",
|
||||
"normalView": "Visualização normal",
|
||||
"galleryView": "Visualização da galeria"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Baixar arquivo",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Número de arquivos{suffix}",
|
||||
"renameMessageConflict": "Já existe um item chamado \"{filename}\"!",
|
||||
"uploadSettingsChunked": "Máximo de uploads simultâneos: {maxConcurrentUpload}, chunk size: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Máximo de uploads simultâneos: {maxConcurrentUpload}, upload em pedaços desativado"
|
||||
"uploadSettingsNoChunk": "Máximo de uploads simultâneos: {maxConcurrentUpload}, upload em pedaços desativado",
|
||||
"quickJump": "Pular rapidamente para outros arquivos",
|
||||
"moveSuccess": "Os arquivos foram movidos com sucesso!",
|
||||
"copySuccess": "Os arquivos foram copiados com sucesso!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Imagens",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Permitir a visualização de arquivos do Office usando o OnlyOffice nesse compartilhamento.",
|
||||
"enableOnlyOfficeEditing": "Ativar a edição do OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Permitir a edição de arquivos do Office usando o OnlyOffice nesse compartilhamento.",
|
||||
"titleDefault": "Arquivos compartilhados - {title}"
|
||||
"titleDefault": "Arquivos compartilhados - {title}",
|
||||
"defaultViewMode": "Modo de exibição padrão",
|
||||
"defaultViewModeDescription": "Define o modo de exibição padrão para usuários anônimos que visualizam esse compartilhamento. Os usuários conectados ainda podem alterar sua preferência de visualização."
|
||||
},
|
||||
"api": {
|
||||
"title": "Chaves de API:",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "Máximo de uploads simultâneos",
|
||||
"maxConcurrentUploadHelp": "O número máximo de uploads simultâneos. Valores mais altos costumam ter melhor desempenho, mas isso depende de muitos fatores.",
|
||||
"uploadChunkSizeMb": "Tamanho do bloco de upload em MB",
|
||||
"uploadChunkSizeMbHelp": "Normalmente, de 5 a 50 MB; quanto menor, melhor para conexões instáveis ou restritas. A configuração como 0 desabilitará o chunking de upload."
|
||||
"uploadChunkSizeMbHelp": "Normalmente, de 5 a 50 MB; quanto menor, melhor para conexões instáveis ou restritas. A configuração como 0 desabilitará o chunking de upload.",
|
||||
"clearAll": "O Clear completed deve remover erros, pausas e conflitos",
|
||||
"clearAllDescription": "O botão limpar concluído limpará todos os uploads em um estado concluído, inclusive os estados de erro, pausa e conflito."
|
||||
},
|
||||
"colors": {
|
||||
"red": "vermelho",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "Ativar o modo de depuração do OnlyOffice",
|
||||
"debugOfficeEditorDescription": "Mostrar um pop-up de depuração com informações adicionais no editor do OnlyOffice.",
|
||||
"fileViewerOptions": "Opções do visualizador de arquivos",
|
||||
"themeAndLanguage": "Tema e linguagem"
|
||||
"themeAndLanguage": "Tema e linguagem",
|
||||
"previewFolder": "Mostrar miniaturas das pastas, se disponíveis",
|
||||
"previewFolderDescription": "Mostrar visualizações em miniatura para pastas com o primeiro item filho com miniatura como capa."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Falha ao inicializar o editor. Favor recarregar.",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Mostrar mais",
|
||||
"showLess": "Mostrar menos",
|
||||
"openParentFolder": "Abrir a pasta principal",
|
||||
"selectedCount": "Número de itens selecionados para executar uma ação"
|
||||
"selectedCount": "Número de itens selecionados para executar uma ação",
|
||||
"listView": "Vista de lista",
|
||||
"compactView": "Vista compacta",
|
||||
"normalView": "Vista normal",
|
||||
"galleryView": "Vista da galeria"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descarregar ficheiro",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Número de ficheiros{suffix}",
|
||||
"renameMessageConflict": "Já existe um item com o nome \"{filename}\"!",
|
||||
"uploadSettingsChunked": "Máximo de carregamentos simultâneos: {maxConcurrentUpload}, tamanho do pedaço: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Máximo de carregamentos simultâneos: {maxConcurrentUpload}, carregamento em pedaços desativado"
|
||||
"uploadSettingsNoChunk": "Máximo de carregamentos simultâneos: {maxConcurrentUpload}, carregamento em pedaços desativado",
|
||||
"quickJump": "Saltar rapidamente para outros ficheiros",
|
||||
"moveSuccess": "Ficheiros movidos com sucesso!",
|
||||
"copySuccess": "Ficheiros copiados com sucesso!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Imagens",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Permitir a visualização de ficheiros de escritório utilizando o OnlyOffice nesta partilha.",
|
||||
"enableOnlyOfficeEditing": "Ativar a edição OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Permitir a edição de ficheiros do Office utilizando o OnlyOffice nesta partilha.",
|
||||
"titleDefault": "Ficheiros partilhados - {title}"
|
||||
"titleDefault": "Ficheiros partilhados - {title}",
|
||||
"defaultViewMode": "Modo de visualização predefinido",
|
||||
"defaultViewModeDescription": "Define o modo de visualização predefinido para os utilizadores anónimos que visualizam esta partilha. Os utilizadores com sessão iniciada ainda podem alterar a sua preferência de visualização."
|
||||
},
|
||||
"api": {
|
||||
"title": "Chaves da API",
|
||||
@@ -446,7 +455,9 @@
|
||||
"maxConcurrentUpload": "Máximo de carregamentos simultâneos",
|
||||
"maxConcurrentUploadHelp": "O número máximo de carregamentos simultâneos. Os valores mais elevados têm normalmente um melhor desempenho, mas isto depende de muitos factores.",
|
||||
"uploadChunkSizeMb": "Tamanho do bloco de carregamento em MB",
|
||||
"uploadChunkSizeMbHelp": "Tipicamente, 5 a 50 MB, mas um valor mais pequeno é melhor para ligações instáveis ou restritas. Definir como 0 desactivará a fragmentação do carregamento."
|
||||
"uploadChunkSizeMbHelp": "Tipicamente, 5 a 50 MB, mas um valor mais pequeno é melhor para ligações instáveis ou restritas. Definir como 0 desactivará a fragmentação do carregamento.",
|
||||
"clearAll": "Limpar concluído deve eliminar erros, pausas e conflitos",
|
||||
"clearAllDescription": "O botão Limpar concluído limpa todos os carregamentos num estado concluído, incluindo os estados de erro, pausa e conflito."
|
||||
},
|
||||
"colors": {
|
||||
"red": "vermelho",
|
||||
@@ -511,7 +522,9 @@
|
||||
"debugOfficeEditor": "Ativar o modo de depuração do OnlyOffice",
|
||||
"debugOfficeEditorDescription": "Mostrar um popup de depuração com informações adicionais no editor OnlyOffice.",
|
||||
"fileViewerOptions": "Opções do visualizador de ficheiros",
|
||||
"themeAndLanguage": "Tema e linguagem"
|
||||
"themeAndLanguage": "Tema e linguagem",
|
||||
"previewFolder": "Mostrar miniaturas das pastas, se disponíveis",
|
||||
"previewFolderDescription": "Mostrar pré-visualizações em miniatura para pastas com o primeiro item filho com miniatura como capa."
|
||||
},
|
||||
"general": {
|
||||
"name": "Nome",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Arată mai multe",
|
||||
"showLess": "Arată mai puțin",
|
||||
"openParentFolder": "Deschideți folderul părinte",
|
||||
"selectedCount": "Numărul de elemente selectate pentru a efectua o acțiune"
|
||||
"selectedCount": "Numărul de elemente selectate pentru a efectua o acțiune",
|
||||
"listView": "Vizualizare listă",
|
||||
"compactView": "Vedere compactă",
|
||||
"normalView": "Vizualizare normală",
|
||||
"galleryView": "Vizualizare galerie"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Descarcă fișier",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Număr de fișiere{suffix}",
|
||||
"renameMessageConflict": "Un element numit \"{filename}\" există deja!",
|
||||
"uploadSettingsChunked": "Încărcări simultane maxime: {maxConcurrentUpload}, dimensiunea bucăților: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Încărcări simultane maxime: {maxConcurrentUpload}, încărcare fragmentată dezactivată"
|
||||
"uploadSettingsNoChunk": "Încărcări simultane maxime: {maxConcurrentUpload}, încărcare fragmentată dezactivată",
|
||||
"quickJump": "Salt rapid la alte fișiere",
|
||||
"moveSuccess": "Fișiere mutate cu succes!",
|
||||
"copySuccess": "Fișiere copiate cu succes!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Imagini",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Permiteți vizualizarea fișierelor office utilizând OnlyOffice în această partajare.",
|
||||
"enableOnlyOfficeEditing": "Activați editarea OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Permiteți editarea fișierelor de birou utilizând OnlyOffice în această partajare.",
|
||||
"titleDefault": "Fișiere partajate - {title}"
|
||||
"titleDefault": "Fișiere partajate - {title}",
|
||||
"defaultViewMode": "Mod de vizualizare implicit",
|
||||
"defaultViewModeDescription": "Setează modul de vizualizare implicit pentru utilizatorii anonimi care vizualizează această partajare. Utilizatorii conectați își pot schimba în continuare preferințele de vizualizare."
|
||||
},
|
||||
"api": {
|
||||
"title": "Chei API:",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "Încărcări simultane maxime",
|
||||
"maxConcurrentUploadHelp": "Numărul maxim de încărcări simultane. Valorile mai mari au de obicei performanțe mai bune, dar acest lucru depinde de mai mulți factori.",
|
||||
"uploadChunkSizeMb": "Mărimea pachetului de încărcare în MB",
|
||||
"uploadChunkSizeMbHelp": "De obicei între 5 și 50 MB, mai mic este mai bine pentru conexiunile instabile sau restricționate. Setarea la 0 va dezactiva fragmentarea încărcării."
|
||||
"uploadChunkSizeMbHelp": "De obicei între 5 și 50 MB, mai mic este mai bine pentru conexiunile instabile sau restricționate. Setarea la 0 va dezactiva fragmentarea încărcării.",
|
||||
"clearAll": "Clear completed ar trebui să elimine erorile, pauzele și conflictele",
|
||||
"clearAllDescription": "Butonul șterge finalizat va șterge toate încărcările în stare finalizată, inclusiv stările de eroare, pauză și conflict."
|
||||
},
|
||||
"colors": {
|
||||
"red": "roșu",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "Activați modul de depanare OnlyOffice",
|
||||
"debugOfficeEditorDescription": "Afișați o fereastră pop-up de depanare cu informații suplimentare în editorul OnlyOffice.",
|
||||
"fileViewerOptions": "Opțiuni vizualizator de fișiere",
|
||||
"themeAndLanguage": "Temă și limbaj"
|
||||
"themeAndLanguage": "Temă și limbaj",
|
||||
"previewFolder": "Afișați miniaturi pentru foldere, dacă sunt disponibile",
|
||||
"previewFolderDescription": "Afișează previzualizări ale miniaturilor pentru folderele cu primul element copil cu miniatură ca copertă."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Nu s-a reușit inițializarea editorului. Vă rugăm să reîncărcați.",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Показать еще",
|
||||
"showLess": "Показать меньше",
|
||||
"openParentFolder": "Откройте родительскую папку",
|
||||
"selectedCount": "Количество элементов, выбранных для выполнения действия"
|
||||
"selectedCount": "Количество элементов, выбранных для выполнения действия",
|
||||
"listView": "Просмотр списка",
|
||||
"compactView": "Компактный вид",
|
||||
"normalView": "Обычный вид",
|
||||
"galleryView": "Просмотр галереи"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Скачать файл",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Количество файлов{suffix}",
|
||||
"renameMessageConflict": "Элемент с именем \"{filename}\" уже существует!",
|
||||
"uploadSettingsChunked": "Максимальное количество одновременных загрузок: {maxConcurrentUpload}, размер чанка: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Максимальное количество одновременных загрузок: {maxConcurrentUpload}, загрузка в куски отключена"
|
||||
"uploadSettingsNoChunk": "Максимальное количество одновременных загрузок: {maxConcurrentUpload}, загрузка в куски отключена",
|
||||
"quickJump": "Быстрый переход к другим файлам",
|
||||
"moveSuccess": "Файлы успешно перемещены!",
|
||||
"copySuccess": "Файлы успешно скопированы!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Изображения",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Разрешите просмотр офисных файлов с помощью OnlyOffice в этом ресурсе.",
|
||||
"enableOnlyOfficeEditing": "Включить редактирование OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Разрешите редактирование офисных файлов с помощью OnlyOffice в этом ресурсе.",
|
||||
"titleDefault": "Общие файлы - {title}"
|
||||
"titleDefault": "Общие файлы - {title}",
|
||||
"defaultViewMode": "Режим просмотра по умолчанию",
|
||||
"defaultViewModeDescription": "Установите режим просмотра по умолчанию для анонимных пользователей, просматривающих этот ресурс. Пользователи, вошедшие в систему, могут изменить свой режим просмотра."
|
||||
},
|
||||
"api": {
|
||||
"title": "Ключи API:",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "Максимальное количество одновременных загрузок",
|
||||
"maxConcurrentUploadHelp": "Максимальное количество одновременных загрузок. Более высокие значения обычно работают лучше, но это зависит от многих факторов.",
|
||||
"uploadChunkSizeMb": "Размер загружаемого фрагмента в МБ",
|
||||
"uploadChunkSizeMbHelp": "Обычно от 5 до 50 МБ, меньший размер лучше для нестабильных или ограниченных соединений. При установке значения 0 отключается функция \"кусковой загрузки\"."
|
||||
"uploadChunkSizeMbHelp": "Обычно от 5 до 50 МБ, меньший размер лучше для нестабильных или ограниченных соединений. При установке значения 0 отключается функция \"кусковой загрузки\".",
|
||||
"clearAll": "Завершение работы должно устранить ошибки, паузы и конфликты",
|
||||
"clearAllDescription": "Кнопка \"Очистить завершенные\" очистит все загрузки, находящиеся в состоянии завершения, включая состояния ошибки, паузы и конфликта."
|
||||
},
|
||||
"colors": {
|
||||
"red": "красный",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "Включить режим отладки OnlyOffice",
|
||||
"debugOfficeEditorDescription": "Отображение всплывающего окна отладки с дополнительной информацией в редакторе OnlyOffice.",
|
||||
"fileViewerOptions": "Параметры просмотра файлов",
|
||||
"themeAndLanguage": "Тема и язык"
|
||||
"themeAndLanguage": "Тема и язык",
|
||||
"previewFolder": "Показывайте миниатюры для папок, если они доступны",
|
||||
"previewFolderDescription": "Показывать предварительный просмотр миниатюр для папок, в которых первый дочерний элемент с миниатюрой является обложкой."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Не удалось инициализировать редактор. Пожалуйста, перезагрузитесь.",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Zobraziť viac",
|
||||
"showLess": "Zobraziť menej",
|
||||
"openParentFolder": "Otvorenie nadradeného priečinka",
|
||||
"selectedCount": "Počet položiek vybraných na vykonanie akcie"
|
||||
"selectedCount": "Počet položiek vybraných na vykonanie akcie",
|
||||
"listView": "Zobrazenie zoznamu",
|
||||
"compactView": "Kompaktný pohľad",
|
||||
"normalView": "Normálne zobrazenie",
|
||||
"galleryView": "Zobrazenie galérie"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Stiahnuť súbor",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Počet súborov{suffix}",
|
||||
"renameMessageConflict": "Položka s názvom \"{filename}\" už existuje!",
|
||||
"uploadSettingsChunked": "Maximálne súbežné nahrávanie: {maxConcurrentUpload}, veľkosť chunk: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Maximálne súbežné nahrávanie: {maxConcurrentUpload}, chunked upload vypnuté"
|
||||
"uploadSettingsNoChunk": "Maximálne súbežné nahrávanie: {maxConcurrentUpload}, chunked upload vypnuté",
|
||||
"quickJump": "Rýchly prechod na iné súbory",
|
||||
"moveSuccess": "Súbory boli úspešne presunuté!",
|
||||
"copySuccess": "Súbory boli úspešne skopírované!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Obrázky",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Povoľte zobrazovanie kancelárskych súborov pomocou OnlyOffice v tejto zdieľanej zložke.",
|
||||
"enableOnlyOfficeEditing": "Povolenie úprav OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Povoľte úpravu kancelárskych súborov pomocou aplikácie OnlyOffice v tejto zdieľanej zložke.",
|
||||
"titleDefault": "Zdieľané súbory - {title}"
|
||||
"titleDefault": "Zdieľané súbory - {title}",
|
||||
"defaultViewMode": "Predvolený režim zobrazenia",
|
||||
"defaultViewModeDescription": "Nastavenie predvoleného režimu zobrazenia pre anonymných používateľov pri prezeraní tejto zdieľanej zložky. Prihlásení používatelia môžu stále zmeniť svoje preferencie zobrazenia."
|
||||
},
|
||||
"api": {
|
||||
"title": "Kľúče API:",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "Maximálne súbežné nahrávanie",
|
||||
"maxConcurrentUploadHelp": "Maximálny počet súbežných odosielaní. Vyššie hodnoty sú zvyčajne výkonnejšie, ale závisí to od mnohých faktorov.",
|
||||
"uploadChunkSizeMb": "Veľkosť nahraného úseku v MB",
|
||||
"uploadChunkSizeMbHelp": "Zvyčajne 5 až 50 MB, v prípade nestabilných alebo obmedzených pripojení je vhodnejšie použiť menšiu veľkosť. Nastavením na hodnotu 0 sa vypne zoskupovanie pri odosielaní."
|
||||
"uploadChunkSizeMbHelp": "Zvyčajne 5 až 50 MB, v prípade nestabilných alebo obmedzených pripojení je vhodnejšie použiť menšiu veľkosť. Nastavením na hodnotu 0 sa vypne zoskupovanie pri odosielaní.",
|
||||
"clearAll": "Vymazanie dokončenia by malo odstrániť chybu, pauzu a konflikt",
|
||||
"clearAllDescription": "Tlačidlo Vymazať dokončené vymaže všetky odoslania v stave dokončenia vrátane chybových, pozastavených a konfliktných stavov."
|
||||
},
|
||||
"colors": {
|
||||
"red": "červená",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "Povolenie režimu ladenia OnlyOffice",
|
||||
"debugOfficeEditorDescription": "Zobrazenie vyskakovacieho okna ladenia s ďalšími informáciami v editore OnlyOffice.",
|
||||
"fileViewerOptions": "Možnosti prehliadača súborov",
|
||||
"themeAndLanguage": "Téma a jazyk"
|
||||
"themeAndLanguage": "Téma a jazyk",
|
||||
"previewFolder": "Zobrazenie miniatúr priečinkov, ak sú k dispozícii",
|
||||
"previewFolderDescription": "Zobrazenie náhľadov miniatúr pre priečinky s prvou podriadenou položkou s miniatúrou ako obálkou."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Nepodarilo sa inicializovať editor. Prosím, načítajte ho znova.",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Visa mer",
|
||||
"showLess": "Visa mindre",
|
||||
"openParentFolder": "Öppna överordnad mapp",
|
||||
"selectedCount": "Antal objekt som valts för att utföra en åtgärd"
|
||||
"selectedCount": "Antal objekt som valts för att utföra en åtgärd",
|
||||
"listView": "Listvy",
|
||||
"compactView": "Kompakt vy",
|
||||
"normalView": "Normal vy",
|
||||
"galleryView": "Galleri utsikt"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Ladda ner fil",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Antal filer{suffix}",
|
||||
"renameMessageConflict": "Ett objekt med namnet \"{filename}\" finns redan!",
|
||||
"uploadSettingsChunked": "Max samtidiga uppladdningar: {maxConcurrentUpload}, chunkstorlek: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Max samtidiga uppladdningar: {maxConcurrentUpload}, chunked uppladdning inaktiverad"
|
||||
"uploadSettingsNoChunk": "Max samtidiga uppladdningar: {maxConcurrentUpload}, chunked uppladdning inaktiverad",
|
||||
"quickJump": "Hoppa snabbt till andra filer",
|
||||
"moveSuccess": "Filerna flyttades framgångsrikt!",
|
||||
"copySuccess": "Filerna kopierades framgångsrikt!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Bilder",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Tillåt visning av kontorsfiler med OnlyOffice i denna delning.",
|
||||
"enableOnlyOfficeEditing": "Aktivera redigering av OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Tillåt redigering av kontorsfiler med hjälp av OnlyOffice i denna andel.",
|
||||
"titleDefault": "Delade filer {title}"
|
||||
"titleDefault": "Delade filer {title}",
|
||||
"defaultViewMode": "Standardläge för visning",
|
||||
"defaultViewModeDescription": "Ange standardvisningsläge för anonyma användare som visar den här andelen. Inloggade användare kan fortfarande ändra sina visningspreferenser."
|
||||
},
|
||||
"api": {
|
||||
"title": "API-nycklar",
|
||||
@@ -446,7 +455,9 @@
|
||||
"maxConcurrentUpload": "Max samtidiga uppladdningar",
|
||||
"maxConcurrentUploadHelp": "Det maximala antalet samtidiga uppladdningar. Högre värden ger normalt bättre prestanda, men detta beror på många faktorer.",
|
||||
"uploadChunkSizeMb": "Ladda upp chunkstorlek i MB",
|
||||
"uploadChunkSizeMbHelp": "Vanligtvis 5 till 50 MB, men mindre är bättre för instabila eller begränsade anslutningar. Om du ställer in 0 inaktiveras chunking för uppladdning."
|
||||
"uploadChunkSizeMbHelp": "Vanligtvis 5 till 50 MB, men mindre är bättre för instabila eller begränsade anslutningar. Om du ställer in 0 inaktiveras chunking för uppladdning.",
|
||||
"clearAll": "Klart avslutat ska ta bort fel, paus och konflikt",
|
||||
"clearAllDescription": "Knappen Clear Completed rensar alla uppladdningar som är klara, inklusive fel-, paus- och konflikttillstånd."
|
||||
},
|
||||
"colors": {
|
||||
"red": "röd",
|
||||
@@ -511,7 +522,9 @@
|
||||
"debugOfficeEditor": "Aktivera OnlyOffice felsökningsläge",
|
||||
"debugOfficeEditorDescription": "Visa en debug-popup med ytterligare information i OnlyOffice-redigeraren.",
|
||||
"fileViewerOptions": "Alternativ för filvisare",
|
||||
"themeAndLanguage": "Tema & språk"
|
||||
"themeAndLanguage": "Tema & språk",
|
||||
"previewFolder": "Visa miniatyrbilder för mappar om de är tillgängliga",
|
||||
"previewFolderDescription": "Visa förhandsvisningar av miniatyrbilder för mappar med första underordnade objekt med miniatyrbild som omslag."
|
||||
},
|
||||
"general": {
|
||||
"name": "Namn",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Daha fazla göster",
|
||||
"showLess": "Daha az göster",
|
||||
"openParentFolder": "Üst klasörü açın",
|
||||
"selectedCount": "Bir eylemi gerçekleştirmek için seçilen öğe sayısı"
|
||||
"selectedCount": "Bir eylemi gerçekleştirmek için seçilen öğe sayısı",
|
||||
"listView": "Liste Görünümü",
|
||||
"compactView": "Kompakt Görünüm",
|
||||
"normalView": "Normal Görünüm",
|
||||
"galleryView": "Galeri Görünümü"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Dosyayı indir",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Dosya sayısı{suffix}",
|
||||
"renameMessageConflict": "\"{filename}\" adında bir öğe zaten var!",
|
||||
"uploadSettingsChunked": "Maksimum eşzamanlı yükleme: {maxConcurrentUpload}, yığın boyutu: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "Maksimum eşzamanlı yükleme: {maxConcurrentUpload}, yığınlanmış yükleme devre dışı"
|
||||
"uploadSettingsNoChunk": "Maksimum eşzamanlı yükleme: {maxConcurrentUpload}, yığınlanmış yükleme devre dışı",
|
||||
"quickJump": "Diğer dosyalara hızlıca atlayın",
|
||||
"moveSuccess": "Dosyalar başarıyla taşındı!",
|
||||
"copySuccess": "Dosyalar başarıyla kopyalandı!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Görseller",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Bu paylaşımda OnlyOffice kullanarak ofis dosyalarının görüntülenmesine izin verin.",
|
||||
"enableOnlyOfficeEditing": "OnlyOffice düzenlemesini etkinleştirin",
|
||||
"enableOnlyOfficeEditingDescription": "Bu paylaşımda OnlyOffice kullanarak ofis dosyalarının düzenlenmesine izin verin.",
|
||||
"titleDefault": "Paylaşılan dosyalar - {title}"
|
||||
"titleDefault": "Paylaşılan dosyalar - {title}",
|
||||
"defaultViewMode": "Varsayılan görünüm modu",
|
||||
"defaultViewModeDescription": "Bu paylaşımı görüntüleyen anonim kullanıcılar için varsayılan görünüm modunu ayarlayın. Oturum açmış kullanıcılar yine de görünüm tercihlerini değiştirebilirler."
|
||||
},
|
||||
"api": {
|
||||
"title": "API Anahtarları:",
|
||||
@@ -482,7 +491,9 @@
|
||||
"maxConcurrentUpload": "Maksimum eşzamanlı yükleme",
|
||||
"maxConcurrentUploadHelp": "Eşzamanlı yüklemelerin maksimum sayısı. Daha yüksek değerler genellikle daha iyi performans gösterir, ancak bu birçok faktöre bağlıdır.",
|
||||
"uploadChunkSizeMb": "MB cinsinden yığın boyutu yükleme",
|
||||
"uploadChunkSizeMbHelp": "Tipik olarak 5 ila 50 MB, daha küçük kararsız veya kısıtlı bağlantılar için daha iyidir. 0 olarak ayarlandığında yükleme yığınlaması devre dışı bırakılır."
|
||||
"uploadChunkSizeMbHelp": "Tipik olarak 5 ila 50 MB, daha küçük kararsız veya kısıtlı bağlantılar için daha iyidir. 0 olarak ayarlandığında yükleme yığınlaması devre dışı bırakılır.",
|
||||
"clearAll": "Clear completed hata, duraklama ve çakışmayı kaldırmalıdır",
|
||||
"clearAllDescription": "Tamamlananları temizle düğmesi hata, duraklatılmış ve çakışma durumları da dahil olmak üzere bitmiş durumdaki tüm yüklemeleri temizler."
|
||||
},
|
||||
"colors": {
|
||||
"red": "kırmızı",
|
||||
@@ -547,7 +558,9 @@
|
||||
"debugOfficeEditor": "OnlyOffice Hata Ayıklama Modunu Etkinleştir",
|
||||
"debugOfficeEditorDescription": "OnlyOffice düzenleyicisinde ek bilgiler içeren bir hata ayıklama açılır penceresi gösterin.",
|
||||
"fileViewerOptions": "Dosya Görüntüleyici Seçenekleri",
|
||||
"themeAndLanguage": "Tema ve Dil"
|
||||
"themeAndLanguage": "Tema ve Dil",
|
||||
"previewFolder": "Varsa klasörler için küçük resimleri göster",
|
||||
"previewFolderDescription": "Kapak olarak küçük resmi olan ilk alt öğeye sahip klasörler için küçük resim önizlemelerini gösterin."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Düzenleyici başlatılamadı. Lütfen yeniden yükleyin.",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "Показати більше",
|
||||
"showLess": "Показати менше",
|
||||
"openParentFolder": "Відкрийте батьківську папку",
|
||||
"selectedCount": "Кількість елементів, вибраних для виконання дії"
|
||||
"selectedCount": "Кількість елементів, вибраних для виконання дії",
|
||||
"listView": "Перегляд списку",
|
||||
"compactView": "Компактний вигляд",
|
||||
"normalView": "Звичайний вигляд",
|
||||
"galleryView": "Перегляд галереї"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "Завантажити файл",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "Кількість файлів{suffix}",
|
||||
"renameMessageConflict": "Елемент з назвою \"{filename}\" вже існує!",
|
||||
"uploadSettingsChunked": "Максимальна кількість одночасних завантажень: {maxConcurrentUpload}, розмір фрагмента: {uploadChunkSizeMb} МБ",
|
||||
"uploadSettingsNoChunk": "Максимальне одночасне завантаження: {maxConcurrentUpload}, chunked upload вимкнено"
|
||||
"uploadSettingsNoChunk": "Максимальне одночасне завантаження: {maxConcurrentUpload}, chunked upload вимкнено",
|
||||
"quickJump": "Швидкий перехід до інших файлів",
|
||||
"moveSuccess": "Файли успішно перенесено!",
|
||||
"copySuccess": "Файли успішно скопійовано!"
|
||||
},
|
||||
"search": {
|
||||
"images": "Зображення",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "Дозволити перегляд офісних файлів за допомогою OnlyOffice у цьому ресурсі.",
|
||||
"enableOnlyOfficeEditing": "Увімкнути редагування OnlyOffice",
|
||||
"enableOnlyOfficeEditingDescription": "Дозволити редагування офісних файлів за допомогою OnlyOffice у цьому ресурсі.",
|
||||
"titleDefault": "Спільні файли - {title}"
|
||||
"titleDefault": "Спільні файли - {title}",
|
||||
"defaultViewMode": "Режим перегляду за замовчуванням",
|
||||
"defaultViewModeDescription": "Встановити режим перегляду за замовчуванням для анонімних користувачів, які переглядають цей ресурс. Користувачі, що увійшли в систему, можуть змінити режим перегляду."
|
||||
},
|
||||
"api": {
|
||||
"title": "Ключі API",
|
||||
@@ -446,7 +455,9 @@
|
||||
"maxConcurrentUpload": "Максимальне одночасне завантаження",
|
||||
"maxConcurrentUploadHelp": "Максимальна кількість одночасних завантажень. Вищі значення зазвичай працюють краще, але це залежить від багатьох факторів.",
|
||||
"uploadChunkSizeMb": "Розмір завантажуваного фрагмента в МБ",
|
||||
"uploadChunkSizeMbHelp": "Зазвичай від 5 до 50 МБ, менший розмір краще для нестабільних або обмежених з'єднань. Якщо встановити значення 0, вимкнеться фрагментація завантаження."
|
||||
"uploadChunkSizeMbHelp": "Зазвичай від 5 до 50 МБ, менший розмір краще для нестабільних або обмежених з'єднань. Якщо встановити значення 0, вимкнеться фрагментація завантаження.",
|
||||
"clearAll": "Очистити завершено має видалити помилку, паузу та конфлікт",
|
||||
"clearAllDescription": "Кнопка \"Очистити завершені\" очистить всі завантаження в завершеному стані, включаючи помилки, призупинені та конфліктні стани."
|
||||
},
|
||||
"colors": {
|
||||
"red": "червоний",
|
||||
@@ -511,7 +522,9 @@
|
||||
"debugOfficeEditor": "Увімкнути режим налагодження OnlyOffice",
|
||||
"debugOfficeEditorDescription": "Показати налагоджувальне спливаюче вікно з додатковою інформацією в редакторі OnlyOffice.",
|
||||
"fileViewerOptions": "Параметри перегляду файлів",
|
||||
"themeAndLanguage": "Тема та мова"
|
||||
"themeAndLanguage": "Тема та мова",
|
||||
"previewFolder": "Показувати мініатюри для папок, якщо вони доступні",
|
||||
"previewFolderDescription": "Показувати попередній перегляд мініатюр для папок з першим дочірнім елементом, що має мініатюру як обкладинку."
|
||||
},
|
||||
"general": {
|
||||
"name": "Ім'я",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "显示更多",
|
||||
"showLess": "显示更少",
|
||||
"openParentFolder": "打开父文件夹",
|
||||
"selectedCount": "选择执行操作的项目数"
|
||||
"selectedCount": "选择执行操作的项目数",
|
||||
"listView": "列表查看",
|
||||
"compactView": "紧凑型视图",
|
||||
"normalView": "正常视图",
|
||||
"galleryView": "图库浏览"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "下载文件",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "文件数量{suffix}",
|
||||
"renameMessageConflict": "名为 \"{filename}\" 的项目已经存在!",
|
||||
"uploadSettingsChunked": "最大并发上传:{maxConcurrentUpload}块大小 {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "最大并发上传:{maxConcurrentUpload}已禁用分块上传"
|
||||
"uploadSettingsNoChunk": "最大并发上传:{maxConcurrentUpload}已禁用分块上传",
|
||||
"quickJump": "快速跳转到其他文件",
|
||||
"moveSuccess": "文件移动成功!",
|
||||
"copySuccess": "文件复制成功!"
|
||||
},
|
||||
"search": {
|
||||
"images": "图像",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "允许在此共享中使用 OnlyOffice 查看办公文件。",
|
||||
"enableOnlyOfficeEditing": "启用 OnlyOffice 编辑功能",
|
||||
"enableOnlyOfficeEditingDescription": "允许在此共享中使用 OnlyOffice 编辑办公文件。",
|
||||
"titleDefault": "共享文件{title}"
|
||||
"titleDefault": "共享文件{title}",
|
||||
"defaultViewMode": "默认视图模式",
|
||||
"defaultViewModeDescription": "设置匿名用户查看此共享的默认视图模式。已登录的用户仍可更改其查看首选项。"
|
||||
},
|
||||
"api": {
|
||||
"title": "应用程序接口密钥",
|
||||
@@ -446,7 +455,9 @@
|
||||
"maxConcurrentUpload": "最大并发上传",
|
||||
"maxConcurrentUploadHelp": "最大并发上传次数。数值越大,性能通常越好,但这取决于很多因素。",
|
||||
"uploadChunkSizeMb": "上传块大小(以 MB 为单位",
|
||||
"uploadChunkSizeMbHelp": "通常为 5 至 50 MB,对于不稳定或受限制的连接,越小越好。设置为 0 将禁用上传分块。"
|
||||
"uploadChunkSizeMbHelp": "通常为 5 至 50 MB,对于不稳定或受限制的连接,越小越好。设置为 0 将禁用上传分块。",
|
||||
"clearAll": "清除完成后,应消除错误、暂停和冲突",
|
||||
"clearAllDescription": "清除已完成按钮将清除所有处于完成状态的上传,包括错误、暂停和冲突状态。"
|
||||
},
|
||||
"colors": {
|
||||
"red": "红",
|
||||
@@ -511,7 +522,9 @@
|
||||
"debugOfficeEditor": "启用 OnlyOffice 调试模式",
|
||||
"debugOfficeEditorDescription": "在 OnlyOffice 编辑器中显示包含附加信息的调试弹出窗口。",
|
||||
"fileViewerOptions": "文件查看器选项",
|
||||
"themeAndLanguage": "主题与语言"
|
||||
"themeAndLanguage": "主题与语言",
|
||||
"previewFolder": "如果有的话,显示文件夹的缩略图",
|
||||
"previewFolderDescription": "显示文件夹的缩略图预览,第一个子项的缩略图为封面。"
|
||||
},
|
||||
"general": {
|
||||
"name": "名称",
|
||||
|
||||
@@ -50,7 +50,11 @@
|
||||
"showMore": "顯示更多",
|
||||
"showLess": "顯示更少",
|
||||
"openParentFolder": "開啟父資料夾",
|
||||
"selectedCount": "選取執行動作的項目數量"
|
||||
"selectedCount": "選取執行動作的項目數量",
|
||||
"listView": "清單檢視",
|
||||
"compactView": "緊湊型視圖",
|
||||
"normalView": "正常檢視",
|
||||
"galleryView": "圖庫瀏覽"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "下載檔案",
|
||||
@@ -188,7 +192,10 @@
|
||||
"numberFiles": "檔案數量{suffix}",
|
||||
"renameMessageConflict": "名為 \"{filename}\" 的項目已經存在!",
|
||||
"uploadSettingsChunked": "最大同時上傳數量:{maxConcurrentUpload}, 分塊大小: {uploadChunkSizeMb} MB",
|
||||
"uploadSettingsNoChunk": "最大同時上傳數量:{maxConcurrentUpload}, 已停用分塊上傳"
|
||||
"uploadSettingsNoChunk": "最大同時上傳數量:{maxConcurrentUpload}, 已停用分塊上傳",
|
||||
"quickJump": "快速跳到其他檔案",
|
||||
"moveSuccess": "檔案移動成功!",
|
||||
"copySuccess": "檔案已成功複製!"
|
||||
},
|
||||
"search": {
|
||||
"images": "影像",
|
||||
@@ -390,7 +397,9 @@
|
||||
"enableOnlyOfficeDescription": "允許在此共用中使用 OnlyOffice 檢視辦公室檔案。",
|
||||
"enableOnlyOfficeEditing": "啟用 OnlyOffice 編輯",
|
||||
"enableOnlyOfficeEditingDescription": "允許在此共用中使用 OnlyOffice 編輯辦公室檔案。",
|
||||
"titleDefault": "共用檔案 -{title}"
|
||||
"titleDefault": "共用檔案 -{title}",
|
||||
"defaultViewMode": "預設檢視模式",
|
||||
"defaultViewModeDescription": "設定匿名使用者檢視此分享的預設檢視模式。登入的使用者仍可變更檢視偏好。"
|
||||
},
|
||||
"api": {
|
||||
"title": "API 金鑰",
|
||||
@@ -485,7 +494,9 @@
|
||||
"debugOfficeEditor": "啟用 OnlyOffice 除錯模式",
|
||||
"debugOfficeEditorDescription": "在 OnlyOffice 編輯器中顯示包含其他資訊的除錯彈出式視窗。",
|
||||
"fileViewerOptions": "檔案檢視器選項",
|
||||
"themeAndLanguage": "主題與語言"
|
||||
"themeAndLanguage": "主題與語言",
|
||||
"previewFolder": "顯示資料夾的縮圖(若有",
|
||||
"previewFolderDescription": "顯示資料夾的縮圖預覽,第一個子項目以縮圖為封面。"
|
||||
},
|
||||
"otp": {
|
||||
"name": "雙因素驗證",
|
||||
@@ -503,7 +514,9 @@
|
||||
"maxConcurrentUpload": "最大同時上傳數量",
|
||||
"maxConcurrentUploadHelp": "最大同時上傳數量。較高的值通常效能較佳,但這取決於許多因素。",
|
||||
"uploadChunkSizeMb": "上傳區塊大小(以 MB 為單位",
|
||||
"uploadChunkSizeMbHelp": "通常為 5 到 50 MB,對於不穩定或受限制的連線,越小越好。設定為 0 會停用上傳分塊。"
|
||||
"uploadChunkSizeMbHelp": "通常為 5 到 50 MB,對於不穩定或受限制的連線,越小越好。設定為 0 會停用上傳分塊。",
|
||||
"clearAll": "清除完成應移除錯誤、暫停和衝突",
|
||||
"clearAllDescription": "清除完成按鈕將會清除所有處於完成狀態的上傳,包括錯誤、暫停和衝突狀態。"
|
||||
},
|
||||
"colors": {
|
||||
"red": "紅色",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mutations, state } from '@/store'
|
||||
import { notify } from '@/notify'
|
||||
import { baseURL } from '@/utils/constants'
|
||||
import { globalVars } from '@/utils/constants'
|
||||
import { filesApi } from '@/api'
|
||||
|
||||
let eventSrc = null
|
||||
@@ -47,7 +47,7 @@ function clearReconnect () {
|
||||
|
||||
// Test the events endpoint to check for authentication before setting up EventSource
|
||||
async function testEventsEndpoint() {
|
||||
const url = `${baseURL}api/events?sessionId=${state.sessionId}`
|
||||
const url = `${globalVars.baseURL}api/events?sessionId=${state.sessionId}`
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -85,7 +85,7 @@ async function setupSSE () {
|
||||
}
|
||||
}
|
||||
|
||||
const url = `${baseURL}api/events?sessionId=${state.sessionId}`
|
||||
const url = `${globalVars.baseURL}api/events?sessionId=${state.sessionId}`
|
||||
eventSrc = new EventSource(url)
|
||||
isManuallyClosed = false
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { RouteLocation, createRouter, createWebHistory } from "vue-router";
|
||||
import { RouteLocation, createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
|
||||
import Login from "@/views/Login.vue";
|
||||
import Layout from "@/views/Layout.vue";
|
||||
import Files from "@/views/Files.vue";
|
||||
import Settings from "@/views/Settings.vue";
|
||||
import Errors from "@/views/Errors.vue";
|
||||
import { baseURL, name, oidcAvailable, passwordAvailable } from "@/utils/constants";
|
||||
import { globalVars } from "@/utils/constants";
|
||||
import { getters, state } from "@/store";
|
||||
import { mutations } from "@/store";
|
||||
import { validateLogin } from "@/utils/auth";
|
||||
@@ -28,7 +28,7 @@ const titles = {
|
||||
InternalServerError: "errors.internal",
|
||||
};
|
||||
|
||||
const routes = [
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/login",
|
||||
name: "Login",
|
||||
@@ -132,7 +132,7 @@ const routes = [
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(baseURL),
|
||||
history: createWebHistory(globalVars.baseURL),
|
||||
routes,
|
||||
});
|
||||
|
||||
@@ -148,7 +148,7 @@ router.beforeResolve(async (to, from, next) => {
|
||||
}
|
||||
|
||||
const title = i18n.global.t(titles[to.name as keyof typeof titles]);
|
||||
document.title = name + " - " + title;
|
||||
document.title = globalVars.name + " - " + title;
|
||||
mutations.setRoute(to);
|
||||
|
||||
if (
|
||||
@@ -168,14 +168,14 @@ router.beforeResolve(async (to, from, next) => {
|
||||
if (getters.isLoggedIn() || to.matched.some((record) => record.meta.optionalAuth)) {
|
||||
// do nothing
|
||||
} else {
|
||||
if (passwordAvailable) {
|
||||
if (globalVars.passwordAvailable) {
|
||||
next({ path: "/login", query: { redirect: to.fullPath } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (oidcAvailable) {
|
||||
const modifiedPath = encodeURIComponent(baseURL+removeLeadingSlash(to.fullPath))
|
||||
window.location.href = baseURL+`api/auth/oidc/login?redirect=${modifiedPath}`;
|
||||
if (globalVars.oidcAvailable) {
|
||||
const modifiedPath = encodeURIComponent(globalVars.baseURL+removeLeadingSlash(to.fullPath))
|
||||
window.location.href = globalVars.baseURL+`api/auth/oidc/login?redirect=${modifiedPath}`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { removePrefix, buildItemUrl, removeLeadingSlash } from '@/utils/url.js'
|
||||
import { getFileExtension } from '@/utils/files.js'
|
||||
import { state, mutations } from '@/store'
|
||||
import { noAuth, shareInfo } from '@/utils/constants.js'
|
||||
import { globalVars, shareInfo, previewViews } from '@/utils/constants.js'
|
||||
import { getTypeInfo } from '@/utils/mimetype'
|
||||
import { fromNow } from '@/utils/moment'
|
||||
import * as i18n from '@/i18n'
|
||||
@@ -32,9 +32,30 @@ export const getters = {
|
||||
}
|
||||
return true
|
||||
},
|
||||
displayPreference: () => {
|
||||
let source = state.sources.current;
|
||||
if (getters.isShare()) {
|
||||
source = getters.currentHash();
|
||||
}
|
||||
const path = state.route.path;
|
||||
if (state.displayPreferences?.[source]?.[path]) {
|
||||
return state.displayPreferences[source][path];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
viewMode: () => {
|
||||
// If user is anonymous and on a share, check for defaultViewMode override
|
||||
if (getters.isShare() && state.user?.username === 'anonymous' && shareInfo.viewMode) {
|
||||
return shareInfo.viewMode;
|
||||
}
|
||||
return getters.displayPreference()?.viewMode || state.user.viewMode || "normal";
|
||||
},
|
||||
sorting: () => {
|
||||
return getters.displayPreference()?.sorting || state.user.sorting || { by: "name", asc: true };
|
||||
},
|
||||
previewType: () => getTypeInfo(state.req.type).simpleType,
|
||||
isCardView: () =>
|
||||
(state.user.viewMode == 'gallery' || state.user.viewMode == 'normal') &&
|
||||
(getters.viewMode() == 'gallery' || getters.viewMode() == 'normal') &&
|
||||
getters.currentView() == 'listingView',
|
||||
currentHash: () => shareInfo.hash,
|
||||
isMobile: () => state.isMobile,
|
||||
@@ -66,7 +87,7 @@ export const getters = {
|
||||
}
|
||||
mutations.updateCurrentUser({ locale: savedLocale })
|
||||
}
|
||||
if (noAuth) {
|
||||
if (globalVars.noAuth) {
|
||||
return true
|
||||
}
|
||||
if (
|
||||
@@ -150,15 +171,6 @@ export const getters = {
|
||||
if (cv == 'onlyOfficeEditor') {
|
||||
return false
|
||||
}
|
||||
const previewViews = [
|
||||
'preview',
|
||||
'markdownViewer',
|
||||
'epubViewer',
|
||||
'docViewer',
|
||||
'onlyOfficeEditor',
|
||||
'editor'
|
||||
]
|
||||
|
||||
let visible = (state.showSidebar || getters.isStickySidebar())
|
||||
if (getters.currentPromptName() && !getters.isStickySidebar()) {
|
||||
visible = false
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
// store/index.js
|
||||
// store/index.ts
|
||||
import { state } from "./state.js";
|
||||
import { getters } from "./getters.js";
|
||||
import { mutations } from "./mutations.js";
|
||||
import type { StoreState } from "./types";
|
||||
|
||||
// Type assertion to tell TypeScript about the actual structure
|
||||
const typedState = state as StoreState;
|
||||
|
||||
export {
|
||||
state,
|
||||
typedState as state,
|
||||
getters,
|
||||
mutations
|
||||
};
|
||||
@@ -1,10 +1,14 @@
|
||||
import * as i18n from "@/i18n";
|
||||
import { state } from "./state.js";
|
||||
import { getters } from "./getters.js";
|
||||
import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js
|
||||
import { usersApi } from "@/api";
|
||||
import { notify } from "@/notify";
|
||||
import { sortedItems } from "@/utils/sort.js";
|
||||
import { serverHasMultipleSources } from "@/utils/constants.js";
|
||||
import { url } from "@/utils";
|
||||
import { getTypeInfo } from "@/utils/mimetype";
|
||||
import { filesApi } from "@/api";
|
||||
|
||||
export const mutations = {
|
||||
setPreviousHistoryItem: (value) => {
|
||||
@@ -241,7 +245,7 @@ export const mutations = {
|
||||
emitStateChanged();
|
||||
return;
|
||||
}
|
||||
if (value.username != "anonymous") {
|
||||
if (value.username !== "anonymous") {
|
||||
mutations.setSources(value);
|
||||
}
|
||||
// Ensure locale exists and is valid
|
||||
@@ -251,8 +255,14 @@ export const mutations = {
|
||||
i18n.setLocale(value.locale);
|
||||
}
|
||||
state.user = value;
|
||||
state.user.sorting = {};
|
||||
state.user.sorting.by = "name";
|
||||
state.user.sorting.asc = true;
|
||||
|
||||
// Load display preferences for the current user
|
||||
const allPreferences = JSON.parse(localStorage.getItem("displayPreferences") || "{}");
|
||||
state.displayPreferences = allPreferences[state.user.username] || {};
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
@@ -377,8 +387,9 @@ export const mutations = {
|
||||
}
|
||||
let sortby = "name"
|
||||
let asc = true
|
||||
sortby = state.user.sorting.by;
|
||||
asc = state.user.sorting.asc;
|
||||
const sorting = getters.sorting();
|
||||
sortby = sorting.by;
|
||||
asc = sorting.asc;
|
||||
// Separate directories and files
|
||||
const dirs = value.items.filter((item) => item.type === 'directory');
|
||||
const files = value.items.filter((item) => item.type !== 'directory');
|
||||
@@ -404,8 +415,7 @@ export const mutations = {
|
||||
if (!state.user.sorting) {
|
||||
state.user.sorting = {};
|
||||
}
|
||||
state.user.sorting.by = field;
|
||||
state.user.sorting.asc = asc;
|
||||
mutations.updateDisplayPreferences({ sorting: { by: field, asc: asc } });
|
||||
emitStateChanged();
|
||||
},
|
||||
updateListingItems: () => {
|
||||
@@ -461,4 +471,174 @@ export const mutations = {
|
||||
state.user.fileLoading.maxConcurrentUpload = value;
|
||||
emitStateChanged();
|
||||
},
|
||||
updateViewModeHistory: ({ source, path, viewMode }) => {
|
||||
if (!source || !path) return;
|
||||
if (!state.viewModeHistory) {
|
||||
state.viewModeHistory = {};
|
||||
}
|
||||
if (!state.viewModeHistory[source]) {
|
||||
state.viewModeHistory[source] = {};
|
||||
}
|
||||
state.viewModeHistory[source][path] = viewMode;
|
||||
localStorage.setItem("viewModeHistory", JSON.stringify(state.viewModeHistory));
|
||||
emitStateChanged();
|
||||
},
|
||||
updateDisplayPreferences: (payload) => {
|
||||
let source = state.sources.current;
|
||||
if (getters.isShare()) {
|
||||
source = getters.currentHash();
|
||||
}
|
||||
const path = state.route.path;
|
||||
|
||||
if (!source || !path) return;
|
||||
if (!state.displayPreferences) {
|
||||
state.displayPreferences = {};
|
||||
}
|
||||
if (!state.displayPreferences[source]) {
|
||||
state.displayPreferences[source] = {};
|
||||
}
|
||||
if (!state.displayPreferences[source][path]) {
|
||||
state.displayPreferences[source][path] = {};
|
||||
}
|
||||
|
||||
state.displayPreferences[source][path] = {
|
||||
...state.displayPreferences[source][path],
|
||||
...payload,
|
||||
};
|
||||
|
||||
const allPreferences = JSON.parse(localStorage.getItem("displayPreferences") || "{}");
|
||||
if (!allPreferences[state.user.username]) {
|
||||
allPreferences[state.user.username] = {};
|
||||
}
|
||||
allPreferences[state.user.username] = state.displayPreferences;
|
||||
localStorage.setItem("displayPreferences", JSON.stringify(allPreferences));
|
||||
|
||||
emitStateChanged();
|
||||
},
|
||||
setNavigationEnabled: (enabled) => {
|
||||
if (state.navigation.enabled === enabled) {
|
||||
return;
|
||||
}
|
||||
state.navigation.enabled = enabled;
|
||||
if (!enabled) {
|
||||
mutations.clearNavigation();
|
||||
}
|
||||
emitStateChanged();
|
||||
},
|
||||
setupNavigation: ({ listing, currentItem, directoryPath }) => {
|
||||
state.navigation.listing = listing;
|
||||
state.navigation.currentIndex = -1;
|
||||
state.navigation.previousItem = null;
|
||||
state.navigation.nextItem = null;
|
||||
state.navigation.previousLink = "";
|
||||
state.navigation.nextLink = "";
|
||||
state.navigation.previousRaw = "";
|
||||
state.navigation.nextRaw = "";
|
||||
|
||||
if (!listing || !currentItem) {
|
||||
emitStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find current item index in the listing
|
||||
for (let i = 0; i < listing.length; i++) {
|
||||
if (listing[i].name === currentItem.name) {
|
||||
state.navigation.currentIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.navigation.currentIndex === -1) {
|
||||
emitStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find previous item (skip directories)
|
||||
for (let j = state.navigation.currentIndex - 1; j >= 0; j--) {
|
||||
let item = listing[j];
|
||||
if (item.type === 'directory') continue;
|
||||
|
||||
item.path = directoryPath + "/" + item.name;
|
||||
state.navigation.previousItem = item;
|
||||
state.navigation.previousLink = url.buildItemUrl(item.source, item.path);
|
||||
|
||||
if (getTypeInfo(item.type).simpleType === "image") {
|
||||
state.navigation.previousRaw = mutations.getPrefetchUrl(item);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Find next item (skip directories)
|
||||
for (let j = state.navigation.currentIndex + 1; j < listing.length; j++) {
|
||||
let item = listing[j];
|
||||
if (item.type === 'directory') continue;
|
||||
|
||||
item.path = directoryPath + "/" + item.name;
|
||||
state.navigation.nextItem = item;
|
||||
state.navigation.nextLink = url.buildItemUrl(item.source, item.path);
|
||||
|
||||
if (getTypeInfo(item.type).simpleType === "image") {
|
||||
state.navigation.nextRaw = mutations.getPrefetchUrl(item);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
emitStateChanged();
|
||||
|
||||
// Auto-show navigation when it's first set up
|
||||
if (state.navigation.enabled && (state.navigation.previousLink || state.navigation.nextLink)) {
|
||||
mutations.setNavigationShow(true);
|
||||
setTimeout(() => {
|
||||
if (!state.navigation.hoverNav) {
|
||||
mutations.setNavigationShow(false);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
},
|
||||
getPrefetchUrl: (item) => {
|
||||
if (getters.isShare()) {
|
||||
return filesApi.getDownloadURL(state.req.source, item.path, true);
|
||||
}
|
||||
return filesApi.getDownloadURL(state.req.source, item.path, true);
|
||||
},
|
||||
setNavigationShow: (show) => {
|
||||
if (state.navigation.show === show) {
|
||||
return;
|
||||
}
|
||||
state.navigation.show = show;
|
||||
emitStateChanged();
|
||||
},
|
||||
setNavigationHover: (hover) => {
|
||||
if (state.navigation.hoverNav === hover) {
|
||||
return;
|
||||
}
|
||||
state.navigation.hoverNav = hover;
|
||||
emitStateChanged();
|
||||
},
|
||||
setNavigationTimeout: (timeout) => {
|
||||
if (state.navigation.timeout) {
|
||||
clearTimeout(state.navigation.timeout);
|
||||
}
|
||||
state.navigation.timeout = timeout;
|
||||
},
|
||||
clearNavigationTimeout: () => {
|
||||
if (state.navigation.timeout) {
|
||||
clearTimeout(state.navigation.timeout);
|
||||
state.navigation.timeout = null;
|
||||
}
|
||||
},
|
||||
clearNavigation: () => {
|
||||
state.navigation.show = false;
|
||||
state.navigation.hoverNav = false;
|
||||
state.navigation.listing = null;
|
||||
state.navigation.currentIndex = -1;
|
||||
state.navigation.previousItem = null;
|
||||
state.navigation.nextItem = null;
|
||||
state.navigation.previousLink = "";
|
||||
state.navigation.nextLink = "";
|
||||
state.navigation.previousRaw = "";
|
||||
state.navigation.nextRaw = "";
|
||||
mutations.clearNavigationTimeout();
|
||||
emitStateChanged();
|
||||
},
|
||||
};
|
||||
@@ -24,6 +24,7 @@ export const state = reactive({
|
||||
isMobile: window.innerWidth <= 800,
|
||||
isSearchActive: false,
|
||||
showSidebar: false,
|
||||
displayPreferences: {},
|
||||
usages: {},
|
||||
editor: null,
|
||||
serverHasMultipleSources: false,
|
||||
@@ -117,6 +118,20 @@ export const state = reactive({
|
||||
files: "",
|
||||
},
|
||||
},
|
||||
navigation: {
|
||||
show: false,
|
||||
hoverNav: false,
|
||||
listing: null,
|
||||
currentIndex: -1,
|
||||
previousItem: null,
|
||||
nextItem: null,
|
||||
previousLink: "",
|
||||
nextLink: "",
|
||||
previousRaw: "",
|
||||
nextRaw: "",
|
||||
timeout: null,
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
function stickyStartup() {
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
// Store type definitions
|
||||
|
||||
export interface ReqObject {
|
||||
// Base properties always present
|
||||
sorting: {
|
||||
by: string;
|
||||
asc: boolean;
|
||||
};
|
||||
items: any[];
|
||||
numDirs: number;
|
||||
numFiles: number;
|
||||
|
||||
// File-specific properties (added dynamically)
|
||||
name?: string;
|
||||
path?: string;
|
||||
size?: number;
|
||||
type?: string;
|
||||
source?: string;
|
||||
content?: string;
|
||||
modified?: string;
|
||||
subtitles?: any[];
|
||||
|
||||
// Directory listing properties
|
||||
listing?: any[];
|
||||
}
|
||||
|
||||
export interface ShareObject {
|
||||
hash: string | null;
|
||||
token: string;
|
||||
subPath: string;
|
||||
}
|
||||
|
||||
export interface UserObject {
|
||||
preview: {
|
||||
video: boolean;
|
||||
image: boolean;
|
||||
popup: boolean;
|
||||
highQuality: boolean;
|
||||
autoplayMedia?: boolean;
|
||||
defaultMediaPlayer?: boolean;
|
||||
};
|
||||
loginType: string;
|
||||
username: string;
|
||||
quickDownloadEnabled: boolean;
|
||||
gallarySize: number;
|
||||
singleClick: boolean;
|
||||
stickySidebar: boolean;
|
||||
locale: string;
|
||||
viewMode: string;
|
||||
showHidden: boolean;
|
||||
scopes: any[];
|
||||
permissions: any;
|
||||
darkMode: boolean;
|
||||
disableSettings: boolean;
|
||||
debugOffice: boolean;
|
||||
profile: {
|
||||
username: string;
|
||||
email: string;
|
||||
avatarUrl: string;
|
||||
};
|
||||
|
||||
// Optional properties that may be added dynamically
|
||||
disableViewingExt?: string[];
|
||||
displayNames?: string[];
|
||||
id?: number;
|
||||
password?: string;
|
||||
scope?: string;
|
||||
rules?: any[];
|
||||
lockPassword?: boolean;
|
||||
hideDotfiles?: boolean;
|
||||
commands?: string[];
|
||||
sorting?: {
|
||||
by: string;
|
||||
asc: boolean;
|
||||
};
|
||||
dateFormat?: boolean;
|
||||
perm?: any;
|
||||
email?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export interface RouteObject {
|
||||
name?: string;
|
||||
path?: string;
|
||||
params?: any;
|
||||
query?: any;
|
||||
}
|
||||
|
||||
export interface StoreState {
|
||||
tooltip: {
|
||||
show: boolean;
|
||||
content: string;
|
||||
x: number;
|
||||
y: number;
|
||||
pointerEvents: boolean;
|
||||
width: number | null;
|
||||
};
|
||||
previousHistoryItem: {
|
||||
name: string;
|
||||
source: string;
|
||||
path: string;
|
||||
};
|
||||
contextMenuHasItems: boolean;
|
||||
deletedItem: boolean;
|
||||
showOverflowMenu: boolean;
|
||||
sessionId: string;
|
||||
isSafari: boolean;
|
||||
activeSettingsView: string;
|
||||
isMobile: boolean;
|
||||
isSearchActive: boolean;
|
||||
showSidebar: boolean;
|
||||
displayPreferences: any;
|
||||
usages: any;
|
||||
editor: any;
|
||||
serverHasMultipleSources: boolean;
|
||||
realtimeActive: boolean | undefined;
|
||||
realtimeDownCount: number;
|
||||
popupPreviewSource: string;
|
||||
share: ShareObject;
|
||||
sources: {
|
||||
current: string;
|
||||
count: number;
|
||||
hasSourceInfo: boolean;
|
||||
info: any;
|
||||
};
|
||||
user: UserObject;
|
||||
req: ReqObject;
|
||||
listing: {
|
||||
category: string;
|
||||
letter: string;
|
||||
scrolling: boolean;
|
||||
scrollRatio: number;
|
||||
};
|
||||
previewRaw: string;
|
||||
oldReq: any;
|
||||
clipboard: {
|
||||
key: string;
|
||||
items: any[];
|
||||
};
|
||||
jwt: string;
|
||||
sharePassword: string;
|
||||
loading: any[];
|
||||
reload: boolean;
|
||||
selected: any[];
|
||||
lastSelectedIndex: number | null;
|
||||
multiple: boolean;
|
||||
upload: {
|
||||
uploads: any;
|
||||
queue: any[];
|
||||
progress: any[];
|
||||
sizes: any[];
|
||||
};
|
||||
prompts: any[];
|
||||
show: any;
|
||||
showConfirm: any;
|
||||
route: RouteObject;
|
||||
settings: {
|
||||
signup: boolean;
|
||||
createUserDir: boolean;
|
||||
userHomeBasePath: string;
|
||||
rules: any[];
|
||||
frontend: {
|
||||
disableExternal: boolean;
|
||||
name: string;
|
||||
files: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mutations, getters,state } from "@/store";
|
||||
import { usersApi } from "@/api";
|
||||
import { getApiPath } from "@/utils/url.js";
|
||||
import { recaptcha, loginPage, baseURL } from "@/utils/constants";
|
||||
import { globalVars } from "@/utils/constants";
|
||||
|
||||
export async function setNewToken(token) {
|
||||
document.cookie = `auth=${token}; path=/`;
|
||||
@@ -65,7 +65,7 @@ export async function logout() {
|
||||
mutations.setCurrentUser(null);
|
||||
mutations.setJWT("");
|
||||
if (!logoutUrl) {
|
||||
logoutUrl = baseURL+"login";
|
||||
logoutUrl = globalVars.baseURL+"login";
|
||||
}
|
||||
// Add a small delay to ensure cookie deletion completes before redirect
|
||||
setTimeout(() => {
|
||||
@@ -92,11 +92,11 @@ export async function logout() {
|
||||
//}
|
||||
|
||||
export async function initAuth() {
|
||||
if (loginPage && !getters.isShare()) {
|
||||
if (globalVars.loginPage && !getters.isShare()) {
|
||||
console.log("validating login");
|
||||
await validateLogin();
|
||||
}
|
||||
if (recaptcha) {
|
||||
if (globalVars.recaptcha) {
|
||||
await new Promise((resolve) => {
|
||||
const check = () => {
|
||||
if (typeof window.grecaptcha === "undefined") {
|
||||
|
||||
@@ -1,30 +1,7 @@
|
||||
const name = window.filebrowser.name;
|
||||
const disableExternal = window.filebrowser.disableExternal;
|
||||
const externalLinks = window.filebrowser.externalLinks;
|
||||
const baseURL = window.filebrowser.baseURL;
|
||||
const staticURL = window.filebrowser.staticURL;
|
||||
const darkMode = window.filebrowser.darkMode;
|
||||
const recaptcha = window.filebrowser.recaptcha;
|
||||
const recaptchaKey = window.filebrowser.recaptchaKey;
|
||||
const signup = window.filebrowser.signup;
|
||||
const version = window.filebrowser.version;
|
||||
const commitSHA = window.filebrowser.commitSHA;
|
||||
const logoURL = `${staticURL}/img/logo.png`;
|
||||
const noAuth = window.filebrowser.noAuth;
|
||||
const loginPage = window.filebrowser.loginPage;
|
||||
const enableThumbs = window.filebrowser.enableThumbs;
|
||||
const externalUrl = window.filebrowser.externalUrl
|
||||
const onlyOfficeUrl = window.filebrowser.onlyOfficeUrl
|
||||
const serverHasMultipleSources = window.filebrowser.sourceCount > 1;
|
||||
const oidcAvailable = window.filebrowser.oidcAvailable;
|
||||
const passwordAvailable = window.filebrowser.passwordAvailable;
|
||||
const mediaAvailable = window.filebrowser.mediaAvailable;
|
||||
const muPdfAvailable = window.filebrowser.muPdfAvailable;
|
||||
const updateAvailable = window.filebrowser.updateAvailable;
|
||||
const disableNavButtons = window.filebrowser.disableNavButtons;
|
||||
const userSelectableThemes = window.filebrowser.userSelectableThemes;
|
||||
const minSearchLength = window.filebrowser.minSearchLength;
|
||||
const shareInfo = window.filebrowser.share;
|
||||
const globalVars = window.globalVars;
|
||||
const logoURL = `${globalVars.staticURL}/img/logo.png`;
|
||||
const serverHasMultipleSources = globalVars.sourceCount > 1;
|
||||
const shareInfo = window.shareVars;
|
||||
const origin = window.location.origin;
|
||||
|
||||
const settings = [
|
||||
@@ -37,33 +14,21 @@ const settings = [
|
||||
{ id: 'systemAdmin', label: 'settings.systemAdmin', component: 'SystemAdmin', permissions: { admin: true } },
|
||||
];
|
||||
|
||||
const previewViews = [
|
||||
'preview',
|
||||
'markdownViewer',
|
||||
'epubViewer',
|
||||
'docViewer',
|
||||
'onlyOfficeEditor',
|
||||
'editor'
|
||||
];
|
||||
|
||||
export {
|
||||
minSearchLength,
|
||||
globalVars,
|
||||
shareInfo,
|
||||
userSelectableThemes,
|
||||
disableNavButtons,
|
||||
updateAvailable,
|
||||
muPdfAvailable,
|
||||
mediaAvailable,
|
||||
oidcAvailable,
|
||||
passwordAvailable,
|
||||
serverHasMultipleSources,
|
||||
name,
|
||||
externalUrl,
|
||||
disableExternal,
|
||||
externalLinks,
|
||||
baseURL,
|
||||
logoURL,
|
||||
recaptcha,
|
||||
recaptchaKey,
|
||||
signup,
|
||||
version,
|
||||
commitSHA,
|
||||
noAuth,
|
||||
loginPage,
|
||||
enableThumbs,
|
||||
origin,
|
||||
darkMode,
|
||||
settings,
|
||||
onlyOfficeUrl,
|
||||
previewViews,
|
||||
};
|
||||
Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user