Compare commits
1 Commits
v2.45.3
...
feat/compo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83492a4dfb |
@@ -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 }}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
20
.github/workflows/site-pr.yml
vendored
Normal file
20
.github/workflows/site-pr.yml
vendored
Normal 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
|
||||||
@@ -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 }}
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
88
Makefile
Normal 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)
|
||||||
16
README.md
16
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
69
Taskfile.yml
69
Taskfile.yml
@@ -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
28
common.mk
Normal 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
14
frontend/assets_dev.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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,75 +41,54 @@ 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,
|
|
||||||
},
|
const pct = computed(() => {
|
||||||
text: {
|
const pct = (props.val / props.max) * 100;
|
||||||
type: String,
|
const pctFixed = pct.toFixed(2);
|
||||||
default: "",
|
return Math.min(parseFloat(pctFixed), props.max);
|
||||||
},
|
});
|
||||||
"text-align": {
|
|
||||||
type: String,
|
const size_px = computed(() => {
|
||||||
default: "center", // 'left', 'right'
|
switch (props.size) {
|
||||||
},
|
|
||||||
"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":
|
case "tiny":
|
||||||
return 2;
|
return 2;
|
||||||
case "small":
|
case "small":
|
||||||
@@ -129,10 +105,11 @@ export default {
|
|||||||
return 64;
|
return 64;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isNumber(this.size) ? this.size : 32;
|
return isNumber(props.size) ? (props.size as number) : 32;
|
||||||
},
|
});
|
||||||
text_padding() {
|
|
||||||
switch (this.size) {
|
const text_padding = computed(() => {
|
||||||
|
switch (props.size) {
|
||||||
case "tiny":
|
case "tiny":
|
||||||
case "small":
|
case "small":
|
||||||
case "medium":
|
case "medium":
|
||||||
@@ -140,13 +117,14 @@ export default {
|
|||||||
case "big":
|
case "big":
|
||||||
case "huge":
|
case "huge":
|
||||||
case "massive":
|
case "massive":
|
||||||
return Math.min(Math.max(Math.ceil(this.size_px / 8), 3), 12);
|
return Math.min(Math.max(Math.ceil(size_px.value / 8), 3), 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isNumber(this.spacing) ? this.spacing : 4;
|
return isNumber(props.spacing) ? props.spacing : 4;
|
||||||
},
|
});
|
||||||
text_font_size() {
|
|
||||||
switch (this.size) {
|
const text_font_size = computed(() => {
|
||||||
|
switch (props.size) {
|
||||||
case "tiny":
|
case "tiny":
|
||||||
case "small":
|
case "small":
|
||||||
case "medium":
|
case "medium":
|
||||||
@@ -154,72 +132,73 @@ export default {
|
|||||||
case "big":
|
case "big":
|
||||||
case "huge":
|
case "huge":
|
||||||
case "massive":
|
case "massive":
|
||||||
return Math.min(Math.max(Math.ceil(this.size_px * 1.4), 11), 32);
|
return Math.min(Math.max(Math.ceil(size_px.value * 1.4), 11), 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
return isNumber(this.fontSize) ? this.fontSize : 13;
|
return isNumber(props.fontSize) ? props.fontSize : 13;
|
||||||
},
|
});
|
||||||
progress_style() {
|
|
||||||
const style = {
|
const progress_style = computed(() => {
|
||||||
background: this.bgColor,
|
const style: Record<string, string> = {
|
||||||
|
background: props.bgColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
if (props.textPosition == "middle" || props.textPosition == "inside") {
|
||||||
style["position"] = "relative";
|
style["position"] = "relative";
|
||||||
style["min-height"] = this.size_px + "px";
|
style["min-height"] = size_px.value + "px";
|
||||||
style["z-index"] = "-2";
|
style["z-index"] = "-2";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.barBorderRadius > 0) {
|
if (props.barBorderRadius > 0) {
|
||||||
style["border-radius"] = this.barBorderRadius + "px";
|
style["border-radius"] = props.barBorderRadius + "px";
|
||||||
}
|
}
|
||||||
|
|
||||||
return style;
|
return style;
|
||||||
},
|
});
|
||||||
bar_style() {
|
|
||||||
const style = {
|
const bar_style = computed(() => {
|
||||||
background: this.barColor,
|
const style: Record<string, string> = {
|
||||||
width: this.pct + "%",
|
background: props.barColor,
|
||||||
height: this.size_px + "px",
|
width: pct.value + "%",
|
||||||
transition: this.barTransition,
|
height: size_px.value + "px",
|
||||||
|
transition: props.barTransition,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.barBorderRadius > 0) {
|
if (props.barBorderRadius > 0) {
|
||||||
style["border-radius"] = this.barBorderRadius + "px";
|
style["border-radius"] = props.barBorderRadius + "px";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.textPosition == "middle" || this.textPosition == "inside") {
|
if (props.textPosition == "middle" || props.textPosition == "inside") {
|
||||||
style["position"] = "absolute";
|
style["position"] = "absolute";
|
||||||
style["top"] = "0";
|
style["top"] = "0";
|
||||||
style["height"] = "100%";
|
style["height"] = "100%";
|
||||||
((style["min-height"] = this.size_px + "px"),
|
style["min-height"] = size_px.value + "px";
|
||||||
(style["z-index"] = "-1"));
|
style["z-index"] = "-1";
|
||||||
}
|
}
|
||||||
|
|
||||||
return style;
|
return style;
|
||||||
},
|
});
|
||||||
text_style() {
|
|
||||||
const style = {
|
const text_style = computed(() => {
|
||||||
color: this.textFgColor,
|
const style: Record<string, string> = {
|
||||||
"font-size": this.text_font_size + "px",
|
color: props.textFgColor,
|
||||||
"text-align": this.textAlign,
|
"font-size": text_font_size.value + "px",
|
||||||
|
"text-align": props.textAlign,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.textPosition == "top" ||
|
props.textPosition == "top" ||
|
||||||
this.textPosition == "middle" ||
|
props.textPosition == "middle" ||
|
||||||
this.textPosition == "inside"
|
props.textPosition == "inside"
|
||||||
)
|
)
|
||||||
style["padding-bottom"] = this.text_padding + "px";
|
style["padding-bottom"] = text_padding.value + "px";
|
||||||
if (
|
if (
|
||||||
this.textPosition == "bottom" ||
|
props.textPosition == "bottom" ||
|
||||||
this.textPosition == "middle" ||
|
props.textPosition == "middle" ||
|
||||||
this.textPosition == "inside"
|
props.textPosition == "inside"
|
||||||
)
|
)
|
||||||
style["padding-top"] = this.text_padding + "px";
|
style["padding-top"] = text_padding.value + "px";
|
||||||
|
|
||||||
return style;
|
return style;
|
||||||
},
|
});
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const fileStore = useFileStore();
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
|
const { showShell } = storeToRefs(layoutStore);
|
||||||
|
const { isFiles } = storeToRefs(fileStore);
|
||||||
|
const { toggleShell } = layoutStore;
|
||||||
|
|
||||||
|
const scrollable = ref<HTMLElement | null>(null);
|
||||||
|
const input = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
const content = ref<Array<{ text: string }>>([]);
|
||||||
|
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)
|
||||||
|
);
|
||||||
|
|
||||||
|
const path = computed(() => {
|
||||||
|
if (isFiles.value) {
|
||||||
|
return route.path;
|
||||||
|
}
|
||||||
return "";
|
return "";
|
||||||
},
|
});
|
||||||
},
|
|
||||||
data: () => ({
|
const checkTheme = () => {
|
||||||
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") {
|
if (theme == "dark") {
|
||||||
return "rgba(255, 255, 255, 0.4)";
|
return "rgba(255, 255, 255, 0.4)";
|
||||||
}
|
}
|
||||||
return "rgba(127, 127, 127, 0.4)";
|
return "rgba(127, 127, 127, 0.4)";
|
||||||
},
|
};
|
||||||
startDrag() {
|
|
||||||
document.addEventListener("pointermove", this.handleDrag);
|
const scroll = () => {
|
||||||
this.shellDrag = true;
|
if (scrollable.value) {
|
||||||
},
|
scrollable.value.scrollTop = scrollable.value.scrollHeight;
|
||||||
stopDrag() {
|
}
|
||||||
document.removeEventListener("pointermove", this.handleDrag);
|
};
|
||||||
this.shellDrag = false;
|
|
||||||
},
|
const focus = () => {
|
||||||
handleDrag: throttle(function (event) {
|
input.value?.focus();
|
||||||
const top = window.innerHeight / this.fontsize - 4;
|
};
|
||||||
const userPos = (window.innerHeight - event.clientY) / this.fontsize;
|
|
||||||
const bottom =
|
const handleDrag = throttle((event: PointerEvent) => {
|
||||||
2.25 +
|
const top = window.innerHeight / fontsize.value - 4;
|
||||||
document.querySelector(".shell__divider").offsetHeight / this.fontsize;
|
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) {
|
if (userPos <= top && userPos >= bottom) {
|
||||||
this.shellHeight = userPos.toFixed(2);
|
shellHeight.value = parseFloat(userPos.toFixed(2));
|
||||||
}
|
}
|
||||||
}, 32),
|
}, 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 resize = throttle(() => {
|
||||||
this.shellHeight = top;
|
const top = window.innerHeight / fontsize.value - 4;
|
||||||
} else if (this.shellHeight < bottom) {
|
const divider = document.querySelector(".shell__divider") as HTMLElement;
|
||||||
this.shellHeight = bottom;
|
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),
|
}, 32);
|
||||||
scroll: function () {
|
|
||||||
this.$refs.scrollable.scrollTop = this.$refs.scrollable.scrollHeight;
|
const startDrag = () => {
|
||||||
},
|
document.addEventListener("pointermove", handleDrag as any);
|
||||||
focus: function () {
|
shellDrag.value = true;
|
||||||
this.$refs.input.focus();
|
};
|
||||||
},
|
|
||||||
historyUp() {
|
const stopDrag = () => {
|
||||||
if (this.historyPos > 0) {
|
document.removeEventListener("pointermove", handleDrag as any);
|
||||||
this.$refs.input.innerText = this.history[--this.historyPos];
|
shellDrag.value = false;
|
||||||
this.focus();
|
};
|
||||||
|
|
||||||
|
const historyUp = () => {
|
||||||
|
if (historyPos.value > 0 && input.value) {
|
||||||
|
historyPos.value--;
|
||||||
|
input.value.innerText = history.value[historyPos.value];
|
||||||
|
focus();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
historyDown() {
|
|
||||||
if (this.historyPos >= 0 && this.historyPos < this.history.length - 1) {
|
const historyDown = () => {
|
||||||
this.$refs.input.innerText = this.history[++this.historyPos];
|
if (
|
||||||
this.focus();
|
historyPos.value >= 0 &&
|
||||||
|
historyPos.value < history.value.length - 1 &&
|
||||||
|
input.value
|
||||||
|
) {
|
||||||
|
historyPos.value++;
|
||||||
|
input.value.innerText = history.value[historyPos.value];
|
||||||
|
focus();
|
||||||
} else {
|
} else {
|
||||||
this.historyPos = this.history.length;
|
historyPos.value = history.value.length;
|
||||||
this.$refs.input.innerText = "";
|
if (input.value) {
|
||||||
|
input.value.innerText = "";
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
submit: function (event) {
|
};
|
||||||
const cmd = event.target.innerText.trim();
|
|
||||||
|
const submit = (event: Event) => {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
const cmd = target.innerText.trim();
|
||||||
|
|
||||||
if (cmd === "") {
|
if (cmd === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === "clear") {
|
if (cmd === "clear") {
|
||||||
this.content = [];
|
content.value = [];
|
||||||
event.target.innerHTML = "";
|
target.innerHTML = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === "exit") {
|
if (cmd === "exit") {
|
||||||
event.target.innerHTML = "";
|
target.innerHTML = "";
|
||||||
this.toggleShell();
|
toggleShell();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.canInput = false;
|
canInput.value = false;
|
||||||
event.target.innerHTML = "";
|
target.innerHTML = "";
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
text: `${cmd}\n\n`,
|
text: `${cmd}\n\n`,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.history.push(cmd);
|
history.value.push(cmd);
|
||||||
this.historyPos = this.history.length;
|
historyPos.value = history.value.length;
|
||||||
this.content.push(results);
|
content.value.push(results);
|
||||||
|
|
||||||
commands(
|
commands(
|
||||||
this.path,
|
path.value,
|
||||||
cmd,
|
cmd,
|
||||||
(event) => {
|
(event: MessageEvent) => {
|
||||||
results.text += `${event.data}\n`;
|
results.text += `${event.data}\n`;
|
||||||
this.scroll();
|
scroll();
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
results.text = results.text
|
results.text = results.text
|
||||||
|
|
||||||
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
|
.replace(/\u001b\[[0-9;]+m/g, "") // Filter ANSI color for now
|
||||||
.trimEnd();
|
.trimEnd();
|
||||||
this.canInput = true;
|
canInput.value = true;
|
||||||
this.$refs.input.focus();
|
input.value?.focus();
|
||||||
this.scroll();
|
scroll();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener("resize", resize as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener("resize", resize as any);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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 authStore = useAuthStore();
|
||||||
|
const fileStore = useFileStore();
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
|
const { user, isLoggedIn } = storeToRefs(authStore);
|
||||||
|
const { isFiles } = storeToRefs(fileStore);
|
||||||
|
const { currentPromptName } = storeToRefs(layoutStore);
|
||||||
|
const { closeHovers, showHover } = layoutStore;
|
||||||
|
|
||||||
const usage = reactive(USAGE_DEFAULT);
|
const usage = reactive(USAGE_DEFAULT);
|
||||||
return { usage, usageAbortController: new AbortController() };
|
const usageAbortController = ref(new AbortController());
|
||||||
},
|
|
||||||
components: {
|
const active = computed(() => {
|
||||||
ProgressBar,
|
return currentPromptName.value === "sidebar";
|
||||||
},
|
});
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
const canLogout = !noAuth && loginPage;
|
||||||
...mapState(useAuthStore, ["user", "isLoggedIn"]),
|
|
||||||
...mapState(useFileStore, ["isFiles", "reload"]),
|
const abortOngoingFetchUsage = () => {
|
||||||
...mapState(useLayoutStore, ["currentPromptName"]),
|
usageAbortController.value.abort();
|
||||||
active() {
|
};
|
||||||
return this.currentPromptName === "sidebar";
|
|
||||||
},
|
const fetchUsage = async () => {
|
||||||
signup: () => signup,
|
const path = route.path.endsWith("/") ? route.path : route.path + "/";
|
||||||
version: () => version,
|
|
||||||
disableExternal: () => disableExternal,
|
|
||||||
disableUsedPercentage: () => disableUsedPercentage,
|
|
||||||
canLogout: () => !noAuth && loginPage,
|
|
||||||
},
|
|
||||||
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;
|
let usageStats = USAGE_DEFAULT;
|
||||||
if (this.disableUsedPercentage) {
|
if (disableUsedPercentage) {
|
||||||
return Object.assign(this.usage, usageStats);
|
return Object.assign(usage, usageStats);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.abortOngoingFetchUsage();
|
abortOngoingFetchUsage();
|
||||||
this.usageAbortController = new AbortController();
|
usageAbortController.value = new AbortController();
|
||||||
const usage = await api.usage(path, this.usageAbortController.signal);
|
const usageData = await api.usage(path, usageAbortController.value.signal);
|
||||||
usageStats = {
|
usageStats = {
|
||||||
used: prettyBytes(usage.used, { binary: true }),
|
used: prettyBytes(usageData.used, { binary: true }),
|
||||||
total: prettyBytes(usage.total, { binary: true }),
|
total: prettyBytes(usageData.total, { binary: true }),
|
||||||
usedPercentage: Math.round((usage.used / usage.total) * 100),
|
usedPercentage: Math.round((usageData.used / usageData.total) * 100),
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
return Object.assign(this.usage, usageStats);
|
return Object.assign(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 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>
|
||||||
|
|||||||
@@ -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,81 +66,77 @@ 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 {
|
const fileStore = useFileStore();
|
||||||
current: window.location.pathname,
|
const layoutStore = useLayoutStore();
|
||||||
dest: null,
|
const authStore = useAuthStore();
|
||||||
};
|
|
||||||
},
|
const { req, selected } = storeToRefs(fileStore);
|
||||||
inject: ["$showError"],
|
const { user } = storeToRefs(authStore);
|
||||||
computed: {
|
const { showHover, closeHovers } = layoutStore;
|
||||||
...mapState(useFileStore, ["req", "selected"]),
|
|
||||||
...mapState(useAuthStore, ["user"]),
|
const fileList = ref<InstanceType<typeof FileList> | null>(null);
|
||||||
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
const dest = ref<string | null>(null);
|
||||||
},
|
|
||||||
methods: {
|
const copy = async (event: Event) => {
|
||||||
...mapActions(useLayoutStore, ["showHover", "closeHovers"]),
|
|
||||||
copy: async function (event) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const items = [];
|
const items: Array<{ from: string; to: string; name: string }> = [];
|
||||||
|
|
||||||
// Create a new promise for each file.
|
// Create a new promise for each file.
|
||||||
for (const item of this.selected) {
|
for (const item of selected.value) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[item].url,
|
from: req.value!.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
|
||||||
name: this.req.items[item].name,
|
name: req.value!.items[item].name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async (overwrite, rename) => {
|
const action = async (overwrite: boolean, rename: boolean) => {
|
||||||
buttons.loading("copy");
|
buttons.loading("copy");
|
||||||
|
|
||||||
await api
|
await api
|
||||||
.copy(items, overwrite, rename)
|
.copy(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.success("copy");
|
buttons.success("copy");
|
||||||
this.preselect = removePrefix(items[0].to);
|
fileStore.preselect = removePrefix(items[0].to);
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
|
||||||
this.reload = true;
|
|
||||||
|
|
||||||
|
if (route.path === dest.value) {
|
||||||
|
fileStore.reload = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$router.push({ path: this.dest });
|
router.push({ path: dest.value! });
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
buttons.done("copy");
|
buttons.done("copy");
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.$route.path === this.dest) {
|
if (route.path === dest.value) {
|
||||||
this.closeHovers();
|
closeHovers();
|
||||||
action(false, true);
|
action(false, true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dstItems = (await api.fetch(this.dest)).items;
|
const dstItems = (await api.fetch(dest.value!)).items;
|
||||||
const conflict = upload.checkConflict(items, dstItems);
|
const conflict = upload.checkConflict(items as any, dstItems);
|
||||||
|
|
||||||
let overwrite = false;
|
let overwrite = false;
|
||||||
let rename = false;
|
let rename = false;
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
this.showHover({
|
showHover({
|
||||||
prompt: "replace-rename",
|
prompt: "replace-rename",
|
||||||
confirm: (event, option) => {
|
confirm: (event: Event, option: string) => {
|
||||||
overwrite = option == "overwrite";
|
overwrite = option == "overwrite";
|
||||||
rename = option == "rename";
|
rename = option == "rename";
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.closeHovers();
|
closeHovers();
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -147,7 +145,5 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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: {
|
const fileStore = useFileStore();
|
||||||
...mapState(useFileStore, [
|
const layoutStore = useLayoutStore();
|
||||||
"isListing",
|
|
||||||
"selectedCount",
|
const { isListing, selectedCount, req, selected } = storeToRefs(fileStore);
|
||||||
"req",
|
const { currentPrompt } = storeToRefs(layoutStore);
|
||||||
"selected",
|
const { closeHovers } = layoutStore;
|
||||||
]),
|
|
||||||
...mapState(useLayoutStore, ["currentPrompt"]),
|
const submit = async () => {
|
||||||
...mapWritableState(useFileStore, ["reload", "preselect"]),
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
submit: async function () {
|
|
||||||
buttons.loading("delete");
|
buttons.loading("delete");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!this.isListing) {
|
if (!isListing.value) {
|
||||||
await api.remove(this.$route.path);
|
await api.remove(route.path);
|
||||||
buttons.success("delete");
|
buttons.success("delete");
|
||||||
|
|
||||||
this.currentPrompt?.confirm();
|
currentPrompt.value?.confirm();
|
||||||
this.closeHovers();
|
closeHovers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closeHovers();
|
closeHovers();
|
||||||
|
|
||||||
if (this.selectedCount === 0) {
|
if (selectedCount.value === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for (const index of this.selected) {
|
for (const index of selected.value) {
|
||||||
promises.push(api.remove(this.req.items[index].url));
|
promises.push(api.remove(req.value!.items[index].url));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
buttons.success("delete");
|
buttons.success("delete");
|
||||||
|
|
||||||
const nearbyItem =
|
const nearbyItem =
|
||||||
this.req.items[Math.max(0, Math.min(this.selected) - 1)];
|
req.value!.items[Math.max(0, Math.min(...selected.value) - 1)];
|
||||||
|
|
||||||
this.preselect = nearbyItem?.path;
|
fileStore.preselect = nearbyItem?.path;
|
||||||
|
|
||||||
this.reload = true;
|
fileStore.reload = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
buttons.done("delete");
|
buttons.done("delete");
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
if (this.isListing) this.reload = true;
|
if (isListing.value) fileStore.reload = true;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
const emit = defineEmits<{
|
||||||
default: () => [],
|
"update:selected": [value: string];
|
||||||
},
|
}>();
|
||||||
},
|
|
||||||
data: function () {
|
const route = useRoute();
|
||||||
return {
|
const $showError = inject<(error: unknown) => void>("$showError");
|
||||||
items: [],
|
|
||||||
touches: {
|
const authStore = useAuthStore();
|
||||||
|
const fileStore = useFileStore();
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
|
const { user } = storeToRefs(authStore);
|
||||||
|
const { req } = storeToRefs(fileStore);
|
||||||
|
const { showHover } = layoutStore;
|
||||||
|
|
||||||
|
const items = ref<Array<{ name: string; url: string }>>([]);
|
||||||
|
const touches = ref({
|
||||||
id: "",
|
id: "",
|
||||||
count: 0,
|
count: 0,
|
||||||
},
|
});
|
||||||
selected: null,
|
const selected = ref<string | null>(null);
|
||||||
current: window.location.pathname,
|
const current = ref(window.location.pathname);
|
||||||
nextAbortController: new AbortController(),
|
const nextAbortController = ref(new AbortController());
|
||||||
|
|
||||||
|
const nav = computed(() => {
|
||||||
|
return decodeURIComponent(current.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const abortOngoingNext = () => {
|
||||||
|
nextAbortController.value.abort();
|
||||||
};
|
};
|
||||||
},
|
|
||||||
inject: ["$showError"],
|
const fillOptions = (reqData: any) => {
|
||||||
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
|
// Sets the current path and resets
|
||||||
// the current items.
|
// the current items.
|
||||||
this.current = req.url;
|
current.value = reqData.url;
|
||||||
this.items = [];
|
items.value = [];
|
||||||
|
|
||||||
this.$emit("update:selected", this.current);
|
emit("update:selected", current.value);
|
||||||
|
|
||||||
// If the path isn't the root path,
|
// If the path isn't the root path,
|
||||||
// show a button to navigate to the previous
|
// show a button to navigate to the previous
|
||||||
// directory.
|
// directory.
|
||||||
if (req.url !== "/files/") {
|
if (reqData.url !== "/files/") {
|
||||||
this.items.push({
|
items.value.push({
|
||||||
name: "..",
|
name: "..",
|
||||||
url: url.removeLastDir(req.url) + "/",
|
url: url.removeLastDir(reqData.url) + "/",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this folder is empty, finish here.
|
// If this folder is empty, finish here.
|
||||||
if (req.items === null) return;
|
if (reqData.items === null) return;
|
||||||
|
|
||||||
// Otherwise we add every directory to the
|
// Otherwise we add every directory to the
|
||||||
// move options.
|
// move options.
|
||||||
for (const item of req.items) {
|
for (const item of reqData.items) {
|
||||||
if (!item.isDir) continue;
|
if (!item.isDir) continue;
|
||||||
if (this.exclude?.includes(item.url)) continue;
|
if (props.exclude?.includes(item.url)) continue;
|
||||||
|
|
||||||
this.items.push({
|
items.value.push({
|
||||||
name: item.name,
|
name: item.name,
|
||||||
url: item.url,
|
url: item.url,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
next: function (event) {
|
|
||||||
|
const next = (event: Event) => {
|
||||||
// Retrieves the URL of the directory the user
|
// Retrieves the URL of the directory the user
|
||||||
// just clicked in and fill the options with its
|
// just clicked in and fill the options with its
|
||||||
// content.
|
// content.
|
||||||
const uri = event.currentTarget.dataset.url;
|
const uri = (event.currentTarget as HTMLElement).dataset.url!;
|
||||||
this.abortOngoingNext();
|
abortOngoingNext();
|
||||||
this.nextAbortController = new AbortController();
|
nextAbortController.value = new AbortController();
|
||||||
files
|
files
|
||||||
.fetch(uri, this.nextAbortController.signal)
|
.fetch(uri, nextAbortController.value.signal)
|
||||||
.then(this.fillOptions)
|
.then(fillOptions)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
if (e instanceof StatusError && e.is_canceled) {
|
if (e instanceof StatusError && e.is_canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
touchstart(event) {
|
|
||||||
const url = event.currentTarget.dataset.url;
|
const touchstart = (event: Event) => {
|
||||||
|
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
|
||||||
|
|
||||||
// In 300 milliseconds, we shall reset the count.
|
// In 300 milliseconds, we shall reset the count.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.touches.count = 0;
|
touches.value.count = 0;
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
// If the element the user is touching
|
// If the element the user is touching
|
||||||
// is different from the last one he touched,
|
// is different from the last one he touched,
|
||||||
// reset the count.
|
// reset the count.
|
||||||
if (this.touches.id !== url) {
|
if (touches.value.id !== urlValue) {
|
||||||
this.touches.id = url;
|
touches.value.id = urlValue;
|
||||||
this.touches.count = 1;
|
touches.value.count = 1;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.touches.count++;
|
touches.value.count++;
|
||||||
|
|
||||||
// If there is more than one touch already,
|
// If there is more than one touch already,
|
||||||
// open the next screen.
|
// open the next screen.
|
||||||
if (this.touches.count > 1) {
|
if (touches.value.count > 1) {
|
||||||
this.next(event);
|
next(event);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
itemClick: function (event) {
|
|
||||||
if (this.user.singleClick) this.next(event);
|
const itemClick = (event: Event) => {
|
||||||
else this.select(event);
|
if (user.value?.singleClick) next(event);
|
||||||
},
|
else select(event);
|
||||||
select: function (event) {
|
};
|
||||||
|
|
||||||
|
const select = (event: Event) => {
|
||||||
|
const urlValue = (event.currentTarget as HTMLElement).dataset.url!;
|
||||||
// If the element is already selected, unselect it.
|
// If the element is already selected, unselect it.
|
||||||
if (this.selected === event.currentTarget.dataset.url) {
|
if (selected.value === urlValue) {
|
||||||
this.selected = null;
|
selected.value = null;
|
||||||
this.$emit("update:selected", this.current);
|
emit("update:selected", current.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise select the element.
|
// Otherwise select the element.
|
||||||
this.selected = event.currentTarget.dataset.url;
|
selected.value = urlValue;
|
||||||
this.$emit("update:selected", this.selected);
|
emit("update:selected", selected.value);
|
||||||
},
|
};
|
||||||
createDir: async function () {
|
|
||||||
this.showHover({
|
const createDir = async () => {
|
||||||
|
showHover({
|
||||||
prompt: "newDir",
|
prompt: "newDir",
|
||||||
action: null,
|
action: undefined,
|
||||||
confirm: null,
|
confirm: undefined,
|
||||||
props: {
|
props: {
|
||||||
redirect: false,
|
redirect: false,
|
||||||
base: this.current === this.$route.path ? null : this.current,
|
base: current.value === route.path ? null : current.value,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (req.value) {
|
||||||
|
fillOptions(req.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
abortOngoingNext();
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
createDir,
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: {
|
const fileStore = useFileStore();
|
||||||
...mapState(useFileStore, [
|
const layoutStore = useLayoutStore();
|
||||||
"req",
|
|
||||||
"selected",
|
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
|
||||||
"selectedCount",
|
const { closeHovers } = layoutStore;
|
||||||
"isListing",
|
|
||||||
]),
|
const humanSize = computed(() => {
|
||||||
humanSize: function () {
|
if (selectedCount.value === 0 || !isListing.value) {
|
||||||
if (this.selectedCount === 0 || !this.isListing) {
|
return filesize(req.value?.size ?? 0);
|
||||||
return filesize(this.req.size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
|
|
||||||
for (const selected of this.selected) {
|
for (const selectedIdx of selected.value) {
|
||||||
sum += this.req.items[selected].size;
|
sum += req.value?.items[selectedIdx]?.size ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return filesize(sum);
|
return filesize(sum);
|
||||||
},
|
});
|
||||||
humanTime: function () {
|
|
||||||
if (this.selectedCount === 0) {
|
const humanTime = computed(() => {
|
||||||
return dayjs(this.req.modified).fromNow();
|
if (selectedCount.value === 0) {
|
||||||
|
return dayjs(req.value?.modified).fromNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
return dayjs(this.req.items[this.selected[0]].modified).fromNow();
|
return dayjs(req.value?.items[selected.value[0]]?.modified).fromNow();
|
||||||
},
|
});
|
||||||
modTime: function () {
|
|
||||||
if (this.selectedCount === 0) {
|
const modTime = computed(() => {
|
||||||
return new Date(Date.parse(this.req.modified)).toLocaleString();
|
if (selectedCount.value === 0) {
|
||||||
|
return new Date(Date.parse(req.value?.modified ?? "")).toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Date(
|
return new Date(
|
||||||
Date.parse(this.req.items[this.selected[0]].modified)
|
Date.parse(req.value?.items[selected.value[0]]?.modified ?? "")
|
||||||
).toLocaleString();
|
).toLocaleString();
|
||||||
},
|
});
|
||||||
name: function () {
|
|
||||||
return this.selectedCount === 0
|
const name = computed(() => {
|
||||||
? this.req.name
|
return selectedCount.value === 0
|
||||||
: this.req.items[this.selected[0]].name;
|
? (req.value?.name ?? "")
|
||||||
},
|
: (req.value?.items[selected.value[0]]?.name ?? "");
|
||||||
dir: function () {
|
});
|
||||||
|
|
||||||
|
const dir = computed(() => {
|
||||||
return (
|
return (
|
||||||
this.selectedCount > 1 ||
|
selectedCount.value > 1 ||
|
||||||
(this.selectedCount === 0
|
(selectedCount.value === 0
|
||||||
? this.req.isDir
|
? (req.value?.isDir ?? false)
|
||||||
: this.req.items[this.selected[0]].isDir)
|
: (req.value?.items[selected.value[0]]?.isDir ?? false))
|
||||||
);
|
);
|
||||||
},
|
});
|
||||||
resolution: function () {
|
|
||||||
if (this.selectedCount === 1) {
|
const resolution = computed(() => {
|
||||||
const selectedItem = this.req.items[this.selected[0]];
|
if (selectedCount.value === 1) {
|
||||||
|
const selectedItem = req.value?.items[selected.value[0]];
|
||||||
if (selectedItem && selectedItem.type === "image") {
|
if (selectedItem && selectedItem.type === "image") {
|
||||||
return selectedItem.resolution;
|
return (selectedItem as any).resolution;
|
||||||
}
|
}
|
||||||
} else if (this.req && this.req.type === "image") {
|
} else if (req.value && req.value.type === "image") {
|
||||||
return this.req.resolution;
|
return (req.value as any).resolution;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
});
|
||||||
},
|
|
||||||
methods: {
|
const checksum = async (event: Event, algo: string) => {
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
checksum: async function (event, algo) {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
let link;
|
let link;
|
||||||
|
|
||||||
if (this.selectedCount) {
|
if (selectedCount.value) {
|
||||||
link = this.req.items[this.selected[0]].url;
|
link = req.value?.items[selected.value[0]]?.url ?? "";
|
||||||
} else {
|
} else {
|
||||||
link = this.$route.path;
|
link = route.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const hash = await api.checksum(link, algo);
|
const hash = await api.checksum(link, algo as any);
|
||||||
event.target.textContent = hash;
|
(event.target as HTMLElement).textContent = hash;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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,71 +67,69 @@ 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();
|
||||||
|
const layoutStore = useLayoutStore();
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
const { req, selected } = storeToRefs(fileStore);
|
||||||
|
const { user } = storeToRefs(authStore);
|
||||||
|
const { showHover, closeHovers } = layoutStore;
|
||||||
|
|
||||||
|
const fileList = ref<InstanceType<typeof FileList> | null>(null);
|
||||||
|
const dest = ref<string | null>(null);
|
||||||
|
|
||||||
|
const excludedFolders = computed(() => {
|
||||||
|
return selected.value
|
||||||
|
.filter((idx) => req.value!.items[idx].isDir)
|
||||||
|
.map((idx) => req.value!.items[idx].url);
|
||||||
|
});
|
||||||
|
|
||||||
|
const move = async (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const items: Array<{ from: string; to: string; name: string }> = [];
|
||||||
|
|
||||||
|
for (const item of selected.value) {
|
||||||
items.push({
|
items.push({
|
||||||
from: this.req.items[item].url,
|
from: req.value!.items[item].url,
|
||||||
to: this.dest + encodeURIComponent(this.req.items[item].name),
|
to: dest.value! + encodeURIComponent(req.value!.items[item].name),
|
||||||
name: this.req.items[item].name,
|
name: req.value!.items[item].name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = async (overwrite, rename) => {
|
const action = async (overwrite: boolean, rename: boolean) => {
|
||||||
buttons.loading("move");
|
buttons.loading("move");
|
||||||
|
|
||||||
await api
|
await api
|
||||||
.move(items, overwrite, rename)
|
.move(items, overwrite, rename)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
buttons.success("move");
|
buttons.success("move");
|
||||||
this.preselect = removePrefix(items[0].to);
|
fileStore.preselect = removePrefix(items[0].to);
|
||||||
this.$router.push({ path: this.dest });
|
router.push({ path: dest.value! });
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
buttons.done("move");
|
buttons.done("move");
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const dstItems = (await api.fetch(this.dest)).items;
|
const dstItems = (await api.fetch(dest.value!)).items;
|
||||||
const conflict = upload.checkConflict(items, dstItems);
|
const conflict = upload.checkConflict(items as any, dstItems);
|
||||||
|
|
||||||
let overwrite = false;
|
let overwrite = false;
|
||||||
let rename = false;
|
let rename = false;
|
||||||
|
|
||||||
if (conflict) {
|
if (conflict) {
|
||||||
this.showHover({
|
showHover({
|
||||||
prompt: "replace-rename",
|
prompt: "replace-rename",
|
||||||
confirm: (event, option) => {
|
confirm: (event: Event, option: string) => {
|
||||||
overwrite = option == "overwrite";
|
overwrite = option == "overwrite";
|
||||||
rename = option == "rename";
|
rename = option == "rename";
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.closeHovers();
|
closeHovers();
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -138,7 +138,5 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
action(overwrite, rename);
|
action(overwrite, rename);
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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 {
|
const fileStore = useFileStore();
|
||||||
name: "",
|
const layoutStore = useLayoutStore();
|
||||||
};
|
|
||||||
},
|
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
|
||||||
created() {
|
const { closeHovers } = layoutStore;
|
||||||
this.name = this.oldName();
|
|
||||||
},
|
const name = ref("");
|
||||||
inject: ["$showError"],
|
|
||||||
computed: {
|
const oldName = (): string => {
|
||||||
...mapState(useFileStore, [
|
if (!isListing.value) {
|
||||||
"req",
|
return req.value?.name ?? "";
|
||||||
"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) {
|
if (selectedCount.value === 0 || selectedCount.value > 1) {
|
||||||
// This shouldn't happen.
|
// This shouldn't happen.
|
||||||
return;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.req.items[this.selected[0]].name;
|
return req.value?.items[selected.value[0]].name ?? "";
|
||||||
},
|
};
|
||||||
submit: async function () {
|
|
||||||
|
onMounted(() => {
|
||||||
|
name.value = oldName();
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
let oldLink = "";
|
let oldLink = "";
|
||||||
let newLink = "";
|
let newLink = "";
|
||||||
|
|
||||||
if (!this.isListing) {
|
if (!req.value) {
|
||||||
oldLink = this.req.url;
|
|
||||||
} else {
|
|
||||||
oldLink = this.req.items[this.selected[0]].url;
|
|
||||||
}
|
|
||||||
|
|
||||||
newLink =
|
|
||||||
url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.move([{ from: oldLink, to: newLink }]);
|
|
||||||
if (!this.isListing) {
|
|
||||||
this.$router.push({ path: newLink });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.preselect = removePrefix(newLink);
|
if (!isListing.value) {
|
||||||
|
oldLink = req.value.url;
|
||||||
this.reload = true;
|
} else {
|
||||||
} catch (e) {
|
oldLink = req.value.items[selected.value[0]].url;
|
||||||
this.$showError(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closeHovers();
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
const fileStore = useFileStore();
|
||||||
links: [],
|
const layoutStore = useLayoutStore();
|
||||||
clip: null,
|
|
||||||
password: "",
|
const { req, selected, selectedCount, isListing } = storeToRefs(fileStore);
|
||||||
listing: true,
|
const { closeHovers } = layoutStore;
|
||||||
};
|
|
||||||
},
|
const time = ref(0);
|
||||||
inject: ["$showError", "$showSuccess"],
|
const unit = ref("hours");
|
||||||
computed: {
|
const links = ref<any[]>([]);
|
||||||
...mapState(useFileStore, [
|
const password = ref("");
|
||||||
"req",
|
const listing = ref(true);
|
||||||
"selected",
|
|
||||||
"selectedCount",
|
const url = computed(() => {
|
||||||
"isListing",
|
if (!isListing.value) {
|
||||||
]),
|
return route.path;
|
||||||
url() {
|
|
||||||
if (!this.isListing) {
|
|
||||||
return this.$route.path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.selectedCount === 0 || this.selectedCount > 1) {
|
if (selectedCount.value === 0 || selectedCount.value > 1) {
|
||||||
// This shouldn't happen.
|
// This shouldn't happen.
|
||||||
return;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.req.items[this.selected[0]].url;
|
return req.value?.items[selected.value[0]].url ?? "";
|
||||||
},
|
});
|
||||||
},
|
|
||||||
async beforeMount() {
|
|
||||||
try {
|
|
||||||
const links = await api.get(this.url);
|
|
||||||
this.links = links;
|
|
||||||
this.sort();
|
|
||||||
|
|
||||||
if (this.links.length == 0) {
|
const copyToClipboard = (text: string) => {
|
||||||
this.listing = false;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.$showError(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions(useLayoutStore, ["closeHovers"]),
|
|
||||||
copyToClipboard: function (text) {
|
|
||||||
copy({ text }).then(
|
copy({ text }).then(
|
||||||
() => {
|
() => {
|
||||||
// clipboard successfully set
|
// clipboard successfully set
|
||||||
this.$showSuccess(this.$t("success.linkCopied"));
|
$showSuccess?.(t("success.linkCopied"));
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
// clipboard write failed
|
// clipboard write failed
|
||||||
copy({ text }, { permission: true }).then(
|
copy({ text }, { permission: true }).then(
|
||||||
() => {
|
() => {
|
||||||
// clipboard successfully set
|
// clipboard successfully set
|
||||||
this.$showSuccess(this.$t("success.linkCopied"));
|
$showSuccess?.(t("success.linkCopied"));
|
||||||
},
|
},
|
||||||
(e) => {
|
(e) => {
|
||||||
// clipboard write failed
|
// clipboard write failed
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
submit: async function () {
|
|
||||||
|
const submit = async () => {
|
||||||
try {
|
try {
|
||||||
let res = null;
|
let res = null;
|
||||||
|
|
||||||
if (!this.time) {
|
if (!time.value) {
|
||||||
res = await api.create(this.url, this.password);
|
res = await api.create(url.value, password.value);
|
||||||
} else {
|
} else {
|
||||||
res = await api.create(this.url, this.password, this.time, this.unit);
|
res = await api.create(
|
||||||
|
url.value,
|
||||||
|
password.value,
|
||||||
|
String(time.value),
|
||||||
|
unit.value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.links.push(res);
|
links.value.push(res);
|
||||||
this.sort();
|
sort();
|
||||||
|
|
||||||
this.time = 0;
|
time.value = 0;
|
||||||
this.unit = "hours";
|
unit.value = "hours";
|
||||||
this.password = "";
|
password.value = "";
|
||||||
|
|
||||||
this.listing = true;
|
listing.value = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
deleteLink: async function (event, link) {
|
|
||||||
|
const deleteLink = async (event: Event, link: any) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
try {
|
||||||
await api.remove(link.hash);
|
await api.remove(link.hash);
|
||||||
this.links = this.links.filter((item) => item.hash !== link.hash);
|
links.value = links.value.filter((item) => item.hash !== link.hash);
|
||||||
|
|
||||||
if (this.links.length == 0) {
|
if (links.value.length == 0) {
|
||||||
this.listing = false;
|
listing.value = false;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.$showError(e);
|
$showError?.(e);
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
humanTime(time) {
|
|
||||||
|
const humanTime = (time: number) => {
|
||||||
return dayjs(time * 1000).fromNow();
|
return dayjs(time * 1000).fromNow();
|
||||||
},
|
};
|
||||||
buildLink(share) {
|
|
||||||
|
const buildLink = (share: any) => {
|
||||||
return api.getShareURL(share);
|
return api.getShareURL(share);
|
||||||
},
|
};
|
||||||
sort() {
|
|
||||||
this.links = this.links.sort((a, b) => {
|
const sort = () => {
|
||||||
|
links.value = links.value.sort((a, b) => {
|
||||||
if (a.expire === 0) return -1;
|
if (a.expire === 0) return -1;
|
||||||
if (b.expire === 0) return 1;
|
if (b.expire === 0) return 1;
|
||||||
return new Date(a.expire) - new Date(b.expire);
|
return new Date(a.expire).getTime() - new Date(b.expire).getTime();
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
switchListing() {
|
|
||||||
if (this.links.length == 0 && !this.listing) {
|
const switchListing = () => {
|
||||||
this.closeHovers();
|
if (links.value.length == 0 && !listing.value) {
|
||||||
|
closeHovers();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listing = !this.listing;
|
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: {
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:commands": [commands: string[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const raw = computed({
|
||||||
get() {
|
get() {
|
||||||
return this.commands.join(" ");
|
return props.commands.join(" ");
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value: string) {
|
||||||
if (value !== "") {
|
if (value !== "") {
|
||||||
this.$emit("update:commands", value.split(" "));
|
emit("update:commands", value.split(" "));
|
||||||
} else {
|
} else {
|
||||||
this.$emit("update:commands", []);
|
emit("update:commands", []);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,15 +6,18 @@
|
|||||||
</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 emit = defineEmits<{
|
||||||
const locales = {
|
"update:locale": [locale: string];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const locales = markRaw({
|
||||||
he: "עברית",
|
he: "עברית",
|
||||||
hr: "Hrvatski",
|
hr: "Hrvatski",
|
||||||
hu: "Magyar",
|
hu: "Magyar",
|
||||||
@@ -44,23 +47,9 @@ export default {
|
|||||||
vi: "Tiếng Việt",
|
vi: "Tiếng Việt",
|
||||||
"zh-cn": "中文 (简体)",
|
"zh-cn": "中文 (简体)",
|
||||||
"zh-tw": "中文 (繁體)",
|
"zh-tw": "中文 (繁體)",
|
||||||
};
|
|
||||||
|
|
||||||
// Vue3 reactivity breaks with this configuration
|
|
||||||
// so we need to use markRaw as a workaround
|
|
||||||
// https://github.com/vuejs/core/issues/3024
|
|
||||||
Object.defineProperty(dataObj, "locales", {
|
|
||||||
value: markRaw(locales),
|
|
||||||
configurable: false,
|
|
||||||
writable: false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return dataObj;
|
const change = (event: Event) => {
|
||||||
},
|
emit("update:locale", (event.target as HTMLSelectElement).value);
|
||||||
methods: {
|
|
||||||
change(event) {
|
|
||||||
this.$emit("update:locale", event.target.value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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",
|
const props = defineProps<{
|
||||||
props: ["perm"],
|
perm: UserPermissions;
|
||||||
computed: {
|
}>();
|
||||||
admin: {
|
|
||||||
|
const admin = computed({
|
||||||
get() {
|
get() {
|
||||||
return this.perm.admin;
|
return props.perm.admin;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value: boolean) {
|
||||||
if (value) {
|
if (value) {
|
||||||
for (const key in this.perm) {
|
for (const key in props.perm) {
|
||||||
this.perm[key] = true;
|
props.perm[key as keyof UserPermissions] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.perm.admin = value;
|
props.perm.admin = value;
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
isExecEnabled: () => enableExec,
|
|
||||||
},
|
const isExecEnabled = enableExec;
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -32,22 +32,36 @@
|
|||||||
</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: {
|
||||||
|
raw: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
rules: Rule[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:rules": [rules: Rule[]];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const remove = (event: Event, index: number) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const rules = [...this.rules];
|
const rules = [...props.rules];
|
||||||
rules.splice(index, 1);
|
rules.splice(index, 1);
|
||||||
this.$emit("update:rules", [...rules]);
|
emit("update:rules", [...rules]);
|
||||||
},
|
};
|
||||||
create(event) {
|
|
||||||
|
const create = (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
this.$emit("update:rules", [
|
emit("update:rules", [
|
||||||
...this.rules,
|
...props.rules,
|
||||||
{
|
{
|
||||||
allow: true,
|
allow: true,
|
||||||
path: "",
|
path: "",
|
||||||
@@ -57,7 +71,5 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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;
|
user: IUserForm;
|
||||||
isNew: boolean;
|
isNew: boolean;
|
||||||
isDefault: boolean;
|
isDefault: false;
|
||||||
createUserDir?: boolean;
|
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;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
2
frontend/src/types/settings.d.ts
vendored
2
frontend/src/types/settings.d.ts
vendored
@@ -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;
|
||||||
|
|||||||
26
frontend/src/types/user.d.ts
vendored
26
frontend/src/types/user.d.ts
vendored
@@ -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
2
go.mod
@@ -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
4
go.sum
@@ -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=
|
||||||
|
|||||||
14
http/auth.go
14
http/auth.go
@@ -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
14
http/headers_dev.go
Normal 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
27
tools.mk
Normal 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
216
tools/go.mod
Normal 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
1001
tools/go.sum
Normal file
File diff suppressed because it is too large
Load Diff
15
tools/tools.go
Normal file
15
tools/tools.go
Normal 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"
|
||||||
|
)
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user