Compare commits

...
26 Commits
Author SHA1 Message Date
Graham Steffaniak 9d6e429a38 Last minute changes v1.3.1 (#2102) 2026-03-07 06:56:02 -06:00
Graham Steffaniak 5a7891c833 Symlink webdav fix (#2100) 2026-03-06 15:35:17 -06:00
Kevin B. 27e62213c3 Fix tools sidebar navigation (#2098)
Fixes #2097
2026-03-06 14:43:16 -05:00
Joel GodfreyandKevin B. b7c020f8a4 Fix download icon + layouts (#2087)
Co-authored-by: Kevin B. <kurami32_kbm@proton.me>
2026-03-06 13:32:17 -06:00
Graham Steffaniak b208e6396a added share changes (#2099) 2026-03-06 12:07:32 -06:00
Kevin B. 955a7de0d6 use closeTopHover instead of closeHovers in some places to close prompts (#2082) 2026-03-05 16:50:00 -05:00
Graham Steffaniak 17e2fce60a Small tweaks (#2076) 2026-03-05 14:59:21 -06:00
Kevin B. 9b8e9c6aef Fix ebook thumbnails showing (#2085) 2026-03-04 19:09:52 -05:00
Kevin B. be244cf0ee fix webm mimetype plus more media mimetypes (#2077) 2026-03-04 19:08:12 -05:00
Gabriel Górski cf196111e5 oidc: Extract group from keys in the map (#2084) 2026-03-04 15:32:28 -06:00
Graham Steffaniak c9dd63f326 updated with another webdav fix (#2070) 2026-03-03 17:05:55 -06:00
Kevin B. 5344a836f4 add option to disable files in tree and archive file deletion toggle (#2072) 2026-03-03 17:05:42 -06:00
Kevin B. 62a6be2852 Add option to disable audio and model thumbnails in settings (#2067) 2026-03-02 11:23:02 -05:00
Graham Steffaniak b18423a496 Hide share icon shares (#2066) 2026-03-01 12:16:10 -06:00
Graham Steffaniak 614e5bb826 updated with webdav fixes (#2065) 2026-03-01 11:46:55 -06:00
Graham Steffaniak 5c4a4d76da Merge branch 'dev/v1.3.1' of github.com:gtsteffaniak/filebrowser into dev/v1.3.1 2026-03-01 11:25:45 -06:00
Graham Steffaniak 46a884d07a Merge v1.2.2 v1.3.1 (#2062) 2026-03-01 10:51:08 -06:00
Graham Steffaniak e28f98f355 Merge branch 'staging/v1.2.2' into dev/v1.3.1 2026-03-01 10:30:58 -06:00
Graham Steffaniak 2af75311bd fix download issue for users with scope 2026-03-01 10:02:48 -06:00
Graham Steffaniak 82c0182c22 updated to use html/template (#2060) 2026-03-01 09:20:58 -06:00
Graham Steffaniak c8f0b5abea updated share link (#2059) 2026-03-01 09:18:03 -06:00
Graham Steffaniak d3d029d1e9 updated share link edit while on share (#2056) 2026-03-01 07:48:34 -06:00
Graham Steffaniak f39002f375 add strict sharing (#2053) 2026-02-28 18:30:55 -06:00
Graham Steffaniak a658f100c0 Fix database path loading (#2055) 2026-02-28 18:28:15 -06:00
Graham Steffaniak c2d255a25e updated deps (#2052) 2026-02-28 14:26:31 -06:00
Graham Steffaniak b0be26b99e updated dependencies (#2051) 2026-02-28 14:24:12 -06:00
117 changed files with 2912 additions and 1878 deletions

No files matched your search

+38
View File
@@ -2,6 +2,28 @@
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.3.1-beta
**Security**:
- Patched Stored XSS in public share page via unsanitized share metadata (text/template misuse) GHSA-r633-fcgp-m532
- Patched Incomplete Remediation of CVE-2026-27611: Password-Protected Share Bypass via /public/api/share/info GHSA-525j-95gf-766f
**New Features**:
- More user options for settings (#2072) (#2067):
- Option to disable thumbnails `userDefaults.preview.audio` and `userDefaults.preview.models` for Audio and 3D Models.
- Option to disable files in the Tree navigation `userDefaults.hideFilesInTree`
- Option to disable source files deletion when creating/extracting archives. `userDefaults.deleteAfterArchive`
**Notes**:
- Share icon does not show in share listing or for shares for other users.
- File Size Analyzer tool max items increased from 100 to 200.
- changed symlink detection logic.
**BugFixes**:
- 405 response code error on Webdav in 1.3.0-beta (#2054)
- Motion Preview setting not saving when changed in profile settings.
- Context menu on tools not working.
## v1.3.0-beta
Note: a potentially breaking change for docker users: \the default user is now "filebrowser" 1000:1000 instead of root.
@@ -59,6 +81,22 @@ Note: a potentially breaking change for docker users: \the default user is now "
**BugFixes**:
- Long folder names get cut off at top navigation bar (#1934)
## v1.2.2-stable
**Security**:
- Patched Stored XSS in public share page via unsanitized share metadata (text/template misuse)
**New Features**:
- "Divider" option in sidebar links to add a text or divider between links ()
- shares offer a "go to source Location" sidebar link and button when editing a share.
**Notes**:
- Share edit/delete permissions are scoped to the user's shares rather than global (#2050)
**BugFixes**:
- fixed the requirement that the database path needed to be set in the config file, now it loads `FILEBROWSER_DATABASE` value by default, fallback to config file property.
- Error downloading zipped directory: no such file or directory (users with scope) #2015
## v1.2.1-stable
**BugFixes**:
+3
View File
@@ -14,6 +14,9 @@ COPY [ "./backend/reduce-rounded-corners.css", "./no-rounded.css" ]
RUN mkdir -p ./http/dist
COPY [ "./backend/http/embed/", "./http/dist/"]
# generate duplicate 1.1MB files
RUN dd if=/dev/zero of=/app/frontend/tests/playwright-files/1.1MB.bin bs=1024 count=1126 && dd if=/dev/zero of=/app/frontend/tests/playwright-files/1.1mb.bin bs=1024 count=1126
# Run tests
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
RUN ./filebrowser set -u testuser1,testuser1
+3 -1
View File
@@ -8,8 +8,10 @@ userDefaults:
image: true
popup: true
video: true
audio: true
models: true
office: true
highQuality: true
permissions:
download: true
create: true
create: true
+3 -1
View File
@@ -26,8 +26,10 @@ userDefaults:
image: true
popup: true
video: true
audio: true
office: true
models: true
highQuality: true
permissions:
download: true
create: true
create: true
@@ -11,14 +11,14 @@ import { defineConfig, devices } from "@playwright/test";
*/
export default defineConfig({
globalSetup: "./tests/playwright/proxy-setup.ts",
timeout: 5000,
timeout: 10000,
testDir: "./tests/playwright/proxy",
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: false,
/* Retry on CI only */
retries: 2,
retries: 3,
/* Opt out of parallel tests on CI. */
workers: 1, // required for now! todo parallel some tests
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
+25 -9
View File
@@ -25,6 +25,9 @@ import (
"github.com/gtsteffaniak/go-logger/logger"
)
// CheckPermissionsFunc allows tests to override CheckPermissions behavior
var CheckPermissionsFunc = checkPermissionsImpl
// processDirectoryMetadata extracts metadata for audio/video files in directories
func processDirectoryMetadata(response *iteminfo.ExtendedFileInfo, idx *indexing.Index, opts utils.FileOptions) {
metadataCount := 0
@@ -116,7 +119,13 @@ func finalizeResponse(response *iteminfo.ExtendedFileInfo, info *iteminfo.FileIn
}
}
// CheckPermissions validates user access and returns the resolved index path and user scope
func CheckPermissions(opts utils.FileOptions, access *access.Storage, user *users.User) (string, string, error) {
return CheckPermissionsFunc(opts, access, user)
}
// checkPermissionsImpl is the actual implementation of CheckPermissions
func checkPermissionsImpl(opts utils.FileOptions, access *access.Storage, user *users.User) (string, string, error) {
if access == nil {
return "", "", fmt.Errorf("access not provided")
}
@@ -203,7 +212,15 @@ func GetDirItems(opts utils.FileOptions, access *access.Storage, user *users.Use
return items, nil
}
// 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)
}
// fileInfoFasterImpl is the actual implementation of FileInfoFaster
func fileInfoFasterImpl(opts utils.FileOptions, access *access.Storage, user *users.User, share *share.Storage) (*iteminfo.ExtendedFileInfo, error) {
response := &iteminfo.ExtendedFileInfo{}
indexPath, userScope, err := CheckPermissions(opts, access, user)
if err != nil {
@@ -239,21 +256,20 @@ func FileInfoFaster(opts utils.FileOptions, access *access.Storage, user *users.
return response, err
}
}
defer finalizeResponse(response, info, response.RealPath, user, userScope)
if opts.SkipExtendedAttrs {
return response, nil
}
if share != nil && user.Permissions.Share {
if share != 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)
file.IsShared = share.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)
folder.IsShared = share.IsShared(response.Path+folder.Name, idx.Path, user.ID)
}
response.IsShared = share.IsShared(response.Path, idx.Path)
response.IsShared = share.IsShared(response.Path, idx.Path, user.ID)
}
defer finalizeResponse(response, info, response.RealPath, user, userScope)
if opts.SkipExtendedAttrs {
return response, nil
}
// Process directory metadata if requested
if info.Type == "directory" {
+61 -6
View File
@@ -149,7 +149,10 @@ var types = map[string]string{
".es": "application/x-esrehber",
".etx": "text/x-setext",
".evy": "application/envoy",
".exe": "application/octet-stream",
".exe": "application/x-msdownload",
".dll": "application/x-msdownload",
".bat": "application/x-msdownload",
".msi": "application/x-msdownload",
".f77": "text/x-fortran",
".f90": "text/x-fortran",
".f": "text/x-fortran",
@@ -243,8 +246,8 @@ var types = map[string]string{
".la": "audio/nspaudio",
".lam": "audio/x-liveaudio",
".latex": "application/x-latex",
".lha": "application/lha",
".lhx": "application/octet-stream",
".lha": "application/x-lzh-compressed",
".lhx": "application/x-lzh-compressed",
".list": ContentTextHeaderValue,
".lma": "audio/nspaudio",
".log": ContentTextHeaderValue,
@@ -257,7 +260,7 @@ var types = map[string]string{
".m1v": "video/mpeg",
".m2a": "audio/mpeg",
".m2v": "video/mpeg",
".m3u": "audio/x-mpegurl",
".m3u": "text/x-mpegurl", // .m3u and .m3u8 are playlist files - They contain paths, we set it as text to allow edition.
".m": "text/x-m",
".man": "application/x-troff-man",
".manifest": "text/cache-manifest",
@@ -375,7 +378,7 @@ var types = map[string]string{
".pre": "application/x-freelance",
".prt": "application/pro_eng",
".ps": "application/postscript",
".psd": "application/octet-stream",
".psd": "image/vnd.adobe.photoshop",
".pvu": "paleovu/x-pv",
".pwz": "application/vndms-powerpoint",
".py": "text/x-scriptphyton",
@@ -597,11 +600,63 @@ var types = map[string]string{
".ogx": "application/ogg",
".m4v": "video/mp4",
".mkv": "video/x-matroska",
".webm": "video/webm",
".weba": "audio/webm",
".caf": "audio/x-caf",
".m3u8": "application/x-mpegURL",
".m3u8": "text/x-mpegURL",
".mpd": "application/dash+xml",
".webp": "image/webp",
".epub": "application/epub+zip",
// the ones below are some from: https://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
".adp": "audio/adpcm",
".mp4a": "audio/mp4",
".mp2a": "audio/mpeg",
".m3a": "audio/mpeg",
".sil": "audio/silk",
".uva": "audio/vnd.dece.audio",
".uvva": "audio/vnd.dece.audio",
".eol": "audio/vnd.digital-winds",
".dra": "audio/vnd.dra",
".dts": "audio/vnd.dts",
".dtshd": "audio/vnd.dts.hd",
".lvp": "audio/vnd.lucent.voice",
".pya": "audio/vnd.ms-playready.media.pya",
".rip": "audio/vnd.rip",
".mka": "audio/x-matroska",
".h261": "video/h261",
".h263": "video/h263",
".h264": "video/h264",
".jpgv": "video/jpeg",
".jpm": "video/jpm",
".jpgm": "video/jpm",
".mj2": "video/mj2",
".mjp2": "video/mj2",
".m2t": "video/mp2t",
".m2ts": "video/mp2t",
".mts": "video/mp2t",
".mp4v": "video/mp4",
".mpg4": "video/mp4",
".uvh": "video/vnd.dece.hd",
".uvvh": "video/vnd.dece.hd",
".uvm": "video/vnd.dece.mobile",
".uvvm": "video/vnd.dece.mobile",
".uvp": "video/vnd.dece.pd",
".uvvp": "video/vnd.dece.pd",
".uvs": "video/vnd.dece.sd",
".uvvs": "video/vnd.dece.sd",
".uvv": "video/vnd.dece.video",
".uvvv": "video/vnd.dece.video",
".dvb": "video/vnd.dvb.file",
".fvt": "video/vnd.fvt",
".mxu": "video/vnd.mpegurl",
".pyv": "video/vnd.ms-playready.media.pyv",
".uvu": "video/vnd.uvvu.mp4",
".uvvu": "video/vnd.uvvu.mp4",
".f4v": "video/x-f4v",
".mk3d": "video/x-matroska",
".mks": "video/x-matroska",
".vob": "video/x-ms-vob",
".smv": "video/x-smv",
}
//nolint:gochecknoinits
+1
View File
@@ -22,6 +22,7 @@ var (
ErrNoTotpConfigured = errors.New("OTP is enforced, but user is not yet configured")
ErrUnauthorized = errors.New("user unauthorized")
ErrNotIndexed = errors.New("directory or item excluded from indexing")
ErrNotViewable = errors.New("directory or item is not viewable")
ErrWrongLoginMethod = errors.New("user attempted to login with wrong login method")
ErrDownloadNotAllowed = errors.New("downloads are not allowed for this share")
ErrUploadNotAllowed = errors.New("upload permission not allowed for this share")
+5 -6
View File
@@ -667,14 +667,9 @@ func setDefaults(generate bool) Settings {
database := os.Getenv("FILEBROWSER_DATABASE")
if database == "" {
database = "database.db"
} else {
// check if database file exists
if _, err := os.Stat(database); os.IsNotExist(err) {
database = "database.db"
}
}
if _, err := os.Stat(database); os.IsNotExist(err) {
logger.Warning("database file could not be found. If this is unexpected, the default path is `./database.db`, but it can be configured in the config file under `server.database`.")
logger.Warning("database file could not be found. If this is unexpected, please set the FILEBROWSER_DATABASE environment variable to the correct path.")
}
s := Settings{
Server: Server{
@@ -712,8 +707,10 @@ func setDefaults(generate bool) Settings {
Name: "FileBrowser Quantum",
},
UserDefaults: UserDefaults{
DeleteAfterArchive: true,
DisableOnlyOfficeExt: ".md .txt .pdf .html .xml",
StickySidebar: true,
HideFilesInTree: false,
LockPassword: false,
ShowHidden: false,
DarkMode: boolPtr(true),
@@ -732,11 +729,13 @@ func setDefaults(generate bool) Settings {
Preview: UserDefaultsPreview{
Image: boolPtr(true),
Video: boolPtr(true),
Audio: boolPtr(true),
MotionVideoPreview: boolPtr(true),
Office: boolPtr(true),
PopUp: boolPtr(true),
AutoplayMedia: boolPtr(true),
Folder: boolPtr(true),
Models: boolPtr(true),
},
FileLoading: users.FileLoading{
MaxConcurrent: 10,
+5 -1
View File
@@ -68,6 +68,8 @@ func AdminPerms() users.Permissions {
func ApplyUserDefaults(u *users.User) {
u.StickySidebar = Config.UserDefaults.StickySidebar
u.DisableSettings = Config.UserDefaults.DisableSettings
u.HideFilesInTree = Config.UserDefaults.HideFilesInTree
u.DeleteAfterArchive = Config.UserDefaults.DeleteAfterArchive
// Handle DarkMode with default - dereference pointer from config
u.DarkMode = boolValueOrDefault(Config.UserDefaults.DarkMode, true)
@@ -90,12 +92,14 @@ func ApplyUserDefaults(u *users.User) {
u.Preview.DisableHideSidebar = Config.UserDefaults.Preview.DisableHideSidebar
u.Preview.Image = boolValueOrDefault(Config.UserDefaults.Preview.Image, true)
u.Preview.Video = boolValueOrDefault(Config.UserDefaults.Preview.Video, true)
u.Preview.Audio = boolValueOrDefault(Config.UserDefaults.Preview.Audio, true)
u.Preview.MotionVideoPreview = boolValueOrDefault(Config.UserDefaults.Preview.MotionVideoPreview, true)
u.Preview.Office = boolValueOrDefault(Config.UserDefaults.Preview.Office, true)
u.Preview.PopUp = boolValueOrDefault(Config.UserDefaults.Preview.PopUp, true)
u.Preview.AutoplayMedia = boolValueOrDefault(Config.UserDefaults.Preview.AutoplayMedia, true)
u.Preview.DefaultMediaPlayer = Config.UserDefaults.Preview.DefaultMediaPlayer
u.Preview.Folder = boolValueOrDefault(Config.UserDefaults.Preview.Folder, true)
u.Preview.Models = boolValueOrDefault(Config.UserDefaults.Preview.Models, true)
u.ShowHidden = Config.UserDefaults.ShowHidden
u.DateFormat = Config.UserDefaults.DateFormat
@@ -107,7 +111,7 @@ func ApplyUserDefaults(u *users.User) {
u.DisableOnlyOfficeExt = Config.UserDefaults.DisableOnlyOfficeExt
u.FileLoading = Config.UserDefaults.FileLoading
u.DisableOfficePreviewExt = Config.UserDefaults.DisableOfficePreviewExt
if len(u.Scopes) == 0 {
if len(u.Scopes) == 0 && u.Username != "anonymous" {
for _, source := range Config.Server.Sources {
if source.Config.DefaultEnabled {
u.Scopes = append(u.Scopes, users.SourceScope{
+4
View File
@@ -306,12 +306,14 @@ type UserDefaultsPreview struct {
HighQuality *bool `json:"highQuality"` // deprecated: always true in v1.3.0+
Image *bool `json:"image"` // show thumbnails for image files
Video *bool `json:"video"` // show thumbnails for video files
Audio *bool `json:"audio"` // show thumbnails for audio files
MotionVideoPreview *bool `json:"motionVideoPreview"` // show multiple frames for videos in thumbnail preview when hovering
Office *bool `json:"office"` // show thumbnails for office files
PopUp *bool `json:"popup"` // show larger popup preview when hovering over thumbnail
AutoplayMedia *bool `json:"autoplayMedia"` // autoplay media files in preview
DefaultMediaPlayer bool `json:"defaultMediaPlayer"` // disable the styled feature-rich media player for browser default
Folder *bool `json:"folder"` // show thumbnails for folders that have previewable contents
Models *bool `json:"models"` // show live thumbnails for 3D models files
}
// UserDefaultsPermissions holds permission settings with pointer types for defaults
@@ -334,6 +336,7 @@ type UserDefaults struct {
DisableQuickToggles bool `json:"disableQuickToggles"` // disable the quick toggles in the sidebar
DisableSearchOptions bool `json:"disableSearchOptions"` // disable the search options in the search bar
StickySidebar bool `json:"stickySidebar"` // keep sidebar open when navigating
HideFilesInTree bool `json:"hideFilesInTree"` // hide files in the sidebar tree navigation, when true, will show only directories.
DarkMode *bool `json:"darkMode"` // should dark mode be enabled
Locale string `json:"locale"` // language to use: eg. de, en, or fr
ViewMode string `json:"viewMode"` // view mode to use: eg. normal, list, grid, or compact
@@ -353,6 +356,7 @@ type UserDefaults struct {
LoginMethod string `json:"loginMethod,omitempty"` // login method to use: eg. password, proxy, oidc
DisableUpdateNotifications bool `json:"disableUpdateNotifications"` // disable update notifications banner for admin users
DeleteWithoutConfirming bool `json:"deleteWithoutConfirming"` // delete files without confirmation
DeleteAfterArchive bool `json:"deleteAfterArchive"` // delete source files after successful creation/extraction of archives
FileLoading users.FileLoading `json:"fileLoading"` // upload and download settings
DisableOfficePreviewExt string `json:"disableOfficePreviewExt"` // deprecated: use disablePreviewExt instead
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // list of file extensions to disable onlyoffice editor for
+1
View File
@@ -60,6 +60,7 @@ type FileOptions struct {
FollowSymlinks bool // whether to follow symlinks
Only string // whether to only get files or folders
SkipExtendedAttrs bool // whether to skip extended attributes
ShowSharedAttr bool // whether to show share attributes
}
// SanitizeUserPath prevents path traversal attacks by cleaning and validating user input.
+1
View File
@@ -8,3 +8,4 @@ userDefaults:
image: true
popup: true
video: true
audio: true
+5 -1
View File
@@ -11,6 +11,7 @@ userDefaults:
disableQuickToggles: false # disable the quick toggles in the sidebar
disableSearchOptions: false # disable the search options in the search bar
stickySidebar: true # keep sidebar open when navigating
hideFilesInTree: false # hide files in the sidebar tree navigation, when true, will show only directories.
darkMode: true # should dark mode be enabled (defaults to true if not set)
viewMode: "normal" # view mode to use: eg. normal, list, grid, or compact
singleClick: false # open directory on single click, also enables middle click to open in new tab
@@ -27,12 +28,14 @@ userDefaults:
disableHideSidebar: false # disable the hide sidebar preview for previews and editors
image: true # show thumbnail preview image for image files (defaults to true if not set)
video: true # show thumbnail preview image for video files (defaults to true if not set)
audio: true # show thumbnail preview image for audio files (defaults to true if not set)
motionVideoPreview: true # show multiple frames for videos in thumbnail preview when hovering (defaults to true if not set)
office: true # show thumbnail preview image for office files (defaults to true if not set)
popup: true # show larger popup preview when hovering over thumbnail (defaults to true if not set)
autoplayMedia: true # autoplay media files in preview (defaults to true if not set)
defaultMediaPlayer: false # disable html5 media player and use the default media player
folder: true # show thumbnail preview image for folder files (defaults to true if not set)
models: true # show live thumbnail preview for 3D Models files (defaults to true if not set)
permissions:
api: false # allow api access
admin: false # allow admin access
@@ -44,7 +47,8 @@ userDefaults:
download: true # allow downloading files (defaults to true if not set)
disableUpdateNotifications: false # disable update notifications banner for admin users
deleteWithoutConfirming: false # delete files without confirmation
deleteAfterArchive: true # delete source files after successful creation/extraction of archives
disableOnlyOfficeExt: ".md .txt .pdf .html .xml" # list of file extensions to disable onlyoffice editor for
customTheme: "" # Name of theme to use chosen from custom themes config.
showSelectMultiple: false # show select multiple files on desktop
debugOffice: false # debug onlyoffice editor
debugOffice: false # debug onlyoffice editor
+23 -4
View File
@@ -1,11 +1,12 @@
package share
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/database/users"
)
// IsSingleFileShare determines if this share is for a single file (not a directory).
@@ -144,10 +145,28 @@ func (l *Link) HasReachedUserLimit(username string) bool {
return count >= l.DownloadsLimit
}
func (l *Link) GetSourceName() (string, error) {
func (l *Link) GetSourceName() string {
sourceInfo, ok := settings.Config.Server.SourceMap[l.Source]
if !ok {
return "", fmt.Errorf("source not found")
return ""
}
return sourceInfo.Name, nil
return sourceInfo.Name
}
func (l *Link) UserCanEdit(user *users.User) bool {
return l.UserID == user.ID || user.Permissions.Admin
}
func (l *Link) SourceURL(user *users.User) string {
sourceName := l.GetSourceName()
// get user scope path from share
userScope, err := user.GetScopeForSourceName(sourceName)
if err != nil {
return ""
}
if !strings.HasPrefix(l.Path, userScope) {
return ""
}
scopedPath := strings.TrimPrefix(l.Path, userScope)
return "/" + filepath.Join("files", sourceName, scopedPath)
}
+2
View File
@@ -45,6 +45,8 @@ type CommonShare struct {
HasPassword bool `json:"hasPassword,omitempty"`
ShowHidden bool `json:"showHidden,omitempty"` // show hidden files in share (true = show, false = hide)
DisableLoginOption bool `json:"disableLoginOption"` // disable login option in share (true = hide, false = show)
SourceURL string `json:"sourceURL,omitempty"`
CanEditShare bool `json:"canEditShare,omitempty"`
}
type CreateBody struct {
CommonShare
+6 -1
View File
@@ -309,8 +309,13 @@ func (s *Storage) GetBySourcePath(path, source string) ([]*Link, error) {
}
// IsShared returns whether the given path and source have any shares in the cache.
func (s *Storage) IsShared(path, source string) bool {
func (s *Storage) IsShared(path, source string, id uint) bool {
links, _ := s.GetBySourcePath(path, source)
for _, l := range links {
if l.UserID == id {
return true
}
}
return len(links) > 0
}
+1
View File
@@ -18,6 +18,7 @@ import (
var userStore *users.Storage
func InitializeDb(path string) (*bolt.BoltStore, bool, error) {
logger.Debugf("Initializing database: %v", path)
exists, err := dbExists(path)
if err != nil {
panic(err)
+4
View File
@@ -56,12 +56,14 @@ type Preview struct {
DisableHideSidebar bool `json:"disableHideSidebar"` // disable the hide sidebar preview for previews and editors
Image bool `json:"image"` // show thumbnail preview image for image files
Video bool `json:"video"` // show thumbnail preview image for video files
Audio bool `json:"audio"` // show thumbnail preview image for audio files
MotionVideoPreview bool `json:"motionVideoPreview"` // show multiple frames for videos in thumbnail preview when hovering
Office bool `json:"office"` // show thumbnail preview image for office files
PopUp bool `json:"popup"` // show larger popup preview when hovering over thumbnail
AutoplayMedia bool `json:"autoplayMedia"` // autoplay media files in preview
DefaultMediaPlayer bool `json:"defaultMediaPlayer"` // disable html5 media player and use the default media player
Folder bool `json:"folder"` // show thumbnail preview image for folder files
Models bool `json:"models"` // show live thumbnails for 3D models files
}
// User describes a user.
@@ -122,6 +124,8 @@ type NonAdminEditable struct {
DebugOffice bool `json:"debugOffice"` // debug onlyoffice editor
OtpEnabled bool `json:"otpEnabled"` // allow non-admin users to disable their own OTP
SidebarLinks []SidebarLink `json:"sidebarLinks"` // customizable sidebar links
HideFilesInTree bool `json:"hideFilesInTree"` // hide files in the sidebar tree navigation, when true, will show only directories.
DeleteAfterArchive bool `json:"deleteAfterArchive"` // delete source files after successful creation/extraction of archives
}
type FileLoading struct {
+17 -17
View File
@@ -16,7 +16,7 @@ require (
github.com/google/go-cmp v0.7.0
github.com/gtsteffaniak/go-cache v1.1.0
github.com/gtsteffaniak/go-logger v1.0.2
github.com/kovidgoyal/imaging v1.8.19
github.com/kovidgoyal/imaging v1.8.20
github.com/mattn/go-sqlite3 v1.14.34
github.com/pquerna/otp v1.5.0
github.com/spf13/afero v1.15.0
@@ -26,13 +26,13 @@ require (
golang.org/x/crypto v0.48.0
golang.org/x/image v0.36.0
golang.org/x/mod v0.33.0
golang.org/x/net v0.50.0
golang.org/x/net v0.51.0
golang.org/x/oauth2 v0.35.0
golang.org/x/sys v0.41.0
golang.org/x/time v0.14.0
golang.org/x/tools v0.42.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.45.0
modernc.org/sqlite v1.46.1
)
require (
@@ -101,7 +101,7 @@ require (
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.0-alpha.4 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
@@ -114,16 +114,16 @@ require (
github.com/go-critic/go-critic v0.14.3 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/spec v0.22.3 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/jsonname v0.25.4 // indirect
github.com/go-openapi/swag/jsonutils v0.25.4 // indirect
github.com/go-openapi/swag/loading v0.25.4 // indirect
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/go-openapi/jsonpointer v0.22.5 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/spec v0.22.4 // indirect
github.com/go-openapi/swag/conv v0.25.5 // indirect
github.com/go-openapi/swag/jsonname v0.25.5 // indirect
github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
github.com/go-openapi/swag/loading v0.25.5 // indirect
github.com/go-openapi/swag/stringutils v0.25.5 // indirect
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
github.com/go-openapi/swag/yamlutils v0.25.5 // 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
@@ -139,7 +139,7 @@ require (
github.com/godoc-lint/godoc-lint v0.11.2 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gohugoio/hugo v0.149.1 // indirect
github.com/golang/geo v0.0.0-20260129164528-943061e2742c // indirect
github.com/golang/geo v0.0.0-20260302211937-87f5a40ea07a // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/asciicheck v0.5.0 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
@@ -273,7 +273,7 @@ require (
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // 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.19.0 // indirect
golang.org/x/text v0.34.0 // indirect
@@ -281,7 +281,7 @@ require (
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.68.0 // indirect
modernc.org/libc v1.69.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
mvdan.cc/gofumpt v0.9.2 // indirect
+44 -44
View File
@@ -248,8 +248,8 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.0-alpha.4 h1:JzPbdf+cqbyT9sZtP4xnqelwUXwf7LvD8xKS6+ofTds=
github.com/ebitengine/purego v0.10.0-alpha.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -306,33 +306,33 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc=
github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=
github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=
github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=
github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4=
github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA=
github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM=
github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s=
github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE=
github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8=
github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0=
github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw=
github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE=
github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw=
github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4=
github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg=
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=
github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=
github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=
github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=
github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=
github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=
github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=
github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=
github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=
github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=
github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag=
github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM=
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -401,8 +401,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20260129164528-943061e2742c h1:ysO2h2Odnl1AJM1I2Lm/fa6JvO0pECMSt2CwBaa+ITo=
github.com/golang/geo v0.0.0-20260129164528-943061e2742c/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8=
github.com/golang/geo v0.0.0-20260302211937-87f5a40ea07a h1:7f/cr+n7Jtd7mzCLrx47sdVIGabpSSxCHrf6fJczDp0=
github.com/golang/geo v0.0.0-20260302211937-87f5a40ea07a/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=
@@ -580,8 +580,8 @@ github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811ui
github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo=
github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds=
github.com/kovidgoyal/imaging v1.8.19 h1:zWJdQqF2tfSKjvoB7XpLRhVGbYsze++M0iaqZ4ZkhNk=
github.com/kovidgoyal/imaging v1.8.19/go.mod h1:I0q8RdoEuyc4G8GFOF9CaluTUHQSf68d6TmsqpvfRI8=
github.com/kovidgoyal/imaging v1.8.20 h1:74GZ7C2rIm3rqmGEjK1GvvPOOnJ0SS5iDOa6Flfo0b0=
github.com/kovidgoyal/imaging v1.8.20/go.mod h1:d3phGYkTChGYkY4y++IjpHgUGhWGELDc2NEQAqxwZZg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -940,8 +940,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk=
@@ -1019,8 +1019,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
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=
@@ -1293,18 +1293,18 @@ 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.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so=
modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/ccgo/v4 v4.31.0 h1:/bsaxqdgX3gy/0DboxcvWrc3NpzH+6wpFfI/ZaA/hrg=
modernc.org/ccgo/v4 v4.31.0/go.mod h1:jKe8kPBjIN/VdGTVqARTQ8N1gAziBmiISY8j5HoKwjg=
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=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/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.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ=
modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0=
modernc.org/libc v1.69.0 h1:YQJ5QMSReTgQ3QFmI0dudfjXIjCcYTUxcH8/9P9f0D8=
modernc.org/libc v1.69.0/go.mod h1:YfLLduUEbodNV2xLU5JOnRHBTAHVHsVW3bVYGw0ZCV4=
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=
@@ -1313,8 +1313,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/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.45.0 h1:r51cSGzKpbptxnby+EIIz5fop4VuE4qFoVEjNvWoObs=
modernc.org/sqlite v1.45.0/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
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=
+17 -60
View File
@@ -344,22 +344,10 @@ func addFile(source string, path string, d *requestContext, tarWriter *tar.Write
}
// Check access control directly for each file and silently skip if access is denied
if d.share == nil && store.Access != nil {
if !store.Access.Permitted(idx.Path, path, d.user.Username) {
return nil // Silently skip this file/folder
}
if !store.Access.Permitted(idx.Path, path, d.user.Username) {
return nil // Silently skip this file/folder
}
// Verify file exists
_, err := files.FileInfoFaster(utils.FileOptions{
Path: path,
Source: source,
Expand: false,
FollowSymlinks: true,
}, store.Access, d.user, store.Share)
if err != nil {
return err
}
realPath, _, _ := idx.GetRealPath(path)
info, err := os.Stat(realPath)
if err != nil {
@@ -490,27 +478,11 @@ func computeArchiveSize(source string, fileList []string, d *requestContext) (in
return 0, fmt.Errorf("source %s is not available", source)
}
var userScope string
var err error
if d.share == nil {
userScope, err = d.user.GetScopeForSourceName(source)
if err != nil {
return 0, fmt.Errorf("source %s is not available for user %s", source, d.user.Username)
}
}
for _, path := range fileList {
var fullPath string
if d.share == nil {
fullPath = utils.JoinPathAsUnix(userScope, path)
if store.Access != nil && !store.Access.Permitted(idx.Path, fullPath, d.user.Username) {
continue
}
} else {
fullPath = path
if !store.Access.Permitted(idx.Path, path, d.user.Username) {
continue
}
realPath, isDir, err := idx.GetRealPath(fullPath)
realPath, isDir, err := idx.GetRealPath(path)
if err != nil {
return 0, err
}
@@ -537,10 +509,10 @@ func createZip(d *requestContext, source string, tmpPath string, filenames ...st
zipWriter := zip.NewWriter(file)
for _, fname := range filenames {
err := addFile(source, fname, d, nil, zipWriter, false)
for _, filepath := range filenames {
err := addFile(source, filepath, d, nil, zipWriter, false)
if err != nil {
logger.Errorf("Failed to add %s to ZIP: %v", fname, err)
logger.Errorf("Failed to add %s to ZIP: %v", filepath, err)
return err
}
}
@@ -562,10 +534,10 @@ func createTarGz(d *requestContext, source string, tmpPath string, filenames ...
gzWriter := gzip.NewWriter(file)
tarWriter := tar.NewWriter(gzWriter)
for _, fname := range filenames {
err := addFile(source, fname, d, tarWriter, nil, false)
for _, filepath := range filenames {
err := addFile(source, filepath, d, tarWriter, nil, false)
if err != nil {
logger.Errorf("Failed to add %s to TAR.GZ: %v", fname, err)
logger.Errorf("Failed to add %s to TAR.GZ: %v", filepath, err)
return err
}
}
@@ -600,10 +572,10 @@ func createTarGzWithLevel(d *requestContext, source string, destPath string, lev
tarWriter := tar.NewWriter(gzWriter)
defer tarWriter.Close()
for _, fname := range filenames {
err := addFile(source, fname, d, tarWriter, nil, false)
for _, filepath := range filenames {
err := addFile(source, filepath, d, tarWriter, nil, false)
if err != nil {
logger.Errorf("Failed to add %s to TAR.GZ: %v", fname, err)
logger.Errorf("Failed to add %s to TAR.GZ: %v", filepath, err)
return err
}
}
@@ -616,32 +588,18 @@ func createTarGzWithLevel(d *requestContext, source string, destPath string, lev
// BuildAndStreamArchive resolves paths, creates a zip or tar.gz archive, and streams it to w.
// It respects access rules and max archive size. Used only by the raw handler for multi-file/directory download.
func BuildAndStreamArchive(w http.ResponseWriter, r *http.Request, d *requestContext, source string, fileList []string) (int, error) {
firstFilePath := fileList[0]
var userscope string
var err error
if d.share == nil {
userscope, err = d.user.GetScopeForSourceName(source)
if err != nil {
return http.StatusForbidden, err
}
firstFilePath = utils.JoinPathAsUnix(userscope, firstFilePath)
}
idx := indexing.GetIndex(source)
if idx == nil {
return http.StatusInternalServerError, fmt.Errorf("source %s is not available", source)
}
realPath, isDir, err := idx.GetRealPath(firstFilePath)
realPath, isDir, err := idx.GetRealPath(fileList[0])
if err != nil {
return http.StatusInternalServerError, err
return http.StatusInternalServerError, fmt.Errorf("failed to get real path for %s: %v", fileList[0], err)
}
estimatedSize, err := computeArchiveSize(source, fileList, d)
if err != nil {
return http.StatusInternalServerError, err
}
if config.Server.MaxArchiveSizeGB > 0 {
maxSize := config.Server.MaxArchiveSizeGB * 1024 * 1024 * 1024
if estimatedSize > maxSize {
@@ -660,7 +618,7 @@ func BuildAndStreamArchive(w http.ResponseWriter, r *http.Request, d *requestCon
return http.StatusInternalServerError, errors.New("format not implemented")
}
baseDirName := filepath.Base(filepath.Dir(firstFilePath))
baseDirName := filepath.Base(filepath.Dir(realPath))
if baseDirName == "" || baseDirName == "/" {
baseDirName = "download"
}
@@ -711,7 +669,6 @@ func BuildAndStreamArchive(w http.ResponseWriter, r *http.Request, d *requestCon
_, err = io.Copy(w, reader)
os.Remove(archiveData)
if err != nil {
logger.Errorf("Failed to copy archive data to response: %v", err)
return http.StatusInternalServerError, err
}
return 0, nil
+5 -1
View File
@@ -139,6 +139,7 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
var documentId string
var logContext *OnlyOfficeLogContext
// modify all filepaths for user scope
if d.share == nil {
userscope, err = d.user.GetScopeForSourceName(source)
if err != nil {
@@ -160,8 +161,11 @@ func rawFilesHandler(w http.ResponseWriter, r *http.Request, d *requestContext,
}
return http.StatusForbidden, err
}
firstFilePath = utils.JoinPathAsUnix(userscope, firstFilePath)
for i, filePath := range fileList {
fileList[i] = utils.JoinPathAsUnix(userscope, filePath)
}
}
firstFilePath = fileList[0]
// For shares, the path is already correctly resolved by publicRawHandler
idx := indexing.GetIndex(source)
if idx == nil {
+1 -1
View File
@@ -6,10 +6,10 @@ import (
"embed"
"encoding/json"
"fmt"
"html/template"
"io/fs"
"net"
"net/http"
"text/template"
"time"
_ "net/http/pprof"
+3
View File
@@ -4,6 +4,7 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"maps"
"net/http"
"net/url"
"slices"
@@ -45,6 +46,8 @@ func (u *userInfoUnmarshaller) UnmarshalJSON(data []byte) error {
if u.groupsClaim != "" {
if v, ok := raw[u.groupsClaim]; ok {
switch val := v.(type) {
case map[string]interface{}:
u.userInfo.Groups = slices.Collect(maps.Keys(val))
case []interface{}:
groups := make([]string, len(val))
for i, g := range val {
+3 -1
View File
@@ -119,6 +119,7 @@ func getDirectoryPreview(r *http.Request, d *requestContext, frameIndex int) (*i
source := d.fileInfo.Source
path := utils.JoinPathAsUnix(d.fileInfo.Path, name)
user := d.user
if d.share != nil {
sourceInfo, ok := settings.Config.Server.SourceMap[d.share.Source]
if !ok {
@@ -126,6 +127,7 @@ func getDirectoryPreview(r *http.Request, d *requestContext, frameIndex int) (*i
}
source = sourceInfo.Name
path = d.IndexPath + name
user = d.shareUser
}
fileInfo, err := files.FileInfoFaster(
utils.FileOptions{
@@ -133,7 +135,7 @@ func getDirectoryPreview(r *http.Request, d *requestContext, frameIndex int) (*i
Source: source,
AlbumArt: true,
Metadata: true,
}, store.Access, d.user, store.Share)
}, store.Access, user, store.Share)
if err != nil {
return nil, err
}
+7 -7
View File
@@ -245,8 +245,8 @@ func publicPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
if !d.share.AllowModify {
return http.StatusForbidden, fmt.Errorf("create is not allowed for this share")
}
source, err := d.share.GetSourceName()
if err != nil {
sourceName := d.share.GetSourceName()
if sourceName == "" {
return http.StatusNotFound, fmt.Errorf("source not available")
}
@@ -263,7 +263,7 @@ func publicPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
}
resolvedPath := utils.JoinPathAsUnix(d.share.Path, cleanPath)
err = files.WriteFile(source, resolvedPath, r.Body)
err = files.WriteFile(sourceName, resolvedPath, r.Body)
// hide the error
if err != nil {
logger.Errorf("public put handler: error updating resource with error %v", err)
@@ -343,8 +343,8 @@ func publicPatchHandler(w http.ResponseWriter, r *http.Request, d *requestContex
}
// Get the source from the share
source, err := d.share.GetSourceName()
if err != nil {
sourceName := d.share.GetSourceName()
if sourceName == "" {
return http.StatusNotFound, fmt.Errorf("source not available")
}
@@ -370,9 +370,9 @@ func publicPatchHandler(w http.ResponseWriter, r *http.Request, d *requestContex
// Note: Share paths are absolute, so we don't strip user scope here
// resourcePatchHandler will skip adding scope for shares
for i := range req.Items {
req.Items[i].FromSource = source
req.Items[i].FromSource = sourceName
req.Items[i].FromPath = utils.JoinPathAsUnix(d.share.Path, req.Items[i].FromPath)
req.Items[i].ToSource = source
req.Items[i].ToSource = sourceName
req.Items[i].ToPath = utils.JoinPathAsUnix(d.share.Path, req.Items[i].ToPath)
}
d.Data = req
+6 -5
View File
@@ -86,6 +86,7 @@ func resourceGetHandler(w http.ResponseWriter, r *http.Request, d *requestContex
ExtractEmbeddedSubtitles: settings.Config.Integrations.Media.ExtractEmbeddedSubtitles,
ShowHidden: d.user.ShowHidden,
SkipExtendedAttrs: skipExtendedAttrs,
ShowSharedAttr: true,
}, store.Access, d.user, store.Share)
if err != nil {
return errToStatus(err), err
@@ -255,12 +256,12 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
}
if d.share != nil {
source, err := d.share.GetSourceName()
if err != nil {
sourceName := d.share.GetSourceName()
if sourceName == "" {
return http.StatusNotFound, fmt.Errorf("source not available")
}
// get user scope path from share
userScope, err := d.shareUser.GetScopeForSourceName(source)
userScope, err := d.shareUser.GetScopeForSourceName(sourceName)
if err != nil {
response.Failed = append(response.Failed, BulkDeleteItem{
Source: item.Source,
@@ -275,7 +276,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
FollowSymlinks: true,
Path: indexPath,
Source: source,
Source: sourceName,
ShowHidden: true,
}, store.Access, filePermUser, store.Share)
if err != nil {
@@ -283,7 +284,7 @@ func resourceBulkDeleteHandler(w http.ResponseWriter, r *http.Request, d *reques
}
// Delete the file/directory
err = files.DeleteFiles(source, fileInfo.RealPath, fileInfo.Type == "directory")
err = files.DeleteFiles(sourceName, fileInfo.RealPath, fileInfo.Type == "directory")
if err != nil {
logger.Errorf("resource bulk delete handler: error deleting file/directory: %v", err)
response.Failed = append(response.Failed, BulkDeleteItem{
+7 -2
View File
@@ -83,6 +83,11 @@ func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
return http.StatusBadRequest, err
}
searchSize := indexing.DefaultSearchResults
if searchOptions.largest {
searchSize = 200
}
var response []*indexing.SearchResult
if len(searchOptions.sources) == 1 {
// Single source - use the existing Search method for backward compatibility
@@ -91,10 +96,10 @@ func searchHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
return http.StatusBadRequest, fmt.Errorf("index not found for source %s", searchOptions.sources[0])
}
combinedPath := searchOptions.combinedPath[searchOptions.sources[0]]
response = index.Search(searchOptions.query, combinedPath, searchOptions.sessionId, searchOptions.largest, indexing.DefaultSearchResults)
response = index.Search(searchOptions.query, combinedPath, searchOptions.sessionId, searchOptions.largest, searchSize)
} else {
// Multiple sources - use the new SearchMultiSources function
response = indexing.SearchMultiSources(searchOptions.query, searchOptions.sources, searchOptions.combinedPath, searchOptions.sessionId, searchOptions.largest, indexing.DefaultSearchResults)
response = indexing.SearchMultiSources(searchOptions.query, searchOptions.sources, searchOptions.combinedPath, searchOptions.sessionId, searchOptions.largest, searchSize)
}
// Filter out items that are not permitted according to access rules and trim user scope from paths
+40 -12
View File
@@ -33,14 +33,10 @@ type ShareResponse struct {
}
// convertToFrontendShareResponse converts shares to response format with usernames
func convertToFrontendShareResponse(r *http.Request, shares []*share.Link) ([]*ShareResponse, error) {
func convertToFrontendShareResponse(r *http.Request, shares []*share.Link, user *users.User) ([]*ShareResponse, error) {
responses := make([]*ShareResponse, 0, len(shares))
for _, s := range shares {
user, err := store.Users.Get(s.UserID)
username := ""
if err == nil {
username = user.Username
}
username := user.Username
// Get source info to convert path to name for frontend
sourceInfo, ok := config.Server.SourceMap[s.Source]
@@ -62,6 +58,9 @@ func convertToFrontendShareResponse(r *http.Request, shares []*share.Link) ([]*S
s.CommonShare.HasPassword = s.HasPassword()
s.DownloadURL = getShareURL(r, s.Hash, true, s.Token)
s.ShareURL = getShareURL(r, s.Hash, false, s.Token)
if s.UserCanEdit(user) {
s.CommonShare.SourceURL = s.SourceURL(user)
}
// Create response with source name (overrides the embedded Link's source field)
responses = append(responses, &ShareResponse{
Link: s,
@@ -94,7 +93,7 @@ func shareListHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
return http.StatusInternalServerError, err
}
shares = utils.NonNilSlice(shares)
sharesWithUsernames, err := convertToFrontendShareResponse(r, shares)
sharesWithUsernames, err := convertToFrontendShareResponse(r, shares, d.user)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -134,7 +133,7 @@ func shareGetHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
if err != nil {
return http.StatusInternalServerError, fmt.Errorf("error getting share info from server")
}
sharesWithUsernames, err := convertToFrontendShareResponse(r, s)
sharesWithUsernames, err := convertToFrontendShareResponse(r, s, d.user)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -159,7 +158,16 @@ func shareDeleteHandler(w http.ResponseWriter, r *http.Request, d *requestContex
return http.StatusBadRequest, nil
}
err := store.Share.Delete(hash)
// only allow users to delete their own shares
thisShare, err := store.Share.GetByHash(hash)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("share not found")
}
if thisShare.UserID != d.user.ID && !d.user.Permissions.Admin {
return http.StatusForbidden, fmt.Errorf("you are not allowed to delete this share")
}
err = store.Share.Delete(hash)
if err != nil {
return errToStatus(err), err
}
@@ -193,8 +201,16 @@ func sharePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext
return http.StatusBadRequest, fmt.Errorf("hash and path are required")
}
// only allow users to update their own shares
thisShare, err := store.Share.GetByHash(body.Hash)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("share not found")
}
if thisShare.UserID != d.user.ID && !d.user.Permissions.Admin {
return http.StatusForbidden, fmt.Errorf("you are not allowed to update this share")
}
// Update the share path
err := store.Share.UpdateSharePath(body.Hash, body.Path)
err = store.Share.UpdateSharePath(body.Hash, body.Path)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -206,7 +222,7 @@ func sharePatchHandler(w http.ResponseWriter, r *http.Request, d *requestContext
}
// Convert to response format
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{updatedShare})
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{updatedShare}, d.user)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -386,7 +402,7 @@ func sharePostHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
if err = store.Share.Save(s); err != nil {
return http.StatusInternalServerError, err
}
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{s})
sharesWithUsernames, err := convertToFrontendShareResponse(r, []*share.Link{s}, d.user)
if err != nil {
return http.StatusInternalServerError, err
}
@@ -618,6 +634,7 @@ func shareInfoHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
}
commonShare.Source = ""
commonShare.Path = ""
commonShare.DownloadURL = ""
commonShare.SidebarLinks = []users.SidebarLink{}
for _, link := range shareLink.SidebarLinks {
if link.Category == "download" && shareLink.ShareType == "upload" {
@@ -626,6 +643,17 @@ func shareInfoHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
commonShare.SidebarLinks = append(commonShare.SidebarLinks, link)
}
}
commonShare.SourceURL = shareLink.SourceURL(d.user)
commonShare.CanEditShare = shareLink.UserCanEdit(d.user)
if commonShare.SourceURL != "" {
link := users.SidebarLink{
Name: "sourceLocation",
Category: "custom",
Target: commonShare.SourceURL,
}
commonShare.SidebarLinks = append(commonShare.SidebarLinks, link)
}
return renderJSON(w, r, commonShare)
}
+17 -9
View File
@@ -3,12 +3,12 @@ package http
import (
"encoding/json"
"fmt"
"html/template"
"io/fs"
"net/http"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
@@ -25,10 +25,18 @@ type TemplateRenderer struct {
// Render renders a template document with headers and data
func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data interface{}) error {
// If in dev mode, reload templates on every render.
templates := t.templates
// If in dev mode, reload templates on every render by creating a fresh instance
if t.devMode {
var err error
t.templates, err = t.templates.ParseFS(assetFs, "public/index.html")
templates = template.New("").Funcs(template.FuncMap{
"marshal": func(v interface{}) (string, error) {
var a []byte
a, err = json.Marshal(v)
return string(a), err
},
})
templates, err = templates.ParseFS(assetFs, "public/index.html")
if err != nil {
return fmt.Errorf("error reloading template: %w", err)
}
@@ -39,7 +47,7 @@ func (t *TemplateRenderer) Render(w http.ResponseWriter, name string, data inter
w.Header().Set("X-Accel-Expires", "0")
w.Header().Set("Transfer-Encoding", "identity")
// Execute the template with the provided data
return t.templates.ExecuteTemplate(w, name, data)
return templates.ExecuteTemplate(w, name, data)
}
func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestContext, file, contentType string) (int, error) {
@@ -180,8 +188,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
data["htmlVars"] = map[string]interface{}{
"title": title,
"customCSS": config.Frontend.Styling.CustomCSSRaw,
"userSelectedTheme": userSelectedTheme,
"customCSS": template.CSS(config.Frontend.Styling.CustomCSSRaw),
"userSelectedTheme": template.CSS(userSelectedTheme),
"lightBackground": config.Frontend.Styling.LightBackground,
"darkBackground": config.Frontend.Styling.DarkBackground,
"staticURL": staticURL,
@@ -192,7 +200,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
"winIcon": staticURL + "/mstile-256x256.png",
"appIcon": staticURL + "/apple-touch-icon.png",
"description": description,
"loadingSpinnersCSS": loadingSpinnersCSS,
"loadingSpinnersCSS": template.CSS(loadingSpinnersCSS),
"banner": banner,
"image": ogImage,
"url": fullURL,
@@ -241,8 +249,8 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
return http.StatusInternalServerError, fmt.Errorf("error marshaling globalVars: %w", err)
}
// Replace with JSON strings for direct template usage
data["globalVars"] = string(globalVarsJSON)
// Mark as JS for html/template to avoid escaping
data["globalVars"] = template.JS(globalVarsJSON)
// Render the template with global variables
if err := templateRenderer.Render(w, file, data); err != nil {
+197 -65
View File
@@ -2,6 +2,7 @@ package http
import (
"context"
"errors"
"fmt"
"net/http"
"os"
@@ -13,6 +14,7 @@ import (
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/fileutils"
commonerrors "github.com/gtsteffaniak/filebrowser/backend/common/errors"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
"github.com/gtsteffaniak/filebrowser/backend/database/users"
"github.com/gtsteffaniak/filebrowser/backend/indexing"
@@ -43,20 +45,22 @@ type filteredFileSystem struct {
fs webdav.FileSystem
source string
user *users.User
userscope string
userScope string
// Cache FileInfoFaster results per path to avoid redundant calls within the same request
fileInfoCache map[string]*iteminfo.ExtendedFileInfo
}
// getCachedFileInfo retrieves cached FileInfoFaster result, or calls FileInfoFaster and caches it
func (ffs *filteredFileSystem) getCachedFileInfo(permissionPath string, expand bool) (*iteminfo.ExtendedFileInfo, error) {
// getFileInfo retrieves file information with caching and access control
// requestPath should NOT include user scope - FileInfoFaster applies it internally
// This function prefers cache, then tries FileInfoFaster which applies access control
func (ffs *filteredFileSystem) getFileInfo(requestPath string, expand bool) (*iteminfo.ExtendedFileInfo, error) {
// Initialize cache if needed
if ffs.fileInfoCache == nil {
ffs.fileInfoCache = make(map[string]*iteminfo.ExtendedFileInfo)
}
// Check cache - first try exact match
cacheKey := permissionPath
// Create cache key (don't modify requestPath itself!)
cacheKey := requestPath
if expand {
cacheKey += ":expand"
}
@@ -67,34 +71,33 @@ func (ffs *filteredFileSystem) getCachedFileInfo(permissionPath string, expand b
// If we're looking for non-expanded but have expanded cached, we can use it
// (expanded contains all the same info plus more)
if !expand {
expandedKey := permissionPath + ":expand"
expandedKey := requestPath + ":expand"
if cached, found := ffs.fileInfoCache[expandedKey]; found {
return cached, nil
}
}
// Call FileInfoFaster
// Call FileInfoFaster with clean requestPath (without scope or cache suffix)
// FileInfoFaster applies user scope internally AND enforces access control
fileInfo, err := files.FileInfoFaster(utils.FileOptions{
Path: permissionPath,
Path: requestPath,
Source: ffs.source,
Expand: expand,
ShowHidden: ffs.user.ShowHidden,
SkipExtendedAttrs: true,
FollowSymlinks: true,
}, store.Access, ffs.user, store.Share)
if err != nil {
return nil, err
}
// Cache result
// Cache result using the cache key
ffs.fileInfoCache[cacheKey] = fileInfo
// If we got an expanded directory listing, also cache individual file entries
// so Stat calls on those files can use the cache
if expand && fileInfo.Type == "directory" {
dirPath := permissionPath
if !strings.HasSuffix(dirPath, "/") && dirPath != "" {
dirPath += "/"
}
dirPath := requestPath
// Cache each file and folder from the directory listing
for _, file := range fileInfo.Files {
filePath := utils.JoinPathAsUnix(dirPath, file.Name)
@@ -121,12 +124,83 @@ func (ffs *filteredFileSystem) getCachedFileInfo(permissionPath string, expand b
return fileInfo, nil
}
// checkAccess validates if the user can access a given path
// This is used by all write operations (mkdir, delete, rename, etc.)
// Returns nil if access is allowed, error otherwise
func (ffs *filteredFileSystem) checkAccess(requestPath string) error {
// First, validate permissions using CheckPermissions
indexPath, _, err := files.CheckPermissions(utils.FileOptions{
FollowSymlinks: false,
Path: requestPath,
Source: ffs.source,
ShowHidden: ffs.user.ShowHidden,
}, store.Access, ffs.user)
if err != nil {
logger.Debugf("checkAccess: CheckPermissions denied for %s: %v", requestPath, err)
return err
}
// Try to get file info to verify it's accessible
// Use non-expanded to be faster (we just need to know if it exists and is accessible)
_, err = ffs.getFileInfo(requestPath, false)
if err == nil {
// Successfully got info - access allowed
return nil
}
// Handle specific error cases
if errors.Is(err, commonerrors.ErrAccessDenied) {
logger.Debugf("checkAccess: access explicitly denied for %s", requestPath)
return os.ErrPermission
}
// CRITICAL: If item is not indexed AND not viewable, deny access
// This prevents WebDAV from creating/modifying files in non-indexed areas
if errors.Is(err, commonerrors.ErrNotViewable) {
logger.Debugf("checkAccess: path not viewable for %s", requestPath)
return os.ErrPermission
}
// If not indexed but potentially viewable, we need to check more carefully
if errors.Is(err, commonerrors.ErrNotIndexed) {
// Get the index to check viewability
idx := indexing.GetIndex(ffs.source)
if idx == nil {
return fmt.Errorf("source not found")
}
// Check if the item is viewable using GetFileInfo (without expand)
info, getErr := idx.GetFileInfo(indexing.FileInfoRequest{
IndexPath: indexPath,
FollowSymlinks: false,
ShowHidden: ffs.user.ShowHidden,
Expand: false,
SkipExtendedAttrs: true,
})
// If GetFileInfo succeeds, the item is viewable despite not being indexed
if getErr == nil && info != nil {
logger.Debugf("checkAccess: path not indexed but viewable for %s", requestPath)
return nil // Allow access to viewable items
}
// Not viewable - deny access
logger.Debugf("checkAccess: path not indexed and not viewable for %s", requestPath)
return os.ErrPermission
}
// For other errors (like file not found), that's okay for new file/directory creation
// The caller will handle whether the operation is appropriate
logger.Debugf("checkAccess: path check returned: %v (may be acceptable for new items)", err)
return nil
}
// filteredFile wraps a webdav.File and filters Readdir results based on FileInfoFaster
type filteredFile struct {
webdav.File
fs *filteredFileSystem
dirPath string // The directory path this file represents (relative to scope root)
isDir bool // Whether this is a directory
fs *filteredFileSystem
requestPath string // The request path (without user scope)
isDir bool // Whether this is a directory
}
func (ff *filteredFile) Readdir(count int) ([]os.FileInfo, error) {
@@ -135,21 +209,23 @@ func (ff *filteredFile) Readdir(count int) ([]os.FileInfo, error) {
return ff.File.Readdir(count)
}
// For directories, use FileInfoFaster to get filtered contents
// Convert dirPath (relative to scope root) to permission path (relative to userscope)
// Handle root path specially
var permissionPath string
if ff.dirPath == "" || ff.dirPath == "/" {
permissionPath = ff.fs.userscope
} else {
permissionPath = utils.JoinPathAsUnix(ff.fs.userscope, ff.dirPath)
}
// Use cached FileInfoFaster result if available
fileInfo, err := ff.fs.getCachedFileInfo(permissionPath, true)
// Pass the requestPath (without scope) to getCachedFileInfo
// FileInfoFaster will apply the user's scope internally
fileInfo, err := ff.fs.getFileInfo(ff.requestPath, true)
if err != nil {
logger.Debugf("readdir: getFileInfo failed for requestPath=%s: %v", ff.requestPath, err)
// Handle errors gracefully - return empty directory for access/indexing issues
// This is especially important when user's scope points to a non-viewable directory
if errors.Is(err, commonerrors.ErrAccessDenied) ||
errors.Is(err, commonerrors.ErrNotIndexed) ||
errors.Is(err, commonerrors.ErrNotViewable) {
logger.Debugf("readdir: access issue for %s: %v - returning empty", ff.requestPath, err)
return []os.FileInfo{}, nil
}
// Other errors - propagate them
return nil, err
}
// Build os.FileInfo list directly from FileInfoFaster's filtered results
// No need to read from underlying filesystem - FileInfoFaster already filtered by permissions
entries := make([]os.FileInfo, 0, len(fileInfo.Files)+len(fileInfo.Folders))
@@ -175,11 +251,34 @@ func (ffs *filteredFileSystem) Mkdir(ctx context.Context, name string, perm os.F
if !ffs.user.Permissions.Create {
return fmt.Errorf("create permission required")
}
// Check access before creating directory
if err := ffs.checkAccess(name); err != nil {
logger.Debugf("Mkdir: access denied for %s: %v", name, err)
return err
}
return ffs.fs.Mkdir(ctx, name, perm)
}
func (ffs *filteredFileSystem) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
file, err := ffs.fs.OpenFile(ctx, name, flag, perm)
func (ffs *filteredFileSystem) OpenFile(ctx context.Context, requestPath string, flag int, perm os.FileMode) (webdav.File, error) {
// Check if this is a write operation
isWrite := (flag&os.O_WRONLY) != 0 || (flag&os.O_RDWR) != 0 || (flag&os.O_CREATE) != 0
if isWrite {
// Check user permissions first
if !ffs.user.Permissions.Create && !ffs.user.Permissions.Modify {
return nil, fmt.Errorf("write permission required")
}
// For write operations, check access
if err := ffs.checkAccess(requestPath); err != nil {
logger.Debugf("OpenFile: write access denied for %s: %v", requestPath, err)
return nil, err
}
}
file, err := ffs.fs.OpenFile(ctx, requestPath, flag, perm)
if err != nil {
return nil, err
}
@@ -191,51 +290,77 @@ func (ffs *filteredFileSystem) OpenFile(ctx context.Context, name string, flag i
}
// Wrap the file to filter directory listings
// name is the request path (without user scope)
return &filteredFile{
File: file,
fs: ffs,
dirPath: name,
isDir: stat.IsDir(),
File: file,
fs: ffs,
requestPath: requestPath,
isDir: stat.IsDir(),
}, nil
}
func (ffs *filteredFileSystem) RemoveAll(ctx context.Context, name string) error {
func (ffs *filteredFileSystem) RemoveAll(ctx context.Context, requestPath string) error {
if !ffs.user.Permissions.Delete {
return fmt.Errorf("delete permission required")
}
return ffs.fs.RemoveAll(ctx, name)
// Check access before deleting
if err := ffs.checkAccess(requestPath); err != nil {
logger.Debugf("RemoveAll: access denied for %s: %v", requestPath, err)
return err
}
return ffs.fs.RemoveAll(ctx, requestPath)
}
func (ffs *filteredFileSystem) Rename(ctx context.Context, oldName, newName string) error {
func (ffs *filteredFileSystem) Rename(ctx context.Context, oldPath, newPath string) error {
if !ffs.user.Permissions.Modify {
return fmt.Errorf("modify permission required")
}
return ffs.fs.Rename(ctx, oldName, newName)
// Check access for both old and new paths
if err := ffs.checkAccess(oldPath); err != nil {
logger.Debugf("Rename: access denied for source %s: %v", oldPath, err)
return err
}
if err := ffs.checkAccess(newPath); err != nil {
logger.Debugf("Rename: access denied for destination %s: %v", newPath, err)
return err
}
return ffs.fs.Rename(ctx, oldPath, newPath)
}
func (ffs *filteredFileSystem) Stat(ctx context.Context, name string) (os.FileInfo, error) {
// Root path "/" should always be allowed
if name == "" || name == "/" {
return ffs.fs.Stat(ctx, name)
func (ffs *filteredFileSystem) Stat(ctx context.Context, requestPath string) (os.FileInfo, error) {
// Try to get file info (uses cache if available, FileInfoFaster otherwise)
_, err := ffs.getFileInfo(requestPath, false)
if err == nil {
// Successfully got info - use underlying filesystem
return ffs.fs.Stat(ctx, requestPath)
}
// Check permission before stat using cached result if available
permissionPath := utils.JoinPathAsUnix(ffs.userscope, name)
_, err := ffs.getCachedFileInfo(permissionPath, false)
if err != nil {
return nil, err
// Handle specific error cases
if errors.Is(err, commonerrors.ErrAccessDenied) || errors.Is(err, commonerrors.ErrNotViewable) {
return nil, os.ErrPermission
}
return ffs.fs.Stat(ctx, name)
// For not indexed, check if it's viewable
if errors.Is(err, commonerrors.ErrNotIndexed) {
// Use checkAccess to determine if viewable
if accessErr := ffs.checkAccess(requestPath); accessErr != nil {
return nil, os.ErrPermission
}
// Viewable - allow underlying filesystem
return ffs.fs.Stat(ctx, requestPath)
}
// For other errors (like file not found), let underlying filesystem handle it
return ffs.fs.Stat(ctx, requestPath)
}
// webDAVHandler serves WebDAV requests.
func webDAVHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
path := r.PathValue("path")
source := r.PathValue("source")
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
if !d.user.Permissions.Download {
return http.StatusForbidden, fmt.Errorf("download permission required")
}
@@ -246,15 +371,19 @@ func webDAVHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
if isWrite && !userCanWrite(d.user.Permissions) {
return http.StatusForbidden, fmt.Errorf("user has no permission to modify")
}
logger.Debugf("webdav: method=%s, request=%s, source=%s, path=%s", r.Method, r.URL.Path, source, path)
indexPath, userScope, err := files.CheckPermissions(utils.FileOptions{
requestPath := utils.AddTrailingSlashIfNotExists(r.PathValue("path"))
source := r.PathValue("source")
if !strings.HasPrefix(requestPath, "/") {
requestPath = "/" + requestPath
}
logger.Debugf("webdav: method=%s, request=%s, source=%s, requestPath=%s", r.Method, r.URL.Path, source, requestPath)
_, userScope, err := files.CheckPermissions(utils.FileOptions{
FollowSymlinks: false,
Path: path,
Path: requestPath,
Source: source,
ShowHidden: d.user.ShowHidden,
}, store.Access, d.user)
if err != nil {
logger.Debugf("error checking file permissions for path %s: %v", path, err)
return http.StatusForbidden, err
}
@@ -262,28 +391,25 @@ func webDAVHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
if idx == nil {
return http.StatusNotFound, fmt.Errorf("source %s not found", source)
}
// Get the user's scope to determine the WebDAV root directory
// Resolve the scope path to get the real filesystem root for WebDAV
// This is the root directory that WebDAV will use to resolve relative paths
scopePath, _, err := idx.GetRealPath(userScope)
if err != nil {
logger.Debugf("error resolving scope path: %v", err)
return http.StatusNotFound, err
}
// Construct the WebDAV prefix from BaseURL
webDavPrefix := config.Server.BaseURL + "dav"
prefix := webDavPrefix + "/" + source
logger.Debugf("webdav: virtual_path=%s, indexPath=%s, scope_path=%s", path, indexPath, scopePath)
// Wrap the filesystem to filter directory listings using FileInfoFaster
// This prevents items the user can't access from appearing in listings,
// which stops clients from repeatedly trying to access them
// We pass requestPath (without scope) to FileInfoFaster, which applies scope internally
filteredFS := &filteredFileSystem{
fs: webdav.Dir(scopePath),
source: source,
user: d.user,
userscope: userScope,
userScope: userScope,
}
wd := &webdav.Handler{
@@ -291,7 +417,13 @@ func webDAVHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (i
FileSystem: filteredFS,
LockSystem: idx.WebdavLock,
Logger: func(req *http.Request, err error) {
if err != nil && !strings.Contains(err.Error(), "no such file or directory") {
if err != nil {
errStr := err.Error()
// Filter out expected/benign errors that don't indicate actual failures
if strings.Contains(errStr, "no such file or directory") ||
strings.Contains(errStr, "skip this directory") {
return
}
logger.Errorf("webdav handler failed on path %s: %s", req.URL.Path, err)
}
},
+803
View File
@@ -0,0 +1,803 @@
package http
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
storm "github.com/asdine/storm/v3"
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/files"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/common/utils"
commonerrors "github.com/gtsteffaniak/filebrowser/backend/common/errors"
"github.com/gtsteffaniak/filebrowser/backend/database/access"
"github.com/gtsteffaniak/filebrowser/backend/database/share"
"github.com/gtsteffaniak/filebrowser/backend/database/storage/bolt"
"github.com/gtsteffaniak/filebrowser/backend/database/users"
"github.com/gtsteffaniak/filebrowser/backend/indexing"
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
)
// setupWebDAVTestEnv sets up the test environment with multiple sources and users
func setupWebDAVTestEnv(t *testing.T) (string, string) {
// Create temp directories for test sources
tempDir := t.TempDir()
source1Path := filepath.Join(tempDir, "source1")
source2Path := filepath.Join(tempDir, "source2")
dockerPath := filepath.Join(source1Path, "_docker")
// Create directory structure
dirs := []string{
filepath.Join(source1Path, "public"),
filepath.Join(source1Path, "private"),
filepath.Join(source1Path, "viewable-only"),
filepath.Join(source1Path, "not-viewable"),
dockerPath,
filepath.Join(dockerPath, "data"),
filepath.Join(source2Path, "shared"),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
t.Fatal(err)
}
}
// Create test files
testFiles := map[string]string{
filepath.Join(source1Path, "public", "readme.txt"): "public content",
filepath.Join(source1Path, "private", "secret.txt"): "private content",
filepath.Join(source1Path, "viewable-only", "data.txt"): "viewable content",
filepath.Join(source1Path, "not-viewable", "hidden.txt"): "hidden content",
filepath.Join(dockerPath, "data", "config.yml"): "docker config",
filepath.Join(source2Path, "shared", "document.txt"): "shared content",
}
for path, content := range testFiles {
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
t.Fatal(err)
}
}
// Setup database
dbPath := filepath.Join(tempDir, "db")
db, err := storm.Open(dbPath)
if err != nil {
t.Fatal(err)
}
store, err = bolt.NewStorage(db)
if err != nil {
t.Fatal(err)
}
// Clear any previous access rules
store.Access.AllRules = make(access.SourceRuleMap)
store.Access.Groups = make(access.GroupMap)
// Set cache directory for index database
settings.Config.Server.CacheDir = tempDir
// Setup config with sources - update both the local config var AND the global settings.Config
config = &settings.Settings{
Server: settings.Server{
BaseURL: "/", // Use / for tests to avoid prefix mismatch
CacheDir: tempDir,
SourceMap: map[string]*settings.Source{
source1Path: {
Path: source1Path,
Name: "source1",
},
source2Path: {
Path: source2Path,
Name: "source2",
},
},
NameToSource: map[string]*settings.Source{
"source1": {
Path: source1Path,
Name: "source1",
},
"source2": {
Path: source2Path,
Name: "source2",
},
},
},
}
// CRITICAL: Also update the global settings.Config so access.Permitted can find sources
settings.Config.Server.SourceMap = config.Server.SourceMap
settings.Config.Server.NameToSource = config.Server.NameToSource
// Initialize user resolvers
settings.InitializeUserResolvers()
// Create minimal mock indices for webdav handler
indexing.SetTestIndex("source1", source1Path)
indexing.SetTestIndex("source2", source2Path)
t.Cleanup(func() {
indexing.ClearTestIndices()
})
// Mock CheckPermissions to bypass index requirement
mockCheckPermissions(t, source1Path, source2Path)
// Initialize mock indexing - this mocks FileInfoFaster
mockWebDAVIndexing(t, source1Path, source2Path)
return source1Path, source2Path
}
// mockCheckPermissions mocks the CheckPermissions function to bypass index lookups
func mockCheckPermissions(t *testing.T, source1Path, source2Path string) {
t.Helper()
// Store original function
originalCheckPermissions := files.CheckPermissionsFunc
t.Cleanup(func() { files.CheckPermissionsFunc = originalCheckPermissions })
// Mock the function
files.CheckPermissionsFunc = func(opts utils.FileOptions, access *access.Storage, user *users.User) (string, string, error) {
// Get the source path
var sourcePath string
if opts.Source == "source1" {
sourcePath = source1Path
} else if opts.Source == "source2" {
sourcePath = source2Path
} else {
return "", "", fmt.Errorf("unknown source: %s", opts.Source)
}
// Get user scope for this source - match by source PATH, not name
userScope := "/"
hasScope := false
for _, scope := range user.Scopes {
if scope.Name == sourcePath {
userScope = scope.Scope
hasScope = true
break
}
}
// If user has no scope for this source, deny access
if !hasScope && len(user.Scopes) > 0 {
return "", "", fmt.Errorf("user has no access to source: %s", opts.Source)
}
// Sanitize path
safePath, err := utils.SanitizeUserPath(opts.Path)
if err != nil {
return "", "", commonerrors.ErrAccessDenied
}
// Combine scope + sanitized path
indexPath := utils.JoinPathAsUnix(userScope, safePath)
// Check access control
if access != nil && !access.Permitted(sourcePath, indexPath, user.Username) {
return "", "", commonerrors.ErrAccessDenied
}
return indexPath, userScope, nil
}
}
// mockWebDAVIndexing mocks the indexing system for WebDAV tests
func mockWebDAVIndexing(t *testing.T, source1Path, source2Path string) {
// Mock FileInfoFaster to simulate indexing behavior
originalFileInfoFaster := files.FileInfoFasterFunc
t.Cleanup(func() { files.FileInfoFasterFunc = originalFileInfoFaster })
files.FileInfoFasterFunc = func(opts utils.FileOptions, access *access.Storage, user *users.User, share *share.Storage) (*iteminfo.ExtendedFileInfo, error) {
// Resolve source name to source path first
sourcePath := ""
if opts.Source == "source1" {
sourcePath = source1Path
} else if opts.Source == "source2" {
sourcePath = source2Path
} else {
return nil, fmt.Errorf("unknown source: %s", opts.Source)
}
// Simulate access control
if access != nil && user != nil {
// Simple scope check based on user's configured scopes (which use source PATH)
hasAccess := false
for _, scope := range user.Scopes {
if scope.Name == sourcePath {
hasAccess = true
// Check if path is within user's scope
userScope := scope.Scope
fullPath := utils.JoinPathAsUnix(userScope, opts.Path)
// Check if user has permission using the access storage
if !access.Permitted(sourcePath, fullPath, user.Username) {
return nil, commonerrors.ErrAccessDenied
}
break
}
}
if !hasAccess && len(user.Scopes) > 0 {
return nil, commonerrors.ErrAccessDenied
}
}
// Simulate indexing rules
path := opts.Path
// Paths containing "not-viewable" are not indexed and not viewable
if strings.Contains(path, "not-viewable") {
return nil, commonerrors.ErrNotViewable
}
// Paths containing "viewable-only" are not indexed but viewable
if strings.Contains(path, "viewable-only") {
return nil, commonerrors.ErrNotIndexed
}
// Simulate directory expansion
var files []iteminfo.ExtendedItemInfo
var folders []iteminfo.ItemInfo
if opts.Expand && (path == "/" || strings.HasSuffix(path, "/")) {
// Return mock directory contents based on path
if path == "/" || path == "" {
folders = []iteminfo.ItemInfo{
{Name: "public", Type: "directory"},
{Name: "private", Type: "directory"},
{Name: "viewable-only", Type: "directory"},
}
} else if strings.Contains(path, "public") {
files = []iteminfo.ExtendedItemInfo{
{ItemInfo: iteminfo.ItemInfo{Name: "readme.txt", Size: 14, Type: "text/plain"}},
}
} else if strings.Contains(path, "private") {
files = []iteminfo.ExtendedItemInfo{
{ItemInfo: iteminfo.ItemInfo{Name: "secret.txt", Size: 15, Type: "text/plain"}},
}
}
}
return &iteminfo.ExtendedFileInfo{
FileInfo: iteminfo.FileInfo{
Path: path,
Files: files,
Folders: folders,
ItemInfo: iteminfo.ItemInfo{
Name: filepath.Base(path),
Type: "directory",
},
},
}, nil
}
}
// Test PROPFIND with different user scopes
func TestWebDAV_PROPFIND_UserScopes(t *testing.T) {
source1Path, source2Path := setupWebDAVTestEnv(t)
// Create users with different scopes
// IMPORTANT: SourceScope.Name should be the source PATH, not the source name
adminUser := &users.User{
ID: 1,
Username: "admin",
Permissions: users.Permissions{
Admin: true,
Download: true,
Create: true,
Delete: true,
Modify: true,
},
Scopes: []users.SourceScope{
{Name: source1Path, Scope: "/"},
{Name: source2Path, Scope: "/"},
},
}
scopedUser := &users.User{
ID: 2,
Username: "scoped",
Permissions: users.Permissions{
Download: true,
},
Scopes: []users.SourceScope{
{Name: source1Path, Scope: "/_docker"},
},
}
restrictedUser := &users.User{
ID: 3,
Username: "restricted",
Permissions: users.Permissions{
Download: true,
},
Scopes: []users.SourceScope{
{Name: source1Path, Scope: "/public"},
},
}
// Save users
if err := store.Users.Save(adminUser, true, true); err != nil {
t.Fatal(err)
}
if err := store.Users.Save(scopedUser, true, true); err != nil {
t.Fatal(err)
}
if err := store.Users.Save(restrictedUser, true, true); err != nil {
t.Fatal(err)
}
testCases := []struct {
name string
user *users.User
source string
path string
expectedStatus int
shouldHaveContent bool
}{
{
name: "Admin can access root of source1",
user: adminUser,
source: "source1",
path: "/",
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
shouldHaveContent: true,
},
{
name: "Scoped user can access their scope root",
user: scopedUser,
source: "source1",
path: "/",
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
shouldHaveContent: false, // Empty because _docker is not viewable
},
{
name: "Restricted user can only see public folder",
user: restrictedUser,
source: "source1",
path: "/",
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
shouldHaveContent: true,
},
{
name: "Admin can access source2",
user: adminUser,
source: "source2",
path: "/",
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
shouldHaveContent: true,
},
{
name: "Scoped user cannot access source2",
user: scopedUser,
source: "source2",
path: "/",
expectedStatus: http.StatusForbidden,
},
}
// Initialize indexing for sources
initTestIndex(t, "source1", source1Path)
initTestIndex(t, "source2", source2Path)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest("PROPFIND", "/dav/"+tc.source+tc.path, nil)
req.SetPathValue("source", tc.source)
req.SetPathValue("path", tc.path)
w := httptest.NewRecorder()
ctx := &requestContext{user: tc.user}
status, err := webDAVHandler(w, req, ctx)
// If handler returned an error status, use that instead of response code
if status != 0 && status != http.StatusOK {
if w.Code == 0 || w.Code == http.StatusOK {
w.Code = status
}
}
// Check the actual HTTP response status code from ResponseWriter
if w.Code != tc.expectedStatus {
t.Errorf("expected status %d, got %d (err: %v)", tc.expectedStatus, w.Code, err)
}
if tc.shouldHaveContent && w.Code == 207 {
body := w.Body.String()
if body == "" {
t.Error("expected response body to have content")
}
}
})
}
}
// Test WebDAV write operations with access control
func TestWebDAV_WriteOperations(t *testing.T) {
source1Path, _ := setupWebDAVTestEnv(t)
// Create users with different permissions
// IMPORTANT: SourceScope.Name should be the source PATH, not the source name
fullAccessUser := &users.User{
ID: 1,
Username: "fullaccess",
Permissions: users.Permissions{
Download: true,
Create: true,
Delete: true,
Modify: true,
},
Scopes: []users.SourceScope{
{Name: source1Path, Scope: "/"},
},
}
readOnlyUser := &users.User{
ID: 2,
Username: "readonly",
Permissions: users.Permissions{
Download: true,
},
Scopes: []users.SourceScope{
{Name: source1Path, Scope: "/"},
},
}
scopedUser := &users.User{
ID: 3,
Username: "scoped",
Permissions: users.Permissions{
Download: true,
Create: true,
Modify: true,
Delete: true,
},
Scopes: []users.SourceScope{
{Name: source1Path, Scope: "/public"},
},
}
// Save users
if err := store.Users.Save(fullAccessUser, true, true); err != nil {
t.Fatal(err)
}
if err := store.Users.Save(readOnlyUser, true, true); err != nil {
t.Fatal(err)
}
if err := store.Users.Save(scopedUser, true, true); err != nil {
t.Fatal(err)
}
initTestIndex(t, "source1", source1Path)
testCases := []struct {
name string
user *users.User
method string
path string
expectedStatus int
}{
{
name: "Full access user can PUT file",
user: fullAccessUser,
method: http.MethodPut,
path: "/public/newfile.txt",
expectedStatus: http.StatusCreated, // 201 for new file creation
},
{
name: "Read-only user cannot PUT file",
user: readOnlyUser,
method: http.MethodPut,
path: "/public/newfile.txt",
expectedStatus: http.StatusForbidden,
},
{
name: "Full access user can MKCOL",
user: fullAccessUser,
method: "MKCOL",
path: "/newdir",
expectedStatus: http.StatusCreated, // 201 for new directory
},
{
name: "Read-only user cannot MKCOL",
user: readOnlyUser,
method: "MKCOL",
path: "/newdir",
expectedStatus: http.StatusForbidden,
},
{
name: "Full access user can DELETE",
user: fullAccessUser,
method: http.MethodDelete,
path: "/public/readme.txt",
expectedStatus: http.StatusNoContent, // 204 for successful deletion
},
{
name: "Read-only user cannot DELETE",
user: readOnlyUser,
method: http.MethodDelete,
path: "/public/readme.txt",
expectedStatus: http.StatusForbidden,
},
{
name: "Scoped user can write in their scope",
user: scopedUser,
method: http.MethodPut,
path: "/scopedfile.txt", // Path relative to scope (/public)
expectedStatus: http.StatusCreated, // 201 for new file
},
{
name: "User cannot write to not-viewable directory",
user: fullAccessUser,
method: http.MethodPut,
path: "/not-viewable/file.txt",
expectedStatus: 404, // Not viewable = acts like it doesn't exist
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var body io.Reader
if tc.method == http.MethodPut {
body = strings.NewReader("test content")
}
req := httptest.NewRequest(tc.method, "/dav/source1"+tc.path, body)
req.SetPathValue("source", "source1")
req.SetPathValue("path", tc.path)
w := httptest.NewRecorder()
ctx := &requestContext{user: tc.user}
status, err := webDAVHandler(w, req, ctx)
// If handler returned an error status, use that instead of response code
if status != 0 && status != http.StatusOK {
if w.Code == 0 || w.Code == http.StatusOK {
w.Code = status
}
}
// Check the actual HTTP response status code from ResponseWriter
if w.Code != tc.expectedStatus {
t.Errorf("expected status %d, got %d (err: %v)", tc.expectedStatus, w.Code, err)
}
})
}
}
// Test access control rules
func TestWebDAV_AccessControl(t *testing.T) {
source1Path, _ := setupWebDAVTestEnv(t)
// Create users
// IMPORTANT: SourceScope.Name should be the source PATH, not the source name
user1 := &users.User{
ID: 1,
Username: "user1",
Permissions: users.Permissions{
Download: true,
Create: true,
},
Scopes: []users.SourceScope{
{Name: source1Path, Scope: "/"},
},
}
user2 := &users.User{
ID: 2,
Username: "user2",
Permissions: users.Permissions{
Download: true,
},
Scopes: []users.SourceScope{
{Name: source1Path, Scope: "/"},
},
}
// Save users
if err := store.Users.Save(user1, true, true); err != nil {
t.Fatal(err)
}
if err := store.Users.Save(user2, true, true); err != nil {
t.Fatal(err)
}
// Initialize index
initTestIndex(t, "source1", source1Path)
// Create access rules by using the internal structure
// User1 can access /private, user2 cannot
// Set up a deny rule for user2 at /private
// IMPORTANT: Access system requires trailing slashes for exact match
rule := &access.AccessRule{
Deny: access.RuleSet{
Users: access.StringSet{"user2": struct{}{}},
},
}
// Add the rule to the access storage
if store.Access.AllRules == nil {
store.Access.AllRules = make(access.SourceRuleMap)
}
if store.Access.AllRules[source1Path] == nil {
store.Access.AllRules[source1Path] = make(access.RuleMap)
}
store.Access.AllRules[source1Path]["/private/"] = rule // Add trailing slash
testCases := []struct {
name string
user *users.User
path string
expectedStatus int
}{
{
name: "User1 can access private folder",
user: user1,
path: "/private",
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
},
{
name: "User2 cannot access private folder",
user: user2,
path: "/private",
expectedStatus: http.StatusForbidden,
},
{
name: "Both users can access public folder",
user: user1,
path: "/public",
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
},
{
name: "User2 can also access public folder",
user: user2,
path: "/public",
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest("PROPFIND", "/dav/source1"+tc.path, nil)
req.SetPathValue("source", "source1")
req.SetPathValue("path", tc.path)
w := httptest.NewRecorder()
ctx := &requestContext{user: tc.user}
status, err := webDAVHandler(w, req, ctx)
// If handler returned an error status, use that instead of response code
if status != 0 && status != http.StatusOK {
if w.Code == 0 || w.Code == http.StatusOK {
w.Code = status
}
}
// Check the actual HTTP response status code from ResponseWriter
if w.Code != tc.expectedStatus {
t.Errorf("expected status %d, got %d (err: %v)", tc.expectedStatus, w.Code, err)
}
})
}
}
// Test indexing states (indexed, viewable, not-viewable)
func TestWebDAV_IndexingStates(t *testing.T) {
source1Path, _ := setupWebDAVTestEnv(t)
// IMPORTANT: SourceScope.Name should be the source PATH, not the source name
user := &users.User{
ID: 1,
Username: "testuser",
Permissions: users.Permissions{
Download: true,
Create: true,
Modify: true,
Delete: true,
},
Scopes: []users.SourceScope{
{Name: source1Path, Scope: "/"},
},
}
if err := store.Users.Save(user, true, true); err != nil {
t.Fatal(err)
}
initTestIndex(t, "source1", source1Path)
testCases := []struct {
name string
path string
method string
expectedStatus int
description string
}{
{
name: "Can read indexed directory",
path: "/public",
method: "PROPFIND",
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
description: "Indexed directories should be accessible",
},
{
name: "Can read viewable-only directory",
path: "/viewable-only",
method: "PROPFIND",
expectedStatus: 207, // PROPFIND returns 207 Multi-Status
description: "Not indexed but viewable directories should be readable",
},
{
name: "Cannot read not-viewable directory",
path: "/not-viewable",
method: "PROPFIND",
expectedStatus: 405, // WebDAV returns 405 Method Not Allowed when resource denied
description: "Not viewable directories should be denied",
},
{
name: "Cannot write to viewable-only directory",
path: "/viewable-only/newfile.txt",
method: http.MethodPut,
expectedStatus: 404, // Returns 404 when trying to write to non-existent file in denied location
description: "Viewable-only directories should deny write",
},
{
name: "Cannot write to not-viewable directory",
path: "/not-viewable/newfile.txt",
method: http.MethodPut,
expectedStatus: 404, // Not viewable = acts like it doesn't exist
description: "Not viewable directories should deny all access",
},
{
name: "Can write to indexed directory",
path: "/public/newfile.txt",
method: http.MethodPut,
expectedStatus: http.StatusCreated, // 201 for new file creation
description: "Indexed directories should allow write with permissions",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var body io.Reader
if tc.method == http.MethodPut {
body = strings.NewReader("test content")
}
req := httptest.NewRequest(tc.method, "/dav/source1"+tc.path, body)
req.SetPathValue("source", "source1")
req.SetPathValue("path", tc.path)
w := httptest.NewRecorder()
ctx := &requestContext{user: user}
status, err := webDAVHandler(w, req, ctx)
// If handler returned an error status, use that instead of response code
if status != 0 && status != http.StatusOK {
if w.Code == 0 || w.Code == http.StatusOK {
w.Code = status
}
}
// Check the actual HTTP response status code from ResponseWriter
if w.Code != tc.expectedStatus {
t.Errorf("%s: expected status %d, got %d (err: %v)", tc.description, tc.expectedStatus, w.Code, err)
}
})
}
}
// Helper function to initialize a test index - simplified for WebDAV tests
func initTestIndex(t *testing.T, name, path string) {
// For WebDAV tests, indices are already initialized in setupWebDAVTestEnv
t.Helper()
}
+3 -3
View File
@@ -508,7 +508,7 @@ func (idx *Index) GetFileInfo(req FileInfoRequest) (*iteminfo.FileInfo, error) {
// Path must be either viewable OR indexable
if !isViewable && !isIndexable {
return nil, errors.ErrNotIndexed
return nil, errors.ErrNotViewable
}
// 3. Return appropriate info
@@ -659,10 +659,10 @@ func (idx *Index) GetFsInfoCore(indexPath string, opts Options) (*iteminfo.FileI
// Deny access if not viewable AND skipped
if opts.CheckViewable {
if !isViewable {
return nil, errors.ErrNotIndexed
return nil, errors.ErrNotViewable
}
} else if isSkipped {
return nil, errors.ErrNotIndexed
return nil, errors.ErrNotViewable
}
realSize, _ := idx.handleFile(dirInfo, indexPath, realPath, false, nil)
+9 -23
View File
@@ -100,29 +100,15 @@ func (info *FileInfo) SortItems() {
}
// ResolveSymlinks resolves symlinks in the given path and returns
// the final resolved path, whether it's a directory (considering bundle logic), and any error.
// Uses Go's filepath.EvalSymlinks which properly detects circular symlinks.
func ResolveSymlinks(path string) (string, bool, error) {
for {
// Get the file info using os.Lstat to handle symlinks
info, err := os.Lstat(path)
if err != nil {
return path, false, fmt.Errorf("could not stat path: %s, %v", path, err)
}
// Check if the path is a symlink
if info.Mode()&os.ModeSymlink != 0 {
// Read the symlink target
target, err := os.Readlink(path)
if err != nil {
return path, false, fmt.Errorf("could not read symlink: %s, %v", path, err)
}
// Resolve the symlink's target relative to its directory
path = filepath.Join(filepath.Dir(path), target)
} else {
// Not a symlink, check with bundle-aware directory logic
isDir := IsDirectory(info)
return path, isDir, nil
}
resolvedPath, err := filepath.EvalSymlinks(path)
if err != nil {
return path, false, fmt.Errorf("could not resolve symlinks for %s: %v", path, err)
}
info, err := os.Lstat(resolvedPath)
if err != nil {
return resolvedPath, false, fmt.Errorf("could not stat resolved path %s: %v", resolvedPath, err)
}
return resolvedPath, IsDirectory(info), nil
}
+30
View File
@@ -0,0 +1,30 @@
package indexing
import (
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"golang.org/x/net/webdav"
)
// SetTestIndex allows tests to register mock indices without database initialization
// This is useful for testing code that depends on GetIndex() without needing full setup
func SetTestIndex(name string, path string) {
indexesMutex.Lock()
defer indexesMutex.Unlock()
idx := &Index{
Source: settings.Source{
Name: name,
Path: path,
},
WebdavLock: webdav.NewMemLS(),
mock: true,
}
indexes[name] = idx
}
// ClearTestIndices removes all test indices - call in test cleanup
func ClearTestIndices() {
indexesMutex.Lock()
defer indexesMutex.Unlock()
indexes = make(map[string]*Index)
}
+56
View File
@@ -4102,6 +4102,9 @@ const docTemplate = `{
"bannerUrl": {
"type": "string"
},
"canEditShare": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -4213,6 +4216,9 @@ const docTemplate = `{
"description": "Override embedded field to show source name",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -5463,6 +5469,10 @@ const docTemplate = `{
"description": "debug onlyoffice editor",
"type": "boolean"
},
"deleteAfterArchive": {
"description": "delete source files after successful creation/extraction of archives",
"type": "boolean"
},
"deleteWithoutConfirming": {
"description": "delete files without confirmation",
"type": "boolean"
@@ -5515,6 +5525,10 @@ const docTemplate = `{
"description": "0-9 - the size of the gallery thumbnails",
"type": "integer"
},
"hideFilesInTree": {
"description": "hide files in the sidebar tree navigation, when true, will show only directories.",
"type": "boolean"
},
"hideSidebarFileActions": {
"description": "hide the file actions in the sidebar",
"type": "boolean"
@@ -5607,6 +5621,10 @@ const docTemplate = `{
"settings.UserDefaultsPreview": {
"type": "object",
"properties": {
"audio": {
"description": "show thumbnails for audio files",
"type": "boolean"
},
"autoplayMedia": {
"description": "autoplay media files in preview",
"type": "boolean"
@@ -5631,6 +5649,10 @@ const docTemplate = `{
"description": "show thumbnails for image files",
"type": "boolean"
},
"models": {
"description": "show live thumbnails for 3D models files",
"type": "boolean"
},
"motionVideoPreview": {
"description": "show multiple frames for videos in thumbnail preview when hovering",
"type": "boolean"
@@ -5679,6 +5701,9 @@ const docTemplate = `{
"bannerUrl": {
"type": "string"
},
"canEditShare": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -5775,6 +5800,9 @@ const docTemplate = `{
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -5817,6 +5845,9 @@ const docTemplate = `{
"bannerUrl": {
"type": "string"
},
"canEditShare": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -5922,6 +5953,9 @@ const docTemplate = `{
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -5967,6 +6001,9 @@ const docTemplate = `{
"bannerUrl": {
"type": "string"
},
"canEditShare": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -6075,6 +6112,9 @@ const docTemplate = `{
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -6205,6 +6245,10 @@ const docTemplate = `{
"users.Preview": {
"type": "object",
"properties": {
"audio": {
"description": "show thumbnail preview image for audio files",
"type": "boolean"
},
"autoplayMedia": {
"description": "autoplay media files in preview",
"type": "boolean"
@@ -6225,6 +6269,10 @@ const docTemplate = `{
"description": "show thumbnail preview image for image files",
"type": "boolean"
},
"models": {
"description": "show live thumbnails for 3D models files",
"type": "boolean"
},
"motionVideoPreview": {
"description": "show multiple frames for videos in thumbnail preview when hovering",
"type": "boolean"
@@ -6316,6 +6364,10 @@ const docTemplate = `{
"description": "debug onlyoffice editor",
"type": "boolean"
},
"deleteAfterArchive": {
"description": "delete source files after successful creation/extraction of archives",
"type": "boolean"
},
"deleteWithoutConfirming": {
"description": "delete files without confirmation",
"type": "boolean"
@@ -6367,6 +6419,10 @@ const docTemplate = `{
"description": "0-9 - the size of the gallery thumbnails",
"type": "integer"
},
"hideFilesInTree": {
"description": "hide files in the sidebar tree navigation, when true, will show only directories.",
"type": "boolean"
},
"hideSidebarFileActions": {
"description": "hide the file actions in the sidebar",
"type": "boolean"
+56
View File
@@ -4091,6 +4091,9 @@
"bannerUrl": {
"type": "string"
},
"canEditShare": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -4202,6 +4205,9 @@
"description": "Override embedded field to show source name",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -5452,6 +5458,10 @@
"description": "debug onlyoffice editor",
"type": "boolean"
},
"deleteAfterArchive": {
"description": "delete source files after successful creation/extraction of archives",
"type": "boolean"
},
"deleteWithoutConfirming": {
"description": "delete files without confirmation",
"type": "boolean"
@@ -5504,6 +5514,10 @@
"description": "0-9 - the size of the gallery thumbnails",
"type": "integer"
},
"hideFilesInTree": {
"description": "hide files in the sidebar tree navigation, when true, will show only directories.",
"type": "boolean"
},
"hideSidebarFileActions": {
"description": "hide the file actions in the sidebar",
"type": "boolean"
@@ -5596,6 +5610,10 @@
"settings.UserDefaultsPreview": {
"type": "object",
"properties": {
"audio": {
"description": "show thumbnails for audio files",
"type": "boolean"
},
"autoplayMedia": {
"description": "autoplay media files in preview",
"type": "boolean"
@@ -5620,6 +5638,10 @@
"description": "show thumbnails for image files",
"type": "boolean"
},
"models": {
"description": "show live thumbnails for 3D models files",
"type": "boolean"
},
"motionVideoPreview": {
"description": "show multiple frames for videos in thumbnail preview when hovering",
"type": "boolean"
@@ -5668,6 +5690,9 @@
"bannerUrl": {
"type": "string"
},
"canEditShare": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -5764,6 +5789,9 @@
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -5806,6 +5834,9 @@
"bannerUrl": {
"type": "string"
},
"canEditShare": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -5911,6 +5942,9 @@
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -5956,6 +5990,9 @@
"bannerUrl": {
"type": "string"
},
"canEditShare": {
"type": "boolean"
},
"description": {
"type": "string"
},
@@ -6064,6 +6101,9 @@
"description": "backend source is path to maintain between name changes",
"type": "string"
},
"sourceURL": {
"type": "string"
},
"themeColor": {
"type": "string"
},
@@ -6194,6 +6234,10 @@
"users.Preview": {
"type": "object",
"properties": {
"audio": {
"description": "show thumbnail preview image for audio files",
"type": "boolean"
},
"autoplayMedia": {
"description": "autoplay media files in preview",
"type": "boolean"
@@ -6214,6 +6258,10 @@
"description": "show thumbnail preview image for image files",
"type": "boolean"
},
"models": {
"description": "show live thumbnails for 3D models files",
"type": "boolean"
},
"motionVideoPreview": {
"description": "show multiple frames for videos in thumbnail preview when hovering",
"type": "boolean"
@@ -6305,6 +6353,10 @@
"description": "debug onlyoffice editor",
"type": "boolean"
},
"deleteAfterArchive": {
"description": "delete source files after successful creation/extraction of archives",
"type": "boolean"
},
"deleteWithoutConfirming": {
"description": "delete files without confirmation",
"type": "boolean"
@@ -6356,6 +6408,10 @@
"description": "0-9 - the size of the gallery thumbnails",
"type": "integer"
},
"hideFilesInTree": {
"description": "hide files in the sidebar tree navigation, when true, will show only directories.",
"type": "boolean"
},
"hideSidebarFileActions": {
"description": "hide the file actions in the sidebar",
"type": "boolean"
+42
View File
@@ -131,6 +131,8 @@ definitions:
type: string
bannerUrl:
type: string
canEditShare:
type: boolean
description:
type: string
disableAnonymous:
@@ -208,6 +210,8 @@ definitions:
source:
description: Override embedded field to show source name
type: string
sourceURL:
type: string
themeColor:
type: string
title:
@@ -1177,6 +1181,9 @@ definitions:
debugOffice:
description: debug onlyoffice editor
type: boolean
deleteAfterArchive:
description: delete source files after successful creation/extraction of archives
type: boolean
deleteWithoutConfirming:
description: delete files without confirmation
type: boolean
@@ -1214,6 +1221,10 @@ definitions:
gallerySize:
description: 0-9 - the size of the gallery thumbnails
type: integer
hideFilesInTree:
description: hide files in the sidebar tree navigation, when true, will show
only directories.
type: boolean
hideSidebarFileActions:
description: hide the file actions in the sidebar
type: boolean
@@ -1284,6 +1295,9 @@ definitions:
type: object
settings.UserDefaultsPreview:
properties:
audio:
description: show thumbnails for audio files
type: boolean
autoplayMedia:
description: autoplay media files in preview
type: boolean
@@ -1302,6 +1316,9 @@ definitions:
image:
description: show thumbnails for image files
type: boolean
models:
description: show live thumbnails for 3D models files
type: boolean
motionVideoPreview:
description: show multiple frames for videos in thumbnail preview when hovering
type: boolean
@@ -1336,6 +1353,8 @@ definitions:
type: string
bannerUrl:
type: string
canEditShare:
type: boolean
description:
type: string
disableAnonymous:
@@ -1403,6 +1422,8 @@ definitions:
source:
description: backend source is path to maintain between name changes
type: string
sourceURL:
type: string
themeColor:
type: string
title:
@@ -1433,6 +1454,8 @@ definitions:
type: string
bannerUrl:
type: string
canEditShare:
type: boolean
description:
type: string
disableAnonymous:
@@ -1506,6 +1529,8 @@ definitions:
source:
description: backend source is path to maintain between name changes
type: string
sourceURL:
type: string
themeColor:
type: string
title:
@@ -1538,6 +1563,8 @@ definitions:
type: string
bannerUrl:
type: string
canEditShare:
type: boolean
description:
type: string
disableAnonymous:
@@ -1613,6 +1640,8 @@ definitions:
source:
description: backend source is path to maintain between name changes
type: string
sourceURL:
type: string
themeColor:
type: string
title:
@@ -1709,6 +1738,9 @@ definitions:
type: object
users.Preview:
properties:
audio:
description: show thumbnail preview image for audio files
type: boolean
autoplayMedia:
description: autoplay media files in preview
type: boolean
@@ -1724,6 +1756,9 @@ definitions:
image:
description: show thumbnail preview image for image files
type: boolean
models:
description: show live thumbnails for 3D models files
type: boolean
motionVideoPreview:
description: show multiple frames for videos in thumbnail preview when hovering
type: boolean
@@ -1790,6 +1825,9 @@ definitions:
debugOffice:
description: debug onlyoffice editor
type: boolean
deleteAfterArchive:
description: delete source files after successful creation/extraction of archives
type: boolean
deleteWithoutConfirming:
description: delete files without confirmation
type: boolean
@@ -1826,6 +1864,10 @@ definitions:
gallerySize:
description: 0-9 - the size of the gallery thumbnails
type: integer
hideFilesInTree:
description: hide files in the sidebar tree navigation, when true, will show
only directories.
type: boolean
hideSidebarFileActions:
description: hide the file actions in the sidebar
type: boolean
+128 -1077
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -27,7 +27,6 @@
"css-vars-ponyfill": "^2.4.3",
"dompurify": "^3.2.4",
"epubjs": "^0.3.93",
"file-loader": "^6.2.0",
"glob": "^9.3.5",
"highlight.js": "^11.11.1",
"mammoth": "^1.9.1",
+4
View File
@@ -156,6 +156,7 @@ userDefaults:
disableQuickToggles: false # disable the quick toggles in the sidebar
disableSearchOptions: false # disable the search options in the search bar
stickySidebar: true # keep sidebar open when navigating
hideFilesInTree: false # hide files in the sidebar tree navigation, when true, will show only directories.
darkMode: true # should dark mode be enabled
locale: "en" # language to use: eg. de, en, or fr
viewMode: "normal" # view mode to use: eg. normal, list, grid, or compact
@@ -173,12 +174,14 @@ userDefaults:
disableHideSidebar: false # keep sidebar open when previewing files
image: true # show thumbnails for image files
video: true # show thumbnails for video files
audio: true # show thumbnails for audio files
motionVideoPreview: true # show multiple frames for videos in thumbnail preview when hovering
office: true # show thumbnails for office files
popup: true # show larger popup preview when hovering over thumbnail
autoplayMedia: true # autoplay media files in preview
defaultMediaPlayer: false # disable the styled feature-rich media player for browser default
folder: true # show thumbnails for folders that have previewable contents
models: true # show live thumbnails for 3D models files
permissions:
api: false # allow api access
admin: false # allow admin access
@@ -191,6 +194,7 @@ userDefaults:
loginMethod: "password" # login method to use: eg. password, proxy, oidc
disableUpdateNotifications: false # disable update notifications banner for admin users
deleteWithoutConfirming: false # delete files without confirmation
deleteAfterArchive: true # delete source files after successful creation/extraction of archives
fileLoading: # upload and download settings
maxConcurrentUpload: 10
uploadChunkSizeMb: 10
+1 -1
View File
@@ -5,7 +5,7 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>{{ .htmlVars.title }}</title>
<title>"{{ .htmlVars.title }}"</title>
<link rel="icon" type="image/png" sizes="256x256" href="{{ .htmlVars.favicon }}">
+5 -1
View File
@@ -132,7 +132,11 @@ export async function getApiKeys() {
const apiPath = getApiPath('auth/token/list')
return await fetchJSON(apiPath)
} catch (err) {
notify.showError(err.message || 'Failed to get API tokens')
// ignore 404 errors
if (err.status !== 404) {
notify.showError(err.message || 'Failed to get API tokens')
throw err
}
throw err
}
}
+31 -45
View File
@@ -14,7 +14,7 @@
:class="{ 'dark-mode': isDarkMode, 'centered': centered }"
:key="showCreate ? 'create-mode' : 'normal-mode'"
>
<div class="context-menu-header">
<div v-if="!showLimitedOptions" class="context-menu-header">
<div
class="action button clickable"
v-if="showCreateButton"
@@ -208,6 +208,10 @@ export default {
type: Boolean,
default: false,
},
showLimitedOptions: {
type: Boolean,
default: false,
},
items: {
type: Array,
default: null, // Array of item objects { name, path, source, isDir, type, ... }
@@ -235,10 +239,7 @@ export default {
return this.hasDownload && !this.req.isDir && !this.isShare;
},
showGoToItem() {
return this.isDuplicateFinder && this.selectedCount == 1;
},
isDuplicateFinder() {
return getters.currentView() === "duplicateFinder";
return this.showLimitedOptions && this.selectedCount == 1;
},
permissions() {
return getters.permissions();
@@ -250,65 +251,65 @@ export default {
return getters.isShare();
},
showCreateActions() {
if (this.isDuplicateFinder) return false;
if (this.showLimitedOptions) return false;
return this.showCreate && !this.isSearchActive;
},
showInfo() {
if (this.isDuplicateFinder) return this.selectedCount == 1;
if (this.showLimitedOptions) return this.selectedCount == 1;
return !this.showCreate && this.selectedCount == 1;
},
showDownload() {
if (this.isDuplicateFinder) return false;
if (this.showLimitedOptions) return false;
return !this.showCreate && this.permissions.download && this.selectedCount > 0;
},
showArchive() {
if (this.isDuplicateFinder || getters.isShare()) return false;
if (this.showLimitedOptions || getters.isShare()) return false;
if (!this.permissions.create) return false;
return !this.showCreate && this.selectedCount > 0 && !this.showUnarchive;
},
showUnarchive() {
if (this.isDuplicateFinder || getters.isShare()) return false;
if (this.showLimitedOptions || getters.isShare()) return false;
if (!this.permissions.create) return false;
if (this.selectedCount !== 1) return false;
const item = this.firstSelected;
return item && isArchivePath(item.path || item.from || item.name);
},
showShareAction() {
if (this.isDuplicateFinder) return false;
if (this.showLimitedOptions) return false;
return (this.showCreate || this.selectedCount == 1) && this.showShare;
},
showRename() {
if (this.isDuplicateFinder) return false;
if (this.showLimitedOptions) return false;
return !this.showCreate && this.selectedCount == 1 && this.permissions.modify && !this.isSearchActive;
},
showCopy() {
if (this.isDuplicateFinder) return false;
if (this.showLimitedOptions) return false;
return !this.showCreate && this.selectedCount > 0 && this.permissions.modify;
},
showOpenParentFolder() {
return !this.showCreate && this.selectedCount == 1 && (this.isSearchActive || this.isDuplicateFinder);
return !this.showCreate && this.selectedCount == 1 && (this.isSearchActive || this.showLimitedOptions);
},
showMove() {
if (this.isDuplicateFinder) return false;
if (this.showLimitedOptions) return false;
return !this.showCreate && this.selectedCount > 0 && this.permissions.modify;
},
showSelectAll() {
if (this.isDuplicateFinder) return false;
if (this.showLimitedOptions) return false;
return !this.showCreate && !this.isSearchActive && this.req?.items?.length > 0;
},
showCreateButton() {
if (this.isDuplicateFinder || this.createOnly) return false;
if (this.showLimitedOptions || this.createOnly) return false;
return !this.isSearchActive && this.permissions.create && !this.isShare;
},
showDivider() {
if (this.isDuplicateFinder || this.createOnly) return false;
if (this.showLimitedOptions || this.createOnly) return false;
if (getters.isShare()) {
return state.shareInfo?.allowCreate
}
return state.user?.permissions?.create || state.user?.permissions?.share || state.user?.permissions?.admin;
},
showSelectMultiple() {
if (this.isDuplicateFinder) return false;
if (this.showLimitedOptions) return false;
if (this.isMultiple || this.isSearchActive) {
return false;
}
@@ -340,7 +341,7 @@ export default {
return cv == "markdownViewer" && this.permissions.modify;
},
showDelete() {
if (this.isDuplicateFinder) return false;
if (this.showLimitedOptions) return false;
if (this.selectedCount == 0) {
return false;
}
@@ -352,15 +353,7 @@ export default {
return this.selectedCount > 0 && this.permissions.download;
},
isPreview() {
const cv = getters.currentView();
return (
cv == "preview" ||
cv == "onlyOfficeEditor" ||
cv == "markdownViewer" ||
cv == "epubViewer" ||
cv == "docViewer" ||
cv == "editor"
);
return getters.isPreviewView();
},
showSave() {
const allowEdit = this.permissions.modify || (getters.isShare() && state.shareInfo.allowEdit);
@@ -370,7 +363,7 @@ export default {
return getters.currentPromptName() == "OverflowMenu";
},
showAccess() {
if (this.isDuplicateFinder) return false;
if (this.showLimitedOptions) return false;
if (getters.isShare()) {
return false;
}
@@ -439,8 +432,8 @@ export default {
},
methods: {
showInfoHover() {
mutations.closeContextMenus();
mutations.showHover({
mutations.closeTopHover(); // close top hover to close overflow menu and replace any other open prompt
mutations.showHover({ // from preview we can open the same prompt multiple times, which is undesirable.
name: "info",
props: {
item: this.firstSelected,
@@ -450,6 +443,7 @@ export default {
goToItem() {
const item = this.firstSelected;
url.goToItem(item.source, item.path, {}, true);
mutations.closeHovers();
},
hideTooltip() {
mutations.hideTooltip();
@@ -607,7 +601,7 @@ export default {
}
},
toggleMultipleSelection() {
mutations.setMultiple(!state.multiple);
mutations.setMultiple(true);
mutations.closeHovers();
},
startDownload() {
@@ -616,7 +610,7 @@ export default {
downloadFiles(items);
},
showDeleteHover() {
mutations.closeContextMenus();
mutations.closeTopHover();
mutations.showHover({
name: 'delete',
props: {
@@ -770,19 +764,11 @@ export default {
openParentFolder() {
const item = this.firstSelected;
const parentPath = url.removeLastDir(item.path) || "/";
url.goToItem(item.source, parentPath, {}, this.isDuplicateFinder);
url.goToItem(item.source, parentPath, {}, this.showLimitedOptions);
mutations.closeHovers();
},
selectAllItems() {
if (state.req && state.req.items && state.req.items.length > 0) {
// Clear current selection first
mutations.resetSelected();
// Add all items from current directory to selection by their indices
state.req.items.forEach((item, index) => {
mutations.addSelected(index);
});
// Close the context menu
mutations.closeContextMenus();
}
mutations.selectAllItems()
},
},
};
+3 -2
View File
@@ -141,7 +141,7 @@ export default {
],
typeSelect: [
{ label: this.$t("general.photos"), value: "type:image" },
{ label: this.$t("general.audio"), value: "type:audio" },
{ label: this.$t("general.audios"), value: "type:audio" },
{ label: this.$t("general.videos"), value: "type:video" },
{ label: this.$t("general.documents"), value: "type:doc" },
{ label: this.$t("general.archives"), value: "type:archive" },
@@ -293,7 +293,7 @@ export default {
let result = url.extractSourceFromPath(decodeURIComponent(state.route.path));
if (this.selectedSource === "" || result.source === this.selectedSource) {
return result.path;
} else {
} else {
return "/"; // if searching on non-current source, search the whole thing
}
},
@@ -307,6 +307,7 @@ export default {
props: {
posX: event.clientX,
posY: event.clientY,
showLimitedOptions: true,
},
});
},
@@ -291,10 +291,10 @@ export default {
action: buttonAction
}]
});
mutations.closeHovers();
mutations.closeTopHover();
mutations.setReload(true);
} catch (error) {
mutations.closeHovers();
mutations.closeTopHover();
notify.showErrorToast(this.$t("prompts.moveFailed"));
console.log("Move failed", e);
}
@@ -42,6 +42,7 @@
:isSelectedProp="selected === item.path"
:clickable="false"
:forceFilesApi="!!browseSource"
:showLimitedOptions="true"
@click.prevent="(event) => handleItemClick(item, index, event)"
@dblclick.prevent="(event) => handleItemDblClick(item, index, event)"
/>
+47 -10
View File
@@ -17,6 +17,7 @@
'current-item': isCurrentItem(node),
'has-children': node.childrenCount > 0,
'drag-over': node.dragOver || isSelected(node),
'hidden-file': node.isHidden,
}"
@click="handleNodeClick(node)"
@contextmenu.prevent="handleContextMenu($event, node)"
@@ -38,7 +39,7 @@
<Icon
:mimetype="node.isDir ? 'directory' : node.type"
:filename="node.name"
:isShared="isShare"
:isShared="node.isShared"
class="node-icon"
/>
<span class="node-name" :title="node.name">
@@ -54,7 +55,8 @@
:currentPath="currentPath"
/>
<div v-else-if="node.expanded && node.children && node.children.length === 0" class="tree-empty-folder">
{{ $t('files.lonely') }}
<i class="material-icons">sentiment_dissatisfied</i>
<span>{{ $t('files.lonely') }}</span>
</div>
<div v-else-if="node.expanded && node.loading" class="tree-loading">
<LoadingSpinner size="small" mode="placeholder" />
@@ -133,6 +135,10 @@ export default {
canModify() {
return getters.permissions()?.modify;
},
showFiles() {
// Defaults to true
return !state.user?.hideFilesInTree;
},
},
watch: {
currentSource: {
@@ -160,6 +166,14 @@ export default {
},
immediate: true,
},
showFiles: {
handler() {
if (this.isRootInstance) {
this.refresh();
}
},
immediate: false,
},
},
mounted() {
this.unwatchReload = this.$watch(
@@ -203,13 +217,19 @@ export default {
}
},
async fetchItems(path) {
let items = [];
if (this.isShare) {
const res = await resourcesApi.fetchFilesPublic(path, this.shareHash, state.shareInfo.password, false, false, true);
return res.items || [];
const res = await resourcesApi.fetchFilesPublic(path, this.shareHash, state.shareInfo?.password, false, false, true);
items = res.items || [];
} else {
const res = await resourcesApi.fetchFiles(this.currentSource, path, false, false, true);
return res.items || [];
items = res.items || [];
}
// Filter out files if showFiles is false
if (!this.showFiles) {
items = items.filter(item => item.type === 'directory');
}
return items;
},
createNode(item) {
@@ -224,7 +244,9 @@ export default {
children: null,
childrenCount: 0,
childrenError: null,
isShared: item.isShared,
dragOver: false, // For drag highlight
isHidden: item.hidden,
...item
};
},
@@ -548,10 +570,10 @@ export default {
}]
};
notify.showSuccess(this.$t('prompts.moveSuccess'), buttonProps);
mutations.closeHovers();
mutations.closeTopHover();
mutations.setReload(true);
} catch (error) {
mutations.closeHovers();
mutations.closeTopHover();
throw error;
}
};
@@ -563,7 +585,7 @@ export default {
const overwrite = option === 'overwrite';
const rename = option === 'rename';
event.preventDefault();
mutations.closeHovers();
mutations.closeTopHover();
await performAction(overwrite, rename);
},
});
@@ -658,6 +680,10 @@ export default {
box-sizing: border-box;
}
.tree-node.hidden-file {
opacity: 0.5;
}
.tree-node:hover {
background-color: var(--surfaceSecondary);
}
@@ -750,11 +776,22 @@ export default {
}
.tree-empty-folder {
display: flex;
align-items: center;
gap: 0.3em;
padding: 0.5em 1em;
margin-left: 1.2em;
text-align: center;
color: var(--textSecondary);
font-style: italic;
font-size: 0.85em;
font-size: 0.9em;
}
.tree-empty-folder i {
font-size: 1.4em;
line-height: 1;
display: inline-flex;
align-items: center;
height: 1em;
}
</style>
+17 -12
View File
@@ -7,7 +7,7 @@
<i v-if="isShared" class="material-icons overlay-icon">group</i>
<!-- Preview content: image, 3D, or fallback -->
<img v-if="hasPreviewImage" :key="imageTargetSrc" :src="imageDisplaySrc" ref="thumbnail" />
<ThreeJs v-else-if="shouldUse3DPreview && !threeJsError"
<ThreeJs v-else-if="shouldUse3DPreview && !threeJsError"
:key="`3d-${path}-${gallerySizeKey}`"
:fbdata="{
name: filename,
@@ -20,7 +20,7 @@
:add-load-delay="true"
@error="handle3DError" />
</span>
<!-- Regular material icon (no preview) -->
<span v-else class="image-preview">
<i :class="[classes, { active: active, clickable: clickable }]"> {{ materialIcon }} </i>
@@ -122,12 +122,18 @@ export default {
return false;
}
const simpleType = this.getIconForType().simpleType;
if (simpleType === "audio" && !getters.previewPerms().audio) {
return false;
}
if (simpleType === "video" && !getters.previewPerms().video) {
return false;
}
if (simpleType === "image" && !getters.previewPerms().image) {
return false;
}
if (simpleType === "ebook" && !getters.previewPerms().image) {
return false;
}
// office files
if ((simpleType === "document" || simpleType === "text") && !getters.previewPerms().office) {
return false;
@@ -136,7 +142,7 @@ export default {
return false;
}
// 3D models - show preview thumbnails (if backend provides them)
if (simpleType === "3d-model" && !getters.previewPerms().image) {
if (simpleType === "3d-model" && !getters.previewPerms().models) {
return false;
}
return this.imageState !== 'error' && !this.disablePreviewExt && !this.officeFileDisabled;
@@ -213,10 +219,11 @@ export default {
shouldUse3DPreview() {
// Check if we should use 3D preview instead of regular icon
if (!this.isFile || !this.size || !this.path) return false;
if (!getters.previewPerms().models) return false;
const MAX_SIZE = 250 * 1024; // 250KB in bytes
if (this.size > MAX_SIZE) return false;
const typeInfo = this.getIconForType();
return typeInfo.simpleType === '3d-model';
},
@@ -365,13 +372,12 @@ export default {
},
updateImageTargetSrc() {
let newSrc = this.thumbnailUrl || PLACEHOLDER_URL;
// If we need large thumbnails and have a thumbnail URL, append &size=large
// Otherwise use the URL as-is (defaults to small)
if (this.thumbnailUrl && this.showLargeIcon) {
newSrc = this.thumbnailUrl + "&size=large";
}
if (this.imageTargetSrc !== newSrc) {
this.imageTargetSrc = newSrc;
}
@@ -525,6 +531,10 @@ export default {
color: white;
}
.brown-icons {
color: brown;
}
#listingView.gallery .listing-item i.white-icons,
.active.white-icons {
color: var(--activeWhiteIcon);
@@ -558,11 +568,6 @@ export default {
color: yellow;
}
.simple-icons {
color: white;
font-size: 1.5em !important;
}
/* Unified .image-preview container - works universally, always square */
.image-preview {
width: var(--icon-size);
+27 -23
View File
@@ -52,6 +52,7 @@
<span>{{ $t("files.duration") }}</span>
<i v-if="durationSorted" class="material-icons">{{ durationIcon }}</i>
</p>
<span v-if="quickDownloadEnabled" class="placeholder"></span>
</div>
</template>
@@ -112,6 +113,18 @@ export default {
}
return "arrow_upward";
},
quickDownloadEnabled() {
// @ts-ignore
if (state.isMobile) {
return false
}
if (getters.isShare()) {
// @ts-ignore
return state.shareInfo?.quickDownload;
}
// @ts-ignore
return state.user?.quickDownload && !this.galleryView;
},
},
methods: {
sort(field) {
@@ -154,18 +167,13 @@ export default {
user-select: none;
}
.name {
margin-right: 1.5em;
box-sizing: border-box;
}
p {
margin: 0;
cursor: pointer;
box-sizing: border-box;
display: flex;
align-items: center;
min-width: 0;
width: 100%;
}
span {
@@ -176,28 +184,24 @@ span {
min-width: 0;
}
.size {
box-sizing: border-box;
.name {
flex: 1;
}
.desktop-view .size,
.desktop-view .modified,
.desktop-view .duration {
min-width: 10%;
flex: 0 !important;
}
.size,
.modified,
.duration {
margin-left: auto;
padding-right: 1em;
box-sizing: border-box;
flex: 1;
padding-left: 1em;
}
.desktop-view {
justify-content: unset !important;
}
/* Desktop-specific column widths */
.desktop-view .name {
width: 50%;
}
.desktop-view .size {
width: 25%;
}
i {
font-size: 1.5em;
+37 -20
View File
@@ -47,10 +47,9 @@
</div>
<div class="text">
<p :class="{ adjustment: quickDownloadEnabled }" class="name">{{ displayName }}</p>
<p class="name">{{ displayName }}</p>
<p
class="size"
:class="{ adjustment: quickDownloadEnabled }"
:data-order="humanSize"
>
{{ humanSize }}
@@ -66,7 +65,6 @@
:filename="name"
:hasPreview="hasPreview"
mimetype="file_download"
style="padding-right: 0.5em"
class="download-icon"
role="button"
aria-label="Download"
@@ -74,6 +72,10 @@
:clickable=true
:isShared="isShared"
/>
<Icon
v-else-if="quickDownloadPlaceholder"
class="placeholder"
/>
</a>
<div
v-else
@@ -116,10 +118,9 @@
</div>
<div class="text">
<p :class="{ adjustment: quickDownloadEnabled }" class="name">{{ displayName }}</p>
<p class="name">{{ displayName }}</p>
<p
class="size"
:class="{ adjustment: quickDownloadEnabled }"
:data-order="humanSize"
>
{{ humanSize }}
@@ -200,6 +201,14 @@ export default {
type: Boolean,
default: false,
},
showLimitedOptions: {
type: Boolean,
default: false,
},
disableContextMenu: {
type: Boolean,
default: false,
},
},
computed: {
displayName() {
@@ -213,11 +222,20 @@ export default {
// @ts-ignore
if (getters.isShare()) {
// @ts-ignore
return state.shareInfo?.quickDownload && !this.isDir;
return state.shareInfo?.quickDownload && !this.galleryView && !this.isDir;
}
// @ts-ignore
return state.user?.quickDownload && !this.galleryView && !this.isDir;
},
quickDownloadPlaceholder() {
// @ts-ignore
if (getters.isShare()) {
// @ts-ignore
return state.shareInfo?.quickDownload && !this.galleryView && this.isDir;
}
// @ts-ignore
return state.user?.quickDownload && !this.galleryView && getters.viewMode() !== "icons" && getters.viewMode() !== "normal" && this.isDir;
},
isHiddenNotSelected() {
return !this.isSelected && this.reducedOpacity;
},
@@ -415,20 +433,15 @@ export default {
},
/** @param {MouseEvent} event */
onRightClick(event) {
if (!this.updateGlobalState) {
event.preventDefault();
return;
}
event.preventDefault(); // Prevent default context menu
// If one or fewer items are selected, reset the selection
if (this.updateGlobalState) {
// If one or fewer items are selected, reset the selection
if (!state.multiple && getters.selectedCount() < 2) {
mutations.resetSelected();
// @ts-ignore
mutations.addSelected(this.index);
}
} else {
// Build full item object similar to Search.vue
const selectedItem = {
name: this.name,
@@ -440,16 +453,21 @@ export default {
path: this.path,
url: this.path,
index: this.index,
};
};
mutations.resetSelected();
// @ts-ignore
mutations.addSelected(selectedItem);
}
if (this.disableContextMenu) {
return;
}
mutations.showHover({
name: "ContextMenu",
props: {
posX: event.clientX,
posY: event.clientY,
showLimitedOptions: this.showLimitedOptions,
},
});
},
@@ -616,11 +634,11 @@ export default {
};
notify.showSuccess(this.$t("prompts.moveSuccess"), buttonProps);
// Close the prompt after successful operation and reload items for reflect the changes
mutations.closeHovers();
mutations.closeTopHover();
mutations.setReload(true);
} catch (error) {
// Close the prompt and let error handling continue
mutations.closeHovers();
mutations.closeTopHover();
throw error;
}
};
@@ -638,7 +656,7 @@ export default {
const rename = option === "rename";
event.preventDefault();
mutations.closeHovers();
mutations.closeTopHover();
await action(overwrite, rename);
},
});
@@ -769,9 +787,9 @@ export default {
// Local selection handling - emit events instead of updating global state
if (event.shiftKey) {
// Shift-click: select range (emit event for parent to handle)
this.$emit('selectRange', {
this.$emit('selectRange', {
startIndex: state.lastSelectedIndex !== null ? state.lastSelectedIndex : this.index,
endIndex: this.index
endIndex: this.index
});
return;
}
@@ -786,7 +804,7 @@ export default {
if (!this.updateGlobalState) {
return;
}
// 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;
@@ -805,7 +823,6 @@ export default {
<style>
.download-icon {
font-size: 1.5em;
cursor: pointer;
color: var(--secondaryColor);
}
@@ -41,6 +41,9 @@ export default {
},
computed: {
sourceInfo() {
if (state.multiple) {
return false;
}
return state.popupPreviewSourceInfo;
},
},
@@ -166,4 +166,9 @@ export default {
.button i {
font-size: 1.2em;
}
.card h3 {
margin: 0;
}
</style>
+10 -7
View File
@@ -46,11 +46,9 @@
max="9"
/>
<div class="archive-options settings-items">
<ToggleSwitch
class="item"
v-model="deleteAfter"
:name="$t('prompts.archiveDeleteAfter')"
/>
<ToggleSwitch class="item" v-model="deleteAfter"
:name="$t('profileSettings.deleteAfterArchive')"
:description="$t('profileSettings.deleteAfterArchiveDescription')" />
</div>
</template>
</div>
@@ -97,7 +95,7 @@
</template>
<script>
import { mutations } from "@/store";
import { state, mutations } from "@/store";
import { notify } from "@/notify";
import { resourcesApi } from "@/api";
import { goToItem } from "@/utils/url";
@@ -131,9 +129,14 @@ export default {
compression: 0,
creating: false,
showFileList: false,
deleteAfter: true,
deleteAfter: state.user?.deleteAfterArchive == true,
};
},
watch: {
deleteAfter(newVal) {
mutations.updateCurrentUser({ deleteAfterArchive: newVal });
}
},
mounted() {
if (this.currentPath) {
this.destPath = this.currentPath.replace(/\/+$/, "") || "/";
+2 -1
View File
@@ -327,7 +327,8 @@ export default {
// Only close prompts and reload on success (or partial success)
mutations.setReload(true);
mutations.closeTopHover();
mutations.closeTopHover(); // close conflict prompt
mutations.closeTopHover(); // close moveCopy prompt after conflict is resolved
mutations.setSearch(false);
// Only show success notification if there were no failures (or partial success was already shown)
+9 -1
View File
@@ -31,8 +31,13 @@
</div>
<div v-else-if="!isPickingFile">
<div aria-label="share-path" class="searchContext button"> {{ $t('general.path', { suffix: ':' }) }} {{
<div v-if="isFiles" aria-label="share-path" class="searchContext button"> {{ $t('general.path', { suffix: ':' }) }} {{
displayPath }}</div>
<div v-else>
<a :href="link.sourceURL" class="button button--block">
{{ $t('buttons.goToSource') }}
</a>
</div>
<p> {{ $t('share.notice') }} </p>
<div v-if="listing">
@@ -462,6 +467,9 @@ export default {
isListing() {
return getters.isListing(); // Access getter directly from the store
},
isFiles() {
return getters.isFiles(); // Access getter directly from the store
},
url() {
if (state.isSearchActive) {
const file = /** @type {FilebrowserFile} */ (this.selected[0]);
@@ -16,7 +16,10 @@
class="link-drag-handle">
<i class="material-icons">drag_indicator</i>
</div>
<div class="link-icon">
<div v-if="link.category === 'divider'" class="link-icon">
<i class="material-icons">horizontal_rule</i>
</div>
<div v-else class="link-icon">
<i :class="getIconClass(link.icon)">{{ link.icon }}</i>
</div>
<div class="link-details">
@@ -64,6 +67,7 @@
<option value="share">{{ $t('general.share') }}</option>
<option v-if="context === 'user'" value="tool">{{ $t('general.tool') }}</option>
<option value="custom">{{ $t('sidebar.customLink') }}</option>
<option value="divider">{{ $t('general.divider') }}</option>
<option v-if="context === 'share'" value="shareInfo">{{ $t('share.shareInfo') }}</option>
<option v-if="context === 'share'" value="download">{{ $t('general.download') }}</option>
</select>
@@ -171,6 +175,13 @@
<p>{{ $t('share.downloadDescription') }}</p>
</div>
<!-- Divider - special visual separator -->
<div v-if="newLink.category === 'divider'" class="form-group">
<p>{{ $t('sidebar.linkName') }}</p>
<input aria-label="Link Name" v-model="newLink.name" type="text" class="input"
:placeholder="$t('sidebar.dividerNamePlaceholder')" />
</div>
<!-- Custom Link Input -->
<div v-if="newLink.category === 'custom'" class="form-group">
<p>{{ $t('sidebar.linkName') }}</p>
@@ -182,8 +193,8 @@
:placeholder="$t('sidebar.linkUrlPlaceholder')" />
</div>
<!-- Icon Selection - Available for ALL link types -->
<div v-if="newLink.category" class="form-group">
<!-- Icon Selection - Available for ALL link types except divider -->
<div v-if="newLink.category && newLink.category !== 'divider'" class="form-group">
<p>{{ $t('sidebar.linkIcon') }}</p>
<div class="icon-input-group">
<input v-model="newLink.icon" type="text" class="input icon-input"
@@ -298,8 +309,10 @@ export default {
isNewLinkValid() {
if (!this.newLink.category) return false;
// Special link types for shares don't need additional validation
if (this.newLink.category === "shareInfo" || this.newLink.category === "download") {
// Special link types that don't need additional validation
if (this.newLink.category === "shareInfo" ||
this.newLink.category === "download" ||
this.newLink.category === "divider") {
return true;
}
@@ -457,10 +470,16 @@ export default {
share: this.$t('general.share'),
shareInfo: this.$t('share.shareInfo'),
download: this.$t('general.download'),
divider: this.$t('general.divider'),
};
return labels[category] || category;
},
getLinkDisplayName(link) {
// For dividers, show the name or a default
if (link.category === 'divider') {
return link.name || this.$t('general.divider');
}
// Check if the name looks like a translation key that needs translating
if (link.category === 'shareInfo' && link.name === 'share.shareInfo') {
return this.$t('share.shareInfo');
@@ -612,6 +631,11 @@ export default {
} else if (this.newLink.category === "download") {
// Download is a special action link
linkData.target = "#";
} else if (this.newLink.category === "divider") {
// Divider is a visual separator with no action
linkData.target = "#";
linkData.name = this.newLink.name || ""; // Keep the custom name if provided
linkData.icon = ""; // Dividers don't need an icon
} else if (this.newLink.category === "custom") {
linkData.target = this.processCustomUrl(this.newLink.target);
} else if (this.isSourceCategory(this.newLink.category)) {
+11 -7
View File
@@ -26,11 +26,9 @@
{{ $t("general.path", { suffix: ":" }) }} {{ destPath }}{{ destSource ? ` (${destSource})` : "" }}
</div>
<div class="unarchive-options settings-items">
<ToggleSwitch
class="item"
v-model="deleteAfter"
:name="$t('prompts.unarchiveDeleteAfter')"
/>
<ToggleSwitch class="item" v-model="deleteAfter"
:name="$t('profileSettings.deleteAfterArchive')"
:description="$t('profileSettings.deleteAfterArchiveDescription')" />
</div>
</template>
</div>
@@ -77,7 +75,7 @@
</template>
<script>
import { mutations } from "@/store";
import { state, mutations } from "@/store";
import { url } from "@/utils";
import { notify } from "@/notify";
import { resourcesApi } from "@/api";
@@ -100,11 +98,17 @@ export default {
destPath: "/",
destSource: null,
destType: null,
deleteAfter: true,
deleteAfter: state.user?.deleteAfterArchive == true,
isLoading: false,
showFileList: false,
};
},
watch: {
deleteAfter(newVal) {
// Update the user preference in real time
mutations.updateCurrentUser({ deleteAfterArchive: newVal });
}
},
mounted() {
this.destPath = this.parentPath || "/";
this.destSource = this.itemSource;
+2 -1
View File
@@ -338,7 +338,8 @@ export default {
await usersApi.remove(this.user.id);
notify.showSuccessToast(this.$t("settings.userDeleted"));
eventBus.emit('usersChanged');
mutations.closeTopHover();
mutations.closeTopHover(); // close delete user prompt
mutations.closeTopHover(); // close user prompt since user doens't exist anymore
} catch (e) {
console.error(e);
notify.showErrorToast(this.$t("settings.userDeleteFailed"));
@@ -147,7 +147,6 @@ export default {
},
methods: {
openContextMenu() {
console.log("openContextMenu")
mutations.resetSelected();
mutations.showHover({
name: "ContextMenu",
+85 -36
View File
@@ -1,18 +1,19 @@
<template>
<div class="sidebar-links card">
<!-- Header - sticks always at the top -->
<div class="sidebar-links-header" :class="{ 'no-edit-options': isShare, 'with-top-spacing': isShare && !disableShareCard }">
<i v-if="!isShare" @click="goHome()" class="material-icons action" :title="$t('general.home')">home</i>
<!-- Links section header -->
<div class="sidebar-links-header"
:class="{ 'with-top-spacing': isShare && !disableShareCard }">
<i :class="{ 'disabled': !isLoggedIn }" @click="goHome()" class="material-icons action">home</i>
<!-- Mode button (is the title) -->
<button @click="cycleMode" class="mode-toggle" :title="$t('sidebar.switchMode')">
<button @click="cycleMode" class="mode-toggle" :title="$t('sidebar.switchMode')" @mouseenter="showTooltip($event, $t('sidebar.switchMode'))" @mouseleave="hideTooltip">
{{ mode === 'links' ? $t('general.links') : $t('general.navigation') }}
</button>
<!-- Edit button always visible - hidden in shares -->
<i v-if="!isShare"
@mouseenter="showTooltip($event, $t('sidebar.customizeLinks'))"
@mouseleave="hideTooltip"
@click="openSidebarLinksPrompt"
class="material-icons action">edit</i>
<i v-if="isShare" aria-label="Edit Share" @mouseenter="showTooltip($event, editShareText())" @mouseleave="hideTooltip"
:class="{ 'disabled': !canEdit }"
@click="showEditShareHover" class="material-icons action">edit</i>
<i v-else @mouseenter="showTooltip($event, $t('sidebar.customizeLinks'))" @mouseleave="hideTooltip"
@click="openSidebarLinksPrompt" class="material-icons action">edit</i>
</div>
<!-- Scrollable Content Area -->
<div class="sidebar-links-content">
@@ -24,8 +25,21 @@
</div>
<transition-group name="expand" tag="div" class="inner-card">
<template v-for="(link, index) in sidebarLinksToDisplay" :key="`link-${index}-${link.category}`">
<!-- Divider: renders as hr line or text label -->
<div v-if="link.category === 'divider'" class="sidebar-divider-container">
<hr v-if="!link.name || link.name.toLowerCase() === 'divider'" class="sidebar-divider" />
<span v-else class="sidebar-divider-text">{{ link.name }}</span>
</div>
<!-- Source Location custom link -->
<a v-else-if="link.category === 'custom' && link.name === 'sourceLocation'" :href="link.target"
:aria-label="link.name" class="action button sidebar-link-button" @click.prevent="handleLinkClick(link)">
<div class="link-container">
<i class="material-icons link-icon">open_in_new</i>
<span>{{ $t('buttons.goToSource') }}</span>
</div>
</a>
<!-- Source-type links (source, source-minimal, source-alt, source-hybrid, source-hybrid-2); usage bar hidden for source-minimal -->
<a v-if="link.category === 'source' || link.category === 'source-minimal' || link.category === 'source-alt' || link.category === 'source-hybrid' || link.category === 'source-hybrid-2'" :href="getLinkHref(link)"
<a v-else-if="link.category === 'source' || link.category === 'source-minimal' || link.category === 'source-alt' || link.category === 'source-hybrid' || link.category === 'source-hybrid-2'" :href="getLinkHref(link)"
class="action button source-button sidebar-link-button" :class="{
active: isLinkActive(link),
disabled: !isLinkAccessible(link)
@@ -81,20 +95,11 @@
<a v-else :aria-label="link.name" :href="getLinkHref(link)" class="action button sidebar-link-button"
:class="{ active: isLinkActive(link) }" @click.prevent="handleLinkClick(link)">
<div class="link-container">
<i :class="getIconClass(link.icon) + ' link-icon'">{{ link.icon }}</i>
<i v-if="link.icon" :class="getIconClass(link.icon) + ' link-icon'">{{ link.icon }}</i>
<span>{{ link.name }}</span>
</div>
</a>
</template>
<!-- Edit Share link - only shown when viewing a share and user has share permissions -->
<a v-if="isShare && canEditShare" :aria-label="$t('general.edit', { suffix: ' ' + $t('general.share') })" href="#"
class="action button sidebar-link-button"
@click.prevent="showEditShareHover">
<div class="link-container">
<i class="material-icons link-icon">edit</i>
<span>{{ $t("general.edit", { suffix: " " + $t("general.share") }) }}</span>
</div>
</a>
</transition-group>
</template>
<!-- Navigation Mode -->
@@ -126,7 +131,7 @@
warning
</i>
<!-- Source name -->
<span>{{ activeSource }}</span>
<span>{{ activeSourceLink.name }}</span>
<i v-if="hasUsageInfo(activeSourceLink)"
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showSourceTooltip($event, activeSourceInfo)"
@@ -161,9 +166,9 @@
<!-- Source Dropdown -->
<transition name="dropdown">
<div v-if="showSourceDropdown" class="source-dropdown" ref="dropdown">
<div v-for="sourceName in sourceNames" :key="sourceName" class="dropdown-item"
@click="selectSource(sourceName)">
{{ sourceName }}
<div v-for="item in dropdownSourceItems" :key="item.rawName" class="dropdown-item"
@click="selectSource(item.rawName)">
{{ item.displayName }}
</div>
</div>
</transition>
@@ -202,6 +207,12 @@ export default {
};
},
computed: {
canEdit() {
return state.shareInfo?.canEditShare || false;
},
isLoggedIn() {
return state.user?.username !== 'anonymous';
},
currentSource() {
return state.req?.source || null;
},
@@ -228,10 +239,6 @@ export default {
// Check if user has customized their links
return this.user?.sidebarLinks && this.user.sidebarLinks.length > 0;
},
canEditShare() {
// Check if user is logged in and has share permissions
return state.user && state.user.permissions && state.user.permissions.share;
},
// Share info card props
disableShareCard() {
return state.shareInfo?.disableShareCard;
@@ -293,6 +300,15 @@ export default {
sourceName: this.activeSource,
};
},
dropdownSourceItems() {
return this.sourceNames.map(rawName => {
const customLink = this.sourceLinkMap[rawName];
return {
rawName,
displayName: customLink ? customLink.name : rawName,
};
});
},
},
mounted() {
document.addEventListener('click', this.closeDropdown);
@@ -342,6 +358,9 @@ export default {
return baseURL + target;
},
goHome() {
if (!this.isLoggedIn) {
return;
}
this.$router.push('/');
},
getDefaultLinks() {
@@ -436,10 +455,15 @@ export default {
return;
}
// For all other links (tools, custom, share), navigate using target directly
if (link.target) {
const currentBase = this.$route.path;
const targetBase = link.target.split('?')[0]; // strip any query from target -- this to avoid an error when navigating back with browser history
if (currentBase === targetBase) {
this.$router.replace(link.target);
} else {
this.$router.push(link.target);
mutations.closeTopHover();
}
mutations.closeTopHover();
},
goToDownload() {
// Check if we're in a directory with multiple items
@@ -480,6 +504,9 @@ export default {
mutations.closeTopHover();
},
openSidebarLinksPrompt() {
if (!this.isLoggedIn) {
return;
}
mutations.showHover({
name: "SidebarLinks",
});
@@ -536,6 +563,9 @@ export default {
return buildIndexInfoTooltipHTML(info, this.$t, state.user.locale);
},
async showEditShareHover() {
if (!this.canEdit) {
return;
}
// Get the current share hash and fetch full share details
const shareHash = state.shareInfo?.hash;
if (!shareHash) {
@@ -648,15 +678,12 @@ export default {
}
.share-info-section {
margin-top: 0 !important;
margin-bottom: 0.5em;
padding-bottom: 0.25em;
border-bottom: 1px solid var(--borderColor);
}
.with-top-spacing {
margin-top: 0.5em;
}
.usage-info .vue-simple-progress {
border-style: solid;
border-color: var(--surfaceSecondary);
@@ -675,7 +702,8 @@ export default {
.sidebar-link-button {
margin: 0;
margin-top: 0.25em;
padding: 0;
padding-left: 0.5em;
padding-right: 0.5em;
border-radius: 0.5em;
justify-content: flex-start;
max-width: 98%;
@@ -893,4 +921,25 @@ a.sidebar-link-button {
background: var(--surfaceSecondary);
}
.sidebar-divider-container {
width: 100%;
margin-top: 0.5em;
}
.sidebar-divider {
width: 50%;
border: none;
border-top: 0.1em solid var(--divider);
}
.sidebar-divider-text {
display: flex;
font-size: .85em;
color: var(--textSecondary);
letter-spacing: .05em;
justify-content: center;
align-content: center;
align-items: center;
}
</style>
+36 -23
View File
@@ -58,7 +58,7 @@ body.rtl .listing-items {
}
.listing-item p {
margin: 0;
margin: 0.2em;
}
.listing-item .size,
@@ -262,41 +262,54 @@ body.rtl .listing-items {
min-width: 0;
}
/* These old rules are now redundant - sizing comes from unified container rules above */
.placeholder {
width: var(--icon-size);
background: transparent;
}
.floating-window .listing-items.list .listing-item .size,
.floating-window .listing-items.compact .listing-item .size,
.floating-window .listing-items.list .listing-item .duration,
.floating-window .listing-items.compact .listing-item .duration,
.floating-window .listing-items.list .listing-item .modified,
.floating-window .listing-items.compact .listing-item .modified {
min-width: 20%;
flex: 0;
}
/* Item-specific styles (not header) */
.listing-items .header .size,
.listing-items.list .listing-item .size,
.listing-items.compact .listing-item .size {
width: 25%;
}
.listing-items .header .duration,
.listing-items.compact .listing-item .size,
.listing-items.list .listing-item .duration,
.listing-items.compact .listing-item .duration {
margin-left: auto;
padding-right: 1em;
}
.listing-items.list .listing-item .size.adjustment,
.listing-items.compact .listing-item .size.adjustment {
width: calc(25% + 1em) !important;
.listing-items.compact .listing-item .duration,
.listing-items.list .listing-item .modified,
.listing-items.compact .listing-item .modified {
min-width: 10%;
flex: 0;
}
.listing-items.list .listing-item .name,
.listing-items.compact .listing-item .name {
width: 50%;
}
.listing-items.list .listing-item .name.adjustment,
.listing-items.compact .listing-item .name.adjustment {
width: calc(50% + 1em) !important;
flex: 1;
}
.listing-item .text {
padding-left: 0.5em;
flex: 1;
}
.listing-items.icons .image-preview.download-icon {
width: 100%;
height: 2em;
}
.listing-items.icons .image-preview.download-icon .material-icons {
font-size: 2em;
width: 100%;
text-align: center;
}
.listing-items.compact .listing-item:first-child {
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "الحفظ والخروج",
"keepEditing": "استمر في التحرير",
"closeAnyway": "إغلاق على أي حال",
"goToItem": "الانتقال إلى العنصر",
"watchFile": "ملف المشاهدة",
"showMore": "إظهار الخيارات المتقدمة",
"showLess": "إخفاء الخيارات المتقدمة"
"showLess": "إخفاء الخيارات المتقدمة",
"goToSource": "الانتقال إلى موقع المصدر",
"goToItem": "انتقل إلى العنصر"
},
"download": {
"downloadFile": "Download File",
@@ -221,12 +222,10 @@
"archiveDestination": "اختر المجلد الوجهة (ثم أدخل اسم الأرشيف أدناه):",
"archiveName": "اسم الأرشيف (archive.zip.zip الافتراضي):",
"archiveCompression": "مستوى الضغط (0-9):",
"archiveDeleteAfter": "حذف ملفات المصدر بعد إنشائها بنجاح",
"archiveSuccess": "تم إنشاء الأرشيف بنجاح!",
"unarchive": "أرشيف المقتطفات",
"unarchiveMessage": "اختر مكان استخراج الأرشيف.",
"unarchiveDestination": "مقتطف إلى:",
"unarchiveDeleteAfter": "حذف الأرشيف بعد الاستخراج",
"unarchiveSuccess": "تم استخراج الأرشيف بنجاح!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "عرض شريط الاستخدام من حجم القرص/القسم كما أبلغ عنه نظام التشغيل. من المرجح أن يكون هذا الحجم أكبر ويتضمن ملفات غير مفهرسة.",
"usageTextDisplay": "ما هو نص الاستخدام الذي يجب عرضه؟",
"usageTextIndexed": "الاستخدام كما هو مفهرس",
"usageTextDisk": "الاستخدام كما تم الإبلاغ عنه بواسطة القرص/القسم"
"usageTextDisk": "الاستخدام كما تم الإبلاغ عنه بواسطة القرص/القسم",
"dividerNamePlaceholder": "نص المقسم (اتركه فارغًا لإظهار سطر)"
},
"success": {
"linkCopied": "تم نسخ الملف"
@@ -564,8 +564,9 @@
"results": "النتائج{suffix}",
"users": "المستخدمون{suffix}",
"photos": "الصور",
"audio": "الصوت{suffix}",
"audios": "ملفات صوتية{suffix}",
"videos": "مقاطع الفيديو{suffix}",
"models": "نماذج ثلاثية الأبعاد",
"documents": "المستندات{suffix}",
"archives": "المحفوظات{suffix}",
"images": "الصور{suffix}",
@@ -648,7 +649,8 @@
"space": "الفضاء{suffix}",
"format": "التنسيق{suffix}",
"office": "المكتب{suffix}",
"show": "عرض{suffix}"
"show": "عرض{suffix}",
"divider": "مقسم{suffix}"
},
"access": {
"rules": "قواعد الوصول",
@@ -686,6 +688,11 @@
"downloadChunkSizeMbHelp": "حجم القطع للتحميلات المقطعة. سيتم تنزيل الملفات الأكبر من هذا الحجم على شكل أجزاء لمنع حدوث مشكلات في المهلة (على سبيل المثال، أخطاء Cloudflare 524). يؤدي الإعداد إلى 0 إلى تعطيل التنزيلات المقطعة."
},
"profileSettings": {
"deleteAfterArchive": "حذف الملفات المصدر بعد إنشاء/استخراج الأرشيفات",
"deleteAfterArchiveDescription": "عند التمكين، سيتم حذف الملفات المصدر بعد إنشاء/استخراج الأرشيفات بنجاح",
"hideFilesInTree": "إخفاء الملفات في شجرة التنقل",
"hideFilesInTreeDescription": "عند التمكين، تظهر المجلدات فقط في التنقل الشجري للشريط الجانبي",
"previewModelsDescription": "عرض صور مصغرة للنماذج ثلاثية الأبعاد، سيتم عرض أيقونة عامة إذا كانت معطلة",
"setDateFormat": "تعيين تنسيق التاريخ الدقيق",
"showHiddenFiles": "إظهار الملفات المخفية",
"showHiddenFilesDescription": "إظهار الملفات المخفية في القائمة (مثل .thumbnails، ProgramData)، ولكن لا يزال يمكن الوصول إليها يدويًا إذا تم تكوينها للفهرسة.",
@@ -737,6 +744,7 @@
"previewFolderDescription": "إظهار معاينات الصور المصغرة للمجلدات التي تحتوي على العنصر الفرعي الأول كغلاف - أو عناصر متعددة إذا تم تمكين معاينة الحركة.",
"previewMotion": "معاينات الحركة",
"previewMotionVideosDescription": "معاينة الحركة هي المعاينة المنبثقة المتحركة التي تتنقل عبر إطارات الفيديو أو الصور المتعددة لمجلد",
"previewAudiosDescription": "عرض صور مصغرة للملفات الصوتية، سيتم عرض أيقونة عامة إذا كانت معطلة",
"popupPreview": "العارض المنبثق",
"popupPreviewDescription": "ستظهر نافذة منبثقة متراكبة فوق واجهة المستخدم عند التمرير فوق العناصر ذات الصور المصغرة",
"thumbnailOptions": "خيارات الصور المصغرة",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Uložit a ukončit",
"keepEditing": "Pokračovat v úpravách",
"closeAnyway": "Stejně zavřít",
"goToItem": "Přejít na položku",
"watchFile": "Sledovat soubor",
"showMore": "Zobrazit rozšířené možnosti",
"showLess": "Skrytí pokročilých možností"
"showLess": "Skrytí pokročilých možností",
"goToSource": "Přejít na zdrojové umístění",
"goToItem": "Přejít na položku"
},
"download": {
"downloadFile": "Stáhnout soubor",
@@ -299,12 +300,10 @@
"archiveDestination": "Zvolte cílovou složku (pak zadejte název archivu níže):",
"archiveName": "Název archivu (výchozí archive.zip):",
"archiveCompression": "Úroveň komprese (0-9):",
"archiveDeleteAfter": "Odstranění zdrojových souborů po úspěšném vytvoření",
"archiveSuccess": "Archiv byl úspěšně vytvořen!",
"unarchive": "Výpis archivu",
"unarchiveMessage": "Vyberte, kam se má archiv rozbalit.",
"unarchiveDestination": "Výpis na:",
"unarchiveDeleteAfter": "Odstranění archivu po extrakci",
"unarchiveSuccess": "Archiv úspěšně extrahován!"
},
"search": {
@@ -323,6 +322,12 @@
"showPreviewImagesDescription": "Zobrazení miniaturních náhledů výsledků vyhledávání, pokud jsou k dispozici, a zobrazení větších ikon u všech výsledků."
},
"profileSettings": {
"deleteAfterArchive": "Smazat zdrojové soubory po vytvoření/extrahování archivů",
"deleteAfterArchiveDescription": "Pokud je povoleno, po úspěšném vytvoření/extrahování archivů dojde ke smazání zdrojových souborů",
"hideFilesInTree": "Skrýt soubory ve stromové navigaci",
"hideFilesInTreeDescription": "Pokud je povoleno, v postranním stromovém navigačním panelu se zobrazují pouze složky",
"previewModelsDescription": "Zobrazit náhledy miniatur pro 3D modely, pokud je zakázáno, zobrazí se obecná ikona",
"previewAudiosDescription": "Zobrazit náhledy miniatur pro zvuky, pokud je zakázáno, zobrazí se obecná ikona",
"setDateFormat": "Nastavit formát data",
"showHiddenFiles": "Zobrazit skryté soubory",
"showQuickDownload": "Zobrazit ikonu rychlého stahování",
@@ -517,7 +522,8 @@
"showDiskUsageDescription": "Zobrazení sloupce využití z velikosti disku/oddílu podle hlášení operačního systému. Tato velikost bude pravděpodobně větší a zahrnuje i soubory, které nejsou indexovány.",
"usageTextDisplay": "Který text použití se má zobrazit?",
"usageTextIndexed": "použití podle indexu",
"usageTextDisk": "využití podle hlášení disku/oddílu"
"usageTextDisk": "využití podle hlášení disku/oddílu",
"dividerNamePlaceholder": "Text oddělovače (pro zobrazení řádku nechte prázdný)"
},
"success": {
"linkCopied": "Odkaz zkopírován!"
@@ -642,8 +648,9 @@
"results": "Výsledky{suffix}",
"users": "Uživatelé{suffix}",
"photos": "Fotografie",
"audio": "Audio{suffix}",
"audios": "Zvuky{suffix}",
"videos": "Videa{suffix}",
"models": "3D modely",
"documents": "Dokumenty{suffix}",
"archives": "Archivy{suffix}",
"images": "Obrázky{suffix}",
@@ -726,7 +733,8 @@
"space": "Prostor{suffix}",
"format": "Formát{suffix}",
"office": "Kancelář{suffix}",
"show": "Zobrazit{suffix}"
"show": "Zobrazit{suffix}",
"divider": "Rozdělovač{suffix}"
},
"access": {
"rules": "Pravidla přístupu",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Speichern und Beenden",
"keepEditing": "Weiter bearbeiten",
"closeAnyway": "Jedenfalls schließen",
"goToItem": "Weiter zu Artikel",
"watchFile": "Datei beobachten",
"showMore": "Erweiterte Optionen anzeigen",
"showLess": "Erweiterte Optionen ausblenden"
"showLess": "Erweiterte Optionen ausblenden",
"goToSource": "Zum Quellort gehen",
"goToItem": "Weiter zum Artikel"
},
"download": {
"downloadFile": "Datei herunterladen",
@@ -221,12 +222,10 @@
"archiveDestination": "Wählen Sie den Zielordner (geben Sie dann unten den Archivnamen ein):",
"archiveName": "Name des Archivs (standardmäßig archive.zip):",
"archiveCompression": "Komprimierungsstufe (0-9):",
"archiveDeleteAfter": "Quelldateien nach erfolgreicher Erstellung löschen",
"archiveSuccess": "Archiv erfolgreich erstellt!",
"unarchive": "Archiv extrahieren",
"unarchiveMessage": "Wählen Sie, wohin das Archiv entpackt werden soll.",
"unarchiveDestination": "Auszug zu:",
"unarchiveDeleteAfter": "Archiv nach Extraktion löschen",
"unarchiveSuccess": "Archiv erfolgreich entpackt!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Anzeige der Nutzungsleiste anhand der vom Betriebssystem gemeldeten Festplatten-/Partitionsgröße. Diese Größe wird wahrscheinlich größer sein und schließt nicht indizierte Dateien ein.",
"usageTextDisplay": "Welcher Verwendungstext soll angezeigt werden?",
"usageTextIndexed": "Nutzung wie indiziert",
"usageTextDisk": "Nutzung, wie von der Festplatte/Partition gemeldet"
"usageTextDisk": "Nutzung, wie von der Festplatte/Partition gemeldet",
"dividerNamePlaceholder": "Teilertext (leer lassen, um eine Zeile anzuzeigen)"
},
"success": {
"linkCopied": "Link wurde kopiert!"
@@ -564,8 +564,9 @@
"results": "Ergebnisse{suffix}",
"users": "Benutzer{suffix}",
"photos": "Fotos",
"audio": "Audio{suffix}",
"audios": "Audios{suffix}",
"videos": "Videos{suffix}",
"models": "3D-Modelle",
"documents": "Dokumente{suffix}",
"archives": "Archiv{suffix}",
"images": "Bilder{suffix}",
@@ -648,7 +649,8 @@
"space": "Weltraum{suffix}",
"format": "Format{suffix}",
"office": "Büro{suffix}",
"show": "anzeigen{suffix}"
"show": "anzeigen{suffix}",
"divider": "Teiler{suffix}"
},
"access": {
"rules": "Zugangsregeln",
@@ -694,6 +696,12 @@
"orange": "orange"
},
"profileSettings": {
"deleteAfterArchive": "Quelldateien nach Erstellung/Extraktion von Archiven löschen",
"deleteAfterArchiveDescription": "Wenn aktiviert, werden die Quelldateien nach erfolgreicher Erstellung/Extraktion von Archiven gelöscht",
"hideFilesInTree": "Dateien in der Baumnavigation ausblenden",
"hideFilesInTreeDescription": "Wenn aktiviert, werden in der Seitenleisten-Baumnavigation nur Ordner angezeigt",
"previewModelsDescription": "Vorschaubilder für 3D-Modelle anzeigen, bei Deaktivierung wird ein generisches Symbol angezeigt",
"previewAudiosDescription": "Vorschaubilder für Audiodateien anzeigen, bei Deaktivierung wird ein generisches Symbol angezeigt",
"setDateFormat": "Genaues Datum statt relativer Zeitangaben verwenden",
"showHiddenFiles": "Versteckte Dateien anzeigen",
"showHiddenFilesDescription": "Versteckte Dateien in der Auflistung anzeigen (z.B. .thumbnails, ProgramData), aber sie sind immer noch manuell zugänglich, wenn sie für die Indizierung konfiguriert sind.",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Αποθήκευση και έξοδος",
"keepEditing": "Συνεχίστε την επεξεργασία",
"closeAnyway": "Κλείστε Ούτως ή άλλως",
"goToItem": "Πηγαίνετε στο στοιχείο",
"watchFile": "Παρακολουθήστε το αρχείο",
"showMore": "Εμφάνιση προηγμένων επιλογών",
"showLess": "Απόκρυψη προηγμένων επιλογών"
"showLess": "Απόκρυψη προηγμένων επιλογών",
"goToSource": "Πηγαίνετε στην τοποθεσία της πηγής",
"goToItem": "Πηγαίνετε στο στοιχείο"
},
"download": {
"downloadFile": "Λήψη αρχείου",
@@ -224,12 +225,10 @@
"archiveDestination": "Επιλέξτε φάκελο προορισμού (στη συνέχεια, εισάγετε το όνομα του αρχείου παρακάτω):",
"archiveName": "Όνομα αρχείου (προεπιλεγμένο archive.zip):",
"archiveCompression": "Επίπεδο συμπίεσης (0-9):",
"archiveDeleteAfter": "Διαγραφή αρχείων πηγής μετά την επιτυχή δημιουργία",
"archiveSuccess": "Το αρχείο δημιουργήθηκε με επιτυχία!",
"unarchive": "Απόσπασμα αρχείου",
"unarchiveMessage": "Επιλέξτε πού θα εξαγάγετε το αρχείο.",
"unarchiveDestination": "Απόσπασμα προς:",
"unarchiveDeleteAfter": "Διαγραφή αρχείου μετά την εξαγωγή",
"unarchiveSuccess": "Το αρχείο εξήχθη με επιτυχία!"
},
"search": {
@@ -384,7 +383,8 @@
"showDiskUsageDescription": "Εμφάνιση μπάρας χρήσης από το μέγεθος του δίσκου/κατάτμησης όπως αναφέρεται από το λειτουργικό σύστημα. Αυτό το μέγεθος είναι πιθανό να είναι μεγαλύτερο και περιλαμβάνει αρχεία που δεν έχουν ευρετηριαστεί.",
"usageTextDisplay": "Ποιο κείμενο χρήσης πρέπει να εμφανίζεται;",
"usageTextIndexed": "χρήση ως ευρετήριο",
"usageTextDisk": "χρήση όπως αναφέρεται από δίσκο/κατάτμηση"
"usageTextDisk": "χρήση όπως αναφέρεται από δίσκο/κατάτμηση",
"dividerNamePlaceholder": "Κείμενο διαχωριστικού (αφήστε το κενό για να εμφανιστεί μια γραμμή)"
},
"success": {
"linkCopied": "Ο σύνδεσμος αντιγράφηκε!"
@@ -564,8 +564,9 @@
"results": "Αποτελέσματα{suffix}",
"users": "Χρήστες{suffix}",
"photos": "Φωτογραφίες",
"audio": "Ήχος{suffix}",
"audios": "Ήχοι{suffix}",
"videos": "Βίντεο{suffix}",
"models": "Τρισδιάστατα μοντέλα",
"documents": "Έγγραφα{suffix}",
"archives": "Αρχεία{suffix}",
"images": "Εικόνες{suffix}",
@@ -648,7 +649,8 @@
"space": "Διάστημα{suffix}",
"format": "Μορφή{suffix}",
"office": "Γραφείο{suffix}",
"show": "Εμφάνιση{suffix}"
"show": "Εμφάνιση{suffix}",
"divider": "Διαχωριστικό{suffix}"
},
"access": {
"rules": "Κανόνες πρόσβασης",
@@ -694,6 +696,12 @@
"orange": "πορτοκαλί"
},
"profileSettings": {
"deleteAfterArchive": "Διαγραφή αρχείων προέλευσης μετά τη δημιουργία/εξαγωγή αρχείων",
"deleteAfterArchiveDescription": "Όταν ενεργοποιηθεί, θα διαγραφούν τα αρχεία προέλευσης μετά την επιτυχή δημιουργία/εξαγωγή αρχείων",
"hideFilesInTree": "Απόκρυψη αρχείων στην πλοήγηση δέντρου",
"hideFilesInTreeDescription": "Όταν ενεργοποιηθεί, μόνο οι φάκελοι εμφανίζονται στην πλευρική δενδρική πλοήγηση",
"previewModelsDescription": "Εμφάνιση μικρογραφιών για τρισδιάστατα μοντέλα, θα εμφανιστεί γενικό εικονίδιο εάν είναι απενεργοποιημένο",
"previewAudiosDescription": "Εμφάνιση μικρογραφιών για ήχους, θα εμφανιστεί γενικό εικονίδιο εάν είναι απενεργοποιημένο",
"setDateFormat": "Ορισμός ακριβούς μορφής ημερομηνίας",
"showHiddenFiles": "Εμφάνιση κρυφών αρχείων",
"showHiddenFilesDescription": "Εμφάνιση κρυμμένων αρχείων στην καταχώριση (π.χ. .thumbnails, ProgramData), αλλά εξακολουθούν να είναι προσβάσιμα χειροκίνητα, εάν έχουν ρυθμιστεί για ευρετηρίαση.",
+12 -4
View File
@@ -8,6 +8,7 @@
"incompleteResultsDetails": "Showing max results ({max}). Try narrowing your search to get more results under {max}."
},
"general": {
"divider": "Divider{suffix}",
"show": "Show{suffix}",
"office": "Office{suffix}",
"format": "Format{suffix}",
@@ -56,12 +57,13 @@
"scopes": "Scopes{suffix}",
"commands": "Commands{suffix}",
"photos": "Photos",
"audio": "Audio{suffix}",
"audios": "Audios{suffix}",
"videos": "Videos{suffix}",
"documents": "Documents{suffix}",
"archives": "Archives{suffix}",
"images": "Images{suffix}",
"music": "Music{suffix}",
"models": "3D Models",
"pdf": "PDF{suffix}",
"search": "Search{suffix}",
"number": "Number{suffix}",
@@ -160,7 +162,8 @@
"saveAndExit": "Save and Exit",
"keepEditing": "Keep Editing",
"closeAnyway": "Close Anyways",
"goToItem": "Go to Item"
"goToItem": "Go to item",
"goToSource": "Go to Source Location"
},
"download": {
"downloadFile": "Download File",
@@ -365,12 +368,10 @@
"archiveDestination": "Choose destination folder (then enter archive name below):",
"archiveName": "Archive name (default archive.zip):",
"archiveCompression": "Compression level (0–9):",
"archiveDeleteAfter": "Delete source files after successful creation",
"archiveSuccess": "Archive created successfully!",
"unarchive": "Extract archive",
"unarchiveMessage": "Choose where to extract the archive.",
"unarchiveDestination": "Extract to:",
"unarchiveDeleteAfter": "Delete archive after successful decompression",
"unarchiveSuccess": "Archive extracted successfully!",
"dragAndDrop": "Drag and drop files here",
"error": "Something went wrong",
@@ -452,6 +453,10 @@
"helpText2": "The index: Search utilizes the index which automatically gets updated on the configured interval (default: 5 minutes). Searching when the program has just started may result in incomplete results."
},
"profileSettings": {
"deleteAfterArchive": "Delete source files after creation/extraction of archives",
"deleteAfterArchiveDescription": "When enabled, will delete the source files after successful creation/extraction of archives",
"hideFilesInTree": "Hide files in tree navigation",
"hideFilesInTreeDescription": "When enabled, only folders are shown in the sidebar Tree Navigation.",
"previewFolder": "Show thumbnails for folders if available",
"previewFolderDescription": "Show thumbnail previews for folders with first child item as cover -- or multiple items if motion preview is enabled.",
"debugOfficeEditor": "Enable OnlyOffice Debug Mode",
@@ -469,6 +474,8 @@
"showQuickDownloadDescription": "Show download icon for quick access on each file item that can be clicked to download instantly.",
"previewImagesDescription": "Show thumbnail previews for images, will show generic icon if disabled.",
"previewVideosDescription": "Show thumbnail previews for videos, will show generic icon if disabled.",
"previewAudiosDescription": "Show thumbnail previews for audios, will show generic icon if disabled.",
"previewModelsDescription": "Show thumbnail previews for 3D Models, will show generic icon if disabled.",
"previewMotion": "Motion previews",
"previewMotionVideosDescription": "Motion preview is the animated popup preview that cycles through the frames of the video or multiple images for a folder",
"editOffice": "View and edit office files with OnlyOffice",
@@ -604,6 +611,7 @@
"adminOptions": "Admin user options"
},
"sidebar": {
"dividerNamePlaceholder": "Divider text (leave blank to show a line)",
"siteSettings": "Site Settings",
"updateIsAvailable": "An update is available!",
"fileActions": "File actions",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Guardar y salir",
"keepEditing": "Seguir editando",
"closeAnyway": "Cerrar de todos modos",
"goToItem": "Ir al elemento",
"watchFile": "Ver archivo",
"showMore": "Mostrar opciones avanzadas",
"showLess": "Ocultar opciones avanzadas"
"showLess": "Ocultar opciones avanzadas",
"goToSource": "Ir a la ubicación de origen",
"goToItem": "Ir al artículo"
},
"download": {
"downloadFile": "Descargar archivo",
@@ -221,12 +222,10 @@
"archiveDestination": "Elija la carpeta de destino (a continuación, introduzca el nombre del archivo):",
"archiveName": "Nombre del archivo (por defecto archive.zip):",
"archiveCompression": "Nivel de compresión (0-9):",
"archiveDeleteAfter": "Eliminar los archivos de origen después de crearlos correctamente",
"archiveSuccess": "¡Archivo creado con éxito!",
"unarchive": "Extraer archivo",
"unarchiveMessage": "Elija dónde extraer el archivo.",
"unarchiveDestination": "Extraer a:",
"unarchiveDeleteAfter": "Eliminar el archivo tras la extracción",
"unarchiveSuccess": "¡Archivo extraído con éxito!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Muestra la barra de uso a partir del tamaño del disco/partición indicado por el sistema operativo. Es probable que este tamaño sea mayor e incluya archivos no indexados.",
"usageTextDisplay": "¿Qué texto de uso debe mostrarse?",
"usageTextIndexed": "uso como indexado",
"usageTextDisk": "uso por disco/partición"
"usageTextDisk": "uso por disco/partición",
"dividerNamePlaceholder": "Texto divisor (déjelo en blanco para mostrar una línea)"
},
"success": {
"linkCopied": "¡Enlace copiado!"
@@ -564,8 +564,9 @@
"results": "Resultados{suffix}",
"users": "Usuarios{suffix}",
"photos": "Fotos",
"audio": "Audio{suffix}",
"audios": "Audios{suffix}",
"videos": "Vídeos{suffix}",
"models": "Modelos 3D",
"documents": "Documentos{suffix}",
"archives": "Archivos comprimidos{suffix}",
"images": "Imágenes{suffix}",
@@ -648,7 +649,8 @@
"space": "Espacio{suffix}",
"format": "Formato{suffix}",
"office": "Oficina{suffix}",
"show": "Mostrar{suffix}"
"show": "Mostrar{suffix}",
"divider": "Divisor{suffix}"
},
"access": {
"rules": "Reglas de acceso",
@@ -694,6 +696,12 @@
"orange": "Naranja"
},
"profileSettings": {
"deleteAfterArchive": "Eliminar archivos originales después de la creación/extracción de archivos",
"deleteAfterArchiveDescription": "Cuando está activado, eliminará los archivos originales después de la creación o extracción de archivos",
"hideFilesInTree": "Ocultar archivos en la navegación de árbol",
"hideFilesInTreeDescription": "Cuando está activado, solo se muestrarán carpetas en la navegación del panel lateral",
"previewModelsDescription": "Mostrar vistas previas en miniatura para modelos 3D, se mostrará un icono genérico si está desactivado",
"previewAudiosDescription": "Mostrar vistas previas en miniatura para audios, si está desactivado mostrará un icono generico",
"autoplayMedia": "Reproducción automática de archivos multimedia",
"autoplayMediaDescription": "Reproduce automaticamente los archivos multimedia al abrirlos",
"sidebarOptions": "Opciones del panel lateral",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Sauvegarder et quitter",
"keepEditing": "Continuer à éditer",
"closeAnyway": "Fermer Quoi qu'il en soit",
"goToItem": "Aller à l'article",
"watchFile": "Regarder le dossier",
"showMore": "Afficher les options avancées",
"showLess": "Masquer les options avancées"
"showLess": "Masquer les options avancées",
"goToSource": "Aller à l'emplacement de la source",
"goToItem": "Aller à l'article"
},
"download": {
"downloadFile": "Télécharger le fichier",
@@ -221,12 +222,10 @@
"archiveDestination": "Choisissez le dossier de destination (puis entrez le nom de l'archive ci-dessous) :",
"archiveName": "Nom de l'archive (par défaut archive.zip) :",
"archiveCompression": "Niveau de compression (0-9) :",
"archiveDeleteAfter": "Supprimer les fichiers sources après une création réussie",
"archiveSuccess": "Archive créée avec succès !",
"unarchive": "Extraire l'archive",
"unarchiveMessage": "Choisissez l'endroit où extraire l'archive.",
"unarchiveDestination": "Extrait à :",
"unarchiveDeleteAfter": "Supprimer l'archive après l'extraction",
"unarchiveSuccess": "L'archive a été extraite avec succès !"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Affichage de la barre d'utilisation à partir de la taille du disque/de la partition telle qu'elle est indiquée par le système d'exploitation. Cette taille est probablement plus importante et inclut les fichiers non indexés.",
"usageTextDisplay": "Quel texte d'utilisation doit être affiché ?",
"usageTextIndexed": "utilisation telle qu'indexée",
"usageTextDisk": "utilisation telle que rapportée par disque/partition"
"usageTextDisk": "utilisation telle que rapportée par disque/partition",
"dividerNamePlaceholder": "Texte du séparateur (laisser en blanc pour montrer une ligne)"
},
"success": {
"linkCopied": "Lien copié !"
@@ -564,8 +564,9 @@
"results": "Résultats{suffix}",
"users": "Utilisateurs{suffix}",
"photos": "Photos",
"audio": "Audio{suffix}",
"audios": "Audios{suffix}",
"videos": "Vidéos{suffix}",
"models": "Modèles 3D",
"documents": "Documents{suffix}",
"archives": "Archives{suffix}",
"images": "Images{suffix}",
@@ -648,7 +649,8 @@
"space": "L'espace{suffix}",
"format": "Format{suffix}",
"office": "Bureau{suffix}",
"show": "Afficher{suffix}"
"show": "Afficher{suffix}",
"divider": "Diviseur{suffix}"
},
"access": {
"rules": "Règles d'accès",
@@ -694,6 +696,12 @@
"orange": "orange"
},
"profileSettings": {
"deleteAfterArchive": "Supprimer les fichiers source après la création/extraction d'archives",
"deleteAfterArchiveDescription": "Lorsque activé, supprimera les fichiers source après la création/extraction réussie d'archives",
"hideFilesInTree": "Masquer les fichiers dans l'arborescence",
"hideFilesInTreeDescription": "Lorsque activé, seuls les dossiers sont affichés dans l'arborescence de navigation de la barre latérale",
"previewModelsDescription": "Afficher les aperçus miniatures pour les modèles 3D, une icône générique s'affichera si désactivé",
"previewAudiosDescription": "Afficher les aperçus miniatures pour les fichiers audio, une icône générique s'affichera si désactivé",
"setDateFormat": "Définir le format exact de la date",
"showHiddenFiles": "Afficher les fichiers cachés",
"showHiddenFilesDescription": "Affiche les fichiers cachés dans la liste (par exemple, .thumbnails, ProgramData), mais ils restent accessibles manuellement s'ils sont configurés pour être indexés.",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "שמור וצא",
"keepEditing": "המשך עריכה",
"closeAnyway": "סגור בכל מקרה",
"goToItem": "עבור לפריט",
"watchFile": "צפה בקובץ",
"showMore": "הצג אפשרויות מתקדמות",
"showLess": "הסתר אפשרויות מתקדמות"
"showLess": "הסתר אפשרויות מתקדמות",
"goToSource": "עבור למיקום המקור",
"goToItem": "עבור לפריט"
},
"download": {
"downloadFile": "הורד קובץ",
@@ -221,12 +222,10 @@
"archiveDestination": "בחר תיקיית יעד (ואז הזן את שם הארכיון למטה):",
"archiveName": "שם הארכיון (ברירת מחדל: archive.zip):",
"archiveCompression": "רמת דחיסה (0–9):",
"archiveDeleteAfter": "מחק קבצי מקור לאחר יצירה מוצלחת",
"archiveSuccess": "הארכיון נוצר בהצלחה!",
"unarchive": "חלץ ארכיון",
"unarchiveMessage": "בחר היכן לחלץ את הארכיון.",
"unarchiveDestination": "חלץ אל:",
"unarchiveDeleteAfter": "מחק את הארכיון לאחר החילוץ",
"unarchiveSuccess": "הארכיון חולץ בהצלחה!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "הצג את סרגל השימוש בגודל הדיסק/מחיצה כפי שדווח על ידי מערכת ההפעלה. גודל זה צפוי להיות גדול יותר וכולל קבצים שלא הוכנסו לאינדקס.",
"usageTextDisplay": "איזה טקסט שימוש יש להציג?",
"usageTextIndexed": "שימוש כפי שמופיע באינדקס",
"usageTextDisk": "שימוש כפי שדווח על ידי דיסק/מחיצה"
"usageTextDisk": "שימוש כפי שדווח על ידי דיסק/מחיצה",
"dividerNamePlaceholder": "טקסט מפריד (השאר ריק כדי להציג קו)"
},
"success": {
"linkCopied": "הקישור הועתק!"
@@ -564,8 +564,9 @@
"results": "תוצאות{suffix}",
"users": "משתמשים{suffix}",
"photos": "תמונות",
"audio": "אודיו{suffix}",
"audios": "שמע{suffix}",
"videos": "סרטונים{suffix}",
"models": "דגמי תלת-ממד",
"documents": "מסמכים{suffix}",
"archives": "ארכיונים{suffix}",
"images": "תמונות{suffix}",
@@ -648,7 +649,8 @@
"space": "חלל{suffix}",
"format": "פורמט{suffix}",
"office": "משרד{suffix}",
"show": "הצג{suffix}"
"show": "הצג{suffix}",
"divider": "מפריד{suffix}"
},
"access": {
"rules": "כללי גישה",
@@ -694,6 +696,12 @@
"orange": "כתום"
},
"profileSettings": {
"deleteAfterArchive": "מחק קבצי מקור לאחר יצירה/חילוץ של ארכיונים",
"deleteAfterArchiveDescription": "כאשר מופעל, ימחק את קבצי המקור לאחר יצירה/חילוץ מוצלחים של ארכיונים",
"hideFilesInTree": "הסתר קבצים בניווט עץ",
"hideFilesInTreeDescription": "כאשר מופעל, רק תיקיות מוצגות בניווט העץ של סרגל הצד",
"previewModelsDescription": "הצג תצוגות מקדימות ממוזערות לדגמי תלת-ממד, יוצג אייקון כללי אם מושבת",
"previewAudiosDescription": "הצג תצוגות מקדימות ממוזערות לשמע, יוצג אייקון כללי אם מושבת",
"setDateFormat": "הגדר פורמט תאריך מדויק",
"showHiddenFiles": "הצג קבצים מוסתרים",
"showHiddenFilesDescription": "הצג קבצים מוסתרים ברשימה (לדוגמה, .thumbnails, ProgramData), אך הם עדיין נגישים באופן ידני אם הוגדרו לאינדקס.",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Mentés és kilépés",
"keepEditing": "Szerkesztés folytatása",
"closeAnyway": "Egyébként bezárni",
"goToItem": "Tovább a tételhez",
"watchFile": "Nézze meg a fájlt",
"showMore": "Speciális beállítások megjelenítése",
"showLess": "Speciális beállítások elrejtése"
"showLess": "Speciális beállítások elrejtése",
"goToSource": "Menj a forráshelyre",
"goToItem": "Tovább a tételhez"
},
"download": {
"downloadFile": "Fájl letöltése",
@@ -221,12 +222,10 @@
"archiveDestination": "Válassza ki a célmappát (majd adja meg az archívum nevét):",
"archiveName": "Archívum neve (alapértelmezett archive.zip):",
"archiveCompression": "Tömörítési szint (0-9):",
"archiveDeleteAfter": "Forrásfájlok törlése sikeres létrehozás után",
"archiveSuccess": "Az archívum sikeresen létrehozva!",
"unarchive": "Kivonat archívum",
"unarchiveMessage": "Válassza ki, hová szeretné kinyomtatni az archívumot.",
"unarchiveDestination": "Kivonat a következő címre:",
"unarchiveDeleteAfter": "Törölje az archívumot az extrakció után",
"unarchiveSuccess": "Az archívum sikeresen kinyomtatva!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "A lemez/partíció méretéből származó, az operációs rendszer által jelentett használati sáv megjelenítése. Ez a méret valószínűleg nagyobb lesz, és tartalmazza az indexeletlen fájlokat is.",
"usageTextDisplay": "Milyen használati szöveget kell megjeleníteni?",
"usageTextIndexed": "indexált használat",
"usageTextDisk": "lemez/partíció által bejelentett használat"
"usageTextDisk": "lemez/partíció által bejelentett használat",
"dividerNamePlaceholder": "Választószöveg (üresen hagyva egy sor megjelenítéséhez)"
},
"success": {
"linkCopied": "Link másolva!"
@@ -564,8 +564,9 @@
"results": "Eredmények{suffix}",
"users": "Felhasználók{suffix}",
"photos": "Fotók",
"audio": "Audio{suffix}",
"audios": "Hangfájlok{suffix}",
"videos": "Videók{suffix}",
"models": "3D modellek",
"documents": "Dokumentumok{suffix}",
"archives": "Archívum{suffix}",
"images": "Képek{suffix}",
@@ -648,7 +649,8 @@
"space": "Űr{suffix}",
"format": "Formátum{suffix}",
"office": "Iroda{suffix}",
"show": "Mutasd meg a{suffix}"
"show": "Mutasd meg a{suffix}",
"divider": "Választó{suffix}"
},
"access": {
"rules": "Hozzáférési szabályok",
@@ -694,6 +696,12 @@
"orange": "narancs"
},
"profileSettings": {
"deleteAfterArchive": "Forrásfájlok törlése archívumok létrehozása/kibontása után",
"deleteAfterArchiveDescription": "Ha engedélyezve van, törli a forrásfájlokat az archívumok sikeres létrehozása/kibontása után",
"hideFilesInTree": "Fájlok elrejtése a fa navigációban",
"hideFilesInTreeDescription": "Ha engedélyezve van, csak mappák jelennek meg az oldalsáv fa navigációjában",
"previewModelsDescription": "Miniatűr előnézetek megjelenítése 3D modellekhez, általános ikon jelenik meg ha letiltott",
"previewAudiosDescription": "Miniatűr előnézetek megjelenítése hangfájlokhoz, általános ikon jelenik meg ha letiltott",
"setDateFormat": "Pontos dátumformátum beállítása",
"showHiddenFiles": "Rejtett fájlok megjelenítése",
"showHiddenFilesDescription": "Rejtett fájlok megjelenítése a listában (pl. .thumbnails, ProgramData), de kézzel továbbra is elérhetőek, ha indexelésre vannak beállítva.",
+8 -1
View File
@@ -274,8 +274,9 @@
"onlyFiles": "Aðeins Skrár",
"showOptions": "Sýna Valmöguleika",
"photos": "Myndir",
"audio": "Hljóð",
"audios": "Hljóðskrár{suffix}",
"videos": "Myndskeið",
"models": "Þrívíð líkön",
"documents": "Skjöl",
"archives": "Safnskrár",
"notEnoughCharacters": "Ekki nógu mörg stafir til að leita (lágmark {minSearchLength})",
@@ -295,6 +296,12 @@
"helpText2": "Vísirinn: Leit nýtir vísinn sem uppfærast sjálfkrafa á stilluðu millibili (sjálfgefið: 5 mínútur). Leit þegar forritið er nýrætt getur leitt til ófullnægjandi niðurstaðna."
},
"profileSettings": {
"deleteAfterArchive": "Eyða upprunaskrám eftir stofnun/útdrátt safnskráa",
"deleteAfterArchiveDescription": "Þegar virkt, mun eyða upprunaskrám eftir árangursríka stofnun/útdrátt safnskráa",
"hideFilesInTree": "Fela skrár í trjáflakki",
"hideFilesInTreeDescription": "Þegar virkt, eru aðeins möppur sýndar í hliðarstiku trjáflakki",
"previewModelsDescription": "Sýna smámyndaforskoðun fyrir þrívíð líkön, mun sýna almenn tákn ef óvirkt",
"previewAudiosDescription": "Sýna smámyndaforskoðun fyrir hljóðskrár, mun sýna almenn tákn ef óvirkt",
"previewFolder": "Sýna smámyndir fyrir möppur ef tiltækar",
"previewFolderDescription": "Sýna smámyndaforskoðun fyrir möppur með fyrsta barnatriði með smámynd sem forsíðu.",
"debugOfficeEditor": "Virkja OnlyOffice Kembunarbil",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Salvare e uscire",
"keepEditing": "Continuare a modificare",
"closeAnyway": "Chiudere comunque",
"goToItem": "Vai all'articolo",
"watchFile": "Guarda il file",
"showMore": "Mostra opzioni avanzate",
"showLess": "Nascondere le opzioni avanzate"
"showLess": "Nascondere le opzioni avanzate",
"goToSource": "Vai alla posizione della fonte",
"goToItem": "Vai alla voce"
},
"download": {
"downloadFile": "Scarica file",
@@ -221,12 +222,10 @@
"archiveDestination": "Scegliere la cartella di destinazione (quindi inserire il nome dell'archivio sotto):",
"archiveName": "Nome dell'archivio (predefinito archive.zip):",
"archiveCompression": "Livello di compressione (0-9):",
"archiveDeleteAfter": "Cancellare i file sorgente dopo la creazione",
"archiveSuccess": "Archivio creato con successo!",
"unarchive": "Estrarre l'archivio",
"unarchiveMessage": "Scegliere dove estrarre l'archivio.",
"unarchiveDestination": "Estratto a:",
"unarchiveDeleteAfter": "Eliminare l'archivio dopo l'estrazione",
"unarchiveSuccess": "Archivio estratto con successo!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Visualizza la barra di utilizzo dalla dimensione del disco/partizione come riportata dal sistema operativo. Questa dimensione sarà probabilmente maggiore e comprende i file non indicizzati.",
"usageTextDisplay": "Quale testo d'uso deve essere visualizzato?",
"usageTextIndexed": "utilizzo come indicizzato",
"usageTextDisk": "utilizzo come riportato dal disco/partizione"
"usageTextDisk": "utilizzo come riportato dal disco/partizione",
"dividerNamePlaceholder": "Testo del divisore (lasciare vuoto per mostrare una riga)"
},
"success": {
"linkCopied": "Link copiato!"
@@ -564,8 +564,9 @@
"results": "Risultati{suffix}",
"users": "Utenti{suffix}",
"photos": "Foto",
"audio": "Audio{suffix}",
"audios": "Audios{suffix}",
"videos": "Video{suffix}",
"models": "Modelli 3D",
"documents": "Documenti{suffix}",
"archives": "Archivio{suffix}",
"images": "Immagini{suffix}",
@@ -648,7 +649,8 @@
"space": "Spazio{suffix}",
"format": "Formato{suffix}",
"office": "Ufficio{suffix}",
"show": "Mostra{suffix}"
"show": "Mostra{suffix}",
"divider": "Divisore{suffix}"
},
"access": {
"rules": "Regole di accesso",
@@ -694,6 +696,12 @@
"orange": "arancione"
},
"profileSettings": {
"deleteAfterArchive": "Elimina i file sorgente dopo la creazione/estrazione di archivi",
"deleteAfterArchiveDescription": "Quando abilitato, eliminerà i file sorgente dopo la creazione/estrazione riuscita di archivi",
"hideFilesInTree": "Nascondi file nella navigazione ad albero",
"hideFilesInTreeDescription": "Quando abilitato, solo le cartelle vengono mostrate nella navigazione ad albero della barra laterale",
"previewModelsDescription": "Mostra anteprime in miniatura per modelli 3D, verrà mostrata un'icona generica se disabilitato",
"previewAudiosDescription": "Mostra anteprime in miniatura per gli audio, verrà mostrata un'icona generica se disabilitato",
"setDateFormat": "Impostare il formato esatto della data",
"showHiddenFiles": "Mostra i file nascosti",
"showHiddenFilesDescription": "Mostra i file nascosti nell'elenco (ad esempio .thumbnails, ProgramData), ma sono ancora accessibili manualmente se configurati per essere indicizzati.",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "保存して終了する",
"keepEditing": "編集を続ける",
"closeAnyway": "とにかく閉じる",
"goToItem": "アイテムへ",
"watchFile": "ウォッチファイル",
"showMore": "詳細オプションを表示",
"showLess": "詳細オプションを隠す"
"showLess": "詳細オプションを隠す",
"goToSource": "ソース・ロケーションへ",
"goToItem": "アイテムへ"
},
"download": {
"downloadFile": "ファイルをダウンロード",
@@ -221,12 +222,10 @@
"archiveDestination": "保存先フォルダを選択します:",
"archiveName": "アーカイブ名(デフォルトはarchive.zip):",
"archiveCompression": "圧縮レベル(0-9):",
"archiveDeleteAfter": "作成に成功したらソースファイルを削除する",
"archiveSuccess": "アーカイブは正常に作成されました!",
"unarchive": "アーカイブの展開",
"unarchiveMessage": "アーカイブを展開する場所を選択します。",
"unarchiveDestination": "に抜粋する:",
"unarchiveDeleteAfter": "抽出後にアーカイブを削除する",
"unarchiveSuccess": "アーカイブは正常に抽出されました!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "OSが報告したディスク/パーティションサイズから使用量バーを表示します。このサイズは、インデックス化されていないファイルを含むため、より大きくなる可能性があります。",
"usageTextDisplay": "どのテキストを表示するべきか?",
"usageTextIndexed": "索引どおり",
"usageTextDisk": "ディスク/パーティションごとの使用量"
"usageTextDisk": "ディスク/パーティションごとの使用量",
"dividerNamePlaceholder": "分割テキスト(行を表示するには空白にする)"
},
"success": {
"linkCopied": "リンクがコピーされました!"
@@ -564,8 +564,9 @@
"results": "結果{suffix}",
"users": "ユーザー{suffix}",
"photos": "写真",
"audio": "オーディオ{suffix}",
"audios": "オーディオ{suffix}",
"videos": "ビデオ{suffix}",
"models": "3Dモデル",
"documents": "資料{suffix}",
"archives": "アーカイブス{suffix}",
"images": "画像{suffix}",
@@ -648,7 +649,8 @@
"space": "スペース{suffix}",
"format": "フォーマット{suffix}",
"office": "オフィス{suffix}",
"show": "ショー{suffix}"
"show": "ショー{suffix}",
"divider": "ディバイダー{suffix}"
},
"access": {
"rules": "アクセスルール",
@@ -694,6 +696,12 @@
"orange": "オレンジ"
},
"profileSettings": {
"deleteAfterArchive": "アーカイブの作成/展開後にソースファイルを削除",
"deleteAfterArchiveDescription": "有効にすると、アーカイブの作成/展開が成功した後にソースファイルを削除します",
"hideFilesInTree": "ツリーナビゲーションでファイルを非表示",
"hideFilesInTreeDescription": "有効にすると、サイドバーのツリーナビゲーションにフォルダーのみが表示されます",
"previewModelsDescription": "3Dモデルのサムネイルプレビューを表示します。無効の場合は一般的なアイコンを表示します",
"previewAudiosDescription": "オーディオのサムネイルプレビューを表示します。無効の場合は一般的なアイコンを表示します",
"setDateFormat": "正確な日付書式を設定する",
"showHiddenFiles": "隠しファイルを表示する",
"showHiddenFilesDescription": "隠しファイル(.thumbnails、ProgramDataなど)をリストに表示します。インデックスが設定されている場合は手動でアクセスできます。",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "저장 및 종료",
"keepEditing": "계속 편집",
"closeAnyway": "어쨌든 닫기",
"goToItem": "항목으로 이동",
"watchFile": "파일 보기",
"showMore": "고급 옵션 표시",
"showLess": "고급 옵션 숨기기"
"showLess": "고급 옵션 숨기기",
"goToSource": "소스 위치로 이동",
"goToItem": "항목으로 이동"
},
"download": {
"downloadFile": "파일 다운로드",
@@ -221,12 +222,10 @@
"archiveDestination": "대상 폴더를 선택한 다음 아래에 아카이브 이름을 입력합니다:",
"archiveName": "아카이브 이름(기본값은 archive.zip):",
"archiveCompression": "압축 수준(0-9):",
"archiveDeleteAfter": "생성 성공 후 소스 파일 삭제",
"archiveSuccess": "아카이브가 성공적으로 생성되었습니다!",
"unarchive": "아카이브 추출",
"unarchiveMessage": "아카이브를 추출할 위치를 선택합니다.",
"unarchiveDestination": "추출 대상:",
"unarchiveDeleteAfter": "추출 후 아카이브 삭제",
"unarchiveSuccess": "아카이브가 성공적으로 추출되었습니다!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "OS에서 보고한 디스크/파티션 크기에서 사용량 막대를 표시합니다. 이 크기는 더 클 수 있으며 인덱싱되지 않은 파일도 포함됩니다.",
"usageTextDisplay": "어떤 사용법 텍스트를 표시해야 하나요?",
"usageTextIndexed": "인덱싱된 사용량",
"usageTextDisk": "디스크/파티션별로 보고된 사용량"
"usageTextDisk": "디스크/파티션별로 보고된 사용량",
"dividerNamePlaceholder": "디바이더 텍스트(줄을 표시하려면 비워둡니다)"
},
"success": {
"linkCopied": "링크가 복사되었습니다!"
@@ -564,8 +564,9 @@
"results": "결과{suffix}",
"users": "사용자{suffix}",
"photos": "사진",
"audio": "오디오{suffix}",
"audios": "오디오{suffix}",
"videos": "동영상{suffix}",
"models": "3D 모델",
"documents": "문서{suffix}",
"archives": "아카이브{suffix}",
"images": "이미지{suffix}",
@@ -648,7 +649,8 @@
"space": "공간{suffix}",
"format": "형식{suffix}",
"office": "Office{suffix}",
"show": "표시{suffix}"
"show": "표시{suffix}",
"divider": "디바이더{suffix}"
},
"access": {
"rules": "액세스 규칙",
@@ -694,6 +696,12 @@
"orange": "오렌지"
},
"profileSettings": {
"deleteAfterArchive": "아카이브 생성/추출 후 소스 파일 삭제",
"deleteAfterArchiveDescription": "활성화되면 아카이브 생성/추출 성공 후 소스 파일을 삭제합니다",
"hideFilesInTree": "트리 탐색에서 파일 숨기기",
"hideFilesInTreeDescription": "활성화되면 사이드바 트리 탐색에 폴더만 표시됩니다",
"previewModelsDescription": "3D 모델의 썸네일 미리보기를 표시합니다. 비활성화된 경우 일반 아이콘이 표시됩니다",
"previewAudiosDescription": "오디오의 썸네일 미리보기를 표시합니다. 비활성화된 경우 일반 아이콘이 표시됩니다",
"setDateFormat": "정확한 날짜 형식 설정",
"showHiddenFiles": "숨겨진 파일 표시",
"showHiddenFilesDescription": "목록에 숨겨진 파일(예: .썸네일, 프로그램 데이터)을 표시하지만 색인화하도록 구성한 경우 여전히 수동으로 액세스할 수 있습니다.",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Opslaan en afsluiten",
"keepEditing": "Blijven bewerken",
"closeAnyway": "Sluiten",
"goToItem": "Ga naar item",
"watchFile": "Bestand bekijken",
"showMore": "Geavanceerde opties weergeven",
"showLess": "Geavanceerde opties verbergen"
"showLess": "Geavanceerde opties verbergen",
"goToSource": "Ga naar de bronlocatie",
"goToItem": "Ga naar item"
},
"download": {
"downloadFile": "Bestand downloaden",
@@ -221,12 +222,10 @@
"archiveDestination": "Kies een bestemmingsmap (voer hieronder de naam van het archief in):",
"archiveName": "Archiefnaam (standaard archive.zip):",
"archiveCompression": "Compressieniveau (0-9):",
"archiveDeleteAfter": "Bronbestanden verwijderen na succesvolle creatie",
"archiveSuccess": "Archief succesvol aangemaakt!",
"unarchive": "Archief uitpakken",
"unarchiveMessage": "Kies waar het archief moet worden uitgepakt.",
"unarchiveDestination": "Uittreksel naar:",
"unarchiveDeleteAfter": "Archief verwijderen na uitpakken",
"unarchiveSuccess": "Archief met succes uitgepakt!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Geef de verbruiksbalk weer van de grootte van de schijf/partitie zoals gerapporteerd door het besturingssysteem. Deze grootte zal waarschijnlijk groter zijn en omvat bestanden die niet geïndexeerd zijn.",
"usageTextDisplay": "Welke gebruikstekst moet worden weergegeven?",
"usageTextIndexed": "gebruik zoals geïndexeerd",
"usageTextDisk": "gebruik zoals gerapporteerd door schijf/partitie"
"usageTextDisk": "gebruik zoals gerapporteerd door schijf/partitie",
"dividerNamePlaceholder": "Scheidingstekst (leeg laten om een lijn weer te geven)"
},
"success": {
"linkCopied": "Link gekopieerd!"
@@ -550,6 +550,12 @@
"orange": "oranje"
},
"profileSettings": {
"deleteAfterArchive": "Bronbestanden verwijderen na aanmaken/uitpakken van archieven",
"deleteAfterArchiveDescription": "Indien ingeschakeld, worden de bronbestanden verwijderd na succesvol aanmaken/uitpakken van archieven",
"hideFilesInTree": "Bestanden verbergen in boomnavigatie",
"hideFilesInTreeDescription": "Indien ingeschakeld, worden alleen mappen weergegeven in de zijbalk Boomnavigatie",
"previewModelsDescription": "Toon miniatuurweergaven voor 3D-modellen, er wordt een algemeen pictogram getoond indien uitgeschakeld",
"previewAudiosDescription": "Toon miniatuurweergaven voor audiobestanden, er wordt een algemeen pictogram getoond indien uitgeschakeld",
"setDateFormat": "Exact datumformaat instellen",
"showHiddenFiles": "Verborgen bestanden weergeven",
"showHiddenFilesDescription": "Toon verborgen bestanden in de lijst (bijv. .thumbnails, ProgramData), maar ze zijn nog steeds handmatig toegankelijk als ze zijn geconfigureerd om te worden geïndexeerd.",
@@ -642,7 +648,8 @@
"results": "Resultaten{suffix}",
"users": "Gebruikers{suffix}",
"photos": "Foto's",
"audio": "Audio{suffix}",
"audios": "Audio's{suffix}",
"models": "3D-modellen",
"videos": "Video's{suffix}",
"documents": "Documenten{suffix}",
"archives": "Archief{suffix}",
@@ -726,7 +733,8 @@
"space": "Ruimte{suffix}",
"format": "Formaat{suffix}",
"office": "Kantoor{suffix}",
"show": "Toon{suffix}"
"show": "Toon{suffix}",
"divider": "Verdeler{suffix}"
},
"access": {
"rules": "Toegangsregels",
+14 -6
View File
@@ -45,7 +45,8 @@
"scopes": "Bereiken{suffix}",
"commands": "Opdrachten{suffix}",
"photos": "Foto's",
"audio": "Audio{suffix}",
"audios": "Audio{suffix}",
"models": "3D-modellen",
"videos": "Video's{suffix}",
"documents": "Documenten{suffix}",
"archives": "Archieven{suffix}",
@@ -126,7 +127,8 @@
"space": "Ruimte{suffix}",
"format": "Formaat{suffix}",
"office": "Kantoor{suffix}",
"show": "Toon{suffix}"
"show": "Toon{suffix}",
"divider": "Verdeler{suffix}"
},
"buttons": {
"generateNewOtp": "Herstellen en nieuwe twee-factorcode genereren",
@@ -160,6 +162,7 @@
"saveAndExit": "Opslaan en afsluiten",
"keepEditing": "Blijven bewerken",
"closeAnyway": "Sluiten",
"goToSource": "Ga naar de bronlocatie",
"goToItem": "Ga naar item"
},
"download": {
@@ -428,12 +431,10 @@
"archiveDestination": "Kies een bestemmingsmap (voer hieronder de naam van het archief in):",
"archiveName": "Archiefnaam (standaard archive.zip):",
"archiveCompression": "Compressieniveau (0-9):",
"archiveDeleteAfter": "Bronbestanden verwijderen na succesvolle creatie",
"archiveSuccess": "Archief succesvol aangemaakt!",
"unarchive": "Archief uitpakken",
"unarchiveMessage": "Kies waar het archief moet worden uitgepakt.",
"unarchiveDestination": "Uittreksel naar:",
"unarchiveDeleteAfter": "Archief verwijderen na uitpakken",
"unarchiveSuccess": "Archief met succes uitgepakt!"
},
"search": {
@@ -452,6 +453,12 @@
"helpText2": "De index: Zoeken maakt gebruik van de index die automatisch wordt bijgewerkt op het ingestelde interval (standaard: 5 minuten). Zoeken als het programma net is gestart, kan onvolledige resultaten opleveren."
},
"profileSettings": {
"deleteAfterArchive": "Bronbestanden verwijderen na aanmaken/uitpakken van archieven",
"deleteAfterArchiveDescription": "Indien ingeschakeld, worden de bronbestanden verwijderd na succesvol aanmaken/uitpakken van archieven",
"hideFilesInTree": "Bestanden verbergen in boomnavigatie",
"hideFilesInTreeDescription": "Indien ingeschakeld, worden alleen mappen weergegeven in de zijbalk Boomnavigatie",
"previewModelsDescription": "Toon miniatuurweergaven voor 3D-modellen, er wordt een algemeen pictogram getoond indien uitgeschakeld",
"previewAudiosDescription": "Toon miniatuurweergaven voor audiobestanden, er wordt een algemeen pictogram getoond indien uitgeschakeld",
"debugOfficeEditor": "OnlyOffice foutopsporingsmodus inschakelen",
"debugOfficeEditorDescription": "Een foutopsporingspop-up weergeven met aanvullende informatie in de OnlyOffice-editor.",
"defaultMediaPlayer": "De ingebouwde mediaspeler van uw browser gebruiken",
@@ -646,7 +653,8 @@
"showDiskUsageDescription": "Geef de verbruiksbalk weer van de schijf-/partitiegrootte zoals gerapporteerd door het besturingssysteem. Deze grootte zal waarschijnlijk groter zijn en omvat bestanden die niet geïndexeerd zijn.",
"usageTextDisplay": "Welke gebruikstekst moet worden weergegeven?",
"usageTextIndexed": "gebruik zoals geïndexeerd",
"usageTextDisk": "gebruik zoals gerapporteerd door schijf/partitie"
"usageTextDisk": "gebruik zoals gerapporteerd door schijf/partitie",
"dividerNamePlaceholder": "Scheidingstekst (leeg laten om een lijn weer te geven)"
},
"success": {
"linkCopied": "Koppeling gekopieerd!"
@@ -915,4 +923,4 @@
"editorSuccess": "Initialisatie Editor - Succes! Alle stappen voltooid.",
"setupError": "Setup-fout"
}
}
}
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Zapisz i wyjdź",
"keepEditing": "Kontynuuj edycję",
"closeAnyway": "W każdym razie blisko",
"goToItem": "Przejdź do pozycji",
"watchFile": "Oglądaj plik",
"showMore": "Pokaż opcje zaawansowane",
"showLess": "Ukryj opcje zaawansowane"
"showLess": "Ukryj opcje zaawansowane",
"goToSource": "Przejdź do lokalizacji źródłowej",
"goToItem": "Przejdź do pozycji"
},
"download": {
"downloadFile": "Pobierz plik",
@@ -221,12 +222,10 @@
"archiveDestination": "Wybierz folder docelowy (a następnie wprowadź nazwę archiwum poniżej):",
"archiveName": "Nazwa archiwum (domyślnie archive.zip):",
"archiveCompression": "Poziom kompresji (0-9):",
"archiveDeleteAfter": "Usuwanie plików źródłowych po pomyślnym utworzeniu",
"archiveSuccess": "Archiwum utworzone pomyślnie!",
"unarchive": "Wyodrębnij archiwum",
"unarchiveMessage": "Wybierz miejsce rozpakowania archiwum.",
"unarchiveDestination": "Wyciąg do:",
"unarchiveDeleteAfter": "Usuń archiwum po wyodrębnieniu",
"unarchiveSuccess": "Archiwum rozpakowane pomyślnie!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Wyświetla pasek użycia na podstawie rozmiaru dysku/partycji zgłoszonego przez system operacyjny. Rozmiar ten będzie prawdopodobnie większy i obejmuje pliki niezindeksowane.",
"usageTextDisplay": "Jaki tekst użytkowania powinien być wyświetlany?",
"usageTextIndexed": "użycie jako indeksowane",
"usageTextDisk": "użycie zgłoszone przez dysk/partycję"
"usageTextDisk": "użycie zgłoszone przez dysk/partycję",
"dividerNamePlaceholder": "Tekst podziału (pozostaw pusty, aby wyświetlić linię)"
},
"success": {
"linkCopied": "Link Skopiowany!"
@@ -564,8 +564,9 @@
"results": "Wyniki{suffix}",
"users": "Użytkownicy{suffix}",
"photos": "Zdjęcia",
"audio": "Audio{suffix}",
"audios": "Audios{suffix}",
"videos": "Filmy{suffix}",
"models": "Modele 3D",
"documents": "Dokumenty{suffix}",
"archives": "Archiwa{suffix}",
"images": "Obrazy{suffix}",
@@ -648,7 +649,8 @@
"space": "Przestrzeń{suffix}",
"format": "Format{suffix}",
"office": "Biuro{suffix}",
"show": "Pokaż{suffix}"
"show": "Pokaż{suffix}",
"divider": "Dzielnik{suffix}"
},
"access": {
"rules": "Zasady dostępu",
@@ -694,6 +696,12 @@
"orange": "pomarańczowy"
},
"profileSettings": {
"deleteAfterArchive": "Usuń pliki źródłowe po utworzeniu/wypakowaniu archiwów",
"deleteAfterArchiveDescription": "Po włączeniu, usunie pliki źródłowe po pomyślnym utworzeniu/wypakowaniu archiwów",
"hideFilesInTree": "Ukryj pliki w nawigacji drzewiastej",
"hideFilesInTreeDescription": "Po włączeniu, w drzewiastej nawigacji na pasku bocznym wyświetlane są tylko foldery",
"previewModelsDescription": "Pokaż miniatury podglądów dla modeli 3D, jeśli wyłączone, pojawi się ogólna ikona",
"previewAudiosDescription": "Pokaż miniatury podglądów dla plików audio, jeśli wyłączone, pojawi się ogólna ikona",
"setDateFormat": "Ustaw dokładny format daty",
"showHiddenFiles": "Pokaż ukryte pliki",
"showHiddenFilesDescription": "Pokaż ukryte pliki na liście (np. .thumbnails, ProgramData), ale są one nadal dostępne ręcznie, jeśli są skonfigurowane do indeksowania.",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Salvar e sair",
"keepEditing": "Continuar editando",
"closeAnyway": "Fechar de qualquer maneira",
"goToItem": "Ir para o item",
"watchFile": "Assista ao arquivo",
"showMore": "Mostrar opções avançadas",
"showLess": "Ocultar opções avançadas"
"showLess": "Ocultar opções avançadas",
"goToSource": "Ir para o local de origem",
"goToItem": "Ir para o item"
},
"download": {
"downloadFile": "Baixar arquivo",
@@ -221,12 +222,10 @@
"archiveDestination": "Escolha a pasta de destino (depois insira o nome do arquivo abaixo):",
"archiveName": "Nome do arquivo (padrão archive.zip):",
"archiveCompression": "Nível de compressão (0-9):",
"archiveDeleteAfter": "Excluir arquivos de origem após a criação bem-sucedida",
"archiveSuccess": "Arquivo criado com sucesso!",
"unarchive": "Extrair arquivo",
"unarchiveMessage": "Escolha onde extrair o arquivo.",
"unarchiveDestination": "Extrair para:",
"unarchiveDeleteAfter": "Excluir o arquivo após a extração",
"unarchiveSuccess": "Arquivo extraído com sucesso!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Exibe a barra de uso do tamanho do disco/partição conforme relatado pelo sistema operacional. Esse tamanho provavelmente será maior e inclui arquivos não indexados.",
"usageTextDisplay": "Qual texto de uso deve ser exibido?",
"usageTextIndexed": "uso conforme indexado",
"usageTextDisk": "uso conforme relatado pelo disco/partição"
"usageTextDisk": "uso conforme relatado pelo disco/partição",
"dividerNamePlaceholder": "Texto do divisor (deixe em branco para mostrar uma linha)"
},
"success": {
"linkCopied": "Link copiado!"
@@ -564,8 +564,9 @@
"results": "Resultados{suffix}",
"users": "Usuários{suffix}",
"photos": "Fotos",
"audio": "Áudio{suffix}",
"audios": "Áudios{suffix}",
"videos": "Vídeos{suffix}",
"models": "Modelos 3D",
"documents": "Documentos{suffix}",
"archives": "Arquivos{suffix}",
"images": "Imagens{suffix}",
@@ -648,7 +649,8 @@
"space": "Espaço{suffix}",
"format": "Formato{suffix}",
"office": "Escritório{suffix}",
"show": "Mostrar{suffix}"
"show": "Mostrar{suffix}",
"divider": "Divisor{suffix}"
},
"access": {
"rules": "Regras de acesso",
@@ -694,6 +696,12 @@
"orange": "laranja"
},
"profileSettings": {
"deleteAfterArchive": "Excluir arquivos de origem após criação/extração de arquivos",
"deleteAfterArchiveDescription": "Quando ativado, excluirá os arquivos de origem após criação/extração bem-sucedida de arquivos",
"hideFilesInTree": "Ocultar arquivos na navegação em árvore",
"hideFilesInTreeDescription": "Quando ativado, apenas pastas são mostradas na navegação em árvore da barra lateral",
"previewModelsDescription": "Mostrar visualizações em miniatura para modelos 3D, um ícone genérico será mostrado se desativado",
"previewAudiosDescription": "Mostrar pré-visualizações em miniatura para áudios, será mostrado um ícone genérico se desativado",
"setDateFormat": "Definir o formato exato da data",
"showHiddenFiles": "Mostrar arquivos ocultos",
"showHiddenFilesDescription": "Mostrar arquivos ocultos na listagem (por exemplo, .thumbnails, ProgramData), mas eles ainda poderão ser acessados manualmente se estiverem configurados para serem indexados.",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Guardar e sair",
"keepEditing": "Continuar a editar",
"closeAnyway": "Fechar de qualquer maneira",
"goToItem": "Ir para o item",
"watchFile": "Ver ficheiro",
"showMore": "Mostrar opções avançadas",
"showLess": "Ocultar opções avançadas"
"showLess": "Ocultar opções avançadas",
"goToSource": "Ir para o local de origem",
"goToItem": "Ir para o item"
},
"download": {
"downloadFile": "Descarregar ficheiro",
@@ -221,12 +222,10 @@
"archiveDestination": "Escolha a pasta de destino (depois introduza o nome do arquivo abaixo):",
"archiveName": "Nome do ficheiro (por defeito archive.zip):",
"archiveCompression": "Nível de compressão (0-9):",
"archiveDeleteAfter": "Eliminar ficheiros de origem após a criação bem sucedida",
"archiveSuccess": "Arquivo criado com sucesso!",
"unarchive": "Extrair arquivo",
"unarchiveMessage": "Escolha onde extrair o arquivo.",
"unarchiveDestination": "Extrair para:",
"unarchiveDeleteAfter": "Eliminar o arquivo após a extração",
"unarchiveSuccess": "Arquivo extraído com sucesso!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Apresenta a barra de utilização a partir do tamanho do disco/partição, conforme indicado pelo SO. Este tamanho será provavelmente maior e inclui ficheiros não indexados.",
"usageTextDisplay": "Que texto de utilização deve ser apresentado?",
"usageTextIndexed": "utilização como indexado",
"usageTextDisk": "utilização conforme registada por disco/partição"
"usageTextDisk": "utilização conforme registada por disco/partição",
"dividerNamePlaceholder": "Texto do separador (deixar em branco para mostrar uma linha)"
},
"success": {
"linkCopied": "Link copiado!"
@@ -550,6 +550,12 @@
"orange": "laranja"
},
"profileSettings": {
"deleteAfterArchive": "Eliminar ficheiros de origem após criação/extração de arquivos",
"deleteAfterArchiveDescription": "Quando ativado, eliminará os ficheiros de origem após criação/extração bem-sucedida de arquivos",
"hideFilesInTree": "Ocultar ficheiros na navegação em árvore",
"hideFilesInTreeDescription": "Quando ativado, apenas pastas são mostradas na navegação em árvore da barra lateral",
"previewModelsDescription": "Mostrar pré-visualizações em miniatura para modelos 3D, será mostrado um ícone genérico se desativado",
"previewAudiosDescription": "Mostrar visualizações em miniatura para áudios, um ícone genérico será mostrado se desativado",
"setDateFormat": "Definir o formato exato da data",
"showHiddenFiles": "Mostrar ficheiros ocultos",
"showHiddenFilesDescription": "Mostrar ficheiros ocultos na listagem (por exemplo, .thumbnails, ProgramData), mas estes continuam a ser acessíveis manualmente se estiverem configurados para serem indexados.",
@@ -642,8 +648,9 @@
"results": "Resultados{suffix}",
"users": "Utilizadores{suffix}",
"photos": "Fotos",
"audio": "Áudio{suffix}",
"audios": "Áudios{suffix}",
"videos": "Vídeos{suffix}",
"models": "Modelos 3D",
"documents": "Documentos{suffix}",
"archives": "Arquivos{suffix}",
"images": "Imagens{suffix}",
@@ -726,7 +733,8 @@
"space": "Espaço{suffix}",
"format": "Formato{suffix}",
"office": "Escritório{suffix}",
"show": "Mostrar{suffix}"
"show": "Mostrar{suffix}",
"divider": "Divisor{suffix}"
},
"access": {
"rules": "Regras de acesso",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Salvare și ieșire",
"keepEditing": "Continuă editarea",
"closeAnyway": "Închide oricum",
"goToItem": "Du-te la articol",
"watchFile": "Fișier de vizionare",
"showMore": "Afișare opțiuni avansate",
"showLess": "Ascundeți opțiunile avansate"
"showLess": "Ascundeți opțiunile avansate",
"goToSource": "Du-te la locația sursei",
"goToItem": "Du-te la articol"
},
"download": {
"downloadFile": "Descarcă fișier",
@@ -221,12 +222,10 @@
"archiveDestination": "Alegeți folderul de destinație (apoi introduceți numele arhivei mai jos):",
"archiveName": "Numele arhivei (implicit archive.zip):",
"archiveCompression": "Nivelul de compresie (0-9):",
"archiveDeleteAfter": "Ștergeți fișierele sursă după crearea cu succes",
"archiveSuccess": "Arhiva creată cu succes!",
"unarchive": "Extras arhivă",
"unarchiveMessage": "Alegeți unde să extrageți arhiva.",
"unarchiveDestination": "Extras la:",
"unarchiveDeleteAfter": "Ștergeți arhiva după extragere",
"unarchiveSuccess": "Arhiva extrasă cu succes!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Afișează bara de utilizare din dimensiunea discului/partiției raportată de sistemul de operare. Această dimensiune va fi probabil mai mare și include fișiere care nu sunt indexate.",
"usageTextDisplay": "Ce text de utilizare ar trebui să fie afișat?",
"usageTextIndexed": "utilizare ca indexat",
"usageTextDisk": "utilizare raportată de disc/partiție"
"usageTextDisk": "utilizare raportată de disc/partiție",
"dividerNamePlaceholder": "Textul separatorului (lăsați gol pentru a afișa o linie)"
},
"success": {
"linkCopied": "Legătură copiată!"
@@ -564,8 +564,9 @@
"results": "Rezultate{suffix}",
"users": "Utilizatori{suffix}",
"photos": "Fotografii",
"audio": "Audio{suffix}",
"audios": "Audios{suffix}",
"videos": "Video{suffix}",
"models": "Modele 3D",
"documents": "Documente{suffix}",
"archives": "Arhive{suffix}",
"images": "Imagini{suffix}",
@@ -648,7 +649,8 @@
"space": "Spațiu{suffix}",
"format": "Format{suffix}",
"office": "Birou{suffix}",
"show": "Spectacol{suffix}"
"show": "Spectacol{suffix}",
"divider": "Divizor{suffix}"
},
"access": {
"rules": "Reguli de acces",
@@ -694,6 +696,12 @@
"orange": "portocaliu"
},
"profileSettings": {
"deleteAfterArchive": "Șterge fișierele sursă după crearea/extragerea arhivelor",
"deleteAfterArchiveDescription": "Când este activat, va șterge fișierele sursă după crearea/extragerea cu succes a arhivelor",
"hideFilesInTree": "Ascunde fișierele în navigarea arborescentă",
"hideFilesInTreeDescription": "Când este activat, numai folderele sunt afișate în navigarea arborescentă a barei laterale",
"previewModelsDescription": "Afișează previzualizări miniatură pentru modele 3D, se va afișa o pictogramă generică dacă este dezactivat",
"previewAudiosDescription": "Afișează previzualizări miniatură pentru fișiere audio, se va afișa o pictogramă generică dacă este dezactivat",
"setDateFormat": "Setați formatul exact al datei",
"showHiddenFiles": "Afișează fișierele ascunse",
"showHiddenFilesDescription": "Afișează fișierele ascunse în listă (de exemplu, .thumbnails, ProgramData), dar acestea sunt încă accesibile manual dacă sunt configurate pentru a fi indexate.",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Сохранить и выйти",
"keepEditing": "Продолжить редактирование",
"closeAnyway": "Закрыть в любом случае",
"goToItem": "Перейти к пункту",
"watchFile": "Смотреть файл",
"showMore": "Показать дополнительные параметры",
"showLess": "Скрыть дополнительные параметры"
"showLess": "Скрыть дополнительные параметры",
"goToSource": "Перейти к исходному местоположению",
"goToItem": "Перейти к пункту"
},
"download": {
"downloadFile": "Скачать файл",
@@ -221,12 +222,10 @@
"archiveDestination": "Выберите папку назначения (затем введите имя архива ниже):",
"archiveName": "Имя архива (по умолчанию archive.zip):",
"archiveCompression": "Уровень сжатия (0-9):",
"archiveDeleteAfter": "Удаление исходных файлов после успешного создания",
"archiveSuccess": "Архив создан успешно!",
"unarchive": "Распаковать архив",
"unarchiveMessage": "Выберите, куда распаковать архив.",
"unarchiveDestination": "Отрывок в:",
"unarchiveDeleteAfter": "Удаление архива после извлечения",
"unarchiveSuccess": "Архив извлечен успешно!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Отображение полосы использования от размера диска/раздела, о котором сообщает ОС. Этот размер может быть больше и включать неиндексированные файлы.",
"usageTextDisplay": "Какой текст должен отображаться?",
"usageTextIndexed": "использование в индексированном виде",
"usageTextDisk": "использование по данным диска/раздела"
"usageTextDisk": "использование по данным диска/раздела",
"dividerNamePlaceholder": "Текст разделителя (оставьте пустым, чтобы показать строку)"
},
"success": {
"linkCopied": "Ссылка скопирована!"
@@ -564,8 +564,9 @@
"results": "Результаты{suffix}",
"users": "Пользователи{suffix}",
"photos": "Фотографии",
"audio": "Аудио{suffix}",
"audios": "Аудио{suffix}",
"videos": "Видео{suffix}",
"models": "3D-модели",
"documents": "Документы{suffix}",
"archives": "Архивы{suffix}",
"images": "Изображения{suffix}",
@@ -648,7 +649,8 @@
"space": "Космос{suffix}",
"format": "Формат{suffix}",
"office": "Офис{suffix}",
"show": "Показать{suffix}"
"show": "Показать{suffix}",
"divider": "Разделитель{suffix}"
},
"access": {
"rules": "Правила доступа",
@@ -694,6 +696,12 @@
"orange": "оранжевый"
},
"profileSettings": {
"deleteAfterArchive": "Удалять исходные файлы после создания/извлечения архивов",
"deleteAfterArchiveDescription": "При включении удалит исходные файлы после успешного создания/извлечения архивов",
"hideFilesInTree": "Скрывать файлы в древовидной навигации",
"hideFilesInTreeDescription": "При включении в древовидной навигации боковой панели отображаются только папки",
"previewModelsDescription": "Показывать миниатюры для 3D-моделей, если отключено, будет показана стандартная иконка",
"previewAudiosDescription": "Показывать миниатюры для аудиофайлов, если отключено, будет показана стандартная иконка",
"setDateFormat": "Установить точный формат даты",
"showHiddenFiles": "Показывать скрытые файлы",
"showHiddenFilesDescription": "Показывать в списке скрытые файлы (например, .thumbnails, ProgramData), но они по-прежнему доступны вручную, если настроены на индексирование.",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Uložiť a ukončiť",
"keepEditing": "Pokračujte v úpravách",
"closeAnyway": "Zavrieť Tak či onak",
"goToItem": "Prejsť na položku",
"watchFile": "Sledovať súbor",
"showMore": "Zobraziť rozšírené možnosti",
"showLess": "Skryť rozšírené možnosti"
"showLess": "Skryť rozšírené možnosti",
"goToSource": "Prejsť na zdrojové miesto",
"goToItem": "Prejsť na položku"
},
"download": {
"downloadFile": "Stiahnuť súbor",
@@ -221,12 +222,10 @@
"archiveDestination": "Vyberte cieľový priečinok (potom zadajte názov archívu nižšie):",
"archiveName": "Názov archívu (štandardne archive.zip):",
"archiveCompression": "Úroveň kompresie (0-9):",
"archiveDeleteAfter": "Odstránenie zdrojových súborov po úspešnom vytvorení",
"archiveSuccess": "Archív bol úspešne vytvorený!",
"unarchive": "Výpis archívu",
"unarchiveMessage": "Vyberte, kam sa má archív extrahovať.",
"unarchiveDestination": "Výpis na:",
"unarchiveDeleteAfter": "Odstránenie archívu po extrakcii",
"unarchiveSuccess": "Archív úspešne extrahovaný!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Zobrazenie lišty využitia z veľkosti disku/oddielu podľa hlásenia operačného systému. Táto veľkosť bude pravdepodobne väčšia a zahŕňa aj súbory, ktoré nie sú indexované.",
"usageTextDisplay": "Ktorý text použitia sa má zobraziť?",
"usageTextIndexed": "použitie ako indexované",
"usageTextDisk": "využitie podľa hlásenia disku/oddielu"
"usageTextDisk": "využitie podľa hlásenia disku/oddielu",
"dividerNamePlaceholder": "Text oddeľovača (nechajte prázdny, aby sa zobrazil riadok)"
},
"success": {
"linkCopied": "Odkaz skopírovaný!"
@@ -564,8 +564,9 @@
"results": "Výsledky{suffix}",
"users": "Používatelia{suffix}",
"photos": "Fotografie",
"audio": "Audio{suffix}",
"audios": "Zvuky{suffix}",
"videos": "Videá{suffix}",
"models": "3D modely",
"documents": "Dokumenty{suffix}",
"archives": "Archív{suffix}",
"images": "Obrázky{suffix}",
@@ -648,7 +649,8 @@
"space": "Priestor{suffix}",
"format": "Formát{suffix}",
"office": "Kancelária{suffix}",
"show": "Zobraziť{suffix}"
"show": "Zobraziť{suffix}",
"divider": "Rozdeľovač{suffix}"
},
"access": {
"rules": "Pravidlá prístupu",
@@ -694,6 +696,12 @@
"orange": "oranžová"
},
"profileSettings": {
"deleteAfterArchive": "Odstrániť zdrojové súbory po vytvorení/extrahovaní archívov",
"deleteAfterArchiveDescription": "Ak je povolené, po úspešnom vytvorení/extrahovaní archívov odstráni zdrojové súbory",
"hideFilesInTree": "Skryť súbory v stromovej navigácii",
"hideFilesInTreeDescription": "Ak je povolené, v stromovej navigácii bočného panela sa zobrazujú iba priečinky",
"previewModelsDescription": "Zobraziť miniatúry náhľadov pre 3D modely, ak je zakázané, zobrazí sa všeobecná ikona",
"previewAudiosDescription": "Zobraziť miniatúry náhľadov pre zvuky, ak je zakázané, zobrazí sa všeobecná ikona",
"setDateFormat": "Nastavenie presného formátu dátumu",
"showHiddenFiles": "Zobrazenie skrytých súborov",
"showHiddenFilesDescription": "Zobrazenie skrytých súborov vo výpise (napr. .thumbnails, ProgramData), ale sú stále manuálne prístupné, ak sú nakonfigurované na indexovanie.",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Spara och avsluta",
"keepEditing": "Fortsätt redigera",
"closeAnyway": "Stäng i alla fall",
"goToItem": "Gå till artikel",
"watchFile": "Titta på filen",
"showMore": "Visa avancerade alternativ",
"showLess": "Dölj avancerade alternativ"
"showLess": "Dölj avancerade alternativ",
"goToSource": "Gå till källplatsen",
"goToItem": "Gå till objektet"
},
"download": {
"downloadFile": "Ladda ner fil",
@@ -221,12 +222,10 @@
"archiveDestination": "Välj destinationsmapp (ange sedan arkivnamn nedan):",
"archiveName": "Arkivnamn (standard archive.zip):",
"archiveCompression": "Kompressionsnivå (0-9):",
"archiveDeleteAfter": "Ta bort källfiler efter lyckad skapelse",
"archiveSuccess": "Arkiv skapat framgångsrikt!",
"unarchive": "Extrahera arkiv",
"unarchiveMessage": "Välj var du vill extrahera arkivet.",
"unarchiveDestination": "Extrahera till:",
"unarchiveDeleteAfter": "Ta bort arkiv efter extraktion",
"unarchiveSuccess": "Arkivet har extraherats framgångsrikt!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Visa användningsstapel från disk-/partitionsstorlek som rapporteras av operativsystemet. Denna storlek är sannolikt större och inkluderar filer som inte är indexerade.",
"usageTextDisplay": "Vilken användningstext ska visas?",
"usageTextIndexed": "användning som indexerad",
"usageTextDisk": "användning som rapporteras av disk/partition"
"usageTextDisk": "användning som rapporteras av disk/partition",
"dividerNamePlaceholder": "Delningstext (lämna tom för att visa en rad)"
},
"success": {
"linkCopied": "Länk kopierad"
@@ -550,6 +550,12 @@
"orange": "apelsin"
},
"profileSettings": {
"deleteAfterArchive": "Ta bort källfiler efter skapande/extrahering av arkiv",
"deleteAfterArchiveDescription": "När aktiverad kommer att ta bort källfilerna efter framgångsrik skapande/extrahering av arkiv",
"hideFilesInTree": "Dölj filer i trädnavigering",
"hideFilesInTreeDescription": "När aktiverad visas endast mappar i sidofältets trädnavigering",
"previewModelsDescription": "Visa miniatyrförhandsvisningar för 3D-modeller, kommer att visa generisk ikon om inaktiverad",
"previewAudiosDescription": "Visa miniatyrförhandsvisningar för ljudfiler, kommer att visa generisk ikon om inaktiverad",
"setDateFormat": "Ange exakt datumformat",
"showHiddenFiles": "Visa dolda filer",
"showHiddenFilesDescription": "Visa dolda filer i listan (t.ex. .thumbnails, ProgramData), men de är fortfarande manuellt åtkomliga om de är konfigurerade för att indexeras.",
@@ -642,8 +648,9 @@
"results": "Resultat{suffix}",
"users": "Användare{suffix}",
"photos": "Bilder",
"audio": "Ljud{suffix}",
"audios": "Ljud{suffix}",
"videos": "Videor{suffix}",
"models": "3D-modeller",
"documents": "Dokument{suffix}",
"archives": "Arkiv{suffix}",
"images": "Bilder{suffix}",
@@ -726,7 +733,8 @@
"space": "Utrymme{suffix}",
"format": "Format{suffix}",
"office": "Kontor{suffix}",
"show": "Visa{suffix}"
"show": "Visa{suffix}",
"divider": "Uppdelare{suffix}"
},
"access": {
"rules": "Regler för åtkomst",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Kaydet ve Çık",
"keepEditing": "Düzenlemeye Devam Et",
"closeAnyway": "Her Neyse Kapat",
"goToItem": "Öğeye git",
"watchFile": "Dosyayı izle",
"showMore": "Gelişmiş Seçenekleri Göster",
"showLess": "Gelişmiş Seçenekleri Gizle"
"showLess": "Gelişmiş Seçenekleri Gizle",
"goToSource": "Kaynak Konuma Git",
"goToItem": "Öğeye git"
},
"download": {
"downloadFile": "Dosyayı indir",
@@ -221,12 +222,10 @@
"archiveDestination": "Hedef klasörü seçin (ardından aşağıya arşiv adını girin):",
"archiveName": "Arşiv adı (varsayılan archive.zip):",
"archiveCompression": "Sıkıştırma seviyesi (0-9):",
"archiveDeleteAfter": "Başarılı bir şekilde oluşturulduktan sonra kaynak dosyaları silme",
"archiveSuccess": "Arşiv başarıyla oluşturuldu!",
"unarchive": "Arşivi ayıkla",
"unarchiveMessage": "Arşivin nereye çıkarılacağını seçin.",
"unarchiveDestination": "Özetle:",
"unarchiveDeleteAfter": "Çıkarma işleminden sonra arşivi silme",
"unarchiveSuccess": "Arşiv başarıyla çıkarıldı!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "İşletim sistemi tarafından bildirilen disk/bölüm boyutundan kullanım çubuğunu görüntüleyin. Bu boyut büyük olasılıkla daha büyük olacaktır ve dizine eklenmemiş dosyaları da içerir.",
"usageTextDisplay": "Hangi kullanım metni görüntülenmelidir?",
"usageTextIndexed": "endeksli olarak kullanım",
"usageTextDisk": "disk/bölüm tarafından bildirilen kullanım"
"usageTextDisk": "disk/bölüm tarafından bildirilen kullanım",
"dividerNamePlaceholder": "Bölücü metin (bir çizgi göstermek için boş bırakın)"
},
"success": {
"linkCopied": "Link kopyalandı!"
@@ -564,8 +564,9 @@
"results": "Sonuçlar{suffix}",
"users": "Kullanıcılar{suffix}",
"photos": "Fotoğraflar",
"audio": "Ses{suffix}",
"audios": "Ses Dosyaları{suffix}",
"videos": "Videolar{suffix}",
"models": "3D Modeller",
"documents": "Belgeler{suffix}",
"archives": "Arşivler{suffix}",
"images": "Görüntüler{suffix}",
@@ -648,7 +649,8 @@
"space": "Uzay{suffix}",
"format": "Biçim{suffix}",
"office": "Ofis{suffix}",
"show": "Göster{suffix}"
"show": "Göster{suffix}",
"divider": "Bölücü{suffix}"
},
"access": {
"rules": "Erişim kuralları",
@@ -694,6 +696,12 @@
"orange": "turuncu"
},
"profileSettings": {
"deleteAfterArchive": "Arşiv oluşturma/çıkarma işleminden sonra kaynak dosyaları sil",
"deleteAfterArchiveDescription": "Etkinleştirildiğinde, arşivlerin başarıyla oluşturulması/çıkarılmasından sonra kaynak dosyaları siler",
"hideFilesInTree": "Ağaç gezintisinde dosyaları gizle",
"hideFilesInTreeDescription": "Etkinleştirildiğinde, kenar çubuğu Ağaç Gezintisinde yalnızca klasörler gösterilir",
"previewModelsDescription": "3D modeller için küçük resim önizlemelerini göster, devre dışı bırakılırsa genel simge gösterilir",
"previewAudiosDescription": "Ses dosyaları için küçük resim önizlemelerini göster, devre dışı bırakılırsa genel simge gösterilir",
"setDateFormat": "Tam tarih biçimini ayarlama",
"showHiddenFiles": "Gizli dosyaları göster",
"showHiddenFilesDescription": "Gizli dosyaları listede gösterin (örn. .thumbnails, ProgramData), ancak dizine eklenecek şekilde yapılandırılmışlarsa yine de manuel olarak erişilebilirler.",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "Зберегти та вийти",
"keepEditing": "Продовжуйте редагувати",
"closeAnyway": "Все одно зачиняй.",
"goToItem": "Перейти до пункту",
"watchFile": "Переглянути файл",
"showMore": "Показати додаткові параметри",
"showLess": "Приховати додаткові параметри"
"showLess": "Приховати додаткові параметри",
"goToSource": "Перейдіть до Розташування джерела",
"goToItem": "Перейти до пункту"
},
"download": {
"downloadFile": "Завантажити файл",
@@ -221,12 +222,10 @@
"archiveDestination": "Виберіть папку призначення (потім введіть ім'я архіву нижче):",
"archiveName": "Ім'я архіву (за замовчуванням archive.zip):",
"archiveCompression": "Рівень стиснення (0-9):",
"archiveDeleteAfter": "Видаліть вихідні файли після успішного створення",
"archiveSuccess": "Архів створено успішно!",
"unarchive": "Витягнути архів",
"unarchiveMessage": "Виберіть, куди розпакувати архів.",
"unarchiveDestination": "Витягнути до:",
"unarchiveDeleteAfter": "Видалення архіву після розпакування",
"unarchiveSuccess": "Архів успішно розпаковано!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "Відображати смужку використання від розміру диска/розділу, про який повідомляє операційна система. Цей розмір, ймовірно, буде більшим і включатиме неіндексовані файли.",
"usageTextDisplay": "Який текст використання повинен відображатися?",
"usageTextIndexed": "використання в індексованому вигляді",
"usageTextDisk": "використання за диском/розділом"
"usageTextDisk": "використання за диском/розділом",
"dividerNamePlaceholder": "Текст роздільника (залиште порожнім, щоб показати лінію)"
},
"success": {
"linkCopied": "Посилання скопійоване!"
@@ -550,6 +550,12 @@
"orange": "помаранчевий"
},
"profileSettings": {
"deleteAfterArchive": "Видаляти вихідні файли після створення/вилучення архівів",
"deleteAfterArchiveDescription": "Якщо ввімкнено, видалятиме вихідні файли після успішного створення/вилучення архівів",
"hideFilesInTree": "Приховувати файли в дереві навігації",
"hideFilesInTreeDescription": "Якщо ввімкнено, у деревоподібній навігації бічної панелі відображаються лише папки",
"previewModelsDescription": "Показувати мініатюри для 3D-моделей, якщо вимкнено, буде показано стандартну іконку",
"previewAudiosDescription": "Показувати мініатюри для аудіофайлів, якщо вимкнено, буде показано стандартну іконку",
"setDateFormat": "Встановіть точний формат дати",
"showHiddenFiles": "Показати приховані файли",
"showHiddenFilesDescription": "Показувати приховані файли у лістингу (наприклад, .thumbnails, ProgramData), але вони все ще доступні вручну, якщо їх налаштовано на індексування.",
@@ -642,7 +648,8 @@
"results": "Результати{suffix}",
"users": "Користувачі{suffix}",
"photos": "Фотографії",
"audio": "Аудіо{suffix}",
"audios": "Аудіо{suffix}",
"models": "3D-моделі",
"videos": "Відео{suffix}",
"documents": "Документи{suffix}",
"archives": "Архіви{suffix}",
@@ -726,7 +733,8 @@
"space": "Простір{suffix}",
"format": "Формат{suffix}",
"office": "Офіс{suffix}",
"show": "Показати{suffix}"
"show": "Показати{suffix}",
"divider": "Перегородка{suffix}"
},
"access": {
"rules": "Правила доступу",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "保存并退出",
"keepEditing": "继续编辑",
"closeAnyway": "无论如何关闭",
"goToItem": "转到项目",
"watchFile": "观看文件",
"showMore": "显示高级选项",
"showLess": "隐藏高级选项"
"showLess": "隐藏高级选项",
"goToSource": "转到源位置",
"goToItem": "转到项目"
},
"download": {
"downloadFile": "下载文件",
@@ -221,12 +222,10 @@
"archiveDestination": "选择目标文件夹(然后在下面输入存档名称):",
"archiveName": "存档名称(默认为 archive.zip):",
"archiveCompression": "压缩级别(0-9):",
"archiveDeleteAfter": "成功创建后删除源文件",
"archiveSuccess": "存档创建成功!",
"unarchive": "提取档案",
"unarchiveMessage": "选择提取存档的位置。",
"unarchiveDestination": "摘录至",
"unarchiveDeleteAfter": "提取后删除存档",
"unarchiveSuccess": "档案提取成功!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "根据操作系统报告的磁盘/分区大小显示用量条。该大小可能更大,包括未编入索引的文件。",
"usageTextDisplay": "应显示哪些使用文本?",
"usageTextIndexed": "指数化使用",
"usageTextDisk": "磁盘/分区报告的使用量"
"usageTextDisk": "磁盘/分区报告的使用量",
"dividerNamePlaceholder": "分隔线文本(留空以显示一行)"
},
"success": {
"linkCopied": "链接已复制!"
@@ -550,6 +550,12 @@
"orange": "橘色"
},
"profileSettings": {
"deleteAfterArchive": "创建/提取存档后删除源文件",
"deleteAfterArchiveDescription": "启用后,将在成功创建/提取存档后删除源文件",
"hideFilesInTree": "在树形导航中隐藏文件",
"hideFilesInTreeDescription": "启用后,侧边栏树形导航中仅显示文件夹",
"previewModelsDescription": "显示3D模型的缩略图预览,如果禁用则显示通用图标",
"previewAudiosDescription": "显示音频的缩略图预览,如果禁用则显示通用图标",
"setDateFormat": "设置准确的日期格式",
"showHiddenFiles": "显示隐藏文件",
"showHiddenFilesDescription": "在列表中显示隐藏文件(如 .缩略图、ProgramData),但如果配置为索引,这些文件仍可手动访问。",
@@ -642,7 +648,8 @@
"results": "成果{suffix}",
"users": "用户{suffix}",
"photos": "照片",
"audio": "音频{suffix}",
"audios": "音频{suffix}",
"models": "3D模型",
"videos": "视频{suffix}",
"documents": "文件{suffix}",
"archives": "档案{suffix}",
@@ -726,7 +733,8 @@
"space": "空间{suffix}",
"format": "格式{suffix}",
"office": "办公室{suffix}",
"show": "显示{suffix}"
"show": "显示{suffix}",
"divider": "分割器{suffix}"
},
"access": {
"rules": "访问规则",
+15 -7
View File
@@ -28,10 +28,11 @@
"saveAndExit": "儲存並離開",
"keepEditing": "繼續編輯",
"closeAnyway": "無論如何關閉",
"goToItem": "前往項目",
"watchFile": "觀看檔案",
"showMore": "顯示進階選項",
"showLess": "隱藏進階選項"
"showLess": "隱藏進階選項",
"goToSource": "前往來源位置",
"goToItem": "前往項目"
},
"download": {
"downloadFile": "下載檔案",
@@ -221,12 +222,10 @@
"archiveDestination": "選擇目的地資料夾(然後在下方輸入存檔名稱):",
"archiveName": "存檔名稱 (預設為 archive.zip):",
"archiveCompression": "壓縮等級 (0-9):",
"archiveDeleteAfter": "成功建立後刪除原始檔案",
"archiveSuccess": "成功建立存檔!",
"unarchive": "擷取存檔",
"unarchiveMessage": "選擇提取存檔的位置。",
"unarchiveDestination": "擷取至:",
"unarchiveDeleteAfter": "萃取後刪除存檔",
"unarchiveSuccess": "存檔提取成功!"
},
"search": {
@@ -381,7 +380,8 @@
"showDiskUsageDescription": "根據作業系統報告的磁碟/磁碟分割大小顯示使用量列。此大小可能較大,並包括未編制索引的檔案。",
"usageTextDisplay": "應該顯示哪些使用文字?",
"usageTextIndexed": "索引的用途",
"usageTextDisk": "磁碟/磁碟分割報告的使用量"
"usageTextDisk": "磁碟/磁碟分割報告的使用量",
"dividerNamePlaceholder": "分割線文字(留空顯示一行)"
},
"success": {
"linkCopied": "連結已複製!"
@@ -519,6 +519,12 @@
"error": "錯誤"
},
"profileSettings": {
"deleteAfterArchive": "建立/解壓縮封存檔後刪除來源檔案",
"deleteAfterArchiveDescription": "啟用後,將在成功建立/解壓縮封存檔後刪除來源檔案",
"hideFilesInTree": "在樹狀導航中隱藏檔案",
"hideFilesInTreeDescription": "啟用後,側邊欄樹狀導航中僅顯示資料夾",
"previewModelsDescription": "顯示3D模型的縮圖預覽,如果停用則顯示通用圖示",
"previewAudiosDescription": "顯示音訊的縮圖預覽,如果停用則顯示通用圖示",
"setDateFormat": "設定精確的日期格式",
"showHiddenFiles": "顯示隱藏檔案",
"showHiddenFilesDescription": "在列出中顯示隱藏的檔案 (例如 .縮圖、ProgramData),但如果設定為索引,這些檔案仍可手動存取。",
@@ -642,8 +648,9 @@
"results": "結果{suffix}",
"users": "使用者{suffix}",
"photos": "圖片",
"audio": "音訊{suffix}",
"audios": "音訊{suffix}",
"videos": "影片{suffix}",
"models": "3D模型",
"documents": "文件{suffix}",
"archives": "檔案{suffix}",
"images": "圖片{suffix}",
@@ -726,7 +733,8 @@
"space": "空間{suffix}",
"format": "格式{suffix}",
"office": "辦公室{suffix}",
"show": "顯示{suffix}"
"show": "顯示{suffix}",
"divider": "分割器{suffix}"
},
"access": {
"rules": "存取規則",
+3 -2
View File
@@ -16,7 +16,8 @@ const titles = {
Login: i18n.global.t("general.login"),
Share: i18n.global.t("general.share"),
Files: i18n.global.t("general.files"),
Tools: i18n.global.t("general.tool"),
Tools: i18n.global.t("tools.title"),
ChildTool: i18n.global.t("tools.title"),
Settings: i18n.global.t("general.settings"),
ProfileSettings: i18n.global.t("settings.profileSettings"),
Shares: i18n.global.t("settings.shareManagement"),
@@ -84,7 +85,7 @@ const routes: RouteRecordRaw[] = [
children: [
{
path: "",
name: "ToolsIndex",
name: "ChildTool",
component: Tools,
},
],
+11 -4
View File
@@ -39,7 +39,7 @@ export const getters = {
},
isPreviewView: () => {
const cv = getters.currentView()
return cv == 'preview' || cv == 'onlyOfficeEditor' || cv == 'epubViewer' || cv == 'docViewer' || cv == 'editor' || cv == 'markdownViewer' || cv == 'threeJsViewer'
return previewViews.includes(cv)
},
isScrollable: () => {
if (getters.currentView() == 'markdownViewer') {
@@ -473,14 +473,19 @@ export const getters = {
},
preview: {
video: true,
audio: true,
image: true,
popup: true,
models: true,
autoplayMedia: true,
},
disableSettings: true,
disableQuickToggles: false,
disableSearchOptions: false,
deleteWithoutConfirming: false,
deleteAfterArchive: true,
stickySidebar: true,
hideFilesInTree: false,
darkMode: true,
dateFormat: false,
disableViewingExt: "",
@@ -542,9 +547,7 @@ export const getters = {
}
// Match by path instead of route name
const tool = tools().find(t => t.path === state.route.path);
if (tool === undefined) {
return { name: "Tools" };
}
// Return null when at /tools (list view) to avoid circular component rendering
return tool;
},
permissions: () => {
@@ -576,11 +579,13 @@ export const getters = {
if (getters.isShare()) {
// For shares, use defaults for preview settings (shares don't have per-share preview config)
return {
audio: state.user?.preview?.audio ?? true,
video: state.user?.preview?.video ?? true,
image: state.user?.preview?.image ?? true,
office: state.user?.preview?.office ?? true,
folder: state.user?.preview?.folder ?? true,
popup: state.user?.preview?.popup ?? true,
models: state.user?.preview?.models ?? true,
motionVideoPreview: state.user?.preview?.motionVideoPreview ?? false,
disableHideSidebar: state.user?.preview?.disableHideSidebar ?? false,
autoplayMedia: state.user?.preview?.autoplayMedia ?? false,
@@ -590,11 +595,13 @@ export const getters = {
}
// For regular users, use their preview settings -- unless is share
return {
audio: state.user?.preview?.audio ?? true,
video: state.user?.preview?.video ?? true,
image: state.user?.preview?.image ?? true,
office: state.user?.preview?.office ?? true,
folder: state.user?.preview?.folder ?? true,
popup: state.user?.preview?.popup ?? true,
models: state.user?.preview?.models ?? true,
motionVideoPreview: state.user?.preview?.motionVideoPreview ?? false,
disableHideSidebar: state.user?.preview?.disableHideSidebar ?? false,
autoplayMedia: state.user?.preview?.autoplayMedia ?? false,
+14
View File
@@ -468,6 +468,19 @@ export const mutations = {
mutations.setMultiple(false);
emitStateChanged();
},
selectAllItems: () => {
if (state.req && state.req.items && state.req.items.length > 0) {
// Close the context menu
mutations.closeContextMenus();
// Clear current selection first
mutations.resetSelected();
// Add all items from current directory to selection by their indices
state.req.items.forEach((item, index) => {
mutations.addSelected(index);
});
mutations.setMultiple(true);
}
},
setLastSelectedIndex: (index) => {
if (index === state.lastSelectedIndex) {
return;
@@ -522,6 +535,7 @@ export const mutations = {
"showFirstLogin",
"sidebarLinks",
"fileLoading",
"deleteAfterArchive",
].includes(key)
);
value.id = state.user.id;
+5 -1
View File
@@ -41,9 +41,11 @@ export const state = reactive({
},
user: {
preview: {
audio: true,
video: true,
image: true,
popup: true,
models: true,
},
loginType: "",
username: "",
@@ -51,6 +53,8 @@ export const state = reactive({
gallerySize: 0,
singleClick: false,
stickySidebar: stickyStartup(),
hideFilesInTree: false,
deleteAfterArchive: true,
locale: detectLocale(), // Default to the locale from moment
viewMode: 'normal', // Default to mosaic view
showHidden: false, // Default to false, assuming this is a boolean
@@ -162,7 +166,7 @@ export const state = reactive({
mode: getSidebarMode(),
isResizing: false,
minWidth: 17, // in em
maxWidth: 37.5, // in em
maxWidth: 48, // in em
},
});
+4
View File
@@ -42,7 +42,9 @@ export interface ShareInfoObject {
export interface UserObject {
preview: {
video: boolean;
audio: boolean;
image: boolean;
models: boolean;
popup: boolean;
autoplayMedia?: boolean;
defaultMediaPlayer?: boolean;
@@ -53,6 +55,8 @@ export interface UserObject {
gallerySize: number;
singleClick: boolean;
stickySidebar: boolean;
hideFilesInTree: boolean,
deleteAfterArchive: boolean,
locale: string;
viewMode: string;
showHidden: boolean;
+16 -1
View File
@@ -9,6 +9,13 @@ const RAW_IMAGE_MIME_TYPES = new Set([
"image/x-phaseone-iiq", "image/x-kodak-nkc", "image/x-red-r3d",
]);
const EBOOK_MIME_TYPES = new Set([
"application/epub+zip", "application/x-mobipocket-ebook", "application/vnd.amazon.ebook",
"application/x-fictionbook+xml", "application/x-fb2", "application/x-cbr", "application/x-cbz",
"application/x-cb7", "application/x-cbt", "application/vnd.comicbook+zip",
"application/vnd.comicbook-rar", "application/x-kindle", "application/x-azw"
]);
export function isRawImageMimeType(mimeType) {
return typeof mimeType === "string" && RAW_IMAGE_MIME_TYPES.has(mimeType);
}
@@ -29,6 +36,14 @@ export function getTypeInfo(mimeType) {
};
}
if (EBOOK_MIME_TYPES.has(mimeType)) {
return {
classes: "brown-icons material-icons",
materialIcon: "menu_book",
simpleType: "ebook",
};
}
if (mimeType.startsWith("image/")) {
return {
classes: "orange-icons material-icons",
@@ -61,7 +76,7 @@ export function getTypeInfo(mimeType) {
if (mimeType == "file_download") {
return {
classes: "material-icons simple-icons",
classes: "material-icons",
materialIcon: "file_download",
simpleType: "file_download",
};
+3 -1
View File
@@ -214,7 +214,9 @@ export function goToItem(source, path, previousHistoryItem, newTab = false) {
let newPath = encodedPath(path);
let fullPath = `/files/${encodeURIComponent(source)}${newPath}`;
if (newTab) {
window.open(fullPath, '_blank');
// Use absolute URL for new tab to ensure proper navigation
const absoluteUrl = `${window.location.origin}${globalVars.baseURL}${fullPath.startsWith('/') ? fullPath.slice(1) : fullPath}`;
window.open(absoluteUrl, '_blank');
return;
}
mutations.setPreviousHistoryItem(previousHistoryItem);
+1 -6
View File
@@ -22,7 +22,7 @@
<Notifications />
<Toast :toasts="toasts" />
<StatusBar :class="{ moveWithSidebar: moveWithSidebar.shouldMove }" />
<ContextMenu v-if="showContextMenu" v-bind="contextMenuProps"></ContextMenu>
<ContextMenu v-bind="contextMenuProps"></ContextMenu>
<Tooltip />
<NextPrevious />
<PopupPreview v-if="popupEnabled" />
@@ -141,10 +141,6 @@ export default {
currentView() {
return getters.currentView();
},
showContextMenu() {
// for now lets disable for tools view
return getters.currentView() != "tools"
},
contextMenuProps() {
const prompt = getters.currentPrompt();
if (prompt && prompt.name === "ContextMenu") {
@@ -171,7 +167,6 @@ export default {
return;
}
const currentView = getters.currentView()
mutations.setMultiple(false);
const currentPrompt = getters.currentPromptName();
if (currentPrompt !== "success" && currentPrompt !== "generic") {
mutations.closeTopHover();
+1 -1
View File
@@ -26,7 +26,7 @@
</div>
<!-- Dynamically render the selected tool component -->
<component v-else-if="currentTool" :is="currentTool.component" />
<component v-else-if="currentTool" :is="currentTool.component" :key="$route.params.toolName" />
<!-- Show error if tool not found -->
<div v-else class="tool-not-found">
+1 -1
View File
@@ -116,7 +116,7 @@ export default {
return getters.isShare();
},
noItems() {
return !state.contextMenuHasItems;
return !state.contextMenuHasItems && !getters.isPreviewView();
},
showEdit() {
return window.location.hash != "#edit" && state.user.permissions.modify;
+32 -5
View File
@@ -14,7 +14,7 @@
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent, watch } from "vue";
import ePub, { type Book, type Rendition } from "epubjs";
import { state, mutations, getters } from "@/store"; // Assuming your store setup
import { resourcesApi } from "@/api";
@@ -68,6 +68,12 @@ export default defineComponent({
// 4. Display the rendered book
await this.rendition.display();
this.applyTheme(getters.isDarkMode());
watch(() => getters.isDarkMode(), (isDark) => {
this.applyTheme(isDark);
});
// Set flags to show the book and trigger animations
this.isReady = true;
setTimeout(() => {
@@ -84,6 +90,20 @@ export default defineComponent({
}
},
methods: {
applyTheme(isDark: boolean) {
if (!this.rendition) return;
if (isDark) {
this.rendition.themes.default({
body: { color: "#fff !important" },
a: { color: "#bb86fc !important" },
});
} else {
this.rendition.themes.default({
body: { color: "#000 !important" },
a: { color: "#6200ee !important" },
});
}
},
// Navigate to the next page
nextPage() {
this.rendition?.next();
@@ -111,7 +131,7 @@ export default defineComponent({
.epub-container {
width: 100%;
height: 100%;
background-color: var(--surfaceSecondary); /* A light background for the reader */
background-color: var(--background); /* background for the reader */
z-index: 1000;
}
@@ -121,7 +141,7 @@ export default defineComponent({
align-items: center;
height: 100%;
font-size: 1.2em;
color: #6c757d;
color: var(--textSecondary);
}
/* The viewer must have a defined height for epub.js to work */
@@ -143,19 +163,25 @@ export default defineComponent({
z-index: 1001; /* Ensure controls are on top */
display: flex;
gap: 1em;
background-color: rgba(255, 255, 255, 0.8);
background-color: var(--surfaceSecondary);
padding: 0.5em;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
align-items: center;
}
.nav-button {
background-color: transparent;
border: none;
font-size: 1.5em;
color: #343a40;
color: var(--textPrimary);
cursor: pointer;
padding: 0.25em 1em;
transition: background-color 0.2s;
}
.nav-button:hover {
background-color: var(--alt-background);
}
/* Copied directly from your example */
@@ -178,5 +204,6 @@ export default defineComponent({
font-size: 2em;
padding-right: 1em;
padding-left: 1em;
color: var(--textPrimary);
}
</style>
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more