Compare commits

..

1 Commits

Author SHA1 Message Date
Henrique Dias
83492a4dfb feat(frontend): migrate Vue to Composition API
Signed-off-by: Henrique Dias <mail@hacdias.com>
2025-11-13 14:23:20 +01:00
44 changed files with 2674 additions and 1396 deletions

View File

@@ -1,4 +1,4 @@
name: Continuous Integration name: main
on: on:
push: push:
@@ -9,8 +9,8 @@ on:
pull_request: pull_request:
jobs: jobs:
# linters
lint-frontend: lint-frontend:
name: Lint Frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
@@ -22,43 +22,26 @@ jobs:
node-version: "24.x" node-version: "24.x"
cache: "pnpm" cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml" cache-dependency-path: "frontend/pnpm-lock.yaml"
- working-directory: frontend - run: make lint-frontend
run: |
pnpm install --frozen-lockfile
pnpm run lint
lint-backend: lint-backend:
name: Lint Backend
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
- uses: golangci/golangci-lint-action@v9
with:
version: "latest"
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: "1.25.x"
- run: go test --race ./...
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- uses: actions/setup-go@v6 - uses: actions/setup-go@v6
with: with:
go-version: '1.25' go-version: '1.25'
- run: make lint-backend
lint:
runs-on: ubuntu-latest
needs: [lint-frontend, lint-backend]
steps:
- run: echo "done"
# tests
test-frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
with: with:
package_json_file: "frontend/package.json" package_json_file: "frontend/package.json"
@@ -67,15 +50,26 @@ jobs:
node-version: "24.x" node-version: "24.x"
cache: "pnpm" cache: "pnpm"
cache-dependency-path: "frontend/pnpm-lock.yaml" cache-dependency-path: "frontend/pnpm-lock.yaml"
- name: Install Task - run: make test-frontend
uses: go-task/setup-task@v1 test-backend:
- run: task build
release:
name: Release
needs: ["lint-frontend", "lint-backend", "test", "build"]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: '1.25'
- run: make test-backend
test:
runs-on: ubuntu-latest
needs: [test-frontend, test-backend]
steps:
- run: echo "done"
# release
release:
runs-on: ubuntu-latest
needs: [lint, test]
if: startsWith(github.event.ref, 'refs/tags/v')
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
@@ -95,9 +89,8 @@ jobs:
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Install Task - name: Build frontend
uses: go-task/setup-task@v1 run: make build-frontend
- run: task build-frontend
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -110,6 +103,3 @@ jobs:
args: release --clean args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GH_PAT }} GITHUB_TOKEN: ${{ secrets.GH_PAT }}

View File

@@ -13,7 +13,7 @@ permissions:
jobs: jobs:
main: main:
name: Validate Title name: Validate PR title
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: amannn/action-semantic-pull-request@v6 - uses: amannn/action-semantic-pull-request@v6
@@ -43,4 +43,4 @@ jobs:
uses: marocchino/sticky-pull-request-comment@v2 uses: marocchino/sticky-pull-request-comment@v2
with: with:
header: pr-title-lint-error header: pr-title-lint-error
delete: true delete: true

20
.github/workflows/site-pr.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Build Site
on:
pull_request:
paths:
- 'www'
- '*.md'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build site
run: make site

View File

