Compare commits

...
10 Commits
165 changed files with 4228 additions and 1491 deletions

No files matched your search

+28
View File
@@ -2,6 +2,34 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## v1.5.0
**New Features**:
- Added basic html viewer with relative reference support (#2522)
- Shares can have pinned files, requires a user to have edit access to share (#2522)
- Enhanced search
- now uses "lazy" match by default. (#2509)
- added missing `case sensitive` option in the UI
- Progressive Web App (PWA) improvements
- restored install prompt with sidebar install button (#2086)
- camera and video capture buttons on upload (mobile-friendly `capture` inputs)
- send files to other apps via the Web Share API (`Send to app` in the context menu)
- App notifications for file operations (#2478)
- optional browser notifications when uploads, chunked downloads, move/copy, or failures finish while the tab is in the background
- single on/off toggle in Notifications settings, stored in browser local storage
**Notes**:
- [docker] upgraded ffmpeg version 8.1 to 8.1.1
**BugFixes**:
- fixed PWA manifest `scope` and `id` so install works when the app is served under a base URL
- installing a public share as a PWA now opens the share URL instead of the site root (#2302)
## v1.4.4
**BugFixes**:
- Pinning/Unpinning file resets users' scope permission (#2532)
## v1.4.3
**Security**:
+1 -1
View File
@@ -1,4 +1,4 @@
FROM gtstef/ffmpeg:8.1-decode AS ffmpeg
FROM gtstef/ffmpeg:8.1.1-decode AS ffmpeg
FROM golang:alpine AS base
ARG VERSION
ARG REVISION
+14 -8
View File
@@ -223,12 +223,12 @@ func GetDirItems(opts utils.FileOptions, access *access.Storage, user *users.Use
// FileInfoFasterFunc is a variable that can be mocked in tests
var FileInfoFasterFunc = fileInfoFasterImpl
func FileInfoFaster(opts utils.FileOptions, access *access.Storage, user *users.User, share *share.Storage) (*iteminfo.ExtendedFileInfo, error) {
return FileInfoFasterFunc(opts, access, user, share)
func FileInfoFaster(opts utils.FileOptions, access *access.Storage, user *users.User, shareStore *share.Storage) (*iteminfo.ExtendedFileInfo, error) {
return FileInfoFasterFunc(opts, access, user, shareStore)
}
// fileInfoFasterImpl is the actual implementation of FileInfoFaster
func fileInfoFasterImpl(opts utils.FileOptions, access *access.Storage, user *users.User, share *share.Storage) (*iteminfo.ExtendedFileInfo, error) {
func fileInfoFasterImpl(opts utils.FileOptions, access *access.Storage, user *users.User, shareStore *share.Storage) (*iteminfo.ExtendedFileInfo, error) {
response := &iteminfo.ExtendedFileInfo{}
indexPath, userScope, topLevelErr := CheckPermissions(opts, access, user)
accessRulesErr := topLevelErr != nil && topLevelErr == errors.ErrAccessDenied && indexPath != ""
@@ -268,19 +268,25 @@ func fileInfoFasterImpl(opts utils.FileOptions, access *access.Storage, user *us
response.FileInfo = *info
response.RealPath = filepath.Join(idx.Path, indexPath)
response.Source = opts.Source
if share != nil && user.Permissions.Share && opts.ShowSharedAttr {
if shareStore != nil && user.Permissions.Share && opts.ShowSharedAttr {
for i := range response.Files {
file := &response.Files[i]
file.IsShared = share.IsShared(response.Path+file.Name, idx.Path, user.ID)
file.IsShared = shareStore.IsShared(response.Path+file.Name, idx.Path, user.ID)
}
for i := range response.Folders {
folder := &response.Folders[i]
folder.IsShared = share.IsShared(response.Path+folder.Name, idx.Path, user.ID)
folder.IsShared = shareStore.IsShared(response.Path+folder.Name, idx.Path, user.ID)
}
response.IsShared = share.IsShared(response.Path, idx.Path, user.ID)
response.IsShared = shareStore.IsShared(response.Path, idx.Path, user.ID)
}
if info.Type == "directory" && opts.ShowPinnedItems {
if source, ok := users.ResolveSourceKey(response.Source); ok {
if opts.ShareHash != "" && shareStore != nil {
if link, err := shareStore.GetByHash(opts.ShareHash); err == nil {
if shareRelDir, err := link.ShareRelativeDir(response.Path); err == nil {
response.PinnedItems = link.PinnedItems.NamesForDirectory(shareRelDir)
}
}
} else if source, ok := users.ResolveSourceKey(response.Source); ok {
response.PinnedItems = user.PinnedNamesForDirectory(source.Path, response.Path)
}
}
+2 -1
View File
@@ -57,7 +57,8 @@ type FileOptions struct {
ExtractEmbeddedSubtitles bool // whether to extract embedded subtitles from media files
AlbumArt bool // whether to get album art from media files
ShowHidden bool // whether to show hidden files (true = show, false = hide)
ShowPinnedItems bool // whether to show pinned items
ShowPinnedItems bool // whether to show pinned items
ShareHash string // when set, pinned items are loaded from this share link
HideFileExt string // Hide files based on extensions (eg: '.lrc' or '.srt')
FollowSymlinks bool // whether to follow symlinks
Only string // whether to only get files or folders
+80
View File
@@ -0,0 +1,80 @@
package share
import (
"fmt"
"slices"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
)
// PinnedItems maps share-relative directory paths to pinned item names.
type PinnedItems map[string][]string
// Add records a pinned item name under shareRelDir.
func (p PinnedItems) Add(shareRelDir, name string) {
if p == nil {
return
}
items := p[shareRelDir]
if slices.Contains(items, name) {
return
}
p[shareRelDir] = append(items, name)
}
// Remove deletes a pinned item name and prunes empty map entries.
func (p PinnedItems) Remove(shareRelDir, name string) {
if p == nil {
return
}
items := p[shareRelDir]
idx := slices.Index(items, name)
if idx < 0 {
return
}
items = append(items[:idx], items[idx+1:]...)
if len(items) == 0 {
delete(p, shareRelDir)
} else {
p[shareRelDir] = items
}
}
// NamesForDirectory returns pinned item names for a share-relative directory path.
func (p PinnedItems) NamesForDirectory(shareRelDir string) []string {
if len(p) == 0 {
return nil
}
names := p[shareRelDir]
if len(names) == 0 {
return nil
}
out := make([]string, len(names))
copy(out, names)
return out
}
// EnsurePinnedItems returns a non-nil map for mutation.
func (l *Link) EnsurePinnedItems() PinnedItems {
if l.PinnedItems == nil {
l.PinnedItems = make(PinnedItems)
}
return l.PinnedItems
}
// ShareRelativeDir maps a source index directory path to a share-relative directory path.
// link.Path is the index path of the share root (e.g. "/share/").
func (l *Link) ShareRelativeDir(indexDirPath string) (string, error) {
shareRootIndex := utils.AddTrailingSlashIfNotExists(l.Path)
indexDirPath = utils.AddTrailingSlashIfNotExists(indexDirPath)
if !strings.HasPrefix(indexDirPath, shareRootIndex) {
return "", fmt.Errorf("path is outside share scope")
}
rel := strings.TrimPrefix(indexDirPath, shareRootIndex)
if rel == "" {
return "/", nil
}
return utils.AddTrailingSlashIfNotExists("/" + strings.TrimPrefix(rel, "/")), nil
}
@@ -0,0 +1,58 @@
package share
import "testing"
func TestSharePinnedItemsAddRemove(t *testing.T) {
p := make(PinnedItems)
const dir = "/"
const name = "vacation.jpg"
p.Add(dir, name)
if len(p[dir]) != 1 || p[dir][0] != name {
t.Fatalf("add failed: %#v", p)
}
p.Add(dir, name)
if len(p[dir]) != 1 {
t.Fatalf("duplicate add should be ignored: %#v", p)
}
p.Remove(dir, name)
if len(p) != 0 {
t.Fatalf("remove should prune empty entries: %#v", p)
}
}
func TestSharePinnedNamesForDirectory(t *testing.T) {
p := PinnedItems{
"/photos/": {"vacation.jpg", "notes.txt"},
}
names := p.NamesForDirectory("/photos/")
if len(names) != 2 || names[0] != "vacation.jpg" || names[1] != "notes.txt" {
t.Fatalf("unexpected names: %#v", names)
}
if got := p.NamesForDirectory("/missing/"); got != nil {
t.Fatalf("expected nil for missing directory, got %#v", got)
}
}
func TestShareRelativeDir(t *testing.T) {
link := &Link{CommonShare: CommonShare{Path: "/share/"}}
got, err := link.ShareRelativeDir("/share/")
if err != nil {
t.Fatalf("ShareRelativeDir root: %v", err)
}
if got != "/" {
t.Fatalf("got %q, want /", got)
}
got, err = link.ShareRelativeDir("/share/test [file2]/")
if err != nil {
t.Fatalf("ShareRelativeDir subdir: %v", err)
}
if got != "/test [file2]/" {
t.Fatalf("got %q, want /test [file2]/", got)
}
}
+6 -5
View File
@@ -60,11 +60,12 @@ type CreateBody struct {
// Link is the information needed to build a shareable link.
type Link struct {
CommonShare
Downloads int `json:"downloads"`
Hash string `json:"hash" storm:"id,index"`
UserID uint `json:"userID"`
Expire int64 `json:"expire"`
PasswordHash string `json:"password_hash,omitempty"`
Downloads int `json:"downloads"`
Hash string `json:"hash" storm:"id,index"`
UserID uint `json:"userID"`
Expire int64 `json:"expire"`
PasswordHash string `json:"password_hash,omitempty"`
PinnedItems PinnedItems `json:"pinnedItems,omitempty"`
// Token is a random value that will only be set when PasswordHash is set. It is
// URL-Safe and is used to download links in password-protected shares via a
// query arg.
+10 -6
View File
@@ -19,13 +19,17 @@ type SearchItem struct {
}
// appendNameGlobSQL adds AND (name GLOB ? OR ...) or AND (name GLOB ? AND ...) when joinAnd is true.
func appendNameGlobSQL(query string, args []interface{}, patterns []string, joinAnd bool) (string, []interface{}) {
func appendNameGlobSQL(query string, args []interface{}, patterns []string, joinAnd, caseExact bool) (string, []interface{}) {
if len(patterns) == 0 {
return query, args
}
nameExpr := "name"
if !caseExact {
nameExpr = "lower(name)"
}
parts := make([]string, len(patterns))
for i := range patterns {
parts[i] = "name GLOB ?"
parts[i] = nameExpr + " GLOB ?"
args = append(args, patterns[i])
}
join := " OR "
@@ -38,7 +42,7 @@ func appendNameGlobSQL(query string, args []interface{}, patterns []string, join
// SearchItems queries the database for items matching the search criteria for a single source.
// When nameGlobPatterns is non-empty (and largest is false), rows are restricted with SQLite name GLOB ... OR ....
// Returns rows that can be iterated to scan search results.
func (db *IndexDB) SearchItems(source string, scope string, largest bool, nameGlobPatterns []string, nameGlobPatternsAnd bool) (*sql.Rows, error) {
func (db *IndexDB) SearchItems(source string, scope string, largest bool, nameGlobPatterns []string, nameGlobPatternsAnd, caseExact bool) (*sql.Rows, error) {
query := `
SELECT path, name, size, mod_time, type, is_dir, has_preview
FROM index_items
@@ -54,7 +58,7 @@ func (db *IndexDB) SearchItems(source string, scope string, largest bool, nameGl
}
if !largest && len(nameGlobPatterns) > 0 {
query, args = appendNameGlobSQL(query, args, nameGlobPatterns, nameGlobPatternsAnd)
query, args = appendNameGlobSQL(query, args, nameGlobPatterns, nameGlobPatternsAnd, caseExact)
}
if largest {
@@ -67,7 +71,7 @@ func (db *IndexDB) SearchItems(source string, scope string, largest bool, nameGl
// SearchItemsMultiSource queries the database for items matching the search criteria across multiple sources.
// When nameGlobPatterns is non-empty (and largest is false), restricts rows with SQLite name GLOB ... OR ....
// Returns rows that can be iterated to scan search results.
func (db *IndexDB) SearchItemsMultiSource(sources []string, sourceScopes map[string]string, largest bool, nameGlobPatterns []string, nameGlobPatternsAnd bool) (*sql.Rows, error) {
func (db *IndexDB) SearchItemsMultiSource(sources []string, sourceScopes map[string]string, largest bool, nameGlobPatterns []string, nameGlobPatternsAnd, caseExact bool) (*sql.Rows, error) {
if len(sources) == 0 {
return nil, fmt.Errorf("at least one source is required")
}
@@ -119,7 +123,7 @@ func (db *IndexDB) SearchItemsMultiSource(sources []string, sourceScopes map[str
}
if !largest && len(nameGlobPatterns) > 0 {
query, args = appendNameGlobSQL(query, args, nameGlobPatterns, nameGlobPatternsAnd)
query, args = appendNameGlobSQL(query, args, nameGlobPatterns, nameGlobPatternsAnd, caseExact)
}
if largest {
+18 -18
View File
@@ -24,16 +24,16 @@ require (
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.52.0
golang.org/x/image v0.41.0
golang.org/x/mod v0.36.0
golang.org/x/net v0.55.0
golang.org/x/crypto v0.53.0
golang.org/x/image v0.42.0
golang.org/x/mod v0.37.0
golang.org/x/net v0.56.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.45.0
golang.org/x/sys v0.46.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.45.0
golang.org/x/tools v0.46.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.51.0
modernc.org/sqlite v1.52.0
)
require (
@@ -123,13 +123,13 @@ require (
github.com/go-openapi/jsonpointer v0.23.1 // indirect
github.com/go-openapi/jsonreference v0.21.6 // indirect
github.com/go-openapi/spec v0.22.5 // indirect
github.com/go-openapi/swag/conv v0.26.0 // indirect
github.com/go-openapi/swag/jsonname v0.26.0 // indirect
github.com/go-openapi/swag/jsonutils v0.26.0 // indirect
github.com/go-openapi/swag/loading v0.26.0 // indirect
github.com/go-openapi/swag/stringutils v0.26.0 // indirect
github.com/go-openapi/swag/typeutils v0.26.0 // indirect
github.com/go-openapi/swag/yamlutils v0.26.0 // indirect
github.com/go-openapi/swag/conv v0.26.1 // indirect
github.com/go-openapi/swag/jsonname v0.26.1 // indirect
github.com/go-openapi/swag/jsonutils v0.26.1 // indirect
github.com/go-openapi/swag/loading v0.26.1 // indirect
github.com/go-openapi/swag/stringutils v0.26.1 // indirect
github.com/go-openapi/swag/typeutils v0.26.1 // indirect
github.com/go-openapi/swag/yamlutils v0.26.1 // 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
@@ -147,7 +147,7 @@ require (
github.com/gofrs/flock v0.13.0 // indirect
github.com/gohugoio/hugo v0.149.1 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/geo v0.0.0-20260605061815-58f45aa500f1 // indirect
github.com/golang/geo v0.0.0-20260612074446-f1a45663b0f3 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/asciicheck v0.5.0 // indirect
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
@@ -288,13 +288,13 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/text v0.38.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
honnef.co/go/tools v0.7.0 // indirect
modernc.org/libc v1.72.5 // indirect
modernc.org/libc v1.73.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
mvdan.cc/gofumpt v0.9.2 // indirect
+42 -42
View File
@@ -323,22 +323,22 @@ github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6v
github.com/go-openapi/spec v0.22.5 h1:KhO7RBlKQfonUWX2WzQCoLIXVA6AcNqDGZ3a1Dutdlo=
github.com/go-openapi/spec v0.22.5/go.mod h1:vxpOtMya5TXtENXKE5bKqv5NjocVhyhxHrlZfvKnZ74=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I=
github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE=
github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=
github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=
github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA=
github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y=
github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko=
github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg=
github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg=
github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE=
github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4=
github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE=
github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ=
github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU=
github.com/go-openapi/swag/conv v0.26.1 h1:slr5FVkg9Wc3Y5zcwenD8Sd/PQ94b2I/QJI7N7KTBpg=
github.com/go-openapi/swag/conv v0.26.1/go.mod h1:mvQXgPptZk9GTrFgGwWvT4q+dN+zQej9JfmGwnipz1A=
github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE=
github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc=
github.com/go-openapi/swag/jsonutils v0.26.1 h1:2hdBfFkHg+7Wrz2VsCbeyR6hzkRDs7AztnMR2u84yOY=
github.com/go-openapi/swag/jsonutils v0.26.1/go.mod h1:U+RMJH3wa+6BRiphuRtIyI8fW9HPFqFQ4sHk2oRx0UQ=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1 h1:1CD7NiLLb/TXl3tOnFYU4b+mNfb5rtgHkaA+q7RMYYQ=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1/go.mod h1:ZWafc8nMdYzTE3uYY6W86f0n46+IF0g4uUyRhJw/kXc=
github.com/go-openapi/swag/loading v0.26.1 h1:E9K4wqXeROlhjFQ13K9zMz6ojFGXIggGe+ad1odrK9w=
github.com/go-openapi/swag/loading v0.26.1/go.mod h1:3qvRIlWzWdq1HvmldwmuJ2ohpcAryN6xVt2OTKd0/7E=
github.com/go-openapi/swag/stringutils v0.26.1 h1:f88uYyTso7TnHrKM/bUBsQ5e2wKf37cpgo6pvbzd9yU=
github.com/go-openapi/swag/stringutils v0.26.1/go.mod h1:Sc6d3bU8fgk5AyZR8/8jEQ+Is/Ald+TD/IIggPN8UJk=
github.com/go-openapi/swag/typeutils v0.26.1 h1:yg42FgMzRR6PVQ3M3qHz1s+Y6/P4HoJ3cBarXa3OVnU=
github.com/go-openapi/swag/typeutils v0.26.1/go.mod h1:VfnV+oUtSP2vCSCn2aJgnr8OevUYemyIzzS1VOzS10o=
github.com/go-openapi/swag/yamlutils v0.26.1 h1:0TSLK+lXs9vfIhAWzBeI/lOzEnIoot6WTCO1aAeWFTk=
github.com/go-openapi/swag/yamlutils v0.26.1/go.mod h1:7W5b7PRX9MxwL7TjeG7H8HkyBGRsIDRObhyMWFgBI2M=
github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8=
github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk=
github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo=
@@ -417,8 +417,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs
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-20260605061815-58f45aa500f1 h1:vkAT/N8DPfmQ3ZltANiX95HhM0nv/tqoq5Innoimcwo=
github.com/golang/geo v0.0.0-20260605061815-58f45aa500f1/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/golang/geo v0.0.0-20260612074446-f1a45663b0f3 h1:UlucSQUu9SZdDRlMWv5N/T3Rig9gv615vWvHFKrXHWA=
github.com/golang/geo v0.0.0-20260612074446-f1a45663b0f3/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
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=
@@ -952,8 +952,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
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=
@@ -972,8 +972,8 @@ golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjO
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
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.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo=
golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
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=
@@ -996,8 +996,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
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=
@@ -1036,8 +1036,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
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=
@@ -1059,8 +1059,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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=
@@ -1107,8 +1107,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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=
@@ -1120,8 +1120,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
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=
@@ -1174,8 +1174,8 @@ golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0t
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
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=
@@ -1292,10 +1292,10 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU=
honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.2 h1:mxsy2FdrB6+qG3NfXefz1AmWv0ehOSDO4jxgxd7h9yo=
modernc.org/ccgo/v4 v4.34.2/go.mod h1:1L7us56+kAKu04p25EATpmBBvhbcqqZ85ibqWVwVgog=
modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
@@ -1304,8 +1304,8 @@ modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.5 h1:m2OGx9Ser1VvTS4Z9ZJlWs+CBMxutLaTiAWkNz+NB9U=
modernc.org/libc v1.72.5/go.mod h1:np0N7KDJ7eUtMZmOqVZNldrZyG+DHLl2B5pg8Hbar3U=
modernc.org/libc v1.73.1 h1:HMa/TQJY2Z8ZjqBRbVJMLjBhmxTOwN2aMcxNrT01Dbc=
modernc.org/libc v1.73.1/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -1314,8 +1314,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo=
modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+1
View File
@@ -166,6 +166,7 @@ func StartHttp(ctx context.Context, storage *bolt.BoltStore, shutdownComplete ch
api.HandleFunc("PATCH /share", withPermShare(sharePatchHandler))
api.HandleFunc("DELETE /share", withPermShare(shareDeleteHandler))
publicApi.HandleFunc("GET /share/info", withOrWithoutUser(shareInfoHandler))
publicApi.HandleFunc("PATCH /share/pinnedItems", withPermShare(sharePatchPinnedItemsHandler))
publicApi.HandleFunc("GET /share/image", withHashFile(getShareImage))
// ========================================
+2
View File
@@ -137,6 +137,8 @@ func withHashFileHelper(fn handleFunc) handleFunc {
ShowHidden: link.ShowHidden,
HideFileExt: link.HideFileExt,
FollowSymlinks: true,
ShowPinnedItems: true,
ShareHash: hash,
}, store.Access, data.shareUser, store.Share)
if err != nil {
logger.Errorf("error fetching file info for share. hash=%v path=%v error=%v", hash, path, err)
+19 -1
View File
@@ -15,7 +15,13 @@ import (
type pinnedItemPatchRequest struct {
Source string `json:"source" validate:"required"`
Path string `json:"path" validate:"required"` // scope-relative parent directory
Name string `json:"name" validate:"required"` // item basename within path
Name string `json:"name" validate:"required"` // item basename within path
}
// sharePinnedItemPatchRequest is the JSON body for PATCH /public/api/share/pinnedItems.
type sharePinnedItemPatchRequest struct {
Path string `json:"path" validate:"required"` // share-relative parent directory
Name string `json:"name" validate:"required"` // item basename within path
}
// scopeRelativeDirToIndexPath maps a scope-relative directory to source and index paths.
@@ -39,6 +45,18 @@ func scopeRelativeDirToIndexPath(sourceName, userScope, directoryPath string) (s
return source.Path, idx.MakeIndexPath(fullDirPath, true), nil
}
// normalizeShareRelativeDir returns a normalized share-relative directory path key.
func normalizeShareRelativeDir(shareRelDir string) (string, error) {
if shareRelDir == "" || shareRelDir == "/" {
return "/", nil
}
cleanDir, err := utils.SanitizeUserPath(shareRelDir)
if err != nil {
return "", fmt.Errorf("invalid path: %w", err)
}
return utils.AddTrailingSlashIfNotExists(cleanDir), nil
}
// pinnedItemAction returns the patch action query param, defaulting to "add".
func pinnedItemAction(r *http.Request) string {
action := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("action")))
+75
View File
@@ -700,3 +700,78 @@ func redirectToShare(w http.ResponseWriter, r *http.Request, d *requestContext)
http.Redirect(w, r, newURL, http.StatusMovedPermanently)
return http.StatusMovedPermanently, nil
}
// publicSharePatchPinnedItemsHandler adds or removes a pinned item on a share link.
// @Summary Add or remove a pinned item on a share
// @Description Patches one pinned item at a time for share owners. Defaults to add; pass ?action=remove to unpin.
// @Tags Shares
// @Accept json
// @Produce json
// @Param hash query string true "Share hash"
// @Param action query string false "add (default) or remove"
// @Param body body sharePinnedItemPatchRequest true "Pinned item"
// @Success 204 "No Content"
// @Failure 400 {object} map[string]string "Bad Request"
// @Failure 403 {object} map[string]string "Forbidden"
// @Failure 404 {object} map[string]string "Share not found"
// @Failure 500 {object} map[string]string "Internal Server Error"
// @Router /public/api/share/pinnedItems [patch]
func sharePatchPinnedItemsHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
hash := r.URL.Query().Get("hash")
if hash == "" {
return http.StatusBadRequest, fmt.Errorf("hash is required")
}
link, err := store.Share.GetByHash(hash)
if err != nil {
return http.StatusNotFound, fmt.Errorf("share hash not found")
}
if d.user.Username == "anonymous" || !link.UserCanEdit(d.user) {
return http.StatusForbidden, fmt.Errorf("share pin editing is not allowed for this user")
}
if link.ShareType == "upload" {
return http.StatusForbidden, fmt.Errorf("pinning is disabled for upload shares")
}
var body sharePinnedItemPatchRequest
if err = json.NewDecoder(r.Body).Decode(&body); err != nil {
return http.StatusBadRequest, fmt.Errorf("failed to decode body: %w", err)
}
defer r.Body.Close()
action := pinnedItemAction(r)
if action != "add" && action != "remove" {
return http.StatusBadRequest, fmt.Errorf("action must be add or remove")
}
if body.Path == "" || body.Name == "" {
return http.StatusBadRequest, fmt.Errorf("path and name are required")
}
shareRelDir, err := normalizeShareRelativeDir(body.Path)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path: %s", body.Path)
}
cleanName, err := utils.SanitizeUserPath(body.Name)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid name: %s", body.Name)
}
body.Name = cleanName
pinned := link.EnsurePinnedItems()
switch action {
case "add":
pinned.Add(shareRelDir, body.Name)
case "remove":
pinned.Remove(shareRelDir, body.Name)
}
if err := store.Share.Save(link); err != nil {
return http.StatusInternalServerError, fmt.Errorf("failed to save share: %w", err)
}
return http.StatusNoContent, nil
}
+38 -4
View File
@@ -6,6 +6,7 @@ import (
"html/template"
"io/fs"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
@@ -96,6 +97,17 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
shareHash := ""
data := make(map[string]interface{})
disableNavButtons := config.Frontend.DisableNavButtons
if d.share != nil {
shareHash = d.share.Hash
}
if shareHash == "" {
shareHash = icons.ShareHashFromHTTPPath(r.URL.Path, config.Server.BaseURL)
}
if shareHash != "" && d.share == nil && store != nil {
if link, err := store.Share.GetByHash(shareHash); err == nil {
d.share = link
}
}
if d.share != nil {
if d.share.Favicon != "" {
if strings.HasPrefix(d.share.Favicon, "http") {
@@ -124,7 +136,6 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
if shareFavicon != "" {
favicon = shareFavicon
}
shareHash = d.share.Hash
}
// Set login icon URL
loginIcon := staticURL + "/loginIcon"
@@ -162,6 +173,20 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
pwaIcon256 := staticURL + "/icons/pwa-icon-256.png"
pwaIcon512 := staticURL + "/icons/pwa-icon-512.png"
manifestURL := staticURL + "/site.webmanifest"
if shareHash != "" {
pwaStartURL := config.Server.BaseURL + "public/share/" + shareHash + "/"
query := url.Values{}
query.Set("start", pwaStartURL)
if title != config.Frontend.Name {
query.Set("name", title)
}
if description != config.Frontend.Description {
query.Set("description", description)
}
manifestURL = staticURL + "/site.webmanifest?" + query.Encode()
}
data["htmlVars"] = map[string]interface{}{
"title": title,
"customCSS": template.CSS(config.Frontend.Styling.CustomCSSRaw),
@@ -183,7 +208,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
"pwaIcon192": pwaIcon192,
"pwaIcon256": pwaIcon256,
"pwaIcon512": pwaIcon512,
"manifestURL": staticURL + "/site.webmanifest",
"manifestURL": manifestURL,
}
data["globalVars"] = map[string]interface{}{
@@ -267,12 +292,21 @@ func setContentType(w http.ResponseWriter, path string) {
}
}
// manifestHandler serves the cached PWA manifest
// manifestHandler serves the cached PWA manifest, optionally scoped to a public share.
func manifestHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/manifest+json")
w.Header().Set("Cache-Control", "public, max-age=3600") // Cache for 1 hour
if err := json.NewEncoder(w).Encode(icons.CachedManifest); err != nil {
manifest := icons.CachedManifest
if startURL := r.URL.Query().Get("start"); startURL != "" {
name := r.URL.Query().Get("name")
description := r.URL.Query().Get("description")
if shareManifest, ok := icons.ManifestForShare(config.Server.BaseURL, startURL, name, description); ok {
manifest = shareManifest
}
}
if err := json.NewEncoder(w).Encode(manifest); err != nil {
http.Error(w, "Failed to serve manifest", http.StatusInternalServerError)
}
}
+4
View File
@@ -8,6 +8,8 @@ import (
type PWAManifest struct {
Name string `json:"name"`
ShortName string `json:"short_name"`
ID string `json:"id"`
Scope string `json:"scope"`
Icons []PWAIcon `json:"icons"`
StartURL string `json:"start_url"`
Display string `json:"display"`
@@ -40,6 +42,8 @@ func generatePWAManifest(name, description, baseURL, themeColor, pwaIcon192, pwa
return PWAManifest{
Name: name,
ShortName: shortName,
ID: baseURL,
Scope: baseURL,
StartURL: baseURL,
Display: "standalone",
BackgroundColor: "#ffffff",
+108
View File
@@ -0,0 +1,108 @@
package icons
import (
"strings"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
)
const maxManifestTextLen = 256
func isValidShareHash(hash string) bool {
if len(hash) < 8 || len(hash) > 64 {
return false
}
if strings.Contains(hash, "..") || strings.Contains(hash, "/") {
return false
}
return true
}
// ShareRootFromStartURL validates a share PWA start URL and returns the share root
// used for manifest scope/id plus the normalized start URL.
func ShareRootFromStartURL(baseURL, startURL string) (shareRoot, normalizedStart string, ok bool) {
if startURL == "" || !strings.HasPrefix(startURL, baseURL) {
return "", "", false
}
rel := strings.TrimPrefix(startURL, baseURL)
if !strings.HasPrefix(rel, "public/share/") {
return "", "", false
}
rest := strings.TrimPrefix(rel, "public/share/")
hash, subPath, _ := strings.Cut(rest, "/")
if !isValidShareHash(hash) {
return "", "", false
}
if subPath != "" {
if _, err := utils.SanitizeUserPath(subPath); err != nil {
return "", "", false
}
}
shareRoot = baseURL + "public/share/" + hash + "/"
normalizedStart = startURL
if !strings.HasPrefix(normalizedStart, shareRoot) && normalizedStart != strings.TrimSuffix(shareRoot, "/") {
return "", "", false
}
if normalizedStart == baseURL+"public/share/"+hash {
normalizedStart = shareRoot
}
return shareRoot, normalizedStart, true
}
// ShareHashFromHTTPPath extracts a share hash from common FileBrowser share routes.
func ShareHashFromHTTPPath(path, baseURL string) string {
prefixes := []string{
strings.TrimSuffix(baseURL, "/") + "/public/share/",
"/public/share/",
"/share/",
}
for _, prefix := range prefixes {
if !strings.HasPrefix(path, prefix) {
continue
}
rest := strings.TrimPrefix(path, prefix)
hash, _, _ := strings.Cut(rest, "/")
if isValidShareHash(hash) {
return hash
}
}
return ""
}
func sanitizeManifestText(value string) string {
if len(value) > maxManifestTextLen {
return value[:maxManifestTextLen]
}
return value
}
// ManifestForShare returns a manifest scoped to a single public share link.
func ManifestForShare(baseURL, startURL, name, description string) (PWAManifest, bool) {
shareRoot, normalizedStart, ok := ShareRootFromStartURL(baseURL, startURL)
if !ok {
return PWAManifest{}, false
}
manifest := CachedManifest
manifest.StartURL = normalizedStart
manifest.Scope = shareRoot
manifest.ID = shareRoot
if name = sanitizeManifestText(strings.TrimSpace(name)); name != "" {
manifest.Name = name
shortName := name
if len(shortName) > 12 {
shortName = shortName[:12]
}
manifest.ShortName = shortName
}
if description = sanitizeManifestText(strings.TrimSpace(description)); description != "" {
manifest.Description = description
}
return manifest, true
}
+80
View File
@@ -0,0 +1,80 @@
package icons
import "testing"
func TestShareRootFromStartURL(t *testing.T) {
baseURL := "/testing/"
startURL := "/testing/public/share/abc123XYZ-_/"
shareRoot, normalizedStart, ok := ShareRootFromStartURL(baseURL, startURL)
if !ok {
t.Fatal("expected valid share start URL")
}
if shareRoot != "/testing/public/share/abc123XYZ-_/" {
t.Fatalf("unexpected share root: %q", shareRoot)
}
if normalizedStart != startURL {
t.Fatalf("unexpected normalized start: %q", normalizedStart)
}
}
func TestShareRootFromStartURLRejectsTraversal(t *testing.T) {
baseURL := "/testing/"
startURL := "/testing/public/share/abc123XYZ-_/../secret/"
_, _, ok := ShareRootFromStartURL(baseURL, startURL)
if ok {
t.Fatal("expected traversal path to be rejected")
}
}
func TestShareHashFromHTTPPath(t *testing.T) {
baseURL := "/testing/"
paths := []string{
"/testing/public/share/hash1234567890abcdef/",
"/public/share/hash1234567890abcdef/files",
"/share/hash1234567890abcdef/",
}
for _, path := range paths {
hash := ShareHashFromHTTPPath(path, baseURL)
if hash != "hash1234567890abcdef" {
t.Fatalf("path %q: got hash %q", path, hash)
}
}
}
func TestShareHashFromHTTPPathRejectsEmbeddedPrefix(t *testing.T) {
baseURL := "/testing/"
path := "/evil/route/share/hash1234567890abcdef/"
hash := ShareHashFromHTTPPath(path, baseURL)
if hash != "" {
t.Fatalf("expected empty hash for non-prefixed path, got %q", hash)
}
}
func TestManifestForShare(t *testing.T) {
CachedManifest = PWAManifest{
Name: "FileBrowser Quantum",
ShortName: "FBQ",
StartURL: "/testing/",
Scope: "/testing/",
ID: "/testing/",
}
manifest, ok := ManifestForShare(
"/testing/",
"/testing/public/share/hash1234567890abcdef/",
"My Share",
"Shared files",
)
if !ok {
t.Fatal("expected share manifest")
}
if manifest.StartURL != "/testing/public/share/hash1234567890abcdef/" {
t.Fatalf("unexpected start_url: %q", manifest.StartURL)
}
if manifest.Scope != "/testing/public/share/hash1234567890abcdef/" {
t.Fatalf("unexpected scope: %q", manifest.Scope)
}
if manifest.Name != "My Share" {
t.Fatalf("unexpected name: %q", manifest.Name)
}
}
-6
View File
@@ -95,12 +95,6 @@ func (s *Scanner) executeScan() {
s.fullScanCounter = 0
}
})
scanMode := "full"
if quick {
scanMode = "quick"
}
logger.Debugf("[%s] scanner [%s] starting %s scan (full/quick cadence step %d of 5)", s.idx.Name, s.scanPath, scanMode, pre+1)
s.runIndexing(quick)
s.updateSchedule()
}
@@ -0,0 +1,92 @@
package iteminfo
import "strings"
type namePatternMode int
const (
patternLazyDefault namePatternMode = iota
patternQuotedLiteral
patternUserGlob
)
func escapeGlobLiteral(s string) string {
var b strings.Builder
for _, r := range s {
switch r {
case '*', '?', '[', '\\':
b.WriteByte('\\')
b.WriteRune(r)
default:
b.WriteRune(r)
}
}
return b.String()
}
func wrapGlobPattern(s string) string {
return "*" + s + "*"
}
// BuildNameGlobPattern converts a search term into a SQLite GLOB pattern for indexed file names.
func BuildNameGlobPattern(term string, quoted, useWildcard, exactCase bool) string {
if term == "" {
return ""
}
var mode namePatternMode
switch {
case useWildcard:
mode = patternUserGlob
case quoted:
mode = patternQuotedLiteral
default:
mode = patternLazyDefault
}
var pattern string
switch mode {
case patternUserGlob:
pattern = term
case patternQuotedLiteral:
pattern = wrapGlobPattern(escapeGlobLiteral(term))
case patternLazyDefault:
parts := strings.Fields(term)
if len(parts) == 0 {
return ""
}
escaped := make([]string, len(parts))
for i, p := range parts {
escaped[i] = escapeGlobLiteral(p)
}
pattern = wrapGlobPattern(strings.Join(escaped, "*"))
}
if !exactCase {
pattern = strings.ToLower(pattern)
}
return pattern
}
// NameGlobPatternsForSearch builds SQLite name GLOB patterns for the given search options.
func NameGlobPatternsForSearch(opts SearchOptions, useWildcard, largest bool) []string {
if largest || len(opts.Terms) == 0 {
return nil
}
exactCase := opts.Conditions["exact"]
var patterns []string
for _, t := range opts.Terms {
if t == "" {
continue
}
p := BuildNameGlobPattern(t, opts.Quoted, useWildcard, exactCase)
if p != "" {
patterns = append(patterns, p)
}
}
if len(patterns) == 0 {
return nil
}
return patterns
}
@@ -0,0 +1,53 @@
package iteminfo
import "testing"
func TestBuildNameGlobPattern(t *testing.T) {
tests := []struct {
term string
quoted bool
useWildcard bool
exactCase bool
want string
}{
{"cons", false, false, false, "*cons*"},
{"new folder", false, false, false, "*new*folder*"},
{"new folder", true, false, false, "*new folder*"},
{"new*folder", false, true, false, "new*folder"},
{"Blade Runner", false, false, false, "*blade*runner*"},
{"Blade Runner", false, false, true, "*Blade*Runner*"},
{"test[1]", false, false, false, "*test\\[1]*"},
}
for _, tt := range tests {
got := BuildNameGlobPattern(tt.term, tt.quoted, tt.useWildcard, tt.exactCase)
if got != tt.want {
t.Errorf("BuildNameGlobPattern(%q, quoted=%v, wildcard=%v, exact=%v) = %q, want %q",
tt.term, tt.quoted, tt.useWildcard, tt.exactCase, got, tt.want)
}
}
}
func TestNameGlobPatternsForSearch(t *testing.T) {
opts := SearchOptions{
Terms: []string{"house", "car"},
}
patterns := NameGlobPatternsForSearch(opts, false, false)
if len(patterns) != 2 || patterns[0] != "*house*" || patterns[1] != "*car*" {
t.Fatalf("unexpected patterns: %#v", patterns)
}
optsQuoted := SearchOptions{
Terms: []string{"new folder"},
Quoted: true,
}
patterns = NameGlobPatternsForSearch(optsQuoted, false, false)
if len(patterns) != 1 || patterns[0] != "*new folder*" {
t.Fatalf("unexpected quoted patterns: %#v", patterns)
}
optsLargest := SearchOptions{Terms: []string{"test"}}
if p := NameGlobPatternsForSearch(optsLargest, false, true); p != nil {
t.Fatalf("largest mode should return nil patterns, got %#v", p)
}
}
+3
View File
@@ -20,6 +20,8 @@ type SearchOptions struct {
// MatchAllTerms, when true and non-wildcard search, an item must contain every term (AND). When false (default), any term matches (OR).
// For name GLOB wildcard search, name must match every pattern when true; when false, patterns are OR-combined in SQL.
MatchAllTerms bool
// Quoted is true when the search query was a double-quoted phrase; spaces are kept literal in name matching.
Quoted bool
}
// BuildSearchOptionsFromQuery merges optional repeated literal terms (HTTP "terms" parameters) with structured filter text ("query" prefix).
@@ -119,6 +121,7 @@ func ParseSearch(value string) SearchOptions {
unique = strings.TrimSuffix(unique, "\"")
opts.Terms = []string{unique}
opts.Quoted = true
return opts
}
value = strings.TrimSpace(value)
+12 -27
View File
@@ -55,10 +55,11 @@ func (idx *Index) SearchParsed(baseOpts iteminfo.SearchOptions, scope string, so
searchOptions.Terms = []string{""}
}
nameGlobPatterns := nameGlobPatternsForSearch(searchOptions, useWildcard, largest)
globAnd := useWildcard && searchOptions.MatchAllTerms
nameGlobPatterns := iteminfo.NameGlobPatternsForSearch(searchOptions, useWildcard, largest)
caseExact := searchOptions.Conditions["exact"]
globAnd := len(nameGlobPatterns) > 0 && searchOptions.MatchAllTerms
rows, err := idx.db.SearchItems(idx.Name, scope, largest, nameGlobPatterns, globAnd)
rows, err := idx.db.SearchItems(idx.Name, scope, largest, nameGlobPatterns, globAnd, caseExact)
if err != nil {
return []*SearchResult{}
}
@@ -95,7 +96,7 @@ func (idx *Index) SearchParsed(baseOpts iteminfo.SearchOptions, scope string, so
HasPreview: hasPreview,
}
matches := itemMatchesSearchFilters(item, isDir, searchOptions, largest, useWildcard, nameGlobPatterns)
matches := itemMatchesSearchFilters(item, isDir, searchOptions, largest, nameGlobPatterns)
if matches {
resType := mimeType
@@ -127,7 +128,7 @@ func (idx *Index) SearchParsed(baseOpts iteminfo.SearchOptions, scope string, so
return sortedKeys
}
func itemMatchesSearchFilters(item iteminfo.ItemInfo, isDir bool, searchOptions iteminfo.SearchOptions, largest, useWildcard bool, nameGlobPatterns []string) bool {
func itemMatchesSearchFilters(item iteminfo.ItemInfo, isDir bool, searchOptions iteminfo.SearchOptions, largest bool, nameGlobPatterns []string) bool {
if largest {
largerThan := int64(searchOptions.LargerThan) * 1024 * 1024
sizeMatches := largerThan == 0 || item.Size > largerThan
@@ -141,7 +142,7 @@ func itemMatchesSearchFilters(item iteminfo.ItemInfo, isDir bool, searchOptions
dateMatches := searchDateMatches(item.ModTime.Unix(), searchOptions)
return sizeMatches && typeMatches && dateMatches
}
if useWildcard && len(nameGlobPatterns) > 0 {
if len(nameGlobPatterns) > 0 {
return item.MatchesSearchAuxiliaryFilters(searchOptions)
}
if searchOptions.MatchAllTerms {
@@ -168,23 +169,6 @@ func itemMatchesSearchFilters(item iteminfo.ItemInfo, isDir bool, searchOptions
return false
}
// nameGlobPatternsForSearch builds non-empty parsed terms for SQLite name GLOB OR clauses.
func nameGlobPatternsForSearch(opts iteminfo.SearchOptions, useWildcard, largest bool) []string {
if largest || !useWildcard || len(opts.Terms) == 0 {
return nil
}
var patterns []string
for _, t := range opts.Terms {
if t != "" {
patterns = append(patterns, t)
}
}
if len(patterns) == 0 {
return nil
}
return patterns
}
func searchDateMatches(modUnix int64, opts iteminfo.SearchOptions) bool {
if opts.ModifiedNewerThan > 0 && modUnix < opts.ModifiedNewerThan {
return false
@@ -234,10 +218,11 @@ func SearchMultiSourcesParsed(baseOpts iteminfo.SearchOptions, sources []string,
searchOptions.Terms = []string{""}
}
nameGlobPatterns := nameGlobPatternsForSearch(searchOptions, useWildcard, largest)
globAnd := useWildcard && searchOptions.MatchAllTerms
nameGlobPatterns := iteminfo.NameGlobPatternsForSearch(searchOptions, useWildcard, largest)
caseExact := searchOptions.Conditions["exact"]
globAnd := len(nameGlobPatterns) > 0 && searchOptions.MatchAllTerms
rows, err := db.SearchItemsMultiSource(sources, normalizedScopes, largest, nameGlobPatterns, globAnd)
rows, err := db.SearchItemsMultiSource(sources, normalizedScopes, largest, nameGlobPatterns, globAnd, caseExact)
if err != nil {
return []*SearchResult{}
}
@@ -275,7 +260,7 @@ func SearchMultiSourcesParsed(baseOpts iteminfo.SearchOptions, sources []string,
HasPreview: hasPreview,
}
matches := itemMatchesSearchFilters(item, isDir, searchOptions, largest, useWildcard, nameGlobPatterns)
matches := itemMatchesSearchFilters(item, isDir, searchOptions, largest, nameGlobPatterns)
if matches {
resType := mimeType
+127
View File
@@ -116,6 +116,14 @@ func TestParseSearch(t *testing.T) {
Terms: []string{"thisfile"},
},
},
{
input: `"new folder"`,
want: iteminfo.SearchOptions{
Conditions: map[string]bool{"exact": false},
Terms: []string{"new folder"},
Quoted: true,
},
},
{
input: "type:olderThan=2024-06-01 type:newerThan=2024-01-01 findme",
want: iteminfo.SearchOptions{
@@ -266,6 +274,13 @@ func TestSearchIndexes(t *testing.T) {
{ItemInfo: iteminfo.ItemInfo{Name: "toggle.gif", Size: 65536, Type: "image/gif"}},
},
},
"/media/": {
ItemInfo: iteminfo.ItemInfo{Name: "media", Type: "directory"},
Files: []iteminfo.ExtendedItemInfo{
{ItemInfo: iteminfo.ItemInfo{Name: "Blade.Runner.mkv", Type: "video"}},
{ItemInfo: iteminfo.ItemInfo{Name: "new.folder.txt", Type: "text"}},
},
},
}
// Insert all directories and files into database
@@ -462,6 +477,65 @@ func TestSearchIndexes(t *testing.T) {
},
},
},
{
search: "Blade Runner",
scope: "/media/",
expectedResult: []*SearchResult{
{
Path: "/media/Blade.Runner.mkv",
Type: "video",
Size: 0,
},
},
},
{
search: "new folder",
scope: "/media/",
expectedResult: []*SearchResult{
{
Path: "/media/new.folder.txt",
Type: "text",
Size: 0,
},
},
},
{
search: "new folder",
scope: "/",
expectedResult: []*SearchResult{
{
Path: "/new+folder/",
Type: "directory",
Size: 0,
},
{
Path: "/media/new.folder.txt",
Type: "text",
Size: 0,
},
},
},
{
search: `"new folder"`,
scope: "/media/",
expectedResult: []*SearchResult{},
},
{
search: "VIDEO",
scope: "/new/test/",
expectedResult: []*SearchResult{
{
Path: "/new/test/video.MP4",
Type: "video",
Size: 0,
},
{
Path: "/new/test/video.mp4",
Type: "video",
Size: 0,
},
},
},
}
for _, tt := range tests {
@@ -481,6 +555,59 @@ func TestSearchIndexes(t *testing.T) {
}
}
func TestSearchUserGlob(t *testing.T) {
if indexDB == nil {
var err error
indexDB, _, err = dbsql.NewIndexDB("test_search_glob", "OFF", 1000, 32, false)
if err != nil {
t.Fatalf("Failed to create test database: %v", err)
}
}
index := Index{
Source: settings.Source{
Name: "test_search_glob",
Path: "/mock/path",
},
db: indexDB,
mock: true,
}
now := time.Now()
directories := map[string]*iteminfo.FileInfo{
"/media/": {
ItemInfo: iteminfo.ItemInfo{Name: "media", Type: "directory"},
Files: []iteminfo.ExtendedItemInfo{
{ItemInfo: iteminfo.ItemInfo{Name: "NEW-folder", Type: "text"}},
},
},
}
for path, dirInfo := range directories {
dirInfo.Path = path
if dirInfo.ModTime.IsZero() {
dirInfo.ModTime = now
}
_ = index.db.InsertItem("test_search_glob", path, dirInfo)
for _, file := range dirInfo.Files {
filePath := strings.TrimSuffix(path, "/") + "/" + file.Name
fileInfo := &iteminfo.FileInfo{
Path: filePath,
ItemInfo: file.ItemInfo,
}
if fileInfo.ModTime.IsZero() {
fileInfo.ModTime = now
}
_ = index.db.InsertItem("test_search_glob", filePath, fileInfo)
}
}
result := index.Search("new*folder", "/media/", "", false, DefaultSearchResults, 0, 0, true)
if len(result) != 1 || result[0].Path != "/media/NEW-folder" {
t.Fatalf("expected new*folder to match NEW-folder, got %#v", result)
}
}
func TestSearchLargestModeExcludesRoot(t *testing.T) {
// Initialize the database if not already done
if indexDB == nil {
+112
View File
@@ -4630,6 +4630,86 @@ const docTemplate = `{
}
}
}
},
"/public/api/share/pinnedItems": {
"patch": {
"description": "Patches one pinned item at a time for share owners. Defaults to add; pass ?action=remove to unpin.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Shares"
],
"summary": "Add or remove a pinned item on a share",
"parameters": [
{
"type": "string",
"description": "Share hash",
"name": "hash",
"in": "query",
"required": true
},
{
"type": "string",
"description": "add (default) or remove",
"name": "action",
"in": "query"
},
{
"description": "Pinned item",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/http.sharePinnedItemPatchRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Share not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
@@ -4923,6 +5003,9 @@ const docTemplate = `{
"perUserDownloadLimit": {
"type": "boolean"
},
"pinnedItems": {
"$ref": "#/definitions/share.PinnedItems"
},
"quickDownload": {
"type": "boolean"
},
@@ -5119,6 +5202,23 @@ const docTemplate = `{
}
}
},
"http.sharePinnedItemPatchRequest": {
"type": "object",
"required": [
"name",
"path"
],
"properties": {
"name": {
"description": "item basename within path",
"type": "string"
},
"path": {
"description": "share-relative parent directory",
"type": "string"
}
}
},
"http.unarchiveRequest": {
"type": "object",
"properties": {
@@ -7283,6 +7383,9 @@ const docTemplate = `{
"perUserDownloadLimit": {
"type": "boolean"
},
"pinnedItems": {
"$ref": "#/definitions/share.PinnedItems"
},
"quickDownload": {
"type": "boolean"
},
@@ -7343,6 +7446,15 @@ const docTemplate = `{
}
}
},
"share.PinnedItems": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"users.AuthToken": {
"type": "object",
"properties": {
+112
View File
@@ -4619,6 +4619,86 @@
}
}
}
},
"/public/api/share/pinnedItems": {
"patch": {
"description": "Patches one pinned item at a time for share owners. Defaults to add; pass ?action=remove to unpin.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Shares"
],
"summary": "Add or remove a pinned item on a share",
"parameters": [
{
"type": "string",
"description": "Share hash",
"name": "hash",
"in": "query",
"required": true
},
{
"type": "string",
"description": "add (default) or remove",
"name": "action",
"in": "query"
},
{
"description": "Pinned item",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/http.sharePinnedItemPatchRequest"
}
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"404": {
"description": "Share not found",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
}
},
"definitions": {
@@ -4912,6 +4992,9 @@
"perUserDownloadLimit": {
"type": "boolean"
},
"pinnedItems": {
"$ref": "#/definitions/share.PinnedItems"
},
"quickDownload": {
"type": "boolean"
},
@@ -5108,6 +5191,23 @@
}
}
},
"http.sharePinnedItemPatchRequest": {
"type": "object",
"required": [
"name",
"path"
],
"properties": {
"name": {
"description": "item basename within path",
"type": "string"
},
"path": {
"description": "share-relative parent directory",
"type": "string"
}
}
},
"http.unarchiveRequest": {
"type": "object",
"properties": {
@@ -7272,6 +7372,9 @@
"perUserDownloadLimit": {
"type": "boolean"
},
"pinnedItems": {
"$ref": "#/definitions/share.PinnedItems"
},
"quickDownload": {
"type": "boolean"
},
@@ -7332,6 +7435,15 @@
}
}
},
"share.PinnedItems": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"users.AuthToken": {
"type": "object",
"properties": {
+76
View File
@@ -193,6 +193,8 @@ definitions:
type: boolean
perUserDownloadLimit:
type: boolean
pinnedItems:
$ref: '#/definitions/share.PinnedItems'
quickDownload:
type: boolean
shareTheme:
@@ -339,6 +341,18 @@ definitions:
- path
- source
type: object
http.sharePinnedItemPatchRequest:
properties:
name:
description: item basename within path
type: string
path:
description: share-relative parent directory
type: string
required:
- name
- path
type: object
http.unarchiveRequest:
properties:
deleteAfter:
@@ -1967,6 +1981,8 @@ definitions:
type: string
perUserDownloadLimit:
type: boolean
pinnedItems:
$ref: '#/definitions/share.PinnedItems'
quickDownload:
type: boolean
shareTheme:
@@ -2013,6 +2029,12 @@ definitions:
"gallery"'
type: string
type: object
share.PinnedItems:
additionalProperties:
items:
type: string
type: array
type: object
users.AuthToken:
properties:
Permissions:
@@ -5587,4 +5609,58 @@ paths:
summary: Get share information by hash
tags:
- Shares
/public/api/share/pinnedItems:
patch:
consumes:
- application/json
description: Patches one pinned item at a time for share owners. Defaults to
add; pass ?action=remove to unpin.
parameters:
- description: Share hash
in: query
name: hash
required: true
type: string
- description: add (default) or remove
in: query
name: action
type: string
- description: Pinned item
in: body
name: body
required: true
schema:
$ref: '#/definitions/http.sharePinnedItemPatchRequest'
produces:
- application/json
responses:
"204":
description: No Content
"400":
description: Bad Request
schema:
additionalProperties:
type: string
type: object
"403":
description: Forbidden
schema:
additionalProperties:
type: string
type: object
"404":
description: Share not found
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal Server Error
schema:
additionalProperties:
type: string
type: object
summary: Add or remove a pinned item on a share
tags:
- Shares
swagger: "2.0"
+22 -1
View File
@@ -5,6 +5,7 @@ import pluginVue from "eslint-plugin-vue";
import vueI18n from '@intlify/eslint-plugin-vue-i18n';
import vueParser from "vue-eslint-parser";
import globals from "globals";
import security from "eslint-plugin-security";
export default defineConfig(
{
@@ -12,7 +13,7 @@ export default defineConfig(
"**/dist/**",
"**/node_modules/**",
"**/public/**",
"**/i18n/is.json",
"**/*.json",
],
},
@@ -21,6 +22,7 @@ export default defineConfig(
...tseslint.configs.recommended,
...pluginVue.configs["flat/essential"],
...vueI18n.configs.recommended,
security.configs.recommended,
// i18n
{
@@ -75,8 +77,13 @@ export default defineConfig(
"prefer-template": "warn",
"@typescript-eslint/consistent-type-definitions": "warn",
"@typescript-eslint/prefer-optional-chain": "warn",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-unnecessary-condition": "off", // this one is useful, but is pretty noisy and found lot of false positives
"@typescript-eslint/no-dynamic-delete": "warn",
"@typescript-eslint/no-misused-promises": "error",
"prefer-object-has-own": "error",
"no-prototype-builtins": "error",
"no-implied-eval": "error",
"no-restricted-globals": [
"error",
{ name: "isNaN", message: "Use Number.isNaN instead." },
@@ -118,6 +125,20 @@ export default defineConfig(
"vue/require-v-for-key": "error",
"vue/no-reserved-component-names": "off",
"vue/no-unused-components": "warn",
//"vue/no-v-html": "warn",
"vue/no-v-text-v-html-on-component": "warn",
},
},
// Relax the rules a bit for tests files
{
files: ["tests/**/*.ts", "**/*.spec.ts", "**/*.test.ts"],
rules: {
"preserve-caught-error": "off",
"no-empty-pattern": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": ["error", { caughtErrors: "none" }],
"no-unused-expressions": "off",
"security/detect-non-literal-regexp": "off",
},
},
);
+196 -137
View File
@@ -32,10 +32,12 @@
"@intlify/eslint-plugin-vue-i18n": "^4.4.0",
"@intlify/unplugin-vue-i18n": "^11.2.3",
"@playwright/test": "^1.54.1",
"@types/eslint-plugin-security": "^3.0.1",
"@types/node": "^24.1.0",
"@vitejs/plugin-vue": "^5.0.4",
"deepl-node": "^1.16.0",
"eslint": "^10.4.0",
"eslint-plugin-security": "^4.0.0",
"eslint-plugin-vue": "^10.9.1",
"fs-extra": "^11.3.0",
"globals": "^17.6.0",
@@ -1798,6 +1800,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
}
},
"node_modules/@types/eslint-plugin-security": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/eslint-plugin-security/-/eslint-plugin-security-3.0.1.tgz",
"integrity": "sha512-z+v78aGgONHyq7mMaXgzG+10HonH0HBubSLDbTXdNGyCLtM3c29MdDc9hQzHO9pDan4WYHWdULVOJyzszqDuqg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint": "*"
}
},
"node_modules/@types/esrecurse": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
@@ -1830,9 +1853,9 @@
}
},
"node_modules/@types/node": {
"version": "24.13.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.1.tgz",
"integrity": "sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg==",
"version": "24.13.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1847,17 +1870,17 @@
"optional": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz",
"integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz",
"integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
"@typescript-eslint/scope-manager": "8.60.1",
"@typescript-eslint/type-utils": "8.60.1",
"@typescript-eslint/utils": "8.60.1",
"@typescript-eslint/visitor-keys": "8.60.1",
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/type-utils": "8.61.0",
"@typescript-eslint/utils": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -1870,22 +1893,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.60.1",
"@typescript-eslint/parser": "^8.61.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz",
"integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz",
"integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.60.1",
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/typescript-estree": "8.60.1",
"@typescript-eslint/visitor-keys": "8.60.1",
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1901,14 +1924,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz",
"integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz",
"integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.60.1",
"@typescript-eslint/types": "^8.60.1",
"@typescript-eslint/tsconfig-utils": "^8.61.0",
"@typescript-eslint/types": "^8.61.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1923,14 +1946,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz",
"integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz",
"integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/visitor-keys": "8.60.1"
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1941,9 +1964,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz",
"integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz",
"integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1958,15 +1981,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz",
"integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz",
"integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/typescript-estree": "8.60.1",
"@typescript-eslint/utils": "8.60.1",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/utils": "8.61.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1983,9 +2006,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz",
"integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz",
"integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1997,16 +2020,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz",
"integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz",
"integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.60.1",
"@typescript-eslint/tsconfig-utils": "8.60.1",
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/visitor-keys": "8.60.1",
"@typescript-eslint/project-service": "8.61.0",
"@typescript-eslint/tsconfig-utils": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/visitor-keys": "8.61.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -2025,16 +2048,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz",
"integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz",
"integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
"@typescript-eslint/scope-manager": "8.60.1",
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/typescript-estree": "8.60.1"
"@typescript-eslint/scope-manager": "8.61.0",
"@typescript-eslint/types": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2049,13 +2072,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz",
"integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz",
"integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.60.1",
"@typescript-eslint/types": "8.61.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -2246,39 +2269,39 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.35",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz",
"integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==",
"version": "3.5.38",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.38.tgz",
"integrity": "sha512-s99aGxWYig9ErHbct27KXEGhrBYlRI6c4MwAgXErOAbX9xiW37/uMa+XUDO69zLz83dng8UUZ70CTOJrLrYrEQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.3",
"@vue/shared": "3.5.35",
"@babel/parser": "^7.29.7",
"@vue/shared": "3.5.38",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.35",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz",
"integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==",
"version": "3.5.38",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.38.tgz",
"integrity": "sha512-JTqp25l8aFfJYF7/KmsXZjAxJz7T+SjmTJLoXVjHtc2BrSgSiW2n9Aem/cWq1OPe68A8JL06B3eVdhlP0H4TVw==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.35",
"@vue/shared": "3.5.35"
"@vue/compiler-core": "3.5.38",
"@vue/shared": "3.5.38"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.35",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz",
"integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==",
"version": "3.5.38",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.38.tgz",
"integrity": "sha512-DuA2GiZawSEW442iw/9+Fkol8hTgb4Ke5KkhmSry65QA7YuyMbIdy8p0XZRMvNwJdgRz307W8g1CSzdvS4nuNg==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.3",
"@vue/compiler-core": "3.5.35",
"@vue/compiler-dom": "3.5.35",
"@vue/compiler-ssr": "3.5.35",
"@vue/shared": "3.5.35",
"@babel/parser": "^7.29.7",
"@vue/compiler-core": "3.5.38",
"@vue/compiler-dom": "3.5.38",
"@vue/compiler-ssr": "3.5.38",
"@vue/shared": "3.5.38",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.15",
@@ -2286,13 +2309,13 @@
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.35",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz",
"integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==",
"version": "3.5.38",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.38.tgz",
"integrity": "sha512-7s+W5Gc42FGxZMcuwl8H5B29T8BJPMdBT7KHFE+BbAuZ/iTEdTtv7z2XiMjiaUUw4w3ZcCEdHs36RuYJ2VA7bA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.35",
"@vue/shared": "3.5.35"
"@vue/compiler-dom": "3.5.38",
"@vue/shared": "3.5.38"
}
},
"node_modules/@vue/compiler-vue2": {
@@ -2338,53 +2361,53 @@
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.35",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz",
"integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==",
"version": "3.5.38",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.38.tgz",
"integrity": "sha512-pG6LV/NDNRbKizcUjFFLAfjaL8mcv4DmR9avNcUw2gDHBzZneuS2TWCmp633ynzxz9YYKNeEPK2I8Wraqy2HUQ==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.35"
"@vue/shared": "3.5.38"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.35",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz",
"integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==",
"version": "3.5.38",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.38.tgz",
"integrity": "sha512-iyW8WVfF1CpCXxncZY5Ei6rSd6oZr5DgEom//fUjRBRl56AXPD+s9ATvukRt77ZFTuYlnVA1bxY+dJB94tWVYw==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.35",
"@vue/shared": "3.5.35"
"@vue/reactivity": "3.5.38",
"@vue/shared": "3.5.38"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.35",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz",
"integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==",
"version": "3.5.38",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.38.tgz",
"integrity": "sha512-apX2wt9sdfDshS+a2xueFZLVpt0GkRJZSoPmrW/SA4yzXTznhfcMVW59gr7h4YQeY0vJhdJkk2rsIDwgfFgC5A==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.35",
"@vue/runtime-core": "3.5.35",
"@vue/shared": "3.5.35",
"@vue/reactivity": "3.5.38",
"@vue/runtime-core": "3.5.38",
"@vue/shared": "3.5.38",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.35",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz",
"integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==",
"version": "3.5.38",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.38.tgz",
"integrity": "sha512-vue8vbf2QlV4quHqzwmJy6dWfmRhP1J8l4wtZg60CL6VoKqcPY2oe7may3+1d9qfpedjK5PRLFqd5k3Isj9mUw==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.35",
"@vue/shared": "3.5.35"
"@vue/compiler-ssr": "3.5.38",
"@vue/shared": "3.5.38"
},
"peerDependencies": {
"vue": "3.5.35"
"vue": "3.5.38"
}
},
"node_modules/@vue/shared": {
"version": "3.5.35",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz",
"integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==",
"version": "3.5.38",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.38.tgz",
"integrity": "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug==",
"license": "MIT"
},
"node_modules/@xmldom/xmldom": {
@@ -2403,9 +2426,9 @@
"license": "BSD-3-Clause"
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -2896,9 +2919,9 @@
"license": "BSD-2-Clause"
},
"node_modules/dompurify": {
"version": "3.4.8",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz",
"integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==",
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.10.tgz",
"integrity": "sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -3211,6 +3234,22 @@
"eslint": ">=6.0.0"
}
},
"node_modules/eslint-plugin-security": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-4.0.0.tgz",
"integrity": "sha512-tfuQT8K/Li1ZxhFzyD8wPIKtlzZxqBcPr9q0jFMQ77wWAbKBVEhaMPVQRTMTvCMUDhwBe5vPVqQPwAGk/ASfxQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"safe-regex": "^2.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-vue": {
"version": "10.9.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.9.2.tgz",
@@ -4616,16 +4655,16 @@
}
},
"node_modules/nwsapi": {
"version": "2.2.23",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
"version": "2.2.24",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz",
"integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==",
"dev": true,
"license": "MIT"
},
"node_modules/obug": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz",
"integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==",
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz",
"integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
@@ -4911,9 +4950,9 @@
}
},
"node_modules/postcss-selector-parser": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz",
"integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5024,6 +5063,16 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/regexp-tree": {
"version": "0.1.27",
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
"dev": true,
"license": "MIT",
"bin": {
"regexp-tree": "bin/regexp-tree"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -5127,6 +5176,16 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/safe-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
"integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
"dev": true,
"license": "MIT",
"dependencies": {
"regexp-tree": "~0.1.1"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -5148,9 +5207,9 @@
}
},
"node_modules/semver": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
"integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
"version": "7.8.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz",
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -5573,16 +5632,16 @@
}
},
"node_modules/typescript-eslint": {
"version": "8.60.1",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz",
"integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==",
"version": "8.61.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz",
"integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.60.1",
"@typescript-eslint/parser": "8.60.1",
"@typescript-eslint/typescript-estree": "8.60.1",
"@typescript-eslint/utils": "8.60.1"
"@typescript-eslint/eslint-plugin": "8.61.0",
"@typescript-eslint/parser": "8.61.0",
"@typescript-eslint/typescript-estree": "8.61.0",
"@typescript-eslint/utils": "8.61.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -5934,16 +5993,16 @@
"license": "MIT"
},
"node_modules/vue": {
"version": "3.5.35",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz",
"integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==",
"version": "3.5.38",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.38.tgz",
"integrity": "sha512-vAMKHfImQlYSy0C+PBue4s3ERZ2xGKfgZg5GXAsLInq1dyh2H78ILVP5sK0KPFPVW4kv+OGCIvBEondcjpZp7A==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.35",
"@vue/compiler-sfc": "3.5.35",
"@vue/runtime-dom": "3.5.35",
"@vue/server-renderer": "3.5.35",
"@vue/shared": "3.5.35"
"@vue/compiler-dom": "3.5.38",
"@vue/compiler-sfc": "3.5.38",
"@vue/runtime-dom": "3.5.38",
"@vue/server-renderer": "3.5.38",
"@vue/shared": "3.5.38"
},
"peerDependencies": {
"typescript": "*"
+2
View File
@@ -50,10 +50,12 @@
"@intlify/eslint-plugin-vue-i18n": "^4.4.0",
"@intlify/unplugin-vue-i18n": "^11.2.3",
"@playwright/test": "^1.54.1",
"@types/eslint-plugin-security": "^3.0.1",
"@types/node": "^24.1.0",
"@vitejs/plugin-vue": "^5.0.4",
"deepl-node": "^1.16.0",
"eslint": "^10.4.0",
"eslint-plugin-security": "^4.0.0",
"eslint-plugin-vue": "^10.9.1",
"fs-extra": "^11.3.0",
"globals": "^17.6.0",
+7
View File
@@ -20,6 +20,7 @@
<meta name="theme-color" content="{{ .htmlVars.color }}">
<!-- Add to home screen for Safari on iOS/iPadOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<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="{{ .htmlVars.appIcon }}">
@@ -51,6 +52,12 @@
<!-- Injected variables come from backend server -->
<script>
window.globalVars = {{ .globalVars }};
window.__pwaDeferredPrompt = null;
window.addEventListener("beforeinstallprompt", function (event) {
event.preventDefault();
window.__pwaDeferredPrompt = event;
window.dispatchEvent(new Event("pwa-install-available"));
});
</script>
<style>
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable security/detect-object-injection */
// scripts/sync-translations.js
import path from 'node:path';
import { fileURLToPath } from 'node:url';
+1 -1
View File
@@ -15,7 +15,7 @@ import { fetchURL } from "./utils";
*/
export async function createArchive(opts) {
const { fromSource, toSource, paths, destination, format, compression, deleteAfter } = opts;
if (!fromSource || !paths?.length || !destination) {
if (!fromSource || !Array.isArray(paths) || paths.length === 0 || !destination) {
throw new Error("fromSource, paths, and destination are required");
}
const body = {
+3 -6
View File
@@ -175,10 +175,10 @@ export async function createApiKey(params) {
}
// DELETE /api/auth/token
export function deleteApiKey(params) {
export async function deleteApiKey(params) {
try {
const apiPath = getApiPath('auth/token', params)
fetchURL(apiPath, {
await fetchURL(apiPath, {
method: 'DELETE'
})
} catch (err) {
@@ -201,10 +201,7 @@ function base64ToArrayBuffer(base64url) {
let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/')
while (base64.length % 4) base64 += '='
const binary = atob(base64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0))
return bytes.buffer
}
+1 -1
View File
@@ -27,7 +27,7 @@ export async function getConfig(req) {
}
// POST /api/office/callback or /public/api/office/callback
export async function callback(params, hash = null) {
export async function callback(params, hash) {
try {
let apiPath
if (hash) {
+22 -12
View File
@@ -2,8 +2,13 @@ import { notify } from '@/notify'
import { mutations, state } from '@/store'
import { globalVars } from '@/utils/constants'
import { downloadManager } from '@/utils/downloadManager'
import {
notifyDownloadComplete,
notifyDownloadError,
} from '@/utils/appNotifications'
import { getApiPath, getPublicApiPath } from '@/utils/url.js'
import { adjustedData, fetchURL } from './utils'
import { getObjectProperty } from '@/utils/object'
export { fetchPreviewImage } from '@/utils/previewRequests'
@@ -53,7 +58,7 @@ export async function getItems(source, path, only = "") {
* Registers a graceful pause for the active chunked upload at this path.
* Call immediately before aborting the chunk XHR so the server keeps partial data.
*/
export async function signalUploadPause(source, path, shareHash = null) {
export async function signalUploadPause(source, path, shareHash) {
if (shareHash) {
const apiPath = getPublicApiPath('resources/pause', {
hash: shareHash,
@@ -497,7 +502,7 @@ async function downloadChunkedArchive(url, format, files, filePaths, source, sha
downloadManager.setStatus(downloadId, 'downloading')
const hasDownloadPrompt = state.prompts?.some((h) => h.name === 'download')
const hasDownloadPrompt = state.prompts.some((h) => h.name === 'download')
if (!hasDownloadPrompt) {
mutations.showPrompt({ name: 'download' })
}
@@ -530,6 +535,7 @@ async function downloadChunkedArchive(url, format, files, filePaths, source, sha
downloadManager.setStatus(downloadId, 'completed')
downloadManager.updateProgress(downloadId, fileSize, fileSize)
notifyDownloadComplete(fileName, fileSize)
setTimeout(() => {
document.body.removeChild(link)
@@ -544,6 +550,7 @@ async function downloadChunkedArchive(url, format, files, filePaths, source, sha
const message =
error instanceof Error ? error.message || 'Unknown error' : String(error)
downloadManager.setError(downloadId, message)
notifyDownloadError(fileName, message)
throw error
}
}
@@ -585,7 +592,7 @@ async function downloadChunked(file, shareHash = "") {
downloadManager.setStatus(downloadId, "downloading")
// Show download prompt if not already shown (it should already be shown by downloadFiles, but check to be safe)
const hasDownloadPrompt = state.prompts?.some(h => h.name === 'download');
const hasDownloadPrompt = state.prompts.some(h => h.name === 'download');
if (!hasDownloadPrompt) {
mutations.showPrompt({ name: 'download' })
@@ -620,6 +627,7 @@ async function downloadChunked(file, shareHash = "") {
const dlDone = downloadManager.findById(downloadId)
const finalSize = dlDone?.size ?? fileSize
downloadManager.updateProgress(downloadId, finalSize, finalSize)
notifyDownloadComplete(fileName, finalSize)
setTimeout(() => {
document.body.removeChild(link)
@@ -636,6 +644,7 @@ async function downloadChunked(file, shareHash = "") {
const message =
error instanceof Error ? error.message || "Unknown error" : String(error)
downloadManager.setError(downloadId, message)
notifyDownloadError(fileName, message)
throw error
}
}
@@ -663,9 +672,9 @@ export function post(
const request = new XMLHttpRequest();
request.open("POST", apiPath, true);
for (const header in headers) {
request.setRequestHeader(header, headers[header]);
}
Object.entries(headers).forEach(([header, value]) => {
request.setRequestHeader(header, value);
});
if (typeof onupload === "function") {
request.upload.onprogress = (event) => {
@@ -808,7 +817,7 @@ export async function checksum(source, path, algo) {
const apiPath = getApiPath('resources', params)
const res = await fetchURL(apiPath)
const data = await res.json()
return data.checksums[algo]
return getObjectProperty(data.checksums, algo)
} catch (err) {
notify.showError(err.message || 'Error fetching checksum')
throw err
@@ -862,7 +871,7 @@ export function getPreviewURL(source, path, modified) {
// POST /api/resources/archive - Create an archive
export async function createArchive(opts) {
const { fromSource, toSource, paths, destination, format, compression, deleteAfter } = opts;
if (!fromSource || !paths?.length || !destination) {
if (!fromSource || !Array.isArray(paths) || paths.length === 0 || !destination) {
throw new Error("fromSource, paths, and destination are required");
}
const body = {
@@ -1051,9 +1060,10 @@ export function postPublic(
const request = new XMLHttpRequest();
request.open("POST", apiPath, true);
for (const header in headers) {
request.setRequestHeader(header, headers[header]);
}
Object.entries(headers).forEach(([header, value]) => {
request.setRequestHeader(header, value);
});
if (typeof onupload === "function") {
request.upload.onprogress = (event) => {
if (event.lengthComputable) {
@@ -1164,7 +1174,7 @@ export async function bulkDeletePublic(items) {
throw new Error('items array is required and must not be empty')
}
const hash = state.shareInfo?.hash;
const hash = state.shareInfo.hash;
if (!hash) {
throw new Error('share hash is required')
}
+12 -1
View File
@@ -64,7 +64,7 @@ export async function create(bodyObj = {}) {
const apiPath = getApiPath("share");
return await fetchJSON(apiPath, {
method: "POST",
body: JSON.stringify(bodyObj || {}),
body: JSON.stringify(bodyObj),
});
} catch (/** @type {any} */ err) {
notify.showError(err.message || "Error creating share");
@@ -124,3 +124,14 @@ export async function getShareInfoPublic(hash) {
throw err
}
}
// PATCH /public/api/share/pinnedItems (add by default; ?action=remove to unpin)
export async function patchPinnedItem({ hash, path, name, action = 'add' }) {
const params = { hash, action }
const apiPath = getPublicApiPath('share/pinnedItems', params)
await fetchURL(apiPath, {
method: 'PATCH',
body: JSON.stringify({ path, name }),
headers: { 'Content-Type': 'application/json' },
})
}
+4 -2
View File
@@ -1,5 +1,6 @@
import { fetchURL, fetchJSON } from '@/api/utils'
import { getApiPath, getPublicApiPath } from '@/utils/url.js'
import { getObjectProperty, setObjectProperty } from '@/utils/object'
import { notify } from '@/notify'
import { state } from '@/store/state.js'
import { mutations } from '@/store/mutations.js'
@@ -107,8 +108,9 @@ export async function update(user, which = ['all'], options = {}) {
if (which.length !== 1 || which[0] !== 'all') {
userData = {}
which.forEach(key => {
if (key in user) {
userData[key] = user[key]
const value = getObjectProperty(user, key)
if (value !== undefined) {
userData = setObjectProperty(userData, key, value)
}
})
}
-1
View File
@@ -129,4 +129,3 @@ describe('adjustedData', () => {
});
});
+62 -15
View File
@@ -82,6 +82,12 @@
:label="$t('general.download')"
@action="startDownload"
/>
<action
v-if="showSendToApp"
icon="ios_share"
:label="$t('buttons.sendToApp')"
@action="sendToApp"
/>
<action
v-if="showUnarchive"
icon="unarchive"
@@ -185,6 +191,7 @@
<action v-if="isPreview && permissions.modify" icon="edit" :label="$t('general.rename')" @action="showRenamePromptForPreview" />
<action v-if="showWatch" icon="visibility" :label="$t('buttons.watchFile')" @action="watchFile()" />
<action v-if="hasDownload" icon="file_download" :label="$t('general.download')" @action="startDownload" />
<action v-if="showSendToAppInPreview" icon="ios_share" :label="$t('buttons.sendToApp')" @action="sendToAppFromPreview" />
<action v-if="showUnarchiveInOverflow" icon="folder_open" :label="$t('prompts.unarchive')" @action="showUnarchivePromptFromPreview" />
<action v-if="showEdit" icon="edit" :label="$t('general.edit')" @action="edit()" />
<action v-if="markdownPreview" icon="visibility" :label="$t('general.preview')" @action="switchToMarkdown" />
@@ -195,7 +202,7 @@
</template>
<script>
import { resourcesApi, usersApi } from "@/api";
import { resourcesApi, shareApi, usersApi } from "@/api";
import Action from "@/components/Action.vue";
import { notify } from "@/notify";
import { getters, mutations, state } from "@/store";
@@ -204,6 +211,8 @@ import buttons from "@/utils/buttons";
import { copyToClipboard } from "@/utils/clipboard";
import { globalVars } from "@/utils/constants.js";
import downloadFiles from "@/utils/download";
import { canNativeShare, nativeShareFile } from "@/utils/nativeShare";
import { isRichTextPreviewMimeType } from "@/utils/mimetype";
function isArchivePath(pathOrName) {
if (!pathOrName || typeof pathOrName !== "string") return false;
@@ -254,7 +263,9 @@ export default {
if (state.selected.length === 0) return [];
// Map to actual items from state.req
if (typeof state.selected[0] === 'number') {
return state.selected.map(index => state.req.items[index]);
return state.selected
.map(index => state.req.items.at(index))
.filter(item => item !== null);
}
return state.selected;
},
@@ -328,13 +339,26 @@ export default {
if (this.showLimitedOptions) return false;
return this.selectedCount <= 1 && this.showShare;
},
showSendToApp() {
if (!canNativeShare()) return false;
if (this.showLimitedOptions || this.showCreate) return false;
if (!this.permissions.download || this.selectedCount !== 1) return false;
const item = this.firstSelected;
return item && !item.isDir && item.type !== "directory";
},
showSendToAppInPreview() {
if (!canNativeShare()) return false;
if (!this.hasDownload || this.req?.isDir) return false;
return this.isPreview;
},
showRename() {
if (this.showLimitedOptions) return false;
return !this.showCreate && this.selectedCount === 1 && this.permissions.modify && !this.isSearchActive;
},
showPinAction() {
if (this.showLimitedOptions) return false;
if (getters.isShare() || !getters.isLoggedIn()) return false;
if (!getters.isLoggedIn()) return false;
if (getters.isShare() && !state.shareInfo?.canEditShare) return false;
if (this.showCreate || this.isSearchActive) return false;
if (this.selectedCount !== 1) return false;
return getters.currentView() === "listingView";
@@ -478,7 +502,7 @@ export default {
},
markdownPreview() {
if (getters.currentView() !== 'editor') return false;
return state.req.type === 'text/markdown';
return isRichTextPreviewMimeType(state.req.type);
},
},
watch: {
@@ -692,6 +716,17 @@ export default {
const items = this.providedItems;
downloadFiles(items);
},
async sendToApp() {
mutations.closeTopPrompt();
const item = this.firstSelected;
if (!item) return;
await nativeShareFile(item);
},
async sendToAppFromPreview() {
mutations.closeTopPrompt();
if (!this.req) return;
await nativeShareFile(this.req);
},
showDeletePrompt() {
mutations.closeTopPrompt();
mutations.showPrompt({
@@ -723,25 +758,37 @@ export default {
},
async togglePin() {
const item = this.firstSelected;
if (!item?.name || getters.isShare() || !getters.isLoggedIn()) {
if (!item?.name || !getters.isLoggedIn()) {
return;
}
if (getters.isShare() && !state.shareInfo?.canEditShare) {
return;
}
mutations.closeHovers();
const source = item.source || state.req?.source || state.sources.current;
if (!source) {
return;
}
const directoryPath = state.req?.type === "directory"
? state.req.path
: url.getParentDir(item.path);
const action = item.pinned ? "remove" : "add";
try {
await usersApi.patchPinnedItem({
source,
path: directoryPath,
name: item.name,
action,
});
if (getters.isShare()) {
await shareApi.patchPinnedItem({
hash: state.shareInfo.hash,
path: directoryPath,
name: item.name,
action,
});
} else {
const source = item.source || state.req?.source || state.sources.current;
if (!source) {
return;
}
await usersApi.patchPinnedItem({
source,
path: directoryPath,
name: item.name,
action,
});
}
mutations.setReload(true);
} catch (e) {
notify.showError(e);
+23 -3
View File
@@ -85,6 +85,12 @@
:name="$t('search.useWildcardSearch')"
:description="$t('search.useWildcardSearchDescription')"
/>
<ToggleSwitch
class="item"
v-model="caseExactSearch"
:name="$t('search.caseExact')"
:description="$t('search.caseExactDescription')"
/>
</div>
</div>
</SettingsItem>
@@ -173,6 +179,7 @@ export default {
isTypeSelectDisabled: false,
showPreviewImages: false,
useWildcardSearch: false,
caseExactSearch: false,
folderSelect: [
{ label: this.$t("search.onlyFolders"), value: "type:folder" },
{ label: this.$t("search.onlyFiles"), value: "type:file" },
@@ -212,6 +219,9 @@ export default {
useWildcardSearch() {
this.submit();
},
caseExactSearch() {
this.submit();
},
searchTypes() {
this.submit();
},
@@ -281,7 +291,7 @@ export default {
return getters.eventTheme();
},
disableSearchOptions() {
return state.user.disableSearchOptions;
return state.user?.disableSearchOptions;
},
foldersOnly() {
return this.isTypeSelectDisabled;
@@ -403,7 +413,7 @@ export default {
},
updateSource(event) {
this.selectedSource = event.target.value;
this.submit();
void this.submit();
},
getItemUrl(s) {
// Use source from result if available, otherwise fall back to selectedSource
@@ -507,6 +517,13 @@ export default {
if (this.useWildcardSearch) {
dateParams.useWildcard = true;
}
let searchQuery = searchTypesFull + this.value;
if (this.caseExactSearch) {
searchQuery =
searchTypesFull.trim() === ""
? `case:exact ${this.value}`
: `case:exact ${searchTypesFull}${this.value}`;
}
this.ongoing++;
// Determine which sources to search
@@ -525,7 +542,7 @@ export default {
this.results = await toolsApi.search(
scope,
sourcesToSearch,
searchTypesFull + this.value,
searchQuery,
false,
dateParams
);
@@ -620,6 +637,9 @@ export default {
if (this.useWildcardSearch) {
query.wildcard = "1";
}
if (this.caseExactSearch) {
query.caseExact = "1";
}
if (this.isTypeSelectDisabled) {
query.typeLock = "1";
}
+5 -5
View File
@@ -51,7 +51,7 @@ export default {
name: "StatusBar",
data() {
return {
gallerySize: state.user.gallerySize,
gallerySize: state.user?.gallerySize,
};
},
computed: {
@@ -91,8 +91,8 @@ export default {
}
let total = 0;
state.selected.forEach(index => {
if (index >= 0 && index < state.req.items.length) {
const item = state.req.items[index];
if (index >= 0 && index < state.req?.items.length) {
const item = state.req.items.at(index);
if (item?.size) {
total += item.size;
}
@@ -206,7 +206,7 @@ export default {
// Only update if the mode actually changed
if (newMode !== currentMode) {
mutations.updateDisplayPreferences({ viewMode: newMode });
mutations.updateCurrentUser({ viewMode: newMode });
void mutations.updateCurrentUser({ viewMode: newMode });
}
},
// Ctrl + Mouse Wheel to adjust the slider sizes
@@ -215,7 +215,7 @@ export default {
const delta = event.deltaY > 0 ? 1 : -1; // Scroll down increases, up decreases
const advSearch = (state.route?.path || "").startsWith("/tools/advancedSearch");
const advSearch = (state.route.path || "").startsWith("/tools/advancedSearch");
if (this.currentView === "listingView" || advSearch) {
event.preventDefault();
const newSize = Math.min(9, Math.max(1, this.gallerySize - delta));
+1 -1
View File
@@ -85,7 +85,7 @@ export default {
computed: {
darkMode() { return getters.isDarkMode(); },
queueCount() {
return state.playbackQueue?.queue?.length || 0;
return state.playbackQueue.queue.length || 0;
},
syncedLyrics() {
return this.lyrics.length > 0 && !this.lyrics.every(line => line.timestamp === 0);
+17 -18
View File
@@ -36,6 +36,8 @@ import { resourcesApi } from "@/api";
import { notify } from "@/notify";
import { getters, mutations, state } from "@/store";
import { url } from "@/utils";
import { getObjectProperty } from '@/utils/object.js';
import { goToItemNotificationButton } from "@/utils/notificationActions";
export default {
name: "breadcrumbs",
@@ -69,7 +71,7 @@ export default {
return state.req.hasUpdate;
},
isDroppable() {
return getters.permissions()?.modify
return getters.permissions().modify
},
homeLink() {
return {
@@ -80,14 +82,12 @@ export default {
};
},
scrollRatio() {
const ratio = state.listing?.scrollRatio || 0;
const ratio = state.listing.scrollRatio || 0;
return ratio;
},
items() {
const req = state.req;
if (!req.items || !req.path) {
return [];
}
if (!req.path) return [];
const encodedPathString = url.encodedPath(state.req.path);
const originalParts = state.req.path.split("/");
const encodedParts = encodedPathString.split("/");
@@ -105,8 +105,8 @@ export default {
let accumulatedPath = "";
for (let i = 0; i < originalParts.length; i++) {
const origPart = originalParts[i];
const encodedElement = encodedParts[i];
const origPart = getObjectProperty(originalParts, i);
const encodedElement = getObjectProperty(encodedParts, i);
buildRef = `${buildRef + encodedElement}/`;
accumulatedPath = accumulatedPath ? `${accumulatedPath}/${origPart}` : origPart;
@@ -210,7 +210,7 @@ export default {
for (const i of state.selected) {
if (i < 0 || i >= state.req.length) continue;
const selectedItem = state.req.items[i];
const selectedItem = state.req.items.at(i);
let fromPath = selectedItem.path;
@@ -279,17 +279,16 @@ export default {
await resourcesApi.moveCopy(itemsToMove, "move", overwrite, rename);
}
const buttonAction = () => {
url.goToItem(source, targetPath, {}, false, getters.isShare());
};
notify.showSuccess(this.$t("prompts.moveSuccess"), {
icon: "folder",
buttons: [{
label: this.$t("buttons.goToItem"),
primary: true,
action: buttonAction
}]
buttons: [
goToItemNotificationButton(
this.$t("buttons.goToItem"),
source,
targetPath,
getters.isShare()
),
],
});
mutations.closeTopPrompt();
mutations.setReload(true);
@@ -308,7 +307,7 @@ export default {
const rename = option === "rename";
event.preventDefault();
mutations.closeTopPrompt();
moveAction(overwrite, rename);
void moveAction(overwrite, rename);
},
});
return;
+8 -12
View File
@@ -148,7 +148,7 @@ export default {
},
availableSources() {
// Get all available sources from state.sources.info
return state.sources?.info ? Object.keys(state.sources.info) : [state.req.source];
return state.sources.info ? Object.keys(state.sources.info) : [state.req.source];
},
showSourceSelector() {
if (this.hideDestinationSource) {
@@ -280,7 +280,7 @@ export default {
this.selectedSource = null;
this.selectedType = null;
// Fetch files for the new source
this.withLoading(() => resourcesApi.fetchFiles(newSource, newPath).then(this.fillOptions));
void this.withLoading(() => resourcesApi.fetchFiles(newSource, newPath).then(this.fillOptions));
},
resetToShare(newHash) {
// Reset to the share root
@@ -291,7 +291,7 @@ export default {
this.selectedSource = null;
this.selectedType = null;
// Fetch files for the share
this.withLoading(() => resourcesApi.fetchFilesPublic("/", newHash).then(this.fillOptions));
void this.withLoading(() => resourcesApi.fetchFilesPublic("/", newHash).then(this.fillOptions));
},
fillOptions(req) {
// Sets the current path and resets the current items.
@@ -373,14 +373,14 @@ export default {
if (this.browseSource) {
// Explicitly browsing a source - use files API
this.source = sourceToUse;
this.withLoading(() => resourcesApi.fetchFiles(sourceToUse, path).then(this.fillOptions));
void this.withLoading(() => resourcesApi.fetchFiles(sourceToUse, path).then(this.fillOptions));
} else if (this.browseShare || getters.isShare()) {
// Browsing a share - use public API
const hashToUse = this.browseShare || state.shareInfo?.hash;
this.withLoading(() => resourcesApi.fetchFilesPublic(path, hashToUse).then(this.fillOptions));
void this.withLoading(() => resourcesApi.fetchFilesPublic(path, hashToUse).then(this.fillOptions));
} else {
this.source = sourceToUse;
this.withLoading(() => resourcesApi.fetchFiles(sourceToUse, path).then(this.fillOptions));
void this.withLoading(() => resourcesApi.fetchFiles(sourceToUse, path).then(this.fillOptions));
}
},
@@ -510,13 +510,9 @@ export default {
this.selectedType = null;
if (this.browseShare || getters.isShare()) {
const hashToUse = this.browseShare || state.shareInfo?.hash;
this.withLoading(() =>
resourcesApi.fetchFilesPublic(p, hashToUse).then(this.fillOptions)
);
void this.withLoading(() => resourcesApi.fetchFilesPublic(p, hashToUse).then(this.fillOptions));
} else {
this.withLoading(() =>
resourcesApi.fetchFiles(source, p).then(this.fillOptions)
);
void this.withLoading(() => resourcesApi.fetchFiles(source, p).then(this.fillOptions));
}
},
fillFromList() {
+15 -18
View File
@@ -81,7 +81,9 @@ import LoadingSpinner from '@/components/LoadingSpinner.vue';
import { state, getters, mutations } from '@/store';
import { eventBus } from '@/store/eventBus';
import { goToItem, joinPath } from '@/utils/url';
import { goToItemNotificationButton } from '@/utils/notificationActions';
import { notify } from '@/notify';
import { getObjectProperty } from '@/utils/object.js';
export default {
name: 'FileTree',
@@ -136,7 +138,7 @@ export default {
return this.isRootInstance ? this.rootNodes : this.nodes;
},
canModify() {
return getters.permissions()?.modify;
return getters.permissions().modify;
},
showFiles() {
// Defaults to true
@@ -315,7 +317,7 @@ export default {
// don't navigate to current item if we're already viewing it
if (this.isCurrentItem(node)) {
if (node.isDir) {
this.toggleExpand(node, false); // If the item is a folder, just expand/collapse instead of navigate again
void this.toggleExpand(node, false); // If the item is a folder, just expand/collapse instead of navigate again
}
return;
}
@@ -379,8 +381,7 @@ export default {
let currentLevel = this.isRootInstance ? this.rootNodes : this.nodes;
for (let i = 0; i < pathParts.length; i++) {
const part = pathParts[i];
for (const part of pathParts) {
const node = currentLevel.find(n => n.name === part && n.isDir);
if (!node) break;
@@ -467,7 +468,7 @@ export default {
}
// Set a new timeout
const timeout = setTimeout(() => {
this.toggleExpand(node, true); // expand without navigating
void this.toggleExpand(node, true); // expand without navigating
this.expandTimeouts.delete(node);
}, 800); // 800ms delay (we need to hover that time to auto expand when dragging a item)
this.expandTimeouts.set(node, timeout);
@@ -529,7 +530,7 @@ export default {
// Build list of items to move
const items = [];
for (const index of selectedIndices) {
const item = state.req.items[index];
const item = getObjectProperty(state.req?.items, index);
if (!item) continue;
// Prevent dropping onto itself or into its own subdirectory
@@ -587,20 +588,16 @@ export default {
} else {
await resourcesApi.moveCopy(items, 'move', overwrite, rename);
}
const buttonAction = () => {
if (this.isShare) {
goToItem(this.shareHash, node.path, {}, false, true);
} else {
goToItem(this.currentSource, node.path, {});
}
};
const buttonProps = {
icon: 'folder',
buttons: [{
label: this.$t('buttons.goToItem'),
primary: true,
action: buttonAction,
}]
buttons: [
goToItemNotificationButton(
this.$t('buttons.goToItem'),
this.isShare ? this.shareHash : this.currentSource,
node.path,
this.isShare
),
],
};
notify.showSuccess(this.$t('prompts.moveSuccess'), buttonProps);
mutations.closeTopPrompt();
+5 -4
View File
@@ -32,6 +32,7 @@
import { fetchPreviewImage } from "@/api/resources";
import { globalVars } from "@/utils/constants";
import { getTypeInfo } from "@/utils/mimetype";
import { getObjectProperty } from '@/utils/object.js';
import { mutations, state, getters } from "@/store";
import { setImageLoaded } from "@/utils/imageCache";
import ThreeJs from "@/views/files/ThreeJs.vue";
@@ -225,7 +226,7 @@ export default {
},
shouldUse3DPreview() {
// Check if we should use 3D preview instead of regular icon
if (!this.isFile || !this.size || !this.path) return false;
if (this.mimetype === "directory" || !this.size || !this.path) return false;
if (!getters.previewPerms().models) return false;
const MAX_SIZE = 250 * 1024; // 250KB in bytes
@@ -240,7 +241,7 @@ export default {
return `${state.user?.gallerySize || 1}-${getters.viewMode()}`;
},
routePath() {
return state.route?.path ?? "";
return state.route.path ?? "";
},
},
methods: {
@@ -366,9 +367,9 @@ export default {
}
// Set the thumbnail or popup preview
if (getters.previewPerms().popup) {
mutations.setPreviewSource(sequence[index]);
mutations.setPreviewSource(getObjectProperty(sequence, index));
} else {
this.currentThumbnail = sequence[index];
this.currentThumbnail = getObjectProperty(sequence, index);
}
// Preload the next frame (cancellable fetch warms HTTP cache)
+1 -1
View File
@@ -33,7 +33,7 @@ export default {
];
},
rows() {
return buildIndexInfoRows(this.info, this.$t, state.user.locale);
return buildIndexInfoRows(this.info, this.$t, state.user?.locale);
},
},
};
+29 -28
View File
@@ -142,11 +142,13 @@
import { globalVars } from "@/utils/constants";
import downloadFiles from "@/utils/download";
import { getHumanReadableFilesize } from "@/utils/filesizes";
import { getObjectProperty } from '@/utils/object.js';
import { resourcesApi } from "@/api";
import * as upload from "@/utils/upload";
import { state, getters, mutations } from "@/store"; // Import your custom store
import { url } from "@/utils";
import { notify } from "@/notify";
import { goToItemNotificationButton } from "@/utils/notificationActions";
import Icon from "@/components/files/Icon.vue";
export default {
@@ -278,15 +280,13 @@ export default {
if (this.readOnly === true) return false;
for (const i of this.selected) {
if (
state.req.items[i].path === this.path &&
state.req.source === this.source
) {
const item = getObjectProperty(state.req.items, i);
if (!item) continue;
// Also check if we're trying to drop an item onto itself
if (item.path === this.path && state.req.source === this.source) {
return false;
}
// Also check if we're trying to drop an item onto itself
if (state.req.items[i].index === this.index) {
if (item.index === this.index) {
return false;
}
}
@@ -302,7 +302,7 @@ export default {
let previewPath;
if (this.path) {
previewPath = this.path;
} else if (state?.req?.path && this.name) {
} else if (state.req?.path && this.name) {
previewPath = url.joinPath(state.req.path, this.name);
} else {
return "";
@@ -310,13 +310,13 @@ export default {
// If forceFilesApi is true, always use authenticated files API
if (this.forceFilesApi) {
return resourcesApi.getPreviewURL(this.source || state?.req?.source, previewPath, this.modified);
return resourcesApi.getPreviewURL(this.source || state.req?.source, previewPath, this.modified);
}
if (getters.isShare()) {
return resourcesApi.getPreviewURLPublic(previewPath);
}
return resourcesApi.getPreviewURL(this.source || state.req.source, previewPath, this.modified);
return resourcesApi.getPreviewURL(this.source || state.req?.source, previewPath, this.modified);
},
isThumbsEnabled() {
return globalVars.enableThumbs;
@@ -553,12 +553,14 @@ export default {
let items = [];
for (const i of state.selected) {
const item = getObjectProperty(state.req.items, i);
if (!item) continue;
items.push({
from: state.req.items[i].path,
fromSource: state.req.items[i].source,
to: url.joinPath(this.path, state.req.items[i].name),
from: item.path,
fromSource: item.source,
to: url.joinPath(this.path, item.name),
toSource: this.source,
name: state.req.items[i].name,
name: item.name,
});
}
@@ -617,18 +619,16 @@ export default {
await resourcesApi.moveCopy(items, "move", overwrite, rename);
}
// Notification to move into the folder
const buttonAction = () => {
this.open();
};
const buttonProps = {
icon: "folder",
buttons: [
{
label: this.$t("buttons.goToItem"),
primary: true,
action: buttonAction
}
]
goToItemNotificationButton(
this.$t("buttons.goToItem"),
this.source,
this.path,
getters.isShare()
),
],
};
notify.showSuccess(this.$t("prompts.moveSuccess"), buttonProps);
// Close the prompt after successful operation and reload items for reflect the changes
@@ -705,7 +705,7 @@ export default {
}
if (
!state.user.singleClick &&
!state.user?.singleClick &&
getters.selectedCount() !== 0 &&
event.button === 0
) {
@@ -767,7 +767,7 @@ export default {
}
if (
!state.user.singleClick &&
!state.user?.singleClick &&
!event.ctrlKey &&
!event.metaKey &&
!event.shiftKey &&
@@ -802,14 +802,15 @@ export default {
// Check if state.req.items exists and has the item at this index
// This prevents errors when ListingItem is used outside of the main file listing (e.g., duplicate finder)
let previousHistoryItem = null;
if (state.req.items?.[this.index]) {
const currentItem = getObjectProperty(state.req.items, this.index);
if (currentItem) {
previousHistoryItem = {
name: state.req.items[this.index].name,
name: currentItem.name,
source: state.req.source,
path: state.req.path,
};
}
url.goToItem(this.source, this.path, previousHistoryItem || {}, false, getters.isShare());
url.goToItem(this.source, this.path, previousHistoryItem, false, getters.isShare());
},
},
};
+1 -1
View File
@@ -62,7 +62,7 @@ export default {
if (!state.shareInfo?.isPasswordProtected) {
return true
}
return state.shareInfo.passwordValid
return state.shareInfo?.passwordValid
},
shareInfo() {
return state.shareInfo;
@@ -577,13 +577,13 @@ export default {
case "ArrowRight":
if (this.hasNext) {
event.preventDefault();
this.next();
void this.next();
}
break;
case "ArrowLeft":
if (this.hasPrevious) {
event.preventDefault();
this.prev();
void this.prev();
}
break;
}
@@ -786,9 +786,9 @@ export default {
// Navigate based on button type
if (this.touchState.buttonType === 'previous' && this.hasPrevious) {
this.prev();
void this.prev();
} else if (this.touchState.buttonType === 'next' && this.hasNext) {
this.next();
void this.next();
}
}
@@ -916,7 +916,7 @@ export default {
}
// Only navigate if this wasn't a drag
if (!this.dragState.triggered) {
this.prev();
void this.prev();
}
this.resetDragState();
},
@@ -930,7 +930,7 @@ export default {
}
// Only navigate if this wasn't a drag
if (!this.dragState.triggered) {
this.next();
void this.next();
}
this.resetDragState();
},
@@ -110,7 +110,7 @@ export default {
async deleteApi() {
// Dummy delete function, to be filled in later
try {
authApi.deleteApiKey({ name: this.name });
await authApi.deleteApiKey({ name: this.name });
// Emit event to refresh API tokens list
setTimeout(() => {
eventBus.emit('apiKeysChanged');
+16 -12
View File
@@ -142,7 +142,7 @@
import { state, mutations, getters } from "@/store";
import { notify } from "@/notify";
import { resourcesApi } from "@/api";
import { goToItem } from "@/utils/url";
import { goToItemNotificationButton } from "@/utils/notificationActions";
import LoadingSpinner from "@/components/LoadingSpinner.vue";
import FileList from "@/components/files/FileList.vue";
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
@@ -180,7 +180,7 @@ export default {
},
watch: {
deleteAfter(newVal) {
mutations.updateCurrentUser({ deleteAfterArchive: newVal });
void mutations.updateCurrentUser({ deleteAfterArchive: newVal });
},
showFileList(newVal) {
if (!newVal) {
@@ -251,7 +251,7 @@ export default {
event.stopPropagation();
event.preventDefault();
if (this.newDirName && this.isDirNameValid) {
this.createDirectory();
void this.createDirectory();
}
},
async createDirectory() {
@@ -264,15 +264,15 @@ export default {
? `${currentPath + this.newDirName}/`
: `${currentPath}/${this.newDirName}/`;
if (getters.isShare()) {
await resourcesApi.postPublic(state.shareInfo?.hash, fullPath, "", false, undefined, {}, true);
await resourcesApi.postPublic(state.shareInfo.hash, fullPath, "", false, undefined, {}, true);
} else {
await resourcesApi.post(currentSource, fullPath, "", false, undefined, {}, true);
}
if (getters.isShare()) {
resourcesApi.fetchFilesPublic(currentPath, state.shareInfo?.hash)
await resourcesApi.fetchFilesPublic(currentPath, state.shareInfo.hash)
.then((req) => this.$refs.fileList.fillOptions(req, true));
} else {
resourcesApi.fetchFiles(currentSource, currentPath)
await resourcesApi.fetchFiles(currentSource, currentPath)
.then((req) => this.$refs.fileList.fillOptions(req, true));
}
mutations.setReload(true);
@@ -312,14 +312,18 @@ export default {
const destSource = this.destSource || this.source;
const archivePath = dest;
const buttonAction = () => {
if (archivePath) {
goToItem(destSource || null, archivePath, {}, false, getters.isShare());
}
};
notify.showSuccess(this.$t("prompts.archiveSuccess"), {
icon: "folder",
buttons: archivePath ? [{ label: this.$t("buttons.goToItem"), primary: true, action: buttonAction }] : undefined,
buttons: archivePath
? [
goToItemNotificationButton(
this.$t("buttons.goToItem"),
destSource,
archivePath,
getters.isShare()
),
]
: undefined,
});
} catch (err) {
console.error(err);
@@ -116,8 +116,9 @@ export default {
// Only include permissions when customizing token
if (this.customizeToken) {
// Filter to get keys of permissions set to true and join them as a comma-separated string
const permissionsString = Object.keys(this.localPerms)
.filter((key) => this.localPerms[key])
const permissionsString = Object.entries(this.localPerms)
.filter(([, value]) => value)
.map(([key]) => key)
.join(",");
params.permissions = permissionsString;
}
+3 -3
View File
@@ -116,7 +116,7 @@ export default {
if (state.isSearchActive || getters.currentView() === "preview") {
const selected = state.selected[0];
const item = state.req.items?.[selected] || selected;
const item = state.req.items?.at(selected) || selected;
const previewUrl = this.getPreviewUrl(item.source || state.req.source, item.path, item.modified, item.type);
items.push({
source: item.source || state.req.source,
@@ -128,7 +128,7 @@ export default {
hasPreview: item.hasPreview,
});
} else if (!this.isListing) {
const item = state.req.items[state.selected[0]];
const item = state.req.items.at(state.selected[0]);
const previewUrl = this.getPreviewUrl(item.source || state.req.source, item.path, item.modified, item.type);
items.push({
source: item.source || state.req.source,
@@ -141,7 +141,7 @@ export default {
});
} else {
for (const index of state.selected) {
const item = state.req.items[index];
const item = state.req.items.at(index);
const previewUrl = this.getPreviewUrl(item.source || state.req.source, item.path, item.modified, item.type);
items.push({
source: item.source || state.req.source,
+4 -4
View File
@@ -106,13 +106,13 @@ export default {
return getters.currentPrompt();
},
downloads() {
return downloadManager?.queue || [];
return downloadManager.queue || [];
},
hasDownloads() {
return (downloadManager?.queue?.length || 0) > 0;
return (downloadManager.queue.length || 0) > 0;
},
hasClearable() {
if (!downloadManager?.queue) {
if (!downloadManager.queue) {
return false;
}
return downloadManager.queue.some((download) =>
@@ -173,7 +173,7 @@ export default {
},
close() {
// Only close if no active downloads
if (!downloadManager?.hasActive()) {
if (!downloadManager.hasActive()) {
const prompt = getters.currentPrompt();
if (prompt?.name === 'download') {
mutations.closeTopPrompt();
+25 -20
View File
@@ -78,7 +78,11 @@ import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload";
import { url } from "@/utils";
import { notify } from "@/notify";
import { goToItem } from "@/utils/url";
import {
notifyMoveCopyComplete,
notifyOperationError,
} from "@/utils/appNotifications";
import { goToItemNotificationButton } from "@/utils/notificationActions";
import LoadingSpinner from "@/components/LoadingSpinner.vue";
import PathPickerButton from "@/components/files/PathPickerButton.vue";
import { eventBus } from '@/store/eventBus';
@@ -160,7 +164,7 @@ export default {
}));
} else if (state.isSearchActive) {
// Add null checks to prevent undefined values
if (state.selected?.[0]?.path) {
if (state.selected[0]?.path) {
this.localItems = [
{
from: state.selected[0].path,
@@ -172,7 +176,7 @@ export default {
} else {
if (state.selected && state.req?.items) {
for (const item of state.selected) {
const reqItem = state.req.items[item];
const reqItem = state.req.items.at(item);
if (reqItem?.path) {
this.localItems.push({
from: reqItem.path,
@@ -221,7 +225,7 @@ export default {
event.stopPropagation();
event.preventDefault();
if (this.newDirName && this.isDirNameValid) {
this.createDirectory();
void this.createDirectory();
}
},
async createDirectory() {
@@ -239,10 +243,10 @@ export default {
}
// Refresh the file list while keeping the current navigation that we did in the prompt
if (getters.isShare()) {
resourcesApi.fetchFilesPublic(currentPath, state.shareInfo?.hash)
await resourcesApi.fetchFilesPublic(currentPath, state.shareInfo.hash)
.then((req) => this.$refs.fileList.fillOptions(req, true));
} else {
resourcesApi.fetchFiles(currentSource, currentPath)
await resourcesApi.fetchFiles(currentSource, currentPath)
.then((req) => this.$refs.fileList.fillOptions(req, true));
}
// Clicking create will also return the buttons to their normal state
@@ -355,6 +359,7 @@ export default {
// All operations failed - show error but DON'T close prompt
const errorMessage = result.failed[0]?.message || this.$t("prompts.operationFailed");
notify.showError(errorMessage);
notifyOperationError(errorMessage);
return;
} else if (hasFailures && hasSuccesses) {
// Partial failure - show warning and continue
@@ -382,6 +387,7 @@ export default {
if (!hasFailures || hasSuccesses) {
notify.showSuccessToast(this.$t("prompts.moveSuccess"));
notifyMoveCopyComplete("move", this.localItems.length);
}
// Navigate next, previous or parent dir (like when deleting)
@@ -408,27 +414,25 @@ export default {
const destSource = this.destSource;
const destPath = this.destPath;
// Show success notification with optional button to navigate to destination
// For shares, destSource might be null, but goToItem handles shares via state.shareInfo.hash
const buttonAction = () => {
if (destPath) {
goToItem(destSource || state.shareInfo?.hash, destPath, {}, false, getters.isShare());
}
};
const buttonProps = {
icon: "folder",
buttons: destPath ? [
{
label: this.$t("buttons.goToItem"),
primary: true,
action: buttonAction
}
] : undefined
buttons: destPath
? [
goToItemNotificationButton(
this.$t("buttons.goToItem"),
destSource || state.shareInfo?.hash,
destPath,
getters.isShare()
),
]
: undefined,
};
if (this.operation === "move") {
notify.showSuccess(this.$t("prompts.moveSuccess"), buttonProps);
notifyMoveCopyComplete("move", this.localItems.length);
} else {
notify.showSuccess(this.$t("prompts.copySuccess"), buttonProps);
notifyMoveCopyComplete("copy", this.localItems.length);
}
}
}
@@ -454,6 +458,7 @@ export default {
}
notify.showError(errorMessage);
notifyOperationError(errorMessage);
} finally {
this.isLoading = false; // Hide loading spinner
}
+9 -8
View File
@@ -36,6 +36,7 @@ import { resourcesApi } from "@/api";
import { getters, mutations, state } from "@/store"; // Import your custom store
import { url } from "@/utils";
import { notify } from "@/notify";
import { goToItemNotificationButton } from "@/utils/notificationActions";
import LoadingSpinner from "@/components/LoadingSpinner.vue";
export default {
name: "new-dir",
@@ -112,16 +113,16 @@ export default {
},
showNotification(source, newPath) {
const buttonAction = () => {
url.goToItem(source, newPath, {}, false, getters.isShare());
};
const buttonProps = {
icon: "folder",
buttons: [{
label: this.$t("buttons.goToItem"),
primary: true,
action: buttonAction
}]
buttons: [
goToItemNotificationButton(
this.$t("buttons.goToItem"),
source,
newPath,
getters.isShare()
),
],
};
notify.showSuccess(this.$t("prompts.newDirSuccess"), buttonProps);
},
+9 -8
View File
@@ -37,6 +37,7 @@ import { resourcesApi } from "@/api";
import { getters, mutations } from "@/store"; // Import your custom store
import { notify } from "@/notify";
import { url } from "@/utils";
import { goToItemNotificationButton } from "@/utils/notificationActions";
import LoadingSpinner from "@/components/LoadingSpinner.vue";
export default {
name: "new-file",
@@ -109,16 +110,16 @@ export default {
},
showNotification(source, newPath) {
const buttonAction = () => {
url.goToItem(source, newPath, {}, false, getters.isShare());
};
const buttonProps = {
icon: "insert_drive_file",
buttons: [{
label: this.$t("buttons.goToItem"),
primary: true,
action: buttonAction
}]
buttons: [
goToItemNotificationButton(
this.$t("buttons.goToItem"),
source,
newPath,
getters.isShare()
),
],
};
notify.showSuccess(this.$t("prompts.newFileSuccess"), buttonProps);
},
@@ -71,13 +71,13 @@ export default {
},
computed: {
playbackQueue() {
return state.playbackQueue?.queue || [];
return state.playbackQueue.queue;
},
currentQueueIndex() {
return state.playbackQueue?.currentIndex ?? -1;
return state.playbackQueue.currentIndex;
},
playbackMode() {
return state.playbackQueue?.mode || 'single';
return state.playbackQueue.mode || 'single';
},
queueCount() {
return this.playbackQueue.length;
@@ -110,7 +110,7 @@ export default {
}));
},
isPlaying() {
return state.playbackQueue?.isPlaying || false;
return state.playbackQueue.isPlaying || false;
},
isPromptVisible() {
// Check if this PlaybackQueue prompt is the current active prompt
@@ -188,7 +188,7 @@ export default {
},
navigateToIndex(index) {
if (index >= 0 && index < this.playbackQueue.length) {
const item = this.playbackQueue[index];
const item = this.playbackQueue.at(index);
// Update store with new current index
mutations.setPlaybackQueue({
queue: this.playbackQueue,
+48 -34
View File
@@ -212,16 +212,16 @@ export default {
},
methods: {
triggerPromptBorderFlash(id) {
if (this.flashBorderTimers[id]) {
clearTimeout(this.flashBorderTimers[id]);
const timer = getObjectProperty(this.flashBorderTimers, id);
if (timer) {
clearTimeout(timer);
}
this.flashBorderIds = { ...this.flashBorderIds, [id]: true };
this.flashBorderTimers[id] = setTimeout(() => {
const next = { ...this.flashBorderIds };
delete next[id];
this.flashBorderIds = setObjectProperty(this.flashBorderIds, id, true);
this.flashBorderTimers = setObjectProperty(this.flashBorderTimers, id, setTimeout(() => {
const next = omitObjectProperty(this.flashBorderIds, id);
this.flashBorderIds = next;
delete this.flashBorderTimers[id];
}, 500);
this.flashBorderTimers = omitObjectProperty(this.flashBorderTimers, id);
}, 500));
},
handleWindowResize() {
const maxWidth = window.innerWidth * 0.9;
@@ -366,7 +366,7 @@ export default {
const promptToClose = state.prompts.find(p => p.id === id);
if (!promptToClose) return;
// Check if it's the upload prompt with active uploads
if (promptToClose?.name === "upload") {
if (promptToClose.name === "upload") {
const hasActiveUploads = state.upload.isUploading;
const hasWarningPrompt = state.prompts.some(p => p.name === "CloseWithActiveUploads");
@@ -395,11 +395,11 @@ export default {
this.cleanupDragState(id);
},
cleanupDragState(id) {
delete this.dragOffsets[id];
delete this.dragStarts[id];
this.dragOffsets = omitObjectProperty(this.dragOffsets, id);
this.dragStarts = omitObjectProperty(this.dragStarts, id);
this.touchIds = omitObjectProperty(this.touchIds, id);
this.draggingIds.delete(id);
delete this.sizes[id];
this.sizes = omitObjectProperty(this.sizes, id);
},
/**
* Escape closes the top prompt. Enter clicks the last `.card-actions` button (far right in DOM).
@@ -471,7 +471,7 @@ export default {
const headerHeight = headerEl ? headerEl.getBoundingClientRect().height : 40;
const rect = el.getBoundingClientRect();
const windowHeight = rect.height;
const offset = this.dragOffsets[id] || { x: 0, y: 0 };
const offset = getObjectProperty(this.dragOffsets, id) || { x: 0, y: 0 };
const centerX = viewportW / 2 + offset.x;
const centerY = viewportH / 2 + offset.y;
@@ -482,10 +482,10 @@ export default {
const clampedX = Math.max(minCenterX, Math.min(maxCenterX, centerX));
const clampedY = Math.max(minCenterY, Math.min(maxCenterY, centerY));
this.dragOffsets[id] = {
this.dragOffsets = setObjectProperty(this.dragOffsets, id, {
x: clampedX - viewportW / 2,
y: clampedY - viewportH / 2,
};
});
},
getPromptElement(id) {
const windows = this.$refs.promptWindow;
@@ -509,8 +509,17 @@ export default {
const pos = this.getPointerPos(e, type);
if (!pos) return;
if (!this.dragOffsets[id]) this.dragOffsets[id] = { x: 0, y: 0 };
this.dragStarts[id] = { x: pos.x - this.dragOffsets[id].x, y: pos.y - this.dragOffsets[id].y };
let dragOffsets = this.dragOffsets;
const existingOffset = getObjectProperty(dragOffsets, id);
if (!existingOffset) {
dragOffsets = setObjectProperty(dragOffsets, id, { x: 0, y: 0 });
this.dragOffsets = dragOffsets;
}
const offset = getObjectProperty(this.dragOffsets, id);
this.dragStarts = setObjectProperty(this.dragStarts, id, {
x: pos.x - offset.x,
y: pos.y - offset.y,
});
this.draggingIds.add(id);
const move = (e) => this.onPointerMove(e, id, type);
@@ -526,7 +535,8 @@ export default {
}
},
onPointerMove(e, id, type) {
if (!this.dragStarts[id]) return;
const dragStart = getObjectProperty(this.dragStarts, id);
if (!dragStart) return;
let pos;
if (type === "touch") {
if (!e.touches) return;
@@ -538,10 +548,10 @@ export default {
pos = { x: e.clientX, y: e.clientY };
}
this.dragOffsets[id] = {
x: pos.x - this.dragStarts[id].x,
y: pos.y - this.dragStarts[id].y,
};
this.dragOffsets = setObjectProperty(this.dragOffsets, id, {
x: pos.x - dragStart.x,
y: pos.y - dragStart.y,
});
// Find the element for this prompt
const windows = this.$refs.promptWindow;
@@ -558,7 +568,7 @@ export default {
window.removeEventListener("mousemove", moveHandler);
window.removeEventListener("mouseup", endHandler);
}
delete this.dragStarts[id];
this.dragStarts = omitObjectProperty(this.dragStarts, id);
this.draggingIds.delete(id);
},
startResize(e, id, edge) {
@@ -572,20 +582,25 @@ export default {
: windows;
if (!el) return;
// Capture current size (if not already fixed, set it)
if (!this.sizes[id]) {
this.sizes[id] = {
let sizes = this.sizes;
const existingSize = getObjectProperty(sizes, id);
if (!existingSize) {
sizes = setObjectProperty(sizes, id, {
width: el.offsetWidth,
height: el.offsetHeight,
};
});
this.sizes = sizes;
}
const currentSize = getObjectProperty(this.sizes, id);
const dragOffset = getObjectProperty(this.dragOffsets, id) || { x: 0, y: 0 };
// Initial values
this.resizingId = id;
this.resizingEdge = edge;
this.resizeStart = {
width: this.sizes[id].width,
height: this.sizes[id].height,
offsetX: this.dragOffsets[id]?.x || 0,
offsetY: this.dragOffsets[id]?.y || 0,
width: currentSize.width,
height: currentSize.height,
offsetX: dragOffset.x,
offsetY: dragOffset.y,
clientX: pos.x,
clientY: pos.y,
};
@@ -604,7 +619,6 @@ export default {
e.preventDefault();
e.stopPropagation();
},
onResizeMove(e, type) {
if (!this.resizingId) return;
const pos = this.getPointerPos(e, type);
@@ -667,12 +681,12 @@ export default {
deltaOffsetY = +(newHeight - start.height) / 2;
}
// Update sizes
this.sizes[id] = { width: newWidth, height: newHeight };
this.sizes = setObjectProperty(this.sizes, id, { width: newWidth, height: newHeight });
// Update drag
this.dragOffsets[id] = {
this.dragOffsets = setObjectProperty(this.dragOffsets, id, {
x: start.offsetX + deltaOffsetX,
y: start.offsetY + deltaOffsetY,
};
});
// Clamp to viewport
const windows = this.$refs.promptWindow;
const el = Array.isArray(windows)
+2 -2
View File
@@ -127,7 +127,7 @@ export default {
event.stopPropagation();
}
if (event.key === 'Enter') {
this.submit();
void this.submit();
}
},
updateFullName() {
@@ -167,7 +167,7 @@ export default {
}
// Use parentItems if we are in a preview, otherwise use state.req.items
const items = this.parentItems.length > 0 ? this.parentItems : (state.req?.items || []);
const items = this.parentItems.length > 0 ? this.parentItems : (state.req?.items ?? []);
for (const item of items) {
if (!item.name) {
continue;
@@ -36,7 +36,7 @@ export default {
},
isSameFile() {
// Check if the current prompt has props indicating same file
return this.currentPrompt?.props?.isSameFile === true;
return this.currentPrompt.props?.isSameFile === true;
},
},
methods: {},
+12 -6
View File
@@ -560,7 +560,9 @@ export default {
return this.req.path;
}
const index = /** @type {number} */ (this.selected[0]);
return buildItemUrl(this.req.items[index].source, this.req.items[index].path);
const item = this.req.items.at(index);
if (!item) return this.req.path;
return buildItemUrl(item.source, item.path);
},
isEditMode() {
return this.editing && this.link && Object.keys(this.link).length > 0;
@@ -805,7 +807,7 @@ export default {
// Update the link in the local list
const index = this.links.findIndex(l => l.hash === this.editingLink.hash);
if (index !== -1) {
this.links[index] = res;
this.links.splice(index, 1, res);
}
this.editingLink = null;
// emit event to reload shares in settings view
@@ -901,7 +903,7 @@ export default {
* @param {number} time
*/
humanTime(time) {
return fromNow(time, state.user.locale)
return fromNow(time, state.user?.locale)
},
sort() {
this.links = this.links.sort((a, b) => {
@@ -1085,9 +1087,13 @@ export default {
this.filePickerField = null;
},
validateExtensions(value) {
if (value === "" || value === "*") return true;
const regex = /^\.\w+(?: \.\w+)*$/;
return regex.test(value);
const normalized = value.trim();
if (normalized === "" || normalized === "*") {
return true;
}
const parts = normalized.split(/\s+/);
const extensionRegex = /^\.\w+$/;
return parts.every(part => extensionRegex.test(part));
},
},
};
@@ -52,7 +52,7 @@ export default {
if (!state.shareInfo?.isPasswordProtected) {
return true
}
return state.shareInfo.passwordValid
return state.shareInfo?.passwordValid
},
getShareBanner() {
if (state.shareInfo?.banner.startsWith("http")) {
@@ -295,6 +295,7 @@ import { notify } from "@/notify";
import { usersApi, shareApi } from "@/api";
import { tools } from "@/utils/constants";
import { getIconClass } from "@/utils/material-symbols";
import { getObjectProperty } from '@/utils/object.js';
import FileList from "../files/FileList.vue";
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
import { eventBus } from "@/store/eventBus";
@@ -404,7 +405,7 @@ export default {
// Initialize with existing sidebar links based on context
if (this.context === 'share' && this.shareData?.sidebarLinks) {
this.links = [...this.shareData.sidebarLinks];
} else if (this.context === 'user' && state.user?.sidebarLinks && state.user.sidebarLinks.length > 0) {
} else if (this.context === 'user' && state.user?.sidebarLinks && state.user?.sidebarLinks.length > 0) {
this.links = [...state.user.sidebarLinks];
} else if (this.context === 'user') {
// Generate default links from sources for user context
@@ -413,7 +414,7 @@ export default {
if (this.context === 'user') {
if (typeof state.user?.showToolsInSidebar === 'boolean') {
this.showToolsInSidebar = state.user.showToolsInSidebar;
this.showToolsInSidebar = state.user?.showToolsInSidebar;
} else {
this.showToolsInSidebar = true;
}
@@ -549,20 +550,28 @@ export default {
}
},
getCategoryLabel(category) {
const labels = {
source: this.$t('general.source'),
'source-minimal': this.$t('general.source'),
'source-alt': this.$t('general.source'),
'source-hybrid': this.$t('general.source'),
'source-hybrid-2': this.$t('general.source'),
tool: this.$t('general.tool'),
custom: this.$t('sidebar.customLink'),
share: this.$t('general.share'),
shareInfo: this.$t('share.shareInfo'),
download: this.$t('general.download'),
divider: this.$t('general.divider'),
};
return labels[category] || category;
switch (category) {
case 'source':
case 'source-minimal':
case 'source-alt':
case 'source-hybrid':
case 'source-hybrid-2':
return this.$t('general.source');
case 'tool':
return this.$t('general.tool');
case 'custom':
return this.$t('sidebar.customLink');
case 'share':
return this.$t('general.share');
case 'shareInfo':
return this.$t('share.shareInfo');
case 'download':
return this.$t('general.download');
case 'divider':
return this.$t('general.divider');
default:
return category;
}
},
getLinkDisplayName(link) {
// For dividers, show the name or a default
@@ -701,7 +710,8 @@ export default {
this.tempSelectedSource = "";
},
editLink(index) {
const link = this.links[index];
const link = this.links.at(index);
if (!link) return;
this.editingIndex = index;
this.showAddForm = true;
@@ -807,7 +817,7 @@ export default {
event.dataTransfer.setData("text/html", event.target);
// Set the entire link item as the drag image
const linkItem = this.linkItemRefs[index];
const linkItem = getObjectProperty(this.linkItemRefs, index);
if (linkItem) {
// Create a clone for the drag image to avoid affecting the original
const clone = linkItem.cloneNode(true);
@@ -899,7 +909,7 @@ export default {
await usersApi.update(updatedUser, ['sidebarLinks', 'showToolsInSidebar']);
mutations.updateCurrentUser({
void mutations.updateCurrentUser({
sidebarLinks: [...this.links],
showToolsInSidebar: this.showToolsInSidebar,
});
+1 -1
View File
@@ -131,7 +131,7 @@ export default {
// Wait for navigation so Layout mounts and registers the toast callback before we toast
await this.$router.push(this.redirect);
}
mutations.updateCurrentUser({ otpEnabled: true });
void mutations.updateCurrentUser({ otpEnabled: true });
this.succeeded = true;
this.error = "";
notify.showSuccessToast(this.$t("otp.verificationSucceed"));
+15 -7
View File
@@ -123,7 +123,7 @@ import { state, mutations, getters } from "@/store";
import { url } from "@/utils";
import { notify } from "@/notify";
import { resourcesApi } from "@/api";
import { goToItem } from "@/utils/url";
import { goToItemNotificationButton } from "@/utils/notificationActions";
import FileList from "@/components/files/FileList.vue";
import LoadingSpinner from "@/components/LoadingSpinner.vue";
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
@@ -152,7 +152,7 @@ export default {
watch: {
deleteAfter(newVal) {
// Update the user preference in real time
mutations.updateCurrentUser({ deleteAfterArchive: newVal });
void mutations.updateCurrentUser({ deleteAfterArchive: newVal });
},
showFileList(newVal) {
if (!newVal) {
@@ -230,7 +230,7 @@ export default {
event.stopPropagation();
event.preventDefault();
if (this.newDirName && this.isDirNameValid) {
this.createDirectory();
void this.createDirectory();
}
},
async createDirectory() {
@@ -248,10 +248,10 @@ export default {
await resourcesApi.post(currentSource, fullPath, "", false, undefined, {}, true);
}
if (getters.isShare()) {
resourcesApi.fetchFilesPublic(currentPath, state.shareInfo?.hash)
await resourcesApi.fetchFilesPublic(currentPath, state.shareInfo.hash)
.then((req) => this.$refs.fileList.fillOptions(req, true));
} else {
resourcesApi.fetchFiles(currentSource, currentPath)
await resourcesApi.fetchFiles(currentSource, currentPath)
.then((req) => this.$refs.fileList.fillOptions(req, true));
}
mutations.setReload(true);
@@ -280,10 +280,18 @@ export default {
const destPath = this.destPath;
const destSource = toSource;
const buttonAction = () => destPath && goToItem(destSource || state.shareInfo?.hash, destPath, {}, false, getters.isShare());
notify.showSuccess(this.$t("prompts.unarchiveSuccess"), {
icon: "folder",
buttons: destPath ? [{ label: this.$t("buttons.goToItem"), primary: true, action: buttonAction }] : undefined,
buttons: destPath
? [
goToItemNotificationButton(
this.$t("buttons.goToItem"),
destSource || state.shareInfo?.hash,
destPath,
getters.isShare()
),
]
: undefined,
});
} catch (err) {
console.error(err);
+43 -9
View File
@@ -56,6 +56,22 @@
>
{{ $t("general.folder") }}
</button>
<button
type="button"
style="margin-left: 1em"
@click="triggerCameraPicker"
class="button button--flat"
>
{{ $t("general.camera") }}
</button>
<button
type="button"
style="margin-left: 1em"
@click="triggerVideoPicker"
class="button button--flat"
>
{{ $t("general.video") }}
</button>
</div>
</div>
</div>
@@ -189,6 +205,10 @@
<input ref="fileInput" @change="onFilePicked" type="file" multiple style="display: none" />
<input ref="folderInput" @change="onFolderPicked" type="file" webkitdirectory directory multiple
style="display: none" />
<input ref="cameraInput" @change="onFilePicked" type="file" accept="image/*" capture="environment"
style="display: none" />
<input ref="videoInput" @change="onFilePicked" type="file" accept="video/*" capture="environment"
style="display: none" />
</template>
<script>
@@ -246,6 +266,8 @@ export default {
setup(props) {
const fileInput = ref(null);
const folderInput = ref(null);
const cameraInput = ref(null);
const videoInput = ref(null);
const files = computed(() => uploadManager.queue);
const isDragging = ref(false);
const showConflictPrompt = ref(false);
@@ -280,7 +302,7 @@ export default {
clearAll: clearAll.value,
},
};
mutations.updateCurrentUser(data);
void mutations.updateCurrentUser(data);
await usersApi.update(data, ["fileLoading"]);
notify.showSuccessToast("Upload settings updated");
} catch (e) {
@@ -324,7 +346,7 @@ export default {
mutations.showPrompt({
name: "rename",
confirm: (newName) => {
renameUploadFolder(conflictingFolder, newName);
void renameUploadFolder(conflictingFolder, newName);
},
props: { folderName: conflictingFolder }
});
@@ -378,7 +400,7 @@ export default {
watch(isUploading, (active) => {
if (active) {
acquireWakeLock();
void acquireWakeLock();
} else {
releaseWakeLock();
}
@@ -423,7 +445,7 @@ export default {
const handleVisibilityChange = async () => {
if (document.visibilityState === "visible" && isUploading.value) {
acquireWakeLock();
void acquireWakeLock();
}
};
@@ -456,7 +478,7 @@ export default {
await processDroppedItems(items, destination);
} else if (first.file instanceof File && typeof first.relativePath === 'string') {
// This is an array of objects {file, relativePath} for paste from clipboard OS.
uploadManager.add(destination, items);
void uploadManager.add(destination, items);
}
} else if (items) {
// This case handles a FileList object from the upload prompt's own input fields.
@@ -466,7 +488,7 @@ export default {
};
onMounted(async () => {
document.addEventListener("visibilitychange", handleVisibilityChange);
document.addEventListener("visibilitychange", void handleVisibilityChange);
window.addEventListener("beforeunload", handleBeforeUnload);
uploadManager.setOnConflict(handleConflict);
if (props.initialItems) {
@@ -475,7 +497,7 @@ export default {
});
onUnmounted(() => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
document.removeEventListener("visibilitychange", void handleVisibilityChange);
window.removeEventListener("beforeunload", handleBeforeUnload);
uploadManager.setOnConflict(() => {}); // cleanup
releaseWakeLock();
@@ -489,6 +511,14 @@ export default {
if (folderInput.value) folderInput.value.click();
};
const triggerCameraPicker = () => {
if (cameraInput.value) cameraInput.value.click();
};
const triggerVideoPicker = () => {
if (videoInput.value) videoInput.value.click();
};
const onFilePicked = (event) => {
const pickedFiles = event.target.files;
if (pickedFiles.length > 0) {
@@ -570,7 +600,7 @@ export default {
allFiles.forEach(files => { filesToUpload.push(...files); });
if (filesToUpload.length > 0) {
uploadManager.add(destination, filesToUpload);
void uploadManager.add(destination, filesToUpload);
}
};
@@ -580,7 +610,7 @@ export default {
relativePath: file.webkitRelativePath || file.name,
}));
if (filesToAdd.length > 0) {
uploadManager.add(destination, filesToAdd);
void uploadManager.add(destination, filesToAdd);
}
};
@@ -637,10 +667,14 @@ export default {
return {
triggerFilePicker,
triggerFolderPicker,
triggerCameraPicker,
triggerVideoPicker,
onFilePicked,
onFolderPicked,
fileInput,
folderInput,
cameraInput,
videoInput,
files,
isDragging,
onDragEnter,
+3 -2
View File
@@ -163,6 +163,7 @@ import Errors from "@/views/Errors.vue";
import { notify } from "@/notify";
import { globalVars } from "@/utils/constants";
import { eventBus } from "@/store/eventBus";
import { setObjectProperty } from '@/utils/object.js';
export default {
name: "user-edit",
@@ -513,7 +514,7 @@ export default {
return "/";
},
openScopePicker(index) {
const row = this.selectedSources[index];
const row = this.selectedSources.at(index);
if (!row?.name) {
return;
}
@@ -584,7 +585,7 @@ export default {
this.emitUserUpdate();
},
updateUserField(field, value) {
this.user[field] = value;
this.user = setObjectProperty(this.user, field, value);
this.emitUserUpdate();
},
setDefaultLoginMethod() {
+3 -2
View File
@@ -244,8 +244,9 @@ export default {
resolvedKey(row) {
const k = this.itemKey;
if (typeof row[k] !== "undefined" && row[k] !== null) {
return String(row[k]);
const value = getObjectProperty(row, k);
if (value !== undefined && value !== null) {
return String(value);
}
return JSON.stringify(row);
},
+2 -2
View File
@@ -155,7 +155,7 @@ export default {
return getters.isLoggedIn() && !getters.routePath().startsWith("/share");
},
toggleClick() {
mutations.updateCurrentUser({ singleClick: !state.user.singleClick });
void mutations.updateCurrentUser({ singleClick: !state.user.singleClick });
},
toggleDarkMode() {
mutations.toggleDarkMode();
@@ -165,7 +165,7 @@ export default {
if (!state.showSidebar && state.user.stickySidebar) {
mutations.toggleSidebar();
}
mutations.updateCurrentUser({ stickySidebar: !state.user.stickySidebar });
void mutations.updateCurrentUser({ stickySidebar: !state.user.stickySidebar });
},
navigateTo(path,hash) {
mutations.setPreviousHistoryItem({
+4 -3
View File
@@ -193,6 +193,7 @@ import { state, getters, mutations } from "@/store";
import ProgressBar from "@/components/ProgressBar.vue";
import { goToItem } from "@/utils/url";
import { getIconClass } from "@/utils/material-symbols";
import { getObjectProperty } from '@/utils/object.js';
import IndexInfo from "@/components/files/IndexInfo.vue";
import { globalVars } from "@/utils/constants";
import { resourcesApi } from "@/api";
@@ -233,7 +234,7 @@ export default {
},
isShare: () => getters.isShare(),
hasLinks() {
return this.sidebarLinksToDisplay?.length > 0;
return this.sidebarLinksToDisplay.length > 0;
},
user: () => (state.user || {username: 'anonymous'}),
sourceInfo() {
@@ -266,7 +267,7 @@ export default {
},
sidebarLinksToDisplay() {
// If viewing a share, use share's links
if (getters.isShare() && state.shareInfo?.sidebarLinks && state.shareInfo.sidebarLinks.length > 0) {
if (getters.isShare() && state.shareInfo?.sidebarLinks?.length > 0) {
return state.shareInfo.sidebarLinks;
}
// If user has custom links, use those
@@ -314,7 +315,7 @@ export default {
},
dropdownSourceItems() {
return this.sourceNames.map(rawName => {
const customLink = this.sourceLinkMap[rawName];
const customLink = getObjectProperty(this.sourceLinkMap, rawName);
return {
rawName,
displayName: customLink ? customLink.name : rawName,
+3 -2
View File
@@ -19,6 +19,7 @@
<script>
import { state, getters, mutations } from "@/store";
import { getObjectProperty } from '@/utils/object.js';
import { settings } from "@/utils/constants";
import { router } from "@/router";
@@ -40,14 +41,14 @@ export default {
shouldShow(setting) {
const perm = setting?.permissions || {};
// Check if all keys in setting.perm exist in state.user.perm and have truthy values
return Object.keys(perm).every((key) => state.user.permissions[key]);
return Object.keys(perm).every((key) => getObjectProperty(state.user.permissions, key));
},
active: (view) => state.activeSettingsView === view,
setView(view) {
mutations.closeHovers();
mutations.closeTopPrompt();
if (state.route.path !== "/settings") {
router.push({ path: "/settings", hash: `#${view}` }, () => {});
void router.push({ path: "/settings", hash: `#${view}` }, () => {});
} else {
mutations.setActiveSettingsView(view);
}
+18 -2
View File
@@ -8,6 +8,10 @@
<a :href="releaseUrl">{{ $t("sidebar.updateIsAvailable") }}</a>
<i @click="setSeenUpdate" aria-label="close-banner" class="material-symbols">close</i>
</div>
<div v-if="showPwaInstall" class="button release-banner">
<a href="#" @click.prevent="installPwa">{{ $t("pwa.install") }}</a>
<i @click="dismissPwaInstall" aria-label="close-banner" class="material-symbols">close</i>
</div>
<SidebarSettings v-if="isSettings"></SidebarSettings>
<SidebarGeneral v-if="!isSettings"></SidebarGeneral>
<div class="buffer"></div>
@@ -38,6 +42,7 @@
<script>
import { globalVars } from "@/utils/constants";
import { getters, mutations, state } from "@/store"; // Import your custom store
import { installAvailable, promptInstall } from "@/utils/pwaInstall";
import SidebarGeneral from "./General.vue";
import SidebarSettings from "./Settings.vue";
@@ -52,6 +57,7 @@ export default {
resizeStartX: 0,
resizeStartWidth: 0,
previousSidebarSize: null, // Remember the previous width when switching from desktop to mobile.
pwaInstallDismissed: sessionStorage.getItem("pwaInstallDismissed") === "true",
};
},
mounted() {
@@ -64,10 +70,10 @@ export default {
const sidebarVisible = getters.isSidebarVisible();
const isSticky = getters.isStickySidebar();
if (!sidebarVisible) {
mutations.updateCurrentUser({ stickySidebar: true });
void mutations.updateCurrentUser({ stickySidebar: true });
mutations.toggleSidebar();
} else if (sidebarVisible && isSticky) {
mutations.updateCurrentUser({ stickySidebar: false });
void mutations.updateCurrentUser({ stickySidebar: false });
} else {
mutations.toggleSidebar();
}
@@ -114,6 +120,9 @@ export default {
!state.user.disableUpdateNotifications
);
},
showPwaInstall() {
return installAvailable.value && !this.pwaInstallDismissed && !this.isSettings;
},
},
methods: {
getBaseFontSize() {
@@ -168,6 +177,13 @@ export default {
setSeenUpdate() {
mutations.setSeenUpdate(globalVars.updateAvailable);
},
async installPwa() {
await promptInstall();
},
dismissPwaInstall() {
this.pwaInstallDismissed = true;
sessionStorage.setItem("pwaInstallDismissed", "true");
},
},
};
</script>
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "انتقل إلى العنصر",
"copyPath": "مسار النسخ",
"pinItem": "تثبيت",
"unpinItem": "إلغاء التثبيت"
"unpinItem": "إلغاء التثبيت",
"sendToApp": "إرسال إلى التطبيق"
},
"errors": {
"forbidden": "You don't have permissions to access this.",
@@ -245,7 +246,9 @@
"helpText2": "مطابقة السلسلة الفرعية على أسماء الملفات أو المجلدات فقط (وليس المسارات الكاملة). حالة الأحرف: مطابقة تامة للمطابقة الحساسة لحالة الأحرف\n\n لـ OR بين المصطلحات استخدم حرف الأنبوب؛ علامات اقتباس مزدوجة لعبارة واحدة\n\nتمكين البحث بحرف بدل لمزيد من الخيارات المتقدمة.",
"useWildcardSearch": "البحث بحرف بدل (حساس لحالة الأحرف)",
"useWildcardSearchDescription": "خيارات أحرف البدل المدعومة:\n\n- تطابق أي تسلسل: *\n\n- مطابقة حرف واحد: ؟\n\n- فئة الأحرف: [a-z]، [0-9]\n\n- النفي: [^0-9]\n\n- حرفيًا * أو ؟ أو [ يجب أن تكون ملفوفة بين قوسين، على سبيل المثال [*]، [?]، [?]، []\n\n- جميع النتائج حساسة لحالة الأحرف",
"openInAdvancedSearch": "فتح في البحث المتقدم"
"openInAdvancedSearch": "فتح في البحث المتقدم",
"caseExact": "مطابقة تراعي تمييز الأحرف الكبيرة والصغيرة",
"caseExactDescription": "قم بمطابقة أسماء الملفات والمجلدات باستخدام الأحرف الكبيرة والصغيرة بالضبط، بدلاً من تجاهل الفروق بين الأحرف الكبيرة والصغيرة."
},
"settings": {
"avoidChanges": "(أتركه فارغاً إن لم ترد تغييره)",
@@ -572,7 +575,9 @@
"no": "لا يوجد{suffix}",
"to": "إلى{suffix}",
"and": "و{suffix}",
"or": "أو{suffix}"
"or": "أو{suffix}",
"camera": "الكاميرا{suffix}",
"video": "فيديو{suffix}"
},
"access": {
"rules": "قواعد الوصول",
@@ -778,7 +783,6 @@
"scopeAria": "قصر البحث على مجلد",
"noTerms": "أدخل مصطلح بحث واحد على الأقل.",
"termTooShort": "يجب ألا يقل حجم كل مصطلح بحث عن {minSearchLength} حرف على الأقل",
"caseExact": "تطابق حساس لحالة الأحرف",
"resultsTitle": "نتائج البحث",
"invalidSources": "قم بتشغيل مصدر واحد على الأقل.",
"noResults": "لا توجد ملفات أو مجلدات مطابقة."
@@ -807,7 +811,18 @@
"description": "عرض جميع التنبيهات من جلستك الحالية",
"empty": "لا توجد إخطارات حتى الآن",
"clearAll": "مسح الكل",
"total": "{count} الإخطارات"
"appEnabled": "إشعارات التطبيقات",
"appEnabledDescription": "عرض إشعارات المتصفح عند انتهاء عمليات الملفات أثناء وجود علامة التبويب هذه في الخلفية. يتطلب إذنًا من المتصفح. يتم تخزينها في هذا المتصفح فقط.",
"appUnsupported": "لا يدعم هذا المتصفح إشعارات التطبيقات.",
"permissionGranted": "مسموح",
"permissionDeniedHelp": "تم حظر الإشعارات. قم بتمكينها في إعدادات متصفحك لهذا الموقع.",
"uploadTitle": "اكتمل التحميل",
"downloadTitle": "تنزيل الملف بالكامل",
"moveTitle": "اكتملت العملية",
"copyTitle": "اكتملت النسخ",
"uploadFailedTitle": "فشل التحميل",
"downloadFailedTitle": "فشل التنزيل",
"operationFailedTitle": "فشلت العملية"
},
"events": {
"serverShutdown": "تم إيقاف تشغيل الخادم.",
@@ -860,5 +875,8 @@
"documentDownload": "تنزيل المستند - مكتمل",
"editorSuccess": "تهيئة المحرر - نجاح! اكتملت جميع الخطوات.",
"setupError": "خطأ في الإعداد"
},
"pwa": {
"install": "تثبيت التطبيق"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Přejít na položku",
"copyPath": "Kopírovat cestu",
"pinItem": "Připnout",
"unpinItem": "Odepnout"
"unpinItem": "Odepnout",
"sendToApp": "Odeslat do aplikace"
},
"errors": {
"forbidden": "Nemáte oprávnění k přístupu.",
@@ -315,7 +316,9 @@
"helpText2": "Shoda podřetězce pouze s názvy souborů nebo složek (ne s úplnými cestami). case:exact pro porovnávání s rozlišováním malých a velkých písmen.\n\n pro OR mezi výrazy použijte znak pipe; dvojité uvozovky pro jednu frázi.\n\nPovolte vyhledávání se zástupnými znaky pro pokročilejší možnosti.",
"useWildcardSearch": "Vyhledávání se zástupnými znaky (rozlišování velkých a malých písmen)",
"useWildcardSearchDescription": "Podporované možnosti zástupných znaků:\n\n- Shodují se s libovolnou sekvencí: *\n\n- Shoda s jedním znakem: ?\n\n- Třída znaků: [a-z], [0-9]\n\n- Negace: [^0-9]\n\n- Doslovné *, ? nebo [ je třeba zabalit do závorek, např. [*], [?], [[].\n\n- U všech výsledků se rozlišují velká a malá písmena",
"openInAdvancedSearch": "Otevřít v rozšířeném vyhledávání"
"openInAdvancedSearch": "Otevřít v rozšířeném vyhledávání",
"caseExact": "Porovnání s rozlišováním velkých a malých písmen",
"caseExactDescription": "Porovnávejte názvy souborů a složek s ohledem na přesné rozlišování velkých a malých písmen, místo aby se velká a malá písmena ignorovala."
},
"profileSettings": {
"hideFileExt": "Skrýt určité soubory (podle přípony)",
@@ -666,7 +669,9 @@
"no": "Ne{suffix}",
"to": "až{suffix}",
"and": "a{suffix}",
"or": "nebo{suffix}"
"or": "nebo{suffix}",
"camera": "Fotoaparát{suffix}",
"video": "Video{suffix}"
},
"access": {
"rules": "Pravidla přístupu",
@@ -778,7 +783,6 @@
"scopeAria": "Omezení vyhledávání na složku",
"noTerms": "Zadejte alespoň jeden hledaný výraz.",
"termTooShort": "Každý hledaný výraz musí mít alespoň {minSearchLength} znaků.",
"caseExact": "Shoda rozlišující malá a velká písmena",
"resultsTitle": "Výsledky vyhledávání",
"invalidSources": "Zapněte alespoň jeden zdroj.",
"noResults": "Žádné odpovídající soubory nebo složky."
@@ -807,7 +811,18 @@
"description": "Zobrazení všech oznámení z aktuální relace",
"empty": "Zatím žádná oznámení",
"clearAll": "Vymazat vše",
"total": "{count} oznámení"
"appEnabled": "Oznámení v aplikaci",
"appEnabledDescription": "Zobrazovat oznámení prohlížeče po dokončení operací se soubory, když je tato karta na pozadí. Vyžaduje oprávnění prohlížeče. Uloženo pouze v tomto prohlížeči.",
"appUnsupported": "Tento prohlížeč nepodporuje oznámení z aplikací.",
"permissionGranted": "povoleno",
"permissionDeniedHelp": "Oznámení jsou zablokována. Povolte je v nastavení prohlížeče pro tento web.",
"uploadTitle": "Nahrávání dokončeno",
"downloadTitle": "Stažení dokončeno",
"moveTitle": "Přesun dokončen",
"copyTitle": "Kopírování dokončeno",
"uploadFailedTitle": "Nahrávání se nezdařilo",
"downloadFailedTitle": "Stahování se nezdařilo",
"operationFailedTitle": "Operace se nezdařila"
},
"events": {
"serverShutdown": "Server byl vypnut.",
@@ -860,5 +875,8 @@
"documentDownload": "Dokument ke stažení - kompletní",
"editorSuccess": "Inicializace editoru - úspěch! Všechny kroky byly dokončeny.",
"setupError": "Chyba nastavení"
},
"pwa": {
"install": "Nainstalovat aplikaci"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Weiter zum Artikel",
"copyPath": "Pfad kopieren",
"pinItem": "Anheften",
"unpinItem": "Lösen"
"unpinItem": "Lösen",
"sendToApp": "An App senden"
},
"errors": {
"forbidden": "Sie haben keine Berechtigung dies abzurufen.",
@@ -245,7 +246,9 @@
"helpText2": "Teilstring-Übereinstimmung nur bei Datei- oder Ordnernamen (nicht bei vollständigen Pfaden). case:exact für Übereinstimmung unter Berücksichtigung der Groß-/Kleinschreibung\n\n für ODER zwischen Begriffen Pipe-Zeichen verwenden; doppelte Anführungszeichen für eine Phrase.\n\nAktivieren Sie die Platzhaltersuche für erweiterte Optionen.",
"useWildcardSearch": "Wildcard-Suche (Groß- und Kleinschreibung beachten)",
"useWildcardSearchDescription": "Unterstützte Platzhalteroptionen:\n\n- Entspricht einer beliebigen Sequenz: *\n\n- Übereinstimmung mit einem Zeichen: ?\n\n- Zeichenklasse: [a-z], [0-9]\n\n- Negation: [^0-9]\n\n- Buchstäbliche *, ? oder [ müssen in Klammern eingeschlossen werden, z. B. [*], [?], [[]\n\n- Bei allen Ergebnissen wird zwischen Groß- und Kleinschreibung unterschieden.",
"openInAdvancedSearch": "In der erweiterten Suche öffnen"
"openInAdvancedSearch": "In der erweiterten Suche öffnen",
"caseExact": "Groß-/Kleinschreibung beachten",
"caseExactDescription": "Datei- und Ordnernamen anhand der genauen Groß- und Kleinschreibung abgleichen, anstatt Groß- und Kleinschreibung zu ignorieren."
},
"settings": {
"avoidChanges": "(leer lassen, um Änderungen zu vermeiden)",
@@ -572,7 +575,9 @@
"no": "Nein{suffix}",
"to": "zu{suffix}",
"and": "und{suffix}",
"or": "oder{suffix}"
"or": "oder{suffix}",
"camera": "Kamera{suffix}",
"video": "Video{suffix}"
},
"access": {
"rules": "Zugangsregeln",
@@ -778,7 +783,6 @@
"scopeAria": "Suche auf einen Ordner beschränken",
"noTerms": "Geben Sie mindestens einen Suchbegriff ein.",
"termTooShort": "Jeder Suchbegriff muss mindestens {minSearchLength} Zeichen umfassen.",
"caseExact": "Abgleich unter Berücksichtigung der Groß-/Kleinschreibung",
"resultsTitle": "Suchergebnisse",
"invalidSources": "Schalten Sie mindestens eine Quelle ein.",
"noResults": "Keine passenden Dateien oder Ordner."
@@ -807,7 +811,18 @@
"description": "Alle Benachrichtigungen aus Ihrer aktuellen Sitzung anzeigen",
"empty": "Noch keine Benachrichtigungen",
"clearAll": "Alle löschen",
"total": "{count} Benachrichtigungen"
"appEnabled": "App-Benachrichtigungen",
"appEnabledDescription": "Browser-Benachrichtigungen anzeigen, wenn Dateioperationen abgeschlossen sind, während sich diese Registerkarte im Hintergrund befindet. Erfordert eine Browser-Berechtigung. Wird nur in diesem Browser gespeichert.",
"appUnsupported": "App-Benachrichtigungen werden in diesem Browser nicht unterstützt.",
"permissionGranted": "zulässig",
"permissionDeniedHelp": "Benachrichtigungen sind deaktiviert. Bitte aktivieren Sie sie in den Browsereinstellungen für diese Website.",
"uploadTitle": "Upload abgeschlossen",
"downloadTitle": "Download abgeschlossen",
"moveTitle": "Verschieben abgeschlossen",
"copyTitle": "Kopieren abgeschlossen",
"uploadFailedTitle": "Der Upload ist fehlgeschlagen",
"downloadFailedTitle": "Der Download ist fehlgeschlagen",
"operationFailedTitle": "Vorgang fehlgeschlagen"
},
"events": {
"serverShutdown": "Der Server wurde abgeschaltet.",
@@ -860,5 +875,8 @@
"documentDownload": "Dokument-Download - Vollständig",
"editorSuccess": "Editor-Initialisierung - Erfolg! Alle Schritte abgeschlossen.",
"setupError": "Setup-Fehler"
},
"pwa": {
"install": "App installieren"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Πηγαίνετε στο στοιχείο",
"copyPath": "Αντιγραφή διαδρομής",
"pinItem": "Καρφίτσωμα",
"unpinItem": "Ξεκαρφίτσωμα"
"unpinItem": "Ξεκαρφίτσωμα",
"sendToApp": "Αποστολή στην εφαρμογή"
},
"errors": {
"forbidden": "Δεν έχετε άδεια πρόσβασης σε αυτό.",
@@ -245,7 +246,9 @@
"helpText2": "Αντιστοίχιση υποσυνόλων μόνο σε ονόματα αρχείων ή φακέλων (όχι σε πλήρεις διαδρομές). case:exact για αντιστοίχιση ευαίσθητη στην πεζότητα\n\n για OR μεταξύ όρων χρησιμοποιήστε τον χαρακτήρα pipe- διπλά εισαγωγικά για μία φράση.\n\nΕνεργοποιήστε την αναζήτηση με μπαλαντέρ για πιο προηγμένες επιλογές.",
"useWildcardSearch": "Αναζήτηση με μπαλαντέρ (ευαίσθητη στην πεζότητα)",
"useWildcardSearchDescription": "Υποστηριζόμενες επιλογές μπαλαντέρ:\n\n- Ταιριάζει με οποιαδήποτε ακολουθία: *\n\n- Ταιριάζει με έναν χαρακτήρα: ?\n\n- Κατηγορία χαρακτήρων: [a-z], [0-9]\n\n- Άρνηση: [^0-9]\n\n- Τα κυριολεκτικά *, ?, ή [ πρέπει να περιτυλίγονται σε αγκύλες, π.χ. [*], [?], [[]\n\n- Όλα τα αποτελέσματα είναι ευαίσθητα στην πεζότητα",
"openInAdvancedSearch": "Άνοιγμα σε Σύνθετη Αναζήτηση"
"openInAdvancedSearch": "Άνοιγμα σε Σύνθετη Αναζήτηση",
"caseExact": "Αντιστοίχιση με διάκριση κεφαλαίων-μικρών",
"caseExactDescription": "Να γίνεται αντιστοίχιση ονομάτων αρχείων και φακέλων με βάση την ακριβή χρήση κεφαλαίων και μικρών γραμμάτων, αντί να αγνοούνται οι διαφορές μεταξύ κεφαλαίων και μικρών γραμμάτων."
},
"settings": {
"avoidChanges": "(αφήστε το κενό για αποφυγή αλλαγών)",
@@ -572,7 +575,9 @@
"no": "Όχι{suffix}",
"to": "προς{suffix}",
"and": "και{suffix}",
"or": "ή{suffix}"
"or": "ή{suffix}",
"camera": "Κάμερα{suffix}",
"video": "Βίντεο{suffix}"
},
"access": {
"rules": "Κανόνες πρόσβασης",
@@ -778,7 +783,6 @@
"scopeAria": "Περιορισμός της αναζήτησης σε έναν φάκελο",
"noTerms": "Εισάγετε τουλάχιστον έναν όρο αναζήτησης.",
"termTooShort": "Κάθε όρος αναζήτησης πρέπει να αποτελείται από τουλάχιστον {minSearchLength} χαρακτήρες.",
"caseExact": "Αντιστοίχιση με ευαισθησία στην πεζότητα",
"resultsTitle": "Αποτελέσματα αναζήτησης",
"invalidSources": "Ενεργοποιήστε τουλάχιστον μία πηγή.",
"noResults": "Δεν υπάρχουν ταιριαστά αρχεία ή φάκελοι."
@@ -807,7 +811,18 @@
"description": "Προβολή όλων των ειδοποιήσεων από την τρέχουσα συνεδρία σας",
"empty": "Δεν υπάρχουν ακόμη ειδοποιήσεις",
"clearAll": "Εκκαθάριση όλων",
"total": "{count} ειδοποιήσεις"
"appEnabled": "Ειδοποιήσεις εφαρμογών",
"appEnabledDescription": "Εμφάνιση ειδοποιήσεων του προγράμματος περιήγησης όταν ολοκληρώνονται οι λειτουργίες αρχείων ενώ αυτή η καρτέλα βρίσκεται στο παρασκήνιο. Απαιτείται άδεια από το πρόγραμμα περιήγησης. Αποθηκεύεται μόνο σε αυτό το πρόγραμμα περιήγησης.",
"appUnsupported": "Οι ειδοποιήσεις εφαρμογών δεν υποστηρίζονται σε αυτόν τον περιηγητή.",
"permissionGranted": "επιτρέπεται",
"permissionDeniedHelp": "Οι ειδοποιήσεις έχουν αποκλειστεί. Ενεργοποιήστε τις στις ρυθμίσεις του προγράμματος περιήγησής σας για αυτόν τον ιστότοπο.",
"uploadTitle": "Η μεταφόρτωση ολοκληρώθηκε",
"downloadTitle": "Η λήψη ολοκληρώθηκε",
"moveTitle": "Η μετακίνηση ολοκληρώθηκε",
"copyTitle": "Η αντιγραφή ολοκληρώθηκε",
"uploadFailedTitle": "Η μεταφόρτωση απέτυχε",
"downloadFailedTitle": "Η λήψη απέτυχε",
"operationFailedTitle": "Η λειτουργία απέτυχε"
},
"events": {
"serverShutdown": "Ο διακομιστής τερματίστηκε.",
@@ -860,5 +875,8 @@
"documentDownload": "Λήψη εγγράφων - Πλήρης",
"editorSuccess": "Αρχικοποίηση επεξεργαστή - Επιτυχία! Όλα τα βήματα ολοκληρώθηκαν.",
"setupError": "Σφάλμα ρύθμισης"
},
"pwa": {
"install": "Εγκατάσταση εφαρμογής"
}
}
+22 -4
View File
@@ -87,6 +87,8 @@
"download": "Download{suffix}",
"file": "File{suffix}",
"folder": "Folder{suffix}",
"camera": "Camera{suffix}",
"video": "Video{suffix}",
"info": "Info{suffix}",
"move": "Move{suffix}",
"new": "New{suffix}",
@@ -148,7 +150,8 @@
"goToItem": "Go to item",
"goToSource": "Go to Source Location",
"pinItem": "Pin",
"unpinItem": "Unpin"
"unpinItem": "Unpin",
"sendToApp": "Send to app"
},
"errors": {
"forbidden": "You don't have permissions to access this.",
@@ -432,8 +435,10 @@
"showPreviewImagesDescription": "Display thumbnail previews for search results when available, and show larger icons for all results.",
"useWildcardSearch": "Wildcard search",
"useWildcardSearchDescription": "Supported wildcard options:\n\n• Match any sequence: *\n\n• Match one character: ?\n\n• Character class: [a-z], [0-9]\n\n• Negation: [^0-9]\n\n• Literal *, ?, or [ need to be wrapped in brackets, e.g. [*], [?], [[]",
"caseExact": "Case-sensitive match",
"caseExactDescription": "Match file and folder names using exact letter casing instead of ignoring upper and lower case.",
"helpText1": "Search occurs on each character you type ({minSearchLength} character minimum).",
"helpText2": "Substring match on file or folder names only (not full paths). case:exact for case-sensitive matching\n\n for OR between terms use pipe character; double quotes for one phrase.\n\nEnable wildcard search for more advanced options.",
"helpText2": "Case-insensitive substring match on file or folder names only (not full paths). Space-separated words match across common delimiters like periods, underscores, and hyphens (e.g. \"Blade Runner\" finds Blade.Runner.mkv). Use case:exact for case-sensitive matching.\n\nFor OR between terms use the pipe character; use double quotes for an exact phrase with spaces.\n\nEnable wildcard search for advanced glob patterns (*, ?, [a-z]).",
"openInAdvancedSearch": "Open in Advanced Search"
},
"profileSettings": {
@@ -766,7 +771,6 @@
"scopeAria": "Limit search to a folder",
"noTerms": "Enter at least one search term.",
"termTooShort": "Each search term must be at least {minSearchLength} characters",
"caseExact": "Case-sensitive match",
"resultsTitle": "Search results",
"invalidSources": "Turn on at least one source.",
"noResults": "No matching files or folders."
@@ -798,7 +802,18 @@
"description": "View all notifications from your current session",
"empty": "No notifications yet",
"clearAll": "Clear All",
"total": "{count} notifications"
"appEnabled": "App notifications",
"appEnabledDescription": "Show browser notifications when file operations finish while this tab is in the background. Requires browser permission. Stored in this browser only.",
"appUnsupported": "App notifications are not supported in this browser.",
"permissionGranted": "allowed",
"permissionDeniedHelp": "Notifications are blocked. Enable them in your browser settings for this site.",
"uploadTitle": "Upload complete",
"downloadTitle": "Download complete",
"moveTitle": "Move complete",
"copyTitle": "Copy complete",
"uploadFailedTitle": "Upload failed",
"downloadFailedTitle": "Download failed",
"operationFailedTitle": "Operation failed"
},
"duplicateFinder": {
"minSize": "Minimum Size (MB){suffix}",
@@ -860,5 +875,8 @@
"documentDownload": "Document Download - Complete",
"editorSuccess": "Editor Initialization - Success! All steps completed.",
"setupError": "Setup Error"
},
"pwa": {
"install": "Install app"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Abrir",
"copyPath": "Copiar ruta",
"pinItem": "Fijar",
"unpinItem": "Desfijar"
"unpinItem": "Desfijar",
"sendToApp": "Enviar a la aplicación"
},
"errors": {
"forbidden": "No tienes permisos para acceder.",
@@ -245,7 +246,9 @@
"helpText2": "Coincidencia de subcadenas sólo en nombres de archivos o carpetas (no en rutas completas). case:exact para coincidencia sensible a mayúsculas y minúsculas.\n\n para OR entre términos utilice el carácter pipa; comillas dobles para una frase.\n\nHabilite la búsqueda con comodines para opciones más avanzadas.",
"useWildcardSearch": "Búsqueda con comodín (distingue mayúsculas de minúsculas)",
"useWildcardSearchDescription": "Opciones de comodín admitidas:\n\n- Coinciden con cualquier secuencia: *\n\n- Coincidir con un carácter: ?\n\n- Clase de caracteres: [a-z], [0-9]\n\n- Negación: [^0-9]\n\n- Los caracteres *, ? o [ deben ir entre paréntesis, por ejemplo [*], [?], [[].\n\n- Todos los resultados distinguen entre mayúsculas y minúsculas",
"openInAdvancedSearch": "Abrir en búsqueda avanzada"
"openInAdvancedSearch": "Abrir en búsqueda avanzada",
"caseExact": "Coincidencia que distingue entre mayúsculas y minúsculas",
"caseExactDescription": "Compara los nombres de archivos y carpetas utilizando el uso exacto de mayúsculas y minúsculas, en lugar de ignorar la distinción entre mayúsculas y minúsculas."
},
"settings": {
"scopes": "Fuentes a las que el usuario tendrá acceso:",
@@ -572,7 +575,9 @@
"no": "No{suffix}",
"to": "a{suffix}",
"and": "y{suffix}",
"or": "o{suffix}"
"or": "o{suffix}",
"camera": "Cámara{suffix}",
"video": "Vídeo{suffix}"
},
"access": {
"rules": "Reglas de acceso",
@@ -778,7 +783,6 @@
"scopeAria": "Limitar la búsqueda a una carpeta",
"noTerms": "Introduzca al menos un término de búsqueda.",
"termTooShort": "Cada término de búsqueda debe tener al menos {minSearchLength} caracteres",
"caseExact": "Coincidencia entre mayúsculas y minúsculas",
"resultsTitle": "Resultados de la búsqueda",
"invalidSources": "Enciende al menos una fuente.",
"noResults": "No hay archivos o carpetas coincidentes."
@@ -807,7 +811,18 @@
"description": "Aquí verás todas las notificaciones de tu sesión actual",
"empty": "Aún no tienes notificaciones",
"clearAll": "Borrar todo",
"total": "{count} notificaciones"
"appEnabled": "Notificaciones de la aplicación",
"appEnabledDescription": "Mostrar notificaciones del navegador cuando finalicen las operaciones con archivos mientras esta pestaña está en segundo plano. Requiere permiso del navegador. Se guarda únicamente en este navegador.",
"appUnsupported": "Este navegador no admite notificaciones de aplicaciones.",
"permissionGranted": "permitido",
"permissionDeniedHelp": "Las notificaciones están bloqueadas. Actívalas en la configuración de tu navegador para este sitio web.",
"uploadTitle": "Carga completada",
"downloadTitle": "Descarga completada",
"moveTitle": "Movimiento completado",
"copyTitle": "Copia completada",
"uploadFailedTitle": "Error al subir el archivo",
"downloadFailedTitle": "Error al descargar",
"operationFailedTitle": "La operación ha fallado"
},
"events": {
"serverShutdown": "El servidor fúe apagado",
@@ -860,5 +875,8 @@
"documentDownload": "Descarga de documentos - Completa",
"editorSuccess": "Inicialización del editor - ¡Éxito! Todos los pasos se completaron.",
"setupError": "Error de configuración"
},
"pwa": {
"install": "Instalar la aplicación"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Aller à l'article",
"copyPath": "Chemin de la copie",
"pinItem": "Épingler",
"unpinItem": "Détacher"
"unpinItem": "Détacher",
"sendToApp": "Envoyer vers l'application"
},
"errors": {
"forbidden": "Vous n'avez pas la permission d'accéder à cela.",
@@ -245,7 +246,9 @@
"helpText2": "Correspondance de sous-chaînes sur les noms de fichiers ou de dossiers uniquement (pas sur les chemins d'accès complets). case:exact pour une correspondance sensible à la casse.\n\n pour le OU entre les termes, utiliser le caractère pipe ; les guillemets doubles pour une seule phrase.\n\nActivez la recherche par caractères génériques pour obtenir des options plus avancées.",
"useWildcardSearch": "Recherche par caractères génériques (sensible à la casse)",
"useWildcardSearchDescription": "Options de caractères génériques prises en charge :\n\n- Correspond à n'importe quelle séquence : *\n\n- Correspondre à un seul caractère : ?\n\n- Classe de caractères : [a-z], [0-9]\n\n- Négation : [^0-9]\n\n- Les valeurs littérales *, ? ou [ doivent être mises entre parenthèses, par exemple [*], [ ?], [[]].\n\n- Tous les résultats sont sensibles à la casse",
"openInAdvancedSearch": "Ouvrir en recherche avancée"
"openInAdvancedSearch": "Ouvrir en recherche avancée",
"caseExact": "Correspondance sensible à la casse",
"caseExactDescription": "Comparer les noms de fichiers et de dossiers en tenant compte de la casse exacte, plutôt qu'en ignorant la distinction entre majuscules et minuscules."
},
"settings": {
"scopes": "Définir les sources autorisées et la portée de l'utilisateur au sein de celles-ci",
@@ -572,7 +575,9 @@
"no": "Non{suffix}",
"to": "à{suffix}",
"and": "et{suffix}",
"or": "ou{suffix}"
"or": "ou{suffix}",
"camera": "Appareil photo{suffix}",
"video": "Vidéo{suffix}"
},
"access": {
"rules": "Règles d'accès",
@@ -778,7 +783,6 @@
"scopeAria": "Limiter la recherche à un dossier",
"noTerms": "Saisissez au moins un terme de recherche.",
"termTooShort": "Chaque terme de recherche doit comporter au moins {minSearchLength} caractères.",
"caseExact": "Correspondance sensible à la casse",
"resultsTitle": "Résultats de la recherche",
"invalidSources": "Allumez au moins une source.",
"noResults": "Aucun fichier ou dossier correspondant."
@@ -807,7 +811,18 @@
"description": "Afficher toutes les notifications de la session en cours",
"empty": "Aucune notification pour l'instant",
"clearAll": "Tout effacer",
"total": "{count} notifications"
"appEnabled": "Notifications de l'application",
"appEnabledDescription": "Afficher les notifications du navigateur lorsque les opérations sur les fichiers sont terminées alors que cet onglet est en arrière-plan. Nécessite l'autorisation du navigateur. Enregistré uniquement dans ce navigateur.",
"appUnsupported": "Les notifications de l'application ne sont pas prises en charge par ce navigateur.",
"permissionGranted": "autorisé",
"permissionDeniedHelp": "Les notifications sont désactivées. Activez-les dans les paramètres de votre navigateur pour ce site.",
"uploadTitle": "Téléchargement terminé",
"downloadTitle": "Téléchargement terminé",
"moveTitle": "Déménagement terminé",
"copyTitle": "Copie terminée",
"uploadFailedTitle": "Échec du téléchargement",
"downloadFailedTitle": "Échec du téléchargement",
"operationFailedTitle": "Échec de l'opération"
},
"events": {
"serverShutdown": "Le serveur a été arrêté.",
@@ -860,5 +875,8 @@
"documentDownload": "Téléchargement du document - Complet",
"editorSuccess": "Initialisation de l'éditeur - Succès ! Toutes les étapes sont terminées.",
"setupError": "Erreur de configuration"
},
"pwa": {
"install": "Installer l'application"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "עבור לפריט",
"copyPath": "העתק נתיב",
"pinItem": "נעץ",
"unpinItem": "הסר נעיצה"
"unpinItem": "הסר נעיצה",
"sendToApp": "שלח לאפליקציה"
},
"errors": {
"forbidden": "אין לך הרשאות גישה",
@@ -245,7 +246,9 @@
"helpText2": "התאמת תת-מחרוזת רק לשמות קבצים או תיקיות (לא לנתיבים מלאים). case:exact להתאמה רגישה לאותיות רישיות וקטנות; השתמש בסימן הצינור (|) לחיבור בין מונחים באמצעות \"או\"; השתמש במרכאות כפולות לביטוי אחד. הפעל חיפוש עם תווים כלליים לקבלת אפשרויות מתקדמות יותר.",
"useWildcardSearch": "חיפוש עם תווים כלליים (רגיש לאותיות רישיות)",
"useWildcardSearchDescription": "אפשרויות תווים כלליים נתמכות: • התאמה לכל רצף: * • התאמה לתו בודד: ? • קבוצת תווים: [a-z], [0-9] • שלילה: [^0-9] • יש להקיף את התווים *, ? או [ בסוגריים, לדוגמה: [*], [?], [[] • כל התוצאות רגישות לאותיות גדולות וקטנות",
"openInAdvancedSearch": "פתח בחיפוש מתקדם"
"openInAdvancedSearch": "פתח בחיפוש מתקדם",
"caseExact": "התאמה רגישה לאותיות רישיות",
"caseExactDescription": "התאם שמות קבצים ותיקיות על פי רישיות אותיות מדויקות, במקום להתעלם מההבדל בין אותיות גדולות לקטנות."
},
"settings": {
"avoidChanges": "(השאר ריק כדי למנוע שינויים)",
@@ -572,7 +575,9 @@
"no": "לא{suffix}",
"to": "אל{suffix}",
"and": "ו-{suffix}",
"or": "או{suffix}"
"or": "או{suffix}",
"camera": "מצלמה{suffix}",
"video": "סרטון{suffix}"
},
"access": {
"rules": "כללי גישה",
@@ -778,7 +783,6 @@
"scopeAria": "הגבל את החיפוש לתיקיה",
"noTerms": "הזן לפחות מונח חיפוש אחד.",
"termTooShort": "כל מונח חיפוש חייב להכיל לפחות {minSearchLength} תווים",
"caseExact": "התאמה רגישה לאותיות רישיות",
"resultsTitle": "תוצאות החיפוש",
"invalidSources": "הפעל לפחות מקור אחד.",
"noResults": "אין קבצים או תיקיות תואמים."
@@ -807,7 +811,18 @@
"description": "הצג את כל ההודעות מהפעלה הנוכחית שלך",
"empty": "אין עדיין הודעות",
"clearAll": "נקה הכל",
"total": "{count} הודעות"
"appEnabled": "התראות באפליקציה",
"appEnabledDescription": "הצג התראות דפדפן עם סיום פעולות הקובץ כאשר הכרטיסייה נמצאת ברקע. דורש הרשאת דפדפן. נשמר בדפדפן זה בלבד.",
"appUnsupported": "הודעות האפליקציה אינן נתמכות בדפדפן זה.",
"permissionGranted": "מותר",
"permissionDeniedHelp": "ההודעות חסומות. הפעל אותן בהגדרות הדפדפן עבור אתר זה.",
"uploadTitle": "ההעלאה הושלמה",
"downloadTitle": "ההורדה הושלמה",
"moveTitle": "הפעולה הושלמה",
"copyTitle": "ההעתקה הושלמה",
"uploadFailedTitle": "ההעלאה נכשלה",
"downloadFailedTitle": "ההורדה נכשלה",
"operationFailedTitle": "הפעולה נכשלה"
},
"events": {
"serverShutdown": "השרת כובה.",
@@ -860,5 +875,8 @@
"documentDownload": "הורדת מסמך - מלא",
"editorSuccess": "אתחול העורך - הצלחה! כל השלבים הושלמו.",
"setupError": "שגיאת התקנה"
},
"pwa": {
"install": "התקן את האפליקציה"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Tovább a tételhez",
"copyPath": "Másolás útvonal",
"pinItem": "Rögzítés",
"unpinItem": "Rögzítés feloldása"
"unpinItem": "Rögzítés feloldása",
"sendToApp": "Küldés az alkalmazásba"
},
"errors": {
"forbidden": "Nincs jogosultsága a hozzáféréshez.",
@@ -245,7 +246,9 @@
"helpText2": "Csak fájl- vagy mappanevek (nem teljes elérési utak) részlánc-illesztése. case:exact a nagy- és kisbetű-érzékeny illesztéshez.\n\n a kifejezések közötti VAGY-hoz használja a pipe karaktert; dupla idézőjelek egy kifejezéshez.\n\nEngedélyezze a vadkártyás keresést a fejlettebb lehetőségekhez.",
"useWildcardSearch": "Wildcard keresés (nagy- és kisbetű-érzékeny)",
"useWildcardSearchDescription": "Támogatott helyettesítő opciók:\n\n- Bármilyen sorozatra illeszkedik: *\n\n- Egy karakterrel való egyezés: ?\n\n- Karakterosztály: [a-z], [0-9]\n\n- Negáció: [^0-9]\n\n- A szó szerinti *, ? vagy [ karaktereket zárójelbe kell tenni, pl. [*], [?], [[]\n\n- Minden eredmény nagy- és kisbetű-érzékeny",
"openInAdvancedSearch": "Megnyitás a bővített keresőben"
"openInAdvancedSearch": "Megnyitás a bővített keresőben",
"caseExact": "Kis- és nagybetűk közötti különbséget figyelembe vevő keresés",
"caseExactDescription": "A fájl- és mappanevek összehasonlításakor vegye figyelembe a betűk pontos írásmódját, ne pedig a kis- és nagybetűk közötti különbséget."
},
"settings": {
"avoidChanges": "(üresen hagyva nincs változás)",
@@ -572,7 +575,9 @@
"no": "Nem{suffix}",
"to": "hogy{suffix}",
"and": "és{suffix}",
"or": "vagy{suffix}"
"or": "vagy{suffix}",
"camera": "Fényképezőgép{suffix}",
"video": "Videó{suffix}"
},
"access": {
"rules": "Hozzáférési szabályok",
@@ -778,7 +783,6 @@
"scopeAria": "Keresés korlátozása egy mappára",
"noTerms": "Adjon meg legalább egy keresési kifejezést.",
"termTooShort": "Minden keresési kifejezésnek legalább {minSearchLength} karakterből kell állnia.",
"caseExact": "Nagy- és kisbetű-érzékeny egyezés",
"resultsTitle": "Keresési eredmények",
"invalidSources": "Kapcsoljon be legalább egy forrást.",
"noResults": "Nincsenek megfelelő fájlok vagy mappák."
@@ -807,7 +811,18 @@
"description": "Az aktuális munkamenet összes értesítésének megtekintése",
"empty": "Még nincsenek értesítések",
"clearAll": "Mindent törölni",
"total": "{count} értesítések"
"appEnabled": "Alkalmazás-értesítések",
"appEnabledDescription": "A böngésző értesítéseket jelenít meg, ha fájlműveletek fejeződnek be, miközben ez a lap háttérben van. Ehhez böngészői engedély szükséges. Csak ebben a böngészőben kerül tárolásra.",
"appUnsupported": "Ez a böngésző nem támogatja az alkalmazásértesítéseket.",
"permissionGranted": "megengedett",
"permissionDeniedHelp": "Az értesítések letiltva vannak. Kérjük, engedélyezze őket a böngésző beállításaiban ezen a webhelyen.",
"uploadTitle": "A feltöltés befejeződött",
"downloadTitle": "Letöltés befejezve",
"moveTitle": "A művelet befejeződött",
"copyTitle": "Másolás befejezve",
"uploadFailedTitle": "A feltöltés nem sikerült",
"downloadFailedTitle": "A letöltés nem sikerült",
"operationFailedTitle": "A művelet sikertelen"
},
"events": {
"serverShutdown": "A kiszolgálót leállították.",
@@ -860,5 +875,8 @@
"documentDownload": "Dokumentum letöltése - Teljes",
"editorSuccess": "Szerkesztő inicializálása - Sikeres! Minden lépés befejeződött.",
"setupError": "Beállítási hiba"
},
"pwa": {
"install": "Alkalmazás telepítése"
}
}
+1 -2
View File
@@ -1022,8 +1022,7 @@
"description": "__NOT_TRANSLATED__",
"empty": "__NOT_TRANSLATED__",
"clearAll": "__NOT_TRANSLATED__",
"clearConfirm": "__NOT_TRANSLATED__",
"total": "__NOT_TRANSLATED__"
"clearConfirm": "__NOT_TRANSLATED__"
},
"duplicateFinder": {
"title": "__NOT_TRANSLATED__",
+23 -5
View File
@@ -116,7 +116,9 @@
"no": "No{suffix}",
"to": "a{suffix}",
"and": "e{suffix}",
"or": "oppure{suffix}"
"or": "oppure{suffix}",
"camera": "Fotocamera{suffix}",
"video": "Video{suffix}"
},
"buttons": {
"copySuccess": "Copiato negli appunti!",
@@ -148,7 +150,8 @@
"goToSource": "Vai alla posizione sorgente",
"copyPath": "Percorso di copia",
"pinItem": "Appunta",
"unpinItem": "Rimuovi puntina"
"unpinItem": "Rimuovi puntina",
"sendToApp": "Invia all'app"
},
"errors": {
"forbidden": "Non hai i permessi per accedere a questa risorsa.",
@@ -434,7 +437,9 @@
"helpText2": "Corrispondenza di sottostringhe solo sui nomi di file o cartelle (non sui percorsi completi). case:exact per una corrispondenza sensibile alle maiuscole e alle minuscole.\n\n per OR tra termini usare il carattere pipe; doppi apici per una frase.\n\nAbilitare la ricerca con caratteri jolly per opzioni più avanzate.",
"useWildcardSearch": "Ricerca con caratteri jolly (sensibile alle maiuscole)",
"useWildcardSearchDescription": "Opzioni jolly supportate:\n\n- Corrisponde a qualsiasi sequenza: *\n\n- Corrisponde a un solo carattere: ?\n\n- Classe di caratteri: [a-z], [0-9]\n\n- Negazione: [^0-9]\n\n- I caratteri letterali *, ?, o [ devono essere racchiusi tra parentesi, ad esempio [*], [?], [[]\n\n- Tutti i risultati sono sensibili alle maiuscole e alle minuscole",
"openInAdvancedSearch": "Aprire la ricerca avanzata"
"openInAdvancedSearch": "Aprire la ricerca avanzata",
"caseExact": "Corrispondenza che distingue tra maiuscole e minuscole",
"caseExactDescription": "Abbinare i nomi di file e cartelle utilizzando la distinzione esatta tra maiuscole e minuscole, anziché ignorarla."
},
"profileSettings": {
"hideFileExt": "Nascondere determinati file (per estensione)",
@@ -787,7 +792,6 @@
"scopeAria": "Limitare la ricerca a una cartella",
"noTerms": "Inserite almeno un termine di ricerca.",
"termTooShort": "Ogni termine di ricerca deve essere composto da almeno {minSearchLength} caratteri.",
"caseExact": "Corrispondenza sensibile alle maiuscole e alle minuscole",
"resultsTitle": "Risultati della ricerca",
"invalidSources": "Accendere almeno una sorgente.",
"noResults": "Nessun file o cartella corrispondente."
@@ -798,7 +802,18 @@
"description": "Visualizza tutte le notifiche della sessione corrente",
"empty": "Ancora nessuna notifica",
"clearAll": "Pulisci tutto",
"total": "{count} notifiche"
"appEnabled": "Notifiche delle app",
"appEnabledDescription": "Mostra le notifiche del browser al termine delle operazioni sui file quando questa scheda è in secondo piano. Richiede l'autorizzazione del browser. Memorizzato solo in questo browser.",
"appUnsupported": "Le notifiche delle app non sono supportate in questo browser.",
"permissionGranted": "ammesso",
"permissionDeniedHelp": "Le notifiche sono bloccate. Abilitale nelle impostazioni del browser per questo sito.",
"uploadTitle": "Caricamento completato",
"downloadTitle": "Download completato",
"moveTitle": "Operazione completata",
"copyTitle": "Copia completata",
"uploadFailedTitle": "Caricamento non riuscito",
"downloadFailedTitle": "Il download non è andato a buon fine",
"operationFailedTitle": "Operazione non riuscita"
},
"duplicateFinder": {
"minSize": "Dimensione Minima (MB){suffix}",
@@ -860,5 +875,8 @@
"documentDownload": "Download Documento - Completato",
"editorSuccess": "Inizializzazione Editor - Successo! Tutti i passaggi completati.",
"setupError": "Errore Setup"
},
"pwa": {
"install": "Installa l'app"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "アイテムへ",
"copyPath": "コピーパス",
"pinItem": "ピン留め",
"unpinItem": "ピン留めを解除"
"unpinItem": "ピン留めを解除",
"sendToApp": "アプリに送信"
},
"errors": {
"forbidden": "このリソースへのアクセス権限がありません。",
@@ -245,7 +246,9 @@
"helpText2": "ファイル名またはフォルダ名のみ(フルパスではない)の部分文字列マッチ case:exactで大文字小文字を区別してマッチング\n\n パイプ文字を使用し、1つのフレーズを二重引用符で囲みます。\n\nワイルドカード検索を有効にすると、より高度なオプションを使用できます。",
"useWildcardSearch": "ワイルドカード検索(大文字と小文字を区別)",
"useWildcardSearchDescription": "サポートされているワイルドカードオプション:\n\n- 任意のシーケンスにマッチします:*\n\n- 1文字にマッチ: ?\n\n- 文字クラス:[a-z], [0-9]\n\n- 否定[^0-9]\n\n- 例えば、[*]、[?]、[[]のように、*、?\n\n- すべての結果は大文字と小文字を区別します。",
"openInAdvancedSearch": "詳細検索で開く"
"openInAdvancedSearch": "詳細検索で開く",
"caseExact": "大文字と小文字を区別する一致",
"caseExactDescription": "大文字と小文字を区別せずに処理するのではなく、ファイル名とフォルダ名の文字の大文字・小文字を厳密に一致させる。"
},
"settings": {
"avoidChanges": "(変更を避けるために空白にしてください)",
@@ -572,7 +575,9 @@
"no": "いいえ{suffix}",
"to": "~へ{suffix}",
"and": "そして{suffix}",
"or": "または{suffix}"
"or": "または{suffix}",
"camera": "カメラ{suffix}",
"video": "動画{suffix}"
},
"access": {
"rules": "アクセスルール",
@@ -778,7 +783,6 @@
"scopeAria": "検索対象をフォルダに限定する",
"noTerms": "少なくとも1つの検索語を入力してください。",
"termTooShort": "各検索語は{minSearchLength} 文字以上でなければなりません。",
"caseExact": "大文字と小文字の一致",
"resultsTitle": "検索結果",
"invalidSources": "少なくとも1つのソースをオンにする。",
"noResults": "一致するファイルやフォルダがありません。"
@@ -807,7 +811,18 @@
"description": "現在のセッションのすべての通知を表示する",
"empty": "まだ通知なし",
"clearAll": "すべてクリア",
"total": "{count} 通知"
"appEnabled": "アプリの通知",
"appEnabledDescription": "このタブがバックグラウンドにある間にファイル操作が完了した際、ブラウザに通知を表示します。ブラウザの権限が必要です。このブラウザにのみ保存されます。",
"appUnsupported": "このブラウザではアプリ通知に対応していません。",
"permissionGranted": "許可された",
"permissionDeniedHelp": "通知がブロックされています。このサイトについて、ブラウザの設定で通知を有効にしてください。",
"uploadTitle": "アップロードが完了しました",
"downloadTitle": "すべてダウンロード",
"moveTitle": "移動が完了しました",
"copyTitle": "コピー完了",
"uploadFailedTitle": "アップロードに失敗しました",
"downloadFailedTitle": "ダウンロードに失敗しました",
"operationFailedTitle": "操作に失敗しました"
},
"events": {
"serverShutdown": "サーバーはシャットダウンされた。",
@@ -860,5 +875,8 @@
"documentDownload": "資料ダウンロード - 完",
"editorSuccess": "エディターの初期化 - 成功!すべてのステップが完了しました。",
"setupError": "セットアップエラー"
},
"pwa": {
"install": "アプリをインストール"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "항목으로 이동",
"copyPath": "경로 복사",
"pinItem": "고정",
"unpinItem": "고정 해제"
"unpinItem": "고정 해제",
"sendToApp": "앱으로 보내기"
},
"errors": {
"forbidden": "접근 권한이 없습니다.",
@@ -245,7 +246,9 @@
"helpText2": "대소문자 구분 일치를 위해 파일 또는 폴더 이름에 대해서만 하위 문자열 일치(전체 경로가 아님).\n\n 용어 사이의 OR에는 파이프 문자를, 한 구문에는 큰따옴표를 사용합니다.\n\n고급 옵션을 사용하려면 와일드카드 검색을 사용 설정하세요.",
"useWildcardSearch": "와일드카드 검색(대소문자 구분)",
"useWildcardSearchDescription": "지원되는 와일드카드 옵션:\n\n- 모든 시퀀스와 일치합니다: *\n\n- 한 문자 일치: ?\n\n- 문자 클래스: [a-z], [0-9]\n\n- 부정: [^0-9]\n\n- 리터럴 *, ? 또는 [는 괄호로 묶어야 합니다(예: [*], [?], [[]).\n\n- 모든 결과는 대소문자를 구분합니다.",
"openInAdvancedSearch": "고급 검색에서 열기"
"openInAdvancedSearch": "고급 검색에서 열기",
"caseExact": "대소문자를 구분하는 일치",
"caseExactDescription": "대소문자를 구분하지 않고 무시하는 대신, 파일 및 폴더 이름을 대소문자를 정확히 일치시켜 검색합니다."
},
"settings": {
"avoidChanges": "(수정하지 않으면 비워두세요)",
@@ -572,7 +575,9 @@
"no": "아니요{suffix}",
"to": "~에{suffix}",
"and": "그리고{suffix}",
"or": "또는{suffix}"
"or": "또는{suffix}",
"camera": "카메라{suffix}",
"video": "동영상{suffix}"
},
"access": {
"rules": "액세스 규칙",
@@ -778,7 +783,6 @@
"scopeAria": "폴더로 검색 제한",
"noTerms": "검색어를 하나 이상 입력합니다.",
"termTooShort": "각 검색어는 {minSearchLength} 자 이상이어야 합니다.",
"caseExact": "대소문자 구분 일치",
"resultsTitle": "검색 결과",
"invalidSources": "하나 이상의 소스를 켭니다.",
"noResults": "일치하는 파일이나 폴더가 없습니다."
@@ -807,7 +811,18 @@
"description": "현재 세션의 모든 알림 보기",
"empty": "아직 알림이 없습니다.",
"clearAll": "모두 지우기",
"total": "{count} 알림"
"appEnabled": "앱 알림",
"appEnabledDescription": "이 탭이 백그라운드에 있을 때 파일 작업이 완료되면 브라우저 알림을 표시합니다. 브라우저 권한이 필요합니다. 이 브라우저에만 저장됩니다.",
"appUnsupported": "이 브라우저에서는 앱 알림이 지원되지 않습니다.",
"permissionGranted": "허용됨",
"permissionDeniedHelp": "알림이 차단되었습니다. 브라우저 설정에서 이 사이트에 대한 알림을 허용해 주세요.",
"uploadTitle": "업로드 완료",
"downloadTitle": "전체 다운로드",
"moveTitle": "이동이 완료되었습니다",
"copyTitle": "복사 완료",
"uploadFailedTitle": "업로드에 실패했습니다",
"downloadFailedTitle": "다운로드에 실패했습니다",
"operationFailedTitle": "작업이 실패했습니다"
},
"events": {
"serverShutdown": "서버가 종료되었습니다.",
@@ -860,5 +875,8 @@
"documentDownload": "문서 다운로드 - 완료",
"editorSuccess": "편집기 초기화 - 성공! 모든 단계가 완료되었습니다.",
"setupError": "설정 오류"
},
"pwa": {
"install": "앱 설치"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Ga naar item",
"copyPath": "Kopieer pad",
"pinItem": "Vastzetten",
"unpinItem": "Losmaken"
"unpinItem": "Losmaken",
"sendToApp": "Naar app verzenden"
},
"errors": {
"forbidden": "U hebt geen rechten om hier toegang toe te krijgen.",
@@ -245,7 +246,9 @@
"helpText2": "Substring match alleen op bestands- of mapnamen (geen volledige paden). case:exact voor hoofdlettergevoelig matchen\n\n voor OR tussen termen gebruik je het teken pipe; dubbele aanhalingstekens voor één zin.\n\nSchakel wildcard zoeken in voor meer geavanceerde opties.",
"useWildcardSearch": "Zoeken met jokertekens (hoofdlettergevoelig)",
"useWildcardSearchDescription": "Ondersteunde jokertekenopties:\n\n- Overeenstemming met een willekeurige reeks: *\n\n- Overeenstemming met één teken: ?\n\n- Tekenklasse: [a-z], [0-9]\n\n- Negatie: [^0-9]\n\n- Letterlijke *, ?, of [ moeten tussen haakjes staan, bijv. [*], [?], [[]\n\n- Alle resultaten zijn hoofdlettergevoelig",
"openInAdvancedSearch": "Openen in geavanceerd zoeken"
"openInAdvancedSearch": "Openen in geavanceerd zoeken",
"caseExact": "Hoofdlettergevoelige zoekopdracht",
"caseExactDescription": "Pas bij het vergelijken van bestands- en mapnamen de exacte hoofdlettergebruik toe in plaats van hoofdletters en kleine letters te negeren."
},
"settings": {
"avoidChanges": "(laat leeg om wijzigingen te voorkomen)",
@@ -666,7 +669,9 @@
"no": "Geen{suffix}",
"to": "naar{suffix}",
"and": "en{suffix}",
"or": "of{suffix}"
"or": "of{suffix}",
"camera": "Camera{suffix}",
"video": "Video{suffix}"
},
"access": {
"rules": "Toegangsregels",
@@ -778,7 +783,6 @@
"scopeAria": "Zoekopdracht beperken tot een map",
"noTerms": "Voer ten minste één zoekterm in.",
"termTooShort": "Elke zoekterm moet minimaal {minSearchLength} tekens bevatten.",
"caseExact": "Hoofdlettergevoelige match",
"resultsTitle": "Zoekresultaten",
"invalidSources": "Zet minstens één bron aan.",
"noResults": "Geen overeenkomende bestanden of mappen."
@@ -807,7 +811,18 @@
"description": "Alle meldingen van je huidige sessie bekijken",
"empty": "Nog geen meldingen",
"clearAll": "Alles wissen",
"total": "{count} meldingen"
"appEnabled": "App-meldingen",
"appEnabledDescription": "Toon browsermeldingen wanneer bestandsbewerkingen zijn voltooid terwijl dit tabblad op de achtergrond staat. Hiervoor is toestemming van de browser vereist. Wordt alleen in deze browser opgeslagen.",
"appUnsupported": "App-meldingen worden in deze browser niet ondersteund.",
"permissionGranted": "toegestaan",
"permissionDeniedHelp": "Meldingen zijn geblokkeerd. Schakel ze in de instellingen van je browser voor deze website in.",
"uploadTitle": "Uploaden voltooid",
"downloadTitle": "Download voltooid",
"moveTitle": "Verplaatsing voltooid",
"copyTitle": "Kopiëren voltooid",
"uploadFailedTitle": "Het uploaden is mislukt",
"downloadFailedTitle": "Het downloaden is mislukt",
"operationFailedTitle": "De bewerking is mislukt"
},
"events": {
"serverShutdown": "De server is uitgeschakeld.",
@@ -860,5 +875,8 @@
"documentDownload": "Document downloaden - Compleet",
"editorSuccess": "Initialisatie Editor - Succes! Alle stappen voltooid.",
"setupError": "Setup-fout"
},
"pwa": {
"install": "App installeren"
}
}
+23 -5
View File
@@ -116,7 +116,9 @@
"no": "Geen{suffix}",
"to": "naar{suffix}",
"and": "en{suffix}",
"or": "of{suffix}"
"or": "of{suffix}",
"camera": "Camera{suffix}",
"video": "Video{suffix}"
},
"buttons": {
"copySuccess": "Gekopieerd naar klembord!",
@@ -148,7 +150,8 @@
"goToItem": "Ga naar item",
"goToSource": "Ga naar bronlocatie",
"pinItem": "Vastzetten",
"unpinItem": "Losmaken"
"unpinItem": "Losmaken",
"sendToApp": "Naar app verzenden"
},
"errors": {
"forbidden": "U hebt hier geen toegangsrechten.",
@@ -434,7 +437,9 @@
"useWildcardSearchDescription": "Ondersteunde jokertekenopties:\n\n- Overeenstemming met een willekeurige reeks: *\n\n- Overeenstemming met één teken: ?\n\n- Tekenklasse: [a-z], [0-9]\n\n- Negatie: [^0-9]\n\n- Letterlijke *, ?, of [ moeten tussen haakjes staan, bijv. [*], [?], [[]\n\n- Alle resultaten zijn hoofdlettergevoelig",
"helpText1": "Er wordt gezocht op elk teken dat je typt (minimaal {minSearchLength} tekens).",
"helpText2": "Substring match alleen op bestands- of mapnamen (geen volledige paden). case:exact voor hoofdlettergevoelig matchen\n\n voor OR tussen termen gebruik je het teken pipe; dubbele aanhalingstekens voor één zin.\n\nSchakel wildcard zoeken in voor meer geavanceerde opties.",
"openInAdvancedSearch": "Openen in geavanceerd zoeken"
"openInAdvancedSearch": "Openen in geavanceerd zoeken",
"caseExact": "Hoofdlettergevoelige zoekopdracht",
"caseExactDescription": "Pas bij het vergelijken van bestands- en mapnamen de exacte hoofdlettergebruik toe, in plaats van hoofdletters en kleine letters te negeren."
},
"profileSettings": {
"hideFileExt": "Bepaalde bestanden verbergen (op extensie)",
@@ -766,7 +771,6 @@
"scopeAria": "Zoekopdracht beperken tot een map",
"noTerms": "Voer ten minste één zoekterm in.",
"termTooShort": "Elke zoekterm moet minimaal {minSearchLength} tekens bevatten.",
"caseExact": "Hoofdlettergevoelige match",
"resultsTitle": "Zoekresultaten",
"invalidSources": "Zet minstens één bron aan.",
"noResults": "Geen overeenkomende bestanden of mappen."
@@ -798,7 +802,18 @@
"description": "Alle meldingen van uw huidige sessie bekijken",
"empty": "Nog geen meldingen",
"clearAll": "Alles wissen",
"total": "{count} meldingen"
"appEnabled": "App-meldingen",
"appEnabledDescription": "Toon browsermeldingen wanneer bestandsbewerkingen zijn voltooid terwijl dit tabblad op de achtergrond staat. Hiervoor is toestemming van de browser vereist. Wordt alleen in deze browser opgeslagen.",
"appUnsupported": "App-meldingen worden in deze browser niet ondersteund.",
"permissionGranted": "toegestaan",
"permissionDeniedHelp": "Meldingen zijn geblokkeerd. Schakel ze in de instellingen van je browser voor deze website in.",
"uploadTitle": "Uploaden voltooid",
"downloadTitle": "Download voltooid",
"moveTitle": "Verplaatsing voltooid",
"copyTitle": "Kopiëren voltooid",
"uploadFailedTitle": "Het uploaden is mislukt",
"downloadFailedTitle": "Het downloaden is mislukt",
"operationFailedTitle": "De bewerking is mislukt"
},
"duplicateFinder": {
"minSize": "Minimale grootte (MB){suffix}",
@@ -860,5 +875,8 @@
"documentDownload": "Document downloaden - Voltooid",
"editorSuccess": "Editor-inintialisatie - Succes! Alle stappen voltooid.",
"setupError": "Instellingenfout"
},
"pwa": {
"install": "App installeren"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Przejdź do pozycji",
"copyPath": "Ścieżka kopiowania",
"pinItem": "Przypnij",
"unpinItem": "Odepnij"
"unpinItem": "Odepnij",
"sendToApp": "Wyślij do aplikacji"
},
"errors": {
"forbidden": "Nie posiadasz uprawnień potrzebnych, by uzyskać do tego dostęp.",
@@ -245,7 +246,9 @@
"helpText2": "Dopasowanie podciągu tylko do nazw plików lub folderów (nie pełnych ścieżek). case:exact dla dopasowania z uwzględnieniem wielkości liter.\n\n dla OR między wyrażeniami użyj znaku potoku; podwójne cudzysłowy dla jednej frazy.\n\nWłącz wyszukiwanie wieloznaczne, aby uzyskać bardziej zaawansowane opcje.",
"useWildcardSearch": "Wyszukiwanie z użyciem symboli wieloznacznych (z uwzględnieniem wielkości liter)",
"useWildcardSearchDescription": "Obsługiwane opcje symboli wieloznacznych:\n\n- Dopasuj dowolną sekwencję: *\n\n- Dopasowanie jednego znaku: ?\n\n- Klasa znaków: [a-z], [0-9]\n\n- Negacja: [^0-9]\n\n- Dosłowne *, ? lub [ muszą być ujęte w nawiasy, np. [*], [?], [[]\n\n- We wszystkich wynikach rozróżniana jest wielkość liter",
"openInAdvancedSearch": "Otwórz w wyszukiwaniu zaawansowanym"
"openInAdvancedSearch": "Otwórz w wyszukiwaniu zaawansowanym",
"caseExact": "Wyszukiwanie z uwzględnieniem wielkości liter",
"caseExactDescription": "Porównuj nazwy plików i folderów, uwzględniając dokładną wielkość liter, zamiast ignorować różnicę między wielkimi a małymi literami."
},
"settings": {
"avoidChanges": "(pozostaw puste aby nie zosatało zmienione)",
@@ -572,7 +575,9 @@
"no": "Nie{suffix}",
"to": "do{suffix}",
"and": "oraz{suffix}",
"or": "lub{suffix}"
"or": "lub{suffix}",
"camera": "Aparat{suffix}",
"video": "Film{suffix}"
},
"access": {
"rules": "Zasady dostępu",
@@ -778,7 +783,6 @@
"scopeAria": "Ograniczenie wyszukiwania do folderu",
"noTerms": "Wprowadź co najmniej jedno wyszukiwane hasło.",
"termTooShort": "Każde wyszukiwane hasło musi zawierać co najmniej {minSearchLength} znaków",
"caseExact": "Dopasowanie z uwzględnieniem wielkości liter",
"resultsTitle": "Wyniki wyszukiwania",
"invalidSources": "Włącz co najmniej jedno źródło.",
"noResults": "Brak pasujących plików lub folderów."
@@ -807,7 +811,18 @@
"description": "Wyświetlanie wszystkich powiadomień z bieżącej sesji",
"empty": "Brak powiadomień",
"clearAll": "Wyczyść wszystko",
"total": "{count} powiadomienia"
"appEnabled": "Powiadomienia w aplikacji",
"appEnabledDescription": "Wyświetlaj powiadomienia przeglądarki po zakończeniu operacji na plikach, gdy ta karta jest w tle. Wymaga uprawnień przeglądarki. Dane są zapisywane wyłącznie w tej przeglądarce.",
"appUnsupported": "W tej przeglądarce nie są obsługiwane powiadomienia z aplikacji.",
"permissionGranted": "dozwolone",
"permissionDeniedHelp": "Powiadomienia są zablokowane. Włącz je w ustawieniach przeglądarki dla tej witryny.",
"uploadTitle": "Przesyłanie zakończone",
"downloadTitle": "Pobierz całość",
"moveTitle": "Operacja zakończona",
"copyTitle": "Kopiowanie zakończone",
"uploadFailedTitle": "Przesyłanie nie powiodło się",
"downloadFailedTitle": "Pobieranie nie powiodło się",
"operationFailedTitle": "Operacja nie powiodła się"
},
"events": {
"serverShutdown": "Serwer został wyłączony.",
@@ -860,5 +875,8 @@
"documentDownload": "Pobieranie dokumentów - zakończone",
"editorSuccess": "Inicjalizacja edytora - sukces! Wszystkie kroki zostały wykonane.",
"setupError": "Błąd konfiguracji"
},
"pwa": {
"install": "Zainstaluj aplikację"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Ir para o item",
"copyPath": "Copiar caminho",
"pinItem": "Fixar",
"unpinItem": "Desafixar"
"unpinItem": "Desafixar",
"sendToApp": "Enviar para o aplicativo"
},
"errors": {
"forbidden": "Você não tem permissões para acessar isto.",
@@ -245,7 +246,9 @@
"helpText2": "Correspondência de subcadeia somente em nomes de arquivos ou pastas (não em caminhos completos). case:exact para correspondência com distinção entre maiúsculas e minúsculas\n\n para OU entre termos, use o caractere pipe; aspas duplas para uma frase.\n\nAtive a pesquisa curinga para obter opções mais avançadas.",
"useWildcardSearch": "Pesquisa curinga (diferencia maiúsculas de minúsculas)",
"useWildcardSearchDescription": "Opções curinga compatíveis:\n\n- Corresponde a qualquer sequência: *\n\n- Corresponde a um caractere: ?\n\n- Classe de caracteres: [a-z], [0-9]\n\n- Negação: [^0-9]\n\n- Os literais *, ? ou [ precisam ser colocados entre colchetes, por exemplo, [*], [?], [[]\n\n- Todos os resultados diferenciam maiúsculas de minúsculas",
"openInAdvancedSearch": "Abrir na pesquisa avançada"
"openInAdvancedSearch": "Abrir na pesquisa avançada",
"caseExact": "Correspondência que diferencia maiúsculas de minúsculas",
"caseExactDescription": "Compare nomes de arquivos e pastas usando a maiúscula e minúscula exatas, em vez de ignorar a diferença entre maiúsculas e minúsculas."
},
"settings": {
"avoidChanges": "(deixe em branco para manter)",
@@ -572,7 +575,9 @@
"no": "Não{suffix}",
"to": "para{suffix}",
"and": "e{suffix}",
"or": "ou{suffix}"
"or": "ou{suffix}",
"camera": "Câmera{suffix}",
"video": "Vídeo{suffix}"
},
"access": {
"rules": "Regras de acesso",
@@ -778,7 +783,6 @@
"scopeAria": "Limitar a pesquisa a uma pasta",
"noTerms": "Digite pelo menos um termo de pesquisa.",
"termTooShort": "Cada termo de pesquisa deve ter pelo menos {minSearchLength} caracteres",
"caseExact": "Correspondência com distinção entre maiúsculas e minúsculas",
"resultsTitle": "Resultados da pesquisa",
"invalidSources": "Ligue pelo menos uma fonte.",
"noResults": "Não há arquivos ou pastas correspondentes."
@@ -807,7 +811,18 @@
"description": "Exibir todas as notificações de sua sessão atual",
"empty": "Ainda não há notificações",
"clearAll": "Limpar tudo",
"total": "{count} notificações"
"appEnabled": "Notificações de aplicativos",
"appEnabledDescription": "Mostrar notificações do navegador quando as operações com arquivos forem concluídas enquanto esta guia estiver em segundo plano. Requer permissão do navegador. Armazenado apenas neste navegador.",
"appUnsupported": "As notificações de aplicativos não são compatíveis com este navegador.",
"permissionGranted": "permitido",
"permissionDeniedHelp": "As notificações estão bloqueadas. Ative-as nas configurações do seu navegador para este site.",
"uploadTitle": "Upload concluído",
"downloadTitle": "Baixar na íntegra",
"moveTitle": "Movimentação concluída",
"copyTitle": "Cópia concluída",
"uploadFailedTitle": "Falha no upload",
"downloadFailedTitle": "Falha no download",
"operationFailedTitle": "A operação falhou"
},
"events": {
"serverShutdown": "O servidor foi desligado.",
@@ -860,5 +875,8 @@
"documentDownload": "Download de documentos - Completo",
"editorSuccess": "Inicialização do editor - Sucesso! Todas as etapas foram concluídas.",
"setupError": "Erro de configuração"
},
"pwa": {
"install": "Instalar o aplicativo"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Ir para o item",
"copyPath": "Caminho de cópia",
"pinItem": "Fixar",
"unpinItem": "Desafixar"
"unpinItem": "Desafixar",
"sendToApp": "Enviar para a aplicação"
},
"errors": {
"forbidden": "Não tem permissões para aceder a isto",
@@ -245,7 +246,9 @@
"helpText2": "Correspondência de subcadeia de caracteres apenas em nomes de ficheiros ou pastas (não em caminhos completos). case:exact para correspondência sensível a maiúsculas e minúsculas\n\n para OU entre termos, utilizar o carácter pipe; aspas duplas para uma frase.\n\nAtivar a pesquisa de wildcards para opções mais avançadas.",
"useWildcardSearch": "Pesquisa com caracteres curinga (sensível a maiúsculas e minúsculas)",
"useWildcardSearchDescription": "Opções curingas suportadas:\n\n- Corresponde a qualquer sequência: *\n\n- Corresponder a um carácter: ?\n\n- Classe de caracteres: [a-z], [0-9]\n\n- Negação: [^0-9]\n\n- Os literais *, ?, ou [ devem ser colocados entre parênteses, por exemplo, [*], [?], [[]\n\n- Todos os resultados são sensíveis a maiúsculas e minúsculas",
"openInAdvancedSearch": "Abrir na Pesquisa Avançada"
"openInAdvancedSearch": "Abrir na Pesquisa Avançada",
"caseExact": "Correspondência que distingue maiúsculas de minúsculas",
"caseExactDescription": "Comparar os nomes dos ficheiros e pastas utilizando a maiúscula e minúscula exatas, em vez de ignorar as maiúsculas e minúsculas."
},
"settings": {
"avoidChanges": "(deixe em branco para manter)",
@@ -666,7 +669,9 @@
"no": "Não{suffix}",
"to": "para{suffix}",
"and": "e{suffix}",
"or": "ou{suffix}"
"or": "ou{suffix}",
"camera": "Câmara{suffix}",
"video": "Vídeo{suffix}"
},
"access": {
"rules": "Regras de acesso",
@@ -778,7 +783,6 @@
"scopeAria": "Limitar a pesquisa a uma pasta",
"noTerms": "Introduzir pelo menos um termo de pesquisa.",
"termTooShort": "Cada termo de pesquisa deve ter, pelo menos, {minSearchLength} caracteres",
"caseExact": "Correspondência com distinção de maiúsculas e minúsculas",
"resultsTitle": "Resultados da pesquisa",
"invalidSources": "Ligar pelo menos uma fonte.",
"noResults": "Não há ficheiros ou pastas correspondentes."
@@ -807,7 +811,18 @@
"description": "Ver todas as notificações da sua sessão atual",
"empty": "Ainda não há notificações",
"clearAll": "Limpar tudo",
"total": "{count} notificações"
"appEnabled": "Notificações da aplicação",
"appEnabledDescription": "Mostrar notificações do navegador quando as operações com ficheiros forem concluídas enquanto este separador estiver em segundo plano. Requer autorização do navegador. Guardado apenas neste navegador.",
"appUnsupported": "As notificações de aplicações não são suportadas neste navegador.",
"permissionGranted": "permitido",
"permissionDeniedHelp": "As notificações estão bloqueadas. Ative-as nas definições do seu navegador para este site.",
"uploadTitle": "Carregamento concluído",
"downloadTitle": "Descarregar na íntegra",
"moveTitle": "Movimentação concluída",
"copyTitle": "Cópia concluída",
"uploadFailedTitle": "Falha no carregamento",
"downloadFailedTitle": "O download falhou",
"operationFailedTitle": "A operação falhou"
},
"events": {
"serverShutdown": "O servidor foi desligado.",
@@ -860,5 +875,8 @@
"documentDownload": "Descarregamento de documentos - Completo",
"editorSuccess": "Inicialização do editor - Sucesso! Todas as etapas foram concluídas.",
"setupError": "Erro de configuração"
},
"pwa": {
"install": "Instalar a aplicação"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Du-te la articol",
"copyPath": "Copiați calea",
"pinItem": "Fixare",
"unpinItem": "Anulare fixare"
"unpinItem": "Anulare fixare",
"sendToApp": "Trimite către aplicație"
},
"errors": {
"forbidden": "Nu ai permisiuni sa accesezi asta.",
@@ -245,7 +246,9 @@
"helpText2": "Potrivire de substringuri numai pe nume de fișiere sau de foldere (nu pe căi complete). case:exact pentru potrivire în funcție de caz\n\n pentru OR între termeni utilizați caracterul pipă; ghilimele duble pentru o frază.\n\nActivați căutarea wildcard pentru opțiuni mai avansate.",
"useWildcardSearch": "Căutare wildcard (sensibil la majuscule)",
"useWildcardSearchDescription": "Opțiuni de wildcard acceptate:\n\n- Potrivește orice secvență: *\n\n- Potrivește un singur caracter: ?\n\n- Clasa de caractere: [a-z], [0-9]\n\n- Negație: [^0-9]\n\n- Literele *, ? sau [ trebuie să fie puse între paranteze, de exemplu [*], [?], [[]\n\n- Toate rezultatele sunt sensibile la majuscule",
"openInAdvancedSearch": "Deschidere în căutarea avansată"
"openInAdvancedSearch": "Deschidere în căutarea avansată",
"caseExact": "Căutare cu diferențiere între majuscule și minuscule",
"caseExactDescription": "Compară numele fișierelor și folderelor folosind exact litere mari și mici, în loc să ignore diferențele dintre ele."
},
"settings": {
"avoidChanges": "(lasă gol pentru a nu schimba)",
@@ -572,7 +575,9 @@
"no": "Nu{suffix}",
"to": "spre{suffix}",
"and": "și{suffix}",
"or": "sau{suffix}"
"or": "sau{suffix}",
"camera": "Cameră foto{suffix}",
"video": "Video{suffix}"
},
"access": {
"rules": "Reguli de acces",
@@ -778,7 +783,6 @@
"scopeAria": "Limitarea căutării la un dosar",
"noTerms": "Introduceți cel puțin un termen de căutare.",
"termTooShort": "Fiecare termen de căutare trebuie să aibă cel puțin {minSearchLength} caractere",
"caseExact": "Potrivire sensibilă la majuscule",
"resultsTitle": "Rezultatele căutării",
"invalidSources": "Porniți cel puțin o sursă.",
"noResults": "Nu există fișiere sau foldere corespunzătoare."
@@ -807,7 +811,18 @@
"description": "Vizualizați toate notificările din sesiunea curentă",
"empty": "Nu există încă notificări",
"clearAll": "Ștergeți tot",
"total": "{count} notificări"
"appEnabled": "Notificări din aplicații",
"appEnabledDescription": "Afișează notificări în browser la finalizarea operațiunilor cu fișiere atunci când această filă se află în fundal. Necesită permisiunea browserului. Se salvează numai în acest browser.",
"appUnsupported": "Notificările aplicațiilor nu sunt acceptate în acest browser.",
"permissionGranted": "permis",
"permissionDeniedHelp": "Notificările sunt blocate. Activează-le în setările browserului pentru acest site.",
"uploadTitle": "Încărcarea s-a încheiat",
"downloadTitle": "Descărcare completă",
"moveTitle": "Mutarea s-a finalizat",
"copyTitle": "Copiere finalizată",
"uploadFailedTitle": "Încărcarea a eșuat",
"downloadFailedTitle": "Descărcarea a eșuat",
"operationFailedTitle": "Operațiunea a eșuat"
},
"events": {
"serverShutdown": "Serverul a fost închis.",
@@ -860,5 +875,8 @@
"documentDownload": "Descărcare document - Complet",
"editorSuccess": "Inițializarea editorului - Succes! Toate etapele au fost finalizate.",
"setupError": "Eroare de configurare"
},
"pwa": {
"install": "Instalează aplicația"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Перейти к пункту",
"copyPath": "Путь копирования",
"pinItem": "Закрепить",
"unpinItem": "Открепить"
"unpinItem": "Открепить",
"sendToApp": "Отправить в приложение"
},
"errors": {
"forbidden": "У вас нет прав доступа к этому.",
@@ -245,7 +246,9 @@
"helpText2": "Подстрочное соответствие только для имен файлов или папок (не полных путей). case:exact для чувствительного к регистру соответствия\n\n для ИЛИ между терминами используйте символ трубы; двойные кавычки для одной фразы.\n\nВключите поиск по подстановочным знакам для получения более широких возможностей.",
"useWildcardSearch": "Поиск с использованием подстановочных знаков (с учетом регистра)",
"useWildcardSearchDescription": "Поддерживаемые параметры подстановочного знака:\n\n- Соответствие любой последовательности: *\n\n- Соответствие одному символу: ?\n\n- Класс символов: [a-z], [0-9]\n\n- Отрицание: [^0-9]\n\n- Буквальные *, ? или [ должны быть заключены в скобки, например, [*], [?], [[]\n\n- Все результаты чувствительны к регистру",
"openInAdvancedSearch": "Открыть в расширенном поиске"
"openInAdvancedSearch": "Открыть в расширенном поиске",
"caseExact": "Поиск с учетом регистра",
"caseExactDescription": "Сопоставлять имена файлов и папок, учитывая регистр букв, а не игнорируя его."
},
"settings": {
"avoidChanges": "(оставьте поле пустым, чтобы избежать изменений)",
@@ -572,7 +575,9 @@
"no": "Нет{suffix}",
"to": "до{suffix}",
"and": "и{suffix}",
"or": "или{suffix}"
"or": "или{suffix}",
"camera": "Камера{suffix}",
"video": "Видео{suffix}"
},
"access": {
"rules": "Правила доступа",
@@ -778,7 +783,6 @@
"scopeAria": "Ограничьте поиск папкой",
"noTerms": "Введите хотя бы один поисковый запрос.",
"termTooShort": "Каждый поисковый запрос должен содержать не менее {minSearchLength} символов.",
"caseExact": "Соответствие с учетом регистра",
"resultsTitle": "Результаты поиска",
"invalidSources": "Включите хотя бы один источник.",
"noResults": "Нет подходящих файлов или папок."
@@ -807,7 +811,18 @@
"description": "Просмотр всех уведомлений в текущем сеансе",
"empty": "Уведомлений пока нет",
"clearAll": "Очистить все",
"total": "{count} уведомления"
"appEnabled": "Уведомления в приложениях",
"appEnabledDescription": "Показывать уведомления браузера по завершении операций с файлами, когда эта вкладка находится в фоновом режиме. Требуется разрешение браузера. Сохраняется только в этом браузере.",
"appUnsupported": "В этом браузере не поддерживаются уведомления приложений.",
"permissionGranted": "разрешено",
"permissionDeniedHelp": "Уведомления заблокированы. Включите их в настройках браузера для этого сайта.",
"uploadTitle": "Загрузка завершена",
"downloadTitle": "Скачать полностью",
"moveTitle": "Перемещение завершено",
"copyTitle": "Копирование завершено",
"uploadFailedTitle": "Загрузка не удалась",
"downloadFailedTitle": "Загрузка не удалась",
"operationFailedTitle": "Операция завершилась неудачей"
},
"events": {
"serverShutdown": "Сервер был выключен.",
@@ -860,5 +875,8 @@
"documentDownload": "Загрузка документов - Полная",
"editorSuccess": "Инициализация редактора - Успех! Все шаги выполнены.",
"setupError": "Ошибка установки"
},
"pwa": {
"install": "Установить приложение"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Prejsť na položku",
"copyPath": "Kopírovať cestu",
"pinItem": "Pripnúť",
"unpinItem": "Odpnúť"
"unpinItem": "Odpnúť",
"sendToApp": "Odoslať do aplikácie"
},
"errors": {
"forbidden": "You don't have permissions to access this.",
@@ -245,7 +246,9 @@
"helpText2": "Zhoda podreťazca len na názvoch súborov alebo priečinkov (nie na celých cestách). case:exact pre porovnávanie podľa veľkosti písmen\n\n pre OR medzi výrazmi použite znak pipe; dvojité úvodzovky pre jednu frázu.\n\nPovoľte vyhľadávanie so zástupnými znakmi pre pokročilejšie možnosti.",
"useWildcardSearch": "Vyhľadávanie so zástupnými znakmi (rozlišovanie veľkých a malých písmen)",
"useWildcardSearchDescription": "Podporované možnosti zástupných znakov:\n\n- Zodpovedajú akejkoľvek sekvencii: *\n\n- Zodpovedá jednému znaku: ?\n\n- Trieda znakov: [a-z], [0-9]\n\n- Negácia: [^0-9]\n\n- Doslovné *, ? alebo [ musia byť uzavreté v zátvorkách, napr. [*], [?], [[]\n\n- Pri všetkých výsledkoch sa rozlišujú veľké a malé písmená",
"openInAdvancedSearch": "Otvoriť v rozšírenom vyhľadávaní"
"openInAdvancedSearch": "Otvoriť v rozšírenom vyhľadávaní",
"caseExact": "Porovnanie s rozlišovaním veľkých a malých písmen",
"caseExactDescription": "Porovnávať názvy súborov a priečinkov na základe presného rozlišovania veľkých a malých písmen namiesto ignorovania veľkých a malých písmen."
},
"settings": {
"avoidChanges": "(nechajte prázdne, aby sa nezmenilo)",
@@ -572,7 +575,9 @@
"no": "Nie{suffix}",
"to": "do{suffix}",
"and": "a{suffix}",
"or": "alebo{suffix}"
"or": "alebo{suffix}",
"camera": "Fotoaparát{suffix}",
"video": "Video{suffix}"
},
"access": {
"rules": "Pravidlá prístupu",
@@ -778,7 +783,6 @@
"scopeAria": "Obmedzenie vyhľadávania na priečinok",
"noTerms": "Zadajte aspoň jeden vyhľadávací výraz.",
"termTooShort": "Každý hľadaný výraz musí mať aspoň {minSearchLength} znakov.",
"caseExact": "Zhoda s rozlišovaním veľkých a malých písmen",
"resultsTitle": "Výsledky vyhľadávania",
"invalidSources": "Zapnite aspoň jeden zdroj.",
"noResults": "Žiadne zodpovedajúce súbory alebo priečinky."
@@ -807,7 +811,18 @@
"description": "Zobrazenie všetkých oznámení z aktuálnej relácie",
"empty": "Zatiaľ žiadne oznámenia",
"clearAll": "Vymazať všetko",
"total": "{count} oznámenia"
"appEnabled": "Oznámenia v aplikácii",
"appEnabledDescription": "Zobrazovať oznámenia prehliadača po dokončení operácií so súbormi, keď je táto karta v pozadí. Vyžaduje povolenie prehliadača. Uložené iba v tomto prehliadači.",
"appUnsupported": "Tento prehliadač nepodporuje oznámenia z aplikácií.",
"permissionGranted": "povolené",
"permissionDeniedHelp": "Oznámenia sú blokované. V nastaveniach prehliadača ich pre túto stránku povolte.",
"uploadTitle": "Nahratie dokončené",
"downloadTitle": "Stiahnuť celé",
"moveTitle": "Presun dokončený",
"copyTitle": "Kopírovanie dokončené",
"uploadFailedTitle": "Nahratie sa nepodarilo",
"downloadFailedTitle": "Stiahnutie sa nepodarilo",
"operationFailedTitle": "Operácia sa nepodarila"
},
"events": {
"serverShutdown": "Server bol vypnutý.",
@@ -860,5 +875,8 @@
"documentDownload": "Stiahnutie dokumentu - Kompletné",
"editorSuccess": "Inicializácia editora - úspech! Všetky kroky boli dokončené.",
"setupError": "Chyba nastavenia"
},
"pwa": {
"install": "Nainštalovať aplikáciu"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Gå till objektet",
"copyPath": "Kopiera sökväg",
"pinItem": "Fäst",
"unpinItem": "Lossa"
"unpinItem": "Lossa",
"sendToApp": "Skicka till appen"
},
"errors": {
"forbidden": "Du saknar rättigheter till detta",
@@ -245,7 +246,9 @@
"helpText2": "Delsträngsmatchning endast på fil- eller mappnamn (inte fullständiga sökvägar). case:exact för skiftlägeskänslig matchning\n\n för OR mellan termer använd pipe-tecken; dubbla citattecken för en fras.\n\nAktivera jokerteckensökning för mer avancerade alternativ.",
"useWildcardSearch": "Sökning med jokertecken (skiftlägeskänslig)",
"useWildcardSearchDescription": "Alternativ för jokertecken som stöds:\n\n- Matchar vilken sekvens som helst: *\n\n- Matcha ett tecken: ?\n\n- Teckenklass: [a-z], [0-9]\n\n- Negation: [^0-9]\n\n- Bokstavliga *, ? eller [ måste omslutas av parenteser, t.ex. [*], [?], [[]\n\n- Alla resultat är skiftlägeskänsliga",
"openInAdvancedSearch": "Öppna i avancerad sökning"
"openInAdvancedSearch": "Öppna i avancerad sökning",
"caseExact": "Skillnad mellan stora och små bokstäver",
"caseExactDescription": "Jämför fil- och mappnamn med exakt bokstavsstorlek istället för att bortse från skillnaden mellan stora och små bokstäver."
},
"settings": {
"avoidChanges": "(lämna blankt för att undvika ändringar)",
@@ -666,7 +669,9 @@
"no": "Nej{suffix}",
"to": "till{suffix}",
"and": "och{suffix}",
"or": "eller{suffix}"
"or": "eller{suffix}",
"camera": "Kamera{suffix}",
"video": "Video{suffix}"
},
"access": {
"rules": "Regler för åtkomst",
@@ -778,7 +783,6 @@
"scopeAria": "Begränsa sökningen till en mapp",
"noTerms": "Ange minst en sökterm.",
"termTooShort": "Varje sökterm måste innehålla minst {minSearchLength} tecken",
"caseExact": "Matchning med skiftlägeskänslighet",
"resultsTitle": "Sökresultat",
"invalidSources": "Slå på minst en källa.",
"noResults": "Inga matchande filer eller mappar."
@@ -807,7 +811,18 @@
"description": "Visa alla meddelanden från din aktuella session",
"empty": "Inga meddelanden ännu",
"clearAll": "Rensa alla",
"total": "{count} meddelanden"
"appEnabled": "Appaviseringar",
"appEnabledDescription": "Visa webbläsaraviseringar när filåtgärder slutförs medan den här fliken är i bakgrunden. Kräver behörighet i webbläsaren. Sparas endast i den här webbläsaren.",
"appUnsupported": "Appaviseringar stöds inte i den här webbläsaren.",
"permissionGranted": "tillåtet",
"permissionDeniedHelp": "Meddelanden är blockerade. Aktivera dem i webbläsarens inställningar för den här webbplatsen.",
"uploadTitle": "Uppladdningen är klar",
"downloadTitle": "Ladda ner hela filen",
"moveTitle": "Flytten är klar",
"copyTitle": "Kopieringen är klar",
"uploadFailedTitle": "Uppladdningen misslyckades",
"downloadFailedTitle": "Nedladdningen misslyckades",
"operationFailedTitle": "Åtgärden misslyckades"
},
"events": {
"serverShutdown": "Servern stängdes av.",
@@ -860,5 +875,8 @@
"documentDownload": "Dokumentnedladdning - Komplett",
"editorSuccess": "Initialisering av Editor - Framgång! Alla steg slutförda.",
"setupError": "Inställningsfel"
},
"pwa": {
"install": "Installera appen"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Öğeye git",
"copyPath": "Kopyalama Yolu",
"pinItem": "Sabitle",
"unpinItem": "Sabitlemeyi Kaldır"
"unpinItem": "Sabitlemeyi Kaldır",
"sendToApp": "Uygulamaya gönder"
},
"errors": {
"forbidden": "Buna erişim izniniz yok.",
@@ -245,7 +246,9 @@
"helpText2": "Yalnızca dosya veya klasör adlarında alt dize eşleşmesi (tam yollarda değil). case:exact büyük/küçük harfe duyarlı eşleştirme için\n\n terimler arasında VEYA için boru karakteri kullanın; bir ifade için çift tırnak kullanın.\n\nDaha gelişmiş seçenekler için joker karakter aramayı etkinleştirin.",
"useWildcardSearch": "Joker karakter araması (büyük/küçük harfe duyarlı)",
"useWildcardSearchDescription": "Desteklenen joker karakter seçenekleri:\n\n- Herhangi bir diziyi eşleştirin: *\n\n- Bir karakteri eşleştirin: ?\n\n- Karakter sınıfı: [a-z], [0-9]\n\n- Negasyon: [^0-9]\n\n- Literal *, ? veya [ parantez içine alınmalıdır, örneğin [*], [?], [[]\n\n- Tüm sonuçlar büyük/küçük harfe duyarlıdır",
"openInAdvancedSearch": "Gelişmiş Aramada Aç"
"openInAdvancedSearch": "Gelişmiş Aramada Aç",
"caseExact": "Büyük/küçük harf duyarlı eşleştirme",
"caseExactDescription": "Büyük ve küçük harfleri göz ardı etmek yerine, dosya ve klasör adlarını tam harf büyük/küçük yazımına göre eşleştirin."
},
"settings": {
"avoidChanges": "(değişiklikleri önlemek için boş bırakın)",
@@ -572,7 +575,9 @@
"no": "Hayır{suffix}",
"to": "...'ye{suffix}",
"and": "ve{suffix}",
"or": "veya{suffix}"
"or": "veya{suffix}",
"camera": "Kamera{suffix}",
"video": "Video{suffix}"
},
"access": {
"rules": "Erişim kuralları",
@@ -778,7 +783,6 @@
"scopeAria": "Aramayı bir klasörle sınırlandırın",
"noTerms": "En az bir arama terimi girin.",
"termTooShort": "Her bir arama terimi en az {minSearchLength} karakter olmalıdır",
"caseExact": "Harfe duyarlı eşleşme",
"resultsTitle": "Arama sonuçları",
"invalidSources": "En az bir kaynağı açın.",
"noResults": "Eşleşen dosya veya klasör yok."
@@ -807,7 +811,18 @@
"description": "Geçerli oturumunuzdaki tüm bildirimleri görüntüleyin",
"empty": "Henüz bildirim yok",
"clearAll": "Tümünü Temizle",
"total": "{count} BİLDİRİMLER"
"appEnabled": "Uygulama bildirimleri",
"appEnabledDescription": "Bu sekme arka planda iken dosya işlemleri tamamlandığında tarayıcı bildirimlerini göster. Tarayıcı izni gerektirir. Yalnızca bu tarayıcıda saklanır.",
"appUnsupported": "Bu tarayıcıda uygulama bildirimleri desteklenmemektedir.",
"permissionGranted": "izin verilen",
"permissionDeniedHelp": "Bildirimler engellenmiştir. Bu site için tarayıcı ayarlarınızdan bildirimleri etkinleştirin.",
"uploadTitle": "Yükleme tamamlandı",
"downloadTitle": "Tam sürümü indir",
"moveTitle": "İşlem tamamlandı",
"copyTitle": "Kopyalama tamamlandı",
"uploadFailedTitle": "Yükleme başarısız",
"downloadFailedTitle": "İndirme başarısız",
"operationFailedTitle": "İşlem başarısız oldu"
},
"events": {
"serverShutdown": "Sunucu kapatıldı.",
@@ -860,5 +875,8 @@
"documentDownload": "Belge İndirme - Tamamlandı",
"editorSuccess": "Editör Başlatma - Başarılı! Tüm adımlar tamamlandı.",
"setupError": "Kurulum Hatası"
},
"pwa": {
"install": "Uygulamayı yükle"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "Перейти до пункту",
"copyPath": "Шлях копіювання",
"pinItem": "Закріпити",
"unpinItem": "Відкріпити"
"unpinItem": "Відкріпити",
"sendToApp": "Надіслати в додаток"
},
"errors": {
"forbidden": "У вас немає прав доступу до цього.",
@@ -245,7 +246,9 @@
"helpText2": "Підрядковий пошук лише за іменами файлів або папок (не за повними шляхами). case:exact для пошуку з урахуванням регістру\n\n для АБО між термінами використовуйте символ труби; подвійні лапки для однієї фрази.\n\nУвімкніть пошук за підстановочними символами для більш розширених можливостей.",
"useWildcardSearch": "Пошук за підстановчими символами (з урахуванням регістру)",
"useWildcardSearchDescription": "Підтримувані символи підстановки:\n\n- Відповідність будь-якій послідовності: *\n\n- Збіг з одним символом: ?\n\n- Клас символів: [a-z], [0-9].\n\n- Заперечення: [^0-9]\n\n- Літерали *, ? або [ слід брати в дужки, наприклад, [*], [?], [[].\n\n- Всі результати чутливі до регістру",
"openInAdvancedSearch": "Відкрити в розширеному пошуку"
"openInAdvancedSearch": "Відкрити в розширеному пошуку",
"caseExact": "З урахуванням регістру",
"caseExactDescription": "Збігати назви файлів і папок, враховуючи точний регістр літер, а не ігноруючи великі та малі літери."
},
"settings": {
"avoidChanges": "(залишіть поле порожнім, щоб уникнути змін)",
@@ -666,7 +669,9 @@
"no": "Ні.{suffix}",
"to": "до{suffix}",
"and": "та{suffix}",
"or": "або{suffix}"
"or": "або{suffix}",
"camera": "Камера{suffix}",
"video": "Відео{suffix}"
},
"access": {
"rules": "Правила доступу",
@@ -778,7 +783,6 @@
"scopeAria": "Обмежити пошук до папки",
"noTerms": "Введіть принаймні один пошуковий запит.",
"termTooShort": "Кожен пошуковий запит повинен містити щонайменше {minSearchLength} символів",
"caseExact": "Збіг з урахуванням регістру",
"resultsTitle": "Результати пошуку",
"invalidSources": "Увімкніть хоча б одне джерело.",
"noResults": "Немає співпадаючих файлів або папок."
@@ -807,7 +811,18 @@
"description": "Переглянути всі сповіщення з поточного сеансу",
"empty": "Ще немає повідомлень",
"clearAll": "Очистити все.",
"total": "{count} сповіщення"
"appEnabled": "Сповіщення в додатках",
"appEnabledDescription": "Показувати сповіщення браузера про завершення операцій з файлами, коли ця вкладка працює у фоновому режимі. Потрібний дозвіл браузера. Зберігається лише в цьому браузері.",
"appUnsupported": "У цьому браузері не підтримуються сповіщення додатків.",
"permissionGranted": "дозволено",
"permissionDeniedHelp": "Повідомлення заблоковано. Увімкніть їх у налаштуваннях браузера для цього сайту.",
"uploadTitle": "Завантаження завершено",
"downloadTitle": "Завантажити повністю",
"moveTitle": "Переміщення завершено",
"copyTitle": "Копіювання завершено",
"uploadFailedTitle": "Завантаження не вдалося",
"downloadFailedTitle": "Завантаження не вдалося",
"operationFailedTitle": "Операція не вдалася"
},
"events": {
"serverShutdown": "Сервер був вимкнений.",
@@ -860,5 +875,8 @@
"documentDownload": "Завантаження документа - Повне",
"editorSuccess": "Ініціалізація редактора - успіх! Всі кроки виконано.",
"setupError": "Помилка встановлення"
},
"pwa": {
"install": "Встановити додаток"
}
}
+23 -5
View File
@@ -29,7 +29,8 @@
"goToItem": "转到项目",
"copyPath": "复制路径",
"pinItem": "固定",
"unpinItem": "取消固定"
"unpinItem": "取消固定",
"sendToApp": "发送至应用"
},
"errors": {
"forbidden": "你无权限访问",
@@ -245,7 +246,9 @@
"helpText2": "仅对文件或文件夹名称(而非完整路径)进行子串匹配。 case:exact 用于大小写敏感匹配\n\n 对于术语之间的 OR,使用管道字符;一个短语使用双引号。\n\n启用通配符搜索可获得更多高级选项。",
"useWildcardSearch": "通配符搜索(区分大小写)",
"useWildcardSearchDescription": "支持通配符选项:\n\n- 匹配任何序列:*\n\n- 匹配一个字符:?\n\n- 字符类别:[a-z]、[0-9]\n\n- 否定:[^0-9]\n\n- 字面 *、? 或 [ 需要用括号包装,例如 [*]、[?]、[[]\n\n- 所有结果均区分大小写",
"openInAdvancedSearch": "在高级搜索中打开"
"openInAdvancedSearch": "在高级搜索中打开",
"caseExact": "区分大小写的匹配",
"caseExactDescription": "匹配文件和文件夹名称时,应严格区分大小写,而非忽略大小写。"
},
"settings": {
"scopes": "定义允许的来源及其中的用户范围",
@@ -666,7 +669,9 @@
"no": "没有{suffix}",
"to": "到{suffix}",
"and": "以及{suffix}",
"or": "或{suffix}"
"or": "或{suffix}",
"camera": "相机{suffix}",
"video": "视频{suffix}"
},
"access": {
"rules": "访问规则",
@@ -778,7 +783,6 @@
"scopeAria": "限制搜索文件夹",
"noTerms": "至少输入一个搜索词。",
"termTooShort": "每个搜索词必须至少包含{minSearchLength} 个字符",
"caseExact": "区分大小写匹配",
"resultsTitle": "搜索结果",
"invalidSources": "至少打开一个信号源。",
"noResults": "没有匹配的文件或文件夹。"
@@ -807,7 +811,18 @@
"description": "查看当前会话的所有通知",
"empty": "尚无通知",
"clearAll": "全部清除",
"total": "{count} 通知"
"appEnabled": "应用通知",
"appEnabledDescription": "当此标签页处于后台时,在文件操作完成后显示浏览器通知。需要浏览器权限。仅存储于此浏览器中。",
"appUnsupported": "此浏览器不支持应用通知。",
"permissionGranted": "允许",
"permissionDeniedHelp": "通知已被屏蔽。请在浏览器设置中为该网站启用通知。",
"uploadTitle": "上传完成",
"downloadTitle": "下载完成",
"moveTitle": "移动完成",
"copyTitle": "复制完成",
"uploadFailedTitle": "上传失败",
"downloadFailedTitle": "下载失败",
"operationFailedTitle": "操作失败"
},
"events": {
"serverShutdown": "服务器已关闭。",
@@ -860,5 +875,8 @@
"documentDownload": "文件下载 - 完成",
"editorSuccess": "编辑器初始化 - 成功!所有步骤已完成。",
"setupError": "设置错误"
},
"pwa": {
"install": "安装应用"
}
}
Loaded 100 of 165 files, more files were not shown because too many files have changed in this diff. Show more