Compare commits
49
Commits
main
...
v0.7.7-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c815a9f623 | ||
|
|
1d5026984b | ||
|
|
7e80179e74 | ||
|
|
4a7390b0dc | ||
|
|
50d3513880 | ||
|
|
9a0cd4552d | ||
|
|
b0aa189d34 | ||
|
|
fa0900d4f8 | ||
|
|
b57c3adaa1 | ||
|
|
282fd6f227 | ||
|
|
cb7ad4af92 | ||
|
|
d896858bcd | ||
|
|
712ad91f20 | ||
|
|
cfa1537e2e | ||
|
|
d4e0c38f06 | ||
|
|
305a54ffb6 | ||
|
|
3acdde80ea | ||
|
|
a3d8833a2e | ||
|
|
421fd126c8 | ||
|
|
dfdc422ccf | ||
|
|
114d2136bb | ||
|
|
cfc4a1be78 | ||
|
|
d172c0fa4e | ||
|
|
fe1984f234 | ||
|
|
27bfee087c | ||
|
|
dc96f9d0ae | ||
|
|
24c83842a5 | ||
|
|
bd822497e1 | ||
|
|
ea9d2fe6df | ||
|
|
11e0681562 | ||
|
|
51d8df62b5 | ||
|
|
328872a620 | ||
|
|
3de0848fef | ||
|
|
ff829772d5 | ||
|
|
8da3a8d14c | ||
|
|
49ad06fecd | ||
|
|
24ca0297ad | ||
|
|
53f2f3c87c | ||
|
|
909987a2cf | ||
|
|
91566b2721 | ||
|
|
19ae24637d | ||
|
|
d5354e556c | ||
|
|
ff77854ed6 | ||
|
|
d53b769e61 | ||
|
|
b111ec66ec | ||
|
|
9622f1226c | ||
|
|
9d1e9e0d72 | ||
|
|
90110ac999 | ||
|
|
81cbaf7766 |
No files matched your search
@@ -1,5 +1,6 @@
|
||||
backend/**.db
|
||||
backend/tmp
|
||||
backend/backend*
|
||||
backend/vendor/**
|
||||
frontend/dist/**
|
||||
frontend/node_modules/**
|
||||
@@ -81,7 +81,7 @@ jobs:
|
||||
file: ./_docker/Dockerfile.playwright-noauth
|
||||
push: false
|
||||
push_pr_to_registry:
|
||||
name: Push PR
|
||||
name: Build docker image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -15,17 +15,18 @@ jobs:
|
||||
go-version: 'stable'
|
||||
- working-directory: backend
|
||||
run: go test -race -v ./...
|
||||
lint-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 'stable'
|
||||
- uses: golangci/golangci-lint-action@v5
|
||||
with:
|
||||
version: 'v1.64'
|
||||
working-directory: backend
|
||||
# golanci-lint is currently disabled due to slowness
|
||||
# lint-backend:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: actions/setup-go@v5
|
||||
# with:
|
||||
# go-version: 'stable'
|
||||
# - uses: golangci/golangci-lint-action@v5
|
||||
# with:
|
||||
# version: 'v1.64'
|
||||
# working-directory: backend
|
||||
format-backend:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
|
||||
REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: ./_docker/Dockerfile.slim
|
||||
file: ./_docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.modify-json.outputs.cleaned_tag }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
+14
-12
@@ -21,9 +21,8 @@ jobs:
|
||||
run: npm i && npm run build
|
||||
working-directory: frontend
|
||||
- name: Install OS Requirements
|
||||
run: sudo apt update && sudo apt install -y upx gcc-multilib g++-multilib
|
||||
- name: Install Cross Compilers
|
||||
run: sudo apt update && sudo apt install -y gcc-mingw-w64 gcc-aarch64-linux-gnu
|
||||
run: sudo apt update && sudo apt install -y upx build-essential musl-tools
|
||||
- uses: goto-bus-stop/setup-zig@v2
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
@@ -65,7 +64,10 @@ jobs:
|
||||
run: |
|
||||
JSON='${{ steps.meta.outputs.json }}'
|
||||
MODIFIED_JSON=$(echo "$JSON" | jq '
|
||||
.tags |= map(sub(":v"; ":")) |
|
||||
.tags |= map(
|
||||
sub(":v"; ":") # Strip the 'v'
|
||||
| select(test(":latest") | not)
|
||||
) |
|
||||
if any(.tags[]; test("beta")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:beta",
|
||||
@@ -126,26 +128,26 @@ jobs:
|
||||
images: |
|
||||
gtstef/filebrowser
|
||||
ghcr.io/gtsteffaniak/filebrowser
|
||||
- name: Modify tags (strip 'v' and add 'beta'/'stable' if needed)
|
||||
- name: Modify tags (strip 'v' and add 'slim' if needed)
|
||||
id: modify-json-slim
|
||||
run: |
|
||||
JSON='${{ steps.meta.outputs.json }}'
|
||||
MODIFIED_JSON=$(echo "$JSON" | jq '
|
||||
.tags |= map(
|
||||
sub(":v"; ":") # Strip the 'v'
|
||||
| select(test(":latest") | not)
|
||||
| sub(":(.+)"; ":\(.captures[0])-slim")
|
||||
sub(":v"; ":") # Strip the 'v'
|
||||
| select(test(":latest") | not) # Remove the "latest" tag
|
||||
| . + "-slim" # Append "-slim" to the versioned tag
|
||||
) |
|
||||
if any(.tags[]; test("beta")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:beta",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:beta"
|
||||
"gtstef/filebrowser:beta-slim",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:beta-slim"
|
||||
]
|
||||
else . end |
|
||||
if any(.tags[]; test("stable")) then
|
||||
.tags += [
|
||||
"gtstef/filebrowser:stable",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:stable"
|
||||
"gtstef/filebrowser:stable-slim",
|
||||
"ghcr.io/gtsteffaniak/filebrowser:stable-slim"
|
||||
]
|
||||
else . end
|
||||
')
|
||||
|
||||
@@ -10,6 +10,7 @@ rice-box.go
|
||||
/backend/filebrowser.exe
|
||||
/backend/backend.exe
|
||||
/backend/tmp
|
||||
/backend/dist
|
||||
/frontend/dist
|
||||
/frontend/pkg
|
||||
/frontend/test-results
|
||||
|
||||
+24
-4
@@ -2,9 +2,31 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
|
||||
|
||||
## v0.7.6-beta
|
||||
## v0.7.7-beta
|
||||
|
||||
NOTE: if using docker arm32 image, you will need to switch to the slim images. The regular docker images are much larger now and support generating office previews out of the box without any only office running. However, they don't support arm32. Also, be aware the docker images are much larger now (600MB I believe) because of the office document preview support -- if thats not something you care about you can switch to the slim images.
|
||||
This release cleans up some of the native preview (image preview) feature logic. And adds simple docx and epub viewers as well. Going through all of this, I think I know how I can add full-fledge google doc and microsoft office viewer support (no edit). But, for now "onlyOffice" remains the most comprehensive solution with most compatibility and ability to fully edit. One day, I think I will be able to integrate a minimal license-free server into the docker image. But that's something for another time.
|
||||
|
||||
Native preview (image preview) support is also available for linux arm64 and amd64 binaries, and windows exe.
|
||||
|
||||
**New Features**:
|
||||
- since theres a wider kind of document preview types, a new disableOfficePreviewExt option has been added.
|
||||
- native (and simple) docx and epub viewers.
|
||||
- Other documents like xlsx get full size image preview when opened and no onlyoffice support.
|
||||
|
||||
**Notes**:
|
||||
- all text mimetype files have preview support.
|
||||
- high-quality preview image sizes bumped from 512x512 to 640x640 to help make text previews readable.
|
||||
- no config is allowed and defaults to on source at current directory.
|
||||
|
||||
**BugFixes**:
|
||||
- fix otp clearing on user save https://github.com/gtsteffaniak/filebrowser/issues/699
|
||||
- admin special characters and general login improvements https://github.com/gtsteffaniak/filebrowser/issues/594
|
||||
- updated editor caching behavior https://github.com/gtsteffaniak/filebrowser/issues/701
|
||||
- move/copy file path issue and overwrite https://github.com/gtsteffaniak/filebrowser/issues/687
|
||||
- fix popup preview loading on safari
|
||||
- `preview.highQuality` only affects gallery view mode. popop preview is always high quality, and icons are always low quality.
|
||||
|
||||
## v0.7.6-beta
|
||||
|
||||
**New Features**:
|
||||
- native document preview generation enabled for certain document types on the regular docker image (no office integration needed)
|
||||
@@ -17,11 +39,9 @@ NOTE: if using docker arm32 image, you will need to switch to the slim images. T
|
||||
- ".cbz", // CBZ
|
||||
- ".svg", // SVG
|
||||
- ".txt", // TXT
|
||||
- ".doc", // DOC
|
||||
- ".docx", // DOCX
|
||||
- ".ppt", // PPT
|
||||
- ".pptx", // PPTX
|
||||
- ".xls", // excel xls
|
||||
- ".xlsx", // exel XLSX
|
||||
- ".hwp", // HWP
|
||||
- ".hwp", // HWPX
|
||||
|
||||
+7
-11
@@ -1,15 +1,16 @@
|
||||
FROM gtstef/ffmpeg:7.1.1 AS ffmpeg
|
||||
FROM golang:1.24-alpine AS base
|
||||
FROM gtstef/ffmpeg:7.1.1-decode AS ffmpeg
|
||||
FROM golang:alpine AS base
|
||||
ARG VERSION
|
||||
ARG REVISION
|
||||
WORKDIR /app
|
||||
COPY ./backend ./
|
||||
#RUN swag init --output swagger/docs
|
||||
RUN ln -s swagger /usr/local/go/src/
|
||||
RUN go build -tags pdf -ldflags="-w -s \
|
||||
RUN apk update && apk add --no-cache gcc musl-dev upx
|
||||
ENV CGO_ENABLED=1
|
||||
RUN go build -tags mupdf,musl -ldflags="-w -s \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.Version=${VERSION}' \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.CommitSHA=${REVISION}'" \
|
||||
-o filebrowser .
|
||||
RUN upx filebrowser
|
||||
|
||||
FROM node:lts-slim AS nbuild
|
||||
WORKDIR /app
|
||||
@@ -20,9 +21,6 @@ RUN npm run build-docker
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=ffmpeg [ "/ffmpeg", "/ffprobe", "/usr/local/bin/" ]
|
||||
# requirements to enable pdf support
|
||||
RUN apk add --no-cache libffi mupdf && \
|
||||
find / -name "libmupdf.so*" -type f -exec mv {} /usr/lib/libmupdf.so \;
|
||||
|
||||
ENV FILEBROWSER_FFMPEG_PATH="/usr/local/bin/"
|
||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||
@@ -31,15 +29,13 @@ RUN apk --no-cache add ca-certificates mailcap tzdata
|
||||
RUN adduser -D -s /bin/true -u 1000 filebrowser
|
||||
USER filebrowser
|
||||
WORKDIR /home/filebrowser
|
||||
COPY --from=base --chown=filebrowser:1000 /app/filebrowser* ./
|
||||
COPY --from=base --chown=filebrowser:1000 /app/filebrowser ./
|
||||
COPY --from=base --chown=filebrowser:1000 /app/config.media.yaml ./config.yaml
|
||||
COPY --from=nbuild --chown=filebrowser:1000 /app/dist/ ./http/dist/
|
||||
|
||||
## sanity checks
|
||||
RUN [ "filebrowser", "version" ]
|
||||
RUN [ "ffmpeg", "-version" ]
|
||||
RUN [ "ffprobe", "-version" ]
|
||||
|
||||
USER root
|
||||
# exposing default port for auto discovery.
|
||||
EXPOSE 80
|
||||
|
||||
+5
-28
@@ -1,14 +1,14 @@
|
||||
FROM golang:1.24-alpine AS base
|
||||
FROM golang:alpine AS base
|
||||
ARG VERSION
|
||||
ARG REVISION
|
||||
WORKDIR /app
|
||||
COPY ./backend ./
|
||||
#RUN swag init --output swagger/docs
|
||||
RUN ln -s swagger /usr/local/go/src/
|
||||
RUN apk update && apk add upx
|
||||
RUN go build -ldflags="-w -s \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.Version=${VERSION}' \
|
||||
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.CommitSHA=${REVISION}'" \
|
||||
-o filebrowser .
|
||||
RUN upx filebrowser
|
||||
|
||||
FROM node:lts-slim AS nbuild
|
||||
WORKDIR /app
|
||||
@@ -18,40 +18,17 @@ COPY ./frontend/ ./
|
||||
RUN npm run build-docker
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache curl xz && \
|
||||
ARCH=$(uname -m) && \
|
||||
case "$ARCH" in \
|
||||
x86_64) URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz" ;; \
|
||||
aarch64) URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-arm64-static.tar.xz" ;; \
|
||||
armv7l) URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-armhf-static.tar.xz" ;; \
|
||||
*) echo "Unsupported architecture: $ARCH" && exit 1 ;; \
|
||||
esac && \
|
||||
mkdir -p /tmp/ffmpeg && \
|
||||
curl -L "$URL" | tar -xJ -C /tmp/ffmpeg && \
|
||||
mv /tmp/ffmpeg/ffmpeg-*/ffmpeg /usr/local/bin/ && \
|
||||
mv /tmp/ffmpeg/ffmpeg-*/ffprobe /usr/local/bin/ && \
|
||||
chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe && \
|
||||
rm -rf /tmp/ffmpeg && \
|
||||
apk del curl xz && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
ENV FILEBROWSER_FFMPEG_PATH="/usr/local/bin/"
|
||||
ENV FILEBROWSER_NO_EMBEDED="true"
|
||||
ENV PATH="$PATH:/home/filebrowser"
|
||||
RUN apk --no-cache add ca-certificates mailcap tzdata
|
||||
RUN adduser -D -s /bin/true -u 1000 filebrowser
|
||||
USER filebrowser
|
||||
WORKDIR /home/filebrowser
|
||||
COPY --from=base --chown=filebrowser:1000 /app/filebrowser* ./
|
||||
COPY --from=base --chown=filebrowser:1000 /app/config.media.yaml ./config.yaml
|
||||
COPY --from=base --chown=filebrowser:1000 /app/filebrowser ./
|
||||
COPY --from=base --chown=filebrowser:1000 /app/config.yaml ./config.yaml
|
||||
COPY --from=nbuild --chown=filebrowser:1000 /app/dist/ ./http/dist/
|
||||
|
||||
## sanity checks
|
||||
RUN [ "filebrowser", "version" ]
|
||||
RUN [ "ffmpeg", "-version" ]
|
||||
RUN [ "ffprobe", "-version" ]
|
||||
|
||||
USER root
|
||||
# exposing default port for auto discovery.
|
||||
EXPOSE 80
|
||||
|
||||
+13
-21
@@ -16,29 +16,29 @@ builds:
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- arm64
|
||||
- amd64
|
||||
hooks:
|
||||
post:
|
||||
- upx {{ .Path }} # Compress the binary with UPX
|
||||
- upx {{ .Path }}
|
||||
|
||||
# Build configuration for linux amd64
|
||||
# Build configuration for linux arm64
|
||||
- id: linux-arm64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=aarch64-linux-gnu-gcc
|
||||
- CXX=aarch64-linux-gnu-g++
|
||||
tags:
|
||||
- mupdf
|
||||
ldflags: *ldflags
|
||||
main: main.go
|
||||
binary: filebrowser
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=zig cc -target aarch64-linux-musl
|
||||
tags:
|
||||
- mupdf
|
||||
- musl
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- arm64
|
||||
hooks:
|
||||
post:
|
||||
- upx {{ .Path }} # Compress the binary with UPX
|
||||
- upx {{ .Path }}
|
||||
|
||||
# Build configuration for linux
|
||||
- id: linux-arm32
|
||||
@@ -60,8 +60,7 @@ builds:
|
||||
- id: windows
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
- CC=x86_64-w64-mingw32-gcc
|
||||
- CXX=x86_64-w64-mingw32-g++
|
||||
- CC=zig cc -target x86_64-windows
|
||||
tags:
|
||||
- mupdf
|
||||
ldflags: *ldflags
|
||||
@@ -76,7 +75,7 @@ builds:
|
||||
- upx {{ .Path }} # Compress the binary with UPX
|
||||
|
||||
# Build configuration for freebsd without arm & upx
|
||||
- id: freeBSD
|
||||
- id: freeBSD and freebsd-arm
|
||||
ldflags: *ldflags
|
||||
main: main.go
|
||||
binary: filebrowser
|
||||
@@ -84,9 +83,6 @@ builds:
|
||||
- freebsd
|
||||
goarch:
|
||||
- amd64
|
||||
hooks:
|
||||
post:
|
||||
- upx {{ .Path }} # Compress the binary with UPX
|
||||
|
||||
# Build configuration for macos without upx
|
||||
- id: macos
|
||||
@@ -96,11 +92,8 @@ builds:
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
hooks:
|
||||
post:
|
||||
- upx {{ .Path }} # Compress the binary with UPX
|
||||
- amd64
|
||||
|
||||
archives:
|
||||
- name_template: >
|
||||
@@ -113,4 +106,3 @@ archives:
|
||||
|
||||
checksum:
|
||||
disable: true
|
||||
|
||||
@@ -31,10 +31,6 @@ type FileCache struct {
|
||||
|
||||
// NewFileCache creates a new FileCache
|
||||
func NewFileCache(dir string) (*FileCache, error) {
|
||||
if err := os.MkdirAll(filepath.Join(dir, "thumbnails", "video"), 0700); err != nil {
|
||||
return nil, fmt.Errorf("can't make directory %s: %v", dir, err)
|
||||
}
|
||||
|
||||
return &FileCache{dir: dir}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ type JSONAuth struct {
|
||||
|
||||
// Auth authenticates the user via a json in content body.
|
||||
func (auther JSONAuth) Auth(r *http.Request, userStore *users.Storage) (*users.User, error) {
|
||||
password := r.URL.Query().Get("password")
|
||||
username := r.URL.Query().Get("username")
|
||||
recaptcha := r.URL.Query().Get("recaptcha")
|
||||
totpCode := r.URL.Query().Get("code")
|
||||
password := r.Header.Get("X-Password")
|
||||
totpCode := r.Header.Get("X-Secret")
|
||||
|
||||
// If ReCaptcha is enabled, check the code.
|
||||
if auther.ReCaptcha != nil && len(auther.ReCaptcha.Secret) > 0 {
|
||||
@@ -54,7 +54,6 @@ func (auther JSONAuth) Auth(r *http.Request, userStore *users.Storage) (*users.U
|
||||
}
|
||||
err = VerifyTotpCode(user, totpCode, userStore)
|
||||
if err != nil {
|
||||
logger.Error("OTP verification failed")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,11 +190,14 @@ func VerifyTotpCode(user *users.User, code string, userStore *users.Storage) err
|
||||
user.TOTPSecret = totpSecret // The encrypted or plaintext secret
|
||||
user.TOTPNonce = totpNonce // The nonce if encrypted, or empty if plaintext
|
||||
user.OtpEnabled = true // Enable OTP for the user
|
||||
// save user
|
||||
if err := userStore.Update(user, user.Permissions.Admin, "TOTPSecret", "TOTPNonce"); err != nil {
|
||||
logger.Error("error updating user with OTP token:", err)
|
||||
return fmt.Errorf("error updating user with OTP token: %w", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("opt secret is empty, cannot enable TOTP")
|
||||
}
|
||||
// save user
|
||||
if err := userStore.Update(user, user.Permissions.Admin, "TOTPSecret", "TOTPNonce"); err != nil {
|
||||
logger.Debug("error updating user with OTP token:", err)
|
||||
return fmt.Errorf("error updating user with OTP token: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -227,6 +227,13 @@ func loadConfigWithDefaults(configFile string) error {
|
||||
yamlFile, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
logger.Warningf("could not open config file '%v', using default settings", configFile)
|
||||
Config.Server.Sources = []Source{
|
||||
{
|
||||
Path: ".",
|
||||
},
|
||||
}
|
||||
loadEnvConfig()
|
||||
return nil
|
||||
}
|
||||
defer yamlFile.Close()
|
||||
stat, err := yamlFile.Stat()
|
||||
|
||||
@@ -39,10 +39,10 @@ type Server struct {
|
||||
CacheDir string `json:"cacheDir"` // path to the cache directory, used for thumbnails and other cached files
|
||||
MaxArchiveSizeGB int64 `json:"maxArchiveSize"` // max pre-archive combined size of files/folder that are allowed to be archived (in GB)
|
||||
// not exposed to config
|
||||
SourceMap map[string]Source `json:"-" validate:"omitempty"` // uses realpath as key
|
||||
NameToSource map[string]Source `json:"-" validate:"omitempty"` // uses name as key
|
||||
DefaultSource Source `json:"-" validate:"omitempty"`
|
||||
PdfAvailable bool `json:"-"` // used internally if compiled with pdf support
|
||||
SourceMap map[string]Source `json:"-" validate:"omitempty"` // uses realpath as key
|
||||
NameToSource map[string]Source `json:"-" validate:"omitempty"` // uses name as key
|
||||
DefaultSource Source `json:"-" validate:"omitempty"`
|
||||
MuPdfAvailable bool `json:"-"` // used internally if compiled with mupdf support
|
||||
}
|
||||
|
||||
type Integrations struct {
|
||||
@@ -113,21 +113,22 @@ type ExternalLink struct {
|
||||
// UserDefaults is a type that holds the default values
|
||||
// for some fields on User.
|
||||
type UserDefaults struct {
|
||||
StickySidebar bool `json:"stickySidebar"` // keep sidebar open when navigating
|
||||
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
|
||||
SingleClick bool `json:"singleClick"` // open directory on single click, also enables middle click to open in new tab
|
||||
ShowHidden bool `json:"showHidden"` // show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
|
||||
DateFormat bool `json:"dateFormat"` // when false, the date is relative, when true, the date is an exact timestamp
|
||||
GallerySize int `json:"gallerySize"` // 0-9 - the size of the gallery thumbnails
|
||||
ThemeColor string `json:"themeColor"` // theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
|
||||
QuickDownload bool `json:"quickDownload"` // show icon to download in one click
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // comma separated list of file extensions to disable onlyoffice preview for
|
||||
LockPassword bool `json:"lockPassword"` // disable the user from changing their password
|
||||
DisableSettings bool `json:"disableSettings,omitempty"` // disable the user from viewing the settings page
|
||||
Preview users.Preview `json:"preview"`
|
||||
DefaultScopes []users.SourceScope `json:"-"`
|
||||
Permissions users.Permissions `json:"permissions"`
|
||||
LoginMethod string `json:"loginMethod,omitempty"` // login method to use: eg. password, proxy, oidc
|
||||
StickySidebar bool `json:"stickySidebar"` // keep sidebar open when navigating
|
||||
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
|
||||
SingleClick bool `json:"singleClick"` // open directory on single click, also enables middle click to open in new tab
|
||||
ShowHidden bool `json:"showHidden"` // show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
|
||||
DateFormat bool `json:"dateFormat"` // when false, the date is relative, when true, the date is an exact timestamp
|
||||
GallerySize int `json:"gallerySize"` // 0-9 - the size of the gallery thumbnails
|
||||
ThemeColor string `json:"themeColor"` // theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
|
||||
QuickDownload bool `json:"quickDownload"` // show icon to download in one click
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // comma separated list of file extensions to disable onlyoffice preview for
|
||||
DisableOfficePreviewExt string `json:"disableOfficePreviewExt"` // comma separated list of file extensions to disable office preview for
|
||||
LockPassword bool `json:"lockPassword"` // disable the user from changing their password
|
||||
DisableSettings bool `json:"disableSettings,omitempty"` // disable the user from viewing the settings page
|
||||
Preview users.Preview `json:"preview"`
|
||||
DefaultScopes []users.SourceScope `json:"-"`
|
||||
Permissions users.Permissions `json:"permissions"`
|
||||
LoginMethod string `json:"loginMethod,omitempty"` // login method to use: eg. password, proxy, oidc
|
||||
}
|
||||
@@ -126,6 +126,14 @@ func (st usersBackend) Update(user *users.User, actorIsAdmin bool, fields ...str
|
||||
|
||||
// Get the value to be stored
|
||||
val := fieldValue.Interface()
|
||||
if field == "OtpEnabled" {
|
||||
otpEnabled, _ := val.(bool)
|
||||
if otpEnabled {
|
||||
continue
|
||||
}
|
||||
field = "TOTPSecret" // if otp is disabled, we also want to clear the TOTPSecret
|
||||
val = "" // clear the TOTPSecret
|
||||
}
|
||||
// Update the database
|
||||
if err := st.db.UpdateField(existingUser, field, val); err != nil {
|
||||
return fmt.Errorf("failed to update user field: %s, error: %v", field, err)
|
||||
@@ -226,7 +234,7 @@ func parseFields(user *users.User, fields []string, actorIsAdmin bool) ([]string
|
||||
field := t.Field(i)
|
||||
// which=all can't update password
|
||||
switch strings.ToLower(field.Name) {
|
||||
case "id", "username", "loginmethod", "password":
|
||||
case "id", "username", "loginmethod", "password", "apikeys", "totpenabled", "totpsecret", "totpnonce":
|
||||
// Skip these fields
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -75,20 +75,21 @@ type SourceScope struct {
|
||||
|
||||
// json tags must match variable name with smaller case first letter
|
||||
type NonAdminEditable struct {
|
||||
Preview Preview `json:"preview"`
|
||||
StickySidebar bool `json:"stickySidebar"` // keep sidebar open when navigating
|
||||
DarkMode bool `json:"darkMode"` // should dark mode be enabled
|
||||
Password string `json:"password,omitempty"`
|
||||
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
|
||||
SingleClick bool `json:"singleClick"` // open directory on single click, also enables middle click to open in new tab
|
||||
Sorting Sorting `json:"sorting"`
|
||||
ShowHidden bool `json:"showHidden"` // show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
|
||||
DateFormat bool `json:"dateFormat"` // when false, the date is relative, when true, the date is an exact timestamp
|
||||
GallerySize int `json:"gallerySize"` // 0-9 - the size of the gallery thumbnails
|
||||
ThemeColor string `json:"themeColor"` // theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
|
||||
QuickDownload bool `json:"quickDownload"` // show icon to download in one click
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // comma separated list of file extensions to disable onlyoffice preview for
|
||||
Preview Preview `json:"preview"`
|
||||
StickySidebar bool `json:"stickySidebar"` // keep sidebar open when navigating
|
||||
DarkMode bool `json:"darkMode"` // should dark mode be enabled
|
||||
Password string `json:"password,omitempty"`
|
||||
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
|
||||
SingleClick bool `json:"singleClick"` // open directory on single click, also enables middle click to open in new tab
|
||||
Sorting Sorting `json:"sorting"`
|
||||
ShowHidden bool `json:"showHidden"` // show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
|
||||
DateFormat bool `json:"dateFormat"` // when false, the date is relative, when true, the date is an exact timestamp
|
||||
GallerySize int `json:"gallerySize"` // 0-9 - the size of the gallery thumbnails
|
||||
ThemeColor string `json:"themeColor"` // theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
|
||||
QuickDownload bool `json:"quickDownload"` // show icon to download in one click
|
||||
DisableOnlyOfficeExt string `json:"disableOnlyOfficeExt"` // comma separated list of file extensions to disable onlyoffice preview for
|
||||
DisableOfficePreviewExt string `json:"disableOfficePreviewExt"` // comma separated list of file extensions to disable office preview for
|
||||
}
|
||||
|
||||
var PublicUser = User{
|
||||
|
||||
+2
-2
@@ -5,7 +5,6 @@ go 1.24.2
|
||||
require (
|
||||
github.com/asdine/storm/v3 v3.2.1
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dsoprea/go-exif/v3 v3.0.1
|
||||
github.com/gen2brain/go-fitz v1.24.14
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
@@ -14,6 +13,7 @@ require (
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gtsteffaniak/go-cache v0.0.0-20250521142451-edc77dfcb063
|
||||
github.com/gtsteffaniak/go-logger v0.1.2
|
||||
github.com/kovidgoyal/imaging v1.6.4
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/spf13/afero v1.14.0
|
||||
@@ -101,7 +101,7 @@ require (
|
||||
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/flock v0.12.1 // indirect
|
||||
github.com/golang/geo v0.0.0-20250516193853-92f93c4cb289 // indirect
|
||||
github.com/golang/geo v0.0.0-20250602202553-92e1c6cfbc31 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
|
||||
github.com/golangci/go-printf-func-name v0.1.0 // indirect
|
||||
|
||||
+4
-2
@@ -147,8 +147,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8=
|
||||
github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
||||
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
|
||||
@@ -276,6 +274,8 @@ github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgR
|
||||
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
|
||||
github.com/golang/geo v0.0.0-20250516193853-92f93c4cb289 h1:HeOFbnyPys/vx/t+d4fwZM782mnjRVtbjxVkDittTUs=
|
||||
github.com/golang/geo v0.0.0-20250516193853-92f93c4cb289/go.mod h1:Vaw7L5b+xa3Rj4/pRtrQkymn3lSBRB/NAEdbF9YEVLA=
|
||||
github.com/golang/geo v0.0.0-20250602202553-92e1c6cfbc31 h1:E1wdqK72/SVuxkxQ2DSE7YZEmcUZvExysw58zmkLKik=
|
||||
github.com/golang/geo v0.0.0-20250602202553-92e1c6cfbc31/go.mod h1:Vaw7L5b+xa3Rj4/pRtrQkymn3lSBRB/NAEdbF9YEVLA=
|
||||
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=
|
||||
@@ -423,6 +423,8 @@ github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/tt
|
||||
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk=
|
||||
github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY=
|
||||
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=
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -114,7 +115,7 @@ func userWithoutOTPhelper(fn handleFunc) handleFunc {
|
||||
// This middleware is used when no user authentication is required
|
||||
// Call the actual handler function with the updated context
|
||||
username := r.URL.Query().Get("username")
|
||||
password := r.URL.Query().Get("password")
|
||||
password := r.Header.Get("X-Password")
|
||||
proxyUser := r.Header.Get(config.Auth.Methods.ProxyAuth.Header)
|
||||
if config.Auth.Methods.ProxyAuth.Enabled && proxyUser != "" {
|
||||
user, err := setupProxyUser(r, &requestContext{}, proxyUser)
|
||||
@@ -345,28 +346,80 @@ func setUserInResponseWriter(w http.ResponseWriter, user *users.User) {
|
||||
// LoggingMiddleware logs each request and its status code.
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
// DEFER RECOVERY FUNCTION
|
||||
defer func() {
|
||||
if rcv := recover(); rcv != nil {
|
||||
// Log detailed information about the panic
|
||||
// Extract as much context as possible for logging
|
||||
method := r.Method
|
||||
url := r.URL.String()
|
||||
remoteAddr := r.RemoteAddr
|
||||
username := "unknown" // Default username
|
||||
|
||||
// Wrap the ResponseWriter to capture the status code.
|
||||
// Attempt to get username from ResponseWriterWrapper if it's set
|
||||
if ww, ok := w.(*ResponseWriterWrapper); ok && ww.User != "" {
|
||||
username = ww.User
|
||||
}
|
||||
// Or try to get it from request context if your other middleware populates it
|
||||
// This depends on your context setup; example:
|
||||
// if dataCtx, ok := r.Context().Value("requestData").(*requestContext); ok && dataCtx.user != nil {
|
||||
// username = dataCtx.user.Username
|
||||
// }
|
||||
|
||||
// Get Go-level stack trace
|
||||
buf := make([]byte, 16384) // Increased buffer size for potentially long CGo traces
|
||||
n := runtime.Stack(buf, false) // false for current goroutine only
|
||||
stackTrace := string(buf[:n])
|
||||
|
||||
logger.Errorf("PANIC RECOVERED: %v\nUser: %s\nMethod: %s\nURL: %s\nRemoteAddr: %s\nGo Stack Trace:\n%s",
|
||||
rcv, username, method, url, remoteAddr, stackTrace)
|
||||
|
||||
// Attempt to send a 500 error response to the client
|
||||
// This is a best-effort; the connection might be broken or process too unstable.
|
||||
if ww, ok := w.(*ResponseWriterWrapper); ok { // Check if it's our wrapper
|
||||
if !ww.wroteHeader { // Only write if headers haven't been sent
|
||||
ww.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
ww.WriteHeader(http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
_, _ = renderJSON(w, r, &HttpResponse{
|
||||
Status: 500,
|
||||
Message: "A critical internal error occurred. Please try again later.",
|
||||
})
|
||||
}
|
||||
|
||||
// IMPORTANT: After a SIGSEGV from C code, the process might be unstable.
|
||||
// Even if Go recovers, continuing to run the process is risky.
|
||||
// Consider a strategy to gracefully shut down or signal an external supervisor
|
||||
// to restart the process after logging. For now, this will allow other requests
|
||||
// to proceed if the process doesn't die, but be wary.
|
||||
}
|
||||
}() // End of deferred recovery function
|
||||
|
||||
start := time.Now()
|
||||
wrappedWriter := &ResponseWriterWrapper{ResponseWriter: w, StatusCode: http.StatusOK}
|
||||
|
||||
// Call the next handler.
|
||||
// Call the next handler in the chain
|
||||
next.ServeHTTP(wrappedWriter, r)
|
||||
|
||||
// Capture the full URL path including the query parameters.
|
||||
// Existing logging logic for normal requests
|
||||
fullURL := r.URL.Path
|
||||
if r.URL.RawQuery != "" {
|
||||
fullURL += "?" + r.URL.RawQuery
|
||||
}
|
||||
truncUser := wrappedWriter.User
|
||||
if len(truncUser) > 12 {
|
||||
if truncUser == "" {
|
||||
truncUser = "N/A" // Handle case where user might not be set (e.g., if panic occurred before user auth)
|
||||
} else if len(truncUser) > 12 {
|
||||
truncUser = truncUser[:10] + ".."
|
||||
}
|
||||
duration := time.Since(start)
|
||||
|
||||
// Use the StatusCode from wrappedWriter, which might have been set to 500 by the recover logic
|
||||
logger.Api(wrappedWriter.StatusCode,
|
||||
fmt.Sprintf("%-7s | %3d | %-15s | %-12s | %-12s | \"%s\"",
|
||||
r.Method,
|
||||
wrappedWriter.StatusCode, // Captured status code
|
||||
wrappedWriter.StatusCode,
|
||||
r.RemoteAddr,
|
||||
truncUser,
|
||||
fmt.Sprintf("%vms", duration.Milliseconds()),
|
||||
|
||||
@@ -30,7 +30,6 @@ type userInfo struct {
|
||||
// If ID token verification or essential claims extraction fails, it falls back to the UserInfo endpoint.
|
||||
func oidcCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
ctx := r.Context()
|
||||
|
||||
oidcCfg := settings.Config.Auth.Methods.OidcAuth
|
||||
if oidcCfg.Provider == nil || oidcCfg.Verifier == nil {
|
||||
// Ensure Provider and Verifier are initialized on application startup
|
||||
@@ -50,7 +49,7 @@ func oidcCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestConte
|
||||
}
|
||||
ctx = oidc.ClientContext(ctx, customClient)
|
||||
}
|
||||
code := r.URL.Query().Get("code")
|
||||
code := r.Header.Get("X-Secret")
|
||||
// state := r.URL.Query().Get("state") // You might want to validate the state parameter for CSRF protection
|
||||
|
||||
// The redirect URI MUST match the one registered with the OIDC provider
|
||||
|
||||
@@ -97,8 +97,8 @@ func rawFileHandler(w http.ResponseWriter, r *http.Request, file iteminfo.Extend
|
||||
|
||||
func previewHelperFunc(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
previewSize := r.URL.Query().Get("size")
|
||||
if previewSize != "small" {
|
||||
previewSize = "large"
|
||||
if !(previewSize == "large" || previewSize == "original") {
|
||||
previewSize = "small"
|
||||
}
|
||||
if d.fileInfo.Type == "directory" {
|
||||
return http.StatusBadRequest, fmt.Errorf("can't create preview for directory")
|
||||
|
||||
@@ -58,7 +58,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, file, contentT
|
||||
"OidcAvailable": config.Auth.Methods.OidcAuth.Enabled,
|
||||
"PasswordAvailable": config.Auth.Methods.PasswordAuth.Enabled,
|
||||
"MediaAvailable": config.Integrations.Media.FfmpegPath != "",
|
||||
"PdfAvailable": config.Server.PdfAvailable,
|
||||
"MuPdfAvailable": config.Server.MuPdfAvailable,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(data)
|
||||
|
||||
@@ -43,7 +43,7 @@ func generateOTPHandler(w http.ResponseWriter, r *http.Request, d *requestContex
|
||||
// @Failure 401 {object} map[string]string "Unauthorized - invalid TOTP token"
|
||||
// @Router /api/auth/otp/verify [post]
|
||||
func verifyOTPHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
|
||||
code := r.URL.Query().Get("code")
|
||||
code := r.Header.Get("X-Secret")
|
||||
if code == "" {
|
||||
return http.StatusUnauthorized, fmt.Errorf("code is required")
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ var AllFiletypeOptions = []string{
|
||||
"text",
|
||||
}
|
||||
|
||||
var PdfConvertable = []string{
|
||||
var MuPdfConvertable = []string{
|
||||
".pdf", // PDF
|
||||
".xps", // XPS
|
||||
".epub", // EPUB
|
||||
@@ -29,14 +29,13 @@ var PdfConvertable = []string{
|
||||
".cbz", // CBZ
|
||||
".svg", // SVG
|
||||
".txt", // TXT
|
||||
".doc", // DOC
|
||||
".docx", // DOCX
|
||||
".ppt", // PPT
|
||||
".pptx", // PPTX
|
||||
".xls", // XLS
|
||||
".xlsx", // XLSX
|
||||
".hwp", // HWP
|
||||
".hwp", // HWPX
|
||||
".md", // Markdown
|
||||
}
|
||||
|
||||
// Known bundle-style extensions that are technically directories but treated as files
|
||||
@@ -375,12 +374,15 @@ func hasBundleExtension(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func HasDocConvertableExtension(name string) bool {
|
||||
if !settings.Config.Server.PdfAvailable {
|
||||
func HasDocConvertableExtension(name, mimetype string) bool {
|
||||
if !settings.Config.Server.MuPdfAvailable {
|
||||
return false
|
||||
}
|
||||
if strings.HasPrefix(mimetype, "text") {
|
||||
return true
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
for _, e := range PdfConvertable {
|
||||
for _, e := range MuPdfConvertable {
|
||||
if ext == e {
|
||||
return true
|
||||
}
|
||||
|
||||
+67
-12
@@ -1,15 +1,19 @@
|
||||
//go:build pdf
|
||||
// +build pdf
|
||||
//go:build mupdf
|
||||
// +build mupdf
|
||||
|
||||
package preview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image/jpeg"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gen2brain/go-fitz"
|
||||
"github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
)
|
||||
|
||||
func docEnabled() bool {
|
||||
@@ -18,21 +22,72 @@ func docEnabled() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Service) GenerateImageFromDoc(docPath string, pageNumber int) ([]byte, error) {
|
||||
if err := s.acquire(context.Background()); err != nil {
|
||||
return nil, err
|
||||
func (s *Service) GenerateImageFromDoc(file iteminfo.ExtendedFileInfo, tempFilePath string, pageNumber int) ([]byte, error) {
|
||||
// 1. Serialize access to the entire go-fitz operation block
|
||||
s.docGenMutex.Lock()
|
||||
defer s.docGenMutex.Unlock()
|
||||
|
||||
// 2. Lock the current goroutine to a single OS thread for CGo calls
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
docPath := file.RealPath
|
||||
// copy file to a temporary location if needed
|
||||
if strings.HasPrefix(file.Type, "text") && !strings.HasSuffix(file.RealPath, ".txt") {
|
||||
originalFile, err := os.Open(file.RealPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("text snippet: failed to open original file '%s': %w", file.RealPath, err)
|
||||
}
|
||||
defer originalFile.Close() // Ensure original file is closed
|
||||
|
||||
buffer := make([]byte, 1024) // Buffer for up to 1KB
|
||||
n, readErr := originalFile.Read(buffer)
|
||||
if readErr != nil && readErr != io.EOF { // io.EOF is not an error if some bytes were read
|
||||
return nil, fmt.Errorf("text snippet: failed to read from original file '%s': %w", file.RealPath, readErr)
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
return nil, fmt.Errorf("text snippet: original file '%s' is empty or unreadable", file.RealPath)
|
||||
} else {
|
||||
tempFile, err := os.Create(tempFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("text snippet: failed to create temporary file '%s': %w", tempFilePath, err)
|
||||
}
|
||||
defer os.Remove(tempFilePath) // Ensure cleanup on error
|
||||
// Write the read content (up to 1KB or EOF) to the temporary file
|
||||
if _, err := tempFile.Write(buffer[:n]); err != nil {
|
||||
tempFile.Close() // Attempt to close
|
||||
os.Remove(tempFilePath) // Clean up on error
|
||||
return nil, fmt.Errorf("text snippet: failed to write to temporary file '%s': %w", tempFilePath, err)
|
||||
}
|
||||
|
||||
// Close the temporary file so it can be reliably opened by path by other processes/functions
|
||||
if err := tempFile.Close(); err != nil {
|
||||
os.Remove(tempFilePath) // Clean up on error
|
||||
return nil, fmt.Errorf("text snippet: failed to close temporary file '%s': %w", tempFilePath, err)
|
||||
}
|
||||
|
||||
docPath = tempFilePath // Update docPath to point to the new temporary text snippet file
|
||||
}
|
||||
}
|
||||
defer s.release()
|
||||
doc, err := fitz.New(docPath)
|
||||
doc, err := fitz.New(docPath) // This calls the CGo version
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open PDF: %w", err)
|
||||
// The error message you received: "failed to open PDF: fitz: cannot open memory"
|
||||
return nil, fmt.Errorf("failed to open PDF from memory for file '%s': %w", docPath, err)
|
||||
}
|
||||
defer doc.Close()
|
||||
|
||||
// Get the image from the doc page
|
||||
img, err := doc.Image(pageNumber) // Assuming page numbers are 0-indexed as per go-fitz common usage
|
||||
// Ensure pageNumber is valid (e.g., >= 0 for 0-indexed or >= 1 for 1-indexed, check go-fitz docs)
|
||||
// And pageNumber < doc.NumPage()
|
||||
numPages := doc.NumPage()
|
||||
if pageNumber < 0 || pageNumber >= numPages { // Assuming 0-indexed for Image()
|
||||
return nil, fmt.Errorf("invalid page number %d for PDF with %d pages ('%s')", pageNumber, numPages, docPath)
|
||||
}
|
||||
|
||||
img, err := doc.Image(pageNumber)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get image from page %d: %w", pageNumber, err)
|
||||
return nil, fmt.Errorf("failed to get image from page %d of '%s': %w", pageNumber, docPath, err)
|
||||
}
|
||||
|
||||
// Create a new buffer to hold the image bytes
|
||||
@@ -41,7 +96,7 @@ func (s *Service) GenerateImageFromDoc(docPath string, pageNumber int) ([]byte,
|
||||
// Encode the image directly into the buffer
|
||||
err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: jpeg.DefaultQuality})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encode image to jpeg: %w", err)
|
||||
return nil, fmt.Errorf("failed to encode image to jpeg for '%s': %w", docPath, err)
|
||||
}
|
||||
|
||||
// Return the byte slice from the buffer
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
//go:build !pdf
|
||||
// +build !pdf
|
||||
//go:build !mupdf
|
||||
// +build !mupdf
|
||||
|
||||
package preview
|
||||
|
||||
import "github.com/gtsteffaniak/filebrowser/backend/indexing/iteminfo"
|
||||
|
||||
func docEnabled() bool {
|
||||
// This function checks if the PDF support is enabled.
|
||||
// In a real implementation, this might check a build tag or configuration.
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Service) GenerateImageFromDoc(pdfPath string, pageNumber int) ([]byte, error) {
|
||||
func (s *Service) GenerateImageFromDoc(file iteminfo.ExtendedFileInfo, tempFilePath string, pageNumber int) ([]byte, error) { // 1. Serialize access to the entire go-fitz operation block
|
||||
s.docGenMutex.Lock()
|
||||
defer s.docGenMutex.Unlock()
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"image"
|
||||
"io"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/kovidgoyal/imaging"
|
||||
exif "github.com/dsoprea/go-exif/v3"
|
||||
exifcommon "github.com/dsoprea/go-exif/v3/common"
|
||||
)
|
||||
|
||||
+45
-21
@@ -3,7 +3,7 @@ package preview
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gtsteffaniak/filebrowser/backend/adapters/fs/diskcache"
|
||||
@@ -32,6 +33,7 @@ type Service struct {
|
||||
ffprobePath string
|
||||
fileCache diskcache.Interface
|
||||
debug bool
|
||||
docGenMutex sync.Mutex // Mutex to serialize access to doc generation
|
||||
}
|
||||
|
||||
func NewPreviewGenerator(concurrencyLimit int, ffmpegPath string, cacheDir string) *Service {
|
||||
@@ -50,7 +52,15 @@ func NewPreviewGenerator(concurrencyLimit int, ffmpegPath string, cacheDir strin
|
||||
// No-op cache if no cacheDir is specified
|
||||
fileCache = diskcache.NewNoOp()
|
||||
}
|
||||
|
||||
// Create directories recursively with 0755 permissions
|
||||
err := os.MkdirAll(filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "docs"), 0755)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
err = os.MkdirAll(filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "videos"), 0755)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
ffmpegMainPath, err := CheckValidFFmpeg(ffmpegPath)
|
||||
if err != nil && ffmpegPath != "" {
|
||||
logger.Fatalf("the configured ffmpeg path does not contain a valid ffmpeg binary %s, err: %v", ffmpegPath, err)
|
||||
@@ -63,7 +73,7 @@ func NewPreviewGenerator(concurrencyLimit int, ffmpegPath string, cacheDir strin
|
||||
logger.Infof("Media Enabled : %v", errprobe == nil)
|
||||
settings.Config.Integrations.Media.FfmpegPath = filepath.Base(ffmpegMainPath)
|
||||
}
|
||||
settings.Config.Server.PdfAvailable = docEnabled()
|
||||
settings.Config.Server.MuPdfAvailable = docEnabled()
|
||||
return &Service{
|
||||
sem: make(chan struct{}, concurrencyLimit),
|
||||
ffmpegPath: ffmpegMainPath,
|
||||
@@ -98,10 +108,14 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
|
||||
err error
|
||||
imageBytes []byte
|
||||
)
|
||||
|
||||
// Generate thumbnail image from video
|
||||
hasher := md5.New() //nolint:gosec
|
||||
_, _ = hasher.Write([]byte(CacheKey(file.RealPath, previewSize, file.ItemInfo.ModTime, seekPercentage)))
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
// Generate an image from office document
|
||||
if iteminfo.HasDocConvertableExtension(file.Name) {
|
||||
imageBytes, err = service.GenerateImageFromDoc(file.RealPath, 0) // 0 for the first page
|
||||
if iteminfo.HasDocConvertableExtension(file.Name, file.Type) {
|
||||
tempFilePath := filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "docs", hash) + ".txt"
|
||||
imageBytes, err = service.GenerateImageFromDoc(file, tempFilePath, 0) // 0 for the first page
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create image for PDF file: %w", err)
|
||||
}
|
||||
@@ -120,11 +134,7 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
|
||||
if seekPercentage == 0 {
|
||||
seekPercentage = 10
|
||||
}
|
||||
// Generate thumbnail image from video
|
||||
hasher := sha1.New() //nolint:gosec
|
||||
_, _ = hasher.Write([]byte(CacheKey(file.RealPath, previewSize, file.ItemInfo.ModTime, seekPercentage)))
|
||||
hash := hex.EncodeToString(hasher.Sum(nil))
|
||||
outPathPattern := filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "video", hash)
|
||||
outPathPattern := filepath.Join(settings.Config.Server.CacheDir, "thumbnails", "videos", hash) + ".jpg"
|
||||
defer os.Remove(outPathPattern) // cleanup
|
||||
imageBytes, err = service.GenerateVideoPreview(file.RealPath, outPathPattern, seekPercentage)
|
||||
if err != nil {
|
||||
@@ -133,16 +143,30 @@ func GeneratePreview(file iteminfo.ExtendedFileInfo, previewSize, officeUrl stri
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported media type: %s", ext)
|
||||
}
|
||||
resizedBytes, err := service.CreatePreview(imageBytes, previewSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resize preview image: %w", err)
|
||||
if len(imageBytes) < 100 {
|
||||
return nil, fmt.Errorf("generated image is too small, likely an error occurred: %d bytes", len(imageBytes))
|
||||
}
|
||||
// Cache and return
|
||||
cacheKey := CacheKey(file.RealPath, previewSize, file.ItemInfo.ModTime, seekPercentage)
|
||||
if err := service.fileCache.Store(context.Background(), cacheKey, resizedBytes); err != nil {
|
||||
logger.Errorf("failed to cache resized image: %v", err)
|
||||
|
||||
if previewSize != "original" {
|
||||
// resize image
|
||||
resizedBytes, err := service.CreatePreview(imageBytes, previewSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resize preview image: %w", err)
|
||||
}
|
||||
// Cache and return
|
||||
cacheKey := CacheKey(file.RealPath, previewSize, file.ItemInfo.ModTime, seekPercentage)
|
||||
if err := service.fileCache.Store(context.Background(), cacheKey, resizedBytes); err != nil {
|
||||
logger.Errorf("failed to cache resized image: %v", err)
|
||||
}
|
||||
return resizedBytes, nil
|
||||
} else {
|
||||
cacheKey := CacheKey(file.RealPath, previewSize, file.ItemInfo.ModTime, seekPercentage)
|
||||
if err := service.fileCache.Store(context.Background(), cacheKey, imageBytes); err != nil {
|
||||
logger.Errorf("failed to cache resized image: %v", err)
|
||||
}
|
||||
return imageBytes, nil
|
||||
}
|
||||
return resizedBytes, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Service) CreatePreview(data []byte, previewSize string) ([]byte, error) {
|
||||
@@ -154,7 +178,7 @@ func (s *Service) CreatePreview(data []byte, previewSize string) ([]byte, error)
|
||||
|
||||
switch previewSize {
|
||||
case "large":
|
||||
width, height = 512, 512
|
||||
width, height = 640, 640
|
||||
options = []Option{WithMode(ResizeModeFit), WithQuality(QualityHigh), WithFormat(FormatJpeg)}
|
||||
case "small":
|
||||
width, height = 256, 256
|
||||
@@ -191,7 +215,7 @@ func AvailablePreview(file iteminfo.ExtendedFileInfo) bool {
|
||||
if strings.HasPrefix(file.Type, "video") && service.ffmpegPath != "" {
|
||||
return true
|
||||
}
|
||||
if file.Type == "application/pdf" {
|
||||
if iteminfo.HasDocConvertableExtension(file.Name, file.Type) {
|
||||
return true
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(file.Name))
|
||||
|
||||
@@ -2222,6 +2222,10 @@ const docTemplate = `{
|
||||
"description": "when false, the date is relative, when true, the date is an exact timestamp",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableOfficePreviewExt": {
|
||||
"description": "comma separated list of file extensions to disable office preview for",
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnlyOfficeExt": {
|
||||
"description": "comma separated list of file extensions to disable onlyoffice preview for",
|
||||
"type": "string"
|
||||
@@ -2443,6 +2447,10 @@ const docTemplate = `{
|
||||
"description": "when false, the date is relative, when true, the date is an exact timestamp",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableOfficePreviewExt": {
|
||||
"description": "comma separated list of file extensions to disable office preview for",
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnlyOfficeExt": {
|
||||
"description": "comma separated list of file extensions to disable onlyoffice preview for",
|
||||
"type": "string"
|
||||
|
||||
@@ -2211,6 +2211,10 @@
|
||||
"description": "when false, the date is relative, when true, the date is an exact timestamp",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableOfficePreviewExt": {
|
||||
"description": "comma separated list of file extensions to disable office preview for",
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnlyOfficeExt": {
|
||||
"description": "comma separated list of file extensions to disable onlyoffice preview for",
|
||||
"type": "string"
|
||||
@@ -2432,6 +2436,10 @@
|
||||
"description": "when false, the date is relative, when true, the date is an exact timestamp",
|
||||
"type": "boolean"
|
||||
},
|
||||
"disableOfficePreviewExt": {
|
||||
"description": "comma separated list of file extensions to disable office preview for",
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnlyOfficeExt": {
|
||||
"description": "comma separated list of file extensions to disable onlyoffice preview for",
|
||||
"type": "string"
|
||||
|
||||
@@ -443,6 +443,10 @@ definitions:
|
||||
description: when false, the date is relative, when true, the date is an exact
|
||||
timestamp
|
||||
type: boolean
|
||||
disableOfficePreviewExt:
|
||||
description: comma separated list of file extensions to disable office preview
|
||||
for
|
||||
type: string
|
||||
disableOnlyOfficeExt:
|
||||
description: comma separated list of file extensions to disable onlyoffice
|
||||
preview for
|
||||
@@ -604,6 +608,10 @@ definitions:
|
||||
description: when false, the date is relative, when true, the date is an exact
|
||||
timestamp
|
||||
type: boolean
|
||||
disableOfficePreviewExt:
|
||||
description: comma separated list of file extensions to disable office preview
|
||||
for
|
||||
type: string
|
||||
disableOnlyOfficeExt:
|
||||
description: comma separated list of file extensions to disable onlyoffice
|
||||
preview for
|
||||
|
||||
@@ -27,9 +27,11 @@
|
||||
"clipboard": "^2.0.4",
|
||||
"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",
|
||||
"marked": "^15.0.6",
|
||||
"normalize.css": "^8.0.1",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
server:
|
||||
numImageProcessors: 14 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
|
||||
numImageProcessors: 8 # number of concurrent image processing jobs used to create previews, default is number of cpu cores available.
|
||||
socket: "" # socket to listen on
|
||||
tlsKey: "" # path to TLS key
|
||||
tlsCert: "" # path to TLS cert
|
||||
@@ -97,6 +97,7 @@ userDefaults:
|
||||
themeColor: var(--blue) # theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
|
||||
quickDownload: false # show icon to download in one click
|
||||
disableOnlyOfficeExt: .txt .csv .html .pdf # comma separated list of file extensions to disable onlyoffice preview for
|
||||
disableOfficePreviewExt: "" # comma separated list of file extensions to disable office preview for
|
||||
lockPassword: false # disable the user from changing their password
|
||||
disableSettings: false # disable the user from viewing the settings page
|
||||
preview:
|
||||
|
||||
@@ -229,7 +229,6 @@ export function getPreviewURL(source, path, modified) {
|
||||
try {
|
||||
const params = {
|
||||
path: encodeURIComponent(path),
|
||||
size: state.user.preview.highQuality ? 'large' : 'small',
|
||||
key: Date.parse(modified), // Use modified date as cache key
|
||||
source: source,
|
||||
inline: 'true'
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fetchURL, fetchJSON, adjustedData } from "./utils";
|
||||
import { state } from '@/store'
|
||||
import { notify } from "@/notify";
|
||||
import { getApiPath,removePrefix } from "@/utils/url.js";
|
||||
import { externalUrl,baseURL } from "@/utils/constants";
|
||||
@@ -54,7 +53,7 @@ export function getPreviewURL(hash, path) {
|
||||
try {
|
||||
const params = {
|
||||
path: encodeURIComponent(path),
|
||||
size: state.user.preview.highQuality ? 'large' : 'small',
|
||||
size: 'small',
|
||||
hash: hash,
|
||||
inline: 'true'
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@ export async function getAllUsers () {
|
||||
}
|
||||
|
||||
export async function generateOTP (username, password) {
|
||||
const params = { username, password }
|
||||
const params = { username }
|
||||
try {
|
||||
let apiPath = getApiPath('api/auth/otp/generate', params)
|
||||
const res = await fetch(apiPath, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'X-Password': password,
|
||||
}
|
||||
})
|
||||
return await res.json()
|
||||
@@ -31,13 +31,14 @@ export async function generateOTP (username, password) {
|
||||
}
|
||||
|
||||
export async function verifyOtp (username, password, otp) {
|
||||
const params = { username, password, code: otp }
|
||||
const params = { username }
|
||||
try {
|
||||
let apiPath = getApiPath('api/auth/otp/verify', params)
|
||||
const res = await fetch(apiPath, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'X-Password': password,
|
||||
'X-Secret': otp,
|
||||
}
|
||||
})
|
||||
if (res.status != 200) {
|
||||
@@ -50,12 +51,23 @@ export async function verifyOtp (username, password, otp) {
|
||||
}
|
||||
|
||||
export async function login(username, password, recaptcha, otp) {
|
||||
const params = { username, password, recaptcha, code: otp };
|
||||
if (!otp) {
|
||||
otp = ''
|
||||
}
|
||||
if (!recaptcha) {
|
||||
recaptcha = ''
|
||||
}
|
||||
if (!password) {
|
||||
password = ''
|
||||
}
|
||||
|
||||
const params = { username, recaptcha };
|
||||
let apiPath = getApiPath('api/auth/login', params);
|
||||
const res = await fetch(apiPath, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'X-Password': password,
|
||||
'X-Secret': otp,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -82,7 +82,9 @@ export default {
|
||||
currentView == "editor" ||
|
||||
currentView == "preview" ||
|
||||
currentView == "onlyOfficeEditor" ||
|
||||
currentView == "markdownViewer"
|
||||
currentView == "markdownViewer" ||
|
||||
currentView == "epubViewer" ||
|
||||
currentView == "docViewer"
|
||||
) {
|
||||
mutations.setMultiButtonState("close");
|
||||
} else {
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="isCardView" class="gallery-size">
|
||||
{{ $t("files.size") }}<span class="sr-only">:</span>
|
||||
<!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
{{ $t("files.size") }}<span class="sr-only">:</span> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<input
|
||||
v-model="gallerySize"
|
||||
type="range"
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
@action="toggleMultipleSelection"
|
||||
/>
|
||||
<action
|
||||
v-if="!showCreate && selectedCount > 0"
|
||||
v-if="(!showCreate && selectedCount > 0)"
|
||||
icon="file_download"
|
||||
:label="$t('buttons.download')"
|
||||
@action="startDownload"
|
||||
@@ -103,9 +103,10 @@
|
||||
class="button no-select"
|
||||
:class="{ 'dark-mode': isDarkMode }"
|
||||
>
|
||||
<action v-if="showDelete" icon="delete" :label="$t('buttons.delete')" show="delete" />
|
||||
<action v-if="showSave" icon="save" :label="$t('buttons.save')" @action="save()" />
|
||||
<action v-if="isPreview" icon="file_download" :label="$t('buttons.download')" @action="startDownload" />
|
||||
<action v-if="showEdit" icon="edit" :label="$t('buttons.edit')" @action="edit()" />
|
||||
<action v-if="showSave" icon="save" :label="$t('buttons.save')" @action="save()" />
|
||||
<action v-if="showDelete" icon="delete" :label="$t('buttons.delete')" show="delete" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -135,10 +136,20 @@ export default {
|
||||
return !this.showEdit && !this.showSave && !this.showDelete;
|
||||
},
|
||||
showEdit() {
|
||||
return window.location.hash != "#edit" && state.user.permissions.modify;
|
||||
return getters.currentView() == "markdownViewer" && state.user.permissions.modify;
|
||||
},
|
||||
showDelete() {
|
||||
return state.user.permissions.modify && getters.currentView() == "preview";
|
||||
return state.user.permissions.modify && this.isPreview;
|
||||
},
|
||||
isPreview() {
|
||||
const cv = getters.currentView();
|
||||
return (
|
||||
cv == "preview" ||
|
||||
cv == "onlyOfficeEditor" ||
|
||||
cv == "markdownViewer" ||
|
||||
cv == "epubViewer" ||
|
||||
cv == "docViewer"
|
||||
);
|
||||
},
|
||||
showSave() {
|
||||
return getters.currentView() == "editor" && state.user.permissions.modify;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<span v-if="isPreviewImg && imageState !== 'error'">
|
||||
<span
|
||||
@mouseenter="handleMouseEnter($event)"
|
||||
@mouseleave="handleMouseLeave($event)"
|
||||
v-if="isPreviewImg && imageState !== 'error'"
|
||||
>
|
||||
<i
|
||||
v-if="hasMotion"
|
||||
class="material-icons"
|
||||
@@ -7,10 +11,8 @@
|
||||
>animation</i
|
||||
>
|
||||
<img
|
||||
:key="thumbnailUrl"
|
||||
:key="imageTargetSrc"
|
||||
:src="imageDisplaySrc"
|
||||
@mouseenter="handleMouseEnter($event)"
|
||||
@mouseleave="handleMouseLeave($event)"
|
||||
class="icon"
|
||||
ref="thumbnail"
|
||||
/>
|
||||
@@ -21,7 +23,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { onlyOfficeUrl, mediaAvailable, pdfAvailable, baseURL } from "@/utils/constants";
|
||||
import {
|
||||
onlyOfficeUrl,
|
||||
mediaAvailable,
|
||||
muPdfAvailable,
|
||||
baseURL,
|
||||
} from "@/utils/constants";
|
||||
import { getTypeInfo } from "@/utils/mimetype";
|
||||
import { mutations, state } from "@/store";
|
||||
|
||||
@@ -56,10 +63,16 @@ export default {
|
||||
previewTimeouts: [],
|
||||
// UPDATED: Manage image state directly
|
||||
imageState: "loading", // Can be 'loading', 'loaded', or 'error'
|
||||
imageTargetSrc: this.thumbnailUrl, // The URL we currently want to display
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
imageTargetSrc() {
|
||||
if (this.showLargeIcon) {
|
||||
// Use the large version of the thumbnail if available
|
||||
return this.thumbnailUrl + "&size=large" || PLACEHOLDER_URL;
|
||||
}
|
||||
return this.thumbnailUrl || PLACEHOLDER_URL;
|
||||
},
|
||||
pdfConvertable() {
|
||||
const ext = "." + this.filename.split(".").pop().toLowerCase(); // Ensure lowercase and dot
|
||||
const pdfConvertCompatibleFileExtensions = {
|
||||
@@ -71,16 +84,17 @@ export default {
|
||||
".cbz": true,
|
||||
".svg": true,
|
||||
".txt": true,
|
||||
".doc": true,
|
||||
".docx": true,
|
||||
".ppt": true,
|
||||
".pptx": true,
|
||||
".xls": true,
|
||||
".xlsx": true,
|
||||
".hwp": true,
|
||||
".hwpx": true, // fix duplication and add this one
|
||||
".hwpx": true,
|
||||
".md": true,
|
||||
};
|
||||
return !!pdfConvertCompatibleFileExtensions[ext];
|
||||
const textType = this.mimetype.startsWith("text/");
|
||||
const disabled = state.user.disableOfficePreviewExt?.includes(ext);
|
||||
return (!!pdfConvertCompatibleFileExtensions[ext] || textType) && !disabled;
|
||||
},
|
||||
// NEW: A single computed property to determine the final image src
|
||||
imageDisplaySrc() {
|
||||
@@ -93,6 +107,9 @@ export default {
|
||||
}
|
||||
return this.imageTargetSrc;
|
||||
},
|
||||
showLargeIcon() {
|
||||
return state.user.viewMode === "gallery" && state.user.preview.highQuality;
|
||||
},
|
||||
showLarger() {
|
||||
return state.user.viewMode === "gallery" || state.user.viewMode === "normal";
|
||||
},
|
||||
@@ -114,7 +131,7 @@ export default {
|
||||
if (this.mimetype == "text/csv") {
|
||||
return false;
|
||||
}
|
||||
if (this.pdfConvertable && pdfAvailable) {
|
||||
if (this.pdfConvertable && muPdfAvailable) {
|
||||
return true;
|
||||
}
|
||||
if (this.getIconForType().simpleType === "image" && state.user.preview?.image) {
|
||||
@@ -165,18 +182,19 @@ export default {
|
||||
targetImage.src = url;
|
||||
},
|
||||
handleMouseEnter() {
|
||||
const imageUrl = this.thumbnailUrl + "&size=large";
|
||||
if (this.imageState == "loaded") {
|
||||
mutations.setPreviewSource(this.thumbnailUrl);
|
||||
mutations.setPreviewSource(imageUrl);
|
||||
}
|
||||
if (!state.user.preview.motionVideoPreview || !this.hasMotion) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sequence = [
|
||||
this.thumbnailUrl,
|
||||
this.thumbnailUrl + "&atPercentage=25",
|
||||
this.thumbnailUrl + "&atPercentage=50",
|
||||
this.thumbnailUrl + "&atPercentage=75",
|
||||
imageUrl,
|
||||
imageUrl + "&atPercentage=25",
|
||||
imageUrl + "&atPercentage=50",
|
||||
imageUrl + "&atPercentage=75",
|
||||
];
|
||||
let index = 0;
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="destContainsSrc"
|
||||
class="button button--flat"
|
||||
@click="copy"
|
||||
:aria-label="$t('buttons.copy')"
|
||||
@@ -52,6 +53,7 @@ import FileList from "./FileList.vue";
|
||||
import { filesApi } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { removeLastDir } from "@/utils/url";
|
||||
import { notify } from "@/notify";
|
||||
|
||||
export default {
|
||||
@@ -65,6 +67,14 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
destContainsSrc() {
|
||||
if (!this.dest) {
|
||||
return false; // If dest is not set, we can't check containment
|
||||
}
|
||||
const itemPath = this.items[0]?.from
|
||||
const parentDir = removeLastDir(itemPath) + "/"
|
||||
return this.dest == parentDir || this.dest.startsWith(itemPath);
|
||||
},
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div aria-label="filelist-path" class="searchContext">{{$t('search.path')}} {{ nav }}</div>
|
||||
<div>Source: {{ sourcePath.source }} </div> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
|
||||
<div aria-label="filelist-path" class="searchContext">{{$t('search.path')}} {{ sourcePath.path }}</div>
|
||||
<ul class="file-list">
|
||||
<li
|
||||
@click="itemClick"
|
||||
@@ -23,7 +24,7 @@
|
||||
<script>
|
||||
import { state, mutations } from "@/store";
|
||||
import url from "@/utils/url.js";
|
||||
import { removePrefix } from "@/utils/url.js";
|
||||
import { extractSourceFromPath } from "@/utils/url.js";
|
||||
import { filesApi } from "@/api";
|
||||
|
||||
export default {
|
||||
@@ -40,8 +41,8 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
nav() {
|
||||
return removePrefix(decodeURIComponent(this.current), "files");
|
||||
sourcePath() {
|
||||
return extractSourceFromPath(decodeURIComponent(this.current), "files");
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
{{ $t("buttons.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
:disabled="destContainsSrc"
|
||||
class="button button--flat"
|
||||
@click="move"
|
||||
:aria-label="$t('buttons.move')"
|
||||
@@ -52,6 +53,7 @@ import { filesApi } from "@/api";
|
||||
import buttons from "@/utils/buttons";
|
||||
import * as upload from "@/utils/upload";
|
||||
import { notify } from "@/notify";
|
||||
import { removeLastDir } from "@/utils/url";
|
||||
|
||||
export default {
|
||||
name: "move",
|
||||
@@ -64,6 +66,14 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
destContainsSrc() {
|
||||
if (!this.dest) {
|
||||
return false; // If dest is not set, we can't check containment
|
||||
}
|
||||
const itemPath = this.items[0]?.from
|
||||
const parentDir = removeLastDir(itemPath) + "/"
|
||||
return this.dest == parentDir || this.dest.startsWith(itemPath);
|
||||
},
|
||||
user() {
|
||||
return state.user;
|
||||
},
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getters } from "@/store"; // Import your custom store
|
||||
import { getters, mutations } from "@/store"; // Import your custom store
|
||||
|
||||
export default {
|
||||
name: "replace-rename",
|
||||
@@ -50,5 +50,10 @@ export default {
|
||||
return getters.currentPrompt(); // Access the getter directly from the store
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
closeHovers() {
|
||||
mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -4,8 +4,8 @@
|
||||
<h2>{{ $t("otp.name") }}</h2>
|
||||
</div>
|
||||
<div v-if="error !== ''" class="wrong-login">{{ error }}</div>
|
||||
|
||||
<div class="card-content">
|
||||
<div v-if="succeeded" >{{ $t("otp.verificationSucceed") }}</div>
|
||||
<div v-if="!succeeded" class="card-content">
|
||||
<p v-if="generate">{{ $t("otp.generate") }}</p>
|
||||
<div v-if="generate" class="share__box__element share__box__center">
|
||||
<p>{{ this.url }}</p>
|
||||
@@ -20,16 +20,18 @@
|
||||
:placeholder="$t('otp.codeInputPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="card-action">
|
||||
<button
|
||||
@click="closeHovers"
|
||||
class="button button--flat button--grey"
|
||||
:aria-label="$t('buttons.cancel')"
|
||||
:title="$t('buttons.cancel')"
|
||||
:aria-label="succeeded ? $t('buttons.close') : $t('buttons.cancel')"
|
||||
:title="succeeded ? $t('buttons.close') : $t('buttons.cancel')"
|
||||
>
|
||||
{{ $t("buttons.cancel") }}
|
||||
{{ succeeded ? $t('buttons.close') : $t('buttons.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="!succeeded"
|
||||
class="button button--flat button--blue"
|
||||
@click="verifyCode"
|
||||
:title="$t('buttons.verify')"
|
||||
@@ -57,6 +59,7 @@ export default {
|
||||
error: "",
|
||||
code: "",
|
||||
url: "",
|
||||
succeeded: false,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
@@ -104,18 +107,21 @@ export default {
|
||||
if (this.redirect != "") {
|
||||
await usersApi.login(this.username, this.password, this.redirect, this.code);
|
||||
await initAuth();
|
||||
if (this.redirect) {
|
||||
this.$router.push(this.redirect);
|
||||
}
|
||||
} else {
|
||||
mutations.closeHovers();
|
||||
this.$router.push(this.redirect);
|
||||
}
|
||||
mutations.updateCurrentUser({ otpEnabled: true });
|
||||
this.succeeded = true
|
||||
this.error = "";
|
||||
notify.showSuccess(this.$t("otp.verificationSucceed"));
|
||||
} catch (error) {
|
||||
this.error = this.$t("otp.verificationFailed");
|
||||
return;
|
||||
}
|
||||
},
|
||||
closeHovers() {
|
||||
if (!this.succeeded) {
|
||||
mutations.updateCurrentUser({ otpEnabled: false });
|
||||
}
|
||||
mutations.closeHovers();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -233,6 +233,10 @@ export default {
|
||||
this.user.scopes = newVal ? { default: "" } : this.originalUserScope;
|
||||
this.emitUserUpdate();
|
||||
},
|
||||
stateUser() {
|
||||
this.user.otpEnabled = state.user.otpEnabled;
|
||||
this.emitUserUpdate();
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
invalidPassword() {
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "أدخل كلمة المرور مرة أخرى",
|
||||
"passwordsDoNotMatch": "كلمات المرور غير متطابقة",
|
||||
"loginMethod": "طريقة تسجيل الدخول",
|
||||
"shareSettings": "مشاركة الإعدادات"
|
||||
"shareSettings": "مشاركة الإعدادات",
|
||||
"disableOfficeEditorDescription": "قائمة مفصولة بمسافات لامتدادات الملفات لتعطيل عارض OnlyOffice لها. (على سبيل المثال، \".txt .html\").",
|
||||
"disableOfficeEditor": "تعطيل عارض المكتب لامتدادات ملفات معينة",
|
||||
"disableOfficePreviews": "تعطيل معاينات المكتب لامتدادات ملفات معينة",
|
||||
"disableOfficePreviewsDescription": "قائمة مفصولة بمسافة من امتدادات الملفات لتعطيل المعاينات المكتبية لها. (على سبيل المثال، \".txt .html\")."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "مساعدة",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "معاينة عالية الجودة",
|
||||
"previewOffice": "معاينة الملفات المكتبية",
|
||||
"popupPreview": "تمكين المعاينة المنبثقة",
|
||||
"filePreviewOptions": "خيارات معاينة الملف"
|
||||
"filePreviewOptions": "خيارات معاينة الملف",
|
||||
"editOffice": "تحرير/عرض ملفات المكتب"
|
||||
},
|
||||
"otp": {
|
||||
"name": "المصادقة الثنائية",
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "Geben Sie das Passwort erneut ein",
|
||||
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
|
||||
"loginMethod": "Login-Methode",
|
||||
"shareSettings": "Einstellungen teilen"
|
||||
"shareSettings": "Einstellungen teilen",
|
||||
"disableOfficeEditorDescription": "Eine durch Leerzeichen getrennte Liste von Dateierweiterungen, für die der OnlyOffice-Viewer deaktiviert werden soll. (z. B. '.txt .html').",
|
||||
"disableOfficeEditor": "Office Viewer für bestimmte Dateierweiterungen deaktivieren",
|
||||
"disableOfficePreviews": "Office-Vorschauen für bestimmte Dateierweiterungen deaktivieren",
|
||||
"disableOfficePreviewsDescription": "Eine durch Leerzeichen getrennte Liste von Dateierweiterungen, für die die Office-Vorschau deaktiviert werden soll. (z. B. '.txt .html')."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Hilfe",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "Hochwertige Vorschau",
|
||||
"previewOffice": "Vorschau von Office-Dateien",
|
||||
"popupPreview": "Pop-up-Vorschau einschalten",
|
||||
"filePreviewOptions": "Optionen für die Dateivorschau"
|
||||
"filePreviewOptions": "Optionen für die Dateivorschau",
|
||||
"editOffice": "Office-Dateien bearbeiten/anzeigen"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Zwei-Faktoren-Authentifizierung",
|
||||
|
||||
@@ -287,7 +287,11 @@
|
||||
"enterPasswordAgain": "Πληκτρολογήστε ξανά τον κωδικό πρόσβασης",
|
||||
"passwordsDoNotMatch": "Οι κωδικοί πρόσβασης δεν ταιριάζουν",
|
||||
"loginMethod": "Μέθοδος σύνδεσης",
|
||||
"shareSettings": "Ρυθμίσεις κοινής χρήσης"
|
||||
"shareSettings": "Ρυθμίσεις κοινής χρήσης",
|
||||
"disableOfficeEditorDescription": "Ένας κατάλογος επεκτάσεων αρχείων με διαχωρισμό διαστήματος για την απενεργοποίηση της προβολής OnlyOffice. (π.χ. '.txt .html').",
|
||||
"disableOfficeEditor": "Απενεργοποίηση της προβολής γραφείου για ορισμένες επεκτάσεις αρχείων",
|
||||
"disableOfficePreviews": "Απενεργοποίηση προεπισκοπήσεων γραφείου για ορισμένες επεκτάσεις αρχείων",
|
||||
"disableOfficePreviewsDescription": "Μια λίστα επεκτάσεων αρχείων με διαχωρισμό διαστήματος για την απενεργοποίηση των προεπισκοπήσεων του Office. (π.χ., '.txt .html')."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Βοήθεια",
|
||||
@@ -366,7 +370,8 @@
|
||||
"highQualityPreview": "Προεπισκόπηση υψηλής ποιότητας",
|
||||
"previewOffice": "Προεπισκόπηση αρχείων γραφείου",
|
||||
"popupPreview": "Ενεργοποίηση αναδυόμενης προεπισκόπησης",
|
||||
"filePreviewOptions": "Επιλογές προεπισκόπησης αρχείων"
|
||||
"filePreviewOptions": "Επιλογές προεπισκόπησης αρχείων",
|
||||
"editOffice": "Επεξεργασία/προβολή αρχείων γραφείου"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Αυθεντικοποίηση δύο παραγόντων",
|
||||
|
||||
@@ -194,8 +194,8 @@
|
||||
"previewImages": "Preview images",
|
||||
"previewVideos": "Preview videos",
|
||||
"previewMotionVideos": "Motion preview for videos",
|
||||
"highQualityPreview": "High quality preview",
|
||||
"previewOffice": "Preview office files",
|
||||
"highQualityPreview": "High quality preview for gallery images",
|
||||
"editOffice": "Edit/view office files",
|
||||
"popupPreview": "Enable pop-up preview",
|
||||
"filePreviewOptions": "File preview options"
|
||||
},
|
||||
@@ -208,8 +208,10 @@
|
||||
"modifyOtherUser": "Modify user:",
|
||||
"modifyCurrentUser": "Modify current user",
|
||||
"themeColor": "Theme Color",
|
||||
"disableOfficePreviewDescription": "A space-separated list of file extensions to disable the OnlyOffice viewer for. (e.g., '.txt .html').",
|
||||
"disableOfficePreview": "Disable onlyoffice viewer for certain file extensions",
|
||||
"disableOfficeEditorDescription": "A space-separated list of file extensions to disable the OnlyOffice viewer for. (e.g., '.txt .html').",
|
||||
"disableOfficeEditor": "Disable office viewer for certain file extensions",
|
||||
"disableOfficePreviews": "Disable office previews for certain file extensions",
|
||||
"disableOfficePreviewsDescription": "A space-separated list of file extensions to disable office previews for. (e.g., '.txt .html').",
|
||||
"listingOptions": "Listing options",
|
||||
"emptyGlobal": "Currently there are no global config options, You can edit the configuration yaml to adjust system settings.",
|
||||
"scopes": "Define allowed sources and the user scope within them",
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "Vuelva a introducir la contraseña",
|
||||
"passwordsDoNotMatch": "Las contraseñas no coinciden",
|
||||
"loginMethod": "Método de inicio de sesión",
|
||||
"shareSettings": "Compartir configuración"
|
||||
"shareSettings": "Compartir configuración",
|
||||
"disableOfficeEditorDescription": "Una lista separada por espacios de las extensiones de archivo para las que desea desactivar el visor de OnlyOffice. (por ejemplo, '.txt .html').",
|
||||
"disableOfficeEditor": "Desactivar el visor de office para determinadas extensiones de archivo",
|
||||
"disableOfficePreviews": "Desactivar la vista previa de office para determinadas extensiones de archivo",
|
||||
"disableOfficePreviewsDescription": "Una lista separada por espacios de extensiones de archivo para las que deshabilitar las vistas previas de office. (por ejemplo, '.txt .html')."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Ayuda",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "Vista previa de alta calidad",
|
||||
"previewOffice": "Vista previa de archivos ofimáticos",
|
||||
"popupPreview": "Activar la vista previa emergente",
|
||||
"filePreviewOptions": "Opciones de previsualización de archivos"
|
||||
"filePreviewOptions": "Opciones de previsualización de archivos",
|
||||
"editOffice": "Editar/ver archivos ofimáticos"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Autenticación de dos factores",
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "Saisir à nouveau le mot de passe",
|
||||
"passwordsDoNotMatch": "Les mots de passe ne correspondent pas",
|
||||
"loginMethod": "Méthode de connexion",
|
||||
"shareSettings": "Paramètres des partages"
|
||||
"shareSettings": "Paramètres des partages",
|
||||
"disableOfficeEditorDescription": "Une liste d'extensions de fichiers séparées par des espaces pour lesquelles le visualisateur OnlyOffice doit être désactivé. (par exemple, '.txt .html').",
|
||||
"disableOfficeEditor": "Désactiver la visionneuse d'office pour certaines extensions de fichiers",
|
||||
"disableOfficePreviews": "Désactiver les aperçus Office pour certaines extensions de fichiers",
|
||||
"disableOfficePreviewsDescription": "Une liste d'extensions de fichiers séparées par des espaces pour lesquelles les aperçus Office doivent être désactivés. (par exemple, '.txt .html')."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Aide",
|
||||
@@ -365,6 +369,7 @@
|
||||
"highQualityPreview": "Aperçu de haute qualité",
|
||||
"previewOffice": "Prévisualisation des fichiers bureautique",
|
||||
"popupPreview": "Activer la prévisualisation dans les fenêtres contextuelles",
|
||||
"editOffice": "Editer/visualiser des fichiers bureautiques",
|
||||
"filePreviewOptions": "Options de prévisualisation des fichiers"
|
||||
},
|
||||
"otp": {
|
||||
|
||||
+119
-11
@@ -35,7 +35,12 @@
|
||||
"toggleSidebar": "פתיחת / סגירת סרגל צד",
|
||||
"update": "עדכון",
|
||||
"upload": "העלאה",
|
||||
"openFile": "פתח קובץ"
|
||||
"openFile": "פתח קובץ",
|
||||
"generateNewOtp": "אפס וצור קוד דו-גורמי חדש",
|
||||
"verify": "אמת",
|
||||
"copyDownloadLinkToClipboard": "העתק את קישור ההורדה ללוח",
|
||||
"continue": "המשך",
|
||||
"edit": "ערוך"
|
||||
},
|
||||
"download": {
|
||||
"downloadFile": "הורד קובץ",
|
||||
@@ -116,7 +121,8 @@
|
||||
"submit": "התחברות",
|
||||
"username": "שם משתמש",
|
||||
"usernameTaken": "שם משתמש כבר קיים",
|
||||
"wrongCredentials": "פרטי התחברות שגויים"
|
||||
"wrongCredentials": "פרטי התחברות שגויים",
|
||||
"or": "או"
|
||||
},
|
||||
"permanent": "קבוע",
|
||||
"prompts": {
|
||||
@@ -153,7 +159,11 @@
|
||||
"upload": "העלאה",
|
||||
"uploadFiles": "מעלה {files} קבצים...",
|
||||
"uploadMessage": "בחר אפשרות העלאה.",
|
||||
"optionalPassword": "סיסמא אופציונלית"
|
||||
"optionalPassword": "סיסמא אופציונלית",
|
||||
"deleteUserMessage": "האם אתה בטוח שברצונך למחוק משתמש זה?",
|
||||
"selected": "נבחר",
|
||||
"filesSelected": "קבצים שנבחרו",
|
||||
"typeName": "סוג:"
|
||||
},
|
||||
"search": {
|
||||
"images": "תמונות",
|
||||
@@ -163,7 +173,12 @@
|
||||
"search": "חפש...",
|
||||
"typeToSearch": "הקלד לחיפוש...",
|
||||
"types": "סוג",
|
||||
"video": "וידאו"
|
||||
"video": "וידאו",
|
||||
"path": "נתיב:",
|
||||
"smallerThan": "קטן מ:",
|
||||
"largerThan": "גדול מ:",
|
||||
"helpText1": "החיפוש מתבצע על כל תו שאתה מקליד (מינימום 3 תווים עבור מונחי חיפוש).",
|
||||
"helpText2": "האינדקס: החיפוש משתמש באינדקס שמתעדכן אוטומטית במרווח הזמן שהוגדר (ברירת מחדל: 5 דקות). חיפוש בעת הפעלת התוכנית עלול להניב תוצאות חלקיות."
|
||||
},
|
||||
"settings": {
|
||||
"admin": "מנהל",
|
||||
@@ -212,7 +227,10 @@
|
||||
"execute": "ביצוע פקודות",
|
||||
"modify": "עריכת קבצים קבצים",
|
||||
"rename": "שינוי שם או העברת קבצים ותקיות",
|
||||
"share": "שיתוף קבצים"
|
||||
"share": "שיתוף קבצים",
|
||||
"admin": "מנהל",
|
||||
"api": "יצירת מפתחות API ארוכי טווח וניהולם",
|
||||
"realtime": "אפשר חיבורים ועדכונים בזמן אמת"
|
||||
},
|
||||
"permissions-name": "הרשאות",
|
||||
"permissionsHelp": "אתה יכול להגדיר את המשתמש להיות מנהל מערכת או לבחור את ההרשאות בנפרד. אם תבחר \"מנהל מערכת\", כל ההרשאות ייבחרו אוטומטית. ניהול המשתמשים נשאר הרשאה של מנהל מערכת.\n",
|
||||
@@ -242,7 +260,31 @@
|
||||
"userManagement": "ניהול משתמש",
|
||||
"userUpdated": "המשתמש עודכן!",
|
||||
"username": "שם משתמש",
|
||||
"users": "משתמשים"
|
||||
"users": "משתמשים",
|
||||
"enterPassword": "הזן סיסמה",
|
||||
"enterPasswordAgain": "הזן את הסיסמה שוב",
|
||||
"passwordsDoNotMatch": "הסיסמאות אינן תואמות",
|
||||
"loginMethod": "שיטת כניסה",
|
||||
"loginMethodDescription": "שיטת כניסה מותרת למשתמש.",
|
||||
"modifyOtherUser": "שינוי משתמש:",
|
||||
"modifyCurrentUser": "שינוי המשתמש הנוכחי",
|
||||
"themeColor": "צבע נושא",
|
||||
"disableOfficeEditorDescription": "רשימה מופרדת ברווחים של סיומות קבצים שיש להשבית את הצופה OnlyOffice עבורן. (לדוגמה, '.txt .html').",
|
||||
"disableOfficeEditor": "השבת תצוגת Office עבור סיומות קבצים מסוימות",
|
||||
"disableOfficePreviews": "השבת תצוגה מקדימה של Office עבור סיומות קבצים מסוימות",
|
||||
"disableOfficePreviewsDescription": "רשימה מופרדת ברווחים של סיומות קבצים שצפייה מקדימה שלהם במשרד תושבת. (לדוגמה, '.txt .html').",
|
||||
"listingOptions": "אפשרויות רישום",
|
||||
"emptyGlobal": "כרגע אין אפשרויות תצורה גלובליות. ניתן לערוך את קובץ ה-yaml כדי לשנות את הגדרות המערכת.",
|
||||
"scopes": "הגדר מקורות מותרים והיקף המשתמשים בתוכם",
|
||||
"UserManagement": "ניהול משתמשים",
|
||||
"userNotAdmin": "המשתמש אינו מנהל",
|
||||
"tusUploads": "העלאות מקובצות",
|
||||
"tusUploadsHelp": "דפדפן הקבצים תומך בהעלאת קבצים מקוטעים, מה שמאפשר יצירת העלאות קבצים יעילות, אמינות, ניתנות לחידוש ולקטעים, אפילו ברשתות לא אמינות.",
|
||||
"tusUploadsChunkSize": "ציון הגודל המרבי של בקשה (העלאות קטנות יותר ייעשה באמצעות העלאה ישירה). ניתן להזין מספר שלם המציין את גודל הקובץ בבytes או מחרוזת כגון 10MB, 1GB וכו'.",
|
||||
"tusUploadsRetryCount": "מספר הניסיונות שיש לבצע אם העלאת קטע נכשלה.",
|
||||
"disableUsedDiskPercentage": "השבת גרף אחוזי השימוש בדיסק",
|
||||
"shareSettings": "הגדרות שיתוף",
|
||||
"api": "מפתחות API"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "עזרה",
|
||||
@@ -255,7 +297,8 @@
|
||||
"preview": "תצוגה מקדימה",
|
||||
"settings": "הגדרות",
|
||||
"signup": "הרשמה",
|
||||
"siteSettings": "הגדרות"
|
||||
"siteSettings": "הגדרות",
|
||||
"sources": "מקורות"
|
||||
},
|
||||
"success": {
|
||||
"linkCopied": "הקישור הועתק!"
|
||||
@@ -267,8 +310,73 @@
|
||||
"seconds": "שניות",
|
||||
"unit": "יחידת זמן"
|
||||
},
|
||||
"upload": {},
|
||||
"share": {},
|
||||
"api": {},
|
||||
"index": {}
|
||||
"upload": {
|
||||
"abortUpload": "האם אתה בטוח שברצונך לבטל?"
|
||||
},
|
||||
"share": {
|
||||
"notice": "הערה: כל מי שיש לו גישה לקישור (ולסיסמה האופציונלית) יכול לגשת לקבצים המשותפים. אין צורך להיות מחובר."
|
||||
},
|
||||
"api": {
|
||||
"title": "מפתחות API",
|
||||
"keyName": "שם מפתח API:",
|
||||
"createdAt": "נוצר ב:",
|
||||
"expiresAt": "תוקף ב:",
|
||||
"permissions": "הרשאות:",
|
||||
"createTitle": "צור מפתח API",
|
||||
"keyNamePlaceholder": "הזן שם מפתח API ייחודי:",
|
||||
"tokenDuration": "משך תוקף האסימון:",
|
||||
"durationNumberPlaceholder": "מספר",
|
||||
"days": "ימים",
|
||||
"months": "חודשים",
|
||||
"permissionNote": "בחר לפחות הרשאה אחת עבור המפתח. גם למשתמש שלך חייבת להיות הרשאה זו.",
|
||||
"createKeyFailed": "לא ניתן ליצור מפתח API.",
|
||||
"createKeySuccess": "מפתח API נוצר בהצלחה.",
|
||||
"description": "מפתחות API מבוססים על המשתמש שיצר אותם. ראה",
|
||||
"swaggerLinkText": "דף סוואגר",
|
||||
"name": "שם",
|
||||
"created": "נוצר",
|
||||
"expires": "תוקף",
|
||||
"actions": "פעולות",
|
||||
"enabled": "מופעל",
|
||||
"disabled": "נכה"
|
||||
},
|
||||
"index": {
|
||||
"status": "סטטוס",
|
||||
"assessment": "הערכה",
|
||||
"files": "קבצים",
|
||||
"folders": "תיקיות",
|
||||
"lastScanned": "נסרק לאחרונה",
|
||||
"quickScan": "סריקה מהירה בשניות",
|
||||
"fullScan": "שניות סריקה מלאה",
|
||||
"settingsHover": "הגדרות למשתמש",
|
||||
"logout": "התנתק",
|
||||
"toggleClick": "החלף לחיצה אחת",
|
||||
"toggleDark": "הפעל מצב כהה",
|
||||
"toggleSticky": "הפעל/כבה מצב דביק",
|
||||
"of": "של",
|
||||
"used": "משומש"
|
||||
},
|
||||
"profileSettings": {
|
||||
"setDateFormat": "הגדר פורמט תאריך מדויק",
|
||||
"showHiddenFiles": "הצג קבצים מוסתרים",
|
||||
"showQuickDownload": "הצג סמל הורדה לגישה מהירה",
|
||||
"previewImages": "תצוגה מקדימה של תמונות",
|
||||
"previewVideos": "הצגת סרטונים",
|
||||
"previewMotionVideos": "תצוגה מקדימה של סרטונים",
|
||||
"highQualityPreview": "תצוגה מקדימה באיכות גבוהה",
|
||||
"editOffice": "ערוך/הצג קבצי Office",
|
||||
"popupPreview": "הפעל תצוגה מקדימה מוקפצת",
|
||||
"filePreviewOptions": "אפשרויות תצוגה מקדימה של קבצים"
|
||||
},
|
||||
"otp": {
|
||||
"name": "אימות דו-גורמי",
|
||||
"verifyInstructions": "הזן את הקוד מהאפליקציה המאמתת שלך",
|
||||
"generate": "סרוק את קוד ה-QR באמצעות אפליקציית האימות שלך",
|
||||
"codeInputPlaceholder": "קוד בן 6 ספרות",
|
||||
"invalidCodeType": "קוד האימות צריך להיות בן 6 ספרות באותיות לטיניות",
|
||||
"enabledSuccessfully": "OTP הופעל בהצלחה",
|
||||
"verificationFailed": "האימות נכשל",
|
||||
"verificationSucceed": "האימות הצליח",
|
||||
"generationFailed": "דור שנכשל"
|
||||
}
|
||||
}
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "Adja meg újra a jelszót",
|
||||
"passwordsDoNotMatch": "A jelszavak nem egyeznek",
|
||||
"loginMethod": "Bejelentkezési módszer",
|
||||
"shareSettings": "Beállítások megosztása"
|
||||
"shareSettings": "Beállítások megosztása",
|
||||
"disableOfficeEditorDescription": "Azon fájlkiterjesztések szóközzel elválasztott listája, amelyek esetében az OnlyOffice megjelenítőjét le kell tiltani. (pl. '.txt .html').",
|
||||
"disableOfficeEditor": "Bizonyos fájlkiterjesztéseknél letilthatja az Office Viewer-t",
|
||||
"disableOfficePreviews": "Office előnézet letiltása bizonyos fájlkiterjesztésekhez",
|
||||
"disableOfficePreviewsDescription": "Az irodai előnézetek letiltásának szóközzel elválasztott fájlkiterjesztések listája. (pl. '.txt .html')."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Súgó",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "Kiváló minőségű előnézet",
|
||||
"previewOffice": "Office fájlok előnézete",
|
||||
"popupPreview": "Pop-up előnézet engedélyezése",
|
||||
"filePreviewOptions": "Fájl előnézeti beállítások"
|
||||
"filePreviewOptions": "Fájl előnézeti beállítások",
|
||||
"editOffice": "Office fájlok szerkesztése/megjelenítése"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Kétfaktoros hitelesítés",
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "Immettere nuovamente la password",
|
||||
"passwordsDoNotMatch": "Le password non corrispondono",
|
||||
"loginMethod": "Metodo di accesso",
|
||||
"shareSettings": "Condividi le impostazioni"
|
||||
"shareSettings": "Condividi le impostazioni",
|
||||
"disableOfficeEditorDescription": "Un elenco separato da spazi di estensioni di file per cui disabilitare il visualizzatore di OnlyOffice. (ad esempio, '.txt .html').",
|
||||
"disableOfficeEditor": "Disattivare il visualizzatore di Office per alcune estensioni di file",
|
||||
"disableOfficePreviews": "Disattivare le anteprime di Office per alcune estensioni di file",
|
||||
"disableOfficePreviewsDescription": "Un elenco separato da spazi di estensioni di file per cui disabilitare le anteprime di Office. (ad esempio, '.txt .html')."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Aiuto",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "Anteprima di alta qualità",
|
||||
"previewOffice": "Anteprima dei file office",
|
||||
"popupPreview": "Abilita l'anteprima a comparsa",
|
||||
"filePreviewOptions": "Opzioni di anteprima dei file"
|
||||
"filePreviewOptions": "Opzioni di anteprima dei file",
|
||||
"editOffice": "Modificare/visualizzare i file dell'ufficio"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Autenticazione a due fattori",
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "もう一度パスワードを入力する",
|
||||
"passwordsDoNotMatch": "パスワードが一致しない",
|
||||
"loginMethod": "ログイン方法",
|
||||
"shareSettings": "共有設定"
|
||||
"shareSettings": "共有設定",
|
||||
"disableOfficeEditorDescription": "OnlyOffice ビューアを無効にするファイル拡張子をスペース区切りで指定します。(例: '.txt .html')。",
|
||||
"disableOfficeEditor": "特定のファイル拡張子のオフィスビューアを無効にする",
|
||||
"disableOfficePreviews": "特定のファイル拡張子でオフィスのプレビューを無効にする",
|
||||
"disableOfficePreviewsDescription": "オフィスプレビューを無効にするファイル拡張子をスペースで区切ったリスト。(例: '.txt .html')。"
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "ヘルプ",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "高画質プレビュー",
|
||||
"previewOffice": "オフィスファイルのプレビュー",
|
||||
"popupPreview": "ポップアップ・プレビューを有効にする",
|
||||
"filePreviewOptions": "ファイルプレビューオプション"
|
||||
"filePreviewOptions": "ファイルプレビューオプション",
|
||||
"editOffice": "オフィスファイルの編集/閲覧"
|
||||
},
|
||||
"otp": {
|
||||
"name": "二要素認証",
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "비밀번호를 다시 입력합니다.",
|
||||
"passwordsDoNotMatch": "비밀번호가 일치하지 않습니다.",
|
||||
"loginMethod": "로그인 방법",
|
||||
"shareSettings": "공유 설정"
|
||||
"shareSettings": "공유 설정",
|
||||
"disableOfficeEditorDescription": "OnlyOffice 뷰어를 비활성화할 파일 확장명을 공백으로 구분한 목록입니다. (예: '.txt .html').",
|
||||
"disableOfficeEditor": "특정 파일 확장자에 대해 Office 뷰어 사용 안 함",
|
||||
"disableOfficePreviews": "특정 파일 확장명에 대한 Office 미리 보기 사용 안 함",
|
||||
"disableOfficePreviewsDescription": "Office 미리 보기를 사용하지 않도록 설정할 파일 확장명을 공백으로 구분한 목록입니다. (예: '.txt .html')."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "도움말",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "고품질 미리보기",
|
||||
"previewOffice": "오피스 파일 미리 보기",
|
||||
"popupPreview": "팝업 미리 보기 사용",
|
||||
"filePreviewOptions": "파일 미리 보기 옵션"
|
||||
"filePreviewOptions": "파일 미리 보기 옵션",
|
||||
"editOffice": "오피스 파일 편집/보기"
|
||||
},
|
||||
"otp": {
|
||||
"name": "2단계 인증",
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "Wprowadź hasło ponownie",
|
||||
"passwordsDoNotMatch": "Hasła nie są zgodne",
|
||||
"loginMethod": "Metoda logowania",
|
||||
"shareSettings": "Ustawienia udostępniania"
|
||||
"shareSettings": "Ustawienia udostępniania",
|
||||
"disableOfficeEditorDescription": "Rozdzielana spacjami lista rozszerzeń plików, dla których przeglądarka OnlyOffice ma zostać wyłączona. (np. \".txt .html\").",
|
||||
"disableOfficeEditor": "Wyłącz przeglądarkę Office dla niektórych rozszerzeń plików",
|
||||
"disableOfficePreviews": "Wyłącz podglądy Office dla niektórych rozszerzeń plików",
|
||||
"disableOfficePreviewsDescription": "Rozdzielana spacjami lista rozszerzeń plików do wyłączenia podglądu plików biurowych. (np. \".txt .html\")."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Pomoc",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "Podgląd wysokiej jakości",
|
||||
"previewOffice": "Podgląd plików biurowych",
|
||||
"popupPreview": "Włącz wyskakujący podgląd",
|
||||
"filePreviewOptions": "Opcje podglądu plików"
|
||||
"filePreviewOptions": "Opcje podglądu plików",
|
||||
"editOffice": "Edycja/przeglądanie plików biurowych"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Uwierzytelnianie dwuskładnikowe",
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "Digite a senha novamente",
|
||||
"passwordsDoNotMatch": "As senhas não correspondem",
|
||||
"loginMethod": "Método de login",
|
||||
"shareSettings": "Compartilhar configurações"
|
||||
"shareSettings": "Compartilhar configurações",
|
||||
"disableOfficeEditorDescription": "Uma lista de extensões de arquivo separada por espaços para desativar o visualizador do OnlyOffice. (por exemplo, '.txt .html').",
|
||||
"disableOfficeEditor": "Desativar o visualizador do Office para determinadas extensões de arquivo",
|
||||
"disableOfficePreviews": "Desativar as visualizações do Office para determinadas extensões de arquivo",
|
||||
"disableOfficePreviewsDescription": "Uma lista de extensões de arquivo separada por espaços para desativar as visualizações do Office. (por exemplo, '.txt .html')."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Ajuda",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "Visualização de alta qualidade",
|
||||
"previewOffice": "Visualizar arquivos do Office",
|
||||
"popupPreview": "Ativar visualização pop-up",
|
||||
"filePreviewOptions": "Opções de visualização de arquivos"
|
||||
"filePreviewOptions": "Opções de visualização de arquivos",
|
||||
"editOffice": "Editar/visualizar arquivos de escritório"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Autenticação de dois fatores",
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "Introduceți din nou parola",
|
||||
"passwordsDoNotMatch": "Parolele nu se potrivesc",
|
||||
"loginMethod": "Metoda de conectare",
|
||||
"shareSettings": "Setări de partajare"
|
||||
"shareSettings": "Setări de partajare",
|
||||
"disableOfficeEditorDescription": "O listă de extensii de fișiere separate prin spațiu pentru care se dezactivează vizualizatorul OnlyOffice. (de exemplu, \".txt .html\").",
|
||||
"disableOfficeEditor": "Dezactivați vizualizatorul office pentru anumite extensii de fișiere",
|
||||
"disableOfficePreviews": "Dezactivați previzualizările office pentru anumite extensii de fișiere",
|
||||
"disableOfficePreviewsDescription": "O listă de extensii de fișiere separate prin spațiu pentru care se dezactivează previzualizările office. (de exemplu, \".txt .html\")."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Ajutor",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "Previzualizare de înaltă calitate",
|
||||
"previewOffice": "Previzualizare fișiere office",
|
||||
"popupPreview": "Activați previzualizarea pop-up",
|
||||
"filePreviewOptions": "Opțiuni de previzualizare a fișierelor"
|
||||
"filePreviewOptions": "Opțiuni de previzualizare a fișierelor",
|
||||
"editOffice": "Editarea/vizualizarea fișierelor office"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Autentificare cu doi factori",
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "Введите пароль еще раз",
|
||||
"passwordsDoNotMatch": "Пароли не совпадают",
|
||||
"loginMethod": "Метод входа в систему",
|
||||
"shareSettings": "Поделиться настройками"
|
||||
"shareSettings": "Поделиться настройками",
|
||||
"disableOfficeEditorDescription": "Список расширений файлов, для которых необходимо отключить программу просмотра OnlyOffice, разделенный пробелами. (например, '.txt .html').",
|
||||
"disableOfficeEditor": "Отключение просмотра офисных файлов для определенных расширений файлов",
|
||||
"disableOfficePreviews": "Отключение предварительного просмотра в офисе для определенных расширений файлов",
|
||||
"disableOfficePreviewsDescription": "Список расширений файлов, для которых необходимо отключить предварительный просмотр в офисе, разделенный пробелами. (например, '.txt .html')."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Помощь",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "Высококачественный предварительный просмотр",
|
||||
"previewOffice": "Предварительный просмотр офисных файлов",
|
||||
"popupPreview": "Включить предварительный просмотр всплывающих окон",
|
||||
"filePreviewOptions": "Параметры предварительного просмотра файлов"
|
||||
"filePreviewOptions": "Параметры предварительного просмотра файлов",
|
||||
"editOffice": "Редактирование/просмотр офисных файлов"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Двухфакторная аутентификация",
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "Znovu zadajte heslo",
|
||||
"passwordsDoNotMatch": "Heslá sa nezhodujú",
|
||||
"loginMethod": "Spôsob prihlásenia",
|
||||
"shareSettings": "Zdieľať nastavenia"
|
||||
"shareSettings": "Zdieľať nastavenia",
|
||||
"disableOfficeEditorDescription": "Medzerou oddelený zoznam prípon súborov, pre ktoré sa má prehliadač OnlyOffice vypnúť. (napr. \".txt .html\").",
|
||||
"disableOfficeEditor": "Zakázanie prehliadača Office pre určité prípony súborov",
|
||||
"disableOfficePreviews": "Zakázanie náhľadov kancelárskych súborov pre určité prípony súborov",
|
||||
"disableOfficePreviewsDescription": "Medzerou oddelený zoznam prípon súborov, pre ktoré chcete vypnúť náhľady kancelárskych súborov. (napr. \".txt .html\")."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Pomoc",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "Vysokokvalitný náhľad",
|
||||
"previewOffice": "Náhľad kancelárskych súborov",
|
||||
"popupPreview": "Povolenie pop-up náhľadu",
|
||||
"filePreviewOptions": "Možnosti náhľadu súborov"
|
||||
"filePreviewOptions": "Možnosti náhľadu súborov",
|
||||
"editOffice": "Úprava/zobrazenie kancelárskych súborov"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Dvojfaktorové overovanie",
|
||||
|
||||
@@ -283,7 +283,11 @@
|
||||
"enterPasswordAgain": "Şifreyi tekrar girin",
|
||||
"passwordsDoNotMatch": "Parolalar eşleşmiyor",
|
||||
"loginMethod": "Giriş yöntemi",
|
||||
"shareSettings": "Paylaşım Ayarları"
|
||||
"shareSettings": "Paylaşım Ayarları",
|
||||
"disableOfficeEditorDescription": "OnlyOffice görüntüleyicisinin devre dışı bırakılacağı dosya uzantılarının boşluk bırakılarak ayrılmış bir listesi. (örneğin, '.txt .html').",
|
||||
"disableOfficeEditor": "Belirli dosya uzantıları için ofis görüntüleyiciyi devre dışı bırakma",
|
||||
"disableOfficePreviews": "Belirli dosya uzantıları için ofis önizlemelerini devre dışı bırakma",
|
||||
"disableOfficePreviewsDescription": "Ofis önizlemelerinin devre dışı bırakılacağı dosya uzantılarının boşluk bırakılarak ayrılmış bir listesi. (örneğin, '.txt .html')."
|
||||
},
|
||||
"sidebar": {
|
||||
"help": "Yardım",
|
||||
@@ -365,7 +369,8 @@
|
||||
"highQualityPreview": "Yüksek kaliteli önizleme",
|
||||
"previewOffice": "Ofis dosyalarını önizleme",
|
||||
"popupPreview": "Açılır pencere önizlemesini etkinleştir",
|
||||
"filePreviewOptions": "Dosya önizleme seçenekleri"
|
||||
"filePreviewOptions": "Dosya önizleme seçenekleri",
|
||||
"editOffice": "Ofis dosyalarını düzenleme/görüntüleme"
|
||||
},
|
||||
"otp": {
|
||||
"name": "İki Faktörlü Kimlik Doğrulama",
|
||||
|
||||
+174
-135
@@ -1,163 +1,181 @@
|
||||
import { removePrefix } from "@/utils/url.js";
|
||||
import { getFileExtension } from "@/utils/files.js";
|
||||
import { state,mutations } from "@/store";
|
||||
import { noAuth } from "@/utils/constants.js";
|
||||
import { getTypeInfo } from "@/utils/mimetype";
|
||||
import { fromNow } from "@/utils/moment";
|
||||
import * as i18n from "@/i18n";
|
||||
import { removePrefix } from '@/utils/url.js'
|
||||
import { getFileExtension } from '@/utils/files.js'
|
||||
import { state, mutations } from '@/store'
|
||||
import { noAuth } from '@/utils/constants.js'
|
||||
import { getTypeInfo } from '@/utils/mimetype'
|
||||
import { fromNow } from '@/utils/moment'
|
||||
import * as i18n from '@/i18n'
|
||||
|
||||
export const getters = {
|
||||
getTime: (timestamp) => {
|
||||
getTime: timestamp => {
|
||||
if (state.user.dateFormat) {
|
||||
// Truncate the fractional seconds to 3 digits (milliseconds)
|
||||
const sanitizedString = timestamp.replace(/\.\d+/, (match) =>
|
||||
const sanitizedString = timestamp.replace(/\.\d+/, match =>
|
||||
match.slice(0, 4)
|
||||
);
|
||||
)
|
||||
// Parse the sanitized string into a Date object
|
||||
const date = new Date(sanitizedString);
|
||||
return date.toLocaleString();
|
||||
const date = new Date(sanitizedString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
return fromNow(timestamp, state.user.locale);
|
||||
return fromNow(timestamp, state.user.locale)
|
||||
},
|
||||
isScrollable: () => {
|
||||
const currentView = getters.currentView();
|
||||
if (currentView == "preview") {
|
||||
return false;
|
||||
const cv = getters.currentView()
|
||||
if (
|
||||
cv == 'preview' ||
|
||||
cv == 'onlyOfficeEditor' ||
|
||||
cv == 'markdownViewer' ||
|
||||
cv == 'epubViewer' ||
|
||||
cv == 'docViewer' ||
|
||||
cv == 'editor'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
previewType: () => getTypeInfo(state.req.type).simpleType,
|
||||
isCardView: () => (state.user.viewMode == "gallery" || state.user.viewMode == "normal" ) && getters.currentView() == "listingView" ,
|
||||
currentHash: () => state.route.hash.replace("#", ""),
|
||||
isCardView: () =>
|
||||
(state.user.viewMode == 'gallery' || state.user.viewMode == 'normal') &&
|
||||
getters.currentView() == 'listingView',
|
||||
currentHash: () => state.route.hash.replace('#', ''),
|
||||
isMobile: () => state.isMobile,
|
||||
isLoading: () => Object.keys(state.loading).length > 0,
|
||||
isSettings: () => getters.currentView() === "settings",
|
||||
isShare: () => getters.currentView() === "share",
|
||||
isSettings: () => getters.currentView() === 'settings',
|
||||
isShare: () => getters.currentView() === 'share',
|
||||
isDarkMode: () => {
|
||||
if (state.user == null) {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
return state.user.darkMode === true;
|
||||
return state.user.darkMode === true
|
||||
},
|
||||
isLoggedIn: () => {
|
||||
if (state.user == null) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
if (state.user.locale == undefined || state.user.locale == null) {
|
||||
let savedLocale = localStorage.getItem("userLocale");
|
||||
let savedLocale = localStorage.getItem('userLocale')
|
||||
if (!savedLocale) {
|
||||
savedLocale = i18n.detectLocale();
|
||||
savedLocale = i18n.detectLocale()
|
||||
}
|
||||
mutations.updateCurrentUser({ locale: savedLocale });
|
||||
mutations.updateCurrentUser({ locale: savedLocale })
|
||||
}
|
||||
if (noAuth) {
|
||||
return true
|
||||
}
|
||||
if (state.user !== null && state.user?.username != "" && state.user?.username != "publicUser") {
|
||||
return true;
|
||||
if (
|
||||
state.user !== null &&
|
||||
state.user?.username != '' &&
|
||||
state.user?.username != 'publicUser'
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
isAdmin: () => state.user.permissions?.admin == true,
|
||||
isFiles: () => state.route.name === "Files",
|
||||
isListing: () => getters.isFiles() && state.req.type === "directory",
|
||||
selectedCount: () => Array.isArray(state.selected) ? state.selected.length : 0,
|
||||
isFiles: () => state.route.name === 'Files',
|
||||
isListing: () => getters.isFiles() && state.req.type === 'directory',
|
||||
selectedCount: () =>
|
||||
Array.isArray(state.selected) ? state.selected.length : 0,
|
||||
getFirstSelected: () => state.req.items[state.selected[0]],
|
||||
isSingleFileSelected: () => getters.selectedCount() === 1 && getters.getFirstSelected()?.type != "directory",
|
||||
selectedDownloadUrl() {
|
||||
isSingleFileSelected: () =>
|
||||
getters.selectedCount() === 1 &&
|
||||
getters.getFirstSelected()?.type != 'directory',
|
||||
selectedDownloadUrl () {
|
||||
if (state.isSearchActive) {
|
||||
return state.selected[0].url;
|
||||
return state.selected[0].url
|
||||
}
|
||||
let selectedItem = state.selected[0]
|
||||
return state.req.items[selectedItem].url;
|
||||
return state.req.items[selectedItem].url
|
||||
},
|
||||
reqNumDirs: () => {
|
||||
let dirCount = 0;
|
||||
let dirCount = 0
|
||||
if (!state.req.items) {
|
||||
return 0;
|
||||
return 0
|
||||
}
|
||||
state.req.items.forEach((item) => {
|
||||
state.req.items.forEach(item => {
|
||||
// Check if the item is a directory
|
||||
if (item.type == "directory") {
|
||||
if (item.type == 'directory') {
|
||||
// Otherwise, count this directory
|
||||
dirCount++;
|
||||
dirCount++
|
||||
}
|
||||
});
|
||||
})
|
||||
// Return the directory count
|
||||
return dirCount;
|
||||
return dirCount
|
||||
},
|
||||
reqNumFiles: () => {
|
||||
let fileCount = 0;
|
||||
let fileCount = 0
|
||||
if (!state.req.items) {
|
||||
return 0;
|
||||
return 0
|
||||
}
|
||||
state.req.items.forEach((item) => {
|
||||
state.req.items.forEach(item => {
|
||||
// Check if the item is a directory
|
||||
if (item.type != "directory") {
|
||||
if (item.type != 'directory') {
|
||||
// Otherwise, count this directory
|
||||
fileCount++;
|
||||
fileCount++
|
||||
}
|
||||
});
|
||||
})
|
||||
// Return the directory count
|
||||
return fileCount;
|
||||
return fileCount
|
||||
},
|
||||
reqItems: () => {
|
||||
if (state.user == null) {
|
||||
return {};
|
||||
return {}
|
||||
}
|
||||
const dirs = [];
|
||||
const files = [];
|
||||
const dirs = []
|
||||
const files = []
|
||||
if (!state.req.items) {
|
||||
return { dirs, files };
|
||||
return { dirs, files }
|
||||
}
|
||||
state.req.items.forEach((item) => {
|
||||
if (item.type == "directory") {
|
||||
dirs.push(item);
|
||||
state.req.items.forEach(item => {
|
||||
if (item.type == 'directory') {
|
||||
dirs.push(item)
|
||||
} else {
|
||||
item.Path = state.req.Path;
|
||||
files.push(item);
|
||||
item.Path = state.req.Path
|
||||
files.push(item)
|
||||
}
|
||||
});
|
||||
return { dirs, files };
|
||||
})
|
||||
return { dirs, files }
|
||||
},
|
||||
isSidebarVisible: () => {
|
||||
const currentView = getters.currentView();
|
||||
let visible = (state.showSidebar || getters.isStickySidebar()) && state.user.username != "publicUser"
|
||||
if (currentView == "settings") {
|
||||
visible = !getters.isMobile();
|
||||
const currentView = getters.currentView()
|
||||
let visible =
|
||||
(state.showSidebar || getters.isStickySidebar()) &&
|
||||
state.user.username != 'publicUser'
|
||||
if (currentView == 'settings') {
|
||||
visible = !getters.isMobile()
|
||||
}
|
||||
if (currentView == "share") {
|
||||
if (currentView == 'share') {
|
||||
visible = false
|
||||
}
|
||||
if (typeof getters.currentPromptName() === "string" && !getters.isStickySidebar()) {
|
||||
visible = false;
|
||||
if (
|
||||
typeof getters.currentPromptName() === 'string' &&
|
||||
!getters.isStickySidebar()
|
||||
) {
|
||||
visible = false
|
||||
}
|
||||
if (currentView == "preview") {
|
||||
if (currentView == 'preview') {
|
||||
const contentType = getters.previewType()
|
||||
if (
|
||||
contentType == "audio" ||
|
||||
contentType == "video" ||
|
||||
contentType == "image" ||
|
||||
contentType == "pdf"
|
||||
contentType == 'audio' ||
|
||||
contentType == 'video' ||
|
||||
contentType == 'image' ||
|
||||
contentType == 'pdf'
|
||||
) {
|
||||
visible = false
|
||||
}
|
||||
}
|
||||
if (
|
||||
currentView == "editor" ||
|
||||
currentView == "onlyOfficeEditor"
|
||||
) {
|
||||
visible = false;
|
||||
if (currentView == 'editor' || currentView == 'onlyOfficeEditor') {
|
||||
visible = false
|
||||
}
|
||||
return visible
|
||||
},
|
||||
isStickySidebar: () => {
|
||||
let sticky = state.user?.stickySidebar;
|
||||
const currentView = getters.currentView();
|
||||
if (currentView == "settings") {
|
||||
let sticky = state.user?.stickySidebar
|
||||
const currentView = getters.currentView()
|
||||
if (currentView == 'settings') {
|
||||
sticky = true
|
||||
}
|
||||
if (currentView == null && !getters.isLoading() && getters.isShare() ) {
|
||||
if (currentView == null && !getters.isLoading() && getters.isShare()) {
|
||||
sticky = true
|
||||
}
|
||||
if (getters.isMobile()) {
|
||||
@@ -166,83 +184,101 @@ export const getters = {
|
||||
return sticky
|
||||
},
|
||||
showOverlay: () => {
|
||||
const hasPrompt = getters.currentPrompt() !== null && getters.currentPromptName() !== "more"
|
||||
const showForSidebar = getters.isSidebarVisible() && !getters.isStickySidebar()
|
||||
return hasPrompt || showForSidebar || state.isSearchActive;
|
||||
const hasPrompt =
|
||||
getters.currentPrompt() !== null && getters.currentPromptName() !== 'more'
|
||||
const showForSidebar =
|
||||
getters.isSidebarVisible() && !getters.isStickySidebar()
|
||||
return hasPrompt || showForSidebar || state.isSearchActive
|
||||
},
|
||||
showBreadCrumbs: () => {
|
||||
return getters.currentView() == "listingView" ;
|
||||
return getters.currentView() == 'listingView'
|
||||
},
|
||||
routePath: (trimModifier="") => {
|
||||
return removePrefix(state.route.path,trimModifier)
|
||||
routePath: (trimModifier = '') => {
|
||||
return removePrefix(state.route.path, trimModifier)
|
||||
},
|
||||
sharePathBase: () => {
|
||||
let urlPath = getters.routePath("share");
|
||||
let urlPath = getters.routePath('share')
|
||||
// Step 1: Split the path by '/'
|
||||
let parts = urlPath.split("/");
|
||||
let parts = urlPath.split('/')
|
||||
// Step 2: Assign hash to the second part (index 2) and join the rest for subPath
|
||||
return "/share/"+ parts[1] + "/";
|
||||
return '/share/' + parts[1] + '/'
|
||||
},
|
||||
currentView: () => {
|
||||
let listingView = null
|
||||
const pathname = getters.routePath()
|
||||
if (pathname.startsWith(`/settings`)) {
|
||||
listingView = "settings"
|
||||
listingView = 'settings'
|
||||
} else if (pathname.startsWith(`/share`)) {
|
||||
listingView = "share"
|
||||
listingView = 'share'
|
||||
} else if (pathname.startsWith(`/files`)) {
|
||||
if (state.req.type !== undefined) {
|
||||
if (state.req.type == "directory") {
|
||||
listingView = "listingView";
|
||||
if (state.req.type == 'directory') {
|
||||
listingView = 'listingView'
|
||||
} else if (getters.onlyOfficeEnabled(state.req.name)) {
|
||||
listingView = "onlyOfficeEditor";
|
||||
} else if ("content" in state.req && state.req.type == "text/markdown" && window.location.hash != "#edit") {
|
||||
listingView = "markdownViewer";
|
||||
} else if ("content" in state.req) {
|
||||
listingView = "editor";
|
||||
listingView = 'onlyOfficeEditor'
|
||||
} else if (
|
||||
'content' in state.req &&
|
||||
state.req.type == 'text/markdown' &&
|
||||
window.location.hash != '#edit'
|
||||
) {
|
||||
listingView = 'markdownViewer'
|
||||
} else if ('content' in state.req) {
|
||||
listingView = 'editor'
|
||||
} else if (state.req.type.startsWith('application/epub')) {
|
||||
listingView = 'epubViewer'
|
||||
} else if (
|
||||
state.req.type.startsWith(
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
)
|
||||
) {
|
||||
listingView = 'docViewer'
|
||||
} else {
|
||||
listingView = "preview";
|
||||
listingView = 'preview'
|
||||
}
|
||||
} else {
|
||||
listingView = "listingView"
|
||||
listingView = 'listingView'
|
||||
}
|
||||
}
|
||||
return listingView
|
||||
},
|
||||
progress: () => {
|
||||
// Check if state.upload is defined and valid
|
||||
if (!state.upload || !Array.isArray(state.upload.progress) || !Array.isArray(state.upload.sizes)) {
|
||||
return 0;
|
||||
if (
|
||||
!state.upload ||
|
||||
!Array.isArray(state.upload.progress) ||
|
||||
!Array.isArray(state.upload.sizes)
|
||||
) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Handle cases where progress or sizes arrays might be empty
|
||||
if (state.upload.progress.length === 0 || state.upload.sizes.length === 0) {
|
||||
return 0;
|
||||
return 0
|
||||
}
|
||||
|
||||
// Calculate totalSize
|
||||
let totalSize = state.upload.sizes.reduce((a, b) => a + b, 0);
|
||||
let totalSize = state.upload.sizes.reduce((a, b) => a + b, 0)
|
||||
|
||||
// Calculate sum of progress
|
||||
let sum = state.upload.progress.reduce((acc, val) => acc + val, 0);
|
||||
let sum = state.upload.progress.reduce((acc, val) => acc + val, 0)
|
||||
|
||||
// Return progress as a percentage
|
||||
return Math.ceil((sum / totalSize) * 100);
|
||||
return Math.ceil((sum / totalSize) * 100)
|
||||
},
|
||||
|
||||
filesInUploadCount: () => {
|
||||
const uploadsCount = state.upload.length
|
||||
const queueCount = state.queue.length
|
||||
return uploadsCount + queueCount;
|
||||
return uploadsCount + queueCount
|
||||
},
|
||||
|
||||
currentPrompt: () => {
|
||||
// Ensure state.prompts is an array
|
||||
if (!Array.isArray(state.prompts)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
if (state.prompts.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return state.prompts[state.prompts.length - 1]
|
||||
},
|
||||
@@ -250,59 +286,62 @@ export const getters = {
|
||||
currentPromptName: () => {
|
||||
// Ensure state.prompts is an array
|
||||
if (!Array.isArray(state.prompts) || state.prompts.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
// Check if the name property is a string
|
||||
const lastPrompt = state.prompts[state.prompts.length - 1];
|
||||
if (typeof lastPrompt?.name !== "string") {
|
||||
return null;
|
||||
const lastPrompt = state.prompts[state.prompts.length - 1]
|
||||
if (typeof lastPrompt?.name !== 'string') {
|
||||
return null
|
||||
}
|
||||
return lastPrompt.name;
|
||||
return lastPrompt.name
|
||||
},
|
||||
|
||||
filesInUpload: () => {
|
||||
// Ensure state.upload.uploads is an object and state.upload.sizes is an array
|
||||
if (typeof state.upload.uploads !== 'object' || !Array.isArray(state.upload.sizes)) {
|
||||
return [];
|
||||
if (
|
||||
typeof state.upload.uploads !== 'object' ||
|
||||
!Array.isArray(state.upload.sizes)
|
||||
) {
|
||||
return []
|
||||
}
|
||||
|
||||
let files = [];
|
||||
let files = []
|
||||
|
||||
for (let index in state.upload.uploads) {
|
||||
let upload = state.upload.uploads[index];
|
||||
let id = upload.id;
|
||||
let type = upload.type;
|
||||
let name = upload.file.name;
|
||||
let size = state.upload.sizes[id] || 0; // Default to 0 if size is undefined
|
||||
let isDir = upload.file.type == "directory";
|
||||
let upload = state.upload.uploads[index]
|
||||
let id = upload.id
|
||||
let type = upload.type
|
||||
let name = upload.file.name
|
||||
let size = state.upload.sizes[id] || 0 // Default to 0 if size is undefined
|
||||
let isDir = upload.file.type == 'directory'
|
||||
let progress = isDir
|
||||
? 100
|
||||
: Math.ceil((state.upload.progress[id] || 0 / size) * 100); // Default to 0 if progress is undefined
|
||||
: Math.ceil((state.upload.progress[id] || 0 / size) * 100) // Default to 0 if progress is undefined
|
||||
|
||||
files.push({
|
||||
id,
|
||||
name,
|
||||
progress,
|
||||
type,
|
||||
isDir,
|
||||
});
|
||||
isDir
|
||||
})
|
||||
}
|
||||
|
||||
return files.sort((a, b) => a.progress - b.progress);
|
||||
return files.sort((a, b) => a.progress - b.progress)
|
||||
},
|
||||
onlyOfficeEnabled: (filename) => {
|
||||
onlyOfficeEnabled: filename => {
|
||||
if (!state.req?.onlyOfficeId) {
|
||||
return false
|
||||
}
|
||||
const ext = getFileExtension(filename);
|
||||
const ext = getFileExtension(filename)
|
||||
if (state.user.disableOnlyOfficeExt) {
|
||||
const disabledList = state.user.disableOnlyOfficeExt.split(" ");
|
||||
const disabledList = state.user.disableOnlyOfficeExt.split(' ')
|
||||
for (const e of disabledList) {
|
||||
if (e.trim().toLowerCase() === ext.toLowerCase()) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const serverHasMultipleSources = window.FileBrowser.SourceCount > 1;
|
||||
const oidcAvailable = window.FileBrowser.OidcAvailable;
|
||||
const passwordAvailable = window.FileBrowser.PasswordAvailable;
|
||||
const mediaAvailable = window.FileBrowser.MediaAvailable;
|
||||
const pdfAvailable = window.FileBrowser.PdfAvailable;
|
||||
const muPdfAvailable = window.FileBrowser.MuPdfAvailable;
|
||||
const origin = window.location.origin;
|
||||
|
||||
const settings = [
|
||||
@@ -33,7 +33,7 @@ const settings = [
|
||||
];
|
||||
|
||||
export {
|
||||
pdfAvailable,
|
||||
muPdfAvailable,
|
||||
mediaAvailable,
|
||||
oidcAvailable,
|
||||
passwordAvailable,
|
||||
|
||||
@@ -5,7 +5,13 @@ import { removePrefix } from "@/utils/url.js";
|
||||
import { publicApi } from "@/api";
|
||||
|
||||
export default function download() {
|
||||
if (getters.currentView() === "share") {
|
||||
const currentView = getters.currentView();
|
||||
if (currentView === "preview") {
|
||||
startDownload(null, [state.req.url]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentView === "share") {
|
||||
let urlPath = getters.routePath("share");
|
||||
let parts = urlPath.split("/");
|
||||
const hash = parts[1];
|
||||
|
||||
@@ -25,6 +25,8 @@ import Preview from "@/views/files/Preview.vue";
|
||||
import ListingView from "@/views/files/ListingView.vue";
|
||||
import Editor from "@/views/files/Editor.vue";
|
||||
import OnlyOfficeEditor from "./files/OnlyOfficeEditor.vue";
|
||||
import EpubViewer from "./files/EpubViewer.vue";
|
||||
import DocViewer from "./files/DocViewer.vue";
|
||||
import MarkdownViewer from "./files/MarkdownViewer.vue";
|
||||
import { state, mutations, getters } from "@/store";
|
||||
import { url } from "@/utils";
|
||||
@@ -41,6 +43,8 @@ export default {
|
||||
Preview,
|
||||
ListingView,
|
||||
Editor,
|
||||
EpubViewer,
|
||||
DocViewer,
|
||||
OnlyOfficeEditor,
|
||||
MarkdownViewer,
|
||||
PopupPreview,
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="viewer-background">
|
||||
<div v-if="loading" class="status-text">Loading document...</div>
|
||||
<div v-else-if="error" class="status-text error">{{ error }}</div>
|
||||
<div v-else class="docx-page" v-html="docxHtml"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "vue";
|
||||
import * as mammoth from "mammoth";
|
||||
import { filesApi } from "@/api";
|
||||
import { state,mutations } from "@/store";
|
||||
|
||||
export default defineComponent({
|
||||
name: "DocxViewer",
|
||||
data() {
|
||||
return {
|
||||
docxHtml: "",
|
||||
loading: false,
|
||||
error: "",
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
"state.req.path": {
|
||||
handler(newPath, oldPath) {
|
||||
// Added a console log to the watcher to see when it runs
|
||||
console.log(
|
||||
`[DEBUG] Watcher triggered: Path changed from "${oldPath}" to "${newPath}"`
|
||||
);
|
||||
this.loadFile();
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected({
|
||||
name: state.req.name,
|
||||
path: state.req.path,
|
||||
size: state.req.size,
|
||||
type: state.req.type,
|
||||
source: state.req.source,
|
||||
url: state.req.url,
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
async loadFile() {
|
||||
console.log("%c[1] loadFile triggered.", "font-weight: bold; color: blue;");
|
||||
|
||||
try {
|
||||
// Using JSON.stringify to snapshot the state object at this exact moment
|
||||
console.log(
|
||||
"[2] Current state.req object:",
|
||||
JSON.parse(JSON.stringify(state.req))
|
||||
);
|
||||
|
||||
const filename = state.req.name;
|
||||
console.log(`[3] Checking filename: "${filename}"`);
|
||||
|
||||
// Check if the filename is valid and ends with .docx
|
||||
if (!filename || !filename.toLowerCase().endsWith(".docx")) {
|
||||
this.error = `This viewer only supports .docx files. Current file: "${
|
||||
filename || "Not available"
|
||||
}"`;
|
||||
console.error(`[3a] Filename check FAILED. Stopping execution.`);
|
||||
this.docxHtml = ""; // Ensure view is cleared
|
||||
return;
|
||||
}
|
||||
console.log("[3a] Filename check PASSED.");
|
||||
|
||||
this.loading = true;
|
||||
this.error = "";
|
||||
this.docxHtml = "";
|
||||
|
||||
console.log("[4] Getting download URL from filesApi...");
|
||||
const downloadUrl = filesApi.getDownloadURL(
|
||||
state.req.source,
|
||||
state.req.path,
|
||||
false,
|
||||
true
|
||||
);
|
||||
|
||||
if (!downloadUrl) {
|
||||
throw new Error("Could not retrieve a valid download URL from the API.");
|
||||
}
|
||||
console.log("[5] Got download URL:", downloadUrl);
|
||||
|
||||
console.log("[6] Fetching data from URL...");
|
||||
const response = await fetch(downloadUrl);
|
||||
console.log("[7] Fetch response received. Status:", response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download file (Status: ${response.status})`);
|
||||
}
|
||||
|
||||
console.log("[8] Getting ArrayBuffer from response...");
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
console.log(`[8a] Got ArrayBuffer. Size: ${arrayBuffer.byteLength} bytes.`);
|
||||
|
||||
if (arrayBuffer.byteLength === 0) {
|
||||
throw new Error("Downloaded file is empty (0 bytes).");
|
||||
}
|
||||
|
||||
console.log("[9] Passing ArrayBuffer to mammoth.js for conversion...");
|
||||
const result = await mammoth.convertToHtml({ arrayBuffer });
|
||||
this.docxHtml = result.value;
|
||||
console.log(
|
||||
"%c[10] SUCCESS: Document rendered.",
|
||||
"font-weight: bold; color: green;"
|
||||
);
|
||||
} catch (e) {
|
||||
console.error("%c[X] CATCH BLOCK ERROR:", "font-weight: bold; color: red;", e);
|
||||
this.error = e.message || "An unknown error occurred.";
|
||||
} finally {
|
||||
this.loading = false;
|
||||
console.log("[F] FINALLY block executed.");
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Styles remain the same */
|
||||
.viewer-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: #f0f2f5;
|
||||
padding: 2em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.docx-page {
|
||||
background: white;
|
||||
width: 8.5in;
|
||||
min-height: 11in;
|
||||
margin: 0 auto;
|
||||
padding: 1in;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.status-text {
|
||||
text-align: center;
|
||||
padding: 3em;
|
||||
font-family: sans-serif;
|
||||
color: #333;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.status-text.error {
|
||||
color: #d9534f;
|
||||
}
|
||||
@media (max-width: 8.5in) {
|
||||
.viewer-background {
|
||||
padding: 0;
|
||||
}
|
||||
.docx-page {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
padding: 1em;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script>
|
||||
import { eventBus } from "@/store/eventBus";
|
||||
import { state, getters } from "@/store";
|
||||
import { state, getters, mutations } from "@/store";
|
||||
import { filesApi } from "@/api";
|
||||
import ace, { version as ace_version } from "ace-builds";
|
||||
import modelist from "ace-builds/src-noconflict/ext-modelist";
|
||||
@@ -16,63 +16,95 @@ import "ace-builds/src-min-noconflict/theme-twilight";
|
||||
export default {
|
||||
name: "editor",
|
||||
data: function () {
|
||||
return {};
|
||||
return {
|
||||
editor: null, // The editor instance
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isDarkMode() {
|
||||
return getters.isDarkMode();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
if (this.editor) {
|
||||
this.editor.destroy();
|
||||
this.editor = null;
|
||||
}
|
||||
// Wait for the DOM to update after the route change
|
||||
this.$nextTick(() => {
|
||||
this.setupEditor();
|
||||
});
|
||||
},
|
||||
},
|
||||
created() {
|
||||
window.addEventListener("keydown", this.keyEvent);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("keydown", this.keyEvent);
|
||||
this.editor.destroy();
|
||||
if (this.editor) {
|
||||
this.editor.destroy();
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
ace.config.set(
|
||||
"basePath",
|
||||
`https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/`
|
||||
);
|
||||
// this is empty content string "empty-file-x6OlSil" which is used to represent empty text file
|
||||
const fileContent =
|
||||
state.req.content == "empty-file-x6OlSil" ? "" : state.req.content || "";
|
||||
this.editor = ace.edit("editor", {
|
||||
mode: modelist.getModeForPath(state.req.name).mode,
|
||||
value: fileContent,
|
||||
showPrintMargin: false,
|
||||
theme: "ace/theme/chrome",
|
||||
readOnly: state.req.type === "textImmutable",
|
||||
wrap: false,
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected({
|
||||
name: state.req.name,
|
||||
path: state.req.path,
|
||||
size: state.req.size,
|
||||
type: state.req.type,
|
||||
source: state.req.source,
|
||||
url: state.req.url,
|
||||
});
|
||||
// Wait for the initial DOM render to complete
|
||||
this.$nextTick(() => {
|
||||
this.setupEditor();
|
||||
});
|
||||
// Set the basePath for Ace Editor
|
||||
if (this.isDarkMode) {
|
||||
this.editor.setTheme("ace/theme/twilight");
|
||||
}
|
||||
eventBus.on("handleEditorValueRequest", this.handleEditorValueRequest);
|
||||
},
|
||||
methods: {
|
||||
setupEditor() {
|
||||
const editorEl = document.getElementById("editor");
|
||||
if (!editorEl) {
|
||||
console.warn(
|
||||
"Editor component mounted, but #editor div was not found in the DOM. Aborting setup."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
ace.config.set(
|
||||
"basePath",
|
||||
`https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/`
|
||||
);
|
||||
|
||||
const fileContent =
|
||||
state.req.content == "empty-file-x6OlSil" ? "" : state.req.content || "";
|
||||
|
||||
this.editor = ace.edit(editorEl, {
|
||||
mode: modelist.getModeForPath(state.req.name).mode,
|
||||
value: fileContent,
|
||||
showPrintMargin: false,
|
||||
theme: "ace/theme/chrome",
|
||||
readOnly: state.req.type === "textImmutable",
|
||||
wrap: false,
|
||||
});
|
||||
|
||||
if (this.isDarkMode) {
|
||||
this.editor.setTheme("ace/theme/twilight");
|
||||
}
|
||||
},
|
||||
handleEditorValueRequest() {
|
||||
filesApi.put(state.req.path, state.req.source, this.editor.getValue());
|
||||
if (this.editor) {
|
||||
filesApi.put(state.req.path, state.req.source, this.editor.getValue());
|
||||
}
|
||||
},
|
||||
keyEvent(event) {
|
||||
const { key, ctrlKey, metaKey } = event;
|
||||
if (getters.currentPromptName() != null) {
|
||||
return;
|
||||
}
|
||||
if (!ctrlKey && !metaKey) {
|
||||
return;
|
||||
}
|
||||
switch (key.toLowerCase()) {
|
||||
case "s":
|
||||
event.preventDefault();
|
||||
this.handleEditorValueRequest();
|
||||
break;
|
||||
|
||||
default:
|
||||
// No action for other keys
|
||||
return;
|
||||
if (getters.currentPromptName() != null) return;
|
||||
if (!ctrlKey && !metaKey) return;
|
||||
if (key.toLowerCase() === "s") {
|
||||
event.preventDefault();
|
||||
this.handleEditorValueRequest();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div class="epub-container">
|
||||
<div v-if="!isReady" class="loading-indicator">
|
||||
<p>{{ $t("files.loading") }}</p>
|
||||
</div>
|
||||
|
||||
<div id="viewer" :class="{ ready: isReady }"></div>
|
||||
|
||||
<div v-if="isReady" class="navigation">
|
||||
<button @click="prevPage" class="nav-button"><</button>
|
||||
<button @click="nextPage" class="nav-button">></button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import ePub, { type Book, type Rendition } from "epubjs";
|
||||
import { state,mutations } from "@/store"; // Assuming your store setup
|
||||
import { filesApi } from "@/api"; // Assuming your api setup
|
||||
import { removeLastDir } from "@/utils/url"; // Assuming your utils setup
|
||||
|
||||
export default defineComponent({
|
||||
name: "epubViewer",
|
||||
data() {
|
||||
return {
|
||||
isReady: false, // Flag to indicate when the book is loaded
|
||||
floatIn: false, // Flag for the float-in animation
|
||||
book: null as Book | null,
|
||||
rendition: null as Rendition | null,
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected({
|
||||
name: state.req.name,
|
||||
path: state.req.path,
|
||||
size: state.req.size,
|
||||
type: state.req.type,
|
||||
source: state.req.source,
|
||||
url: state.req.url,
|
||||
});
|
||||
try {
|
||||
// 1. Fetch the download URL for the EPUB file
|
||||
const epubUrl = await filesApi.getDownloadURL(
|
||||
state.req.source,
|
||||
state.req.path,
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
// 2. Initialize the EPUB book
|
||||
this.book = ePub(epubUrl);
|
||||
|
||||
// 3. Render the book to the "viewer" div
|
||||
this.rendition = this.book.renderTo("viewer", {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
spread: "auto", // Automatically handle single or double page spreads
|
||||
flow: "paginated", // Standard book-like pagination
|
||||
});
|
||||
|
||||
// 4. Display the rendered book
|
||||
await this.rendition.display();
|
||||
|
||||
// Set flags to show the book and trigger animations
|
||||
this.isReady = true;
|
||||
setTimeout(() => {
|
||||
this.floatIn = true;
|
||||
}, 100); // slight delay to allow rendering
|
||||
} catch (error) {
|
||||
this.onLoadComponentError(error);
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
// Clean up and destroy the epub instance to free up memory
|
||||
if (this.book) {
|
||||
this.book.destroy();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Navigate to the next page
|
||||
nextPage() {
|
||||
this.rendition?.next();
|
||||
},
|
||||
// Navigate to the previous page
|
||||
prevPage() {
|
||||
this.rendition?.prev();
|
||||
},
|
||||
// Close the viewer and navigate away
|
||||
close() {
|
||||
const current = window.location.pathname;
|
||||
const newPath = removeLastDir(current);
|
||||
window.location.href = newPath + "#" + state.req.name;
|
||||
},
|
||||
// Error handler
|
||||
onLoadComponentError(error: any) {
|
||||
console.error("Error loading EPUB file:", error);
|
||||
// You could add logic here to display an error message to the user
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.epub-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--surfaceSecondary); /* A light background for the reader */
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-size: 1.2em;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* The viewer must have a defined height for epub.js to work */
|
||||
#viewer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
visibility: hidden; /* Hide until ready to prevent flicker */
|
||||
}
|
||||
|
||||
#viewer.ready {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
position: fixed;
|
||||
bottom: 1.5em;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1001; /* Ensure controls are on top */
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
padding: 0.5em;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
font-size: 1.5em;
|
||||
color: #343a40;
|
||||
cursor: pointer;
|
||||
padding: 0.25em 1em;
|
||||
}
|
||||
|
||||
/* Copied directly from your example */
|
||||
.floating-close {
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -5em); /* Start offscreen */
|
||||
transition: transform 0.4s ease;
|
||||
background: var(--surfaceSecondary);
|
||||
font-size: 0.5em;
|
||||
top: 0;
|
||||
z-index: 1002;
|
||||
}
|
||||
|
||||
.float-in {
|
||||
transform: translate(-50%, 2.75em); /* Animate to final position */
|
||||
}
|
||||
|
||||
.floating-close i {
|
||||
font-size: 2em;
|
||||
padding-right: 1em;
|
||||
padding-left: 1em;
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,7 @@
|
||||
<script lang="ts">
|
||||
import { marked } from "marked";
|
||||
import DOMPurify from 'dompurify';
|
||||
import { state } from "@/store";
|
||||
import { state, mutations } from "@/store";
|
||||
import hljs from 'highlight.js';
|
||||
|
||||
// --- We have removed all `marked.use()` configuration ---
|
||||
@@ -73,6 +73,15 @@ export default {
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
mutations.resetSelected();
|
||||
mutations.addSelected({
|
||||
name: state.req.name,
|
||||
path: state.req.path,
|
||||
size: state.req.size,
|
||||
type: state.req.type,
|
||||
source: state.req.source,
|
||||
url: state.req.url,
|
||||
});
|
||||
this.setHighlightTheme(state.user.darkMode);
|
||||
// Set initial content. The `watch` will trigger the first highlight.
|
||||
const fileContent = state.req.content == "empty-file-x6OlSil" ? "" : state.req.content || "";
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div id="previewer" @mousemove="toggleNavigation" @touchstart="toggleNavigation">
|
||||
<div class="preview">
|
||||
<ExtendedImage v-if="previewType == 'image'" :src="raw"> </ExtendedImage>
|
||||
<ExtendedImage v-if="previewType == 'image' || pdfConvertable" :src="raw">
|
||||
</ExtendedImage>
|
||||
<audio
|
||||
v-else-if="previewType == 'audio'"
|
||||
ref="player"
|
||||
@@ -111,6 +112,23 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
pdfConvertable() {
|
||||
const ext = "." + state.req.name.split(".").pop().toLowerCase(); // Ensure lowercase and dot
|
||||
const pdfConvertCompatibleFileExtensions = {
|
||||
".xps": true,
|
||||
".mobi": true,
|
||||
".fb2": true,
|
||||
".cbz": true,
|
||||
".svg": true,
|
||||
".docx": true,
|
||||
".ppt": true,
|
||||
".pptx": true,
|
||||
".xlsx": true,
|
||||
".hwp": true,
|
||||
".hwpx": true,
|
||||
};
|
||||
return !!pdfConvertCompatibleFileExtensions[ext];
|
||||
},
|
||||
sidebarShowing() {
|
||||
return getters.isSidebarVisible();
|
||||
},
|
||||
@@ -118,6 +136,12 @@ export default {
|
||||
return getters.previewType();
|
||||
},
|
||||
raw() {
|
||||
if (this.pdfConvertable) {
|
||||
return (
|
||||
filesApi.getPreviewURL(state.req.source, state.req.path, state.req.modified) +
|
||||
"&size=original"
|
||||
);
|
||||
}
|
||||
return filesApi.getDownloadURL(state.req.source, state.req.path, true);
|
||||
},
|
||||
isDarkMode() {
|
||||
|
||||
@@ -78,6 +78,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="muPdfAvailable">
|
||||
<h3> {{ $t('settings.disableOfficePreviews')}} </h3>
|
||||
<p>
|
||||
{{ $t('settings.disableOfficePreviewsDescription') }}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<input
|
||||
class="input input--block form-form flat-right"
|
||||
:class="{ 'invalid-form': !formValidationOfficePreviews() }"
|
||||
type="text"
|
||||
placeholder="enter file extensions"
|
||||
id="officePreviewExt"
|
||||
v-model="formOfficePreviewExt"
|
||||
/>
|
||||
<button type="button" class="button form-button" @click="submitOfficePreviewsChange"> {{ $t('buttons.save') }} </button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3> {{ $t('settings.themeColor') }} </h3>
|
||||
<ButtonGroup
|
||||
:buttons="colorChoices"
|
||||
@@ -98,7 +116,7 @@
|
||||
|
||||
<script>
|
||||
import { notify } from "@/notify";
|
||||
import { onlyOfficeUrl, mediaAvailable } from "@/utils/constants.js";
|
||||
import { onlyOfficeUrl, mediaAvailable, muPdfAvailable } from "@/utils/constants.js";
|
||||
import { state, mutations } from "@/store";
|
||||
import { usersApi } from "@/api";
|
||||
import Languages from "@/components/settings/Languages.vue";
|
||||
@@ -117,6 +135,7 @@ export default {
|
||||
localuser: { preview: {} },
|
||||
initialized: false,
|
||||
formOnlyOfficeExt: "", // holds temporary input before saving
|
||||
formOfficePreviewExt: "", // holds temporary input before saving
|
||||
colorChoices: [
|
||||
{ label: "blue", value: "var(--blue)" },
|
||||
{ label: "red", value: "var(--red)" },
|
||||
@@ -139,6 +158,9 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
muPdfAvailable() {
|
||||
return muPdfAvailable;
|
||||
},
|
||||
hasOnlyOfficeEnabled() {
|
||||
return onlyOfficeUrl != "";
|
||||
},
|
||||
@@ -155,6 +177,7 @@ export default {
|
||||
mounted() {
|
||||
this.localuser = { ...state.user };
|
||||
this.formOnlyOfficeExt = this.localuser.disableOnlyOfficeExt;
|
||||
this.formOfficePreviewExt = this.localuser.disableOfficePreviewExt;
|
||||
},
|
||||
methods: {
|
||||
formValidation() {
|
||||
@@ -171,6 +194,20 @@ export default {
|
||||
}
|
||||
this.localuser.disableOnlyOfficeExt = this.formOnlyOfficeExt;
|
||||
},
|
||||
formValidationOfficePreviews() {
|
||||
if (this.formOfficePreviewExt == "") {
|
||||
return true;
|
||||
}
|
||||
let regex = /^\.\w+(?: \.\w+)*$/;
|
||||
return regex.test(this.formOfficePreviewExt);
|
||||
},
|
||||
submitOfficePreviewsChange() {
|
||||
if (!this.formValidationOfficePreviews()) {
|
||||
notify.showError("Invalid input, does not match requirement.");
|
||||
return;
|
||||
}
|
||||
this.localuser.disableOfficePreviewExt = this.formOfficePreviewExt;
|
||||
},
|
||||
setColor(string) {
|
||||
this.localuser.themeColor = string;
|
||||
},
|
||||
@@ -194,6 +231,7 @@ export default {
|
||||
"themeColor",
|
||||
"quickDownload",
|
||||
"disableOnlyOfficeExt",
|
||||
"disableOfficePreviewExt",
|
||||
"preview",
|
||||
]);
|
||||
notify.showSuccess(this.$t("settings.settingsUpdated"));
|
||||
|
||||
@@ -61,6 +61,7 @@ export default {
|
||||
username: "",
|
||||
password: "",
|
||||
permissions: { admin: false },
|
||||
otpEnabled: false,
|
||||
},
|
||||
showDelete: false,
|
||||
createUserDir: false,
|
||||
|
||||
@@ -71,7 +71,7 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context
|
||||
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
|
||||
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
|
||||
await page.locator('button[aria-label="Copy file"]').click();
|
||||
await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /playwright-files/');
|
||||
await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /');
|
||||
await expect(page.locator('li[aria-selected="true"]')).toHaveCount(0);
|
||||
await page.locator('li[aria-label="myfolder"]').click();
|
||||
await expect(page.locator('li[aria-selected="true"]')).toHaveCount(1);
|
||||
@@ -101,7 +101,7 @@ test("2x copy from listing to new folder", async({ page, checkForErrors, context
|
||||
await page.locator('.selected-count-header').waitFor({ state: 'visible' });
|
||||
await expect(page.locator('.selected-count-header')).toHaveText('1 selected');
|
||||
await page.locator('button[aria-label="Copy file"]').click();
|
||||
await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /playwright-files/myfolder/');
|
||||
await expect(page.locator('div[aria-label="filelist-path"]')).toHaveText('Path: /myfolder/');
|
||||
await page.locator('li[aria-label="newfolder"]').click();
|
||||
await page.locator('button[aria-label="Copy"]').click();
|
||||
const popup2 = page.locator('#popup-notification-content');
|
||||
|
||||
@@ -28,7 +28,7 @@ run: build-frontend
|
||||
else \
|
||||
sed -i '/func init/,+3d' ./swagger/docs/docs.go; \
|
||||
fi && \
|
||||
FILEBROWSER_NO_EMBEDED=true go run -tags "pdf" \
|
||||
FILEBROWSER_NO_EMBEDED=true CGO_ENABLED=1 go run -tags mupdf \
|
||||
--ldflags="-w -s -X 'github.com/gtsteffaniak/filebrowser/backend/version.CommitSHA=testingCommit' -X 'github.com/gtsteffaniak/filebrowser/backend/version.Version=testing'" . -c test_config.yaml
|
||||
|
||||
build-frontend:
|
||||
|
||||
Reference in new issue
Block a user