Compare commits

...
7 Commits
114 changed files with 2723 additions and 1526 deletions

No files matched your search

+1 -1
View File
@@ -5,7 +5,7 @@
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
"version": "22"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
},
+2
View File
@@ -20,6 +20,8 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- uses: actions/setup-node@v4
with:
node-version-file: frontend/.nvmrc
- working-directory: frontend
run: npm i && npm run build
- uses: actions/setup-go@v5
+2
View File
@@ -33,6 +33,8 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: frontend/.nvmrc
- name: Build frontend
working-directory: frontend
run: npm i && npm run build
+8
View File
@@ -38,6 +38,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: frontend/.nvmrc
- working-directory: frontend
run: npm i && npm run lint
test-frontend:
@@ -45,6 +47,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: frontend/.nvmrc
- working-directory: frontend
run: npm i && npm run test
check-translations:
@@ -52,6 +56,8 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: frontend/.nvmrc
- working-directory: frontend
run: npm i && npm run i18n:check
check-docs:
@@ -68,5 +74,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: frontend/.nvmrc
- working-directory: frontend
run: npm i && npm run icons:check
+2
View File
@@ -17,6 +17,8 @@ jobs:
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: frontend/.nvmrc
- name: Install dependencies and build frontend
run: npm i && npm run build
working-directory: frontend
+17
View File
@@ -2,6 +2,23 @@
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).
## v2.0.5
**New Features**:
- Users can set default view mode and thumbnail size from Profile settings (Listing options). Admins can set the same defaults for existing users from the user edit prompt. Values persist to the account and apply on new browsers or devices; they do not override view preferences already saved locally ([#2884](https://github.com/gtsteffaniak/filebrowser/issues/2884)).
- Admins can edit all profile preference defaults for existing users from the user management panel (listing, preview, sidebar, theme, and related options), matching what was previously available only for new users and global user defaults.
**Security**:
- [Moderate] public metadata api returns file content to anonymous share visitors, ignoring the share's download limit and file-viewer setting (GHSA-55mw-cwg7-m8f5) -- thanks @kta1kri
**Bugfixes**:
- Fixed infinite loading spinner on `/` and `/login` after CSP security hardening ([#2886](https://github.com/gtsteffaniak/filebrowser/issues/2886), [#2890](https://github.com/gtsteffaniak/filebrowser/issues/2890))
- fixed darkmode styling bug where background wouldn't respect configured settings when not logged in
- fixed styling bug where padding was missing from right side of listing view.
- tilde paths for sources were not properly expanded. (eg "~" for home)
- fixed tooltip not showing up for some circumstances on mobile, improved consistency.
- a few styling fixes for inconsistencies (#2908)
## v2.0.4
**Bugfixes**:
+2 -2
View File
@@ -14,11 +14,11 @@ RUN go build -tags analytics,mupdf,musl -ldflags="-w -s \
FROM node:jod-slim AS nbuild
WORKDIR /build
COPY ./frontend/package.json ./frontend/
COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
&& git config --global url."https://github.com/".insteadOf "git@github.com:" \
&& git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
RUN cd frontend && npm i --maxsockets 1
RUN cd frontend && npm ci --maxsockets 1
COPY ./frontend/ ./frontend/
WORKDIR /build/frontend
RUN npm run build:docker
+1 -1
View File
@@ -13,4 +13,4 @@ RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test --project dark-screenshots
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test --project dark-screenshots
+2 -2
View File
@@ -11,11 +11,11 @@ RUN go build -tags analytics -ldflags="-w -s \
FROM node:jod-slim AS nbuild
WORKDIR /build
COPY ./frontend/package.json ./frontend/
COPY ./frontend/package.json ./frontend/package-lock.json ./frontend/
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
&& git config --global url."https://github.com/".insteadOf "git@github.com:" \
&& git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"
RUN cd frontend && npm i
RUN cd frontend && npm ci
COPY ./frontend/ ./frontend/
WORKDIR /build/frontend
RUN npm run build:docker
@@ -1,4 +1,5 @@
import { defineConfig, devices } from "@playwright/test";
import type { PlaywrightFixtureOptions } from "./tests/playwright/test-setup";
/**
* Read environment variables from file.
@@ -9,7 +10,7 @@ import { defineConfig, devices } from "@playwright/test";
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
export default defineConfig<PlaywrightFixtureOptions>({
globalSetup: "./tests/playwright/screenshots-setup.ts",
timeout: 5000,
testDir: "./tests/playwright/screenshots",
+43 -35
View File
@@ -1,19 +1,19 @@
module github.com/gtsteffaniak/filebrowser/backend
go 1.26.0
go 1.27.0
require (
github.com/alecthomas/kong v1.16.1
github.com/asdine/storm/v3 v3.2.1
github.com/coreos/go-oidc/v3 v3.20.0
github.com/coreos/go-oidc/v3 v3.21.0
github.com/coreos/go-systemd/v22 v22.7.0
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
github.com/dsoprea/go-exif/v3 v3.0.1
github.com/evanoberholster/imagemeta v1.0.0
github.com/gen2brain/go-fitz v1.28.2
github.com/go-ldap/ldap/v3 v3.4.14
github.com/go-playground/validator/v10 v10.30.3
github.com/go-webauthn/webauthn v0.17.4
github.com/go-playground/validator/v10 v10.30.4
github.com/go-webauthn/webauthn v0.18.0
github.com/goccy/go-yaml v1.19.2
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/google/go-cmp v0.7.0
@@ -28,7 +28,7 @@ require (
github.com/stretchr/testify v1.12.1
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.6
golang.org/x/crypto v0.55.0
golang.org/x/crypto v0.56.0
golang.org/x/image v0.45.0
golang.org/x/mod v0.40.0
golang.org/x/net v0.58.0
@@ -38,7 +38,7 @@ require (
golang.org/x/time v0.15.0
golang.org/x/tools v0.49.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.57.0
modernc.org/sqlite v1.58.0
)
require (
@@ -51,7 +51,7 @@ require (
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
dev.gaijin.team/go/exhaustruct/v5 v5.0.3 // indirect
dev.gaijin.team/go/golib v0.8.1 // indirect
github.com/4meepo/tagalign v1.4.3 // indirect
github.com/4meepo/tagalign v1.4.4 // indirect
github.com/Abirdcfly/dupword v0.1.8 // indirect
github.com/AdminBenni/iota-mixing v1.0.0 // indirect
github.com/AlwxSin/noinlineerr v1.0.6 // indirect
@@ -68,13 +68,13 @@ require (
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/air-verse/air v1.67.4 // indirect
github.com/alecthomas/chroma/v2 v2.27.0 // indirect
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
github.com/alecthomas/go-check-sumtype v0.4.0 // indirect
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
github.com/alexkohler/prealloc v1.1.0 // indirect
github.com/alfatraining/structtag v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/alingse/nilnesserr v0.2.0 // indirect
github.com/andybalholm/brotli v1.2.2 // indirect
github.com/andybalholm/brotli v1.2.3 // indirect
github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect
github.com/ashanbrown/makezero/v2 v2.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@@ -95,7 +95,7 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charithe/durationcheck v0.0.11 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260812204455-68fa937c71be // indirect
github.com/charmbracelet/ultraviolet v0.0.0-20260903151058-ae99b731b8c5 // indirect
github.com/charmbracelet/x/ansi v0.11.8 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/charmbracelet/x/termios v0.1.1 // indirect
@@ -112,7 +112,7 @@ require (
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.2 // indirect
github.com/ebitengine/purego v0.11.0 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
@@ -121,14 +121,14 @@ require (
github.com/fxamacker/cbor/v2 v2.9.3 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/ghostiam/protogetter v0.3.21 // indirect
github.com/ghostiam/protogetter v1.0.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8 // indirect
github.com/go-critic/go-critic v0.14.4 // indirect
github.com/go-critic/go-critic v0.15.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.0 // indirect
github.com/go-openapi/spec v0.22.9 // indirect
github.com/go-jose/go-jose/v4 v4.1.5 // indirect
github.com/go-openapi/jsonpointer v1.0.1 // indirect
github.com/go-openapi/jsonreference v1.0.2 // indirect
github.com/go-openapi/spec v1.0.1 // indirect
github.com/go-openapi/swag/conv v0.29.1 // indirect
github.com/go-openapi/swag/jsonutils v0.29.1 // indirect
github.com/go-openapi/swag/loading v0.29.1 // indirect
@@ -149,23 +149,24 @@ require (
github.com/go-webauthn/x v0.3.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/godoc-lint/godoc-lint v0.11.2 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gohugoio/hashstructure v1.0.0 // indirect
github.com/godoc-lint/godoc-lint v0.11.4 // indirect
github.com/gofrs/flock v0.13.1 // indirect
github.com/gohugoio/hashstructure v1.1.0 // indirect
github.com/gohugoio/hugo v0.165.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/geo v0.0.0-20260818125358-b200a1149890 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golangci/asciicheck v0.5.0 // indirect
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 // indirect
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect
github.com/golangci/go-printf-func-name v0.1.1 // indirect
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 // indirect
github.com/golangci/golangci-lint/v2 v2.13.1 // indirect
github.com/golangci/golangci-lint/v2 v2.13.2 // indirect
github.com/golangci/golines v0.15.0 // indirect
github.com/golangci/misspell v0.8.0 // indirect
github.com/golangci/plugin-module-register v0.1.2 // indirect
github.com/golangci/revgrep v0.8.0 // indirect
github.com/golangci/rowserrcheck v0.0.0-20260801150524-d2031e3af752 // indirect
github.com/golangci/rowserrcheck v0.0.0-20260901154044-241134db7241 // indirect
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
github.com/google/go-tpm v0.9.8 // indirect
@@ -190,7 +191,6 @@ require (
github.com/kovidgoyal/go-shm v1.0.0 // indirect
github.com/kulti/thelper v0.7.1 // indirect
github.com/kunwardeep/paralleltest v1.0.15 // indirect
github.com/lasiar/canonicalheader v1.1.2 // indirect
github.com/ldez/exptostd v0.4.5 // indirect
github.com/ldez/gomoddirectives v0.9.0 // indirect
github.com/ldez/grignotin v0.10.1 // indirect
@@ -208,7 +208,7 @@ require (
github.com/matoous/godox v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.28 // indirect
github.com/mattn/go-runewidth v0.0.29 // indirect
github.com/mgechev/revive v1.16.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moricho/tparallel v0.3.2 // indirect
@@ -224,15 +224,15 @@ require (
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.24.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.70.1 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/prometheus/client_model v0.6.3 // indirect
github.com/prometheus/common v0.71.0 // indirect
github.com/prometheus/procfs v0.22.0 // indirect
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/raeperd/recvcheck v0.3.0 // indirect
github.com/raeperd/recvcheck v0.3.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.16.0 // indirect
@@ -246,8 +246,8 @@ require (
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
github.com/securego/gosec/v2 v2.28.0 // indirect
github.com/sirupsen/logrus v1.10.1 // indirect
github.com/securego/gosec/v2 v2.29.0 // indirect
github.com/sirupsen/logrus v1.10.2 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sonatard/noctx v0.5.1 // indirect
github.com/sourcegraph/go-diff v0.8.0 // indirect
@@ -263,7 +263,7 @@ require (
github.com/tdewolff/parse/v2 v2.8.16 // indirect
github.com/tetafro/godot v1.5.6 // indirect
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c // indirect
github.com/timonwong/loggercheck v0.11.0 // indirect
github.com/timonwong/loggercheck v0.12.0 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
@@ -271,7 +271,7 @@ require (
github.com/ultraware/whitespace v0.2.0 // indirect
github.com/urfave/cli/v2 v2.27.7 // indirect
github.com/uudashr/gocognit v1.2.1 // indirect
github.com/uudashr/iface v1.5.0 // indirect
github.com/uudashr/iface v1.5.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xen0n/gosmopolitan v1.3.0 // indirect
github.com/xo/terminfo v1.0.0 // indirect
@@ -290,17 +290,17 @@ require (
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
golang.org/x/exp/typeparams v0.0.0-20260820142414-ca536658362e // indirect
golang.org/x/exp/typeparams v0.0.0-20260824195058-e88cd73687aa // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/text v0.41.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
honnef.co/go/tools v0.8.1 // indirect
modernc.org/libc v1.75.4 // indirect
modernc.org/libc v1.75.7 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.12.1 // indirect
mvdan.cc/gofumpt v0.11.0 // indirect
mvdan.cc/unparam v0.0.0-20260820064413-a201bf8e0538 // indirect
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
@@ -309,3 +309,11 @@ tool (
github.com/golangci/golangci-lint/v2/cmd/golangci-lint
github.com/swaggo/swag/cmd/swag
)
exclude (
github.com/alecthomas/go-check-sumtype v0.5.0
github.com/gobwas/glob v1.0.0
github.com/gtsteffaniak/go-ffmpeg v1.0.0
github.com/gtsteffaniak/go-ffmpeg v1.1.0
github.com/gtsteffaniak/go-ffmpeg v1.2.0
)
+76 -78
View File
@@ -16,8 +16,8 @@ dev.gaijin.team/go/exhaustruct/v5 v5.0.3 h1:yOeA7DNjlT8y4yfmN6nWWYYggA13N523YAj9
dev.gaijin.team/go/exhaustruct/v5 v5.0.3/go.mod h1:KwtBsX8nHHH1YxhxkpiBq6bfsmw5WnazWpNvJPHgY9Y=
dev.gaijin.team/go/golib v0.8.1 h1:JYju4x9BSo+QD/AYeHULVDcvEhiFg8wOi6pT0IaZF5E=
dev.gaijin.team/go/golib v0.8.1/go.mod h1:c5fu7t1RSGMxSQgcUYO1sODbzsYnOCXJLmHeNG1Eb+0=
github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8=
github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
github.com/4meepo/tagalign v1.4.4 h1:052JDGj2pBORc6NkD30dDXpsUpoYszDeUwr9/5YZuVA=
github.com/4meepo/tagalign v1.4.4/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c=
github.com/Abirdcfly/dupword v0.1.8 h1:SrhcUuGsROBuChFxHALRYzyyPODWn9zwghmzPvD9Cd8=
github.com/Abirdcfly/dupword v0.1.8/go.mod h1:XZrhVnI7YGpsTiWZANSQaBJ4QpL/Tq5vIEdKJJAs9WI=
github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo=
@@ -62,8 +62,8 @@ github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8v
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU=
github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E=
github.com/alecthomas/go-check-sumtype v0.4.0 h1:gd4mE4QsA62sXlnFIs8GUv8/0xH3DOUqTkYQR1l2v28=
github.com/alecthomas/go-check-sumtype v0.4.0/go.mod h1:aYYllI8IXNjo7ZYUzI2+wyLRECMg3zo+8CZj9JTBAWU=
github.com/alecthomas/kong v1.16.1 h1:ixhCt93XkJ98kGposQ54+bl0IK6XwqB40AsMynU7Z8E=
github.com/alecthomas/kong v1.16.1/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
@@ -80,8 +80,8 @@ github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQ
github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I=
github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w=
github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/brotli v1.2.3 h1:8H1qwOkl2LPfjf3YezB90JnCliZb6SInJ/OJkEbA5NQ=
github.com/andybalholm/brotli v1.2.3/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/asdine/storm/v3 v3.2.1 h1:I5AqhkPK6nBZ/qJXySdI7ot5BlXSZ7qvDY1zAn5ZJac=
github.com/asdine/storm/v3 v3.2.1/go.mod h1:LEpXwGt4pIqrE/XcTvCnZHT5MgZCV6Ub9q7yQzOFWr0=
github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTiLh/8JROI=
@@ -153,8 +153,8 @@ github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1Di
github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4=
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/ultraviolet v0.0.0-20260812204455-68fa937c71be h1:qEvkJy1sjJXP+yf8IH2o13NV+Rh4nIUHjaLNJ+pWxpI=
github.com/charmbracelet/ultraviolet v0.0.0-20260812204455-68fa937c71be/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
github.com/charmbracelet/ultraviolet v0.0.0-20260903151058-ae99b731b8c5 h1:omidyaLW3GwtN5N35bPZWCxygynfhMRO2dTJ+OCcjLg=
github.com/charmbracelet/ultraviolet v0.0.0-20260903151058-ae99b731b8c5/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro=
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
@@ -171,8 +171,8 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/coreos/go-oidc/v3 v3.20.0 h1:EtE0WIBHk03N+DqGkY4+UONzzZHk7amKt6IyNd7OsZE=
github.com/coreos/go-oidc/v3 v3.20.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/coreos/go-oidc/v3 v3.21.0 h1:wZo4Q9Pum8dYEj0eMUPrqR+kvuGkeUplbLpNCkBqoWM=
github.com/coreos/go-oidc/v3 v3.21.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -215,8 +215,8 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.11.0 h1:jhp/D+Nyv7UUW8HAcmcjt2N2rYrYi9m3SL21k0Ua/NI=
github.com/ebitengine/purego v0.11.0/go.mod h1:DCHPP08djqhNSoTfImcnHYQRZmd0qhakvrozqaEYhGQ=
github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q=
github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A=
github.com/evanoberholster/imagemeta v1.0.0 h1:FlSihlsjkWIoS83wmgPOP3CbAxlRpxM8jQGZgLu+MWQ=
@@ -244,30 +244,30 @@ github.com/gen2brain/go-fitz v1.28.2 h1:845G85N5TUgnq5oDqyYrW0JvehAkeo35UkkK2dJt
github.com/gen2brain/go-fitz v1.28.2/go.mod h1:pY2hqAjp9Zy7qfPI2gwbJMHBFAdZpVXOLrRxD82l3Bs=
github.com/getkin/kin-openapi v0.146.0 h1:RA/1RdxrSJW4oc1+6IfnYB6AO9CaGy8GTKPh0k4Ordo=
github.com/getkin/kin-openapi v0.146.0/go.mod h1:3BH9M9XDe/y9M5DSvEocVYAYq1w0qrhJHjC/vZi0AaY=
github.com/ghostiam/protogetter v0.3.21 h1:EeWTGvL/Eyosp653hiWb6Byx4b69iJC4/E+za7vQHoI=
github.com/ghostiam/protogetter v0.3.21/go.mod h1:iAKSpyoHwYzay+OpjoWgwzRtPFthEfuUvmlomTThck0=
github.com/ghostiam/protogetter v1.0.0 h1:g1s6hbyrNJKYJ2X/RcLMmfDo9afmxQe1MRmBitfxSAs=
github.com/ghostiam/protogetter v1.0.0/go.mod h1:50Fh5yZfs6SFGrd1FlmT04NcguyEOxyjNK5zGD6/EvM=
github.com/go-asn1-ber/asn1-ber v1.5.8 h1:H9AZkK22UOmfX8J84ubyaZxKJZ3FMHVwn8swoMML7iQ=
github.com/go-asn1-ber/asn1-ber v1.5.8/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-critic/go-critic v0.14.4 h1:dSX4C3pWSeuMVxvQh6yG8U0ReSf3YOmKi4nwX5q7n/8=
github.com/go-critic/go-critic v0.14.4/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
github.com/go-critic/go-critic v0.15.0 h1:MDUZIQKytIl+vPEc0Igw51m4uLTdi1ZGz2jatEqNs1g=
github.com/go-critic/go-critic v0.15.0/go.mod h1:3BPrw8nHATGm3o2n37cssOmJzpySGKCdY8JWkCzxgKQ=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA=
github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-ldap/ldap/v3 v3.4.14 h1:D6PYdEgsaVzsXyr6w/yDC06Ria4uUhWm+Rb+er8lfAs=
github.com/go-ldap/ldap/v3 v3.4.14/go.mod h1:S4eJUMUNjDkE0ZJtIZdybwyb03sGGLW6gxXT1Hs8VKA=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
github.com/go-openapi/spec v0.22.9 h1:/vKIFDcGKp0ktZWGbym/tJEWbk6/XOEmAVU0kqKMH+w=
github.com/go-openapi/spec v0.22.9/go.mod h1:b/mNUYIOQOyIiUzUzXEE8xzyZqf93KvM9hQGP91yfl0=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v1.0.1 h1:2KxywRmNwJkT/FMBa3iRNHEaAxSJvjqoufQZy3au1Mg=
github.com/go-openapi/jsonpointer v1.0.1/go.mod h1:wI7ZYsFmbIi9nBXOZqgDaS/bqOchRGZjqxFli7FBYxY=
github.com/go-openapi/jsonreference v1.0.2 h1:oS4et8FOf3p3UQxEo4Xt0esijmBUM+F259Xl72OSZsc=
github.com/go-openapi/jsonreference v1.0.2/go.mod h1:TbUNSOo+fcorZjFaNoiSDSoaNnnZtqJtLGR1PuvE/Cs=
github.com/go-openapi/spec v1.0.1 h1:lj2vdGpNDcVgwRc6qXdw6qt/KQpCtSa9tnUH6vpDPDk=
github.com/go-openapi/spec v1.0.1/go.mod h1:M//GWQGtDUAjnP37gE6fInLgaczB+FatoipV3H1fYw8=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.29.1 h1:AC4Eh/5c/eUDOUCzzsRC9ghmFgOSBHeRMGIngY0ZUGA=
github.com/go-openapi/swag/conv v0.29.1/go.mod h1:S1X7/ZrBEZOC0Wc8AGxjbcGS92l3WEjA7aPtpl+RaqM=
@@ -285,18 +285,18 @@ github.com/go-openapi/swag/typeutils v0.29.1 h1:Nzv9nhnlLCRBPQqfOX+7lB6Guju370or
github.com/go-openapi/swag/typeutils v0.29.1/go.mod h1:hxpgDZJVBkBsi/d3MIUosafoFdE5exaQRmVp0zwu3YE=
github.com/go-openapi/swag/yamlutils v0.29.1 h1:69w3tsBajm7MR/fejLy7HD/3J68Ys1SeeZMEzZ3w2sk=
github.com/go-openapi/swag/yamlutils v0.29.1/go.mod h1:rgsp3vT/QdWzKwn43CigDwjOGIenPyTZMKnxEM8jZOA=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.1 h1:Jm+/ze2rMtbD98yen92AhATGLGREDYXG56Xr4gMjEtE=
github.com/go-openapi/testify/enable/yaml/v2 v2.6.1/go.mod h1:YDPnwCRDu38/oJBVMBVXOUDiJ9cIeBHWvfImHaXqnv4=
github.com/go-openapi/testify/v2 v2.6.1 h1:6CNJhTjMzgaeaH8WhshcsZNPIvRemiOcFpU7seO/y7Q=
github.com/go-openapi/testify/v2 v2.6.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-openapi/testify/enable/yaml/v2 v2.7.0 h1:wPW6YRgx3+SID1yUy/Xwa17L8kFEaEKod2VRbJDZNUs=
github.com/go-openapi/testify/enable/yaml/v2 v2.7.0/go.mod h1:mI1M88etYbc3PhgHsWQK2kwvNwW5aGFqMPbmib+SGIs=
github.com/go-openapi/testify/v2 v2.7.0 h1:bycOreEj6wfBvijg3YFogZ/sFjTCDmQnwSodSzHa3X8=
github.com/go-openapi/testify/v2 v2.7.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
github.com/go-playground/validator/v10 v10.30.4 h1:9Rcod2ZPO6mOEG6b4GqyoHE/H6//Ze0RuhOo1hT1x0w=
github.com/go-playground/validator/v10 v10.30.4/go.mod h1:numpT+RPLE91R9oYWMY/R9zRgJBewr3IXHko4OISPpk=
github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474=
github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
@@ -322,8 +322,8 @@ github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUN
github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-webauthn/webauthn v0.17.4 h1:KFTSz3R2RYDiUn/0cDi3XTJgFenSG74eKTTHlqWhlxk=
github.com/go-webauthn/webauthn v0.17.4/go.mod h1:pZk63EE/BdztlmyS4Yc+9H5g4a8blNlbtGmdHQHbZX8=
github.com/go-webauthn/webauthn v0.18.0 h1:PC8R3PNLEmjZf++WwcQlo1Z39S9rf8ma69rlwkypZhA=
github.com/go-webauthn/webauthn v0.18.0/go.mod h1:ymzZQhx3D/PrDjznemBdQJ23gHTaSDxUchM7sH1lUCg=
github.com/go-webauthn/x v0.3.0 h1:Q2X9vbrlP0Ed+QGEzixh1hthGZlDnzVT0XH/9IIQ0kE=
github.com/go-webauthn/x v0.3.0/go.mod h1:5OkdSQdOy7taRXWqvNHggtaPffmW94ybu3rZEER4I+I=
github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY=
@@ -334,18 +334,18 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godoc-lint/godoc-lint v0.11.2 h1:Bp0FkJWoSdNsBikdNgIcgtaoo+xz6I/Y9s5WSBQUeeM=
github.com/godoc-lint/godoc-lint v0.11.2/go.mod h1:iVpGdL1JCikNH2gGeAn3Hh+AgN5Gx/I/cxV+91L41jo=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/godoc-lint/godoc-lint v0.11.4 h1:N9zUYVJ/OkznBbrBsU/zODvua64EUplMBLQWStO3+1M=
github.com/godoc-lint/godoc-lint v0.11.4/go.mod h1:7usTXoxS0QYIi5usTSCPwR0w/6xLjcz450shXjLn3t4=
github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
github.com/gohugoio/gift v0.2.0 h1:vA31pP0rTVmBxBrhpY3WEt+4zM4g+1sDqYeemwsYeqc=
github.com/gohugoio/gift v0.2.0/go.mod h1:1Mrm5CjF33KpD749Dwj+UAjWZ3LC6cBXGuTMa5XwoP4=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6 h1:pxlAea9eRwuAnt/zKbGqlFO2ZszpIe24YpOVLf+N+4I=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20251018145728-cfcc22d823c6/go.mod h1:m5hu1im5Qc7LDycVLvee6MPobJiRLBYHklypFJR0/aE=
github.com/gohugoio/go-radix v1.2.0 h1:D5GTk8jIoeXirBSc2P4E4NdHKDrenk9k9N0ctU5Yrhg=
github.com/gohugoio/go-radix v1.2.0/go.mod h1:k6vDa0ebpbpgtzSj9lPGJcA4AZwJ9xUNObUy2vczPFM=
github.com/gohugoio/hashstructure v1.0.0 h1:vWYuyzs1n0LdI0F54TJQeYAiB44fHX7H9hCp9X6gHKg=
github.com/gohugoio/hashstructure v1.0.0/go.mod h1:FSbTK4QwxucJ2bC4Lvrs9a6x0DbQDXNoyBO+h4nlCgE=
github.com/gohugoio/hashstructure v1.1.0 h1:38yUfZBca6qXSbUpteLhjDGLNskclHaguFBYpjaRjf4=
github.com/gohugoio/hashstructure v1.1.0/go.mod h1:Pz8dcwjZs6FBKWu9x/ZIChrTHIM175zfUJK0KLvC1z8=
github.com/gohugoio/httpcache v0.8.0 h1:hNdsmGSELztetYCsPVgjA960zSa4dfEqqF/SficorCU=
github.com/gohugoio/httpcache v0.8.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI=
github.com/gohugoio/hugo v0.165.0 h1:E9uRFnDw3E+Nv69OU8vsD1YLI3+tRcSHhs82y4fBXdc=
@@ -371,14 +371,16 @@ github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0=
github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ=
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6 h1:fVLolA3dG6s0brGetsiDAtcnpMSwVa2LqXJEw/9RJG4=
github.com/golangci/canonicalheader v0.0.0-20260827115959-a25c71c521f6/go.mod h1:1xo+NFW5S+bEf2DKXhaxuzvcDN5AR6o/KMwvqC5Mkpo=
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A=
github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E=
github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U=
github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss=
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 h1:WL8YKrt3UbOBqSRU7GpP5BTtQTMWtVtj+mfPijgZeIg=
github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792/go.mod h1:te5hX0dW4C5r6YbXs+6ysNr8Q5UTmdIqGbb+mlFiYmA=
github.com/golangci/golangci-lint/v2 v2.13.1 h1:RuM4OcluM4xFQcGuRE6R7jA33pqxK/W1EsBxpugdZjg=
github.com/golangci/golangci-lint/v2 v2.13.1/go.mod h1:HwX7mDzqHbcSxlhrTygjX1GJbAfQ3sJAqOx41qQlhDE=
github.com/golangci/golangci-lint/v2 v2.13.2 h1:bCyq3E4vo9qwzifjpzJqYndEt7Ncva80qkk8G2B4TXU=
github.com/golangci/golangci-lint/v2 v2.13.2/go.mod h1:5xaMd1kAxV7GSBPEyngw8gnnjoRocqW9UVKdBR6627w=
github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0=
github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10=
github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg=
@@ -387,8 +389,8 @@ github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3H
github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw=
github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s=
github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k=
github.com/golangci/rowserrcheck v0.0.0-20260801150524-d2031e3af752 h1:ivRL3tv3jTvJ6L9W3mdX8i+IuT8CxvEtsIfeH93Iqk4=
github.com/golangci/rowserrcheck v0.0.0-20260801150524-d2031e3af752/go.mod h1:dGge5Cok/D3UixuSKYyrqg7QpdmTJrdHAqu9OiiYWiw=
github.com/golangci/rowserrcheck v0.0.0-20260901154044-241134db7241 h1:0kPqfqytQizeQ9WBk487NcsGcRdS8YhHeUhXxkfEAhA=
github.com/golangci/rowserrcheck v0.0.0-20260901154044-241134db7241/go.mod h1:lczq5oJ8PxpMESQG6dO3iFYTQPhpqoMOYw//n2FEnHA=
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM=
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s=
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM=
@@ -425,8 +427,6 @@ github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+
github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs=
github.com/gtsteffaniak/go-cache v1.1.0 h1:ni/2cJpCxpGmaMZd92iTDIGOy+yUcMcxQuW7bSKhkCE=
github.com/gtsteffaniak/go-cache v1.1.0/go.mod h1:rHUZvf4huhMX/bnNK4/oQ3sNAE2fYC2jFpAWZiBPWac=
github.com/gtsteffaniak/go-ffmpeg v0.5.1 h1:gpKXi/0mn0j1Ug3v9x21tqbkT/2EZ6cDB1Ngqe8YfjI=
github.com/gtsteffaniak/go-ffmpeg v0.5.1/go.mod h1:FefhoD+KZzHWqBaUpntcKp6lXtUzdFW00F74axnENUQ=
github.com/gtsteffaniak/go-ffmpeg v0.5.2 h1:KVnKM3zewY9yeZVd59Hb0/10gSbhYf01yRM5RftXGEU=
github.com/gtsteffaniak/go-ffmpeg v0.5.2/go.mod h1:FefhoD+KZzHWqBaUpntcKp6lXtUzdFW00F74axnENUQ=
github.com/gtsteffaniak/go-logger v1.1.0 h1:baFds+iibuLLNpNLwygz32bDqdsunI6dGWcdxwMIVKI=
@@ -495,8 +495,6 @@ github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJ
github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk=
github.com/kyokomi/emoji/v2 v2.2.14 h1:YOF6VL52613M0Qr9v4puJDD9QQPmyyjXedDDlrGzH80=
github.com/kyokomi/emoji/v2 v2.2.14/go.mod h1:1AnYl9IgmJZXKd5m1PEijyyUw85SqYsuAr8lpU/s+9s=
github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4=
github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI=
github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ=
github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM=
github.com/ldez/gomoddirectives v0.9.0 h1:2YV/EX7nVlWL4jySusYTzBKHuE3D2fgcRsQuMa3yIoo=
@@ -537,8 +535,8 @@ github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mattn/go-runewidth v0.0.29 h1:3oGF3R/S2N9DQ3ptftzVIvg2eicmojCzlwBEmqEPDfQ=
github.com/mattn/go-runewidth v0.0.29/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mattn/go-sqlite3 v1.14.50 h1:dmdFvo1XG4MPzA4IkAmE9upVz/Nj31uRoM5+jC8hYbY=
github.com/mattn/go-sqlite3 v1.14.50/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/mgechev/revive v1.16.0 h1:9OZVO8xOPUwIYynplHXv7liMllPQ/BTNZLp07sM/Im0=
@@ -581,8 +579,8 @@ github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E=
github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
github.com/onsi/ginkgo/v2 v2.32.1 h1:6tlvcDm/3sE8lGJbZ4+d4mO3RLy24/tQWOFzVSQNIfw=
github.com/onsi/ginkgo/v2 v2.32.1/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44=
github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I=
github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg=
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
@@ -607,12 +605,12 @@ github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
github.com/prometheus/common v0.71.0 h1:9KDAKb7Mj3HEVKyFCK6Dc/HIwlBzZIN2l7/lrHl3KK8=
github.com/prometheus/common v0.71.0/go.mod h1:CLJ5H8TEsGX8bl31BdMkfhIZ+QmZ9tBPPotUxUbfcmk=
github.com/prometheus/procfs v0.22.0 h1:6q9+/JL9IKAPbCmBrv9n5O5Ty3NKnciV5X7YGw0oics=
github.com/prometheus/procfs v0.22.0/go.mod h1:CvmFr/GVhIjIvWJZW3tgkODBQMRIf0EyWMQLHCHab58=
github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA=
github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=
github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY=
@@ -623,8 +621,8 @@ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs=
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ=
github.com/raeperd/recvcheck v0.3.0 h1:PM+XYvyxIj3bo+kobJfFTdTuU3Lmfu96mKDbyHDbRt8=
github.com/raeperd/recvcheck v0.3.0/go.mod h1:PZNwG+HztFYMH2ZPq0Hu3QgkV2yiA6VrtNz9c1fXWJo=
github.com/raeperd/recvcheck v0.3.1 h1:p7nZMxiCWkdUtY2i3o+pv1h4fJCR5gpvqAXr9326hEY=
github.com/raeperd/recvcheck v0.3.1/go.mod h1:CbE7iRocQ4uc71JwpW/CPSh4BShnkre4A9Gb508RypU=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
@@ -651,12 +649,12 @@ github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tM
github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ=
github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ=
github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8=
github.com/securego/gosec/v2 v2.28.0 h1:ZsSdiDb0AtTpLFVol5z91gbMei9ZiLEPG/pZjZujp7c=
github.com/securego/gosec/v2 v2.28.0/go.mod h1:lb4/9AHe+lJy/kjWmWRWWsEipvbwGKuxf+tY1Pmjdnk=
github.com/securego/gosec/v2 v2.29.0 h1:pF2HSLcnY5voqpxQumEe0O5YAAbvF4Syu4qeIewpLyc=
github.com/securego/gosec/v2 v2.29.0/go.mod h1:47C7ej+L2bC1NOxteU2aCGnCi4cZaoKzRt5y9plIvuY=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q=
github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk=
github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo=
github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q=
github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE=
github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4=
github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs=
@@ -711,8 +709,8 @@ github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZ
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c h1:d/FUP8GD4QiY/rdFzZmBu3KqL9eBNVzMu7W/5pGkaMM=
github.com/timakin/bodyclose v0.0.0-20260723120731-857993a2939c/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M=
github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M=
github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8=
github.com/timonwong/loggercheck v0.12.0 h1:7Cb9XGLJZSWYNjgL459DIhpSiEHupD+MLQ6vaN2Pl3g=
github.com/timonwong/loggercheck v0.12.0/go.mod h1:Wa6Y+SJHQR01mViZlLLY5GAZVOWOLNMbkXddMjFWQCs=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is=
@@ -727,8 +725,8 @@ github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4=
github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q=
github.com/uudashr/iface v1.5.0 h1:PgdMt4uAettGG8K/Kbamc4B9FABgUgnS3TLbl6fnjEk=
github.com/uudashr/iface v1.5.0/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg=
github.com/uudashr/iface v1.5.1 h1:BS3yrgaT55s3dAtJoxuTnsZNbsejq/mVuzp49YDyfBA=
github.com/uudashr/iface v1.5.1/go.mod h1:5UWoT6SvTdTww/KToRj6clO+n4Kg3/NrAQvgOB+5Ggw=
github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI=
github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -788,14 +786,14 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
golang.org/x/exp/typeparams v0.0.0-20260820142414-ca536658362e h1:4If57HMRswXp1wjYKdxwh795kBkFIzRawdB6rJHlLQQ=
golang.org/x/exp/typeparams v0.0.0-20260820142414-ca536658362e/go.mod h1:qkMnvgP8C7+xkci04txL2SJFc/0PGqnkf6gs5JUC43E=
golang.org/x/exp/typeparams v0.0.0-20260824195058-e88cd73687aa h1:0crASk1XE9p9RP0n9EbRutyWZoOkzTSuwTp3mqwppb0=
golang.org/x/exp/typeparams v0.0.0-20260824195058-e88cd73687aa/go.mod h1:qkMnvgP8C7+xkci04txL2SJFc/0PGqnkf6gs5JUC43E=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -918,8 +916,8 @@ modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc=
modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.75.4 h1:EHQJNYDC6LiDIOqM76862xe4frbDc7IzEOZTtGxZV8Q=
modernc.org/libc v1.75.4/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
modernc.org/libc v1.75.7 h1:o3DTP9/0p9pKmY2WCKQaySW6wIiZhNM7wc2lUoyhfew=
modernc.org/libc v1.75.7/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
@@ -928,16 +926,16 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.57.0 h1:qNQP6xnx5M0ISNtlnxoOX0+cD5bJ0/gr9aMmndFczzg=
modernc.org/sqlite v1.57.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
modernc.org/sqlite v1.58.0 h1:38u40/bwkfM7f0Myhosl+SEMltSDxnGdQf8o6Kjmys0=
modernc.org/sqlite v1.58.0/go.mod h1:rsD2CckafgObKC4DhBlGBf+RiHxkc3hINGt1Xw32tVY=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
mvdan.cc/gofumpt v0.11.0 h1:0H01XB95PnN2QgCSR9ELdZyTlJqNZ7181B0BTMh5VZc=
mvdan.cc/gofumpt v0.11.0/go.mod h1:BeT5wCsOJt6J9zT2MZIOGszjUHzFkn1/l9g6xAzqsXo=
mvdan.cc/unparam v0.0.0-20260820064413-a201bf8e0538 h1:gAyMBEFTs+oVyKRmgsn+P9WlYFFZ6EI8dpgu7bCulCo=
mvdan.cc/unparam v0.0.0-20260820064413-a201bf8e0538/go.mod h1:MrS/+zJ1M2xvGXhKktiHbNQeQPyg3Qel+KkzT+f7/i4=
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8 h1:Re1NRyLpiAt9kB+ImaaoapwWiQXrKwER4tY8fLOkDew=
mvdan.cc/unparam v0.0.0-20260823230713-2fa3d841b0c8/go.mod h1:MrS/+zJ1M2xvGXhKktiHbNQeQPyg3Qel+KkzT+f7/i4=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
+7 -3
View File
@@ -15,13 +15,13 @@ import (
"github.com/dhowden/tag"
"github.com/gtsteffaniak/filebrowser/backend/internal/adapters/fs/fileutils"
"github.com/gtsteffaniak/filebrowser/backend/internal/errors"
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
"github.com/gtsteffaniak/filebrowser/backend/internal/database/users"
"github.com/gtsteffaniak/filebrowser/backend/internal/errors"
"github.com/gtsteffaniak/filebrowser/backend/internal/ffmpeg"
"github.com/gtsteffaniak/filebrowser/backend/internal/utils"
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing"
"github.com/gtsteffaniak/filebrowser/backend/pkg/indexing/iteminfo"
"github.com/gtsteffaniak/filebrowser/backend/pkg/settings"
"github.com/gtsteffaniak/go-logger/logger"
)
@@ -403,6 +403,10 @@ func processContent(info *iteminfo.ExtendedFileInfo, idx *indexing.Index, opts u
return
}
if !opts.Content {
return
}
// Process text content for non-video, non-audio files
if info.Size < 20*1024*1024 { // 20 megabytes in bytes
content, err := getContent(info.RealPath)
+1 -2
View File
@@ -222,6 +222,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
"title": title,
"customCSS": template.CSS(settings.Config.Frontend.Styling.CustomCSSRaw),
"userSelectedTheme": template.CSS(userSelectedTheme),
"defaultDarkMode": settings.DefaultDarkMode(),
"lightBackground": settings.Config.Frontend.Styling.LightBackground,
"darkBackground": settings.Config.Frontend.Styling.DarkBackground,
"staticURL": staticURL,
@@ -247,8 +248,6 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
"minSearchLength": settings.Config.Server.MinSearchLength,
"disableExternal": settings.Config.Frontend.DisableDefaultLinks,
"darkMode": settings.Config.UserDefaults.UI.DarkMode,
"lightBackground": settings.Config.Frontend.Styling.LightBackground,
"darkBackground": settings.Config.Frontend.Styling.DarkBackground,
"baseURL": settings.Config.Http.BaseURL,
"version": versionString,
"commitSHA": commitSHAString,
+2 -2
View File
@@ -417,7 +417,7 @@ func setupSources(generate bool) {
if source.Config.Disabled {
continue
}
realPath, err := filepath.Abs(source.Path)
realPath, err := AbsPath(source.Path)
if err != nil {
logger.Fatalf("error getting real path for source %v: %v", source.Path, err)
}
@@ -975,7 +975,7 @@ func SetDefaults(generate bool) Settings {
// validateCustomImage validates a custom image file path and returns the absolute path or error
func validateCustomImage(configPath, imageName string, allowedFormats []string) (absolutePath string, err error) {
// Get absolute path
absolutePath, err = filepath.Abs(configPath)
absolutePath, err = AbsPath(configPath)
if err != nil {
return "", fmt.Errorf("could not resolve path: %w", err)
}
+38
View File
@@ -0,0 +1,38 @@
package settings
import (
"fmt"
"os"
"path/filepath"
)
// ExpandTilde replaces a leading ~ or ~/ with the user home directory.
func ExpandTilde(path string) (string, error) {
if path == "" || path[0] != '~' {
return path, nil
}
homeDir, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("failed to expand home directory: %w", err)
}
if path == "~" {
return homeDir, nil
}
if path[1] == '/' || path[1] == '\\' {
return filepath.Join(homeDir, path[2:]), nil
}
return path, nil
}
// AbsPath expands a leading tilde, then resolves the path to an absolute path.
func AbsPath(path string) (string, error) {
expanded, err := ExpandTilde(path)
if err != nil {
return "", err
}
return filepath.Abs(expanded)
}
+54
View File
@@ -0,0 +1,54 @@
package settings
import (
"os"
"path/filepath"
"testing"
)
func TestExpandTilde(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil {
t.Fatalf("UserHomeDir: %v", err)
}
tests := []struct {
name string
in string
want string
}{
{name: "empty", in: "", want: ""},
{name: "absolute", in: "/abs/path", want: "/abs/path"},
{name: "relative", in: "relative/path", want: "relative/path"},
{name: "tilde only", in: "~", want: home},
{name: "tilde slash", in: "~/", want: filepath.Join(home, "")},
{name: "tilde subpath", in: "~/Documents", want: filepath.Join(home, "Documents")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ExpandTilde(tt.in)
if err != nil {
t.Fatalf("ExpandTilde(%q): %v", tt.in, err)
}
if got != tt.want {
t.Fatalf("ExpandTilde(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
func TestAbsPath_expandsTilde(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil {
t.Fatalf("UserHomeDir: %v", err)
}
got, err := AbsPath("~/")
if err != nil {
t.Fatalf("AbsPath: %v", err)
}
if got != home {
t.Fatalf("AbsPath(\"~/\") = %q, want %q", got, home)
}
}
+6 -1
View File
@@ -119,9 +119,14 @@ func readCustomCSS(path string) (string, error) {
return string(content), nil
}
// DefaultDarkMode reports whether the instance default theme is dark.
func DefaultDarkMode() bool {
return Config.UserDefaults.UI.DarkMode != nil && *Config.UserDefaults.UI.DarkMode
}
// DefaultBackgroundColor returns the background color of the instance's default theme.
func DefaultBackgroundColor() string {
if Config.UserDefaults.UI.DarkMode != nil && *Config.UserDefaults.UI.DarkMode {
if DefaultDarkMode() {
return Config.Frontend.Styling.DarkBackground
}
return Config.Frontend.Styling.LightBackground
+1 -11
View File
@@ -17,17 +17,7 @@ import (
// - test.yaml + database-test.yaml + frontend-test.yaml
// - myapp.yml + server-myapp.yml
func combineYAMLFiles(configFilePath string) ([]byte, error) {
// Get absolute path and expand tilde
expandedPath := configFilePath
if strings.HasPrefix(configFilePath, "~/") {
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to expand home directory: %v", err)
}
expandedPath = filepath.Join(homeDir, configFilePath[2:])
}
absPath, err := filepath.Abs(expandedPath)
absPath, err := AbsPath(configFilePath)
if err != nil {
return nil, fmt.Errorf("failed to resolve config file path: %v", err)
}
+1
View File
@@ -0,0 +1 @@
22
+367 -499
View File
File diff suppressed because it is too large. Load diff
+2 -3
View File
@@ -5,7 +5,7 @@
"type": "module",
"engines": {
"npm": ">=9.0.0",
"node": ">=20.0.0"
"node": "^22.12.0 || ^24.0.0 || >=26.0.0"
},
"scripts": {
"build": "rm -rf ../backend/internal/web/embed/* && vite build && cp -r ../backend/internal/web/dist/* ../backend/internal/web/embed",
@@ -31,7 +31,6 @@
"css-vars-ponyfill": "^2.4.3",
"dompurify": "^3.2.4",
"epubjs": "^0.3.93",
"glob": "^9.3.5",
"highlight.js": "^11.11.1",
"mammoth": "^1.9.1",
"marked": "^15.0.6",
@@ -69,7 +68,7 @@
"vite": "^6.2.0",
"vite-plugin-checker": "^0.10.3",
"vite-plugin-compression2": "^1.0.0",
"vitest": "^4.1.8",
"vitest": "^5.0.0",
"vue-eslint-parser": "^10.4.0",
"vue-tsc": "^2.0.7",
"wawoff2": "^2.0.1"
+2 -1
View File
@@ -1,4 +1,5 @@
import { defineConfig, devices } from "@playwright/test";
import type { PlaywrightFixtureOptions } from "./tests/playwright/test-setup";
/**
* Read environment variables from file.
@@ -9,7 +10,7 @@ import { defineConfig, devices } from "@playwright/test";
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
export default defineConfig<PlaywrightFixtureOptions>({
globalSetup: "./tests/playwright/global-setup",
timeout: 5000,
testDir: "./tests/playwright/settings",
+4 -7
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="{{ if .htmlVars.defaultDarkMode }}dark-mode{{ end }}">
<head>
<meta charset="utf-8">
@@ -49,9 +49,9 @@
<link rel="canonical" href="https://github.com/quantumx-apps">
<link rel="canonical" href="https://filebrowserquantum.com">
<!-- Injected variables come from backend server -->
<!-- Injected variables come from backend server (JSON block is not subject to script-src CSP) -->
<script id="app-config" type="application/json">{{ .globalVars }}</script>
<script nonce="{{ .cspNonce }}">
window.globalVars = {{ .globalVars }};
window.__pwaDeferredPrompt = null;
window.addEventListener("beforeinstallprompt", function (event) {
event.preventDefault();
@@ -74,12 +74,9 @@
}
/* paint safe areas + iOS overscroll with app background (notched PWAs) */
html {
background-color: {{ .htmlVars.lightBackground }};
background-color: var(--background);
min-height: calc(100% + env(safe-area-inset-top, 0px));
}
html:has(.dark-mode) {
background-color: {{ .htmlVars.darkBackground }};
}
.dark-mode {
--background: {{ .htmlVars.darkBackground }};
--alt-background: #283136;
+2 -2
View File
@@ -1,6 +1,6 @@
// scripts/build-icon-subset.js
import fs from 'fs-extra';
import { sync } from 'glob';
import { globSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import wawoff2 from 'wawoff2';
@@ -73,7 +73,7 @@ function addIcon(icons, value) {
}
function scanVueAndTemplates(icons) {
const files = sync(path.join(srcDir, '**/*.vue'));
const files = globSync(path.join(srcDir, '**/*.vue'));
const materialTag = new RegExp(
`material-symbols(?:-outlined)?[^>]*>\\s*(${ICON_NAME})\\s*<`,
'g',
+2 -2
View File
@@ -1,10 +1,10 @@
/* eslint-disable security/detect-object-injection */
// scripts/sync-translations.js
import path from 'node:path';
import { globSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { Translator } from 'deepl-node';
import fs from 'fs-extra';
import { sync } from 'glob';
// Parse command line arguments
const args = process.argv.slice(2);
@@ -18,7 +18,7 @@ const __dirname = path.dirname(__filename);
const localesDir = path.resolve(__dirname, '../src/i18n');
const masterLocaleFile = path.join(localesDir, 'en.json');
const masterLanguageCode = 'en';
const targetLocaleFiles = sync(path.join(localesDir, '*.json'))
const targetLocaleFiles = globSync(path.join(localesDir, '*.json'))
.filter(file => path.basename(file) !== `${masterLanguageCode}.json`)
const requireApiKey = !checkOnly && !enforceOrder && !cleanupOnly;
+5 -3
View File
@@ -17,10 +17,12 @@ export async function getAllUsers() {
}
}
// GET /public/api/users?username= (single user by login name)
export async function get(username) {
// GET /api/users?username= (single user by login name; use public: true for public routes)
export async function get(username, options = {}) {
try {
const apiPath = getPublicApiPath('users', { username })
const apiPath = options.public === true
? getPublicApiPath('users', { username })
: getApiPath('users', { username })
return await fetchJSON(apiPath)
} catch (err) {
notify.showError(err.message || `Failed to fetch user: ${username}`)
+6 -6
View File
@@ -206,6 +206,10 @@ import { resourcesApi, shareApi, usersApi } from "@/api";
import Action from "@/components/Action.vue";
import { notify } from "@/notify";
import { getters, mutations, state } from "@/store";
import {
hideInteractiveTooltip,
showHoverTooltip,
} from "@/utils/tooltipHelp.js";
import { url } from "@/utils";
import buttons from "@/utils/buttons";
import { copyToClipboard } from "@/utils/clipboard";
@@ -587,14 +591,10 @@ export default {
mutations.closeHovers();
},
hideTooltip() {
mutations.hideTooltip();
hideInteractiveTooltip();
},
showTooltip(event, text) {
mutations.showTooltip({
content: text,
x: event.clientX,
y: event.clientY,
});
showHoverTooltip(text, event);
},
onCreateToggleClick() {
if (this.createToggleDisabled) return;
@@ -0,0 +1,94 @@
<template>
<i
:class="[iconClasses, { 'tooltip-info-icon--pressed': pressed }]"
role="button"
tabindex="0"
:aria-label="ariaLabel || undefined"
@click.stop="onClick"
@touchstart.stop="onTouchStart"
@touchend.stop="onTouchEnd"
@touchcancel.stop="onTouchCancel"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
{{ icon }}
</i>
</template>
<script>
import {
onTooltipHelpClick,
onTooltipHelpMouseEnter,
onTooltipHelpMouseLeave,
onTooltipHelpTouchEnd,
useTapForTooltip,
} from "@/utils/tooltipHelp.js";
export default {
name: "HelpTooltipIcon",
props: {
text: {
type: String,
required: true,
},
icon: {
type: String,
default: "help",
},
iconStyle: {
type: String,
default: "outlined",
validator: (value) => value === "outlined" || value === "symbols",
},
iconClass: {
type: String,
default: "",
},
ariaLabel: {
type: String,
default: "",
},
},
data() {
return {
pressed: false,
};
},
computed: {
iconClasses() {
return [
"no-select",
"tooltip-info-icon",
this.iconStyle === "symbols"
? "material-symbols"
: "material-symbols-outlined",
this.iconClass,
];
},
},
methods: {
onClick(event) {
onTooltipHelpClick(event, this.text);
},
onTouchStart() {
if (useTapForTooltip()) {
this.pressed = true;
}
},
onTouchEnd(event) {
this.pressed = false;
onTooltipHelpTouchEnd(event, this.text);
},
onTouchCancel() {
this.pressed = false;
},
onMouseEnter(event) {
onTooltipHelpMouseEnter(event, this.text);
},
onMouseLeave() {
this.pressed = false;
onTooltipHelpMouseLeave();
},
},
};
</script>
+3 -23
View File
@@ -19,20 +19,13 @@ https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/compon
v-if="textPosition === 'inside'"
>
{{ displayed_text }}
<i
v-if="helpText && status === 'error'"
class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
help
</i>
<HelpTooltipIcon v-if="helpText && status === 'error'" :text="helpText" />
</div>
</div>
</template>
<script>
import { mutations } from "@/store";
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
import { getHumanReadableFilesize } from "@/utils/filesizes.js";
// We're leaving this untouched as you can read in the beginning
@@ -46,6 +39,7 @@ const isNumber = (n) => {
export default {
name: "progress-bar",
components: { HelpTooltipIcon },
props: {
val: {
default: 0,
@@ -317,20 +311,6 @@ export default {
return style;
},
},
methods: {
showTooltip(event) {
if (this.helpText) {
mutations.showTooltip({
content: this.helpText,
x: event.clientX,
y: event.clientY,
});
}
},
hideTooltip() {
mutations.hideTooltip();
},
},
};
</script>
+28 -28
View File
@@ -65,20 +65,20 @@
<div class="sizeInputWrapper">
<p>{{ $t("search.smallerThan") }}</p>
<input
class="sizeInput"
class="sizeInput input"
v-model="smallerThan"
type="number"
min="0"
placeholder="MB"
/>
<p>{{ $t("search.largerThan") }}</p>
<input class="sizeInput" v-model="largerThan" type="number" placeholder="MB" />
<input class="sizeInput input" v-model="largerThan" type="number" placeholder="MB" />
</div>
<div class="sizeInputWrapper">
<p>{{ $t("search.olderThanDate") }}</p>
<input class="sizeInput" v-model="modifiedOlderThan" type="date" />
<input class="sizeInput input" v-model="modifiedOlderThan" type="date" />
<p>{{ $t("search.newerThanDate") }}</p>
<input class="sizeInput" v-model="modifiedNewerThan" type="date" />
<input class="sizeInput input" v-model="modifiedNewerThan" type="date" />
</div>
</div>
<div class="settings-items">
@@ -110,10 +110,7 @@
<!-- Message when no results are found -->
<div class="searchPrompt" v-show="isEmpty && !isRunning">
<p>{{ noneMessage }}</p>
<i class="material-symbols-outlined tooltip-info-icon" @mouseenter="showHelpTooltip"
@mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="searchHelpText" />
</div>
<!-- List of search results -->
<ul v-show="results.length > 0">
@@ -146,6 +143,7 @@
<script>
import { resourcesApi, toolsApi } from "@/api";
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
import Icon from "@/components/files/Icon.vue";
import LoadingSpinner from "@/components/LoadingSpinner.vue";
import SettingsItem from "@/components/settings/SettingsItem.vue";
@@ -158,6 +156,7 @@ import { globalVars } from "@/utils/constants";
import { getHumanReadableFilesize } from "@/utils/filesizes";
import { isTypeAheadSessionActive } from "@/utils/listingTypeAhead.js";
import { utcStartOfDaySecondsFromDateInput } from "@/utils/moment";
import { shouldIgnoreOutsideTap } from "@/utils/tooltipHelp.js";
const boxes = {
folder: { label: "folders", icon: "folder" },
@@ -171,6 +170,7 @@ const boxes = {
export default {
components: {
HelpTooltipIcon,
Icon,
ToggleSwitch,
SettingsItem,
@@ -352,6 +352,12 @@ export default {
isEmpty() {
return this.results.length === 0;
},
searchHelpText() {
return [
this.$t("search.helpText1", { minSearchLength: globalVars.minSearchLength }),
this.$t("search.helpText2"),
].join("\n\n");
},
text() {
if (this.ongoing > 0) {
return "";
@@ -580,21 +586,14 @@ export default {
this.noneMessage = this.$t("search.noResults");
}
},
showHelpTooltip(event) {
const helpText = [
this.$t("search.helpText1", { minSearchLength: globalVars.minSearchLength }),
this.$t("search.helpText2"),
].join("\n\n");
mutations.showTooltip({
content: helpText,
x: event.clientX,
y: event.clientY,
});
},
hideTooltip() {
mutations.hideTooltip();
},
clearContext() {
clearContext(event) {
const target = event?.target;
if (target instanceof Element && target.closest(".tooltip-info-icon, .floating-tooltip")) {
return;
}
if (shouldIgnoreOutsideTap()) {
return;
}
mutations.closeHovers();
},
getThumbnailUrl(s) {
@@ -907,7 +906,7 @@ export default {
border-top-right-radius: 0px;
border: var(--borderWidth) solid var(--surfaceSecondary);
box-shadow: 0px 2em 50px 10px rgba(0, 0, 0, 0.3);
background-color: lightgray;
background-color: var(--surfacePrimary);
max-height: 80vh;
overflow: hidden;
display: flex;
@@ -973,7 +972,7 @@ export default {
border: 0;
background-color: transparent;
padding: 0;
color: rgba(255, 255, 255, 0.9);
color: var(--divider);
font-size: 0.95em;
}
@@ -983,7 +982,7 @@ export default {
}
#search.active .search-input-container input {
color: inherit;
color: var(--textPrimary);
}
#search .search-input-container input::placeholder {
@@ -1107,11 +1106,11 @@ body.rtl #search #result ul>* {
}
#search .search-input-container input::placeholder {
color: rgba(255, 255, 255, 0.5);
color: color-mix(in srgb, var(--divider) 50%, transparent);
}
#search.active .search-input-container input::placeholder {
color: rgba(0, 0, 0, 0.5);
color: var(--textSecondary);
}
#search.dark-mode .search-input-container {
@@ -1269,6 +1268,7 @@ body.rtl #search .boxes h3 {
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(6px);
height: 4em;
background: var(--surfacePrimary)
}
#search.active>div {
+1 -4
View File
@@ -239,7 +239,7 @@ export default {
<style scoped>
#status-bar {
background-color: rgb(37 49 55 / 5%) !important;
background-color: color-mix(in srgb, var(--alt-background) 15%, transparent);
height: 2.5em;
display: flex;
align-items: center;
@@ -314,9 +314,6 @@ input[type="range"] {
#status-bar {
backdrop-filter: blur(16px) invert(0.1);
}
#status-bar.dark-mode-header {
background-color: rgb(37 49 55 / 33%) !important;
}
}
/* Mobile styles */
+15 -2
View File
@@ -20,7 +20,8 @@
</template>
<script>
import { getters, mutations, state } from "@/store";
import { getters, state } from "@/store";
import { hideInteractiveTooltip } from "@/utils/tooltipHelp.js";
export default {
name: "Tooltip",
@@ -63,7 +64,7 @@ export default {
watch: {
$route: {
handler() {
mutations.hideTooltip();
hideInteractiveTooltip(true);
},
},
tooltip: {
@@ -149,5 +150,17 @@ export default {
font-size: 1em !important;
padding: 0.1em !important;
padding-left: 0.5em !important;
touch-action: manipulation;
}
.tooltip-info-icon[role="button"] {
cursor: pointer;
}
.tooltip-info-icon:active,
.tooltip-info-icon.tooltip-info-icon--pressed,
.tooltip-info-icon:hover {
font-variation-settings: 'FILL' 1;
opacity: 1;
}
</style>
+13 -16
View File
@@ -18,6 +18,7 @@
</div>
<div v-else>
<PathPickerButton
class="path-picker"
v-model:path="currentPath"
v-model:source="currentSource"
aria-label="access-path"
@@ -26,17 +27,16 @@
:placeholder="$t('sidebar.chooseSource')"
@navigate="onPathPickerNavigate"
/>
<ActivityViewerButton :href="activityViewerHref" />
<div class="settings-items">
<ActivityViewerButton class="item" :href="activityViewerHref" />
</div>
<!-- Default behavior banner -->
<div class="card item">
<div class="card-content banner-content">
<i class="material-symbols">{{ sourceDenyDefault ? 'do_not_disturb_on' : 'check_circle' }}</i> <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
{{ $t("access.defaultBehavior", { suffix: ":" }) }} {{ sourceDenyDefault ? $t("access.deny") : $t("access.allow")
}}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('access.defaultBehaviorDescription'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('access.defaultBehaviorDescription')" />
</div>
</div>
@@ -110,7 +110,7 @@
<script>
import { notify } from "@/notify";
import { accessApi } from "@/api";
import { mutations } from "@/store";
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
import FileList from "../files/FileList.vue";
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
import LoadingSpinner from "@/components/LoadingSpinner.vue";
@@ -123,6 +123,7 @@ import { eventBus } from "@/store/eventBus";
export default {
name: "access",
components: {
HelpTooltipIcon,
FileList,
ToggleSwitch,
LoadingSpinner,
@@ -333,16 +334,6 @@ export default {
console.error(e);
}
},
showTooltip(event, text) {
mutations.showTooltip({
content: text,
x: event.clientX,
y: event.clientY,
});
},
hideTooltip() {
mutations.hideTooltip();
}
}
};
</script>
@@ -358,6 +349,12 @@ export default {
align-items: center;
padding: 0.25em !important;
gap: 0.5em;
margin-top: 0.70rem;
}
.path-picker,
.banner-content {
margin-bottom: 0.70rem;
}
.cascade-toggle-section {
+16 -72
View File
@@ -117,10 +117,7 @@
<div v-if="!showMoreExpanded">
<p>
{{ $t("files.duration") }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.shareDurationDescription'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.shareDurationDescription')" />
</p>
<div class="form-flex-group">
<input class="form-grow input flat-right" v-focus type="number" max="2147483647" min="0"
@@ -134,10 +131,7 @@
</div>
<p>
{{ $t("prompts.optionalPassword") }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.passwordDescription'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.passwordDescription')" />
</p>
<div v-if="hasExistingPassword && !isChangingPassword" class="password-change-section">
<button
@@ -152,10 +146,7 @@
<input v-else class="input" type="password" autocomplete="new-password" v-model.trim="password" />
<p>
{{ $t("share.shareType") }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.shareTypeDescription'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.shareTypeDescription')" />
</p>
<ExpandDropdown
v-model="shareType"
@@ -187,10 +178,7 @@
<div class="settings-items">
<p>
{{ shareThemeLabel() }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.shareThemeDescription'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.shareThemeDescription')" />
</p>
<div v-if="Object.keys(availableThemes).length > 0" class="form-flex-group">
<ExpandDropdown
@@ -202,10 +190,7 @@
<div v-if="shareType === 'normal'">
<p>
{{ $t("share.defaultViewMode") }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.defaultViewModeDescription'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.defaultViewModeDescription')" />
</p>
<ExpandDropdown
v-model="viewMode"
@@ -238,11 +223,7 @@
:disabled="sourceReadOnly" />
<p>
{{ $t("share.enforceDarkLightMode") }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.enforceDarkLightModeDescription'))"
@mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.enforceDarkLightModeDescription')" />
</p>
<ExpandDropdown
v-model="enforceDarkLightMode"
@@ -259,11 +240,7 @@
<div>
<p>
{{ $t("profileSettings.hideFileExt") }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('profileSettings.hideFileExtDescription'))"
@mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('profileSettings.hideFileExtDescription')" />
</p>
<input class="input"
:class="{ 'form-invalid': !validateExtensions(hideFileExt) }"
@@ -289,18 +266,12 @@
<div v-if="shareType === 'normal'">
<p>
{{ $t("prompts.downloadsLimit") }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.downloadsLimitDescription'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.downloadsLimitDescription')" />
</p>
<input class="input" type="number" min="0" v-model.number="downloadsLimit" />
<p>
{{ $t("prompts.maxBandwidth") }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.maxBandwidthDescription'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.maxBandwidthDescription')" />
</p>
<input class="input" type="number" min="0" v-model.number="maxBandwidth" />
</div>
@@ -308,37 +279,25 @@
<p>
{{ $t("prompts.shareThemeColor") }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.shareThemeColorDescription'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.shareThemeColorDescription')" />
</p>
<input class="input" type="text" v-model.trim="themeColor" />
<p>
{{ shareTitleLabel() }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.shareTitleDescription'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.shareTitleDescription')" />
</p>
<input class="input" type="text" v-model.trim="title" />
<p>
{{ $t("prompts.shareDescription") }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.shareDescriptionHelp'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.shareDescriptionHelp')" />
</p>
<textarea class="input" v-model.trim="description"></textarea>
<p>
{{ $t("prompts.shareBanner") }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.shareBannerDescription'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.shareBannerDescription')" />
</p>
<div class="file-picker-input-group">
<input class="input file-picker-input" type="text" v-model.trim="banner" />
@@ -349,10 +308,7 @@
<p>
{{ $t("prompts.shareFavicon") }}
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('share.shareFaviconDescription'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('share.shareFaviconDescription')" />
</p>
<div class="file-picker-input-group">
<input class="input file-picker-input" type="text" v-model.trim="favicon" />
@@ -395,6 +351,7 @@ import { shareApi } from "@/api";
import { copyToClipboard } from "@/utils/clipboard";
import { fromNow } from "@/utils/moment";
import { buildItemUrl } from "@/utils/url";
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
import SettingsItem from "@/components/settings/SettingsItem.vue";
import SettingsTable from "@/components/settings/Table.vue";
@@ -409,6 +366,7 @@ import { eventBus } from "@/store/eventBus";
export default {
name: "share",
components: {
HelpTooltipIcon,
ToggleSwitch,
SettingsItem,
SettingsTable,
@@ -777,20 +735,6 @@ export default {
async copyToClipboard(text) {
await copyToClipboard(text);
},
/**
* @param {MouseEvent} event
* @param {string} text
*/
showTooltip(event, text) {
mutations.showTooltip({
content: text,
x: event.clientX,
y: event.clientY,
});
},
hideTooltip() {
mutations.hideTooltip();
},
async submit() {
if (this.hideFileExt !== "" && !this.validateExtensions(this.hideFileExt)) {
notify.showError("Invalid input, does not match requirement.");
@@ -1002,6 +1002,7 @@ export default {
display: flex;
flex-direction: column;
gap: 0.5em;
padding-bottom: 0.5em;
}
/* Link item styles */
+4 -22
View File
@@ -7,10 +7,7 @@
<div class="settings-number-input item">
<div class="no-padding">
<label for="maxConcurrentUpload">{{ $t("fileLoading.maxConcurrentUpload") }}</label>
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('fileLoading.maxConcurrentUploadHelp'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('fileLoading.maxConcurrentUploadHelp')" />
</div>
<div>
<input v-model.number="maxConcurrentUpload" type="range" min="1" max="10" @change="updateUploadSettings" />
@@ -20,10 +17,7 @@
<div class="settings-number-input item">
<div class="no-padding">
<label for="uploadChunkSizeMb">{{ $t("fileLoading.uploadChunkSizeMb") }}</label>
<i class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('fileLoading.uploadChunkSizeMbHelp'))" @mouseleave="hideTooltip">
help
</i>
<HelpTooltipIcon :text="$t('fileLoading.uploadChunkSizeMbHelp')" />
</div>
<div class="no-padding">
<input class="sizeInput input" v-model.number="uploadChunkSizeMb" type="number" min="0" @change="updateUploadSettings" />
@@ -189,6 +183,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from "vue";
import { readAllDirectoryEntries, uploadManager } from "@/utils/upload";
import { mutations, state } from "@/store";
import { notify } from "@/notify";
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
import ProgressBar from "@/components/ProgressBar.vue";
import SettingsItem from "@/components/settings/SettingsItem.vue";
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
@@ -198,6 +193,7 @@ import i18n from "@/i18n";
export default {
name: "UploadFiles",
components: {
HelpTooltipIcon,
ProgressBar,
SettingsItem,
ToggleSwitch,
@@ -254,18 +250,6 @@ export default {
const uploadChunkSizeMb = ref(state.user.fileLoading?.uploadChunkSizeMb);
const clearAll = ref(state.user.fileLoading?.clearAll || false);
const showTooltip = (event, text) => {
mutations.showTooltip({
content: text,
x: event.clientX,
y: event.clientY,
});
};
const hideTooltip = () => {
mutations.hideTooltip();
};
const updateUploadSettings = async () => {
try {
const data = {
@@ -698,8 +682,6 @@ export default {
maxConcurrentUpload,
uploadChunkSizeMb,
clearAll,
showTooltip,
hideTooltip,
getStatusText,
getHelpText,
updateUploadSettings,
+179 -22
View File
@@ -125,10 +125,6 @@
</div>
</div>
<p v-if="stateUser.username !== user.username">
<label for="locale">{{ $t("general.language") }}</label>
<languages id="locale" v-model:locale="user.locale" @input="emitUpdate"></languages>
</p>
<div v-if="stateUser.permissions.admin">
<label for="loginMethod">{{ $t("settings.loginMethod") }}</label>
<ExpandDropdown
@@ -141,12 +137,27 @@
</div>
<UserDefaultsAccountSection
v-if="stateUser.permissions.admin"
v-if="stateUser.permissions.admin && loaded"
:enforceable="false"
:start-collapsed="false"
:account="editAccount"
:enforced="enforcedAccount"
:enforced-permissions="enforcedAccountPermissions"
respect-enforced-policy
@account-change="onEditAccountChange"
/>
<UserProfilePreferences
v-if="stateUser.permissions.admin && loaded"
:key="profileLoadKey"
v-model="profileSections"
:enforced="enforcedPreferences"
:default-expanded-section="null"
respect-enforced-policy
show-extension-inputs
:show-thumbnail-master="false"
@change="onPreferenceChange"
/>
</div>
</div>
@@ -168,28 +179,66 @@
<script>
import { mutations, state } from "@/store";
import { usersApi, settingsApi, authApi } from "@/api";
import Languages from "@/components/settings/Languages.vue";
import ExpandDropdown from "@/components/settings/ExpandDropdown.vue";
import SourceFilePermissions from "@/components/settings/SourceFilePermissions.vue";
import SettingsItem from "@/components/settings/SettingsItem.vue";
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
import UserDefaultsAccountSection from "@/components/settings/UserDefaultsAccountSection.vue";
import UserProfilePreferences from "@/components/settings/UserProfilePreferences.vue";
import Errors from "@/views/Errors.vue";
import { notify } from "@/notify";
import { validateLogin } from "@/utils/auth";
import { globalVars } from "@/utils/constants";
import { eventBus } from "@/store/eventBus";
import { setObjectProperty } from '@/utils/object.js';
import {
sectionsFromFlatUser,
applySectionsToFlatUser,
isFlatProfileFieldEnforced,
} from "@/utils/userProfileSections.js";
/** Flat user fields editable via admin user edit (matches profile PATCH surface). */
const PROFILE_SNAPSHOT_FIELDS = [
"locale",
"dateFormat",
"themeColor",
"customTheme",
"quickDownload",
"preview",
"stickySidebar",
"singleClick",
"darkMode",
"showHidden",
"showToolsInSidebar",
"fileLoading",
"deleteAfterArchive",
"deleteWithoutConfirming",
"preferEditorForMarkdown",
"disablePreviewExt",
"disableViewingExt",
"disableOnlyOfficeExt",
"hideFileExt",
"disableQuickToggles",
"disableSearchOptions",
"hideSidebarFileActions",
"showCopyPath",
"hideFilesInTree",
"editorQuickSave",
"showSelectMultiple",
"debugOffice",
"viewMode",
"gallerySize",
];
export default {
name: "user-edit",
components: {
Languages,
ExpandDropdown,
SourceFilePermissions,
SettingsItem,
ToggleSwitch,
UserDefaultsAccountSection,
UserProfilePreferences,
Errors,
},
props: {
@@ -218,6 +267,8 @@ export default {
showDelete: false,
createUserDir: false,
loaded: false,
profileLoadKey: 0,
profileUser: { preview: {}, permissions: {}, fileLoading: {} },
originalUserScope: ".",
sourceList: [],
selectedSources: [],
@@ -241,6 +292,7 @@ export default {
};
},
async created() {
await mutations.syncEnforcedUserDefaults();
await this.fetchData();
await this.initializeForm();
},
@@ -374,6 +426,23 @@ export default {
if (this.globalVars.ldapAvailable) return "ldap";
return "password"; // fallback
},
profileSections: {
get() {
return sectionsFromFlatUser(this.profileUser);
},
set(sections) {
applySectionsToFlatUser(this.profileUser, sections);
},
},
enforcedPreferences() {
return state.enforcedUserDefaults || {};
},
enforcedAccount() {
return this.enforcedPreferences.account || {};
},
enforcedAccountPermissions() {
return this.enforcedAccount.permissions || {};
},
},
watch: {
stateUser() {
@@ -391,6 +460,28 @@ export default {
},
},
methods: {
cloneUserRecord(user) {
return JSON.parse(
JSON.stringify(user ?? { preview: {}, permissions: {}, fileLoading: {} })
);
},
syncProfileUserFromFormUser() {
const prepared = this.prepareProfileUser(this.cloneUserRecord(this.user));
if (typeof prepared.showToolsInSidebar !== "boolean") {
prepared.showToolsInSidebar = true;
}
this.profileUser = prepared;
this.profileLoadKey += 1;
},
applyProfileUserToFormUser() {
applySectionsToFlatUser(this.user, sectionsFromFlatUser(this.profileUser));
},
async fetchExistingUserRecord(username) {
if (username === state.user?.username) {
return this.cloneUserRecord(state.user);
}
return await usersApi.get(username);
},
defaultPermissions() {
return {
admin: false,
@@ -461,8 +552,21 @@ export default {
sourceBlockTitle(source) {
return source?.name || "";
},
normalizeFormUser(raw) {
normalizeFormUser(raw, { fromDefaultsTemplate = false } = {}) {
const user = { ...(raw ?? {}) };
// Flat API users always have `preview`; only merge nested defaults for template payloads.
if (fromDefaultsTemplate) {
applySectionsToFlatUser(user, {
sidebar: user.sidebar || {},
listing: user.listing || {},
preview: user.preview || {},
fileViewer: user.fileViewer || {},
search: user.search || {},
ui: user.ui || {},
account: user.account || {},
fileLoading: user.fileLoading || {},
});
}
if (user.account && typeof user.account === "object") {
const account = user.account;
user.lockPassword = !!account.lockPassword;
@@ -496,13 +600,23 @@ export default {
delete user.account;
delete user.sidebar;
delete user.listing;
delete user.preview;
delete user.fileViewer;
delete user.search;
delete user.ui;
delete user.fileLoading;
user.scopes = this.normalizeScopesForForm(user.scopes, user.sourcePermissions);
delete user.sourcePermissions;
return this.prepareProfileUser(user);
},
prepareProfileUser(user) {
if (!user.preview || typeof user.preview !== "object") {
user.preview = {};
}
if (!user.fileLoading || typeof user.fileLoading !== "object") {
user.fileLoading = {};
}
if (typeof user.showToolsInSidebar !== "boolean") {
user.showToolsInSidebar = true;
}
return user;
},
normalizeScopesForForm(scopes, legacySourcePermissions) {
@@ -542,7 +656,7 @@ export default {
if (this.isNew) {
const response = await settingsApi.getUserDefaults();
const defaults = response?.values ?? response;
this.user = this.normalizeFormUser(defaults);
this.user = this.normalizeFormUser(defaults, { fromDefaultsTemplate: true });
this.user.password = "";
// Ensure loginMethod is valid, set to first available method if not set or invalid
const validMethods = [];
@@ -555,12 +669,13 @@ export default {
if (!this.user.loginMethod || !validMethods.includes(this.user.loginMethod)) {
this.user.loginMethod = this.firstAvailableLoginMethod;
}
this.syncProfileUserFromFormUser();
} else {
const uname = this.targetUsername;
if (!uname) {
return;
}
this.user = this.normalizeFormUser(await usersApi.get(uname));
this.user = this.normalizeFormUser(await this.fetchExistingUserRecord(uname));
this.user.password = "";
// Normalize scopes to ensure they're in {name, scope} format only
if (this.user.scopes && Array.isArray(this.user.scopes)) {
@@ -577,12 +692,12 @@ export default {
if (!this.user.loginMethod || !validMethods.includes(this.user.loginMethod)) {
this.user.loginMethod = this.firstAvailableLoginMethod;
}
this.syncProfileUserFromFormUser();
}
} catch (e) {
this.error = e;
} finally {
mutations.setLoading("users", false);
this.loaded = true;
// Update prompt name after user data is loaded
this.updatePromptTitle();
}
@@ -617,6 +732,7 @@ export default {
if (!this.isNew) {
this.originalSnapshot = JSON.parse(JSON.stringify(this.buildEditableSnapshot()));
}
this.loaded = true;
},
buildScopesPayload() {
return this.selectedSources.map((source) => ({
@@ -634,13 +750,25 @@ export default {
}))
.sort((a, b) => a.name.localeCompare(b.name));
},
buildProfileSnapshot(user) {
const snapshot = {};
for (const field of PROFILE_SNAPSHOT_FIELDS) {
snapshot[field] = user?.[field];
}
if (snapshot.preview && typeof snapshot.preview === "object") {
snapshot.preview = { ...snapshot.preview };
}
if (snapshot.fileLoading && typeof snapshot.fileLoading === "object") {
snapshot.fileLoading = { ...snapshot.fileLoading };
}
return snapshot;
},
buildEditableSnapshot() {
this.applyEditAccountToUser();
const permissions = this.user.permissions || this.defaultPermissions();
return {
scopes: this.normalizeScopesForCompare(this.buildScopesPayload()),
loginMethod: this.user.loginMethod ?? null,
locale: this.user.locale ?? "",
otpEnabled: !!this.user.otpEnabled,
lockPassword: !!this.user.lockPassword,
disableSettings: !!this.user.disableSettings,
@@ -651,6 +779,7 @@ export default {
api: !!permissions.api,
realtime: !!permissions.realtime,
},
profile: this.buildProfileSnapshot(this.profileUser),
};
},
computeChangedFields() {
@@ -666,23 +795,40 @@ export default {
if (current.loginMethod !== orig.loginMethod) {
fields.push("loginMethod");
}
if (current.locale !== orig.locale) {
fields.push("locale");
}
if (current.otpEnabled !== orig.otpEnabled) {
fields.push("otpEnabled");
}
if (current.lockPassword !== orig.lockPassword) {
if (current.lockPassword !== orig.lockPassword && !this.enforcedAccount.lockPassword) {
fields.push("lockPassword");
}
if (current.disableSettings !== orig.disableSettings) {
if (current.disableSettings !== orig.disableSettings && !this.enforcedAccount.disableSettings) {
fields.push("disableSettings");
}
if (current.disableUpdateNotifications !== orig.disableUpdateNotifications) {
if (
current.disableUpdateNotifications !== orig.disableUpdateNotifications
&& !this.enforcedAccount.disableUpdateNotifications
) {
fields.push("disableUpdateNotifications");
}
if (JSON.stringify(current.permissions) !== JSON.stringify(orig.permissions)) {
fields.push("permissions");
const permissionFields = ["admin", "share", "api", "realtime"];
for (const perm of permissionFields) {
if (
current.permissions[perm] !== orig.permissions[perm]
&& !this.enforcedAccountPermissions[perm]
) {
fields.push("permissions");
break;
}
}
}
for (const field of PROFILE_SNAPSHOT_FIELDS) {
if (isFlatProfileFieldEnforced(this.enforcedPreferences, field)) {
continue;
}
if (JSON.stringify(current.profile?.[field]) !== JSON.stringify(orig.profile?.[field])) {
fields.push(field);
}
}
return fields;
},
@@ -714,6 +860,9 @@ export default {
this.applyEditAccountToUser();
this.emitUpdate();
},
onPreferenceChange() {
this.emitUpdate();
},
deletePrompt() {
mutations.showPrompt({
name: "generic",
@@ -745,6 +894,9 @@ export default {
async save(event) {
event.preventDefault();
try {
this.applyEditAccountToUser();
this.applyProfileUserToFormUser();
// Profile sections carry a stale account snapshot; restore admin-edited account fields.
this.applyEditAccountToUser();
const scopesToSend = this.buildScopesPayload();
const payload = {
@@ -960,6 +1112,10 @@ export default {
</script>
<style scoped>
label + .form-flex-group {
margin-top: 0.35em;
}
.scope-blocks {
display: flex;
flex-direction: column;
@@ -989,8 +1145,9 @@ export default {
.scope-block :deep(.settings-group-title) {
padding: 0.5em 0.75em;
border: 1px solid var(--borderColor, #ddd);
border: 1px solid var(--divider);
border-radius: var(--borderRadius, 4px);
margin-top: -0.5em;
}
.scope-path-display {
@@ -3,11 +3,7 @@
<div class="setting-row item">
<div class="setting-label">
<label for="vis-bar-count">{{ $t("player.visualizer.barCount") }}</label>
<i
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('player.visualizer.barCountDescription'))"
@mouseleave="hideTooltip"
>help</i>
<HelpTooltipIcon :text="$t('player.visualizer.barCountDescription')" />
</div>
<div class="setting-control">
<input
@@ -24,11 +20,7 @@
<div class="setting-row item">
<div class="setting-label">
<label id="vis-fft-size-label">{{ $t("player.visualizer.fftSize") }}</label>
<i
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('player.visualizer.fftSizeDescription'))"
@mouseleave="hideTooltip"
>help</i>
<HelpTooltipIcon :text="$t('player.visualizer.fftSizeDescription')" />
</div>
<div class="setting-control">
<ExpandDropdown
@@ -41,11 +33,7 @@
<div class="setting-row item">
<div class="setting-label">
<label id="vis-min-freq-label">{{ $t("player.visualizer.minFrequency") }}</label>
<i
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('player.visualizer.minFrequencyDescription'))"
@mouseleave="hideTooltip"
>help</i>
<HelpTooltipIcon :text="$t('player.visualizer.minFrequencyDescription')" />
</div>
<div class="setting-control">
<ExpandDropdown
@@ -58,11 +46,7 @@
<div class="setting-row item">
<div class="setting-label">
<label id="vis-max-freq-label">{{ $t("player.visualizer.maxFrequency") }}</label>
<i
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('player.visualizer.maxFrequencyDescription'))"
@mouseleave="hideTooltip"
>help</i>
<HelpTooltipIcon :text="$t('player.visualizer.maxFrequencyDescription')" />
</div>
<div class="setting-control">
<ExpandDropdown
@@ -75,11 +59,7 @@
<div class="setting-row slider-setting item">
<div class="setting-label">
<label for="vis-smoothing">{{ $t("player.visualizer.smoothing") }}</label>
<i
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('player.visualizer.smoothingDescription'))"
@mouseleave="hideTooltip"
>help</i>
<HelpTooltipIcon :text="$t('player.visualizer.smoothingDescription')" />
</div>
<div class="setting-control slider-setting-value">
<input id="vis-smoothing" type="range" min="0" max="1" step="0.01" v-model.number="smoothing" />
@@ -89,11 +69,7 @@
<div class="setting-row slider-setting item">
<div class="setting-label">
<label for="vis-min-db">{{ $t("player.visualizer.minDecibels") }}</label>
<i
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('player.visualizer.minDecibelsDescription'))"
@mouseleave="hideTooltip"
>help</i>
<HelpTooltipIcon :text="$t('player.visualizer.minDecibelsDescription')" />
</div>
<div class="setting-control slider-setting-value">
<input id="vis-min-db" type="range" min="-120" max="-60" step="1" v-model.number="minDecibels" />
@@ -103,11 +79,7 @@
<div class="setting-row slider-setting item">
<div class="setting-label">
<label for="vis-max-db">{{ $t("player.visualizer.maxDecibels") }}</label>
<i
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('player.visualizer.maxDecibelsDescription'))"
@mouseleave="hideTooltip"
>help</i>
<HelpTooltipIcon :text="$t('player.visualizer.maxDecibelsDescription')" />
</div>
<div class="setting-control slider-setting-value">
<input id="vis-max-db" type="range" min="-40" max="20" step="1" v-model.number="maxDecibels" />
@@ -148,6 +120,7 @@
</template>
<script>
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
import { mutations } from "@/store";
import { visualizerConfig, saveVisualizerConfig, resetVisualizerConfig } from "@/utils/visualizerConfig.js";
import ExpandDropdown from "@/components/settings/ExpandDropdown.vue";
@@ -159,7 +132,7 @@ function freqOptions(values) {
export default {
name: "VisualizerSettings",
components: { ExpandDropdown, ToggleSwitch },
components: { HelpTooltipIcon, ExpandDropdown, ToggleSwitch },
data() {
return {
fftSizeOptions: [1024, 2048, 4096, 8192, 16384, 32768].map((value) => ({ value, label: `${value}` })),
@@ -215,16 +188,6 @@ export default {
resetVisualizerConfig() {
resetVisualizerConfig();
},
showTooltip(event, text) {
mutations.showTooltip({
content: text,
x: event.clientX,
y: event.clientY,
});
},
hideTooltip() {
mutations.hideTooltip();
},
},
};
</script>
@@ -2,14 +2,7 @@
<div class="toggle-container" :class="{ disabled }">
<div class="toggle-name-container">
<span class="toggle-name">{{ name }}</span>
<i
v-if="description"
class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
help
</i>
<HelpTooltipIcon v-if="description" :text="description" />
</div>
<button
type="button"
@@ -24,10 +17,11 @@
</template>
<script>
import { mutations } from "@/store";
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
export default {
name: "SettingsButton",
components: { HelpTooltipIcon },
props: {
name: {
type: String,
@@ -47,20 +41,6 @@ export default {
},
},
emits: ["click"],
methods: {
showTooltip(event) {
if (this.description) {
mutations.showTooltip({
content: this.description,
x: event.clientX,
y: event.clientY,
});
}
},
hideTooltip() {
mutations.hideTooltip();
},
},
};
</script>
@@ -120,6 +120,7 @@ export default {
background: var(--alt-background) !important;
color: var(--textPrimary) !important;
padding: 0.5em;
margin-top: 0.75em;
}
.settings-group-title h3 {
+85 -79
View File
@@ -1,89 +1,91 @@
<template>
<table
class="settings-table border-radius"
:class="{ 'settings-table--loading': loading }"
:aria-label="ariaLabel"
:aria-busy="loading ? 'true' : undefined"
>
<thead>
<tr v-if="headerTitle">
<th
:colspan="emptyColumnSpan"
scope="col"
class="settings-table__align-center settings-table__th--nosort settings-table__th--unified"
>
{{ headerTitle }}
</th>
</tr>
<tr v-else>
<th
v-for="column in columns"
:key="column.key"
scope="col"
:class="[
alignClass(column),
headerSortClass(column),
]"
:aria-sort="ariaSortState(column)"
@click="column.sortable === true ? toggleSort(column) : undefined"
>
{{ column.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-if="loading" class="settings-table__loading-row">
<td :colspan="emptyColumnSpan" class="settings-table__loading-cell">
<div class="settings-table__loading-inner">
<LoadingSpinner size="small" mode="placeholder" />
</div>
</td>
</tr>
<template v-else>
<tr
v-for="item in sortedItems"
:key="resolvedKey(item)"
:class="{ 'settings-table__row--clickable': rowClickable }"
:tabindex="rowClickable ? 0 : undefined"
:role="rowClickable ? 'button' : undefined"
@click="onRowClick(item, $event)"
@keydown.enter.prevent="onRowClick(item, $event)"
@keydown.space.prevent="onRowClick(item, $event)"
>
<td
<div class="settings-table-wrapper">
<table
class="settings-table border-radius"
:class="{ 'settings-table--loading': loading }"
:aria-label="ariaLabel"
:aria-busy="loading ? 'true' : undefined"
>
<thead>
<tr v-if="headerTitle">
<th
:colspan="emptyColumnSpan"
scope="col"
class="settings-table__align-center settings-table__th--nosort settings-table__th--unified"
>
{{ headerTitle }}
</th>
</tr>
<tr v-else>
<th
v-for="column in columns"
:key="column.key"
scope="col"
:class="[
alignClass(column),
column.narrow === true ? 'settings-table__td--narrow' : '',
headerSortClass(column),
]"
:aria-sort="ariaSortState(column)"
@click="column.sortable === true ? toggleSort(column) : undefined"
>
<slot
:name="cellSlot(column.key)"
:row="item"
:value="cellValue(item, column)"
:column="column"
{{ column.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-if="loading" class="settings-table__loading-row">
<td :colspan="emptyColumnSpan" class="settings-table__loading-cell">
<div class="settings-table__loading-inner">
<LoadingSpinner size="small" mode="placeholder" />
</div>
</td>
</tr>
<template v-else>
<tr
v-for="item in sortedItems"
:key="resolvedKey(item)"
:class="{ 'settings-table__row--clickable': rowClickable }"
:tabindex="rowClickable ? 0 : undefined"
:role="rowClickable ? 'button' : undefined"
@click="onRowClick(item, $event)"
@keydown.enter.prevent="onRowClick(item, $event)"
@keydown.space.prevent="onRowClick(item, $event)"
>
<td
v-for="column in columns"
:key="column.key"
:class="[
alignClass(column),
column.narrow === true ? 'settings-table__td--narrow' : '',
]"
>
{{ renderCell(item, column) }}
</slot>
</td>
</tr>
<tr v-if="sortedItems.length === 0">
<td
:colspan="emptyColumnSpan"
class="settings-table__empty settings-table__empty-cell"
>
<slot name="empty">
<h2 class="message settings-table__lonely">
<i class="material-symbols-outlined" aria-hidden="true">sentiment_dissatisfied</i>
<span>{{ lonelyCaption }}</span>
</h2>
</slot>
</td>
</tr>
</template>
</tbody>
</table>
<slot
:name="cellSlot(column.key)"
:row="item"
:value="cellValue(item, column)"
:column="column"
>
{{ renderCell(item, column) }}
</slot>
</td>
</tr>
<tr v-if="sortedItems.length === 0">
<td
:colspan="emptyColumnSpan"
class="settings-table__empty settings-table__empty-cell"
>
<slot name="empty">
<h2 class="message settings-table__lonely">
<i class="material-symbols-outlined" aria-hidden="true">sentiment_dissatisfied</i>
<span>{{ lonelyCaption }}</span>
</h2>
</slot>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
<script>
@@ -355,6 +357,11 @@ export default {
<style scoped>
/* Radius: separate .border-radius utility (_variables.css) so it stays easy to override */
.settings-table-wrapper {
width: 100%;
overflow-x: auto;
}
.settings-table {
width: 100%;
border-collapse: collapse;
@@ -545,4 +552,3 @@ body.rtl .settings-table__align-center {
text-align: center;
}
</style>
@@ -13,14 +13,7 @@
>
<div class="toggle-name-container">
<span class="toggle-name">{{ name }}</span>
<i
v-if="description"
class="material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
help
</i>
<HelpTooltipIcon v-if="description" :text="description" />
</div>
<label class="switch">
<input
@@ -55,12 +48,17 @@
</template>
<script>
import { mutations } from "@/store";
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
import {
hideInteractiveTooltip,
showHoverTooltip,
} from "@/utils/tooltipHelp.js";
let enforcedIdCounter = 0;
export default {
name: "ToggleSwitch",
components: { HelpTooltipIcon },
props: {
modelValue: {
type: Boolean,
@@ -125,37 +123,20 @@ export default {
updateEnforced(event) {
this.$emit("update:enforced", event.target.checked);
},
showTooltip(event) {
if (this.description) {
mutations.showTooltip({
content: this.description,
x: event.clientX,
y: event.clientY,
});
}
},
showValueRowTooltipIfNeeded(event) {
if (!this.disabled) {
return;
}
if (this.valueTooltip) {
mutations.showTooltip({
content: this.valueTooltip,
x: event.clientX,
y: event.clientY,
});
showHoverTooltip(this.valueTooltip, event);
return;
}
if (this.enforcementLocked) {
mutations.showTooltip({
content: this.$t("profileSettings.enforcedByAdmin"),
x: event.clientX,
y: event.clientY,
});
showHoverTooltip(this.$t("profileSettings.enforcedByAdmin"), event);
}
},
hideTooltip() {
mutations.hideTooltip();
hideInteractiveTooltip();
},
},
};
@@ -8,8 +8,8 @@
v-model="account.lockPassword"
@change="$emit('account-change', 'lockPassword')"
@update:enforced="(v) => emitEnforced('lockPassword', v)"
:disabled="isFieldLocked('lockPassword')"
:value-tooltip="configLockTooltipForField('lockPassword')"
:disabled="isFieldDisabled('lockPassword')"
:value-tooltip="fieldDisabledTooltip('lockPassword')"
:name="$t('settings.lockPassword')"
/>
<ToggleSwitch
@@ -19,8 +19,8 @@
v-model="account.disableSettings"
@change="$emit('account-change', 'disableSettings')"
@update:enforced="(v) => emitEnforced('disableSettings', v)"
:disabled="isFieldLocked('disableSettings')"
:value-tooltip="configLockTooltipForField('disableSettings')"
:disabled="isFieldDisabled('disableSettings')"
:value-tooltip="fieldDisabledTooltip('disableSettings')"
:name="$t('settings.disableUserSettings')"
/>
<ToggleSwitch
@@ -30,8 +30,8 @@
v-model="account.disableUpdateNotifications"
@change="$emit('account-change', 'disableUpdateNotifications')"
@update:enforced="(v) => emitEnforced('disableUpdateNotifications', v)"
:disabled="isFieldLocked('disableUpdateNotifications')"
:value-tooltip="configLockTooltipForField('disableUpdateNotifications')"
:disabled="isFieldDisabled('disableUpdateNotifications')"
:value-tooltip="fieldDisabledTooltip('disableUpdateNotifications')"
:name="$t('profileSettings.disableUpdateNotifications')"
:description="$t('profileSettings.disableUpdateNotificationsDescription')"
/>
@@ -46,8 +46,8 @@
v-model="account.permissions.admin"
@change="$emit('account-change', 'permissions.admin')"
@update:enforced="(v) => emitEnforcedPermission('admin', v)"
:disabled="isPermissionLocked('admin')"
:value-tooltip="configLockTooltipForPermission('admin')"
:disabled="isPermissionDisabled('admin')"
:value-tooltip="permissionDisabledTooltip('admin')"
:name="$t('settings.permissions.admin')"
/>
<ToggleSwitch
@@ -57,8 +57,8 @@
v-model="account.permissions.share"
@change="$emit('account-change', 'permissions.share')"
@update:enforced="(v) => emitEnforcedPermission('share', v)"
:disabled="isPermissionLocked('share')"
:value-tooltip="configLockTooltipForPermission('share')"
:disabled="isPermissionDisabled('share')"
:value-tooltip="permissionDisabledTooltip('share')"
:name="$t('general.shareFiles')"
/>
<ToggleSwitch
@@ -68,8 +68,8 @@
v-model="account.permissions.api"
@change="$emit('account-change', 'permissions.api')"
@update:enforced="(v) => emitEnforcedPermission('api', v)"
:disabled="isPermissionLocked('api')"
:value-tooltip="configLockTooltipForPermission('api')"
:disabled="isPermissionDisabled('api')"
:value-tooltip="permissionDisabledTooltip('api')"
:name="$t('settings.permissions.api')"
/>
<ToggleSwitch
@@ -79,8 +79,8 @@
v-model="account.permissions.realtime"
@change="$emit('account-change', 'permissions.realtime')"
@update:enforced="(v) => emitEnforcedPermission('realtime', v)"
:disabled="isPermissionLocked('realtime')"
:value-tooltip="configLockTooltipForPermission('realtime')"
:disabled="isPermissionDisabled('realtime')"
:value-tooltip="permissionDisabledTooltip('realtime')"
:name="$t('settings.permissions.realtime')"
/>
</div>
@@ -122,6 +122,10 @@ export default {
type: Array,
default: () => [],
},
respectEnforcedPolicy: {
type: Boolean,
default: false,
},
},
emits: ["account-change", "enforced-change", "enforced-permission-change"],
methods: {
@@ -131,6 +135,30 @@ export default {
isPermissionLocked(field) {
return this.configLockedPaths.includes(`account.permissions.${field}`);
},
isFieldDisabled(field) {
if (this.isFieldLocked(field)) {
return true;
}
return this.respectEnforcedPolicy && !!this.enforced[field];
},
isPermissionDisabled(field) {
if (this.isPermissionLocked(field)) {
return true;
}
return this.respectEnforcedPolicy && !!this.enforcedPermissions[field];
},
fieldDisabledTooltip(field) {
if (this.respectEnforcedPolicy && this.enforced[field]) {
return this.$t("profileSettings.enforcedByAdmin");
}
return this.configLockTooltipForField(field);
},
permissionDisabledTooltip(field) {
if (this.respectEnforcedPolicy && this.enforcedPermissions[field]) {
return this.$t("profileSettings.enforcedByAdmin");
}
return this.configLockTooltipForPermission(field);
},
configLockTooltipForField(field) {
if (this.isFieldLocked(field)) {
return this.$t("settings.userDefaultFieldLockedFromConfig");
@@ -56,6 +56,62 @@
:name="$t('profileSettings.deleteAfterArchive')"
:description="$t('profileSettings.deleteAfterArchiveDescription')"
/>
<div
class="preference-field-block"
:class="{ 'preference-field-block--enforceable': enforceable }"
>
<div class="centered-with-tooltip">
<h3>{{ $t("profileSettings.defaultViewMode") }}</h3>
<HelpTooltipIcon :text="$t('profileSettings.defaultViewModeDescription')" />
</div>
<div
@mouseenter="showEnforcedTooltipIfLocked($event, 'listing', 'viewMode')"
@mouseleave="hideTooltip"
>
<ViewMode
:view-mode="listingViewMode"
:disabled="fieldDisabled('listing', 'viewMode')"
@update:view-mode="onListingViewModeChange"
/>
</div>
<ProfileEnforceSwitch
:visible="enforceable"
:enforced="enforcedFlag('listing', 'viewMode')"
:disabled="disabled"
@update:enforced="(v) => emitEnforced('listing', 'viewMode', v)"
/>
</div>
<div
class="preference-field-block"
:class="{ 'preference-field-block--enforceable': enforceable }"
>
<div class="centered-with-tooltip">
<h3>{{ $t("profileSettings.defaultGallerySize") }}</h3>
<HelpTooltipIcon :text="$t('profileSettings.defaultGallerySizeDescription')" />
</div>
<div
class="gallery-size-field"
@mouseenter="showEnforcedTooltipIfLocked($event, 'listing', 'gallerySize')"
@mouseleave="hideTooltip"
>
<span class="size-label">{{ $t("general.size") }}</span>
<input
v-model.number="listingGallerySize"
type="range"
min="1"
max="9"
:disabled="fieldDisabled('listing', 'gallerySize')"
@change="() => emitSectionChange('listing', 'gallerySize')"
/>
<span class="size-value">{{ listingGallerySize }}</span>
</div>
<ProfileEnforceSwitch
:visible="enforceable"
:enforced="enforcedFlag('listing', 'gallerySize')"
:disabled="disabled"
@update:enforced="(v) => emitEnforced('listing', 'gallerySize', v)"
/>
</div>
</div>
<template v-if="showExtensionInputs">
<div
@@ -64,13 +120,7 @@
>
<div class="centered-with-tooltip">
<h3>{{ $t("profileSettings.hideFileExt") }}</h3>
<i
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showFieldHelp($event, 'listing', 'hideFileExt', $t('profileSettings.hideFileExtDescription'))"
@mouseleave="hideTooltip"
>
help
</i>
<HelpTooltipIcon :text="$t('profileSettings.hideFileExtDescription')" />
</div>
<div
class="form-flex-group"
@@ -187,13 +237,7 @@
>
<div class="centered-with-tooltip">
<h3>{{ $t("profileSettings.disableThumbnailPreviews") }}</h3>
<i
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showFieldHelp($event, 'preview', 'disablePreviewExt', $t('profileSettings.disableThumbnailPreviewsDescription'))"
@mouseleave="hideTooltip"
>
help
</i>
<HelpTooltipIcon :text="$t('profileSettings.disableThumbnailPreviewsDescription')" />
</div>
<div
class="form-flex-group"
@@ -340,13 +384,7 @@
>
<div class="centered-with-tooltip">
<h3>{{ $t("profileSettings.disableViewingFiles") }}</h3>
<i
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showFieldHelp($event, 'fileViewer', 'disableViewingExt', $t('profileSettings.disableViewingFilesDescription'))"
@mouseleave="hideTooltip"
>
help
</i>
<HelpTooltipIcon :text="$t('profileSettings.disableViewingFilesDescription')" />
</div>
<div
class="form-flex-group"
@@ -384,13 +422,7 @@
>
<div class="centered-with-tooltip">
<h3>{{ $t("profileSettings.disableOfficeEditor") }}</h3>
<i
class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showFieldHelp($event, 'fileViewer', 'disableOnlyOfficeExt', $t('profileSettings.disableOfficeEditorDescription'))"
@mouseleave="hideTooltip"
>
help
</i>
<HelpTooltipIcon :text="$t('profileSettings.disableOfficeEditorDescription')" />
</div>
<div
class="form-flex-group"
@@ -552,8 +584,13 @@
<script>
import { notify } from "@/notify";
import { globalVars } from "@/utils/constants.js";
import { state, mutations, getters } from "@/store";
import {
hideInteractiveTooltip,
showHoverTooltip,
} from "@/utils/tooltipHelp.js";
import { state, getters, mutations } from "@/store";
import { getObjectProperty, setObjectProperty } from "@/utils/object.js";
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
import ProfilePreferenceToggle from "@/components/settings/ProfilePreferenceToggle.vue";
import ProfileEnforceSwitch from "@/components/settings/ProfileEnforceSwitch.vue";
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
@@ -561,10 +598,12 @@ import SettingsItem from "@/components/settings/SettingsItem.vue";
import Languages from "@/components/settings/Languages.vue";
import ExpandDropdown from "@/components/settings/ExpandDropdown.vue";
import ButtonGroup from "@/components/ButtonGroup.vue";
import ViewMode from "@/components/settings/ViewMode.vue";
export default {
name: "UserProfilePreferences",
components: {
HelpTooltipIcon,
ToggleSwitch,
SettingsItem,
Languages,
@@ -572,6 +611,7 @@ export default {
ButtonGroup,
ProfilePreferenceToggle,
ProfileEnforceSwitch,
ViewMode,
},
provide() {
return { profilePrefs: this };
@@ -605,11 +645,21 @@ export default {
type: Boolean,
default: false,
},
/** When true, enforced policy disables fields even for admin actors (e.g. admin user edit). */
respectEnforcedPolicy: {
type: Boolean,
default: false,
},
/** Which settings group is expanded initially; null collapses all. */
defaultExpandedSection: {
type: String,
default: "listingOptions",
},
},
emits: ["update:modelValue", "change", "enforced-change", "theme-color", "locale-change"],
data() {
return {
expandedSection: "listingOptions",
expandedSection: null,
formDisablePreviews: "",
formDisabledViewing: "",
formDisableOfficeViewing: "",
@@ -723,6 +773,19 @@ export default {
this.sections = next;
},
},
listingViewMode() {
return this.sections.listing?.viewMode || "normal";
},
listingGallerySize: {
get() {
const size = this.sections.listing?.gallerySize;
return typeof size === "number" ? size : 3;
},
set(value) {
const nextSize = Math.min(9, Math.max(1, Number(value) || 3));
this.setSectionBool("listing", "gallerySize", nextSize);
},
},
},
watch: {
modelValue: {
@@ -732,6 +795,9 @@ export default {
},
},
},
created() {
this.expandedSection = this.defaultExpandedSection || null;
},
mounted() {
this.syncExtensionFormsFromSections();
},
@@ -771,7 +837,7 @@ export default {
return this.configLockedPaths.includes(this.configLockedPath(section, field));
},
fieldLocked(section, field) {
if (getters.isAdmin()) {
if (getters.isAdmin() && !this.respectEnforcedPolicy) {
return false;
}
if (this.isConfigLocked(section, field)) {
@@ -807,22 +873,25 @@ export default {
return "";
},
isEnforcementLocked(section, field) {
if (getters.isAdmin() || this.enforceable) {
if (this.enforceable) {
return false;
}
if (this.isConfigLocked(section, field)) {
return false;
}
if (this.respectEnforcedPolicy && this.enforcedFlag(section, field)) {
return true;
}
if (getters.isAdmin()) {
return false;
}
return this.enforcedFlag(section, field);
},
showEnforcedTooltipIfLocked(event, section, field) {
if (!this.fieldDisabled(section, field) || !this.isEnforcementLocked(section, field)) {
return;
}
this.showTooltip(event, this.$t("profileSettings.enforcedByAdmin"));
},
showFieldHelp(event, section, field, description) {
this.showTooltip(event, this.helpText(section, field, description));
showHoverTooltip(this.$t("profileSettings.enforcedByAdmin"), event);
},
sectionBool(section, field) {
if (section === "account" && field.includes(".")) {
@@ -913,15 +982,12 @@ export default {
this.$emit("locale-change", locale);
this.emitSectionChange("ui", "locale");
},
showTooltip(event, text) {
mutations.showTooltip({
content: text,
x: event.clientX,
y: event.clientY,
});
onListingViewModeChange(viewMode) {
this.setSectionBool("listing", "viewMode", viewMode);
this.emitSectionChange("listing", "viewMode");
},
hideTooltip() {
mutations.hideTooltip();
hideInteractiveTooltip();
},
validateExtensions(value) {
const normalized = String(value ?? "").trim();
@@ -1001,4 +1067,16 @@ export default {
border-radius: var(--borderRadius);
margin-bottom: 0.5em;
}
.gallery-size-field {
display: flex;
align-items: center;
gap: 0.75em;
}
.gallery-size-field input[type="range"] {
flex: 1 1 auto;
}
.gallery-size-field .size-value {
min-width: 1.25em;
text-align: center;
}
</style>
+11 -1
View File
@@ -3,6 +3,7 @@
<ExpandDropdown
:model-value="viewMode"
:options="viewModeOptions"
:disabled="disabled"
:aria-label="$t('buttons.normalView')"
@update:model-value="$emit('update:viewMode', $event)"
/>
@@ -17,7 +18,16 @@ export default {
components: {
ExpandDropdown,
},
props: ["viewMode"],
props: {
viewMode: {
type: String,
default: "normal",
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ["update:viewMode"],
computed: {
viewModeOptions() {
+6 -6
View File
@@ -75,6 +75,10 @@
import { logout } from "@/utils/auth";
import { globalVars } from "@/utils/constants";
import { state, getters, mutations } from "@/store";
import {
hideInteractiveTooltip,
showHoverTooltip,
} from "@/utils/tooltipHelp.js";
import SidebarLinks from "./Links.vue";
import { url } from "@/utils";
@@ -233,15 +237,11 @@ export default {
},
showTooltip(event, text) {
if (text) {
mutations.showTooltip({
content: text,
x: event.clientX,
y: event.clientY,
});
showHoverTooltip(text, event);
}
},
hideTooltip() {
mutations.hideTooltip();
hideInteractiveTooltip();
},
},
};
+69 -81
View File
@@ -60,13 +60,22 @@
<circle class="center" cx="50%" cy="50%" r="7px"></circle>
<circle class="pulse" cx="50%" cy="50%" r="10px"></circle>
</svg>
<i v-else class="material-symbols warning-icon"
@mouseenter="showTooltip($event, $t('sidebar.sourceNotAccessible'))" @mouseleave="hideTooltip">
warning
</i>
<HelpTooltipIcon
v-else
icon="warning"
icon-style="symbols"
icon-class="warning-icon"
:text="$t('sidebar.sourceNotAccessible')"
/>
<span>{{ link.name }}</span>
<i v-if="hasUsageInfo(link)" class="no-select material-symbols-outlined tooltip-info-icon"
@click="onSourceInfoClick($event, sourceInfo[link.sourceName] || {})"
:class="{ 'tooltip-info-icon--pressed': pressedSourceInfo === link.sourceName }"
role="button"
tabindex="0"
@touchstart.stop="onSourceInfoTouchStart(link.sourceName)"
@touchend.stop="onSourceInfoTouchEnd($event, sourceInfo[link.sourceName] || {})"
@touchcancel.stop="onSourceInfoTouchCancel"
@click.stop="onSourceInfoClick($event, sourceInfo[link.sourceName] || {})"
@mouseenter="onSourceInfoMouseEnter($event, sourceInfo[link.sourceName] || {})"
@mouseleave="onSourceInfoMouseLeave">
info
@@ -131,15 +140,24 @@
<circle class="center" cx="50%" cy="50%" r="7px"></circle>
<circle class="pulse" cx="50%" cy="50%" r="10px"></circle>
</svg>
<i v-else class="material-symbols warning-icon"
@mouseenter="showTooltip($event, $t('sidebar.sourceNotAccessible'))" @mouseleave="hideTooltip">
warning
</i>
<HelpTooltipIcon
v-else
icon="warning"
icon-style="symbols"
icon-class="warning-icon"
:text="$t('sidebar.sourceNotAccessible')"
/>
<!-- Source name -->
<span>{{ activeSourceLink.name }}</span>
<i v-if="hasUsageInfo(activeSourceLink)"
class="no-select material-symbols-outlined tooltip-info-icon"
@click="onSourceInfoClick($event, activeSourceInfo)"
:class="{ 'tooltip-info-icon--pressed': pressedSourceInfo === activeSource }"
role="button"
tabindex="0"
@touchstart.stop="onSourceInfoTouchStart(activeSource)"
@touchend.stop="onSourceInfoTouchEnd($event, activeSourceInfo)"
@touchcancel.stop="onSourceInfoTouchCancel"
@click.stop="onSourceInfoClick($event, activeSourceInfo)"
@mouseenter="onSourceInfoMouseEnter($event, activeSourceInfo)"
@mouseleave="onSourceInfoMouseLeave">
info
@@ -184,7 +202,16 @@
</template>
<script>
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
import { state, getters, mutations } from "@/store";
import {
hideInteractiveTooltip,
onComponentTooltipClick,
onComponentTooltipTouchEnd,
showHoverComponentTooltip,
showHoverTooltip,
useTapForTooltip,
} from "@/utils/tooltipHelp.js";
import ProgressBar from "@/components/ProgressBar.vue";
import { goToItem } from "@/utils/url";
import { getIconClass } from "@/utils/material-symbols";
@@ -198,6 +225,7 @@ import ExpandDropdown from "@/components/settings/ExpandDropdown.vue";
export default {
name: "SidebarLinks",
components: {
HelpTooltipIcon,
ProgressBar,
ShareInfo,
FileTree,
@@ -205,12 +233,12 @@ export default {
},
data() {
return {
sourceTooltipDismissHandler: null,
pressedSourceInfo: null,
};
},
computed: {
useTapForSourceTooltip() {
return getters.isMobile();
return useTapForTooltip();
},
editShareText() {
return this.$t("general.shareManagement");
@@ -325,7 +353,7 @@ export default {
},
},
beforeUnmount() {
this.unregisterSourceTooltipDismiss();
hideInteractiveTooltip(true);
},
methods: {
isSourceCategory(category) {
@@ -558,95 +586,55 @@ export default {
},
showTooltip(event, text) {
if (text) {
mutations.showTooltip({
content: text,
x: event.clientX,
y: event.clientY,
});
showHoverTooltip(text, event);
}
},
hideTooltip() {
this.unregisterSourceTooltipDismiss();
mutations.hideTooltip();
},
isSourceTooltipVisible(info) {
if (!state.tooltip.show || state.tooltip.component !== IndexInfo) {
return false;
}
const shown = state.tooltip.componentProps?.info;
if (!shown || !info) {
return false;
}
return shown.name === info.name;
hideInteractiveTooltip();
},
onSourceInfoClick(event, info) {
if (!this.useTapForSourceTooltip) {
return;
onComponentTooltipClick(this.sourceInfoTooltipOptions(event, info));
},
onSourceInfoTouchStart(sourceName) {
if (useTapForTooltip()) {
this.pressedSourceInfo = sourceName;
}
this.toggleSourceTooltip(event, info);
},
onSourceInfoTouchEnd(event, info) {
this.pressedSourceInfo = null;
onComponentTooltipTouchEnd(this.sourceInfoTooltipOptions(event, info));
},
onSourceInfoTouchCancel() {
this.pressedSourceInfo = null;
},
sourceInfoTooltipOptions(event, info) {
return {
event,
component: IndexInfo,
componentProps: { info },
pointerEvents: this.useTapForSourceTooltip,
propsMatcher: (props) => props.info?.name === info?.name,
};
},
onSourceInfoMouseEnter(event, info) {
if (this.useTapForSourceTooltip) {
return;
}
this.showSourceTooltip(event, info);
this.showSourceHoverTooltip(event, info);
},
onSourceInfoMouseLeave() {
this.pressedSourceInfo = null;
if (this.useTapForSourceTooltip) {
return;
}
this.hideTooltip();
},
toggleSourceTooltip(event, info) {
if (this.isSourceTooltipVisible(info)) {
this.hideTooltip();
return;
}
this.showSourceTooltip(event, info);
},
showSourceTooltip(event, info) {
showSourceHoverTooltip(event, info) {
if (!info?.name) {
return;
}
mutations.showTooltip({
showHoverComponentTooltip({
component: IndexInfo,
componentProps: { info },
x: event.clientX,
y: event.clientY,
pointerEvents: this.useTapForSourceTooltip,
event,
});
if (this.useTapForSourceTooltip) {
this.registerSourceTooltipDismiss();
}
},
registerSourceTooltipDismiss() {
this.unregisterSourceTooltipDismiss();
this.sourceTooltipDismissHandler = (e) => {
const target = e.target;
if (target instanceof Element && (
target.closest(".tooltip-info-icon") ||
target.closest(".floating-tooltip")
)) {
return;
}
this.hideTooltip();
};
// Defer so the same tap does not immediately dismiss the tooltip.
requestAnimationFrame(() => {
if (!this.sourceTooltipDismissHandler) {
return;
}
document.addEventListener("click", this.sourceTooltipDismissHandler, true);
document.addEventListener("touchstart", this.sourceTooltipDismissHandler, true);
});
},
unregisterSourceTooltipDismiss() {
if (!this.sourceTooltipDismissHandler) {
return;
}
document.removeEventListener("click", this.sourceTooltipDismissHandler, true);
document.removeEventListener("touchstart", this.sourceTooltipDismissHandler, true);
this.sourceTooltipDismissHandler = null;
},
async showEditShareHover() {
if (!this.canEdit) {
+1 -4
View File
@@ -204,7 +204,7 @@ export default {
transition: 0.4s ease;
top: 4em;
padding-bottom: 4em;
background-color: rgb(37 49 55 / 5%) !important;
background-color: color-mix(in srgb, var(--alt-background) 20%, transparent);
will-change: left;
backface-visibility: hidden;
}
@@ -215,9 +215,6 @@ export default {
backdrop-filter: blur(8px) invert(0.1);
isolation: isolate;
}
#sidebar.dark-mode {
background-color: rgb(37 49 55 / 33%) !important;
}
#sidebar:not(.active) {
backdrop-filter: blur(0) invert(0);
}
@@ -3,15 +3,22 @@
<div v-else class="table-value-cell">
<span
:class="badgeClasses"
@mouseenter="showTooltip"
@click.stop="onBadgeClick"
@touchend.stop="onBadgeTouchEnd"
@mouseenter="onBadgeMouseEnter"
@mouseleave="hideTooltip"
>{{ displayText }}</span>
</div>
</template>
<script>
import { mutations } from "@/store";
import ActivityTableValueInfo from "@/components/tools/ActivityTableValueInfo.vue";
import {
hideInteractiveTooltip,
onComponentTooltipClick,
onComponentTooltipTouchEnd,
showHoverComponentTooltip,
} from "@/utils/tooltipHelp.js";
export default {
name: "ActivityTableValueCell",
@@ -72,24 +79,55 @@ export default {
},
},
methods: {
showTooltip(event) {
componentPropsMatcher(props) {
return props.label === this.label && props.value === this.tooltipText;
},
showComponentTooltip(event) {
const text = this.tooltipText;
if (!text || !this.label) {
return;
}
mutations.showTooltip({
showHoverComponentTooltip({
component: ActivityTableValueInfo,
componentProps: {
label: this.label,
value: text,
},
x: event.clientX,
y: event.clientY,
event,
width: "22rem",
});
},
onBadgeClick(event) {
const text = this.tooltipText;
if (!text || !this.label) {
return;
}
onComponentTooltipClick(this.badgeTooltipOptions(event, text));
},
onBadgeTouchEnd(event) {
const text = this.tooltipText;
if (!text || !this.label) {
return;
}
onComponentTooltipTouchEnd(this.badgeTooltipOptions(event, text));
},
badgeTooltipOptions(event, text) {
return {
event,
component: ActivityTableValueInfo,
componentProps: {
label: this.label,
value: text,
},
width: "22rem",
propsMatcher: (props) => this.componentPropsMatcher(props),
};
},
onBadgeMouseEnter(event) {
this.showComponentTooltip(event);
},
hideTooltip() {
mutations.hideTooltip();
hideInteractiveTooltip();
},
},
};
+4
View File
@@ -89,6 +89,10 @@ input.sizeInput:disabled {
color: var(--textPrimary);
}
.sizeInputWrapper:focus-within {
border-color: var(--primaryColor);
}
.sizeInputWrapper .sizeInput {
text-align: center;
border: none;
+4 -3
View File
@@ -71,8 +71,8 @@ i.spin {
-ms-overflow-style: none; /* IE and Edge */
position: fixed;
padding-top: var(--header-height);
padding-left: var(--safe-area-left);
padding-right: var(--safe-area-right);
padding-left: calc(0.5em + var(--safe-area-left));
padding-right: calc(0.5em + var(--safe-area-right));
overflow: unset;
top: 0;
/* fill the visible viewport only; the html background paints the black-translucent safe areas */
@@ -92,7 +92,8 @@ i.spin {
}
.main-padding {
padding: 4em .5em .5em;
padding-top: 4em;
padding-bottom: 0.5em;
}
/* Files */
-14
View File
@@ -27,20 +27,6 @@
border-color: var(--surfaceSecondary);
}
.dark-mode #search .search-input-container input::placeholder {
color: var(--textSecondary);
}
/* Active Search input */
.dark-mode #search.active .search-input-container {
background: var(--surfacePrimary);
border-color: white;
}
.dark-mode #search.active .search-input-container input {
color: var(--textPrimary);
}
/* Search result */
.dark-mode #search #result {
background: var(--background);
+3 -1
View File
@@ -407,7 +407,7 @@ button.flat-right.flat-left,
/* ExpandDropdown integration with adjacent inputs/buttons */
.expand-dropdown.form-compact {
width: auto;
min-width: 0;
min-width: max-content;
flex: 0 1 auto;
}
@@ -481,6 +481,8 @@ button.flat-right.flat-left,
display: flex;
flex-direction: row;
align-items: stretch;
margin-bottom: 1em;
margin-top: -0.5em;
}
.form-flex-group>.form-compact {
+5 -1
View File
@@ -717,7 +717,11 @@
"enforcedByAdmin": "لقد منع المسؤول المستخدمين من تغيير هذه الخاصية",
"singleClick": "انقر مرة واحدة لفتح الملفات",
"stickySidebar": "الشريط الجانبي الثابت",
"darkMode": "الوضع المظلم"
"darkMode": "الوضع المظلم",
"defaultViewMode": "وضع العرض الافتراضي",
"defaultViewModeDescription": "وضع العرض الافتراضي في المتصفحات أو الأجهزة الجديدة. لا يلغي تفضيلات العرض التي تم حفظها مسبقًا محليًّا.",
"defaultGallerySize": "الحجم الافتراضي للصورة المصغرة",
"defaultGallerySizeDescription": "مقياس الصور المصغرة الأولي للمعرض أو القائمة (1–9) في المتصفحات أو الأجهزة الجديدة. لا يلغي الأحجام المحفوظة مسبقًا محليًّا."
},
"colors": {
"red": "أحمر",
+5 -1
View File
@@ -555,7 +555,11 @@
"enforcedByAdmin": "Вашият администратор е ограничил възможността на потребителите да променят това свойство",
"singleClick": "Отваряне на файлове с едно кликване",
"stickySidebar": "Залепена странична лента",
"darkMode": "Тъмен режим"
"darkMode": "Тъмен режим",
"defaultViewMode": "Режим на изглед по подразбиране",
"defaultViewModeDescription": "Режим на първоначално показване в нови браузъри или устройства. Не отменя вече запазените локално настройки за показване.",
"defaultGallerySize": "Стандартен размер на миниатюрите",
"defaultGallerySizeDescription": "Начален мащаб на миниатюрите в галерията или списъка (1–9) при нови браузъри или устройства. Не отменя размерите, които вече са запазени локално."
},
"settings": {
"systemAdmin": "Система и администратор",
+5 -1
View File
@@ -391,7 +391,11 @@
"enforcedByAdmin": "Váš správce zakázal uživatelům měnit tuto vlastnost",
"singleClick": "Otevření souborů jedním kliknutím",
"stickySidebar": "Lepivý postranní panel",
"darkMode": "Tmavý režim"
"darkMode": "Tmavý režim",
"defaultViewMode": "Výchozí režim zobrazení",
"defaultViewModeDescription": "Výchozí režim zobrazení v nových prohlížečích nebo na nových zařízeních. Nepřepíše nastavení zobrazení, která jsou již uložena lokálně.",
"defaultGallerySize": "Výchozí velikost miniatury",
"defaultGallerySizeDescription": "Výchozí měřítko miniatur v galerii nebo seznamu (1–9) v nových prohlížečích nebo na nových zařízeních. Nepřepíše velikosti, které jsou již uloženy lokálně."
},
"settings": {
"enterPassword": "Zadejte heslo",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "Ihr Administrator hat verhindert, dass Benutzer diese Eigenschaft ändern können.",
"singleClick": "Dateien mit einem Klick öffnen",
"stickySidebar": "Feste Seitenleiste",
"darkMode": "Dunkler Modus"
"darkMode": "Dunkler Modus",
"defaultViewMode": "Standard-Anzeigemodus",
"defaultViewModeDescription": "Standardansichtsmodus in neuen Browsern oder auf neuen Geräten. Überschreibt keine bereits lokal gespeicherten Anzeigeeinstellungen.",
"defaultGallerySize": "Standardgröße der Miniaturansicht",
"defaultGallerySizeDescription": "Anfängliche Skalierung der Miniaturansichten in Galerien oder Listen (1–9) bei neuen Browsern oder Geräten. Überschreibt keine bereits lokal gespeicherten Größen."
},
"editor": {
"uninitialized": "Der Editor konnte nicht initialisiert werden. Bitte neu laden.",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "Ο διαχειριστής σας έχει απαγορεύσει στους χρήστες να τροποποιούν αυτήν την ιδιότητα",
"singleClick": "Ανοίξτε τα αρχεία με ένα μόνο κλικ",
"stickySidebar": "Πλευρική στήλη που παραμένει σταθερή",
"darkMode": "Σκοτεινή λειτουργία"
"darkMode": "Σκοτεινή λειτουργία",
"defaultViewMode": "Προεπιλεγμένη λειτουργία προβολής",
"defaultViewModeDescription": "Λειτουργία αρχικής προβολής σε νέους περιηγητές ή συσκευές. Δεν αντικαθιστά τις προτιμήσεις προβολής που έχουν ήδη αποθηκευτεί τοπικά.",
"defaultGallerySize": "Προεπιλεγμένο μέγεθος μικρογραφίας",
"defaultGallerySizeDescription": "Αρχική κλίμακα μικρογραφιών της γκαλερί ή της λίστας (1–9) σε νέους περιηγητές ή συσκευές. Δεν αντικαθιστά τα μεγέθη που έχουν ήδη αποθηκευτεί τοπικά."
},
"editor": {
"uninitialized": "Απέτυχε η αρχικοποίηση του συντάκτη. Παρακαλούμε επαναφορτώστε.",
+4
View File
@@ -489,6 +489,10 @@
"preferEditorForMarkdownDescription": "When enabled, open markdown files in the editor first instead of the Markdown Viewer.",
"deleteAfterArchive": "Delete source files after creation/extraction of archives",
"deleteAfterArchiveDescription": "When enabled, will delete the source files after successful creation/extraction of archives",
"defaultViewMode": "Default view mode",
"defaultViewModeDescription": "Initial view mode on new browsers or devices. Does not override view preferences already saved locally.",
"defaultGallerySize": "Default thumbnail size",
"defaultGallerySizeDescription": "Initial gallery or list thumbnail scale (1–9) on new browsers or devices. Does not override sizes already saved locally.",
"hideFilesInTree": "Hide files in tree navigation",
"hideFilesInTreeDescription": "When enabled, only folders are shown in the sidebar Tree Navigation.",
"previewFolderDescription": "Show thumbnail previews for folders with first child item as cover -- or multiple items if motion preview is enabled.",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "El administrador ha desactivado la posibilidad de que los usuarios modifiquen esta propiedad.",
"singleClick": "Abre los archivos con un solo clic",
"stickySidebar": "Barra lateral fija",
"darkMode": "Modo oscuro"
"darkMode": "Modo oscuro",
"defaultViewMode": "Modo de visualización predeterminado",
"defaultViewModeDescription": "Modo de visualización inicial en navegadores o dispositivos nuevos. No anula las preferencias de visualización ya guardadas localmente.",
"defaultGallerySize": "Tamaño predeterminado de las miniaturas",
"defaultGallerySizeDescription": "Escala inicial de las miniaturas de la galería o la lista (1–9) en navegadores o dispositivos nuevos. No anula los tamaños ya guardados localmente."
},
"editor": {
"uninitialized": "No se pudo iniciar el editor",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "Votre administrateur a désactivé la possibilité pour les utilisateurs de modifier cette propriété.",
"singleClick": "Un simple clic pour ouvrir les fichiers",
"stickySidebar": "Barre latérale fixe",
"darkMode": "Mode sombre"
"darkMode": "Mode sombre",
"defaultViewMode": "Mode d'affichage par défaut",
"defaultViewModeDescription": "Mode d'affichage par défaut sur les nouveaux navigateurs ou appareils. Ne remplace pas les préférences d'affichage déjà enregistrées localement.",
"defaultGallerySize": "Taille par défaut des vignettes",
"defaultGallerySizeDescription": "Échelle initiale des vignettes de la galerie ou de la liste (1 à 9) sur les nouveaux navigateurs ou appareils. Ne remplace pas les tailles déjà enregistrées localement."
},
"editor": {
"uninitialized": "Échec de l'initialisation de l'éditeur. Veuillez recharger.",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "המנהל שלך מנע ממשתמשים לשנות מאפיין זה",
"singleClick": "לחיצה אחת לפתיחת קבצים",
"stickySidebar": "סרגל צדדי קבוע",
"darkMode": "מצב כהה"
"darkMode": "מצב כהה",
"defaultViewMode": "מצב תצוגה ברירת מחדל",
"defaultViewModeDescription": "מצב תצוגה ראשוני בדפדפנים או במכשירים חדשים. אינו מבטל את הגדרות התצוגה שכבר נשמרו באופן מקומי.",
"defaultGallerySize": "גודל תמונה ממוזערת ברירת מחדל",
"defaultGallerySizeDescription": "גודל התמונות הממוזערות הראשוני בגלריה או ברשימה (1–9) בדפדפנים או במכשירים חדשים. הגדרה זו אינה מבטלת את הגדלים שכבר נשמרו באופן מקומי."
},
"editor": {
"uninitialized": "לא ניתן לאתחל את העורך. אנא טען מחדש.",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "A rendszergazda letiltotta a felhasználók számára ennek a tulajdonságnak a módosítását",
"singleClick": "A fájlok megnyitásához elég egy kattintás",
"stickySidebar": "Rögzített oldalsáv",
"darkMode": "Sötét mód"
"darkMode": "Sötét mód",
"defaultViewMode": "Alapértelmezett megjelenítési mód",
"defaultViewModeDescription": "Alapértelmezett megjelenítési mód új böngészőkön vagy eszközökön. Nem írja felül a már helyileg elmentett megjelenítési beállításokat.",
"defaultGallerySize": "Alapértelmezett miniatűrméret",
"defaultGallerySizeDescription": "A galéria vagy lista miniatűrjeinek kezdeti mérete (1–9) új böngészőkön vagy eszközökön. A már helyileg elmentett méreteket nem írja felül."
},
"editor": {
"uninitialized": "Nem sikerült inicializálni a szerkesztőt. Kérjük, töltse újra.",
+5 -1
View File
@@ -555,7 +555,11 @@
"enforcedByAdmin": "L'amministratore ha impedito agli utenti di modificare questa proprietà",
"singleClick": "Basta un clic per aprire i file",
"stickySidebar": "Barra laterale fissa",
"darkMode": "Modalità scura"
"darkMode": "Modalità scura",
"defaultViewMode": "Modalità di visualizzazione predefinita",
"defaultViewModeDescription": "Modalità di visualizzazione iniziale su nuovi browser o dispositivi. Non sovrascrive le preferenze di visualizzazione già salvate localmente.",
"defaultGallerySize": "Dimensione predefinita delle miniature",
"defaultGallerySizeDescription": "Scala iniziale delle miniature della galleria o dell'elenco (1–9) sui nuovi browser o dispositivi. Non sovrascrive le dimensioni già salvate localmente."
},
"settings": {
"systemAdmin": "Sistema e Admin",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "管理者が、ユーザーによるこのプロパティの変更を禁止しています",
"singleClick": "ワンクリックでファイルを開く",
"stickySidebar": "固定サイドバー",
"darkMode": "ダークモード"
"darkMode": "ダークモード",
"defaultViewMode": "デフォルトの表示モード",
"defaultViewModeDescription": "新しいブラウザやデバイスでの初期表示モードです。ローカルにすでに保存されている表示設定を上書きすることはありません。",
"defaultGallerySize": "デフォルトのサムネイルサイズ",
"defaultGallerySizeDescription": "新しいブラウザやデバイスでのギャラリーまたはリストのサムネイルの初期スケール(1~9)。ローカルにすでに保存されているサイズを上書きすることはありません。"
},
"editor": {
"uninitialized": "エディタの初期化に失敗しました。再読み込みしてください。",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "관리자가 사용자가 이 속성을 변경하지 못하도록 제한했습니다.",
"singleClick": "한 번의 클릭으로 파일 열기",
"stickySidebar": "고정 사이드바",
"darkMode": "다크 모드"
"darkMode": "다크 모드",
"defaultViewMode": "기본 보기 모드",
"defaultViewModeDescription": "새로운 브라우저나 기기에서 초기 보기 모드로 설정됩니다. 이미 로컬에 저장된 보기 환경설정은 무시되지 않습니다.",
"defaultGallerySize": "기본 썸네일 크기",
"defaultGallerySizeDescription": "새로운 브라우저나 기기에서 갤러리 또는 목록의 썸네일 초기 크기(1–9). 이미 로컬에 저장된 크기는 덮어쓰지 않습니다."
},
"editor": {
"uninitialized": "편집기를 초기화하지 못했습니다. 다시 로드하세요.",
+5 -1
View File
@@ -551,7 +551,11 @@
"enforcedByAdmin": "Uw beheerder heeft ervoor gezorgd dat gebruikers deze eigenschap niet kunnen wijzigen",
"singleClick": "Bestanden openen met één muisklik",
"stickySidebar": "Vaste zijbalk",
"darkMode": "Donkere modus"
"darkMode": "Donkere modus",
"defaultViewMode": "Standaardweergavemodus",
"defaultViewModeDescription": "Standaardweergavemodus in nieuwe browsers of op nieuwe apparaten. Hiermee worden de lokaal opgeslagen weergavevoorkeuren niet overschreven.",
"defaultGallerySize": "Standaardgrootte van miniatuurafbeeldingen",
"defaultGallerySizeDescription": "Standaardschaal (1–9) voor miniatuurafbeeldingen in galerijen of lijsten in nieuwe browsers of op nieuwe apparaten. Dit overschrijft geen afmetingen die al lokaal zijn opgeslagen."
},
"general": {
"name": "Naam",
+5 -1
View File
@@ -555,7 +555,11 @@
"enforcedByAdmin": "Uw beheerder heeft ervoor gezorgd dat gebruikers deze eigenschap niet kunnen wijzigen",
"singleClick": "Bestanden openen met één muisklik",
"stickySidebar": "Vaste zijbalk",
"darkMode": "Donkere modus"
"darkMode": "Donkere modus",
"defaultViewMode": "Standaardweergavemodus",
"defaultViewModeDescription": "Standaardweergavemodus in nieuwe browsers of op nieuwe apparaten. Dit overschrijft geen weergavevoorkeuren die al lokaal zijn opgeslagen.",
"defaultGallerySize": "Standaardgrootte van miniatuurafbeeldingen",
"defaultGallerySizeDescription": "Standaardschaal (1–9) voor miniaturen in galerijen of lijsten in nieuwe browsers of op nieuwe apparaten. Dit overschrijft geen afmetingen die al lokaal zijn opgeslagen."
},
"settings": {
"systemAdmin": "Systeem & Beheer",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "Administrator zablokował użytkownikom możliwość zmiany tej właściwości",
"singleClick": "Wystarczy jedno kliknięcie, aby otworzyć pliki",
"stickySidebar": "Przyklejony pasek boczny",
"darkMode": "Tryb ciemny"
"darkMode": "Tryb ciemny",
"defaultViewMode": "Domyślny tryb wyświetlania",
"defaultViewModeDescription": "Tryb wyświetlania domyślnego w nowych przeglądarkach lub na nowych urządzeniach. Nie zastępuje preferencji wyświetlania zapisanych już lokalnie.",
"defaultGallerySize": "Domyślny rozmiar miniatury",
"defaultGallerySizeDescription": "Początkowa skala miniatur w galerii lub na liście (1–9) w nowych przeglądarkach lub na nowych urządzeniach. Nie zastępuje rozmiarów już zapisanych lokalnie."
},
"editor": {
"uninitialized": "Nie udało się zainicjować edytora. Przeładuj ponownie.",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "O administrador impediu que os usuários alterem essa propriedade",
"singleClick": "Basta um clique para abrir os arquivos",
"stickySidebar": "Barra lateral fixa",
"darkMode": "Modo escuro"
"darkMode": "Modo escuro",
"defaultViewMode": "Modo de visualização padrão",
"defaultViewModeDescription": "Modo de visualização inicial em novos navegadores ou dispositivos. Não substitui as preferências de visualização já salvas localmente.",
"defaultGallerySize": "Tamanho padrão da miniatura",
"defaultGallerySizeDescription": "Escala inicial das miniaturas da galeria ou da lista (1–9) em novos navegadores ou dispositivos. Não substitui os tamanhos já salvos localmente."
},
"editor": {
"uninitialized": "Falha ao inicializar o editor. Favor recarregar.",
+5 -1
View File
@@ -551,7 +551,11 @@
"enforcedByAdmin": "O seu administrador impediu que os utilizadores alterem esta propriedade",
"singleClick": "Basta um clique para abrir ficheiros",
"stickySidebar": "Barra lateral fixa",
"darkMode": "Modo escuro"
"darkMode": "Modo escuro",
"defaultViewMode": "Modo de visualização predefinido",
"defaultViewModeDescription": "Modo de visualização inicial em novos navegadores ou dispositivos. Não substitui as preferências de visualização já guardadas localmente.",
"defaultGallerySize": "Tamanho padrão da miniatura",
"defaultGallerySizeDescription": "Escala inicial das miniaturas da galeria ou da lista (1–9) em novos navegadores ou dispositivos. Não substitui os tamanhos já guardados localmente."
},
"general": {
"name": "Nome",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "Administratorul a blocat utilizatorilor posibilitatea de a modifica această proprietate",
"singleClick": "Deschideți fișierele cu un singur clic",
"stickySidebar": "Bară laterală fixă",
"darkMode": "Modul întunecat"
"darkMode": "Modul întunecat",
"defaultViewMode": "Modul de vizualizare implicit",
"defaultViewModeDescription": "Modul de afișare inițial pe browserele sau dispozitivele noi. Nu înlocuiește preferințele de afișare salvate deja local.",
"defaultGallerySize": "Dimensiunea implicită a miniaturilor",
"defaultGallerySizeDescription": "Scala inițială a miniaturilor din galerie sau listă (1–9) pe browserele sau dispozitivele noi. Nu înlocuiește dimensiunile deja salvate local."
},
"editor": {
"uninitialized": "Nu s-a reușit inițializarea editorului. Vă rugăm să reîncărcați.",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "Ваш администратор запретил пользователям изменять это свойство",
"singleClick": "Открытие файлов одним щелчком мыши",
"stickySidebar": "Фиксированная боковая панель",
"darkMode": "Темный режим"
"darkMode": "Темный режим",
"defaultViewMode": "Режим просмотра по умолчанию",
"defaultViewModeDescription": "Режим начального отображения в новых браузерах или на новых устройствах. Не переопределяет настройки просмотра, уже сохраненные локально.",
"defaultGallerySize": "Размер миниатюры по умолчанию",
"defaultGallerySizeDescription": "Начальный масштаб миниатюр в галерее или списке (1–9) в новых браузерах или на новых устройствах. Не переопределяет размеры, уже сохраненные локально."
},
"editor": {
"uninitialized": "Не удалось инициализировать редактор. Пожалуйста, перезагрузитесь.",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "Váš správca zakázal používateľom meniť túto vlastnosť",
"singleClick": "Otvorenie súborov jedným kliknutím",
"stickySidebar": "Lepivý bočný panel",
"darkMode": "Tmavý režim"
"darkMode": "Tmavý režim",
"defaultViewMode": "Predvolený režim zobrazenia",
"defaultViewModeDescription": "Režim počiatočného zobrazenia v nových prehliadačoch alebo na nových zariadeniach. Neprepisuje nastavenia zobrazenia, ktoré sú už uložené lokálne.",
"defaultGallerySize": "Predvolená veľkosť náhľadu",
"defaultGallerySizeDescription": "Počiatočné zväčšenie miniatúr v galérii alebo zozname (1–9) v nových prehliadačoch alebo na nových zariadeniach. Neprepisuje veľkosti, ktoré sú už uložené lokálne."
},
"editor": {
"uninitialized": "Nepodarilo sa inicializovať editor. Prosím, načítajte ho znova.",
+5 -1
View File
@@ -551,7 +551,11 @@
"enforcedByAdmin": "Din administratör har spärrat användarnas möjlighet att ändra den här egenskapen",
"singleClick": "Öppna filer med ett enda klick",
"stickySidebar": "Fast sidomeny",
"darkMode": "Mörkt läge"
"darkMode": "Mörkt läge",
"defaultViewMode": "Standardvisningsläge",
"defaultViewModeDescription": "Standardvisningsläge i nya webbläsare eller på nya enheter. Åsidosätter inte visningsinställningar som redan sparats lokalt.",
"defaultGallerySize": "Standardstorlek på miniatyrbilder",
"defaultGallerySizeDescription": "Initial skala för miniatyrbilder i gallerier eller listor (1–9) i nya webbläsare eller på nya enheter. Åsidosätter inte storlekar som redan sparats lokalt."
},
"general": {
"name": "Namn",
+5 -1
View File
@@ -725,7 +725,11 @@
"enforcedByAdmin": "Yöneticiniz, kullanıcıların bu özelliği değiştirmesini engellemiştir",
"singleClick": "Dosyaları tek tıklamayla aç",
"stickySidebar": "Sabit kenar çubuğu",
"darkMode": "Koyu mod"
"darkMode": "Koyu mod",
"defaultViewMode": "Varsayılan görüntüleme modu",
"defaultViewModeDescription": "Yeni tarayıcılarda veya cihazlarda ilk görüntüleme modu. Yerel olarak önceden kaydedilmiş görüntüleme tercihlerini geçersiz kılmaz.",
"defaultGallerySize": "Varsayılan küçük resim boyutu",
"defaultGallerySizeDescription": "Yeni tarayıcılarda veya cihazlarda galerinin veya listenin başlangıçtaki küçük resim ölçeği (1–9). Yerel olarak önceden kaydedilmiş boyutları geçersiz kılmaz."
},
"editor": {
"uninitialized": "Düzenleyici başlatılamadı. Lütfen yeniden yükleyin.",
+5 -1
View File
@@ -551,7 +551,11 @@
"enforcedByAdmin": "Ваш адміністратор заборонив користувачам змінювати це властивість",
"singleClick": "Відкриття файлів одним клацанням миші",
"stickySidebar": "Фіксована бічна панель",
"darkMode": "Темний режим"
"darkMode": "Темний режим",
"defaultViewMode": "Режим перегляду за замовчуванням",
"defaultViewModeDescription": "Режим початкового відображення в нових браузерах або на нових пристроях. Не замінює налаштування відображення, які вже збережено локально.",
"defaultGallerySize": "Розмір мініатюри за замовчуванням",
"defaultGallerySizeDescription": "Початковий масштаб мініатюр у галереї або списку (1–9) у нових браузерах або на нових пристроях. Не замінює розміри, які вже збережено локально."
},
"general": {
"name": "Ім'я",
+5 -1
View File
@@ -551,7 +551,11 @@
"enforcedByAdmin": "系统管理员已禁止用户更改此属性",
"singleClick": "单击即可打开文件",
"stickySidebar": "固定侧边栏",
"darkMode": "深色模式"
"darkMode": "深色模式",
"defaultViewMode": "默认视图模式",
"defaultViewModeDescription": "在新浏览器或设备上的初始视图模式。不会覆盖已保存在本地的视图偏好设置。",
"defaultGallerySize": "默认缩略图大小",
"defaultGallerySizeDescription": "在新浏览器或设备上显示的图库或列表缩略图初始缩放比例(1–9)。不会覆盖已保存在本地中的尺寸设置。"
},
"general": {
"name": "名称",
+5 -1
View File
@@ -520,7 +520,11 @@
"enforcedByAdmin": "您的系統管理員已禁止使用者變更此屬性",
"singleClick": "單擊即可開啟檔案",
"stickySidebar": "固定側邊欄",
"darkMode": "深色模式"
"darkMode": "深色模式",
"defaultViewMode": "預設檢視模式",
"defaultViewModeDescription": "在新瀏覽器或裝置上的初始檢視模式。不會覆寫已儲存於本機的檢視偏好設定。",
"defaultGallerySize": "預設縮圖大小",
"defaultGallerySizeDescription": "在新瀏覽器或裝置上顯示的初始圖庫或清單縮圖比例(1–9)。此設定不會覆寫已儲存於本地的尺寸。"
},
"otp": {
"name": "雙因素驗證",
+2
View File
@@ -7,8 +7,10 @@ import router from "./router"; // Adjust the path as per your setup
import "./css/styles.css";
import "./css/dark.css";
import { initPwaInstall } from "@/utils/pwaInstall";
import { defaultDarkMode, syncDocumentTheme } from "@/utils/theme";
initPwaInstall();
syncDocumentTheme(defaultDarkMode());
const app = createApp(App);
@@ -0,0 +1,75 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { stateMock, globalVarsMock } = vi.hoisted(() => ({
stateMock: {
user: { username: '', darkMode: true },
shareInfo: null,
disableEventThemes: false,
route: { path: '/login' },
},
globalVarsMock: { darkMode: false, noAuth: false },
}));
vi.mock('./state', () => ({ state: stateMock }));
vi.mock('./mutations', () => ({
mutations: {
updateCurrentUser: vi.fn(),
},
}));
vi.mock('@/utils/constants', () => ({ globalVars: globalVarsMock, previewViews: [], tools: () => [] }));
vi.mock('@/i18n', () => ({
default: { global: { t: (key) => key } },
detectLocale: () => 'en',
}));
vi.mock('@/utils/url.js', () => ({
buildItemUrl: vi.fn(),
removeLeadingSlash: vi.fn((value) => value),
removePrefix: vi.fn((value) => value),
}));
vi.mock('@/utils', () => ({}));
vi.mock('@/utils/files.js', () => ({ getFileExtension: vi.fn() }));
vi.mock('@/utils/mimetype', () => ({
getTypeInfo: vi.fn(),
isHtmlMimeType: vi.fn(),
isRichTextPreviewMimeType: vi.fn(),
}));
vi.mock('@/utils/moment', () => ({ fromNow: vi.fn() }));
vi.mock('@/utils/object.js', () => ({
getNestedProperty: vi.fn(),
getObjectProperty: vi.fn(),
}));
import { getters } from './getters.ts';
describe('getters.isDarkMode logged-out behavior', () => {
beforeEach(() => {
stateMock.user = { username: '', darkMode: true, locale: 'en' };
stateMock.shareInfo = null;
stateMock.disableEventThemes = false;
globalVarsMock.darkMode = false;
globalVarsMock.noAuth = false;
});
it('uses userDefaults (globalVars.darkMode) when not logged in', () => {
globalVarsMock.darkMode = false;
expect(getters.isDarkMode()).toBe(false);
globalVarsMock.darkMode = true;
expect(getters.isDarkMode()).toBe(true);
});
it('uses user preference when logged in', () => {
stateMock.user = { username: 'alice', darkMode: false, locale: 'en' };
globalVarsMock.darkMode = true;
expect(getters.isDarkMode()).toBe(false);
stateMock.user.darkMode = true;
expect(getters.isDarkMode()).toBe(true);
});
it('anonymous user follows userDefaults because they are not logged in', () => {
stateMock.user = { username: 'anonymous', darkMode: true, locale: 'en' };
globalVarsMock.darkMode = false;
expect(getters.isDarkMode()).toBe(false);
});
});
+4 -3
View File
@@ -8,6 +8,7 @@ import { getTypeInfo, isHtmlMimeType, isRichTextPreviewMimeType } from '@/utils/
import { fromNow } from '@/utils/moment';
import { getNestedProperty, getObjectProperty } from '@/utils/object.js';
import { buildItemUrl, removeLeadingSlash, removePrefix } from '@/utils/url.js';
import { defaultDarkMode } from '@/utils/theme';
import type { DisplayPreference, FileListItem } from './types';
export const getters = {
@@ -165,8 +166,8 @@ export const getters = {
if (!getters.isShare() && getters.eventTheme() === "halloween") {
return true
}
if (state.user === null) {
return true
if (!getters.isLoggedIn()) {
return defaultDarkMode()
}
return state.user.darkMode === true
},
@@ -546,7 +547,7 @@ export const getters = {
deleteAfterArchive: true,
stickySidebar: true,
hideFilesInTree: false,
darkMode: true,
darkMode: defaultDarkMode(),
dateFormat: false,
disableViewingExt: "",
disableOfficePreviewExt: "",
+2
View File
@@ -658,6 +658,8 @@ export const mutations = {
"showSelectMultiple",
"debugOffice",
"disableUpdateNotifications",
"viewMode",
"gallerySize",
];
const updatedProperties = Object.keys(value).filter(
(key) =>
+2 -1
View File
@@ -1,5 +1,6 @@
import { reactive } from 'vue';
import { detectLocale } from "@/i18n";
import { globalVars } from "@/utils/constants";
import type { StoreState } from "./types";
export const state: StoreState = reactive({
@@ -71,7 +72,7 @@ export const state: StoreState = reactive({
showHidden: false, // Default to false, assuming this is a boolean
scopes: [],
permissions: {}, // Default to an empty object for permissions
darkMode: true, // Default to false, assuming this is a boolean
darkMode: globalVars.darkMode === true,
disableSettings: false,
debugOffice: false, // Debug mode for OnlyOffice integration
preferEditorForMarkdown: false,
+12 -1
View File
@@ -1,7 +1,18 @@
import i18n from '@/i18n';
import { getIconClass } from './material-symbols';
const globalVars = window.globalVars;
function readAppConfig() {
const el = document.getElementById('app-config');
if (el?.textContent) {
return JSON.parse(el.textContent);
}
return window.globalVars;
}
const globalVars = readAppConfig();
if (typeof window !== 'undefined') {
window.globalVars = globalVars;
}
const origin = window.location.origin;
const settings = [
@@ -89,7 +89,6 @@
"gps_off",
"grid_view",
"group",
"help",
"home",
"horizontal_rule",
"hourglass",
+18
View File
@@ -0,0 +1,18 @@
import { globalVars } from '@/utils/constants';
export function defaultDarkMode() {
return globalVars.darkMode === true;
}
export function syncDocumentTheme(dark) {
document.documentElement.classList.toggle('dark-mode', dark);
document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
const meta = document.querySelector('meta[name="theme-color"]');
if (!meta) {
return;
}
const bg = getComputedStyle(document.documentElement).getPropertyValue('--background').trim();
if (bg) {
meta.setAttribute('content', bg);
}
}
+49
View File
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const { globalVarsMock } = vi.hoisted(() => ({
globalVarsMock: { darkMode: false },
}));
vi.mock('@/utils/constants', () => ({
globalVars: globalVarsMock,
}));
import { defaultDarkMode, syncDocumentTheme } from './theme.js';
describe('theme utils', () => {
beforeEach(() => {
globalVarsMock.darkMode = false;
document.documentElement.className = '';
document.documentElement.style.setProperty('--background', '#f5f5f5');
document.head.innerHTML = '<meta name="theme-color" content="#000000">';
});
afterEach(() => {
document.documentElement.className = '';
document.head.innerHTML = '';
});
it('defaultDarkMode returns true only when globalVars.darkMode is true', () => {
globalVarsMock.darkMode = false;
expect(defaultDarkMode()).toBe(false);
globalVarsMock.darkMode = true;
expect(defaultDarkMode()).toBe(true);
globalVarsMock.darkMode = null;
expect(defaultDarkMode()).toBe(false);
});
it('syncDocumentTheme toggles html class and updates theme-color from --background', () => {
document.documentElement.style.setProperty('--background', '#141D24');
syncDocumentTheme(true);
expect(document.documentElement.classList.contains('dark-mode')).toBe(true);
expect(document.querySelector('meta[name="theme-color"]').getAttribute('content')).toBe('#141D24');
document.documentElement.style.setProperty('--background', '#f5f5f5');
syncDocumentTheme(false);
expect(document.documentElement.classList.contains('dark-mode')).toBe(false);
expect(document.querySelector('meta[name="theme-color"]').getAttribute('content')).toBe('#f5f5f5');
});
});
+260
View File
@@ -0,0 +1,260 @@
import { getters, mutations, state } from "@/store";
let dismissHandler = null;
let dismissTimer = null;
let tapCooldown = false;
let tapCooldownTimer = null;
let globalDismissListenersRegistered = false;
function onGlobalDismiss() {
if (state.tooltip.show) {
hideInteractiveTooltip(true);
}
}
function ensureGlobalDismissListeners() {
if (globalDismissListenersRegistered || typeof document === "undefined") {
return;
}
globalDismissListenersRegistered = true;
document.addEventListener("scroll", onGlobalDismiss, {
capture: true,
passive: true,
});
window.addEventListener("popstate", onGlobalDismiss);
window.addEventListener("hashchange", onGlobalDismiss);
}
ensureGlobalDismissListeners();
export function useTapForTooltip() {
if (getters.isMobile()) {
return true;
}
if (typeof window !== "undefined" && window.matchMedia) {
return window.matchMedia("(hover: none)").matches;
}
return false;
}
export function tooltipEventCoords(event) {
const touch = event?.changedTouches?.[0] || event?.touches?.[0];
if (touch) {
return { x: touch.clientX, y: touch.clientY };
}
if (typeof event?.clientX === "number" && typeof event?.clientY === "number") {
return { x: event.clientX, y: event.clientY };
}
if (event?.currentTarget instanceof Element) {
const rect = event.currentTarget.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
}
return { x: 0, y: 0 };
}
function isTooltipTrigger(target) {
return (
target instanceof Element &&
target.closest(".tooltip-info-icon, .floating-tooltip")
);
}
function isTapOpenedTooltip() {
return state.tooltip.show && state.tooltip.pointerEvents;
}
function startTapCooldown() {
tapCooldown = true;
if (tapCooldownTimer) {
clearTimeout(tapCooldownTimer);
}
tapCooldownTimer = setTimeout(() => {
tapCooldown = false;
tapCooldownTimer = null;
}, 300);
}
export function shouldIgnoreOutsideTap() {
return tapCooldown;
}
function unregisterTooltipDismiss() {
if (dismissTimer) {
clearTimeout(dismissTimer);
dismissTimer = null;
}
if (!dismissHandler) {
return;
}
document.removeEventListener("click", dismissHandler);
dismissHandler = null;
}
function registerTooltipDismiss() {
unregisterTooltipDismiss();
dismissHandler = (event) => {
if (tapCooldown || isTooltipTrigger(event.target)) {
return;
}
hideInteractiveTooltip(true);
};
dismissTimer = setTimeout(() => {
dismissTimer = null;
if (dismissHandler) {
document.addEventListener("click", dismissHandler);
}
}, 0);
}
export function hideInteractiveTooltip(force = false) {
if (!force && useTapForTooltip() && isTapOpenedTooltip()) {
return;
}
unregisterTooltipDismiss();
mutations.hideTooltip();
}
export function showHoverTooltip(content, event) {
const { x, y } = tooltipEventCoords(event);
mutations.showTooltip({
content,
x,
y,
pointerEvents: false,
});
}
function showTapTooltip(content, event) {
const { x, y } = tooltipEventCoords(event);
mutations.showTooltip({
content,
x,
y,
pointerEvents: true,
});
registerTooltipDismiss();
}
/** @deprecated Use showHoverTooltip or tap handlers instead */
export function showInteractiveTooltip(content, event) {
if (useTapForTooltip()) {
showTapTooltip(content, event);
return;
}
showHoverTooltip(content, event);
}
export function isTooltipContentVisible(content) {
return (
state.tooltip.show &&
!state.tooltip.component &&
state.tooltip.content === content
);
}
export function onTooltipHelpMouseEnter(event, content) {
if (tapCooldown || isTapOpenedTooltip()) {
return;
}
showHoverTooltip(content, event);
}
export function onTooltipHelpMouseLeave() {
if (isTapOpenedTooltip()) {
return;
}
hideInteractiveTooltip(true);
}
export function onTooltipHelpTouchEnd(event, content) {
if (!useTapForTooltip()) {
return;
}
event.stopPropagation();
startTapCooldown();
unregisterTooltipDismiss();
showTapTooltip(content, event);
}
export function onTooltipHelpClick(event, content) {
event.stopPropagation();
if (useTapForTooltip()) {
return;
}
showHoverTooltip(content, event);
}
export function isComponentTooltipVisible(component, propsMatcher = () => true) {
if (!state.tooltip.show || state.tooltip.component !== component) {
return false;
}
return propsMatcher(state.tooltip.componentProps ?? {});
}
export function showHoverComponentTooltip({
component,
componentProps,
event,
width,
}) {
const { x, y } = tooltipEventCoords(event);
mutations.showTooltip({
component,
componentProps,
x,
y,
width: width ?? null,
pointerEvents: false,
});
}
function showTapComponentTooltip({ component, componentProps, event, width }) {
const { x, y } = tooltipEventCoords(event);
mutations.showTooltip({
component,
componentProps,
x,
y,
width: width ?? null,
pointerEvents: true,
});
registerTooltipDismiss();
}
export function showInteractiveComponentTooltip({
component,
componentProps,
event,
pointerEvents,
width,
}) {
const tap = pointerEvents ?? useTapForTooltip();
if (tap) {
showTapComponentTooltip({ component, componentProps, event, width });
return;
}
showHoverComponentTooltip({ component, componentProps, event, width });
}
export function onComponentTooltipTouchEnd(options) {
if (!useTapForTooltip()) {
return;
}
const { event } = options;
event.stopPropagation();
startTapCooldown();
unregisterTooltipDismiss();
showTapComponentTooltip(options);
}
export function onComponentTooltipClick(options) {
const { event } = options;
event.stopPropagation();
if (useTapForTooltip()) {
return;
}
showHoverComponentTooltip(options);
}
+296
View File
@@ -0,0 +1,296 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const { storeMock } = vi.hoisted(() => ({
storeMock: {
state: {
tooltip: {
show: false,
content: "",
component: null,
pointerEvents: false,
},
},
getters: {
isMobile: vi.fn(() => false),
},
mutations: {
showTooltip: vi.fn(),
hideTooltip: vi.fn(),
},
},
}));
vi.mock("@/store", () => storeMock);
import {
hideInteractiveTooltip,
isTooltipContentVisible,
onTooltipHelpClick,
onTooltipHelpMouseEnter,
onTooltipHelpMouseLeave,
onTooltipHelpTouchEnd,
shouldIgnoreOutsideTap,
showHoverTooltip,
showInteractiveTooltip,
tooltipEventCoords,
} from "./tooltipHelp.js";
describe("tooltipHelp", () => {
beforeEach(() => {
storeMock.getters.isMobile.mockReturnValue(false);
storeMock.state.tooltip.show = false;
storeMock.state.tooltip.content = "";
storeMock.state.tooltip.component = null;
storeMock.state.tooltip.pointerEvents = false;
storeMock.mutations.showTooltip.mockClear();
storeMock.mutations.hideTooltip.mockClear();
hideInteractiveTooltip(true);
storeMock.mutations.showTooltip.mockClear();
storeMock.mutations.hideTooltip.mockClear();
vi.stubGlobal("matchMedia", vi.fn(() => ({ matches: false })));
});
afterEach(() => {
hideInteractiveTooltip(true);
vi.unstubAllGlobals();
});
it("uses touch coordinates from touchend events", () => {
const coords = tooltipEventCoords({
changedTouches: [{ clientX: 42, clientY: 84 }],
});
expect(coords).toEqual({ x: 42, y: 84 });
});
it("prefers touch coordinates when clientX is undefined", () => {
const coords = tooltipEventCoords({
clientX: undefined,
clientY: undefined,
changedTouches: [{ clientX: 10, clientY: 20 }],
});
expect(coords).toEqual({ x: 10, y: 20 });
});
it("uses mouse coordinates for hover events", () => {
const coords = tooltipEventCoords({ clientX: 15, clientY: 25 });
expect(coords).toEqual({ x: 15, y: 25 });
});
it("shows tooltip on desktop hover", () => {
const event = { clientX: 10, clientY: 20 };
onTooltipHelpMouseEnter(event, "help text");
expect(storeMock.mutations.showTooltip).toHaveBeenCalledWith({
content: "help text",
x: 10,
y: 20,
pointerEvents: false,
});
});
it("shows hover tooltip even when tap mode is enabled", () => {
vi.stubGlobal("matchMedia", vi.fn(() => ({ matches: true })));
const event = { clientX: 10, clientY: 20 };
onTooltipHelpMouseEnter(event, "help text");
expect(storeMock.mutations.showTooltip).toHaveBeenCalledWith({
content: "help text",
x: 10,
y: 20,
pointerEvents: false,
});
});
it("hides tooltip on desktop mouse leave", () => {
onTooltipHelpMouseLeave();
expect(storeMock.mutations.hideTooltip).toHaveBeenCalled();
});
it("shows tooltip on mobile tap and keeps it open on repeat tap", () => {
storeMock.getters.isMobile.mockReturnValue(true);
const event = {
clientX: 5,
clientY: 6,
preventDefault: vi.fn(),
stopPropagation: vi.fn(),
};
onTooltipHelpTouchEnd(event, "mobile help");
expect(event.stopPropagation).toHaveBeenCalled();
expect(storeMock.mutations.showTooltip).toHaveBeenCalledWith({
content: "mobile help",
x: 5,
y: 6,
pointerEvents: true,
});
storeMock.state.tooltip.show = true;
storeMock.state.tooltip.content = "mobile help";
storeMock.state.tooltip.pointerEvents = true;
storeMock.mutations.hideTooltip.mockClear();
storeMock.mutations.showTooltip.mockClear();
onTooltipHelpTouchEnd(event, "mobile help");
expect(storeMock.mutations.hideTooltip).not.toHaveBeenCalled();
expect(storeMock.mutations.showTooltip).toHaveBeenCalledTimes(1);
});
it("ignores the synthetic click after a mobile tap", () => {
storeMock.getters.isMobile.mockReturnValue(true);
const touchEvent = {
clientX: 8,
clientY: 9,
stopPropagation: vi.fn(),
};
const clickEvent = {
clientX: 8,
clientY: 9,
stopPropagation: vi.fn(),
};
onTooltipHelpTouchEnd(touchEvent, "touch help");
onTooltipHelpClick(clickEvent, "touch help");
expect(storeMock.mutations.showTooltip).toHaveBeenCalledTimes(1);
expect(shouldIgnoreOutsideTap()).toBe(true);
});
it("tracks visible tooltip content", () => {
storeMock.state.tooltip.show = true;
storeMock.state.tooltip.content = "visible";
expect(isTooltipContentVisible("visible")).toBe(true);
expect(isTooltipContentVisible("other")).toBe(false);
});
it("ignores hover-driven hide while a tap tooltip is open", () => {
storeMock.getters.isMobile.mockReturnValue(true);
showInteractiveTooltip("help", { clientX: 1, clientY: 2 });
storeMock.state.tooltip.show = true;
storeMock.state.tooltip.pointerEvents = true;
storeMock.mutations.hideTooltip.mockClear();
hideInteractiveTooltip();
expect(storeMock.mutations.hideTooltip).not.toHaveBeenCalled();
hideInteractiveTooltip(true);
expect(storeMock.mutations.hideTooltip).toHaveBeenCalled();
});
it("hides tooltip on scroll", () => {
storeMock.state.tooltip.show = true;
document.dispatchEvent(new Event("scroll"));
expect(storeMock.mutations.hideTooltip).toHaveBeenCalled();
});
it("hides tooltip on browser navigation", () => {
storeMock.state.tooltip.show = true;
window.dispatchEvent(new Event("popstate"));
expect(storeMock.mutations.hideTooltip).toHaveBeenCalled();
});
it("ignores synthetic mouseleave while a tap tooltip is open", () => {
storeMock.getters.isMobile.mockReturnValue(true);
onTooltipHelpTouchEnd(
{ clientX: 1, clientY: 2, stopPropagation: vi.fn() },
"tap help",
);
storeMock.state.tooltip.show = true;
storeMock.state.tooltip.pointerEvents = true;
storeMock.mutations.hideTooltip.mockClear();
onTooltipHelpMouseLeave();
expect(storeMock.mutations.hideTooltip).not.toHaveBeenCalled();
});
it("does not replace a tap tooltip with a hover tooltip on mouseenter", () => {
storeMock.getters.isMobile.mockReturnValue(true);
onTooltipHelpTouchEnd(
{ clientX: 1, clientY: 2, stopPropagation: vi.fn() },
"tap help",
);
storeMock.state.tooltip.show = true;
storeMock.state.tooltip.pointerEvents = true;
storeMock.mutations.showTooltip.mockClear();
onTooltipHelpMouseEnter({ clientX: 3, clientY: 4 }, "hover help");
expect(storeMock.mutations.showTooltip).not.toHaveBeenCalled();
});
it("switches tap tooltip when tapping another icon", () => {
storeMock.getters.isMobile.mockReturnValue(true);
const firstEvent = {
clientX: 5,
clientY: 6,
stopPropagation: vi.fn(),
};
const secondEvent = {
clientX: 20,
clientY: 30,
stopPropagation: vi.fn(),
};
onTooltipHelpTouchEnd(firstEvent, "first help");
storeMock.state.tooltip.show = true;
storeMock.state.tooltip.content = "first help";
storeMock.state.tooltip.pointerEvents = true;
onTooltipHelpMouseLeave();
storeMock.mutations.showTooltip.mockClear();
storeMock.mutations.hideTooltip.mockClear();
onTooltipHelpTouchEnd(secondEvent, "second help");
storeMock.state.tooltip.show = true;
storeMock.state.tooltip.pointerEvents = true;
onTooltipHelpMouseLeave();
expect(storeMock.mutations.hideTooltip).not.toHaveBeenCalled();
expect(storeMock.mutations.showTooltip).toHaveBeenCalledWith({
content: "second help",
x: 20,
y: 30,
pointerEvents: true,
});
});
it("switches tooltip when tapping a different help icon", () => {
storeMock.getters.isMobile.mockReturnValue(true);
const firstEvent = {
clientX: 5,
clientY: 6,
stopPropagation: vi.fn(),
};
const secondEvent = {
clientX: 20,
clientY: 30,
stopPropagation: vi.fn(),
};
onTooltipHelpTouchEnd(firstEvent, "first help");
storeMock.state.tooltip.show = true;
storeMock.state.tooltip.content = "first help";
storeMock.state.tooltip.pointerEvents = true;
storeMock.mutations.showTooltip.mockClear();
onTooltipHelpTouchEnd(secondEvent, "second help");
storeMock.state.tooltip.show = true;
storeMock.state.tooltip.pointerEvents = true;
storeMock.mutations.hideTooltip.mockClear();
hideInteractiveTooltip();
expect(storeMock.mutations.hideTooltip).not.toHaveBeenCalled();
expect(storeMock.mutations.showTooltip).toHaveBeenCalledWith({
content: "second help",
x: 20,
y: 30,
pointerEvents: true,
});
});
it("showHoverTooltip always uses hover mode", () => {
storeMock.getters.isMobile.mockReturnValue(true);
showHoverTooltip("hover help", { clientX: 3, clientY: 4 });
expect(storeMock.mutations.showTooltip).toHaveBeenCalledWith({
content: "hover help",
x: 3,
y: 4,
pointerEvents: false,
});
});
});
+57
View File
@@ -174,3 +174,60 @@ export function applySectionsToFlatUser(user, sections) {
user.fileLoading = { ...(user.fileLoading || {}), ...s.fileLoading };
}
}
/** Maps flat PATCH field names to nested enforced user-default paths. */
const FLAT_PROFILE_FIELD_ENFORCED_PATHS = {
locale: ["ui", "locale"],
dateFormat: ["listing", "dateFormat"],
themeColor: ["ui", "themeColor"],
customTheme: ["ui", "customTheme"],
quickDownload: ["listing", "quickDownload"],
stickySidebar: ["sidebar", "sticky"],
singleClick: ["listing", "singleClick"],
darkMode: ["ui", "darkMode"],
showHidden: ["listing", "showHidden"],
showToolsInSidebar: ["sidebar", "showTools"],
deleteAfterArchive: ["listing", "deleteAfterArchive"],
deleteWithoutConfirming: ["listing", "deleteWithoutConfirming"],
preferEditorForMarkdown: ["fileViewer", "preferEditorForMarkdown"],
disablePreviewExt: ["preview", "disablePreviewExt"],
disableViewingExt: ["fileViewer", "disableViewingExt"],
disableOnlyOfficeExt: ["fileViewer", "disableOnlyOfficeExt"],
hideFileExt: ["listing", "hideFileExt"],
disableQuickToggles: ["sidebar", "disableQuickToggles"],
disableSearchOptions: ["search", "disableOptions"],
hideSidebarFileActions: ["sidebar", "hideFileActions"],
showCopyPath: ["listing", "showCopyPath"],
hideFilesInTree: ["sidebar", "hideFiles"],
editorQuickSave: ["fileViewer", "editorQuickSave"],
showSelectMultiple: ["listing", "showSelectMultiple"],
debugOffice: ["fileViewer", "debugOffice"],
viewMode: ["listing", "viewMode"],
gallerySize: ["listing", "gallerySize"],
};
function enforcedFlagAt(enforced, section, field) {
if (!enforced || !section) {
return false;
}
const sectionData = enforced[section];
if (!sectionData || typeof sectionData !== "object") {
return false;
}
return !!sectionData[field];
}
/** True when global user-default enforcement locks a flat profile PATCH field. */
export function isFlatProfileFieldEnforced(enforced, flatField) {
if (!enforced || !flatField) {
return false;
}
if (flatField === "preview" || flatField === "fileLoading") {
return false;
}
const path = FLAT_PROFILE_FIELD_ENFORCED_PATHS[flatField];
if (!path) {
return false;
}
return enforcedFlagAt(enforced, path[0], path[1]);
}
+2 -4
View File
@@ -46,6 +46,7 @@ import { state, getters, mutations } from "@/store";
import { events, notify } from "@/notify";
import { generateRandomCode } from "@/utils/auth";
import { globalVars } from "@/utils/constants";
import { syncDocumentTheme } from "@/utils/theme";
import Search from "@/components/Search.vue";
export default {
@@ -170,10 +171,7 @@ export default {
isDarkMode: {
immediate: true,
handler(dark) {
const color = dark ? globalVars.darkBackground : globalVars.lightBackground;
if (!color) return;
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", color);
syncDocumentTheme(dark);
},
},
},
+6 -15
View File
@@ -35,10 +35,7 @@
<div v-if="passwordAvailable || ldapAvailable" class="password-entry">
<div v-if="error !== ''" class="wrong-login card">
<span>{{ $t("login.failedLogin") }}</span>
<i class="no-select material-symbols-outlined tooltip-info-icon" @mouseenter="showTooltip($event, error)"
@mouseleave="hideTooltip">
help <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</i>
<HelpTooltipIcon :text="error" />
</div>
<input autofocus class="input" type="text" autocapitalize="off" v-model="username"
:placeholder="$t('general.username')" />
@@ -200,6 +197,8 @@ import { authApi } from "@/api";
import { initAuth } from "@/utils/auth";
import { removeLeadingSlash } from "@/utils/url";
import { globalVars } from "@/utils/constants";
import { defaultDarkMode, syncDocumentTheme } from "@/utils/theme";
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
import Tooltip from "@/components/Tooltip.vue";
import LoadingSpinner from "@/components/LoadingSpinner.vue";
@@ -207,6 +206,7 @@ export default {
name: "login",
components: {
Prompts,
HelpTooltipIcon,
Tooltip,
LoadingSpinner,
},
@@ -220,7 +220,7 @@ export default {
name: () => globalVars.name || "FileBrowser Quantum",
loginIconUrl: () => globalVars.loginIcon,
isDarkMode() {
return globalVars.darkMode;
return defaultDarkMode();
},
loginName() {
return this.name;
@@ -237,6 +237,7 @@ export default {
inProgress: false,
}),
mounted() {
syncDocumentTheme(this.isDarkMode);
let redirect = state.route.query.redirect;
if (redirect) {
redirect = removeLeadingSlash(redirect);
@@ -297,16 +298,6 @@ export default {
el.style.opacity = '0';
setTimeout(done, 300);
},
showTooltip(event, text) {
mutations.showTooltip({
content: text,
x: event.clientX,
y: event.clientY,
});
},
hideTooltip() {
mutations.hideTooltip();
},
toggleMode() {
this.createMode = !this.createMode;
},
-4
View File
@@ -174,10 +174,6 @@ export default {
opacity: 1;
}
.settings-items {
margin-bottom: 0.5em;
}
.settings-items > .item {
padding: 1em;
border-radius: 1em;
+11 -5
View File
@@ -2,6 +2,7 @@
<header v-if="!isOnlyOffice" :class="['flexbar', { 'dark-mode-header': isDarkMode }]">
<action
v-if="!disableNavButtons"
class="nav-toggle-button"
icon="close_back"
:label="$t('general.close')"
:disabled="isDisabledMultiAction"
@@ -332,18 +333,23 @@ header button:hover {
-webkit-box-shadow: unset !important;
}
header {
background-color: rgb(37 49 55 / 5%) !important;
background-color: color-mix(in srgb, var(--alt-background) 15%, transparent);
}
/* Header with backdrop-filter support */
@supports (backdrop-filter: none) {
header {
backdrop-filter: blur(16px) invert(0.1);
}
.dark-mode-header {
background-color: rgb(37 49 55 / 33%) !important;
}
}
:deep(.action.nav-toggle-button .line),
:deep(.action.menu-button),
:deep(.action.overflow-menu-button) {
stroke: #546e7a;
color: #546e7a;
}
.search-bar-container {
display: flex;
align-items: center;
@@ -407,4 +413,4 @@ header {
.dark-mode-header .search-bar-container:hover {
background-color: rgba(255, 255, 255, 0.15);
}
</style>
</style>
+1
View File
@@ -347,6 +347,7 @@ export default {
}
.card-content.full :deep(.settings-group) {
margin-bottom: 0.75rem;
margin-top: 0.5em;
}
.loading-hint {
opacity: 0.7;
+5
View File
@@ -241,3 +241,8 @@ export default {
flex-shrink: 0;
}
</style>
<style scoped>
.card-content.full :deep(.settings-table-wrapper) {
margin-top: 0.75rem;
}
</style>
+9 -22
View File
@@ -7,10 +7,7 @@
<div class="settings-number-input item">
<div class="no-padding">
<label for="maxConcurrentUpload">{{ $t("fileLoading.maxConcurrentUpload") }}</label>
<i class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('fileLoading.maxConcurrentUploadHelp'))" @mouseleave="hideTooltip">
help <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</i>
<HelpTooltipIcon :text="$t('fileLoading.maxConcurrentUploadHelp')" />
</div>
<div>
<input v-model.number="localuser.fileLoading.maxConcurrentUpload" type="range" min="1" max="10"
@@ -21,10 +18,7 @@
<div class="settings-number-input item">
<div class="no-padding">
<label for="uploadChunkSizeMb">{{ $t("fileLoading.uploadChunkSizeMb") }}</label>
<i class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('fileLoading.uploadChunkSizeMbHelp'))" @mouseleave="hideTooltip">
help <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</i>
<HelpTooltipIcon :text="$t('fileLoading.uploadChunkSizeMbHelp')" />
</div>
<div class="no-padding">
<input class="sizeInput input" v-model.number="localuser.fileLoading.uploadChunkSizeMb" type="number" min="0"
@@ -34,10 +28,7 @@
<div class="settings-number-input item">
<div class="no-padding">
<label for="downloadChunkSizeMb">{{ $t("fileLoading.downloadChunkSizeMb") }}</label>
<i class="no-select material-symbols-outlined tooltip-info-icon"
@mouseenter="showTooltip($event, $t('fileLoading.downloadChunkSizeMbHelp'))" @mouseleave="hideTooltip">
help <!-- eslint-disable-line @intlify/vue-i18n/no-raw-text -->
</i>
<HelpTooltipIcon :text="$t('fileLoading.downloadChunkSizeMbHelp')" />
</div>
<div class="no-padding">
<input class="sizeInput input" v-model.number="localuser.fileLoading.downloadChunkSizeMb" type="number" min="0"
@@ -62,12 +53,14 @@
<script>
import { notify } from "@/notify";
import HelpTooltipIcon from "@/components/HelpTooltipIcon.vue";
import { state, mutations } from "@/store";
import ToggleSwitch from "@/components/settings/ToggleSwitch.vue";
export default {
name: "fileLoading",
components: {
HelpTooltipIcon,
ToggleSwitch,
},
@@ -94,16 +87,6 @@ export default {
}
},
methods: {
showTooltip(event, text) {
mutations.showTooltip({
content: text,
x: event.clientX,
y: event.clientY,
});
},
hideTooltip() {
mutations.hideTooltip();
},
async updateSettings(event) {
if (event !== undefined) {
event.preventDefault();
@@ -147,4 +130,8 @@ export default {
.item {
padding: 1em;
}
.card-actions {
margin-top: 1em;
}
</style>
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff. Show more