mirror of
https://github.com/RoastSlav/quickdrop.git
synced 2026-05-12 15:29:40 -05:00
feature: implement internationalization support with locale switching and validation
This commit is contained in:
@@ -1,15 +1,82 @@
|
||||
# QuickDrop AI Working Guide (concise)
|
||||
# QuickDrop — Agent Guide
|
||||
|
||||
- **Build/run**: Java 21. Use `./mvnw clean package` then `java -jar target/quickdrop.jar`. SQLite DB at `db/quickdrop.db`; Flyway auto-runs baseline-on-migrate. Docker image `roastslav/quickdrop:latest` exposes 8080.
|
||||
- **App shape**: Spring Boot MVC + Thymeleaf. Controllers in [src/main/java/org/rostislav/quickdrop/controller](src/main/java/org/rostislav/quickdrop/controller); services in [service](src/main/java/org/rostislav/quickdrop/service); interceptors in [interceptor](src/main/java/org/rostislav/quickdrop/interceptor); entities/repos in [entity](src/main/java/org/rostislav/quickdrop/entity) and [repository](src/main/java/org/rostislav/quickdrop/repository); static assets + templates under `src/main/resources/static` and `.../templates`.
|
||||
- **Settings source of truth**: Single-row settings (id=1) via [ApplicationSettingsService](src/main/java/org/rostislav/quickdrop/service/ApplicationSettingsService.java). Startup seeds defaults (max size 1GB, life 30d, storage `files`/`logs`, cron `0 0 2 * * *`, app password off, admin password empty, file list & admin button enabled, encryption on, default home upload) and schedules cleanup. Never create extra rows.
|
||||
- **Security**: [SecurityConfig](src/main/java/org/rostislav/quickdrop/config/SecurityConfig.java) enables whole-app password mode (`/password/login`), BCrypt auth, CSRF cookie, permissive CORS. Session timeout set in [WebConfig](src/main/java/org/rostislav/quickdrop/config/WebConfig.java). Admin and file tokens stored in-memory in [SessionService](src/main/java/org/rostislav/quickdrop/service/SessionService.java).
|
||||
- **Interceptors**: [AdminPasswordSetupInterceptor](src/main/java/org/rostislav/quickdrop/interceptor/AdminPasswordSetupInterceptor.java) enforces `/admin/setup` until admin password exists. [AdminPasswordInterceptor](src/main/java/org/rostislav/quickdrop/interceptor/AdminPasswordInterceptor.java) gates `/admin/**` and file history. [FilePasswordInterceptor](src/main/java/org/rostislav/quickdrop/interceptor/FilePasswordInterceptor.java) redirects to `/file/password/{uuid}` when missing a valid file session token.
|
||||
- **Upload pipeline**: `/api/file/upload-chunk` accepts chunked uploads with metadata (`description`, `keepIndefinitely`, `password`, `hidden`, `fileSize`, folder fields). [AsyncFileMergeService](src/main/java/org/rostislav/quickdrop/service/AsyncFileMergeService.java) buffers/merges chunks; [FileEncryptionService](src/main/java/org/rostislav/quickdrop/service/FileEncryptionService.java) encrypts when password + encryption enabled; final persist via [FileService](src/main/java/org/rostislav/quickdrop/service/FileService.java). Intermediate chunk calls return null—frontends must handle.
|
||||
- **Storage/model**: Files stored on disk under `fileStoragePath` (from settings) with UUID filenames; metadata in `file_entity` (description, keepIndefinitely, hidden, optional password hash, encrypted flag, folder fields). [QuickdropApplication](src/main/java/org/rostislav/quickdrop/QuickdropApplication.java) ensures `./db` and storage path exist.
|
||||
- **Downloads & history**: `/file/download/{uuid}` decrypts on the fly; logs uploads/renewals/downloads with IP/UA into `file_history_log` (types `UPLOAD`, `RENEWAL`, `DOWNLOAD`). Share links via `/api/file/share/{uuid}` and `/api/file/download/{uuid}/{token}` decrement/delete tokens.
|
||||
- **Admin UX**: Dashboard lists files with download counts and actions (view, history, download, delete, keep indefinitely toggle, hidden toggle) plus analytics (total downloads, space, avg size). Public list and admin dashboard now paginated/searchable (page/size/query params) with in-memory caching of page results; cache evicted on uploads, deletions, renewals, hidden/keep toggles, downloads.
|
||||
- **Cleanup/scheduling**: [ScheduleService](src/main/java/org/rostislav/quickdrop/service/ScheduleService.java) schedules deletions per cron/max life, missing-file cleanup, share token cleanup. App uses @EnableScheduling.
|
||||
- **Thymeleaf flags**: [GlobalControllerAdvice](src/main/java/org/rostislav/quickdrop/config/GlobalControllerAdvice.java) injects template booleans (file list enabled, app password set, admin button enabled, encryption enabled).
|
||||
- **Conventions**: Route all file mutations through `FileService` to ensure logging/notifications/encryption. Update interceptors when adding protected routes. Keep single settings row intact. For pagination/search, reuse Page-based service methods and propagate `page/size/query` to templates. Cache keys live in `publicFiles`, `adminFiles`, `analytics` (see [CacheConfig](src/main/java/org/rostislav/quickdrop/config/CacheConfig.java)).
|
||||
- **Dev tips**: SQLite dialect configured; migrations live under `src/main/resources/db/migration`. Few/no automated tests—manual `./mvnw clean package` is typical. Logs at `log/quickdrop.log` (see properties). Default port 8080.
|
||||
Self-hosted file sharing: Spring Boot 3.5 + Thymeleaf + SQLite (Flyway) + Tailwind. Java 21.
|
||||
|
||||
## Build & run
|
||||
|
||||
- Build: `./mvnw clean package` (Windows: `mvnw.cmd clean package`). Output: `target/quickdrop.jar`.
|
||||
- Run: `java -jar target/quickdrop.jar` → http://localhost:8080.
|
||||
- Tailwind CSS: `npm run tw:build` rebuilds `src/main/resources/static/css/tailwind.css` from `tailwind-input.css`.
|
||||
- No test suite — verify via manual package + smoke test. Logs at `log/quickdrop.log`, DB at `db/quickdrop.db`, uploads
|
||||
under `files/` (all auto-created by [
|
||||
`QuickdropApplication`](src/main/java/org/rostislav/quickdrop/QuickdropApplication.java)).
|
||||
- Docker: `roastslav/quickdrop:latest`, exposes 8080; mount `/app/db`, `/app/files`, `/app/log`.
|
||||
|
||||
## Architecture (follow the layers)
|
||||
|
||||
- Controllers (`controller/`): `FileViewController`, `AdminViewController`, `FileRestController` (chunked upload + share
|
||||
APIs), `ShareViewController`, `PasswordViewController`, `IndexViewController`. Thymeleaf views in
|
||||
`src/main/resources/templates/`.
|
||||
- Services (`service/`) own all business logic. **Route every file mutation
|
||||
through [`FileService`](src/main/java/org/rostislav/quickdrop/service/FileService.java)** so history logging, cache
|
||||
eviction, encryption and notifications fire.
|
||||
- Upload pipeline: `POST /api/file/upload-chunk` → [
|
||||
`AsyncFileMergeService`](src/main/java/org/rostislav/quickdrop/service/AsyncFileMergeService.java) buffers chunks → [
|
||||
`FileEncryptionService`](src/main/java/org/rostislav/quickdrop/service/FileEncryptionService.java) encrypts when
|
||||
password + encryption flag set → `FileService` persists. Intermediate chunk calls return `null`; frontend must handle.
|
||||
Metadata fields: `description`, `keepIndefinitely`, `password`, `hidden`, `fileSize`, folder fields, `isPaste`.
|
||||
- Download: `/file/download/{uuid}` streams with on-the-fly decryption and logs `DOWNLOAD` into `file_history_log` (
|
||||
types: `UPLOAD`, `RENEWAL`, `DOWNLOAD`). Share links via `/api/file/share/{uuid}` issue tokens redeemed at
|
||||
`/api/file/download/{uuid}/{token}` (decrement/delete).
|
||||
- Storage/model: UUID filenames on disk at `fileStoragePath` (from settings); `file_entity` row holds metadata +
|
||||
optional BCrypt password hash + `encrypted` flag + folder + `isPaste`.
|
||||
|
||||
## Settings (single-row, id=1)
|
||||
|
||||
- [`ApplicationSettingsService`](src/main/java/org/rostislav/quickdrop/service/ApplicationSettingsService.java) seeds
|
||||
defaults on startup (max 1 GB, 30-day life, `files`/`logs` paths, cron `0 0 2 * * *`, encryption on, default home =
|
||||
upload, SMTP/Discord off, etc.) and re-schedules cleanup when cron changes. **Never insert a second settings row.**
|
||||
- Template-level feature flags (file list enabled, app password set, admin button, encryption on, previews, pastebin)
|
||||
are injected by [`GlobalControllerAdvice`](src/main/java/org/rostislav/quickdrop/config/GlobalControllerAdvice.java) —
|
||||
read from there; don't duplicate.
|
||||
|
||||
## Security & interceptors
|
||||
|
||||
- [`SecurityConfig`](src/main/java/org/rostislav/quickdrop/config/SecurityConfig.java): optional whole-app password at
|
||||
`/password/login`, BCrypt, CSRF cookie, permissive CORS. Session timeout configured in [
|
||||
`WebConfig`](src/main/java/org/rostislav/quickdrop/config/WebConfig.java).
|
||||
- In-memory admin + file session tokens live in [
|
||||
`SessionService`](src/main/java/org/rostislav/quickdrop/service/SessionService.java) (not persisted — restarts
|
||||
invalidate).
|
||||
- Add new protected routes via the right interceptor: `AdminPasswordSetupInterceptor` (forces `/admin/setup` until admin
|
||||
pwd exists), `AdminPasswordInterceptor` (`/admin/**` + file history), `FilePasswordInterceptor` (redirects to
|
||||
`/file/password/{uuid}` when the file has a password and no valid session token).
|
||||
|
||||
## Migrations & persistence
|
||||
|
||||
- Flyway migrations in `src/main/resources/db/migration/V*__*.sql` (currently V1–V20). Baseline-on-migrate is enabled —
|
||||
**append a new `V{n+1}__...sql`, never edit existing files.** SQLite dialect is
|
||||
`org.hibernate.community.dialect.SQLiteDialect`.
|
||||
- Entities in `entity/`, Spring Data repos in `repository/`. Use `@Transactional` on service methods that mutate.
|
||||
|
||||
## Pagination, search & cache
|
||||
|
||||
- Public list (`/files`) and admin dashboard use `page`/`size`/`query` params backed by `Page<FileEntity>` service
|
||||
methods — reuse them and pass all three params through to templates.
|
||||
- Spring Cache keys are `publicFiles`, `adminFiles`, `analytics` (see [
|
||||
`CacheConfig`](src/main/java/org/rostislav/quickdrop/config/CacheConfig.java)). Any upload, deletion, renewal,
|
||||
visibility/keep toggle, or download **must evict** these — copy the `@CacheEvict` pattern from `FileService`.
|
||||
|
||||
## Scheduling & notifications
|
||||
|
||||
- `@EnableScheduling` + [`ScheduleService`](src/main/java/org/rostislav/quickdrop/service/ScheduleService.java) handles
|
||||
expiry deletion (cron from settings), missing-file row cleanup, share token expiry. Reschedule by calling its
|
||||
`rescheduleCleanup(...)` when cron changes.
|
||||
- [`NotificationService`](src/main/java/org/rostislav/quickdrop/service/NotificationService.java) batches email +
|
||||
Discord webhook events per configured interval; call it from `FileService`, not controllers.
|
||||
|
||||
## i18n
|
||||
|
||||
- Bundles: `messages.properties`, `messages_bg.properties`, `messages_de.properties`. Add every new user-facing string
|
||||
to all three (see `docs/i18n.md`). Resolver wired in [
|
||||
`I18nConfig`](src/main/java/org/rostislav/quickdrop/config/I18nConfig.java).
|
||||
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Lightweight i18n key consistency check for QuickDrop.
|
||||
|
||||
Compares locale bundles against messages.properties and reports:
|
||||
- missing keys in locale bundles
|
||||
- extra keys that do not exist in English baseline
|
||||
- empty values in locale bundles
|
||||
|
||||
Default mode is warn-only (exit code 0). Use --strict to fail on findings.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Tuple
|
||||
|
||||
|
||||
def read_text_with_fallback(path: Path) -> str:
|
||||
raw = path.read_bytes()
|
||||
for encoding in ("utf-8", "utf-8-sig", "cp1252", "latin-1"):
|
||||
try:
|
||||
return raw.decode(encoding)
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
# Last resort keeps the check running even on malformed files.
|
||||
return raw.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def parse_properties(path: Path) -> Dict[str, str]:
|
||||
data: Dict[str, str] = {}
|
||||
if not path.exists():
|
||||
return data
|
||||
|
||||
text = read_text_with_fallback(path)
|
||||
for raw_line in text.splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#") or line.startswith("!"):
|
||||
continue
|
||||
|
||||
key, value = split_property(line)
|
||||
if key:
|
||||
data[key] = value
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def split_property(line: str) -> Tuple[str, str]:
|
||||
# Java properties separators: '=', ':' or first whitespace.
|
||||
escaped = False
|
||||
for idx, ch in enumerate(line):
|
||||
if escaped:
|
||||
escaped = False
|
||||
continue
|
||||
if ch == "\\":
|
||||
escaped = True
|
||||
continue
|
||||
if ch in ("=", ":"):
|
||||
return line[:idx].strip(), line[idx + 1 :].strip()
|
||||
if ch.isspace():
|
||||
return line[:idx].strip(), line[idx + 1 :].strip()
|
||||
return line.strip(), ""
|
||||
|
||||
|
||||
def diff_keys(base: Dict[str, str], target: Dict[str, str]) -> Tuple[List[str], List[str], List[str]]:
|
||||
base_keys = set(base.keys())
|
||||
target_keys = set(target.keys())
|
||||
missing = sorted(base_keys - target_keys)
|
||||
extra = sorted(target_keys - base_keys)
|
||||
empty = sorted([k for k, v in target.items() if not v.strip()])
|
||||
return missing, extra, empty
|
||||
|
||||
|
||||
def find_locale_files(resources_dir: Path) -> Iterable[Path]:
|
||||
for path in sorted(resources_dir.glob("messages_*.properties")):
|
||||
yield path
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--strict", action="store_true", help="Exit non-zero when findings exist.")
|
||||
parser.add_argument(
|
||||
"--resources-dir",
|
||||
default="src/main/resources",
|
||||
help="Resources directory containing message bundles.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
resources_dir = Path(args.resources_dir)
|
||||
base_file = resources_dir / "messages.properties"
|
||||
base = parse_properties(base_file)
|
||||
|
||||
if not base:
|
||||
print(f"::error::Base bundle not found or empty: {base_file}")
|
||||
return 1
|
||||
|
||||
locale_files = list(find_locale_files(resources_dir))
|
||||
if not locale_files:
|
||||
print("::warning::No locale bundles found (messages_*.properties).")
|
||||
return 0
|
||||
|
||||
findings = 0
|
||||
summary_lines: List[str] = []
|
||||
|
||||
for locale_file in locale_files:
|
||||
try:
|
||||
locale_file.read_text(encoding="utf-8")
|
||||
except UnicodeDecodeError:
|
||||
print(
|
||||
f"::warning file={locale_file}::Bundle is not UTF-8 encoded. Consider converting to UTF-8."
|
||||
)
|
||||
|
||||
target = parse_properties(locale_file)
|
||||
missing, extra, empty = diff_keys(base, target)
|
||||
|
||||
locale_findings = len(missing) + len(extra) + len(empty)
|
||||
findings += locale_findings
|
||||
|
||||
summary_lines.append(f"## {locale_file.name}")
|
||||
if locale_findings == 0:
|
||||
summary_lines.append("- OK: key set is aligned with messages.properties")
|
||||
continue
|
||||
|
||||
if missing:
|
||||
summary_lines.append(f"- Missing keys ({len(missing)}):")
|
||||
summary_lines.extend([f" - `{k}`" for k in missing])
|
||||
if extra:
|
||||
summary_lines.append(f"- Extra keys ({len(extra)}):")
|
||||
summary_lines.extend([f" - `{k}`" for k in extra])
|
||||
if empty:
|
||||
summary_lines.append(f"- Empty values ({len(empty)}):")
|
||||
summary_lines.extend([f" - `{k}`" for k in empty])
|
||||
|
||||
for line in summary_lines:
|
||||
print(line)
|
||||
|
||||
if findings > 0:
|
||||
print(f"::warning::i18n key check found {findings} issue(s).")
|
||||
if args.strict:
|
||||
print("::error::Strict mode enabled. Failing due to i18n findings.")
|
||||
return 2
|
||||
else:
|
||||
print("i18n key check passed with no findings.")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: i18n Key Diff
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "dev", "master" ]
|
||||
push:
|
||||
branches: [ "dev", "master" ]
|
||||
|
||||
jobs:
|
||||
i18n-key-diff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Check i18n key consistency (warn-only)
|
||||
run: |
|
||||
python .github/scripts/check_i18n_keys.py
|
||||
|
||||
@@ -182,6 +182,19 @@ PR requirements:
|
||||
- Add screenshots for UI changes.
|
||||
- Add migration notes if you touched DB.
|
||||
|
||||
If your PR includes UI copy or localization updates:
|
||||
|
||||
- Follow `docs/i18n.md` key conventions and workflow.
|
||||
- Update `messages.properties` first (English source of truth).
|
||||
- Add locale translations where available, or rely on English fallback by omitting missing locale keys.
|
||||
- Keep i18n changes scoped by page/feature where possible.
|
||||
|
||||
Run i18n key check locally before opening/updating PR:
|
||||
|
||||
```powershell
|
||||
python .github/scripts/check_i18n_keys.py
|
||||
```
|
||||
|
||||
Code style & dependencies:
|
||||
|
||||
- Follow existing patterns in the codebase.
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
# Internationalization (i18n)
|
||||
|
||||
QuickDrop uses standard Spring Boot + Thymeleaf i18n:
|
||||
|
||||
- `MessageSource` message bundles (`messages*.properties`)
|
||||
- Thymeleaf message expressions (`#{...}`)
|
||||
- locale switching with `?lang=<code>`
|
||||
|
||||
## Files
|
||||
|
||||
- `src/main/resources/messages.properties` (English source of truth)
|
||||
- `src/main/resources/messages_bg.properties`
|
||||
- `src/main/resources/messages_de.properties`
|
||||
|
||||
Optional helper tooling:
|
||||
|
||||
- `.github/scripts/check_i18n_keys.py` (key consistency checker)
|
||||
|
||||
## Conventions
|
||||
|
||||
- Use dotted keys grouped by domain/page, for example:
|
||||
- `nav.*`
|
||||
- `button.*`
|
||||
- `page.upload.*`
|
||||
- `form.*`
|
||||
- `validation.*`
|
||||
- Keep keys semantic, not sentence-based.
|
||||
- Add new keys to English first, then translate.
|
||||
|
||||
## Key style guide
|
||||
|
||||
Use flat dotted keys with stable prefixes. Recommended families:
|
||||
|
||||
- `nav.*`
|
||||
- `page.upload.*`
|
||||
- `page.settings.*`
|
||||
- `form.*`
|
||||
- `button.*`
|
||||
- `error.*`
|
||||
- `notification.*`
|
||||
- `validation.*`
|
||||
|
||||
Rules:
|
||||
|
||||
1. Reuse keys only when semantics are exactly the same.
|
||||
2. Avoid generic names like `label1` or `textA`.
|
||||
3. Prefer placeholders over concatenation.
|
||||
4. Do not include HTML markup in message values.
|
||||
5. Keep English as canonical source.
|
||||
|
||||
Examples:
|
||||
|
||||
- Good: `page.upload.title`, `button.save`, `validation.email.invalid`
|
||||
- Bad: `uploadTitleText`, `message_12`, `saveTextForBlueButton`
|
||||
|
||||
## Translator notes
|
||||
|
||||
- Add translator notes as comments directly above keys in `messages.properties`.
|
||||
- Use notes when placeholders or split strings are involved.
|
||||
- Example note style:
|
||||
- `# Translator note: {0} is the app name.`
|
||||
- `# Translator note: split around numeric day count in template.`
|
||||
|
||||
## Fallback behavior
|
||||
|
||||
- Default locale is English.
|
||||
- If a key is missing in a locale file, Spring falls back to `messages.properties`.
|
||||
- QuickDrop does not rely on the server system locale.
|
||||
- For unfinished translations, prefer omitting a key (fallback) over leaving an empty value.
|
||||
|
||||
## Adding keys
|
||||
|
||||
1. Add the key to `messages.properties` first.
|
||||
2. Add translated values in each `messages_<locale>.properties` file you can validate.
|
||||
3. If a translation is not ready, omit the locale key (English fallback applies).
|
||||
4. Keep keys in the proper prefix family (`nav.*`, `page.*`, `button.*`, etc.).
|
||||
|
||||
## Adding a new locale file
|
||||
|
||||
1. Create `src/main/resources/messages_<locale>.properties` (for example `messages_fr.properties`).
|
||||
2. Start by copying keys from `messages.properties`.
|
||||
3. Translate values and keep key names unchanged.
|
||||
4. Verify UI manually with `?lang=<locale>` and run the key check script.
|
||||
|
||||
## QA guardrails
|
||||
|
||||
QuickDrop includes a lightweight i18n CI check:
|
||||
|
||||
- Workflow: `.github/workflows/i18n-key-diff.yml`
|
||||
- Script: `.github/scripts/check_i18n_keys.py`
|
||||
- Current mode: warn-only (non-blocking)
|
||||
|
||||
The script reports:
|
||||
|
||||
- keys missing in locale bundles
|
||||
- extra keys not present in English baseline
|
||||
- empty values in locale bundles
|
||||
|
||||
Run locally:
|
||||
|
||||
```powershell
|
||||
python .github/scripts/check_i18n_keys.py
|
||||
```
|
||||
|
||||
Optional strict mode (for maintainers/future enforcement):
|
||||
|
||||
```powershell
|
||||
python .github/scripts/check_i18n_keys.py --strict
|
||||
```
|
||||
|
||||
## How to test locally
|
||||
|
||||
Open the same page with different query parameters:
|
||||
|
||||
- `?lang=en`
|
||||
- `?lang=bg`
|
||||
- `?lang=de`
|
||||
|
||||
Example:
|
||||
|
||||
- `/file/upload?lang=de`
|
||||
|
||||
## Contributor workflow
|
||||
|
||||
1. Add/adjust key in `messages.properties` first.
|
||||
2. Add translations for the same key in locale files.
|
||||
3. Keep changes scoped to one page/feature when possible.
|
||||
4. In PR description, include screenshots for changed text-heavy pages.
|
||||
5. If translation is uncertain, leave the locale key out (English fallback is preferred over incorrect copy).
|
||||
6. Follow key prefixes from this guide so contributors can find keys quickly.
|
||||
7. If you rename or delete keys, include migration notes in the PR.
|
||||
8. Keep i18n PRs focused by page/feature to simplify review.
|
||||
|
||||
## Suggested review checklist
|
||||
|
||||
- Keys follow approved prefixes and naming style.
|
||||
- English text is clear and context-accurate.
|
||||
- Placeholder usage is correct (`{0}`, `{1}`, ...).
|
||||
- No HTML is embedded in message values.
|
||||
- Locale files are updated or fallback behavior is intentional.
|
||||
|
||||
## Notes on key ordering
|
||||
|
||||
- There is currently no auto-sort tool rewriting bundle files.
|
||||
- Keep related keys grouped by section; avoid large unrelated reorder-only diffs.
|
||||
|
||||
|
||||
|
||||
@@ -53,6 +53,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
|
||||
@@ -3,10 +3,13 @@ package org.rostislav.quickdrop.config;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.rostislav.quickdrop.service.ApplicationSettingsService;
|
||||
import org.rostislav.quickdrop.service.SessionService;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
@ControllerAdvice
|
||||
public class GlobalControllerAdvice {
|
||||
|
||||
@@ -43,5 +46,10 @@ public class GlobalControllerAdvice {
|
||||
model.addAttribute("canUseKeepIndefinitely", !keepIndefinitelyAdminOnly || hasAdminSession);
|
||||
model.addAttribute("isHideFromListAdminOnly", hideFromListAdminOnly);
|
||||
model.addAttribute("canHideFromList", !hideFromListAdminOnly || hasAdminSession);
|
||||
Locale activeLocale = LocaleContextHolder.getLocale();
|
||||
String currentLang = activeLocale == null || activeLocale.getLanguage() == null || activeLocale.getLanguage().isBlank()
|
||||
? "en"
|
||||
: activeLocale.getLanguage();
|
||||
model.addAttribute("currentLang", currentLang);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package org.rostislav.quickdrop.config;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
|
||||
import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
||||
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
|
||||
@Configuration
|
||||
public class I18nConfig implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public MessageSource messageSource() {
|
||||
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
|
||||
messageSource.setBasename("classpath:messages");
|
||||
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
|
||||
messageSource.setFallbackToSystemLocale(false);
|
||||
messageSource.setUseCodeAsDefaultMessage(true);
|
||||
return messageSource;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocaleResolver localeResolver() {
|
||||
SessionLocaleResolver localeResolver = new SessionLocaleResolver();
|
||||
localeResolver.setDefaultLocale(Locale.ENGLISH);
|
||||
return localeResolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocaleChangeInterceptor localeChangeInterceptor() {
|
||||
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
|
||||
interceptor.setParamName("lang");
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocalValidatorFactoryBean validator(MessageSource messageSource) {
|
||||
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
|
||||
factoryBean.setValidationMessageSource(messageSource);
|
||||
return factoryBean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(localeChangeInterceptor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Validator getValidator() {
|
||||
return validator(messageSource());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
package org.rostislav.quickdrop.model;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.rostislav.quickdrop.entity.ApplicationSettingsEntity;
|
||||
|
||||
public class ApplicationSettingsViewModel {
|
||||
private Long id;
|
||||
|
||||
@Min(value = 1, message = "{validation.number.positive}")
|
||||
private long maxFileSize;
|
||||
@Min(value = 1, message = "{validation.number.positive}")
|
||||
private long maxFileLifeTime;
|
||||
@NotBlank(message = "{validation.required}")
|
||||
private String fileStoragePath;
|
||||
private String logStoragePath;
|
||||
@NotBlank(message = "{validation.required}")
|
||||
private String fileDeletionCron;
|
||||
private boolean appPasswordEnabled;
|
||||
private String appPassword;
|
||||
@Min(value = 0, message = "{validation.number.nonNegative}")
|
||||
private long sessionLifeTime;
|
||||
private boolean isFileListPageEnabled;
|
||||
private boolean isAdminDashboardButtonEnabled;
|
||||
@@ -19,6 +26,7 @@ public class ApplicationSettingsViewModel {
|
||||
private boolean disableUploadPassword;
|
||||
private boolean disablePreview;
|
||||
private boolean metadataStrippingEnabled;
|
||||
@Min(value = 1, message = "{validation.number.positive}")
|
||||
private long maxPreviewSizeBytes;
|
||||
private String defaultHomePage;
|
||||
private boolean keepIndefinitelyAdminOnly;
|
||||
|
||||
@@ -15,12 +15,16 @@ import java.util.concurrent.*;
|
||||
|
||||
import static org.rostislav.quickdrop.util.DataValidator.safeString;
|
||||
|
||||
import org.springframework.context.MessageSource;
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
|
||||
@Service
|
||||
public class NotificationService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(NotificationService.class);
|
||||
private static final long DEFAULT_FLUSH_POLL_SECONDS = 60;
|
||||
private static final RestTemplate REST_TEMPLATE = new RestTemplate();
|
||||
private final ApplicationSettingsService applicationSettingsService;
|
||||
private final MessageSource messageSource;
|
||||
private final Queue<String> pendingMessages = new ConcurrentLinkedQueue<>();
|
||||
private final Object schedulerLock = new Object();
|
||||
private final ExecutorService notificationExecutor = Executors.newSingleThreadExecutor(r -> {
|
||||
@@ -34,14 +38,13 @@ public class NotificationService {
|
||||
private volatile long lastFlushEpochMillis = System.currentTimeMillis();
|
||||
private ScheduledExecutorService scheduler;
|
||||
|
||||
|
||||
public NotificationService(ApplicationSettingsService applicationSettingsService) {
|
||||
public NotificationService(ApplicationSettingsService applicationSettingsService, MessageSource messageSource) {
|
||||
this.applicationSettingsService = applicationSettingsService;
|
||||
startSchedulerIfNeeded(shouldSendDiscord(), shouldSendEmail());
|
||||
this.messageSource = messageSource;
|
||||
}
|
||||
|
||||
public void notifyFileAction(FileEntity fileEntity, FileHistoryType type) {
|
||||
if (fileEntity == null) {
|
||||
public void notifyFileAction(FileEntity file, FileHistoryType type) {
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,10 +62,10 @@ public class NotificationService {
|
||||
case DELETION -> "deleted";
|
||||
};
|
||||
|
||||
String summary = "File '" + fileEntity.name + "' (" + fileEntity.uuid + ") was " + event + ".";
|
||||
String summary = "File '" + file.name + "' (" + file.uuid + ") was " + event + ".";
|
||||
StringBuilder detailsBuilder = new StringBuilder();
|
||||
if (type == FileHistoryType.UPLOAD) {
|
||||
detailsBuilder.append("Size: ").append(fileEntity.size).append(" bytes");
|
||||
detailsBuilder.append("Size: ").append(file.size).append(" bytes");
|
||||
}
|
||||
String details = detailsBuilder.toString();
|
||||
String formattedMessage = details.isBlank() ? summary : summary + "\n---\n" + details;
|
||||
@@ -106,15 +109,16 @@ public class NotificationService {
|
||||
public NotificationTestResult sendTestDiscord() {
|
||||
String url = safeString(applicationSettingsService.getDiscordWebhookUrl());
|
||||
if (url.isBlank()) {
|
||||
return NotificationTestResult.failure("Discord webhook URL is not configured.");
|
||||
return NotificationTestResult.failure(messageSource.getMessage("page.settings.notifications.discord.missingUrl", null, "Discord webhook URL is not configured.", LocaleContextHolder.getLocale()));
|
||||
}
|
||||
|
||||
try {
|
||||
REST_TEMPLATE.postForEntity(url, Map.of("content", "QuickDrop notification test (Discord)"), Void.class);
|
||||
return NotificationTestResult.success("Discord test notification sent.");
|
||||
return NotificationTestResult.success(messageSource.getMessage("page.settings.notifications.discord.success", null, "Discord test notification sent.", LocaleContextHolder.getLocale()));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Discord test notification failed: {}", e.getMessage());
|
||||
return NotificationTestResult.failure("Discord test failed: " + summarizeReason(e.getMessage()));
|
||||
String errorMsg = messageSource.getMessage("page.settings.notifications.discord.failed", new Object[]{summarizeReason(e.getMessage())}, "Discord test failed: " + summarizeReason(e.getMessage()), LocaleContextHolder.getLocale());
|
||||
return NotificationTestResult.failure(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +150,7 @@ public class NotificationService {
|
||||
String from = safeString(applicationSettingsService.getEmailFrom());
|
||||
String[] recipients = parseRecipients();
|
||||
if (mailSender == null || from.isBlank() || recipients.length == 0) {
|
||||
return NotificationTestResult.failure("Email settings incomplete (host/from/recipients).");
|
||||
return NotificationTestResult.failure(messageSource.getMessage("page.settings.notifications.email.incomplete", null, "Email settings incomplete (host/from/recipients).", LocaleContextHolder.getLocale()));
|
||||
}
|
||||
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
@@ -157,10 +161,11 @@ public class NotificationService {
|
||||
helper.setText("This is a QuickDrop notification test email. If you see this, SMTP settings are working.");
|
||||
|
||||
mailSender.send(message);
|
||||
return NotificationTestResult.success("Email test notification sent.");
|
||||
return NotificationTestResult.success(messageSource.getMessage("page.settings.notifications.email.success", null, "Email test notification sent.", LocaleContextHolder.getLocale()));
|
||||
} catch (Exception e) {
|
||||
logger.warn("Email test notification failed: {}", e.getMessage());
|
||||
return NotificationTestResult.failure("Email test failed: " + summarizeReason(e.getMessage()));
|
||||
String errorMsg = messageSource.getMessage("page.settings.notifications.email.failed", new Object[]{summarizeReason(e.getMessage())}, "Email test failed: " + summarizeReason(e.getMessage()), LocaleContextHolder.getLocale());
|
||||
return NotificationTestResult.failure(errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -157,7 +157,6 @@ public class FileUtils {
|
||||
|
||||
public static String guessContentType(String fileName, boolean isImage, boolean isText, boolean isPdf) {
|
||||
if (isImage) {
|
||||
if (fileName.toLowerCase().endsWith(".svg")) return "image/svg+xml";
|
||||
if (fileName.toLowerCase().endsWith(".webp")) return "image/webp";
|
||||
if (fileName.toLowerCase().endsWith(".gif")) return "image/gif";
|
||||
if (fileName.toLowerCase().endsWith(".png")) return "image/png";
|
||||
@@ -167,11 +166,6 @@ public class FileUtils {
|
||||
return "application/pdf";
|
||||
}
|
||||
if (isText) {
|
||||
if (fileName.toLowerCase().endsWith(".json")) return "application/json";
|
||||
if (fileName.toLowerCase().endsWith(".xml")) return "application/xml";
|
||||
if (fileName.toLowerCase().endsWith(".csv")) return "text/csv";
|
||||
if (fileName.toLowerCase().endsWith(".tsv")) return "text/tab-separated-values";
|
||||
if (fileName.toLowerCase().endsWith(".md")) return "text/markdown";
|
||||
return "text/plain; charset=UTF-8";
|
||||
}
|
||||
return "application/octet-stream";
|
||||
|
||||
@@ -0,0 +1,358 @@
|
||||
# ==========================================================
|
||||
# QuickDrop i18n catalog (English source of truth)
|
||||
#
|
||||
# Key style:
|
||||
# - Use flat dotted keys.
|
||||
# - Prefix by domain/page: nav.*, page.upload.*, page.settings.*, form.*
|
||||
# - Reuse common keys only when meaning is identical.
|
||||
# - Do not embed HTML in values.
|
||||
# ==========================================================
|
||||
# ------------------------------
|
||||
# Common reusable labels
|
||||
# ------------------------------
|
||||
button.cancel=Cancel
|
||||
button.upload=Upload
|
||||
button.delete=Delete
|
||||
button.edit=Edit
|
||||
button.search=Search
|
||||
button.selectFile=Select file
|
||||
button.selectFolder=Select folder
|
||||
button.copy=Copy
|
||||
button.confirm=Confirm
|
||||
form.description.placeholder=Description
|
||||
form.password.optional=Password (Optional)
|
||||
# ------------------------------
|
||||
# Navigation
|
||||
# ------------------------------
|
||||
nav.viewFiles=View Files
|
||||
nav.pastebin=Pastebin
|
||||
nav.adminDashboard=Admin Dashboard
|
||||
nav.logout=Logout
|
||||
nav.poweredBy=Powered by QuickDrop
|
||||
nav.toggleNavigation=Toggle navigation
|
||||
# ------------------------------
|
||||
# Page: Upload
|
||||
# ------------------------------
|
||||
# Translator note: {0} is the configured app name.
|
||||
page.upload.title.browser={0} - Upload
|
||||
page.upload.title=Upload a File or Folder
|
||||
page.upload.acceptedTypes=Accepted file types: any
|
||||
page.upload.maxSizeLabel=Max size
|
||||
page.upload.dropzone.title=Drop file or folder
|
||||
page.upload.encryptionNotice=All password-protected files are also encrypted for additional security.
|
||||
page.upload.metadataNotice=Metadata stripping is enabled. Supported:
|
||||
page.upload.validation.fileExceedsLimit=File exceeds the {0} limit.
|
||||
page.upload.validation.folderExceedsLimit=Folder exceeds the {0} limit.
|
||||
page.upload.processingFolder=Processing folder...
|
||||
page.upload.folderSelected=Folder selected: {0} ({1} items)
|
||||
page.upload.noCandidate=No upload candidate is available.
|
||||
form.keepIndefinitely=Keep indefinitely
|
||||
# Translator note: split around numeric max days value in template.
|
||||
form.keepIndefinitely.help.beforeDays=If checked, this file will not be auto-deleted after
|
||||
form.keepIndefinitely.help.afterDays=days.
|
||||
form.hideFromList=Hide from file list
|
||||
form.hideFromList.help=If checked, this file will not appear on the View Files page.
|
||||
upload.warning.privacyNotice=All known metadata will be removed, but this format may still contain details that give information about your identity.
|
||||
upload.warning.technicalInfo=Technical info
|
||||
# ------------------------------
|
||||
# Page: Settings (reserved for migration)
|
||||
# ------------------------------
|
||||
# ------------------------------
|
||||
# Errors
|
||||
# ------------------------------
|
||||
# ------------------------------
|
||||
# Notifications
|
||||
# ------------------------------
|
||||
notification.upload.failed=Upload failed. Please try again.
|
||||
notification.upload.noFileInfo=Upload finished but no file information was returned from the server.
|
||||
notification.selection.required=Select a file or folder to upload first.
|
||||
notification.folder.processingFailed=Unable to prepare folder for upload.
|
||||
notification.copied=Copied
|
||||
notification.failed=Failed
|
||||
# ------------------------------
|
||||
# Validation (shared)
|
||||
# ------------------------------
|
||||
validation.required=This field is required.
|
||||
validation.number.positive=Enter a value greater than 0.
|
||||
# ------------------------------
|
||||
# Phase 3 incremental migration keys
|
||||
# ------------------------------
|
||||
nav.uploadFile=Upload File
|
||||
nav.settings=Settings
|
||||
nav.dashboard=Dashboard
|
||||
button.submit=Submit
|
||||
button.view=View
|
||||
button.history=History
|
||||
button.download=Download
|
||||
form.password.label=Password
|
||||
form.password.placeholder=Password
|
||||
form.password.file.placeholder=Enter the file password...
|
||||
form.adminPassword.label=Admin Password
|
||||
form.adminPassword.placeholder=Admin Password
|
||||
page.settings.title.browser={0} Admin Settings
|
||||
page.settings.adminTitle=Admin Settings
|
||||
page.password.title.browser={0} - Password
|
||||
page.password.enterFile=Enter Password
|
||||
page.password.enterAppPrompt=Enter the app password to continue.
|
||||
page.password.protectedLabel=Protected
|
||||
page.adminPassword.title.browser={0} - Admin Login
|
||||
page.adminPassword.title=Admin Password Required
|
||||
page.files.title.browser={0} - Files
|
||||
page.files.title=All files
|
||||
page.files.search.label=Search files
|
||||
page.files.search.placeholder=Search files by name or date...
|
||||
page.files.empty.title=No files have been uploaded yet.
|
||||
page.files.empty.startBy=Start by
|
||||
page.files.empty.uploadLink=uploading a file
|
||||
page.files.goToFilePage=Go to file page
|
||||
pagination.previous=Previous
|
||||
pagination.next=Next
|
||||
pagination.perPage=Per page
|
||||
pagination.page=Page
|
||||
pagination.of=of
|
||||
page.fileView.title.browser={0} - File View
|
||||
page.fileView.title=File View
|
||||
page.dashboard.title.browser={0} Admin
|
||||
page.dashboard.title=Admin Dashboard
|
||||
page.dashboard.analytics=Analytics
|
||||
page.dashboard.files=Files
|
||||
page.dashboard.totalDownloads=Total Downloads
|
||||
page.dashboard.excludingDeleted=Excluding deleted files
|
||||
page.dashboard.totalSpaceUsed=Total Space Used
|
||||
page.dashboard.averageFileSize=Average File Size
|
||||
page.dashboard.search.label=Search files
|
||||
page.dashboard.search.placeholder=Search by name, description, or UUID
|
||||
table.name=Name
|
||||
table.uploadDateLastRenewed=Upload Date/Last Renewed
|
||||
table.size=Size
|
||||
table.downloads=Downloads
|
||||
page.error.title.browser={0} - Error
|
||||
page.error.title=Oops!
|
||||
page.error.message=Something went wrong. Please try again later.
|
||||
page.error.goHome=Go Back to Main Page
|
||||
# ------------------------------
|
||||
# Phase 5 validation + backend messages
|
||||
# ------------------------------
|
||||
validation.number.nonNegative=Enter a value greater than or equal to 0.
|
||||
validation.cron.invalid=Invalid cron expression.
|
||||
validation.cron.invalid.example=Invalid cron expression. Please enter a valid cron pattern (e.g., 0 0 2 * * * for 2:00 AM daily).
|
||||
validation.cron.nextRun.none=No upcoming run.
|
||||
validation.cron.unableToParse=Unable to parse cron.
|
||||
notification.settings.saved=Settings saved.
|
||||
notification.target.unknown=Unknown notification target.
|
||||
# ------------------------------
|
||||
# Remaining page migration keys
|
||||
# ------------------------------
|
||||
nav.language=Language
|
||||
page.welcome.title.browser=Welcome to {0}
|
||||
page.welcome.title=Welcome to {0}
|
||||
page.welcome.setupThanks=Thank you for setting up {0}!
|
||||
page.welcome.setupPrompt=Please set an admin password for the dashboard.
|
||||
page.welcome.passwordMismatch=Passwords do not match.
|
||||
page.welcome.setAdminPassword=Set Admin Password
|
||||
form.password.confirm=Confirm Password
|
||||
form.password.confirm.placeholder=Confirm Password
|
||||
page.history.title.browser={0} Admin - History
|
||||
page.history.title=History
|
||||
page.history.downloadHistory=Download History
|
||||
page.history.fileDetails=File Details
|
||||
page.history.size=Size
|
||||
page.history.uploadDate=Upload Date
|
||||
page.history.totalDownloads=Total Downloads
|
||||
page.history.date=Date
|
||||
page.history.action=Action
|
||||
page.history.ipAddress=IP Address
|
||||
page.history.userAgent=User Agent
|
||||
page.history.noDescription=No description provided
|
||||
page.share.title.browser={0} - Shared File
|
||||
page.share.title=Shared File
|
||||
page.share.uploaded=Uploaded
|
||||
page.share.fileSize=File Size
|
||||
page.invalidShare.title.browser={0} - Invalid Link
|
||||
page.invalidShare.title=Link Expired
|
||||
page.invalidShare.message=This share link is no longer valid. The file you are trying to access has expired or the link has been used.
|
||||
page.invalidShare.backHome=Return to Homepage
|
||||
page.paste.title.browser={0} - Pastebin
|
||||
page.paste.newTitle=New Paste
|
||||
page.paste.editTitle=Edit Paste
|
||||
page.paste.field.title=Title
|
||||
page.paste.field.mode=Mode
|
||||
page.paste.mode.markdown=Markdown (.md)
|
||||
page.paste.mode.text=Plain Text (.txt)
|
||||
page.paste.lineNumbers=Line numbers
|
||||
page.paste.wordWrap=Word wrap
|
||||
page.paste.fontSize=Font size
|
||||
page.paste.preview=Preview
|
||||
page.paste.content.placeholder=Write your text here...
|
||||
page.paste.password.label=Password (optional)
|
||||
page.paste.password.placeholder=Set password to protect this paste
|
||||
page.paste.password.help=Leaving this empty removes password protection for the saved paste.
|
||||
page.paste.save=Save Paste
|
||||
page.paste.saveChanges=Save Changes
|
||||
page.pasteView.title.browser={0} - Paste View
|
||||
page.pasteView.untitled=Untitled paste
|
||||
page.pasteView.copyText=Copy text
|
||||
page.pasteView.raw=Raw
|
||||
page.pasteView.markdown=Markdown
|
||||
page.pasteView.renderedHint=Rendered markdown is enabled for this paste.
|
||||
page.pasteView.share=Share
|
||||
page.pasteView.generateLink=Generate link
|
||||
page.pasteView.copyShareLink=Copy share link
|
||||
page.pasteView.generateLinkFailed=Unable to generate share link
|
||||
page.fileView.keepIndefinitely=Keep indefinitely:
|
||||
page.fileView.hideFromList=Hide file from list:
|
||||
page.fileView.fileSize=File Size:
|
||||
page.fileView.folderContents=Folder contents
|
||||
page.fileView.editPaste=Edit Paste
|
||||
page.fileView.preview=Preview
|
||||
page.fileView.share=Share
|
||||
page.fileView.shareBypassNotice=Share links always bypass file and app passwords.
|
||||
page.fileView.daysValid=Days valid
|
||||
page.fileView.allowedDownloads=Allowed downloads
|
||||
page.fileView.autoShareLink=Automatic share link
|
||||
page.fileView.uploadedAt=Uploaded at:
|
||||
page.fileView.uploadedRenewedAt=Uploaded/Renewed at:
|
||||
page.fileView.retention.beforeDays=Files are kept only for
|
||||
page.fileView.retention.afterDays=days after this date.
|
||||
page.fileView.preparingDownload=Your file is being prepared for download. Please wait...
|
||||
page.fileView.renewLifetime=Renew File Lifetime
|
||||
page.fileView.deleteFile=Delete File
|
||||
page.fileView.loadPreview=Load preview ({0} MB limit)
|
||||
page.fileView.preview.clickToLoad=Click Load preview to fetch.
|
||||
page.fileView.preview.loading=Loading preview...
|
||||
page.fileView.shareAdvancedHint=Add an expiration date and/or download limit, or choose unlimited to keep the link open-ended.
|
||||
page.fileView.shareSimplifiedHint=Simplified share links are enabled: a non-expiring link with unlimited downloads is generated automatically.
|
||||
page.fileView.noExpiration=No expiration (unlimited time)
|
||||
page.fileView.unlimitedDownloads=Unlimited downloads
|
||||
page.fileView.sharePlaceholder=Generate a share link that bypasses passwords
|
||||
page.fileView.shareDisabled=Share links are disabled by the administrator.
|
||||
page.fileView.daysValidNonNegative=Days valid cannot be negative.
|
||||
page.fileView.allowedDownloadsNonNegative=Allowed downloads cannot be negative.
|
||||
page.fileView.shareGenerateFailed=Failed to generate share link.
|
||||
page.fileView.preview.unavailable=Preview unavailable.
|
||||
page.fileView.preview.imageAlt=Preview
|
||||
page.fileView.preview.openInNewTab=Open {0} in a new tab
|
||||
page.fileView.preview.jsonFormatted=Formatted
|
||||
page.fileView.preview.jsonTree=Tree
|
||||
page.fileView.preview.csvNoRows=No rows to display.
|
||||
page.fileView.preview.csvShowingFirst=Showing first {0} rows out of {1}.
|
||||
page.fileView.preview.codeTruncated=... (truncated)
|
||||
page.fileView.folderFallbackName=folder
|
||||
page.fileView.folderNoManifest=No manifest available.
|
||||
page.fileView.folderRenderFailed=Unable to render folder contents.
|
||||
page.dashboard.keepIndefinitely=Keep indefinitely
|
||||
page.dashboard.hidden=Hidden
|
||||
page.dashboard.noFilesFound=No files found.
|
||||
page.dashboard.deleteConfirm=Are you sure you want to delete this file? This action cannot be undone.
|
||||
page.settings.branding.title=Branding
|
||||
page.settings.branding.appName=Application name
|
||||
page.settings.branding.appName.placeholder=My Brand
|
||||
page.settings.branding.appName.help=Shown in the navbar, page titles, and share views. Leave empty to reset to QuickDrop.
|
||||
page.settings.branding.logo=Logo / favicon
|
||||
page.settings.branding.logo.browse=Browse
|
||||
page.settings.branding.logo.reset=Reset to default logo
|
||||
page.settings.branding.logo.resetPending=Default logo will be restored on save.
|
||||
page.settings.branding.logo.help=Upload a square image for best results. Leave empty to keep the current logo or reset to restore the QuickDrop icon.
|
||||
page.settings.file.title=File Settings
|
||||
page.settings.file.maxSizeMb=Max File Size (MB)
|
||||
page.settings.file.maxLifetimeDays=Max File Lifetime (days)
|
||||
page.settings.file.storagePath=File Storage Path
|
||||
page.settings.file.disablePreview=Disable file previews
|
||||
page.settings.file.disablePreview.help=Hide inline previews for images/text on the file page.
|
||||
page.settings.file.maxPreviewMb=Max preview size (MB)
|
||||
page.settings.file.maxPreviewMb.help=Above this size users must click "Load preview".
|
||||
page.settings.file.keepIndefinitely.title=Keep indefinitely
|
||||
page.settings.file.keepIndefinitely.adminOnly=Require admin session for "Keep indefinitely"
|
||||
page.settings.file.keepIndefinitely.adminOnly.help=When enabled, only admins with an active session can mark uploads to keep indefinitely.
|
||||
page.settings.file.hideFromList.title=Hide from list
|
||||
page.settings.file.hideFromList.adminOnly=Require admin session to hide files
|
||||
page.settings.file.hideFromList.adminOnly.help=When enabled, only admins can set or change "Hide from file list".
|
||||
page.settings.system.title=System Settings
|
||||
page.settings.system.sessionLifetimeMinutes=Session Lifetime (minutes)
|
||||
page.settings.system.sessionLifetimeMinutes.help=This impacts how long file and admin sessions are kept.
|
||||
page.settings.system.defaultHomePage=Default Home Page
|
||||
page.settings.system.defaultHomePage.upload=Upload Page
|
||||
page.settings.system.defaultHomePage.list=File List Page
|
||||
page.settings.system.defaultHomePage.paste=Pastebin Page
|
||||
page.settings.system.fileDeletionCron=File Deletion Cron Expression
|
||||
page.settings.system.nextRun=Next run
|
||||
page.settings.system.fileList=File List
|
||||
page.settings.system.fileList.enabled=Enable File List Page
|
||||
page.settings.system.adminDashboard=Admin Dashboard
|
||||
page.settings.system.adminDashboard.showButton=Show Admin Dashboard Button
|
||||
page.settings.system.adminDashboard.directUrl=Still available at /admin/dashboard
|
||||
page.settings.system.pastebin=Pastebin
|
||||
page.settings.system.pastebin.enabled=Enable Pastebin Page
|
||||
page.settings.system.pastebin.help=Adds a text editor tab and allows selecting Pastebin as default home page.
|
||||
page.settings.notifications.title=Notification Settings
|
||||
page.settings.notifications.discord.title=Discord Webhook
|
||||
page.settings.notifications.discord.help=Post file activity to a Discord channel.
|
||||
page.settings.notifications.discord.webhookUrl=Webhook URL
|
||||
page.settings.notifications.discord.webhookUrl.placeholder=https://discord.com/api/webhooks/...
|
||||
page.settings.notifications.discord.testButton=Send Test to Discord
|
||||
page.settings.notifications.email.title=Email Notifications
|
||||
page.settings.notifications.email.help=Send emails on upload, download, and renewal events.
|
||||
page.settings.notifications.email.fromAddress=From Address
|
||||
page.settings.notifications.email.fromAddress.placeholder=noreply@example.com
|
||||
page.settings.notifications.email.recipients=Recipients (comma separated)
|
||||
page.settings.notifications.email.recipients.placeholder=admin@example.com, alerts@example.com
|
||||
page.settings.notifications.email.smtpHost=SMTP Host
|
||||
page.settings.notifications.email.smtpHost.placeholder=smtp.example.com
|
||||
page.settings.notifications.email.smtpPort=SMTP Port
|
||||
page.settings.notifications.email.smtpUsername=SMTP Username
|
||||
page.settings.notifications.email.smtpPassword=SMTP Password
|
||||
page.settings.notifications.email.useStartTls=Use STARTTLS
|
||||
page.settings.notifications.email.useImplicitSsl=Use Implicit SSL (SMTPS)
|
||||
page.settings.notifications.email.tlsHelp=Use STARTTLS with port 587. Use Implicit SSL with port 465.
|
||||
page.settings.notifications.email.testButton=Send Test Email
|
||||
page.settings.notifications.batch.title=Batch notifications
|
||||
page.settings.notifications.batch.help=Pool events and send every N minutes. Requires at least one channel enabled.
|
||||
page.settings.notifications.batch.interval=Batch interval (minutes)
|
||||
page.settings.notifications.testInProgress=Testing...
|
||||
page.settings.notifications.requestFailed=Request failed. See logs.
|
||||
page.settings.security.title=Security Settings
|
||||
page.settings.security.appPasswordEnabled=Enable Password Protection
|
||||
page.settings.security.appPasswordEnabled.help=Protect the whole app with a password
|
||||
page.settings.security.appPassword=App Password
|
||||
page.settings.security.disableUploadPasswords=Disable upload passwords
|
||||
page.settings.security.disableUploadPasswords.help=Prevents users from setting passwords on uploads (and disables per-file encryption).
|
||||
page.settings.security.disableShareLinks=Disable share links
|
||||
page.settings.security.disableShareLinks.help=Hide share options on file pages and block new share links from being generated.
|
||||
page.settings.security.simplifyShareLinks=Simplify share links
|
||||
page.settings.security.simplifyShareLinks.help=Auto-generate an unlimited, non-expiring share link and hide manual share options on file pages.
|
||||
page.settings.security.disableFileEncryption=Disable File Encryption
|
||||
page.settings.security.disableFileEncryption.help=If checked, files will not be encrypted even if file password protection is enabled.
|
||||
page.settings.security.metadataStripping=Metadata stripping
|
||||
page.settings.security.metadataStripping.help=Strip optional file metadata where possible.
|
||||
page.settings.save=Save Settings
|
||||
page.settings.about.title=About {0}
|
||||
page.settings.about.version=Version
|
||||
page.settings.about.database=Database
|
||||
page.settings.about.database.sqlite=SQLite
|
||||
page.settings.about.javaVersion=Java Version
|
||||
page.settings.about.osInfo=OS Info
|
||||
page.settings.about.unknown=Unknown
|
||||
page.settings.about.notAvailable=N/A
|
||||
validation.settings.maxFileSize=Enter a max file size (MB) greater than 0.
|
||||
validation.settings.maxFileLifetime=Enter a max file lifetime (days) greater than 0.
|
||||
validation.settings.fileStoragePathRequired=File storage path is required.
|
||||
validation.settings.cronRequired=Cron expression is required.
|
||||
validation.settings.sessionLifetime=Enter a session lifetime in minutes (positive number).
|
||||
validation.settings.previewSize=Enter preview size (MB) greater than 0.
|
||||
validation.settings.defaultHomePage=Choose upload, list, or paste.
|
||||
validation.settings.appPasswordRequired=App password is required when protection is enabled.
|
||||
validation.settings.discordWebhook=Enter a valid Discord webhook URL.
|
||||
validation.settings.emailFrom=Enter a valid From email.
|
||||
validation.settings.emailRecipients=Enter at least one recipient.
|
||||
validation.settings.smtpHost=SMTP host is required.
|
||||
validation.settings.smtpPort=Enter a valid SMTP port.
|
||||
validation.settings.enableChannelBeforeBatch=Enable Discord or Email before batching.
|
||||
validation.settings.batchInterval=Enter a batch interval in minutes.
|
||||
validation.settings.failed=Validation failed
|
||||
page.settings.notifications.discord.missingUrl=Discord webhook URL is not configured.
|
||||
page.settings.notifications.discord.success=Discord test notification sent.
|
||||
page.settings.notifications.discord.failed=Discord test failed: {0}
|
||||
page.settings.notifications.email.incomplete=Email settings incomplete (host/from/recipients).
|
||||
page.settings.notifications.email.success=Email test notification sent.
|
||||
page.settings.notifications.email.failed=Email test failed: {0}
|
||||
page.settings.notifications.testFailed=Request failed. See logs.
|
||||
@@ -0,0 +1,335 @@
|
||||
# Common reusable labels
|
||||
button.cancel=Отказ
|
||||
button.upload=Качи
|
||||
button.delete=Изтрий
|
||||
button.edit=Редактирай
|
||||
button.search=Търси
|
||||
button.selectFile=Избери файл
|
||||
button.selectFolder=Избери папка
|
||||
button.copy=Копирай
|
||||
button.confirm=Потвърди
|
||||
form.description.placeholder=Описание
|
||||
form.password.optional=Парола (по избор)
|
||||
# Navigation
|
||||
nav.viewFiles=Преглед на файлове
|
||||
nav.pastebin=Pastebin
|
||||
nav.adminDashboard=Админ табло
|
||||
nav.logout=Изход
|
||||
nav.poweredBy=Powered by QuickDrop
|
||||
nav.uploadFile=Качи файл
|
||||
nav.settings=Настройки
|
||||
nav.dashboard=Табло
|
||||
nav.toggleNavigation=Превключване на навигацията
|
||||
# Page: Upload
|
||||
page.upload.title.browser={0} - Качване
|
||||
page.upload.title=Качване на файл или папка
|
||||
page.upload.acceptedTypes=Приети типове файлове: всички
|
||||
page.upload.maxSizeLabel=Макс. размер
|
||||
page.upload.dropzone.title=Пуснете файл или папка
|
||||
page.upload.encryptionNotice=Всички файлове с парола също се криптират за допълнителна сигурност.
|
||||
page.upload.metadataNotice=Премахването на метаданни е включено. Поддържани:
|
||||
page.upload.retention.standard=Файловете се изтриват след {0} дни, освен ако не е избрано „Запази безсрочно“.
|
||||
page.upload.retention.adminOnly=Файловете се изтриват след {0} дни.
|
||||
page.upload.status.started=Качването започна...
|
||||
page.upload.validation.fileExceedsLimit=Файлът надвишава лимита {0}.
|
||||
page.upload.validation.folderExceedsLimit=Папката надвишава лимита {0}.
|
||||
page.upload.processingFolder=Обработване на папка...
|
||||
page.upload.folderSelected=Избрана папка: {0} ({1} елемента)
|
||||
page.upload.noCandidate=Няма наличен кандидат за качване.
|
||||
form.keepIndefinitely=Запази безсрочно
|
||||
form.keepIndefinitely.help.beforeDays=Ако е отметнато, файлът няма да бъде изтрит автоматично след
|
||||
form.keepIndefinitely.help.afterDays=дни.
|
||||
form.hideFromList=Скрий от списъка
|
||||
form.hideFromList.help=Ако е отметнато, файлът няма да се показва на страницата View Files.
|
||||
page.files.keepIndefinitely=Запази безсрочно:
|
||||
page.files.passwordProtected=Защитено с парола:
|
||||
upload.warning.privacyNotice=Всички познати метаданни ще бъдат премахнати, но този формат може да съдържа данни, които да разкрият самоличността ви.
|
||||
upload.warning.technicalInfo=Техническа информация
|
||||
# Page: Settings
|
||||
page.settings.title.browser={0} Админ настройки
|
||||
page.settings.adminTitle=Админ настройки
|
||||
# Errors
|
||||
# Notifications
|
||||
notification.upload.failed=Качването не бе успешно. Моля, опитайте отново.
|
||||
notification.upload.noFileInfo=Качването приключи, но сървърът не върна информация за файла.
|
||||
notification.selection.required=Първо изберете файл или папка за качване.
|
||||
notification.folder.processingFailed=Папката не може да бъде подготвена за качване.
|
||||
notification.copied=Копирано
|
||||
notification.failed=Неуспешно
|
||||
notification.settings.saved=Настройките са запазени.
|
||||
notification.target.unknown=Непозната цел за известяване.
|
||||
# Validation
|
||||
validation.required=Това поле е задължително.
|
||||
validation.number.positive=Въведете стойност по-голяма от 0.
|
||||
validation.number.nonNegative=Въведете стойност по-голяма или равна на 0.
|
||||
validation.cron.invalid=Невалиден cron израз.
|
||||
validation.cron.invalid.example=Невалиден cron израз. Моля, въведете валиден шаблон (например 0 0 2 * * * за 2:00 всеки ден).
|
||||
validation.cron.nextRun.none=Няма предстоящо изпълнение.
|
||||
validation.cron.unableToParse=Неуспешно разчитане на cron израз.
|
||||
# Phase 3 incremental migration keys
|
||||
button.submit=Изпрати
|
||||
button.view=Преглед
|
||||
button.history=История
|
||||
button.download=Свали
|
||||
form.password.label=Парола
|
||||
form.password.placeholder=Парола
|
||||
form.password.file.placeholder=Въведете паролата за файла...
|
||||
form.adminPassword.label=Админ парола
|
||||
form.adminPassword.placeholder=Админ парола
|
||||
page.password.title.browser={0} - Парола
|
||||
page.password.enterFile=Въведете парола
|
||||
page.password.enterAppPrompt=Въведете паролата за приложението, за да продължите.
|
||||
page.password.protectedLabel=Защитено
|
||||
page.adminPassword.title.browser={0} - Админ вход
|
||||
page.adminPassword.title=Изисква се админ парола
|
||||
page.files.title.browser={0} - Файлове
|
||||
page.files.title=Всички файлове
|
||||
page.files.search.label=Търсене на файлове
|
||||
page.files.search.placeholder=Търсете файлове по име или дата...
|
||||
page.files.empty.title=Все още няма качени файлове.
|
||||
page.files.empty.startBy=Започнете с
|
||||
page.files.empty.uploadLink=качване на файл
|
||||
page.files.goToFilePage=Към страницата на файла
|
||||
pagination.previous=Предишна
|
||||
pagination.next=Следваща
|
||||
pagination.perPage=На страница
|
||||
pagination.page=Страница
|
||||
pagination.of=от
|
||||
page.fileView.title.browser={0} - Изглед на файл
|
||||
page.fileView.title=Изглед на файл
|
||||
page.dashboard.title.browser={0} Админ
|
||||
page.dashboard.title=Админ табло
|
||||
page.dashboard.analytics=Анализ
|
||||
page.dashboard.files=Файлове
|
||||
page.dashboard.totalDownloads=Общо изтегляния
|
||||
page.dashboard.excludingDeleted=Без изтритите файлове
|
||||
page.dashboard.totalSpaceUsed=Общо използвано място
|
||||
page.dashboard.averageFileSize=Среден размер на файл
|
||||
page.dashboard.search.label=Търсене на файлове
|
||||
page.dashboard.search.placeholder=Търсене по име, описание или UUID
|
||||
table.name=Име
|
||||
table.uploadDateLastRenewed=Дата на качване/последно подновяване
|
||||
table.size=Размер
|
||||
table.downloads=Изтегляния
|
||||
page.error.title.browser={0} - Грешка
|
||||
page.error.title=Опа!
|
||||
page.error.message=Нещо се обърка. Моля, опитайте отново по-късно.
|
||||
page.error.goHome=Назад към началната страница
|
||||
# Phase 7 full-page translation keys
|
||||
nav.language=Език
|
||||
page.welcome.title.browser=Добре дошли в {0}
|
||||
page.welcome.title=Добре дошли в {0}
|
||||
page.welcome.setupThanks=Благодарим, че настроихте {0}!
|
||||
page.welcome.setupPrompt=Моля, задайте админ парола за таблото.
|
||||
page.welcome.passwordMismatch=Паролите не съвпадат.
|
||||
page.welcome.setAdminPassword=Задай админ парола
|
||||
form.password.confirm=Потвърди парола
|
||||
form.password.confirm.placeholder=Потвърди парола
|
||||
page.history.title.browser={0} Админ - История
|
||||
page.history.title=История
|
||||
page.history.downloadHistory=История на изтеглянията
|
||||
page.history.fileDetails=Детайли за файла
|
||||
page.history.size=Размер
|
||||
page.history.uploadDate=Дата на качване
|
||||
page.history.totalDownloads=Общо изтегляния
|
||||
page.history.date=Дата
|
||||
page.history.action=Действие
|
||||
page.history.ipAddress=IP адрес
|
||||
page.history.userAgent=Потребителски агент
|
||||
page.history.noDescription=Няма описание
|
||||
page.share.title.browser={0} - Споделен файл
|
||||
page.share.title=Споделен файл
|
||||
page.share.uploaded=Качен
|
||||
page.share.fileSize=Размер на файла
|
||||
page.invalidShare.title.browser={0} - Невалиден линк
|
||||
page.invalidShare.title=Линкът е изтекъл
|
||||
page.invalidShare.message=Този линк за споделяне вече не е валиден. Файлът, до който се опитвате да получите достъп, е изтекъл или линкът вече е използван.
|
||||
page.invalidShare.backHome=Към началната страница
|
||||
page.paste.title.browser={0} - Pastebin
|
||||
page.paste.newTitle=Нова бележка
|
||||
page.paste.editTitle=Редакция на бележка
|
||||
page.paste.field.title=Заглавие
|
||||
page.paste.field.mode=Режим
|
||||
page.paste.mode.markdown=Markdown (.md)
|
||||
page.paste.mode.text=Обикновен текст (.txt)
|
||||
page.paste.lineNumbers=Номера на редове
|
||||
page.paste.wordWrap=Пренасяне на редове
|
||||
page.paste.fontSize=Размер на шрифта
|
||||
page.paste.preview=Преглед
|
||||
page.paste.content.placeholder=Напишете текста тук...
|
||||
page.paste.password.label=Парола (по избор)
|
||||
page.paste.password.placeholder=Задайте парола за защита на тази бележка
|
||||
page.paste.password.help=Ако оставите полето празно, защитата с парола за записа ще бъде премахната.
|
||||
page.paste.save=Запази бележката
|
||||
page.paste.saveChanges=Запази промените
|
||||
page.pasteView.title.browser={0} - Преглед на бележка
|
||||
page.pasteView.untitled=Бележка без заглавие
|
||||
page.pasteView.copyText=Копирай текста
|
||||
page.pasteView.raw=Суров текст
|
||||
page.pasteView.markdown=Markdown
|
||||
page.pasteView.renderedHint=За тази бележка е активиран рендериран markdown.
|
||||
page.pasteView.share=Споделяне
|
||||
page.pasteView.generateLink=Генерирай линк
|
||||
page.pasteView.copyShareLink=Копирай линка
|
||||
page.pasteView.generateLinkFailed=Неуспешно генериране на линк за споделяне
|
||||
page.fileView.keepIndefinitely=Запази безсрочно:
|
||||
page.fileView.hideFromList=Скрий файла от списъка:
|
||||
page.fileView.fileSize=Размер на файла:
|
||||
page.fileView.folderContents=Съдържание на папка
|
||||
page.fileView.editPaste=Редактирай бележката
|
||||
page.fileView.preview=Преглед
|
||||
page.fileView.share=Споделяне
|
||||
page.fileView.shareBypassNotice=Линковете за споделяне винаги заобикалят паролите за файл и приложение.
|
||||
page.fileView.daysValid=Дни валидност
|
||||
page.fileView.allowedDownloads=Разрешени изтегляния
|
||||
page.fileView.autoShareLink=Автоматичен линк за споделяне
|
||||
page.fileView.uploadedAt=Качен на:
|
||||
page.fileView.uploadedRenewedAt=Качен/подновен на:
|
||||
page.fileView.retention.beforeDays=Файловете се пазят само
|
||||
page.fileView.retention.afterDays=дни след тази дата.
|
||||
page.fileView.preparingDownload=Файлът ви се подготвя за изтегляне. Моля, изчакайте...
|
||||
page.fileView.renewLifetime=Поднови живота на файла
|
||||
page.fileView.deleteFile=Изтрий файл
|
||||
page.fileView.loadPreview=Зареди преглед ({0} MB лимит)
|
||||
page.fileView.preview.clickToLoad=Натиснете „Зареди преглед“, за да заредите.
|
||||
page.fileView.preview.loading=Зареждане на преглед...
|
||||
page.fileView.shareAdvancedHint=Добавете дата на изтичане и/или лимит на изтеглянията, или изберете неограничено.
|
||||
page.fileView.shareSimplifiedHint=Опростените линкове за споделяне са активирани: автоматично се генерира неограничаващ линк без изтичане.
|
||||
page.fileView.noExpiration=Без изтичане (неограничено време)
|
||||
page.fileView.unlimitedDownloads=Неограничени изтегляния
|
||||
page.fileView.sharePlaceholder=Генерирайте линк за споделяне, който заобикаля паролите
|
||||
page.fileView.shareDisabled=Линковете за споделяне са деактивирани от администратора.
|
||||
page.fileView.daysValidNonNegative=Дните на валидност не могат да са отрицателни.
|
||||
page.fileView.allowedDownloadsNonNegative=Разрешените изтегляния не могат да са отрицателни.
|
||||
page.fileView.shareGenerateFailed=Неуспешно генериране на линк за споделяне.
|
||||
page.fileView.preview.unavailable=Прегледът не е наличен.
|
||||
page.fileView.preview.imageAlt=Преглед
|
||||
page.fileView.preview.openInNewTab=Отвори {0} в нов раздел
|
||||
page.fileView.preview.jsonFormatted=Форматиран
|
||||
page.fileView.preview.jsonTree=Дърво
|
||||
page.fileView.preview.csvNoRows=Няма редове за показване.
|
||||
page.fileView.preview.csvShowingFirst=Показани са първите {0} от общо {1} реда.
|
||||
page.fileView.preview.codeTruncated=... (съкратено)
|
||||
page.fileView.folderFallbackName=папка
|
||||
page.fileView.folderNoManifest=Няма наличен манифест.
|
||||
page.fileView.folderRenderFailed=Неуспешно визуализиране на съдържанието на папката.
|
||||
page.dashboard.keepIndefinitely=Запази безсрочно
|
||||
page.dashboard.hidden=Скрит
|
||||
page.dashboard.noFilesFound=Няма намерени файлове.
|
||||
page.dashboard.deleteConfirm=Сигурни ли сте, че искате да изтриете този файл? Това действие не може да бъде отменено.
|
||||
page.settings.branding.title=Брандинг
|
||||
page.settings.branding.appName=Име на приложението
|
||||
page.settings.branding.appName.placeholder=Моята марка
|
||||
page.settings.branding.appName.help=Показва се в навигацията, заглавията и изгледите за споделяне. Оставете празно, за да върнете QuickDrop.
|
||||
page.settings.branding.logo=Лого / favicon
|
||||
page.settings.branding.logo.browse=Преглед
|
||||
page.settings.branding.logo.reset=Нулирай към стандартното лого
|
||||
page.settings.branding.logo.resetPending=Стандартното лого ще бъде възстановено при запазване.
|
||||
page.settings.branding.logo.help=Качете квадратно изображение за най-добри резултати. Оставете празно, за да запазите текущото лого.
|
||||
page.settings.file.title=Настройки на файловете
|
||||
page.settings.file.maxSizeMb=Максимален размер на файл (MB)
|
||||
page.settings.file.maxLifetimeDays=Максимален живот на файл (дни)
|
||||
page.settings.file.storagePath=Път за съхранение на файлове
|
||||
page.settings.file.disablePreview=Изключи прегледите на файлове
|
||||
page.settings.file.disablePreview.help=Скрива вградените прегледи за изображения/текст на страницата на файла.
|
||||
page.settings.file.maxPreviewMb=Максимален размер за преглед (MB)
|
||||
page.settings.file.maxPreviewMb.help=Над този размер потребителите трябва да натиснат "Зареди преглед".
|
||||
page.settings.file.keepIndefinitely.title=Запазване безсрочно
|
||||
page.settings.file.keepIndefinitely.adminOnly=Изисквай админ сесия за "Запази безсрочно"
|
||||
page.settings.file.keepIndefinitely.adminOnly.help=Когато е включено, само админи с активна сесия могат да маркират качванията като безсрочни.
|
||||
page.settings.file.hideFromList.title=Скриване от списъка
|
||||
page.settings.file.hideFromList.adminOnly=Изисквай админ сесия за скриване на файлове
|
||||
page.settings.file.hideFromList.adminOnly.help=Когато е включено, само админи могат да променят "Скрий от файловия списък".
|
||||
page.settings.system.title=Системни настройки
|
||||
page.settings.system.sessionLifetimeMinutes=Продължителност на сесията (минути)
|
||||
page.settings.system.sessionLifetimeMinutes.help=Влияе колко дълго се пазят файлови и админ сесии.
|
||||
page.settings.system.defaultHomePage=Начална страница по подразбиране
|
||||
page.settings.system.defaultHomePage.upload=Страница за качване
|
||||
page.settings.system.defaultHomePage.list=Страница със списък на файлове
|
||||
page.settings.system.defaultHomePage.paste=Страница Pastebin
|
||||
page.settings.system.fileDeletionCron=Cron израз за изтриване на файлове
|
||||
page.settings.system.nextRun=Следващо изпълнение
|
||||
page.settings.system.fileList=Списък с файлове
|
||||
page.settings.system.fileList.enabled=Разреши страницата със списък на файлове
|
||||
page.settings.system.adminDashboard=Админ табло
|
||||
page.settings.system.adminDashboard.showButton=Покажи бутон за админ табло
|
||||
page.settings.system.adminDashboard.directUrl=Все още достъпно на /admin/dashboard
|
||||
page.settings.system.pastebin=Pastebin
|
||||
page.settings.system.pastebin.enabled=Разреши страницата Pastebin
|
||||
page.settings.system.pastebin.help=Добавя таб за текстов редактор и позволява избор на Pastebin като начална страница.
|
||||
page.settings.notifications.title=Настройки за известия
|
||||
page.settings.notifications.discord.title=Discord webhook
|
||||
page.settings.notifications.discord.help=Публикува файлова активност в Discord канал.
|
||||
page.settings.notifications.discord.webhookUrl=Webhook URL
|
||||
page.settings.notifications.discord.webhookUrl.placeholder=https://discord.com/api/webhooks/...
|
||||
page.settings.notifications.discord.testButton=Изпрати тест в Discord
|
||||
page.settings.notifications.email.title=Имейл известия
|
||||
page.settings.notifications.email.help=Изпраща имейли при качване, изтегляне и подновяване.
|
||||
page.settings.notifications.email.fromAddress=Адрес подател
|
||||
page.settings.notifications.email.fromAddress.placeholder=noreply@example.com
|
||||
page.settings.notifications.email.recipients=Получатели (разделени със запетая)
|
||||
page.settings.notifications.email.recipients.placeholder=admin@example.com, alerts@example.com
|
||||
page.settings.notifications.email.smtpHost=SMTP хост
|
||||
page.settings.notifications.email.smtpHost.placeholder=smtp.example.com
|
||||
page.settings.notifications.email.smtpPort=SMTP порт
|
||||
page.settings.notifications.email.smtpUsername=SMTP потребител
|
||||
page.settings.notifications.email.smtpPassword=SMTP парола
|
||||
page.settings.notifications.email.useStartTls=Използвай STARTTLS
|
||||
page.settings.notifications.email.useImplicitSsl=Използвай Implicit SSL (SMTPS)
|
||||
page.settings.notifications.email.tlsHelp=Използвайте STARTTLS с порт 587. Използвайте Implicit SSL с порт 465.
|
||||
page.settings.notifications.email.testButton=Изпрати тестов имейл
|
||||
page.settings.notifications.batch.title=Групови известия
|
||||
page.settings.notifications.batch.help=Групира събитията и изпраща на всеки N минути. Изисква поне един активен канал.
|
||||
page.settings.notifications.batch.interval=Интервал за групиране (минути)
|
||||
page.settings.notifications.testInProgress=Тестване...
|
||||
page.settings.notifications.requestFailed=Заявката се провали. Вижте логовете.
|
||||
page.settings.security.title=Настройки за сигурност
|
||||
page.settings.security.appPasswordEnabled=Включи защита с парола
|
||||
page.settings.security.appPasswordEnabled.help=Защитава цялото приложение с парола
|
||||
page.settings.security.appPassword=Парола за приложение
|
||||
page.settings.security.disableUploadPasswords=Забрани пароли при качване
|
||||
page.settings.security.disableUploadPasswords.help=Не позволява на потребителите да задават пароли при качване (и изключва криптирането по файл).
|
||||
page.settings.security.disableShareLinks=Забрани линковете за споделяне
|
||||
page.settings.security.disableShareLinks.help=Скрива опциите за споделяне и блокира генерирането на нови линкове.
|
||||
page.settings.security.simplifyShareLinks=Опрости линковете за споделяне
|
||||
page.settings.security.simplifyShareLinks.help=Автоматично генерира неограничен линк без изтичане и скрива ръчните опции.
|
||||
page.settings.security.disableFileEncryption=Забрани криптирането на файлове
|
||||
page.settings.security.disableFileEncryption.help=Ако е включено, файловете няма да се криптират дори при защита с парола.
|
||||
page.settings.security.metadataStripping=Премахване на метаданни
|
||||
page.settings.security.metadataStripping.help=Премахва допълнителни файлови метаданни, когато е възможно.
|
||||
page.settings.save=Запази настройките
|
||||
page.settings.about.title=Относно {0}
|
||||
page.settings.about.version=Версия
|
||||
page.settings.about.database=База данни
|
||||
page.settings.about.database.sqlite=SQLite
|
||||
page.settings.about.javaVersion=Java версия
|
||||
page.settings.about.osInfo=Информация за ОС
|
||||
page.settings.about.unknown=Неизвестно
|
||||
page.settings.about.notAvailable=Няма данни
|
||||
validation.settings.maxFileSize=Въведете максимален размер на файл (MB), по-голям от 0.
|
||||
validation.settings.maxFileLifetime=Въведете максимален живот на файл (дни), по-голям от 0.
|
||||
validation.settings.fileStoragePathRequired=Пътят за съхранение на файлове е задължителен.
|
||||
validation.settings.cronRequired=Cron изразът е задължителен.
|
||||
validation.settings.sessionLifetime=Въведете продължителност на сесията в минути (положително число).
|
||||
validation.settings.previewSize=Въведете размер за преглед (MB), по-голям от 0.
|
||||
validation.settings.defaultHomePage=Изберете upload, list или paste.
|
||||
validation.settings.appPasswordRequired=Паролата за приложението е задължителна при включена защита.
|
||||
validation.settings.discordWebhook=Въведете валиден Discord webhook URL.
|
||||
validation.settings.emailFrom=Въведете валиден имейл подател.
|
||||
validation.settings.emailRecipients=Въведете поне един получател.
|
||||
validation.settings.smtpHost=SMTP хостът е задължителен.
|
||||
validation.settings.smtpPort=Въведете валиден SMTP порт.
|
||||
validation.settings.enableChannelBeforeBatch=Включете Discord или Email преди групиране.
|
||||
validation.settings.batchInterval=Въведете интервал за групиране в минути.
|
||||
validation.settings.failed=Валидирането е неуспешно
|
||||
page.paste.content.empty=Съдържанието на пастата не може да бъде празно.
|
||||
form.password.show=Покажи парола
|
||||
page.files.keepIndefinitely=Запази безсрочно:
|
||||
page.files.passwordProtected=Защитено с парола:
|
||||
page.settings.notifications.discord.missingUrl=URL адресът на Discord уебхука не е конфигуриран.
|
||||
page.settings.notifications.discord.success=Изпратено е тестово известие до Discord.
|
||||
page.settings.notifications.discord.failed=Discord тестът се провали: {0}
|
||||
page.settings.notifications.email.incomplete=Имейл настройките са непълни (хост/подател/получатели).
|
||||
page.settings.notifications.email.success=Изпратено е тестово имейл известие.
|
||||
page.settings.notifications.email.failed=Имейл тестът се провали: {0}
|
||||
page.settings.notifications.testFailed=???????? ? ?????????. ????? ????????.
|
||||
@@ -0,0 +1,335 @@
|
||||
# Common reusable labels
|
||||
button.cancel=Abbrechen
|
||||
button.upload=Hochladen
|
||||
button.delete=Loeschen
|
||||
button.edit=Bearbeiten
|
||||
button.search=Suchen
|
||||
button.selectFile=Datei auswaehlen
|
||||
button.selectFolder=Ordner auswaehlen
|
||||
button.copy=Kopieren
|
||||
button.confirm=Bestaetigen
|
||||
form.description.placeholder=Beschreibung
|
||||
form.password.optional=Passwort (optional)
|
||||
# Navigation
|
||||
nav.viewFiles=Dateien anzeigen
|
||||
nav.pastebin=Pastebin
|
||||
nav.adminDashboard=Admin-Dashboard
|
||||
nav.logout=Abmelden
|
||||
nav.poweredBy=Powered by QuickDrop
|
||||
nav.uploadFile=Datei hochladen
|
||||
nav.settings=Einstellungen
|
||||
nav.dashboard=Dashboard
|
||||
nav.toggleNavigation=Navigation umschalten
|
||||
# Page: Upload
|
||||
page.upload.title.browser={0} - Upload
|
||||
page.upload.title=Datei oder Ordner hochladen
|
||||
page.upload.acceptedTypes=Akzeptierte Dateitypen: alle
|
||||
page.upload.maxSizeLabel=Max. Groesse
|
||||
page.upload.dropzone.title=Datei oder Ordner hier ablegen
|
||||
page.upload.encryptionNotice=Alle passwortgeschuetzten Dateien werden zusaetzlich verschluesselt.
|
||||
page.upload.metadataNotice=Metadaten-Entfernung ist aktiviert. Unterstuetzt:
|
||||
page.upload.retention.standard=Dateien werden nach {0} Tagen geloescht, sofern nicht "Unbegrenzt aufbewahren" gewaehlt ist.
|
||||
page.upload.retention.adminOnly=Dateien werden nach {0} Tagen geloescht.
|
||||
page.upload.status.started=Upload gestartet...
|
||||
page.upload.validation.fileExceedsLimit=Datei ueberschreitet das Limit {0}.
|
||||
page.upload.validation.folderExceedsLimit=Ordner ueberschreitet das Limit {0}.
|
||||
page.upload.processingFolder=Ordner wird verarbeitet...
|
||||
page.upload.folderSelected=Ordner ausgewaehlt: {0} ({1} Elemente)
|
||||
page.upload.noCandidate=Kein Upload-Kandidat verfuegbar.
|
||||
form.keepIndefinitely=Unbegrenzt aufbewahren
|
||||
form.keepIndefinitely.help.beforeDays=Wenn aktiviert, wird diese Datei nicht automatisch geloescht nach
|
||||
form.keepIndefinitely.help.afterDays=Tagen.
|
||||
form.hideFromList=Aus Dateiliste ausblenden
|
||||
form.hideFromList.help=Wenn aktiviert, erscheint diese Datei nicht auf der Seite View Files.
|
||||
page.files.keepIndefinitely=Unbegrenzt aufbewahren:
|
||||
page.files.passwordProtected=Passwortgeschuetzt:
|
||||
upload.warning.privacyNotice=Alle bekannten Metadaten werden entfernt, aber dieses Format kann weiterhin Details enthalten, die Informationen zu Ihrer Identitaet preisgeben.
|
||||
upload.warning.technicalInfo=Technische Details
|
||||
# Page: Settings
|
||||
page.settings.title.browser={0} Admin-Einstellungen
|
||||
page.settings.adminTitle=Admin-Einstellungen
|
||||
# Errors
|
||||
# Notifications
|
||||
notification.upload.failed=Upload fehlgeschlagen. Bitte versuche es erneut.
|
||||
notification.upload.noFileInfo=Upload abgeschlossen, aber der Server hat keine Dateiinformationen zurueckgegeben.
|
||||
notification.selection.required=Waehle zuerst eine Datei oder einen Ordner aus.
|
||||
notification.folder.processingFailed=Ordner konnte nicht fuer den Upload vorbereitet werden.
|
||||
notification.copied=Kopiert
|
||||
notification.failed=Fehlgeschlagen
|
||||
notification.settings.saved=Einstellungen gespeichert.
|
||||
notification.target.unknown=Unbekanntes Benachrichtigungsziel.
|
||||
# Validation
|
||||
validation.required=Dieses Feld ist erforderlich.
|
||||
validation.number.positive=Bitte gib einen Wert groesser als 0 ein.
|
||||
validation.number.nonNegative=Bitte gib einen Wert groesser oder gleich 0 ein.
|
||||
validation.cron.invalid=Ungueltiger Cron-Ausdruck.
|
||||
validation.cron.invalid.example=Ungueltiger Cron-Ausdruck. Bitte gib ein gueltiges Muster ein (z. B. 0 0 2 * * * fuer taeglich um 2:00 Uhr).
|
||||
validation.cron.nextRun.none=Keine bevorstehende Ausfuehrung.
|
||||
validation.cron.unableToParse=Cron-Ausdruck konnte nicht verarbeitet werden.
|
||||
# Phase 3 incremental migration keys
|
||||
button.submit=Senden
|
||||
button.view=Ansehen
|
||||
button.history=Verlauf
|
||||
button.download=Herunterladen
|
||||
form.password.label=Passwort
|
||||
form.password.placeholder=Passwort
|
||||
form.password.file.placeholder=Dateipasswort eingeben...
|
||||
form.adminPassword.label=Admin-Passwort
|
||||
form.adminPassword.placeholder=Admin-Passwort
|
||||
page.password.title.browser={0} - Passwort
|
||||
page.password.enterFile=Passwort eingeben
|
||||
page.password.enterAppPrompt=Bitte gib das App-Passwort ein, um fortzufahren.
|
||||
page.password.protectedLabel=Geschuetzt
|
||||
page.adminPassword.title.browser={0} - Admin-Anmeldung
|
||||
page.adminPassword.title=Admin-Passwort erforderlich
|
||||
page.files.title.browser={0} - Dateien
|
||||
page.files.title=Alle Dateien
|
||||
page.files.search.label=Dateien suchen
|
||||
page.files.search.placeholder=Dateien nach Name oder Datum suchen...
|
||||
page.files.empty.title=Es wurden noch keine Dateien hochgeladen.
|
||||
page.files.empty.startBy=Starte mit
|
||||
page.files.empty.uploadLink=dem Hochladen einer Datei
|
||||
page.files.goToFilePage=Zur Dateiseite
|
||||
pagination.previous=Zurueck
|
||||
pagination.next=Weiter
|
||||
pagination.perPage=Pro Seite
|
||||
pagination.page=Seite
|
||||
pagination.of=von
|
||||
page.fileView.title.browser={0} - Dateiansicht
|
||||
page.fileView.title=Dateiansicht
|
||||
page.dashboard.title.browser={0} Admin
|
||||
page.dashboard.title=Admin-Dashboard
|
||||
page.dashboard.analytics=Analysen
|
||||
page.dashboard.files=Dateien
|
||||
page.dashboard.totalDownloads=Downloads gesamt
|
||||
page.dashboard.excludingDeleted=Ohne geloeschte Dateien
|
||||
page.dashboard.totalSpaceUsed=Gesamter Speicherverbrauch
|
||||
page.dashboard.averageFileSize=Durchschnittliche Dateigroesse
|
||||
page.dashboard.search.label=Dateien suchen
|
||||
page.dashboard.search.placeholder=Suche nach Name, Beschreibung oder UUID
|
||||
table.name=Name
|
||||
table.uploadDateLastRenewed=Upload-Datum/zuletzt verlaengert
|
||||
table.size=Groesse
|
||||
table.downloads=Downloads
|
||||
page.error.title.browser={0} - Fehler
|
||||
page.error.title=Hoppla!
|
||||
page.error.message=Etwas ist schiefgelaufen. Bitte versuche es spaeter erneut.
|
||||
page.error.goHome=Zurueck zur Startseite
|
||||
# Phase 7 full-page translation keys
|
||||
nav.language=Sprache
|
||||
page.welcome.title.browser=Willkommen bei {0}
|
||||
page.welcome.title=Willkommen bei {0}
|
||||
page.welcome.setupThanks=Danke, dass du {0} eingerichtet hast!
|
||||
page.welcome.setupPrompt=Bitte lege ein Admin-Passwort fuer das Dashboard fest.
|
||||
page.welcome.passwordMismatch=Passwoerter stimmen nicht ueberein.
|
||||
page.welcome.setAdminPassword=Admin-Passwort festlegen
|
||||
form.password.confirm=Passwort bestaetigen
|
||||
form.password.confirm.placeholder=Passwort bestaetigen
|
||||
page.history.title.browser={0} Admin - Verlauf
|
||||
page.history.title=Verlauf
|
||||
page.history.downloadHistory=Download-Verlauf
|
||||
page.history.fileDetails=Dateidetails
|
||||
page.history.size=Groesse
|
||||
page.history.uploadDate=Upload-Datum
|
||||
page.history.totalDownloads=Downloads gesamt
|
||||
page.history.date=Datum
|
||||
page.history.action=Aktion
|
||||
page.history.ipAddress=IP-Adresse
|
||||
page.history.userAgent=User-Agent
|
||||
page.history.noDescription=Keine Beschreibung vorhanden
|
||||
page.share.title.browser={0} - Freigegebene Datei
|
||||
page.share.title=Freigegebene Datei
|
||||
page.share.uploaded=Hochgeladen
|
||||
page.share.fileSize=Dateigroesse
|
||||
page.invalidShare.title.browser={0} - Ungueltiger Link
|
||||
page.invalidShare.title=Link abgelaufen
|
||||
page.invalidShare.message=Dieser Freigabelink ist nicht mehr gueltig. Die Datei ist abgelaufen oder der Link wurde bereits verwendet.
|
||||
page.invalidShare.backHome=Zurueck zur Startseite
|
||||
page.paste.title.browser={0} - Pastebin
|
||||
page.paste.newTitle=Neues Paste
|
||||
page.paste.editTitle=Paste bearbeiten
|
||||
page.paste.field.title=Titel
|
||||
page.paste.field.mode=Modus
|
||||
page.paste.mode.markdown=Markdown (.md)
|
||||
page.paste.mode.text=Klartext (.txt)
|
||||
page.paste.lineNumbers=Zeilennummern
|
||||
page.paste.wordWrap=Zeilenumbruch
|
||||
page.paste.fontSize=Schriftgroesse
|
||||
page.paste.preview=Vorschau
|
||||
page.paste.content.placeholder=Schreibe hier deinen Text...
|
||||
page.paste.password.label=Passwort (optional)
|
||||
page.paste.password.placeholder=Passwort zum Schutz dieses Paste setzen
|
||||
page.paste.password.help=Wenn dieses Feld leer bleibt, wird der Passwortschutz entfernt.
|
||||
page.paste.save=Paste speichern
|
||||
page.paste.saveChanges=Aenderungen speichern
|
||||
page.pasteView.title.browser={0} - Paste-Ansicht
|
||||
page.pasteView.untitled=Unbenanntes Paste
|
||||
page.pasteView.copyText=Text kopieren
|
||||
page.pasteView.raw=Rohtext
|
||||
page.pasteView.markdown=Markdown
|
||||
page.pasteView.renderedHint=Gerendertes Markdown ist fuer dieses Paste aktiviert.
|
||||
page.pasteView.share=Teilen
|
||||
page.pasteView.generateLink=Link erzeugen
|
||||
page.pasteView.copyShareLink=Freigabelink kopieren
|
||||
page.pasteView.generateLinkFailed=Freigabelink konnte nicht erstellt werden
|
||||
page.fileView.keepIndefinitely=Unbegrenzt aufbewahren:
|
||||
page.fileView.hideFromList=Datei in Liste ausblenden:
|
||||
page.fileView.fileSize=Dateigroesse:
|
||||
page.fileView.folderContents=Ordnerinhalt
|
||||
page.fileView.editPaste=Paste bearbeiten
|
||||
page.fileView.preview=Vorschau
|
||||
page.fileView.share=Teilen
|
||||
page.fileView.shareBypassNotice=Freigabelinks umgehen immer Datei- und App-Passwoerter.
|
||||
page.fileView.daysValid=Tage gueltig
|
||||
page.fileView.allowedDownloads=Erlaubte Downloads
|
||||
page.fileView.autoShareLink=Automatischer Freigabelink
|
||||
page.fileView.uploadedAt=Hochgeladen am:
|
||||
page.fileView.uploadedRenewedAt=Hochgeladen/verlaengert am:
|
||||
page.fileView.retention.beforeDays=Dateien werden nur fuer
|
||||
page.fileView.retention.afterDays=Tage nach diesem Datum aufbewahrt.
|
||||
page.fileView.preparingDownload=Deine Datei wird fuer den Download vorbereitet. Bitte warten...
|
||||
page.fileView.renewLifetime=Dateilebensdauer verlaengern
|
||||
page.fileView.deleteFile=Datei loeschen
|
||||
page.fileView.loadPreview=Vorschau laden ({0} MB Limit)
|
||||
page.fileView.preview.clickToLoad=Auf "Vorschau laden" klicken, um zu laden.
|
||||
page.fileView.preview.loading=Vorschau wird geladen...
|
||||
page.fileView.shareAdvancedHint=Fuege ein Ablaufdatum und/oder Download-Limit hinzu oder waehle unbegrenzt fuer einen offenen Link.
|
||||
page.fileView.shareSimplifiedHint=Vereinfachte Freigabelinks sind aktiviert: Ein nicht ablaufender Link mit unbegrenzten Downloads wird automatisch erstellt.
|
||||
page.fileView.noExpiration=Kein Ablauf (unbegrenzte Zeit)
|
||||
page.fileView.unlimitedDownloads=Unbegrenzte Downloads
|
||||
page.fileView.sharePlaceholder=Erzeuge einen Freigabelink, der Passwoerter umgeht
|
||||
page.fileView.shareDisabled=Freigabelinks wurden vom Administrator deaktiviert.
|
||||
page.fileView.daysValidNonNegative=Tage gueltig duerfen nicht negativ sein.
|
||||
page.fileView.allowedDownloadsNonNegative=Erlaubte Downloads duerfen nicht negativ sein.
|
||||
page.fileView.shareGenerateFailed=Freigabelink konnte nicht erstellt werden.
|
||||
page.fileView.preview.unavailable=Vorschau nicht verfuegbar.
|
||||
page.fileView.preview.imageAlt=Vorschau
|
||||
page.fileView.preview.openInNewTab=Oeffne {0} in einem neuen Tab
|
||||
page.fileView.preview.jsonFormatted=Formatiert
|
||||
page.fileView.preview.jsonTree=Baum
|
||||
page.fileView.preview.csvNoRows=Keine Zeilen zum Anzeigen.
|
||||
page.fileView.preview.csvShowingFirst=Zeige die ersten {0} von {1} Zeilen.
|
||||
page.fileView.preview.codeTruncated=... (gekuerzt)
|
||||
page.fileView.folderFallbackName=ordner
|
||||
page.fileView.folderNoManifest=Kein Manifest verfuegbar.
|
||||
page.fileView.folderRenderFailed=Ordnerinhalt konnte nicht dargestellt werden.
|
||||
page.dashboard.keepIndefinitely=Unbegrenzt aufbewahren
|
||||
page.dashboard.hidden=Ausgeblendet
|
||||
page.dashboard.noFilesFound=Keine Dateien gefunden.
|
||||
page.dashboard.deleteConfirm=Moechtest du diese Datei wirklich loeschen? Diese Aktion kann nicht rueckgaengig gemacht werden.
|
||||
page.settings.branding.title=Branding
|
||||
page.settings.branding.appName=Anwendungsname
|
||||
page.settings.branding.appName.placeholder=Meine Marke
|
||||
page.settings.branding.appName.help=Wird in Navigation, Seitentiteln und Freigabeansichten angezeigt. Leer lassen, um QuickDrop wiederherzustellen.
|
||||
page.settings.branding.logo=Logo / Favicon
|
||||
page.settings.branding.logo.browse=Durchsuchen
|
||||
page.settings.branding.logo.reset=Auf Standardlogo zuruecksetzen
|
||||
page.settings.branding.logo.resetPending=Das Standardlogo wird beim Speichern wiederhergestellt.
|
||||
page.settings.branding.logo.help=Lade ein quadratisches Bild fuer beste Ergebnisse hoch. Leer lassen, um das aktuelle Logo zu behalten.
|
||||
page.settings.file.title=Dateieinstellungen
|
||||
page.settings.file.maxSizeMb=Maximale Dateigroesse (MB)
|
||||
page.settings.file.maxLifetimeDays=Maximale Dateilebensdauer (Tage)
|
||||
page.settings.file.storagePath=Dateispeicherpfad
|
||||
page.settings.file.disablePreview=Dateivorschau deaktivieren
|
||||
page.settings.file.disablePreview.help=Blendet Inline-Vorschauen fuer Bilder/Text auf der Dateiseite aus.
|
||||
page.settings.file.maxPreviewMb=Maximale Vorschaugroesse (MB)
|
||||
page.settings.file.maxPreviewMb.help=Ueber dieser Groesse muessen Benutzer "Vorschau laden" klicken.
|
||||
page.settings.file.keepIndefinitely.title=Unbegrenzt aufbewahren
|
||||
page.settings.file.keepIndefinitely.adminOnly=Admin-Sitzung fuer "Unbegrenzt aufbewahren" erforderlich
|
||||
page.settings.file.keepIndefinitely.adminOnly.help=Wenn aktiviert, koennen nur Admins mit aktiver Sitzung Uploads unbegrenzt markieren.
|
||||
page.settings.file.hideFromList.title=Aus Liste ausblenden
|
||||
page.settings.file.hideFromList.adminOnly=Admin-Sitzung zum Ausblenden von Dateien erforderlich
|
||||
page.settings.file.hideFromList.adminOnly.help=Wenn aktiviert, koennen nur Admins "Aus Dateiliste ausblenden" aendern.
|
||||
page.settings.system.title=Systemeinstellungen
|
||||
page.settings.system.sessionLifetimeMinutes=Sitzungsdauer (Minuten)
|
||||
page.settings.system.sessionLifetimeMinutes.help=Beeinflusst, wie lange Datei- und Admin-Sitzungen erhalten bleiben.
|
||||
page.settings.system.defaultHomePage=Standard-Startseite
|
||||
page.settings.system.defaultHomePage.upload=Upload-Seite
|
||||
page.settings.system.defaultHomePage.list=Dateiliste-Seite
|
||||
page.settings.system.defaultHomePage.paste=Pastebin-Seite
|
||||
page.settings.system.fileDeletionCron=Cron-Ausdruck fuer Dateiloeschung
|
||||
page.settings.system.nextRun=Naechster Lauf
|
||||
page.settings.system.fileList=Dateiliste
|
||||
page.settings.system.fileList.enabled=Dateiliste-Seite aktivieren
|
||||
page.settings.system.adminDashboard=Admin-Dashboard
|
||||
page.settings.system.adminDashboard.showButton=Admin-Dashboard-Button anzeigen
|
||||
page.settings.system.adminDashboard.directUrl=Weiterhin verfuegbar unter /admin/dashboard
|
||||
page.settings.system.pastebin=Pastebin
|
||||
page.settings.system.pastebin.enabled=Pastebin-Seite aktivieren
|
||||
page.settings.system.pastebin.help=Fuegt einen Texteditor-Tab hinzu und erlaubt Pastebin als Startseite.
|
||||
page.settings.notifications.title=Benachrichtigungseinstellungen
|
||||
page.settings.notifications.discord.title=Discord-Webhook
|
||||
page.settings.notifications.discord.help=Dateiaktivitaet in einen Discord-Kanal posten.
|
||||
page.settings.notifications.discord.webhookUrl=Webhook-URL
|
||||
page.settings.notifications.discord.webhookUrl.placeholder=https://discord.com/api/webhooks/...
|
||||
page.settings.notifications.discord.testButton=Discord-Test senden
|
||||
page.settings.notifications.email.title=E-Mail-Benachrichtigungen
|
||||
page.settings.notifications.email.help=Sendet E-Mails bei Upload-, Download- und Erneuerungsereignissen.
|
||||
page.settings.notifications.email.fromAddress=Absenderadresse
|
||||
page.settings.notifications.email.fromAddress.placeholder=noreply@example.com
|
||||
page.settings.notifications.email.recipients=Empfaenger (kommagetrennt)
|
||||
page.settings.notifications.email.recipients.placeholder=admin@example.com, alerts@example.com
|
||||
page.settings.notifications.email.smtpHost=SMTP-Host
|
||||
page.settings.notifications.email.smtpHost.placeholder=smtp.example.com
|
||||
page.settings.notifications.email.smtpPort=SMTP-Port
|
||||
page.settings.notifications.email.smtpUsername=SMTP-Benutzername
|
||||
page.settings.notifications.email.smtpPassword=SMTP-Passwort
|
||||
page.settings.notifications.email.useStartTls=STARTTLS verwenden
|
||||
page.settings.notifications.email.useImplicitSsl=Implizites SSL (SMTPS) verwenden
|
||||
page.settings.notifications.email.tlsHelp=STARTTLS mit Port 587 verwenden. Implizites SSL mit Port 465 verwenden.
|
||||
page.settings.notifications.email.testButton=Test-E-Mail senden
|
||||
page.settings.notifications.batch.title=Batch-Benachrichtigungen
|
||||
page.settings.notifications.batch.help=Ereignisse sammeln und alle N Minuten senden. Mindestens ein Kanal muss aktiviert sein.
|
||||
page.settings.notifications.batch.interval=Batch-Intervall (Minuten)
|
||||
page.settings.notifications.testInProgress=Teste...
|
||||
page.settings.notifications.requestFailed=Anfrage fehlgeschlagen. Siehe Logs.
|
||||
page.settings.security.title=Sicherheitseinstellungen
|
||||
page.settings.security.appPasswordEnabled=Passwortschutz aktivieren
|
||||
page.settings.security.appPasswordEnabled.help=Schuetzt die gesamte Anwendung mit einem Passwort
|
||||
page.settings.security.appPassword=App-Passwort
|
||||
page.settings.security.disableUploadPasswords=Upload-Passwoerter deaktivieren
|
||||
page.settings.security.disableUploadPasswords.help=Verhindert Upload-Passwoerter (und deaktiviert dateibezogene Verschluesselung).
|
||||
page.settings.security.disableShareLinks=Freigabelinks deaktivieren
|
||||
page.settings.security.disableShareLinks.help=Blendet Freigabeoptionen aus und blockiert neue Freigabelinks.
|
||||
page.settings.security.simplifyShareLinks=Freigabelinks vereinfachen
|
||||
page.settings.security.simplifyShareLinks.help=Erzeugt automatisch einen unbegrenzten, nicht ablaufenden Freigabelink.
|
||||
page.settings.security.disableFileEncryption=Dateiverschluesselung deaktivieren
|
||||
page.settings.security.disableFileEncryption.help=Wenn aktiviert, werden Dateien nicht verschluesselt, selbst bei Passwortschutz.
|
||||
page.settings.security.metadataStripping=Metadaten entfernen
|
||||
page.settings.security.metadataStripping.help=Entfernt optionale Dateimetadaten, wenn moeglich.
|
||||
page.settings.save=Einstellungen speichern
|
||||
page.settings.about.title=Ueber {0}
|
||||
page.settings.about.version=Version
|
||||
page.settings.about.database=Datenbank
|
||||
page.settings.about.database.sqlite=SQLite
|
||||
page.settings.about.javaVersion=Java-Version
|
||||
page.settings.about.osInfo=OS-Info
|
||||
page.settings.about.unknown=Unbekannt
|
||||
page.settings.about.notAvailable=N/V
|
||||
validation.settings.maxFileSize=Gib eine maximale Dateigroesse (MB) groesser als 0 ein.
|
||||
validation.settings.maxFileLifetime=Gib eine maximale Dateilebensdauer (Tage) groesser als 0 ein.
|
||||
validation.settings.fileStoragePathRequired=Dateispeicherpfad ist erforderlich.
|
||||
validation.settings.cronRequired=Cron-Ausdruck ist erforderlich.
|
||||
validation.settings.sessionLifetime=Gib eine Sitzungsdauer in Minuten ein (positive Zahl).
|
||||
validation.settings.previewSize=Gib eine Vorschaugroesse (MB) groesser als 0 ein.
|
||||
validation.settings.defaultHomePage=Waehle upload, list oder paste.
|
||||
validation.settings.appPasswordRequired=App-Passwort ist erforderlich, wenn Schutz aktiviert ist.
|
||||
validation.settings.discordWebhook=Gib eine gueltige Discord-Webhook-URL ein.
|
||||
validation.settings.emailFrom=Gib eine gueltige Absender-E-Mail ein.
|
||||
validation.settings.emailRecipients=Gib mindestens einen Empfaenger ein.
|
||||
validation.settings.smtpHost=SMTP-Host ist erforderlich.
|
||||
validation.settings.smtpPort=Gib einen gueltigen SMTP-Port ein.
|
||||
validation.settings.enableChannelBeforeBatch=Aktiviere Discord oder E-Mail vor Batch-Benachrichtigungen.
|
||||
validation.settings.batchInterval=Gib ein Batch-Intervall in Minuten ein.
|
||||
validation.settings.failed=Validierung fehlgeschlagen
|
||||
page.paste.content.empty=Paste-Inhalt darf nicht leer sein.
|
||||
form.password.show=Passwort anzeigen
|
||||
page.files.keepIndefinitely=Unbegrenzt aufbewahren:
|
||||
page.files.passwordProtected=Passwortgeschuetzt:
|
||||
page.settings.notifications.discord.missingUrl=Discord-Webhook-URL ist nicht konfiguriert.
|
||||
page.settings.notifications.discord.success=Discord-Testbenachrichtigung gesendet.
|
||||
page.settings.notifications.discord.failed=Discord-Test fehlgeschlagen: {0}
|
||||
page.settings.notifications.email.incomplete=E-Mail-Einstellungen unvollständig (Host/Absender/Empfänger).
|
||||
page.settings.notifications.email.success=E-Mail-Testbenachrichtigung gesendet.
|
||||
page.settings.notifications.email.failed=E-Mail-Test fehlgeschlagen: {0}
|
||||
page.settings.notifications.testFailed=Anfrage fehlgeschlagen. Siehe Logs.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,27 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const updateLocale = (lang) => {
|
||||
if (!lang) return;
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("lang", lang);
|
||||
window.location.assign(url.toString());
|
||||
};
|
||||
|
||||
const selects = document.querySelectorAll(".locale-switch-select[data-locale-select]");
|
||||
selects.forEach((select) => {
|
||||
select.addEventListener("change", () => {
|
||||
updateLocale(select.value);
|
||||
});
|
||||
});
|
||||
|
||||
const links = document.querySelectorAll(".locale-switch-link[data-lang]");
|
||||
if (!links.length) return;
|
||||
|
||||
links.forEach((link) => {
|
||||
link.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
updateLocale(link.dataset.lang);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -410,7 +410,10 @@ async function sendNotificationTest(target, buttonId, statusId) {
|
||||
const status = document.getElementById(statusId);
|
||||
if (!button || !status) return;
|
||||
|
||||
status.textContent = "Testing…";
|
||||
const testingText = button.dataset.testingText || "Testing…";
|
||||
const errorText = button.dataset.errorText || "Request failed. See logs.";
|
||||
|
||||
status.textContent = testingText;
|
||||
status.className = "text-slate-600 dark:text-slate-300";
|
||||
button.disabled = true;
|
||||
|
||||
@@ -431,7 +434,7 @@ async function sendNotificationTest(target, buttonId, statusId) {
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: "text-red-600 dark:text-red-400";
|
||||
} catch (e) {
|
||||
status.textContent = "Request failed. See logs.";
|
||||
status.textContent = errorText;
|
||||
status.className = "text-red-600 dark:text-red-400";
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title th:text="${appName} + ' - Admin Login'">Enter Admin Password</title>
|
||||
<title th:text="#{page.adminPassword.title.browser(${appName})}">Enter Admin Password</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
@@ -37,13 +37,13 @@
|
||||
target="_blank"
|
||||
th:if="${!#strings.equalsIgnoreCase(#strings.trim(appName), 'QuickDrop')}"
|
||||
>
|
||||
Powered by QuickDrop
|
||||
<span th:text="#{nav.poweredBy}">Powered by QuickDrop</span>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -72,9 +72,11 @@
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/list"
|
||||
th:if="${isFileListPageEnabled or hasAdminSession}"
|
||||
th:text="#{nav.viewFiles}"
|
||||
>View Files</a
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload"
|
||||
th:text="#{nav.uploadFile}"
|
||||
>Upload File</a
|
||||
>
|
||||
<form
|
||||
@@ -87,11 +89,23 @@
|
||||
th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}"
|
||||
/>
|
||||
<button class="nav-menu-item hover:text-white" type="submit">
|
||||
<button class="nav-menu-item hover:text-white" th:text="#{nav.logout}" type="submit">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchAdminPassword" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchAdminPassword"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Toggle theme"
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -107,7 +121,7 @@
|
||||
<main class="max-w-7xl mx-auto px-4 py-6 md:py-8">
|
||||
<header class="text-center mb-8 space-y-1">
|
||||
<h1 class="text-3xl md:text-4xl font-semibold tracking-tight">
|
||||
Admin Password Required
|
||||
<span th:text="#{page.adminPassword.title}">Admin Password Required</span>
|
||||
</h1>
|
||||
</header>
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
@@ -126,11 +140,12 @@
|
||||
type="hidden"
|
||||
/>
|
||||
<label class="block w-full">
|
||||
<span class="sr-only">Admin Password</span>
|
||||
<span class="sr-only" th:text="#{form.adminPassword.label}">Admin Password</span>
|
||||
<input
|
||||
class="block w-full rounded-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-gray-700 px-4 py-3 text-lg text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:border-sky-500 focus:ring-2 focus:ring-sky-500 focus:outline-none"
|
||||
id="adminPassword"
|
||||
name="password"
|
||||
th:placeholder="#{form.adminPassword.placeholder}"
|
||||
placeholder="Admin Password"
|
||||
required
|
||||
style="line-height: 2.7rem"
|
||||
@@ -142,7 +157,7 @@
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
<span th:text="#{button.submit}">Submit</span>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
@@ -154,5 +169,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title th:text="${appName} + ' - Password'">Password Required</title>
|
||||
<title th:text="#{page.password.title.browser(${appName})}">Password Required</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
@@ -16,7 +16,19 @@
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-800 dark:bg-slate-900 dark:text-gray-100">
|
||||
<div class="max-w-4xl mx-auto px-4 py-10">
|
||||
<div class="flex justify-end mb-6">
|
||||
<div class="flex justify-end items-center gap-3 mb-6">
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchAppPassword" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-800 dark:text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchAppPassword"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Toggle theme"
|
||||
class="rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -33,13 +45,13 @@
|
||||
<p
|
||||
class="text-sm uppercase tracking-wide text-slate-500 dark:text-slate-400"
|
||||
>
|
||||
Protected
|
||||
<span th:text="#{page.password.protectedLabel}">Protected</span>
|
||||
</p>
|
||||
<h1 class="text-3xl font-semibold tracking-tight">
|
||||
<span th:text="${appName} + ' - Password'">Password Required</span>
|
||||
<span th:text="#{page.password.title.browser(${appName})}">Password Required</span>
|
||||
</h1>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
Enter the app password to continue.
|
||||
<span th:text="#{page.password.enterAppPrompt}">Enter the app password to continue.</span>
|
||||
</p>
|
||||
<a
|
||||
class="text-xs text-gray-400 dark:text-gray-500 hover:text-white"
|
||||
@@ -49,6 +61,7 @@
|
||||
>
|
||||
<span
|
||||
th:if="${!#strings.equalsIgnoreCase(#strings.trim(appName), 'QuickDrop')}"
|
||||
th:text="#{nav.poweredBy}"
|
||||
>Powered by QuickDrop</span
|
||||
>
|
||||
</a>
|
||||
@@ -60,13 +73,14 @@
|
||||
type="hidden"
|
||||
/>
|
||||
<label class="block" for="password">
|
||||
<span class="sr-only">Password</span>
|
||||
<span class="sr-only" th:text="#{form.password.label}">Password</span>
|
||||
<input
|
||||
class="mt-1 w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 text-lg focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
type="password"
|
||||
th:placeholder="#{form.password.placeholder}"
|
||||
placeholder="Password"
|
||||
/>
|
||||
</label>
|
||||
@@ -74,7 +88,7 @@
|
||||
class="w-full rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-semibold py-2 transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
<span th:text="#{button.submit}">Submit</span>
|
||||
</button>
|
||||
</form>
|
||||
<div th:if="${error}">
|
||||
@@ -82,5 +96,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title th:text="${appName} + ' Admin'">QuickDrop Admin</title>
|
||||
<title th:text="#{page.dashboard.title.browser(${appName})}">QuickDrop Admin</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
@@ -39,13 +39,13 @@
|
||||
target="_blank"
|
||||
th:if="${!#strings.equalsIgnoreCase(#strings.trim(appName), 'QuickDrop')}"
|
||||
>
|
||||
Powered by QuickDrop
|
||||
<span th:text="#{nav.poweredBy}">Powered by QuickDrop</span>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -71,12 +71,13 @@
|
||||
class="flex flex-col space-y-2 md:flex-row md:items-center md:space-x-4 md:space-y-0"
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/file/list"
|
||||
th:text="#{nav.viewFiles}"
|
||||
>View Files</a
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload"
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload" th:text="#{nav.uploadFile}"
|
||||
>Upload File</a
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/admin/settings"
|
||||
<a class="nav-menu-item hover:text-white" href="/admin/settings" th:text="#{nav.settings}"
|
||||
>Settings</a
|
||||
>
|
||||
<form
|
||||
@@ -89,11 +90,23 @@
|
||||
th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}"
|
||||
/>
|
||||
<button class="nav-menu-item hover:text-white" type="submit">
|
||||
<button class="nav-menu-item hover:text-white" th:text="#{nav.logout}" type="submit">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchDashboard" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchDashboard"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="themeToggle"
|
||||
@@ -109,21 +122,21 @@
|
||||
<main class="max-w-7xl mx-auto px-4 py-6 md:py-8">
|
||||
<header class="text-center mb-8 space-y-1">
|
||||
<h1 class="text-3xl md:text-4xl font-semibold tracking-tight">
|
||||
Admin Dashboard
|
||||
<span th:text="#{page.dashboard.title}">Admin Dashboard</span>
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 p-6 md:p-8 rounded-2xl shadow-lg mb-8"
|
||||
>
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4">Analytics</h2>
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4" th:text="#{page.dashboard.analytics}">Analytics</h2>
|
||||
<div class="flex flex-wrap justify-between gap-6 text-center">
|
||||
<div class="flex-1 min-w-[8rem] space-y-1">
|
||||
<h3 class="text-lg font-semibold tracking-tight">
|
||||
Total Downloads
|
||||
<span th:text="#{page.dashboard.totalDownloads}">Total Downloads</span>
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Excluding deleted files
|
||||
<span th:text="#{page.dashboard.excludingDeleted}">Excluding deleted files</span>
|
||||
</p>
|
||||
<p
|
||||
class="text-2xl font-semibold"
|
||||
@@ -134,7 +147,7 @@
|
||||
</div>
|
||||
<div class="flex-1 min-w-[8rem] space-y-1">
|
||||
<h3 class="text-lg font-semibold tracking-tight">
|
||||
Total Space Used
|
||||
<span th:text="#{page.dashboard.totalSpaceUsed}">Total Space Used</span>
|
||||
</h3>
|
||||
<p
|
||||
class="text-2xl font-semibold"
|
||||
@@ -145,7 +158,7 @@
|
||||
</div>
|
||||
<div class="flex-1 min-w-[8rem] space-y-1">
|
||||
<h3 class="text-lg font-semibold tracking-tight">
|
||||
Average File Size
|
||||
<span th:text="#{page.dashboard.averageFileSize}">Average File Size</span>
|
||||
</h3>
|
||||
<p
|
||||
class="text-2xl font-semibold"
|
||||
@@ -158,7 +171,7 @@
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 p-6 md:p-8 rounded-2xl shadow-lg">
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4">Files</h2>
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4" th:text="#{page.dashboard.files}">Files</h2>
|
||||
<div class="mb-6">
|
||||
<form
|
||||
action="/admin/dashboard"
|
||||
@@ -167,7 +180,7 @@
|
||||
>
|
||||
<input name="page" type="hidden" value="0" />
|
||||
<input name="size" th:value="${pageSize}" type="hidden" />
|
||||
<label class="sr-only" for="adminSearch">Search files</label>
|
||||
<label class="sr-only" for="adminSearch" th:text="#{page.dashboard.search.label}">Search files</label>
|
||||
<div
|
||||
class="flex-1 h-12 flex items-center space-x-2 border border-slate-200 dark:border-slate-700 bg-white dark:bg-gray-800 rounded-full px-4 shadow-sm focus-within:border-sky-500 focus-within:ring-2 focus-within:ring-sky-500"
|
||||
>
|
||||
@@ -187,6 +200,7 @@
|
||||
class="flex-grow bg-transparent py-2 focus:outline-none focus-visible:ring-0"
|
||||
id="adminSearch"
|
||||
name="query"
|
||||
th:placeholder="#{page.dashboard.search.placeholder}"
|
||||
placeholder="Search by name, description, or UUID"
|
||||
th:value="${query}"
|
||||
type="text"
|
||||
@@ -196,7 +210,7 @@
|
||||
class="rounded-full bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 h-12 transition-colors active:scale-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-500"
|
||||
type="submit"
|
||||
>
|
||||
Search
|
||||
<span th:text="#{button.search}">Search</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -211,13 +225,13 @@
|
||||
class="bg-gray-100 dark:bg-gray-800 divide-x divide-slate-200 dark:divide-slate-600"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold">Name</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold" th:text="#{table.name}">Name</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold">
|
||||
Upload Date/Last Renewed
|
||||
<span th:text="#{table.uploadDateLastRenewed}">Upload Date/Last Renewed</span>
|
||||
</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold">Size</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold" th:text="#{table.size}">Size</th>
|
||||
<th class="px-6 py-4 text-left text-sm font-semibold">
|
||||
Downloads
|
||||
<span th:text="#{table.downloads}">Downloads</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -246,18 +260,21 @@
|
||||
<a
|
||||
class="inline-flex rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-1.5 text-sm transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
th:href="@{/file/{uuid}(uuid=${file.uuid})}"
|
||||
th:text="#{button.view}"
|
||||
>View</a
|
||||
>
|
||||
|
||||
<a
|
||||
class="inline-flex rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-1.5 text-sm transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
th:href="@{/file/history/{uuid}(uuid=${file.uuid})}"
|
||||
th:text="#{button.history}"
|
||||
>History</a
|
||||
>
|
||||
|
||||
<a
|
||||
class="inline-flex rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-1.5 text-sm transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
th:href="@{/file/download/{uuid}(uuid=${file.uuid})}"
|
||||
th:text="#{button.download}"
|
||||
>Download</a
|
||||
>
|
||||
|
||||
@@ -276,7 +293,7 @@
|
||||
class="inline-flex rounded-lg bg-red-600 hover:bg-red-700 dark:bg-red-500 dark:hover:bg-red-600 text-white font-medium px-4 py-1.5 text-sm transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-red-500"
|
||||
type="submit"
|
||||
>
|
||||
Delete
|
||||
<span th:text="#{button.delete}">Delete</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -306,7 +323,7 @@
|
||||
type="checkbox"
|
||||
value="true"
|
||||
/>
|
||||
<span class="text-sm">Keep indefinitely</span>
|
||||
<span class="text-sm" th:text="#{page.dashboard.keepIndefinitely}">Keep indefinitely</span>
|
||||
</label>
|
||||
</form>
|
||||
</td>
|
||||
@@ -331,7 +348,7 @@
|
||||
type="checkbox"
|
||||
value="true"
|
||||
/>
|
||||
<span class="text-sm">Hidden</span>
|
||||
<span class="text-sm" th:text="#{page.dashboard.hidden}">Hidden</span>
|
||||
</label>
|
||||
</form>
|
||||
</td>
|
||||
@@ -344,7 +361,7 @@
|
||||
class="text-center text-gray-600 dark:text-gray-300"
|
||||
th:if="${filesPage.totalElements == 0}"
|
||||
>
|
||||
<p>No files found.</p>
|
||||
<p th:text="#{page.dashboard.noFilesFound}">No files found.</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col md:flex-row items-center justify-between gap-4 mt-6"
|
||||
@@ -358,13 +375,13 @@
|
||||
class="px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-700 text-sm font-semibold transition disabled:opacity-50 disabled:pointer-events-none"
|
||||
th:classappend="${filesPage.first} ? ' opacity-50 pointer-events-none' : ' hover:bg-slate-300 dark:hover:bg-slate-600'"
|
||||
th:href="@{/admin/dashboard(page=${filesPage.first ? filesPage.number : filesPage.number - 1}, size=${pageSize}, query=${query})}"
|
||||
>Previous</a
|
||||
><span th:text="#{pagination.previous}">Previous</span></a
|
||||
>
|
||||
<a
|
||||
class="px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-700 text-sm font-semibold transition disabled:opacity-50 disabled:pointer-events-none"
|
||||
th:classappend="${filesPage.last} ? ' opacity-50 pointer-events-none' : ' hover:bg-slate-300 dark:hover:bg-slate-600'"
|
||||
th:href="@{/admin/dashboard(page=${filesPage.last ? filesPage.number : filesPage.number + 1}, size=${pageSize}, query=${query})}"
|
||||
>Next</a
|
||||
><span th:text="#{pagination.next}">Next</span></a
|
||||
>
|
||||
</div>
|
||||
<form
|
||||
@@ -375,7 +392,7 @@
|
||||
<label
|
||||
class="text-sm text-gray-700 dark:text-gray-300"
|
||||
for="adminPageSize"
|
||||
>Per page</label
|
||||
th:text="#{pagination.perPage}">Per page</label
|
||||
>
|
||||
<select
|
||||
class="rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -392,14 +409,15 @@
|
||||
<input name="query" th:value="${query}" type="hidden" />
|
||||
</form>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Page <span th:text="${filesPage.number + 1}"></span> of
|
||||
<span th:text="#{pagination.page}">Page</span> <span th:text="${filesPage.number + 1}"></span> <span
|
||||
th:text="#{pagination.of}">of</span>
|
||||
<span th:text="${filesPage.totalPages}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
<script th:inline="javascript">
|
||||
function updateHiddenState(event, checkbox) {
|
||||
event.preventDefault();
|
||||
const hiddenField = checkbox.form.querySelector(
|
||||
@@ -412,9 +430,7 @@
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
return confirm(
|
||||
"Are you sure you want to delete this file? This action cannot be undone."
|
||||
);
|
||||
return confirm(/*[[#{page.dashboard.deleteConfirm}]]*/ "Are you sure you want to delete this file? This action cannot be undone.");
|
||||
}
|
||||
|
||||
function updateCheckboxState(event, checkbox) {
|
||||
@@ -428,5 +444,6 @@
|
||||
checkbox.form.submit();
|
||||
}
|
||||
</script>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title th:text="${appName} + ' - Error'">Error</title>
|
||||
<title th:text="#{page.error.title.browser(${appName})}">Error</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
@@ -37,13 +37,13 @@
|
||||
target="_blank"
|
||||
th:if="${!#strings.equalsIgnoreCase(#strings.trim(appName), 'QuickDrop')}"
|
||||
>
|
||||
Powered by QuickDrop
|
||||
<span th:text="#{nav.poweredBy}">Powered by QuickDrop</span>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -72,9 +72,11 @@
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/list"
|
||||
th:if="${isFileListPageEnabled or hasAdminSession}"
|
||||
th:text="#{nav.viewFiles}"
|
||||
>View Files</a
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload"
|
||||
th:text="#{nav.uploadFile}"
|
||||
>Upload File</a
|
||||
>
|
||||
<a
|
||||
@@ -82,6 +84,7 @@
|
||||
href="/admin/dashboard"
|
||||
onclick="requestAdminPassword()"
|
||||
th:if="${isAdminDashboardButtonEnabled}"
|
||||
th:text="#{nav.adminDashboard}"
|
||||
>Admin Dashboard</a
|
||||
>
|
||||
<form
|
||||
@@ -94,11 +97,23 @@
|
||||
th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}"
|
||||
/>
|
||||
<button class="nav-menu-item hover:text-white" type="submit">
|
||||
<button class="nav-menu-item hover:text-white" th:text="#{nav.logout}" type="submit">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchError" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchError"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="themeToggle"
|
||||
@@ -118,16 +133,18 @@
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 p-6 md:p-8 rounded-2xl shadow-lg text-center space-y-4"
|
||||
>
|
||||
<h1 class="text-3xl font-semibold tracking-tight">Oops!</h1>
|
||||
<p>Something went wrong. Please try again later.</p>
|
||||
<h1 class="text-3xl font-semibold tracking-tight" th:text="#{page.error.title}">Oops!</h1>
|
||||
<p th:text="#{page.error.message}">Something went wrong. Please try again later.</p>
|
||||
<a
|
||||
class="inline-block rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
href="/"
|
||||
th:text="#{page.error.goHome}"
|
||||
>Go Back to Main Page</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
|
||||
<title th:text="${appName} + ' Admin - History'">Download History</title>
|
||||
<title th:text="#{page.history.title.browser(${appName})}">Download History</title>
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
<link
|
||||
@@ -40,13 +40,13 @@
|
||||
target="_blank"
|
||||
th:if="${!#strings.equalsIgnoreCase(#strings.trim(appName), 'QuickDrop')}"
|
||||
>
|
||||
Powered by QuickDrop
|
||||
<span th:text="#{nav.poweredBy}">Powered by QuickDrop</span>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -71,10 +71,22 @@
|
||||
<div
|
||||
class="flex flex-col space-y-2 md:flex-row md:items-center md:space-x-4 md:space-y-0"
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/admin/dashboard"
|
||||
<a class="nav-menu-item hover:text-white" href="/admin/dashboard" th:text="#{nav.dashboard}"
|
||||
>Dashboard</a
|
||||
>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchHistory" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchHistory"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Toggle theme"
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -90,7 +102,7 @@
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container mx-auto mt-5 px-4">
|
||||
<h1 class="text-center text-3xl font-semibold mb-4">History</h1>
|
||||
<h1 class="text-center text-3xl font-semibold mb-4" th:text="#{page.history.title}">History</h1>
|
||||
|
||||
<!-- File Name and Description -->
|
||||
<div class="text-center mb-8">
|
||||
@@ -99,7 +111,7 @@
|
||||
</p>
|
||||
<p
|
||||
class="text-lg text-gray-600 dark:text-gray-300"
|
||||
th:text="${file.description ?: 'No description provided'}"
|
||||
th:text="${file.description ?: #messages.msg('page.history.noDescription')}"
|
||||
>
|
||||
File Description
|
||||
</p>
|
||||
@@ -107,11 +119,11 @@
|
||||
|
||||
<!-- File Details Grid -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-lg mb-8 p-6">
|
||||
<div class="text-center text-xl font-semibold mb-4">File Details</div>
|
||||
<div class="text-center text-xl font-semibold mb-4" th:text="#{page.history.fileDetails}">File Details</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
<div class="bg-sky-500 text-white p-4 rounded-lg">
|
||||
<p class="text-2xl font-semibold" th:text="${file.size}">--</p>
|
||||
<p class="text-sm">Size</p>
|
||||
<p class="text-sm" th:text="#{page.history.size}">Size</p>
|
||||
</div>
|
||||
<div class="bg-sky-500 text-white p-4 rounded-lg">
|
||||
<p
|
||||
@@ -120,13 +132,13 @@
|
||||
>
|
||||
--
|
||||
</p>
|
||||
<p class="text-sm">Upload Date</p>
|
||||
<p class="text-sm" th:text="#{page.history.uploadDate}">Upload Date</p>
|
||||
</div>
|
||||
<div class="bg-sky-500 text-white p-4 rounded-lg">
|
||||
<p class="text-2xl font-semibold" th:text="${file.totalDownloads}">
|
||||
--
|
||||
</p>
|
||||
<p class="text-sm">Total Downloads</p>
|
||||
<p class="text-sm" th:text="#{page.history.totalDownloads}">Total Downloads</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,7 +146,7 @@
|
||||
<!-- History Table -->
|
||||
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-lg p-6">
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4">
|
||||
Download History
|
||||
<span th:text="#{page.history.downloadHistory}">Download History</span>
|
||||
</h2>
|
||||
|
||||
<!-- scroll container -->
|
||||
@@ -148,22 +160,22 @@
|
||||
<th
|
||||
class="px-6 py-4 text-left text-sm font-semibold whitespace-nowrap"
|
||||
>
|
||||
Date
|
||||
<span th:text="#{page.history.date}">Date</span>
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-4 text-left text-sm font-semibold whitespace-nowrap"
|
||||
>
|
||||
Action
|
||||
<span th:text="#{page.history.action}">Action</span>
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-4 text-left text-sm font-semibold whitespace-nowrap"
|
||||
>
|
||||
IP Address
|
||||
<span th:text="#{page.history.ipAddress}">IP Address</span>
|
||||
</th>
|
||||
<th
|
||||
class="px-6 py-4 text-left text-sm font-semibold whitespace-nowrap"
|
||||
>
|
||||
User Agent
|
||||
<span th:text="#{page.history.userAgent}">User Agent</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -209,5 +221,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title th:text="${appName} + ' - Password'">Enter Password</title>
|
||||
<title th:text="#{page.password.title.browser(${appName})}">Enter Password</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
@@ -25,13 +25,13 @@
|
||||
target="_blank"
|
||||
th:if="${!#strings.equalsIgnoreCase(#strings.trim(appName), 'QuickDrop')}"
|
||||
>
|
||||
Powered by QuickDrop
|
||||
<span th:text="#{nav.poweredBy}">Powered by QuickDrop</span>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -60,9 +60,10 @@
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/list"
|
||||
th:if="${isFileListPageEnabled or hasAdminSession}"
|
||||
th:text="#{nav.viewFiles}"
|
||||
>View Files</a
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload"
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload" th:text="#{nav.uploadFile}"
|
||||
>Upload File</a
|
||||
>
|
||||
<form
|
||||
@@ -75,11 +76,23 @@
|
||||
th:value="${_csrf.token}"
|
||||
type="hidden"
|
||||
/>
|
||||
<button class="nav-menu-item hover:text-white" type="submit">
|
||||
<button class="nav-menu-item hover:text-white" th:text="#{nav.logout}" type="submit">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchFilePassword" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchFilePassword"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Toggle theme"
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -95,7 +108,7 @@
|
||||
<main class="max-w-7xl mx-auto px-4 py-6 md:py-8">
|
||||
<header class="text-center mb-8">
|
||||
<h1 class="text-3xl md:text-4xl font-semibold tracking-tight">
|
||||
Enter Password
|
||||
<span th:text="#{page.password.enterFile}">Enter Password</span>
|
||||
</h1>
|
||||
</header>
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
@@ -119,13 +132,14 @@
|
||||
th:if="${fileName}"
|
||||
th:text="${fileName}"
|
||||
></h2>
|
||||
<label class="sr-only" for="password-input">Password</label>
|
||||
<label class="sr-only" for="password-input" th:text="#{form.password.label}">Password</label>
|
||||
<div class="relative w-full">
|
||||
<input
|
||||
autocomplete="current-password"
|
||||
class="block w-full rounded-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-gray-700 px-4 py-3 pr-12 text-lg text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:border-sky-500 focus:ring-2 focus:ring-sky-500 focus:outline-none"
|
||||
id="password-input"
|
||||
name="password"
|
||||
th:placeholder="#{form.password.file.placeholder}"
|
||||
placeholder="Enter the file password…"
|
||||
required
|
||||
style="line-height: 2.7rem"
|
||||
@@ -178,7 +192,7 @@
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
<span th:text="#{button.submit}">Submit</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -201,5 +215,6 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title th:text="${appName} + ' - Shared File'">Shared File View</title>
|
||||
<title th:text="#{page.share.title.browser(${appName})}">Shared File View</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
@@ -37,13 +37,13 @@
|
||||
target="_blank"
|
||||
th:if="${!#strings.equalsIgnoreCase(#strings.trim(appName), 'QuickDrop')}"
|
||||
>
|
||||
Powered by QuickDrop
|
||||
<span th:text="#{nav.poweredBy}">Powered by QuickDrop</span>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -65,6 +65,18 @@
|
||||
class="nav-menu-panel hidden md:flex flex-col md:flex-row md:items-center md:space-x-4 w-full md:w-auto mt-3 md:mt-0 space-y-2 md:space-y-0 bg-gray-800 dark:bg-gray-900 md:bg-transparent md:dark:bg-transparent rounded-lg md:rounded-none p-4 md:p-0 shadow md:shadow-none absolute left-0 right-0 top-full md:static z-20 md:ml-auto"
|
||||
id="navMenu"
|
||||
>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchShareView" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchShareView"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Toggle theme"
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -86,7 +98,7 @@
|
||||
<p
|
||||
class="text-sm uppercase tracking-wide text-slate-500 dark:text-slate-400"
|
||||
>
|
||||
Shared File
|
||||
<span th:text="#{page.share.title}">Shared File</span>
|
||||
</p>
|
||||
<h1
|
||||
class="text-3xl font-semibold tracking-tight"
|
||||
@@ -109,6 +121,7 @@
|
||||
>
|
||||
<span
|
||||
class="text-sm font-semibold text-slate-600 dark:text-slate-300"
|
||||
th:text="#{page.share.uploaded}"
|
||||
>Uploaded</span
|
||||
>
|
||||
<span
|
||||
@@ -121,6 +134,7 @@
|
||||
>
|
||||
<span
|
||||
class="text-sm font-semibold text-slate-600 dark:text-slate-300"
|
||||
th:text="#{page.share.fileSize}"
|
||||
>File Size</span
|
||||
>
|
||||
<span class="text-base font-medium" th:text="${file.size}"></span>
|
||||
@@ -133,10 +147,11 @@
|
||||
id="downloadButton"
|
||||
th:href="${downloadLink}"
|
||||
>
|
||||
Download
|
||||
<span th:text="#{button.download}">Download</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="_csrf" th:content="${_csrf.token}" />
|
||||
<title th:text="${appName} + ' - File View'">File View</title>
|
||||
<title th:text="#{page.fileView.title.browser(${appName})}">File View</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
@@ -47,13 +47,13 @@
|
||||
target="_blank"
|
||||
th:if="${!#strings.equalsIgnoreCase(#strings.trim(appName), 'QuickDrop')}"
|
||||
>
|
||||
Powered by QuickDrop
|
||||
<span th:text="#{nav.poweredBy}">Powered by QuickDrop</span>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -82,15 +82,17 @@
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/list"
|
||||
th:if="${isFileListPageEnabled or hasAdminSession}"
|
||||
th:text="#{nav.viewFiles}"
|
||||
>View Files</a
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload"
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload" th:text="#{nav.uploadFile}"
|
||||
>Upload File</a
|
||||
>
|
||||
<a
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/paste/new"
|
||||
th:if="${isPastebinEnabled}"
|
||||
th:text="#{nav.pastebin}"
|
||||
>Pastebin</a
|
||||
>
|
||||
<a
|
||||
@@ -98,6 +100,7 @@
|
||||
href="/admin/dashboard"
|
||||
onclick="requestAdminPassword()"
|
||||
th:if="${isAdminDashboardButtonEnabled}"
|
||||
th:text="#{nav.adminDashboard}"
|
||||
>Admin Dashboard</a
|
||||
>
|
||||
<form
|
||||
@@ -110,11 +113,23 @@
|
||||
th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}"
|
||||
/>
|
||||
<button class="nav-menu-item hover:text-white" type="submit">
|
||||
<button class="nav-menu-item hover:text-white" th:text="#{nav.logout}" type="submit">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchFileView" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchFileView"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
type="button"
|
||||
@@ -128,7 +143,7 @@
|
||||
<main class="max-w-7xl mx-auto px-4 py-6 md:py-8">
|
||||
<header class="text-center mb-8 space-y-1">
|
||||
<h1 class="text-3xl md:text-4xl font-semibold tracking-tight">
|
||||
File View
|
||||
<span th:text="#{page.fileView.title}">File View</span>
|
||||
</h1>
|
||||
</header>
|
||||
<span hidden id="fileUuid" th:text="${file.uuid}"></span>
|
||||
@@ -158,7 +173,7 @@
|
||||
<div class="flex justify-between border-t pt-4">
|
||||
<span
|
||||
class="font-semibold"
|
||||
th:text="${file.keepIndefinitely} ? 'Uploaded at:' : 'Uploaded/Renewed at:'"
|
||||
th:text="${file.keepIndefinitely} ? #{page.fileView.uploadedAt} : #{page.fileView.uploadedRenewedAt}"
|
||||
></span>
|
||||
<span
|
||||
th:text="${#temporals.format(file.uploadDate, 'dd.MM.yyyy')}"
|
||||
@@ -167,16 +182,15 @@
|
||||
<small
|
||||
class="text-gray-600 dark:text-gray-400"
|
||||
th:if="${file.keepIndefinitely == false}"
|
||||
>Files are kept only for
|
||||
<span th:text="${maxFileLifeTime}">30</span> days after this
|
||||
date.</small
|
||||
><span th:text="#{page.fileView.retention.beforeDays}">Files are kept only for </span>
|
||||
<span th:text="${maxFileLifeTime}">30</span> <span th:text="#{page.fileView.retention.afterDays}">days after this date.</span></small
|
||||
>
|
||||
<div
|
||||
class="flex justify-between items-center pt-4"
|
||||
th:with="keepDisabled=${(isKeepIndefinitelyAdminOnly and !hasAdminSession) or (!hasAdminSession and file.passwordHash == null)}"
|
||||
th:if="${keepDisabled == false or file.keepIndefinitely == true}"
|
||||
>
|
||||
<span class="font-semibold">Keep indefinitely:</span>
|
||||
<span class="font-semibold" th:text="#{page.fileView.keepIndefinitely}">Keep indefinitely:</span>
|
||||
<form
|
||||
method="post"
|
||||
th:action="@{/file/keep-indefinitely/{uuid}(uuid=${file.uuid})}"
|
||||
@@ -206,7 +220,7 @@
|
||||
th:with="hiddenDisabled=${(isHideFromListAdminOnly and !hasAdminSession) or (!hasAdminSession and file.passwordHash == null)}"
|
||||
th:if="${hiddenDisabled == false or file.hidden == true}"
|
||||
>
|
||||
<span class="font-semibold">Hide file from list:</span>
|
||||
<span class="font-semibold" th:text="#{page.fileView.hideFromList}">Hide file from list:</span>
|
||||
<form
|
||||
method="post"
|
||||
th:action="@{/file/toggle-hidden/{uuid}(uuid=${file.uuid})}"
|
||||
@@ -232,12 +246,12 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex justify-between items-center border-t pt-4">
|
||||
<span class="font-semibold">File Size:</span>
|
||||
<span class="font-semibold" th:text="#{page.fileView.fileSize}">File Size:</span>
|
||||
<span th:text="${fileSize}"></span>
|
||||
</div>
|
||||
<div class="border-t pt-4" th:if="${file.folderUpload}">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="text-sm font-semibold">Folder contents</div>
|
||||
<div class="text-sm font-semibold" th:text="#{page.fileView.folderContents}">Folder contents</div>
|
||||
</div>
|
||||
<div
|
||||
id="folderTree"
|
||||
@@ -253,6 +267,7 @@
|
||||
<div
|
||||
class="hidden bg-sky-50 dark:bg-sky-900 text-sky-800 dark:text-sky-300 rounded-lg p-2"
|
||||
id="preparingMessage"
|
||||
th:text="#{page.fileView.preparingDownload}"
|
||||
>
|
||||
Your file is being prepared for download. Please wait...
|
||||
</div>
|
||||
@@ -262,6 +277,7 @@
|
||||
id="downloadButton"
|
||||
th:href="@{/file/download/{uuid}(uuid=${file.uuid})}"
|
||||
th:onclick="${file.passwordHash != null} ? 'showPreparingMessage()' : ''"
|
||||
th:text="#{button.download}"
|
||||
>Download</a
|
||||
>
|
||||
<form
|
||||
@@ -277,6 +293,7 @@
|
||||
<button
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors active:scale-95"
|
||||
type="submit"
|
||||
th:text="#{page.fileView.renewLifetime}"
|
||||
>
|
||||
Renew File Lifetime
|
||||
</button>
|
||||
@@ -285,71 +302,46 @@
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors active:scale-95"
|
||||
th:href="@{/file/paste/edit/{uuid}(uuid=${file.uuid})}"
|
||||
th:if="${file.paste}"
|
||||
th:text="#{page.fileView.editPaste}"
|
||||
>Edit Paste</a
|
||||
>
|
||||
<form
|
||||
class="flex gap-2 items-center"
|
||||
method="post"
|
||||
th:action="@{/file/delete/{uuid}(uuid=${file.uuid})}"
|
||||
th:if="${file.passwordHash != null}"
|
||||
class="flex gap-2 items-center"
|
||||
method="post"
|
||||
th:action="@{/file/delete/{uuid}(uuid=${file.uuid})}"
|
||||
th:if="${file.passwordHash != null}"
|
||||
>
|
||||
<input
|
||||
th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}"
|
||||
type="hidden"
|
||||
th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}"
|
||||
type="hidden"
|
||||
/>
|
||||
<button
|
||||
class="rounded-lg bg-red-600 hover:bg-red-700 text-white font-medium px-4 py-2 transition-colors active:scale-95"
|
||||
id="deleteStartBtn"
|
||||
type="button"
|
||||
class="rounded-lg bg-red-600 hover:bg-red-700 text-white font-medium px-4 py-2 transition-colors active:scale-95"
|
||||
id="deleteStartBtn"
|
||||
th:text="#{page.fileView.deleteFile}"
|
||||
type="button"
|
||||
>
|
||||
Delete File
|
||||
</button>
|
||||
<button
|
||||
class="hidden rounded-lg bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-500 text-white font-medium px-4 py-2 transition-all active:scale-95 focus:outline-none focus:ring-2 focus:ring-red-300 dark:focus:ring-red-700 shadow-sm"
|
||||
id="deleteConfirmBtn"
|
||||
type="submit"
|
||||
class="hidden rounded-lg bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-500 text-white font-medium px-4 py-2 transition-all active:scale-95 focus:outline-none focus:ring-2 focus:ring-red-300 dark:focus:ring-red-700 shadow-sm"
|
||||
id="deleteConfirmBtn"
|
||||
th:text="#{button.confirm}"
|
||||
type="submit"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
<button
|
||||
class="hidden rounded-lg border border-slate-400/70 bg-slate-200 hover:bg-slate-300 dark:border-slate-500 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-900 dark:text-slate-100 font-medium px-4 py-2 transition-all active:scale-95 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:focus:ring-slate-600"
|
||||
id="deleteCancelBtn"
|
||||
type="button"
|
||||
class="hidden rounded-lg border border-slate-400/70 bg-slate-200 hover:bg-slate-300 dark:border-slate-500 dark:bg-slate-700 dark:hover:bg-slate-600 text-slate-900 dark:text-slate-100 font-medium px-4 py-2 transition-all active:scale-95 focus:outline-none focus:ring-2 focus:ring-slate-400 dark:focus:ring-slate-600"
|
||||
id="deleteCancelBtn"
|
||||
th:text="#{button.cancel}"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-3 bg-slate-50 dark:bg-slate-900"
|
||||
id="previewContainer"
|
||||
th:if="${isPreviewEnabled and (isPreviewableImage or isPreviewableText or isPreviewablePdf)}"
|
||||
th:attr="data-preview-url=${previewUrl},data-preview-image=${isPreviewableImage},data-preview-text=${isPreviewableText},data-preview-pdf=${isPreviewablePdf},data-preview-json=${isPreviewableJson},data-preview-csv=${isPreviewableCsv},data-preview-type=${previewType},data-file-name=${file.name},data-require-manual=${requireManualPreview},data-max-preview-mb=${maxPreviewSizeMB}"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between mb-2 text-sm text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<span>Preview</span>
|
||||
<button
|
||||
class="text-xs px-2 py-1 rounded bg-sky-500 text-white hover:bg-sky-600 disabled:opacity-50"
|
||||
type="button"
|
||||
id="loadPreviewBtn"
|
||||
th:if="${requireManualPreview}"
|
||||
>
|
||||
Load preview (<span th:text="${maxPreviewSizeMB}">5</span> MB
|
||||
limit)
|
||||
</button>
|
||||
</div>
|
||||
<div id="previewContent" class="flex justify-center">
|
||||
<div
|
||||
class="text-sm text-gray-500"
|
||||
id="previewStatus"
|
||||
th:text="${requireManualPreview} ? 'Click Load preview to fetch.' : 'Loading preview…'"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<aside class="sm:col-span-1" th:if="${!isShareLinksDisabled}">
|
||||
@@ -358,13 +350,14 @@
|
||||
id="sharePanel"
|
||||
th:attr="data-simplified-share-links=${isSimplifiedShareLinksEnabled},data-share-links-disabled=${isShareLinksDisabled}"
|
||||
>
|
||||
<h2 class="text-xl font-semibold tracking-tight">Share</h2>
|
||||
<h2 class="text-xl font-semibold tracking-tight" th:text="#{page.fileView.share}">Share</h2>
|
||||
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300 space-y-1">
|
||||
<p>Share links always bypass file and app passwords.</p>
|
||||
<p th:text="#{page.fileView.shareBypassNotice}">Share links always bypass file and app passwords.</p>
|
||||
<p
|
||||
class="text-amber-600 dark:text-amber-400"
|
||||
th:if="${!isSimplifiedShareLinksEnabled}"
|
||||
th:text="#{page.fileView.shareAdvancedHint}"
|
||||
>
|
||||
Add an expiration date and/or download limit, or choose
|
||||
unlimited to keep the link open-ended.
|
||||
@@ -372,6 +365,7 @@
|
||||
<p
|
||||
class="text-amber-600 dark:text-amber-400"
|
||||
th:if="${isSimplifiedShareLinksEnabled}"
|
||||
th:text="#{page.fileView.shareSimplifiedHint}"
|
||||
>
|
||||
Simplified share links are enabled: a non-expiring link with
|
||||
unlimited downloads is generated automatically.
|
||||
@@ -384,7 +378,7 @@
|
||||
id="linkOptions"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm" for="daysValid"
|
||||
<label class="block text-sm" for="daysValid" th:text="#{page.fileView.daysValid}"
|
||||
>Days valid</label
|
||||
>
|
||||
<input
|
||||
@@ -404,13 +398,14 @@
|
||||
onchange="toggleExpirationLimit()"
|
||||
type="checkbox"
|
||||
/>
|
||||
No expiration (unlimited time)
|
||||
<span th:text="#{page.fileView.noExpiration}">No expiration (unlimited time)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<label
|
||||
class="block text-sm"
|
||||
for="allowedNumberOfDownloadsCount"
|
||||
th:text="#{page.fileView.allowedDownloads}"
|
||||
>Allowed downloads</label
|
||||
>
|
||||
<input
|
||||
@@ -430,7 +425,7 @@
|
||||
onchange="toggleDownloadLimit()"
|
||||
type="checkbox"
|
||||
/>
|
||||
Unlimited downloads
|
||||
<span th:text="#{page.fileView.unlimitedDownloads}">Unlimited downloads</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -438,13 +433,14 @@
|
||||
<div class="flex justify-end items-center gap-2">
|
||||
<div
|
||||
class="hidden animate-spin rounded-full h-5 w-5 border-2 border-sky-500 border-t-transparent"
|
||||
id="spinner"
|
||||
id="spinnerShare"
|
||||
></div>
|
||||
<button
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2"
|
||||
id="generateLinkButton"
|
||||
onclick="createShareLink()"
|
||||
type="button"
|
||||
th:text="#{page.pasteView.generateLink}"
|
||||
>
|
||||
Generate link
|
||||
</button>
|
||||
@@ -457,9 +453,9 @@
|
||||
>
|
||||
<div
|
||||
class="hidden animate-spin rounded-full h-5 w-5 border-2 border-sky-500 border-t-transparent"
|
||||
id="spinner"
|
||||
id="spinnerShareAuto"
|
||||
></div>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300"
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300" th:text="#{page.fileView.autoShareLink}"
|
||||
>Automatic share link</span
|
||||
>
|
||||
</div>
|
||||
@@ -467,10 +463,12 @@
|
||||
<div class="border-t border-slate-200 dark:border-slate-700"></div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<label class="sr-only" for="shareLink" th:text="#{page.fileView.sharePlaceholder}">Share Link</label>
|
||||
<input
|
||||
class="w-full min-w-0 flex-1 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2"
|
||||
id="shareLink"
|
||||
readonly
|
||||
th:placeholder="#{page.fileView.sharePlaceholder}"
|
||||
placeholder="Generate a share link that bypasses passwords"
|
||||
type="text"
|
||||
/>
|
||||
@@ -479,6 +477,7 @@
|
||||
id="copyShareButton"
|
||||
onclick="copyShareLink()"
|
||||
type="button"
|
||||
th:text="#{button.copy}"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
@@ -493,9 +492,58 @@
|
||||
></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-3 bg-slate-50 dark:bg-slate-900 mt-6"
|
||||
id="previewContainer"
|
||||
th:attr="data-preview-url=${previewUrl},data-preview-image=${isPreviewableImage},data-preview-text=${isPreviewableText},data-preview-pdf=${isPreviewablePdf},data-preview-json=${isPreviewableJson},data-preview-csv=${isPreviewableCsv},data-preview-type=${previewType},data-file-name=${file.name},data-require-manual=${requireManualPreview},data-max-preview-mb=${maxPreviewSizeMB}"
|
||||
th:if="${isPreviewEnabled and (isPreviewableImage or isPreviewableText or isPreviewablePdf)}"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between mb-2 text-sm text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<span th:text="#{page.fileView.preview}">Preview</span>
|
||||
<button
|
||||
class="text-xs px-2 py-1 rounded bg-sky-500 text-white hover:bg-sky-600 disabled:opacity-50"
|
||||
id="loadPreviewBtn"
|
||||
th:if="${requireManualPreview}"
|
||||
th:text="#{page.fileView.loadPreview(${maxPreviewSizeMB})}"
|
||||
type="button"
|
||||
>
|
||||
Load preview (<span th:text="${maxPreviewSizeMB}">5</span> MB
|
||||
limit)
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex justify-center" id="previewContent">
|
||||
<div
|
||||
class="text-sm text-gray-500"
|
||||
id="previewStatus"
|
||||
th:text="${requireManualPreview} ? #{page.fileView.preview.clickToLoad} : #{page.fileView.preview.loading}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</main>
|
||||
<script th:inline="javascript">
|
||||
/*<![CDATA[*/
|
||||
window.i18n = window.i18n || {};
|
||||
window.i18n.fileView = {
|
||||
copied: /*[[#{notification.copied}]]*/ 'Copied',
|
||||
failed: /*[[#{notification.failed}]]*/ 'Failed',
|
||||
copy: /*[[#{button.copy}]]*/ 'Copy',
|
||||
previewUnavailable: /*[[#{page.fileView.preview.unavailable}]]*/ 'Preview unavailable.',
|
||||
jsonFormatted: /*[[#{page.fileView.preview.jsonFormatted}]]*/ 'Formatted',
|
||||
jsonTree: /*[[#{page.fileView.preview.jsonTree}]]*/ 'Tree',
|
||||
csvNoRows: /*[[#{page.fileView.preview.csvNoRows}]]*/ 'No rows to display.',
|
||||
folderNoManifest: /*[[#{page.fileView.folderNoManifest}]]*/ 'No manifest available.',
|
||||
folderRenderFailed: /*[[#{page.fileView.folderRenderFailed}]]*/ 'Unable to render folder contents.',
|
||||
openInNewTab: /*[[#{page.fileView.preview.openInNewTab}]]*/ 'Open {0} in a new tab',
|
||||
csvShowingFirst: /*[[#{page.fileView.preview.csvShowingFirst}]]*/ 'Showing first {0} rows out of {1}.',
|
||||
deleteConfirm: /*[[#{page.dashboard.deleteConfirm}]]*/ 'Are you sure you want to delete this file? This action cannot be undone.'
|
||||
};
|
||||
/*]]>*/
|
||||
</script>
|
||||
<script src="/js/fileView.js"></script>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title th:text="${appName} + ' - Invalid Link'">Share Link Invalid</title>
|
||||
<title th:text="#{page.invalidShare.title.browser(${appName})}">Share Link Invalid</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
@@ -37,13 +37,13 @@
|
||||
target="_blank"
|
||||
th:if="${!#strings.equalsIgnoreCase(#strings.trim(appName), 'QuickDrop')}"
|
||||
>
|
||||
Powered by QuickDrop
|
||||
<span th:text="#{nav.poweredBy}">Powered by QuickDrop</span>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -72,9 +72,11 @@
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/list"
|
||||
th:if="${isFileListPageEnabled or hasAdminSession}"
|
||||
th:text="#{nav.viewFiles}"
|
||||
>View Files</a
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload"
|
||||
th:text="#{nav.uploadFile}"
|
||||
>Upload File</a
|
||||
>
|
||||
<a
|
||||
@@ -82,6 +84,7 @@
|
||||
href="/admin/dashboard"
|
||||
onclick="requestAdminPassword()"
|
||||
th:if="${isAdminDashboardButtonEnabled}"
|
||||
th:text="#{nav.adminDashboard}"
|
||||
>Admin Dashboard</a
|
||||
>
|
||||
<form
|
||||
@@ -94,11 +97,23 @@
|
||||
th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}"
|
||||
/>
|
||||
<button class="nav-menu-item hover:text-white" type="submit">
|
||||
<button class="nav-menu-item hover:text-white" th:text="#{nav.logout}" type="submit">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchInvalidShare" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchInvalidShare"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="themeToggle"
|
||||
@@ -118,19 +133,20 @@
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 p-6 md:p-8 rounded-2xl shadow-lg text-center space-y-4"
|
||||
>
|
||||
<h1 class="text-3xl font-semibold tracking-tight">Link Expired</h1>
|
||||
<h1 class="text-3xl font-semibold tracking-tight" th:text="#{page.invalidShare.title}">Link Expired</h1>
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
This share link is no longer valid. The file you are trying to
|
||||
access has expired or the link has been used.
|
||||
<span th:text="#{page.invalidShare.message}">This share link is no longer valid. The file you are trying to access has expired or the link has been used.</span>
|
||||
</p>
|
||||
<a
|
||||
class="inline-block rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
href="/"
|
||||
th:text="#{page.invalidShare.backHome}"
|
||||
>Return to Homepage</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title th:text="${appName} + ' - Files'">All files</title>
|
||||
<title th:text="#{page.files.title.browser(${appName})}">All files</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
@@ -37,13 +37,13 @@
|
||||
target="_blank"
|
||||
th:if="${!#strings.equalsIgnoreCase(#strings.trim(appName), 'QuickDrop')}"
|
||||
>
|
||||
Powered by QuickDrop
|
||||
<span th:text="#{nav.poweredBy}">Powered by QuickDrop</span>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -69,12 +69,14 @@
|
||||
class="flex flex-col space-y-2 md:flex-row md:items-center md:space-x-4 md:space-y-0"
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload"
|
||||
th:text="#{nav.uploadFile}"
|
||||
>Upload File</a
|
||||
>
|
||||
<a
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/paste/new"
|
||||
th:if="${isPastebinEnabled}"
|
||||
th:text="#{nav.pastebin}"
|
||||
>Pastebin</a
|
||||
>
|
||||
<a
|
||||
@@ -82,6 +84,7 @@
|
||||
href="/admin/dashboard"
|
||||
onclick="requestAdminPassword()"
|
||||
th:if="${isAdminDashboardButtonEnabled}"
|
||||
th:text="#{nav.adminDashboard}"
|
||||
>Admin Dashboard</a
|
||||
>
|
||||
<form
|
||||
@@ -94,11 +97,23 @@
|
||||
th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}"
|
||||
/>
|
||||
<button class="nav-menu-item hover:text-white" type="submit">
|
||||
<button class="nav-menu-item hover:text-white" th:text="#{nav.logout}" type="submit">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchList" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchList"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
type="button"
|
||||
@@ -113,12 +128,12 @@
|
||||
<main class="max-w-7xl mx-auto px-4 py-6 md:py-8">
|
||||
<header class="text-center mb-8 space-y-1">
|
||||
<h1 class="text-3xl md:text-4xl font-semibold tracking-tight">
|
||||
All files
|
||||
<span th:text="#{page.files.title}">All files</span>
|
||||
</h1>
|
||||
</header>
|
||||
<div class="mx-auto w-full max-w-xl mb-8">
|
||||
<form action="/file/list" class="w-full" method="GET">
|
||||
<label class="sr-only" for="searchInput">Search files</label>
|
||||
<label class="sr-only" for="searchInput" th:text="#{page.files.search.label}">Search files</label>
|
||||
<div
|
||||
class="h-14 flex items-center space-x-2 border border-slate-200 dark:border-slate-700 bg-white dark:bg-gray-800 rounded-full px-4 shadow-sm focus-within:border-sky-500 focus-within:ring-2 focus-within:ring-sky-500"
|
||||
>
|
||||
@@ -138,6 +153,7 @@
|
||||
class="flex-grow bg-transparent py-2 focus:outline-none focus-visible:ring-0"
|
||||
id="searchInput"
|
||||
name="query"
|
||||
th:placeholder="#{page.files.search.placeholder}"
|
||||
placeholder="Search files by name or date…"
|
||||
th:value="${query}"
|
||||
type="text"
|
||||
@@ -149,7 +165,7 @@
|
||||
id="search-button"
|
||||
type="submit"
|
||||
>
|
||||
Search
|
||||
<span th:text="#{button.search}">Search</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -158,59 +174,60 @@
|
||||
class="text-center text-gray-600 dark:text-gray-300 my-5"
|
||||
th:if="${filesPage.totalElements == 0}"
|
||||
>
|
||||
<h3 class="mb-2">No files have been uploaded yet.</h3>
|
||||
<h3 class="mb-2" th:text="#{page.files.empty.title}">No files have been uploaded yet.</h3>
|
||||
<p>
|
||||
Start by
|
||||
<span th:text="#{page.files.empty.startBy}">Start by</span>
|
||||
<a
|
||||
class="text-sky-600 dark:text-sky-400 hover:underline"
|
||||
href="/file/upload"
|
||||
th:text="#{page.files.empty.uploadLink}"
|
||||
>uploading a file</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 p-6"
|
||||
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
th:if="${filesPage.totalElements > 0}"
|
||||
>
|
||||
<div class="h-full" th:each="file : ${filesPage.content}">
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 rounded-2xl shadow-lg flex flex-col h-full p-6 md:p-8"
|
||||
>
|
||||
<div class="flex-grow space-y-3 overflow-hidden">
|
||||
<h2
|
||||
class="text-xl font-semibold tracking-tight truncate"
|
||||
th:text="${file.name}"
|
||||
>
|
||||
File Name
|
||||
</h2>
|
||||
<hr class="border-t border-slate-200 dark:border-slate-700" />
|
||||
<p
|
||||
class="text-sm text-gray-700 dark:text-gray-300 line-clamp-2 overflow-hidden"
|
||||
th:if="${!#strings.isEmpty(file.description)}"
|
||||
th:text="${file.description}"
|
||||
></p>
|
||||
<div
|
||||
class="flex justify-between text-sm text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<span
|
||||
th:text="'Keep indefinitely: ' + (${file.keepIndefinitely} ? 'Yes' : 'No')"
|
||||
></span>
|
||||
<span
|
||||
th:text="'Password protected: ' + (${file.passwordHash != null} ? 'Yes' : 'No')"
|
||||
></span>
|
||||
</div>
|
||||
<p
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
th:text="${file.keepIndefinitely} ? 'Uploaded: ' + ${#temporals.format(file.uploadDate, 'dd.MM.yyyy')} : 'Uploaded/Renewed: ' + ${#temporals.format(file.uploadDate, 'dd.MM.yyyy')}"
|
||||
></p>
|
||||
</div>
|
||||
<div class="mt-4 text-center">
|
||||
<a
|
||||
class="inline-block rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors active:scale-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-500"
|
||||
th:href="@{/file/{UUID}(UUID=${file.uuid})}"
|
||||
>Go to file page</a
|
||||
>
|
||||
<div
|
||||
class="bg-white dark:bg-slate-800 rounded-2xl shadow-lg p-6 flex flex-col"
|
||||
th:each="file : ${filesPage.content}"
|
||||
>
|
||||
<div class="flex-grow space-y-3 overflow-hidden">
|
||||
<h2
|
||||
class="text-xl font-semibold tracking-tight truncate"
|
||||
th:text="${file.name}"
|
||||
>
|
||||
File Name
|
||||
</h2>
|
||||
<hr class="border-t border-slate-200 dark:border-slate-700"/>
|
||||
<p
|
||||
class="text-sm text-gray-700 dark:text-gray-300 line-clamp-2 overflow-hidden"
|
||||
th:if="${!#strings.isEmpty(file.description)}"
|
||||
th:text="${file.description}"
|
||||
></p>
|
||||
<div
|
||||
class="flex justify-between text-sm text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<span
|
||||
th:text="${#messages.msg('page.dashboard.keepIndefinitely')} + ': ' + (${file.keepIndefinitely} ? 'Yes' : 'No')"
|
||||
></span>
|
||||
<span
|
||||
th:text="${#messages.msg('page.password.protectedLabel')} + ': ' + (${file.passwordHash != null} ? 'Yes' : 'No')"
|
||||
></span>
|
||||
</div>
|
||||
<p
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
th:text="(${file.keepIndefinitely} ? #{page.fileView.uploadedAt} : #{page.fileView.uploadedRenewedAt}) + ' ' + ${#temporals.format(file.uploadDate, 'dd.MM.yyyy')}"
|
||||
></p>
|
||||
</div>
|
||||
<div class="mt-4 text-center">
|
||||
<a
|
||||
class="inline-block rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors active:scale-95 focus:outline-none focus-visible:ring-2 focus-visible:ring-sky-500"
|
||||
th:href="@{/file/{UUID}(UUID=${file.uuid})}"
|
||||
th:text="#{page.files.goToFilePage}"
|
||||
>Go to file page</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,19 +241,21 @@
|
||||
class="px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-700 text-sm font-semibold transition disabled:opacity-50 disabled:pointer-events-none"
|
||||
th:classappend="${filesPage.first} ? ' opacity-50 pointer-events-none' : ' hover:bg-slate-300 dark:hover:bg-slate-600'"
|
||||
th:href="@{/file/list(page=${filesPage.first ? filesPage.number : filesPage.number - 1}, size=${pageSize}, query=${query})}"
|
||||
th:text="#{pagination.previous}"
|
||||
>Previous</a
|
||||
>
|
||||
<a
|
||||
class="px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-700 text-sm font-semibold transition disabled:opacity-50 disabled:pointer-events-none"
|
||||
th:classappend="${filesPage.last} ? ' opacity-50 pointer-events-none' : ' hover:bg-slate-300 dark:hover:bg-slate-600'"
|
||||
th:href="@{/file/list(page=${filesPage.last ? filesPage.number : filesPage.number + 1}, size=${pageSize}, query=${query})}"
|
||||
th:text="#{pagination.next}"
|
||||
>Next</a
|
||||
>
|
||||
</div>
|
||||
<form action="/file/list" class="flex items-center gap-2" method="get">
|
||||
<input name="query" th:value="${query}" type="hidden" />
|
||||
<input name="page" type="hidden" value="0" />
|
||||
<label class="text-sm text-gray-700 dark:text-gray-300" for="pageSize"
|
||||
<label class="text-sm text-gray-700 dark:text-gray-300" for="pageSize" th:text="#{pagination.perPage}"
|
||||
>Per page</label
|
||||
>
|
||||
<select
|
||||
@@ -252,10 +271,12 @@
|
||||
</select>
|
||||
</form>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Page <span th:text="${filesPage.number + 1}"></span> of
|
||||
<span th:text="#{pagination.page}">Page</span> <span th:text="${filesPage.number + 1}"></span> <span
|
||||
th:text="#{pagination.of}">of</span>
|
||||
<span th:text="${filesPage.totalPages}"></span>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="_csrf" th:content="${_csrf != null ? _csrf.token : ''}"/>
|
||||
<title th:text="${appName} + ' - Paste View'">Paste View</title>
|
||||
<title th:text="#{page.pasteView.title.browser(${appName})}">Paste View</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
||||
<link href="/css/tailwind.css" rel="stylesheet"/>
|
||||
<link href="/css/custom.css" rel="stylesheet"/>
|
||||
@@ -132,7 +132,7 @@
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -157,25 +157,28 @@
|
||||
<div
|
||||
class="flex flex-col space-y-2 md:flex-row md:items-center md:space-x-4 md:space-y-0"
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload"
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload" th:text="#{nav.uploadFile}"
|
||||
>Upload File</a
|
||||
>
|
||||
<a
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/list"
|
||||
th:if="${isFileListPageEnabled or hasAdminSession}"
|
||||
th:text="#{nav.viewFiles}"
|
||||
>View Files</a
|
||||
>
|
||||
<a
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/paste/new"
|
||||
th:if="${isPastebinEnabled}"
|
||||
th:text="#{nav.pastebin}"
|
||||
>Pastebin</a
|
||||
>
|
||||
<a
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/admin/dashboard"
|
||||
th:if="${isAdminDashboardButtonEnabled}"
|
||||
th:text="#{nav.adminDashboard}"
|
||||
>Admin Dashboard</a
|
||||
>
|
||||
<form
|
||||
@@ -188,11 +191,23 @@
|
||||
th:value="${_csrf.token}"
|
||||
type="hidden"
|
||||
/>
|
||||
<button class="nav-menu-item hover:text-white" type="submit">
|
||||
<button class="nav-menu-item hover:text-white" th:text="#{nav.logout}" type="submit">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchPasteView" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchPasteView"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
type="button"
|
||||
@@ -213,7 +228,7 @@
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<h1
|
||||
class="paste-title text-2xl md:text-3xl font-semibold tracking-tight break-all"
|
||||
th:text="${#strings.isEmpty(file.name) ? 'Untitled paste' : file.name}"
|
||||
th:text="${#strings.isEmpty(file.name) ? #{page.pasteView.untitled} : file.name}"
|
||||
>Paste</h1
|
||||
>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@@ -221,17 +236,20 @@
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors"
|
||||
id="copyTextButton"
|
||||
type="button"
|
||||
th:text="#{page.pasteView.copyText}"
|
||||
>
|
||||
Copy text
|
||||
</button>
|
||||
<a
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors"
|
||||
th:href="@{/file/paste/edit/{uuid}(uuid=${file.uuid})}"
|
||||
th:text="#{button.edit}"
|
||||
>Edit</a
|
||||
>
|
||||
<a
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors"
|
||||
th:href="@{/file/download/{uuid}(uuid=${file.uuid})}"
|
||||
th:text="#{button.download}"
|
||||
>Download</a
|
||||
>
|
||||
</div>
|
||||
@@ -239,12 +257,15 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-sm pt-1">
|
||||
<button class="rounded-lg px-3 py-1 bg-sky-500 text-white" id="showRawBtn" type="button">Raw</button>
|
||||
<button class="rounded-lg px-3 py-1 bg-sky-500 text-white" id="showRawBtn" th:text="#{page.pasteView.raw}"
|
||||
type="button">Raw
|
||||
</button>
|
||||
<button class="hidden rounded-lg px-3 py-1 border border-slate-300 dark:border-slate-600"
|
||||
id="showMarkdownBtn" type="button">
|
||||
id="showMarkdownBtn" th:text="#{page.pasteView.markdown}" type="button">
|
||||
Markdown
|
||||
</button>
|
||||
<span class="text-gray-600 dark:text-gray-400" th:if="${isMarkdownPaste}">Rendered markdown is enabled for this paste.</span>
|
||||
<span class="text-gray-600 dark:text-gray-400" th:if="${isMarkdownPaste}"
|
||||
th:text="#{page.pasteView.renderedHint}">Rendered markdown is enabled for this paste.</span>
|
||||
</div>
|
||||
|
||||
<pre
|
||||
@@ -261,11 +282,12 @@
|
||||
|
||||
<aside class="xl:col-span-1 bg-white dark:bg-slate-800 rounded-2xl shadow-lg p-6 md:p-8 space-y-4"
|
||||
th:if="${!isShareLinksDisabled}">
|
||||
<h2 class="text-lg font-semibold tracking-tight">Share</h2>
|
||||
<h2 class="text-lg font-semibold tracking-tight" th:text="#{page.pasteView.share}">Share</h2>
|
||||
<button
|
||||
class="w-full rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors"
|
||||
id="generateShareButton"
|
||||
type="button"
|
||||
th:text="#{page.pasteView.generateLink}"
|
||||
>
|
||||
Generate link
|
||||
</button>
|
||||
@@ -280,6 +302,7 @@
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 px-3 py-2 text-sm hover:bg-slate-100 dark:hover:bg-slate-700"
|
||||
id="copyShareButton"
|
||||
type="button"
|
||||
th:text="#{page.pasteView.copyShareLink}"
|
||||
>
|
||||
Copy share link
|
||||
</button>
|
||||
@@ -343,17 +366,22 @@
|
||||
} else if (isMarkdownPaste && !markdownRuntimeAvailable) {
|
||||
markdownButton.classList.add("hidden");
|
||||
}
|
||||
const copySuccessText = /*[[#{notification.copied}]]*/ "Copied";
|
||||
const copyFailedText = /*[[#{notification.failed}]]*/ "Failed";
|
||||
const copyButtonText = /*[[#{page.pasteView.copyText}]]*/ "Copy text";
|
||||
const copyShareButtonText = /*[[#{page.pasteView.copyShareLink}]]*/ "Copy share link";
|
||||
const generateLinkFailedText = /*[[#{page.pasteView.generateLinkFailed}]]*/ "Unable to generate share link";
|
||||
copyTextButton.addEventListener("click", async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(rawText);
|
||||
copyTextButton.textContent = "Copied";
|
||||
copyTextButton.textContent = copySuccessText;
|
||||
setTimeout(() => {
|
||||
copyTextButton.textContent = "Copy text";
|
||||
copyTextButton.textContent = copyButtonText;
|
||||
}, 1200);
|
||||
} catch (_) {
|
||||
copyTextButton.textContent = "Failed";
|
||||
copyTextButton.textContent = copyFailedText;
|
||||
setTimeout(() => {
|
||||
copyTextButton.textContent = "Copy text";
|
||||
copyTextButton.textContent = copyButtonText;
|
||||
}, 1200);
|
||||
}
|
||||
});
|
||||
@@ -383,10 +411,10 @@
|
||||
? new URL(sharePath, window.location.origin).toString()
|
||||
: "";
|
||||
} else {
|
||||
shareLinkInput.value = data?.message || "Unable to generate share link";
|
||||
shareLinkInput.value = data?.message || generateLinkFailedText;
|
||||
}
|
||||
} catch (_) {
|
||||
shareLinkInput.value = "Unable to generate share link";
|
||||
shareLinkInput.value = generateLinkFailedText;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -398,14 +426,14 @@
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareLinkInput.value);
|
||||
copyShareButton.textContent = "Copied";
|
||||
copyShareButton.textContent = copySuccessText;
|
||||
setTimeout(() => {
|
||||
copyShareButton.textContent = "Copy share link";
|
||||
copyShareButton.textContent = copyShareButtonText;
|
||||
}, 1200);
|
||||
} catch (_) {
|
||||
copyShareButton.textContent = "Failed";
|
||||
copyShareButton.textContent = copyFailedText;
|
||||
setTimeout(() => {
|
||||
copyShareButton.textContent = "Copy share link";
|
||||
copyShareButton.textContent = copyShareButtonText;
|
||||
}, 1200);
|
||||
}
|
||||
});
|
||||
@@ -417,6 +445,7 @@
|
||||
showRaw();
|
||||
}
|
||||
</script>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title th:text="${appName} + ' - Pastebin'">Pastebin</title>
|
||||
<title th:text="#{page.paste.title.browser(${appName})}">Pastebin</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
||||
<link href="/css/tailwind.css" rel="stylesheet"/>
|
||||
<link href="/css/custom.css" rel="stylesheet"/>
|
||||
@@ -361,7 +361,7 @@
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -386,25 +386,28 @@
|
||||
<div
|
||||
class="flex flex-col space-y-2 md:flex-row md:items-center md:space-x-4 md:space-y-0"
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload"
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload" th:text="#{nav.uploadFile}"
|
||||
>Upload File</a
|
||||
>
|
||||
<a
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/list"
|
||||
th:if="${isFileListPageEnabled or hasAdminSession}"
|
||||
th:text="#{nav.viewFiles}"
|
||||
>View Files</a
|
||||
>
|
||||
<a
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/paste/new"
|
||||
th:if="${isPastebinEnabled}"
|
||||
th:text="#{nav.pastebin}"
|
||||
>Pastebin</a
|
||||
>
|
||||
<a
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/admin/dashboard"
|
||||
th:if="${isAdminDashboardButtonEnabled}"
|
||||
th:text="#{nav.adminDashboard}"
|
||||
>Admin Dashboard</a
|
||||
>
|
||||
<form
|
||||
@@ -417,11 +420,23 @@
|
||||
th:value="${_csrf.token}"
|
||||
type="hidden"
|
||||
/>
|
||||
<button class="nav-menu-item hover:text-white" type="submit">
|
||||
<button class="nav-menu-item hover:text-white" th:text="#{nav.logout}" type="submit">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchPastebin" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchPastebin"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
type="button"
|
||||
@@ -437,7 +452,7 @@
|
||||
<header class="text-center mb-8">
|
||||
<h1
|
||||
class="text-3xl md:text-4xl font-semibold tracking-tight"
|
||||
th:text="${isEditMode} ? 'Edit Paste' : 'New Paste'"
|
||||
th:text="${isEditMode} ? #{page.paste.editTitle} : #{page.paste.newTitle}"
|
||||
>
|
||||
New Paste
|
||||
</h1>
|
||||
@@ -463,7 +478,7 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<label class="block space-y-1">
|
||||
<span class="text-sm font-medium">Title</span>
|
||||
<span class="text-sm font-medium" th:text="#{page.paste.field.title}">Title</span>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
maxlength="255"
|
||||
@@ -474,16 +489,17 @@
|
||||
/>
|
||||
</label>
|
||||
<label class="block space-y-1">
|
||||
<span class="text-sm font-medium">Mode</span>
|
||||
<span class="text-sm font-medium" th:text="#{page.paste.field.mode}">Mode</span>
|
||||
<select
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="syntax"
|
||||
name="syntax"
|
||||
>
|
||||
<option th:selected="${pasteSyntax == 'markdown'}" value="markdown">
|
||||
<option th:selected="${pasteSyntax == 'markdown'}" th:text="#{page.paste.mode.markdown}"
|
||||
value="markdown">
|
||||
Markdown (.md)
|
||||
</option>
|
||||
<option th:selected="${pasteSyntax != 'markdown'}" value="text">
|
||||
<option th:selected="${pasteSyntax != 'markdown'}" th:text="#{page.paste.mode.text}" value="text">
|
||||
Plain Text (.txt)
|
||||
</option>
|
||||
</select>
|
||||
@@ -497,7 +513,7 @@
|
||||
id="lineNumbersToggle"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>Line numbers</span>
|
||||
<span th:text="#{page.paste.lineNumbers}">Line numbers</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<input
|
||||
@@ -506,10 +522,10 @@
|
||||
id="wordWrapToggle"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>Word wrap</span>
|
||||
<span th:text="#{page.paste.wordWrap}">Word wrap</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center gap-2">
|
||||
<span>Font size</span>
|
||||
<span th:text="#{page.paste.fontSize}">Font size</span>
|
||||
<select
|
||||
class="rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-2 py-1"
|
||||
id="fontSizeSelect"
|
||||
@@ -627,13 +643,14 @@
|
||||
<path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
<span>Preview</span>
|
||||
<span th:text="#{page.paste.preview}">Preview</span>
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="content"
|
||||
name="content"
|
||||
placeholder="Write your text here..."
|
||||
th:placeholder="#{page.paste.content.placeholder}"
|
||||
th:text="${pasteContent}"
|
||||
></textarea>
|
||||
<div class="manual-preview-pane markdown-content" id="manualPreviewPane"></div>
|
||||
@@ -648,20 +665,22 @@
|
||||
type="checkbox"
|
||||
value="true"
|
||||
/>
|
||||
<span class="text-sm">Keep indefinitely</span>
|
||||
<span class="text-sm" th:text="#{form.keepIndefinitely}">Keep indefinitely</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1" th:if="${uploadPasswordEnabled and !isEditMode}">
|
||||
<label class="text-sm font-medium" for="password">Password (optional)</label>
|
||||
<label class="text-sm font-medium" for="password" th:text="#{page.paste.password.label}">Password
|
||||
(optional)</label>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="password"
|
||||
name="password"
|
||||
th:placeholder="#{page.paste.password.placeholder}"
|
||||
placeholder="Set password to protect this paste"
|
||||
type="password"
|
||||
/>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400" th:text="#{page.paste.password.help}">
|
||||
Leaving this empty removes password protection for the saved paste.
|
||||
</div>
|
||||
</div>
|
||||
@@ -670,7 +689,7 @@
|
||||
<button
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors active:scale-95"
|
||||
id="savePasteButton"
|
||||
th:text="${isEditMode} ? 'Save Changes' : 'Save Paste'"
|
||||
th:text="${isEditMode} ? #{page.paste.saveChanges} : #{page.paste.save}"
|
||||
type="submit"
|
||||
>
|
||||
Save Paste
|
||||
@@ -680,7 +699,8 @@
|
||||
</main>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.js"></script>
|
||||
<script>
|
||||
<script th:inline="javascript">
|
||||
const emptyPasteMessage = /*[[#{page.paste.content.empty}]]*/ "Paste content cannot be empty.";
|
||||
const markedApi = window.marked;
|
||||
const markdownRuntimeAvailable =
|
||||
!!markedApi &&
|
||||
@@ -838,7 +858,7 @@
|
||||
|
||||
if (!contentValue) {
|
||||
event.preventDefault();
|
||||
alert("Paste content cannot be empty.");
|
||||
alert(emptyPasteMessage);
|
||||
editor.codemirror.focus();
|
||||
return;
|
||||
}
|
||||
@@ -847,6 +867,7 @@
|
||||
savePasteButton.classList.add("opacity-70", "cursor-not-allowed");
|
||||
});
|
||||
</script>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<title th:text="${appName} + ' Admin Settings'">Admin Settings</title>
|
||||
<title th:text="#{page.settings.title.browser(${appName})}">Admin Settings</title>
|
||||
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
@@ -43,13 +43,13 @@
|
||||
target="_blank"
|
||||
th:if="${!#strings.equalsIgnoreCase(#strings.trim(appName), 'QuickDrop')}"
|
||||
>
|
||||
Powered by QuickDrop
|
||||
<span th:text="#{nav.poweredBy}">Powered by QuickDrop</span>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -74,13 +74,13 @@
|
||||
<div
|
||||
class="flex flex-col space-y-2 md:flex-row md:items-center md:space-x-4 md:space-y-0"
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/file/list"
|
||||
<a class="nav-menu-item hover:text-white" href="/file/list" th:text="#{nav.viewFiles}"
|
||||
>View Files</a
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload"
|
||||
<a class="nav-menu-item hover:text-white" href="/file/upload" th:text="#{nav.uploadFile}"
|
||||
>Upload File</a
|
||||
>
|
||||
<a class="nav-menu-item hover:text-white" href="/admin/dashboard"
|
||||
<a class="nav-menu-item hover:text-white" href="/admin/dashboard" th:text="#{nav.dashboard}"
|
||||
>Dashboard</a
|
||||
>
|
||||
<form
|
||||
@@ -93,17 +93,29 @@
|
||||
th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}"
|
||||
/>
|
||||
<button class="nav-menu-item hover:text-white" type="submit">
|
||||
<button class="nav-menu-item hover:text-white" th:text="#{nav.logout}" type="submit">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchSettings" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchSettings"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="themeToggle"
|
||||
type="button"
|
||||
>
|
||||
🌙
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,7 +125,7 @@
|
||||
<main class="max-w-7xl mx-auto px-4 py-6 md:py-8">
|
||||
<header class="text-center mb-8 space-y-1">
|
||||
<h1 class="text-3xl md:text-4xl font-semibold tracking-tight">
|
||||
Admin Settings
|
||||
<span th:text="#{page.settings.adminTitle}">Admin Settings</span>
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
@@ -131,29 +143,30 @@
|
||||
class="bg-white dark:bg-slate-800 p-6 md:p-8 rounded-2xl shadow-lg"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold tracking-tight">Branding</h2>
|
||||
<h2 class="text-xl font-semibold tracking-tight" th:text="#{page.settings.branding.title}">Branding</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium mb-1" for="appName"
|
||||
>Application name</label
|
||||
<label class="block text-sm font-medium mb-1" for="appName" th:text="#{page.settings.branding.appName}">
|
||||
Application name</label
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="appName"
|
||||
th:field="*{appName}"
|
||||
th:placeholder="#{page.settings.branding.appName.placeholder}"
|
||||
placeholder="My Brand"
|
||||
type="text"
|
||||
/>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400" th:text="#{page.settings.branding.appName.help}">
|
||||
Shown in the navbar, page titles, and share views. Leave empty
|
||||
to reset to QuickDrop.
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<label class="block text-sm font-medium mb-1" for="appLogo"
|
||||
>Logo / favicon</label
|
||||
<label class="block text-sm font-medium mb-1" for="appLogo" th:text="#{page.settings.branding.logo}">
|
||||
Logo / favicon</label
|
||||
>
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
@@ -171,7 +184,7 @@
|
||||
class="inline-flex items-center justify-center rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-4 py-2 text-sm font-medium text-gray-800 dark:text-gray-200 hover:bg-slate-50 dark:hover:bg-slate-600 transition cursor-pointer"
|
||||
id="appLogoButton"
|
||||
>
|
||||
<span id="appLogoLabelText">Browse</span>
|
||||
<span id="appLogoLabelText" th:text="#{page.settings.branding.logo.browse}">Browse</span>
|
||||
</label>
|
||||
<input
|
||||
id="clearLogo"
|
||||
@@ -184,17 +197,18 @@
|
||||
type="button"
|
||||
id="clearLogoButton"
|
||||
>
|
||||
Reset to default logo
|
||||
<span th:text="#{page.settings.branding.logo.reset}">Reset to default logo</span>
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
id="logoStatus"
|
||||
class="text-xs text-sky-600 dark:text-sky-400 hidden"
|
||||
th:text="#{page.settings.branding.logo.resetPending}"
|
||||
>
|
||||
Default logo will be restored on save.
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400" th:text="#{page.settings.branding.logo.help}">
|
||||
Upload a square image for best results. Leave empty to keep the
|
||||
current logo or reset to restore the QuickDrop icon.
|
||||
</p>
|
||||
@@ -206,15 +220,15 @@
|
||||
class="bg-white dark:bg-slate-800 p-6 md:p-8 rounded-2xl shadow-lg"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold tracking-tight">File Settings</h2>
|
||||
<h2 class="text-xl font-semibold tracking-tight" th:text="#{page.settings.file.title}">File Settings</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-4"
|
||||
>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Max File Size (MB)</label
|
||||
<label class="block text-sm font-medium mb-1" th:text="#{page.settings.file.maxSizeMb}">
|
||||
Max File Size (MB)</label
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -227,8 +241,8 @@
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-4"
|
||||
>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Max File Lifetime (days)</label
|
||||
<label class="block text-sm font-medium mb-1" th:text="#{page.settings.file.maxLifetimeDays}">
|
||||
Max File Lifetime (days)</label
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -241,8 +255,8 @@
|
||||
<div
|
||||
class="md:col-span-2 rounded-xl border border-slate-200 dark:border-slate-700 p-4"
|
||||
>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>File Storage Path</label
|
||||
<label class="block text-sm font-medium mb-1" th:text="#{page.settings.file.storagePath}">
|
||||
File Storage Path</label
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -263,10 +277,12 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
<span class="text-sm font-medium">Disable file previews</span
|
||||
<span class="text-sm font-medium"
|
||||
th:text="#{page.settings.file.disablePreview}">Disable file previews</span
|
||||
><br />
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400"
|
||||
>Hide inline previews for images/text on the file
|
||||
th:text="#{page.settings.file.disablePreview.help}">
|
||||
Hide inline previews for images/text on the file
|
||||
page.</span
|
||||
>
|
||||
</span>
|
||||
@@ -279,6 +295,7 @@
|
||||
<label
|
||||
class="block text-sm font-medium mb-1"
|
||||
for="maxPreviewSizeBytes"
|
||||
th:text="#{page.settings.file.maxPreviewMb}"
|
||||
>Max preview size (MB)</label
|
||||
>
|
||||
<input
|
||||
@@ -291,7 +308,8 @@
|
||||
name="maxPreviewSizeBytes"
|
||||
th:disabled="${settings.disablePreview}"
|
||||
/>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1"
|
||||
th:text="#{page.settings.file.maxPreviewMb.help}">
|
||||
Above this size users must click "Load preview".
|
||||
</p>
|
||||
</div>
|
||||
@@ -299,7 +317,9 @@
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-4"
|
||||
>
|
||||
<div class="text-sm font-medium mb-2">Keep Indefinitely</div>
|
||||
<div class="text-sm font-medium mb-2" th:text="#{page.settings.file.keepIndefinitely.title}">Keep
|
||||
Indefinitely
|
||||
</div>
|
||||
<label class="flex items-start gap-3 pl-2">
|
||||
<input
|
||||
class="mt-1 h-4 w-4 rounded border-gray-400 dark:border-gray-600 accent-sky-500 dark:accent-sky-400 focus:ring-sky-500"
|
||||
@@ -308,11 +328,12 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
<span class="text-sm"
|
||||
>Require admin session for “Keep indefinitely”</span
|
||||
<span class="text-sm" th:text="#{page.settings.file.keepIndefinitely.adminOnly}">
|
||||
Require admin session for “Keep indefinitely”</span
|
||||
><br />
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400"
|
||||
>When enabled, only admins with an active session can mark
|
||||
th:text="#{page.settings.file.keepIndefinitely.adminOnly.help}">
|
||||
When enabled, only admins with an active session can mark
|
||||
uploads to keep indefinitely.</span
|
||||
>
|
||||
</span>
|
||||
@@ -322,7 +343,8 @@
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-4"
|
||||
>
|
||||
<div class="text-sm font-medium mb-2">Hide from list</div>
|
||||
<div class="text-sm font-medium mb-2" th:text="#{page.settings.file.hideFromList.title}">Hide from list
|
||||
</div>
|
||||
<label class="flex items-start gap-3 pl-2">
|
||||
<input
|
||||
class="mt-1 h-4 w-4 rounded border-gray-400 dark:border-gray-600 accent-sky-500 dark:accent-sky-400 focus:ring-sky-500"
|
||||
@@ -331,11 +353,12 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
<span class="text-sm"
|
||||
>Require admin session to hide files</span
|
||||
<span class="text-sm" th:text="#{page.settings.file.hideFromList.adminOnly}">
|
||||
Require admin session to hide files</span
|
||||
><br />
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400"
|
||||
>When enabled, only admins can set or change “Hide from file
|
||||
th:text="#{page.settings.file.hideFromList.adminOnly.help}">
|
||||
When enabled, only admins can set or change “Hide from file
|
||||
list”.</span
|
||||
>
|
||||
</span>
|
||||
@@ -348,27 +371,27 @@
|
||||
<section
|
||||
class="bg-white dark:bg-slate-800 p-6 md:p-8 rounded-2xl shadow-lg"
|
||||
>
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4">
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4" th:text="#{page.settings.system.title}">
|
||||
System Settings
|
||||
</h2>
|
||||
|
||||
<div
|
||||
th:if="${param.error != null && param.error.contains('invalidCron')}"
|
||||
class="mb-4 rounded-lg border border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-200 px-4 py-3 text-sm"
|
||||
th:text="#{validation.cron.invalid.example}"
|
||||
>
|
||||
Invalid cron expression. Please enter a valid cron pattern (e.g.,
|
||||
<span class="font-mono">0 0 2 * * *</span> for 2:00 AM daily).
|
||||
Invalid cron expression. Please enter a valid cron pattern (e.g., 0 0 2 * * * for 2:00 AM daily).
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- MIDDLE: Lifetime (left) -->
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-4"
|
||||
>
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>Session Lifetime (minutes)</label
|
||||
<label class="block text-sm font-medium mb-1" th:text="#{page.settings.system.sessionLifetimeMinutes}">
|
||||
Session Lifetime (minutes)</label
|
||||
>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mb-1">
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mb-1"
|
||||
th:text="#{page.settings.system.sessionLifetimeMinutes.help}">
|
||||
This impacts how long file and admin sessions are kept.
|
||||
</p>
|
||||
<input
|
||||
@@ -380,13 +403,13 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- MIDDLE: Default Home Page (right) -->
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-4 h-full"
|
||||
>
|
||||
<label
|
||||
class="block text-sm font-medium mb-2"
|
||||
for="defaultHomePage"
|
||||
th:text="#{page.settings.system.defaultHomePage}"
|
||||
>Default Home Page</label
|
||||
>
|
||||
<select
|
||||
@@ -394,13 +417,12 @@
|
||||
id="defaultHomePage"
|
||||
th:field="*{defaultHomePage}"
|
||||
>
|
||||
<option value="upload">Upload Page</option>
|
||||
<option value="list">File List Page</option>
|
||||
<option value="paste">Pastebin Page</option>
|
||||
<option th:text="#{page.settings.system.defaultHomePage.upload}" value="upload">Upload Page</option>
|
||||
<option th:text="#{page.settings.system.defaultHomePage.list}" value="list">File List Page</option>
|
||||
<option th:text="#{page.settings.system.defaultHomePage.paste}" value="paste">Pastebin Page</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Cron + Next Run side by side -->
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-4 md:col-span-2"
|
||||
>
|
||||
@@ -409,6 +431,7 @@
|
||||
<label
|
||||
class="block text-sm font-medium mb-1"
|
||||
for="fileDeletionCron"
|
||||
th:text="#{page.settings.system.fileDeletionCron}"
|
||||
>File Deletion Cron Expression</label
|
||||
>
|
||||
<input
|
||||
@@ -420,20 +443,19 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium mb-1">Next run</div>
|
||||
<div class="text-sm font-medium mb-1" th:text="#{page.settings.system.nextRun}">Next run</div>
|
||||
<div
|
||||
class="text-sm text-gray-700 dark:text-gray-300"
|
||||
th:text="${cronNextRunText != null ? cronNextRunText : 'Unable to parse cron'}"
|
||||
th:text="${cronNextRunText != null ? cronNextRunText : #messages.msg('validation.cron.unableToParse')}"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BOTTOM: File List Enabled (left) -->
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-4 h-full"
|
||||
>
|
||||
<div class="text-sm font-medium mb-2">File List</div>
|
||||
<div class="text-sm font-medium mb-2" th:text="#{page.settings.system.fileList}">File List</div>
|
||||
<label class="flex items-center gap-3 pl-2">
|
||||
<input
|
||||
class="h-4 w-4 rounded border-gray-400 dark:border-gray-600 accent-sky-500 dark:accent-sky-400 focus:ring-sky-500"
|
||||
@@ -441,15 +463,15 @@
|
||||
th:field="*{fileListPageEnabled}"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span class="text-sm">Enable File List Page</span>
|
||||
<span class="text-sm" th:text="#{page.settings.system.fileList.enabled}">Enable File List Page</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- BOTTOM: Admin Dashboard Button Enabled (right) -->
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-4 h-full"
|
||||
>
|
||||
<div class="text-sm font-medium mb-2">Admin Dashboard</div>
|
||||
<div class="text-sm font-medium mb-2" th:text="#{page.settings.system.adminDashboard}">Admin Dashboard
|
||||
</div>
|
||||
<label class="flex items-start gap-3 pl-2">
|
||||
<input
|
||||
class="mt-1 h-4 w-4 rounded border-gray-400 dark:border-gray-600 accent-sky-500 dark:accent-sky-400 focus:ring-sky-500"
|
||||
@@ -458,8 +480,9 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
<span class="text-sm">Show Admin Dashboard Button</span><br />
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span class="text-sm" th:text="#{page.settings.system.adminDashboard.showButton}">Show Admin Dashboard Button</span><br/>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400"
|
||||
th:text="#{page.settings.system.adminDashboard.directUrl}">
|
||||
Still available at /admin/dashboard
|
||||
</span>
|
||||
</span>
|
||||
@@ -469,7 +492,7 @@
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-4 h-full md:col-span-2"
|
||||
>
|
||||
<div class="text-sm font-medium mb-2">Pastebin</div>
|
||||
<div class="text-sm font-medium mb-2" th:text="#{page.settings.system.pastebin}">Pastebin</div>
|
||||
<label class="flex items-start gap-3 pl-2">
|
||||
<input
|
||||
class="mt-1 h-4 w-4 rounded border-gray-400 dark:border-gray-600 accent-sky-500 dark:accent-sky-400 focus:ring-sky-500"
|
||||
@@ -478,9 +501,11 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
<span class="text-sm">Enable Pastebin Page</span><br/>
|
||||
<span class="text-sm"
|
||||
th:text="#{page.settings.system.pastebin.enabled}">Enable Pastebin Page</span><br/>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400"
|
||||
>Adds a text editor tab and allows selecting Pastebin as
|
||||
th:text="#{page.settings.system.pastebin.help}">
|
||||
Adds a text editor tab and allows selecting Pastebin as
|
||||
default home page.</span
|
||||
>
|
||||
</span>
|
||||
@@ -493,7 +518,7 @@
|
||||
<section
|
||||
class="bg-white dark:bg-slate-800 p-6 md:p-8 rounded-2xl shadow-lg"
|
||||
>
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4">
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4" th:text="#{page.settings.notifications.title}">
|
||||
Notification Settings
|
||||
</h2>
|
||||
|
||||
@@ -509,8 +534,11 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-sm font-medium">Discord Webhook</div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<div class="text-sm font-medium" th:text="#{page.settings.notifications.discord.title}">Discord
|
||||
Webhook
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400"
|
||||
th:text="#{page.settings.notifications.discord.help}">
|
||||
Post file activity to a Discord channel.
|
||||
</p>
|
||||
</div>
|
||||
@@ -520,11 +548,13 @@
|
||||
<label
|
||||
class="block text-sm font-medium mb-1"
|
||||
for="discordWebhookUrl"
|
||||
th:text="#{page.settings.notifications.discord.webhookUrl}"
|
||||
>Webhook URL</label
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="discordWebhookUrl"
|
||||
th:placeholder="#{page.settings.notifications.discord.webhookUrl.placeholder}"
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
th:field="*{discordWebhookUrl}"
|
||||
type="url"
|
||||
@@ -536,8 +566,10 @@
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white text-sm font-medium px-4 py-2 transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="testDiscord"
|
||||
type="button"
|
||||
th:data-error-text="#{page.settings.notifications.testFailed}"
|
||||
th:data-testing-text="#{page.settings.notifications.testInProgress}"
|
||||
>
|
||||
Send Test to Discord
|
||||
<span th:text="#{page.settings.notifications.discord.testButton}">Send Test to Discord</span>
|
||||
</button>
|
||||
<span class="text-xs" id="discordTestStatus"></span>
|
||||
</div>
|
||||
@@ -554,8 +586,11 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-sm font-medium">Email Notifications</div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<div class="text-sm font-medium" th:text="#{page.settings.notifications.email.title}">Email
|
||||
Notifications
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400"
|
||||
th:text="#{page.settings.notifications.email.help}">
|
||||
Send emails on upload, download, and renewal events.
|
||||
</p>
|
||||
</div>
|
||||
@@ -567,11 +602,13 @@
|
||||
<label
|
||||
class="block text-sm font-medium mb-1"
|
||||
for="emailFrom"
|
||||
th:text="#{page.settings.notifications.email.fromAddress}"
|
||||
>From Address</label
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="emailFrom"
|
||||
th:placeholder="#{page.settings.notifications.email.fromAddress.placeholder}"
|
||||
placeholder="noreply@example.com"
|
||||
th:field="*{emailFrom}"
|
||||
type="email"
|
||||
@@ -579,11 +616,13 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" for="emailTo"
|
||||
>Recipients (comma separated)</label
|
||||
th:text="#{page.settings.notifications.email.recipients}">
|
||||
Recipients (comma separated)</label
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="emailTo"
|
||||
th:placeholder="#{page.settings.notifications.email.recipients.placeholder}"
|
||||
placeholder="admin@example.com, alerts@example.com"
|
||||
th:field="*{emailTo}"
|
||||
type="text"
|
||||
@@ -594,11 +633,13 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" for="smtpHost"
|
||||
>SMTP Host</label
|
||||
th:text="#{page.settings.notifications.email.smtpHost}">
|
||||
SMTP Host</label
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="smtpHost"
|
||||
th:placeholder="#{page.settings.notifications.email.smtpHost.placeholder}"
|
||||
placeholder="smtp.example.com"
|
||||
th:field="*{smtpHost}"
|
||||
type="text"
|
||||
@@ -606,7 +647,8 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1" for="smtpPort"
|
||||
>SMTP Port</label
|
||||
th:text="#{page.settings.notifications.email.smtpPort}">
|
||||
SMTP Port</label
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -620,10 +662,9 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium mb-1"
|
||||
for="smtpUsername"
|
||||
>SMTP Username</label
|
||||
<label class="block text-sm font-medium mb-1" for="smtpUsername"
|
||||
th:text="#{page.settings.notifications.email.smtpUsername}">
|
||||
SMTP Username</label
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -634,10 +675,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label
|
||||
class="block text-sm font-medium mb-1"
|
||||
for="smtpPassword"
|
||||
>SMTP Password</label
|
||||
<label class="block text-sm font-medium mb-1" for="smtpPassword"
|
||||
th:text="#{page.settings.notifications.email.smtpPassword}">
|
||||
SMTP Password</label
|
||||
>
|
||||
<input
|
||||
class="w-full rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -656,7 +696,7 @@
|
||||
th:field="*{smtpUseTls}"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span class="text-sm">Use STARTTLS</span>
|
||||
<span class="text-sm" th:text="#{page.settings.notifications.email.useStartTls}">Use STARTTLS</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center gap-3">
|
||||
@@ -666,9 +706,10 @@
|
||||
th:field="*{smtpUseSsl}"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span class="text-sm">Use Implicit SSL (SMTPS)</span>
|
||||
<span class="text-sm" th:text="#{page.settings.notifications.email.useImplicitSsl}">Use Implicit SSL (SMTPS)</span>
|
||||
</label>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400"
|
||||
th:text="#{page.settings.notifications.email.tlsHelp}">
|
||||
Use STARTTLS with port 587. Use Implicit SSL with port 465.
|
||||
</p>
|
||||
|
||||
@@ -677,8 +718,10 @@
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white text-sm font-medium px-4 py-2 transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="testEmail"
|
||||
type="button"
|
||||
th:data-error-text="#{page.settings.notifications.testFailed}"
|
||||
th:data-testing-text="#{page.settings.notifications.testInProgress}"
|
||||
>
|
||||
Send Test Email
|
||||
<span th:text="#{page.settings.notifications.email.testButton}">Send Test Email</span>
|
||||
</button>
|
||||
<span class="text-xs" id="emailTestStatus"></span>
|
||||
</div>
|
||||
@@ -697,8 +740,11 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<div>
|
||||
<div class="text-sm font-medium">Batch notifications</div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<div class="text-sm font-medium" th:text="#{page.settings.notifications.batch.title}">Batch
|
||||
notifications
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400"
|
||||
th:text="#{page.settings.notifications.batch.help}">
|
||||
Pool events and send every N minutes. Requires at least one
|
||||
channel enabled.
|
||||
</p>
|
||||
@@ -712,6 +758,7 @@
|
||||
<label
|
||||
class="block text-sm font-medium mb-1"
|
||||
for="notificationBatchMinutes"
|
||||
th:text="#{page.settings.notifications.batch.interval}"
|
||||
>Batch interval (minutes)</label
|
||||
>
|
||||
<input
|
||||
@@ -731,7 +778,7 @@
|
||||
<section
|
||||
class="bg-white dark:bg-slate-800 p-6 md:p-8 rounded-2xl shadow-lg"
|
||||
>
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4">
|
||||
<h2 class="text-xl font-semibold tracking-tight mb-4" th:text="#{page.settings.security.title}">
|
||||
Security Settings
|
||||
</h2>
|
||||
|
||||
@@ -748,19 +795,19 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
<span class="text-sm font-medium"
|
||||
>Enable Password Protection</span
|
||||
<span class="text-sm font-medium" th:text="#{page.settings.security.appPasswordEnabled}">
|
||||
Enable Password Protection</span
|
||||
><br />
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400"
|
||||
>Protect the whole app with a password</span
|
||||
th:text="#{page.settings.security.appPasswordEnabled.help}">
|
||||
Protect the whole app with a password</span
|
||||
>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<!-- App Password -->
|
||||
<div class="mt-4 hidden" id="passwordInputGroup">
|
||||
<label class="block text-sm font-medium mb-1"
|
||||
>App Password</label
|
||||
<label class="block text-sm font-medium mb-1" th:text="#{page.settings.security.appPassword}">
|
||||
App Password</label
|
||||
>
|
||||
<input
|
||||
id="appPassword"
|
||||
@@ -782,10 +829,11 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
<span class="text-sm font-medium"
|
||||
>Disable upload passwords</span
|
||||
<span class="text-sm font-medium" th:text="#{page.settings.security.disableUploadPasswords}">
|
||||
Disable upload passwords</span
|
||||
><br />
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400"
|
||||
th:text="#{page.settings.security.disableUploadPasswords.help}">
|
||||
Prevents users from setting passwords on uploads (and
|
||||
disables per-file encryption).
|
||||
</span>
|
||||
@@ -806,9 +854,10 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
<span class="text-sm font-medium">Disable share links</span
|
||||
<span class="text-sm font-medium" th:text="#{page.settings.security.disableShareLinks}">Disable share links</span
|
||||
><br />
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400"
|
||||
th:text="#{page.settings.security.disableShareLinks.help}">
|
||||
Hide share options on file pages and block new share links
|
||||
from being generated.
|
||||
</span>
|
||||
@@ -829,9 +878,10 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
<span class="text-sm font-medium">Simplify share links</span
|
||||
<span class="text-sm font-medium" th:text="#{page.settings.security.simplifyShareLinks}">Simplify share links</span
|
||||
><br />
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400"
|
||||
th:text="#{page.settings.security.simplifyShareLinks.help}">
|
||||
Auto-generate an unlimited, non-expiring share link and hide
|
||||
manual share options on file pages.
|
||||
</span>
|
||||
@@ -850,10 +900,11 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
<span class="text-sm font-medium"
|
||||
>Disable File Encryption</span
|
||||
<span class="text-sm font-medium" th:text="#{page.settings.security.disableFileEncryption}">
|
||||
Disable File Encryption</span
|
||||
><br />
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400"
|
||||
th:text="#{page.settings.security.disableFileEncryption.help}">
|
||||
If checked, files will not be encrypted even if file
|
||||
password protection is enabled.
|
||||
</span>
|
||||
@@ -872,7 +923,8 @@
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>
|
||||
<span class="flex items-center gap-2 text-sm font-medium">
|
||||
<span class="flex items-center gap-2 text-sm font-medium"
|
||||
th:text="#{page.settings.security.metadataStripping}">
|
||||
Metadata stripping
|
||||
<span
|
||||
class="text-xs text-sky-700 dark:text-sky-400"
|
||||
@@ -881,7 +933,8 @@
|
||||
</span>
|
||||
<br />
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400"
|
||||
>Strip optional file metadata where possible.</span
|
||||
th:text="#{page.settings.security.metadataStripping.help}">
|
||||
Strip optional file metadata where possible.</span
|
||||
>
|
||||
</span>
|
||||
</label>
|
||||
@@ -895,7 +948,7 @@
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-6 py-2 transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
type="submit"
|
||||
>
|
||||
Save Settings
|
||||
<span th:text="#{page.settings.save}">Save Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -904,7 +957,7 @@
|
||||
<section
|
||||
class="bg-white dark:bg-slate-800 p-6 md:p-8 rounded-2xl shadow-lg mt-12"
|
||||
>
|
||||
<h2 class="text-2xl font-semibold tracking-tight mb-6">
|
||||
<h2 class="text-2xl font-semibold tracking-tight mb-6" th:text="#{page.settings.about.title(${appName})}">
|
||||
About QuickDrop
|
||||
</h2>
|
||||
|
||||
@@ -912,7 +965,7 @@
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-5"
|
||||
>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Version</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400" th:text="#{page.settings.about.version}">Version</div>
|
||||
<div
|
||||
class="text-lg font-semibold mt-1"
|
||||
th:text="${aboutInfo.appVersion}"
|
||||
@@ -924,11 +977,13 @@
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-5"
|
||||
>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">Database</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400" th:text="#{page.settings.about.database}">Database
|
||||
</div>
|
||||
<div class="text-lg font-semibold mt-1">
|
||||
SQLite
|
||||
<span class="font-normal" th:text="${aboutInfo.sqliteVersion}"
|
||||
>Unknown</span
|
||||
<span th:text="#{page.settings.about.database.sqlite}">SQLite</span>
|
||||
<span class="font-normal"
|
||||
th:text="${aboutInfo.sqliteVersion != null ? aboutInfo.sqliteVersion : #messages.msg('page.settings.about.unknown')}">
|
||||
Unknown</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -936,7 +991,7 @@
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-5"
|
||||
>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400" th:text="#{page.settings.about.javaVersion}">
|
||||
Java Version
|
||||
</div>
|
||||
<div
|
||||
@@ -950,7 +1005,7 @@
|
||||
<div
|
||||
class="rounded-xl border border-slate-200 dark:border-slate-700 p-5"
|
||||
>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">OS Info</div>
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400" th:text="#{page.settings.about.osInfo}">OS Info</div>
|
||||
<div
|
||||
class="text-lg font-semibold mt-1"
|
||||
th:text="${aboutInfo.osInfo}"
|
||||
@@ -963,5 +1018,6 @@
|
||||
</main>
|
||||
|
||||
<script src="/js/settings.js"></script>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title th:text="${appName} + ' - Upload'">Upload File</title>
|
||||
<title th:text="#{page.upload.title.browser(${appName})}">Upload File</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
@@ -40,13 +40,13 @@
|
||||
target="_blank"
|
||||
th:if="${!#strings.equalsIgnoreCase(#strings.trim(appName), 'QuickDrop')}"
|
||||
>
|
||||
Powered by QuickDrop
|
||||
<span th:text="#{nav.poweredBy}">Powered by QuickDrop</span>
|
||||
</a>
|
||||
</div>
|
||||
<button
|
||||
aria-controls="navMenu"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
th:aria-label="#{nav.toggleNavigation}"
|
||||
class="md:hidden rounded-lg p-2 hover:bg-sky-600 dark:hover:bg-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="navToggle"
|
||||
type="button"
|
||||
@@ -75,18 +75,21 @@
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/list"
|
||||
th:if="${isFileListPageEnabled or hasAdminSession}"
|
||||
th:text="#{nav.viewFiles}"
|
||||
>View Files</a
|
||||
>
|
||||
<a
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/file/paste/new"
|
||||
th:if="${isPastebinEnabled}"
|
||||
th:text="#{nav.pastebin}"
|
||||
>Pastebin</a
|
||||
>
|
||||
<a
|
||||
class="nav-menu-item hover:text-white"
|
||||
href="/admin/dashboard"
|
||||
th:if="${isAdminDashboardButtonEnabled}"
|
||||
th:text="#{nav.adminDashboard}"
|
||||
>Admin Dashboard</a
|
||||
>
|
||||
<form
|
||||
@@ -99,11 +102,23 @@
|
||||
th:name="${_csrf.parameterName}"
|
||||
th:value="${_csrf.token}"
|
||||
/>
|
||||
<button class="nav-menu-item hover:text-white" type="submit">
|
||||
<button class="nav-menu-item hover:text-white" th:text="#{nav.logout}" type="submit">
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchUpload" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-600 bg-slate-800 text-slate-100 px-2 py-1"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchUpload"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
aria-label="Toggle theme"
|
||||
class="nav-menu-item theme-toggle rounded-lg p-2 transition-colors hover:bg-sky-600 dark:hover:bg-sky-500 active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
@@ -118,27 +133,29 @@
|
||||
<main class="max-w-7xl mx-auto px-4 p-6 md:p-8">
|
||||
<header class="text-center mb-8 space-y-1">
|
||||
<h1 class="text-3xl md:text-4xl font-semibold tracking-tight">
|
||||
Upload a File or Folder
|
||||
<span th:text="#{page.upload.title}">Upload a File or Folder</span>
|
||||
</h1>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
Accepted file types: any • Max size
|
||||
<span th:text="#{page.upload.acceptedTypes}">Accepted file types: any</span>
|
||||
•
|
||||
<span th:text="#{page.upload.maxSizeLabel}">Max size</span>
|
||||
<span class="maxFileSize" th:text="${maxFileSize}">1GB</span>
|
||||
</p>
|
||||
<p
|
||||
class="text-gray-600 dark:text-gray-300"
|
||||
th:if="${!isKeepIndefinitelyAdminOnly}"
|
||||
>
|
||||
Files are deleted after
|
||||
<span class="maxFileLifeTime" th:text="${maxFileLifeTime}">30</span>
|
||||
days unless “Keep indefinitely” is selected.
|
||||
<span
|
||||
th:text="#{page.upload.retention.standard(${maxFileLifeTime})}"
|
||||
>Files are deleted after 30 days unless “Keep indefinitely” is selected.</span>
|
||||
</p>
|
||||
<p
|
||||
class="text-gray-600 dark:text-gray-300"
|
||||
th:if="${isKeepIndefinitelyAdminOnly}"
|
||||
>
|
||||
Files are deleted after
|
||||
<span class="maxFileLifeTime" th:text="${maxFileLifeTime}">30</span>
|
||||
days.
|
||||
<span th:text="#{page.upload.retention.adminOnly(${maxFileLifeTime})}">
|
||||
Files are deleted after 30 days.
|
||||
</span>
|
||||
</p>
|
||||
</header>
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
@@ -165,6 +182,8 @@
|
||||
class="block text-gray-700 dark:text-gray-100 text-xl font-semibold"
|
||||
data-default-text="Drop file or folder"
|
||||
id="dropZoneInstructions"
|
||||
th:attr="data-default-text=#{page.upload.dropzone.title}"
|
||||
th:text="#{page.upload.dropzone.title}"
|
||||
>Drop file or folder</span
|
||||
>
|
||||
<p
|
||||
@@ -178,14 +197,14 @@
|
||||
type="button"
|
||||
id="fileSelectButton"
|
||||
>
|
||||
Select file
|
||||
<span th:text="#{button.selectFile}">Select file</span>
|
||||
</button>
|
||||
<button
|
||||
class="upload-shell-button"
|
||||
type="button"
|
||||
id="folderSelectButton"
|
||||
>
|
||||
Select folder
|
||||
<span th:text="#{button.selectFolder}">Select folder</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,11 +218,12 @@
|
||||
multiple
|
||||
/>
|
||||
<label class="block">
|
||||
<span class="sr-only">Description</span>
|
||||
<span class="sr-only" th:text="#{form.description.placeholder}">Description</span>
|
||||
<input
|
||||
class="mt-1 w-full rounded-lg border border-gray-400 dark:border-gray-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="description"
|
||||
name="description"
|
||||
th:placeholder="#{form.description.placeholder}"
|
||||
placeholder="Description"
|
||||
type="text"
|
||||
/>
|
||||
@@ -217,16 +237,15 @@
|
||||
name="keepIndefinitely"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span class="text-sm">Keep indefinitely</span>
|
||||
<span class="text-sm" th:text="#{form.keepIndefinitely}">Keep indefinitely</span>
|
||||
</label>
|
||||
<small
|
||||
class="block ml-6 mt-1 text-sm text-gray-600 dark:text-gray-300"
|
||||
>If checked, this file will not be auto-deleted after
|
||||
<span class="maxFileLifeTime" th:text="${maxFileLifeTime}"
|
||||
>30</span
|
||||
>
|
||||
days.</small
|
||||
>
|
||||
<span th:text="#{form.keepIndefinitely.help.beforeDays}">If checked, this file will not be auto-deleted after</span>
|
||||
<span class="maxFileLifeTime" th:text="${maxFileLifeTime}">30</span>
|
||||
<span th:text="#{form.keepIndefinitely.help.afterDays}">days.</span>
|
||||
</small>
|
||||
</div>
|
||||
<div th:if="${isFileListPageEnabled and canHideFromList}">
|
||||
<label class="inline-flex items-start gap-2">
|
||||
@@ -236,21 +255,21 @@
|
||||
name="hidden"
|
||||
type="checkbox"
|
||||
/>
|
||||
<span class="text-sm">Hide from file list</span>
|
||||
<span class="text-sm" th:text="#{form.hideFromList}">Hide from file list</span>
|
||||
</label>
|
||||
<small
|
||||
class="block ml-6 mt-1 text-sm text-gray-600 dark:text-gray-300"
|
||||
>If checked, this file won’t appear on the “View Files”
|
||||
page.</small
|
||||
>
|
||||
th:text="#{form.hideFromList.help}"
|
||||
>If checked, this file won’t appear on the “View Files” page.</small>
|
||||
</div>
|
||||
</div>
|
||||
<label class="block" th:if="${uploadPasswordEnabled}">
|
||||
<span class="sr-only">Password (Optional)</span>
|
||||
<span class="sr-only" th:text="#{form.password.optional}">Password (Optional)</span>
|
||||
<input
|
||||
class="mt-1 w-full rounded-lg border border-gray-400 dark:border-gray-600 bg-white dark:bg-slate-700 px-3 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
id="password"
|
||||
name="password"
|
||||
th:placeholder="#{form.password.optional}"
|
||||
placeholder="Password (Optional)"
|
||||
type="password"
|
||||
/>
|
||||
@@ -281,14 +300,14 @@
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-6 py-2 transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
type="button"
|
||||
>
|
||||
Upload
|
||||
<span th:text="#{button.upload}">Upload</span>
|
||||
</button>
|
||||
<button
|
||||
id="uploadCancel"
|
||||
class="hidden rounded-lg border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-100 font-medium px-4 py-2 transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-slate-400"
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
<span th:text="#{button.cancel}">Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -301,7 +320,7 @@
|
||||
class="text-gray-600 dark:text-gray-300 mb-2"
|
||||
id="uploadStatus"
|
||||
>
|
||||
Upload started...
|
||||
<span th:text="#{page.upload.status.started}">Upload started...</span>
|
||||
</p>
|
||||
<div
|
||||
class="w-full max-w-md mx-auto bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden"
|
||||
@@ -321,23 +340,24 @@
|
||||
class="mt-4 text-center text-gray-600 dark:text-gray-300"
|
||||
th:if="${isEncryptionEnabled and uploadPasswordEnabled}"
|
||||
>
|
||||
All password-protected files are also encrypted for additional security.
|
||||
<span th:text="#{page.upload.encryptionNotice}">All password-protected files are also encrypted for additional security.</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-2 text-center text-gray-600 dark:text-gray-300"
|
||||
th:if="${isMetadataStrippingEnabled}"
|
||||
>
|
||||
Metadata stripping is enabled. Supported:
|
||||
<span th:text="#{page.upload.metadataNotice}">Metadata stripping is enabled. Supported:</span>
|
||||
<span data-supported-metadata></span>.
|
||||
</div>
|
||||
</main>
|
||||
<script th:inline="javascript">
|
||||
/*<![CDATA[*/
|
||||
window.uploadPasswordEnabled = [[${uploadPasswordEnabled}]];
|
||||
window.isMetadataStrippingEnabled = [[${isMetadataStrippingEnabled}]];
|
||||
window.uploadPasswordEnabled = /*[[${uploadPasswordEnabled}]]*/ false;
|
||||
window.isMetadataStrippingEnabled = /*[[${isMetadataStrippingEnabled}]]*/ false;
|
||||
/*]]>*/
|
||||
</script>
|
||||
<script type="module" src="/js/upload.js"></script>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title th:text="'Welcome to ' + ${appName}">Welcome to QuickDrop</title>
|
||||
<title th:text="#{page.welcome.title.browser(${appName})}">Welcome to QuickDrop</title>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
<link href="/css/tailwind.css" rel="stylesheet" />
|
||||
<link href="/css/custom.css" rel="stylesheet" />
|
||||
@@ -16,15 +16,29 @@
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-800 dark:bg-slate-900 dark:text-gray-100">
|
||||
<main class="max-w-7xl mx-auto px-4 py-6 md:py-8">
|
||||
<div class="flex justify-end mb-4">
|
||||
<div class="flex items-center text-xs">
|
||||
<label class="sr-only" for="localeSwitchWelcome" th:text="#{nav.language}">Language</label>
|
||||
<select
|
||||
class="locale-switch-select rounded-md border border-slate-300 bg-white text-slate-700 px-2 py-1 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-100"
|
||||
data-locale-select="true"
|
||||
id="localeSwitchWelcome"
|
||||
>
|
||||
<option th:selected="${currentLang == 'en'}" value="en">EN</option>
|
||||
<option th:selected="${currentLang == 'bg'}" value="bg">BG</option>
|
||||
<option th:selected="${currentLang == 'de'}" value="de">DE</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<header class="text-center mb-8 space-y-1">
|
||||
<h1 class="text-3xl md:text-4xl font-semibold tracking-tight">
|
||||
<span th:text="'Welcome to ' + ${appName}">Welcome to QuickDrop</span>
|
||||
<span th:text="#{page.welcome.title(${appName})}">Welcome to QuickDrop</span>
|
||||
</h1>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
<span th:text="'Thank you for setting up ' + ${appName} + '!'">
|
||||
<span th:text="#{page.welcome.setupThanks(${appName})}">
|
||||
Thank you for setting up QuickDrop!
|
||||
</span>
|
||||
Please set an admin password for the dashboard.
|
||||
<span th:text="#{page.welcome.setupPrompt}">Please set an admin password for the dashboard.</span>
|
||||
</p>
|
||||
</header>
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
@@ -43,12 +57,13 @@
|
||||
type="hidden"
|
||||
/>
|
||||
<label class="block w-full">
|
||||
<span class="sr-only">Admin Password</span>
|
||||
<span class="sr-only" th:text="#{form.adminPassword.label}">Admin Password</span>
|
||||
<input
|
||||
class="block w-full rounded-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-gray-700 px-4 py-3 text-lg text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:border-sky-500 focus:ring-2 focus:ring-sky-500 focus:outline-none"
|
||||
id="adminPassword"
|
||||
minlength="8"
|
||||
name="adminPassword"
|
||||
th:placeholder="#{form.adminPassword.placeholder}"
|
||||
placeholder="Admin Password"
|
||||
required
|
||||
style="line-height: 2.7rem"
|
||||
@@ -56,12 +71,13 @@
|
||||
/>
|
||||
</label>
|
||||
<label class="block w-full">
|
||||
<span class="sr-only">Confirm Password</span>
|
||||
<span class="sr-only" th:text="#{form.password.confirm}">Confirm Password</span>
|
||||
<input
|
||||
class="block w-full rounded-full border border-slate-300 dark:border-slate-600 bg-white dark:bg-gray-700 px-4 py-3 text-lg text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:border-sky-500 focus:ring-2 focus:ring-sky-500 focus:outline-none"
|
||||
id="confirmPassword"
|
||||
minlength="8"
|
||||
name="confirmPassword"
|
||||
th:placeholder="#{form.password.confirm.placeholder}"
|
||||
placeholder="Confirm Password"
|
||||
required
|
||||
style="line-height: 2.7rem"
|
||||
@@ -73,14 +89,14 @@
|
||||
id="error-message"
|
||||
style="display: none"
|
||||
>
|
||||
Passwords do not match.
|
||||
<span th:text="#{page.welcome.passwordMismatch}">Passwords do not match.</span>
|
||||
</div>
|
||||
<div class="pt-2 text-center">
|
||||
<button
|
||||
class="rounded-lg bg-sky-500 hover:bg-sky-600 dark:bg-sky-400 dark:hover:bg-sky-500 text-white font-medium px-4 py-2 transition-colors active:scale-95 focus:outline-none focus:ring-2 focus:ring-sky-500"
|
||||
type="submit"
|
||||
>
|
||||
Set Admin Password
|
||||
<span th:text="#{page.welcome.setAdminPassword}">Set Admin Password</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -100,5 +116,6 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/js/locale-switcher.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user