@@ -1,32 +1,12 @@
name: Docs name: Build and Deploy Site
on: on:
pull_request:
paths:
- 'www'
- '*.md'
push: push:
branches: branches:
- master - master
jobs: jobs:
build: deploy:
name: Build Docs
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install Task
uses: go-task/setup-task@v1
- name: Build site
run: task docs
build-and-release:
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
name: Build and Release Docs
permissions: permissions:
contents: read contents: read
deployments: write deployments: write
@@ -36,12 +16,13 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v5 uses: actions/checkout@v5
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Install Task
uses: go-task/setup-task@v1
- name: Build site - name: Build site
run: task docs run: make site
- name: Deploy to Cloudflare Pages - name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3 uses: cloudflare/wrangler-action@v3
with: with:
@@ -49,4 +30,3 @@ jobs:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy www/public --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }} command: pages deploy www/public --project-name=${{ secrets.CLOUDFLARE_PROJECT_NAME }}
gitHubToken: ${{ secrets.GITHUB_TOKEN }} gitHubToken: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -2,8 +2,6 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [2.45.3](https://github.com/filebrowser/filebrowser/compare/v2.45.2...v2.45.3) (2025-11-13)
## [2.45.2](https://github.com/filebrowser/filebrowser/compare/v2.45.1...v2.45.2) (2025-11-13) ## [2.45.2](https://github.com/filebrowser/filebrowser/compare/v2.45.1...v2.45.2) (2025-11-13)

View File

@@ -15,23 +15,11 @@ We encourage you to use git to manage your fork. To clone the main repository, j
git clone https://github.com/filebrowser/filebrowser git clone https://github.com/filebrowser/filebrowser
``` ```
We use [Taskfile](https://taskfile.dev/) to manage the different processes (building, releasing, etc) automatically.
## Build ## Build
You can fully build the project in order to produce a binary by running:
```bash
task build
```
## Development
For development, there are a few things to have in mind.
### Frontend ### Frontend
We use [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. Prepare the frontend environment: We are using [Node.js](https://nodejs.org/en/) on the frontend to manage the build process. The steps to build it are:
```bash ```bash
# From the root of the repo, go to frontend/ # From the root of the repo, go to frontend/
@@ -39,62 +27,37 @@ cd frontend
# Install the dependencies # Install the dependencies
pnpm install pnpm install
```
If you just want to develop the backend, you can create a static build of the frontend: # Build the frontend
```bash
pnpm run build pnpm run build
``` ```
If you want to develop the frontend, start a development server which watches for changes: This will install the dependencies and build the frontend so you can then embed it into the Go app. Although, if you want to play with it, you'll get bored of building it after every change you do. So, you can run the command below to watch for changes:
```bash ```bash
pnpm run dev pnpm run dev
``` ```
Please note that you need to access File Browser's interface through the development server of the frontend.
### Backend ### Backend
First prepare the backend environment by downloading all required dependencies: First of all, you need to download the required dependencies. We are using the built-in `go mod` tool for dependency management. To get the modules, run:
```bash ```bash
go mod download go mod download
``` ```
You can now build or run File Browser as any other Go project: The magic of File Browser is that the static assets are bundled into the final binary. For that, we use [Go embed.FS](https://golang.org/pkg/embed/). The files from `frontend/dist` will be embedded during the build process.
To build File Browser is just like any other Go program:
```bash ```bash
# Build
go build go build
# Run
go run .
``` ```
## Documentation To create a development build use the "dev" tag, this way the content inside the frontend folder will not be embedded in the binary but will be reloaded at every change:
We rely on Docker to abstract all the dependencies required for building the documentation.
To build the documentation to `www/public`:
```bash ```bash
task docs go build -tags dev
```
To start a local server on port `8000` to view the built documentation:
```bash
task docs-serve
```
## Release
To make a release, just run:
```bash
task release
``` ```
## Translations ## Translations

88
Makefile Normal file
View File

@@ -0,0 +1,88 @@
include common.mk
include tools.mk
LDFLAGS += -X "$(MODULE)/version.Version=$(VERSION)" -X "$(MODULE)/version.CommitSHA=$(VERSION_HASH)"
SITE_DOCKER_FLAGS = \
-v $(CURDIR)/www:/docs \
-v $(CURDIR)/LICENSE:/docs/docs/LICENSE \
-v $(CURDIR)/SECURITY.md:/docs/docs/security.md \
-v $(CURDIR)/CHANGELOG.md:/docs/docs/changelog.md \
-v $(CURDIR)/CODE-OF-CONDUCT.md:/docs/docs/code-of-conduct.md \
-v $(CURDIR)/CONTRIBUTING.md:/docs/docs/contributing.md
## Build:
.PHONY: build
build: | build-frontend build-backend ## Build binary
.PHONY: build-frontend
build-frontend: ## Build frontend
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run build
.PHONY: build-backend
build-backend: ## Build backend
$Q $(go) build -ldflags '$(LDFLAGS)' -o .
.PHONY: test
test: | test-frontend test-backend ## Run all tests
.PHONY: test-frontend
test-frontend: ## Run frontend tests
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run typecheck
.PHONY: test-backend
test-backend: ## Run backend tests
$Q $(go) test -v ./...
.PHONY: lint
lint: lint-frontend lint-backend ## Run all linters
.PHONY: lint-frontend
lint-frontend: ## Run frontend linters
$Q cd frontend && pnpm install --frozen-lockfile && pnpm run lint
.PHONY: lint-backend
lint-backend: | $(golangci-lint) ## Run backend linters
$Q $(golangci-lint) run -v
fmt: $(goimports) ## Format source files
$Q $(goimports) -local $(MODULE) -w $$(find . -type f -name '*.go' -not -path "./vendor/*")
clean: clean-tools ## Clean
## Release:
.PHONY: release-dry-run
release-dry-run:
pnpm dlx commit-and-tag-version --dry-run --skip
.PHONY: release
release:
pnpm dlx commit-and-tag-version -s
.PHONY: site
site: ## Build site
@rm -rf www/public
docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
docker run --rm $(SITE_DOCKER_FLAGS) filebrowser.site build -d "public"
.PHONY: site-serve
site-serve: ## Serve site for development
docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
docker run --rm -it -p 8000:8000 $(SITE_DOCKER_FLAGS) filebrowser.site
## Help:
help: ## Show this help
@echo ''
@echo 'Usage:'
@echo ' ${YELLOW}make${RESET} ${GREEN}<target> [options]${RESET}'
@echo ''
@echo 'Options:'
@$(call global_option, "V [0|1]", "enable verbose mode (default:0)")
@echo ''
@echo 'Targets:'
@awk 'BEGIN {FS = ":.*?## "} { \
if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \
else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \
}' $(MAKEFILE_LIST)

View File

@@ -14,12 +14,18 @@ Documentation on how to install, configure, and contribute to this project is ho
## Project Status ## Project Status
This project is a finished product which fulfills its goal: be a single binary web File Browser which can be run by anyone anywhere. That means that File Browser is currently on **maintenance-only** mode. Therefore, please note the following: > [!WARNING]
>
> This project is currently on **maintenance-only** mode, and is looking for new maintainers. For more information, please read the [discussion #4906](https://github.com/filebrowser/filebrowser/discussions/4906). Therefore, please note the following:
>
> - It can take a while until someone gets back to you. Please be patient.
> - [Issues][issues] are only being used to track bugs. Any unrelated issues will be converted into a [discussion][discussions].
> - No new features will be implemented until further notice. The priority is on triaging issues and merge bug fixes.
>
> If you're interested in maintaining this project, please reach out via the discussion above.
- It can take a while until someone gets back to you. Please be patient. [issues]: https://github.com/filebrowser/filebrowser/issues
- [Issues](https://github.com/filebrowser/filebrowser/issues) are meant to track bugs. Unrelated issues will be converted into [discussions](https://github.com/filebrowser/filebrowser/discussions). [discussions]: https://github.com/filebrowser/filebrowser/discussions
- No new features will be implemented by maintainers. Pull requests for new features will be reviewed on a case by case basis.
- The priority is triaging issues, addressing security issues, and fixing bug fixes.
## Contributing ## Contributing

View File

@@ -1,69 +0,0 @@
version: '3'
vars:
SITE_DOCKER_FLAGS: >-
-v ./www:/docs
-v ./LICENSE:/docs/docs/LICENSE
-v ./SECURITY.md:/docs/docs/security.md
-v ./CHANGELOG.md:/docs/docs/changelog.md
-v ./CODE-OF-CONDUCT.md:/docs/docs/code-of-conduct.md
-v ./CONTRIBUTING.md:/docs/docs/contributing.md
tasks:
build-frontend:
desc: Build frontend assets
dir: frontend
cmds:
- pnpm install --frozen-lockfile
- pnpm run build
build-backend:
desc: Build backend binary
cmds:
- go build -ldflags='-s -w -X "github.com/filebrowser/filebrowser/v2/version.Version={{.VERSION}}" -X "github.com/filebrowser/filebrowser/v2/version.CommitSHA={{.GIT_COMMIT}}"' -o filebrowser .
vars:
GIT_COMMIT:
sh: git log -n 1 --format=%h
VERSION:
sh: git describe --tags --abbrev=0 --match=v* | cut -c 2-
build:
desc: Build both frontend and backend
cmds:
- task: build-frontend
- task: build-backend
release-make:
internal: true
prompt: Do you wish to proceed?
cmds:
- pnpm dlx commit-and-tag-version -s
release-dry-run:
internal: true
cmds:
- pnpm dlx commit-and-tag-version --dry-run --skip
release:
desc: Create a new release
cmds:
- task: release-dry-run
- task: release-make
docs-image-make:
internal: true
cmds:
- docker build -f www/Dockerfile --progress=plain -t filebrowser.site www
docs:
desc: Generate documentation
cmds:
- rm -rf www/public
- task: docs-image-make
- docker run --rm {{.SITE_DOCKER_FLAGS}} filebrowser.site build -d "public"
docs-serve:
desc: Serve documentation
cmds:
- task: docs-image-make
- docker run --rm -it -p 8000:8000 {{.SITE_DOCKER_FLAGS}} filebrowser.site

28
common.mk Normal file
View File

@@ -0,0 +1,28 @@
SHELL := /usr/bin/env bash
DATE ?= $(shell date +%FT%T%z)
BASE_PATH := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
VERSION ?= $(shell git describe --tags --always --match=v* 2> /dev/null || \
cat $(CURDIR)/.version 2> /dev/null || echo v0)
VERSION_HASH = $(shell git rev-parse HEAD)
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
go = GOGC=off go
MODULE = $(shell env GO111MODULE=on go list -m)
# printing
# $Q (quiet) is used in the targets as a replacer for @.
# This macro helps to print the command for debugging by setting V to 1. Example `make test-unit V=1`
V = 0
Q = $(if $(filter 1,$V),,@)
# $M is a macro to print a colored ▶ character. Example `$(info $(M) running coverage tests…)` will print "▶ running coverage tests…"
M = $(shell printf "\033[34;1m▶\033[0m")
GREEN := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
WHITE := $(shell tput -Txterm setaf 7)
CYAN := $(shell tput -Txterm setaf 6)
RESET := $(shell tput -Txterm sgr0)
define global_option
printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n" $(1) $(2)
endef

14
frontend/assets_dev.go Normal file
View File

@@ -0,0 +1,14 @@
//go:build dev
package frontend
import (
"io/fs"
"os"
)
var assets fs.FS = os.DirFS("frontend")
func Assets() fs.FS {
return assets
}

View File

@@ -1,6 +1,3 @@
<!-- This component taken directly from vue-simple-progress
since it didnt support Vue 3 but the component itself does
https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/components/Progress.vue -->
<template> <template>
<div> <div>
<div <div
@@ -44,182 +41,164 @@ https://raw.githubusercontent.com/dzwillia/vue-simple-progress/master/src/compon
</div> </div>
</template> </template>
<script> <script setup lang="ts">
// We're leaving this untouched as you can read in the beginning import { computed } from "vue";
const isNumber = function (n) {
return !isNaN(parseFloat(n)) && isFinite(n); const isNumber = (n: number | string): boolean => {
return !isNaN(parseFloat(n as string)) && isFinite(n as number);
}; };
export default { const props = withDefaults(
name: "progress-bar", defineProps<{
props: { val?: number;
val: { max?: number;
default: 0, size?: number | string;
}, bgColor?: string;
max: { barColor?: string;
default: 100, barTransition?: string;
}, barBorderRadius?: number;
size: { spacing?: number;
// either a number (pixel width/height) or 'tiny', 'small', text?: string;
// 'medium', 'large', 'huge', 'massive' for common sizes textAlign?: string;
default: 3, textPosition?: string;
}, fontSize?: number;
"bg-color": { textFgColor?: string;
type: String, }>(),
default: "#eee", {
}, val: 0,
"bar-color": { max: 100,
type: String, size: 3,
default: "#2196f3", // match .blue color to Material Design's 'Blue 500' color bgColor: "#eee",
}, barColor: "#2196f3",
"bar-transition": { barTransition: "all 0.5s ease",
type: String, barBorderRadius: 0,
default: "all 0.5s ease", spacing: 4,
}, text: "",
"bar-border-radius": { textAlign: "center",
type: Number, textPosition: "bottom",
default: 0, fontSize: 13,
}, textFgColor: "#222",
spacing: { }
type: Number, );
default: 4,
},
text: {
type: String,
default: "",
},
"text-align": {
type: String,
default: "center", // 'left', 'right'
},
"text-position": {
type: String,
default: "bottom", // 'bottom', 'top', 'middle', 'inside'
},
"font-size": {
type: Number,
default: 13,
},
"text-fg-color": {
type: String,
default: "#222",
},
},
computed: {
pct() {
let pct = (this.val / this.max) * 100;
pct = pct.toFixed(2);
return Math.min(pct, this.max);
},
size_px() {
switch (this.size) {
case "tiny":
return 2;
case "small":
return 4;
case "medium":
return 8;
case "large":
return 12;
case "big":
return 16;
case "huge":
return 32;
case "massive":
return 64;
}
return isNumber(this.size) ? this.size : 32; const pct = computed(() => {
}, const pct = (props.val / props.max) * 100;
text_padding() { const pctFixed = pct.toFixed(2);
switch (this.size) { return Math.min(parseFloat(pctFixed), props.max);
case "tiny": });
case "small":
case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
}
return isNumber(this.spacing) ? this.spacing : 4; const size_px = computed(() => {
}, switch (props.size) {
text_font_size() { case "tiny":
switch (this.size) { return 2;
case "tiny": case "small":
case "small": return 4;
case "medium": case "medium":
case "large": return 8;
case "big": case "large":
case "huge": return 12;
case "massive": case "big":
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32); return 16;
} case "huge":
return 32;
case "massive":
return 64;
}
return isNumber(this.fontSize) ? this.fontSize : 13; return isNumber(props.size) ? (props.size as number) : 32;
}, });
progress_style() {
const style = {
background: this.bgColor,
};
if (this.textPosition == "middle" || this.textPosition == "inside") { const text_padding = computed(() => {
style["position"] = "relative"; switch (props.size) {
style["min-height"] = this.size_px + "px"; case "tiny":
style["z-index"] = "-2"; case "small":
} case "medium":
case "large":
case "big":
case "huge":
case "massive":
return Math.min(Math.max(Math.ceil(size_px.value / 8), 3), 12);
}
if (this.barBorderRadius > 0) { return isNumber(props.spacing) ? props.spacing : 4;
style["border-radius"] = this.barBorderRadius + "px"; });
}
return style; const text_font_size = computed(() => {
}, switch (props.size) {
bar_style() { case "tiny":
const style = { case "small":
background: this.barColor, case "medium":
width: this.pct + "%", case "large":
height: this.size_px + "px", case "big":
transition: this.barTransition, case "huge":
}; case "massive":
return Math.min(Math.max(Math.ceil(size_px.value * 1.4), 11), 32);
}
if (this.barBorderRadius > 0) { return isNumber(props.fontSize) ? props.fontSize : 13;
style["border-radius"] = this.barBorderRadius + "px"; });
}
if (this.textPosition == "middle" || this.textPosition == "inside") { const progress_style = computed(() => {
style["position"] = "absolute"; const style: Record<string, string> = {
style["top"] = "0"; background: props.bgColor,
style["height"] = "100%"; };
((style["min-height"] = this.size_px + "px"),
(style["z-index"] = "-1"));
}
return style; if (props.textPosition == "middle" || props.textPosition == "inside") {
}, style["position"] = "relative";
text_style() { style["min-height"] = size_px.value + "px";
const style = { style["z-index"] = "-2";
color: this.textFgColor, }
"font-size": this.text_font_size + "px",
"text-align": this.textAlign,
};
if ( if (props.barBorderRadius > 0) {
this.textPosition == "top" || style["border-radius"] = props.barBorderRadius + "px";
this.textPosition == "middle" || }
this.textPosition == "inside"
)
style["padding-bottom"] = this.text_padding + "px";
if (
this.textPosition == "bottom" ||
this.textPosition == "middle" ||
this.textPosition == "inside"
)
style["padding-top"] = this.text_padding + "px";
return style; return style;
}, });
},
}; const bar_style = computed(() => {
const style: Record<string, string> = {
background: props.barColor,
width: pct.value + "%",
height: size_px.value + "px",
transition: props.barTransition,
};
if (props.barBorderRadius > 0) {
style["border-radius"] = props.barBorderRadius + "px";
}
if (props.textPosition == "middle" || props.textPosition == "inside") {
style["position"] = "absolute";
style["top"] = "0";
style["height"] = "100%";
style["min-height"] = size_px.value + "px";
style["z-index"] = "-1";
}
return style;
});
const text_style = computed(() => {
const style: Record<string, string> = {
color: props.textFgColor,
"font-size": text_font_size.value + "px",
"text-align": props.textAlign,
};
if (
props.textPosition == "top" ||
props.textPosition == "middle" ||
props.textPosition == "inside"
)
style["padding-bottom"] = text_padding.value + "px";
if (
props.textPosition == "bottom" ||
props.textPosition == "middle" ||
props.textPosition == "inside"
)
style["padding-top"] = text_padding.value + "px";
return style;
});
</script> </script>

View File

@@ -2,13 +2,13 @@
<div <div
class="shell" class="shell"
:class="{ ['shell--hidden']: !showShell }" :class="{ ['shell--hidden']: !showShell }"
:style="{ height: `${this.shellHeight}em`, direction: 'ltr' }" :style="{ height: `${shellHeight}em`, direction: 'ltr' }"
> >
<div <div
@pointerdown="startDrag()" @pointerdown="startDrag()"
@pointerup="stopDrag()" @pointerup="stopDrag()"
class="shell__divider" class="shell__divider"
:style="this.shellDrag ? { background: `${checkTheme()}` } : ''" :style="shellDrag ? { background: `${checkTheme()}` } : ''"
></div> ></div>
<div @click="focus" class="shell__content" ref="scrollable"> <div @click="focus" class="shell__content" ref="scrollable">
<div v-for="(c, index) in content" :key="index" class="shell__result"> <div v-for="(c, index) in content" :key="index" class="shell__result">
@@ -39,13 +39,15 @@
<div <div
@pointerup="stopDrag()" @pointerup="stopDrag()"
class="shell__overlay" class="shell__overlay"
v-show="this.shellDrag" v-show="shellDrag"
></div> ></div>
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapState, mapActions } from "pinia"; import { ref, computed, onMounted, onBeforeUnmount } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
@@ -53,142 +55,164 @@ import { commands } from "@/api";
import { throttle } from "lodash-es"; import { throttle } from "lodash-es";
import { theme } from "@/utils/constants"; import { theme } from "@/utils/constants";
export default { const route = useRoute();
name: "shell",
computed: {
...mapState(useLayoutStore, ["showShell"]),
...mapState(useFileStore, ["isFiles"]),
path: function () {
if (this.isFiles) {
return this.$route.path;
}
return ""; const fileStore = useFileStore();
}, const layoutStore = useLayoutStore();
},
data: () => ({
content: [],
history: [],
historyPos: 0,
canInput: true,
shellDrag: false,
shellHeight: 25,
fontsize: parseFloat(getComputedStyle(document.documentElement).fontSize),
}),
mounted() {
window.addEventListener("resize", this.resize);
},
beforeUnmount() {
window.removeEventListener("resize", this.resize);
},
methods: {
...mapActions(useLayoutStore, ["toggleShell"]),
checkTheme() {
if (theme == "dark") {
return "rgba(255, 255, 255, 0.4)";
}
return "rgba(127, 127, 127, 0.4)";
},
startDrag() {
document.addEventListener("pointermove", this.handleDrag);
this.shellDrag = true;
},
stopDrag() {
document.removeEventListener("pointermove", this.handleDrag);
this.shellDrag = false;
},
handleDrag: throttle(function (event) {
const top = window.innerHeight / this.fontsize - 4;
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (userPos <= top && userPos >= bottom) { const { showShell } = storeToRefs(layoutStore);
this.shellHeight = userPos.toFixed(2); const { isFiles } = storeToRefs(fileStore);
} const { toggleShell } = layoutStore;
}, 32),
resize: throttle(function () {
const top = window.innerHeight / this.fontsize - 4;
const bottom =
2.25 +
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
if (this.shellHeight > top) { const scrollable = ref<HTMLElement | null>(null);
this.shellHeight = top; const input = ref<HTMLElement | null>(null);
} else if (this.shellHeight < bottom) {
this.shellHeight = bottom;
}
}, 32),
scroll: function () {
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
},
focus: function () {
this.$refs.input.focus();
},
historyUp() {
if (this.historyPos > 0) {
this.$refs.input.innerText = this.history[--this.historyPos];
this.focus();
}
},
historyDown() {
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
this.$refs.input.innerText = this.history[++this.historyPos];
this.focus();
} else {
this.historyPos = this.history.length;
this.$refs.input.innerText = "";
}
},
submit: function (event) {
const cmd = event.target.innerText.trim();
if (cmd === "") { const content = ref<Array<{ text: string }>>([]);
return; const history = ref<string[]>([]);
} const historyPos = ref(0);
const canInput = ref(true);
const shellDrag = ref(false);
const shellHeight = ref(25);
const fontsize = ref(
parseFloat(getComputedStyle(document.documentElement).fontSize)
);
if (cmd === "clear") { const path = computed(() => {
this.content = []; if (isFiles.value) {
event.target.innerHTML = ""; return route.path;
return; }
} return "";
});
if (cmd === "exit") { const checkTheme = () => {
event.target.innerHTML = ""; if (theme == "dark") {
this.toggleShell(); return "rgba(255, 255, 255, 0.4)";
return; }
} return "rgba(127, 127, 127, 0.4)";
this.canInput = false;
event.target.innerHTML = "";
const results = {
text: `${cmd}\n\n`,
};
this.history.push(cmd);
this.historyPos = this.history.length;
this.content.push(results);
commands(
this.path,
cmd,
(event) => {
results.text += `${event.data}\n`;
this.scroll();
},
() => {
results.text = results.text
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd();
this.canInput = true;
this.$refs.input.focus();
this.scroll();
}
);
},
},
}; };
const scroll = () => {
if (scrollable.value) {
scrollable.value.scrollTop = scrollable.value.scrollHeight;
}
};
const focus = () => {
input.value?.focus();
};
const handleDrag = throttle((event: PointerEvent) => {
const top = window.innerHeight / fontsize.value - 4;
const userPos = (window.innerHeight - event.clientY) / fontsize.value;
const divider = document.querySelector(".shell__divider") as HTMLElement;
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
if (userPos <= top && userPos >= bottom) {
shellHeight.value = parseFloat(userPos.toFixed(2));
}
}, 32);
const resize = throttle(() => {
const top = window.innerHeight / fontsize.value - 4;
const divider = document.querySelector(".shell__divider") as HTMLElement;
const bottom = 2.25 + (divider?.offsetHeight ?? 0) / fontsize.value;
if (shellHeight.value > top) {
shellHeight.value = top;
} else if (shellHeight.value < bottom) {
shellHeight.value = bottom;
}
}, 32);
const startDrag = () => {
document.addEventListener("pointermove", handleDrag as any);
shellDrag.value = true;
};
const stopDrag = () => {
document.removeEventListener("pointermove", handleDrag as any);
shellDrag.value = false;
};
const historyUp = () => {
if (historyPos.value > 0 && input.value) {
historyPos.value--;
input.value.innerText = history.value[historyPos.value];
focus();
}
};
const historyDown = () => {
if (
historyPos.value >= 0 &&
historyPos.value < history.value.length - 1 &&
input.value
) {
historyPos.value++;
input.value.innerText = history.value[historyPos.value];
focus();
} else {
historyPos.value = history.value.length;
if (input.value) {
input.value.innerText = "";
}
}
};
const submit = (event: Event) => {
const target = event.target as HTMLElement;
const cmd = target.innerText.trim();
if (cmd === "") {
return;
}
if (cmd === "clear") {
content.value = [];
target.innerHTML = "";
return;
}
if (cmd === "exit") {
target.innerHTML = "";
toggleShell();
return;
}
canInput.value = false;
target.innerHTML = "";
const results = {
text: `${cmd}\n\n`,
};
history.value.push(cmd);
historyPos.value = history.value.length;
content.value.push(results);
commands(
path.value,
cmd,
(event: MessageEvent) => {
results.text += `${event.data}\n`;
scroll();
},
() => {
results.text = results.text
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
.trimEnd();
canInput.value = true;
input.value?.focus();
scroll();
}
);
};
onMounted(() => {
window.addEventListener("resize", resize as any);
});
onBeforeUnmount(() => {
window.removeEventListener("resize", resize as any);
});
</script> </script>

View File

@@ -4,7 +4,7 @@
<template v-if="isLoggedIn"> <template v-if="isLoggedIn">
<button @click="toAccountSettings" class="action"> <button @click="toAccountSettings" class="action">
<i class="material-icons">person</i> <i class="material-icons">person</i>
<span>{{ user.username }}</span> <span>{{ user?.username }}</span>
</button> </button>
<button <button
class="action" class="action"
@@ -16,7 +16,7 @@
<span>{{ $t("sidebar.myFiles") }}</span> <span>{{ $t("sidebar.myFiles") }}</span>
</button> </button>
<div v-if="user.perm.create"> <div v-if="user?.perm.create">
<button <button
@click="showHover('newDir')" @click="showHover('newDir')"
class="action" class="action"
@@ -38,7 +38,7 @@
</button> </button>
</div> </div>
<div v-if="user.perm.admin"> <div v-if="user?.perm.admin">
<button <button
class="action" class="action"
@click="toGlobalSettings" @click="toGlobalSettings"
@@ -113,9 +113,10 @@
</nav> </nav>
</template> </template>
<script> <script setup lang="ts">
import { reactive } from "vue"; import { reactive, ref, computed, watch, onUnmounted } from "vue";
import { mapActions, mapState } from "pinia"; import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
@@ -135,84 +136,85 @@ import prettyBytes from "pretty-bytes";
const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 }; const USAGE_DEFAULT = { used: "0 B", total: "0 B", usedPercentage: 0 };
export default { const route = useRoute();
name: "sidebar", const router = useRouter();
setup() {
const usage = reactive(USAGE_DEFAULT); const authStore = useAuthStore();
return { usage, usageAbortController: new AbortController() }; const fileStore = useFileStore();
}, const layoutStore = useLayoutStore();
components: {
ProgressBar, const { user, isLoggedIn } = storeToRefs(authStore);
}, const { isFiles } = storeToRefs(fileStore);
inject: ["$showError"], const { currentPromptName } = storeToRefs(layoutStore);
computed: { const { closeHovers, showHover } = layoutStore;
...mapState(useAuthStore, ["user", "isLoggedIn"]),
...mapState(useFileStore, ["isFiles", "reload"]), const usage = reactive(USAGE_DEFAULT);
...mapState(useLayoutStore, ["currentPromptName"]), const usageAbortController = ref(new AbortController());
active() {
return this.currentPromptName === "sidebar"; const active = computed(() => {
}, return currentPromptName.value === "sidebar";
signup: () => signup, });
version: () => version,
disableExternal: () => disableExternal, const canLogout = !noAuth && loginPage;
disableUsedPercentage: () => disableUsedPercentage,
canLogout: () => !noAuth && loginPage, const abortOngoingFetchUsage = () => {
}, usageAbortController.value.abort();
methods: {
...mapActions(useLayoutStore, ["closeHovers", "showHover"]),
abortOngoingFetchUsage() {
this.usageAbortController.abort();
},
async fetchUsage() {
const path = this.$route.path.endsWith("/")
? this.$route.path
: this.$route.path + "/";
let usageStats = USAGE_DEFAULT;
if (this.disableUsedPercentage) {
return Object.assign(this.usage, usageStats);
}
try {
this.abortOngoingFetchUsage();
this.usageAbortController = new AbortController();
const usage = await api.usage(path, this.usageAbortController.signal);
usageStats = {
used: prettyBytes(usage.used, { binary: true }),
total: prettyBytes(usage.total, { binary: true }),
usedPercentage: Math.round((usage.used / usage.total) * 100),
};
} finally {
return Object.assign(this.usage, usageStats);
}
},
toRoot() {
this.$router.push({ path: "/files" });
this.closeHovers();
},
toAccountSettings() {
this.$router.push({ path: "/settings/profile" });
this.closeHovers();
},
toGlobalSettings() {
this.$router.push({ path: "/settings/global" });
this.closeHovers();
},
help() {
this.showHover("help");
},
logout: auth.logout,
},
watch: {
$route: {
handler(to) {
if (to.path.includes("/files")) {
this.fetchUsage();
}
},
immediate: true,
},
},
unmounted() {
this.abortOngoingFetchUsage();
},
}; };
const fetchUsage = async () => {
const path = route.path.endsWith("/") ? route.path : route.path + "/";
let usageStats = USAGE_DEFAULT;
if (disableUsedPercentage) {
return Object.assign(usage, usageStats);
}
try {
abortOngoingFetchUsage();
usageAbortController.value = new AbortController();
const usageData = await api.usage(path, usageAbortController.value.signal);
usageStats = {
used: prettyBytes(usageData.used, { binary: true }),
total: prettyBytes(usageData.total, { binary: true }),
usedPercentage: Math.round((usageData.used / usageData.total) * 100),
};
} finally {
return Object.assign(usage, usageStats);
}
};
const toRoot = () => {
router.push({ path: "/files" });
closeHovers();
};
const toAccountSettings = () => {
router.push({ path: "/settings/profile" });
closeHovers();
};
const toGlobalSettings = () => {
router.push({ path: "/settings/global" });
closeHovers();
};
const help = () => {
showHover("help");
};
const logout = () => {
auth.logout();
};
watch(
() => route.path,
(newPath) => {
if (newPath.includes("/files")) {
fetchUsage();
}
},
{ immediate: true }
);
onUnmounted(() => {
abortOngoingFetchUsage();
});
</script> </script>

View File

@@ -8,7 +8,7 @@
<p>{{ $t("prompts.copyMessage") }}</p> <p>{{ $t("prompts.copyMessage") }}</p>
<file-list <file-list
ref="fileList" ref="fileList"
@update:selected="(val) => (dest = val)" @update:selected="(val: string) => (dest = val)"
tabindex="1" tabindex="1"
/> />
</div> </div>
@@ -17,10 +17,10 @@
class="card-action" class="card-action"
style="display: flex; align-items: center; justify-content: space-between" style="display: flex; align-items: center; justify-content: space-between"
> >
<template v-if="user.perm.create"> <template v-if="user?.perm.create">
<button <button
class="button button--flat" class="button button--flat"
@click="$refs.fileList.createDir()" @click="fileList?.createDir()"
:aria-label="$t('sidebar.newFolder')" :aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"
style="justify-self: left" style="justify-self: left"
@@ -53,8 +53,10 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState, mapWritableState } from "pinia"; import { ref, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRoute, useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
@@ -64,90 +66,84 @@ import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
export default { const route = useRoute();
name: "copy", const router = useRouter();
components: { FileList }, const $showError = inject<(error: unknown) => void>("$showError");
data: function () {
return {
current: window.location.pathname,
dest: null,
};
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
copy: async function (event) {
event.preventDefault();
const items = [];
// Create a new promise for each file. const fileStore = useFileStore();
for (const item of this.selected) { const layoutStore = useLayoutStore();
items.push({ const authStore = useAuthStore();
from: this.req.items[item].url,
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name,
});
}
const action = async (overwrite, rename) => { const { req, selected } = storeToRefs(fileStore);
buttons.loading("copy"); const { user } = storeToRefs(authStore);
const { showHover, closeHovers } = layoutStore;
await api const fileList = ref<InstanceType<typeof FileList> | null>(null);
.copy(items, overwrite, rename) const dest = ref<string | null>(null);
.then(() => {
buttons.success("copy");
this.preselect = removePrefix(items[0].to);
if (this.$route.path === this.dest) { const copy = async (event: Event) => {
this.reload = true; event.preventDefault();
const items: Array<{ from: string; to: string; name: string }> = [];
return; // Create a new promise for each file.
} for (const item of selected.value) {
items.push({
from: req.value!.items[item].url,
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
name: req.value!.items[item].name,
});
}
this.$router.push({ path: this.dest }); const action = async (overwrite: boolean, rename: boolean) => {
}) buttons.loading("copy");
.catch((e) => {
buttons.done("copy");
this.$showError(e);
});
};
if (this.$route.path === this.dest) { await api
this.closeHovers(); .copy(items, overwrite, rename)
action(false, true); .then(() => {
buttons.success("copy");
fileStore.preselect = removePrefix(items[0].to);
return; if (route.path === dest.value) {
} fileStore.reload = true;
return;
}
const dstItems = (await api.fetch(this.dest)).items; router.push({ path: dest.value! });
const conflict = upload.checkConflict(items, dstItems); })
.catch((e) => {
buttons.done("copy");
$showError?.(e);
});
};
let overwrite = false; if (route.path === dest.value) {
let rename = false; closeHovers();
action(false, true);
return;
}
if (conflict) { const dstItems = (await api.fetch(dest.value!)).items;
this.showHover({ const conflict = upload.checkConflict(items as any, dstItems);
prompt: "replace-rename",
confirm: (event, option) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault(); let overwrite = false;
this.closeHovers(); let rename = false;
action(overwrite, rename);
},
});
return; if (conflict) {
} showHover({
prompt: "replace-rename",
confirm: (event: Event, option: string) => {
overwrite = option == "overwrite";
rename = option == "rename";
action(overwrite, rename); event.preventDefault();
}, closeHovers();
}, action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
}; };
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="card floating"> <div class="card floating">
<div class="card-content"> <div class="card-content">
<p v-if="!this.isListing || selectedCount === 1"> <p v-if="!isListing || selectedCount === 1">
{{ $t("prompts.deleteMessageSingle") }} {{ $t("prompts.deleteMessageSingle") }}
</p> </p>
<p v-else> <p v-else>
@@ -32,67 +32,62 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState, mapWritableState } from "pinia"; import { inject } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { files as api } from "@/api"; import { files as api } from "@/api";
import buttons from "@/utils/buttons"; import buttons from "@/utils/buttons";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const route = useRoute();
name: "delete", const $showError = inject<(error: unknown) => void>("$showError");
inject: ["$showError"],
computed: {
...mapState(useFileStore, [
"isListing",
"selectedCount",
"req",
"selected",
]),
...mapState(useLayoutStore, ["currentPrompt"]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
submit: async function () {
buttons.loading("delete");
try { const fileStore = useFileStore();
if (!this.isListing) { const layoutStore = useLayoutStore();
await api.remove(this.$route.path);
buttons.success("delete");
this.currentPrompt?.confirm(); const { isListing, selectedCount, req, selected } = storeToRefs(fileStore);
this.closeHovers(); const { currentPrompt } = storeToRefs(layoutStore);
return; const { closeHovers } = layoutStore;
}
this.closeHovers(); const submit = async () => {
buttons.loading("delete");
if (this.selectedCount === 0) { try {
return; if (!isListing.value) {
} await api.remove(route.path);
buttons.success("delete");
const promises = []; currentPrompt.value?.confirm();
for (const index of this.selected) { closeHovers();
promises.push(api.remove(this.req.items[index].url)); return;
} }
await Promise.all(promises); closeHovers();
buttons.success("delete");
const nearbyItem = if (selectedCount.value === 0) {
this.req.items[Math.max(0, Math.min(this.selected) - 1)]; return;
}
this.preselect = nearbyItem?.path; const promises = [];
for (const index of selected.value) {
promises.push(api.remove(req.value!.items[index].url));
}
this.reload = true; await Promise.all(promises);
} catch (e) { buttons.success("delete");
buttons.done("delete");
this.$showError(e); const nearbyItem =
if (this.isListing) this.reload = true; req.value!.items[Math.max(0, Math.min(...selected.value) - 1)];
}
}, fileStore.preselect = nearbyItem?.path;
},
fileStore.reload = true;
} catch (e) {
buttons.done("delete");
$showError?.(e);
if (isListing.value) fileStore.reload = true;
}
}; };
</script> </script>

View File

@@ -33,8 +33,4 @@ import { useI18n } from "vue-i18n";
const layoutStore = useLayoutStore(); const layoutStore = useLayoutStore();
const { t } = useI18n(); const { t } = useI18n();
// const emit = defineEmits<{
// (e: "confirm"): void;
// }>();
</script> </script>

View File

@@ -17,7 +17,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="currentPrompt.saveAction" @click="currentPrompt?.saveAction"
:aria-label="$t('buttons.saveChanges')" :aria-label="$t('buttons.saveChanges')"
:title="$t('buttons.saveChanges')" :title="$t('buttons.saveChanges')"
tabindex="1" tabindex="1"
@@ -26,7 +26,7 @@
</button> </button>
<button <button
id="focus-prompt" id="focus-prompt"
@click="currentPrompt.confirm" @click="currentPrompt?.confirm"
class="button button--flat button--red" class="button button--flat button--red"
:aria-label="$t('buttons.discardChanges')" :aria-label="$t('buttons.discardChanges')"
:title="$t('buttons.discardChanges')" :title="$t('buttons.discardChanges')"
@@ -38,17 +38,11 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { mapActions, mapState } from "pinia";
export default { const layoutStore = useLayoutStore();
name: "discardEditorChanges", const { currentPrompt } = storeToRefs(layoutStore);
computed: { const { closeHovers } = layoutStore;
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -24,8 +24,10 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapState, mapActions } from "pinia"; import { ref, computed, inject, onMounted, onUnmounted } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
@@ -34,147 +36,162 @@ import url from "@/utils/url";
import { files } from "@/api"; import { files } from "@/api";
import { StatusError } from "@/api/utils.js"; import { StatusError } from "@/api/utils.js";
export default { const props = defineProps<{
name: "file-list", exclude?: string[];
props: { }>();
exclude: {
type: Array,
default: () => [],
},
},
data: function () {
return {
items: [],
touches: {
id: "",
count: 0,
},
selected: null,
current: window.location.pathname,
nextAbortController: new AbortController(),
};
},
inject: ["$showError"],
computed: {
...mapState(useAuthStore, ["user"]),
...mapState(useFileStore, ["req"]),
nav() {
return decodeURIComponent(this.current);
},
},
mounted() {
this.fillOptions(this.req);
},
unmounted() {
this.abortOngoingNext();
},
methods: {
...mapActions(useLayoutStore, ["showHover"]),
abortOngoingNext() {
this.nextAbortController.abort();
},
fillOptions(req) {
// Sets the current path and resets
// the current items.
this.current = req.url;
this.items = [];
this.$emit("update:selected", this.current); const emit = defineEmits<{
"update:selected": [value: string];
}>();
// If the path isn't the root path, const route = useRoute();
// show a button to navigate to the previous const $showError = inject<(error: unknown) => void>("$showError");
// directory.
if (req.url !== "/files/") {
this.items.push({
name: "..",
url: url.removeLastDir(req.url) + "/",
});
}
// If this folder is empty, finish here. const authStore = useAuthStore();
if (req.items === null) return; const fileStore = useFileStore();
const layoutStore = useLayoutStore();
// Otherwise we add every directory to the const { user } = storeToRefs(authStore);
// move options. const { req } = storeToRefs(fileStore);
for (const item of req.items) { const { showHover } = layoutStore;
if (!item.isDir) continue;
if (this.exclude?.includes(item.url)) continue;
this.items.push({ const items = ref<Array<{ name: string; url: string }>>([]);
name: item.name, const touches = ref({
url: item.url, id: "",
}); count: 0,
} });
}, const selected = ref<string | null>(null);
next: function (event) { const current = ref(window.location.pathname);
// Retrieves the URL of the directory the user const nextAbortController = ref(new AbortController());
// just clicked in and fill the options with its
// content.
const uri = event.currentTarget.dataset.url;
this.abortOngoingNext();
this.nextAbortController = new AbortController();
files
.fetch(uri, this.nextAbortController.signal)
.then(this.fillOptions)
.catch((e) => {
if (e instanceof StatusError && e.is_canceled) {
return;
}
this.$showError(e);
});
},
touchstart(event) {
const url = event.currentTarget.dataset.url;
// In 300 milliseconds, we shall reset the count. const nav = computed(() => {
setTimeout(() => { return decodeURIComponent(current.value);
this.touches.count = 0; });
}, 300);
// If the element the user is touching const abortOngoingNext = () => {
// is different from the last one he touched, nextAbortController.value.abort();
// reset the count.
if (this.touches.id !== url) {
this.touches.id = url;
this.touches.count = 1;
return;
}
this.touches.count++;
// If there is more than one touch already,
// open the next screen.
if (this.touches.count > 1) {
this.next(event);
}
},
itemClick: function (event) {
if (this.user.singleClick) this.next(event);
else this.select(event);
},
select: function (event) {
// If the element is already selected, unselect it.
if (this.selected === event.currentTarget.dataset.url) {
this.selected = null;
this.$emit("update:selected", this.current);
return;
}
// Otherwise select the element.
this.selected = event.currentTarget.dataset.url;
this.$emit("update:selected", this.selected);
},
createDir: async function () {
this.showHover({
prompt: "newDir",
action: null,
confirm: null,
props: {
redirect: false,
base: this.current === this.$route.path ? null : this.current,
},
});
},
},
}; };
const fillOptions = (reqData: any) => {
// Sets the current path and resets
// the current items.
current.value = reqData.url;
items.value = [];
emit("update:selected", current.value);
// If the path isn't the root path,
// show a button to navigate to the previous
// directory.
if (reqData.url !== "/files/") {
items.value.push({
name: "..",
url: url.removeLastDir(reqData.url) + "/",
});
}
// If this folder is empty, finish here.
if (reqData.items === null) return;
// Otherwise we add every directory to the
// move options.
for (const item of reqData.items) {
if (!item.isDir) continue;
if (props.exclude?.includes(item.url)) continue;
items.value.push({
name: item.name,
url: item.url,
});
}
};
const next = (event: Event) => {
// Retrieves the URL of the directory the user
// just clicked in and fill the options with its
// content.
const uri = (event.currentTarget as HTMLElement).dataset.url!;
abortOngoingNext();
nextAbortController.value = new AbortController();
files
.fetch(uri, nextAbortController.value.signal)
.then(fillOptions)
.catch((e) => {
if (e instanceof StatusError && e.is_canceled) {
return;
}
$showError?.(e);
});
};
const touchstart = (event: Event) => {
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// In 300 milliseconds, we shall reset the count.
setTimeout(() => {
touches.value.count = 0;
}, 300);
// If the element the user is touching
// is different from the last one he touched,
// reset the count.
if (touches.value.id !== urlValue) {
touches.value.id = urlValue;
touches.value.count = 1;
return;
}
touches.value.count++;
// If there is more than one touch already,
// open the next screen.
if (touches.value.count > 1) {
next(event);
}
};
const itemClick = (event: Event) => {
if (user.value?.singleClick) next(event);
else select(event);
};
const select = (event: Event) => {
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
// If the element is already selected, unselect it.
if (selected.value === urlValue) {
selected.value = null;
emit("update:selected", current.value);
return;
}
// Otherwise select the element.
selected.value = urlValue;
emit("update:selected", selected.value);
};
const createDir = async () => {
showHover({
prompt: "newDir",
action: undefined,
confirm: undefined,
props: {
redirect: false,
base: current.value === route.path ? null : current.value,
},
});
};
onMounted(() => {
if (req.value) {
fillOptions(req.value);
}
});
onUnmounted(() => {
abortOngoingNext();
});
defineExpose({
createDir,
});
</script> </script>

View File

@@ -34,14 +34,8 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const { closeHovers } = useLayoutStore();
name: "help",
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -29,10 +29,10 @@
<template v-if="dir && selected.length === 0"> <template v-if="dir && selected.length === 0">
<p> <p>
<strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req.numFiles }} <strong>{{ $t("prompts.numberFiles") }}:</strong> {{ req?.numFiles }}
</p> </p>
<p> <p>
<strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req.numDirs }} <strong>{{ $t("prompts.numberDirs") }}:</strong> {{ req?.numDirs }}
</p> </p>
</template> </template>
@@ -99,98 +99,100 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { computed, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { filesize } from "@/utils"; import { filesize } from "@/utils";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { files as api } from "@/api"; import { files as api } from "@/api";
export default { const route = useRoute();
name: "info", const $showError = inject<(error: unknown) => void>("$showError");
inject: ["$showError"],
computed: {
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
humanSize: function () {
if (this.selectedCount === 0 || !this.isListing) {
return filesize(this.req.size);
}
let sum = 0; const fileStore = useFileStore();
const layoutStore = useLayoutStore();
for (const selected of this.selected) { const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
sum += this.req.items[selected].size; const { closeHovers } = layoutStore;
}
return filesize(sum); const humanSize = computed(() => {
}, if (selectedCount.value === 0 || !isListing.value) {
humanTime: function () { return filesize(req.value?.size ?? 0);
if (this.selectedCount === 0) { }
return dayjs(this.req.modified).fromNow();
}
return dayjs(this.req.items[this.selected[0]].modified).fromNow(); let sum = 0;
},
modTime: function () {
if (this.selectedCount === 0) {
return new Date(Date.parse(this.req.modified)).toLocaleString();
}
return new Date( for (const selectedIdx of selected.value) {
Date.parse(this.req.items[this.selected[0]].modified) sum += req.value?.items[selectedIdx]?.size ?? 0;
).toLocaleString(); }
},
name: function () {
return this.selectedCount === 0
? this.req.name
: this.req.items[this.selected[0]].name;
},
dir: function () {
return (
this.selectedCount > 1 ||
(this.selectedCount === 0
? this.req.isDir
: this.req.items[this.selected[0]].isDir)
);
},
resolution: function () {
if (this.selectedCount === 1) {
const selectedItem = this.req.items[this.selected[0]];
if (selectedItem && selectedItem.type === "image") {
return selectedItem.resolution;
}
} else if (this.req && this.req.type === "image") {
return this.req.resolution;
}
return null;
},
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
checksum: async function (event, algo) {
event.preventDefault();
let link; return filesize(sum);
});
if (this.selectedCount) { const humanTime = computed(() => {
link = this.req.items[this.selected[0]].url; if (selectedCount.value === 0) {
} else { return dayjs(req.value?.modified).fromNow();
link = this.$route.path; }
}
try { return dayjs(req.value?.items[selected.value[0]]?.modified).fromNow();
const hash = await api.checksum(link, algo); });
event.target.textContent = hash;
} catch (e) { const modTime = computed(() => {
this.$showError(e); if (selectedCount.value === 0) {
} return new Date(Date.parse(req.value?.modified ?? "")).toLocaleString();
}, }
},
return new Date(
Date.parse(req.value?.items[selected.value[0]]?.modified ?? "")
).toLocaleString();
});
const name = computed(() => {
return selectedCount.value === 0
? (req.value?.name ?? "")
: (req.value?.items[selected.value[0]]?.name ?? "");
});
const dir = computed(() => {
return (
selectedCount.value > 1 ||
(selectedCount.value === 0
? (req.value?.isDir ?? false)
: (req.value?.items[selected.value[0]]?.isDir ?? false))
);
});
const resolution = computed(() => {
if (selectedCount.value === 1) {
const selectedItem = req.value?.items[selected.value[0]];
if (selectedItem && selectedItem.type === "image") {
return (selectedItem as any).resolution;
}
} else if (req.value && req.value.type === "image") {
return (req.value as any).resolution;
}
return null;
});
const checksum = async (event: Event, algo: string) => {
event.preventDefault();
let link;
if (selectedCount.value) {
link = req.value?.items[selected.value[0]]?.url ?? "";
} else {
link = route.path;
}
try {
const hash = await api.checksum(link, algo as any);
(event.target as HTMLElement).textContent = hash;
} catch (e) {
$showError?.(e);
}
}; };
</script> </script>

View File

@@ -7,7 +7,7 @@
<div class="card-content"> <div class="card-content">
<file-list <file-list
ref="fileList" ref="fileList"
@update:selected="(val) => (dest = val)" @update:selected="(val: string) => (dest = val)"
:exclude="excludedFolders" :exclude="excludedFolders"
tabindex="1" tabindex="1"
/> />
@@ -17,10 +17,10 @@
class="card-action" class="card-action"
style="display: flex; align-items: center; justify-content: space-between" style="display: flex; align-items: center; justify-content: space-between"
> >
<template v-if="user.perm.create"> <template v-if="user?.perm.create">
<button <button
class="button button--flat" class="button button--flat"
@click="$refs.fileList.createDir()" @click="fileList?.createDir()"
:aria-label="$t('sidebar.newFolder')" :aria-label="$t('sidebar.newFolder')"
:title="$t('sidebar.newFolder')" :title="$t('sidebar.newFolder')"
style="justify-self: left" style="justify-self: left"
@@ -54,8 +54,10 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState, mapWritableState } from "pinia"; import { ref, computed, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
@@ -65,80 +67,76 @@ import buttons from "@/utils/buttons";
import * as upload from "@/utils/upload"; import * as upload from "@/utils/upload";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
export default { const router = useRouter();
name: "move", const $showError = inject<(error: unknown) => void>("$showError");
components: { FileList },
data: function () {
return {
current: window.location.pathname,
dest: null,
};
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, ["req", "selected"]),
...mapState(useAuthStore, ["user"]),
...mapWritableState(useFileStore, ["preselect"]),
excludedFolders() {
return this.selected
.filter((idx) => this.req.items[idx].isDir)
.map((idx) => this.req.items[idx].url);
},
},
methods: {
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
move: async function (event) {
event.preventDefault();
const items = [];
for (const item of this.selected) { const fileStore = useFileStore();
items.push({ const layoutStore = useLayoutStore();
from: this.req.items[item].url, const authStore = useAuthStore();
to: this.dest + encodeURIComponent(this.req.items[item].name),
name: this.req.items[item].name,
});
}
const action = async (overwrite, rename) => { const { req, selected } = storeToRefs(fileStore);
buttons.loading("move"); const { user } = storeToRefs(authStore);
const { showHover, closeHovers } = layoutStore;
await api const fileList = ref<InstanceType<typeof FileList> | null>(null);
.move(items, overwrite, rename) const dest = ref<string | null>(null);
.then(() => {
buttons.success("move");
this.preselect = removePrefix(items[0].to);
this.$router.push({ path: this.dest });
})
.catch((e) => {
buttons.done("move");
this.$showError(e);
});
};
const dstItems = (await api.fetch(this.dest)).items; const excludedFolders = computed(() => {
const conflict = upload.checkConflict(items, dstItems); return selected.value
.filter((idx) => req.value!.items[idx].isDir)
.map((idx) => req.value!.items[idx].url);
});
let overwrite = false; const move = async (event: Event) => {
let rename = false; event.preventDefault();
const items: Array<{ from: string; to: string; name: string }> = [];
if (conflict) { for (const item of selected.value) {
this.showHover({ items.push({
prompt: "replace-rename", from: req.value!.items[item].url,
confirm: (event, option) => { to: dest.value! + encodeURIComponent(req.value!.items[item].name),
overwrite = option == "overwrite"; name: req.value!.items[item].name,
rename = option == "rename"; });
}
event.preventDefault(); const action = async (overwrite: boolean, rename: boolean) => {
this.closeHovers(); buttons.loading("move");
action(overwrite, rename);
},
});
return; await api
} .move(items, overwrite, rename)
.then(() => {
buttons.success("move");
fileStore.preselect = removePrefix(items[0].to);
router.push({ path: dest.value! });
})
.catch((e) => {
buttons.done("move");
$showError?.(e);
});
};
action(overwrite, rename); const dstItems = (await api.fetch(dest.value!)).items;
}, const conflict = upload.checkConflict(items as any, dstItems);
},
let overwrite = false;
let rename = false;
if (conflict) {
showHover({
prompt: "replace-rename",
confirm: (event: Event, option: string) => {
overwrite = option == "overwrite";
rename = option == "rename";
event.preventDefault();
closeHovers();
action(overwrite, rename);
},
});
return;
}
action(overwrite, rename);
}; };
</script> </script>

