Compare commits

...
Author SHA1 Message Date
Graham Steffaniak c6a4b435d6 fixed build 2026-11-02 12:28:12 -06:00
Graham Steffaniak 470f3fad92 use dist folder instead of embed 2026-11-02 11:04:59 -06:00
Graham Steffaniak 129168c691 don't hunt for right path 2026-11-01 14:28:23 -06:00
Graham Steffaniak d4fcfe62d5 phase 2 build optimizations 2026-11-01 14:24:04 -06:00
120 changed files with 1588 additions and 298 deletions

No files matched your search

-1
View File
@@ -9,7 +9,6 @@ backend/filebrowser.sqlite*
backend/tmp
backend/dist
backend/backend*
backend/internal/web/dist/**
backend/vendor/**
frontend/dist/**
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Builds the comma-separated Docker tag list for a release image.
#
# Usage: docker-release-tags.sh <metadata.json> <variant> <update_track> <update_major>
# metadata.json JSON output of docker/metadata-action (uses .tags)
# variant default | slim | media
# default and media: floating tags include latest
# slim: every tag gets a -slim suffix and latest is not added
# update_track true when the semver policy allows floating beta/stable/latest tags
# update_major true when the semver policy allows major-only version tags
#
# Image repositories are fixed per variant so that each image only gets its own tags.
set -euo pipefail
if [[ $# -ne 4 ]]; then
echo "usage: $0 <metadata.json> <default|slim|media> <true|false> <true|false>" >&2
exit 2
fi
metadata_file=$1
variant=$2
update_track=$3
update_major=$4
case "$variant" in
default)
suffix=""
add_latest=true
repos=(gtstef/filebrowser ghcr.io/gtsteffaniak/filebrowser)
;;
slim)
suffix="-slim"
add_latest=false
repos=(gtstef/filebrowser ghcr.io/gtsteffaniak/filebrowser)
;;
media)
suffix=""
add_latest=true
repos=(gtstef/filebrowser-media ghcr.io/gtsteffaniak/filebrowser-media)
;;
*)
echo "unknown variant: $variant" >&2
exit 2
;;
esac
jq -r \
--arg suffix "$suffix" \
--argjson addLatest "$add_latest" \
--argjson updateTrack "$update_track" \
--argjson updateMajor "$update_major" \
--arg dh "${repos[0]}" \
--arg gh "${repos[1]}" '
# Strip the v prefix, drop metadata latest, and append the variant suffix.
.tags |= map(
sub(":v"; ":")
| select(test(":latest") | not)
| . + $suffix
) |
# Version tags with a prerelease suffix (X.Y.Z-suffix) get major.minor, and major when allowed.
.tags += [
.tags[]
| select(test(":[0-9]+\\.[0-9]+\\.[0-9]+-"))
| split(":")
| .[-1] as $version
| ($version | split("-")) as $version_split
| ($version_split[0] | split(".")) as $version_parts
| ($version_split[1:] | join("-")) as $tag_suffix
| ($version_parts | "\(.[0]).\(.[1])") as $major_minor
| ($version_parts[0]) as $major
| "\($dh):\($major_minor)-\($tag_suffix)",
"\($gh):\($major_minor)-\($tag_suffix)",
if $updateMajor then
"\($dh):\($major)-\($tag_suffix)",
"\($gh):\($major)-\($tag_suffix)"
else empty end
] |
# Floating beta tag.
if $updateTrack and any(.tags[]; split(":")[-1] | endswith("-beta" + $suffix)) then
.tags += ["\($dh):beta\($suffix)", "\($gh):beta\($suffix)"]
else . end |
# Floating stable tag, plus latest for variants that have it.
if $updateTrack and any(.tags[]; split(":")[-1] | endswith("-stable" + $suffix)) then
.tags += (
["\($dh):stable\($suffix)", "\($gh):stable\($suffix)"]
+ (if $addLatest then ["\($dh):latest", "\($gh):latest"] else [] end)
)
else . end |
.tags | join(",")
' "$metadata_file"
+1 -5
View File
@@ -97,7 +97,7 @@ jobs:
uses: actions/download-artifact@v4
with:
name: dist
path: frontend/dist
path: backend/internal/web/dist
- name: Download backend build
uses: actions/download-artifact@v4
with:
@@ -105,10 +105,6 @@ jobs:
path: backend/
- name: Make backend executable
run: chmod +x backend/filebrowser
- name: Copy frontend build to backend
run: |
mkdir -p backend/internal/web/embed
cp -r frontend/dist/* backend/internal/web/embed
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
+6 -130
View File
@@ -75,50 +75,8 @@ jobs:
id: modify-json
run: |
JSON='${{ steps.meta.outputs.json }}'
MODIFIED_JSON=$(echo "$JSON" | jq \
--argjson updateTrack '${{ steps.floating.outputs.update_track == 'true' }}' \
--argjson updateMajor '${{ steps.floating.outputs.update_major == 'true' }}' '
.tags |= map(
sub(":v"; ":") # Strip the 'v'
| select(test(":latest") | not)
) |
# Find all version tags with suffix (X.Y.Z-suffix) and add major.minor (always) and major tags (when allowed)
.tags += [
.tags[]
| select(test(":[0-9]+\\.[0-9]+\\.[0-9]+-"))
| split(":")
| .[-1] as $version
| ($version | split("-")) as $version_split
| ($version_split[0] | split(".")) as $version_parts
| ($version_split[1:] | join("-")) as $suffix
| ($version_parts | "\(.[0]).\(.[1])") as $major_minor
| ($version_parts[0]) as $major
| "gtstef/filebrowser:\($major_minor)-\($suffix)",
"ghcr.io/gtsteffaniak/filebrowser:\($major_minor)-\($suffix)",
if $updateMajor then
"gtstef/filebrowser:\($major)-\($suffix)",
"ghcr.io/gtsteffaniak/filebrowser:\($major)-\($suffix)"
else empty end
] |
# Add suffix-only tags for beta when semver policy allows
if $updateTrack and any(.tags[]; split(":")[-1] | endswith("-beta")) then
.tags += [
"gtstef/filebrowser:beta",
"ghcr.io/gtsteffaniak/filebrowser:beta"
]
else . end |
# Add suffix-only tags for stable (and latest) when semver policy allows
if $updateTrack and any(.tags[]; split(":")[-1] | endswith("-stable")) then
.tags += [
"gtstef/filebrowser:stable",
"ghcr.io/gtsteffaniak/filebrowser:stable",
"gtstef/filebrowser:latest",
"ghcr.io/gtsteffaniak/filebrowser:latest"
]
else . end
')
TAGS_CSV=$(echo "$MODIFIED_JSON" | jq -r '.tags | join(",")')
echo "$TAGS_CSV" > modified_tags.txt # Optional debug output
printf '%s' "$JSON" > metadata.json
TAGS_CSV=$(bash .github/scripts/docker-release-tags.sh metadata.json default '${{ steps.floating.outputs.update_track }}' '${{ steps.floating.outputs.update_major }}')
echo "cleaned_tag=$TAGS_CSV" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v6
@@ -171,50 +129,8 @@ jobs:
id: modify-json
run: |
JSON='${{ steps.meta.outputs.json }}'
MODIFIED_JSON=$(echo "$JSON" | jq \
--argjson updateTrack '${{ steps.floating.outputs.update_track == 'true' }}' \
--argjson updateMajor '${{ steps.floating.outputs.update_major == 'true' }}' '
.tags |= map(
sub(":v"; ":") # Strip the 'v'
| select(test(":latest") | not)
) |
# Find all version tags with suffix (X.Y.Z-suffix) and add major.minor (always) and major tags (when allowed)
.tags += [
.tags[]
| select(test(":[0-9]+\\.[0-9]+\\.[0-9]+-"))
| split(":")
| .[-1] as $version
| ($version | split("-")) as $version_split
| ($version_split[0] | split(".")) as $version_parts
| ($version_split[1:] | join("-")) as $suffix
| ($version_parts | "\(.[0]).\(.[1])") as $major_minor
| ($version_parts[0]) as $major
| "gtstef/filebrowser-media:\($major_minor)-\($suffix)",
"ghcr.io/gtsteffaniak/filebrowser-media:\($major_minor)-\($suffix)",
if $updateMajor then
"gtstef/filebrowser-media:\($major)-\($suffix)",
"ghcr.io/gtsteffaniak/filebrowser-media:\($major)-\($suffix)"
else empty end
] |
# Add suffix-only tags for beta when semver policy allows
if $updateTrack and any(.tags[]; split(":")[-1] | endswith("-beta")) then
.tags += [
"gtstef/filebrowser-media:beta",
"ghcr.io/gtsteffaniak/filebrowser-media:beta"
]
else . end |
# Add suffix-only tags for stable (and latest) when semver policy allows
if $updateTrack and any(.tags[]; split(":")[-1] | endswith("-stable")) then
.tags += [
"gtstef/filebrowser-media:stable",
"ghcr.io/gtsteffaniak/filebrowser-media:stable",
"gtstef/filebrowser-media:latest",
"ghcr.io/gtsteffaniak/filebrowser-media:latest"
]
else . end
')
TAGS_CSV=$(echo "$MODIFIED_JSON" | jq -r '.tags | join(",")')
echo "$TAGS_CSV" > modified_tags.txt # Optional debug output
printf '%s' "$JSON" > metadata.json
TAGS_CSV=$(bash .github/scripts/docker-release-tags.sh metadata.json media '${{ steps.floating.outputs.update_track }}' '${{ steps.floating.outputs.update_major }}')
echo "cleaned_tag=$TAGS_CSV" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v6
@@ -267,48 +183,8 @@ jobs:
id: modify-json-slim
run: |
JSON='${{ steps.meta.outputs.json }}'
MODIFIED_JSON=$(echo "$JSON" | jq \
--argjson updateTrack '${{ steps.floating.outputs.update_track == 'true' }}' \
--argjson updateMajor '${{ steps.floating.outputs.update_major == 'true' }}' '
.tags |= map(
sub(":v"; ":") # Strip the 'v'
| select(test(":latest") | not) # Remove the "latest" tag
| . + "-slim" # Append "-slim" to the versioned tag
) |
# Find all version tags with suffix (X.Y.Z-suffix-slim) and add major.minor (always) and major tags (when allowed)
.tags += [
.tags[]
| select(test(":[0-9]+\\.[0-9]+\\.[0-9]+-"))
| split(":")
| .[-1] as $version
| ($version | split("-")) as $version_split
| ($version_split[0] | split(".")) as $version_parts
| ($version_split[1:] | join("-")) as $suffix
| ($version_parts | "\(.[0]).\(.[1])") as $major_minor
| ($version_parts[0]) as $major
| "gtstef/filebrowser:\($major_minor)-\($suffix)",
"ghcr.io/gtsteffaniak/filebrowser:\($major_minor)-\($suffix)",
if $updateMajor then
"gtstef/filebrowser:\($major)-\($suffix)",
"ghcr.io/gtsteffaniak/filebrowser:\($major)-\($suffix)"
else empty end
] |
# Add suffix-only tags for beta-slim when semver policy allows
if $updateTrack and any(.tags[]; split(":")[-1] | endswith("-beta-slim")) then
.tags += [
"gtstef/filebrowser:beta-slim",
"ghcr.io/gtsteffaniak/filebrowser:beta-slim"
]
else . end |
# Add suffix-only tags for stable-slim when semver policy allows
if $updateTrack and any(.tags[]; split(":")[-1] | endswith("-stable-slim")) then
.tags += [
"gtstef/filebrowser:stable-slim",
"ghcr.io/gtsteffaniak/filebrowser:stable-slim"
]
else . end
')
TAGS_CSV=$(echo "$MODIFIED_JSON" | jq -r '.tags | join(",")')
printf '%s' "$JSON" > metadata.json
TAGS_CSV=$(bash .github/scripts/docker-release-tags.sh metadata.json slim '${{ steps.floating.outputs.update_track }}' '${{ steps.floating.outputs.update_major }}')
echo "cleaned_tag=$TAGS_CSV" >> $GITHUB_OUTPUT
- name: Build and push slim image
uses: docker/build-push-action@v6
+2 -2
View File
@@ -43,8 +43,8 @@ frontend/.tmp-icons-*
/backend/*test_config.yaml
/backend/generated.yaml
/backend/srv
/backend/internal/web/dist
/backend/internal/web/embed
/backend/internal/web/dist/*
!/backend/internal/web/dist/.gitkeep
/backend/debug
/backend/http
+14
View File
@@ -3,7 +3,9 @@
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
## v2.2.0
**New Features**:
- Core web assets are loaded into memory instead of served from disk.
- Media enhancements
- All server media settings can be managed in settings by admins.
- Transcoding for media playback with quality presets -- requires ffmpeg.
@@ -13,6 +15,17 @@ All notable changes to this project will be documented in this file. For commit
# office previews can use embedded thumbnail if possible
# text file previews are more efficient direct creation
**Notes**:
- Smaller binaries: the Swagger UI serves only the four files it needs instead of the full swagger-ui bundle
- UPX compression is removed from release builds.
- Runtime debug tooling (pprof) is only compiled in with `-tags debug`. Dev targets (`make run`, `make dev`) enable it automatically.
- The CGO SQLite driver and the `cgosql` build tag are removed.
- A `filebrowser-media` image (`_docker/Dockerfile.media`) is published alongside the main and slim images.
- Non-dev builds (including Docker) load the frontend into memory at startup when preload succeeds; if preload fails, assets are served from disk. Dev mode still serves from disk.
- Frontend build output lives only in `backend/internal/web/dist`. Release builds compile that tree into the binary with `go:embed`; Docker and other disk-only runs preload or serve the same `dist` folder at runtime.
- Release image tags are generated by one script (`.github/scripts/docker-release-tags.sh`) for the main, slim and media images.
- `make build-docker-media` builds the media image locally.
**Bug Fixes**:
- Closing a video preview from the header bar now stops playback immediately instead of leaving audio running in the background.
- Closing a preview no longer navigates back to the same file when a stale "previous item" was remembered from an earlier navigation.
@@ -21,6 +34,7 @@ All notable changes to this project will be documented in this file. For commit
- getDirectoryPreview no longer requests metadata, so folder previews skip the per-file probe and subtitle scan.
- Media probes are cached by file identity and concurrent reads are coalesced, so opening a file no longer triggers repeated ffprobe runs.
- Client-initiated stream cancels (seek/navigation) no longer log spurious write-error responses.
- Gzipped `.woff` font files are no longer generated without being served; only `.js` and `.woff2` are precompressed.
## v2.1.4
+1 -1
View File
@@ -12,7 +12,7 @@ WORKDIR /app/backend/
COPY [ "./backend/filebrowser", "./"]
COPY [ "./backend/reduce-rounded-corners.css", "./no-rounded.css" ]
RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
COPY [ "./backend/internal/web/dist/", "./internal/web/dist/"]
# generate duplicate 1.1MB files
RUN dd if=/dev/zero of=/app/frontend/tests/playwright-files/1.1MB.bin bs=1024 count=1126 && dd if=/dev/zero of=/app/frontend/tests/playwright-files/1.1mb.bin bs=1024 count=1126
+1 -1
View File
@@ -6,7 +6,7 @@ COPY [ "./frontend/tests", "./tests" ]
WORKDIR /app/backend/
COPY [ "./backend/filebrowser", "./"]
RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
COPY [ "./backend/internal/web/dist/", "./internal/web/dist/"]
RUN apt update && apt install nginx -y
RUN mv default.conf /etc/nginx/conf.d/default.conf
RUN sed -i 's/filebrowser/localhost/g' /etc/nginx/conf.d/default.conf
+1 -1
View File
@@ -11,7 +11,7 @@ COPY [ "./_docker/src/settings/backend/config.yaml", "./"]
COPY [ "./_docker/src/settings/backend/database.db.old", "./"]
COPY [ "./backend/reduce-rounded-corners.css", "./no-rounded.css"]
RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
COPY [ "./backend/internal/web/dist/", "./internal/web/dist/"]
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
RUN ./filebrowser user set testuser1 --password testuser1
+1 -1
View File
@@ -11,7 +11,7 @@ COPY [ "./frontend/tests/", "./tests" ]
WORKDIR /app/backend/
COPY [ "./backend/filebrowser", "./"]
RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
COPY [ "./backend/internal/web/dist/", "./internal/web/dist/"]
# Run tests
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
+1 -1
View File
@@ -11,7 +11,7 @@ COPY [ "./frontend/tests/playwright-files", "./tests/playwright-files2" ]
WORKDIR /app/backend/
COPY [ "./backend/filebrowser", "./"]
RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
COPY [ "./backend/internal/web/dist/", "./internal/web/dist/"]
# Run tests
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
+1 -1
View File
@@ -7,7 +7,7 @@ WORKDIR /app/backend/
RUN mkdir -p ./internal/web/dist
COPY [ "./_docker/src/oidc/backend/", "./"]
COPY ["./backend/filebrowser", "./"]
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
COPY [ "./backend/internal/web/dist/", "./internal/web/dist/"]
# Run tests
WORKDIR /app/frontend
+1 -1
View File
@@ -23,7 +23,7 @@ WORKDIR /app/backend/
COPY [ "./backend/filebrowser", "./"]
COPY [ "./backend/reduce-rounded-corners.css", "./no-rounded.css" ]
RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
COPY [ "./backend/internal/web/dist/", "./internal/web/dist/"]
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
ENV CI=true
+1 -1
View File
@@ -13,7 +13,7 @@ COPY [ "./frontend/tests", "./tests" ]
WORKDIR /app/backend/
COPY [ "./backend/filebrowser", "./"]
RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
COPY [ "./backend/internal/web/dist/", "./internal/web/dist/"]
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test
+1 -1
View File
@@ -6,7 +6,7 @@ COPY [ "./frontend/tests", "./tests" ]
WORKDIR /app/backend/
COPY [ "./backend/filebrowser", "./"]
RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
COPY [ "./backend/internal/web/dist/", "./internal/web/dist/"]
RUN apt update && apt install nginx -y
RUN mv default.conf /etc/nginx/conf.d/default.conf
RUN mv .htpasswd /etc/nginx/conf.d/.htpasswd
+1 -1
View File
@@ -10,7 +10,7 @@ COPY [ "./backend/filebrowser", "./"]
COPY [ "./_docker/src/screenshots/backend/*", "./"]
RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
COPY [ "./backend/internal/web/dist/", "./internal/web/dist/"]
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
RUN ./filebrowser & sleep 2 && cd ../frontend && npx playwright test --project dark-screenshots
+1 -1
View File
@@ -13,7 +13,7 @@ WORKDIR /app/backend/
COPY [ "./backend/filebrowser", "./"]
COPY [ "./backend/reduce-rounded-corners.css", "./no-rounded.css"]
RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
COPY [ "./backend/internal/web/dist/", "./internal/web/dist/"]
# Run tests
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
+1 -1
View File
@@ -11,7 +11,7 @@ COPY [ "./frontend/tests/", "./tests" ]
WORKDIR /app/backend/
COPY [ "./backend/filebrowser", "./"]
RUN mkdir -p ./internal/web/dist
COPY [ "./backend/internal/web/embed/", "./internal/web/dist/"]
COPY [ "./backend/internal/web/dist/", "./internal/web/dist/"]
# Run tests
ENV FILEBROWSER_PLAYWRIGHT_TEST=true
+1 -1
View File
@@ -7,7 +7,7 @@ env_files = [".air.env"]
entrypoint = "./tmp/filebrowser"
args_bin = ["-c", "test_config.yaml"]
include_ext = ["go", "tpl", "tmpl", "html"]
exclude_dir = ["tmp", "vendor", "internal/web/dist", "internal/web/embed", "internal/preview/testdata", "internal/preview/thumbnails", "internal/preview/heic", "swagger/docs", "internal/preview/gofitzinclude"]
exclude_dir = ["tmp", "vendor", "internal/web/dist", "internal/preview/testdata", "internal/preview/thumbnails", "internal/preview/heic", "swagger/docs", "internal/preview/gofitzinclude"]
poll = true
poll_interval = 1000
log = "air.log"
+16 -11
View File
@@ -293,18 +293,23 @@ func createConfig(configpath string, noInput bool) error {
return nil
}
// setupConfigYAMLCandidates lists on-disk setup templates for config.generated.yaml.
var setupConfigYAMLCandidates = []string{
"frontend/public/config.generated.yaml",
"../frontend/public/config.generated.yaml",
"internal/web/dist/config.generated.yaml",
"../internal/web/dist/config.generated.yaml",
"backend/internal/web/dist/config.generated.yaml",
}
func readSetupConfigTemplateYAML() (string, error) {
return settings.ReadFirstExistingFile(setupConfigYAMLCandidates)
}
func generateSetupConfigYAML(config *settings.Settings) (string, error) {
embeddedPaths := []string{
"frontend/public/config.generated.yaml",
"../frontend/public/config.generated.yaml",
"internal/web/dist/config.generated.yaml",
"../internal/web/dist/config.generated.yaml",
}
for _, path := range embeddedPaths {
embeddedYaml, err := os.ReadFile(path)
if err == nil && len(embeddedYaml) > 0 {
return settings.GenerateConfigYamlWithEmbedded(config, true, false, false, string(embeddedYaml))
}
embeddedYaml, err := readSetupConfigTemplateYAML()
if err == nil {
return settings.GenerateConfigYamlWithEmbedded(config, true, false, false, embeddedYaml)
}
return settings.GenerateConfigYaml(config, true, false, false)
}
+47
View File
@@ -0,0 +1,47 @@
package cmd
import (
"os"
"path/filepath"
"testing"
)
func writeSetupConfigYAML(t *testing.T, root, relPath, content string) {
t.Helper()
path := filepath.Join(root, filepath.FromSlash(relPath))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func TestReadSetupConfigTemplateYAML_fromDist(t *testing.T) {
root := t.TempDir()
writeSetupConfigYAML(t, root, "internal/web/dist/config.generated.yaml", "dist-content")
t.Chdir(root)
got, err := readSetupConfigTemplateYAML()
if err != nil {
t.Fatal(err)
}
if got != "dist-content" {
t.Fatalf("readSetupConfigTemplateYAML() = %q, want dist-content", got)
}
}
func TestReadSetupConfigTemplateYAML_prefersFrontendPublic(t *testing.T) {
root := t.TempDir()
writeSetupConfigYAML(t, root, "frontend/public/config.generated.yaml", "from-public")
writeSetupConfigYAML(t, root, "internal/web/dist/config.generated.yaml", "from-dist")
t.Chdir(root)
got, err := readSetupConfigTemplateYAML()
if err != nil {
t.Fatal(err)
}
if got != "from-public" {
t.Fatalf("readSetupConfigTemplateYAML() = %q, want from-public", got)
}
}
+5 -10
View File
@@ -92,14 +92,7 @@ func StartFilebrowser() {
done := make(chan struct{}) // Signals server has stopped
shutdownComplete := make(chan struct{}) // Signals shutdown process is complete
// Dev mode enables development features like template hot-reloading
_, err := os.Stat("internal/web/dist")
// In dev mode, always use filesystem assets. Otherwise, check if internal/web/dist exists
if !settings.Env.IsDevMode {
settings.Env.EmbeddedFs = os.IsNotExist(err)
}
err = utils.SetInvalidPasswordHash()
err := utils.SetInvalidPasswordHash()
if err != nil {
logger.Fatalf("Failed to set security hash: %v", err)
}
@@ -206,14 +199,16 @@ func rootCMD(ctx context.Context, serverConfig *settings.Server, a *app.App, shu
numWorkers := settings.Config.Server.NumImageProcessors
// Initialize asset filesystem before starting services
if settings.Env.EmbeddedFs {
if !settings.Env.IsDevMode && web.EmbeddedFrontendAvailable() {
embeddedAssets := web.GetEmbeddedAssets()
subAssets, err := fs.Sub(embeddedAssets, "embed")
subAssets, err := fs.Sub(embeddedAssets, "dist")
if err != nil {
logger.Fatalf("Failed to create sub filesystem: %v", err)
}
settings.Env.EmbeddedFs = true
fileutils.InitAssetFS(subAssets, true, false)
} else {
settings.Env.EmbeddedFs = false
fileutils.InitAssetFS(nil, false, settings.Env.IsDevMode)
}
@@ -3,6 +3,8 @@ package fileutils
import (
"io/fs"
"net/http"
"github.com/gtsteffaniak/go-logger/logger"
)
var (
@@ -28,7 +30,17 @@ func InitAssetFS(embeddedAssets fs.FS, useEmbedded bool, devMode bool) {
// Dev mode: serve frontend source assets (templates, fonts, icons)
assetFs = dirFS{Dir: http.Dir("../frontend/public")}
} else {
assetFs = dirFS{Dir: http.Dir("internal/web/dist")}
// Non-dev disk build (e.g. Docker): dist is shipped beside the binary. Preload it
// into memory when possible; otherwise serve from disk.
diskAssets := dirFS{Dir: http.Dir("internal/web/dist")}
cached, err := loadMemFS(diskAssets)
if err != nil {
logger.Warningf("could not preload frontend assets into memory, serving from disk: %v", err)
assetFs = diskAssets
} else {
logger.Debugf("preloaded %d frontend assets (%d bytes) into memory", cached.Len(), cached.Size())
assetFs = cached
}
}
}
@@ -0,0 +1,110 @@
package fileutils
import (
"bytes"
"io/fs"
"time"
)
// memFS is a read-only fs.FS that holds every file's contents in memory.
// It is built once at startup from a disk-backed filesystem so that asset
// requests never touch the disk.
type memFS struct {
files map[string]memFile
}
type memFile struct {
data []byte
modTime time.Time
}
// loadMemFS reads every regular file under src into memory.
func loadMemFS(src fs.FS) (*memFS, error) {
m := &memFS{files: make(map[string]memFile)}
err := fs.WalkDir(src, ".", func(name string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
data, err := fs.ReadFile(src, name)
if err != nil {
return err
}
info, err := d.Info()
if err != nil {
return err
}
m.files[name] = memFile{data: data, modTime: info.ModTime()}
return nil
})
if err != nil {
return nil, err
}
return m, nil
}
// Size returns the total number of bytes held in memory.
func (m *memFS) Size() int {
total := 0
for _, f := range m.files {
total += len(f.data)
}
return total
}
// Len returns the number of files held in memory.
func (m *memFS) Len() int {
return len(m.files)
}
// Open implements fs.FS. Only regular files are supported, which is all the
// asset callers need (ReadFile, Stat and template parsing).
func (m *memFS) Open(name string) (fs.File, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid}
}
f, ok := m.files[name]
if !ok {
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
}
return &memOpenFile{
Reader: bytes.NewReader(f.data),
info: memFileInfo{
name: baseName(name),
size: int64(len(f.data)),
modTime: f.modTime,
},
}, nil
}
type memOpenFile struct {
*bytes.Reader
info memFileInfo
}
func (f *memOpenFile) Stat() (fs.FileInfo, error) { return f.info, nil }
func (f *memOpenFile) Close() error { return nil }
type memFileInfo struct {
name string
size int64
modTime time.Time
}
func (i memFileInfo) Name() string { return i.name }
func (i memFileInfo) Size() int64 { return i.size }
func (i memFileInfo) Mode() fs.FileMode { return 0o444 }
func (i memFileInfo) ModTime() time.Time { return i.modTime }
func (i memFileInfo) IsDir() bool { return false }
func (i memFileInfo) Sys() any { return nil }
func baseName(name string) string {
for i := len(name) - 1; i >= 0; i-- {
if name[i] == '/' {
return name[i+1:]
}
}
return name
}
@@ -0,0 +1,69 @@
package fileutils
import (
"io/fs"
"net/http"
"os"
"path/filepath"
"testing"
)
func TestLoadMemFSServesFromMemory(t *testing.T) {
dir := t.TempDir()
files := map[string]string{
"index.html": "<html>index</html>",
"assets/app.js.gz": "gzip-bytes",
"fonts/icons.woff2": "font-bytes",
}
for name, content := range files {
path := filepath.Join(dir, filepath.FromSlash(name))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
m, err := loadMemFS(dirFS{Dir: http.Dir(dir)})
if err != nil {
t.Fatalf("loadMemFS: %v", err)
}
if m.Len() != len(files) {
t.Fatalf("loaded %d files, want %d", m.Len(), len(files))
}
// Remove everything from disk: the cache must still serve every file.
if removeErr := os.RemoveAll(dir); removeErr != nil {
t.Fatal(removeErr)
}
for name, want := range files {
got, readErr := fs.ReadFile(m, name)
if readErr != nil {
t.Fatalf("ReadFile(%s) after disk removal: %v", name, readErr)
}
if string(got) != want {
t.Errorf("ReadFile(%s) = %q, want %q", name, got, want)
}
info, statErr := fs.Stat(m, name)
if statErr != nil {
t.Fatalf("Stat(%s): %v", name, statErr)
}
if info.IsDir() || info.Size() != int64(len(want)) || info.Name() != filepath.Base(name) {
t.Errorf("Stat(%s) = %+v, unexpected", name, info)
}
}
// Template-style glob on a plain name must resolve via Stat.
matches, globErr := fs.Glob(m, "index.html")
if globErr != nil || len(matches) != 1 {
t.Fatalf("Glob(index.html) = %v, %v", matches, globErr)
}
if _, readErr := fs.ReadFile(m, "missing.css"); readErr == nil {
t.Error("ReadFile(missing.css) should fail")
}
if _, readErr := fs.ReadFile(m, "../escape"); readErr == nil {
t.Error("invalid path should be rejected")
}
}
@@ -1,95 +0,0 @@
package gomedia
import (
"bytes"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestSupportsVideoContainer(t *testing.T) {
cases := map[string]bool{
"movie.mp4": true,
"MOVIE.MKV": true,
"clip.webm": true,
"clip.mov": true,
"clip.m4v": true,
"old.avi": false,
"legacy.wmv": false,
"stream.ts": false,
"no-extension": false,
"dir.mp4/folder": false,
}
for path, want := range cases {
require.Equal(t, want, supportsVideoContainer(path), path)
}
}
// TestNonTargetVideoContainerSkipsGoMedia checks that containers go-media does
// not target return the fallback result without reading the file. The path does
// not exist, so any file read would also fail. Only the extension gate can
// return false here before go-media is called.
func TestNonTargetVideoContainerSkipsGoMedia(t *testing.T) {
withGate(t, false)
missing := filepath.Join(t.TempDir(), "missing.wmv")
_, ok := Probe(missing, nil)
require.False(t, ok)
_, ok = VideoThumbnail(missing, 256, 256, 72, 10, nil)
require.False(t, ok)
_, ok = EmbeddedSubtitles(missing, time.Now(), nil)
require.False(t, ok)
_, ok = ExtractSubtitle(missing, 0, nil)
require.False(t, ok)
}
// encoderAvailable reports whether ffmpeg was built with the named encoder.
func encoderAvailable(t *testing.T, name string) bool {
t.Helper()
requireTool(t, "ffmpeg")
out, err := exec.Command("ffmpeg", "-hide_banner", "-encoders").Output()
require.NoError(t, err)
return bytes.Contains(out, []byte(" "+name+" "))
}
// makeMKV encodes a short clip with the given video encoder into an MKV file.
func makeMKV(t *testing.T, encoder string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "clip.mkv")
out, err := exec.Command("ffmpeg", "-v", "error", "-y",
"-f", "lavfi", "-i", "testsrc2=size=320x240:rate=25",
"-t", "2", "-c:v", encoder, "-b:v", "500k", "-an", path).CombinedOutput()
require.NoError(t, err, string(out))
return path
}
// TestH264MatroskaThumbnailFallsBack checks that H.264 in MKV, which go-media
// does not decode, is reported as unsupported so the caller uses ffmpeg.
func TestH264MatroskaThumbnailFallsBack(t *testing.T) {
withGate(t, false)
if !encoderAvailable(t, "libx264") {
t.Skip("ffmpeg has no libx264 encoder")
}
mkv := makeMKV(t, "libx264")
_, ok := VideoThumbnail(mkv, 160, 120, 72, 10, nil)
require.False(t, ok, "go-media must not thumbnail H.264 in MKV")
}
// TestVP8MatroskaThumbnailUsesGoMedia checks that the codec gate in go-media
// does not block VP8, which go-media still decodes.
func TestVP8MatroskaThumbnailUsesGoMedia(t *testing.T) {
withGate(t, false)
if !encoderAvailable(t, "libvpx") {
t.Skip("ffmpeg has no libvpx encoder")
}
mkv := makeMKV(t, "libvpx")
data, ok := VideoThumbnail(mkv, 160, 120, 72, 50, nil)
require.True(t, ok, "go-media must thumbnail VP8 in MKV")
require.True(t, strings.HasPrefix(string(data[:2]), "\xff\xd8"), "output must be JPEG")
}
View File
Whitespace-only changes.
@@ -0,0 +1 @@
.activity-details-table[data-v-be79e288] thead th,.activity-details-table[data-v-be79e288] tbody td{padding:.5em}.activity-details-table[data-v-be79e288] tbody td:first-child{color:var(--textSecondary);white-space:nowrap;vertical-align:top;font-weight:500}.activity-details-table[data-v-be79e288] tbody td:last-child{overflow-wrap:break-word;white-space:pre-wrap}.activity-table-value-info[data-v-d0f8eda4] thead th,.activity-table-value-info[data-v-d0f8eda4] tbody td{padding:.5em}.activity-table-value-info[data-v-d0f8eda4] tbody td{overflow-wrap:break-word;white-space:pre-wrap}.share-picker-field[data-v-97464281]{align-items:stretch;gap:.35rem;max-width:100%;display:flex}.unified-share-picker[data-v-97464281]{overflow-wrap:break-word;box-sizing:border-box;flex:1;justify-content:flex-start;align-items:center;min-width:0;display:flex}.share-clear[data-v-97464281]{min-width:unset;flex-shrink:0;padding:.35rem}.share-clear .material-symbols[data-v-97464281]{font-size:1.1rem}.activity-viewer[data-v-2d793054]{max-width:1200px;margin-left:auto;margin-right:auto;padding:1em}.activity-scope-card[data-v-2d793054]{margin-bottom:1rem}.activity-scope-select[data-v-2d793054]{width:100%;padding:.65rem .85rem;font-size:1rem}.path-filter-mode[data-v-2d793054]{flex-wrap:wrap;gap:1rem;margin-bottom:.75rem;display:flex}.mode-option[data-v-2d793054]{cursor:pointer;align-items:center;gap:.35rem;font-size:.9rem;display:flex}.path-filter-picker[data-v-2d793054]{margin-top:.75rem}.path-filter-picker[data-v-2d793054] .unified-path-picker{max-width:100%}.path-filter-glob[data-v-2d793054]{margin-top:.75rem}.glob-fields[data-v-2d793054]{flex-wrap:wrap;gap:.75rem;display:flex}.glob-field[data-v-2d793054]{flex:1;min-width:10rem}.glob-field-wide[data-v-2d793054]{flex:2;min-width:14rem}.share-hash-field[data-v-2d793054]{margin-top:.75rem}.activity-viewer-results[data-v-2d793054]{margin-top:1.25rem;margin-bottom:2em}.results-table[data-v-2d793054]{min-width:0}.results-table-scroll[data-v-2d793054]{max-width:100%}.results-table-scroll[data-v-2d793054] .settings-table{table-layout:fixed;width:100%}.results-table-scroll[data-v-2d793054] .settings-table tbody td{max-width:0;overflow:hidden}@media (width<=768px){.results-table-scroll[data-v-2d793054]{width:100%;max-width:1000px;overflow-x:auto}.results-table-scroll[data-v-2d793054] .settings-table{table-layout:auto;width:max-content;min-width:100%;max-width:1000px}.results-table-scroll[data-v-2d793054] .settings-table thead th,.results-table-scroll[data-v-2d793054] .settings-table tbody td{white-space:nowrap;max-width:none}}.results-table-scroll[data-v-2d793054] .table-value-cell{min-width:0;max-width:100%;display:block;overflow:hidden}.results-table-scroll[data-v-2d793054] .table-value-badge,.results-table-scroll[data-v-2d793054] .detail-badge{box-sizing:border-box;vertical-align:top;border:1px solid var(--divider);white-space:nowrap;text-overflow:ellipsis;min-width:0;max-width:100%;color:var(--textPrimary,inherit);background:0 0;padding:.12rem .5rem;font-size:.8em;display:inline-block;overflow:hidden}.results-table-scroll[data-v-2d793054] .details-muted{color:var(--textSecondary);font-size:.9em}.results-stats[data-v-2d793054]{color:var(--textSecondary);flex-wrap:wrap;align-items:baseline;gap:.75rem 2rem;margin-bottom:.75rem;font-size:.95rem;display:flex}.results-chart[data-v-2d793054]{margin-top:.25rem}.chart-interval-note[data-v-2d793054]{color:var(--textSecondary);margin:0 0 .75rem;font-size:.9rem}.results-empty[data-v-2d793054]{text-align:center;padding:3rem 1.5rem 4rem}.results-empty-hint[data-v-2d793054]{color:var(--textSecondary);margin:0;font-size:.95rem}.results-empty .lonely-message[data-v-2d793054]{margin-bottom:.5rem}.config-grid[data-v-2d793054]{grid-template-columns:repeat(auto-fill,minmax(12rem,1fr));align-items:end;gap:1rem;display:grid}.config-grid.mobile[data-v-2d793054]{grid-template-columns:1fr}.config-field[data-v-2d793054]{min-width:0}.config-field h3[data-v-2d793054]{text-align:center;margin:0 0 .5rem;font-size:.95rem;font-weight:600}.config-field-wide[data-v-2d793054]{grid-column:1/-1}.custom-range[data-v-2d793054]{flex-wrap:wrap;grid-column:1/-1;gap:.5rem;display:flex}.custom-range .input[data-v-2d793054]{flex:1;min-width:10rem}.config-actions[data-v-2d793054]{flex-wrap:wrap;grid-column:1/-1;align-items:center;gap:.5rem;margin-top:.5rem;display:flex}.button[data-v-2d793054]{align-items:center;gap:.5rem;display:flex}.button .material-symbols[data-v-2d793054]{font-size:1.2rem}.pagination[data-v-2d793054]{justify-content:center;align-items:center;gap:1rem;margin-top:1rem;display:flex}.page-label[data-v-2d793054]{opacity:.85}.details-cell-wrap[data-v-2d793054]{min-width:0;max-width:100%;overflow:hidden}.details-badges[data-v-2d793054]{flex-wrap:nowrap;gap:.35rem;min-width:0;max-width:100%;display:flex;overflow:hidden}.results-table-scroll[data-v-2d793054] .details-badges .detail-badge{vertical-align:top;flex:1 1 0;min-width:0;max-width:100%;display:inline-block}.details-restriLine truncated
@@ -0,0 +1 @@
.settings-number-input[data-v-f3e670fc],.settings-number-input>div[data-v-f3e670fc]{align-items:center;gap:1em;display:flex}.settings-number-input[data-v-f3e670fc]{flex-wrap:wrap;justify-content:space-between}.range-value[data-v-f3e670fc]{text-align:center;min-width:4ch}.advanced-search-root[data-v-f3e670fc]{flex-direction:column;flex:1;align-self:stretch;width:100%;min-height:0;display:flex}.advanced-search-toolbar[data-v-f3e670fc]{margin:1em}.advanced-search-card-form[data-v-f3e670fc]{box-sizing:border-box;flex-direction:column;width:100%;display:flex}.advanced-search-config-row[data-v-f3e670fc]{box-sizing:border-box;flex-wrap:wrap;align-items:flex-start;gap:1.25rem;width:100%;display:flex}.config-primary[data-v-f3e670fc],.config-secondary[data-v-f3e670fc]{box-sizing:border-box;flex:min(500px,100%);min-width:min(500px,100%);max-width:100%}.config-secondary[data-v-f3e670fc]{flex-direction:column;gap:1rem;display:flex}.advanced-search-options-section[data-v-f3e670fc]{box-sizing:border-box;width:100%}.advanced-search-options-section[data-v-f3e670fc] .settings-group{width:100%;max-width:100%}.as-section-label[data-v-f3e670fc]{color:var(--textSecondary);text-transform:uppercase;letter-spacing:.02em;margin-bottom:.35rem;font-size:.8125rem;font-weight:600}.term-join-divider[data-v-f3e670fc]{box-sizing:border-box;justify-content:center;align-items:center;gap:.2em;width:fit-content;max-width:100%;margin:.25em auto;display:flex}.term-join-line[data-v-f3e670fc]{background-color:var(--divider,#aaa);flex:none;align-self:center;width:1.65em;height:1px}.term-join-text[data-v-f3e670fc]{color:var(--textSecondary);-webkit-user-select:none;user-select:none;flex-shrink:0;font-size:.8125rem;font-weight:600;line-height:1.2}.advanced-search-inline-header[data-v-f3e670fc]{flex-shrink:0;width:100%}.advanced-search-sticky-header[data-v-f3e670fc]{z-index:999;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);padding:.5em 0;transition:transform .25s,opacity .25s;position:sticky;top:0}.advanced-search-sticky-header.header-hidden[data-v-f3e670fc]{opacity:0;pointer-events:none;transform:translateY(-100%)}.search-term-row[data-v-f3e670fc]{align-items:stretch;gap:0;display:flex}.flex-grow-input[data-v-f3e670fc]{flex:1;min-width:0}.add-term-button[data-v-f3e670fc]{margin:.35rem 0 .75rem}.advanced-search-switch[data-v-f3e670fc]{margin-top:.25rem}.source-dropdown-select[data-v-f3e670fc]{width:100%}.scope-picker[data-v-f3e670fc]{margin-top:.35rem}.advanced-search-actions-row[data-v-f3e670fc]{flex-wrap:wrap;align-items:center;gap:.75rem;width:100%;margin-top:auto;display:flex}.search-submit[data-v-f3e670fc]{margin-left:auto}@media (width<=559px){.advanced-search-actions-row .search-submit[data-v-f3e670fc]{flex:100%;width:100%;margin-left:0}}.advanced-search-results-plane[data-v-f3e670fc]{flex-direction:column;flex:1;width:100%;min-height:min(65vh,800px);display:flex}.results-loading[data-v-f3e670fc]{justify-content:center;align-items:center;gap:.75em;padding:2rem;display:flex}.padded-message[data-v-f3e670fc]{padding:1.5rem}.error-message[data-v-f3e670fc]{color:var(--error,#c00)}.empty-state-message[data-v-f3e670fc]{text-align:center;color:var(--textSecondary)}.spin[data-v-f3e670fc]{animation:1s linear infinite rotate-f3e670fc}@keyframes rotate-f3e670fc{to{transform:rotate(360deg)}}.advanced-search-listing-results .listing-items[data-v-f3e670fc]{position:relative}.advanced-search-listing-results[data-v-f3e670fc]{margin:0 1em 1em}.advanced-search-inner[data-v-f3e670fc]{margin-top:.5em}.advanced-search-options-section[data-v-f3e670fc] .constraints{box-sizing:border-box;width:calc(100% - 2em);margin-left:1em;margin-right:1em}
@@ -0,0 +1 @@
.viewer-background[data-v-47d3581c]{box-sizing:border-box;background-color:#f0f2f5;width:100%;height:100%;padding:2em;overflow:hidden auto}.docx-page[data-v-47d3581c]{box-sizing:border-box;color:#000;overflow-wrap:anywhere;background:#fff;width:min(8.5in,100%);min-height:11in;margin:0 auto;padding:clamp(1em,8vw,1in);box-shadow:0 0 10px #00000026}.docx-page[data-v-47d3581c] img,.docx-page[data-v-47d3581c] svg,.docx-page[data-v-47d3581c] video{max-width:100%;height:auto}.docx-page[data-v-47d3581c] table{border-collapse:collapse;max-width:100%;margin:1em 0;display:block;overflow-x:auto}.docx-page[data-v-47d3581c] td,.docx-page[data-v-47d3581c] th{vertical-align:top;text-align:left;border:1px solid #000;padding:.4em .7em}.docx-page[data-v-47d3581c] th{background-color:#f3f4f6;font-weight:600}.docx-page[data-v-47d3581c] td>p,.docx-page[data-v-47d3581c] th>p{margin:0}.docx-page[data-v-47d3581c] pre{max-width:100%;overflow-x:auto}.status-text[data-v-47d3581c]{text-align:center;color:#333;padding:3em;font-family:sans-serif;font-size:1.2em}.status-text.error[data-v-47d3581c]{color:var(--red)}@media (width<=8.5in){.viewer-background[data-v-47d3581c]{padding:0}.docx-page[data-v-47d3581c]{min-height:100%;box-shadow:none}}
@@ -0,0 +1 @@
.duplicate-finder[data-v-83e23830]{max-width:1200px;margin-left:auto;margin-right:auto;padding:1em}.duplicate-finder-results[data-v-83e23830]{margin-bottom:2em}.button[data-v-83e23830]{align-items:center;gap:.5rem;margin-top:1rem;display:flex}.button .material-symbols[data-v-83e23830]{font-size:1.2rem}.error-message[data-v-83e23830]{color:var(--dark-red);border:1px solid color-mix(in srgb, var(--red), transparent 60%);background:#fee;border-radius:4px;margin-bottom:1rem;padding:1rem}.hint[data-v-83e23830]{color:var(--textSecondary);margin-top:.25rem;margin-bottom:1rem;font-size:.85rem}.stats[data-v-83e23830]{background:var(--surfaceSecondary);border-radius:4px;gap:2rem;margin-bottom:1.5rem;padding:1rem;display:flex}.duplicate-groups[data-v-83e23830]{flex-direction:column;gap:1.5rem;display:flex}.duplicate-group[data-v-83e23830]{border:1px solid var(--divider);border-radius:var(--borderRadius);overflow:hidden}.group-header[data-v-83e23830]{background:var(--surfaceSecondary);border-radius:.5em;align-items:center;gap:1rem;padding:1rem;display:flex}.group-title[data-v-83e23830]{font-size:1rem;font-weight:600}.group-size[data-v-83e23830]{color:var(--textSecondary);font-size:.9rem}.wasted-space[data-v-83e23830]{color:var(--icon-red);margin-left:auto;font-size:.9rem;font-weight:600}.group-files[data-v-83e23830]{flex-direction:column;padding:0;display:flex}.file-item-wrapper[data-v-83e23830]{border:1px solid #0000001a;border-top:0;border-radius:0;align-items:center;gap:.5rem;padding:.5em;display:flex}.file-item-wrapper[data-v-83e23830]:first-child{border-top:1px solid #0000001a}.file-item-wrapper.deleted[data-v-83e23830]{opacity:.5;background:var(--surfaceSecondary)}.file-item-wrapper.failed[data-v-83e23830]{border-left:3px solid var(--icon-red)}.file-item-content[data-v-83e23830]{flex-direction:column;flex:1;gap:.25rem;display:flex}.group-files .listing-item[data-v-83e23830]{cursor:pointer;width:100%;margin:.25em}.group-files .listing-item.activebutton[data-v-83e23830]{background:var(--primaryColor)!important;color:#fff!important}.path-text[data-v-83e23830]{white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:var(--textSecondary)!important;font-size:.75rem!important}.empty-state[data-v-83e23830]{text-align:center;color:var(--textSecondary);padding:4rem 2rem}.empty-state .material-symbols[data-v-83e23830]{opacity:.3;margin-bottom:1rem;font-size:4rem}.empty-state p[data-v-83e23830]{font-size:1.1rem}.spin[data-v-83e23830]{animation:1s linear infinite spin-83e23830}@keyframes spin-83e23830{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media (width<=768px){.duplicate-finder[data-v-83e23830]{padding:1rem}.stats[data-v-83e23830]{flex-direction:column;gap:.5rem}.group-header[data-v-83e23830]{flex-wrap:wrap}.wasted-space[data-v-83e23830]{width:100%;margin-left:0}}
@@ -0,0 +1 @@
.editor-toolbar[data-v-83c0efe7]{background:var(--background);border-bottom:1px solid var(--alt-background);scrollbar-width:none;flex-shrink:0;align-items:center;gap:.15em;padding:.35em 0;display:flex;overflow:auto hidden}.editor-toolbar-btn[data-v-83c0efe7]{border-radius:var(--borderRadius);width:2em;height:2em;color:var(--textPrimary);cursor:pointer;background:0 0;border:none;flex-shrink:0;justify-content:center;align-items:center;transition:opacity .15s;display:flex;position:relative}.md-toolbar-group[data-v-83c0efe7]{flex-shrink:0;display:flex}.editor-toolbar-sticky[data-v-83c0efe7]{z-index:1;background:var(--background);border-right:1px solid var(--alt-background);flex-shrink:0;align-items:center;margin:-.35em 0;padding:.25em;display:flex;position:sticky;left:0}.editor-toolbar-sticky--right[data-v-83c0efe7]{border-left:1px solid var(--alt-background);border-right:none;padding-right:calc(.25em + 1px);right:-1px}.toolbar-color-indicator[data-v-83c0efe7]{background-color:var(--alt-background);width:1.4em;height:.5em;box-shadow:inset 0 0 0 1px var(--alt-background);cursor:pointer;border-radius:.2em;position:absolute;bottom:.15em;left:50%;transform:translate(-50%)}.editor-toolbar-btn .material-symbols.toolbar-color-glyph[data-v-83c0efe7]{font-size:1em;transform:translateY(-.2em)}.toolbar-color-indicator--empty[data-v-83c0efe7]{opacity:0;background-color:#0000}.color-input[data-v-83c0efe7]{opacity:0;cursor:pointer;border:none;width:100%;height:100%;margin:0;padding:0;position:absolute;inset:0}.editor-toolbar-btn[data-v-83c0efe7]:disabled{opacity:.35}.editor-toolbar-btn .material-symbols[data-v-83c0efe7]{font-size:1.2em}.toolbar-icon-enter-active[data-v-83c0efe7],.toolbar-icon-leave-active[data-v-83c0efe7]{transition:opacity .12s,transform .12s}.toolbar-icon-enter-from[data-v-83c0efe7],.toolbar-icon-leave-to[data-v-83c0efe7]{opacity:0;transform:scale(.6)}.editor-toolbar-menu[data-v-83c0efe7]{z-index:9999;margin:0;padding:.25em .25em .65em;list-style:none;position:fixed}.editor-toolbar-menu--icon-menu[data-v-83c0efe7]{gap:.15em;padding-bottom:.5em;display:flex}.editor-toolbar-menu-btn[data-v-83c0efe7]{justify-content:flex-start;gap:.5em;width:100%;height:auto;padding:.5em .75em}.editor-toolbar-menu-btn .material-symbols[data-v-83c0efe7]{font-size:1.1em}.expand-enter-active[data-v-83c0efe7],.expand-leave-active[data-v-83c0efe7]{overflow:hidden}.split-divider[data-v-351bb28e]{cursor:col-resize;background-color:#0000;flex:0 0 5px;position:relative}.split-divider[data-v-351bb28e]:before{content:"";background-color:var(--alt-background);width:1px;position:absolute;top:0;bottom:0;left:2px}.split-divider[data-v-351bb28e]:hover:before,.split-divider.resizing[data-v-351bb28e]:before{background-color:var(--primaryColor)}.split-preview-pane[data-v-351bb28e]{flex:none;min-width:0;height:100%;position:relative;overflow:hidden}.split-preview-scrollbar[data-v-351bb28e]{scrollbar-width:none;width:100%;height:100%;overflow-y:auto}.split-preview-scrollbar[data-v-351bb28e]::-webkit-scrollbar{width:0;display:none}#editor-root[data-v-e20ff5ce]{height:100%}#editor-root.split-active[data-v-e20ff5ce]{width:100%;height:100%;display:flex}#editor-container[data-v-e20ff5ce]{flex-direction:column;width:100%;height:100%;display:flex;position:relative;overflow:hidden}#editor-root.split-active #editor-container[data-v-e20ff5ce]{flex:none;min-width:0;position:relative}#editor-container #editor[data-v-e20ff5ce]{flex:1;min-height:0}#editor-container.viewer-mode[data-v-e20ff5ce]{position:absolute;inset:0}.ace_editor{--ace-dark-bg:#151515;--ace-popup-bg:#1a1a1a;--ace-control-bg:#232323;font-size:14px;line-height:1.4;z-index:1!important}.ace_content{-webkit-user-select:text;user-select:text}.ace_editor .ace_selection{background-color:color-mix(in srgb, var(--primaryColor) 25%, transparent)!important}.ace_editor .ace_selection.ace_start{box-shadow:0 0 3px 0 color-mix(in srgb, var(--primaryColor) 40%, transparent)!important}.ace_editor .ace_gutter-active-line{background-color:color-mix(in srgb, var(--primaryColor) 20%, transparent)!important;color:var(--primaryColor)!important;font-weight:700!important}.ace_editor .ace_indent-guide{border-right:1px solid color-mix(in srgb, var(--primaryColor) 50%, transparent)!important;opacity:1!important;z-index:5!important}.ace_editor .ace_indent-guide-active{border-right:1px solid color-mix(in srgb, var(--primaryColor) 75%, transparent)!important}.ace_editor.ace_dark{background-color:var(--ace-dark-bg)!important}.ace_editor.ace_dark .ace_marker-layer .ace_active-line{background:color-mix(in srgb, var(--surfaceSecondary) 35%, transparent)}#editor-root.split-active .ace_scrollbar{scrollbar-width:none}#editor-root.split-active .ace_scrollbar::-webkit-scrollbar{display:none}.ace_editor .ace_search{color:var(--textPrimary);border-color:var(--divider)}.ace_editor.ace_dark .ace_search{background-color:var(--ace-popup-bg)}.ace_editor .ace_search_field{color:var(--textPrimary);border:1px solid var(--divider)}.ace_editorLine truncated
@@ -0,0 +1 @@
.epub-container[data-v-2f4b46b5]{background-color:var(--background);z-index:1000;width:100%;height:100%;position:relative}.loading-indicator[data-v-2f4b46b5]{height:100%;color:var(--textSecondary);justify-content:center;align-items:center;font-size:1.2em;display:flex}#viewer[data-v-2f4b46b5]{visibility:hidden;width:100%;height:100%}#viewer.ready[data-v-2f4b46b5]{visibility:visible}.navigation[data-v-2f4b46b5]{z-index:1001;background-color:var(--surfacePrimary);border-radius:var(--borderRadius);align-items:center;gap:1em;padding:.5em;display:flex;position:absolute;bottom:1em;left:50%;transform:translate(-50%);box-shadow:0 2px 10px #0000001a}.nav-button[data-v-2f4b46b5]{border-radius:var(--borderRadius);color:var(--textPrimary);cursor:pointer;background-color:#0000;padding:.25em 1em;font-size:1.5em;transition:background-color .2s}.nav-button[data-v-2f4b46b5]:hover{background-color:var(--hoverOverlay);color:var(--primaryColor)}
@@ -0,0 +1 @@
.image-ex-container{overscroll-behavior:none;touch-action:none;justify-content:center;width:100%;max-width:100%;height:100%;min-height:0;max-height:100%;display:flex;position:relative;overflow:hidden}.image-ex-img{object-fit:contain;max-width:100%;max-height:100%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.image-loading-overlay{z-index:10;justify-content:center;align-items:center;display:flex;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}
@@ -0,0 +1 @@
.file-watcher[data-v-a9d38c6d]{max-width:1200px;margin-left:auto;margin-right:auto;padding:1em}.file-watcher-config[data-v-a9d38c6d]{margin-bottom:2em}.searchContext[data-v-a9d38c6d]{margin-bottom:0!important}.config-row[data-v-a9d38c6d]{flex-wrap:nowrap;align-items:center;gap:.75rem;min-width:0;display:flex;padding:1em!important}.config-row.mobile[data-v-a9d38c6d]{flex-direction:column;align-items:stretch}.config-row-second[data-v-a9d38c6d]{flex:none;align-items:center;gap:.75rem;display:flex}.config-row-second.mobile[data-v-a9d38c6d]{grid-template-columns:minmax(0,1fr) auto auto;align-items:center;gap:.5rem;width:100%;display:grid}.config-item[data-v-a9d38c6d]{align-items:center;display:flex}.config-item.lines-input[data-v-a9d38c6d]{flex-direction:row;align-items:center;gap:.5rem}.lines-label[data-v-a9d38c6d]{color:var(--textSecondary);white-space:nowrap;flex-shrink:0;margin:0;font-size:.875rem}.file-picker-button[data-v-a9d38c6d]{white-space:nowrap;text-overflow:ellipsis;align-items:center;gap:.5rem;width:100%;display:flex;overflow:hidden}.file-picker[data-v-a9d38c6d]{flex:auto;min-width:0}.config-item.interval-select[data-v-a9d38c6d]{min-width:0}.config-row.mobile .file-picker[data-v-a9d38c6d]{width:100%}.file-watcher-output[data-v-a9d38c6d]{width:100%;padding:0!important}.status-indicator[data-v-a9d38c6d]{color:var(--textSecondary);align-items:center;gap:.5rem;font-size:.9rem;display:flex}.terminal-header[data-v-a9d38c6d]{background:var(--surfacePrimary);border-bottom:1px solid var(--divider);border-bottom-right-radius:0;border-bottom-left-radius:0;flex-direction:column;gap:.5rem;margin-bottom:0;padding:.75rem 1rem;font-size:.9rem;display:flex}.header-row[data-v-a9d38c6d]{justify-content:space-between;align-items:center;display:flex;position:relative}.header-row-first[data-v-a9d38c6d]{min-height:1.5rem}.header-row-second[data-v-a9d38c6d]{min-height:1.5rem;padding-top:.25rem}.header-row-second-mobile[data-v-a9d38c6d]{justify-content:center;min-height:1.5rem;padding-top:.25rem}.header-row-third-mobile[data-v-a9d38c6d]{min-height:1.5rem;padding-top:.25rem}.header-center-mobile[data-v-a9d38c6d]{justify-content:center;align-items:center;gap:.5rem;width:100%;display:flex}.header-left[data-v-a9d38c6d],.header-center[data-v-a9d38c6d],.header-right[data-v-a9d38c6d]{align-items:center;gap:.5rem;display:flex}.header-center[data-v-a9d38c6d]{white-space:nowrap;position:absolute;left:50%;transform:translate(-50%)}.header-right[data-v-a9d38c6d]{justify-content:flex-end;margin-left:auto}.header-filename[data-v-a9d38c6d]{color:var(--textPrimary);font-weight:700}.header-filesize[data-v-a9d38c6d]{color:var(--textSecondary)}.header-label[data-v-a9d38c6d]{color:var(--textSecondary);font-weight:500}.header-value[data-v-a9d38c6d]{color:var(--textPrimary);font-weight:600}.header-value.latency-good[data-v-a9d38c6d]{color:var(--icon-green)}.header-value.latency-ok[data-v-a9d38c6d]{color:var(--icon-deep-orange)}.header-value.latency-slow[data-v-a9d38c6d]{color:var(--icon-red)}.header-value.latency-inactive[data-v-a9d38c6d]{color:var(--textSecondary)}.status-dot[data-v-a9d38c6d]{background:var(--icon-green);border-radius:50%;width:8px;height:8px;animation:2s infinite pulse-a9d38c6d}@keyframes pulse-a9d38c6d{0%,to{opacity:1}50%{opacity:.5}}.terminal-output[data-v-a9d38c6d]{background:var(--iconBackground);min-height:100px;color:var(--textPrimary);border-top-left-radius:0;border-top-right-radius:0;padding:1rem;font-family:Courier New,Consolas,Monaco,monospace;font-size:14px;overflow:auto}.terminal-line[data-v-a9d38c6d]{white-space:nowrap;align-items:flex-start;gap:.5rem;margin-bottom:2px;line-height:1.5;display:flex}.terminal-text[data-v-a9d38c6d]{white-space:nowrap}.empty-state[data-v-a9d38c6d]{text-align:center;color:var(--textSecondary);padding:4rem 2rem}.empty-state .material-symbols[data-v-a9d38c6d]{opacity:.3;margin-bottom:1rem;font-size:4rem}.empty-state p[data-v-a9d38c6d]{font-size:1.1rem}@media (width<=768px){.file-watcher[data-v-a9d38c6d]{padding:1rem}.config-row[data-v-a9d38c6d]{flex-direction:column;align-items:stretch;gap:.5rem}.config-item.file-picker[data-v-a9d38c6d]{width:100%!important}.config-item.file-picker .file-picker-button[data-v-a9d38c6d]{width:100%}.config-row-second[data-v-a9d38c6d]{grid-template-columns:minmax(0,1fr) auto auto;gap:.5rem;width:100%;display:grid}.config-item.interval-select[data-v-a9d38c6d],.config-item.lines-input[data-v-a9d38c6d],.config-item.play-button[data-v-a9d38c6d]{flex:1;align-items:center;width:auto!important}.terminal-output[data-v-a9d38c6d]{min-height:100px;font-size:12px}}@media (width<=1100px){.config-row[data-v-a9d38c6d]:not(.mobile){flex-direction:column;align-items:stretch;gap:.5rem}.config-row:not(.mobile) .config-row-second[data-v-a9d38c6d]{grid-template-columns:minmax(0,1fr) auto auto;gap:.5rem;width:100%;display:grid}}
@@ -0,0 +1 @@
#markedown-viewer{overflow-wrap:break-word;margin:1em}#markedown-viewer.html-viewer-mode{box-sizing:border-box;flex-direction:column;width:calc(100% - 1em);margin:.5em;display:flex;overflow:hidden}#markedown-viewer .markdown-content-container{background-color:color-mix(in srgb, var(--surfacePrimary) 75%, transparent);border-radius:var(--borderRadius);box-shadow:var(--surfaceElevationShadow);margin:0 auto;padding:1.2em}#markedown-viewer .markdown-content,#markedown-viewer .html-content{width:100%}#markedown-viewer .html-content{box-sizing:border-box;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark;background:#fff;border:none;min-height:24em}@media (prefers-color-scheme:dark){#markedown-viewer .html-content{--lightningcss-light: ;--lightningcss-dark:initial}}#markedown-viewer.html-viewer-mode .html-content{flex:auto;width:100%;height:100%;min-height:0}#markedown-viewer .html-content img{max-width:100%;height:auto}#markedown-viewer .spacer{pointer-events:none;width:100%}#markedown-viewer .markdown-content-container .code-block-wrapper{background-color:var(--background);border-radius:.5em;max-width:100%;margin:1em 0;font-family:SFMono-Regular,Monaco,Inconsolata,Liberation Mono,Courier New,monospace;font-size:.85em;line-height:1.45;display:flex;position:relative;overflow:hidden}#markedown-viewer .markdown-content code:not(pre code){background-color:var(--background);border-radius:.5em;padding:.25em .4em;font-family:SFMono-Regular,Monaco,Inconsolata,Liberation Mono,Courier New,monospace;font-size:.85em}#markedown-viewer .markdown-content kbd{background-color:var(--background);border:1px solid var(--divider);box-shadow:inset 0 -2px 0 var(--divider);border-radius:.35em;padding:.1em .5em;font-family:SFMono-Regular,Monaco,Inconsolata,Liberation Mono,Courier New,monospace;font-size:.85em}#markedown-viewer .markdown-content-container .copy-code-button{color:var(--primaryColor);background:var(--background);border-radius:.45em;justify-content:center;align-items:center;padding:.2em .4em;font-size:.8em;transition:background .2s,color .2s;display:flex;position:absolute;top:.4em;right:.3em}#markedown-viewer .markdown-content-container .line-numbers{-webkit-touch-callout:none;-webkit-user-select:none;user-select:none;background-color:var(--background);border-right:1px solid var(--divider);text-align:right;color:var(--textSecondary);flex-shrink:0;min-width:2em;padding:.625em .5em .625em .75em}#markedown-viewer .markdown-content-container .line-number{white-space:nowrap;cursor:pointer;height:1.45em;line-height:1.45;transition:background-color .2s,color .2s;display:block}#markedown-viewer .markdown-content-container .line-number:hover{color:var(--primaryColor);background-color:#0000}#markedown-viewer .markdown-content-container .line-number.active{background-color:color-mix(in srgb, var(--primaryColor) 30%, transparent);color:var(--primaryColor);border-radius:.25em}#markedown-viewer .markdown-content-container .code-line{white-space:pre;min-height:1.45em;line-height:1.45;transition:background-color .2s;display:block}#markedown-viewer .markdown-content-container .code-line.highlighted{background-color:color-mix(in srgb, var(--primaryColor) 20%, transparent)}#markedown-viewer .markdown-content-container .code-content{flex:1;max-width:100%;overflow-x:auto}#markedown-viewer .markdown-content-container .code-content pre{font-family:inherit;font-size:inherit;border-radius:0;width:100%;margin:0;padding:0;line-height:1.45}#markedown-viewer .markdown-content-container .code-content code{font-family:inherit;font-size:inherit;line-height:inherit;white-space:pre;padding:.65em .5em .5em;display:block}#markedown-viewer .markdown-content-container .code-content pre code,#markedown-viewer .markdown-content-container .code-content pre code br{line-height:1.45}#markedown-viewer .markdown-content a{color:var(--primaryColor);text-decoration:underline}#markedown-viewer .markdown-content-container .code-content a{color:var(--primaryColor);font-weight:500;text-decoration:underline}#markedown-viewer .markdown-content h1,#markedown-viewer .markdown-content h2,#markedown-viewer .markdown-content h3,#markedown-viewer .markdown-content h4,#markedown-viewer .markdown-content h5,#markedown-viewer .markdown-content h6{text-wrap:balance;margin:1.4em 0 .6em;font-weight:600;line-height:1.25}#markedown-viewer .markdown-content h1{border-bottom:1px solid var(--divider);margin-top:0;padding-bottom:.3em;font-size:1.8em}#markedown-viewer .markdown-content h2{border-bottom:1px solid var(--divider);padding-bottom:.3em;font-size:1.45em}#markedown-viewer .markdown-content h3{font-size:1.2em}#markedown-viewer .markdown-content h4{font-size:1em}#markedown-viewer .markdown-content p{text-wrap:pretty;margin-top:0;margin-bottom:.85em;line-height:1.45}#markedown-viewer .markdown-content hr{border:none;border-top:1px solid var(--divider);margin:1.5em 0}#markedown-viewer .markdown-content ul{margin-top:0;margin-bottom:0;padding-bottom:.85em;padding-lefLine truncated
@@ -0,0 +1 @@
a.button[data-v-5a3a53bf]{justify-content:center;align-items:center;width:500px;max-width:100%;margin:auto;display:flex}.controls[data-v-5a3a53bf]{margin:1.5em}.search-box[data-v-5a3a53bf]{margin-bottom:1em;position:relative}.search-box .search-icon[data-v-5a3a53bf]{position:absolute;top:.25em;left:.5em}.clear-button[data-v-5a3a53bf]{position:absolute;top:50%;right:.5em;transform:translateY(-50%);min-width:auto!important;padding:.25em!important}.results-info[data-v-5a3a53bf]{color:var(--textSecondary);margin-bottom:1em;font-size:.9em}.icon-grid[data-v-5a3a53bf]{grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:.75em;margin-bottom:2em;display:grid}.icon-card[data-v-5a3a53bf]{background:var(--surfaceSecondary);border:1px solid var(--divider);border-radius:var(--borderRadius);flex-direction:column;align-items:center;padding:1em .6em;display:flex;position:relative}.icon-card.custom-icon[data-v-5a3a53bf]{border-color:var(--primaryColor);border-style:dashed}.icon-display[data-v-5a3a53bf]{color:var(--primaryColor);justify-content:center;align-items:center;height:1em;margin-bottom:.4em;font-size:2em;display:flex}.icon-name[data-v-5a3a53bf]{color:var(--textSecondary);text-align:center;overflow-wrap:break-word;font-size:.75em;line-height:1.2}.custom-badge[data-v-5a3a53bf]{background:var(--primaryColor);color:#fff;text-transform:uppercase;border-radius:.5em;padding:.25em .6em;font-size:.7em;font-weight:600;position:absolute;top:.5em;right:.5em}.empty-state[data-v-5a3a53bf]{text-align:center;color:var(--textSecondary);padding:4em 2em}.empty-state .material-symbols[data-v-5a3a53bf]{opacity:.5;margin-bottom:.5em;font-size:4em}.empty-state p[data-v-5a3a53bf]{margin-bottom:.5em;font-size:1.2em}.empty-hint[data-v-5a3a53bf]{opacity:.7;font-size:.9em}@media (width<=768px){#tool-material-icon-picker[data-v-5a3a53bf]{padding:1em}.icon-grid[data-v-5a3a53bf]{grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:.75em}.icon-card[data-v-5a3a53bf]{padding:1em .5em}.icon-display[data-v-5a3a53bf]{font-size:2em}}
@@ -0,0 +1 @@
.size-viewer[data-v-42452d2e]{max-width:1200px;margin-left:auto;margin-right:auto;padding:1em}.toggle-container[data-v-42452d2e]{padding:1em}.size-viewer-results[data-v-42452d2e]{margin-bottom:2em}.button[data-v-42452d2e]{align-items:center;gap:.5rem;margin-top:1rem;display:flex}.button .material-symbols[data-v-42452d2e]{font-size:1.2rem}.error-message[data-v-42452d2e]{background:color-mix(in srgb, var(--red), transparent 90%);color:var(--icon-red);border:1px solid color-mix(in srgb, var(--red), transparent 60%);border-radius:4px;margin-bottom:1rem;padding:1rem}.stats[data-v-42452d2e]{gap:2rem;margin-bottom:1rem;display:flex}.treemap[data-v-42452d2e]{background:var(--surfacePrimary);border-radius:4px;width:100%;height:600px;margin-bottom:2rem;position:relative;overflow:hidden}.treemap.has-expanded[data-v-42452d2e]{overflow:visible}.treemap-overlay[data-v-42452d2e]{background:var(--overlay);z-index:50;cursor:pointer;position:absolute;inset:0}.treemap-hit-area[data-v-42452d2e]{z-index:200;cursor:pointer;pointer-events:auto;background:0 0;border:none;outline:none;position:absolute}.treemap.has-expanded .treemap-hit-area[data-v-42452d2e]:not(.active){pointer-events:none;z-index:1}.treemap.has-expanded .treemap-hit-area.active[data-v-42452d2e]{z-index:150;pointer-events:none}.treemap-item[data-v-42452d2e]{cursor:pointer;box-sizing:border-box;pointer-events:none;border:1px solid #0000001a;flex-direction:column;justify-content:center;align-items:center;transition:left .4s cubic-bezier(.4,0,.2,1),top .4s cubic-bezier(.4,0,.2,1),width .4s cubic-bezier(.4,0,.2,1),height .4s cubic-bezier(.4,0,.2,1),opacity .4s cubic-bezier(.4,0,.2,1),filter .4s cubic-bezier(.4,0,.2,1);display:flex;position:absolute;overflow:visible}.treemap-item.small-item[data-v-42452d2e]{padding:0}.treemap-item.dimmed[data-v-42452d2e]{opacity:.3;filter:brightness(.5)}.treemap-item.expanded[data-v-42452d2e]{z-index:100;pointer-events:auto;border:2px solid #ffffffe6;box-shadow:0 8px 32px #000000b3;width:50%!important;height:50%!important;top:25%!important;left:25%!important}.item-content[data-v-42452d2e]{text-align:center;color:#fff;text-shadow:1px 1px 2px #00000080;flex-direction:column;justify-content:center;align-items:center;width:100%;padding:.5rem;font-weight:500;transition:opacity .2s;display:flex;overflow:hidden}.treemap-item.expanded .item-content[data-v-42452d2e]{opacity:0;pointer-events:none;transition:opacity .2s}.item-name[data-v-42452d2e]{text-overflow:ellipsis;white-space:nowrap;max-width:100%;margin-bottom:.25rem;font-size:.85rem;overflow:hidden}.item-size[data-v-42452d2e]{opacity:.95;white-space:nowrap;font-size:.75rem}.item-percentage[data-v-42452d2e]{opacity:.85;white-space:nowrap;margin-top:.2rem;font-size:.7rem}.item-expanded[data-v-42452d2e]{opacity:0;pointer-events:none;z-index:1;flex-direction:column;justify-content:center;gap:.4rem;padding:.75rem;transition:opacity .2s,transform .2s;display:flex;position:absolute;inset:0;transform:scale(.9)}.treemap-item.expanded .item-expanded[data-v-42452d2e]{opacity:1;pointer-events:auto;z-index:200;transform:scale(1)}.expanded-field[data-v-42452d2e]{text-align:left;flex-direction:column;gap:.15rem;display:flex}.field-label[data-v-42452d2e]{opacity:.85;text-transform:uppercase;letter-spacing:.05em;color:#fff;text-shadow:1px 1px 2px #00000080;font-size:.65rem;font-weight:600}.field-value[data-v-42452d2e]{color:#fff;text-shadow:1px 1px 2px #00000080;overflow-wrap:break-word;text-overflow:ellipsis;line-clamp:2;-webkit-box-orient:vertical;max-height:3em;font-size:.8rem;font-weight:500;display:-webkit-box;overflow:hidden}.expanded-field:first-child .field-value[data-v-42452d2e]{line-clamp:1;max-height:1.5em}.expanded-field:nth-child(2) .field-value[data-v-42452d2e]{line-clamp:3;max-height:4em}.type-video[data-v-42452d2e]{background:var(--blue)}.type-image[data-v-42452d2e]{background:var(--icon-coral)}.type-audio[data-v-42452d2e]{background:var(--icon-plum)}.type-archive[data-v-42452d2e]{background:var(--icon-tan)}.type-document[data-v-42452d2e]{background:indigo}.type-binary[data-v-42452d2e]{background:var(--divider)}.type-directory[data-v-42452d2e]{background:var(--primaryColor)}.type-other[data-v-42452d2e]{background:var(--light-gray)}.legend[data-v-42452d2e]{flex-wrap:wrap;gap:1rem;margin-top:.5rem;display:flex}.legend-item[data-v-42452d2e]{align-items:center;gap:.5rem;display:flex}.legend-color[data-v-42452d2e]{border-radius:3px;width:20px;height:20px;box-shadow:0 1px 3px #0003}.empty-state[data-v-42452d2e]{text-align:center;color:var(--textSecondary);padding:4rem 2rem}.empty-state .material-symbols[data-v-42452d2e]{opacity:.3;margin-bottom:1rem;font-size:4rem}.empty-state p[data-v-42452d2e]{font-size:1.1rem}.spin[data-v-42452d2e]{animation:1s linear infinite spin-42452d2e}@keyframes spin-42452d2e{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@media (width<=768px){.size-viewer[data-v-42452d2e]{padding:1rem}.treemap[data-v-42452d2e]{height:400px}.item-path[data-v-42452d2e]{font-size:.7Line truncated
@@ -0,0 +1 @@
.threejs-viewer[data-v-a7499d14]{width:100%;height:100%;position:relative;overflow:hidden}.threejs-viewer canvas[data-v-a7499d14]{display:block;position:absolute;top:0;left:0;width:100%!important;height:100%!important}.loading-overlay[data-v-a7499d14],.error-overlay[data-v-a7499d14]{background:var(--background);z-index:10;justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.error-content[data-v-a7499d14]{text-align:center;max-width:500px;padding:2rem}.error-content i[data-v-a7499d14]{color:var(--red);margin-bottom:1rem;font-size:64px}.error-content h3[data-v-a7499d14]{color:var(--textPrimary);margin-bottom:.5rem}.error-content p[data-v-a7499d14]{color:var(--textSecondary)}
@@ -0,0 +1 @@
.wopi-editor[data-v-5e12c31f]{width:100%;height:100vh;position:relative}.wopi-form[data-v-5e12c31f]{display:none}.wopi-frame[data-v-5e12c31f]{border:0;width:100%;height:100%;display:block}.wopi-loading[data-v-5e12c31f]{text-align:center;position:absolute;top:1em;left:0;right:0}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
html{line-height:1.15}body{margin:0}pre,code,kbd,samp{font-family:monospace;font-size:1em}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:100%;line-height:1.15}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;white-space:normal;max-width:100%;padding:0;display:table}progress{vertical-align:baseline}[type=search]{appearance:textfield;outline-offset:-2px}[hidden]{display:none}:root{--blue:#2196f3;--dark-blue:#1e88e5;--red:#f44336;--dark-red:#be2626;--icon-red:#d23b4a;--icon-orange:#ff8c00;--icon-deep-orange:#ff6f00;--icon-yellow:#fdd34b;--icon-green:#4caf50;--icon-blue:#1d99f3;--icon-violet:#9b59b6;--icon-brown:brown;--icon-coral:lightcoral;--icon-plum:plum;--icon-tan:tan;--primaryColor:var(--blue);--light-gray:lightgray;--icon-size:3em;--safe-area-top:env(safe-area-inset-top,0px);--safe-area-left:env(safe-area-inset-left,0px);--safe-area-right:env(safe-area-inset-right,0px);--safe-area-bottom:env(safe-area-inset-bottom,0px);--header-height:calc(4em + var(--safe-area-top));--borderRadius:.85rem;--borderWidth:2px;--panel-bg:color-mix(in srgb, var(--alt-background) 65%, transparent);--panel-blur:blur(12px) invert(.1);--surfaceElevationShadow:0 1px 2px #00000012, 0 2px 4px #0000000f, 0 4px 8px #0000000d;--overlay:#00000080;--hoverOverlay:#0000001a}.dark-mode{--hoverOverlay:#ffffff1f;color:var(--textPrimary)}.border-radius{border-radius:var(--borderRadius)}.button{padding:var(--button-padding-y,.5em) var(--button-padding-x,1em);border-radius:var(--borderRadius);cursor:pointer;background:var(--button-background,var(--primaryColor));color:var(--button-color,white);box-shadow:var(--button-shadow,0 0 5px #0000000d);min-width:var(--button-min-width,2em);min-height:var(--button-min-height,2em);outline:0;justify-content:center;align-items:center;transition:transform .1s,opacity .1s,background-color .1s,box-shadow .2s;display:flex}button{background-color:var(--surfaceSecondary);min-width:var(--button-min-width,2em);min-height:var(--button-min-height,2em);border:none;justify-content:center;align-items:center;padding:.5em;transition:background-color .3s;display:flex}button:disabled{opacity:.5;cursor:not-allowed}.button--block{border-style:unset;width:100%;margin:0 0 .5em;display:block}.button--red{background:var(--red)}.button--blue{background:var(--blue)}.button--flat{color:var(--primaryColor);background:var(--surfaceSecondary);text-transform:uppercase;padding-left:1em;padding-right:1em;box-shadow:0 0}.button--flat.button--red{color:var(--dark-red)}.button--flat.button--grey{color:#6f6f6f}.button--settings-control{border-radius:50px;width:4em;min-width:4em;max-width:4em;height:34px;min-height:34px;padding:0}.button[disabled]{opacity:.5;cursor:not-allowed}:root{--fab-size:50px;--fab-size-small:36px;--fab-icon-size:24px;--fab-edge-offset:16px;--fab-edge-offset-media:20px;--fab-elevation-rest:0 1px 3px 1px #00000026, 0 1px 2px #0000004d;--fab-elevation-hover:0 2px 6px 2px #00000026, 0 1px 2px #0000004d;--fab-elevation-pressed:0 4px 8px 3px #00000026, 0 1px 3px #0000004d;--fab-shadow:var(--fab-elevation-rest);--fab-transition:.2s cubic-bezier(.2, 0, 0, 1);--fab-fade-transition:.4s ease;--fab-safe-inset:calc(var(--fab-size) + 2rem)}.has-fab-bottom-right{margin-bottom:var(--fab-safe-inset)}.input{box-sizing:border-box;border-radius:var(--borderRadius);border:var(--borderWidth) solid var(--surfaceSecondary);background:var(--background);width:100%;min-height:2.5em;color:var(--textPrimary);margin:0;padding:.5em 1em;transition:border-color .2s}.input:hover{border-color:color-mix(in srgb, var(--surfaceSecondary) 70%, var(--textPrimary))}.input:focus{border-color:var(--primaryColor);outline:none}.input:disabled{cursor:not-allowed;color:var(--textSecondary);opacity:.75}.expand-dropdown.input,.expand-dropdown.input:hover,.expand-dropdown.input:focus{box-shadow:none;background:0 0;border:none;padding:0}input:-webkit-autofill{box-shadow:0 0 0 1000px var(--background) inset;-webkit-text-fill-color:var(--textPrimary);border-color:unset}input:-webkit-autofill:hover{box-shadow:0 0 0 1000px var(--background) inset;-webkit-text-fill-color:var(--textPrimary);border-color:unset}input:-webkit-autofill:focus{box-shadow:0 0 0 1000px var(--background) inset;-webkit-text-fill-color:var(--textPrimary);border-color:unset}input:-webkit-autofill:active{box-shadow:0 0 0 1000px var(--background) inset;-webkit-text-fill-color:var(--textPrimary);border-color:unset}input[type=range]{accent-color:var(--primaryColor)}input.sizeInput:disabled{cursor:not-allowed}.sizeInputWrapper{box-sizing:border-box;border-radius:var(--borderRadius);background-color:var(--background);border:var(--borderWidth) solid var(--surfaceSecondary);color:var(--textPrimary);justify-content:center;align-items:center;margin:.5em;padding:.25em .5em;display:flex;box-shadow:0 1pxLine truncated
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
.audio-side-panel[data-v-788790b4]{background:color-mix(in srgb, var(--alt-background) 20%, var(--background) 20%);border-radius:var(--borderRadius);max-height:65vh;box-shadow:var(--surfaceElevationShadow);flex-direction:column;display:flex;overflow:hidden}.panel-tabs[data-v-788790b4]{border-bottom:1px solid var(--divider);padding:.5em}.tab-container[data-v-788790b4]{gap:.5em;display:flex;position:relative}.tab-btn[data-v-788790b4]{color:var(--textSecondary);border-radius:var(--borderRadius);cursor:pointer;z-index:1;-webkit-user-select:none;user-select:none;white-space:nowrap;background:0 0;border:none;flex:1;justify-content:center;align-items:center;gap:.3em;width:100%;min-width:0;padding:.5em;font-size:.9rem;transition:color .2s,transform .2s;display:flex;position:relative}.tab-btn>i[data-v-788790b4],.tab-btn>span+span[data-v-788790b4]{flex-shrink:0}.tab-btn>span[data-v-788790b4]:first-of-type{text-overflow:ellipsis;min-width:0;overflow:hidden}.tab-btn.active[data-v-788790b4]{color:#fff}.tab-indicator[data-v-788790b4]{background:var(--primaryColor);border-radius:var(--borderRadius);z-index:0;pointer-events:none;transition:left .35s cubic-bezier(.25,.8,.25,1),width .35s cubic-bezier(.25,.8,.25,1);position:absolute;top:0;bottom:0;left:0}.panel-content[data-v-788790b4]{flex-direction:column;flex:1;display:flex;overflow:hidden}.tab-queue[data-v-788790b4],.tab-lyrics[data-v-788790b4],.tab-visualizer[data-v-788790b4]{height:100%}.tab-queue[data-v-788790b4]{flex-direction:column;display:flex}.tab-lyrics[data-v-788790b4]{flex-direction:column;display:flex;position:relative}.lyrics-scrollable[data-v-788790b4]{flex:1;min-height:0;overflow-y:auto}.tab-queue[data-v-788790b4] .card-content{flex:1;margin:0;padding:.5em;overflow-y:auto}.tab-queue[data-v-788790b4] .card-actions{flex-shrink:0;justify-content:flex-end;padding:.25em;display:flex}.tab-queue[data-v-788790b4] .card-actions .button--flat{background:0 0}.lyrics-list[data-v-788790b4]{text-align:center;color:var(--textPrimary);padding-top:1em;padding-bottom:1em}.lyrics-meta-header[data-v-788790b4]{opacity:.55;letter-spacing:.02em;text-transform:uppercase;cursor:default;-webkit-user-select:none;user-select:none;margin:0;padding:0 0 .6em;font-size:.8rem;font-weight:600}.lyric-line[data-v-788790b4]{opacity:.6;cursor:pointer;transform-origin:50%;padding:.5em 0;font-size:1.15rem;transition:opacity .25s,color .25s,font-size .25s,transform .25s;transform:scale(1)}.lyric-line[data-v-788790b4]:hover{opacity:1}.lyric-line.active[data-v-788790b4]{opacity:1;color:var(--primaryColor);font-size:1.33rem;font-weight:700;animation:.3s lyric-line-in-788790b4}@keyframes lyric-line-in-788790b4{0%{transform:scale(.98)}to{transform:scale(1)}}.lyric-word[data-v-788790b4]{opacity:.4;transition:opacity .35s,transform .35s cubic-bezier(.25,.8,.25,1);display:inline-block;transform:scale(1)}.lyric-word.sung[data-v-788790b4]{opacity:1}.lyric-word.current[data-v-788790b4]{opacity:1;transform:scale(1.05)}.no-lyrics[data-v-788790b4]{height:100%;color:var(--textSecondary);flex-direction:column;justify-content:center;align-items:center;gap:.5em;display:flex}.no-lyrics i[data-v-788790b4]{opacity:.5;font-size:3rem}.lyric-line.no-seek[data-v-788790b4]{cursor:default}.tab-btn[data-v-788790b4]:hover:not(.active){color:var(--primaryColor);transform:scale(1.02)}.tab-visualizer[data-v-788790b4]{box-sizing:border-box;justify-content:center;align-items:center;height:100%;padding:.5em;display:flex;position:relative}.visualizer-canvas[data-v-788790b4]{border-radius:var(--borderRadius);background:#0000001f;width:100%;height:100%;display:block}@keyframes plyr-progress{to{background-position:25px 0;background-position:var(--plyr-progress-loading-size,25px) 0}}@keyframes plyr-popup{0%{opacity:.5;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes plyr-fade-in{0%{opacity:0}to{opacity:1}}.plyr{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:inherit;font-family:var(--plyr-font-family,inherit);font-variant-numeric:tabular-nums;font-weight:400;font-weight:var(--plyr-font-weight-regular,400);line-height:1.7;line-height:var(--plyr-line-height,1.7);min-width:200px;max-width:100%;text-shadow:none;z-index:0;direction:ltr;flex-direction:column;align-items:center;transition:box-shadow .3s;display:flex;position:relative}.plyr audio,.plyr iframe,.plyr video{width:100%;height:100%;display:block}.plyr button{font:inherit;line-height:inherit;width:auto}.plyr:focus{outline:0}.plyr--full-ui{box-sizing:border-box}.plyr--full-ui *,.plyr--full-ui :after,.plyr--full-ui :before{box-sizing:inherit}.plyr--full-ui a,.plyr--full-ui button,.plyr--full-ui input,.plyr--full-ui label{touch-action:manipulation}.plyr__badge{background:#4a5464;background:var(--plyr-badge-background,#4a5464);border-radius:2px;border-radius:var(--plyr-badge-border-radius,2px);color:var(--plyr-badge-text-color,#fff);font-size:9px;font-size:var(--plyr-font-size-badge,9px);padding:3px 4px;line-height:1}.plyr--fuLine truncated
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
.transition-loading[data-v-72e6950b]{background:var(--background);z-index:10000;flex-direction:column;justify-content:center;align-items:center;transition:opacity .1s;display:flex;position:fixed;inset:0}.transition-loading .spinner[data-v-72e6950b]{text-align:center;width:70px}.transition-loading .spinner>div[data-v-72e6950b]{background-color:var(--textPrimary);border-radius:100%;width:18px;height:18px;animation:1.4s ease-in-out infinite both sk-bouncedelay-72e6950b;display:inline-block}.transition-loading .spinner .bounce1[data-v-72e6950b]{animation-delay:-.32s}.transition-loading .spinner .bounce2[data-v-72e6950b]{animation-delay:-.16s}@keyframes sk-bouncedelay-72e6950b{0%,80%,to{transform:scale(0)}40%{transform:scale(1)}}.preview.transitioning[data-v-72e6950b]{opacity:.3;pointer-events:none}.pdf-wrapper[data-v-72e6950b]{width:100%;height:100%;position:relative}.pdf-wrapper .pdf[data-v-72e6950b]{border:0;width:100%;height:100%}.pdf-wrapper .floating-btn[data-v-72e6950b]{color:#fff;background:#00000080}.pdf-wrapper .floating-btn[data-v-72e6950b]:hover{background:#000000b3}.preview-buttons[data-v-72e6950b]{flex-direction:row;gap:1em;display:flex}.av-preview-wrap[data-v-72e6950b]{width:100%;height:100%;min-height:12rem}.av-preview-loading[data-v-72e6950b]{justify-content:center;align-items:center;width:100%;min-height:12rem;display:flex}.transcode-offer-message[data-v-72e6950b]{text-align:center;margin:1rem 0}.transcode-session-list[data-v-72e6950b]{text-align:center;margin:0 0 1rem;padding:0;list-style:none}.transcode-session-list li[data-v-72e6950b]{margin:.25rem 0}.transcode-session-source[data-v-72e6950b]{opacity:.7}.overlay-icon{text-shadow:0 0 3px #000c;z-index:2;color:#fff;opacity:.7;position:absolute;top:.2em;left:.2em;width:100%!important;height:100%!important;font-size:1.2em!important}.overlay-icon--bottom{pointer-events:auto;cursor:pointer;background:0 0;border:none;margin:0;padding:0;line-height:1;inset:auto 0 0 auto;width:auto!important;height:auto!important}.overlay-icon--bottom.clickable:hover{opacity:1;transform:none;box-shadow:none!important}.file-icons [aria-label^=\.],.file-icons [aria-label$=\.bak]{opacity:.33}.svg-icons{max-width:100px;display:flex}.icon{fill:currentColor;background:var(--iconBackground);will-change:auto;border-radius:.2em;padding:.1em;font-size:1.5rem;transform:translateZ(0)}.icon.active{background:var(--background)}.primary-icons{color:var(--primaryColor)}.primary-icons.active{text-shadow:0 0 1px #000}.blue-icons{color:var(--icon-blue)}.deep-blue-icons{color:#1d5fbf}.lightblue-icons{color:#87cefa}.purple-icons{color:var(--icon-violet)}.plum-icons{color:var(--icon-plum)}.yellow-icons{color:var(--icon-yellow)}.orange-icons{color:var(--icon-orange)}.red-icons{color:var(--red)}.deep-orange-icons{color:var(--icon-deep-orange)}.brown-icons{color:var(--icon-brown)}.coral-icons{color:var(--icon-coral)}.tan-icons{color:var(--icon-tan)}.green-icons{color:var(--icon-green)}.white-icons{color:#fff}.gray-icons{color:var(--divider)}.lightgray-icons{color:var(--light-gray)}#listingView.gallery .listing-item i.white-icons,.active.white-icons{color:var(--activeWhiteIcon)}.image-preview{width:var(--icon-size);height:var(--icon-size);aspect-ratio:1;background:var(--iconBackground);border-radius:.5em;justify-content:center;align-items:center;display:flex;position:relative;overflow:hidden}.gallery .image-preview{border-radius:0}.image-preview i{box-sizing:border-box;font-size:var(--icon-font-size,3em);padding:.1em;transform:translateZ(0)}.image-preview img{object-fit:cover;object-position:center;width:100%;height:100%;display:block}.image-preview .placeholder-icon{color:#fff;opacity:.7;text-shadow:0 0 3px #000c}.image-preview .threejs-icon-container{background:#000;justify-content:center;align-items:center;width:100%;height:100%;display:flex}.image-preview .threejs-icon-container canvas{display:block;width:100%!important;height:100%!important}.download-icon{cursor:pointer;color:var(--surfaceSecondary)}.icon-download{font-size:.5em}.listing-item{-webkit-touch-callout:none}.listing-item.pinned-item{border-color:color-mix(in srgb, var(--primaryColor) 35%, transparent)}.listing-item>div{min-height:1em}.hiddenFile{opacity:.5}.activebutton{height:10em}.iconActive{font-size:6em!important}.activetitle{width:9em!important;margin-right:1em!important}.half-selected{border-width:var(--borderWidth)!important;border-color:var(--primaryColor)!important;border-style:solid!important}.vue-simple-progress{background:var(--primaryColor);margin:.5em}.vue-simple-progress,.vue-simple-progress-bar{border-radius:.5em}.vue-simple-progress-text{color:#000}.tooltip-info-icon{cursor:pointer;vertical-align:middle;opacity:.7;font-size:1rem}.tooltip-info-icon:hover,.tooltip-info-icon:hover .material-symbols-outlined{font-variation-settings:"FILL" 1;opacity:1}@keyframes progress-pulse{0%,to{opacity:1}50%{opacity:.6}}.settings-group-title[data-v-7bce02d3]{justify-content:space-between;align-items:center;margin-top:.75em;pLine truncated
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,304 @@
server:
searchResultsLimit: 1000 # maximum requested search results across all sources (default: 1000); quick search defaults to 100, advanced search to 500; size viewer remains limited to 200 validate:gte=1
minSearchLength: 3 # minimum length of search query to begin searching (default: 3)
disableUpdateCheck: false # disables backend update check service
numImageProcessors: 4 # number of concurrent image processing jobs used to create previews, default is 4.
disablePreviews: false # disable all previews thumbnails, simple icons will be used
disablePreviewResize: false # disable resizing of previews for faster loading over slow connections
disableTypeDetectionByHeader: false # disable type detection by header, useful if filesystem is slow.
logging:
- levels: "" # separated list of log levels to enable. (eg. "info|warning|error|debug")
apiLevels: "" # separated list of log levels to enable for the API. (eg. "info|warning|error")
output: "stdout" # output location. (eg. "stdout" or "path/to/file.log")
noColors: false # disable colors in the output
json: false # output in json format
utc: false # use UTC time in the output instead of local time.
apiFilter: "" # regex filter that excludes matching full api paths from being logged. (eg. '/user\?id\=self') Defaults to '^/health|^/favicon.ico|^/static|^/public/static'
sources: # validate:required,dive
- path: "/relative/or/absolute/path" # file system path. (Can be relative) validate:required
name: "Source Name" # display name
config:
denyByDefault: false # deny access unless an "allow" access rule was specifically created.
private: false # designate as source as private -- currently just means no sharing permitted.
readOnly: false # read-only source, changes from the UI, webdav, and API will be disabled.
disabled: false # disable the source, this is useful so you don't need to remove it from the config file
rules: # list of item rules to apply to specific paths
- neverWatchPath: "" # index the folder in the first pass to get included in search, but never re-indexed.
includeRootItem: "" # include only these items at root folder level
fileStartsWith: "" # (global) exclude files that start with these prefixes. Eg. "archive-" or "backup-"
folderStartsWith: "" # (global) exclude folders that start with these prefixes. Eg. "archive-" or "backup-"
fileEndsWith: "" # (global) exclude files that end with these suffixes. Eg. ".jpg" or ".txt"
folderEndsWith: "" # (global) exclude folders that end with these suffixes. Eg. ".thumbnails" or ".git"
folderPath: "" # (global) exclude folders that match this path. Eg. "/path/to/folder" or "/path/to/folder/subfolder"
filePath: "" # (global) exclude files that match this path. Eg. "/path/to/file.txt" or "/path/to/file.txt/subfile.txt"
fileName: "" # (global) exclude files that match these names. Eg. "file.txt" or "test.csv"
folderName: "" # (global) exclude folders that match these names. Eg. "folder" or "subfolder"
viewable: false # Enable viewing in UI but exclude from indexing
ignoreHidden: false # Excludes only hidden files and folders
ignoreZeroSizeFolders: false # Excludes only folders with 0 size
ignoreSymlinks: false # Excludes symbolic links
defaultUserScope: "/" # defaults to root of index "/" should match folders under path
defaultEnabled: true # should be added as a default source for new users?
createUserDir: false # create a user directory for each user under defaultUserScope + username
useLogicalSize: false # calculate sizes based on logical size instead of disk utilization (du -sh), folders will be 0 bytes when empty.
defaultPermissions: # DefaultPermissions is the template for new user scopes on this source (also synced globally via Access settings).
view: false
download: false
modify: false
delete: false
create: false
configured: false # true when explicitly set (allows intentional deny-all)
cacheDir: "tmp" # path to the cache directory, used for thumbnails and other cached files
cacheDirCleanup: false # whether to automatically cleanup the cache directory. Note: docker must also mount a persistent volume to persist the cache (default: false)
maxArchiveSize: 20 # maximum archive/unarchive size in GB. 0 means no limit. (default: 20)
filesystem: # filesystem settings
createFilePermission: "644" # Unix permissions like 644, 755, 2755 (default: 644) validate:required,file_permission
createDirectoryPermission: "755" # Unix permissions like 755, 2755, 1777 (default: 755) validate:required,file_permission
indexSqlConfig: # Index database SQL configuration
batchSize: 1000 # number of items to batch in a single transaction, typically 500-5000. higher = faster but could use more memory.
cacheSizeMB: 32 # size of the SQLite cache in MB
walMode: false # enable the more complex WAL journaling mode. Slower, more memory usage, but better for deployments with constant user activity.
disableReuse: false # enable to always create a new indexing database on startup.
fts5: false # Enable FTS5 to provide faster search queries, but 40% slower indexing and 15% larger database size
startupIntegrityCheck: "quickCheck" # the method used to check the integrity of the index database on startup (default: quickCheck) validate:omitempty,oneof=quickCheck probe off
database: # SQLite database configuration
path: "filebrowser.sqlite" # path to SQLite database file
migrateFrom: "" # path to legacy database file for migration (optional)
activity: # activity audit logging configuration
disabled: false # disable semantic activity audit logging (default: false)
retentionDays: 30 # purge activity rows older than this many days (default 30)
flushIntervalSeconds: 10 # buffer flush interval in seconds (default 10)
maxBufferSize: 10000 # max in-memory buffer before immediate flush (default 10000)
quotas: # quota counter batch flush configuration
flushIntervalSeconds: 10 # default 10
flushMaxBuffers: 500 # default 500
auth:
tokenExpirationHours: 2 # time in hours each web UI session token is valid for. Default is 2 hours.
methods:
proxy: # validate:omitempty
enabled: false # whether to enable this authentication method.
adminGroup: "" # if set, users in this group will be granted admin privileges
userGroups: # if set, only users in these groups are allowed to log in. Blocks all other users even with valid credentials.
groupsClaim: "" # the JSON field name to read groups from. Default is "groups"
userIdentifier: "" # the field value to use as the username. Default is "preferred_username" in oidc, "sub" in jwt. Other common values are "email" or "username", or "phone"
disableVerifyTLS: false # disable TLS verification (insecure, for testing only)
logoutRedirectUrl: "" # if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
header: "" # required header to use for authentication. Security Warning: FileBrowser blindly accepts the header value as username.
noauth: false # if set to true, overrides all other auth methods and disables authentication
password: # validate:omitempty
enabled: true
adminUsername: "admin" # secret: admin username auto-assigned. If not set, the default is "admin".
adminPassword: "admin" # secret: password for admin auto-assigned admin account. If set, reset on startup.
minLength: 5 # minimum pasword length required, default is 5. validate:omitempty
signup: false # allow signups on login page if enabled -- not secure. validate:omitempty
recaptcha: # recaptcha config. If configured will show up the checkbox verification in the login page validate:omitempty
host: "" # google recaptcha host, for example: https://www.google.com/recaptcha/api.js validate:required
key: "" # v2 site key validate:required
secret: "" # secret: v2 secret key validate:required
enforcedOtp: false # if set to true, TOTP is enforced for all password users users. Otherwise, users can choose to enable TOTP.
oidc: # validate:omitempty
enabled: false # whether to enable this authentication method.
adminGroup: "" # if set, users in this group will be granted admin privileges
userGroups: # if set, only users in these groups are allowed to log in. Blocks all other users even with valid credentials.
groupsClaim: "groups" # the JSON field name to read groups from. Default is "groups"
userIdentifier: "preferred_username" # the field value to use as the username. Default is "preferred_username" in oidc, "sub" in jwt. Other common values are "email" or "username", or "phone"
disableVerifyTLS: false # disable TLS verification (insecure, for testing only)
logoutRedirectUrl: "" # if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
clientId: "" # secret: client id of the OIDC application
clientSecret: "" # secret: client secret of the OIDC application
issuerUrl: "" # authorization URL of the OIDC provider
scopes: "openid email profile" # scopes to request from the OIDC provider
ldap: # validate:omitempty
enabled: false # whether to enable this authentication method.
adminGroup: "" # if set, users in this group will be granted admin privileges
userGroups: # if set, only users in these groups are allowed to log in. Blocks all other users even with valid credentials.
groupsClaim: "" # the JSON field name to read groups from. Default is "groups"
userIdentifier: "" # the field value to use as the username. Default is "preferred_username" in oidc, "sub" in jwt. Other common values are "email" or "username", or "phone"
disableVerifyTLS: false # disable TLS verification (insecure, for testing only)
logoutRedirectUrl: "" # if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
server: "" # scheme://host:port of the LDAP server (e.g. ldap://localhost:389)
baseDN: "" # LDAP search base DN (e.g. dc=ldap,dc=goauthentik,dc=io)
userDN: "" # Bind DN for service account (e.g. cn=admin,ou=users,dc=ldap,dc=goauthentik,dc=io)
userPassword: "" # secret: bind password for the LDAP service account
userFilter: "" # Search filter for finding user by username. Default (&(cn=%s)(objectClass=user)); override e.g. (email=%s) or (sAMAccountName=%s) for other directories.
jwt: # validate:omitempty
enabled: false # whether to enable this authentication method.
adminGroup: "" # if set, users in this group will be granted admin privileges
userGroups: # if set, only users in these groups are allowed to log in. Blocks all other users even with valid credentials.
groupsClaim: "" # the JSON field name to read groups from. Default is "groups"
userIdentifier: "" # the field value to use as the username. Default is "preferred_username" in oidc, "sub" in jwt. Other common values are "email" or "username", or "phone"
disableVerifyTLS: false # disable TLS verification (insecure, for testing only)
logoutRedirectUrl: "" # if provider logout url is provided, filebrowser will also redirect to logout url. Custom logout query params are respected.
header: "" # HTTP header to look for JWT token (e.g. X-JWT-Assertion). Default is "X-JWT-Assertion"
secret: "" # secret: Shared secret key/bytes for verifying JWT token signatures (required, eg PUBLIC KEY, RSA PUBLIC KEY, EC PUBLIC KEY, or CERTIFICATE)
algorithm: "" # JWT signing algorithm (HS256, HS384, HS512, RS256, ES256). Default is "HS256"
passkey: # validate:omitempty
enabled: false # whether to enable passkey MFA
rpDisplayName: "" # the Relying Party display name (defaults to frontend name)
rpId: "" # the Relying Party ID; if empty, auto-derived from base URL host
rpOrigins: # allowed origins; if empty, auto-derived from base URL
loginButtonText: "" # custom text for the passkey login button
key: "" # secret: HMAC key for JWT tokens. If unset in config/env, one is generated and stored in the application database.
totpSecret: "" # secret: secret used to encrypt TOTP secrets
frontend:
name: "FileBrowser Quantum" # display name, PWA installation name is capped at 30 characters
disableDefaultLinks: false # disable default links in the sidebar
disableUsedPercentage: false # disable used percentage for the sources in the sidebar
externalLinks:
- text: "(untracked)" # the text to display on the link validate:required
title: "untracked" # the title to display on hover
url: "https://github.com/gtsteffaniak/filebrowser/releases/" # the url to link to validate:required
- text: "Help" # the text to display on the link validate:required
title: "" # the title to display on hover
url: "help prompt" # the url to link to validate:required
disableNavButtons: false # disable the nav buttons in the sidebar
styling:
disableEventThemes: false # disable the event based themes,
customCSS: "" # if a valid path to a css file is provided, it will be applied for all users. (eg. "reduce-rounded-corners.css")
lightBackground: "#f5f5f5" # specify a valid CSS color property value to use as the background color in light mode
darkBackground: "#141D24" # Specify a valid CSS color property value to use as the background color in dark mode
customThemes: # A list of custom css files that each user can select to override the default styling. if "default" is key name then it will be the default option.
alternative:
description: "Reduce rounded corners" # The description of the theme to display in the UI.
css: "reduce-rounded-corners.css" # The css file path and filename to use for the theme.
default:
description: "The default theme" # The description of the theme to display in the UI.
css: "" # The css file path and filename to use for the theme.
favicon: "" # path to a favicon to use for the frontend
description: "FileBrowser Quantum is a file manager for the web which can be used to manage files on your server" # description that shows up in html head meta description
loginIcon: "" # path to an image file for the login page icon
loginButtonText: "" # text to display on the login button
oidcLoginButtonText: "" # text to display on the OIDC login button
disablePWAInstall: false # disable the PWA install button in the sidebar
userDefaults: # optional signup/CLI defaults; per-user values are managed in the UI
sidebar:
disableQuickToggles: false # disable the quick toggles in the sidebar
hideFileActions: false # hide the file actions in the sidebar
disableHideOnPreview: false # keep sidebar open when previewing files (was preview.disableHideSidebar)
sticky: true # keep sidebar open when navigating
hideFiles: false # hide files in the sidebar tree navigation, when true, will show only directories
showTools: true # show sidebar links with category "tool"; default is true
listing:
deleteWithoutConfirming: false # delete files without confirmation
promptRightCloseButton: false # show the prompts close button on the right
dateFormat: false # when false, the date is relative, when true, the date is an exact timestamp
showHidden: false # show hidden files in the UI. On windows this includes files starting with a dot and windows hidden files
quickDownload: false # show icon to download in one click
showSelectMultiple: false # show select multiple files on desktop
singleClick: false # open directory on single click, also enables middle click to open in new tab
hideFileExt: "" # space separated list of file extensions to hide in UI
showCopyPath: false # show copy path button in the context menu
showTypeColumn: false # show the Type column in list and compact views
showCreationDateColumn: false # show the Creation date column in list and compact views
deleteAfterArchive: true # delete source files after successful creation/extraction of archives
viewMode: "normal" # view mode to use: eg. normal, list, grid, or compact
gallerySize: 3 # 0-9 - the size of the gallery thumbnails
newFileTemplate: # list of custom filenames that will be used as template for new files
- ""
preview:
image: true # show thumbnails for image files
video: true # show thumbnails for video files
audio: true # show thumbnails for audio files
motionVideoPreview: true # show multiple frames for videos in thumbnail preview when hovering
office: true # show thumbnails for office files
popup: true # show larger popup preview when hovering over thumbnail
disablePreviewExt: "" # comma separated list of file extensions to disable preview for
highQuality: true # high quality preview thumbnails
folder: true # show thumbnails for folders that have previewable contents
models: true # show live thumbnails for 3D models files
fileViewer:
autoplayMedia: true # autoplay media files in preview
disableViewingExt: "" # comma separated list of file extensions to disable viewing for
disableOnlyOfficeExt: ".md .txt .pdf .html .xml" # list of file extensions to disable onlyoffice editor for
preferEditorForMarkdown: false # prefer editor first for markdown files instead of the Markdown Viewer
debugOffice: false # debug onlyoffice editor
search:
disableOptions: false # disable the search options in the search bar
ui:
darkMode: true # should dark mode be enabled
themeColor: "var(--blue)" # theme color to use: eg. #ff0000, or var(--red), var(--purple), etc
customTheme: "" # Name of theme to use chosen from custom themes config
locale: "en" # language to use: eg. de, en, or fr
fileLoading:
maxConcurrentUpload: 10
uploadChunkSizeMb: 10
clearAll: false
downloadChunkSizeMb: 0
account:
permissions:
api: false # allow api access
admin: false # allow admin access
share: false # allow sharing files
realtime: false # allow realtime updates
lockPassword: false # disable the user from changing their password
requirePasswordChange: false # force password change at next password login
disableSettings: false # disable the user from viewing the settings page
loginMethod: "password" # login method to use: eg. password, proxy, oidc
disableUpdateNotifications: false # disable update notifications banner for admin users
showAdvancedProfile: false # show advanced profile settings in the user profile page
maxConcurrentTranscodes: 1 # per-user concurrent transcode sessions (default: 1)
shareDefaults: # optional defaults for new shares; managed in the UI
shareTheme: ""
disableAnonymous: false
disableThumbnails: false
keepAfterExpiration: false
themeColor: ""
title: ""
description: ""
favicon: ""
quickDownload: false
hideNavButtons: false
disableSidebar: false
disableShareCard: false
enforceDarkLightMode: ""
viewMode: ""
enableOnlyOffice: false
shareType: ""
allowDelete: false
allowCreate: false
allowModify: false
disableFileViewer: false
disableDownload: false
allowReplacements: false
sidebarLinks:
showHidden: false
disableLoginOption: false
maxBandwidth: 0
allowedUsernames:
perUserDownloadLimit: false
downloadsLimit: 0
quotaLimitBytes: 0
hideFileExt: ""
banner: ""
integrations:
office: # validate:omitempty
url: "" # The URL to the office editor (OnlyOffice Document Server or Collabora Online), needs to be accessible to the user. validate:required
internalUrl: "" # An optional internal address that the filebrowser server can use to communicate with the office editor, could be useful to bypass proxy.
secret: "" # secret: OnlyOffice only, the JWT secret shared with the Document Server. Unused with collabora: editing sessions are signed with a key derived from auth.key, and Collabora trusts FileBrowser through its storage.wopi.host allow-list, where FileBrowser's host must be added. validate:required_unless=Product collabora
product: "" # office editor behind url: onlyoffice (default) or collabora validate:omitempty,oneof=onlyoffice collabora
tokenExpirationHours: 10 # collabora only: lifetime of an editing session, in hours (default: 10)
viewOnly: false # view only mode for the office editor
media: # validate:omitempty
ffmpegPath: "" # path to ffmpeg directory with ffmpeg and ffprobe (eg. /usr/local/bin)
skipHwCheck: false # SkipHwCheck skips the ffmpeg hardware codec probe entirely (software-only).
# Config-only, not a runtime setting — advanced use, e.g. keeping the
# multi-second probe out of CI startup.
http:
socket: "" # socket to listen on - eg. /var/run/filebrowser.sock
tlsKey: "" # path to TLS key
tlsCert: "" # path to TLS cert
port: 80 # port to listen on
listen: "" # address to listen on (default: 0.0.0.0)
baseURL: "/" # base URL for the server, the subpath that the server is running on.
externalUrl: "" # used by share links if set (eg. http://mydomain.com)
internalUrl: "" # used by integrations if set, this is the base domain that an integration service will use to communicate with filebrowser (eg. http://localhost:8080)
disableWebDAV: false # disable webdav support (default: false)
trustProxyHeaders: false # honor X-Forwarded-* and X-Real-IP from a reverse proxy (default: false)
trustedProxies: # IPs/CIDRs of trusted proxies; forwarded client headers are only honored when the socket peer matches one of these (default: empty = forwarded headers ignored)
unixSocketTrustedUIDs: # UnixSocketTrustedUIDs and UnixSocketTrustedGIDs list peer credentials (Linux SO_PEERCRED) allowed to
# present X-Forwarded-* on a filesystem http.socket listener. When both are empty, forwarded headers
# are not trusted on Unix sockets. Abstract sockets (@name) never trust forwarded headers.
unixSocketTrustedGIDs:
socketFileMode: "" # octal mode for filesystem http.socket (default "0660"); ignored for abstract sockets
socketGroup: "" # optional group name or numeric gid for the socket file (filesystem sockets only)
disableRateLimit: false # turns off built-in auth route rate limiting and failed-login lockout (default false).
@@ -0,0 +1,357 @@
/*
* LoadingSpinner.vue + initial index.html shell.
* Embedded in index.html via the backend; also @import'd from src/css/styles.css for the Vue app.
*
* Spinner glyph: filled SVG path (data URI mask) so ring thickness tapers along the arc — not alpha-only.
* Regenerate: polygon from outer/inner circular arcs (~120° sweep) in viewBox 0 0 100 100; mirror X if spin/orientation needs flipping.
*/
/* -------------------------------------------------------------------------- */
/* Core box */
/* -------------------------------------------------------------------------- */
.loader {
box-sizing: border-box;
margin: 0 auto;
border: none;
overflow: visible;
position: relative;
display: block;
border-radius: 50%;
}
/* -------------------------------------------------------------------------- */
/* Tapered ring: solid fill + SVG mask (true stroke-width taper, not alpha) */
/* Path is mirrored so the thick head leads when rotating clockwise (W3C +angle = CW). */
/* -------------------------------------------------------------------------- */
.loader__comet {
pointer-events: none;
position: absolute;
inset: 0;
display: block;
direction: ltr;
background-color: var(--spinner-color, var(--primaryColor));
/* Mirrored annulus-sector polygon, 120° sweep, viewBox 0 0 100 100 */
--fb-loader-taper-mask: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20100%20100%22%3E%3Cpath%20fill%3D%22black%22%20d%3D%22M57.47%2C7.65%20L54.99%2C7.29%20L52.50%2C7.07%20L50.00%2C7.00%20L47.50%2C7.07%20L45.01%2C7.29%20L42.53%2C7.65%20L40.08%2C8.16%20L37.67%2C8.81%20L35.29%2C9.59%20L32.97%2C10.52%20L30.70%2C11.57%20L28.50%2C12.76%20L26.37%2C14.07%20L24.32%2C15.51%20L22.36%2C17.06%20L20.49%2C18.72%20L18.72%2C20.49%20L17.06%2C22.36%20L15.51%2C24.32%20L14.07%2C26.37%20L12.76%2C28.50%20L11.57%2C30.70%20L10.52%2C32.97%20L9.59%2C35.29%20L8.81%2C37.67%20L8.16%2C40.08%20L7.65%2C42.53%20L7.29%2C45.01%20L7.07%2C47.50%20L7.00%2C50.00%20L7.07%2C52.50%20L7.29%2C54.99%20L7.65%2C57.47%20L8.16%2C59.92%20L8.81%2C62.33%20L9.59%2C64.71%20L10.16%2C64.50%20L9.56%2C62.11%20L9.12%2C59.69%20L8.81%2C57.26%20L8.65%2C54.83%20L8.63%2C52.41%20L8.75%2C50.00%20L9.01%2C47.61%20L9.41%2C45.26%20L9.94%2C42.94%20L10.61%2C40.66%20L11.40%2C38.44%20L12.32%2C36.28%20L13.36%2C34.19%20L14.51%2C32.18%20L15.77%2C30.24%20L17.14%2C28.39%20L18.60%2C26.63%20L20.16%2C24.96%20L21.81%2C23.40%20L23.53%2C21.95%20L25.33%2C20.60%20L27.20%2C19.37%20L29.12%2C18.26%20L31.10%2C17.26%20L33.12%2C16.39%20L35.18%2C15.64%20L37.27%2C15.02%20L39.38%2C14.52%20L41.50%2C14.15%20L43.64%2C13.91%20L45.77%2C13.79%20L47.89%2C13.79%20L50.00%2C13.92%20L52.09%2C14.18%20L54.14%2C14.55%20L56.16%2C15.04Z%22%2F%3E%3C%2Fsvg%3E");
mask-image: var(--fb-loader-taper-mask);
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
transform-origin: 50% 50%;
transform-box: fill-box;
animation: fb-spinner-rotation var(--animation-duration, 1s) linear infinite;
}
@keyframes fb-spinner-rotation {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* -------------------------------------------------------------------------- */
/* Size variants */
/* -------------------------------------------------------------------------- */
.loader--xsmall {
width: 12px;
height: 12px;
}
.loader--small {
width: 24px;
height: 24px;
}
.loader--medium {
width: 48px;
height: 48px;
}
.loader--large {
width: 96px;
height: 96px;
}
/* -------------------------------------------------------------------------- */
/* Initial full-page shell (index.html) */
/* -------------------------------------------------------------------------- */
body > .loader {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
transition: 0.1s ease opacity;
pointer-events: none;
border: none;
animation: none;
border-radius: 0;
margin: 0;
}
body > .loader.loader--xsmall,
body > .loader.loader--small,
body > .loader.loader--medium,
body > .loader.loader--large {
width: 100%;
height: 100%;
}
body > .loader .loader__comet {
position: relative;
inset: auto;
flex-shrink: 0;
}
body > .loader.loader--xsmall .loader__comet {
width: 12px;
height: 12px;
}
body > .loader.loader--small .loader__comet {
width: 24px;
height: 24px;
}
body > .loader.loader--medium .loader__comet {
width: 48px;
height: 48px;
}
body > .loader.loader--large .loader__comet {
width: 96px;
height: 96px;
}
body > .loader.done {
opacity: 0;
}
/* -------------------------------------------------------------------------- */
/* Pulse mode */
/* -------------------------------------------------------------------------- */
.loader--pulse {
border: none;
background: var(--spinner-color, var(--primaryColor));
animation: fb-pulse-scale var(--animation-duration, 0.75s) ease infinite;
}
@keyframes fb-pulse-scale {
0%,
100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(0.95);
opacity: 0.8;
}
}
/* -------------------------------------------------------------------------- */
/* Bounce mode */
/* -------------------------------------------------------------------------- */
.loader--bounce {
display: flex;
align-items: center;
justify-content: center;
gap: 0.3em;
border: none;
background: transparent;
animation: none;
}
.loader--bounce::before,
.loader--bounce::after {
content: "";
width: 0.6em;
height: 0.6em;
background-color: var(--spinner-color, var(--primaryColor));
border-radius: 50%;
display: inline-block;
animation: fb-bounce-scale var(--animation-duration, 0.75s) infinite ease-in-out both;
}
.loader--bounce::before {
animation-delay: -0.32s;
}
.loader--bounce::after {
animation-delay: -0.16s;
}
.loader--bounce .bounce-dot {
width: 0.6em;
height: 0.6em;
background-color: var(--spinner-color, var(--primaryColor));
border-radius: 50%;
display: inline-block;
animation: fb-bounce-scale var(--animation-duration, 0.75s) infinite ease-in-out both;
}
@keyframes fb-bounce-scale {
0%,
80%,
100% {
transform: scale(0);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.loader--bounce.loader--xsmall::before,
.loader--bounce.loader--xsmall::after,
.loader--bounce.loader--xsmall .bounce-dot {
width: 0.3em;
height: 0.3em;
}
.loader--bounce.loader--small::before,
.loader--bounce.loader--small::after,
.loader--bounce.loader--small .bounce-dot {
width: 0.45em;
height: 0.45em;
}
.loader--bounce.loader--medium::before,
.loader--bounce.loader--medium::after,
.loader--bounce.loader--medium .bounce-dot {
width: 0.6em;
height: 0.6em;
}
.loader--bounce.loader--large::before,
.loader--bounce.loader--large::after,
.loader--bounce.loader--large .bounce-dot {
width: 0.9em;
height: 0.9em;
}
/* -------------------------------------------------------------------------- */
/* Placeholder mode */
/* -------------------------------------------------------------------------- */
.loader--placeholder {
display: flex;
flex-direction: column;
gap: 0.5em;
border: none;
animation: none;
width: auto;
height: auto;
border-radius: 0;
}
.placeholder-item {
background-color: rgb(128 128 128 / 10%);
animation: fb-placeholder-ripple 1.8s cubic-bezier(0.25, 0.46, 0.45, 0.94) infinite,
fb-placeholder-bg-fade 2s ease-in-out infinite;
display: block;
border-radius: 0.25em;
margin: 0;
padding: 0;
transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94),
box-shadow 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.placeholder-item-1 {
animation-delay: 0s;
}
.placeholder-item-2 {
animation-delay: 0.36s;
}
.placeholder-item-3 {
animation-delay: 0.72s;
}
@keyframes fb-placeholder-ripple {
0% {
transform: scale(1);
box-shadow: none;
opacity: 0.3;
}
25% {
transform: scale(1.07);
box-shadow: inset 0 -3em 3em rgb(172 172 172 / 21.1%),
0 0 0 2px var(--alt-background), 0 4px 12px rgb(0 0 0 / 15%);
opacity: 0.6;
}
50% {
transform: scale(0.97);
box-shadow: none;
opacity: 0.3;
}
100% {
transform: scale(1);
box-shadow: none;
opacity: 0.3;
}
}
@keyframes fb-placeholder-bg-fade {
0% {
background-color: rgb(128 128 128 / 10%);
}
50% {
background-color: rgb(128 128 128 / 40%);
}
100% {
background-color: rgb(128 128 128 / 10%);
}
}
.loader--placeholder.loader--xsmall .placeholder-item {
width: 30px;
height: 7.5px;
min-width: 30px;
min-height: 7.5px;
}
.loader--placeholder.loader--small .placeholder-item {
width: 150px;
height: 12.5px;
min-width: 150px;
min-height: 12.5px;
}
.loader--placeholder.loader--medium .placeholder-item {
width: 200px;
height: 25px;
min-width: 200px;
min-height: 25px;
}
.loader--placeholder.loader--large .placeholder-item {
width: 400px;
height: 50px;
min-width: 400px;
min-height: 50px;
}
/* -------------------------------------------------------------------------- */
/* Standard portrait mobile responsive fix */
/* -------------------------------------------------------------------------- */
@media (width <= 768px) {
.loader--placeholder.loader--large .placeholder-item {
width: 100%;
min-width: auto;
}
}
@@ -0,0 +1,24 @@
/* `--background` is intentionally set here as a placeholder for stylelint */
:root {
--background: #f5f5f5;
--alt-background: #ddd;
--surfacePrimary: #ebebeb;
--surfaceSecondary: lightgray;
--textPrimary: #546e7a;
--textSecondary: gray;
--divider: rgb(176 176 176 / 50%);
--iconBackground: #ddd;
--activeWhiteIcon: gray;
}
.dark-mode {
--background: #141D24;
--alt-background: #283136;
--surfacePrimary: #20292F;
--surfaceSecondary: #3A4147;
--divider: rgb(255 255 255 / 12%);
--textPrimary: rgb(255 255 255 / 87%);
--textSecondary: rgb(255 255 255 / 60%);
--iconBackground: #171718;
--activeWhiteIcon: white;
}
Binary file not shown.
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff. Show more