Beta/v0.8.11 (#1453)

Co-authored-by: Kevin B. <kurami32_kbm@proton.me>
Co-authored-by: Srinivas A <56465971+srini-abhiram@users.noreply.github.com>
Co-authored-by: Chanwoong Kim <me@chanwoong.kim>
Co-authored-by: fox34 <github@noecho.de>
Co-authored-by: Pyro <pyrodd@gmail.com>
Co-authored-by: Noé Larrieu-Lacoste <noelarrieulacoste@yahoo.fr>
This commit is contained in:
authored and GitHub committed 2025-10-28 12:29:55 -05:00
1 parent 6d11e01054
commit 8b1bf3cebf
138 files changed
+4010 -592

No files matched your search

+3 -2
View File
@@ -3,7 +3,8 @@ backend/tmp
backend/dist
backend/backend*
backend/http/dist/**
backend/http/embed/*()
backend/vendor/**
frontend/dist/**
frontend/node_modules/**
frontend/node_modules/**
frontend/pkg/**
+4 -1
View File
@@ -10,11 +10,14 @@ rice-box.go
/backend/filebrowser.exe
frontend/tsconfig.tsbuildinfo
/backend/backend.exe
/backend/playwright-files.db
/backend/tmp
/backend/dist
/frontend/dist
/frontend/pkg
/frontend/test-results
/frontend/playwright-files
/frontend/screenshots
/frontend/tests/playwright-files/demo*
/frontend/loginAuth.json
/frontend/package-lock.json
@@ -24,7 +27,7 @@ frontend/tsconfig.tsbuildinfo
/backend/generated.yaml
/backend/srv
/backend/http/dist
/backend/http/embed/*
/backend/http/embed
.DS_Store
node_modules
+27
View File
@@ -2,6 +2,33 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## v0.8.11-beta
**New Features**:
- OnlyOffice debugger now shows backend logs as well for admin users.
- If proxy auth based `username` equals `auth.adminUsername`, the user will be promoted to admin.
- Guard against accidently cancelling uploads #1419
- Added a quirky halloween theme that automatically applies on october 31st -- you can disable this from happening by setting `frontend.styling.disableEventThemes`
**Notes**:
- Removed share setting `enableOnlyOfficeEditing` -- uses value from `allowEditing`
- Anonymous users can now edit if a share has edit permissions enabled.
- No "Incompatible user settings detected" message if its first setup
- cookie handling revamped
- fully backend managed
- `auth` cookie name changed to `filebrowser_quantum_jwt`
- auth cookie for password users no longer session based #1439
- removed state jwt state variable -- 100% cookie based.
**BugFixes**:
- Fixed issue editing onlyoffice on shares #1397
- Added proper and robust logging / error handling for the onlyoffice callback. #1422 #1068
- If a file was moved/renamed/deleted onlyoffice would re-save the original file without error, now it gives error the file doesn't exist anymore.
- OnlyOffice integration does not work behind proxy authentication #1422
- proxy-based authentication with docker does not work unless set up with password-based login first #1226
- Select all bug on safari mobile #1421
- Dragging a item into itself #1446
## v0.8.10-beta
**New Features**:
+3 -4
View File
@@ -8,7 +8,7 @@
[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://github.com/gtsteffaniak/filebrowser/wiki/Q&A#is-there-a-way-to-donate-or-support-this-project)
<img width="150" src="https://github.com/user-attachments/assets/59986a2a-f960-4536-aa35-4a9a7c98ad48" title="Logo">
<img width="150" src="https://github.com/user-attachments/assets/c40b22c9-33da-47b7-bc4c-ce69bb5cc174" title="Logo">
<h3>FileBrowser Quantum</h3>
The best free self-hosted web-based file manager.
<br/><br/>
@@ -20,7 +20,7 @@
## Pinned
:loudspeaker: [Stable Release & 0.9.0 Update](https://github.com/gtsteffaniak/filebrowser/discussions/1293)
:loudspeaker: [Stable Release & v1.0.0 Update](https://github.com/gtsteffaniak/filebrowser/discussions/1293)
:pushpin: [Read The Official Docs](https://filebrowserquantum.com/) (currently english-only)
@@ -98,8 +98,7 @@ a pop-up menu.
See the [Official Docs](https://filebrowserquantum.com/). Contributions are welcome and encouraged! See [FilebrowserDocs Github](https://github.com/quantumx-apps/filebrowserDocs).
## Comparison Chart
Application Name | <img width="48" src="https://github.com/user-attachments/assets/59986a2a-f960-4536-aa35-4a9a7c98ad48" > Quantum | <img width="48" src="https://github.com/filebrowser/filebrowser/blob/master/frontend/public/img/logo.svg" > Filebrowser | <img width="48" src="https://github.com/mickael-kerjean/filestash/blob/master/public/assets/logo/app_icon.png?raw=true" > Filestash | <img width="48" src="https://avatars.githubusercontent.com/u/19211038?s=200&v=4" > Nextcloud | <img width="48" src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Google_Drive_logo.png/480px-Google_Drive_logo.png" > Google_Drive | <img width="48" src="https://avatars.githubusercontent.com/u/6422152?v=4" > FileRun
Application Name | <img width="48" src="https://github.com/user-attachments/assets/c40b22c9-33da-47b7-bc4c-ce69bb5cc174" > Quantum | <img width="48" src="https://github.com/filebrowser/filebrowser/blob/master/frontend/public/img/logo.svg" > Filebrowser | <img width="48" src="https://github.com/mickael-kerjean/filestash/blob/master/public/assets/logo/app_icon.png?raw=true" > Filestash | <img width="48" src="https://avatars.githubusercontent.com/u/19211038?s=200&v=4" > Nextcloud | <img width="48" src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/da/Google_Drive_logo.png/480px-Google_Drive_logo.png" > Google_Drive | <img width="48" src="https://avatars.githubusercontent.com/u/6422152?v=4" > FileRun
--- | --- | --- | --- | --- | --- | --- |
Filesystem support | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
Linux | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
+6
View File
@@ -4,6 +4,12 @@ frontend:
<<: *frontend-main
userDefaults:
preview:
image: true
popup: true
video: true
office: true
highQuality: true
permissions:
download: true
create: true
@@ -10,7 +10,7 @@ import { defineConfig, devices } from "@playwright/test";
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
globalSetup: "./tests/playwright/local-setup",
globalSetup: "./tests/playwright/no-config-setup",
timeout: 5000,
testDir: "./tests/playwright/no-config",
/* Run tests in files in parallel */
+6
View File
@@ -42,6 +42,12 @@ userDefaults:
realtime: true
userDefaults:
preview:
image: true
popup: true
video: true
office: true
highQuality: true
permissions:
download: true
create: true
+6
View File
@@ -28,6 +28,12 @@ auth:
adminGroup: "Admins"
userDefaults:
preview:
image: true
popup: true
video: true
office: true
highQuality: true
darkMode: true
disableSettings: false
singleClick: false
+6
View File
@@ -28,6 +28,12 @@ auth:
logoutRedirectUrl: "https://localhost/api/auth/logout2"
userDefaults:
preview:
image: true
popup: true
video: true
office: true
highQuality: true
darkMode: true
disableSettings: false
singleClick: false
+1
View File
@@ -13,6 +13,7 @@ server:
config:
private: true
defaultEnabled: true
frontend:
name: "Graham's Filebrowser"
disableDefaultLinks: true
+49
View File
@@ -0,0 +1,49 @@
FROM gtstef/ffmpeg:8.0-decode AS ffmpeg
FROM golang:alpine AS base
ARG VERSION
ARG REVISION
WORKDIR /app
COPY ./backend ./
RUN apk update && apk add --no-cache gcc musl-dev upx
ENV CGO_ENABLED=1
ENV GOEXPERIMENT=greenteagc
RUN go build -tags mupdf,musl -ldflags="-w -s \
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.Version=${VERSION}' \
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.CommitSHA=${REVISION}'" \
-o filebrowser .
RUN upx filebrowser
FROM node:lts-slim AS nbuild
WORKDIR /app
COPY ./frontend/package.json ./
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 npm i --maxsockets 1
COPY ./frontend/ ./
RUN npm run build:docker
FROM alpine:latest
COPY --from=ffmpeg [ "/ffmpeg", "/ffprobe", "/usr/local/bin/" ]
ENV FILEBROWSER_FFMPEG_PATH="/usr/local/bin/"
ENV FILEBROWSER_DATABASE="/home/filebrowser/data/database.db"
ENV PATH="$PATH:/home/filebrowser"
RUN apk --no-cache add ca-certificates mailcap tzdata curl
RUN adduser -D -s /bin/true -u 1000 filebrowser
USER filebrowser
WORKDIR /home/filebrowser
# copy config to two locations for compatibility
COPY --from=base --chown=filebrowser:1000 [ "/app/filebrowser", "/app/config.yaml", "./" ]
COPY --from=base --chown=filebrowser:1000 [ "/app/config.yaml", "./data/config.yaml" ]
COPY --from=base --chown=filebrowser:1000 [ "/app/reduce-rounded-corners.css", "./" ]
COPY --from=nbuild --chown=filebrowser:1000 [ "/app/dist/", "./http/dist/" ]
## sanity checks
RUN [ "filebrowser", "version" ]
RUN [ "ffmpeg", "-version" ]
RUN [ "ffprobe", "-version" ]
USER root
# exposing default port for auto discovery.
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:80/health || exit 1
ENTRYPOINT [ "./filebrowser" ]
@@ -0,0 +1,4 @@
FROM node:22-slim
WORKDIR /app/frontend
RUN npm i @playwright/test oauth2-mock-server @types/node
RUN npx playwright install --with-deps firefox
@@ -0,0 +1,19 @@
FROM ghcr.io/gtsteffaniak/playwright-base:latest
WORKDIR /app
# Copy setup scripts first to leverage caching
COPY [ "./_docker/src/general/", "./" ]
WORKDIR /app/frontend
COPY [ "./_docker/src/general/frontend/", "./" ]
COPY [ "./frontend/tests/", "./tests" ]
WORKDIR /app/backend/
COPY [ "./backend/filebrowser*", "./"]
COPY [ "./backend/reduce-rounded-corners.css", "./no-rounded.css" ]
RUN mkdir -p ./http/dist
COPY [ "./backend/http/embed/", "./http/dist/"]
# Run tests
RUN ./filebrowser set -u testuser1,testuser1
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
@@ -0,0 +1,17 @@
FROM ghcr.io/gtsteffaniak/playwright-base:latest
WORKDIR /app
# Copy setup scripts first to leverage caching
COPY [ "./_docker/src/no-config/", "./" ]
WORKDIR /app/frontend
COPY [ "./_docker/src/no-config/frontend/", "./" ]
COPY [ "./frontend/tests/", "./tests" ]
WORKDIR /app/backend/
COPY [ "./backend/filebrowser*", "./"]
RUN mkdir -p ./http/dist
COPY [ "./backend/http/embed/", "./http/dist/"]
# Run tests
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
@@ -0,0 +1,17 @@
FROM ghcr.io/gtsteffaniak/playwright-base:latest
WORKDIR /app
# Copy setup scripts first to leverage caching
COPY [ "./_docker/src/noauth/", "./" ]
WORKDIR /app/frontend
COPY [ "./frontend/tests", "./tests" ]
COPY [ "./frontend/tests/playwright-files", "./tests/playwright-files2" ]
WORKDIR /app/backend/
COPY [ "./backend/filebrowser", "./"]
RUN mkdir -p ./http/dist
COPY [ "./backend/http/embed/", "./http/dist/"]
# Run tests
#RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
@@ -0,0 +1,13 @@
FROM ghcr.io/gtsteffaniak/playwright-base:latest
COPY [ "./_docker/src/oidc/frontend/", "./" ]
COPY [ "./frontend/tests/", "./tests" ]
WORKDIR /app/backend/
RUN mkdir -p ./http/dist
COPY [ "./_docker/src/oidc/backend/", "./"]
COPY ["./backend/filebrowser", "./"]
COPY [ "./backend/http/embed/", "./http/dist/"]
# Run tests
WORKDIR /app/frontend
RUN node mock-oidc-server.js & sleep 2 && cd ../backend/ && ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
@@ -0,0 +1,14 @@
FROM ghcr.io/gtsteffaniak/playwright-base:latest
WORKDIR /app
COPY [ "./_docker/src/proxy/", "./" ]
WORKDIR /app/frontend
COPY [ "./frontend/tests", "./tests" ]
WORKDIR /app/backend/
COPY [ "./backend/filebrowser", "./"]
RUN mkdir -p ./http/dist
COPY [ "./backend/http/embed/", "./http/dist/"]
RUN apt update && apt install nginx -y
RUN mv default.conf /etc/nginx/conf.d/default.conf
RUN mv .htpasswd /etc/nginx/conf.d/.htpasswd
RUN sed -i 's/filebrowser/localhost/g' /etc/nginx/conf.d/default.conf
RUN nginx & ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
@@ -0,0 +1,17 @@
FROM ghcr.io/gtsteffaniak/playwright-base:latest
WORKDIR /app
# Copy setup scripts first to leverage caching
COPY [ "./_docker/src/no-config/", "./" ]
WORKDIR /app/frontend
COPY [ "./_docker/src/no-config/frontend/", "./" ]
COPY [ "./frontend/tests/", "./tests" ]
WORKDIR /app/backend/
COPY [ "./backend/filebrowser*", "./"]
RUN mkdir -p ./http/dist
COPY [ "./backend/http/embed/", "./http/dist/"]
# Run tests
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
@@ -0,0 +1,17 @@
FROM ghcr.io/gtsteffaniak/playwright-base:latest
WORKDIR /app
# Copy setup scripts first to leverage caching
COPY [ "./_docker/src/sharing/", "./" ]
WORKDIR /app/frontend
COPY [ "./_docker/src/sharing/frontend/", "./" ]
COPY [ "./frontend/tests/", "./tests" ]
WORKDIR /app/backend/
COPY [ "./backend/filebrowser*", "./"]
RUN mkdir -p ./http/dist
COPY [ "./backend/http/embed/", "./http/dist/"]
# Run tests
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
+1
View File
@@ -0,0 +1 @@
FROM ghcr.io/gtsteffaniak/playwright-base:latest
+42
View File
@@ -0,0 +1,42 @@
FROM golang:alpine AS base
ARG VERSION
ARG REVISION
WORKDIR /app
COPY ./backend ./
RUN apk update && apk add upx
ENV GOEXPERIMENT=greenteagc
RUN go build -ldflags="-w -s \
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.Version=${VERSION}' \
-X 'github.com/gtsteffaniak/filebrowser/backend/common/version.CommitSHA=${REVISION}'" \
-o filebrowser .
RUN upx filebrowser
FROM node:lts-slim AS nbuild
WORKDIR /app
COPY ./frontend/package.json ./
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 npm i --maxsockets 1
COPY ./frontend/ ./
RUN npm run build:docker
FROM alpine:latest
ENV FILEBROWSER_DATABASE="/home/filebrowser/data/database.db"
ENV PATH="$PATH:/home/filebrowser"
RUN apk --no-cache add ca-certificates mailcap tzdata curl
RUN adduser -D -s /bin/true -u 1000 filebrowser
USER filebrowser
WORKDIR /home/filebrowser
COPY --from=base --chown=filebrowser:1000 [ "/app/filebrowser", "./" ]
COPY --from=base --chown=filebrowser:1000 [ "/app/config.yaml", "./data/config.yaml" ]
COPY --from=base --chown=filebrowser:1000 [ "/app/reduce-rounded-corners.css", "./" ]
COPY --from=nbuild --chown=filebrowser:1000 [ "/app/dist/", "./http/dist/" ]
## sanity checks
RUN [ "filebrowser", "version" ]
USER root
# exposing default port for auto discovery.
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD curl -f http://localhost:80/health || exit 1
ENTRYPOINT [ "./filebrowser" ]
+17
View File
@@ -0,0 +1,17 @@
services:
nginx-proxy-auth:
image: nginx
container_name: nginx-proxy-auth
ports:
- "80:80"
volumes:
- ./src/proxy/backend/default.conf:/etc/nginx/conf.d/default.conf
- ./src/proxy/backend/.htpasswd:/etc/nginx/conf.d/.htpasswd
filebrowser:
hostname: filebrowser
volumes:
- '../frontend:/home/frontend'
- "./src/proxy/backend/config.yaml:/home/filebrowser/config.yaml"
build:
context: ../
dockerfile: ./_docker/Dockerfile.slim
@@ -0,0 +1,15 @@
server:
<<: *server-main
frontend:
<<: *frontend-main
userDefaults:
preview:
image: true
popup: true
video: true
office: true
highQuality: true
permissions:
download: true
create: true
@@ -0,0 +1,12 @@
frontend-main: &frontend-main
name: "Graham's Filebrowser"
disableDefaultLinks: true
styling:
customThemes:
no-rounded:
description: "No rounded corners" # The description of the theme to display in the UI.
css: "no-rounded.css" # The css file path and filename to use for the theme.
externalLinks:
- text: "A playwright test"
url: "https://playwright.dev/"
title: "Playwright"
@@ -0,0 +1,15 @@
server-main: &server-main
port: 80
baseURL: "/"
logging:
- levels: "info|error|debug"
sources:
- path: "../frontend/tests/playwright-files"
name: "playwright + files"
config:
defaultEnabled: true
- path: "."
name: "docker"
config:
disableIndexing: true
defaultEnabled: true
@@ -0,0 +1,47 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
globalSetup: "./tests/playwright/global-setup",
timeout: 10000,
testDir: "./tests/playwright/general",
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: false,
/* Retry on CI only */
retries: 2,
/* Opt out of parallel tests on CI. */
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "line",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
actionTimeout: 5000,
storageState: "loginAuth.json",
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
],
});
@@ -0,0 +1,47 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
globalSetup: "./tests/playwright/no-config-setup",
timeout: 5000,
testDir: "./tests/playwright/no-config",
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: false,
/* Retry on CI only */
retries: 2,
/* Opt out of parallel tests on CI. */
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "line",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
actionTimeout: 5000,
storageState: "loginAuth.json",
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
],
});
@@ -0,0 +1,53 @@
server:
port: 80
baseURL: "/files/"
logging:
- levels: "info|error|debug"
sources:
- path: "../frontend/tests/playwright-files"
name: "exclude"
config:
defaultEnabled: true
conditionals:
rules:
- folderPath: "excluded"
- folderPath: "excludedButVisible"
viewable: true
- path: "../frontend/tests/playwright-files2"
name: "include"
config:
defaultEnabled: true
conditionals:
rules:
- includeRootItem: "myfolder"
- includeRootItem: "subfolderExclusions"
- filePath: "/subfolderExclusions/exclusionlist.sh"
- folderPath: "/subfolderExclusions/folderItem"
viewable: true
- fileStartsWith: "startsWith"
- folderEndsWith: "Folder"
auth:
methods:
noauth: true
frontend:
name: "Graham's Filebrowser"
disableDefaultLinks: true
externalLinks:
- text: "A playwright test"
url: "https://playwright.dev/"
title: "Playwright"
userDefaults:
permissions:
realtime: true
userDefaults:
preview:
image: true
popup: true
video: true
office: true
highQuality: true
permissions:
download: true
create: true
@@ -0,0 +1,48 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
globalSetup: "./tests/playwright/noauth-setup",
timeout: 10000,
testDir: "./tests/playwright/noauth",
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: false,
/* Retry on CI only */
retries: 2,
/* Opt out of parallel tests on CI. */
workers: 1, // required for now! todo parallel some tests
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "line",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
storageState: "noauth.json",
actionTimeout: 5000,
//storageState: "loginAuth.json",
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
],
});
@@ -0,0 +1,46 @@
server:
logging:
- levels: "info|error|warning|debug"
sources:
- path: "../frontend/tests/playwright-files"
config:
defaultEnabled: true
createUserDir: true
frontend:
name: "Graham's Filebrowser"
disableDefaultLinks: true
externalLinks:
- text: "A playwright test"
url: "https://playwright.dev/"
title: "Playwright"
auth:
methods:
password:
enabled: false
oidc:
enabled: true
createUser: true
clientId: "vyBrzOmcrU3OokiWEZoObdGBscw0eYPNJauBtOJr"
clientSecret: "gIMKbkEcckUhlGrnE2Vxi8eZhEcfxQqfwFgM0ATzk4DciHGrW2YRtQfqcpRTsmzrxEETkKvZ8yeK3sfoXnUV1IQTzqK7wxw3lEMtmeXi5clj0h7nH1rVGFq2Q3HL8zUe"
issuerUrl: "http://localhost:8080"
adminGroup: "Admins"
userDefaults:
preview:
image: true
popup: true
video: true
office: true
highQuality: true
darkMode: true
disableSettings: false
singleClick: false
permissions:
admin: false
modify: true
share: false
api: false
download: true
create: true
@@ -0,0 +1,60 @@
import { OAuth2Server } from 'oauth2-mock-server';
// This is a list of pre-defined users. You can add more users here.
// The filebrowser application will use this data when a user logs in.
const users = {
// Default user
johndoe: {
sub: 'johndoe',
preferred_username: 'johndoe',
email: 'johndoe@example.com',
groups: ['Admins', 'Users'],
},
// Another example user
testuser: {
sub: 'testuser',
preferred_username: 'testuser',
email: 'testuser@example.com',
groups: ['Users'],
},
};
const server = new OAuth2Server();
// Generate a new RSA key for the server.
await server.issuer.keys.generate('RS256');
/**
* This hook is called before the ID token is signed.
* We're using it to add claims to the ID token.
* This is the primary way the application will get user information.
*/
server.service.on('beforeIdTokenSigning', (token, req) => {
// By default, the mock server uses 'johndoe' as the subject.
const user = users[token.payload.sub] || users.johndoe;
console.log(`[ID Token] Customizing token for user: ${user.sub}`);
Object.assign(token.payload, user);
});
/**
* This hook is called before the userinfo response is sent.
* We're using it to provide user claims when the application calls the /userinfo endpoint.
*/
server.service.on('beforeUserinfo', (userInfoResponse, req) => {
const user = users.johndoe;
console.log(`[UserInfo] Customizing userinfo for user: ${user.sub}`);
userInfoResponse.body = user;
});
const port = 8080;
const host = '0.0.0.0'; // Bind to 0.0.0.0 to be accessible within Docker
await server.start(port, host);
console.log(`OAuth 2 Mock Server listening on http://${host}:${port}`);
console.log(`Issuer URL: ${server.issuer.url}`);
console.log('---');
console.log('Pre-defined users:');
console.log(JSON.stringify(users, null, 2));
console.log('---');
@@ -0,0 +1,48 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
globalSetup: "./tests/playwright/oidc-setup",
timeout: 5000,
testDir: "./tests/playwright/oidc",
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: false,
/* Retry on CI only */
retries: 2,
/* Opt out of parallel tests on CI. */
workers: 1, // required for now! todo parallel some tests
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "line",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
storageState: "loginAuth.json",
actionTimeout: 5000,
//storageState: "loginAuth.json",
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
],
});
@@ -0,0 +1 @@
demo-127.0.0.1:$apr1$j5FRA4Bc$RYdNFuM7YAAo9Qtie8w4b/
@@ -0,0 +1,46 @@
server:
port: 8080
baseURL: "/subpath"
logging:
- levels: "info|error|debug"
sources:
- path: "../frontend/tests/playwright-files"
config:
defaultEnabled: true
createUserDir: true
frontend:
name: "Graham's Filebrowser"
disableDefaultLinks: true
externalLinks:
- text: "A playwright test"
url: "https://playwright.dev/"
title: "Playwright"
auth:
methods:
password:
enabled: false
proxy:
enabled: true
header: "X-Username"
createUser: true
logoutRedirectUrl: "https://localhost/api/auth/logout2"
userDefaults:
preview:
image: true
popup: true
video: true
office: true
highQuality: true
darkMode: true
disableSettings: false
singleClick: false
permissions:
admin: false
modify: true
share: true
api: false
download: true
create: true
@@ -0,0 +1,32 @@
map $remote_addr $uuid {
default "demo-${remote_addr}";
}
server {
listen 80;
server_name localhost 127.0.0.1;
location /subpath/public/ {
proxy_pass http://filebrowser:8080/subpath/public/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Username $uuid;
}
# requires basic auth demo-127.0.0.1:SecurePass123!
location /subpath {
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/conf.d/.htpasswd;
proxy_set_header X-Username $uuid;
add_header X-Username $uuid;
proxy_pass http://filebrowser:8080/subpath;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
@@ -0,0 +1,47 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
globalSetup: "./tests/playwright/proxy-setup.ts",
timeout: 5000,
testDir: "./tests/playwright/proxy",
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: false,
/* Retry on CI only */
retries: 2,
/* Opt out of parallel tests on CI. */
workers: 1, // required for now! todo parallel some tests
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "line",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
actionTimeout: 5000,
storageState: "loginAuth.json",
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
],
});
@@ -0,0 +1,27 @@
server:
port: 80
baseURL: "/"
logging:
- levels: "info|error|debug"
sources:
- path: "../frontend/tests/playwright-files"
name: "playwright + files"
config:
defaultEnabled: true
- path: "."
name: "docker"
config:
disableIndexing: true
defaultEnabled: true
frontend:
name: "Graham's Filebrowser"
disableDefaultLinks: true
styling:
customThemes:
no-rounded:
description: "No rounded corners" # The description of the theme to display in the UI.
css: "no-rounded.css" # The css file path and filename to use for the theme.
externalLinks:
- text: "A playwright test"
url: "https://playwright.dev/"
title: "Playwright"
@@ -0,0 +1,47 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
globalSetup: "./tests/playwright/global-setup",
timeout: 10000,
testDir: "./tests/playwright/settings",
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: false,
/* Retry on CI only */
retries: 2,
/* Opt out of parallel tests on CI. */
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "line",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
actionTimeout: 5000,
storageState: "loginAuth.json",
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
],
});
@@ -0,0 +1,23 @@
server:
port: 80
baseURL: "/"
logging:
- levels: "info|error|debug"
sources:
- path: "../frontend/tests/playwright-files"
name: "playwright + files"
config:
defaultEnabled: true
- path: "."
name: "docker"
config:
private: true
defaultEnabled: true
frontend:
name: "Graham's Filebrowser"
disableDefaultLinks: true
externalLinks:
- text: "A playwright test"
url: "https://playwright.dev/"
title: "Playwright"
@@ -0,0 +1,47 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
globalSetup: "./tests/playwright/global-setup",
timeout: 10000,
testDir: "./tests/playwright/sharing",
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: false,
/* Retry on CI only */
retries: 2,
/* Opt out of parallel tests on CI. */
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "line",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
actionTimeout: 5000,
storageState: "loginAuth.json",
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://127.0.0.1/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
/* Set default locale to English (US) */
locale: "en-US",
},
/* Configure projects for major browsers */
projects: [
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
],
});
+5 -5
View File
@@ -420,12 +420,12 @@ func WriteDirectory(opts utils.FileOptions) error {
return RefreshIndex(idx.Name, opts.Path, true, true)
}
func WriteFile(opts utils.FileOptions, in io.Reader) error {
idx := indexing.GetIndex(opts.Source)
func WriteFile(source, path string, in io.Reader) error {
idx := indexing.GetIndex(source)
if idx == nil {
return fmt.Errorf("could not get index: %v ", opts.Source)
return fmt.Errorf("could not get index: %v ", source)
}
realPath, _, _ := idx.GetRealPath(opts.Path)
realPath, _, _ := idx.GetRealPath(path)
// Strip trailing slash from realPath if it's meant to be a file
realPath = strings.TrimRight(realPath, "/")
// Ensure the parent directories exist
@@ -466,7 +466,7 @@ func WriteFile(opts utils.FileOptions, in io.Reader) error {
return err
}
return RefreshIndex(opts.Source, opts.Path, false, false)
return RefreshIndex(source, path, false, false)
}
// getContent reads and returns the file content if it's considered an editable text file.
+2 -2
View File
@@ -29,14 +29,14 @@ func MoveFile(src, dst string) error {
// fallback
err = CopyFile(src, dst)
if err != nil {
logger.Errorf("CopyFile failed %v %v %v ", src, dst, err)
logger.Errorf("CopyFile failed %v", err)
return err
}
go func() {
err = os.RemoveAll(src)
if err != nil {
logger.Errorf("os.Remove failed %v %v ", src, err)
logger.Errorf("os.Remove failed %v", err)
}
}()
+1 -1
View File
@@ -108,7 +108,7 @@ func StartFilebrowser() {
for _, source := range settings.Config.Server.SourceMap {
go indexing.Initialize(source, false)
}
validateUserInfo()
validateUserInfo(!dbExists)
validateOfficeIntegration()
validateAccessRules()
// Start the rootCMD in a goroutine
+5 -5
View File
@@ -11,9 +11,9 @@ import (
"github.com/gtsteffaniak/go-logger/logger"
)
var createBackup = []bool{}
var createBackup = false
func validateUserInfo() {
func validateUserInfo(newDB bool) {
// update source info for users if names/sources/paths might have changed
usersList, err := store.Users.Gets()
if err != nil {
@@ -45,8 +45,8 @@ func validateUserInfo() {
changePass = true
}
if updateUser {
skipCreateBackup := os.Getenv("FILEBROWSER_DISABLE_AUTOMATIC_BACKUP") == "true"
if len(createBackup) == 1 && !skipCreateBackup {
skipCreateBackup := os.Getenv("FILEBROWSER_DISABLE_AUTOMATIC_BACKUP") == "true" || newDB
if createBackup && !skipCreateBackup {
logger.Warning("Incompatible user settings detected, creating backup of database before converting.")
err = fileutils.CopyFile(settings.Config.Server.Database, fmt.Sprintf("%s.bak", settings.Config.Server.Database))
if err != nil {
@@ -157,7 +157,7 @@ func updatePermissions(user *users.User) bool {
}
user.Version = 1
if updateUser {
createBackup = append(createBackup, true)
createBackup = true
}
return updateUser
}
+2 -1
View File
@@ -125,7 +125,6 @@ func setupFrontend(generate bool) {
if !ok {
addCustomTheme("default", "The default theme", "")
}
// Load custom favicon if configured
loadCustomFavicon()
}
@@ -816,6 +815,8 @@ func modifyExcludeInclude(config *Source) {
// normalize names
config.Config.Conditionals.ItemRules[i].FileNames = normalizeName(rule.FileNames)
config.Config.Conditionals.ItemRules[i].FolderNames = normalizeName(rule.FolderNames)
config.Config.Conditionals.ItemRules[i].FileName = normalizeName(rule.FileName)
config.Config.Conditionals.ItemRules[i].FolderName = normalizeName(rule.FolderName)
}
}
+12 -8
View File
@@ -189,9 +189,12 @@ type ConditionalIndexConfig struct {
FolderEndsWith string `json:"folderEndsWith"` // (global) exclude folders that end with these suffixes. Eg. ".thumbnails" or ".git"
FolderPath string `json:"folderPath"` // (global) exclude folders that match this path. Eg. "/path/to/folder" or "/path/to/folder/subfolder"
FilePath string `json:"filePath"` // (global) exclude files that match this path. Eg. "/path/to/file.txt" or "/path/to/file.txt/subfile.txt"
FileNames string `json:"fileNames"` // (global) exclude files that match these names. Eg. "file.txt" or "test.csv"
FolderNames string `json:"folderNames"` // (global) exclude folders that match these names. Eg. "folder" or "subfolder"
Viewable bool `json:"viewable"` // Enable viewing in UI but exclude from indexing
FileNames string `json:"fileNames"` // deprecated: exclude files that match these names. Eg. "file.txt" or "test.csv"
FolderNames string `json:"folderNames"` // deprecated: exclude folders that match these names. Eg. "folder" or "subfolder"
FileName string `json:"fileName"` // (global) exclude files that match these names. Eg. "file.txt" or "test.csv"
FolderName string `json:"folderName"` // (global) exclude folders that match these names. Eg. "folder" or "subfolder"
Viewable bool `json:"viewable"` // Enable viewing in UI but exclude from indexing
}
// ConditionalMaps provides O(1) lookup performance for conditional rules
@@ -228,11 +231,12 @@ type Frontend struct {
}
type StylingConfig struct {
CustomCSS string `json:"customCSS"` // if a valid path to a css file is provided, it will be applied for all users. (eg. "reduce-rounded-corners.css")
CustomCSSRaw string `json:"-"` // The css raw content to use for the custom css.
LightBackground string `json:"lightBackground"` // specify a valid CSS color property value to use as the background color in light mode
DarkBackground string `json:"darkBackground"` // Specify a valid CSS color property value to use as the background color in dark mode
CustomThemes map[string]CustomTheme `json:"customThemes"` // A list of custom css files that each user can select to override the default styling. if "default" is key name then it will be the default option.
DisableEventBasedThemes bool `json:"disableEventThemes"` // disable the event based themes,
CustomCSS string `json:"customCSS"` // if a valid path to a css file is provided, it will be applied for all users. (eg. "reduce-rounded-corners.css")
CustomCSSRaw string `json:"-"` // The css raw content to use for the custom css.
LightBackground string `json:"lightBackground"` // specify a valid CSS color property value to use as the background color in light mode
DarkBackground string `json:"darkBackground"` // Specify a valid CSS color property value to use as the background color in dark mode
CustomThemes map[string]CustomTheme `json:"customThemes"` // A list of custom css files that each user can select to override the default styling. if "default" is key name then it will be the default option.
// In-memory (not exposed to config)
CustomThemeOptions map[string]CustomTheme `json:"-"` // not exposed
}
+40 -12
View File
@@ -6,17 +6,45 @@ server:
sources:
- path: "/srv"
userDefaults:
editorQuickSave: false # show quick save button in editor
hideSidebarFileActions: false # hide the file actions in the sidebar
disableQuickToggles: false # disable the quick toggles in the sidebar
disableSearchOptions: false # disable the search options in the search bar
stickySidebar: true # keep sidebar open when navigating
darkMode: true # should dark mode be enabled
viewMode: "normal" # view mode to use: eg. normal, list, grid, or compact
singleClick: false # open directory on single click, also enables middle click to open in new tab
showHidden: false # show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
dateFormat: false # when false, the date is relative, when true, the date is an exact timestamp
gallerySize: 3 # 0-9 - the size of the gallery thumbnails
themeColor: "var(--blue)" # theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
quickDownload: false # show icon to download in one click
disablePreviewExt: "" # space separated list of file extensions to disable preview for
disableViewingExt: "" # space separated list of file extensions to disable viewing for
lockPassword: false # disable the user from changing their password
disableSettings: false # disable the user from viewing the settings page
preview:
image: true
popup: true
video: false
office: false
highQuality: false
darkMode: true
disableSettings: false
singleClick: false
disableHideSidebar: true # disable the hide sidebar preview for previews and editors
highQuality: true # generate high quality thumbnail preview images
image: true # show thumbnail preview image for image files
video: true # show thumbnail preview image for video files
motionVideoPreview: true # show multiple frames for videos in thumbnail preview when hovering
office: true # show thumbnail preview image for office files
popup: true # show larger popup preview when hovering over thumbnail
autoplayMedia: true # autoplay media files in preview
defaultMediaPlayer: true # disable html5 media player and use the default media player
folder: true # show thumbnail preview image for folder files
permissions:
admin: false
modify: false
share: false
api: false
api: false # allow api access
admin: false # allow admin access
modify: false # allow modifying files
share: false # allow sharing files
realtime: false # allow realtime updates
delete: false # allow deleting files
create: false # allow creating or uploading files
download: true # allow downloading files
disableUpdateNotifications: false # disable update notifications banner for admin users
deleteWithoutConfirming: false # delete files without confirmation
disableOnlyOfficeExt: ".md .txt .pdf" # list of file extensions to disable onlyoffice editor for
showSelectMultiple: false # show select multiple files on desktop
defaultLandingPage: "" # default landing page to use if no redirect is specified: eg. /files/mysource/mysubpath, /settings, etc.
-1
View File
@@ -25,7 +25,6 @@ type CommonShare struct {
EnforceDarkLightMode string `json:"enforceDarkLightMode,omitempty"` // "dark" or "light"
ViewMode string `json:"viewMode,omitempty"` // default view mode for anonymous users: "list", "compact", "normal", "gallery"
EnableOnlyOffice bool `json:"enableOnlyOffice,omitempty"`
EnableOnlyOfficeEditing bool `json:"enableOnlyOfficeEditing,omitempty"`
ShareType string `json:"shareType"` // type of share: normal, upload, max
PerUserDownloadLimit bool `json:"perUserDownloadLimit,omitempty"`
ExtractEmbeddedSubtitles bool `json:"extractEmbeddedSubtitles,omitempty"` // can be io intensive for large files and take 10-30 seconds.
+4 -2
View File
@@ -14,7 +14,7 @@ require (
github.com/google/go-cmp v0.7.0
github.com/gtsteffaniak/go-cache v1.0.0
github.com/gtsteffaniak/go-logger v0.2.3
github.com/kovidgoyal/imaging v1.7.0
github.com/kovidgoyal/imaging v1.7.2
github.com/pquerna/otp v1.5.0
github.com/spf13/afero v1.15.0
github.com/stretchr/testify v1.11.1
@@ -129,7 +129,7 @@ require (
github.com/godoc-lint/godoc-lint v0.10.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gohugoio/hugo v0.149.1 // indirect
github.com/golang/geo v0.0.0-20251008113719-ccf458c07da6 // indirect
github.com/golang/geo v0.0.0-20251020193347-f750d7aa221b // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/asciicheck v0.5.0 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
@@ -161,6 +161,7 @@ require (
github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect
github.com/kisielk/errcheck v1.9.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
github.com/kovidgoyal/go-parallel v1.0.1 // indirect
github.com/kulti/thelper v0.7.1 // indirect
github.com/kunwardeep/paralleltest v1.0.14 // indirect
github.com/lasiar/canonicalheader v1.1.2 // indirect
@@ -210,6 +211,7 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/ryancurrah/gomodguard v1.4.1 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
+8 -4
View File
@@ -383,8 +383,8 @@ github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20251008113719-ccf458c07da6 h1:9GWB2BLkXw73MNn6FZZIea1ZHtrdBpffeSz0JFGnj8I=
github.com/golang/geo v0.0.0-20251008113719-ccf458c07da6/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
github.com/golang/geo v0.0.0-20251020193347-f750d7aa221b h1:cwGXba7hinTTzhlNQW2qbBqSktSP+KSvrKPbn3d7S0g=
github.com/golang/geo v0.0.0-20251020193347-f750d7aa221b/go.mod h1:AN0OjM34c3PbjAsX+QNma1nYtJtRxl+s9MZNV7S+efw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -542,8 +542,10 @@ github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/tt
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kovidgoyal/imaging v1.7.0 h1:SFWQPJdEuaRY7WhL+MJyoN5IvslgqU+z0iPaoGl3yuM=
github.com/kovidgoyal/imaging v1.7.0/go.mod h1:mBprO214rATK/6OaPAUXmHbSMelPSFEmoBAt/IJdmno=
github.com/kovidgoyal/go-parallel v1.0.1 h1:nYUjN+EdpbmQjTg3N5eTUInuXTB3/1oD2vHdaMfuHoI=
github.com/kovidgoyal/go-parallel v1.0.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw=
github.com/kovidgoyal/imaging v1.7.2 h1:mmT6k6Az3mC6dbqdZ6Q9KQCdZFWTAQ+q97NyGZgJ/2c=
github.com/kovidgoyal/imaging v1.7.2/go.mod h1:GdkCORjfZMMGFY0Pb7TDmRhj7PDhxF/QShKukSCj0VU=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -723,6 +725,8 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g=
github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I=
github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU=
+49 -3
View File
@@ -28,7 +28,7 @@ import (
// then checks for query parameter
func extractToken(r *http.Request) (string, error) {
hasToken := false
tokenObj, err := r.Cookie("auth")
tokenObj, err := r.Cookie("filebrowser_quantum_jwt")
if err == nil {
hasToken = true
token := tokenObj.Value
@@ -81,6 +81,9 @@ func setupProxyUser(r *http.Request, data *requestContext, proxyUser string) (*u
Username: proxyUser,
}
settings.ApplyUserDefaults(&user)
if user.Username == config.Auth.AdminUsername {
user.Permissions.Admin = true
}
err = storage.CreateUser(user)
if err != nil {
return nil, err
@@ -96,6 +99,13 @@ func setupProxyUser(r *http.Request, data *requestContext, proxyUser string) (*u
if data.user.LoginMethod != users.LoginMethodProxy {
return nil, errors.ErrWrongLoginMethod
}
if data.user.Username == config.Auth.AdminUsername && !data.user.Permissions.Admin {
data.user.Permissions.Admin = true
err = store.Users.Update(data.user, true, "Permissions")
if err != nil {
return nil, err
}
}
return data.user, nil
}
@@ -110,6 +120,9 @@ func setupProxyUser(r *http.Request, data *requestContext, proxyUser string) (*u
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/auth/login [post]
func loginHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
if d.user.LoginMethod == users.LoginMethodProxy {
return printToken(w, r, d.user)
}
passwordUser := d.user.LoginMethod == users.LoginMethodPassword
enforcedOtp := config.Auth.Methods.PasswordAuth.EnforcedOtp
missingOtp := d.user.TOTPSecret == ""
@@ -129,6 +142,19 @@ func loginHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (in
// @Router /api/auth/logout [post]
func logoutHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
defer auth.RevokeAPIKey(d.token)
// Clear the authentication cookie by setting it to expire in the past
cookie := &http.Cookie{
Name: "filebrowser_quantum_jwt",
Value: "",
Domain: strings.Split(r.Host, ":")[0],
Path: "/",
SameSite: http.SameSiteLaxMode,
Expires: time.Unix(0, 0), // Expire immediately
MaxAge: -1, // Delete cookie
}
http.SetCookie(w, cookie)
logoutUrl := fmt.Sprintf("%vlogin", config.Server.BaseURL) // Default fallback
if d.user != nil && d.user.LoginMethod == users.LoginMethodProxy {
proxyRedirectUrl := config.Auth.Methods.ProxyAuth.LogoutRedirectUrl
@@ -221,14 +247,34 @@ func renewHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (in
return printToken(w, r, d.user)
}
func printToken(w http.ResponseWriter, _ *http.Request, user *users.User) (int, error) {
signed, err := makeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), time.Hour*time.Duration(config.Auth.TokenExpirationHours), user.Permissions)
func printToken(w http.ResponseWriter, r *http.Request, user *users.User) (int, error) {
expires := time.Hour * time.Duration(config.Auth.TokenExpirationHours)
signed, err := makeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), expires, user.Permissions)
if err != nil {
if strings.Contains(err.Error(), "key already exists with same name") {
return http.StatusConflict, err
}
return 401, errors.ErrUnauthorized
}
// Add 30 minutes buffer so expired token doesn't get automatically deleted by the browser
// This allows backend to identify expired sessions and provide better user feedback
expiresTime := time.Now().Add(expires).Add(time.Minute * 30)
// Set the authentication token as an HTTP cookie
cookie := &http.Cookie{
Name: "filebrowser_quantum_jwt",
Value: signed.Key,
Domain: strings.Split(r.Host, ":")[0], // Set domain to the host without port
Path: "/",
SameSite: http.SameSiteLaxMode,
Expires: expiresTime,
// HttpOnly: true, // Cannot use HttpOnly since frontend needs to read cookie for renew operations
// Secure: true, // Enable this in production with HTTPS
}
http.SetCookie(w, cookie)
// Still return token in body for backward compatibility and state management
w.Header().Set("Content-Type", "text/plain")
if _, err := w.Write([]byte(signed.Key)); err != nil {
return 401, errors.ErrUnauthorized
-1
View File
@@ -1 +0,0 @@
*
+94 -1
View File
@@ -1,9 +1,12 @@
package http
import (
"encoding/json"
"fmt"
"io"
"net/http"
"sync"
"time"
"github.com/gtsteffaniak/filebrowser/backend/common/settings"
"github.com/gtsteffaniak/filebrowser/backend/events"
@@ -26,7 +29,7 @@ func (msgr messenger) sendEvent(eventType, message string) error {
}
func sseHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
if !d.user.Permissions.Realtime {
if !(d.user.Permissions.Realtime || d.user.Permissions.Admin) {
return http.StatusForbidden, fmt.Errorf("realtime is disabled for this user")
}
@@ -80,3 +83,93 @@ func sseHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int,
}
}
}
// OnlyOfficeLogContext stores context for OnlyOffice operations
type OnlyOfficeLogContext struct {
Username string
SessionID string
DocumentID string
FilePath string
Source string
ShareHash string
isAdmin bool
StartTime time.Time
}
// OnlyOfficeLogEvent represents a log event for SSE
type OnlyOfficeLogEvent struct {
EventType string `json:"eventType"`
DocumentID string `json:"documentId"`
Username string `json:"username"`
SessionID string `json:"sessionId"`
LogLevel string `json:"logLevel"`
Message string `json:"message"`
Timestamp string `json:"timestamp"`
Component string `json:"component"`
}
// Global map to store OnlyOffice log contexts
var onlyOfficeContexts = make(map[string]*OnlyOfficeLogContext)
var onlyOfficeContextsMutex sync.RWMutex
// Helper functions for OnlyOffice log context management
func createOnlyOfficeLogContext(username, sessionID, documentID, filePath, source, shareHash string, isAdmin bool) *OnlyOfficeLogContext {
return &OnlyOfficeLogContext{
Username: username,
SessionID: sessionID,
DocumentID: documentID,
FilePath: filePath,
Source: source,
ShareHash: shareHash,
StartTime: time.Now(),
isAdmin: isAdmin,
}
}
func storeOnlyOfficeLogContext(documentID string, context *OnlyOfficeLogContext) {
onlyOfficeContextsMutex.Lock()
defer onlyOfficeContextsMutex.Unlock()
onlyOfficeContexts[documentID] = context
}
func getOnlyOfficeLogContext(documentID string) *OnlyOfficeLogContext {
onlyOfficeContextsMutex.RLock()
defer onlyOfficeContextsMutex.RUnlock()
return onlyOfficeContexts[documentID]
}
func removeOnlyOfficeLogContext(documentID string) {
onlyOfficeContextsMutex.Lock()
defer onlyOfficeContextsMutex.Unlock()
delete(onlyOfficeContexts, documentID)
}
func sendOnlyOfficeLogEvent(context *OnlyOfficeLogContext, level, component, message string) {
if context == nil {
return
}
if !context.isAdmin {
return
}
// Create the log event message
logData := map[string]interface{}{
"documentId": context.DocumentID,
"username": context.Username,
"sessionId": context.SessionID,
"logLevel": level,
"message": message,
"timestamp": time.Now().Format(time.RFC3339),
"component": component,
}
// Convert to JSON
jsonData, err := json.Marshal(logData)
if err != nil {
logger.Errorf("Failed to marshal OnlyOffice log event: %v", err)
return
}
// Send to specific user only (not broadcast to all users)
events.SendToUsers("onlyOfficeLog", string(jsonData), []string{context.Username})
}
+27 -30
View File
@@ -157,15 +157,6 @@ func extractUserFromExpiredToken(r *http.Request, data *requestContext) *users.U
return user
}
proxyUser := r.Header.Get(config.Auth.Methods.ProxyAuth.Header)
if config.Auth.Methods.ProxyAuth.Enabled && proxyUser != "" {
user, err := setupProxyUser(r, data, proxyUser)
if err != nil {
return nil
}
return user
}
keyFunc := func(token *jwt.Token) (interface{}, error) {
return []byte(config.Auth.Key), nil
}
@@ -304,14 +295,7 @@ func userWithoutOTPhelper(fn handleFunc) handleFunc {
// Call the actual handler function with the updated context
username := r.URL.Query().Get("username")
password := r.Header.Get("X-Password")
proxyUser := r.Header.Get(config.Auth.Methods.ProxyAuth.Header)
if config.Auth.Methods.ProxyAuth.Enabled && proxyUser != "" {
user, err := setupProxyUser(r, &requestContext{}, proxyUser)
if err != nil {
return 401, errors.ErrUnauthorized
}
d.user = user
} else if username == "" || password == "" {
if username == "" || password == "" {
return withUserHelper(fn)(w, r, d)
} else {
if !config.Auth.Methods.PasswordAuth.Enabled {
@@ -353,35 +337,30 @@ func withUserHelper(fn handleFunc) handleFunc {
return fn(w, r, data)
}
proxyUser := r.Header.Get(config.Auth.Methods.ProxyAuth.Header)
if config.Auth.Methods.ProxyAuth.Enabled && proxyUser != "" {
user, err := setupProxyUser(r, data, proxyUser)
if err != nil {
return http.StatusForbidden, err
}
data.user = user
setUserInResponseWriter(w, data.user)
if fn == nil {
return http.StatusOK, nil
}
return fn(w, r, data)
}
isProxyUser := config.Auth.Methods.ProxyAuth.Enabled && proxyUser != ""
keyFunc := func(token *jwt.Token) (interface{}, error) {
return []byte(config.Auth.Key), nil
}
tokenString, err := extractToken(r)
if err != nil {
if err != nil && !isProxyUser {
return http.StatusUnauthorized, err
}
data.token = tokenString
var tk users.AuthToken
token, err := jwt.ParseWithClaims(tokenString, &tk, keyFunc)
if err != nil {
if isProxyUser {
return getProxyUser(w, r, data, fn, proxyUser)
}
return http.StatusUnauthorized, fmt.Errorf("error processing token, %v", err)
}
if !token.Valid {
return http.StatusUnauthorized, fmt.Errorf("invalid token")
}
if auth.IsRevokedApiKey(data.token) || tk.Expires < time.Now().Unix() {
if isProxyUser {
return getProxyUser(w, r, data, fn, proxyUser)
}
return http.StatusUnauthorized, fmt.Errorf("token expired or revoked")
}
// Check if the token is about to expire and send a header to renew it
@@ -405,6 +384,24 @@ func withUserHelper(fn handleFunc) handleFunc {
}
}
func getProxyUser(w http.ResponseWriter, r *http.Request, data *requestContext, fn handleFunc, proxyUser string) (int, error) {
// proxy user logic
user, err := setupProxyUser(r, data, proxyUser)
if err != nil {
return http.StatusForbidden, err
}
data.user = user
setUserInResponseWriter(w, data.user)
if data.user.Username == "" {
return http.StatusForbidden, errors.ErrUnauthorized
}
// Call the handler function, passing in the context (or return OK if no handler)
if fn == nil {
return http.StatusOK, nil
}
return fn(w, r, data)
}
// Middleware to ensure the user is either the requested user or an admin
func withSelfOrAdminHelper(fn handleFunc) handleFunc {
return withUserHelper(func(w http.ResponseWriter, r *http.Request, data *requestContext) (int, error) {
+1 -1
View File
@@ -117,7 +117,7 @@ func TestWithAdminHelper(t *testing.T) {
t.Fatalf("Error creating request: %v", err)
}
req.AddCookie(&http.Cookie{
Name: "auth",
Name: "filebrowser_quantum_jwt",
Value: token.Key,
})
+9 -6
View File
@@ -329,6 +329,7 @@ func loginWithOidcUser(w http.ResponseWriter, r *http.Request, username string,
if err = store.Access.SyncUserGroups(username, groups); err != nil {
logger.Warningf("failed to sync oidc user %s groups: %v", username, err)
}
expires := time.Hour * time.Duration(config.Auth.TokenExpirationHours)
// Generate a signed token for the user
signed, err2 := makeSignedTokenAPI(user, "WEB_TOKEN_"+utils.InsecureRandomIdentifier(4), expires, user.Permissions)
@@ -340,16 +341,18 @@ func loginWithOidcUser(w http.ResponseWriter, r *http.Request, username string,
return http.StatusInternalServerError, fmt.Errorf("failed to generate authentication token for user %s: %v", username, err)
}
// add 30 minutes so expired token doesn't get automatically deleted by the browser and the backend can see who's making a new request
// Add 30 minutes buffer so expired token doesn't get automatically deleted by the browser
// This allows backend to identify expired sessions and provide better user feedback
expiresTime := time.Now().Add(expires).Add(time.Minute * 30)
// Set the authentication token as an HTTP cookie
cookie := &http.Cookie{
Name: "auth", // The name of your auth cookie
Value: signed.Key, // The generated token value
Name: "filebrowser_quantum_jwt",
Value: signed.Key,
Domain: strings.Split(r.Host, ":")[0], // Set domain to the host without port
Path: "/", // Make the cookie available to the whole site
SameSite: http.SameSiteLaxMode, // Lax mode allows cookie on navigation from OIDC provider
Expires: expiresTime, // Set cookie expiration
Path: "/",
SameSite: http.SameSiteLaxMode, // Lax mode allows cookie on navigation from OIDC provider
Expires: expiresTime,
// HttpOnly: true, // Cannot use HttpOnly since frontend needs to read cookie for renew operations
// Secure: true, // Enable this in production with HTTPS
}
+254 -51
View File
@@ -22,18 +22,36 @@ import (
)
const (
onlyOfficeStatusDocumentBeingEdited = 1
onlyOfficeStatusDocumentClosedWithChanges = 2
onlyOfficeStatusDocumentSavingError = 3
onlyOfficeStatusDocumentClosedWithNoChanges = 4
onlyOfficeStatusForceSaveWhileDocumentStillOpen = 6
onlyOfficeStatusForceSaveError = 7
)
type OnlyOfficeCallback struct {
ChangesURL string `json:"changesurl,omitempty"`
Key string `json:"key,omitempty"`
Status int `json:"status,omitempty"`
URL string `json:"url,omitempty"`
Users []string `json:"users,omitempty"`
UserData string `json:"userdata,omitempty"`
Actions []OnlyOfficeAction `json:"actions,omitempty"`
ChangesURL string `json:"changesurl,omitempty"`
FileType string `json:"filetype,omitempty"`
ForceSaveType int `json:"forcesavetype,omitempty"`
FormsDataURL string `json:"formsdataurl,omitempty"`
History *OnlyOfficeHistory `json:"history,omitempty"`
Key string `json:"key,omitempty"`
Status int `json:"status,omitempty"`
URL string `json:"url,omitempty"`
UserData string `json:"userdata,omitempty"`
Users []string `json:"users,omitempty"`
}
type OnlyOfficeAction struct {
Type int `json:"type"`
UserID string `json:"userid"`
}
type OnlyOfficeHistory struct {
Changes interface{} `json:"changes"`
ServerVersion string `json:"serverVersion"`
}
// OnlyOfficeJWTPayload represents the JWT payload structure for OnlyOffice callbacks
@@ -115,18 +133,19 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
// Determine file type and editing permissions
fileType := strings.TrimPrefix(filepath.Ext(d.fileInfo.Name), ".")
canEdit := iteminfo.CanEditOnlyOffice(d.user.Permissions.Modify, fileType)
canEditMode := utils.Ternary(canEdit, "edit", "view")
if d.fileInfo.Hash != "" {
// For shares, check both EnableOnlyOfficeEditing and AllowEdit permissions
if d.share.EnableOnlyOfficeEditing && d.share.AllowModify {
// Editing also requires authentication (not anonymous)
if d.user.Username != "anonymous" {
canEditMode = "edit"
}
}
// Determine modify permissions based on whether this is a share or regular request
var modifyPerms bool
if d.fileInfo.Hash != "" && d.share != nil {
// Share request - check share permissions
modifyPerms = d.share.AllowModify
} else {
// Regular user request - check user permissions
modifyPerms = d.user.Permissions.Modify
}
canEdit := iteminfo.CanEditOnlyOffice(modifyPerms, fileType)
canEditMode := utils.Ternary(canEdit, "edit", "view")
// Generate document ID for OnlyOffice
documentId, err := getOnlyOfficeId(d.fileInfo.RealPath)
if err != nil {
@@ -134,6 +153,31 @@ func onlyofficeClientConfigGetHandler(w http.ResponseWriter, r *http.Request, d
return http.StatusNotFound, fmt.Errorf("failed to generate document ID: %v", err)
}
// Create and store log context for this OnlyOffice session
sessionID := r.URL.Query().Get("sessionId")
if sessionID == "" {
sessionID = "unknown"
}
shareHash := ""
if d.fileInfo.Hash != "" {
shareHash = d.fileInfo.Hash
}
logContext := createOnlyOfficeLogContext(
d.user.Username,
sessionID,
documentId,
path,
source,
shareHash,
d.user.Permissions.Admin,
)
storeOnlyOfficeLogContext(documentId, logContext)
// Send initial log event
sendOnlyOfficeLogEvent(logContext, "INFO", "config", fmt.Sprintf("OnlyOffice session started for document: %s", d.fileInfo.Name))
// Build download URL that OnlyOffice server will use
downloadURL := buildOnlyOfficeDownloadURL(source, providedPath, d.fileInfo.Hash, d.token)
@@ -253,7 +297,7 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
// Validate required parameters
if (path == "" || source == "") && d.fileInfo.Hash == "" {
logger.Errorf("OnlyOffice callback missing required parameters: source=%s, path=%s", source, path)
return http.StatusBadRequest, errors.New("missing required parameters: path + source/hash are required")
return returnOnlyOfficeError(w, r, 400, "missing required parameters: path + source/hash are required")
}
var sourceInfo *settings.Source
var ok bool
@@ -261,13 +305,13 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
sourceInfo, ok = settings.Config.Server.SourceMap[d.share.Source]
if !ok {
logger.Error("OnlyOffice: share source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
return returnOnlyOfficeError(w, r, 404, "source not found")
}
} else {
sourceInfo, ok = settings.Config.Server.NameToSource[source]
if !ok {
logger.Error("OnlyOffice: source not found")
return http.StatusInternalServerError, fmt.Errorf("source not found")
return returnOnlyOfficeError(w, r, 404, "source not found")
}
}
@@ -276,7 +320,7 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
userScope, scopeErr := settings.GetScopeFromSourceName(d.user.Scopes, source)
if scopeErr != nil {
logger.Errorf("OnlyOffice callback: source %s not available for user %s: %v", source, d.user.Username, scopeErr)
return http.StatusForbidden, fmt.Errorf("source %s is not available", source)
return returnOnlyOfficeError(w, r, 403, "source not available")
}
path = utils.JoinPathAsUnix(userScope, path)
} else {
@@ -294,76 +338,193 @@ func processOnlyOfficeCallback(w http.ResponseWriter, r *http.Request, d *reques
// When the document is fully closed by all editors,
// the document key should no longer be re-used.
deleteOfficeId(source, path)
// Send log event for document closure and clean up log context
if logContext := getOnlyOfficeLogContext(data.Key); logContext != nil {
statusMsg := "Document closed with changes"
if data.Status == onlyOfficeStatusDocumentClosedWithNoChanges {
statusMsg = "Document closed with no changes"
}
sendOnlyOfficeLogEvent(logContext, "INFO", "callback", statusMsg)
removeOnlyOfficeLogContext(data.Key)
}
}
// Handle document save operations
// Handle document being edited (status 1) - just log for now
if data.Status == onlyOfficeStatusDocumentBeingEdited {
logger.Debugf("OnlyOffice callback: document being edited, key=%s, users=%v", data.Key, data.Users)
// Send log event for document being edited
if logContext := getOnlyOfficeLogContext(data.Key); logContext != nil {
sendOnlyOfficeLogEvent(logContext, "DEBUG", "callback", fmt.Sprintf("Document being edited, users: %v", data.Users))
}
// Handle actions if present
for _, action := range data.Actions {
actionMsg := ""
switch action.Type {
case 0: // User disconnects
actionMsg = fmt.Sprintf("User ID %s disconnected from document", action.UserID)
logger.Debugf("OnlyOffice callback: user ID %s disconnected from document", action.UserID)
case 1: // New user connects
actionMsg = fmt.Sprintf("User ID %s connected to document", action.UserID)
logger.Debugf("OnlyOffice callback: user ID %s connected to document", action.UserID)
case 2: // User clicked forcesave button
actionMsg = fmt.Sprintf("User ID %s clicked forcesave button", action.UserID)
logger.Debugf("OnlyOffice callback: user ID %s clicked forcesave button", action.UserID)
default:
actionMsg = fmt.Sprintf("Unknown action type %d for user ID %s", action.Type, action.UserID)
logger.Debugf("OnlyOffice callback: unknown action type %d for user ID %s", action.Type, action.UserID)
}
// Send log event for action
if logContext := getOnlyOfficeLogContext(data.Key); logContext != nil {
sendOnlyOfficeLogEvent(logContext, "DEBUG", "callback", actionMsg)
}
}
}
// Handle document save operations (status 2, 3, 6, 7)
if data.Status == onlyOfficeStatusDocumentClosedWithChanges ||
data.Status == onlyOfficeStatusForceSaveWhileDocumentStillOpen {
data.Status == onlyOfficeStatusDocumentSavingError ||
data.Status == onlyOfficeStatusForceSaveWhileDocumentStillOpen ||
data.Status == onlyOfficeStatusForceSaveError {
// Log the save operation details
statusDesc := ""
switch data.Status {
case onlyOfficeStatusDocumentClosedWithChanges:
statusDesc = "document closed with changes"
case onlyOfficeStatusDocumentSavingError:
statusDesc = "document saving error"
case onlyOfficeStatusForceSaveWhileDocumentStillOpen:
statusDesc = "force save while document still open"
case onlyOfficeStatusForceSaveError:
statusDesc = "force save error"
}
logger.Debugf("OnlyOffice callback: processing save operation - %s, key=%s, url=%s, forcesavetype=%d",
statusDesc, data.Key, data.URL, data.ForceSaveType)
// Send log event for save operation
if logContext := getOnlyOfficeLogContext(data.Key); logContext != nil {
sendOnlyOfficeLogEvent(logContext, "INFO", "callback", fmt.Sprintf("Processing save operation: %s", statusDesc))
}
// Handle history and changes URL if present
if data.History != nil {
logger.Debugf("OnlyOffice callback: received history data with serverVersion=%s", data.History.ServerVersion)
if logContext := getOnlyOfficeLogContext(data.Key); logContext != nil {
sendOnlyOfficeLogEvent(logContext, "DEBUG", "callback", fmt.Sprintf("Received history data with serverVersion=%s", data.History.ServerVersion))
}
}
if data.ChangesURL != "" {
logger.Debugf("OnlyOffice callback: received changes URL: %s", data.ChangesURL)
if logContext := getOnlyOfficeLogContext(data.Key); logContext != nil {
sendOnlyOfficeLogEvent(logContext, "DEBUG", "callback", "Received changes URL for document history")
}
}
// For status 3 (saving error), don't attempt to save the file
if data.Status == onlyOfficeStatusDocumentSavingError {
logger.Warningf("OnlyOffice callback: document saving error occurred, not attempting to save")
return returnOnlyOfficeSuccess(w, r)
}
// Check share permissions first if this is a share request
if d.fileInfo.Hash != "" {
if !d.share.AllowModify {
logger.Warningf("OnlyOffice callback: edit permission not allowed for this share")
return http.StatusForbidden, fmt.Errorf("edit permission not allowed for this share")
logger.Errorf("OnlyOffice callback: edit permission not allowed for this share")
return returnOnlyOfficeError(w, r, 403, "edit permission not allowed for this share")
}
// Share edit operations also require authentication (not anonymous)
if d.user.Username == "anonymous" {
logger.Warningf("OnlyOffice callback: edit operations require authentication")
return http.StatusForbidden, fmt.Errorf("edit operations require authentication")
} else {
// Verify user has modify permissions
if !d.user.Permissions.Modify {
logger.Errorf("OnlyOffice callback: user %s lacks modify permissions for source=%s, path=%s",
d.user.Username, source, path)
return returnOnlyOfficeError(w, r, 403, "user lacks modify permissions")
}
}
// Verify user has modify permissions
if !d.user.Permissions.Modify {
logger.Warningf("OnlyOffice callback: user %s lacks modify permissions for source=%s, path=%s",
d.user.Username, source, path)
return http.StatusForbidden, nil
}
// Download the updated document from OnlyOffice server
doc, err := http.Get(data.URL)
if err != nil {
logger.Errorf("OnlyOffice callback: failed to download updated document: %v", err)
return http.StatusInternalServerError, err
return returnOnlyOfficeError(w, r, 500, "failed to download updated document")
}
defer doc.Body.Close()
// Check if the download was successful
if doc.StatusCode != 200 {
logger.Errorf("OnlyOffice callback: failed to download document, status code: %d", doc.StatusCode)
return returnOnlyOfficeError(w, r, 500, "failed to download document from OnlyOffice server")
}
// Resolve file path for writing (same logic as in config handler)
var resolvedPath string
resolvedPath := path
if d.fileInfo.Hash == "" {
// Regular user request - need to resolve scope
userScope, scopeErr := settings.GetScopeFromSourceName(d.user.Scopes, source)
if scopeErr != nil {
logger.Errorf("OnlyOffice callback: source %s not available for user %s: %v",
source, d.user.Username, scopeErr)
return http.StatusForbidden, fmt.Errorf("source %s is not available", source)
return returnOnlyOfficeError(w, r, 403, "source not available")
}
resolvedPath = utils.JoinPathAsUnix(userScope, path)
}
// Write the updated document
fileOpts := utils.FileOptions{
Path: resolvedPath,
logger.Debugf("OnlyOffice callback: saving document to path=%s, source=%s, user=%s",
resolvedPath, source, d.user.Username)
// CRITICAL: Validate that the original file still exists before saving
// This prevents creating duplicate files if the original was renamed/moved
_, err = files.FileInfoFaster(utils.FileOptions{
Source: source,
Path: resolvedPath,
}, nil)
if err != nil {
logger.Errorf("OnlyOffice callback: original file no longer exists at path=%s, source=%s: %v",
resolvedPath, source, err)
// Send error log event
if logContext := getOnlyOfficeLogContext(data.Key); logContext != nil {
sendOnlyOfficeLogEvent(logContext, "ERROR", "callback",
fmt.Sprintf("Original file no longer exists: %v -- was it renamed or moved?", err))
}
return returnOnlyOfficeError(w, r, 404, "original file no longer exists - it may have been renamed or moved")
}
writeErr := files.WriteFile(fileOpts, doc.Body)
writeErr := files.WriteFile(source, resolvedPath, doc.Body)
if writeErr != nil {
logger.Errorf("OnlyOffice callback: failed to write updated document: %v", writeErr)
return http.StatusInternalServerError, writeErr
logger.Errorf("OnlyOffice callback: failed to write updated document to path=%s, source=%s: %v",
resolvedPath, source, writeErr)
// Send error log event
if logContext := getOnlyOfficeLogContext(data.Key); logContext != nil {
sendOnlyOfficeLogEvent(logContext, "ERROR", "callback", fmt.Sprintf("Failed to save document: %v", writeErr))
}
return returnOnlyOfficeError(w, r, 500, "failed to save document")
}
logger.Infof("OnlyOffice callback: successfully saved document to path=%s, source=%s",
resolvedPath, source)
// Send success log event
if logContext := getOnlyOfficeLogContext(data.Key); logContext != nil {
sendOnlyOfficeLogEvent(logContext, "INFO", "callback", "Document saved successfully")
}
}
// Return success response to OnlyOffice server
resp := map[string]int{
"error": 0,
}
return renderJSON(w, r, resp)
return returnOnlyOfficeSuccess(w, r)
}
func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *requestContext) (int, error) {
// Parse callback data based on request method
var callbackData *OnlyOfficeCallback
var err error
if r.Method == "GET" {
// OnlyOffice sends callback data in Authorization header as JWT
callbackData, err = parseOnlyOfficeCallbackFromJWT(r)
@@ -371,12 +532,17 @@ func onlyofficeCallbackHandler(w http.ResponseWriter, r *http.Request, d *reques
// OnlyOffice sends callback data in request body as JSON
callbackData, err = parseOnlyOfficeCallbackFromJSON(r)
} else {
return http.StatusMethodNotAllowed, fmt.Errorf("unsupported method: %s", r.Method)
return returnOnlyOfficeError(w, r, 405, fmt.Sprintf("unsupported method: %s", r.Method))
}
if err != nil {
logger.Errorf("OnlyOffice callback: failed to parse callback data: %v", err)
return http.StatusBadRequest, err
return returnOnlyOfficeError(w, r, 400, "failed to parse callback data")
}
if callbackData == nil {
logger.Errorf("OnlyOffice callback: parsed callback data is nil")
return returnOnlyOfficeError(w, r, 400, "parsed callback data is nil")
}
// Process the callback data using shared logic
@@ -501,3 +667,40 @@ func parseOnlyOfficeJWT(tokenString string) (*OnlyOfficeCallback, error) {
return callback, nil
}
// returnOnlyOfficeSuccess returns a success response to OnlyOffice server
func returnOnlyOfficeSuccess(w http.ResponseWriter, r *http.Request) (int, error) {
resp := map[string]int{
"error": 0,
}
return renderJSON(w, r, resp)
}
// returnOnlyOfficeError returns an error response to OnlyOffice server with proper status code
func returnOnlyOfficeError(w http.ResponseWriter, r *http.Request, statusCode int, message string) (int, error) {
// OnlyOffice expects specific error codes in the response body
errorCode := 0
switch statusCode {
case 400:
errorCode = 1 // Bad request
case 403:
errorCode = 1 // Forbidden (treated as bad request by OnlyOffice)
case 404:
errorCode = 1 // Not found (treated as bad request by OnlyOffice)
case 500:
errorCode = 1 // Internal server error (treated as bad request by OnlyOffice)
default:
errorCode = 1 // Default to bad request
}
resp := map[string]interface{}{
"error": errorCode,
}
// Log the error for debugging
logger.Errorf("OnlyOffice callback error (HTTP %d): %s", statusCode, message)
// Set the appropriate HTTP status code
w.WriteHeader(statusCode)
return renderJSON(w, r, resp)
}
+2 -7
View File
@@ -289,13 +289,8 @@ func publicPutHandler(w http.ResponseWriter, r *http.Request, d *requestContext)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid path encoding: %v", err)
}
fileOpts := utils.FileOptions{
Username: d.user.Username,
Path: utils.JoinPathAsUnix(d.share.Path, path),
Source: source,
Expand: false,
}
err = files.WriteFile(fileOpts, r.Body)
resolvedPath := utils.JoinPathAsUnix(d.share.Path, path)
err = files.WriteFile(source, resolvedPath, r.Body)
return errToStatus(err), err
}
+4 -13
View File
@@ -368,8 +368,8 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
// Move the completed file from the temp location to the final destination
err = fileutils.MoveFile(tempFilePath, realPath)
if err != nil {
logger.Debugf("could not move temp file to destination: %v", err)
return http.StatusInternalServerError, fmt.Errorf("could not move temp file to destination: %v", err)
logger.Debugf("could not move file from %v to %v: %v", tempFilePath, realPath, err)
return http.StatusInternalServerError, fmt.Errorf("could not move file from chunked folder to destination: %v", err)
}
go files.RefreshIndex(source, realPath, false, false) //nolint:errcheck
}
@@ -381,7 +381,7 @@ func resourcePostHandler(w http.ResponseWriter, r *http.Request, d *requestConte
if err != nil {
preview.DelThumbs(r.Context(), *fileInfo)
}
err = files.WriteFile(fileOpts, r.Body)
err = files.WriteFile(fileOpts.Source, fileOpts.Path, r.Body)
if err != nil {
logger.Debugf("error writing file: %v", err)
return errToStatus(err), err
@@ -428,15 +428,6 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
if err != nil {
return http.StatusForbidden, err
}
userscope = strings.TrimRight(userscope, "/")
fileOpts := utils.FileOptions{
Username: d.user.Username,
Path: utils.JoinPathAsUnix(userscope, path),
Source: source,
Expand: false,
}
// Check access control for the target path
idx := indexing.GetIndex(source)
if idx == nil {
@@ -447,7 +438,7 @@ func resourcePutHandler(w http.ResponseWriter, r *http.Request, d *requestContex
return http.StatusForbidden, fmt.Errorf("access denied to path %s", path)
}
err = files.WriteFile(fileOpts, r.Body)
err = files.WriteFile(source, utils.JoinPathAsUnix(userscope, path), r.Body)
return errToStatus(err), err
}
+1 -1
View File
@@ -102,7 +102,6 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
shareProps["enforceDarkLightMode"] = d.share.EnforceDarkLightMode
shareProps["viewMode"] = d.share.ViewMode
shareProps["enableOnlyOffice"] = d.share.EnableOnlyOffice
shareProps["enableOnlyOfficeEditing"] = d.share.EnableOnlyOfficeEditing
shareProps["shareType"] = utils.Ternary(d.share.ShareType == "", "normal", d.share.ShareType)
shareProps["perUserDownloadLimit"] = d.share.PerUserDownloadLimit
shareProps["extractEmbeddedSubtitles"] = d.share.ExtractEmbeddedSubtitles
@@ -189,6 +188,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
"disableNavButtons": disableNavButtons,
"userSelectableThemes": config.Frontend.Styling.CustomThemeOptions,
"enableHeicConversion": config.Integrations.Media.Convert.ImagePreview[settings.HEICImagePreview],
"eventBasedThemes": !config.Frontend.Styling.DisableEventBasedThemes,
}
// Marshal each variable to JSON strings for direct template usage
+57
View File
@@ -0,0 +1,57 @@
server:
port: 8080
baseURL: "/"
database: "playwright-files.db"
logging:
- levels: "info|warning|debug|error"
sources:
- path: "../frontend/playwright-files"
config:
defaultEnabled: true
userDefaults:
editorQuickSave: false # show quick save button in editor
hideSidebarFileActions: false # hide the file actions in the sidebar
disableQuickToggles: false # disable the quick toggles in the sidebar
disableSearchOptions: false # disable the search options in the search bar
stickySidebar: true # keep sidebar open when navigating
darkMode: true # should dark mode be enabled
viewMode: "normal" # view mode to use: eg. normal, list, grid, or compact
singleClick: false # open directory on single click, also enables middle click to open in new tab
showHidden: false # show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
dateFormat: false # when false, the date is relative, when true, the date is an exact timestamp
gallerySize: 3 # 0-9 - the size of the gallery thumbnails
themeColor: "var(--blue)" # theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
quickDownload: false # show icon to download in one click
disablePreviewExt: "" # space separated list of file extensions to disable preview for
disableViewingExt: "" # space separated list of file extensions to disable viewing for
lockPassword: false # disable the user from changing their password
disableSettings: false # disable the user from viewing the settings page
preview:
disableHideSidebar: true # disable the hide sidebar preview for previews and editors
highQuality: true # generate high quality thumbnail preview images
image: true # show thumbnail preview image for image files
video: true # show thumbnail preview image for video files
motionVideoPreview: true # show multiple frames for videos in thumbnail preview when hovering
office: true # show thumbnail preview image for office files
popup: true # show larger popup preview when hovering over thumbnail
autoplayMedia: true # autoplay media files in preview
defaultMediaPlayer: true # disable html5 media player and use the default media player
folder: true # show thumbnail preview image for folder files
permissions:
api: false # allow api access
admin: false # allow admin access
modify: false # allow modifying files
share: false # allow sharing files
realtime: false # allow realtime updates
delete: false # allow deleting files
create: false # allow creating or uploading files
download: true # allow downloading files
disableUpdateNotifications: false # disable update notifications banner for admin users
deleteWithoutConfirming: false # delete files without confirmation
disableOnlyOfficeExt: ".md .txt .pdf" # list of file extensions to disable onlyoffice editor for
showSelectMultiple: false # show select multiple files on desktop
defaultLandingPage: "" # default landing page to use if no redirect is specified: eg. /files/mysource/mysubpath, /settings, etc.
#office:
# url: "http://localhost:9052"
# secret: "6H1H7GzUk2alRkNxAPav0W02g8JS5oUd"
+18 -8
View File
@@ -2922,6 +2922,10 @@ const docTemplate = `{
"type": "object",
"properties": {
"hidden": {
"description": "deprecated: use ignoreHidden instead to exclude hidden files and folders.",
"type": "boolean"
},
"ignoreHidden": {
"description": "exclude hidden files and folders.",
"type": "boolean"
},
@@ -2945,10 +2949,14 @@ const docTemplate = `{
"description": "(global) exclude files that end with these suffixes. Eg. \".jpg\" or \".txt\"",
"type": "string"
},
"fileNames": {
"fileName": {
"description": "(global) exclude files that match these names. Eg. \"file.txt\" or \"test.csv\"",
"type": "string"
},
"fileNames": {
"description": "deprecated: exclude files that match these names. Eg. \"file.txt\" or \"test.csv\"",
"type": "string"
},
"filePath": {
"description": "(global) exclude files that match this path. Eg. \"/path/to/file.txt\" or \"/path/to/file.txt/subfile.txt\"",
"type": "string"
@@ -2961,10 +2969,14 @@ const docTemplate = `{
"description": "(global) exclude folders that end with these suffixes. Eg. \".thumbnails\" or \".git\"",
"type": "string"
},
"folderNames": {
"folderName": {
"description": "(global) exclude folders that match these names. Eg. \"folder\" or \"subfolder\"",
"type": "string"
},
"folderNames": {
"description": "deprecated: exclude folders that match these names. Eg. \"folder\" or \"subfolder\"",
"type": "string"
},
"folderPath": {
"description": "(global) exclude folders that match this path. Eg. \"/path/to/folder\" or \"/path/to/folder/subfolder\"",
"type": "string"
@@ -3513,6 +3525,10 @@ const docTemplate = `{
"description": "Specify a valid CSS color property value to use as the background color in dark mode",
"type": "string"
},
"disableEventThemes": {
"description": "disable the event based themes,",
"type": "boolean"
},
"lightBackground": {
"description": "specify a valid CSS color property value to use as the background color in light mode",
"type": "string"
@@ -3705,9 +3721,6 @@ const docTemplate = `{
"enableOnlyOffice": {
"type": "boolean"
},
"enableOnlyOfficeEditing": {
"type": "boolean"
},
"enforceDarkLightMode": {
"description": "\"dark\" or \"light\"",
"type": "string"
@@ -3834,9 +3847,6 @@ const docTemplate = `{
"enableOnlyOffice": {
"type": "boolean"
},
"enableOnlyOfficeEditing": {
"type": "boolean"
},
"enforceDarkLightMode": {
"description": "\"dark\" or \"light\"",
"type": "string"
+18 -8
View File
@@ -2911,6 +2911,10 @@
"type": "object",
"properties": {
"hidden": {
"description": "deprecated: use ignoreHidden instead to exclude hidden files and folders.",
"type": "boolean"
},
"ignoreHidden": {
"description": "exclude hidden files and folders.",
"type": "boolean"
},
@@ -2934,10 +2938,14 @@
"description": "(global) exclude files that end with these suffixes. Eg. \".jpg\" or \".txt\"",
"type": "string"
},
"fileNames": {
"fileName": {
"description": "(global) exclude files that match these names. Eg. \"file.txt\" or \"test.csv\"",
"type": "string"
},
"fileNames": {
"description": "deprecated: exclude files that match these names. Eg. \"file.txt\" or \"test.csv\"",
"type": "string"
},
"filePath": {
"description": "(global) exclude files that match this path. Eg. \"/path/to/file.txt\" or \"/path/to/file.txt/subfile.txt\"",
"type": "string"
@@ -2950,10 +2958,14 @@
"description": "(global) exclude folders that end with these suffixes. Eg. \".thumbnails\" or \".git\"",
"type": "string"
},
"folderNames": {
"folderName": {
"description": "(global) exclude folders that match these names. Eg. \"folder\" or \"subfolder\"",
"type": "string"
},
"folderNames": {
"description": "deprecated: exclude folders that match these names. Eg. \"folder\" or \"subfolder\"",
"type": "string"
},
"folderPath": {
"description": "(global) exclude folders that match this path. Eg. \"/path/to/folder\" or \"/path/to/folder/subfolder\"",
"type": "string"
@@ -3502,6 +3514,10 @@
"description": "Specify a valid CSS color property value to use as the background color in dark mode",
"type": "string"
},
"disableEventThemes": {
"description": "disable the event based themes,",
"type": "boolean"
},
"lightBackground": {
"description": "specify a valid CSS color property value to use as the background color in light mode",
"type": "string"
@@ -3694,9 +3710,6 @@
"enableOnlyOffice": {
"type": "boolean"
},
"enableOnlyOfficeEditing": {
"type": "boolean"
},
"enforceDarkLightMode": {
"description": "\"dark\" or \"light\"",
"type": "string"
@@ -3823,9 +3836,6 @@
"enableOnlyOffice": {
"type": "boolean"
},
"enableOnlyOfficeEditing": {
"type": "boolean"
},
"enforceDarkLightMode": {
"description": "\"dark\" or \"light\"",
"type": "string"
+17 -6
View File
@@ -170,6 +170,10 @@ definitions:
settings.ConditionalFilter:
properties:
hidden:
description: 'deprecated: use ignoreHidden instead to exclude hidden files
and folders.'
type: boolean
ignoreHidden:
description: exclude hidden files and folders.
type: boolean
ignoreZeroSizeFolders:
@@ -187,10 +191,14 @@ definitions:
description: (global) exclude files that end with these suffixes. Eg. ".jpg"
or ".txt"
type: string
fileNames:
fileName:
description: (global) exclude files that match these names. Eg. "file.txt"
or "test.csv"
type: string
fileNames:
description: 'deprecated: exclude files that match these names. Eg. "file.txt"
or "test.csv"'
type: string
filePath:
description: (global) exclude files that match this path. Eg. "/path/to/file.txt"
or "/path/to/file.txt/subfile.txt"
@@ -203,10 +211,14 @@ definitions:
description: (global) exclude folders that end with these suffixes. Eg. ".thumbnails"
or ".git"
type: string
folderNames:
folderName:
description: (global) exclude folders that match these names. Eg. "folder"
or "subfolder"
type: string
folderNames:
description: 'deprecated: exclude folders that match these names. Eg. "folder"
or "subfolder"'
type: string
folderPath:
description: (global) exclude folders that match this path. Eg. "/path/to/folder"
or "/path/to/folder/subfolder"
@@ -618,6 +630,9 @@ definitions:
description: Specify a valid CSS color property value to use as the background
color in dark mode
type: string
disableEventThemes:
description: disable the event based themes,
type: boolean
lightBackground:
description: specify a valid CSS color property value to use as the background
color in light mode
@@ -761,8 +776,6 @@ definitions:
type: integer
enableOnlyOffice:
type: boolean
enableOnlyOfficeEditing:
type: boolean
enforceDarkLightMode:
description: '"dark" or "light"'
type: string
@@ -851,8 +864,6 @@ definitions:
type: integer
enableOnlyOffice:
type: boolean
enableOnlyOfficeEditing:
type: boolean
enforceDarkLightMode:
description: '"dark" or "light"'
type: string
+1
View File
@@ -22,6 +22,7 @@
}
},
"rules": {
"no-unreachable": "off",
// vue-i18n rules:
// This rule will check if the key used in $t() exists in your locale messages.
// By default, it checks against all locales. We want it to primarily use 'en.json'
+1 -1
View File
@@ -8,7 +8,7 @@
"node": ">=18.0.0"
},
"scripts": {
"build": "vite build --outDir ../backend/http/dist && cp -r ../backend/http/dist/* ../backend/http/embed",
"build": "rm -rf ../backend/http/dist/* && rm -rf ../backend/http/embed/* && vite build --outDir ../backend/http/dist && cp -r ../backend/http/dist/* ../backend/http/embed",
"build:windows": "vite build --outDir ../backend/http/dist && robocopy ../backend/http/dist ../backend/http/embed /e",
"build:docker": "vite build",
"watch": "DEV_BUILD=true vite build --watch --outDir ../backend/http/dist",
+17 -4
View File
@@ -12,7 +12,7 @@ import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
globalSetup: "./tests/playwright/local-setup",
timeout: 5000,
testDir: "./tests/playwright/copy",
testDir: "./tests/playwright/local",
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
@@ -20,7 +20,6 @@ export default defineConfig({
/* Retry on CI only */
retries: 2,
/* Opt out of parallel tests on CI. */
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "line",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
@@ -40,8 +39,22 @@ export default defineConfig({
/* Configure projects for major browsers */
projects: [
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
name: "dark-screenshots",
use: {
...devices["Desktop Firefox"],
theme: 'dark',
},
testMatch: /.*screenshots.spec.ts/,
retries: 0,
},
{
name: "light-screenshots",
use: {
...devices["Desktop Firefox"],
theme: 'light',
},
testMatch: /.*screenshots.spec.ts/,
retries: 0,
},
],
});
+4 -3
View File
@@ -28,7 +28,7 @@ server:
indexingIntervalMinutes: 0 # optional manual overide interval in minutes to re-index the source
disableIndexing: false # disable the indexing of this source
conditionals: # conditional rules to apply when indexing to include/exclude certain items
hidden: false # exclude hidden files and folders.
ignoreHidden: false # exclude hidden files and folders.
ignoreZeroSizeFolders: false # ignore folders with 0 size
rules: # list of item rules to apply to specific paths
- neverWatchPath: "" # index the folder in the first pass to get included in search, but never re-indexed.
@@ -39,8 +39,8 @@ server:
folderEndsWith: "" # (global) exclude folders that end with these suffixes. Eg. ".thumbnails" or ".git"
folderPath: "" # (global) exclude folders that match this path. Eg. "/path/to/folder" or "/path/to/folder/subfolder"
filePath: "" # (global) exclude files that match this path. Eg. "/path/to/file.txt" or "/path/to/file.txt/subfile.txt"
fileNames: "" # (global) exclude files that match these names. Eg. "file.txt" or "test.csv"
folderNames: "" # (global) exclude folders that match these names. Eg. "folder" or "subfolder"
fileName: "" # (global) exclude files that match these names. Eg. "file.txt" or "test.csv"
folderName: "" # (global) exclude folders that match these names. Eg. "folder" or "subfolder"
viewable: false # Enable viewing in UI but exclude from indexing
defaultUserScope: "/" # default "/" should match folders under path
defaultEnabled: true # should be added as a default source for new users?
@@ -99,6 +99,7 @@ frontend:
url: "help prompt" # the url to link to validate:required
disableNavButtons: false # disable the nav buttons in the sidebar
styling:
disableEventThemes: false # disable the event based themes,
customCSS: "" # if a valid path to a css file is provided, it will be applied for all users. (eg. "reduce-rounded-corners.css")
lightBackground: "#f5f5f5" # specify a valid CSS color property value to use as the background color in light mode
darkBackground: "#141D24" # Specify a valid CSS color property value to use as the background color in dark mode
+1 -6
View File
@@ -1,7 +1,6 @@
import { fetchURL, fetchJSON } from '@/api/utils'
import { getApiPath, getPublicApiPath } from '@/utils/url.js'
import { notify } from '@/notify' // Import notify for error handling
import { setNewToken } from '@/utils/auth.js' // Import setNewToken for token management
export async function getAllUsers() {
try {
const apiPath = getApiPath('api/users')
@@ -80,13 +79,9 @@ export async function login(username, password, recaptcha, otp) {
body = { message: bodyText };
}
if (res.status === 200) {
await setNewToken(bodyText);
} else if (res.status === 403) {
if (res.status != 200) {
const msg = body.message || 'Forbidden';
throw new Error(msg);
} else {
throw new Error(body.message || bodyText || 'Failed to login');
}
}
export async function get(id) {
+2 -1
View File
@@ -28,7 +28,8 @@ export async function fetchURL(url, opts, auth = true) {
}
if (auth && res.headers.get("X-Renew-Token") === "true") {
await renew(state.jwt);
// Cookie is automatically sent, no need to pass JWT from state
await renew();
}
if (res.status < 200 || res.status > 299) {
+20
View File
@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<defs>
<pattern id="halloween-pattern" width="200" height="200" patternUnits="userSpaceOnUse">
<rect width="200" height="200" fill="#1a1a1a"/>
<g opacity="0.08" fill="#cccccc">
<!-- Ghost 1 (top-left) - straight sides ghost -->
<path d="M 60 60 A 15 15 0 1 1 90 60 L 90 90 C 80 100, 70 100, 60 90 L 60 60 Z" />
<circle cx="70" cy="55" r="2" fill="#1a1a1a"/>
<circle cx="80" cy="55" r="2" fill="#1a1a1a"/>
<!-- Ghost 2 (bottom-right, offset for diagonal pattern) - straight sides ghost -->
<path d="M 160 160 A 15 15 0 1 1 190 160 L 190 190 C 180 200, 170 200, 160 190 L 160 160 Z" />
<circle cx="170" cy="155" r="2" fill="#1a1a1a"/>
<circle cx="180" cy="155" r="2" fill="#1a1a1a"/>
</g>
</pattern>
</defs>
<rect width="200" height="200" fill="url(#halloween-pattern)"/>
</svg>

After

Width:  |  Height:  |  Size: 949 B

+17 -5
View File
@@ -5,7 +5,15 @@
@click="clearContext"
>
<!-- Search input section -->
<div id="search-input" @click="open">
<div id="search-input" @click="open" :class="{'halloween-eyes': eventTheme === 'halloween'}">
<div id="halloween-eyes" v-if="eventTheme=== 'halloween' && active">
<div class="eye left">
<div class="pupil"></div>
</div>
<div class="eye right">
<div class="pupil"></div>
</div>
</div>
<!-- Close button visible when search is active -->
<button
v-if="active"
@@ -22,6 +30,7 @@
<input
id="main-input"
class="main-input"
:class="{ 'halloween-theme': eventTheme === 'halloween' }"
type="text"
@keyup.exact="keyup"
@input="submit"
@@ -34,7 +43,7 @@
</div>
<!-- Search results for desktop -->
<div v-show="active" id="results" class="fb-shadow" ref="result">
<div v-show="active" id="results" class="fb-shadow" ref="result">
<div class="inputWrapper" style="display: flex">
<select
v-if="multipleSources"
@@ -249,7 +258,7 @@ export default {
// For other browsers, use regular contextmenu
this.$el.addEventListener("contextmenu", this.openContext);
}
// Add keyboard event listener for "/" to activate search
this.handleKeydown = (event) => {
if (event.key === '/' && !state.isSearchActive) {
@@ -257,7 +266,7 @@ export default {
this.open();
}
};
document.addEventListener('keydown', this.handleKeydown);
},
beforeUnmount() {
@@ -271,13 +280,16 @@ export default {
} else {
this.$el.removeEventListener("contextmenu", this.openContext);
}
// Clean up keyboard event listener
if (this.handleKeydown) {
document.removeEventListener('keydown', this.handleKeydown);
}
},
computed: {
eventTheme() {
return getters.eventTheme();
},
disableSearchOptions() {
return state.user.disableSearchOptions;
},
+32 -3
View File
@@ -357,6 +357,31 @@ export default {
});
}
// Filter out items being dropped onto themselves or into their own subdirectories
items = items.filter(item => {
// Skip if source and destination are the same
if (item.from === item.to) {
return false;
}
// Skip if trying to move a directory into itself
// Check if the destination path would be within the source path
const fromPath = item.from;
const destinationDir = this.path;
// If destination dir is the same as or contains the source path, skip
if (fromPath === destinationDir || fromPath.startsWith(destinationDir + '/')) {
return false;
}
return true;
});
// If all items were filtered out, silently skip the operation
if (items.length === 0) {
return;
}
let checkAction = async () => {
if (getters.isShare()) {
return await publicApi.fetchPub(this.path, shareInfo.hash);
@@ -435,9 +460,13 @@ export default {
// @ts-ignore
this.contextTimeout = setTimeout(() => {
if (!this.isSwipe) {
mutations.resetSelected();
// @ts-ignore
mutations.addSelected(this.index);
// Only reset selection if this item is not already selected
// This prevents resetting selection when trying to open context menu on selected item
if (!this.isSelected) {
mutations.resetSelected();
// @ts-ignore
mutations.addSelected(this.index);
}
}
}, 500);
},
+4 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="scroll-wrapper" ref="wrapper">
<div class="scroll-wrapper" :class="{ 'halloween-theme': eventTheme === 'halloween' }" ref="wrapper">
<slot />
<div
class="custom-scrollbar"
@@ -52,6 +52,9 @@ export default {
};
},
computed: {
eventTheme() {
return getters.eventTheme();
},
isScrollable() {
return getters.isScrollable();
},
+90 -7
View File
@@ -4,7 +4,6 @@
v-if="enabled && hasPrevious"
class="nav-zone nav-zone-left"
:class="{ moveWithSidebar: moveWithSidebar }"
@mousemove="toggleNavigation"
@touchstart="(e) => { handleTouchStart(e); toggleNavigation(e); }"
@touchmove="handleTouchMove"
></div>
@@ -13,7 +12,6 @@
<div
v-if="enabled && hasNext"
class="nav-zone nav-zone-right"
@mousemove="toggleNavigation"
@touchstart="(e) => { handleTouchStart(e); toggleNavigation(e); }"
@touchmove="handleTouchMove"
></div>
@@ -213,6 +211,7 @@ export default {
window.addEventListener("touchmove", this.handleDrag, { passive: false });
window.addEventListener("touchend", this.endDrag);
document.addEventListener("click", this.handleDocumentClick);
window.addEventListener("mousemove", this.handleGlobalMouseMove);
// Calculate 10em threshold in pixels
const emSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
@@ -238,6 +237,7 @@ export default {
window.removeEventListener("touchmove", this.handleDrag);
window.removeEventListener("touchend", this.endDrag);
document.removeEventListener("click", this.handleDocumentClick);
window.removeEventListener("mousemove", this.handleGlobalMouseMove);
// Clear our local timeout
if (this.navigationTimeout) {
@@ -255,6 +255,48 @@ export default {
const shouldEnable = previewViews.includes(this.currentView);
mutations.setNavigationEnabled(shouldEnable);
},
async checkForUnsavedChanges() {
// Check if editor has unsaved changes
const editorDirty = state.editorDirty || false;
if (!editorDirty) {
return true; // No unsaved changes, allow navigation
}
// There are unsaved changes - show prompt
return new Promise((resolve) => {
mutations.showHover({
name: "SaveBeforeExit",
confirm: async () => {
// Save and proceed
try {
const saveHandler = state.editorSaveHandler;
if (saveHandler && typeof saveHandler === 'function') {
await saveHandler();
}
// Close the prompt after successful save
mutations.closeTopHover();
resolve(true); // Allow navigation
} catch (error) {
// Save failed - keep prompt open by not resolving
resolve(false); // Block navigation
}
},
discard: () => {
// Discard changes and proceed
mutations.setEditorDirty(false);
// Close the prompt
mutations.closeTopHover();
resolve(true); // Allow navigation
},
cancel: () => {
// Cancel navigation
// Close the prompt
mutations.closeTopHover();
resolve(false); // Block navigation
},
});
});
},
async setupNavigationForCurrentItem() {
if (!this.enabled || !state.req || state.req.type === 'directory') {
// Clear navigation when not applicable
@@ -332,9 +374,15 @@ export default {
}, 3000);
}
},
prev() {
async prev() {
if (this.hasPrevious) {
this.hoverNav = false;
// Check for unsaved changes in editor before navigating
if (!await this.checkForUnsavedChanges()) {
return; // Navigation blocked
}
// Set transitioning state - keeps old req visible until new one loads
// Editor and other components check isTransitioning to prevent saves
mutations.setNavigationTransitioning(true);
@@ -345,9 +393,14 @@ export default {
}
}
},
next() {
async next() {
if (this.hasNext) {
this.hoverNav = false;
// Check for unsaved changes in editor before navigating
if (!await this.checkForUnsavedChanges()) {
return; // Navigation blocked
}
// Set transitioning state - keeps old req visible until new one loads
// Editor and other components check isTransitioning to prevent saves
@@ -945,7 +998,37 @@ export default {
isDirectory: item.type === 'directory',
originalItem: item
}));
}
},
handleGlobalMouseMove(event) {
// Check if mouse is in the nav zone areas to show navigation buttons
if (!this.enabled) return;
const emSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
const zoneWidth = 5 * emSize; // 5em in pixels
// Check left zone
const sidebarOffset = this.moveWithSidebar ? 20 * emSize : 0; // Account for sidebar
if (this.hasPrevious && event.clientX >= sidebarOffset && event.clientX <= (sidebarOffset + zoneWidth)) {
const viewportHeight = window.innerHeight;
const zoneTop = viewportHeight * 0.25; // 25% from top
const zoneBottom = viewportHeight * 0.75; // 25% from bottom (75% of height)
if (event.clientY >= zoneTop && event.clientY <= zoneBottom) {
this.toggleNavigation();
}
}
// Check right zone
if (this.hasNext && event.clientX >= window.innerWidth - zoneWidth) {
const viewportHeight = window.innerHeight;
const zoneTop = viewportHeight * 0.25; // 25% from top
const zoneBottom = viewportHeight * 0.75; // 25% from bottom (75% of height)
if (event.clientY >= zoneTop && event.clientY <= zoneBottom) {
this.toggleNavigation();
}
}
},
},
};
</script>
@@ -957,8 +1040,8 @@ export default {
top: 25%; /* Start at 25% from top */
bottom: 25%; /* End at 25% from bottom (so middle 50%) */
width: 5em;
pointer-events: none; /* Allow clicks to pass through to content behind */
z-index: -1; /* Negative z-index to ensure content appears above */
pointer-events: none; /* Allow clicks and interactions to pass through */
z-index: -1; /* Behind content, only used for geometric detection */
background: transparent; /* Invisible zones for mouse/touch detection */
}
@@ -0,0 +1,67 @@
<template>
<div class="card-title">
<h2>{{ $t("prompts.closeWithActiveUploads") }}</h2>
</div>
<div class="card-content">
<p>{{ $t("prompts.closeWithActiveUploadsMessage") }}</p>
</div>
<div class="card-action">
<button
class="button button--flat button--grey"
@click="stayOnPrompt"
:aria-label="$t('buttons.stayOnPrompt')"
:title="$t('buttons.stayOnPrompt')">
{{ $t("buttons.stayOnPrompt") }}
</button>
<button
class="button button--flat button--red"
@click="closeAnyway"
:aria-label="$t('buttons.closeAnyway')"
:title="$t('buttons.closeAnyway')">
{{ $t("buttons.closeAnyway") }}
</button>
</div>
</template>
<script>
import { getters } from "@/store";
export default {
name: "closeWithActiveUploads",
computed: {
currentPrompt() {
return getters.currentPrompt();
},
},
created() {
// Listen for ESC key to treat it as "Stay on Prompt"
this.escKeyHandler = (event) => {
if (event.keyCode === 27 && getters.currentPromptName() === "CloseWithActiveUploads") {
event.stopImmediatePropagation();
this.stayOnPrompt();
}
};
window.addEventListener("keydown", this.escKeyHandler, true);
},
beforeUnmount() {
window.removeEventListener("keydown", this.escKeyHandler, true);
},
methods: {
stayOnPrompt() {
if (this.currentPrompt && this.currentPrompt.cancel) {
this.currentPrompt.cancel();
}
// Don't call closeTopHover here - the cancel callback handles it
},
closeAnyway() {
if (this.currentPrompt && this.currentPrompt.confirm) {
this.currentPrompt.confirm();
}
// Don't call closeTopHover here - the confirm callback handles it
},
},
};
</script>
@@ -0,0 +1,90 @@
<template>
<div class="card-title">
<h2>{{ title }}</h2>
</div>
<div class="card-content" v-html="body"></div>
<div class="card-action">
<button
v-for="(button, index) in displayButtons"
:key="index"
:class="getButtonClass(button)"
@click="handleButtonClick(button)"
:aria-label="button.label"
:title="button.label"
>
{{ button.label }}
</button>
</div>
</template>
<script>
import { mutations } from "@/store";
export default {
name: "generic-prompt",
props: {
title: {
type: String,
required: true,
},
body: {
type: String,
required: true,
},
buttons: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
displayButtons() {
// If buttons are provided, use them
if (this.buttons && this.buttons.length > 0) {
return this.buttons;
}
// Otherwise, return a default close button
return [
{
label: this.$t('buttons.close'),
action: () => {
// Just close the prompt
},
},
];
},
},
created() {
console.log("Generic component created");
},
methods: {
handleButtonClick(button) {
// Execute the button's action
if (typeof button.action === 'function') {
button.action();
}
// Close the prompt unless the button specifies to keep it open
if (button.keepOpen !== true) {
mutations.closeHovers();
}
},
getButtonClass(button) {
// Default button classes
let classes = 'button button--flat';
// Add custom class if provided
if (button.className) {
classes += ` ${button.className}`;
} else if (button.primary) {
// Primary button style (default)
// No additional class needed
} else {
// Secondary button style (grey)
classes += ' button--grey';
}
return classes;
},
},
};
</script>
@@ -35,6 +35,8 @@ import PlaybackQueue from "./PlaybackQueue.vue";
import FileList from "./FileList.vue";
import SaveBeforeExit from "./SaveBeforeExit.vue";
import CopyPasteConfirm from "./CopyPasteConfirm.vue";
import CloseWithActiveUploads from "./CloseWithActiveUploads.vue";
import Generic from "./Generic.vue";
import { state, getters, mutations } from "@/store"; // Import your custom store
export default {
@@ -66,6 +68,8 @@ export default {
"file-list": FileList,
SaveBeforeExit,
CopyPasteConfirm,
CloseWithActiveUploads,
generic: Generic,
},
data() {
return {
@@ -103,6 +107,9 @@ export default {
case "replace":
prompt.showConfirm(event);
break;
case "generic":
prompt.submit();
break;
case "CopyPasteConfirm":
prompt.confirm();
break;
+1 -6
View File
@@ -117,7 +117,6 @@
<input class="input" type="text" v-model.trim="allowedUsernames" :placeholder="$t('share.allowedUsernamesPlaceholder')" />
</div>
<ToggleSwitch v-if="shareType === 'normal' && onlyOfficeAvailable" class="item" v-model="enableOnlyOffice" :name="$t('share.enableOnlyOffice')" :description="$t('share.enableOnlyOfficeDescription')" />
<ToggleSwitch v-if="shareType === 'normal' && onlyOfficeAvailable" class="item" v-model="enableOnlyOfficeEditing" :name="$t('share.enableOnlyOfficeEditing')" :description="$t('share.enableOnlyOfficeEditingDescription')" />
<p>
{{ $t("share.enforceDarkLightMode") }}
<i
@@ -365,8 +364,7 @@ export default {
disableSidebar: false,
enforceDarkLightMode: "default",
viewMode: "normal",
enableOnlyOffice: false,
enableOnlyOfficeEditing: false,
enableOnlyOffice: false,
shareType: "normal",
extractEmbeddedSubtitles: false,
/** @type {Share | null} */
@@ -469,7 +467,6 @@ export default {
this.enforceDarkLightMode = this.link.enforceDarkLightMode || "default";
this.viewMode = this.link.viewMode || "normal";
this.enableOnlyOffice = this.link.enableOnlyOffice || false;
this.enableOnlyOfficeEditing = this.link.enableOnlyOfficeEditing || false;
this.shareType = this.link.shareType || "normal";
this.extractEmbeddedSubtitles = this.link.extractEmbeddedSubtitles || false;
//this.viewMode = this.link.viewMode || "normal";
@@ -562,7 +559,6 @@ export default {
enforceDarkLightMode: this.enforceDarkLightMode,
viewMode: this.viewMode,
enableOnlyOffice: this.enableOnlyOffice,
enableOnlyOfficeEditing: this.enableOnlyOfficeEditing,
shareType: this.shareType,
extractEmbeddedSubtitles: this.extractEmbeddedSubtitles,
};
@@ -638,7 +634,6 @@ export default {
this.enforceDarkLightMode = link.enforceDarkLightMode || "default";
this.viewMode = link.viewMode || "normal";
this.enableOnlyOffice = link.enableOnlyOffice || false;
this.enableOnlyOfficeEditing = link.enableOnlyOfficeEditing || false;
this.shareType = link.shareType || "normal";
this.extractEmbeddedSubtitles = link.extractEmbeddedSubtitles || false;
// Store the link being edited
+14 -5
View File
@@ -315,11 +315,7 @@ export default {
}
};
const isUploading = computed(() =>
files.value.some(
(file) => file.status === "pending" || file.status === "uploading"
)
);
const isUploading = computed(() => state.upload.isUploading);
watch(isUploading, (active) => {
if (active) {
@@ -359,6 +355,7 @@ export default {
);
const close = () => {
// closeHovers will automatically check isUploading and show warning if needed
mutations.closeHovers();
};
@@ -372,6 +369,16 @@ export default {
}
};
const handleBeforeUnload = (event) => {
// Warn user if they try to leave/refresh the page while uploads are active
if (isUploading.value) {
event.preventDefault();
// Chrome requires returnValue to be set
event.returnValue = '';
return '';
}
};
const processItems = async (items) => {
// When items are passed as a prop from ListingView, they can be either
// an array of DataTransferItem (from drag and drop) or an array of File (from input).
@@ -392,6 +399,7 @@ export default {
onMounted(async () => {
document.addEventListener("visibilitychange", handleVisibilityChange);
window.addEventListener("beforeunload", handleBeforeUnload);
uploadManager.setOnConflict(handleConflict);
if (props.initialItems) {
await processItems(props.initialItems);
@@ -400,6 +408,7 @@ export default {
onUnmounted(() => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
window.removeEventListener("beforeunload", handleBeforeUnload);
uploadManager.setOnConflict(() => {}); // cleanup
releaseWakeLock();
});
@@ -53,6 +53,7 @@
<i class="material-icons">ads_click</i>
</div>
<div
aria-label="Toggle Theme"
v-if="darkModeTogglePossible"
class="clickable"
:class="{ active: user?.darkMode }"
+2 -1
View File
@@ -5,10 +5,11 @@
--dark-red: #D32F2F;
--moon-grey: #f2f2f2;
--icon-red: #da4453;
--icon-orange: #f47750;
--icon-orange: #ff8c00;
--icon-yellow: #fdbc4b;
--icon-green: #2ecc71;
--icon-blue: #1d99f3;
--icon-violet: #9b59b6;
--primaryColor: var(--blue);
--divider: lightgray;
}
+759
View File
@@ -0,0 +1,759 @@
/* Halloween Theme Styles - Only apply when halloween-theme class is present */
/* Halloween-specific CSS variables */
.halloween-theme {
--primaryColor: var(--icon-orange) !important;
--halloween-dark-orange: #ff6600;
--halloween-black: #1a1a1a;
--halloween-gray: #cccccc;
}
body.halloween-theme,
.scroll-wrapper.halloween-theme,
#login.halloween-theme {
background: black !important;
background-color: black !important;
}
/* Tiled Halloween pattern background for scroll wrapper */
.scroll-wrapper.halloween-theme {
background-image: url("../assets/halloween-pattern.svg") !important;
background-repeat: repeat !important;
}
#login.halloween-theme {
position: relative;
overflow: hidden;
}
/* Halloween Background */
.halloween-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 0;
background: radial-gradient(circle at center, rgba(0, 0, 0, .5) 0%, #000000 70%);
}
/* Tombstone Shape for Login Card */
.login-card.tombstone {
position: relative;
/* Updated background for more subtle levels of detail/texture */
background:
/* Subtle rock grain/texture */
radial-gradient(ellipse at 50% 50%, rgba(0, 0, 0, 0.05) 0%, transparent 80%),
/* Light gray moss/ageing */
linear-gradient(170deg, rgba(168, 168, 168, 0.05) 5%, transparent 60%),
linear-gradient(135deg, rgba(0, 0, 0, 0.1) 0%, transparent 100%),
linear-gradient(225deg, rgba(0, 0, 0, 0.1) 0%, transparent 100%),
linear-gradient(180deg, #e8e8e8 0%, #c0c0c0 50%, #a8a8a8 100%) !important;
border: 4px solid #888 !important;
border-radius: 0 0 20px 20px !important;
box-shadow:
0 15px 50px rgba(255, 140, 0, 0.6),
inset 0 2px 20px rgba(0, 0, 0, 0.3),
inset 3px 3px 10px rgba(255, 255, 255, 0.2),
0 0 40px rgba(255, 140, 0, 0.4) !important;
padding: 2.5em 2em 2em 2em !important;
z-index: 10;
position: relative;
overflow: visible;
}
/* Tombstone rounded top */
.login-card.tombstone::before {
content: '';
position: absolute;
top: -70px;
left: -4px;
right: -4px;
height: 70px;
background:
linear-gradient(135deg, rgba(0, 0, 0, 0.1) 0%, transparent 100%),
linear-gradient(225deg, rgba(0, 0, 0, 0.1) 0%, transparent 100%),
linear-gradient(180deg, #e8e8e8 0%, #c0c0c0 100%);
border: 4px solid #888;
border-bottom: none;
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
box-shadow:
inset 0 2px 20px rgba(0, 0, 0, 0.3),
inset 3px 3px 10px rgba(255, 255, 255, 0.2);
}
/* R.I.P. text */
.login-card.tombstone::after {
content: 'R.I.P.';
position: absolute;
top: -55px;
left: 50%;
transform: translateX(-50%);
font-family: 'Times New Roman', serif;
font-size: 2.2em;
font-weight: bold;
color: #333;
text-shadow:
2px 2px 4px rgba(0, 0, 0, 0.5),
inset -1px -1px 2px rgba(255, 255, 255, 0.3);
letter-spacing: 0.3em;
z-index: 1;
font-style: italic;
}
/* Cracks in the tombstone - Increased detail */
.halloween-theme .login-card.tombstone {
background-image:
/* Fine, subtle cracks */
linear-gradient(45deg, transparent 30%, rgba(0, 0, 0, 0.15) 30.2%, transparent 30.5%),
linear-gradient(135deg, transparent 60%, rgba(0, 0, 0, 0.12) 60.3%, transparent 60.6%),
linear-gradient(75deg, transparent 45%, rgba(0, 0, 0, 0.1) 45.1%, transparent 45.2%),
linear-gradient(165deg, transparent 70%, rgba(0, 0, 0, 0.08) 70.4%, transparent 70.8%),
/* Deeper, more noticeable crack */
linear-gradient(-30deg, transparent 55%, rgba(0, 0, 0, 0.3) 55.5%, transparent 56.5%),
linear-gradient(150deg, transparent 35%, rgba(0, 0, 0, 0.35) 35.5%, transparent 36.5%),
/* Original background gradients */
linear-gradient(135deg, rgba(0, 0, 0, 0.1) 0%, transparent 100%),
linear-gradient(225deg, rgba(0, 0, 0, 0.1) 0%, transparent 100%),
linear-gradient(180deg, #e8e8e8 0%, #c0c0c0 50%, #a8a8a8 100%);
}
/* Tombstone Field Labels */
.tombstone-field {
position: relative;
margin-bottom: 1.8em;
}
.tombstone-label {
display: block;
font-family: 'Times New Roman', serif;
font-size: 1.1em;
font-weight: bold;
color: #444;
text-transform: uppercase;
letter-spacing: 0.25em;
margin-bottom: 0.6em;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
text-align: center;
font-style: italic;
}
.tombstone-field:first-of-type .tombstone-label::before {
content: '⚰ BORN ⚰';
display: block;
font-size: 0.9em;
}
.tombstone-field:last-of-type .tombstone-label::before {
content: '† DIED †';
display: block;
font-size: 0.9em;
}
.tombstone-field .input {
background: rgba(0, 0, 0, 0.7) !important;
border: 3px solid #ff8c00 !important;
color: #ff8c00 !important;
text-shadow: 0 0 8px rgba(255, 140, 0, 0.6);
font-weight: 500;
}
.tombstone-field .input:focus {
border-color: #ff6600 !important;
box-shadow: 0 0 20px rgba(255, 140, 0, 0.8) !important;
background: rgba(0, 0, 0, 0.8) !important;
}
.tombstone-field .input::placeholder {
color: #666 !important;
}
/* Floating Clouds - Twice as big and with light coming from them */
.cloud {
position: absolute;
background: rgba(40, 40, 40, 0.7);
border-radius: 400px;
opacity: 0.9;
animation: float-cloud linear infinite;
filter: blur(5px);
box-shadow:
0 0 10px rgba(255, 140, 0, 0.2),
0 0 60px rgba(255, 255, 255, 0.3),
0 0 30px rgba(40, 40, 40, 0.6);
transform: scale(2);
}
.cloud::before,
.cloud::after {
content: '';
position: absolute;
background: rgba(40, 40, 40, 0.7);
border-radius: 400px;
filter: blur(4px);
}
/* --- CLOUD START POSITIONS ADJUSTED FOR RANDOMNESS --- */
.cloud-1 {
width: 250px;
height: 80px;
/* Adjusted top and left for scatter */
top: 15%;
left: 40%;
/* Starts visible on the screen */
animation-duration: 80s;
}
.cloud-1::before {
width: 120px;
height: 100px;
top: -50px;
left: 40px;
}
.cloud-1::after {
width: 140px;
height: 90px;
top: -40px;
right: 30px;
}
.cloud-2 {
width: 300px;
height: 100px;
/* Adjusted top and left for scatter */
top: 50%;
left: -150px;
/* Starts slightly off-screen to the left */
animation-duration: 100s;
animation-delay: 20s;
}
.cloud-2::before {
width: 140px;
height: 120px;
top: -60px;
left: 50px;
}
.cloud-2::after {
width: 160px;
height: 110px;
top: -50px;
right: 40px;
}
.cloud-3 {
width: 200px;
height: 70px;
/* Adjusted top and left for scatter */
top: 75%;
left: 80%;
/* Starts visible on the screen far to the right */
animation-duration: 70s;
animation-delay: 40s;
}
.cloud-3::before {
width: 100px;
height: 80px;
top: -40px;
left: 30px;
}
.cloud-3::after {
width: 110px;
height: 76px;
top: -36px;
right: 25px;
}
@keyframes float-cloud {
0% {
transform: translateX(0) scale(2);
}
100% {
/* Kept the final position far to the right so they all eventually move off-screen */
transform: translateX(calc(100vw + 400px)) scale(2);
}
}
/* Lightning Flash Effect - Adjusted to match cloud light */
.lightning-flash {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* Made light more focused, simulating it coming from above/clouds */
background: radial-gradient(circle at 50% 10%, rgba(255, 255, 255, 0.95), rgba(255, 220, 100, 0.8) 15%, transparent 50%);
opacity: 0;
animation: lightning-flash-animation 12s infinite;
pointer-events: none;
}
@keyframes lightning-flash-animation {
0%, 10%, 100% {
opacity: 0;
}
10.1% {
opacity: 0.9;
}
10.15% {
opacity: 0.4;
}
10.2%, 10.35% {
opacity: 0;
}
10.4% {
opacity: 1;
}
10.5% {
opacity: 0;
}
}
/* Lightning Bolts */
.lightning-bolt {
position: absolute;
width: 50px;
height: 250px;
opacity: 0;
filter: drop-shadow(0 0 15px #fff) drop-shadow(0 0 30px #ffd700) drop-shadow(0 0 45px #ff8c00);
z-index: 100;
}
.lightning-1 {
top: 8%;
left: 30%;
animation: lightning-strike 12s infinite;
}
.lightning-2 {
top: 10%;
right: 25%;
animation: lightning-strike 12s infinite;
animation-delay: 0.05s;
}
@keyframes lightning-strike {
0%, 10%, 100% {
opacity: 0;
transform: scaleY(0);
transform-origin: top;
}
10.1% {
opacity: 1;
transform: scaleY(1);
}
10.15% {
opacity: 0.6;
}
10.2%, 10.35% {
opacity: 0;
transform: scaleY(0.8);
}
10.4% {
opacity: 1;
transform: scaleY(1);
}
10.5% {
opacity: 0;
transform: scaleY(0);
}
}
.lightning-bolt path {
fill: #fff;
animation: bolt-flicker 12s infinite;
}
@keyframes bolt-flicker {
0%, 10%, 100% {
opacity: 0;
}
10.1% {
opacity: 1;
fill: #fff;
}
10.15% {
opacity: 0.7;
fill: #ffd700;
}
10.2%, 10.35% {
opacity: 0;
}
10.4% {
opacity: 1;
fill: #fff;
}
10.5% {
opacity: 0;
}
}
/* Halloween Decorations Container */
.halloween-decorations {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
z-index: 5;
}
/* Spooky Cat */
.halloween-cat {
position: absolute;
bottom: 5%;
left: 8%;
width: 180px;
height: 200px;
filter: drop-shadow(0 0 20px rgba(255, 140, 0, 0.5));
animation: cat-tail-swish 3s ease-in-out infinite;
z-index: 6;
}
@keyframes cat-tail-swish {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
.halloween-cat .cat-eye {
animation: cat-blink 5s infinite;
}
@keyframes cat-blink {
0%, 96%, 100% {
opacity: 1;
}
97%, 99% {
opacity: 0.1;
}
}
/* Skeleton */
.halloween-skeleton {
position: absolute;
bottom: 3%;
right: 8%;
width: 160px;
height: 240px;
filter: drop-shadow(0 0 20px rgba(255, 140, 0, 0.4));
animation: skeleton-wobble 3s ease-in-out infinite;
z-index: 6;
}
@keyframes skeleton-wobble {
0%, 100% {
transform: translateY(0) rotate(-5deg);
}
50% {
transform: translateY(-15px) rotate(5deg);
}
}
/* Skeleton head bob */
.halloween-skeleton .skeleton-head {
animation: head-bob 2s ease-in-out infinite;
transform-origin: center bottom;
}
@keyframes head-bob {
0%, 100% {
transform: rotate(-5deg);
}
50% {
transform: rotate(5deg);
}
}
/* Additional Effects for Login Card in Halloween Theme */
.halloween-theme .login-card.tombstone .button {
background: linear-gradient(135deg, #1a1a1a 0%, #000000 100%) !important;
border: 3px solid #ff8c00 !important;
color: #ff8c00 !important;
font-weight: bold;
text-shadow: 0 0 10px rgba(255, 140, 0, 0.8);
box-shadow: 0 4px 20px rgba(255, 140, 0, 0.5) !important;
transition: all 0.3s ease;
}
.halloween-theme .login-card.tombstone .button:hover {
background: linear-gradient(135deg, #ff8c00 0%, #ff6600 100%) !important;
border-color: #ffa500 !important;
color: #000 !important;
text-shadow: 0 0 15px rgba(255, 255, 255, 0.9);
box-shadow: 0 6px 30px rgba(255, 140, 0, 0.8) !important;
transform: translateY(-2px);
}
/* Brand Icon Glow */
.halloween-theme .login-brand i {
color: #ff8c00 !important;
filter: drop-shadow(0 0 20px rgba(255, 140, 0, 0.8));
animation: icon-pulse 3s ease-in-out infinite;
}
@keyframes icon-pulse {
0%, 100% {
filter: drop-shadow(0 0 20px rgba(255, 140, 0, 0.8));
}
50% {
filter: drop-shadow(0 0 35px rgba(255, 69, 0, 1));
}
}
/* Brand Text */
.halloween-theme .brand-text h3 {
color: #ff8c00 !important;
text-shadow:
2px 2px 4px rgba(0, 0, 0, 0.8),
0 0 15px rgba(255, 140, 0, 0.6);
font-family: 'Courier New', monospace;
letter-spacing: 0.1em;
}
/* Error Message */
.halloween-theme .wrong-login {
background: rgba(0, 0, 0, 0.8) !important;
border: 2px solid #ff8c00 !important;
color: #ff8c00 !important;
box-shadow: 0 0 20px rgba(255, 140, 0, 0.5);
}
/* Loading Spinner */
.halloween-theme .loading-spinner i {
color: #ff8c00 !important;
filter: drop-shadow(0 0 10px rgba(255, 140, 0, 0.8));
}
/* Mobile Responsiveness */
@media (max-width: 768px) {
.halloween-cat {
width: 80px;
height: 80px;
bottom: 5%;
left: 5%;
}
.halloween-skeleton {
width: 100px;
height: 150px;
bottom: 3%;
right: 5%;
}
.login-card.tombstone::after {
font-size: 1.4em;
top: -40px;
}
}
@media (max-width: 480px) {
.halloween-cat {
width: 60px;
height: 60px;
}
.halloween-skeleton {
width: 70px;
height: 110px;
}
.cloud {
/* Adjusted scale for mobile to compensate for the overall increase */
transform: scale(1.4);
}
@keyframes float-cloud {
0% {
transform: translateX(0) scale(1.4);
}
100% {
transform: translateX(calc(100vw + 400px)) scale(1.4);
}
}
}
#search-input {
/* ... existing search-input styles ... */
background-color: rgba(100, 100, 100, 0.2);
display: flex;
height: 100%;
padding: 0em 0.75em;
border-style: solid;
border-radius: 1em;
border-style: unset;
border-width: 1px;
align-items: center;
height: 3em;
position: relative;
overflow: hidden;
}
/* --- HALLOWEEN EYES STYLES --- */
.halloween-eyes {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
}
/* Position the input field above the eyes */
.main-input.halloween-theme {
position: relative;
z-index: 3 !important;
background-color: transparent !important;
}
#halloween-eyes {
position: fixed;
width: 100%;
height: 30%;
display: flex;
justify-content: center;
align-items: center;
gap: 1em;
pointer-events: none;
z-index: 0;
box-sizing: border-box;
}
.eye {
width: 45%;
height: 80%;
background: var(--icon-orange);
border-radius: 50% / 60% 60% 40% 40%;
position: relative;
overflow: hidden; /* Ensure eyelids don't overflow when moving */
box-shadow:
0 0 10px var(--icon-orange),
0 0 30px rgba(255, 255, 0, 0.8),
inset 0 0 10px rgba(0,0,0,0.3);
/* Removed 'eye-blink' from here, will use pseudo-elements for blink */
animation:
eye-look 4s infinite alternate ease-in-out;
}
/* NEW: Eyelids using pseudo-elements */
.eye::before,
.eye::after {
content: '';
position: absolute;
left: 0;
width: 100%;
height: 50%; /* Half the eye height for each lid */
background: #1a1a1a; /* Black for the eyelid */
z-index: 1; /* Above the pupil, below the main input */
/* Cat-eye shape for eyelids */
border-radius: 50% / 60% 60% 40% 40%;
transform-origin: center center;
animation: lid-blink 5s infinite; /* Eyelid blink animation */
}
.eye::before { /* Top eyelid */
top: 0;
/* Mask out the bottom half of the eyelid to blend */
clip-path: inset(0 0 50% 0);
transform: translateY(-100%) scaleY(1.2); /* Start fully open, slightly above eye */
/* Ensure the rounded top of the lid is visible */
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.eye::after { /* Bottom eyelid */
bottom: 0;
/* Mask out the top half of the eyelid to blend */
clip-path: inset(50% 0 0 0);
transform: translateY(100%) scaleY(1.2); /* Start fully open, slightly below eye */
/* Ensure the rounded bottom of the lid is visible */
border-top-left-radius: 0;
border-top-right-radius: 0;
}
/* Adjust animation delay for the right eye to be slightly desynced */
.eye.right {
animation-delay: 0.1s;
}
.eye.right .pupil {
animation-delay: 0.3s;
}
.eye.right::before, .eye.right::after {
animation-delay: 0.2s; /* Slightly delay eyelid blink */
}
.pupil {
width: 10%;
height: 70%;
background: #1a1a1a;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: pupil-look 4s infinite alternate ease-in-out;
}
/* Adjust pupil movement for cat eyes */
@keyframes pupil-look {
0% { transform: translate(-100%, -50%); }
50% { transform: translate(0%, -50%); }
75% { transform: translate(-50%, -50%); }
100% { transform: translate(-100%, -50%); }
}
/* NEW: Eyelid blinking animation */
@keyframes lid-blink-top {
0%, 20%, 40%, 60%, 80%, 100% {
/* Top lid is up and open */
transform: translateY(-100%) scaleY(1.2);
}
18%, 22%, 38%, 42%, 58%, 62%, 78%, 82% {
/* Top lid moves down to cover the eye */
transform: translateY(0%) scaleY(1.2);
}
}
@keyframes lid-blink-bottom {
0%, 20%, 40%, 60%, 80%, 100% {
/* Bottom lid is down and open */
transform: translateY(100%) scaleY(1.2);
}
18%, 22%, 38%, 42%, 58%, 62%, 78%, 82% {
/* Bottom lid moves up to cover the eye */
transform: translateY(0%) scaleY(1.2);
}
}
+4 -1
View File
@@ -42,6 +42,9 @@ body.rtl #listingView {
width: var(--item-width);
height: var(--item-height);
border-radius: 1em;
background: var(--item-secondary);
color: var(--textPrimary);
border-color: var(--divider);
}
#listingView .item div:last-of-type {
@@ -445,4 +448,4 @@ body.rtl #listingView {
.half-selected {
border-color: var(--primaryColor) !important;
}
}
+1
View File
@@ -9,6 +9,7 @@
@import "./dashboard.css";
@import "./login.css";
@import "./mobile.css";
@import "./halloween.css";
.link {
color: var(--blue);
+12 -4
View File
@@ -61,7 +61,11 @@
"selectAll": "اختر الكل",
"discardAndExit": "التخلص والخروج",
"saveAndExit": "الحفظ والخروج",
"keepEditing": "استمر في التحرير"
"keepEditing": "استمر في التحرير",
"stayOnPrompt": "ابق في مكانك",
"closeAnyway": "إغلاق على أي حال",
"disable": "تعطيل",
"confirm": "تأكيد"
},
"download": {
"downloadFile": "Download File",
@@ -220,7 +224,9 @@
"copyItemQuestion": "نسخ هذا العنصر إلى الموقع الجديد؟",
"moveItemQuestion": "نقل هذا العنصر إلى الموقع الجديد؟",
"sourcePath": "المصدر:",
"destinationPath": "الوجهة"
"destinationPath": "الوجهة",
"closeWithActiveUploadsMessage": "تحذير: لديك تحميلات نشطة وقد يؤدي إغلاق مطالبة التحميل إلى تحميلات غير مكتملة. ماذا تريد أن تفعل؟",
"closeWithActiveUploads": "التحميلات النشطة"
},
"search": {
"images": "الصور",
@@ -430,8 +436,6 @@
"light": "خفيف",
"enableOnlyOffice": "تمكين عارض OnlyOffice فقط",
"enableOnlyOfficeDescription": "السماح بعرض ملفات المكتب باستخدام OnlyOffice في هذه المشاركة.",
"enableOnlyOfficeEditing": "تمكين التحرير في المكتب فقط",
"enableOnlyOfficeEditingDescription": "السماح بتحرير الملفات المكتبية باستخدام OnlyOffice في هذه الحصة.",
"titleDefault": "الملفات المشتركة - {title}",
"defaultViewMode": "وضع العرض الافتراضي",
"defaultViewModeDescription": "تعيين وضع العرض الافتراضي للمستخدمين المجهولين الذين يشاهدون هذه المشاركة. لا يزال بإمكان المستخدمين المسجلين الدخول تغيير تفضيلات العرض الخاصة بهم.",
@@ -639,5 +643,9 @@
"changePlaybackMode": "تغيير وضع التشغيل",
"changePlaybackModeHint": "غير وضع التشغيل لرؤية المزيد من الملفات",
"emptyQueue": "قائمة التشغيل فارغة"
},
"halloween": {
"title": "عيد هالوين سعيد!",
"body": "تم تطبيق سمة الهالوين. يمكنك تعطيله إذا لم يعجبك، أو تعطيله بشكل دائم في إعدادات الملف الشخصي عن طريق تحديد أي سمة أخرى."
}
}
+12 -4
View File
@@ -61,7 +61,11 @@
"selectAll": "Vyberte vše",
"discardAndExit": "Vyřazení a ukončení",
"saveAndExit": "Uložit a ukončit",
"keepEditing": "Pokračovat v úpravách"
"keepEditing": "Pokračovat v úpravách",
"stayOnPrompt": "Zůstaňte na",
"closeAnyway": "Stejně zavřít",
"disable": "Zakázat",
"confirm": "Potvrďte"
},
"download": {
"downloadFile": "Stáhnout soubor",
@@ -195,8 +199,6 @@
"light": "Světlo",
"enableOnlyOffice": "Povolení prohlížeče OnlyOffice",
"enableOnlyOfficeDescription": "Povolit prohlížení kancelářských souborů pomocí OnlyOffice v této sdílené složce.",
"enableOnlyOfficeEditing": "Povolení úprav OnlyOffice",
"enableOnlyOfficeEditingDescription": "Povolit úpravy kancelářských souborů pomocí OnlyOffice v této sdílené složce.",
"titleDefault": "Sdílené soubory - {title}",
"defaultViewMode": "Výchozí režim zobrazení",
"defaultViewModeDescription": "Nastavení výchozího režimu zobrazení pro anonymní uživatele prohlížející tuto sdílenou složku. Přihlášení uživatelé mohou stále měnit předvolby zobrazení.",
@@ -288,7 +290,9 @@
"copyItemQuestion": "Zkopírovat tuto položku do nového umístění?",
"moveItemQuestion": "Přesunout tuto položku na nové místo?",
"sourcePath": "Zdroj:",
"destinationPath": "Cílová destinace:"
"destinationPath": "Cílová destinace:",
"closeWithActiveUploadsMessage": "Varování: Upozornění: Máte aktivní nahrávání a zavření výzvy k nahrávání by mohlo vést k nedokončenému nahrávání. Co byste chtěli udělat?",
"closeWithActiveUploads": "Aktivní nahrávání"
},
"search": {
"images": "Obrázky",
@@ -639,5 +643,9 @@
"changePlaybackMode": "Změnit režim přehrávání",
"changePlaybackModeHint": "Změňte režim přehrávání, abyste viděli více souborů",
"emptyQueue": "Fronta je prázdná"
},
"halloween": {
"title": "Šťastný Halloween!",
"body": "Bylo použito téma Halloween. Pokud se vám nelíbí, můžete jej vypnout nebo trvale zakázat v nastavení profilu výběrem jiného motivu."
}
}
+38 -30
View File
@@ -61,7 +61,11 @@
"selectAll": "Alle auswählen",
"discardAndExit": "Verwerfen und Beenden",
"saveAndExit": "Speichern und Beenden",
"keepEditing": "Weiter bearbeiten"
"keepEditing": "Weiter bearbeiten",
"stayOnPrompt": "Bleiben Sie",
"closeAnyway": "Jedenfalls schließen",
"disable": "Deaktivieren Sie",
"confirm": "Bestätigen Sie"
},
"download": {
"downloadFile": "Datei herunterladen",
@@ -109,9 +113,9 @@
"f2": "Datei umbenennen",
"help": "Hilfe",
"f1": "Hilfeprompt anzeigen",
"description": "Nachstehend finden Sie die grundlegenden Navigationsoptionen. Für zusätzliche Informationen besuchen Sie bitte",
"description": "Nachstehend finden Sie die grundlegenden Navigationsoptionen. Für zusätzliche Informationen besuchen Sie bitte das",
"slash": "Suche aktivieren",
"wiki": "FileBrowser Quantum Offizielle Dokumente"
"wiki": "FileBrowser Quantum-Wiki"
},
"languages": {
"he": "עברית",
@@ -220,7 +224,9 @@
"copyItemQuestion": "Dieses Element an den neuen Ort kopieren?",
"moveItemQuestion": "Diesen Artikel an den neuen Ort verschieben?",
"sourcePath": "Quelle:",
"destinationPath": "Zielort:"
"destinationPath": "Zielort:",
"closeWithActiveUploadsMessage": "Warnung: Sie haben aktive Uploads und das Schließen der Upload-Aufforderung könnte zu unvollendeten Uploads führen. Was würden Sie gerne tun?",
"closeWithActiveUploads": "Aktive Uploads"
},
"search": {
"images": "Bilder",
@@ -300,7 +306,7 @@
},
"permissions-name": "Berechtigungen",
"permissionsHelp": "Sie können einem Benutzer Administratorrechte einräumen oder die Berechtigunen individuell festlegen. Wenn Sie \"Administrator\" auswählen, werden alle anderen Rechte automatisch vergeben. Die Nutzerverwaltung kann nur durch einen Administrator erfolgen.\n",
"profileSettings": "Profileinstellungen",
"profileSettings": "Darstellung",
"ruleExample1": "Verhindert den Zugang zu versteckten Dateien (dot-Files, wie .git, .gitignore) in allen Ordnern\n",
"ruleExample2": "Blockiert den Zugang auf Dateien mit dem Namen Caddyfile in der Wurzel/Basis des Scopes.",
"rules": "Regeln",
@@ -328,7 +334,7 @@
"modifyOtherUser": "Benutzer ändern:",
"modifyCurrentUser": "Aktuellen Benutzer ändern",
"themeColor": "Designfarbe",
"listingOptions": "Auflistung der Optionen",
"listingOptions": "Listenansicht",
"emptyGlobal": "Derzeit gibt es keine globalen Konfigurationsoptionen. Sie können die Konfigurations-Yaml bearbeiten, um die Systemeinstellungen anzupassen.",
"scopes": "Definition der zulässigen Quellen und des Benutzerbereichs innerhalb dieser Quellen",
"UserManagement": "Benutzerverwaltung",
@@ -342,7 +348,7 @@
"enterPasswordAgain": "Geben Sie das Passwort erneut ein",
"passwordsDoNotMatch": "Passwörter stimmen nicht überein",
"loginMethod": "Login-Methode",
"shareSettings": "Einstellungen teilen",
"shareSettings": "Dateifreigaben",
"adminOptions": "Optionen für Verwaltungsbenutzer",
"shareDuration": "Abgelaufen",
"searchOptions": "Suchoptionen",
@@ -351,16 +357,16 @@
"configViewer": "Config Viewer",
"configViewerShowFull": "Vollständige Konfiguration anzeigen",
"configViewerShowComments": "Kommentare anzeigen",
"configViewerLoadConfig": "Lastkonfiguration",
"configViewerLoadConfig": "Konfiguration laden",
"brandingHelp": "Sie können das Aussehen Ihrer Dateibrowser-Instanz anpassen, indem Sie den Namen ändern, das Logo ersetzen, eigene Stile hinzufügen und sogar externe Links zu GitHub deaktivieren.\nWeitere Informationen zum benutzerdefinierten Branding finden Sie unter {0}.",
"commandRunnerHelp": "Hier können Sie Befehle eingeben, die bei den genannten Ereignissen ausgeführt werden. Sie müssen einen pro Zeile schreiben. Die Umgebungsvariablen {0} und {1} werden verfügbar sein, wobei {0} relativ zu {1} ist. Weitere Informationen über diese Funktion und die verfügbaren Umgebungsvariablen finden Sie unter {2}.",
"loginMethods": {
"password": "Passwort",
"oidc": "OIDC",
"proxy": "Vollmacht"
"proxy": "Proxy"
},
"newUserHintSubFolder": "z.B. '/subfolder' - leer für Standard",
"userScopes": "Geltungsbereiche"
"userScopes": "Zugangsbereiche"
},
"sidebar": {
"help": "Hilfe",
@@ -423,15 +429,13 @@
"disableShareCardDescription": "Deaktivieren Sie die Freigabekarte auf der freigegebenen Seite in der Seitenleiste oder oben auf der Seite auf dem Handy.",
"disableSidebar": "Seitenleiste deaktivieren",
"disableSidebarDescription": "Deaktivieren Sie die Seitenleiste auf der gemeinsamen Seite.",
"enforceDarkLightMode": "Themenmodus erzwingen",
"enforceDarkLightModeDescription": "Erzwingt einen bestimmten Themenmodus (dunkel oder hell) für diese Freigabe und setzt die Benutzereinstellungen außer Kraft.",
"enforceDarkLightMode": "Erscheinungsbild erzwingen",
"enforceDarkLightModeDescription": "Erzwingt ein bestimmtes Erscheinungsbild (dunkel oder hell) für diese Freigabe und setzt die Benutzereinstellungen außer Kraft.",
"default": "Keinen Modus erzwingen",
"dark": "Dunkelheit",
"light": "Licht",
"dark": "Dunkel",
"light": "Hell",
"enableOnlyOffice": "OnlyOffice viewer aktivieren",
"enableOnlyOfficeDescription": "Erlauben Sie die Anzeige von Office-Dateien mit OnlyOffice in dieser Freigabe.",
"enableOnlyOfficeEditing": "OnlyOffice-Bearbeitung aktivieren",
"enableOnlyOfficeEditingDescription": "Erlauben Sie die Bearbeitung von Office-Dateien mit OnlyOffice in dieser Freigabe.",
"titleDefault": "Gemeinsame Dateien - {title}",
"defaultViewMode": "Standard-Ansichtsmodus",
"defaultViewModeDescription": "Legen Sie den Standardanzeigemodus für anonyme Benutzer fest, die diese Freigabe anzeigen. Angemeldete Benutzer können ihre Ansichtseinstellungen immer noch ändern.",
@@ -457,7 +461,7 @@
"allowCreateDescription": "Jeder kann neue Dateien und Ordner in dieser Freigabe erstellen und hochladen."
},
"api": {
"title": "API-Schlüssel:",
"title": "API-Schlüssel",
"keyName": "API-Schlüsselname:",
"createdAt": "Erstellt am:",
"expiresAt": "Läuft ab am:",
@@ -486,7 +490,7 @@
"lastScanned": "Zuletzt gescannt",
"quickScan": "Schnell-Scan (Sekunden)",
"fullScan": "Vollständiger Scan (Sekunden)",
"settingsHover": "Einstellungen für Benutzer",
"settingsHover": "Einstellungen",
"logout": "Abmelden",
"toggleClick": "Einzelklick umschalten",
"toggleDark": "Dunkelmodus umschalten",
@@ -527,7 +531,7 @@
"deniedGroups": "Abgelehnte Gruppen",
"allowedUsers": "Erlaubte Benutzer",
"allowedGroups": "Erlaubte Gruppen",
"allow": "Erlauben Sie",
"allow": "Zulassen",
"allowDeny": "Zulassen/Verweigern",
"userGroup": "Benutzer/Gruppe",
"enterName": "Benutzer- oder Gruppenname eingeben",
@@ -560,7 +564,7 @@
"orange": "orange"
},
"profileSettings": {
"setDateFormat": "Genaues Datumsformat einstellen",
"setDateFormat": "Genaues Datum statt relativer Zeitangaben verwenden",
"showHiddenFiles": "Versteckte Dateien anzeigen",
"showHiddenFilesDescription": "Versteckte Dateien in der Auflistung anzeigen (z.B. .thumbnails, ProgramData), aber sie sind immer noch manuell zugänglich, wenn sie für die Indizierung konfiguriert sind.",
"showQuickDownload": "Download-Symbol für schnellen Zugriff anzeigen",
@@ -569,9 +573,9 @@
"previewImagesDescription": "Thumbnail-Vorschau für Bilder anzeigen, bei Deaktivierung wird ein allgemeines Symbol angezeigt.",
"previewVideos": "Miniaturansichten für Videos anzeigen",
"previewVideosDescription": "Thumbnail-Vorschau für Videos anzeigen, bei Deaktivierung wird ein allgemeines Symbol angezeigt.",
"previewMotionVideos": "Aktivieren der Bewegungsvorschau für Videos",
"previewMotionVideosDescription": "Bei der Bewegungsvorschau handelt es sich um eine animierte Popup-Vorschau, die sich durch die Einzelbilder des Videos bewegt.",
"highQualityPreview": "Hochwertige Miniaturansichten",
"previewMotionVideos": "Live-Vorschau für Videos aktivieren",
"previewMotionVideosDescription": "Bei der Live-Vorschau handelt es sich um eine animierte Popup-Vorschau, die sich durch die Einzelbilder des Videos bewegt.",
"highQualityPreview": "Hochauflösende Miniaturansichten",
"highQualityPreviewDescription": "Betrifft nur die Galerieansicht. Bei einer langsamen Verbindung kann das Laden länger dauern.",
"editOffice": "Anzeigen und Bearbeiten von Office-Dateien mit OnlyOffice",
"editOfficeDescription": "Wenn nur Office aktiviert ist, wird der Dateibetrachter/-editor für alle Dateierweiterungen deaktiviert, die mit Office-Dateien zusammenhängen.",
@@ -584,13 +588,13 @@
"disableHideSidebarDescription": "Wenn diese Option aktiviert ist, wird die Seitenleiste nicht automatisch ausgeblendet, wenn Dateien in der Vorschau angezeigt oder bearbeitet werden.",
"autoplayMedia": "Autoplay von Mediendateien bei der Anzeige",
"autoplayMediaDescription": "Autoplay von Mediendateien bei der Anzeige. Dies funktioniert nur, wenn die Datei eine Mediendatei ist.",
"sidebarOptions": "Optionen für die Seitenleiste",
"disableQuickToggles": "Quick Toggles deaktivieren",
"disableQuickTogglesDescription": "Deaktivieren Sie die Quick-Toggles, die in der Seitenleiste angezeigt werden.",
"sidebarOptions": "Seitenleiste",
"disableQuickToggles": "Schnelleinstellungen ausblenden",
"disableQuickTogglesDescription": "Deaktivieren Sie die Schnelleinstellungen, die in der Seitenleiste angezeigt werden.",
"disableSearchOptions": "Suchoptionen deaktivieren",
"disableSearchOptionsDescription": "Deaktivieren Sie die Suchoptionen, die in der Seitenleiste angezeigt werden.",
"hideSidebarFileActions": "Schaltfläche \"Dateiaktionen\" in der Seitenleiste ausblenden",
"deleteWithoutConfirming": "Dateien ohne Sicherheitsabfrage löschen",
"deleteWithoutConfirming": "Dateien ohne Rückfrage löschen",
"deleteWithoutConfirmingDescription": "Dateien ohne Sicherheitsabfrage löschen. Dies funktioniert nur, wenn die Datei eine Mediendatei ist.",
"editorViewerOptions": "Editor- und Viewer-Optionen",
"editorQuickSave": "Schaltfläche zum schnellen Speichern im Editor",
@@ -606,15 +610,15 @@
"disableOfficeEditorDescription": "Eine durch Leerzeichen getrennte Liste von Dateierweiterungen, für die der Editor und der Viewer von OnlyOffice deaktiviert werden sollen. (z. B. '.txt .html .pdf').",
"disableOfficeEditor": "Office Viewer für bestimmte Dateierweiterungen deaktivieren",
"defaultThemeDescription": "default - Das Standardthema",
"customTheme": "Wählen Sie Ihr Thema",
"customTheme": "Design",
"showSelectMultiple": "Mehrfachauswahl im Kontextmenü auf dem Desktop anzeigen",
"showSelectMultipleDescription": "Standardmäßig verfügt das Desktop-Kontextmenü nicht über die Option \"Mehrere auswählen\" wie bei mobilen Geräten. Diese Option wird immer im Kontextmenü angezeigt.",
"defaultMediaPlayer": "Verwenden Sie den nativen Media Player Ihres Browsers",
"defaultMediaPlayerDescription": "Verwenden Sie für Video- und Audiodateien den nativen Mediaplayer Ihres Browsers und nicht den in FileBrowser enthaltenen 'vue-plyr' Mediaplayer.",
"debugOfficeEditor": "OnlyOffice-Debug-Modus einschalten",
"debugOfficeEditorDescription": "Ein Debug-Popup mit zusätzlichen Informationen im OnlyOffice-Editor anzeigen.",
"fileViewerOptions": "Optionen des Dateibetrachters",
"themeAndLanguage": "Thema und Sprache",
"fileViewerOptions": "Dateibetrachter",
"themeAndLanguage": "Design und Sprache",
"previewFolder": "Miniaturansichten für Ordner anzeigen, falls vorhanden",
"previewFolderDescription": "Zeigen Sie Miniaturvorschauen für Ordner an, deren erstes untergeordnetes Element ein Miniaturbild als Cover hat.",
"disableFileExtensions": "Zu deaktivierende Dateierweiterungen eingeben",
@@ -639,5 +643,9 @@
"changePlaybackMode": "Wiedergabemodus ändern",
"changePlaybackModeHint": "Ändern Sie den Wiedergabemodus, um mehr Dateien zu sehen",
"emptyQueue": "Warteschlange ist leer"
},
"halloween": {
"title": "Fröhliches Halloween!",
"body": "Das Halloween-Thema wurde angewendet. Sie können es deaktivieren, wenn es Ihnen nicht gefällt, oder es in den Profileinstellungen dauerhaft deaktivieren, indem Sie ein anderes Thema auswählen."
}
}
+12 -4
View File
@@ -61,7 +61,11 @@
"selectAll": "Επιλέξτε όλα",
"discardAndExit": "Απόρριψη και έξοδος",
"saveAndExit": "Αποθήκευση και έξοδος",
"keepEditing": "Συνεχίστε την επεξεργασία"
"keepEditing": "Συνεχίστε την επεξεργασία",
"stayOnPrompt": "Μείνετε",
"closeAnyway": "Κλείστε Ούτως ή άλλως",
"disable": "Απενεργοποίηση",
"confirm": "Επιβεβαίωση"
},
"download": {
"downloadFile": "Λήψη αρχείου",
@@ -223,7 +227,9 @@
"copyItemQuestion": "Αντιγράψτε αυτό το στοιχείο στη νέα θέση;",
"moveItemQuestion": "Μετακινήστε αυτό το στοιχείο στη νέα θέση;",
"sourcePath": "Πηγή:",
"destinationPath": "Προορισμός:"
"destinationPath": "Προορισμός:",
"closeWithActiveUploadsMessage": "Προειδοποίηση: Το κλείσιμο της προτροπής μεταφόρτωσης μπορεί να οδηγήσει σε ημιτελείς μεταφορτώσεις. Τι θα θέλατε να κάνετε;",
"closeWithActiveUploads": "Ενεργές μεταφορτώσεις"
},
"search": {
"images": "Εικόνες",
@@ -430,8 +436,6 @@
"light": "Φως",
"enableOnlyOffice": "Ενεργοποίηση της προβολής OnlyOffice",
"enableOnlyOfficeDescription": "Επιτρέψτε την προβολή αρχείων γραφείου χρησιμοποιώντας το OnlyOffice σε αυτό το κοινόχρηστο χώρο.",
"enableOnlyOfficeEditing": "Ενεργοποίηση της επεξεργασίας OnlyOffice",
"enableOnlyOfficeEditingDescription": "Επιτρέψτε την επεξεργασία αρχείων γραφείου χρησιμοποιώντας το OnlyOffice σε αυτό το κοινόχρηστο χώρο.",
"titleDefault": "Κοινόχρηστα αρχεία - {title}",
"defaultViewMode": "Προεπιλεγμένη λειτουργία προβολής",
"defaultViewModeDescription": "Ορίστε την προεπιλεγμένη λειτουργία προβολής για τους ανώνυμους χρήστες που προβάλλουν αυτή την κοινή χρήση. Οι συνδεδεμένοι χρήστες μπορούν ακόμα να αλλάξουν τις προτιμήσεις προβολής τους.",
@@ -639,5 +643,9 @@
"changePlaybackMode": "Αλλαγή λειτουργίας αναπαραγωγής",
"changePlaybackModeHint": "Αλλάξτε λειτουργία αναπαραγωγής για να δείτε περισσότερα αρχεία",
"emptyQueue": "Η ουρά είναι άδεια"
},
"halloween": {
"title": "Καλές Απόκριες!",
"body": "Το θέμα των αποκριών έχει εφαρμοστεί. Μπορείτε να το απενεργοποιήσετε αν δεν σας αρέσει ή να το απενεργοποιήσετε μόνιμα στις ρυθμίσεις προφίλ επιλέγοντας οποιοδήποτε άλλο θέμα."
}
}
+12 -4
View File
@@ -61,7 +61,11 @@
"openUploadViewer": "Open Upload Manager",
"discardAndExit": "Discard and Exit",
"saveAndExit": "Save and Exit",
"keepEditing": "Keep Editing"
"keepEditing": "Keep Editing",
"stayOnPrompt": "Stay",
"closeAnyway": "Close Anyways",
"disable": "Disable",
"confirm": "Confirm"
},
"download": {
"downloadFile": "Download File",
@@ -200,8 +204,6 @@
"light": "Light",
"enableOnlyOffice": "Enable OnlyOffice viewer",
"enableOnlyOfficeDescription": "Allow viewing office files using OnlyOffice in this share.",
"enableOnlyOfficeEditing": "Enable OnlyOffice editing",
"enableOnlyOfficeEditingDescription": "Allow editing of office files using OnlyOffice in this share.",
"shareType": "Share type",
"shareTypeDescription": "The type of share to create. Normal shares allow viewing and downloading files. Upload shares allow uploading files to the share.",
"normalShare": "Normal",
@@ -288,7 +290,9 @@
"copyItemQuestion": "Copy this item to the new location?",
"moveItemQuestion": "Move this item to the new location?",
"sourcePath": "Source:",
"destinationPath": "Destination:"
"destinationPath": "Destination:",
"closeWithActiveUploads": "Active uploads",
"closeWithActiveUploadsMessage": "Warning: You have active uploads and closing the upload prompt could result in unfinished uploads. What would you like to do?"
},
"search": {
"searchContext": "Search Context: {context}",
@@ -639,5 +643,9 @@
"changePlaybackMode": "Change playback mode",
"changePlaybackModeHint": "Change playback mode to see more files",
"emptyQueue": "Queue is empty"
},
"halloween": {
"title": "Happy Halloween!",
"body": "The Halloween theme has been applied. You can disable it if you don't like it, or permanently disable it in profile settings by selecting any other theme."
}
}
+12 -4
View File
@@ -61,7 +61,11 @@
"selectAll": "Seleccionar todo",
"discardAndExit": "Descartar y salir",
"saveAndExit": "Guardar y salir",
"keepEditing": "Seguir editando"
"keepEditing": "Seguir editando",
"stayOnPrompt": "Permanezca en",
"closeAnyway": "Cerrar De todos modos",
"disable": "Desactivar",
"confirm": "Confirme"
},
"download": {
"downloadFile": "Descargar archivo",
@@ -220,7 +224,9 @@
"copyItemQuestion": "¿Copiar este elemento en la nueva ubicación?",
"moveItemQuestion": "¿Mover este elemento a la nueva ubicación?",
"sourcePath": "Fuente:",
"destinationPath": "Destino:"
"destinationPath": "Destino:",
"closeWithActiveUploadsMessage": "Advertencia: Tienes subidas activas y si cierras el aviso de subidas, podrían quedar subidas inacabadas. ¿Qué desea hacer?",
"closeWithActiveUploads": "Cargas activas"
},
"search": {
"images": "Imágenes",
@@ -431,8 +437,6 @@
"light": "Claro",
"enableOnlyOffice": "Activar el visor de OnlyOffice",
"enableOnlyOfficeDescription": "Permitir la visualización de archivos de oficina utilizando OnlyOffice en este recurso compartido.",
"enableOnlyOfficeEditing": "Habilitar editor de OnlyOffice",
"enableOnlyOfficeEditingDescription": "Permitir la edición de archivos de oficina utilizando OnlyOffice en este recurso compartido.",
"titleDefault": "Archivos compartidos - {title}",
"defaultViewMode": "Modo de visualización",
"defaultViewModeDescription": "Establece el modo de visualización predeterminado para los usuarios anónimos que vean esta acción. Los usuarios registrados pueden cambiar su preferencia de visualización.",
@@ -639,5 +643,9 @@
"changePlaybackMode": "Cambiar modo de Reproducción",
"changePlaybackModeHint": "Cambia el modo de reproducción para ver más",
"emptyQueue": "La cola de reproducción está vacia..."
},
"halloween": {
"title": "¡Feliz Halloween!",
"body": "Se ha aplicado el tema de Halloween. Puedes desactivarlo si no te gusta, o desactivarlo permanentemente en los ajustes del perfil seleccionando cualquier otro tema."
}
}
+49 -41
View File
@@ -38,14 +38,14 @@
"openFile": "Ouvrir le fichier",
"copyDownloadLinkToClipboard": "Copier le lien de téléchargement dans le presse-papier",
"continue": "Continuer",
"edit": "Editer",
"edit": "Éditer",
"generateNewOtp": "Réinitialiser et générer un nouveau code à deux facteurs",
"verify": "Vérifier",
"pause": "Pause",
"resume": "Curriculum vitae",
"resume": "Reprendre",
"retry": "Réessayer",
"pauseAll": "Pause tous",
"resumeAll": "Curriculum vitae",
"pauseAll": "Tout mettre en pause",
"resumeAll": "Tout reprendre",
"clearCompleted": "Effacement terminé",
"showMore": "Afficher plus",
"showLess": "Montrer moins",
@@ -55,13 +55,17 @@
"compactView": "Vue compacte",
"normalView": "Vue normale",
"galleryView": "Vue de la galerie",
"uploadFiles": "Téléchargement de fichiers",
"uploadFolder": "Télécharger le dossier",
"openUploadViewer": "Ouvrir le gestionnaire de téléchargement",
"uploadFiles": "Importer des fichiers",
"uploadFolder": "Importer un dossier",
"openUploadViewer": "Ouvrir le gestionnaire d'importation",
"selectAll": "Sélectionner tout",
"discardAndExit": "Rejeter et quitter",
"saveAndExit": "Sauvegarder et quitter",
"keepEditing": "Continuer à éditer"
"keepEditing": "Continuer à éditer",
"stayOnPrompt": "Séjour",
"closeAnyway": "Fermer Quoi qu'il en soit",
"disable": "Désactiver",
"confirm": "Confirmer"
},
"download": {
"downloadFile": "Télécharger le fichier",
@@ -94,9 +98,9 @@
"noDownloadAccess": "Le téléchargement n'est pas disponible."
},
"help": {
"click": "Sélectionner un élément",
"click": "sélectionner un élément",
"ctrl": {
"click": "Sélectionner plusieurs éléments",
"click": "sélectionner plusieurs éléments",
"d": "télécharger les éléments sélectionnés",
"b": "barre latérale à bascule",
"comma": "accéder aux paramètres",
@@ -104,11 +108,11 @@
"v": "coller les éléments copiés/coupés",
"x": "couper les articles sélectionnés"
},
"del": "Supprimer les éléments sélectionnés",
"esc": "Désélectionner et/ou fermer la boîte de dialogue",
"f2": "Renommer le fichier",
"del": "supprimer les éléments sélectionnés",
"esc": "désélectionner et/ou fermer la boîte de dialogue",
"f2": "renommer le fichier",
"help": "Aide",
"f1": "show help prompt",
"f1": "afficher l'aide",
"description": "Vous pouvez consulter les options de navigation de base ci-dessous. Pour plus d'informations, veuillez consulter le site",
"slash": "activer la recherche",
"wiki": "Documents officiels de FileBrowser Quantum"
@@ -206,8 +210,8 @@
"numberDirs": "Nombre de répertoires{suffix}",
"numberFiles": "Nombre de fichiers{suffix}",
"renameMessageConflict": "Un article nommé \"{filename}\" existe déjà !",
"uploadSettingsChunked": "Nombre maximum de téléchargements simultanés : {maxConcurrentUpload}, taille de chunk : {uploadChunkSizeMb} MO",
"uploadSettingsNoChunk": "Nombre maximum de téléchargements simultanés : {maxConcurrentUpload}, chunked upload désactivé",
"uploadSettingsChunked": "Nombre maximum d'importations simultanées : {maxConcurrentUpload}, taille de chunk : {uploadChunkSizeMb} Mo",
"uploadSettingsNoChunk": "Nombre maximum d'importations simultanées : {maxConcurrentUpload}, importation par morceaux désactivée",
"quickJump": "Passer rapidement à d'autres fichiers",
"moveSuccess": "Les fichiers ont été déplacés avec succès !",
"copySuccess": "Les fichiers ont été copiés avec succès !",
@@ -220,7 +224,9 @@
"copyItemQuestion": "Copier cet élément vers le nouvel emplacement ?",
"moveItemQuestion": "Déplacer cet élément vers le nouvel emplacement ?",
"sourcePath": "Source :",
"destinationPath": "Destination :"
"destinationPath": "Destination :",
"closeWithActiveUploadsMessage": "Attention : Vous avez des téléchargements actifs et la fermeture de l'invite de téléchargement pourrait entraîner des téléchargements inachevés. Que voulez-vous faire ?",
"closeWithActiveUploads": "Téléchargements actifs"
},
"search": {
"images": "Images",
@@ -253,7 +259,7 @@
"admin": "Admin",
"administrator": "Administrateur",
"allowCommands": "Exécuter des commandes",
"allowEdit": "Editer, renommer et supprimer des fichiers ou des dossiers",
"allowEdit": "Éditer, renommer et supprimer des fichiers ou des dossiers",
"allowNew": "Créer de nouveaux fichiers et dossiers",
"allowPublish": "Publier de nouveaux posts et pages",
"allowSignup": "Autoriser les utilisateurs à s'inscrire",
@@ -298,8 +304,8 @@
"permissions-name": "Permissions",
"permissionsHelp": "Vous pouvez définir l'utilisateur comme étant un administrateur ou encore choisir les permissions individuellement. Si vous sélectionnez \"Administrateur\", toutes les autres options seront automatiquement activées. La gestion des utilisateurs est un privilège que seul l'administrateur possède.\n",
"profileSettings": "Paramètres du profil",
"ruleExample1": "Bloque l'accès à tous les fichiers commençant par un point (comme par exemple .git, .gitignore) dans tous les dossiers",
"ruleExample2": "Bloque l'accès au fichier nommé \"Caddyfile\" à la racine du dossier utilisateur",
"ruleExample1": "bloque l'accès à tous les fichiers commençant par un point (comme par exemple .git, .gitignore) dans tous les dossiers",
"ruleExample2": "bloque l'accès au fichier nommé \"Caddyfile\" à la racine du dossier utilisateur",
"rules": "Règles",
"rulesHelp": "Vous pouvez définir ici un ensemble de règles pour cet utilisateur. Les fichiers bloqués ne seront pas affichés et ne seront pas accessibles par l'utilisateur. Les expressions régulières sont supportées et les chemins d'accès sont relatifs par rapport au dossier de l'utilisateur.\n",
"scope": "Portée du dossier utilisateur",
@@ -309,7 +315,7 @@
"singleClick": "Utiliser un simple clic pour ouvrir les fichiers et les dossiers",
"themes": {
"dark": "Sombre",
"light": "Lumineux",
"light": "Clair",
"title": "Thème"
},
"user": "Utilisateur",
@@ -328,10 +334,10 @@
"listingOptions": "Options d'inscription",
"emptyGlobal": "Actuellement, il n'y a pas d'options de configuration globale. Vous pouvez éditer le fichier de configuration yaml pour ajuster les paramètres du système.",
"UserManagement": "Gestion des utilisateurs",
"tusUploads": "Téléversements groupés",
"tusUploadsHelp": "File Browser prend en charge les téléversements de fichiers par morceaux, ce qui permet de créer des téléversements de fichiers efficaces, fiables, résumables et par morceaux, même sur des réseaux peu fiables.",
"tusUploadsChunkSize": "Indique la taille maximale d'une demande (les téléversements directs seront utilisés pour les téléversements plus petits). Vous pouvez saisir un nombre entier simple correspondant à un nombre d'octets ou une chaîne de caractères telle que 10 Mo, 1 Go, etc.",
"tusUploadsRetryCount": "Nombre de tentatives à effectuer si un bloc ne parvient pas à être téléversé.",
"tusUploads": "Importations groupées",
"tusUploadsHelp": "File Browser prend en charge les importations de fichiers par morceaux, ce qui permet de créer des importations de fichiers efficaces, fiables, résumables et par morceaux, même sur des réseaux peu fiables.",
"tusUploadsChunkSize": "Indique la taille maximale d'une demande (les importations directes seront utilisées pour les importations plus petites). Vous pouvez saisir un nombre entier simple correspondant à un nombre d'octets ou une chaîne de caractères telle que 10 Mo, 1 Go, etc.",
"tusUploadsRetryCount": "Nombre de tentatives à effectuer si un bloc ne parvient pas à être importé.",
"userHomeBasePath": "Chemin de base pour les répertoires personnels des utilisateurs",
"userScopeGenerationPlaceholder": "Le champ d'application sera généré automatiquement",
"createUserHomeDirectory": "Créer le répertoire personnel de l'utilisateur",
@@ -389,7 +395,7 @@
"unit": "Unité de temps"
},
"upload": {
"abortUpload": "Êtes-vous sûre de vouloir annuler ?"
"abortUpload": "Êtes-vous sûr de vouloir annuler ?"
},
"share": {
"notice": "Remarque : lorsqu'un partage est créé, il est davantage exposé au public et les règles d'accès sont différentes. Si votre partage n'est pas configuré de manière sécurisée, n'importe qui peut y accéder.",
@@ -427,11 +433,9 @@
"enforceDarkLightModeDescription": "Forcer un mode de thème spécifique (sombre ou clair) pour ce partage, sans tenir compte des préférences de l'utilisateur.",
"default": "Ne pas imposer de mode",
"dark": "Sombre",
"light": "Lumière",
"light": "Clair",
"enableOnlyOffice": "Activer la visionneuse OnlyOffice",
"enableOnlyOfficeDescription": "Autoriser la visualisation de fichiers bureautiques en utilisant OnlyOffice dans ce partage.",
"enableOnlyOfficeEditing": "Activer l'édition OnlyOffice",
"enableOnlyOfficeEditingDescription": "Autoriser l'édition de fichiers bureautiques en utilisant OnlyOffice dans ce partage.",
"titleDefault": "Fichiers partagés - {title}",
"defaultViewMode": "Mode d'affichage par défaut",
"defaultViewModeDescription": "Définit le mode d'affichage par défaut pour les utilisateurs anonymes qui consultent ce partage. Les utilisateurs connectés peuvent toujours modifier leurs préférences d'affichage.",
@@ -483,9 +487,9 @@
"index": {
"status": "Statut",
"assessment": "Évaluation",
"lastScanned": "Dernier balayage",
"quickScan": "secondes pour un balayage rapide",
"fullScan": "secondes pour un balayage complet",
"lastScanned": "Dernier scan",
"quickScan": "Scan rapide (secondes)",
"fullScan": "Scan complet (secondes)",
"settingsHover": "Paramètres pour l'utilisateur",
"logout": "Déconnexion",
"toggleClick": "Activation du mode simple clic",
@@ -507,7 +511,7 @@
},
"general": {
"name": "Nom",
"path": "Trajectoire",
"path": "Chemin",
"user": "Utilisateur",
"group": "Groupe",
"signup": "S'inscrire",
@@ -542,14 +546,14 @@
"defaultBehavior": "Action par défaut{suffix}"
},
"fileLoading": {
"title": "Chargements et téléchargements",
"maxConcurrentUpload": "Nombre maximum de téléchargements simultanés",
"maxConcurrentUploadHelp": "Nombre maximal de téléchargements simultanés. Des valeurs plus élevées sont généralement plus performantes, mais cela dépend de nombreux facteurs.",
"uploadChunkSizeMb": "Taille de la zone de téléchargement en Mo",
"uploadChunkSizeMbHelp": "Généralement de 5 à 50 Mo, une taille inférieure est préférable pour les connexions instables ou restreintes. La valeur 0 désactive le fractionnement de l'upload.",
"clearAll": "Clear completed devrait supprimer les erreurs, les pauses et les conflits.",
"clearAllDescription": "Le bouton \"Effacer les téléchargements terminés\" efface tous les téléchargements terminés, y compris les erreurs, les pauses et les conflits.",
"uploadSettings": "Paramètres de téléchargement"
"title": "Importations et téléchargements",
"maxConcurrentUpload": "Nombre maximum d'importations simultanées",
"maxConcurrentUploadHelp": "Nombre maximal d'importations simultanées. Des valeurs plus élevées sont généralement plus performantes, mais cela dépend de nombreux facteurs.",
"uploadChunkSizeMb": "Taille de chunk pour l'importation en Mo",
"uploadChunkSizeMbHelp": "Généralement de 5 à 50 Mo, une taille inférieure est préférable pour les connexions instables ou restreintes. La valeur 0 désactive l'importation par morceaux.",
"clearAll": "Effacer les importations terminées (erreurs, pauses et conflits inclus)",
"clearAllDescription": "Le bouton \"Effacer les importations terminées\" efface toutes les importations terminées, y compris les erreurs, les pauses et les conflits.",
"uploadSettings": "Paramètres d'importation"
},
"colors": {
"red": "rouge",
@@ -639,5 +643,9 @@
"changePlaybackMode": "Changer le mode de lecture",
"changePlaybackModeHint": "Changez le mode de lecture pour voir plus de fichiers",
"emptyQueue": "La file est vide"
},
"halloween": {
"title": "Joyeux Halloween !",
"body": "Le thème Halloween a été appliqué. Vous pouvez le désactiver si vous ne l'aimez pas, ou le désactiver définitivement dans les paramètres du profil en sélectionnant un autre thème."
}
}
+12 -4
View File
@@ -61,7 +61,11 @@
"selectAll": "בחר הכל",
"discardAndExit": "מחק וצא",
"saveAndExit": "שמור וצא",
"keepEditing": "המשך עריכה"
"keepEditing": "המשך עריכה",
"stayOnPrompt": "הישאר",
"closeAnyway": "סגור בכל מקרה",
"disable": "השבת",
"confirm": "אשר"
},
"download": {
"downloadFile": "הורד קובץ",
@@ -220,7 +224,9 @@
"copyItemQuestion": "האם להעתיק פריט זה למיקום החדש?",
"moveItemQuestion": "האם להעביר פריט זה למיקום החדש?",
"sourcePath": "מקור:",
"destinationPath": "יעד:"
"destinationPath": "יעד:",
"closeWithActiveUploadsMessage": "אזהרה: יש לך העלאות פעילות וסגירת חלון ההעלאה עלולה לגרום להשלמת העלאות לא גמורות. מה ברצונך לעשות?",
"closeWithActiveUploads": "העלאות פעילות"
},
"search": {
"images": "תמונות",
@@ -430,8 +436,6 @@
"light": "אור",
"enableOnlyOffice": "הפעל את תוכנת הצפייה OnlyOffice",
"enableOnlyOfficeDescription": "אפשר צפייה בקבצי Office באמצעות OnlyOffice בשיתוף זה.",
"enableOnlyOfficeEditing": "אפשר עריכה ב-OnlyOffice",
"enableOnlyOfficeEditingDescription": "אפשר עריכת קבצי Office באמצעות OnlyOffice בשיתוף זה.",
"titleDefault": "קבצים משותפים - {title}",
"defaultViewMode": "מצב תצוגה ברירת מחדל",
"defaultViewModeDescription": "הגדר את מצב התצוגה המוגדר כברירת מחדל עבור משתמשים אנונימיים הצופים בשיתוף זה. משתמשים מחוברים עדיין יכולים לשנות את העדפות התצוגה שלהם.",
@@ -639,5 +643,9 @@
"changePlaybackMode": "שנה מצב ניגון",
"changePlaybackModeHint": "שנה מצב ניגון כדי לראות יותר קבצים",
"emptyQueue": "התור ריק"
},
"halloween": {
"title": "חג ליל כל הקדושים שמח!",
"body": "נושא ליל כל הקדושים הוחל. ניתן להשבית אותו אם אינו מוצא חן בעיניך, או להשבית אותו לצמיתות בהגדרות הפרופיל על ידי בחירת נושא אחר."
}
}
+12 -4
View File
@@ -61,7 +61,11 @@
"selectAll": "Válasszon ki mindent",
"discardAndExit": "Dobás és kilépés",
"saveAndExit": "Mentés és kilépés",
"keepEditing": "Szerkesztés folytatása"
"keepEditing": "Szerkesztés folytatása",
"stayOnPrompt": "Maradj",
"closeAnyway": "Egyébként bezárni",
"disable": "A letiltása",
"confirm": "Megerősítés"
},
"download": {
"downloadFile": "Fájl letöltése",
@@ -220,7 +224,9 @@
"copyItemQuestion": "Másolja ezt az elemet az új helyre?",
"moveItemQuestion": "Áthelyezni ezt az elemet az új helyre?",
"sourcePath": "Forrás:",
"destinationPath": "Úti cél:"
"destinationPath": "Úti cél:",
"closeWithActiveUploadsMessage": "Figyelmeztetés: A feltöltési prompt bezárása befejezetlen feltöltéseket eredményezhet. Mit szeretne tenni?",
"closeWithActiveUploads": "Aktív feltöltések"
},
"search": {
"images": "Képek",
@@ -430,8 +436,6 @@
"light": "Fény",
"enableOnlyOffice": "OnlyOffice megjelenítő engedélyezése",
"enableOnlyOfficeDescription": "Engedélyezze az irodai fájlok megtekintését az OnlyOffice használatával ezen a megosztáson.",
"enableOnlyOfficeEditing": "Engedélyezze az OnlyOffice szerkesztést",
"enableOnlyOfficeEditingDescription": "Engedélyezze az irodai fájlok szerkesztését az OnlyOffice használatával ezen a megosztáson.",
"titleDefault": "Megosztott fájlok - {title}",
"defaultViewMode": "Alapértelmezett nézetmód",
"defaultViewModeDescription": "A megosztást megtekintő névtelen felhasználók alapértelmezett megtekintési módjának beállítása. A bejelentkezett felhasználók továbbra is módosíthatják a nézeti beállításaikat.",
@@ -639,5 +643,9 @@
"changePlaybackMode": "Lejátszási mód változtatása",
"changePlaybackModeHint": "Változtasd meg a lejátszási módot, hogy több fájlt láss",
"emptyQueue": "A sor üres"
},
"halloween": {
"title": "Boldog Halloweent!",
"body": "A Halloween témát alkalmazták. Ha nem tetszik, letilthatja, vagy véglegesen letilthatja a profilbeállításokban bármely más téma kiválasztásával."
}
}
+12 -4
View File
@@ -61,7 +61,11 @@
"selectAll": "Seleziona tutti",
"discardAndExit": "Scartare e uscire",
"saveAndExit": "Salvare e uscire",
"keepEditing": "Continuare a modificare"
"keepEditing": "Continuare a modificare",
"stayOnPrompt": "Soggiorno",
"closeAnyway": "Chiudere comunque",
"disable": "Disattivare",
"confirm": "Confermare"
},
"download": {
"downloadFile": "Scarica file",
@@ -220,7 +224,9 @@
"copyItemQuestion": "Copiare questo elemento nella nuova posizione?",
"moveItemQuestion": "Spostare questo elemento nella nuova posizione?",
"sourcePath": "Fonte:",
"destinationPath": "Destinazione:"
"destinationPath": "Destinazione:",
"closeWithActiveUploadsMessage": "Attenzione: Ci sono caricamenti attivi e la chiusura del prompt di caricamento potrebbe causare caricamenti non completati. Cosa si desidera fare?",
"closeWithActiveUploads": "Carichi attivi"
},
"search": {
"images": "Immagini",
@@ -430,8 +436,6 @@
"light": "Luce",
"enableOnlyOffice": "Abilitate il visualizzatore OnlyOffice",
"enableOnlyOfficeDescription": "Consente di visualizzare i file di Office utilizzando OnlyOffice in questa condivisione.",
"enableOnlyOfficeEditing": "Abilita la modifica di OnlyOffice",
"enableOnlyOfficeEditingDescription": "Consentire la modifica dei file di Office utilizzando OnlyOffice in questa condivisione.",
"titleDefault": "File condivisi - {title}",
"defaultViewMode": "Modalità di visualizzazione predefinita",
"defaultViewModeDescription": "Imposta la modalità di visualizzazione predefinita per gli utenti anonimi che visualizzano questa condivisione. Gli utenti registrati possono comunque modificare le loro preferenze di visualizzazione.",
@@ -639,5 +643,9 @@
"changePlaybackMode": "Cambia modalità di riproduzione",
"changePlaybackModeHint": "Cambia modalità di riproduzione per vedere più file",
"emptyQueue": "La coda è vuota"
},
"halloween": {
"title": "Buon Halloween!",
"body": "Il tema di Halloween è stato applicato. È possibile disattivarlo se non piace, oppure disattivarlo definitivamente nelle impostazioni del profilo selezionando un altro tema."
}
}
+12 -4
View File
@@ -61,7 +61,11 @@
"selectAll": "すべて選択",
"discardAndExit": "破棄と終了",
"saveAndExit": "保存して終了する",
"keepEditing": "編集を続ける"
"keepEditing": "編集を続ける",
"stayOnPrompt": "滞在",
"closeAnyway": "とにかく閉じる",
"disable": "無効",
"confirm": "確認"
},
"download": {
"downloadFile": "ファイルをダウンロード",
@@ -220,7 +224,9 @@
"copyItemQuestion": "このアイテムを新しい場所にコピーしますか?",
"moveItemQuestion": "このアイテムを新しい場所に移動しますか?",
"sourcePath": "ソース",
"destinationPath": "目的地だ:"
"destinationPath": "目的地だ:",
"closeWithActiveUploadsMessage": "警告アクティブなアップロードがあり、アップロードプロンプトを閉じるとアップロードが未完了になる可能性があります。どうしますか?",
"closeWithActiveUploads": "アクティブなアップロード"
},
"search": {
"images": "画像",
@@ -430,10 +436,8 @@
"light": "ライト",
"enableOnlyOffice": "OnlyOfficeビューアを有効にする",
"enableOnlyOfficeDescription": "この共有でOnlyOfficeを使用したオフィスファイルの閲覧を許可する",
"enableOnlyOfficeEditing": "OnlyOffice編集を有効にする",
"defaultViewMode": "デフォルトの表示モード",
"defaultViewModeDescription": "匿名ユーザーがこの共有を閲覧する際のデフォルトの表示モードを設定します。ログインしているユーザーは、表示モードを変更することができます。",
"enableOnlyOfficeEditingDescription": "この共有でOnlyOfficeを使ったオフィスファイルの編集を許可する",
"titleDefault": "共有ファイル{title}",
"disableFileViewer": "ファイルビューアを無効にする",
"shareType": "シェアタイプ",
@@ -639,5 +643,9 @@
"changePlaybackMode": "再生モードを変更",
"changePlaybackModeHint": "再生モードを変更してより多くのファイルを表示",
"emptyQueue": "キューは空です"
},
"halloween": {
"title": "ハッピー・ハロウィーン",
"body": "ハロウィーンのテーマが適用されました。気に入らなければ無効にするか、プロフィール設定で他のテーマを選択して永久に無効にすることができます。"
}
}
+12 -4
View File
@@ -61,7 +61,11 @@
"selectAll": "모두 선택",
"discardAndExit": "삭제 및 종료",
"saveAndExit": "저장 및 종료",
"keepEditing": "계속 편집"
"keepEditing": "계속 편집",
"stayOnPrompt": "Stay",
"closeAnyway": "어쨌든 닫기",
"disable": "비활성화",
"confirm": "확인"
},
"download": {
"downloadFile": "파일 다운로드",
@@ -220,7 +224,9 @@
"copyItemQuestion": "이 항목을 새 위치로 복사하시겠습니까?",
"moveItemQuestion": "이 항목을 새 위치로 옮기시겠습니까?",
"sourcePath": "출처:",
"destinationPath": "목적지:"
"destinationPath": "목적지:",
"closeWithActiveUploadsMessage": "경고: 업로드가 진행 중이며 업로드 프롬프트를 닫으면 완료되지 않은 업로드가 발생할 수 있습니다. 어떻게 하시겠습니까?",
"closeWithActiveUploads": "활성 업로드"
},
"search": {
"images": "이미지",
@@ -430,8 +436,6 @@
"light": "빛",
"enableOnlyOffice": "Office 뷰어만 사용",
"enableOnlyOfficeDescription": "이 공유에서 OnlyOffice를 사용하여 Office 파일을 볼 수 있도록 허용합니다.",
"enableOnlyOfficeEditing": "Office 편집만 사용",
"enableOnlyOfficeEditingDescription": "이 공유에서 OnlyOffice를 사용하여 Office 파일을 편집할 수 있도록 허용합니다.",
"titleDefault": "공유 파일 - {title}",
"defaultViewMode": "기본 보기 모드",
"defaultViewModeDescription": "이 공유를 보는 익명 사용자의 기본 보기 모드를 설정합니다. 로그인한 사용자는 여전히 보기 기본 설정을 변경할 수 있습니다.",
@@ -639,5 +643,9 @@
"changePlaybackMode": "재생 모드 변경",
"changePlaybackModeHint": "더 많은 파일을 보려면 재생 모드를 변경하세요",
"emptyQueue": "대기열이 비어 있습니다"
},
"halloween": {
"title": "해피 할로윈!",
"body": "할로윈 테마가 적용되었습니다. 마음에 들지 않으면 비활성화하거나 프로필 설정에서 다른 테마를 선택하여 영구적으로 비활성화할 수 있습니다."
}
}
+12 -4
View File
@@ -61,7 +61,11 @@
"selectAll": "Alles selecteren",
"discardAndExit": "Weggooien en afsluiten",
"saveAndExit": "Opslaan en afsluiten",
"keepEditing": "Blijven bewerken"
"keepEditing": "Blijven bewerken",
"stayOnPrompt": "Blijf",
"closeAnyway": "Sluiten",
"disable": "uitschakelen",
"confirm": "Bevestig"
},
"download": {
"downloadFile": "Bestand downloaden",
@@ -220,7 +224,9 @@
"copyItemQuestion": "Dit item kopiëren naar de nieuwe locatie?",
"moveItemQuestion": "Dit item verplaatsen naar de nieuwe locatie?",
"sourcePath": "Bron:",
"destinationPath": "Bestemming:"
"destinationPath": "Bestemming:",
"closeWithActiveUploadsMessage": "Waarschuwing: U hebt actieve uploads en het sluiten van de uploadprompt kan resulteren in onvoltooide uploads. Wat wilt u doen?",
"closeWithActiveUploads": "Actieve uploads"
},
"search": {
"images": "Afbeeldingen",
@@ -430,8 +436,6 @@
"light": "Licht",
"enableOnlyOffice": "OnlyOffice viewer inschakelen",
"enableOnlyOfficeDescription": "Sta het bekijken van Office-bestanden toe met OnlyOffice in deze share.",
"enableOnlyOfficeEditing": "Bewerken met OnlyOffice inschakelen",
"enableOnlyOfficeEditingDescription": "Laat het bewerken van Office-bestanden met OnlyOffice toe in deze share.",
"titleDefault": "Gedeelde bestanden - {title}",
"defaultViewMode": "Standaard weergavemodus",
"defaultViewModeDescription": "Stel de standaard weergavemodus in voor anonieme gebruikers die deze share bekijken. Aangemelde gebruikers kunnen nog steeds hun weergavevoorkeur wijzigen.",
@@ -639,5 +643,9 @@
"changePlaybackMode": "Afspeelmodus wijzigen",
"changePlaybackModeHint": "Wijzig afspeelmodus om meer bestanden te zien",
"emptyQueue": "Wachtrij is leeg"
},
"halloween": {
"title": "Gelukkig Halloween!",
"body": "Het Halloween-thema is toegepast. Je kunt het uitschakelen als je het niet leuk vindt, of permanent uitschakelen in de profielinstellingen door een ander thema te selecteren."
}
}
+12 -4
View File
@@ -61,7 +61,11 @@
"selectAll": "Wybierz wszystko",
"discardAndExit": "Odrzuć i wyjdź",
"saveAndExit": "Zapisz i wyjdź",
"keepEditing": "Kontynuuj edycję"
"keepEditing": "Kontynuuj edycję",
"stayOnPrompt": "Zostań",
"closeAnyway": "W każdym razie blisko",
"disable": "Wyłącz",
"confirm": "Potwierdzenie"
},
"download": {
"downloadFile": "Pobierz plik",
@@ -220,7 +224,9 @@
"copyItemQuestion": "Skopiować ten element do nowej lokalizacji?",
"moveItemQuestion": "Przenieść ten element do nowej lokalizacji?",
"sourcePath": "Źródło:",
"destinationPath": "Miejsce docelowe:"
"destinationPath": "Miejsce docelowe:",
"closeWithActiveUploadsMessage": "Ostrzeżenie: Masz aktywne przesyłanie, a zamknięcie monitu o przesłanie może spowodować niedokończone przesyłanie. Co chcesz zrobić?",
"closeWithActiveUploads": "Aktywne przesyłanie"
},
"search": {
"images": "Zdjęcia",
@@ -430,8 +436,6 @@
"light": "Światło",
"enableOnlyOffice": "Włącz przeglądarkę OnlyOffice",
"enableOnlyOfficeDescription": "Zezwalaj na wyświetlanie plików biurowych przy użyciu OnlyOffice w tym udziale.",
"enableOnlyOfficeEditing": "Włącz edycję OnlyOffice",
"enableOnlyOfficeEditingDescription": "Zezwalaj na edycję plików biurowych za pomocą OnlyOffice w tym udziale.",
"titleDefault": "Udostępnione pliki - {title}",
"defaultViewMode": "Domyślny tryb widoku",
"defaultViewModeDescription": "Ustawia domyślny tryb widoku dla anonimowych użytkowników przeglądających ten folder udostępniony. Zalogowani użytkownicy mogą nadal zmieniać swoje preferencje widoku.",
@@ -639,5 +643,9 @@
"changePlaybackMode": "Zmień tryb odtwarzania",
"changePlaybackModeHint": "Zmień tryb odtwarzania, aby zobaczyć więcej plików",
"emptyQueue": "Kolejka jest pusta"
},
"halloween": {
"title": "Wesołego Halloween!",
"body": "Motyw Halloween został zastosowany. Możesz go wyłączyć, jeśli Ci się nie podoba, lub wyłączyć go na stałe w ustawieniach profilu, wybierając inny motyw."
}
}
+12 -4
View File
@@ -61,7 +61,11 @@
"selectAll": "Selecionar tudo",
"discardAndExit": "Descartar e sair",
"saveAndExit": "Salvar e sair",
"keepEditing": "Continuar editando"
"keepEditing": "Continuar editando",
"stayOnPrompt": "Permanecer",
"closeAnyway": "Fechar de qualquer maneira",
"disable": "Desativar",
"confirm": "Confirmar"
},
"download": {
"downloadFile": "Baixar arquivo",
@@ -220,7 +224,9 @@
"copyItemQuestion": "Copiar esse item para o novo local?",
"moveItemQuestion": "Mover esse item para o novo local?",
"sourcePath": "Fonte:",
"destinationPath": "Destino:"
"destinationPath": "Destino:",
"closeWithActiveUploadsMessage": "Aviso: Você tem uploads ativos e fechar o prompt de upload pode resultar em uploads inacabados. O que você gostaria de fazer?",
"closeWithActiveUploads": "Uploads ativos"
},
"search": {
"images": "Imagens",
@@ -430,8 +436,6 @@
"light": "Luz",
"enableOnlyOffice": "Ativar o visualizador do OnlyOffice",
"enableOnlyOfficeDescription": "Permitir a visualização de arquivos do Office usando o OnlyOffice nesse compartilhamento.",
"enableOnlyOfficeEditing": "Ativar a edição do OnlyOffice",
"enableOnlyOfficeEditingDescription": "Permitir a edição de arquivos do Office usando o OnlyOffice nesse compartilhamento.",
"titleDefault": "Arquivos compartilhados - {title}",
"defaultViewMode": "Modo de exibição padrão",
"defaultViewModeDescription": "Define o modo de exibição padrão para usuários anônimos que visualizam esse compartilhamento. Os usuários conectados ainda podem alterar sua preferência de visualização.",
@@ -639,5 +643,9 @@
"changePlaybackMode": "Alterar modo de reprodução",
"changePlaybackModeHint": "Altere o modo de reprodução para ver mais arquivos",
"emptyQueue": "A fila está vazia"
},
"halloween": {
"title": "Feliz Dia das Bruxas!",
"body": "O tema Halloween foi aplicado. Você pode desativá-lo se não gostar dele ou desativá-lo permanentemente nas configurações de perfil, selecionando qualquer outro tema."
}
}
Loaded 100 of 138 files, more files were not shown because too many files have changed in this diff. Show more