diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 00000000..dcc59474 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,54 @@ +name: Deploy Docs + +on: + push: + branches: [main] + paths: + - 'docs/**' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install dependencies + working-directory: docs + run: npm install + + - name: Build docs + working-directory: docs + run: npm run docs:build + + - uses: actions/configure-pages@v5 + + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/Dockerfile b/Dockerfile index 525712ae..d8d35140 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,11 @@ -# Stage 1: Cache dependencies -FROM rust:1.94.0-alpine3.23 AS cacher -WORKDIR /app +# ─── Stage 1: Shared build base (avoids duplicate apk install) ──────────────── +FROM rust:1.94.1-alpine3.23 AS base RUN apk --no-cache upgrade && \ apk add --no-cache musl-dev pkgconfig postgresql-dev gcc perl make + +# ─── Stage 2: Cache dependencies ───────────────────────────────────────────── +FROM base AS cacher +WORKDIR /app COPY Cargo.toml Cargo.lock ./ # build.rs + static/ are needed so the build script can run and set OUT_DIR COPY build.rs ./ @@ -13,11 +16,10 @@ RUN mkdir -p src/bin && \ echo 'fn main() {}' > src/bin/generate-openapi.rs && \ cargo build --release && \ rm -rf src static-dist target/release/deps/oxicloud* target/release/build/oxicloud-* -# Stage 2: Build the application -FROM rust:1.94.0-alpine3.23 AS builder + +# ─── Stage 3: Build the application ────────────────────────────────────────── +FROM base AS builder WORKDIR /app -RUN apk --no-cache upgrade && \ - apk add --no-cache musl-dev pkgconfig postgresql-dev gcc perl make # Copy cached dependencies (only target dir and cargo registry) COPY --from=cacher /app/target target COPY --from=cacher /usr/local/cargo/registry /usr/local/cargo/registry @@ -30,7 +32,7 @@ COPY migrations migrations ARG DATABASE_URL="postgres://postgres:postgres@localhost/oxicloud" RUN DATABASE_URL="${DATABASE_URL}" cargo build --release -# Stage 3: Create minimal final image +# ─── Stage 4: Minimal runtime image ────────────────────────────────────────── FROM alpine:3.23.3 # OCI image metadata @@ -44,19 +46,13 @@ LABEL org.opencontainers.image.title="OxiCloud" \ # Install only necessary runtime dependencies and update packages # su-exec is needed by the entrypoint to drop privileges after fixing volume permissions RUN apk --no-cache upgrade && \ - apk add --no-cache libgcc ca-certificates libpq tzdata su-exec - -# Create non-root user -RUN addgroup -g 1001 -S oxicloud && \ + apk add --no-cache libgcc ca-certificates libpq tzdata su-exec && \ + addgroup -g 1001 -S oxicloud && \ adduser -u 1001 -S oxicloud -G oxicloud -# Copy only the compiled binary -COPY --from=builder /app/target/release/oxicloud /usr/local/bin/ -RUN chmod +x /usr/local/bin/oxicloud - -# Copy entrypoint script -COPY entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/entrypoint.sh +# Copy the compiled binary and entrypoint (--chmod avoids extra RUN chmod layers) +COPY --from=builder --chmod=755 /app/target/release/oxicloud /usr/local/bin/ +COPY --chmod=755 entrypoint.sh /usr/local/bin/entrypoint.sh # Copy processed static files (bundled/minified by build.rs in release) COPY --from=builder --chown=oxicloud:oxicloud /app/static-dist /app/static @@ -69,6 +65,11 @@ WORKDIR /app # Expose application port EXPOSE 8086 +# Basic health check — verifies the HTTP server responds on the main port. +# Docker / Compose / Swarm will mark the container unhealthy after 3 failures. +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://localhost:8086/api/version || exit 1 + # Entrypoint fixes volume permissions then drops to oxicloud user. # The container starts as root so it can chown mounted volumes, # then su-exec drops privileges before running the application. diff --git a/README.md b/README.md index c0231010..dccc38af 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,11 @@ [![GitHub Issues](https://img.shields.io/github/issues/diocrafts/OxiCloud?style=for-the-badge)](https://github.com/diocrafts/OxiCloud/issues) [![Last Commit](https://img.shields.io/github/last-commit/diocrafts/OxiCloud?style=for-the-badge)](https://github.com/diocrafts/OxiCloud/commits/main) - +[**Documentation**](https://diocrafts.github.io/OxiCloud/) | [**Quick Start**](#-quick-start) +
+**English** | [Español](docs/es/) -
+ NextCloud was too slow on my home server. So I built OxiCloud — a complete cloud platform written in Rust that runs on minimal hardware and stays out of your way. @@ -86,8 +88,8 @@ NextCloud was too slow on my home server. So I built OxiCloud — a complete clo - **Dual DB pool** — dedicated maintenance pool so background tasks never starve user queries - **LTO-optimised release** — fat LTO, 1 codegen-unit, `opt-level = 3`, stripped - **Write-behind caching** (moka) — sub-millisecond hot reads -- **112 automated tests** — `cargo test` on every push (CI) -- **9 languages** — EN, ES, DE, FR, IT, PT, NL, ZH, FA +- **222+ automated tests** — `cargo test` on every push (CI) +- **14 languages** — EN, ES, DE, FR, IT, PT, NL, ZH, JA, KO, AR, HI, FA, RU --- @@ -105,6 +107,8 @@ NextCloud was too slow on my home server. So I built OxiCloud — a complete clo | **Trash / recycle bin** | ✅ Working | Soft-delete with restore | | **Full-text search** | ✅ Working | Recursive subtree search | | **Shared links** | ✅ Working | Optional password protection | +| **Music library & playlists** | ✅ Working | Streaming player, drag-and-drop reorder | +| **Photo gallery** | ✅ Working | Day/month/year views, multi-select | | **Desktop sync client** | ❌ Planned | Not yet available | | **Android / iOS app** | ❌ Planned | Not yet available | | **End-to-end encryption** | ❌ Planned | Roadmap item | @@ -208,7 +212,7 @@ All config via environment variables (see [`example.env`](example.env)): | `OXICLOUD_ENABLE_AUTH` | `true` | Toggle authentication | | `OXICLOUD_ENABLE_TRASH` | `true` | Toggle trash / recycle bin | -Full reference: [`example.env`](example.env) · [Deployment guide](doc/deployment.md) · [OIDC examples](doc/oidc-config-examples.md) +Full reference: [`example.env`](example.env) · [Deployment guide](https://diocrafts.github.io/OxiCloud/config/deployment) · [OIDC setup](https://diocrafts.github.io/OxiCloud/config/oidc) --- @@ -241,30 +245,24 @@ CSS & JS linter is `biome` (can be installed via `cargo install biome-cli` or on |--------|-------| | Rust source files | 170 | | Lines of code | ~50 000 | -| Automated tests | 112 | -| Documentation pages | 35 | +| Automated tests | 222+ | +| Documentation pages | 35+ | --- ## Documentation -Extensive docs live in [`doc/`](doc/): +📖 **Full documentation:** [**diocrafts.github.io/OxiCloud**](https://diocrafts.github.io/OxiCloud/) -| Topic | Link | -|-------|------| -| Deployment & Docker | [deployment.md](doc/deployment.md) | -| WebDAV integration | [webdav-integration-guide.md](doc/webdav-integration-guide.md) | -| CalDAV / CardDAV | [caldav-technical-spec.md](doc/caldav-technical-spec.md) · [carddav-technical-spec.md](doc/carddav-technical-spec.md) | -| OIDC / SSO setup | [oidc-integration.md](doc/oidc-integration.md) · [oidc-config-examples.md](doc/oidc-config-examples.md) | -| WOPI (Office editing) | [wopi-integration.md](doc/wopi-integration.md) | -| Chunked uploads | [chunked-uploads.md](doc/chunked-uploads.md) | -| Deduplication | [deduplication.md](doc/deduplication.md) | -| Search | [search.md](doc/search.md) | -| Caching architecture | [caching-architecture.md](doc/caching-architecture.md) | -| Storage quotas | [storage-quotas.md](doc/storage-quotas.md) | -| Trash / recycle bin | [trash-feature-summary.md](doc/trash-feature-summary.md) | -| Internationalisation | [i18n.md](doc/i18n.md) | -| Internal architecture | [internal-architecture.md](doc/internal-architecture.md) | +| Topic | Online | Source | +|-------|--------|--------| +| Quick Start | [Guide](https://diocrafts.github.io/OxiCloud/guide/installation) | [doc/deployment.md](doc/deployment.md) | +| WebDAV | [Guide](https://diocrafts.github.io/OxiCloud/guide/webdav) | [doc/webdav-integration-guide.md](doc/webdav-integration-guide.md) | +| CalDAV / CardDAV | [Guide](https://diocrafts.github.io/OxiCloud/guide/caldav-carddav) | [doc/caldav-technical-spec.md](doc/caldav-technical-spec.md) | +| OIDC / SSO | [Guide](https://diocrafts.github.io/OxiCloud/config/oidc) | [doc/oidc-integration.md](doc/oidc-integration.md) | +| WOPI (Office) | [Guide](https://diocrafts.github.io/OxiCloud/config/wopi) | [doc/wopi-integration.md](doc/wopi-integration.md) | +| Architecture | [Guide](https://diocrafts.github.io/OxiCloud/architecture/) | [doc/internal-architecture.md](doc/internal-architecture.md) | +| All env variables | [Reference](https://diocrafts.github.io/OxiCloud/config/env) | [`example.env`](example.env) | --- diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..2c1fa99c --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +.vitepress/dist/ +.vitepress/cache/ diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 00000000..6af8c0f3 --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,165 @@ +import { defineConfig } from "vitepress"; + +export default defineConfig({ + title: "OxiCloud", + description: "Self-hosted cloud storage, calendar & contacts — blazingly fast", + + base: "/OxiCloud/", + + sitemap: { + hostname: "https://diocrafts.github.io/OxiCloud", + lastmodDateOnly: false, + }, + + markdown: { + image: { + lazyLoading: true, + }, + }, + + lastUpdated: true, + + ignoreDeadLinks: [ + /^https?:\/\/localhost/, + ], + + locales: { + root: { + label: "English", + lang: "en", + }, + es: { + label: "Español", + lang: "es", + link: "/es/", + title: "OxiCloud", + description: "Almacenamiento en la nube autoalojado, calendario y contactos — increíblemente rápido", + themeConfig: { + nav: [ + { text: "Inicio", link: "/es/" }, + { text: "Guía", link: "/es/guide/" }, + { text: "Configuración", link: "/es/config/" }, + { text: "FAQ", link: "/es/faq" }, + ], + editLink: { + pattern: "https://github.com/DioCrafts/OxiCloud/tree/main/docs/:path", + text: "Editar esta página en GitHub", + }, + sidebar: { + "/es/": [ + { + text: "Introducción", + items: [ + { text: "¿Qué es OxiCloud?", link: "/es/guide/" }, + { text: "Inicio Rápido", link: "/es/guide/installation" }, + ], + }, + { + text: "Configuración", + items: [ + { text: "Despliegue & Docker", link: "/es/config/deployment" }, + { text: "Variables de Entorno", link: "/es/config/env" }, + { text: "OIDC / SSO", link: "/es/config/oidc" }, + { text: "WOPI (Office)", link: "/es/config/wopi" }, + ], + }, + { + text: "Características", + items: [ + { text: "WebDAV", link: "/es/guide/webdav" }, + { text: "CalDAV & CardDAV", link: "/es/guide/caldav-carddav" }, + { text: "Subida Chunked", link: "/es/guide/chunked-uploads" }, + { text: "Deduplicación", link: "/es/guide/deduplication" }, + { text: "Búsqueda", link: "/es/guide/search" }, + { text: "Papelera", link: "/es/guide/trash" }, + ], + }, + { + text: "Arquitectura", + items: [ + { text: "Arquitectura Interna", link: "/es/architecture/" }, + { text: "Caché", link: "/es/architecture/caching" }, + { text: "Cuotas de Almacenamiento", link: "/es/architecture/storage-quotas" }, + ], + }, + { text: "FAQ", link: "/es/faq" }, + ], + }, + }, + }, + }, + + head: [ + ["link", { rel: "icon", href: "/OxiCloud/logo.svg" }], + ], + + themeConfig: { + logo: "/logo.svg", + + search: { + provider: "local", + }, + + editLink: { + pattern: "https://github.com/DioCrafts/OxiCloud/tree/main/docs/:path", + text: "Edit this page on GitHub", + }, + + nav: [ + { text: "Home", link: "/" }, + { text: "Guide", link: "/guide/" }, + { text: "Configuration", link: "/config/" }, + { text: "FAQ", link: "/faq" }, + ], + + sidebar: { + "/": [ + { + text: "Introduction", + items: [ + { text: "What is OxiCloud?", link: "/guide/" }, + { text: "Quick Start", link: "/guide/installation" }, + ], + }, + { + text: "Configuration", + items: [ + { text: "Deployment & Docker", link: "/config/deployment" }, + { text: "Environment Variables", link: "/config/env" }, + { text: "OIDC / SSO", link: "/config/oidc" }, + { text: "WOPI (Office Editing)", link: "/config/wopi" }, + ], + }, + { + text: "Features", + items: [ + { text: "WebDAV", link: "/guide/webdav" }, + { text: "CalDAV & CardDAV", link: "/guide/caldav-carddav" }, + { text: "Chunked Uploads", link: "/guide/chunked-uploads" }, + { text: "Deduplication", link: "/guide/deduplication" }, + { text: "Search", link: "/guide/search" }, + { text: "Trash & Recycle Bin", link: "/guide/trash" }, + ], + }, + { + text: "Architecture", + items: [ + { text: "Internal Architecture", link: "/architecture/" }, + { text: "Caching", link: "/architecture/caching" }, + { text: "Storage Quotas", link: "/architecture/storage-quotas" }, + ], + }, + { text: "FAQ", link: "/faq" }, + ], + }, + + socialLinks: [ + { icon: "github", link: "https://github.com/DioCrafts/OxiCloud" }, + ], + + footer: { + message: "Released under the MIT License.", + copyright: "Copyright © 2025-present DioCrafts", + }, + }, +}); diff --git a/docs/architecture/caching.md b/docs/architecture/caching.md new file mode 100644 index 00000000..be1776cc --- /dev/null +++ b/docs/architecture/caching.md @@ -0,0 +1,37 @@ +# Caching Architecture + +OxiCloud uses **moka** (a lock-free, concurrent cache) for write-behind caching that delivers sub-millisecond hot reads. + +## Cache Layers + +| Cache | TTL | Max Entries | Purpose | +|---|---|---|---| +| File metadata | 60 s | 10 000 | Avoid re-querying PostgreSQL for file info | +| Directory listings | 120 s | 10 000 | Frequently accessed folder contents | +| Thumbnail cache | configurable | 1 000 | Generated WebP/AVIF thumbnails | +| Image transcode | configurable | 500 | On-the-fly image transcoding results | +| Blob hash | 30 s TTI | 5 000 | SHA-256 hashes for dedup lookups | +| Audio metadata | — | 2 000 | ID3 tags and duration | + +## How It Works + +1. **Read path:** check cache → if hit, return immediately (sub-ms); if miss, query PostgreSQL, populate cache, return +2. **Write path:** update PostgreSQL → invalidate relevant cache entries +3. **TTL expiry:** entries are evicted after their time-to-live, ensuring eventual consistency + +## Why moka? + +- **Lock-free** — no mutex contention under concurrent access +- **Bounded memory** — max entries prevent unbounded growth +- **TTL + TTI** — supports both time-to-live and time-to-idle eviction +- **Async-ready** — works natively with Tokio + +## Configuration + +Cache parameters are currently hardcoded in `src/common/config.rs`. Key defaults: + +```rust +file_cache_ttl_ms: 60_000, // 1 minute +directory_cache_ttl_ms: 120_000, // 2 minutes +max_cache_entries: 10_000, +``` diff --git a/docs/architecture/index.md b/docs/architecture/index.md new file mode 100644 index 00000000..fb6418a7 --- /dev/null +++ b/docs/architecture/index.md @@ -0,0 +1,63 @@ +# Internal Architecture + +OxiCloud follows a **hexagonal (ports & adapters) architecture** with four layers: + +``` +┌───────────────────────────────────────────────────────────────┐ +│ Interfaces │ REST API, WebDAV, CalDAV, CardDAV, WOPI │ +├───────────────────────────────────────────────────────────────┤ +│ Application │ Use cases, DTOs, port definitions │ +├───────────────────────────────────────────────────────────────┤ +│ Domain │ Entities, business rules, repository traits │ +├───────────────────────────────────────────────────────────────┤ +│ Infrastructure│ PostgreSQL, filesystem, caching, auth │ +└───────────────────────────────────────────────────────────────┘ +``` + +All cross-layer dependencies point **inward** via trait-based ports. The DI container (`AppServiceFactory`) wires concrete implementations at startup. + +## Storage Model: 100% Blob Storage + +- **File metadata** (name, folder, size, user, timestamps, trash status) → PostgreSQL (`storage.files`) +- **File content** → content-addressed blobs via DedupService at `.blobs/{prefix}/{hash}.blob` +- **Folder structure** → purely virtual, rows in `storage.folders` (no filesystem directories per user) +- **Trash** → soft-delete flags on files/folders, exposed via `storage.trash_items` VIEW + +## Dependency Injection + +`AppServiceFactory` in `src/common/di.rs` builds all services in a defined order: + +1. **Core services** — paths, content cache, thumbnails, chunked upload, transcode, dedup, compression +2. **Repositories** — `FolderDbRepository`, `FileBlobReadRepository`, `FileBlobWriteRepository`, `TrashDbRepository` +3. **Trash service** (if enabled) +4. **Application services** — folder, file upload/retrieval/management, search, i18n +5. **Share service** (if enabled) +6. **DB services** — favorites, recent, storage usage, auth +7. **CalDAV/CardDAV services** +8. **ZIP service** (last, depends on file & folder services) +9. **Assemble `AppState`** + +## Project Structure + +``` +src/ +├── common/ # Config, DI container, errors +├── domain/ # Entities, repository traits +├── application/ # Use cases, DTOs, port traits +├── infrastructure/ # PostgreSQL repos, filesystem, caching +└── interfaces/ # HTTP handlers, WebDAV, CalDAV, CardDAV +``` + +## Key Metrics + +| Metric | Value | +|--------|-------| +| Rust source files | ~170 | +| Lines of code | ~50 000 | +| Automated tests | 222+ | +| Docker image | ~40 MB | + +## Further Reading + +- [Caching Architecture →](/architecture/caching) +- [Storage Quotas →](/architecture/storage-quotas) diff --git a/docs/architecture/storage-quotas.md b/docs/architecture/storage-quotas.md new file mode 100644 index 00000000..165be11e --- /dev/null +++ b/docs/architecture/storage-quotas.md @@ -0,0 +1,36 @@ +# Storage Quotas + +OxiCloud supports per-user storage quotas to limit disk usage. + +## Enabling Quotas + +```bash +OXICLOUD_ENABLE_USER_STORAGE_QUOTAS=true +``` + +## How It Works + +1. Each user has a `storage_quota` field (in bytes, `0` = unlimited) +2. On every file upload, the current usage is checked against the quota +3. If the upload would exceed the quota, it's rejected with a `413 Payload Too Large` error +4. Admins can view and set quotas via the admin panel or API + +## API + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/admin/users/{id}/quota` | Get user's quota and current usage | +| PUT | `/api/admin/users/{id}/quota` | Set user's quota | + +## Admin Panel + +The admin panel (`/admin.html`) shows each user's current usage vs. quota with a visual progress bar. + +## Deduplication Interaction + +Storage usage is calculated based on **logical file size** (what the user uploaded), not physical blob size. This means: + +- If two users upload the same 100 MB file, each user's quota is charged 100 MB +- But on disk, only one 100 MB blob exists + +This ensures fair quota accounting while maintaining dedup benefits. diff --git a/docs/config/deployment.md b/docs/config/deployment.md new file mode 100644 index 00000000..55e7602d --- /dev/null +++ b/docs/config/deployment.md @@ -0,0 +1,96 @@ +# Deployment & Docker + +## Docker Image + +OxiCloud uses a multi-stage Alpine build producing a **~40 MB** image: + +1. **Base** — shared build dependencies (`musl-dev`, `pkgconfig`, `openssl-dev`, `libpq-dev`) +2. **Cacher** — pre-builds the dependency layer for fast rebuilds +3. **Builder** — compiles OxiCloud (`rust:1.94.0-alpine3.23`) +4. **Runtime** — minimal Alpine (`alpine:3.23.3`) with `libgcc`, `ca-certificates`, `libpq`, `tzdata`, `su-exec` + +The final image runs as non-root user `oxicloud` (UID/GID 1001). Exposed port: **8086**. + +## Docker Compose + +```yaml +services: + postgres: + image: postgres:17.4-alpine + environment: + POSTGRES_DB: oxicloud + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres # change in production! + volumes: + - pg_data:/var/lib/postgresql/data + - ./db/schema.sql:/docker-entrypoint-initdb.d/schema.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + oxicloud: + image: ghcr.io/diocrafts/oxicloud:latest + ports: + - "8086:8086" + env_file: + - .env + volumes: + - storage_data:/app/storage + depends_on: + postgres: + condition: service_healthy + +volumes: + pg_data: + storage_data: +``` + +## Kubernetes (Helm) + +### Prerequisites +- Kubernetes cluster +- Default StorageClass +- Ingress Controller +- Helm 3+ + +### Install + +```bash +helm upgrade --install oxicloud charts/oxicloud \ + -f charts/oxicloud/values.yaml +``` + +### Verify + +```bash +kubectl get pods -n oxicloud +kubectl logs statefulset/oxicloud -n oxicloud +``` + +### WOPI Verification + +If Collabora/OnlyOffice is enabled: + +```bash +kubectl logs statefulset/oxicloud -n oxicloud | grep "WOPI discovery loaded" +``` + +## Feature Dependency Matrix + +| Feature | Requires DB | Requires Auth | Feature Flag | +|---|---|---|---| +| File storage | Yes | No | Always on | +| Authentication | Yes | — | `OXICLOUD_ENABLE_AUTH` | +| OIDC / SSO | Yes | Yes | `OXICLOUD_OIDC_ENABLED` | +| File sharing | Yes | Yes | `OXICLOUD_ENABLE_FILE_SHARING` | +| Trash | Yes | No | `OXICLOUD_ENABLE_TRASH` | +| Search | Yes | No | `OXICLOUD_ENABLE_SEARCH` | +| Favorites | Yes | Yes | Always on | +| Storage quotas | Yes | Yes | `OXICLOUD_ENABLE_USER_STORAGE_QUOTAS` | +| WebDAV | Yes | Optional | Always on | +| CalDAV / CardDAV | Yes | Yes | Always on | +| Deduplication | No | No | Always on | +| Thumbnails | No | No | Always on | +| Chunked uploads | No | No | Always on | diff --git a/docs/config/env.md b/docs/config/env.md new file mode 100644 index 00000000..c7ba51ce --- /dev/null +++ b/docs/config/env.md @@ -0,0 +1,83 @@ +# Environment Variables + +All variables use the `OXICLOUD_` prefix. + +## Server + +| Variable | Default | Description | +|---|---|---| +| `OXICLOUD_STORAGE_PATH` | `./storage` | Root storage directory | +| `OXICLOUD_STATIC_PATH` | `./static` | Static files directory | +| `OXICLOUD_SERVER_PORT` | `8086` | Server port | +| `OXICLOUD_SERVER_HOST` | `127.0.0.1` | Server bind address | +| `OXICLOUD_BASE_URL` | (auto) | Public base URL for share links | + +## Database + +| Variable | Default | Description | +|---|---|---| +| `OXICLOUD_DB_CONNECTION_STRING` | `postgres://postgres:postgres@localhost:5432/oxicloud` | PostgreSQL connection string | +| `OXICLOUD_DB_MAX_CONNECTIONS` | `20` | Max pool connections | +| `OXICLOUD_DB_MIN_CONNECTIONS` | `5` | Min pool connections | + +## Authentication + +| Variable | Default | Description | +|---|---|---| +| `OXICLOUD_JWT_SECRET` | (random) | JWT signing secret | +| `OXICLOUD_ACCESS_TOKEN_EXPIRY_SECS` | `3600` | Access token lifetime (seconds) | +| `OXICLOUD_REFRESH_TOKEN_EXPIRY_SECS` | `2592000` | Refresh token lifetime (seconds) | + +## Feature Flags + +| Variable | Default | Description | +|---|---|---| +| `OXICLOUD_ENABLE_AUTH` | `true` | Enable authentication | +| `OXICLOUD_ENABLE_USER_STORAGE_QUOTAS` | `false` | Per-user storage quotas | +| `OXICLOUD_ENABLE_FILE_SHARING` | `true` | File/folder sharing | +| `OXICLOUD_ENABLE_TRASH` | `true` | Trash / recycle bin | +| `OXICLOUD_ENABLE_SEARCH` | `true` | Search | + +## OIDC / SSO + +See the [OIDC configuration guide](/config/oidc) for details. + +| Variable | Default | Description | +|---|---|---| +| `OXICLOUD_OIDC_ENABLED` | `false` | Enable OIDC | +| `OXICLOUD_OIDC_ISSUER_URL` | — | OIDC issuer URL | +| `OXICLOUD_OIDC_CLIENT_ID` | — | Client ID | +| `OXICLOUD_OIDC_CLIENT_SECRET` | — | Client secret | +| `OXICLOUD_OIDC_REDIRECT_URI` | `http://localhost:8086/api/auth/oidc/callback` | Callback URL | +| `OXICLOUD_OIDC_SCOPES` | `openid profile email` | Requested scopes | +| `OXICLOUD_OIDC_FRONTEND_URL` | `http://localhost:8086` | Frontend URL | +| `OXICLOUD_OIDC_AUTO_PROVISION` | `true` | Auto-create users on first SSO login | +| `OXICLOUD_OIDC_ADMIN_GROUPS` | — | Groups that grant admin role | +| `OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN` | `false` | Hide password form when OIDC enabled | +| `OXICLOUD_OIDC_PROVIDER_NAME` | `SSO` | Display name for the provider | + +## WOPI (Office Editing) + +See the [WOPI configuration guide](/config/wopi) for details. + +| Variable | Default | Description | +|---|---|---| +| `OXICLOUD_WOPI_ENABLED` | `false` | Enable WOPI | +| `OXICLOUD_WOPI_DISCOVERY_URL` | — | Collabora/OnlyOffice discovery URL | +| `OXICLOUD_WOPI_SECRET` | (JWT secret) | WOPI token signing key | +| `OXICLOUD_WOPI_TOKEN_TTL_SECS` | `86400` | Token lifetime | +| `OXICLOUD_WOPI_LOCK_TTL_SECS` | `1800` | Lock expiration | + +## Internal Defaults (not configurable via env) + +| Parameter | Default | +|---|---| +| File cache TTL | 60 s | +| Directory cache TTL | 120 s | +| Max cache entries | 10 000 | +| Large file threshold | 100 MB | +| Streaming chunk size | 1 MB | +| Max parallel chunks | 8 | +| Trash retention | 30 days | +| Argon2id memory cost | 64 MB | +| Argon2id time cost | 3 iterations | diff --git a/docs/config/index.md b/docs/config/index.md new file mode 100644 index 00000000..cee60b2c --- /dev/null +++ b/docs/config/index.md @@ -0,0 +1,21 @@ +# Configuration + +OxiCloud is configured entirely via **environment variables** (no config files needed). + +## Sections + +- [Deployment & Docker](/config/deployment) — Docker Compose, Kubernetes Helm chart, image details +- [Environment Variables](/config/env) — complete reference of all `OXICLOUD_*` variables +- [OIDC / SSO](/config/oidc) — single sign-on with Keycloak, Authentik, Authelia, Google, Azure AD +- [WOPI (Office Editing)](/config/wopi) — Collabora Online / OnlyOffice integration + +## Minimal `.env` + +```bash +OXICLOUD_DB_CONNECTION_STRING=postgres://postgres:postgres@postgres:5432/oxicloud +OXICLOUD_STORAGE_PATH=/app/storage +OXICLOUD_SERVER_HOST=0.0.0.0 +OXICLOUD_SERVER_PORT=8086 +``` + +That's enough to get started. All other settings have sensible defaults. diff --git a/docs/config/oidc.md b/docs/config/oidc.md new file mode 100644 index 00000000..c30c8a6a --- /dev/null +++ b/docs/config/oidc.md @@ -0,0 +1,95 @@ +# OIDC / SSO + +OxiCloud supports OpenID Connect for single sign-on with providers like **Keycloak**, **Authentik**, **Authelia**, **Google**, and **Azure AD**. + +## How It Works + +1. User clicks "Sign in with SSO" on the login page +2. Browser redirects to the identity provider (IdP) +3. User authenticates with their existing credentials +4. IdP redirects back to OxiCloud with an auth code +5. OxiCloud exchanges the code for user info and issues its own JWT tokens + +## Configuration + +```bash +OXICLOUD_OIDC_ENABLED=true +OXICLOUD_OIDC_ISSUER_URL="https://authentik.example.com/application/o/oxicloud/" +OXICLOUD_OIDC_CLIENT_ID="your-client-id" +OXICLOUD_OIDC_CLIENT_SECRET="your-client-secret" +OXICLOUD_OIDC_REDIRECT_URI="https://oxicloud.example.com/api/auth/oidc/callback" +OXICLOUD_OIDC_SCOPES="openid profile email" +OXICLOUD_OIDC_FRONTEND_URL="https://oxicloud.example.com" +OXICLOUD_OIDC_AUTO_PROVISION=true +OXICLOUD_OIDC_ADMIN_GROUPS="oxicloud-admins" +OXICLOUD_OIDC_PROVIDER_NAME="Authentik" +``` + +### Variable Reference + +| Variable | Default | Description | +|---|---|---| +| `OXICLOUD_OIDC_ENABLED` | `false` | Master switch | +| `OXICLOUD_OIDC_ISSUER_URL` | — | Provider's OIDC issuer URL | +| `OXICLOUD_OIDC_CLIENT_ID` | — | OAuth client ID | +| `OXICLOUD_OIDC_CLIENT_SECRET` | — | OAuth client secret | +| `OXICLOUD_OIDC_REDIRECT_URI` | `http://localhost:8086/api/auth/oidc/callback` | Callback URL registered with the IdP | +| `OXICLOUD_OIDC_SCOPES` | `openid profile email` | Requested scopes | +| `OXICLOUD_OIDC_FRONTEND_URL` | `http://localhost:8086` | Where to redirect the browser after auth | +| `OXICLOUD_OIDC_AUTO_PROVISION` | `true` | Auto-create users on first login | +| `OXICLOUD_OIDC_ADMIN_GROUPS` | — | OIDC groups that grant admin role | +| `OXICLOUD_OIDC_DISABLE_PASSWORD_LOGIN` | `false` | Hide password login when OIDC is active | +| `OXICLOUD_OIDC_PROVIDER_NAME` | `SSO` | Label shown on the login button | + +::: warning +If `OXICLOUD_OIDC_ENABLED=true` but `issuer_url`, `client_id`, or `client_secret` are empty, OIDC is automatically disabled with an error log. +::: + +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/auth/oidc/providers` | Returns OIDC provider info | +| GET | `/api/auth/oidc/authorize` | Authorization URL for redirect to IdP | +| GET | `/api/auth/oidc/callback` | Callback from IdP with auth code | +| POST | `/api/auth/oidc/exchange` | Exchange auth code for JWT tokens | + +## Provider Examples + +### Keycloak + +```yaml +# docker-compose.yml +services: + oxicloud: + environment: + OXICLOUD_OIDC_ENABLED: "true" + OXICLOUD_OIDC_ISSUER_URL: "https://keycloak.example.com/realms/your-realm" + OXICLOUD_OIDC_CLIENT_ID: "oxicloud" + OXICLOUD_OIDC_CLIENT_SECRET: "your-client-secret" + OXICLOUD_OIDC_REDIRECT_URI: "https://oxicloud.example.com/api/auth/oidc/callback" + OXICLOUD_OIDC_FRONTEND_URL: "https://oxicloud.example.com" + OXICLOUD_OIDC_PROVIDER_NAME: "Keycloak" +``` + +### Authentik + +```bash +OXICLOUD_OIDC_ISSUER_URL="https://authentik.example.com/application/o/oxicloud/" +OXICLOUD_OIDC_PROVIDER_NAME="Authentik" +``` + +### Google + +```bash +OXICLOUD_OIDC_ISSUER_URL="https://accounts.google.com" +OXICLOUD_OIDC_PROVIDER_NAME="Google" +``` + +## Notes + +- Always use **HTTPS** for OIDC connections +- One OIDC provider per instance (single-provider model) +- OIDC users share the same permissions model as local users +- After OIDC auth, the backend issues its own JWT tokens (no IdP token dependency) +- Use the admin settings UI (`/admin.html`) to configure and test OIDC at runtime diff --git a/docs/config/wopi.md b/docs/config/wopi.md new file mode 100644 index 00000000..e0d42913 --- /dev/null +++ b/docs/config/wopi.md @@ -0,0 +1,78 @@ +# WOPI (Office Document Editing) + +OxiCloud integrates with **Collabora Online** and **OnlyOffice** via the WOPI protocol, letting users edit documents, spreadsheets, and presentations directly in the browser. + +## How It Works + +1. User opens a document (`.docx`, `.xlsx`, `.pptx`, `.odt`, etc.) +2. OxiCloud generates a WOPI access token and redirects to the editor +3. The editor fetches the file from OxiCloud via WOPI endpoints +4. Edits are saved back via `PutFile` + +## Configuration + +```bash +OXICLOUD_WOPI_ENABLED=true +OXICLOUD_WOPI_DISCOVERY_URL="http://collabora:9980/hosting/discovery" +``` + +| Variable | Default | Description | +|---|---|---| +| `OXICLOUD_WOPI_ENABLED` | `false` | Enable WOPI integration | +| `OXICLOUD_WOPI_DISCOVERY_URL` | — | Editor's WOPI discovery URL | +| `OXICLOUD_WOPI_SECRET` | (JWT secret) | Token signing key | +| `OXICLOUD_WOPI_TOKEN_TTL_SECS` | `86400` | Access token lifetime | +| `OXICLOUD_WOPI_LOCK_TTL_SECS` | `1800` | Lock expiration | + +## Docker Compose with Collabora + +```yaml +services: + collabora: + image: collabora/code:latest + environment: + - domain=oxicloud\\.example\\.com + - extra_params=--o:ssl.enable=false + ports: + - "9980:9980" + cap_add: + - MKNOD + + oxicloud: + environment: + OXICLOUD_WOPI_ENABLED: "true" + OXICLOUD_WOPI_DISCOVERY_URL: "http://collabora:9980/hosting/discovery" +``` + +## Docker Compose with OnlyOffice + +```yaml +services: + onlyoffice: + image: onlyoffice/documentserver:latest + environment: + - JWT_ENABLED=false + ports: + - "8443:443" + + oxicloud: + environment: + OXICLOUD_WOPI_ENABLED: "true" + OXICLOUD_WOPI_DISCOVERY_URL: "http://onlyoffice/hosting/discovery" +``` + +## WOPI Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/wopi/files/{id}` | CheckFileInfo — file metadata | +| GET | `/wopi/files/{id}/contents` | GetFile — download file content | +| POST | `/wopi/files/{id}/contents` | PutFile — save edited content | +| POST | `/wopi/files/{id}` | Lock / Unlock / RefreshLock | + +## Supported Formats + +Any format supported by your Collabora or OnlyOffice installation, typically: +- Documents: `.docx`, `.odt`, `.doc`, `.rtf` +- Spreadsheets: `.xlsx`, `.ods`, `.xls`, `.csv` +- Presentations: `.pptx`, `.odp`, `.ppt` diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 00000000..071e2da3 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,79 @@ +# FAQ + +## General + +### What is OxiCloud? + +OxiCloud is a self-hosted cloud platform written in Rust. It provides file storage, calendar sync, contacts sync, and office editing from a single binary. + +### How does it compare to NextCloud? + +OxiCloud uses ~20× less RAM, produces a ~25× smaller Docker image, and starts in under 1 second. The trade-off is fewer plugins — OxiCloud builds everything into the core. See the [comparison table](/guide/#oxicloud-vs-nextcloud). + +### What hardware do I need? + +Minimum: 1 vCPU, 512 MB RAM, and a few GB of disk for PostgreSQL + file storage. OxiCloud runs comfortably on a Raspberry Pi 4. + +--- + +## Installation + +### Which database is supported? + +PostgreSQL 13 or later. SQLite is not supported — PostgreSQL's ltree, array operations, and concurrent access are essential. + +### Can I use MySQL / MariaDB? + +Not currently. PostgreSQL is the only supported database. + +### How do I update? + +Pull the latest image and restart: + +```bash +docker compose pull +docker compose up -d +``` + +--- + +## Features + +### Can I sync files from my desktop? + +WebDAV is built-in. You can mount OxiCloud in Windows Explorer, macOS Finder, or Linux file managers. A dedicated desktop sync client is on the roadmap. + +### Does CalDAV/CardDAV work with my phone? + +On Android, use [DAVx⁵](https://www.davx5.com/). On iOS, CalDAV and CardDAV work via the built-in Accounts settings. + +### Can I edit Office documents? + +Yes, via WOPI integration with Collabora Online or OnlyOffice. See [WOPI configuration](/config/wopi). + +### Is there end-to-end encryption? + +Not yet — it's on the roadmap. Files are stored unencrypted on the server (standard for self-hosted solutions that support server-side processing like thumbnails and search). + +--- + +## Troubleshooting + +### I can't connect via WebDAV + +1. Check that the URL is `http(s)://host:8086/webdav/` (note the trailing slash) +2. Ensure authentication is correct (HTTP Basic) +3. Check server logs: `docker compose logs oxicloud` + +### Uploads fail for large files + +OxiCloud automatically switches to chunked upload for files over 200 MB. If you're behind a reverse proxy, ensure it allows large request bodies: + +```nginx +client_max_body_size 0; # unlimited +proxy_read_timeout 600s; +``` + +### OIDC login doesn't redirect back + +Verify that `OXICLOUD_OIDC_REDIRECT_URI` matches exactly what's configured in your identity provider, and that `OXICLOUD_OIDC_FRONTEND_URL` points to your actual frontend URL. diff --git a/docs/guide/caldav-carddav.md b/docs/guide/caldav-carddav.md new file mode 100644 index 00000000..86c923fb --- /dev/null +++ b/docs/guide/caldav-carddav.md @@ -0,0 +1,70 @@ +# CalDAV & CardDAV + +OxiCloud provides built-in CalDAV (calendar) and CardDAV (contacts) servers — no extra apps or plugins needed. + +## CalDAV (Calendars) + +### Endpoint + +``` +https://your-server:8086/caldav/ +``` + +### Protocol Compliance + +- RFC 4791 (Calendar Access) +- RFC 5545 (iCalendar format) +- DAV capabilities: `1, 2, calendar-access` + +### Client Setup + +| Client | URL | +|--------|-----| +| Thunderbird | `https://your-server:8086/caldav/` | +| GNOME Calendar | `https://your-server:8086/caldav/` | +| Apple Calendar (macOS/iOS) | `https://your-server:8086/caldav/` | +| DAVx⁵ (Android) | `https://your-server:8086/` (auto-discovery) | + +### Thunderbird Setup + +1. Open Thunderbird → **Calendar** tab +2. Right-click → **New Calendar** → **On the Network** +3. Format: **CalDAV** +4. URL: `https://your-server:8086/caldav/` +5. Enter your OxiCloud credentials + +--- + +## CardDAV (Contacts) + +### Endpoint + +``` +https://your-server:8086/carddav/ +``` + +### Protocol Compliance + +- RFC 6352 (CardDAV) +- RFC 6350 (vCard 4.0) + +### Client Setup + +| Client | URL | +|--------|-----| +| Thunderbird | `https://your-server:8086/carddav/` | +| GNOME Contacts | `https://your-server:8086/carddav/` | +| Apple Contacts (macOS/iOS) | `https://your-server:8086/carddav/` | +| DAVx⁵ (Android) | `https://your-server:8086/` (auto-discovery) | + +### DAVx⁵ (Android) Setup + +1. Install [DAVx⁵](https://www.davx5.com/) from F-Droid or Play Store +2. Add account → **Login with URL and user name** +3. Base URL: `https://your-server:8086/` +4. Enter your OxiCloud credentials +5. DAVx⁵ auto-discovers both CalDAV and CardDAV endpoints + +::: info +DAVx⁵ file sync works. CalDAV/CardDAV support on DAVx⁵ is still being refined. +::: diff --git a/docs/guide/chunked-uploads.md b/docs/guide/chunked-uploads.md new file mode 100644 index 00000000..dbc0b27f --- /dev/null +++ b/docs/guide/chunked-uploads.md @@ -0,0 +1,63 @@ +# Chunked Uploads + +OxiCloud supports TUS-like chunked uploads for large files. Uploads are parallel, resumable, and have MD5 integrity checks. + +## How It Works + +1. Client sends `POST /api/files/upload/init` with file metadata → receives an `upload_id` +2. Client splits the file into chunks and uploads them in parallel via `POST /api/files/upload/chunk` +3. Each chunk includes its index, MD5 hash, and the `upload_id` +4. When all chunks are uploaded, client calls `POST /api/files/upload/complete` +5. Server reassembles the file, verifies integrity, and runs deduplication + +## API Endpoints + +### Initialize Upload + +```http +POST /api/files/upload/init +Content-Type: application/json + +{ + "file_name": "large-video.mp4", + "folder_id": "folder-uuid", + "total_size": 524288000, + "chunk_size": 8388608, + "total_chunks": 63 +} +``` + +### Upload Chunk + +```http +POST /api/files/upload/chunk +Content-Type: multipart/form-data + +upload_id: "uuid" +chunk_index: 0 +chunk_hash: "md5-hex" +file: +``` + +### Complete Upload + +```http +POST /api/files/upload/complete +Content-Type: application/json + +{ + "upload_id": "uuid" +} +``` + +## Configuration + +| Parameter | Default | Description | +|---|---|---| +| Max parallel chunks | 8 | Concurrent chunk uploads | +| Min size for chunking | 200 MB | Below this, single-shot upload is used | +| Chunk size | 8 MB | Default chunk size | + +## Frontend Behaviour + +The OxiCloud web UI automatically selects chunked upload for large files. A progress bar shows overall completion and current chunk status. diff --git a/docs/guide/deduplication.md b/docs/guide/deduplication.md new file mode 100644 index 00000000..18ee02d9 --- /dev/null +++ b/docs/guide/deduplication.md @@ -0,0 +1,46 @@ +# File Deduplication + +OxiCloud uses **SHA-256 content-addressable storage** to avoid storing duplicate files. If two users upload the same file, only one copy is stored on disk. + +## How It Works + +1. When a file is uploaded, its SHA-256 hash is computed +2. The hash is checked against the blob store (`.blobs/{prefix}/{hash}.blob`) +3. If a blob with that hash already exists, the file metadata points to the existing blob (no extra disk usage) +4. If not, the content is saved as a new blob +5. A reference counter tracks how many files point to each blob + +## Automatic Cleanup + +When a file is permanently deleted: + +1. The blob's reference count is decremented +2. If the reference count reaches zero, the blob is removed from disk + +This means disk space is only freed when the **last** reference to a blob is removed. + +## Storage Layout + +``` +storage/ +├── .blobs/ +│ ├── a1/ +│ │ └── a1b2c3d4...sha256.blob +│ ├── f8/ +│ │ └── f8e7d6c5...sha256.blob +│ └── ... +``` + +The first two hex characters of the hash are used as a directory prefix to avoid having millions of files in a single directory. + +## Benefits + +- **Disk savings** — identical files across users consume storage only once +- **Instant uploads** — if the blob already exists, the upload completes immediately +- **Integrity** — SHA-256 ensures bit-for-bit correctness + +## Limitations + +- Deduplication is based on exact content match (byte-identical files) +- Near-duplicate files (e.g., a JPEG re-saved at slightly different quality) are stored separately +- Encryption at rest would require per-user keys, which breaks deduplication (planned as opt-in) diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 00000000..25fd1e0e --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,81 @@ +# What is OxiCloud? + +OxiCloud is a self-hosted cloud platform written in Rust. It provides file storage, calendar sync (CalDAV), contacts sync (CardDAV), and office document editing (WOPI) — all from a single binary. + +NextCloud was too slow on a home server. So OxiCloud was built to run on minimal hardware and stay out of the way. + +## OxiCloud vs NextCloud + +| Metric | OxiCloud | NextCloud | +|--------|----------|-----------| +| **Language** | Rust (compiled, zero-cost abstractions) | PHP (interpreted) | +| **Docker image** | ~40 MB (Alpine, static binary) | ~1 GB+ (Apache + PHP + modules) | +| **Idle RAM** | ~30–50 MB | ~250–512 MB | +| **Cold start** | < 1 s | 5–15 s | +| **CPU at idle** | ~0 % | 1–5 % (cron, background jobs) | +| **Min. hardware** | 1 vCPU / 512 MB RAM | 2 vCPU / 2 GB RAM | +| **File dedup** | SHA-256 content-addressable | None | +| **Dependencies** | Single binary + PostgreSQL | PHP, Apache/Nginx, Redis, Cron, … | +| **WebDAV** | Built-in (RFC 4918) | Built-in | +| **CalDAV / CardDAV** | Built-in | Via apps | +| **WOPI** | Built-in | Via apps | +| **OIDC / SSO** | Built-in | Via apps | + +> NextCloud is a mature, feature-rich ecosystem. OxiCloud targets users who prioritise raw performance, simplicity, and low resource usage over plugin breadth. + +## Key Features + +### Storage & Files +- Drag-and-drop upload, multi-file, grid & list views +- Chunked uploads (TUS-like, parallel, resumable, MD5 integrity) +- SHA-256 content-addressable file deduplication with ref-counting +- Adaptive compression (zstd / gzip per MIME type) +- Trash bin with soft-delete and auto-purge +- Favourites, recent files, full-text search +- Inline preview for images, PDF, text, audio & video +- On-the-fly thumbnails & transcoding (WebP / AVIF) + +### Protocols +- **WebDAV** — RFC 4918, streaming PROPFIND, locking +- **CalDAV** — calendar sync (Thunderbird, GNOME Calendar, iOS, DAVx⁵) +- **CardDAV** — contacts sync with vCard support +- **WOPI** — Collabora Online / OnlyOffice +- **REST API** — complete JSON API + +### Security +- JWT + Argon2id password hashing +- OIDC / SSO (Keycloak, Authentik, Authelia, Google, Azure AD) +- Role-based access, per-folder permissions, storage quotas +- Shared links with optional password protection + +### Infrastructure +- Single binary, ~40 MB Docker image +- Dual DB pool (user queries never starved by background tasks) +- Write-behind caching (moka) for sub-millisecond reads +- LTO-optimised release builds +- 222+ automated tests + +## Feature Status + +| Feature | Status | +|---------|--------| +| File storage & upload | ✅ Working | +| WebDAV | ✅ Working | +| CalDAV | ✅ Working | +| CardDAV | ✅ Working | +| WOPI / Office editing | ✅ Working | +| OIDC / SSO | ✅ Working | +| Trash / recycle bin | ✅ Working | +| Full-text search | ✅ Working | +| Shared links | ✅ Working | +| Music library & playlists | ✅ Working | +| Photo gallery | ✅ Working | +| Desktop sync client | ❌ Planned | +| Android / iOS app | ❌ Planned | +| E2E encryption | ❌ Planned | + +## Next Steps + +- [Quick Start →](/guide/installation) +- [Deployment & Docker →](/config/deployment) +- [Architecture →](/architecture/) diff --git a/docs/guide/installation.md b/docs/guide/installation.md new file mode 100644 index 00000000..c38bd911 --- /dev/null +++ b/docs/guide/installation.md @@ -0,0 +1,94 @@ +# Quick Start + +## Docker (recommended) + +```bash +git clone https://github.com/DioCrafts/oxicloud.git +cd oxicloud +cp example.env .env +docker compose up -d +``` + +Open **http://localhost:8086**. That's it. + +### Docker Compose + +```yaml +services: + postgres: + image: postgres:17.4-alpine + environment: + POSTGRES_DB: oxicloud + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + volumes: + - pg_data:/var/lib/postgresql/data + - ./db/schema.sql:/docker-entrypoint-initdb.d/schema.sql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + oxicloud: + image: oxicloud:latest + ports: + - "8086:8086" + env_file: + - .env + volumes: + - storage_data:/app/storage + depends_on: + postgres: + condition: service_healthy + +volumes: + pg_data: + storage_data: +``` + +## From Source + +Requires **Rust 1.93+** and **PostgreSQL 13+**. + +```bash +git clone https://github.com/DioCrafts/oxicloud.git +cd oxicloud +echo "DATABASE_URL=postgres://user:pass@localhost/oxicloud" > .env +cargo build --release +cargo run --release +``` + +## Kubernetes (Helm) + +```bash +helm upgrade --install oxicloud charts/oxicloud \ + -f charts/oxicloud/values.yaml +``` + +Verify: + +```bash +kubectl get pods -n oxicloud +kubectl logs statefulset/oxicloud -n oxicloud +``` + +## Client Setup + +| Client | Protocol | URL | +|--------|----------|-----| +| Windows Explorer | WebDAV | `http://host:8086/webdav/` | +| macOS Finder | WebDAV | `http://host:8086/webdav/` | +| Nautilus / Dolphin | WebDAV | `dav://host:8086/webdav/` | +| Thunderbird (calendar) | CalDAV | `http://host:8086/caldav/` | +| Thunderbird (contacts) | CardDAV | `http://host:8086/carddav/` | +| DAVx⁵ (Android) | CalDAV + CardDAV | `http://host:8086/` | +| GNOME Calendar | CalDAV | `http://host:8086/caldav/` | +| GNOME Contacts | CardDAV | `http://host:8086/carddav/` | +| Collabora / OnlyOffice | WOPI | See [WOPI configuration](/config/wopi) | + +## What's Next? + +- [Environment Variables →](/config/env) +- [OIDC / SSO Setup →](/config/oidc) +- [WebDAV Guide →](/guide/webdav) diff --git a/docs/guide/search.md b/docs/guide/search.md new file mode 100644 index 00000000..ebc4ec32 --- /dev/null +++ b/docs/guide/search.md @@ -0,0 +1,43 @@ +# Search + +OxiCloud provides full-text search across your files with multiple filter options. + +## Endpoint + +```http +GET /api/search?q=report&type_filter=pdf,docx&recursive=true +``` + +## Query Parameters + +| Parameter | Description | +|---|---| +| `q` | Search query (matches file name) | +| `type_filter` | Comma-separated file extensions to filter by | +| `folder_id` | Restrict search to a specific folder | +| `recursive` | `true` to search subfolders | +| `date_from` / `date_to` | Filter by modification date | +| `size_min` / `size_max` | Filter by file size (bytes) | +| `limit` | Maximum results to return | +| `offset` | Pagination offset | + +## How It Works + +OxiCloud stores file metadata in PostgreSQL with an `ltree` path column, enabling efficient recursive subtree queries: + +```sql +SELECT * FROM storage.files +WHERE path <@ 'root.folder_id' + AND LOWER(name) LIKE '%query%' + AND LOWER(extension) = ANY('{pdf,docx}') +ORDER BY updated_at DESC +LIMIT 50; +``` + +## Frontend + +The web UI includes a search bar in the toolbar. Results appear instantly with file name, path, size, and type. Clicking a result navigates to the file's location. + +## Feature Flag + +Search can be disabled via `OXICLOUD_ENABLE_SEARCH=false`. diff --git a/docs/guide/trash.md b/docs/guide/trash.md new file mode 100644 index 00000000..e4987505 --- /dev/null +++ b/docs/guide/trash.md @@ -0,0 +1,27 @@ +# Trash & Recycle Bin + +OxiCloud provides a trash system that soft-deletes files and folders, allowing users to restore or permanently remove them. + +## How It Works + +1. When a file or folder is deleted, it's **soft-deleted** — a flag (`is_trashed`) is set and a `trashed_at` timestamp is recorded +2. Trashed items are hidden from normal file listings but remain on disk and in the database +3. Users can browse the trash, restore items, or permanently delete them +4. Items older than the retention period (default: **30 days**) are automatically purged + +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/trash` | List trashed items | +| POST | `/api/trash/restore/{id}` | Restore a trashed item | +| DELETE | `/api/trash/{id}` | Permanently delete | +| DELETE | `/api/trash/empty` | Empty the entire trash | + +## Deduplication Interaction + +Permanent deletion decrements the blob reference count. If no other file points to the same blob, the blob is removed from disk. + +## Feature Flag + +Trash can be disabled via `OXICLOUD_ENABLE_TRASH=false`. When disabled, deletions are permanent. diff --git a/docs/guide/webdav.md b/docs/guide/webdav.md new file mode 100644 index 00000000..cafa4415 --- /dev/null +++ b/docs/guide/webdav.md @@ -0,0 +1,80 @@ +# WebDAV + +OxiCloud exposes a fully RFC 4918 compliant WebDAV interface at `/webdav/`. It works with all major file managers and sync clients. + +## Base URL + +``` +https://your-server:8086/webdav/ +``` + +## Authentication + +HTTP Basic Authentication: + +``` +Authorization: Basic base64(username:password) +``` + +::: tip +Always use HTTPS in production — Basic auth sends credentials in every request. +::: + +## Supported Methods + +| Method | Description | +|--------|-------------| +| `PROPFIND` | List directory contents / get file properties | +| `GET` | Download a file | +| `PUT` | Upload a file | +| `MKCOL` | Create a folder | +| `MOVE` | Move or rename a file/folder | +| `COPY` | Copy a file/folder | +| `DELETE` | Delete a file/folder | +| `LOCK` / `UNLOCK` | File locking | + +## Client Setup + +### Windows Explorer + +1. Open **This PC** → **Map network drive** +2. Enter: `https://your-server:8086/webdav/` +3. Check **Connect using different credentials** +4. Enter your OxiCloud username and password + +### macOS Finder + +1. **Go** → **Connect to Server** (⌘K) +2. Enter: `https://your-server:8086/webdav/` +3. Enter credentials when prompted + +### Linux (Nautilus / Files) + +1. Open Files → **Other Locations** +2. In the address bar, type: `davs://your-server:8086/webdav/` +3. Enter credentials + +### Linux (Dolphin / KDE) + +1. In the address bar, type: `webdavs://your-server:8086/webdav/` + +### Command Line (curl) + +```bash +# List root directory +curl -u user:pass -X PROPFIND https://your-server:8086/webdav/ \ + -H "Depth: 1" + +# Download a file +curl -u user:pass https://your-server:8086/webdav/document.pdf -o document.pdf + +# Upload a file +curl -u user:pass -T localfile.txt https://your-server:8086/webdav/remotefile.txt + +# Create a folder +curl -u user:pass -X MKCOL https://your-server:8086/webdav/new-folder/ +``` + +## Streaming PROPFIND + +OxiCloud streams PROPFIND responses, so listing directories with thousands of files doesn't consume excessive memory. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..e65f730c --- /dev/null +++ b/docs/index.md @@ -0,0 +1,41 @@ +--- +layout: home + +hero: + name: "OxiCloud" + text: "Self-hosted cloud storage" + tagline: "Files, calendar & contacts — blazingly fast, written in Rust" + image: + src: /logo.svg + alt: OxiCloud Logo + actions: + - theme: brand + text: Get Started + link: /guide/installation + - theme: alt + text: View on GitHub + link: https://github.com/DioCrafts/OxiCloud + - theme: alt + text: Why OxiCloud? + link: /guide/ + +features: + - icon: 🚀 + title: Blazingly Fast + details: Single Rust binary, ~40 MB Docker image, <1s cold start, 30–50 MB idle RAM. + - icon: 📁 + title: Full File Management + details: Chunked uploads, SHA-256 deduplication, trash, favourites, full-text search, thumbnails. + - icon: 🔗 + title: WebDAV / CalDAV / CardDAV + details: RFC-compliant protocols for files, calendars, and contacts. Works with all major clients. + - icon: 📝 + title: Office Editing (WOPI) + details: Edit documents in Collabora Online or OnlyOffice directly in the browser. + - icon: 🔐 + title: Security First + details: JWT + Argon2id, OIDC/SSO (Keycloak, Authentik, Azure AD), role-based access, shared links. + - icon: 🌍 + title: 14 Languages + details: EN, ES, DE, FR, IT, PT, NL, ZH, JA, KO, AR, HI, FA, RU — and growing. +--- diff --git a/docs/package-lock.json b/docs/package-lock.json new file mode 100644 index 00000000..940ef83e --- /dev/null +++ b/docs/package-lock.json @@ -0,0 +1,2510 @@ +{ + "name": "docs", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "vitepress": "^1.6.3" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.16.1.tgz", + "integrity": "sha512-Xxk4l00pYI+jE0PNw8y0MvsQWh5278WRtZQav8/BMMi3HKi2xmeuqe11WJ3y8/6nuBHdv39w76OpJb09TMfAVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1", + "@algolia/requester-browser-xhr": "5.50.1", + "@algolia/requester-fetch": "5.50.1", + "@algolia/requester-node-http": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.50.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.50.1.tgz", + "integrity": "sha512-4peZlPXMwTOey9q1rQKMdCnwZb/E95/1e+7KujXpLLSh0FawJzg//U2NM+r4AiJy4+naT2MTBhj0K30yshnVTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1", + "@algolia/requester-browser-xhr": "5.50.1", + "@algolia/requester-fetch": "5.50.1", + "@algolia/requester-node-http": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.50.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.50.1.tgz", + "integrity": "sha512-i+aWHHG8NZvGFHtPeMZkxL2Loc6Fm7iaRo15lYSMx8gFL+at9vgdWxhka7mD1fqxkrxXsQstUBCIsSY8FvkEOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1", + "@algolia/requester-browser-xhr": "5.50.1", + "@algolia/requester-fetch": "5.50.1", + "@algolia/requester-node-http": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.50.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.50.1.tgz", + "integrity": "sha512-Hw52Fwapyk/7hMSV/fI4+s3H9MGZEUcRh4VphyXLAk2oLYdndVUkc6KBi0zwHSzwPAr+ZBwFPe2x6naUt9mZGw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.50.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.50.1.tgz", + "integrity": "sha512-Bn/wtwhJ7p1OD/6pY+Zzn+zlu2N/SJnH46md/PAbvqIzmjVuwjNwD4y0vV5Ov8naeukXdd7UU9v550+v8+mtlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1", + "@algolia/requester-browser-xhr": "5.50.1", + "@algolia/requester-fetch": "5.50.1", + "@algolia/requester-node-http": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.50.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.50.1.tgz", + "integrity": "sha512-0V4Tu0RWR8YxkgI9EPVOZHGE4K5pEIhkLNN0CTkP/rnPsqaaSQpNMYW3/mGWdiKOWbX0iVmwLB9QESk3H0jS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1", + "@algolia/requester-browser-xhr": "5.50.1", + "@algolia/requester-fetch": "5.50.1", + "@algolia/requester-node-http": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.50.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.50.1.tgz", + "integrity": "sha512-jofcWNYMXJDDr87Z2eivlWY6o71Zn7F7aOvQCXSDAo9QTlyf7BhXEsZymLUvF0O1yU9Q9wvrjAWn8uVHYnAvgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1", + "@algolia/requester-browser-xhr": "5.50.1", + "@algolia/requester-fetch": "5.50.1", + "@algolia/requester-node-http": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.50.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.50.1.tgz", + "integrity": "sha512-OteRb8WubcmEvU0YlMJwCXs3Q6xrdkb0v50/qZBJP1TF0CvujFZQM++9BjEkTER/Jr9wbPHvjSFKnbMta0b4dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1", + "@algolia/requester-browser-xhr": "5.50.1", + "@algolia/requester-fetch": "5.50.1", + "@algolia/requester-node-http": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.50.1.tgz", + "integrity": "sha512-0GmfSgDQK6oiIVXnJvGxtNFOfosBspRTR7csCOYCTL1P8QtxX2vDCIKwTM7xdSAEbJaZ43QlWg25q0Qdsndz8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1", + "@algolia/requester-browser-xhr": "5.50.1", + "@algolia/requester-fetch": "5.50.1", + "@algolia/requester-node-http": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.50.1.tgz", + "integrity": "sha512-ySuigKEe4YjYV3si8NVk9BHQpFj/1B+ON7DhhvTvbrZJseHQQloxzq0yHwKmznSdlO6C956fx4pcfOKkZClsyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1", + "@algolia/requester-browser-xhr": "5.50.1", + "@algolia/requester-fetch": "5.50.1", + "@algolia/requester-node-http": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.50.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.50.1.tgz", + "integrity": "sha512-Cp8T/B0gVmjFlzzp6eP47hwKh5FGyeqQp1N48/ANDdvdiQkPqLyFHQVDwLBH0LddfIPQE+yqmZIgmKc82haF4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1", + "@algolia/requester-browser-xhr": "5.50.1", + "@algolia/requester-fetch": "5.50.1", + "@algolia/requester-node-http": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.50.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.50.1.tgz", + "integrity": "sha512-XKdGGLikfrlK66ZSXh/vWcXZZ8Vg3byDFbJD8pwEvN1FoBRGxhxya476IY2ohoTymLa4qB5LBRlIa+2TLHx3Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.50.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.50.1.tgz", + "integrity": "sha512-mBAU6WyVsDwhHyGM+nodt1/oebHxgvuLlOAoMGbj/1i6LygDHZWDgL1t5JEs37x9Aywv7ZGhqbM1GsfZ54sU6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.50.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.50.1.tgz", + "integrity": "sha512-qmo1LXrNKLHvJE6mdQbLnsZAoZvj7VyF2ft4xmbSGWI2WWm87fx/CjUX4kEExt4y0a6T6nEts6ofpUfH5TEE1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.77", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.77.tgz", + "integrity": "sha512-oaENvo6C3BkAEWMlcQA3XemxU9v2SFOTlApSUCODAkIu1haeLCjzrmH3HgmGqjRnJjM+LevO8sA+MgdMHBFBDA==", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz", + "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/shared": "3.5.32", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz", + "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz", + "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.2", + "@vue/compiler-core": "3.5.32", + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.8", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz", + "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz", + "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz", + "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/shared": "3.5.32" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz", + "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.32", + "@vue/runtime-core": "3.5.32", + "@vue/shared": "3.5.32", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz", + "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "vue": "3.5.32" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz", + "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/algoliasearch": { + "version": "5.50.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.50.1.tgz", + "integrity": "sha512-/bwdue1/8LWELn/DBalGRfuLsXBLXULJo/yOeavJtDu8rBwxIzC6/Rz9Jg19S21VkJvRuZO1k8CZXBMS73mYbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.16.1", + "@algolia/client-abtesting": "5.50.1", + "@algolia/client-analytics": "5.50.1", + "@algolia/client-common": "5.50.1", + "@algolia/client-insights": "5.50.1", + "@algolia/client-personalization": "5.50.1", + "@algolia/client-query-suggestions": "5.50.1", + "@algolia/client-search": "5.50.1", + "@algolia/ingestion": "1.50.1", + "@algolia/monitoring": "1.50.1", + "@algolia/recommend": "5.50.1", + "@algolia/requester-browser-xhr": "5.50.1", + "@algolia/requester-fetch": "5.50.1", + "@algolia/requester-node-http": "5.50.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz", + "integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true, + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "dev": true, + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz", + "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.32", + "@vue/compiler-sfc": "3.5.32", + "@vue/runtime-dom": "3.5.32", + "@vue/server-renderer": "3.5.32", + "@vue/shared": "3.5.32" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..de1e4536 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,10 @@ +{ + "scripts": { + "docs:dev": "vitepress dev .", + "docs:build": "vitepress build .", + "docs:preview": "vitepress preview ." + }, + "devDependencies": { + "vitepress": "^1.6.3" + } +} diff --git a/docs/public/logo.svg b/docs/public/logo.svg new file mode 100644 index 00000000..d463f6b3 --- /dev/null +++ b/docs/public/logo.svg @@ -0,0 +1,10 @@ + + + + + + + + + OxiCloud + diff --git a/static/css/views/music.css b/static/css/views/music.css index 98ccd415..cfcd8b70 100644 --- a/static/css/views/music.css +++ b/static/css/views/music.css @@ -1288,3 +1288,186 @@ [data-theme="dark"] .music-empty-state-desc { color: var(--color-text-muted, #888); } + +/* ═══════════════ Audio file picker modal ═══════════════ */ + +.music-picker-overlay { + position: fixed; + inset: 0; + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.45); + opacity: 0; + transition: opacity 0.2s ease; +} +.music-picker-overlay.active { + opacity: 1; +} + +.music-picker-modal { + display: flex; + flex-direction: column; + width: min(560px, 94vw); + max-height: min(620px, 85vh); + background: var(--bg-primary, #fff); + border-radius: 12px; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2); + overflow: hidden; + transform: translateY(12px); + transition: transform 0.2s ease; +} +.music-picker-overlay.active .music-picker-modal { + transform: translateY(0); +} + +/* ── header ── */ +.music-picker-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid var(--border-color, #e2e8f0); +} +.music-picker-header h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + color: var(--color-text, #2d3748); +} +.music-picker-header h3 i { margin-right: 8px; color: var(--color-primary, #667eea); } +.music-picker-close { + background: none; + border: none; + font-size: 22px; + cursor: pointer; + color: var(--color-text-muted, #718096); + padding: 0 4px; + line-height: 1; +} +.music-picker-close:hover { color: var(--color-text, #2d3748); } + +/* ── search bar ── */ +.music-picker-search { + position: relative; + padding: 12px 20px; + border-bottom: 1px solid var(--border-color, #e2e8f0); +} +.music-picker-search i { + position: absolute; + left: 32px; + top: 50%; + transform: translateY(-50%); + color: var(--color-text-muted, #a0aec0); + font-size: 13px; +} +.music-picker-search input { + width: 100%; + padding: 8px 12px 8px 32px; + border: 1px solid var(--border-color, #e2e8f0); + border-radius: 6px; + font-size: 13px; + background: var(--bg-secondary, #f7fafc); + color: var(--color-text, #2d3748); + outline: none; +} +.music-picker-search input:focus { + border-color: var(--color-primary, #667eea); + box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15); +} + +/* ── file list ── */ +.music-picker-list { + flex: 1; + overflow-y: auto; + padding: 4px 0; + min-height: 200px; +} +.music-picker-loading, +.music-picker-empty { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 40px 20px; + color: var(--color-text-muted, #718096); + font-size: 13px; +} + +.music-picker-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 20px; + cursor: pointer; + transition: background 0.1s; + font-size: 13px; + color: var(--color-text, #2d3748); +} +.music-picker-item:hover { background: var(--bg-hover, #edf2f7); } +.music-picker-item.selected { background: rgba(102, 126, 234, 0.08); } + +.music-picker-item input[type="checkbox"] { + accent-color: var(--color-primary, #667eea); + width: 16px; + height: 16px; + flex-shrink: 0; +} +.music-picker-item i.fa-file-audio { + color: var(--color-primary, #667eea); + font-size: 14px; + flex-shrink: 0; +} +.music-picker-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.music-picker-size { + flex-shrink: 0; + font-size: 12px; + color: var(--color-text-muted, #a0aec0); +} + +/* ── footer ── */ +.music-picker-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 20px; + border-top: 1px solid var(--border-color, #e2e8f0); +} +.music-picker-selected-count { + font-size: 12px; + color: var(--color-text-muted, #718096); + font-weight: 500; +} +.music-picker-actions { + display: flex; + gap: 8px; +} + +/* ── dark theme ── */ +[data-theme="dark"] .music-picker-modal { + background: var(--bg-primary, #1a202c); +} +[data-theme="dark"] .music-picker-header h3 { + color: var(--color-text, #e0e0e0); +} +[data-theme="dark"] .music-picker-search input { + background: var(--bg-secondary, #2d3748); + border-color: var(--border-color, #4a5568); + color: var(--color-text, #e0e0e0); +} +[data-theme="dark"] .music-picker-item { + color: var(--color-text, #e0e0e0); +} +[data-theme="dark"] .music-picker-item:hover { + background: var(--bg-hover, #2d3748); +} +[data-theme="dark"] .music-picker-item.selected { + background: rgba(102, 126, 234, 0.12); +} diff --git a/static/js/features/library/music.js b/static/js/features/library/music.js index 87e0f440..07fea07c 100644 --- a/static/js/features/library/music.js +++ b/static/js/features/library/music.js @@ -738,53 +738,117 @@ const musicView = { return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key; }; - // Create a file picker input for audio files - const input = document.createElement('input'); - input.type = 'file'; - input.accept = 'audio/*'; - input.multiple = true; - input.style.display = 'none'; - document.body.appendChild(input); + // ── Build modal overlay ── + const overlay = document.createElement('div'); + overlay.className = 'music-picker-overlay'; + overlay.innerHTML = ` +
+
+

${t('music.add_tracks', 'Add Tracks')}

+ +
+ +
+
${t('music.loading', 'Loading…')}
+
+ +
+ `; + document.body.appendChild(overlay); + requestAnimationFrame(() => overlay.classList.add('active')); - input.addEventListener('change', async () => { - const files = Array.from(input.files); - input.remove(); - if (files.length === 0) return; + const listEl = document.getElementById('music-picker-list'); + const queryInput = document.getElementById('music-picker-query'); + const addBtn = document.getElementById('music-picker-add-btn'); + const countEl = document.getElementById('music-picker-count'); + const selectedIds = new Set(); - // Upload each file first, then add to playlist - const fileIds = []; - for (const file of files) { - try { - const formData = new FormData(); - formData.append('file', file); - const folderId = window.app?.currentPath || window.app?.userHomeFolderId || ''; - formData.append('folder_id', folderId); + // ── Close helpers ── + const close = () => { + overlay.classList.remove('active'); + setTimeout(() => overlay.remove(), 200); + }; + overlay.querySelector('.music-picker-close').addEventListener('click', close); + overlay.querySelector('.music-picker-cancel').addEventListener('click', close); + overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); - const uploadResp = await fetch('/api/files/upload', { - method: 'POST', - credentials: 'include', - headers: typeof getCsrfHeaders === 'function' ? getCsrfHeaders() : {}, - body: formData - }); + // ── Fetch & render audio files ── + const AUDIO_EXTENSIONS = 'mp3,ogg,flac,wav,aac,m4a,wma,opus,webm'; - if (!uploadResp.ok) throw new Error(`Upload failed: ${file.name}`); - const uploaded = await uploadResp.json(); - if (uploaded.id) fileIds.push(uploaded.id); - } catch (err) { - console.error('Upload error:', err); - } + const fetchAudioFiles = async (query = '') => { + listEl.innerHTML = `
${t('music.loading', 'Loading…')}
`; + try { + const params = new URLSearchParams({ type_filter: AUDIO_EXTENSIONS, limit: '200', recursive: 'true' }); + if (query.trim()) params.set('query', query.trim()); + const resp = await fetch(`/api/search?${params}`, { credentials: 'include' }); + if (!resp.ok) throw new Error('Search failed'); + const data = await resp.json(); + renderFiles(data.files || []); + } catch (err) { + console.error('Audio search error:', err); + listEl.innerHTML = `
${t('music.search_error', 'Could not load audio files')}
`; } + }; - if (fileIds.length === 0) return; + const renderFiles = (files) => { + if (files.length === 0) { + listEl.innerHTML = `
${t('music.no_audio_files', 'No audio files found')}
`; + return; + } + listEl.innerHTML = ''; + for (const file of files) { + const row = document.createElement('label'); + row.className = 'music-picker-item' + (selectedIds.has(file.id) ? ' selected' : ''); + const sizeStr = file.size != null && window.formatFileSize ? window.formatFileSize(file.size) : ''; + row.innerHTML = ` + + + ${this._escapeHtml(file.name)} + ${sizeStr} + `; + const cb = row.querySelector('input'); + cb.addEventListener('change', () => { + if (cb.checked) { selectedIds.add(file.id); row.classList.add('selected'); } + else { selectedIds.delete(file.id); row.classList.remove('selected'); } + countEl.textContent = `${selectedIds.size} ${t('music.selected', 'selected')}`; + addBtn.disabled = selectedIds.size === 0; + }); + listEl.appendChild(row); + } + }; + + // ── Debounced search ── + let searchTimer = null; + queryInput.addEventListener('input', () => { + clearTimeout(searchTimer); + searchTimer = setTimeout(() => fetchAudioFiles(queryInput.value), 300); + }); + + // ── Add button ── + addBtn.addEventListener('click', async () => { + if (selectedIds.size === 0) return; + addBtn.disabled = true; + addBtn.innerHTML = ` ${t('music.adding', 'Adding…')}`; try { const resp = await fetch(`/api/playlists/${this.currentPlaylist.id}/tracks`, { method: 'POST', credentials: 'include', headers: this._headers(true), - body: JSON.stringify({ file_ids: fileIds }) + body: JSON.stringify({ file_ids: [...selectedIds] }) }); - if (!resp.ok) throw new Error('Failed to add tracks'); if (window.notifications) { @@ -792,15 +856,14 @@ const musicView = { icon: 'fa-check-circle', iconClass: 'upload', title: t('music.add_tracks', 'Add Tracks'), - text: `${fileIds.length} ${t('music.added_to_playlist', 'added to playlist')}` + text: `${selectedIds.size} ${t('music.added_to_playlist', 'added to playlist')}` }); } - + close(); await this._loadPlaylistTracks(this.currentPlaylist.id); - // Update track count const playlist = this.playlists.find((p) => p.id === this.currentPlaylist.id); if (playlist) { - playlist.track_count = (playlist.track_count || 0) + fileIds.length; + playlist.track_count = (playlist.track_count || 0) + selectedIds.size; this.currentPlaylist.track_count = playlist.track_count; this._renderPlaylistList(); const metaEl = document.getElementById('music-playlist-meta'); @@ -816,10 +879,14 @@ const musicView = { text: t('music.add_error', 'Could not add tracks to playlist') }); } + addBtn.disabled = false; + addBtn.innerHTML = ` ${t('music.add', 'Add')}`; } }); - input.click(); + // ── Initial load (all audio files) ── + queryInput.focus(); + fetchAudioFiles(); }, async _removeTrackFromPlaylist(trackId, fileId) { diff --git a/static/locales/ar.json b/static/locales/ar.json index b03e0ce3..ff58b98f 100644 --- a/static/locales/ar.json +++ b/static/locales/ar.json @@ -62,7 +62,13 @@ "add_error": "تعذر إضافة المقاطع", "no_playlists_yet": "لا توجد قوائم بعد. أنشئ واحدة أولاً!", "selected_files": "محدد:", - "error": "خطأ" + "error": "خطأ", + "search_audio": "البحث عن ملفات صوتية…", + "no_audio_files": "لم يتم العثور على ملفات صوتية", + "selected": "محدد", + "loading": "جارٍ التحميل…", + "search_error": "تعذر تحميل الملفات الصوتية", + "adding": "جارٍ الإضافة…" }, "actions": { "search": "البحث في الملفات...", diff --git a/static/locales/de.json b/static/locales/de.json index 323c4f64..aec5f7b8 100644 --- a/static/locales/de.json +++ b/static/locales/de.json @@ -62,7 +62,13 @@ "add_error": "Tracks konnten nicht hinzugefügt werden", "no_playlists_yet": "Noch keine Playlists. Erstellen Sie zuerst eine!", "selected_files": "Ausgewählt:", - "error": "Fehler" + "error": "Fehler", + "search_audio": "Audiodateien suchen…", + "no_audio_files": "Keine Audiodateien gefunden", + "selected": "ausgewählt", + "loading": "Wird geladen…", + "search_error": "Audiodateien konnten nicht geladen werden", + "adding": "Wird hinzugefügt…" }, "actions": { "search": "Dateien suchen...", diff --git a/static/locales/en.json b/static/locales/en.json index ced6d6db..d3d73095 100644 --- a/static/locales/en.json +++ b/static/locales/en.json @@ -79,7 +79,13 @@ "make_public": "Make public", "make_private": "Make private", "set_cover": "Set cover", - "cover_updated": "Cover updated" + "cover_updated": "Cover updated", + "search_audio": "Search audio files…", + "no_audio_files": "No audio files found", + "selected": "selected", + "loading": "Loading…", + "search_error": "Could not load audio files", + "adding": "Adding…" }, "actions": { "search": "Search files...", diff --git a/static/locales/es.json b/static/locales/es.json index a35863fa..4613c2a4 100644 --- a/static/locales/es.json +++ b/static/locales/es.json @@ -79,7 +79,13 @@ "make_public": "Hacer pública", "make_private": "Hacer privada", "set_cover": "Establecer portada", - "cover_updated": "Portada actualizada" + "cover_updated": "Portada actualizada", + "search_audio": "Buscar archivos de audio…", + "no_audio_files": "No se encontraron archivos de audio", + "selected": "seleccionados", + "loading": "Cargando…", + "search_error": "No se pudieron cargar los archivos de audio", + "adding": "Añadiendo…" }, "share": { "dialogTitle": "Compartir Enlace", diff --git a/static/locales/fa.json b/static/locales/fa.json index 958ae0db..a3625e81 100644 --- a/static/locales/fa.json +++ b/static/locales/fa.json @@ -62,7 +62,13 @@ "add_error": "امکان افزودن آهنگ‌ها به فهرست پخش نیست", "no_playlists_yet": "فهرست پخشی وجود ندارد. اول یکی بسازید!", "selected_files": "انتخاب شده:", - "error": "خطا" + "error": "خطا", + "search_audio": "جستجوی فایل‌های صوتی…", + "no_audio_files": "فایل صوتی یافت نشد", + "selected": "انتخاب شده", + "loading": "در حال بارگذاری…", + "search_error": "بارگذاری فایل‌های صوتی ممکن نشد", + "adding": "در حال افزودن…" }, "actions": { "search": "جست‌و‌جوی پرونده‌ها..", diff --git a/static/locales/fr.json b/static/locales/fr.json index 57d0ff45..7d083895 100644 --- a/static/locales/fr.json +++ b/static/locales/fr.json @@ -62,7 +62,13 @@ "add_error": "Impossible d'ajouter les pistes", "no_playlists_yet": "Pas encore de playlists. Créez-en une d'abord !", "selected_files": "Sélectionnés :", - "error": "Erreur" + "error": "Erreur", + "search_audio": "Rechercher des fichiers audio…", + "no_audio_files": "Aucun fichier audio trouvé", + "selected": "sélectionnés", + "loading": "Chargement…", + "search_error": "Impossible de charger les fichiers audio", + "adding": "Ajout en cours…" }, "actions": { "search": "Rechercher des fichiers...", diff --git a/static/locales/hi.json b/static/locales/hi.json index 07c69806..e6d7acd5 100644 --- a/static/locales/hi.json +++ b/static/locales/hi.json @@ -62,7 +62,13 @@ "add_error": "प्लेलिस्ट में ट्रैक नहीं जोड़े जा सके", "no_playlists_yet": "अभी तक कोई प्लेलिस्ट नहीं। पहले एक बनाएं!", "selected_files": "चयनित:", - "error": "त्रुटि" + "error": "त्रुटि", + "search_audio": "ऑडियो फ़ाइलें खोजें…", + "no_audio_files": "कोई ऑडियो फ़ाइल नहीं मिली", + "selected": "चयनित", + "loading": "लोड हो रहा है…", + "search_error": "ऑडियो फ़ाइलें लोड नहीं हो सकीं", + "adding": "जोड़ा जा रहा है…" }, "actions": { "search": "फ़ाइलें खोजें...", diff --git a/static/locales/it.json b/static/locales/it.json index fa69fe59..f81b2d2b 100644 --- a/static/locales/it.json +++ b/static/locales/it.json @@ -62,7 +62,13 @@ "add_error": "Impossibile aggiungere le tracce", "no_playlists_yet": "Nessuna playlist ancora. Creane una prima!", "selected_files": "Selezionati:", - "error": "Errore" + "error": "Errore", + "search_audio": "Cerca file audio…", + "no_audio_files": "Nessun file audio trovato", + "selected": "selezionati", + "loading": "Caricamento…", + "search_error": "Impossibile caricare i file audio", + "adding": "Aggiunta in corso…" }, "actions": { "search": "Cerca file...", diff --git a/static/locales/ja.json b/static/locales/ja.json index 0c4520de..2e2dc09d 100644 --- a/static/locales/ja.json +++ b/static/locales/ja.json @@ -62,7 +62,13 @@ "add_error": "曲を追加できませんでした", "no_playlists_yet": "プレイリストがありません。最初に作成してください!", "selected_files": "選択中:", - "error": "エラー" + "error": "エラー", + "search_audio": "オーディオファイルを検索…", + "no_audio_files": "オーディオファイルが見つかりません", + "selected": "件選択中", + "loading": "読み込み中…", + "search_error": "オーディオファイルを読み込めませんでした", + "adding": "追加中…" }, "actions": { "search": "ファイルを検索...", diff --git a/static/locales/ko.json b/static/locales/ko.json index 6466898c..f8714310 100644 --- a/static/locales/ko.json +++ b/static/locales/ko.json @@ -62,7 +62,13 @@ "add_error": "트랙을 플레이리스트에 추가할 수 없습니다", "no_playlists_yet": "플레이리스트가 없습니다. 먼저 하나를 만드세요!", "selected_files": "선택됨:", - "error": "오류" + "error": "오류", + "search_audio": "오디오 파일 검색…", + "no_audio_files": "오디오 파일을 찾을 수 없습니다", + "selected": "선택됨", + "loading": "로딩 중…", + "search_error": "오디오 파일을 불러올 수 없습니다", + "adding": "추가 중…" }, "actions": { "search": "파일 검색...", diff --git a/static/locales/nl.json b/static/locales/nl.json index 8380f05e..69ab174e 100644 --- a/static/locales/nl.json +++ b/static/locales/nl.json @@ -62,7 +62,13 @@ "add_error": "Kon tracks niet toevoegen aan playlist", "no_playlists_yet": "Nog geen playlists. Maak er eerst een!", "selected_files": "Geselecteerd:", - "error": "Fout" + "error": "Fout", + "search_audio": "Audiobestanden zoeken…", + "no_audio_files": "Geen audiobestanden gevonden", + "selected": "geselecteerd", + "loading": "Laden…", + "search_error": "Kan audiobestanden niet laden", + "adding": "Toevoegen…" }, "actions": { "search": "Zoek bestanden...", diff --git a/static/locales/pt.json b/static/locales/pt.json index 6d58dae0..3ebfa8fa 100644 --- a/static/locales/pt.json +++ b/static/locales/pt.json @@ -62,7 +62,13 @@ "add_error": "Não foi possível adicionar as faixas", "no_playlists_yet": "Nenhuma playlist ainda. Crie uma primeiro!", "selected_files": "Selecionados:", - "error": "Erro" + "error": "Erro", + "search_audio": "Pesquisar ficheiros de áudio…", + "no_audio_files": "Nenhum ficheiro de áudio encontrado", + "selected": "selecionados", + "loading": "A carregar…", + "search_error": "Não foi possível carregar os ficheiros de áudio", + "adding": "A adicionar…" }, "actions": { "search": "Pesquisar arquivos...", diff --git a/static/locales/ru.json b/static/locales/ru.json index c8eb1df1..2caabb30 100644 --- a/static/locales/ru.json +++ b/static/locales/ru.json @@ -62,7 +62,13 @@ "add_error": "Не удалось добавить треки в плейлист", "no_playlists_yet": "Плейлистов пока нет. Создайте сначала!", "selected_files": "Выбрано:", - "error": "Ошибка" + "error": "Ошибка", + "search_audio": "Поиск аудиофайлов…", + "no_audio_files": "Аудиофайлы не найдены", + "selected": "выбрано", + "loading": "Загрузка…", + "search_error": "Не удалось загрузить аудиофайлы", + "adding": "Добавление…" }, "actions": { "search": "Поиск файлов...", diff --git a/static/locales/zh.json b/static/locales/zh.json index fb5b2127..30bec702 100644 --- a/static/locales/zh.json +++ b/static/locales/zh.json @@ -62,7 +62,13 @@ "add_error": "无法将曲目添加到播放列表", "no_playlists_yet": "暂无播放列表。请先创建一个!", "selected_files": "已选择:", - "error": "错误" + "error": "错误", + "search_audio": "搜索音频文件…", + "no_audio_files": "未找到音频文件", + "selected": "已选择", + "loading": "加载中…", + "search_error": "无法加载音频文件", + "adding": "添加中…" }, "actions": { "search": "搜索文件...",