View File

@@ -40,80 +40,74 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState, mapWritableState } from "pinia"; import { ref, onMounted, inject } from "vue";
import { storeToRefs } from "pinia";
import { useRouter } from "vue-router";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import url from "@/utils/url"; import url from "@/utils/url";
import { files as api } from "@/api"; import { files as api } from "@/api";
import { removePrefix } from "@/api/utils"; import { removePrefix } from "@/api/utils";
export default { const router = useRouter();
name: "rename", const $showError = inject<(error: unknown) => void>("$showError");
data: function () {
return {
name: "",
};
},
created() {
this.name = this.oldName();
},
inject: ["$showError"],
computed: {
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
...mapWritableState(useFileStore, ["reload", "preselect"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
cancel: function () {
this.closeHovers();
},
oldName: function () {
if (!this.isListing) {
return this.req.name;
}
if (this.selectedCount === 0 || this.selectedCount > 1) { const fileStore = useFileStore();
// This shouldn't happen. const layoutStore = useLayoutStore();
return;
}
return this.req.items[this.selected[0]].name; const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
}, const { closeHovers } = layoutStore;
submit: async function () {
let oldLink = "";
let newLink = "";
if (!this.isListing) { const name = ref("");
oldLink = this.req.url;
} else {
oldLink = this.req.items[this.selected[0]].url;
}
newLink = const oldName = (): string => {
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name); if (!isListing.value) {
return req.value?.name ?? "";
}
try { if (selectedCount.value === 0 || selectedCount.value > 1) {
await api.move([{ from: oldLink, to: newLink }]); // This shouldn't happen.
if (!this.isListing) { return "";
this.$router.push({ path: newLink }); }
return;
}
this.preselect = removePrefix(newLink); return req.value?.items[selected.value[0]].name ?? "";
};
this.reload = true; onMounted(() => {
} catch (e) { name.value = oldName();
this.$showError(e); });
}
this.closeHovers(); const submit = async () => {
}, let oldLink = "";
}, let newLink = "";
if (!req.value) {
return;
}
if (!isListing.value) {
oldLink = req.value.url;
} else {
oldLink = req.value.items[selected.value[0]].url;
}
newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(name.value);
try {
await api.move([{ from: oldLink, to: newLink }]);
if (!isListing.value) {
router.push({ path: newLink });
return;
}
fileStore.preselect = removePrefix(newLink);
fileStore.reload = true;
} catch (e) {
$showError?.(e);
}
closeHovers();
}; };
</script> </script>

View File

@@ -20,7 +20,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="currentPrompt.action" @click="currentPrompt?.action"
:aria-label="$t('buttons.continue')" :aria-label="$t('buttons.continue')"
:title="$t('buttons.continue')" :title="$t('buttons.continue')"
tabindex="2" tabindex="2"
@@ -30,7 +30,7 @@
<button <button
id="focus-prompt" id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="currentPrompt.confirm" @click="currentPrompt?.confirm"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1" tabindex="1"
@@ -41,17 +41,11 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const layoutStore = useLayoutStore();
name: "replace", const { currentPrompt } = storeToRefs(layoutStore);
computed: { const { closeHovers } = layoutStore;
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -20,7 +20,7 @@
</button> </button>
<button <button
class="button button--flat button--blue" class="button button--flat button--blue"
@click="(event) => currentPrompt.confirm(event, 'rename')" @click="(event) => currentPrompt?.confirm(event, 'rename')"
:aria-label="$t('buttons.rename')" :aria-label="$t('buttons.rename')"
:title="$t('buttons.rename')" :title="$t('buttons.rename')"
tabindex="2" tabindex="2"
@@ -30,7 +30,7 @@
<button <button
id="focus-prompt" id="focus-prompt"
class="button button--flat button--red" class="button button--flat button--red"
@click="(event) => currentPrompt.confirm(event, 'overwrite')" @click="(event) => currentPrompt?.confirm(event, 'overwrite')"
:aria-label="$t('buttons.replace')" :aria-label="$t('buttons.replace')"
:title="$t('buttons.replace')" :title="$t('buttons.replace')"
tabindex="1" tabindex="1"
@@ -41,17 +41,11 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const layoutStore = useLayoutStore();
name: "replace-rename", const { currentPrompt } = storeToRefs(layoutStore);
computed: { const { closeHovers } = layoutStore;
...mapState(useLayoutStore, ["currentPrompt"]),
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
},
};
</script> </script>

View File

@@ -129,138 +129,146 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { ref, computed, inject, onBeforeMount } from "vue";
import { storeToRefs } from "pinia";
import { useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import { useFileStore } from "@/stores/file"; import { useFileStore } from "@/stores/file";
import { share as api } from "@/api"; import { share as api } from "@/api";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
import { copy } from "@/utils/clipboard"; import { copy } from "@/utils/clipboard";
export default { const route = useRoute();
name: "share", const { t } = useI18n();
data: function () { const $showError = inject<(error: unknown) => void>("$showError");
return { const $showSuccess = inject<(message: string) => void>("$showSuccess");
time: 0,
unit: "hours",
links: [],
clip: null,
password: "",
listing: true,
};
},
inject: ["$showError", "$showSuccess"],
computed: {
...mapState(useFileStore, [
"req",
"selected",
"selectedCount",
"isListing",
]),
url() {
if (!this.isListing) {
return this.$route.path;
}
if (this.selectedCount === 0 || this.selectedCount > 1) { const fileStore = useFileStore();
// This shouldn't happen. const layoutStore = useLayoutStore();
return;
}
return this.req.items[this.selected[0]].url; const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
const { closeHovers } = layoutStore;
const time = ref(0);
const unit = ref("hours");
const links = ref<any[]>([]);
const password = ref("");
const listing = ref(true);
const url = computed(() => {
if (!isListing.value) {
return route.path;
}
if (selectedCount.value === 0 || selectedCount.value > 1) {
// This shouldn't happen.
return "";
}
return req.value?.items[selected.value[0]].url ?? "";
});
const copyToClipboard = (text: string) => {
copy({ text }).then(
() => {
// clipboard successfully set
$showSuccess?.(t("success.linkCopied"));
}, },
}, () => {
async beforeMount() { // clipboard write failed
try { copy({ text }, { permission: true }).then(
const links = await api.get(this.url);
this.links = links;
this.sort();
if (this.links.length == 0) {
this.listing = false;
}
} catch (e) {
this.$showError(e);
}
},
methods: {
...mapActions(useLayoutStore, ["closeHovers"]),
copyToClipboard: function (text) {
copy({ text }).then(
() => { () => {
// clipboard successfully set // clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied")); $showSuccess?.(t("success.linkCopied"));
}, },
() => { (e) => {
// clipboard write failed // clipboard write failed
copy({ text }, { permission: true }).then( $showError?.(e);
() => {
// clipboard successfully set
this.$showSuccess(this.$t("success.linkCopied"));
},
(e) => {
// clipboard write failed
this.$showError(e);
}
);
} }
); );
}, }
submit: async function () { );
try {
let res = null;
if (!this.time) {
res = await api.create(this.url, this.password);
} else {
res = await api.create(this.url, this.password, this.time, this.unit);
}
this.links.push(res);
this.sort();
this.time = 0;
this.unit = "hours";
this.password = "";
this.listing = true;
} catch (e) {
this.$showError(e);
}
},
deleteLink: async function (event, link) {
event.preventDefault();
try {
await api.remove(link.hash);
this.links = this.links.filter((item) => item.hash !== link.hash);
if (this.links.length == 0) {
this.listing = false;
}
} catch (e) {
this.$showError(e);
}
},
humanTime(time) {
return dayjs(time * 1000).fromNow();
},
buildLink(share) {
return api.getShareURL(share);
},
sort() {
this.links = this.links.sort((a, b) => {
if (a.expire === 0) return -1;
if (b.expire === 0) return 1;
return new Date(a.expire) - new Date(b.expire);
});
},
switchListing() {
if (this.links.length == 0 && !this.listing) {
this.closeHovers();
}
this.listing = !this.listing;
},
},
}; };
const submit = async () => {
try {
let res = null;
if (!time.value) {
res = await api.create(url.value, password.value);
} else {
res = await api.create(
url.value,
password.value,
String(time.value),
unit.value
);
}
links.value.push(res);
sort();
time.value = 0;
unit.value = "hours";
password.value = "";
listing.value = true;
} catch (e) {
$showError?.(e);
}
};
const deleteLink = async (event: Event, link: any) => {
event.preventDefault();
try {
await api.remove(link.hash);
links.value = links.value.filter((item) => item.hash !== link.hash);
if (links.value.length == 0) {
listing.value = false;
}
} catch (e) {
$showError?.(e);
}
};
const humanTime = (time: number) => {
return dayjs(time * 1000).fromNow();
};
const buildLink = (share: any) => {
return api.getShareURL(share);
};
const sort = () => {
links.value = links.value.sort((a, b) => {
if (a.expire === 0) return -1;
if (b.expire === 0) return 1;
return new Date(a.expire).getTime() - new Date(b.expire).getTime();
});
};
const switchListing = () => {
if (links.value.length == 0 && !listing.value) {
closeHovers();
}
listing.value = !listing.value;
};
onBeforeMount(async () => {
try {
const fetchedLinks = await api.get(url.value);
links.value = Array.isArray(fetchedLinks) ? fetchedLinks : [fetchedLinks];
sort();
if (links.value.length == 0) {
listing.value = false;
}
} catch (e) {
$showError?.(e);
}
});
</script> </script>

View File

@@ -27,20 +27,15 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { mapActions, mapState } from "pinia"; import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout"; import { useLayoutStore } from "@/stores/layout";
export default { const layoutStore = useLayoutStore();
name: "share-delete", const { currentPrompt } = storeToRefs(layoutStore);
computed: { const { closeHovers } = layoutStore;
...mapState(useLayoutStore, ["currentPrompt"]),
}, const submit = () => {
methods: { currentPrompt.value?.confirm();
...mapActions(useLayoutStore, ["closeHovers"]),
submit: function () {
this.currentPrompt?.confirm();
},
},
}; };
</script> </script>

View File

@@ -8,23 +8,27 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
export default { import { computed } from "vue";
name: "permissions",
props: ["commands"], const props = defineProps<{
computed: { commands: string[];
raw: { }>();
get() {
return this.commands.join(" "); const emit = defineEmits<{
}, "update:commands": [commands: string[]];
set(value) { }>();
if (value !== "") {
this.$emit("update:commands", value.split(" ")); const raw = computed({
} else { get() {
this.$emit("update:commands", []); return props.commands.join(" ");
}
},
},
}, },
}; set(value: string) {
if (value !== "") {
emit("update:commands", value.split(" "));
} else {
emit("update:commands", []);
}
},
});
</script> </script>

View File

@@ -6,61 +6,50 @@
</select> </select>
</template> </template>
<script> <script setup lang="ts">
import { markRaw } from "vue"; import { markRaw } from "vue";
export default { defineProps<{
name: "languages", locale: string;
props: ["locale"], }>();
data() {
const dataObj = {};
const locales = {
he: "עברית",
hr: "Hrvatski",
hu: "Magyar",
ar: "العربية",
ca: "Català",
cs: "Čeština",
de: "Deutsch",
el: "Ελληνικά",
en: "English",
es: "Español",
fr: "Français",
is: "Icelandic",
it: "Italiano",
ja: "日本語",
ko: "한국어",
"nl-be": "Dutch (Belgium)",
no: "Norsk",
pl: "Polski",
"pt-br": "Português",
pt: "Português (Brasil)",
ro: "Romanian",
ru: "Русский",
sk: "Slovenčina",
"sv-se": "Swedish (Sweden)",
tr: "Türkçe",
uk: "Українська",
vi: "Tiếng Việt",
"zh-cn": "中文 (简体)",
"zh-tw": "中文 (繁體)",
};
// Vue3 reactivity breaks with this configuration const emit = defineEmits<{
// so we need to use markRaw as a workaround "update:locale": [locale: string];
// https://github.com/vuejs/core/issues/3024 }>();
Object.defineProperty(dataObj, "locales", {
value: markRaw(locales),
configurable: false,
writable: false,
});
return dataObj; const locales = markRaw({
}, he: "עברית",
methods: { hr: "Hrvatski",
change(event) { hu: "Magyar",
this.$emit("update:locale", event.target.value); ar: "العربية",
}, ca: "Català",
}, cs: "Čeština",
de: "Deutsch",
el: "Ελληνικά",
en: "English",
es: "Español",
fr: "Français",
is: "Icelandic",
it: "Italiano",
ja: "日本語",
ko: "한국어",
"nl-be": "Dutch (Belgium)",
no: "Norsk",
pl: "Polski",
"pt-br": "Português",
pt: "Português (Brasil)",
ro: "Romanian",
ru: "Русский",
sk: "Slovenčina",
"sv-se": "Swedish (Sweden)",
tr: "Türkçe",
uk: "Українська",
vi: "Tiếng Việt",
"zh-cn": "中文 (简体)",
"zh-tw": "中文 (繁體)",
});
const change = (event: Event) => {
emit("update:locale", (event.target as HTMLSelectElement).value);
}; };
</script> </script>

View File

@@ -39,27 +39,28 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
import { computed } from "vue";
import { enableExec } from "@/utils/constants"; import { enableExec } from "@/utils/constants";
export default {
name: "permissions",
props: ["perm"],
computed: {
admin: {
get() {
return this.perm.admin;
},
set(value) {
if (value) {
for (const key in this.perm) {
this.perm[key] = true;
}
}
this.perm.admin = value; const props = defineProps<{
}, perm: UserPermissions;
}, }>();
isExecEnabled: () => enableExec,
const admin = computed({
get() {
return props.perm.admin;
}, },
}; set(value: boolean) {
if (value) {
for (const key in props.perm) {
props.perm[key as keyof UserPermissions] = true;
}
}
props.perm.admin = value;
},
});
const isExecEnabled = enableExec;
</script> </script>

View File

@@ -32,32 +32,44 @@
</form> </form>
</template> </template>
<script> <script setup lang="ts">
export default { interface Rule {
name: "rules-textarea", allow: boolean;
props: ["rules"], path: string;
methods: { regex: boolean;
remove(event, index) { regexp: {
event.preventDefault(); raw: string;
const rules = [...this.rules]; };
rules.splice(index, 1); }
this.$emit("update:rules", [...rules]);
},
create(event) {
event.preventDefault();
this.$emit("update:rules", [ const props = defineProps<{
...this.rules, rules: Rule[];
{ }>();
allow: true,
path: "", const emit = defineEmits<{
regex: false, "update:rules": [rules: Rule[]];
regexp: { }>();
raw: "",
}, const remove = (event: Event, index: number) => {
}, event.preventDefault();
]); const rules = [...props.rules];
rules.splice(index, 1);
emit("update:rules", [...rules]);
};
const create = (event: Event) => {
event.preventDefault();
emit("update:rules", [
...props.rules,
{
allow: true,
path: "",
regex: false,
regexp: {
raw: "",
},
}, },
}, ]);
}; };
</script> </script>

View File

@@ -80,12 +80,20 @@ const { t } = useI18n();
const createUserDirData = ref<boolean | null>(null); const createUserDirData = ref<boolean | null>(null);
const originalUserScope = ref<string | null>(null); const originalUserScope = ref<string | null>(null);
const props = defineProps<{ const props = defineProps<
user: IUserForm; | {
isNew: boolean; user: IUserForm;
isDefault: boolean; isNew: boolean;
createUserDir?: boolean; isDefault: false;
}>(); createUserDir?: boolean;
}
| {
user: SettingsDefaults;
isNew: boolean;
isDefault: true;
createUserDir?: boolean;
}
>();
onMounted(() => { onMounted(() => {
if (props.user.scope) { if (props.user.scope) {
@@ -108,6 +116,7 @@ watch(
() => props.user, () => props.user,
() => { () => {
if (!props.user?.perm?.admin) return; if (!props.user?.perm?.admin) return;
if (props.isDefault) return;
props.user.lockPassword = false; props.user.lockPassword = false;
} }
); );

View File

@@ -17,7 +17,7 @@ interface SettingsDefaults {
viewMode: ViewModeType; viewMode: ViewModeType;
singleClick: boolean; singleClick: boolean;
sorting: Sorting; sorting: Sorting;
perm: Permissions; perm: UserPermissions;
commands: any[]; commands: any[];
hideDotfiles: boolean; hideDotfiles: boolean;
dateFormat: boolean; dateFormat: boolean;

View File

@@ -4,7 +4,7 @@ interface IUser {
password: string; password: string;
scope: string; scope: string;
locale: string; locale: string;
perm: Permissions; perm: UserPermissions;
commands: string[]; commands: string[];
rules: IRule[]; rules: IRule[];
lockPassword: boolean; lockPassword: boolean;
@@ -20,20 +20,20 @@ type ViewModeType = "list" | "mosaic" | "mosaic gallery";
interface IUserForm { interface IUserForm {
id?: number; id?: number;
username?: string; username: string;
password?: string; password: string;
scope?: string; scope: string;
locale?: string; locale: string;
perm?: Permissions; perm: UserPermissions;
commands?: string[]; commands: string[];
rules?: IRule[]; rules: IRule[];
lockPassword?: boolean; lockPassword: boolean;
hideDotfiles?: boolean; hideDotfiles: boolean;
singleClick?: boolean; singleClick: boolean;
dateFormat?: boolean; dateFormat: boolean;
} }
interface Permissions { interface UserPermissions {
admin: boolean; admin: boolean;
copy: boolean; copy: boolean;
create: boolean; create: boolean;

2
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/dsoprea/go-exif/v3 v3.0.1 github.com/dsoprea/go-exif/v3 v3.0.1
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v4 v4.5.2
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/jellydator/ttlcache/v3 v3.4.0 github.com/jellydator/ttlcache/v3 v3.4.0

4
go.sum
View File

@@ -97,8 +97,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=

View File

@@ -9,8 +9,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v4"
"github.com/golang-jwt/jwt/v5/request" "github.com/golang-jwt/jwt/v4/request"
fbErrors "github.com/filebrowser/filebrowser/v2/errors" fbErrors "github.com/filebrowser/filebrowser/v2/errors"
"github.com/filebrowser/filebrowser/v2/users" "github.com/filebrowser/filebrowser/v2/users"
@@ -69,19 +69,15 @@ func withUser(fn handleFunc) handleFunc {
var tk authToken var tk authToken
token, err := request.ParseFromRequest(r, &extractor{}, keyFunc, request.WithClaims(&tk)) token, err := request.ParseFromRequest(r, &extractor{}, keyFunc, request.WithClaims(&tk))
if err != nil || !token.Valid { if err != nil || !token.Valid {
return http.StatusUnauthorized, nil return http.StatusUnauthorized, nil
} }
err = jwt.NewValidator(jwt.WithExpirationRequired()).Validate(tk) expired := !tk.VerifyExpiresAt(time.Now().Add(time.Hour), true)
if err != nil {
return http.StatusUnauthorized, nil
}
expiresSoon := tk.ExpiresAt != nil && time.Until(tk.ExpiresAt.Time) < time.Hour
updated := tk.IssuedAt != nil && tk.IssuedAt.Unix() < d.store.Users.LastUpdate(tk.User.ID) updated := tk.IssuedAt != nil && tk.IssuedAt.Unix() < d.store.Users.LastUpdate(tk.User.ID)
if expiresSoon || updated { if expired || updated {
w.Header().Add("X-Renew-Token", "true") w.Header().Add("X-Renew-Token", "true")
} }

14
http/headers_dev.go Normal file
View File

@@ -0,0 +1,14 @@
//go:build dev
package http
// global headers to append to every response
// cross-origin headers are necessary to be able to
// access them from a different URL during development
var globalHeaders = map[string]string{
"Cache-Control": "no-cache, no-store, must-revalidate",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "*",
"Access-Control-Allow-Credentials": "true",
}

27
tools.mk Normal file
View File

@@ -0,0 +1,27 @@
include common.mk
# tools
TOOLS_DIR := $(BASE_PATH)/tools
TOOLS_GO_DEPS := $(TOOLS_DIR)/go.mod $(TOOLS_DIR)/go.sum
TOOLS_BIN := $(TOOLS_DIR)/bin
$(eval $(shell mkdir -p $(TOOLS_BIN)))
PATH := $(TOOLS_BIN):$(PATH)
export PATH
.PHONY: clean-tools
clean-tools:
$Q rm -rf $(TOOLS_BIN)
goimports=$(TOOLS_BIN)/goimports
$(goimports): $(TOOLS_GO_DEPS)
$Q cd $(TOOLS_DIR) && $(go) build -o $@ golang.org/x/tools/cmd/goimports
golangci-lint=$(TOOLS_BIN)/golangci-lint
$(golangci-lint): $(TOOLS_GO_DEPS)
$Q cd $(TOOLS_DIR) && $(go) build -o $@ github.com/golangci/golangci-lint/v2/cmd/golangci-lint
# js tools
TOOLS_JS_DEPS=$(TOOLS_DIR)/node_modules/.modified
$(TOOLS_JS_DEPS): $(TOOLS_DIR)/package.json $(TOOLS_DIR)/yarn.lock
$Q cd ${TOOLS_DIR} && yarn install
$Q touch -am $@

216
tools/go.mod Normal file
View File

@@ -0,0 +1,216 @@
module github.com/filebrowser/filebrowser/v2/tools
go 1.25
require (
github.com/golangci/golangci-lint/v2 v2.6.1
golang.org/x/tools v0.38.0
)
require (
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
4d63.com/gochecknoglobals v0.2.2 // indirect
codeberg.org/chavacava/garif v0.2.0 // indirect
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
dev.gaijin.team/go/golib v0.6.0 // indirect
github.com/4meepo/tagalign v1.4.3 // indirect
github.com/Abirdcfly/dupword v0.1.7 // indirect
github.com/AdminBenni/iota-mixing v1.0.0 // indirect
github.com/AlwxSin/noinlineerr v1.0.5 // indirect
github.com/Antonboom/errname v1.1.1 // indirect
github.com/Antonboom/nilnil v1.1.1 // indirect
github.com/Antonboom/testifylint v1.6.4 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/Djarvur/go-err113 v0.1.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/MirrexOne/unqueryvet v1.2.1 // indirect
github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/alecthomas/go-check-sumtype v0.3.1 // indirect
github.com/alexkohler/nakedret/v2 v2.0.6 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alfatraining/structtag v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/alingse/nilnesserr v0.2.0 // indirect
github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect
github.com/ashanbrown/makezero/v2 v2.1.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.3 // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v4 v4.7.0 // indirect
github.com/bombsimon/wsl/v5 v5.3.0 // indirect
github.com/breml/bidichk v0.3.3 // indirect
github.com/breml/errchkjson v0.4.1 // indirect
github.com/butuzov/ireturn v0.4.0 // indirect
github.com/butuzov/mirror v1.3.0 // indirect
github.com/catenacyber/perfsprint v0.10.0 // indirect
github.com/ccojocar/zxcvbn-go v1.0.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charithe/durationcheck v0.0.11 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/ckaznocha/intrange v0.3.1 // indirect
github.com/curioswitch/go-reassign v0.3.0 // indirect
github.com/daixiang0/gci v0.13.7 // indirect
github.com/dave/dst v0.27.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.5.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/ettle/strcase v0.2.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.6 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/ghostiam/protogetter v0.3.17 // indirect
github.com/go-critic/go-critic v0.14.2 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.2.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/godoc-lint/godoc-lint v0.10.1 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golangci/asciicheck v0.5.0 // indirect
github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect
github.com/golangci/go-printf-func-name v0.1.1 // indirect
github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect
github.com/golangci/golines v0.0.0-20250217134842-442fd0091d95 // indirect
github.com/golangci/misspell v0.7.0 // indirect
github.com/golangci/plugin-module-register v0.1.2 // indirect
github.com/golangci/revgrep v0.8.0 // indirect
github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect
github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/gordonklaus/ineffassign v0.2.0 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
github.com/gostaticanalysis/comment v1.5.0 // indirect
github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect
github.com/gostaticanalysis/nilerr v0.1.2 // indirect
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jgautheron/goconst v1.8.2 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
github.com/jjti/go-spancheck v0.6.5 // indirect
github.com/julz/importas v0.2.0 // indirect
github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect
github.com/kisielk/errcheck v1.9.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.6 // indirect
github.com/kulti/thelper v0.7.1 // indirect
github.com/kunwardeep/paralleltest v1.0.15 // indirect
github.com/lasiar/canonicalheader v1.1.2 // indirect
github.com/ldez/exptostd v0.4.5 // indirect
github.com/ldez/gomoddirectives v0.7.1 // indirect
github.com/ldez/grignotin v0.10.1 // indirect
github.com/ldez/tagliatelle v0.7.2 // indirect
github.com/ldez/usetesting v0.5.0 // indirect
github.com/leonklingele/grouper v1.1.2 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/macabu/inamedparam v0.2.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect
github.com/manuelarte/funcorder v0.5.0 // indirect
github.com/maratori/testableexamples v1.0.0 // indirect
github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mgechev/revive v1.12.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moricho/tparallel v0.3.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nishanths/exhaustive v0.12.0 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.21.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polyfloyd/go-errorlint v1.8.0 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/raeperd/recvcheck v0.2.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/ryancurrah/gomodguard v1.4.1 // indirect
github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect
github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect
github.com/securego/gosec/v2 v2.22.10 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sivchari/containedctx v1.0.3 // indirect
github.com/sonatard/noctx v0.4.0 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.14.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.10.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.12.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/tetafro/godot v1.5.4 // indirect
github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect
github.com/timonwong/loggercheck v0.11.0 // indirect
github.com/tomarrell/wrapcheck/v2 v2.11.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/ultraware/funlen v0.2.0 // indirect
github.com/ultraware/whitespace v0.2.0 // indirect
github.com/uudashr/gocognit v1.2.0 // indirect
github.com/uudashr/iface v1.4.1 // indirect
github.com/xen0n/gosmopolitan v1.3.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yeya24/promlinter v0.3.0 // indirect
github.com/ykadowak/zerologlint v0.1.5 // indirect
gitlab.com/bosi/decorder v0.4.2 // indirect
go-simpler.org/musttag v0.14.0 // indirect
go-simpler.org/sloglint v0.11.1 // indirect
go.augendre.info/arangolint v0.3.1 // indirect
go.augendre.info/fatcontext v0.9.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
honnef.co/go/tools v0.6.1 // indirect
mvdan.cc/gofumpt v0.9.2 // indirect
mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect
)

1001
tools/go.sum Normal file

File diff suppressed because it is too large Load Diff

15
tools/tools.go Normal file
View File

@@ -0,0 +1,15 @@
//go:build tools
// +build tools
package tools
// Manage tool dependencies via go.mod.
//
// https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module
// https://github.com/golang/go/issues/25922
//
// nolint
import (
_ "github.com/golangci/golangci-lint/v2/cmd/golangci-lint"
_ "golang.org/x/tools/cmd/goimports"
)

View File

@@ -2,12 +2,6 @@
Most of the configuration can be understood through the command line interface documentation. To access it, you need to install File Browser and run `filebrowser --help`. In this page, we cover some specific, more complex, topics. Most of the configuration can be understood through the command line interface documentation. To access it, you need to install File Browser and run `filebrowser --help`. In this page, we cover some specific, more complex, topics.
## Flags as Environment Variables
In some situations, it is easier to use environment variables instead of flags. For example, if you're using our provided Docker image, it's easier for you to use environment variables to customize the settings instead of flags.
All flags should be available as environment variables prefixed with `FB_`. For example, the flag `--disable-thumbnails` is available as `FB_DISABLE_THUMBNAILS`.
## Custom Branding ## Custom Branding
You can customize File Browser to use your own branding. This includes the following: You can customize File Browser to use your own branding. This includes the following:

View File

@@ -10,7 +10,7 @@
> [!WARNING] > [!WARNING]
> >
> This project is on **maintenance-only** mode. For more information, read the information on [GitHub](https://github.com/filebrowser/filebrowser#project-status). > This project is currently on **maintenance-only** mode. For more information, read the information on [GitHub](https://github.com/filebrowser/filebrowser#project-status).
![Preview](static/example.gif) ![Preview](static/example.gif)