mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-05 01:00:11 -05:00
Compare commits
7
Commits
main
...
v2.0.5-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
462f19af2e | ||
|
|
45b0ae5955 | ||
|
|
3e17afa337 | ||
|
|
23113bf740 | ||
|
|
0a330e2c38 | ||
|
|
bf18a264be | ||
|
|
81a8848165 |
No files matched your search
@@ -5,7 +5,7 @@
|
||||
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "20"
|
||||
"version": "22"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
22
|
||||
Generated
+367
-499
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
Binary file not shown.
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -89,6 +89,10 @@ input.sizeInput:disabled {
|
||||
color: var(--textPrimary);
|
||||
}
|
||||
|
||||
.sizeInputWrapper:focus-within {
|
||||
border-color: var(--primaryColor);
|
||||
}
|
||||
|
||||
.sizeInputWrapper .sizeInput {
|
||||
text-align: center;
|
||||
border: none;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -717,7 +717,11 @@
|
||||
"enforcedByAdmin": "لقد منع المسؤول المستخدمين من تغيير هذه الخاصية",
|
||||
"singleClick": "انقر مرة واحدة لفتح الملفات",
|
||||
"stickySidebar": "الشريط الجانبي الثابت",
|
||||
"darkMode": "الوضع المظلم"
|
||||
"darkMode": "الوضع المظلم",
|
||||
"defaultViewMode": "وضع العرض الافتراضي",
|
||||
"defaultViewModeDescription": "وضع العرض الافتراضي في المتصفحات أو الأجهزة الجديدة. لا يلغي تفضيلات العرض التي تم حفظها مسبقًا محليًّا.",
|
||||
"defaultGallerySize": "الحجم الافتراضي للصورة المصغرة",
|
||||
"defaultGallerySizeDescription": "مقياس الصور المصغرة الأولي للمعرض أو القائمة (1–9) في المتصفحات أو الأجهزة الجديدة. لا يلغي الأحجام المحفوظة مسبقًا محليًّا."
|
||||
},
|
||||
"colors": {
|
||||
"red": "أحمر",
|
||||
|
||||
@@ -555,7 +555,11 @@
|
||||
"enforcedByAdmin": "Вашият администратор е ограничил възможността на потребителите да променят това свойство",
|
||||
"singleClick": "Отваряне на файлове с едно кликване",
|
||||
"stickySidebar": "Залепена странична лента",
|
||||
"darkMode": "Тъмен режим"
|
||||
"darkMode": "Тъмен режим",
|
||||
"defaultViewMode": "Режим на изглед по подразбиране",
|
||||
"defaultViewModeDescription": "Режим на първоначално показване в нови браузъри или устройства. Не отменя вече запазените локално настройки за показване.",
|
||||
"defaultGallerySize": "Стандартен размер на миниатюрите",
|
||||
"defaultGallerySizeDescription": "Начален мащаб на миниатюрите в галерията или списъка (1–9) при нови браузъри или устройства. Не отменя размерите, които вече са запазени локално."
|
||||
},
|
||||
"settings": {
|
||||
"systemAdmin": "Система и администратор",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -725,7 +725,11 @@
|
||||
"enforcedByAdmin": "Ο διαχειριστής σας έχει απαγορεύσει στους χρήστες να τροποποιούν αυτήν την ιδιότητα",
|
||||
"singleClick": "Ανοίξτε τα αρχεία με ένα μόνο κλικ",
|
||||
"stickySidebar": "Πλευρική στήλη που παραμένει σταθερή",
|
||||
"darkMode": "Σκοτεινή λειτουργία"
|
||||
"darkMode": "Σκοτεινή λειτουργία",
|
||||
"defaultViewMode": "Προεπιλεγμένη λειτουργία προβολής",
|
||||
"defaultViewModeDescription": "Λειτουργία αρχικής προβολής σε νέους περιηγητές ή συσκευές. Δεν αντικαθιστά τις προτιμήσεις προβολής που έχουν ήδη αποθηκευτεί τοπικά.",
|
||||
"defaultGallerySize": "Προεπιλεγμένο μέγεθος μικρογραφίας",
|
||||
"defaultGallerySizeDescription": "Αρχική κλίμακα μικρογραφιών της γκαλερί ή της λίστας (1–9) σε νέους περιηγητές ή συσκευές. Δεν αντικαθιστά τα μεγέθη που έχουν ήδη αποθηκευτεί τοπικά."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Απέτυχε η αρχικοποίηση του συντάκτη. Παρακαλούμε επαναφορτώστε.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -725,7 +725,11 @@
|
||||
"enforcedByAdmin": "המנהל שלך מנע ממשתמשים לשנות מאפיין זה",
|
||||
"singleClick": "לחיצה אחת לפתיחת קבצים",
|
||||
"stickySidebar": "סרגל צדדי קבוע",
|
||||
"darkMode": "מצב כהה"
|
||||
"darkMode": "מצב כהה",
|
||||
"defaultViewMode": "מצב תצוגה ברירת מחדל",
|
||||
"defaultViewModeDescription": "מצב תצוגה ראשוני בדפדפנים או במכשירים חדשים. אינו מבטל את הגדרות התצוגה שכבר נשמרו באופן מקומי.",
|
||||
"defaultGallerySize": "גודל תמונה ממוזערת ברירת מחדל",
|
||||
"defaultGallerySizeDescription": "גודל התמונות הממוזערות הראשוני בגלריה או ברשימה (1–9) בדפדפנים או במכשירים חדשים. הגדרה זו אינה מבטלת את הגדלים שכבר נשמרו באופן מקומי."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "לא ניתן לאתחל את העורך. אנא טען מחדש.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -725,7 +725,11 @@
|
||||
"enforcedByAdmin": "管理者が、ユーザーによるこのプロパティの変更を禁止しています",
|
||||
"singleClick": "ワンクリックでファイルを開く",
|
||||
"stickySidebar": "固定サイドバー",
|
||||
"darkMode": "ダークモード"
|
||||
"darkMode": "ダークモード",
|
||||
"defaultViewMode": "デフォルトの表示モード",
|
||||
"defaultViewModeDescription": "新しいブラウザやデバイスでの初期表示モードです。ローカルにすでに保存されている表示設定を上書きすることはありません。",
|
||||
"defaultGallerySize": "デフォルトのサムネイルサイズ",
|
||||
"defaultGallerySizeDescription": "新しいブラウザやデバイスでのギャラリーまたはリストのサムネイルの初期スケール(1~9)。ローカルにすでに保存されているサイズを上書きすることはありません。"
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "エディタの初期化に失敗しました。再読み込みしてください。",
|
||||
|
||||
@@ -725,7 +725,11 @@
|
||||
"enforcedByAdmin": "관리자가 사용자가 이 속성을 변경하지 못하도록 제한했습니다.",
|
||||
"singleClick": "한 번의 클릭으로 파일 열기",
|
||||
"stickySidebar": "고정 사이드바",
|
||||
"darkMode": "다크 모드"
|
||||
"darkMode": "다크 모드",
|
||||
"defaultViewMode": "기본 보기 모드",
|
||||
"defaultViewModeDescription": "새로운 브라우저나 기기에서 초기 보기 모드로 설정됩니다. 이미 로컬에 저장된 보기 환경설정은 무시되지 않습니다.",
|
||||
"defaultGallerySize": "기본 썸네일 크기",
|
||||
"defaultGallerySizeDescription": "새로운 브라우저나 기기에서 갤러리 또는 목록의 썸네일 초기 크기(1–9). 이미 로컬에 저장된 크기는 덮어쓰지 않습니다."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "편집기를 초기화하지 못했습니다. 다시 로드하세요.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -725,7 +725,11 @@
|
||||
"enforcedByAdmin": "Ваш администратор запретил пользователям изменять это свойство",
|
||||
"singleClick": "Открытие файлов одним щелчком мыши",
|
||||
"stickySidebar": "Фиксированная боковая панель",
|
||||
"darkMode": "Темный режим"
|
||||
"darkMode": "Темный режим",
|
||||
"defaultViewMode": "Режим просмотра по умолчанию",
|
||||
"defaultViewModeDescription": "Режим начального отображения в новых браузерах или на новых устройствах. Не переопределяет настройки просмотра, уже сохраненные локально.",
|
||||
"defaultGallerySize": "Размер миниатюры по умолчанию",
|
||||
"defaultGallerySizeDescription": "Начальный масштаб миниатюр в галерее или списке (1–9) в новых браузерах или на новых устройствах. Не переопределяет размеры, уже сохраненные локально."
|
||||
},
|
||||
"editor": {
|
||||
"uninitialized": "Не удалось инициализировать редактор. Пожалуйста, перезагрузитесь.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -551,7 +551,11 @@
|
||||
"enforcedByAdmin": "Ваш адміністратор заборонив користувачам змінювати це властивість",
|
||||
"singleClick": "Відкриття файлів одним клацанням миші",
|
||||
"stickySidebar": "Фіксована бічна панель",
|
||||
"darkMode": "Темний режим"
|
||||
"darkMode": "Темний режим",
|
||||
"defaultViewMode": "Режим перегляду за замовчуванням",
|
||||
"defaultViewModeDescription": "Режим початкового відображення в нових браузерах або на нових пристроях. Не замінює налаштування відображення, які вже збережено локально.",
|
||||
"defaultGallerySize": "Розмір мініатюри за замовчуванням",
|
||||
"defaultGallerySizeDescription": "Початковий масштаб мініатюр у галереї або списку (1–9) у нових браузерах або на нових пристроях. Не замінює розміри, які вже збережено локально."
|
||||
},
|
||||
"general": {
|
||||
"name": "Ім'я",
|
||||
|
||||
@@ -551,7 +551,11 @@
|
||||
"enforcedByAdmin": "系统管理员已禁止用户更改此属性",
|
||||
"singleClick": "单击即可打开文件",
|
||||
"stickySidebar": "固定侧边栏",
|
||||
"darkMode": "深色模式"
|
||||
"darkMode": "深色模式",
|
||||
"defaultViewMode": "默认视图模式",
|
||||
"defaultViewModeDescription": "在新浏览器或设备上的初始视图模式。不会覆盖已保存在本地的视图偏好设置。",
|
||||
"defaultGallerySize": "默认缩略图大小",
|
||||
"defaultGallerySizeDescription": "在新浏览器或设备上显示的图库或列表缩略图初始缩放比例(1–9)。不会覆盖已保存在本地中的尺寸设置。"
|
||||
},
|
||||
"general": {
|
||||
"name": "名称",
|
||||
|
||||
@@ -520,7 +520,11 @@
|
||||
"enforcedByAdmin": "您的系統管理員已禁止使用者變更此屬性",
|
||||
"singleClick": "單擊即可開啟檔案",
|
||||
"stickySidebar": "固定側邊欄",
|
||||
"darkMode": "深色模式"
|
||||
"darkMode": "深色模式",
|
||||
"defaultViewMode": "預設檢視模式",
|
||||
"defaultViewModeDescription": "在新瀏覽器或裝置上的初始檢視模式。不會覆寫已儲存於本機的檢視偏好設定。",
|
||||
"defaultGallerySize": "預設縮圖大小",
|
||||
"defaultGallerySizeDescription": "在新瀏覽器或裝置上顯示的初始圖庫或清單縮圖比例(1–9)。此設定不會覆寫已儲存於本地的尺寸。"
|
||||
},
|
||||
"otp": {
|
||||
"name": "雙因素驗證",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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: "",
|
||||
|
||||
@@ -658,6 +658,8 @@ export const mutations = {
|
||||
"showSelectMultiple",
|
||||
"debugOffice",
|
||||
"disableUpdateNotifications",
|
||||
"viewMode",
|
||||
"gallerySize",
|
||||
];
|
||||
const updatedProperties = Object.keys(value).filter(
|
||||
(key) =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -174,10 +174,6 @@ export default {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.settings-items {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.settings-items > .item {
|
||||
padding: 1em;
|
||||
border-radius: 1em;
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -241,3 +241,8 @@ export default {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
<style scoped>
|
||||
.card-content.full :deep(.settings-table-wrapper) {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -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
Reference in new issue
Block a user