V0.2.9 release (#205)
This commit is contained in:
		
							parent
							
								
									0bad14b51e
								
							
						
					
					
						commit
						62d1cd88a1
					
				| 
						 | 
					@ -12,6 +12,10 @@ jobs:
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - name: Checkout
 | 
					      - name: Checkout
 | 
				
			||||||
        uses: actions/checkout@v4
 | 
					        uses: actions/checkout@v4
 | 
				
			||||||
 | 
					      - name: Find latest tag
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          echo "LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV
 | 
				
			||||||
 | 
					          echo "latest tag is $LATEST_TAG"
 | 
				
			||||||
      - name: Set up QEMU
 | 
					      - name: Set up QEMU
 | 
				
			||||||
        uses: docker/setup-qemu-action@v3.0.0
 | 
					        uses: docker/setup-qemu-action@v3.0.0
 | 
				
			||||||
      - name: Set up Docker Buildx
 | 
					      - name: Set up Docker Buildx
 | 
				
			||||||
| 
						 | 
					@ -31,7 +35,7 @@ jobs:
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          context: .
 | 
					          context: .
 | 
				
			||||||
          build-args: |
 | 
					          build-args: |
 | 
				
			||||||
            VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}
 | 
					            VERSION=${{ env.LATEST_TAG }}
 | 
				
			||||||
            REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
 | 
					            REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }}
 | 
				
			||||||
          platforms: linux/amd64,linux/arm64,linux/arm/v7
 | 
					          platforms: linux/amd64,linux/arm64,linux/arm/v7
 | 
				
			||||||
          file: ./Dockerfile
 | 
					          file: ./Dockerfile
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,5 +52,5 @@ jobs:
 | 
				
			||||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
					          tags: ${{ steps.meta.outputs.tags }}
 | 
				
			||||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
					          labels: ${{ steps.meta.outputs.labels }}
 | 
				
			||||||
          build-args: |
 | 
					          build-args: |
 | 
				
			||||||
            version=${{ steps.meta.outputs.version }}
 | 
					            VERSION=${{ steps.meta.outputs.version }}
 | 
				
			||||||
            commitSHA=${{ steps.meta.outputs.revision }}
 | 
					            REVISION=${{ steps.meta.outputs.revision }}
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,7 @@ jobs:
 | 
				
			||||||
          JSON="${{ steps.meta.outputs.tags }}"
 | 
					          JSON="${{ steps.meta.outputs.tags }}"
 | 
				
			||||||
          # Use jq to remove 'v' from the version field
 | 
					          # Use jq to remove 'v' from the version field
 | 
				
			||||||
          JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
 | 
					          JSON=$(echo "$JSON" | sed 's/filebrowser:v/filebrowser:/')
 | 
				
			||||||
          echo "cleaned_tag=$JSON" >> $GITHUB_OUTPUT
 | 
					          echo "CLEANED_TAG=$JSON" >> $GITHUB_ENV
 | 
				
			||||||
      - name: Build and push
 | 
					      - name: Build and push
 | 
				
			||||||
        uses: docker/build-push-action@v6
 | 
					        uses: docker/build-push-action@v6
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
| 
						 | 
					@ -46,5 +46,5 @@ jobs:
 | 
				
			||||||
          platforms: linux/amd64
 | 
					          platforms: linux/amd64
 | 
				
			||||||
          file: ./Dockerfile
 | 
					          file: ./Dockerfile
 | 
				
			||||||
          push: true
 | 
					          push: true
 | 
				
			||||||
          tags: ${{ steps.modify-json.outputs.cleaned_tag }}
 | 
					          tags: ${{ env.CLEANED_TAG }}
 | 
				
			||||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
					          labels: ${{ steps.meta.outputs.labels }}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										19
									
								
								CHANGELOG.md
								
								
								
								
							
							
						
						
									
										19
									
								
								CHANGELOG.md
								
								
								
								
							| 
						 | 
					@ -2,6 +2,25 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
 | 
					All notable changes to this project will be documented in this file. For commit guidelines, please refer to [Standard Version](https://github.com/conventional-changelog/standard-version).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## v0.2.9
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  This release focused on UI navigation experience. Improving keyboard navigation and adds right click context menu.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  **New Features**:
 | 
				
			||||||
 | 
					  - listing view items are middle-clickable on selected listing or when in single-click mode.
 | 
				
			||||||
 | 
					  - listing view items can be navigated via arrow keys.
 | 
				
			||||||
 | 
					  - listing view can jump to items using letters and number keys to cycle through files that start with that character.
 | 
				
			||||||
 | 
					  - You can use the enter key and backspace key to navigate backwards and forwards on selected items.
 | 
				
			||||||
 | 
					  - ctr-space will open/close the search (leaving ctr-f to browser default find prompt)
 | 
				
			||||||
 | 
					  - Added right-click context menu to replace the file selection prompt.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  **Bugfixes**:
 | 
				
			||||||
 | 
					  - Fixed drag to upload not working.
 | 
				
			||||||
 | 
					  - Fixed shared video link issues.
 | 
				
			||||||
 | 
					  - Fixed user edit bug related to other user.
 | 
				
			||||||
 | 
					  - Fixed password reset bug.
 | 
				
			||||||
 | 
					  - Fixed loading state getting stuck.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## v0.2.8
 | 
					## v0.2.8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- **Feature**: New gallary view scaling options (closes [#141](https://github.com/gtsteffaniak/filebrowser/issues/141))
 | 
					- **Feature**: New gallary view scaling options (closes [#141](https://github.com/gtsteffaniak/filebrowser/issues/141))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@
 | 
				
			||||||
<p align="center">
 | 
					<p align="center">
 | 
				
			||||||
  <img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
 | 
					  <img src="frontend/public/img/icons/favicon-256x256.png" width="100" title="Login With Custom URL">
 | 
				
			||||||
</p>
 | 
					</p>
 | 
				
			||||||
<h3 align="center">Filebrowser Quantum - A modern web-based file manager</h3>
 | 
					<h3 align="center">FileBrowser Quantum - A modern web-based file manager</h3>
 | 
				
			||||||
<p align="center">
 | 
					<p align="center">
 | 
				
			||||||
  <img width="800" src="https://github.com/user-attachments/assets/8ba93582-aba2-4996-8ac3-25f763a2e596" title="Main Screenshot">
 | 
					  <img width="800" src="https://github.com/user-attachments/assets/8ba93582-aba2-4996-8ac3-25f763a2e596" title="Main Screenshot">
 | 
				
			||||||
</p>
 | 
					</p>
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@
 | 
				
			||||||
> Starting with v0.2.4 *ALL* share links need to be re-created (due to
 | 
					> Starting with v0.2.4 *ALL* share links need to be re-created (due to
 | 
				
			||||||
> security fix).
 | 
					> security fix).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Filebrowser Quantum is a fork of the filebrowser opensource project with the 
 | 
					FileBrowser Quantum is a fork of the filebrowser opensource project with the 
 | 
				
			||||||
following changes:
 | 
					following changes:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  1. [x] Enhanced lightning fast indexed search
 | 
					  1. [x] Enhanced lightning fast indexed search
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,7 @@ following changes:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## About
 | 
					## About
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Filebrowser Quantum provides a file managing interface within a specified directory
 | 
					FileBrowser Quantum provides a file managing interface within a specified directory
 | 
				
			||||||
and can be used to upload, delete, preview, rename, and edit your files.
 | 
					and can be used to upload, delete, preview, rename, and edit your files.
 | 
				
			||||||
It allows the creation of multiple users and each user can have its 
 | 
					It allows the creation of multiple users and each user can have its 
 | 
				
			||||||
directory.
 | 
					directory.
 | 
				
			||||||
| 
						 | 
					@ -44,7 +44,7 @@ aesthetics and performance. Improved search, simplified ui
 | 
				
			||||||
(without removing features) and more secure and up-to-date
 | 
					(without removing features) and more secure and up-to-date
 | 
				
			||||||
build are just a few examples.
 | 
					build are just a few examples.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Filebrowser Quantum differs significantly to the original.
 | 
					FileBrowser Quantum differs significantly to the original.
 | 
				
			||||||
There are hundreds of thousands of lines changed and they are generally
 | 
					There are hundreds of thousands of lines changed and they are generally
 | 
				
			||||||
no longer compatible with each other. This has been intentional -- the
 | 
					no longer compatible with each other. This has been intentional -- the
 | 
				
			||||||
focus of this fork is on a few key principles:
 | 
					focus of this fork is on a few key principles:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -187,7 +187,7 @@ func (a *HookAuth) SaveUser() (*users.User, error) {
 | 
				
			||||||
func (a *HookAuth) GetUser(d *users.User) *users.User {
 | 
					func (a *HookAuth) GetUser(d *users.User) *users.User {
 | 
				
			||||||
	// adds all permissions when user is admin
 | 
						// adds all permissions when user is admin
 | 
				
			||||||
	isAdmin := d.Perm.Admin
 | 
						isAdmin := d.Perm.Admin
 | 
				
			||||||
	perms := users.Permissions{
 | 
						perms := settings.Permissions{
 | 
				
			||||||
		Admin:    isAdmin,
 | 
							Admin:    isAdmin,
 | 
				
			||||||
		Execute:  isAdmin || d.Perm.Execute,
 | 
							Execute:  isAdmin || d.Perm.Execute,
 | 
				
			||||||
		Create:   isAdmin || d.Perm.Create,
 | 
							Create:   isAdmin || d.Perm.Create,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,49 +5,37 @@
 | 
				
			||||||
?   	github.com/gtsteffaniak/filebrowser/auth	[no test files]
 | 
					?   	github.com/gtsteffaniak/filebrowser/auth	[no test files]
 | 
				
			||||||
?   	github.com/gtsteffaniak/filebrowser/cmd	[no test files]
 | 
					?   	github.com/gtsteffaniak/filebrowser/cmd	[no test files]
 | 
				
			||||||
PASS
 | 
					PASS
 | 
				
			||||||
ok  	github.com/gtsteffaniak/filebrowser/diskcache	0.003s
 | 
					ok  	github.com/gtsteffaniak/filebrowser/diskcache	0.004s
 | 
				
			||||||
?   	github.com/gtsteffaniak/filebrowser/errors	[no test files]
 | 
					?   	github.com/gtsteffaniak/filebrowser/errors	[no test files]
 | 
				
			||||||
goos: linux
 | 
					goos: linux
 | 
				
			||||||
goarch: amd64
 | 
					goarch: amd64
 | 
				
			||||||
pkg: github.com/gtsteffaniak/filebrowser/files
 | 
					pkg: github.com/gtsteffaniak/filebrowser/files
 | 
				
			||||||
cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
 | 
					cpu: 11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz
 | 
				
			||||||
BenchmarkFillIndex-8          	      10	   3587120 ns/op	  273640 B/op	    2013 allocs/op
 | 
					BenchmarkFillIndex-8          	      10	   3559830 ns/op	  274639 B/op	    2026 allocs/op
 | 
				
			||||||
BenchmarkSearchAllIndexes-8   	      10	  31291180 ns/op	19500700 B/op	  298636 allocs/op
 | 
					BenchmarkSearchAllIndexes-8   	      10	  31912612 ns/op	20545741 B/op	  312477 allocs/op
 | 
				
			||||||
PASS
 | 
					PASS
 | 
				
			||||||
ok  	github.com/gtsteffaniak/filebrowser/files	0.408s
 | 
					ok  	github.com/gtsteffaniak/filebrowser/files	0.417s
 | 
				
			||||||
PASS
 | 
					PASS
 | 
				
			||||||
ok  	github.com/gtsteffaniak/filebrowser/fileutils	0.003s
 | 
					ok  	github.com/gtsteffaniak/filebrowser/fileutils	0.002s
 | 
				
			||||||
2024/02/07 07:16:43 Saving new user: publicUser
 | 
					2024/08/27 16:16:13 h: 401  <nil>
 | 
				
			||||||
2024/02/07 07:16:43 Saving new user: publicUser
 | 
					2024/08/27 16:16:13 h: 401  <nil>
 | 
				
			||||||
2024/02/07 07:16:43 Saving new user: publicUser
 | 
					2024/08/27 16:16:13 h: 401  <nil>
 | 
				
			||||||
2024/02/07 07:16:43 Saving new user: publicUser
 | 
					2024/08/27 16:16:13 h: 401  <nil>
 | 
				
			||||||
2024/02/07 07:16:43 Saving new user: publicUser
 | 
					2024/08/27 16:16:13 h: 401  <nil>
 | 
				
			||||||
2024/02/07 07:16:43 Saving new user: publicUser
 | 
					2024/08/27 16:16:13 h: 401  <nil>
 | 
				
			||||||
2024/02/07 07:16:43 Saving new user: publicUser
 | 
					 | 
				
			||||||
2024/02/07 07:16:43 Saving new user: publicUser
 | 
					 | 
				
			||||||
2024/02/07 07:16:43 h: 401  <nil>
 | 
					 | 
				
			||||||
2024/02/07 07:16:43 h: 401  <nil>
 | 
					 | 
				
			||||||
2024/02/07 07:16:43 h: 401  <nil>
 | 
					 | 
				
			||||||
2024/02/07 07:16:43 h: 401  <nil>
 | 
					 | 
				
			||||||
2024/02/07 07:16:43 Saving new user: publicUser
 | 
					 | 
				
			||||||
2024/02/07 07:16:43 Saving new user: publicUser
 | 
					 | 
				
			||||||
2024/02/07 07:16:43 Saving new user: publicUser
 | 
					 | 
				
			||||||
2024/02/07 07:16:43 Saving new user: publicUser
 | 
					 | 
				
			||||||
2024/02/07 07:16:43 h: 401  <nil>
 | 
					 | 
				
			||||||
2024/02/07 07:16:43 h: 401  <nil>
 | 
					 | 
				
			||||||
PASS
 | 
					PASS
 | 
				
			||||||
ok  	github.com/gtsteffaniak/filebrowser/http	0.202s
 | 
					ok  	github.com/gtsteffaniak/filebrowser/http	0.100s
 | 
				
			||||||
PASS
 | 
					PASS
 | 
				
			||||||
ok  	github.com/gtsteffaniak/filebrowser/img	0.125s
 | 
					ok  	github.com/gtsteffaniak/filebrowser/img	0.124s
 | 
				
			||||||
PASS
 | 
					PASS
 | 
				
			||||||
ok  	github.com/gtsteffaniak/filebrowser/rules	0.002s
 | 
					ok  	github.com/gtsteffaniak/filebrowser/rules	0.002s
 | 
				
			||||||
PASS
 | 
					PASS
 | 
				
			||||||
ok  	github.com/gtsteffaniak/filebrowser/runner	0.003s
 | 
					ok  	github.com/gtsteffaniak/filebrowser/runner	0.003s
 | 
				
			||||||
PASS
 | 
					PASS
 | 
				
			||||||
ok  	github.com/gtsteffaniak/filebrowser/settings	0.005s
 | 
					ok  	github.com/gtsteffaniak/filebrowser/settings	0.004s
 | 
				
			||||||
?   	github.com/gtsteffaniak/filebrowser/share	[no test files]
 | 
					?   	github.com/gtsteffaniak/filebrowser/share	[no test files]
 | 
				
			||||||
?   	github.com/gtsteffaniak/filebrowser/storage	[no test files]
 | 
					?   	github.com/gtsteffaniak/filebrowser/storage	[no test files]
 | 
				
			||||||
?   	github.com/gtsteffaniak/filebrowser/storage/bolt	[no test files]
 | 
					?   	github.com/gtsteffaniak/filebrowser/storage/bolt	[no test files]
 | 
				
			||||||
PASS
 | 
					PASS
 | 
				
			||||||
ok  	github.com/gtsteffaniak/filebrowser/users	0.003s
 | 
					ok  	github.com/gtsteffaniak/filebrowser/users	0.002s
 | 
				
			||||||
?   	github.com/gtsteffaniak/filebrowser/version	[no test files]
 | 
					?   	github.com/gtsteffaniak/filebrowser/version	[no test files]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,7 @@ import (
 | 
				
			||||||
	"github.com/gtsteffaniak/filebrowser/img"
 | 
						"github.com/gtsteffaniak/filebrowser/img"
 | 
				
			||||||
	"github.com/gtsteffaniak/filebrowser/settings"
 | 
						"github.com/gtsteffaniak/filebrowser/settings"
 | 
				
			||||||
	"github.com/gtsteffaniak/filebrowser/users"
 | 
						"github.com/gtsteffaniak/filebrowser/users"
 | 
				
			||||||
 | 
						"github.com/gtsteffaniak/filebrowser/version"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//go:embed dist/*
 | 
					//go:embed dist/*
 | 
				
			||||||
| 
						 | 
					@ -47,7 +48,7 @@ func init() {
 | 
				
			||||||
	// Bind the flags to the pflag command line parser
 | 
						// Bind the flags to the pflag command line parser
 | 
				
			||||||
	pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
 | 
						pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
 | 
				
			||||||
	pflag.Parse()
 | 
						pflag.Parse()
 | 
				
			||||||
	log.Println("Initializing with config file:", *configFlag)
 | 
						log.Printf("Initializing FileBrowser Quantum (%v) with config file: %v \n", version.Version, *configFlag)
 | 
				
			||||||
	log.Println("Embeded Frontend:", !nonEmbededFS)
 | 
						log.Println("Embeded Frontend:", !nonEmbededFS)
 | 
				
			||||||
	settings.Initialize(*configFlag)
 | 
						settings.Initialize(*configFlag)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -162,8 +163,7 @@ func quickSetup(d pythonData) {
 | 
				
			||||||
	checkErr("d.store.Settings.Save", err)
 | 
						checkErr("d.store.Settings.Save", err)
 | 
				
			||||||
	err = d.store.Settings.SaveServer(&settings.Config.Server)
 | 
						err = d.store.Settings.SaveServer(&settings.Config.Server)
 | 
				
			||||||
	checkErr("d.store.Settings.SaveServer", err)
 | 
						checkErr("d.store.Settings.SaveServer", err)
 | 
				
			||||||
	user := &users.User{}
 | 
						user := users.ApplyDefaults(users.User{})
 | 
				
			||||||
	settings.Config.UserDefaults.Apply(user)
 | 
					 | 
				
			||||||
	user.Username = settings.Config.Auth.AdminUsername
 | 
						user.Username = settings.Config.Auth.AdminUsername
 | 
				
			||||||
	user.Password = settings.Config.Auth.AdminPassword
 | 
						user.Password = settings.Config.Auth.AdminPassword
 | 
				
			||||||
	user.Perm.Admin = true
 | 
						user.Perm.Admin = true
 | 
				
			||||||
| 
						 | 
					@ -171,7 +171,7 @@ func quickSetup(d pythonData) {
 | 
				
			||||||
	user.DarkMode = true
 | 
						user.DarkMode = true
 | 
				
			||||||
	user.ViewMode = "normal"
 | 
						user.ViewMode = "normal"
 | 
				
			||||||
	user.LockPassword = false
 | 
						user.LockPassword = false
 | 
				
			||||||
	user.Perm = users.Permissions{
 | 
						user.Perm = settings.Permissions{
 | 
				
			||||||
		Create:   true,
 | 
							Create:   true,
 | 
				
			||||||
		Rename:   true,
 | 
							Rename:   true,
 | 
				
			||||||
		Modify:   true,
 | 
							Modify:   true,
 | 
				
			||||||
| 
						 | 
					@ -180,6 +180,6 @@ func quickSetup(d pythonData) {
 | 
				
			||||||
		Download: true,
 | 
							Download: true,
 | 
				
			||||||
		Admin:    true,
 | 
							Admin:    true,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	err = d.store.Users.Save(user)
 | 
						err = d.store.Users.Save(&user)
 | 
				
			||||||
	checkErr("d.store.Users.Save", err)
 | 
						checkErr("d.store.Users.Save", err)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,11 +67,6 @@ func getUserIdentifier(flags *pflag.FlagSet) interface{} {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func printRules(rulez []rules.Rule, id interface{}) {
 | 
					func printRules(rulez []rules.Rule, id interface{}) {
 | 
				
			||||||
	if id == nil {
 | 
					 | 
				
			||||||
		fmt.Printf("Global Rules:\n\n")
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		fmt.Printf("Rules for user %v:\n\n", id)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for id, rule := range rulez {
 | 
						for id, rule := range rulez {
 | 
				
			||||||
		fmt.Printf("(%d) ", id)
 | 
							fmt.Printf("(%d) ", id)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
package cmd
 | 
					package cmd
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"log"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/spf13/cobra"
 | 
						"github.com/spf13/cobra"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,6 @@ var usersRmCmd = &cobra.Command{
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		checkErr("usersRmCmd", err)
 | 
							checkErr("usersRmCmd", err)
 | 
				
			||||||
		fmt.Println("user deleted successfully")
 | 
							log.Println("user deleted successfully")
 | 
				
			||||||
	}, pythonConfig{}),
 | 
						}, pythonConfig{}),
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,6 @@ var versionCmd = &cobra.Command{
 | 
				
			||||||
	Use:   "version",
 | 
						Use:   "version",
 | 
				
			||||||
	Short: "Print the version number",
 | 
						Short: "Print the version number",
 | 
				
			||||||
	Run: func(cmd *cobra.Command, args []string) {
 | 
						Run: func(cmd *cobra.Command, args []string) {
 | 
				
			||||||
		fmt.Println("File Browser v" + version.Version + "/" + version.CommitSHA)
 | 
							fmt.Println("File Browser " + version.Version + "/" + version.CommitSHA)
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,8 @@
 | 
				
			||||||
package files
 | 
					package files
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"crypto/md5"  //nolint:gosec
 | 
						"crypto/md5"
 | 
				
			||||||
	"crypto/sha1" //nolint:gosec
 | 
						"crypto/sha1"
 | 
				
			||||||
	"crypto/sha256"
 | 
						"crypto/sha256"
 | 
				
			||||||
	"crypto/sha512"
 | 
						"crypto/sha512"
 | 
				
			||||||
	"encoding/hex"
 | 
						"encoding/hex"
 | 
				
			||||||
| 
						 | 
					@ -52,6 +52,7 @@ type FileInfo struct {
 | 
				
			||||||
// FileOptions are the options when getting a file info.
 | 
					// FileOptions are the options when getting a file info.
 | 
				
			||||||
type FileOptions struct {
 | 
					type FileOptions struct {
 | 
				
			||||||
	Path       string // realpath
 | 
						Path       string // realpath
 | 
				
			||||||
 | 
						IsDir      bool
 | 
				
			||||||
	Modify     bool
 | 
						Modify     bool
 | 
				
			||||||
	Expand     bool
 | 
						Expand     bool
 | 
				
			||||||
	ReadHeader bool
 | 
						ReadHeader bool
 | 
				
			||||||
| 
						 | 
					@ -83,7 +84,7 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
 | 
				
			||||||
	if !opts.Checker.Check(opts.Path) {
 | 
						if !opts.Checker.Check(opts.Path) {
 | 
				
			||||||
		return nil, os.ErrPermission
 | 
							return nil, os.ErrPermission
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	file, err := stat(opts.Path, opts) // Pass opts.Path here
 | 
						file, err := stat(opts)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -101,7 +102,6 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return file, err
 | 
						return file, err
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
 | 
					func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
 | 
				
			||||||
	// Lock access for the specific path
 | 
						// Lock access for the specific path
 | 
				
			||||||
	pathMutex := getMutex(opts.Path)
 | 
						pathMutex := getMutex(opts.Path)
 | 
				
			||||||
| 
						 | 
					@ -111,71 +111,65 @@ func FileInfoFaster(opts FileOptions) (*FileInfo, error) {
 | 
				
			||||||
		return nil, os.ErrPermission
 | 
							return nil, os.ErrPermission
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	index := GetIndex(rootPath)
 | 
						index := GetIndex(rootPath)
 | 
				
			||||||
	trimmed := strings.TrimPrefix(opts.Path, "/")
 | 
						adjustedPath := index.makeIndexPath(opts.Path, opts.IsDir)
 | 
				
			||||||
	if trimmed == "" {
 | 
						if opts.IsDir {
 | 
				
			||||||
		trimmed = "/"
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	adjustedPath := makeIndexPath(trimmed, index.Root)
 | 
					 | 
				
			||||||
	var info FileInfo
 | 
					 | 
				
			||||||
		info, exists := index.GetMetadataInfo(adjustedPath)
 | 
							info, exists := index.GetMetadataInfo(adjustedPath)
 | 
				
			||||||
		if exists && !opts.Content {
 | 
							if exists && !opts.Content {
 | 
				
			||||||
		// Check if the cache time is less than 1 second
 | 
								// Let's not refresh if less than a second has passed
 | 
				
			||||||
			if time.Since(info.CacheTime) > time.Second {
 | 
								if time.Since(info.CacheTime) > time.Second {
 | 
				
			||||||
			go RefreshFileInfo(opts)
 | 
									go RefreshFileInfo(opts) //nolint:errcheck
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			// refresh cache after
 | 
								// refresh cache after
 | 
				
			||||||
			return &info, nil
 | 
								return &info, nil
 | 
				
			||||||
	} else {
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	// don't bother caching content
 | 
						// don't bother caching content
 | 
				
			||||||
	if opts.Content {
 | 
						if opts.Content {
 | 
				
			||||||
		file, err := NewFileInfo(opts)
 | 
							file, err := NewFileInfo(opts)
 | 
				
			||||||
		return file, err
 | 
							return file, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
		updated := RefreshFileInfo(opts)
 | 
						err := RefreshFileInfo(opts)
 | 
				
			||||||
		if !updated {
 | 
						if err != nil {
 | 
				
			||||||
		file, err := NewFileInfo(opts)
 | 
							file, err := NewFileInfo(opts)
 | 
				
			||||||
		return file, err
 | 
							return file, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
		info, exists = index.GetMetadataInfo(adjustedPath)
 | 
						info, exists := index.GetMetadataInfo(adjustedPath)
 | 
				
			||||||
	if !exists || info.Name == "" {
 | 
						if !exists || info.Name == "" {
 | 
				
			||||||
		return &FileInfo{}, errors.ErrEmptyKey
 | 
							return &FileInfo{}, errors.ErrEmptyKey
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return &info, nil
 | 
						return &info, nil
 | 
				
			||||||
	}
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func RefreshFileInfo(opts FileOptions) bool {
 | 
					func RefreshFileInfo(opts FileOptions) error {
 | 
				
			||||||
	if !opts.Checker.Check(opts.Path) {
 | 
						if !opts.Checker.Check(opts.Path) {
 | 
				
			||||||
		return false
 | 
							return fmt.Errorf("permission denied: %s", opts.Path)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	index := GetIndex(rootPath)
 | 
						index := GetIndex(rootPath)
 | 
				
			||||||
	trimmed := strings.TrimPrefix(opts.Path, "/")
 | 
						adjustedPath := index.makeIndexPath(opts.Path, opts.IsDir)
 | 
				
			||||||
	if trimmed == "" {
 | 
						file, err := stat(opts)
 | 
				
			||||||
		trimmed = "/"
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	adjustedPath := makeIndexPath(trimmed, index.Root)
 | 
					 | 
				
			||||||
	file, err := stat(opts.Path, opts) // Pass opts.Path here
 | 
					 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return false
 | 
							return fmt.Errorf("File/folder does not exist to refresh data: %s", opts.Path)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	_ = file.detectType(adjustedPath, true, opts.Content, opts.ReadHeader)
 | 
						_ = file.detectType(opts.Path, true, opts.Content, opts.ReadHeader)
 | 
				
			||||||
	if file.IsDir {
 | 
						if file.IsDir {
 | 
				
			||||||
		err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader)
 | 
							err := file.readListing(opts.Path, opts.Checker, opts.ReadHeader)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return false
 | 
								return fmt.Errorf("Dir info could not be read: %s", opts.Path)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return index.UpdateFileMetadata(adjustedPath, *file)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		return index.UpdateFileMetadata(adjustedPath, *file)
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						result := index.UpdateFileMetadata(adjustedPath, *file)
 | 
				
			||||||
 | 
						if !result {
 | 
				
			||||||
 | 
							return fmt.Errorf("File/folder does not exist in metadata: %s", adjustedPath)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func stat(path string, opts FileOptions) (*FileInfo, error) {
 | 
					func stat(opts FileOptions) (*FileInfo, error) {
 | 
				
			||||||
	info, err := os.Lstat(path)
 | 
						info, err := os.Lstat(opts.Path)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	file := &FileInfo{
 | 
						file := &FileInfo{
 | 
				
			||||||
		Path:      opts.Path,
 | 
							Path:      opts.Path,
 | 
				
			||||||
		Name:      info.Name(),
 | 
							Name:      info.Name(),
 | 
				
			||||||
| 
						 | 
					@ -185,13 +179,12 @@ func stat(path string, opts FileOptions) (*FileInfo, error) {
 | 
				
			||||||
		Extension: filepath.Ext(info.Name()),
 | 
							Extension: filepath.Ext(info.Name()),
 | 
				
			||||||
		Token:     opts.Token,
 | 
							Token:     opts.Token,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	if info.IsDir() {
 | 
						if info.IsDir() {
 | 
				
			||||||
		file.IsDir = true
 | 
							file.IsDir = true
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if info.Mode()&os.ModeSymlink != 0 {
 | 
						if info.Mode()&os.ModeSymlink != 0 {
 | 
				
			||||||
		file.IsSymlink = true
 | 
							file.IsSymlink = true
 | 
				
			||||||
		targetInfo, err := os.Stat(path)
 | 
							targetInfo, err := os.Stat(opts.Path)
 | 
				
			||||||
		if err == nil {
 | 
							if err == nil {
 | 
				
			||||||
			file.Size = targetInfo.Size()
 | 
								file.Size = targetInfo.Size()
 | 
				
			||||||
			file.IsDir = targetInfo.IsDir()
 | 
								file.IsDir = targetInfo.IsDir()
 | 
				
			||||||
| 
						 | 
					@ -248,20 +241,19 @@ func (i *FileInfo) RealPath() string {
 | 
				
			||||||
	return i.Path
 | 
						return i.Path
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func GetRealPath(relativePath ...string) (string, error) {
 | 
					func GetRealPath(relativePath ...string) (string, bool, error) {
 | 
				
			||||||
	combined := []string{settings.Config.Server.Root}
 | 
						combined := []string{settings.Config.Server.Root}
 | 
				
			||||||
	for _, path := range relativePath {
 | 
						for _, path := range relativePath {
 | 
				
			||||||
		combined = append(combined, strings.TrimPrefix(path, settings.Config.Server.Root))
 | 
							combined = append(combined, strings.TrimPrefix(path, settings.Config.Server.Root))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	joinedPath := filepath.Join(combined...)
 | 
						joinedPath := filepath.Join(combined...)
 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Convert relative path to absolute path
 | 
						// Convert relative path to absolute path
 | 
				
			||||||
	absolutePath, err := filepath.Abs(joinedPath)
 | 
						absolutePath, err := filepath.Abs(joinedPath)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return "", err
 | 
							return "", false, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if !Exists(absolutePath) {
 | 
						if !Exists(absolutePath) {
 | 
				
			||||||
		return absolutePath, nil // return without error
 | 
							return absolutePath, false, nil // return without error
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// Resolve symlinks and get the real path
 | 
						// Resolve symlinks and get the real path
 | 
				
			||||||
	return resolveSymlinks(absolutePath)
 | 
						return resolveSymlinks(absolutePath)
 | 
				
			||||||
| 
						 | 
					@ -272,10 +264,9 @@ func DeleteFiles(absPath string, opts FileOptions) error {
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	parentDir := filepath.Dir(absPath)
 | 
						opts.Path = filepath.Dir(absPath)
 | 
				
			||||||
	opts.Path = parentDir
 | 
						err = RefreshFileInfo(opts)
 | 
				
			||||||
	updated := RefreshFileInfo(opts)
 | 
						if err != nil {
 | 
				
			||||||
	if !updated {
 | 
					 | 
				
			||||||
		return errors.ErrEmptyKey
 | 
							return errors.ErrEmptyKey
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
| 
						 | 
					@ -288,16 +279,14 @@ func WriteDirectory(opts FileOptions) error {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	opts.Path = filepath.Dir(opts.Path)
 | 
						opts.Path = filepath.Dir(opts.Path)
 | 
				
			||||||
	updated := RefreshFileInfo(opts)
 | 
						err = RefreshFileInfo(opts)
 | 
				
			||||||
	if !updated {
 | 
						if err != nil {
 | 
				
			||||||
		return errors.ErrEmptyKey
 | 
							return errors.ErrEmptyKey
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func WriteFile(opts FileOptions, in io.Reader) error {
 | 
					func WriteFile(opts FileOptions, in io.Reader) error {
 | 
				
			||||||
	fmt.Println("writing file", opts.Path)
 | 
					 | 
				
			||||||
	dst := opts.Path
 | 
						dst := opts.Path
 | 
				
			||||||
	parentDir := filepath.Dir(dst)
 | 
						parentDir := filepath.Dir(dst)
 | 
				
			||||||
	// Split the directory from the destination path
 | 
						// Split the directory from the destination path
 | 
				
			||||||
| 
						 | 
					@ -321,23 +310,21 @@ func WriteFile(opts FileOptions, in io.Reader) error {
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	fmt.Println("refreshing info for ", parentDir)
 | 
					 | 
				
			||||||
	opts.Path = parentDir
 | 
						opts.Path = parentDir
 | 
				
			||||||
	updated := RefreshFileInfo(opts)
 | 
						err = RefreshFileInfo(opts)
 | 
				
			||||||
	if !updated {
 | 
						if err != nil {
 | 
				
			||||||
		return errors.ErrEmptyKey
 | 
							return errors.ErrEmptyKey
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// resolveSymlinks resolves symlinks in the given path
 | 
					// resolveSymlinks resolves symlinks in the given path
 | 
				
			||||||
func resolveSymlinks(path string) (string, error) {
 | 
					func resolveSymlinks(path string) (string, bool, error) {
 | 
				
			||||||
	for {
 | 
						for {
 | 
				
			||||||
		// Get the file info
 | 
							// Get the file info
 | 
				
			||||||
		info, err := os.Lstat(path)
 | 
							info, err := os.Lstat(path)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return "", err
 | 
								return "", false, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Check if it's a symlink
 | 
							// Check if it's a symlink
 | 
				
			||||||
| 
						 | 
					@ -345,14 +332,14 @@ func resolveSymlinks(path string) (string, error) {
 | 
				
			||||||
			// Read the symlink target
 | 
								// Read the symlink target
 | 
				
			||||||
			target, err := os.Readlink(path)
 | 
								target, err := os.Readlink(path)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				return "", err
 | 
									return "", false, err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Resolve the target relative to the symlink's directory
 | 
								// Resolve the target relative to the symlink's directory
 | 
				
			||||||
			path = filepath.Join(filepath.Dir(path), target)
 | 
								path = filepath.Join(filepath.Dir(path), target)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			// Not a symlink, so we are done
 | 
								// Not a symlink, so return the resolved path and check if it's a directory
 | 
				
			||||||
			return path, nil
 | 
								return path, info.IsDir(), nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,76 @@
 | 
				
			||||||
 | 
					package files
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func Test_GetRealPath(t *testing.T) {
 | 
				
			||||||
 | 
						cwd, err := os.Getwd()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						trimPrefix := filepath.Dir(filepath.Dir(cwd)) + "/"
 | 
				
			||||||
 | 
						tests := []struct {
 | 
				
			||||||
 | 
							name  string
 | 
				
			||||||
 | 
							paths []string
 | 
				
			||||||
 | 
							want  struct {
 | 
				
			||||||
 | 
								path  string
 | 
				
			||||||
 | 
								isDir bool
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "current directory",
 | 
				
			||||||
 | 
								paths: []string{
 | 
				
			||||||
 | 
									"./",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								want: struct {
 | 
				
			||||||
 | 
									path  string
 | 
				
			||||||
 | 
									isDir bool
 | 
				
			||||||
 | 
								}{
 | 
				
			||||||
 | 
									path:  "backend/files",
 | 
				
			||||||
 | 
									isDir: true,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "current directory",
 | 
				
			||||||
 | 
								paths: []string{
 | 
				
			||||||
 | 
									"./file.go",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								want: struct {
 | 
				
			||||||
 | 
									path  string
 | 
				
			||||||
 | 
									isDir bool
 | 
				
			||||||
 | 
								}{
 | 
				
			||||||
 | 
									path:  "backend/files/file.go",
 | 
				
			||||||
 | 
									isDir: false,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "other test case",
 | 
				
			||||||
 | 
								paths: []string{
 | 
				
			||||||
 | 
									"/mnt/doesnt/exist",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								want: struct {
 | 
				
			||||||
 | 
									path  string
 | 
				
			||||||
 | 
									isDir bool
 | 
				
			||||||
 | 
								}{
 | 
				
			||||||
 | 
									path:  "/mnt/doesnt/exist",
 | 
				
			||||||
 | 
									isDir: false,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						for _, tt := range tests {
 | 
				
			||||||
 | 
							t.Run(tt.name, func(t *testing.T) {
 | 
				
			||||||
 | 
								realPath, isDir, err := GetRealPath(tt.paths...)
 | 
				
			||||||
 | 
								adjustedRealPath := strings.TrimPrefix(realPath, trimPrefix)
 | 
				
			||||||
 | 
								if tt.want.path != adjustedRealPath || tt.want.isDir != isDir {
 | 
				
			||||||
 | 
									t.Errorf("expected %v:%v but got: %v:%v", tt.want.path, tt.want.isDir, adjustedRealPath, isDir)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									t.Error("got error", err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
| 
						 | 
					@ -15,6 +16,7 @@ type Directory struct {
 | 
				
			||||||
	Metadata map[string]FileInfo
 | 
						Metadata map[string]FileInfo
 | 
				
			||||||
	Files    string
 | 
						Files    string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type File struct {
 | 
					type File struct {
 | 
				
			||||||
	Name  string
 | 
						Name  string
 | 
				
			||||||
	IsDir bool
 | 
						IsDir bool
 | 
				
			||||||
| 
						 | 
					@ -80,8 +82,7 @@ func indexingScheduler(intervalMinutes uint32) {
 | 
				
			||||||
// Define a function to recursively index files and directories
 | 
					// Define a function to recursively index files and directories
 | 
				
			||||||
func (si *Index) indexFiles(path string) error {
 | 
					func (si *Index) indexFiles(path string) error {
 | 
				
			||||||
	// Check if the current directory has been modified since the last indexing
 | 
						// Check if the current directory has been modified since the last indexing
 | 
				
			||||||
	path = strings.TrimSuffix(path, "/")
 | 
						adjustedPath := si.makeIndexPath(path, true)
 | 
				
			||||||
	adjustedPath := makeIndexPath(path, si.Root)
 | 
					 | 
				
			||||||
	dir, err := os.Open(path)
 | 
						dir, err := os.Open(path)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		// Directory must have been deleted, remove it from the index
 | 
							// Directory must have been deleted, remove it from the index
 | 
				
			||||||
| 
						 | 
					@ -114,7 +115,7 @@ func (si *Index) indexFiles(path string) error {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (si *Index) InsertFiles(path string) {
 | 
					func (si *Index) InsertFiles(path string) {
 | 
				
			||||||
	adjustedPath := makeIndexPath(path, si.Root)
 | 
						adjustedPath := si.makeIndexPath(path, false)
 | 
				
			||||||
	subDirectory := Directory{}
 | 
						subDirectory := Directory{}
 | 
				
			||||||
	buffer := bytes.Buffer{}
 | 
						buffer := bytes.Buffer{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -130,9 +131,9 @@ func (si *Index) InsertFiles(path string) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (si *Index) InsertDirs(path string) {
 | 
					func (si *Index) InsertDirs(path string) {
 | 
				
			||||||
	adjustedPath := makeIndexPath(path, si.Root)
 | 
					 | 
				
			||||||
	for _, f := range si.GetQuickList() {
 | 
						for _, f := range si.GetQuickList() {
 | 
				
			||||||
		if f.IsDir {
 | 
							if f.IsDir {
 | 
				
			||||||
 | 
								adjustedPath := si.makeIndexPath(path, true)
 | 
				
			||||||
			if _, exists := si.Directories[adjustedPath]; exists {
 | 
								if _, exists := si.Directories[adjustedPath]; exists {
 | 
				
			||||||
				si.UpdateCount("dirs")
 | 
									si.UpdateCount("dirs")
 | 
				
			||||||
				// Add or update the directory in the map
 | 
									// Add or update the directory in the map
 | 
				
			||||||
| 
						 | 
					@ -154,14 +155,21 @@ func (si *Index) InsertDirs(path string) {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func makeIndexPath(path string, root string) string {
 | 
					func (si *Index) makeIndexPath(subPath string, isDir bool) string {
 | 
				
			||||||
	if path == root {
 | 
						if si.Root == subPath {
 | 
				
			||||||
		return "/"
 | 
							return "/"
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	adjustedPath := strings.TrimPrefix(path, root+"/")
 | 
						// clean path
 | 
				
			||||||
 | 
						subPath = strings.TrimSuffix(subPath, "/")
 | 
				
			||||||
 | 
						// remove index prefix
 | 
				
			||||||
 | 
						adjustedPath := strings.TrimPrefix(subPath, si.Root)
 | 
				
			||||||
 | 
						// remove trailing slash
 | 
				
			||||||
	adjustedPath = strings.TrimSuffix(adjustedPath, "/")
 | 
						adjustedPath = strings.TrimSuffix(adjustedPath, "/")
 | 
				
			||||||
 | 
						// add leading slash for root of index
 | 
				
			||||||
	if adjustedPath == "" {
 | 
						if adjustedPath == "" {
 | 
				
			||||||
		adjustedPath = "/"
 | 
							adjustedPath = "/"
 | 
				
			||||||
 | 
						} else if !isDir {
 | 
				
			||||||
 | 
							adjustedPath = filepath.Dir(adjustedPath)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return adjustedPath
 | 
						return adjustedPath
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,24 +8,6 @@ import (
 | 
				
			||||||
	"github.com/gtsteffaniak/filebrowser/settings"
 | 
						"github.com/gtsteffaniak/filebrowser/settings"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetFileMetadata retrieves the FileInfo from the specified directory in the index.
 | 
					 | 
				
			||||||
func (si *Index) GetFileMetadata(adjustedPath string) (FileInfo, bool) {
 | 
					 | 
				
			||||||
	si.mu.RLock()
 | 
					 | 
				
			||||||
	dir, exists := si.Directories[adjustedPath]
 | 
					 | 
				
			||||||
	si.mu.RUnlock()
 | 
					 | 
				
			||||||
	if exists {
 | 
					 | 
				
			||||||
		// Initialize the Metadata map if it is nil
 | 
					 | 
				
			||||||
		if dir.Metadata == nil {
 | 
					 | 
				
			||||||
			dir.Metadata = make(map[string]FileInfo)
 | 
					 | 
				
			||||||
			si.SetDirectoryInfo(adjustedPath, dir)
 | 
					 | 
				
			||||||
			return FileInfo{}, false
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			return dir.Metadata[adjustedPath], true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return FileInfo{}, false
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
 | 
					// UpdateFileMetadata updates the FileInfo for the specified directory in the index.
 | 
				
			||||||
func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool {
 | 
					func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool {
 | 
				
			||||||
	si.mu.Lock()
 | 
						si.mu.Lock()
 | 
				
			||||||
| 
						 | 
					@ -45,7 +27,6 @@ func (si *Index) UpdateFileMetadata(adjustedPath string, info FileInfo) bool {
 | 
				
			||||||
// SetFileMetadata sets the FileInfo for the specified directory in the index.
 | 
					// SetFileMetadata sets the FileInfo for the specified directory in the index.
 | 
				
			||||||
// internal use only
 | 
					// internal use only
 | 
				
			||||||
func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool {
 | 
					func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool {
 | 
				
			||||||
 | 
					 | 
				
			||||||
	_, exists := si.Directories[adjustedPath]
 | 
						_, exists := si.Directories[adjustedPath]
 | 
				
			||||||
	if !exists {
 | 
						if !exists {
 | 
				
			||||||
		return false
 | 
							return false
 | 
				
			||||||
| 
						 | 
					@ -57,6 +38,7 @@ func (si *Index) SetFileMetadata(adjustedPath string, info FileInfo) bool {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetMetadataInfo retrieves the FileInfo from the specified directory in the index.
 | 
					// GetMetadataInfo retrieves the FileInfo from the specified directory in the index.
 | 
				
			||||||
func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) {
 | 
					func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) {
 | 
				
			||||||
 | 
						fi := FileInfo{}
 | 
				
			||||||
	si.mu.RLock()
 | 
						si.mu.RLock()
 | 
				
			||||||
	dir, exists := si.Directories[adjustedPath]
 | 
						dir, exists := si.Directories[adjustedPath]
 | 
				
			||||||
	si.mu.RUnlock()
 | 
						si.mu.RUnlock()
 | 
				
			||||||
| 
						 | 
					@ -65,11 +47,11 @@ func (si *Index) GetMetadataInfo(adjustedPath string) (FileInfo, bool) {
 | 
				
			||||||
		if dir.Metadata == nil {
 | 
							if dir.Metadata == nil {
 | 
				
			||||||
			dir.Metadata = make(map[string]FileInfo)
 | 
								dir.Metadata = make(map[string]FileInfo)
 | 
				
			||||||
			si.SetDirectoryInfo(adjustedPath, dir)
 | 
								si.SetDirectoryInfo(adjustedPath, dir)
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								fi = dir.Metadata[adjustedPath]
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		info, metadataExists := dir.Metadata[adjustedPath]
 | 
					 | 
				
			||||||
		return info, metadataExists
 | 
					 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return FileInfo{}, false
 | 
						return fi, exists
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SetDirectoryInfo sets the directory information in the index.
 | 
					// SetDirectoryInfo sets the directory information in the index.
 | 
				
			||||||
| 
						 | 
					@ -84,10 +66,7 @@ func (si *Index) GetDirectoryInfo(adjustedPath string) (Directory, bool) {
 | 
				
			||||||
	si.mu.RLock()
 | 
						si.mu.RLock()
 | 
				
			||||||
	dir, exists := si.Directories[adjustedPath]
 | 
						dir, exists := si.Directories[adjustedPath]
 | 
				
			||||||
	si.mu.RUnlock()
 | 
						si.mu.RUnlock()
 | 
				
			||||||
	if exists {
 | 
						return dir, exists
 | 
				
			||||||
		return dir, true
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return Directory{}, false
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (si *Index) RemoveDirectory(path string) {
 | 
					func (si *Index) RemoveDirectory(path string) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,220 @@
 | 
				
			||||||
 | 
					package files
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"io/fs"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Mock for fs.FileInfo
 | 
				
			||||||
 | 
					type mockFileInfo struct {
 | 
				
			||||||
 | 
						name  string
 | 
				
			||||||
 | 
						isDir bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (m mockFileInfo) Name() string       { return m.name }
 | 
				
			||||||
 | 
					func (m mockFileInfo) Size() int64        { return 0 }
 | 
				
			||||||
 | 
					func (m mockFileInfo) Mode() os.FileMode  { return 0 }
 | 
				
			||||||
 | 
					func (m mockFileInfo) ModTime() time.Time { return time.Now() }
 | 
				
			||||||
 | 
					func (m mockFileInfo) IsDir() bool        { return m.isDir }
 | 
				
			||||||
 | 
					func (m mockFileInfo) Sys() interface{}   { return nil }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var testIndex Index
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Test for GetFileMetadata
 | 
				
			||||||
 | 
					//func TestGetFileMetadata(t *testing.T) {
 | 
				
			||||||
 | 
					//	t.Parallel()
 | 
				
			||||||
 | 
					//	tests := []struct {
 | 
				
			||||||
 | 
					//		name           string
 | 
				
			||||||
 | 
					//		adjustedPath   string
 | 
				
			||||||
 | 
					//		fileName       string
 | 
				
			||||||
 | 
					//		expectedName   string
 | 
				
			||||||
 | 
					//		expectedExists bool
 | 
				
			||||||
 | 
					//	}{
 | 
				
			||||||
 | 
					//		{
 | 
				
			||||||
 | 
					//			name:           "testpath exists",
 | 
				
			||||||
 | 
					//			adjustedPath:   "/testpath",
 | 
				
			||||||
 | 
					//			fileName:       "testfile.txt",
 | 
				
			||||||
 | 
					//			expectedName:   "testfile.txt",
 | 
				
			||||||
 | 
					//			expectedExists: true,
 | 
				
			||||||
 | 
					//		},
 | 
				
			||||||
 | 
					//		{
 | 
				
			||||||
 | 
					//			name:           "testpath not exists",
 | 
				
			||||||
 | 
					//			adjustedPath:   "/testpath",
 | 
				
			||||||
 | 
					//			fileName:       "nonexistent.txt",
 | 
				
			||||||
 | 
					//			expectedName:   "",
 | 
				
			||||||
 | 
					//			expectedExists: false,
 | 
				
			||||||
 | 
					//		},
 | 
				
			||||||
 | 
					//		{
 | 
				
			||||||
 | 
					//			name:           "File exists in /anotherpath",
 | 
				
			||||||
 | 
					//			adjustedPath:   "/anotherpath",
 | 
				
			||||||
 | 
					//			fileName:       "afile.txt",
 | 
				
			||||||
 | 
					//			expectedName:   "afile.txt",
 | 
				
			||||||
 | 
					//			expectedExists: true,
 | 
				
			||||||
 | 
					//		},
 | 
				
			||||||
 | 
					//		{
 | 
				
			||||||
 | 
					//			name:           "File does not exist in /anotherpath",
 | 
				
			||||||
 | 
					//			adjustedPath:   "/anotherpath",
 | 
				
			||||||
 | 
					//			fileName:       "nonexistentfile.txt",
 | 
				
			||||||
 | 
					//			expectedName:   "",
 | 
				
			||||||
 | 
					//			expectedExists: false,
 | 
				
			||||||
 | 
					//		},
 | 
				
			||||||
 | 
					//		{
 | 
				
			||||||
 | 
					//			name:           "Directory does not exist",
 | 
				
			||||||
 | 
					//			adjustedPath:   "/nonexistentpath",
 | 
				
			||||||
 | 
					//			fileName:       "testfile.txt",
 | 
				
			||||||
 | 
					//			expectedName:   "",
 | 
				
			||||||
 | 
					//			expectedExists: false,
 | 
				
			||||||
 | 
					//		},
 | 
				
			||||||
 | 
					//	}
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//	for _, tt := range tests {
 | 
				
			||||||
 | 
					//		t.Run(tt.name, func(t *testing.T) {
 | 
				
			||||||
 | 
					//			fileInfo, exists := testIndex.GetFileMetadata(tt.adjustedPath)
 | 
				
			||||||
 | 
					//			if exists != tt.expectedExists || fileInfo.Name != tt.expectedName {
 | 
				
			||||||
 | 
					//				t.Errorf("expected %v:%v but got: %v:%v", tt.expectedName, tt.expectedExists, //fileInfo.Name, exists)
 | 
				
			||||||
 | 
					//			}
 | 
				
			||||||
 | 
					//		})
 | 
				
			||||||
 | 
					//	}
 | 
				
			||||||
 | 
					//}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Test for UpdateFileMetadata
 | 
				
			||||||
 | 
					func TestUpdateFileMetadata(t *testing.T) {
 | 
				
			||||||
 | 
						index := &Index{
 | 
				
			||||||
 | 
							Directories: map[string]Directory{
 | 
				
			||||||
 | 
								"/testpath": {
 | 
				
			||||||
 | 
									Metadata: map[string]FileInfo{
 | 
				
			||||||
 | 
										"testfile.txt":    {Name: "testfile.txt"},
 | 
				
			||||||
 | 
										"anotherfile.txt": {Name: "anotherfile.txt"},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						info := FileInfo{Name: "testfile.txt"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						success := index.UpdateFileMetadata("/testpath", info)
 | 
				
			||||||
 | 
						if !success {
 | 
				
			||||||
 | 
							t.Fatalf("expected UpdateFileMetadata to succeed")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						dir, exists := index.Directories["/testpath"]
 | 
				
			||||||
 | 
						if !exists || dir.Metadata["testfile.txt"].Name != "testfile.txt" {
 | 
				
			||||||
 | 
							t.Fatalf("expected testfile.txt to be updated in the directory metadata")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Test for GetDirMetadata
 | 
				
			||||||
 | 
					func TestGetDirMetadata(t *testing.T) {
 | 
				
			||||||
 | 
						t.Parallel()
 | 
				
			||||||
 | 
						_, exists := testIndex.GetMetadataInfo("/testpath")
 | 
				
			||||||
 | 
						if !exists {
 | 
				
			||||||
 | 
							t.Fatalf("expected GetDirMetadata to return initialized metadata map")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, exists = testIndex.GetMetadataInfo("/nonexistent")
 | 
				
			||||||
 | 
						if exists {
 | 
				
			||||||
 | 
							t.Fatalf("expected GetDirMetadata to return false for nonexistent directory")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Test for SetDirectoryInfo
 | 
				
			||||||
 | 
					func TestSetDirectoryInfo(t *testing.T) {
 | 
				
			||||||
 | 
						index := &Index{
 | 
				
			||||||
 | 
							Directories: map[string]Directory{
 | 
				
			||||||
 | 
								"/testpath": {
 | 
				
			||||||
 | 
									Metadata: map[string]FileInfo{
 | 
				
			||||||
 | 
										"testfile.txt":    {Name: "testfile.txt"},
 | 
				
			||||||
 | 
										"anotherfile.txt": {Name: "anotherfile.txt"},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						dir := Directory{Metadata: map[string]FileInfo{"testfile.txt": {Name: "testfile.txt"}}}
 | 
				
			||||||
 | 
						index.SetDirectoryInfo("/newPath", dir)
 | 
				
			||||||
 | 
						storedDir, exists := index.Directories["/newPath"]
 | 
				
			||||||
 | 
						if !exists || storedDir.Metadata["testfile.txt"].Name != "testfile.txt" {
 | 
				
			||||||
 | 
							t.Fatalf("expected SetDirectoryInfo to store directory info correctly")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Test for GetDirectoryInfo
 | 
				
			||||||
 | 
					func TestGetDirectoryInfo(t *testing.T) {
 | 
				
			||||||
 | 
						t.Parallel()
 | 
				
			||||||
 | 
						dir, exists := testIndex.GetDirectoryInfo("/testpath")
 | 
				
			||||||
 | 
						if !exists || dir.Metadata["testfile.txt"].Name != "testfile.txt" {
 | 
				
			||||||
 | 
							t.Fatalf("expected GetDirectoryInfo to return correct directory info")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, exists = testIndex.GetDirectoryInfo("/nonexistent")
 | 
				
			||||||
 | 
						if exists {
 | 
				
			||||||
 | 
							t.Fatalf("expected GetDirectoryInfo to return false for nonexistent directory")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Test for RemoveDirectory
 | 
				
			||||||
 | 
					func TestRemoveDirectory(t *testing.T) {
 | 
				
			||||||
 | 
						index := &Index{
 | 
				
			||||||
 | 
							Directories: map[string]Directory{
 | 
				
			||||||
 | 
								"/testpath": {},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						index.RemoveDirectory("/testpath")
 | 
				
			||||||
 | 
						_, exists := index.Directories["/testpath"]
 | 
				
			||||||
 | 
						if exists {
 | 
				
			||||||
 | 
							t.Fatalf("expected directory to be removed")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Test for UpdateCount
 | 
				
			||||||
 | 
					func TestUpdateCount(t *testing.T) {
 | 
				
			||||||
 | 
						index := &Index{}
 | 
				
			||||||
 | 
						index.UpdateCount("files")
 | 
				
			||||||
 | 
						if index.NumFiles != 1 {
 | 
				
			||||||
 | 
							t.Fatalf("expected NumFiles to be 1 after UpdateCount('files')")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if index.NumFiles != 1 {
 | 
				
			||||||
 | 
							t.Fatalf("expected NumFiles to be 1 after UpdateCount('files')")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						index.UpdateCount("dirs")
 | 
				
			||||||
 | 
						if index.NumDirs != 1 {
 | 
				
			||||||
 | 
							t.Fatalf("expected NumDirs to be 1 after UpdateCount('dirs')")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						index.UpdateCount("unknown")
 | 
				
			||||||
 | 
						// Just ensure it does not panic or update any counters
 | 
				
			||||||
 | 
						if index.NumFiles != 1 || index.NumDirs != 1 {
 | 
				
			||||||
 | 
							t.Fatalf("expected counts to remain unchanged for unknown type")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						index.resetCount()
 | 
				
			||||||
 | 
						if index.NumFiles != 0 || index.NumDirs != 0 || !index.inProgress {
 | 
				
			||||||
 | 
							t.Fatalf("expected resetCount to reset counts and set inProgress to true")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func init() {
 | 
				
			||||||
 | 
						testIndex = Index{
 | 
				
			||||||
 | 
							NumFiles:   10,
 | 
				
			||||||
 | 
							NumDirs:    5,
 | 
				
			||||||
 | 
							inProgress: false,
 | 
				
			||||||
 | 
							Directories: map[string]Directory{
 | 
				
			||||||
 | 
								"/testpath": {
 | 
				
			||||||
 | 
									Metadata: map[string]FileInfo{
 | 
				
			||||||
 | 
										"testfile.txt":    {Name: "testfile.txt"},
 | 
				
			||||||
 | 
										"anotherfile.txt": {Name: "anotherfile.txt"},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								"/anotherpath": {
 | 
				
			||||||
 | 
									Metadata: map[string]FileInfo{
 | 
				
			||||||
 | 
										"afile.txt": {Name: "afile.txt"},
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						files := []fs.FileInfo{
 | 
				
			||||||
 | 
							mockFileInfo{name: "file1.txt", isDir: false},
 | 
				
			||||||
 | 
							mockFileInfo{name: "dir1", isDir: true},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						testIndex.UpdateQuickList(files)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -47,7 +47,7 @@ require (
 | 
				
			||||||
	github.com/ulikunitz/xz v0.5.12 // indirect
 | 
						github.com/ulikunitz/xz v0.5.12 // indirect
 | 
				
			||||||
	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
 | 
						github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
 | 
				
			||||||
	github.com/yusufpapurcu/wmi v1.2.4 // indirect
 | 
						github.com/yusufpapurcu/wmi v1.2.4 // indirect
 | 
				
			||||||
	go.etcd.io/bbolt v1.3.10 // indirect
 | 
						go.etcd.io/bbolt v1.3.11 // indirect
 | 
				
			||||||
	golang.org/x/net v0.28.0 // indirect
 | 
						golang.org/x/net v0.28.0 // indirect
 | 
				
			||||||
	golang.org/x/sys v0.24.0 // indirect
 | 
						golang.org/x/sys v0.24.0 // indirect
 | 
				
			||||||
	golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
 | 
						golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,6 @@ github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM=
 | 
				
			||||||
github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
 | 
					github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
 | 
				
			||||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
 | 
					github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863 h1:BRrxwOZBolJN4gIwvZMJY1tzqBvQgpaZiQRuIDD40jM=
 | 
				
			||||||
github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
 | 
					github.com/Sereal/Sereal v0.0.0-20190618215532-0b8ac451a863/go.mod h1:D0JMgToj/WdxCgd30Kc1UcA9E+WdZoJqeVOuYW7iTBM=
 | 
				
			||||||
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
 | 
					 | 
				
			||||||
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
 | 
					github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
 | 
				
			||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
 | 
					github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
 | 
				
			||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
 | 
					github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
 | 
				
			||||||
| 
						 | 
					@ -33,8 +32,6 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV
 | 
				
			||||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
 | 
					github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU=
 | 
				
			||||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
 | 
					github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw=
 | 
				
			||||||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
 | 
					github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8=
 | 
				
			||||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
 | 
					 | 
				
			||||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
 | 
					 | 
				
			||||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
 | 
					github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
 | 
				
			||||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
 | 
					github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
 | 
				
			||||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
 | 
					github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
 | 
				
			||||||
| 
						 | 
					@ -42,11 +39,9 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI
 | 
				
			||||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 | 
					github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
 | 
				
			||||||
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
 | 
					github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
 | 
				
			||||||
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
 | 
					github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
 | 
				
			||||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
 | 
					 | 
				
			||||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
 | 
					github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
 | 
				
			||||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
 | 
					github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
 | 
				
			||||||
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
 | 
					github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
 | 
				
			||||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
 | 
					 | 
				
			||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 | 
					github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
 | 
				
			||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
 | 
					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=
 | 
				
			||||||
| 
						 | 
					@ -62,7 +57,6 @@ github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOW
 | 
				
			||||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 | 
					github.com/golang-jwt/jwt/v4 v4.5.0/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 h1:gtexQ/VGyN+VVFRXSFiguSNcXmS6rkKT+X7FdIrTtfo=
 | 
					 | 
				
			||||||
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=
 | 
				
			||||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
 | 
					github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
 | 
				
			||||||
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
 | 
					github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
 | 
				
			||||||
| 
						 | 
					@ -71,7 +65,6 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
 | 
				
			||||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 | 
					github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 | 
				
			||||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 | 
					github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 | 
				
			||||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
					github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
				
			||||||
github.com/golang/snappy v0.0.2 h1:aeE13tS0IiQgFjYdoL8qN3K1N2bXXtI6Vi51/y7BpMw=
 | 
					 | 
				
			||||||
github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
					github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
				
			||||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 | 
					github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
 | 
				
			||||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
					github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
 | 
				
			||||||
| 
						 | 
					@ -85,12 +78,10 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
 | 
				
			||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 | 
					github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 | 
				
			||||||
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
 | 
					github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
 | 
				
			||||||
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 | 
					github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 | 
				
			||||||
github.com/klauspost/compress v1.11.4 h1:kz40R/YWls3iqT9zX9AHN3WoVsrAWVyui5sxuLqiXqU=
 | 
					 | 
				
			||||||
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 | 
					github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 | 
				
			||||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
 | 
					github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
 | 
				
			||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 | 
					github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 | 
				
			||||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 | 
					github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 | 
				
			||||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
 | 
					 | 
				
			||||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 | 
					github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 | 
				
			||||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
 | 
					github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
 | 
				
			||||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 | 
					github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 | 
				
			||||||
| 
						 | 
					@ -103,29 +94,21 @@ github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
 | 
				
			||||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 | 
					github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
 | 
				
			||||||
github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM=
 | 
					github.com/marusama/semaphore/v2 v2.5.0 h1:o/1QJD9DBYOWRnDhPwDVAXQn6mQYD0gZaS1Tpx6DJGM=
 | 
				
			||||||
github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
 | 
					github.com/marusama/semaphore/v2 v2.5.0/go.mod h1:z9nMiNUekt/LTpTUQdpp+4sJeYqUGpwMHfW0Z8V8fnQ=
 | 
				
			||||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
 | 
					 | 
				
			||||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
 | 
					 | 
				
			||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 | 
					github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
 | 
				
			||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 | 
					github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
 | 
				
			||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
 | 
					 | 
				
			||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
 | 
					 | 
				
			||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 | 
					github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 | 
				
			||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 | 
					github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 | 
				
			||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 | 
					github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 | 
				
			||||||
github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
 | 
					github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo=
 | 
				
			||||||
github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
 | 
					github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
 | 
				
			||||||
github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ=
 | 
					 | 
				
			||||||
github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
 | 
					github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
 | 
				
			||||||
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
 | 
					github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
 | 
				
			||||||
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
 | 
					github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
 | 
				
			||||||
github.com/pierrec/lz4/v4 v4.1.2 h1:qvY3YFXRQE/XB8MlLzJH7mSzBs74eA2gg52YTk6jUPM=
 | 
					 | 
				
			||||||
github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 | 
					github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 | 
				
			||||||
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
 | 
					github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
 | 
				
			||||||
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 | 
					github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
 | 
				
			||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
					github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 | 
				
			||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
					github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
				
			||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
 | 
					 | 
				
			||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
 | 
					 | 
				
			||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
 | 
					github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
 | 
				
			||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
 | 
					github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
 | 
				
			||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 | 
					github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 | 
				
			||||||
| 
						 | 
					@ -143,7 +126,6 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8
 | 
				
			||||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
 | 
					github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
 | 
				
			||||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
 | 
					github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
 | 
				
			||||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 | 
					github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 | 
				
			||||||
github.com/ulikunitz/xz v0.5.9 h1:RsKRIA2MO8x56wkkcd3LbtcE/uMszhb6DpRf+3uwa3I=
 | 
					 | 
				
			||||||
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 | 
					github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 | 
				
			||||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
 | 
					github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
 | 
				
			||||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 | 
					github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
 | 
				
			||||||
| 
						 | 
					@ -153,18 +135,13 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofm
 | 
				
			||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
 | 
					github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
 | 
				
			||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
 | 
					github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
 | 
				
			||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
 | 
					github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
 | 
				
			||||||
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
 | 
					 | 
				
			||||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
 | 
					go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
 | 
				
			||||||
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
 | 
					go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
 | 
				
			||||||
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
 | 
					go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
 | 
				
			||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
					golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
				
			||||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
 | 
					 | 
				
			||||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
 | 
					 | 
				
			||||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
 | 
					golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
 | 
				
			||||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
 | 
					golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
 | 
				
			||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 | 
					golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 | 
				
			||||||
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
 | 
					 | 
				
			||||||
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
 | 
					 | 
				
			||||||
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
 | 
					golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
 | 
				
			||||||
golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
 | 
					golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys=
 | 
				
			||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 | 
					golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 | 
				
			||||||
| 
						 | 
					@ -174,15 +151,13 @@ golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLL
 | 
				
			||||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
					golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
				
			||||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
					golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 | 
				
			||||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 | 
					golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 | 
				
			||||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
 | 
					 | 
				
			||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
 | 
					 | 
				
			||||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
 | 
					golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
 | 
				
			||||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
 | 
					golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 | 
				
			||||||
 | 
					golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
					golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					 | 
				
			||||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					 | 
				
			||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
					golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
				
			||||||
| 
						 | 
					@ -192,22 +167,16 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
 | 
				
			||||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
					golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
					golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
					golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
 | 
					 | 
				
			||||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 | 
					 | 
				
			||||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
 | 
					golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
 | 
				
			||||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 | 
					golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 | 
				
			||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 | 
					golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 | 
				
			||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
					golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
				
			||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
					golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 | 
				
			||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 | 
					golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 | 
				
			||||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
 | 
					 | 
				
			||||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
 | 
					 | 
				
			||||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
 | 
					golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
 | 
				
			||||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 | 
					golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
 | 
				
			||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
					golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
				
			||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
					golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 | 
				
			||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
 | 
					 | 
				
			||||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
 | 
					 | 
				
			||||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
 | 
					golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
 | 
				
			||||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
 | 
					golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
 | 
				
			||||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 | 
					google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,6 @@ package http
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
| 
						 | 
					@ -127,11 +126,10 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
 | 
				
			||||||
		return http.StatusBadRequest, nil
 | 
							return http.StatusBadRequest, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user := &users.User{
 | 
						user := users.ApplyDefaults(users.User{})
 | 
				
			||||||
		Username: info.Username,
 | 
						user.Username = info.Username
 | 
				
			||||||
		Password: info.Password,
 | 
						user.Password = info.Password
 | 
				
			||||||
	}
 | 
					
 | 
				
			||||||
	settings.Config.UserDefaults.Apply(user)
 | 
					 | 
				
			||||||
	userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
 | 
						userHome, err := d.settings.MakeUserDir(user.Username, user.Scope, d.server.Root)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
 | 
							log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
 | 
				
			||||||
| 
						 | 
					@ -139,8 +137,7 @@ var signupHandler = func(w http.ResponseWriter, r *http.Request, d *data) (int,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	user.Scope = userHome
 | 
						user.Scope = userHome
 | 
				
			||||||
	log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
 | 
						log.Printf("new user: %s, home dir: [%s].", user.Username, userHome)
 | 
				
			||||||
	settings.Config.UserDefaults.Apply(user)
 | 
						err = d.store.Users.Save(&user)
 | 
				
			||||||
	err = d.store.Users.Save(user)
 | 
					 | 
				
			||||||
	if err == errors.ErrExist {
 | 
						if err == errors.ErrExist {
 | 
				
			||||||
		return http.StatusConflict, err
 | 
							return http.StatusConflict, err
 | 
				
			||||||
	} else if err != nil {
 | 
						} else if err != nil {
 | 
				
			||||||
| 
						 | 
					@ -157,7 +154,6 @@ var renewHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data
 | 
				
			||||||
func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
 | 
					func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User) (int, error) {
 | 
				
			||||||
	duration, err := time.ParseDuration(settings.Config.Auth.TokenExpirationTime)
 | 
						duration, err := time.ParseDuration(settings.Config.Auth.TokenExpirationTime)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		fmt.Println("Error parsing duration:", err)
 | 
					 | 
				
			||||||
		duration = time.Hour * 2
 | 
							duration = time.Hour * 2
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	claims := &authToken{
 | 
						claims := &authToken{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,25 +19,25 @@ import (
 | 
				
			||||||
var withHashFile = func(fn handleFunc) handleFunc {
 | 
					var withHashFile = func(fn handleFunc) handleFunc {
 | 
				
			||||||
	return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
 | 
						return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
 | 
				
			||||||
		id, path := ifPathWithName(r)
 | 
							id, path := ifPathWithName(r)
 | 
				
			||||||
		fmt.Println(id, path)
 | 
					 | 
				
			||||||
		link, err := d.store.Share.GetByHash(id)
 | 
							link, err := d.store.Share.GetByHash(id)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return errToStatus(err), err
 | 
								return errToStatus(err), err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if link.Hash != "" {
 | 
							if link.Hash != "" {
 | 
				
			||||||
			var status int
 | 
								var status int
 | 
				
			||||||
			status, err = authenticateShareRequest(r, link) // Assign to the existing `err` variable
 | 
								status, err = authenticateShareRequest(r, link)
 | 
				
			||||||
			if err != nil || status != 0 {
 | 
								if err != nil || status != 0 {
 | 
				
			||||||
				return status, err
 | 
									return status, err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		d.user = &users.PublicUser
 | 
							d.user = &users.PublicUser
 | 
				
			||||||
		realPath, err := files.GetRealPath(d.user.Scope, link.Path, path)
 | 
							realPath, isDir, err := files.GetRealPath(d.user.Scope, link.Path, path)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return http.StatusNotFound, err
 | 
								return http.StatusNotFound, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		file, err := files.FileInfoFaster(files.FileOptions{
 | 
							file, err := files.FileInfoFaster(files.FileOptions{
 | 
				
			||||||
			Path:       realPath,
 | 
								Path:       realPath,
 | 
				
			||||||
 | 
								IsDir:      isDir,
 | 
				
			||||||
			Modify:     d.user.Perm.Modify,
 | 
								Modify:     d.user.Perm.Modify,
 | 
				
			||||||
			Expand:     true,
 | 
								Expand:     true,
 | 
				
			||||||
			ReadHeader: d.server.TypeDetectionByHeader,
 | 
								ReadHeader: d.server.TypeDetectionByHeader,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -81,12 +81,13 @@ var rawHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data)
 | 
				
			||||||
	if !d.user.Perm.Download {
 | 
						if !d.user.Perm.Download {
 | 
				
			||||||
		return http.StatusAccepted, nil
 | 
							return http.StatusAccepted, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
 | 
						realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return http.StatusInternalServerError, err
 | 
							return http.StatusInternalServerError, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	file, err := files.FileInfoFaster(files.FileOptions{
 | 
						file, err := files.FileInfoFaster(files.FileOptions{
 | 
				
			||||||
		Path:       realPath,
 | 
							Path:       realPath,
 | 
				
			||||||
 | 
							IsDir:      isDir,
 | 
				
			||||||
		Modify:     d.user.Perm.Modify,
 | 
							Modify:     d.user.Perm.Modify,
 | 
				
			||||||
		Expand:     false,
 | 
							Expand:     false,
 | 
				
			||||||
		ReadHeader: d.server.TypeDetectionByHeader,
 | 
							ReadHeader: d.server.TypeDetectionByHeader,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,13 +18,13 @@ import (
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
 | 
					var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
 | 
				
			||||||
	realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
 | 
						realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		fmt.Println("unable to get real path", d.user.Scope, r.URL.Path)
 | 
					 | 
				
			||||||
		return http.StatusNotFound, err
 | 
							return http.StatusNotFound, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	file, err := files.FileInfoFaster(files.FileOptions{
 | 
						file, err := files.FileInfoFaster(files.FileOptions{
 | 
				
			||||||
		Path:       realPath,
 | 
							Path:       realPath,
 | 
				
			||||||
 | 
							IsDir:      isDir,
 | 
				
			||||||
		Modify:     d.user.Perm.Modify,
 | 
							Modify:     d.user.Perm.Modify,
 | 
				
			||||||
		Expand:     true,
 | 
							Expand:     true,
 | 
				
			||||||
		ReadHeader: d.server.TypeDetectionByHeader,
 | 
							ReadHeader: d.server.TypeDetectionByHeader,
 | 
				
			||||||
| 
						 | 
					@ -34,10 +34,7 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return errToStatus(err), err
 | 
							return errToStatus(err), err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if file.IsDir {
 | 
						if !file.IsDir {
 | 
				
			||||||
		file.Listing.Sorting = d.user.Sorting
 | 
					 | 
				
			||||||
		return renderJSON(w, r, file)
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
		if checksum := r.URL.Query().Get("checksum"); checksum != "" {
 | 
							if checksum := r.URL.Query().Get("checksum"); checksum != "" {
 | 
				
			||||||
			err := file.Checksum(checksum)
 | 
								err := file.Checksum(checksum)
 | 
				
			||||||
			if err == errors.ErrInvalidOption {
 | 
								if err == errors.ErrInvalidOption {
 | 
				
			||||||
| 
						 | 
					@ -46,7 +43,7 @@ var resourceGetHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
 | 
				
			||||||
				return http.StatusInternalServerError, err
 | 
									return http.StatusInternalServerError, err
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	return renderJSON(w, r, file)
 | 
						return renderJSON(w, r, file)
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,12 +52,13 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
 | 
				
			||||||
		if r.URL.Path == "/" || !d.user.Perm.Delete {
 | 
							if r.URL.Path == "/" || !d.user.Perm.Delete {
 | 
				
			||||||
			return http.StatusForbidden, nil
 | 
								return http.StatusForbidden, nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
 | 
							realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return http.StatusNotFound, err
 | 
								return http.StatusNotFound, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		fileOpts := files.FileOptions{
 | 
							fileOpts := files.FileOptions{
 | 
				
			||||||
			Path:       realPath,
 | 
								Path:       realPath,
 | 
				
			||||||
 | 
								IsDir:      isDir,
 | 
				
			||||||
			Modify:     d.user.Perm.Modify,
 | 
								Modify:     d.user.Perm.Modify,
 | 
				
			||||||
			Expand:     false,
 | 
								Expand:     false,
 | 
				
			||||||
			ReadHeader: d.server.TypeDetectionByHeader,
 | 
								ReadHeader: d.server.TypeDetectionByHeader,
 | 
				
			||||||
| 
						 | 
					@ -90,12 +88,13 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
 | 
				
			||||||
		if !d.user.Perm.Create || !d.Check(r.URL.Path) {
 | 
							if !d.user.Perm.Create || !d.Check(r.URL.Path) {
 | 
				
			||||||
			return http.StatusForbidden, nil
 | 
								return http.StatusForbidden, nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
 | 
							realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return http.StatusNotFound, err
 | 
								return http.StatusNotFound, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		fileOpts := files.FileOptions{
 | 
							fileOpts := files.FileOptions{
 | 
				
			||||||
			Path:       realPath,
 | 
								Path:       realPath,
 | 
				
			||||||
 | 
								IsDir:      isDir,
 | 
				
			||||||
			Modify:     d.user.Perm.Modify,
 | 
								Modify:     d.user.Perm.Modify,
 | 
				
			||||||
			Expand:     false,
 | 
								Expand:     false,
 | 
				
			||||||
			ReadHeader: d.server.TypeDetectionByHeader,
 | 
								ReadHeader: d.server.TypeDetectionByHeader,
 | 
				
			||||||
| 
						 | 
					@ -109,7 +108,6 @@ func resourcePostHandler(fileCache FileCache) handleFunc {
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return http.StatusOK, nil
 | 
								return http.StatusOK, nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					 | 
				
			||||||
		file, err := files.FileInfoFaster(fileOpts)
 | 
							file, err := files.FileInfoFaster(fileOpts)
 | 
				
			||||||
		if err == nil {
 | 
							if err == nil {
 | 
				
			||||||
			if r.URL.Query().Get("override") != "true" {
 | 
								if r.URL.Query().Get("override") != "true" {
 | 
				
			||||||
| 
						 | 
					@ -141,12 +139,13 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d
 | 
				
			||||||
		return http.StatusMethodNotAllowed, nil
 | 
							return http.StatusMethodNotAllowed, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
 | 
						realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return http.StatusNotFound, err
 | 
							return http.StatusNotFound, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	fileOpts := files.FileOptions{
 | 
						fileOpts := files.FileOptions{
 | 
				
			||||||
		Path:       realPath,
 | 
							Path:       realPath,
 | 
				
			||||||
 | 
							IsDir:      isDir,
 | 
				
			||||||
		Modify:     d.user.Perm.Modify,
 | 
							Modify:     d.user.Perm.Modify,
 | 
				
			||||||
		Expand:     false,
 | 
							Expand:     false,
 | 
				
			||||||
		ReadHeader: d.server.TypeDetectionByHeader,
 | 
							ReadHeader: d.server.TypeDetectionByHeader,
 | 
				
			||||||
| 
						 | 
					@ -187,7 +186,6 @@ func resourcePatchHandler(fileCache FileCache) handleFunc {
 | 
				
			||||||
			return http.StatusForbidden, nil
 | 
								return http.StatusForbidden, nil
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		err = d.RunHook(func() error {
 | 
							err = d.RunHook(func() error {
 | 
				
			||||||
			fmt.Println("hook", src, dst)
 | 
					 | 
				
			||||||
			return patchAction(r.Context(), action, src, dst, d, fileCache)
 | 
								return patchAction(r.Context(), action, src, dst, d, fileCache)
 | 
				
			||||||
		}, action, src, dst, d.user)
 | 
							}, action, src, dst, d.user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -237,16 +235,17 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		src = path.Clean("/" + src)
 | 
							src = path.Clean("/" + src)
 | 
				
			||||||
		dst = path.Clean("/" + dst)
 | 
							dst = path.Clean("/" + dst)
 | 
				
			||||||
		realDest, err := files.GetRealPath(d.user.Scope, dst)
 | 
							realDest, _, err := files.GetRealPath(d.user.Scope, dst)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		realSrc, err := files.GetRealPath(d.user.Scope, src)
 | 
							realSrc, isDir, err := files.GetRealPath(d.user.Scope, src)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return err
 | 
								return err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		file, err := files.FileInfoFaster(files.FileOptions{
 | 
							file, err := files.FileInfoFaster(files.FileOptions{
 | 
				
			||||||
			Path:       realSrc,
 | 
								Path:       realSrc,
 | 
				
			||||||
 | 
								IsDir:      isDir,
 | 
				
			||||||
			Modify:     d.user.Perm.Modify,
 | 
								Modify:     d.user.Perm.Modify,
 | 
				
			||||||
			Expand:     false,
 | 
								Expand:     false,
 | 
				
			||||||
			ReadHeader: false,
 | 
								ReadHeader: false,
 | 
				
			||||||
| 
						 | 
					@ -274,12 +273,13 @@ type DiskUsageResponse struct {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
 | 
					var diskUsage = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) {
 | 
				
			||||||
	realPath, err := files.GetRealPath(d.user.Scope, r.URL.Path)
 | 
						realPath, isDir, err := files.GetRealPath(d.user.Scope, r.URL.Path)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return http.StatusNotFound, err
 | 
							return http.StatusNotFound, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	file, err := files.FileInfoFaster(files.FileOptions{
 | 
						file, err := files.FileInfoFaster(files.FileOptions{
 | 
				
			||||||
		Path:       realPath,
 | 
							Path:       realPath,
 | 
				
			||||||
 | 
							IsDir:      isDir,
 | 
				
			||||||
		Modify:     d.user.Perm.Modify,
 | 
							Modify:     d.user.Perm.Modify,
 | 
				
			||||||
		Expand:     false,
 | 
							Expand:     false,
 | 
				
			||||||
		ReadHeader: false,
 | 
							ReadHeader: false,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,6 @@ package http
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"fmt"
 | 
					 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
| 
						 | 
					@ -131,19 +130,21 @@ var userPostHandler = withAdmin(func(w http.ResponseWriter, r *http.Request, d *
 | 
				
			||||||
		return http.StatusBadRequest, errors.ErrEmptyPassword
 | 
							return http.StatusBadRequest, errors.ErrEmptyPassword
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						newUser := users.ApplyDefaults(*req.Data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root)
 | 
						userHome, err := d.settings.MakeUserDir(req.Data.Username, req.Data.Scope, d.server.Root)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
 | 
							log.Printf("create user: failed to mkdir user home dir: [%s]", userHome)
 | 
				
			||||||
		return http.StatusInternalServerError, err
 | 
							return http.StatusInternalServerError, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	req.Data.Scope = userHome
 | 
						newUser.Scope = userHome
 | 
				
			||||||
	log.Printf("user: %s, home dir: [%s].", req.Data.Username, userHome)
 | 
						log.Printf("user: %s, home dir: [%s].", req.Data.Username, userHome)
 | 
				
			||||||
	_, err = files.GetRealPath(d.server.Root, req.Data.Scope)
 | 
						_, _, err = files.GetRealPath(d.server.Root, req.Data.Scope)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		fmt.Println("user path is not valid", req.Data.Scope)
 | 
							log.Println("user path is not valid", req.Data.Scope)
 | 
				
			||||||
		return http.StatusBadRequest, nil
 | 
							return http.StatusBadRequest, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	err = d.store.Users.Save(req.Data)
 | 
						err = d.store.Users.Save(&newUser)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return http.StatusInternalServerError, err
 | 
							return http.StatusInternalServerError, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -161,7 +162,7 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
 | 
				
			||||||
	if req.Data.ID != d.raw.(uint) {
 | 
						if req.Data.ID != d.raw.(uint) {
 | 
				
			||||||
		return http.StatusBadRequest, nil
 | 
							return http.StatusBadRequest, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	_, err = files.GetRealPath(d.server.Root, req.Data.Scope)
 | 
						_, _, err = files.GetRealPath(d.server.Root, req.Data.Scope)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return http.StatusBadRequest, nil
 | 
							return http.StatusBadRequest, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -175,7 +176,9 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request
 | 
				
			||||||
		t := v.Type()
 | 
							t := v.Type()
 | 
				
			||||||
		for i := 0; i < t.NumField(); i++ {
 | 
							for i := 0; i < t.NumField(); i++ {
 | 
				
			||||||
			field := t.Field(i)
 | 
								field := t.Field(i)
 | 
				
			||||||
			if field.Name != "Password" && field.Name != "Fs" {
 | 
								if field.Name == "Password" && req.Data.Password != "" {
 | 
				
			||||||
 | 
									req.Which = append(req.Which, field.Name)
 | 
				
			||||||
 | 
								} else if field.Name != "Password" && field.Name != "Fs" {
 | 
				
			||||||
				req.Which = append(req.Which, field.Name)
 | 
									req.Which = append(req.Which, field.Name)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ checkExit() {
 | 
				
			||||||
if command -v go &> /dev/null
 | 
					if command -v go &> /dev/null
 | 
				
			||||||
then
 | 
					then
 | 
				
			||||||
    printf "\n == Running tests == \n"
 | 
					    printf "\n == Running tests == \n"
 | 
				
			||||||
    go test -race -v ./...
 | 
					    go test -race -parallel -v ./...
 | 
				
			||||||
    checkExit
 | 
					    checkExit
 | 
				
			||||||
else
 | 
					else
 | 
				
			||||||
    echo "ERROR: unable to perform tests"
 | 
					    echo "ERROR: unable to perform tests"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,8 +20,8 @@ type Runner struct {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// RunHook runs the hooks for the before and after event.
 | 
					// RunHook runs the hooks for the before and after event.
 | 
				
			||||||
func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.User) error {
 | 
					func (r *Runner) RunHook(fn func() error, evt, path, dst string, user *users.User) error {
 | 
				
			||||||
	path, _ = files.GetRealPath(user.Scope, path)
 | 
						path, _, _ = files.GetRealPath(user.Scope, path)
 | 
				
			||||||
	dst, _ = files.GetRealPath(user.Scope, dst)
 | 
						dst, _, _ = files.GetRealPath(user.Scope, dst)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if r.Enabled {
 | 
						if r.Enabled {
 | 
				
			||||||
		if val, ok := r.Commands["before_"+evt]; ok {
 | 
							if val, ok := r.Commands["before_"+evt]; ok {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,10 +3,9 @@ package settings
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"os"
 | 
						"os"
 | 
				
			||||||
	"strings"
 | 
						"path/filepath"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/goccy/go-yaml"
 | 
						"github.com/goccy/go-yaml"
 | 
				
			||||||
	"github.com/gtsteffaniak/filebrowser/users"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var Config Settings
 | 
					var Config Settings
 | 
				
			||||||
| 
						 | 
					@ -19,7 +18,16 @@ func Initialize(configFile string) {
 | 
				
			||||||
		log.Fatalf("Error unmarshaling YAML data: %v", err)
 | 
							log.Fatalf("Error unmarshaling YAML data: %v", err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	Config.UserDefaults.Perm = Config.UserDefaults.Permissions
 | 
						Config.UserDefaults.Perm = Config.UserDefaults.Permissions
 | 
				
			||||||
	Config.Server.Root = strings.TrimSuffix(Config.Server.Root, "/")
 | 
						// Convert relative path to absolute path
 | 
				
			||||||
 | 
						realRoot, err := filepath.Abs(Config.Server.Root)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("Error getting root path: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						_, err = os.Stat(realRoot)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Fatalf("ERROR: Configured Root Path does not exist! %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						Config.Server.Root = realRoot
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func loadConfigFile(configFile string) []byte {
 | 
					func loadConfigFile(configFile string) []byte {
 | 
				
			||||||
| 
						 | 
					@ -77,8 +85,9 @@ func setDefaults() Settings {
 | 
				
			||||||
			HideDotfiles:    true,
 | 
								HideDotfiles:    true,
 | 
				
			||||||
			DarkMode:        false,
 | 
								DarkMode:        false,
 | 
				
			||||||
			DisableSettings: false,
 | 
								DisableSettings: false,
 | 
				
			||||||
 | 
								ViewMode:        "normal",
 | 
				
			||||||
			Locale:          "en",
 | 
								Locale:          "en",
 | 
				
			||||||
			Permissions: users.Permissions{
 | 
								Permissions: Permissions{
 | 
				
			||||||
				Create:   false,
 | 
									Create:   false,
 | 
				
			||||||
				Rename:   false,
 | 
									Rename:   false,
 | 
				
			||||||
				Modify:   false,
 | 
									Modify:   false,
 | 
				
			||||||
| 
						 | 
					@ -90,19 +99,3 @@ func setDefaults() Settings {
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
// Apply applies the default options to a user.
 | 
					 | 
				
			||||||
func (d *UserDefaults) Apply(u *users.User) {
 | 
					 | 
				
			||||||
	u.StickySidebar = d.StickySidebar
 | 
					 | 
				
			||||||
	u.DisableSettings = d.DisableSettings
 | 
					 | 
				
			||||||
	u.DarkMode = d.DarkMode
 | 
					 | 
				
			||||||
	u.Scope = d.Scope
 | 
					 | 
				
			||||||
	u.Locale = d.Locale
 | 
					 | 
				
			||||||
	u.ViewMode = d.ViewMode
 | 
					 | 
				
			||||||
	u.SingleClick = d.SingleClick
 | 
					 | 
				
			||||||
	u.Perm = d.Perm
 | 
					 | 
				
			||||||
	u.Sorting = d.Sorting
 | 
					 | 
				
			||||||
	u.Commands = d.Commands
 | 
					 | 
				
			||||||
	u.HideDotfiles = d.HideDotfiles
 | 
					 | 
				
			||||||
	u.DateFormat = d.DateFormat
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,6 @@ package settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"github.com/gtsteffaniak/filebrowser/rules"
 | 
						"github.com/gtsteffaniak/filebrowser/rules"
 | 
				
			||||||
	"github.com/gtsteffaniak/filebrowser/users"
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Settings struct {
 | 
					type Settings struct {
 | 
				
			||||||
| 
						 | 
					@ -82,9 +81,20 @@ type UserDefaults struct {
 | 
				
			||||||
		By  string `json:"by"`
 | 
							By  string `json:"by"`
 | 
				
			||||||
		Asc bool   `json:"asc"`
 | 
							Asc bool   `json:"asc"`
 | 
				
			||||||
	} `json:"sorting"`
 | 
						} `json:"sorting"`
 | 
				
			||||||
	Perm         users.Permissions `json:"perm"`
 | 
						Perm         Permissions `json:"perm"`
 | 
				
			||||||
	Permissions  users.Permissions `json:"permissions"`
 | 
						Permissions  Permissions `json:"permissions"`
 | 
				
			||||||
	Commands     []string    `json:"commands,omitempty"`
 | 
						Commands     []string    `json:"commands,omitempty"`
 | 
				
			||||||
	HideDotfiles bool        `json:"hideDotfiles"`
 | 
						HideDotfiles bool        `json:"hideDotfiles"`
 | 
				
			||||||
	DateFormat   bool        `json:"dateFormat"`
 | 
						DateFormat   bool        `json:"dateFormat"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Permissions struct {
 | 
				
			||||||
 | 
						Admin    bool `json:"admin"`
 | 
				
			||||||
 | 
						Execute  bool `json:"execute"`
 | 
				
			||||||
 | 
						Create   bool `json:"create"`
 | 
				
			||||||
 | 
						Rename   bool `json:"rename"`
 | 
				
			||||||
 | 
						Modify   bool `json:"modify"`
 | 
				
			||||||
 | 
						Delete   bool `json:"delete"`
 | 
				
			||||||
 | 
						Share    bool `json:"share"`
 | 
				
			||||||
 | 
						Download bool `json:"download"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,6 @@
 | 
				
			||||||
package users
 | 
					package users
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"log"
 | 
					 | 
				
			||||||
	"sync"
 | 
						"sync"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -123,7 +122,6 @@ func (s *Storage) DeleteRule(userID string, ruleID string) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Save saves the user in a storage.
 | 
					// Save saves the user in a storage.
 | 
				
			||||||
func (s *Storage) Save(user *User) error {
 | 
					func (s *Storage) Save(user *User) error {
 | 
				
			||||||
	log.Println("Saving new user:", user.Username)
 | 
					 | 
				
			||||||
	return s.back.Save(user)
 | 
						return s.back.Save(user)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,19 +4,9 @@ import (
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/gtsteffaniak/filebrowser/rules"
 | 
						"github.com/gtsteffaniak/filebrowser/rules"
 | 
				
			||||||
 | 
						"github.com/gtsteffaniak/filebrowser/settings"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Permissions struct {
 | 
					 | 
				
			||||||
	Admin    bool `json:"admin"`
 | 
					 | 
				
			||||||
	Execute  bool `json:"execute"`
 | 
					 | 
				
			||||||
	Create   bool `json:"create"`
 | 
					 | 
				
			||||||
	Rename   bool `json:"rename"`
 | 
					 | 
				
			||||||
	Modify   bool `json:"modify"`
 | 
					 | 
				
			||||||
	Delete   bool `json:"delete"`
 | 
					 | 
				
			||||||
	Share    bool `json:"share"`
 | 
					 | 
				
			||||||
	Download bool `json:"download"`
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// SortingSettings represents the sorting settings.
 | 
					// SortingSettings represents the sorting settings.
 | 
				
			||||||
type Sorting struct {
 | 
					type Sorting struct {
 | 
				
			||||||
	By  string `json:"by"`
 | 
						By  string `json:"by"`
 | 
				
			||||||
| 
						 | 
					@ -36,7 +26,7 @@ type User struct {
 | 
				
			||||||
	LockPassword    bool                 `json:"lockPassword"`
 | 
						LockPassword    bool                 `json:"lockPassword"`
 | 
				
			||||||
	ViewMode        string               `json:"viewMode"`
 | 
						ViewMode        string               `json:"viewMode"`
 | 
				
			||||||
	SingleClick     bool                 `json:"singleClick"`
 | 
						SingleClick     bool                 `json:"singleClick"`
 | 
				
			||||||
	Perm            Permissions  `json:"perm"`
 | 
						Perm            settings.Permissions `json:"perm"`
 | 
				
			||||||
	Commands        []string             `json:"commands"`
 | 
						Commands        []string             `json:"commands"`
 | 
				
			||||||
	Sorting         Sorting              `json:"sorting"`
 | 
						Sorting         Sorting              `json:"sorting"`
 | 
				
			||||||
	Rules           []rules.Rule         `json:"rules"`
 | 
						Rules           []rules.Rule         `json:"rules"`
 | 
				
			||||||
| 
						 | 
					@ -51,7 +41,7 @@ var PublicUser = User{
 | 
				
			||||||
	Scope:        "./",
 | 
						Scope:        "./",
 | 
				
			||||||
	ViewMode:     "normal",
 | 
						ViewMode:     "normal",
 | 
				
			||||||
	LockPassword: true,
 | 
						LockPassword: true,
 | 
				
			||||||
	Perm: Permissions{
 | 
						Perm: settings.Permissions{
 | 
				
			||||||
		Create:   false,
 | 
							Create:   false,
 | 
				
			||||||
		Rename:   false,
 | 
							Rename:   false,
 | 
				
			||||||
		Modify:   false,
 | 
							Modify:   false,
 | 
				
			||||||
| 
						 | 
					@ -81,3 +71,20 @@ func (u *User) CanExecute(command string) bool {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return false
 | 
						return false
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Apply applies the default options to a user.
 | 
				
			||||||
 | 
					func ApplyDefaults(u User) User {
 | 
				
			||||||
 | 
						u.StickySidebar = settings.Config.UserDefaults.StickySidebar
 | 
				
			||||||
 | 
						u.DisableSettings = settings.Config.UserDefaults.DisableSettings
 | 
				
			||||||
 | 
						u.DarkMode = settings.Config.UserDefaults.DarkMode
 | 
				
			||||||
 | 
						u.Scope = settings.Config.UserDefaults.Scope
 | 
				
			||||||
 | 
						u.Locale = settings.Config.UserDefaults.Locale
 | 
				
			||||||
 | 
						u.ViewMode = settings.Config.UserDefaults.ViewMode
 | 
				
			||||||
 | 
						u.SingleClick = settings.Config.UserDefaults.SingleClick
 | 
				
			||||||
 | 
						u.Perm = settings.Config.UserDefaults.Perm
 | 
				
			||||||
 | 
						u.Sorting = settings.Config.UserDefaults.Sorting
 | 
				
			||||||
 | 
						u.Commands = settings.Config.UserDefaults.Commands
 | 
				
			||||||
 | 
						u.HideDotfiles = settings.Config.UserDefaults.HideDotfiles
 | 
				
			||||||
 | 
						u.DateFormat = settings.Config.UserDefaults.DateFormat
 | 
				
			||||||
 | 
						return u
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@
 | 
				
			||||||
    <script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit" data-vite-ignore></script>
 | 
					    <script src="[{[ .ReCaptchaHost ]}]/recaptcha/api.js?render=explicit" data-vite-ignore></script>
 | 
				
			||||||
  [{[ end ]}]
 | 
					  [{[ end ]}]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]</title>
 | 
					  <title>[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]FileBrowser Quantum[{[ end ]}]</title>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <link rel="icon" type="image/png" sizes="256x256" href="[{[ .StaticURL ]}]/img/icons/favicon-256x256.png">
 | 
					  <link rel="icon" type="image/png" sizes="256x256" href="[{[ .StaticURL ]}]/img/icons/favicon-256x256.png">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
 | 
					    var fullStaticURL = window.location.origin + window.FileBrowser.StaticURL;
 | 
				
			||||||
    var dynamicManifest = {
 | 
					    var dynamicManifest = {
 | 
				
			||||||
      "name": window.FileBrowser.Name || 'File Browser',
 | 
					      "name": window.FileBrowser.Name || 'FileBrowser Quantum',
 | 
				
			||||||
      "short_name": window.FileBrowser.Name || 'FileBrowser',
 | 
					      "short_name": window.FileBrowser.Name || 'FileBrowser',
 | 
				
			||||||
      "icons": [
 | 
					      "icons": [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,13 +1,15 @@
 | 
				
			||||||
import { createURL, fetchURL, removePrefix } from "./utils";
 | 
					import { createURL, fetchURL, removePrefix } from "./utils";
 | 
				
			||||||
import { baseURL } from "@/utils/constants";
 | 
					import { baseURL } from "@/utils/constants";
 | 
				
			||||||
import { state } from "@/store";
 | 
					import { state } from "@/store";
 | 
				
			||||||
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Notify if errors occur
 | 
				
			||||||
export async function fetch(url, content = false) {
 | 
					export async function fetch(url, content = false) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    url = removePrefix(url);
 | 
					    url = removePrefix(url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = await fetchURL(`/api/resources${url}?content=${content}`, {});
 | 
					    const res = await fetchURL(`/api/resources${url}?content=${content}`, {});
 | 
				
			||||||
 | 
					    const data = await res.json();
 | 
				
			||||||
  let data = await res.json();
 | 
					 | 
				
			||||||
    data.url = `/files${url}`;
 | 
					    data.url = `/files${url}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (data.isDir) {
 | 
					    if (data.isDir) {
 | 
				
			||||||
| 
						 | 
					@ -25,9 +27,14 @@ export async function fetch(url,content=false) {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return data;
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error fetching data");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function resourceAction(url, method, content) {
 | 
					async function resourceAction(url, method, content) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    url = removePrefix(url);
 | 
					    url = removePrefix(url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let opts = { method };
 | 
					    let opts = { method };
 | 
				
			||||||
| 
						 | 
					@ -37,19 +44,33 @@ async function resourceAction(url, method, content) {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = await fetchURL(`/api/resources${url}`, opts);
 | 
					    const res = await fetchURL(`/api/resources${url}`, opts);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return res;
 | 
					    return res;
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error performing resource action");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function remove(url) {
 | 
					export async function remove(url) {
 | 
				
			||||||
  return resourceAction(url, "DELETE");
 | 
					  try {
 | 
				
			||||||
 | 
					    return await resourceAction(url, "DELETE");
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error deleting resource");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function put(url, content = "") {
 | 
					export async function put(url, content = "") {
 | 
				
			||||||
  return resourceAction(url, "PUT", content);
 | 
					  try {
 | 
				
			||||||
 | 
					    return await resourceAction(url, "PUT", content);
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error putting resource");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function download(format, ...files) {
 | 
					export function download(format, ...files) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    let url = `${baseURL}/api/raw`;
 | 
					    let url = `${baseURL}/api/raw`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (files.length === 1) {
 | 
					    if (files.length === 1) {
 | 
				
			||||||
| 
						 | 
					@ -75,9 +96,13 @@ export function download(format, ...files) {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    window.open(url);
 | 
					    window.open(url);
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error downloading files");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function post(url, content = "", overwrite = false, onupload) {
 | 
					export async function post(url, content = "", overwrite = false, onupload) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    url = removePrefix(url);
 | 
					    url = removePrefix(url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let bufferContent;
 | 
					    let bufferContent;
 | 
				
			||||||
| 
						 | 
					@ -117,6 +142,10 @@ export async function post(url, content = "", overwrite = false, onupload) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      request.send(bufferContent || content);
 | 
					      request.send(bufferContent || content);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error posting resource");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function moveCopy(items, copy = false, overwrite = false, rename = false) {
 | 
					function moveCopy(items, copy = false, overwrite = false, rename = false) {
 | 
				
			||||||
| 
						 | 
					@ -131,7 +160,10 @@ function moveCopy(items, copy = false, overwrite = false, rename = false) {
 | 
				
			||||||
    promises.push(resourceAction(url, "PATCH"));
 | 
					    promises.push(resourceAction(url, "PATCH"));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return Promise.all(promises);
 | 
					  return Promise.all(promises).catch((err) => {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error moving/copying resources");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function move(items, overwrite = false, rename = false) {
 | 
					export function move(items, overwrite = false, rename = false) {
 | 
				
			||||||
| 
						 | 
					@ -143,28 +175,44 @@ export function copy(items, overwrite = false, rename = false) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function checksum(url, algo) {
 | 
					export async function checksum(url, algo) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
 | 
					    const data = await resourceAction(`${url}?checksum=${algo}`, "GET");
 | 
				
			||||||
    return (await data.json()).checksums[algo];
 | 
					    return (await data.json()).checksums[algo];
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error fetching checksum");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getDownloadURL(file, inline) {
 | 
					export function getDownloadURL(file, inline) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    const params = {
 | 
					    const params = {
 | 
				
			||||||
      ...(inline && { inline: "true" }),
 | 
					      ...(inline && { inline: "true" }),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return createURL("api/raw" + file.path, params);
 | 
					    return createURL("api/raw" + file.path, params);
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error getting download URL");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getPreviewURL(file, size) {
 | 
					export function getPreviewURL(file, size) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    const params = {
 | 
					    const params = {
 | 
				
			||||||
      inline: "true",
 | 
					      inline: "true",
 | 
				
			||||||
      key: Date.parse(file.modified),
 | 
					      key: Date.parse(file.modified),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return createURL("api/preview/" + size + file.path, params);
 | 
					    return createURL("api/preview/" + size + file.path, params);
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error getting preview URL");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getSubtitlesURL(file) {
 | 
					export function getSubtitlesURL(file) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    const params = {
 | 
					    const params = {
 | 
				
			||||||
      inline: "true",
 | 
					      inline: "true",
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
| 
						 | 
					@ -175,12 +223,20 @@ export function getSubtitlesURL(file) {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return subtitles;
 | 
					    return subtitles;
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error fetching subtitles URL");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function usage(url) {
 | 
					export async function usage(url) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    url = removePrefix(url);
 | 
					    url = removePrefix(url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const res = await fetchURL(`/api/usage${url}`, {});
 | 
					    const res = await fetchURL(`/api/usage${url}`, {});
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return await res.json();
 | 
					    return await res.json();
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error fetching usage data");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -84,5 +84,6 @@ export function getDownloadURL(share, inline = false) {
 | 
				
			||||||
  if (share.path == undefined) {
 | 
					  if (share.path == undefined) {
 | 
				
			||||||
    share.path = ""
 | 
					    share.path = ""
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return createURL("api/public/dl/" + share.hash + "/"+share.path, params, false);
 | 
					  const path = share.path.replace("/share/"+share.hash +"/","")
 | 
				
			||||||
 | 
					  return createURL("api/public/dl/" + share.hash + "/"+path, params, false);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,9 @@
 | 
				
			||||||
import { fetchURL, removePrefix } from "./utils";
 | 
					import { fetchURL, removePrefix } from "./utils";
 | 
				
			||||||
import url from "../utils/url";
 | 
					import url from "../utils/url";
 | 
				
			||||||
 | 
					import { notify } from "@/notify";  // Import notify for error handling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default async function search(base, query) {
 | 
					export default async function search(base, query) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    base = removePrefix(base);
 | 
					    base = removePrefix(base);
 | 
				
			||||||
    query = encodeURIComponent(query);
 | 
					    query = encodeURIComponent(query);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +11,7 @@ export default async function search(base, query) {
 | 
				
			||||||
      base += "/";
 | 
					      base += "/";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let res = await fetchURL(`/api/search${base}?query=${query}`, {});
 | 
					    const res = await fetchURL(`/api/search${base}?query=${query}`, {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let data = await res.json();
 | 
					    let data = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,4 +21,8 @@ export default async function search(base, query) {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return data;
 | 
					    return data;
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error occurred during search");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,14 +1,26 @@
 | 
				
			||||||
import { fetchURL, fetchJSON } from "@/api/utils";
 | 
					import { fetchURL, fetchJSON } from "@/api/utils";
 | 
				
			||||||
 | 
					import { notify } from "@/notify";  // Import notify for error handling
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function getAllUsers() {
 | 
					export async function getAllUsers() {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    return await fetchJSON(`/api/users`, {});
 | 
					    return await fetchJSON(`/api/users`, {});
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Failed to fetch users");
 | 
				
			||||||
 | 
					    throw err; // Re-throw to handle further if needed
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function get(id) {
 | 
					export async function get(id) {
 | 
				
			||||||
  return fetchJSON(`/api/users/${id}`, {});
 | 
					  try {
 | 
				
			||||||
 | 
					    return await fetchJSON(`/api/users/${id}`, {});
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || `Failed to fetch user with ID: ${id}`);
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function create(user) {
 | 
					export async function create(user) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    const res = await fetchURL(`/api/users`, {
 | 
					    const res = await fetchURL(`/api/users`, {
 | 
				
			||||||
      method: "POST",
 | 
					      method: "POST",
 | 
				
			||||||
      body: JSON.stringify({
 | 
					      body: JSON.stringify({
 | 
				
			||||||
| 
						 | 
					@ -20,15 +32,23 @@ export async function create(user) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (res.status === 201) {
 | 
					    if (res.status === 201) {
 | 
				
			||||||
      return res.headers.get("Location");
 | 
					      return res.headers.get("Location");
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      throw new Error("Failed to create user");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || "Error creating user");
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function update(user, which = ["all"]) {
 | 
					export async function update(user, which = ["all"]) {
 | 
				
			||||||
  if (which[0] != "password") {
 | 
					  try {
 | 
				
			||||||
    user.password = "";
 | 
					    // List of keys to exclude from the "which" array
 | 
				
			||||||
  }
 | 
					    const excludeKeys = ["id", "name"];
 | 
				
			||||||
  if (user.username == "publicUser") {
 | 
					    // Filter out the keys from "which"
 | 
				
			||||||
    return
 | 
					    which = which.filter(item => !excludeKeys.includes(item));
 | 
				
			||||||
 | 
					    if (user.username === "publicUser") {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    await fetchURL(`/api/users/${user.id}`, {
 | 
					    await fetchURL(`/api/users/${user.id}`, {
 | 
				
			||||||
      method: "PUT",
 | 
					      method: "PUT",
 | 
				
			||||||
| 
						 | 
					@ -38,10 +58,19 @@ export async function update(user, which = ["all"]) {
 | 
				
			||||||
        data: user,
 | 
					        data: user,
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || `Failed to update user with ID: ${user.id}`);
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function remove(id) {
 | 
					export async function remove(id) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    await fetchURL(`/api/users/${id}`, {
 | 
					    await fetchURL(`/api/users/${id}`, {
 | 
				
			||||||
      method: "DELETE",
 | 
					      method: "DELETE",
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    notify.showError(err.message || `Failed to delete user with ID: ${id}`);
 | 
				
			||||||
 | 
					    throw err;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@ import { state } from "@/store";
 | 
				
			||||||
import { renew, logout } from "@/utils/auth";
 | 
					import { renew, logout } from "@/utils/auth";
 | 
				
			||||||
import { baseURL } from "@/utils/constants";
 | 
					import { baseURL } from "@/utils/constants";
 | 
				
			||||||
import { encodePath } from "@/utils/url";
 | 
					import { encodePath } from "@/utils/url";
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function fetchURL(url, opts, auth = true) {
 | 
					export async function fetchURL(url, opts, auth = true) {
 | 
				
			||||||
  opts = opts || {};
 | 
					  opts = opts || {};
 | 
				
			||||||
| 
						 | 
					@ -51,7 +51,7 @@ export async function fetchJSON(url, opts) {
 | 
				
			||||||
  if (res.status === 200) {
 | 
					  if (res.status === 200) {
 | 
				
			||||||
    return res.json();
 | 
					    return res.json();
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    showError("unable to fetch : " + url + "status" + res.status);
 | 
					    notify.showError("unable to fetch : " + url + "status" + res.status);
 | 
				
			||||||
    throw new Error(res.status);
 | 
					    throw new Error(res.status);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@
 | 
				
			||||||
      <component :is="element" :to="link.url">{{ link.name }}</component>
 | 
					      <component :is="element" :to="link.url">{{ link.name }}</component>
 | 
				
			||||||
    </span>
 | 
					    </span>
 | 
				
			||||||
    <action style="display: contents" v-if="showShare" icon="share" show="share" />
 | 
					    <action style="display: contents" v-if="showShare" icon="share" show="share" />
 | 
				
			||||||
    <div v-if="isResizableView">
 | 
					    <div v-if="isCardView">
 | 
				
			||||||
      Size:
 | 
					      Size:
 | 
				
			||||||
      <input
 | 
					      <input
 | 
				
			||||||
        v-model="gallerySize"
 | 
					        v-model="gallerySize"
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { state, mutations, getters } from "@/store"; // Import mutations as well
 | 
					import { state, mutations, getters } from "@/store"; // Import mutations as well
 | 
				
			||||||
import Action from "@/components/header/Action.vue";
 | 
					import Action from "@/components/Action.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "breadcrumbs",
 | 
					  name: "breadcrumbs",
 | 
				
			||||||
| 
						 | 
					@ -51,8 +51,8 @@ export default {
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: ["base", "noLink"],
 | 
					  props: ["base", "noLink"],
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    isResizableView() {
 | 
					    isCardView() {
 | 
				
			||||||
      return getters.isResizableView();
 | 
					      return getters.isCardView();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    items() {
 | 
					    items() {
 | 
				
			||||||
      const relativePath = state.route.path.replace(this.base, "");
 | 
					      const relativePath = state.route.path.replace(this.base, "");
 | 
				
			||||||
| 
						 | 
					@ -107,11 +107,6 @@ export default {
 | 
				
			||||||
      return state.user?.perm && state.user?.perm.share; // Access from state directly
 | 
					      return state.user?.perm && state.user?.perm.share; // Access from state directly
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: { },
 | 
				
			||||||
    // Example of a method using mutations
 | 
					 | 
				
			||||||
    updateUserPermissions(newPerms) {
 | 
					 | 
				
			||||||
      mutations.updateUser({ perm: newPerms });
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,230 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div
 | 
				
			||||||
 | 
					    id="context-menu"
 | 
				
			||||||
 | 
					    ref="contextMenu"
 | 
				
			||||||
 | 
					    v-show="showContext"
 | 
				
			||||||
 | 
					    :style="{
 | 
				
			||||||
 | 
					      top: `${top}px`,
 | 
				
			||||||
 | 
					      left: `${left}px`,
 | 
				
			||||||
 | 
					    }"
 | 
				
			||||||
 | 
					    class="button"
 | 
				
			||||||
 | 
					    :class="{ 'dark-mode': isDarkMode, mobile: isMobile }"
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <div v-if="selectedCount > 0" class="button selected-count-header">
 | 
				
			||||||
 | 
					      <span>{{ selectedCount }} selected</span>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <action
 | 
				
			||||||
 | 
					      v-if="!headerButtons.select"
 | 
				
			||||||
 | 
					      icon="create_new_folder"
 | 
				
			||||||
 | 
					      :label="$t('sidebar.newFolder')"
 | 
				
			||||||
 | 
					      @action="showHover('newDir')"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <action
 | 
				
			||||||
 | 
					      v-if="!headerButtons.select"
 | 
				
			||||||
 | 
					      icon="note_add"
 | 
				
			||||||
 | 
					      :label="$t('sidebar.newFile')"
 | 
				
			||||||
 | 
					      @action="showHover('newFile')"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <action
 | 
				
			||||||
 | 
					      v-if="!headerButtons.select"
 | 
				
			||||||
 | 
					      icon="file_upload"
 | 
				
			||||||
 | 
					      :label="$t('buttons.upload')"
 | 
				
			||||||
 | 
					      @action="uploadFunc"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <action
 | 
				
			||||||
 | 
					      v-if="headerButtons.select"
 | 
				
			||||||
 | 
					      icon="info"
 | 
				
			||||||
 | 
					      :label="$t('buttons.info')"
 | 
				
			||||||
 | 
					      show="info"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <action
 | 
				
			||||||
 | 
					      v-if="!isMultiple"
 | 
				
			||||||
 | 
					      icon="check_circle"
 | 
				
			||||||
 | 
					      :label="$t('buttons.selectMultiple')"
 | 
				
			||||||
 | 
					      @action="toggleMultipleSelection"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <action
 | 
				
			||||||
 | 
					      v-if="headerButtons.download"
 | 
				
			||||||
 | 
					      icon="file_download"
 | 
				
			||||||
 | 
					      :label="$t('buttons.download')"
 | 
				
			||||||
 | 
					      @action="startDownload"
 | 
				
			||||||
 | 
					      :counter="selectedCount"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <action
 | 
				
			||||||
 | 
					      v-if="headerButtons.share"
 | 
				
			||||||
 | 
					      icon="share"
 | 
				
			||||||
 | 
					      :label="$t('buttons.share')"
 | 
				
			||||||
 | 
					      show="share"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <action
 | 
				
			||||||
 | 
					      v-if="headerButtons.rename"
 | 
				
			||||||
 | 
					      icon="mode_edit"
 | 
				
			||||||
 | 
					      :label="$t('buttons.rename')"
 | 
				
			||||||
 | 
					      show="rename"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <action
 | 
				
			||||||
 | 
					      v-if="headerButtons.copy"
 | 
				
			||||||
 | 
					      icon="content_copy"
 | 
				
			||||||
 | 
					      :label="$t('buttons.copyFile')"
 | 
				
			||||||
 | 
					      show="copy"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <action
 | 
				
			||||||
 | 
					      v-if="headerButtons.move"
 | 
				
			||||||
 | 
					      icon="forward"
 | 
				
			||||||
 | 
					      :label="$t('buttons.moveFile')"
 | 
				
			||||||
 | 
					      show="move"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					    <action
 | 
				
			||||||
 | 
					      v-if="headerButtons.delete"
 | 
				
			||||||
 | 
					      icon="delete"
 | 
				
			||||||
 | 
					      :label="$t('buttons.delete')"
 | 
				
			||||||
 | 
					      show="delete"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					import downloadFiles from "@/utils/download";
 | 
				
			||||||
 | 
					import { state, getters, mutations } from "@/store"; // Import your custom store
 | 
				
			||||||
 | 
					import Action from "@/components/Action.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					  name: "ContextMenu",
 | 
				
			||||||
 | 
					  components: {
 | 
				
			||||||
 | 
					    Action,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  data() {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      posX: 0,
 | 
				
			||||||
 | 
					      posY: 0,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  computed: {
 | 
				
			||||||
 | 
					    isMultiple() {
 | 
				
			||||||
 | 
					      return state.multiple;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    user() {
 | 
				
			||||||
 | 
					      return state.user;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    isMobile() {
 | 
				
			||||||
 | 
					      return getters.isMobile();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showContext() {
 | 
				
			||||||
 | 
					      if (getters.currentPromptName() == "ContextMenu" && state.prompts != []) {
 | 
				
			||||||
 | 
					        this.setPositions();
 | 
				
			||||||
 | 
					        return true;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    top() {
 | 
				
			||||||
 | 
					      // Ensure the context menu stays within the viewport
 | 
				
			||||||
 | 
					      return Math.min(
 | 
				
			||||||
 | 
					        this.posY,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        window.innerHeight - (this.$refs.contextMenu?.clientHeight ?? 0)
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    left() {
 | 
				
			||||||
 | 
					      return Math.min(
 | 
				
			||||||
 | 
					        this.posX,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        window.innerWidth - (this.$refs.contextMenu?.clientWidth ?? 0)
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    isDarkMode() {
 | 
				
			||||||
 | 
					      return getters.isDarkMode();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    headerButtons() {
 | 
				
			||||||
 | 
					      return {
 | 
				
			||||||
 | 
					        select: state.selected.length > 0,
 | 
				
			||||||
 | 
					        upload: state.user.perm?.create && state.selected.length > 0,
 | 
				
			||||||
 | 
					        download: state.user.perm.download && state.selected.length > 0,
 | 
				
			||||||
 | 
					        delete: state.selected.length > 0 && state.user.perm.delete,
 | 
				
			||||||
 | 
					        rename: state.selected.length === 1 && state.user.perm.rename,
 | 
				
			||||||
 | 
					        share: state.selected.length === 1 && state.user.perm.share,
 | 
				
			||||||
 | 
					        move: state.selected.length > 0 && state.user.perm.rename,
 | 
				
			||||||
 | 
					        copy: state.selected.length > 0 && state.user.perm?.create,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    selectedCount() {
 | 
				
			||||||
 | 
					      return getters.selectedCount();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    uploadFunc() {
 | 
				
			||||||
 | 
					      mutations.showHover("upload");
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    showHover(value) {
 | 
				
			||||||
 | 
					      return mutations.showHover(value);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    setPositions() {
 | 
				
			||||||
 | 
					      const contextProps = getters.currentPrompt().props;
 | 
				
			||||||
 | 
					      this.posX = contextProps.posX;
 | 
				
			||||||
 | 
					      this.posY = contextProps.posY;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    toggleMultipleSelection() {
 | 
				
			||||||
 | 
					      mutations.setMultiple(!state.multiple);
 | 
				
			||||||
 | 
					      mutations.closeHovers();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    startDownload() {
 | 
				
			||||||
 | 
					      downloadFiles();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style scoped>
 | 
				
			||||||
 | 
					#context-menu {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  z-index: 1000;
 | 
				
			||||||
 | 
					  background-color: white;
 | 
				
			||||||
 | 
					  max-width: 20em;
 | 
				
			||||||
 | 
					  min-width: 15em;
 | 
				
			||||||
 | 
					  min-height: 4em;
 | 
				
			||||||
 | 
					  height: auto;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#context-menu.mobile {
 | 
				
			||||||
 | 
					  top: 50% !important;
 | 
				
			||||||
 | 
					  left: 50% !important;
 | 
				
			||||||
 | 
					  -webkit-transform: translate(-50%, -50%);
 | 
				
			||||||
 | 
					  transform: translate(-50%, -50%);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.selected-count-header {
 | 
				
			||||||
 | 
					  border-radius: 0.5em;
 | 
				
			||||||
 | 
					  cursor: unset;
 | 
				
			||||||
 | 
					  margin-bottom: 0.5em;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#context-menu .action {
 | 
				
			||||||
 | 
					  width: auto;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#context-menu > span {
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					  margin-left: 1em;
 | 
				
			||||||
 | 
					  color: #6f6f6f;
 | 
				
			||||||
 | 
					  margin-right: auto;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#context-menu .action span {
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* File selection */
 | 
				
			||||||
 | 
					#context-menu.dark-mode {
 | 
				
			||||||
 | 
					  background: var(--surfaceSecondary) !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#context-menu.dark-mode span {
 | 
				
			||||||
 | 
					  color: var(--textPrimary) !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -1,168 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <div v-if="selectedCount > 0" id="file-selection" :class="{ 'dark-mode': isDarkMode }">
 | 
					 | 
				
			||||||
    <span>{{ selectedCount }} selected</span>
 | 
					 | 
				
			||||||
    <div>
 | 
					 | 
				
			||||||
      <action
 | 
					 | 
				
			||||||
        v-if="headerButtons.select"
 | 
					 | 
				
			||||||
        icon="info"
 | 
					 | 
				
			||||||
        :label="$t('buttons.info')"
 | 
					 | 
				
			||||||
        show="info"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <action
 | 
					 | 
				
			||||||
        v-if="headerButtons.select"
 | 
					 | 
				
			||||||
        icon="check_circle"
 | 
					 | 
				
			||||||
        :label="$t('buttons.selectMultiple')"
 | 
					 | 
				
			||||||
        @action="toggleMultipleSelection"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <action
 | 
					 | 
				
			||||||
        v-if="headerButtons.download"
 | 
					 | 
				
			||||||
        icon="file_download"
 | 
					 | 
				
			||||||
        :label="$t('buttons.download')"
 | 
					 | 
				
			||||||
        @action="download"
 | 
					 | 
				
			||||||
        :counter="selectedCount"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <action
 | 
					 | 
				
			||||||
        v-if="headerButtons.share"
 | 
					 | 
				
			||||||
        icon="share"
 | 
					 | 
				
			||||||
        :label="$t('buttons.share')"
 | 
					 | 
				
			||||||
        show="share"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <action
 | 
					 | 
				
			||||||
        v-if="headerButtons.rename"
 | 
					 | 
				
			||||||
        icon="mode_edit"
 | 
					 | 
				
			||||||
        :label="$t('buttons.rename')"
 | 
					 | 
				
			||||||
        show="rename"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <action
 | 
					 | 
				
			||||||
        v-if="headerButtons.copy"
 | 
					 | 
				
			||||||
        icon="content_copy"
 | 
					 | 
				
			||||||
        :label="$t('buttons.copyFile')"
 | 
					 | 
				
			||||||
        show="copy"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <action
 | 
					 | 
				
			||||||
        v-if="headerButtons.move"
 | 
					 | 
				
			||||||
        icon="forward"
 | 
					 | 
				
			||||||
        :label="$t('buttons.moveFile')"
 | 
					 | 
				
			||||||
        show="move"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <action
 | 
					 | 
				
			||||||
        v-if="headerButtons.delete"
 | 
					 | 
				
			||||||
        icon="delete"
 | 
					 | 
				
			||||||
        :label="$t('buttons.delete')"
 | 
					 | 
				
			||||||
        show="delete"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
import { state, getters, mutations } from "@/store"; // Import your custom store
 | 
					 | 
				
			||||||
import { files as api } from "@/api";
 | 
					 | 
				
			||||||
import Action from "@/components/header/Action.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  name: "fileSelection",
 | 
					 | 
				
			||||||
  components: {
 | 
					 | 
				
			||||||
    Action,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  computed: {
 | 
					 | 
				
			||||||
    isDarkMode() {
 | 
					 | 
				
			||||||
      return getters.isDarkMode();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    headerButtons() {
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        select: state.selected.length > 0,
 | 
					 | 
				
			||||||
        upload: state.user.perm?.create && state.selected.length > 0,
 | 
					 | 
				
			||||||
        download: state.user.perm.download && state.selected.length > 0,
 | 
					 | 
				
			||||||
        delete: state.selected.length > 0 && state.user.perm.delete,
 | 
					 | 
				
			||||||
        rename: state.selected.length === 1 && state.user.perm.rename,
 | 
					 | 
				
			||||||
        share: state.selected.length === 1 && state.user.perm.share,
 | 
					 | 
				
			||||||
        move: state.selected.length > 0 && state.user.perm.rename,
 | 
					 | 
				
			||||||
        copy: state.selected.length > 0 && state.user.perm?.create,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    selectedCount() {
 | 
					 | 
				
			||||||
      return getters.selectedCount();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  methods: {
 | 
					 | 
				
			||||||
    toggleMultipleSelection() {
 | 
					 | 
				
			||||||
      mutations.setMultiple(!state.multiple);
 | 
					 | 
				
			||||||
      mutations.closeHovers();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    download() {
 | 
					 | 
				
			||||||
      if (getters.isSingleFileSelected()) {
 | 
					 | 
				
			||||||
        api.download(null, getters.selectedDownloadUrl());
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      mutations.showHover({
 | 
					 | 
				
			||||||
        name: "download",
 | 
					 | 
				
			||||||
        confirm: (format) => {
 | 
					 | 
				
			||||||
          mutations.closeHovers();
 | 
					 | 
				
			||||||
          let files = [];
 | 
					 | 
				
			||||||
          if (state.selected.length > 0) {
 | 
					 | 
				
			||||||
            for (let i of state.selected) {
 | 
					 | 
				
			||||||
              files.push(state.req.items[i].url);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          } else {
 | 
					 | 
				
			||||||
            files.push(state.route.path);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          try {
 | 
					 | 
				
			||||||
            api.download(format, ...files);
 | 
					 | 
				
			||||||
            showSuccess("download started");
 | 
					 | 
				
			||||||
          } catch (e) {
 | 
					 | 
				
			||||||
            showError("error downloading", e);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
<style>
 | 
					 | 
				
			||||||
@media (min-width: 800px) {
 | 
					 | 
				
			||||||
  #file-selection {
 | 
					 | 
				
			||||||
    bottom: 4em;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#file-selection .action {
 | 
					 | 
				
			||||||
  border-radius: 50%;
 | 
					 | 
				
			||||||
  width: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#file-selection > span {
 | 
					 | 
				
			||||||
  display: inline-block;
 | 
					 | 
				
			||||||
  margin-left: 1em;
 | 
					 | 
				
			||||||
  color: #6f6f6f;
 | 
					 | 
				
			||||||
  margin-right: auto;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#file-selection .action span {
 | 
					 | 
				
			||||||
  display: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* File Selection */
 | 
					 | 
				
			||||||
#file-selection {
 | 
					 | 
				
			||||||
  box-shadow: rgba(0, 0, 0, 0.3) 0px 2em 50px 10px;
 | 
					 | 
				
			||||||
  position: fixed;
 | 
					 | 
				
			||||||
  bottom: 4em;
 | 
					 | 
				
			||||||
  left: 50%;
 | 
					 | 
				
			||||||
  transform: translateX(-50%);
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  background: #fff;
 | 
					 | 
				
			||||||
  max-width: 30em;
 | 
					 | 
				
			||||||
  z-index: 3;
 | 
					 | 
				
			||||||
  border-radius: 1em;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  width: 90%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
/* File selection */
 | 
					 | 
				
			||||||
#file-selection.dark-mode {
 | 
					 | 
				
			||||||
  background: var(--surfaceSecondary) !important;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#file-selection.dark-mode span {
 | 
					 | 
				
			||||||
  color: var(--textPrimary) !important;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -16,6 +16,7 @@
 | 
				
			||||||
      <i v-else class="material-icons">search</i>
 | 
					      <i v-else class="material-icons">search</i>
 | 
				
			||||||
      <!-- Input field for search -->
 | 
					      <!-- Input field for search -->
 | 
				
			||||||
      <input
 | 
					      <input
 | 
				
			||||||
 | 
					        id="main-input"
 | 
				
			||||||
        class="main-input"
 | 
					        class="main-input"
 | 
				
			||||||
        type="text"
 | 
					        type="text"
 | 
				
			||||||
        @keyup.exact="keyup"
 | 
					        @keyup.exact="keyup"
 | 
				
			||||||
| 
						 | 
					@ -194,7 +195,6 @@
 | 
				
			||||||
import ButtonGroup from "./ButtonGroup.vue";
 | 
					import ButtonGroup from "./ButtonGroup.vue";
 | 
				
			||||||
import { search } from "@/api";
 | 
					import { search } from "@/api";
 | 
				
			||||||
import { getters, mutations, state } from "@/store";
 | 
					import { getters, mutations, state } from "@/store";
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
var boxes = {
 | 
					var boxes = {
 | 
				
			||||||
  folder: { label: "folders", icon: "folder" },
 | 
					  folder: { label: "folders", icon: "folder" },
 | 
				
			||||||
| 
						 | 
					@ -248,13 +248,18 @@ export default {
 | 
				
			||||||
      this.submit();
 | 
					      this.submit();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    active(active) {
 | 
					    active(active) {
 | 
				
			||||||
 | 
					      // this is hear to allow for animation
 | 
				
			||||||
      const resultList = document.getElementById("result-list");
 | 
					      const resultList = document.getElementById("result-list");
 | 
				
			||||||
      if (!active) {
 | 
					      if (!active) {
 | 
				
			||||||
        resultList.classList.remove("active");
 | 
					        resultList.classList.remove("active");
 | 
				
			||||||
 | 
					        this.value = "";
 | 
				
			||||||
 | 
					        event.stopPropagation();
 | 
				
			||||||
 | 
					        mutations.closeHovers();
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      setTimeout(() => {
 | 
					      setTimeout(() => {
 | 
				
			||||||
        resultList.classList.add("active");
 | 
					        resultList.classList.add("active");
 | 
				
			||||||
 | 
					        document.getElementById("main-input").focus();
 | 
				
			||||||
      }, 100);
 | 
					      }, 100);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    value() {
 | 
					    value() {
 | 
				
			||||||
| 
						 | 
					@ -394,11 +399,9 @@ export default {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      let path = state.route.path;
 | 
					      let path = state.route.path;
 | 
				
			||||||
      this.ongoing = true;
 | 
					      this.ongoing = true;
 | 
				
			||||||
      try {
 | 
					
 | 
				
			||||||
      this.results = await search(path, searchTypesFull + this.value);
 | 
					      this.results = await search(path, searchTypesFull + this.value);
 | 
				
			||||||
      } catch (error) {
 | 
					
 | 
				
			||||||
        showError(error);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.ongoing = false;
 | 
					      this.ongoing = false;
 | 
				
			||||||
      if (this.results.length == 0) {
 | 
					      if (this.results.length == 0) {
 | 
				
			||||||
        this.noneMessage = "No results found in indexed search.";
 | 
					        this.noneMessage = "No results found in indexed search.";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,9 +24,9 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { state, mutations, getters } from "@/store";
 | 
					import { state, mutations } from "@/store";
 | 
				
			||||||
import throttle from "@/utils/throttle";
 | 
					import throttle from "@/utils/throttle";
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  props: {
 | 
					  props: {
 | 
				
			||||||
    src: String,
 | 
					    src: String,
 | 
				
			||||||
| 
						 | 
					@ -131,8 +131,7 @@ export default {
 | 
				
			||||||
          imgex.onload = () => URL.revokeObjectURL(imgex.src); // Clean up URL object after loading
 | 
					          imgex.onload = () => URL.revokeObjectURL(imgex.src); // Clean up URL object after loading
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (error) {
 | 
					      } catch (error) {
 | 
				
			||||||
        showError("Error decoding TIFF");
 | 
					        notify.showError("Error decoding TIFF");
 | 
				
			||||||
        console.error("Error decoding TIFF:", error);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    onMouseUp() {
 | 
					    onMouseUp() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,37 +1,38 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div
 | 
					  <component
 | 
				
			||||||
    :class="{ activebutton: this.isMaximized && this.isSelected }"
 | 
					    :is="isSelected || user.singleClick ? 'a' : 'div'"
 | 
				
			||||||
    class="item"
 | 
					    :href="isSelected || user.singleClick ? url : undefined"
 | 
				
			||||||
 | 
					    :class="{
 | 
				
			||||||
 | 
					      item: true,
 | 
				
			||||||
 | 
					      activebutton: isMaximized && isSelected,
 | 
				
			||||||
 | 
					    }"
 | 
				
			||||||
    role="button"
 | 
					    role="button"
 | 
				
			||||||
    tabindex="0"
 | 
					    tabindex="0"
 | 
				
			||||||
    :draggable="isDraggable"
 | 
					    :draggable="isDraggable"
 | 
				
			||||||
    @dragstart="dragStart"
 | 
					    @dragstart="dragStart"
 | 
				
			||||||
    @dragover="dragOver"
 | 
					    @dragover="dragOver"
 | 
				
			||||||
    @drop="drop"
 | 
					    @drop="drop"
 | 
				
			||||||
    @click="itemClick"
 | 
					 | 
				
			||||||
    :data-dir="isDir"
 | 
					    :data-dir="isDir"
 | 
				
			||||||
    :data-type="type"
 | 
					    :data-type="type"
 | 
				
			||||||
    :aria-label="name"
 | 
					    :aria-label="name"
 | 
				
			||||||
    :aria-selected="isSelected"
 | 
					    :aria-selected="isSelected"
 | 
				
			||||||
 | 
					    @click="isSelected || user.singleClick ? toggleClick() : itemClick($event)"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <div
 | 
					    <div @click="toggleClick" :class="{ activetitle: isMaximized && isSelected }">
 | 
				
			||||||
      @click="toggleClick"
 | 
					 | 
				
			||||||
      :class="{ activetitle: this.isMaximized && this.isSelected }"
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <img
 | 
					      <img
 | 
				
			||||||
        v-if="readOnly === undefined && type === 'image' && isThumbsEnabled && isInView"
 | 
					        v-if="readOnly === undefined && type === 'image' && isThumbsEnabled && isInView"
 | 
				
			||||||
        v-lazy="thumbnailUrl"
 | 
					        v-lazy="thumbnailUrl"
 | 
				
			||||||
        :class="{ activeimg: this.isMaximized && this.isSelected }"
 | 
					        :class="{ activeimg: isMaximized && isSelected }"
 | 
				
			||||||
        ref="thumbnail"
 | 
					        ref="thumbnail"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      <i
 | 
					      <i
 | 
				
			||||||
        :class="{ iconActive: this.isMaximized && this.isSelected }"
 | 
					        :class="{ iconActive: isMaximized && isSelected }"
 | 
				
			||||||
        v-else
 | 
					        v-else
 | 
				
			||||||
        class="material-icons"
 | 
					        class="material-icons"
 | 
				
			||||||
      ></i>
 | 
					      ></i>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="text" :class="{ activecontent: this.isMaximized && this.isSelected }">
 | 
					    <div class="text" :class="{ activecontent: isMaximized && isSelected }">
 | 
				
			||||||
      <p class="name">{{ name }}</p>
 | 
					      <p class="name">{{ name }}</p>
 | 
				
			||||||
      <p v-if="isDir" class="size" data-order="-1">—</p>
 | 
					      <p v-if="isDir" class="size" data-order="-1">—</p>
 | 
				
			||||||
      <p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
 | 
					      <p v-else class="size" :data-order="humanSize()">{{ humanSize() }}</p>
 | 
				
			||||||
| 
						 | 
					@ -39,7 +40,7 @@
 | 
				
			||||||
        <time :datetime="modified">{{ humanTime() }}</time>
 | 
					        <time :datetime="modified">{{ humanTime() }}</time>
 | 
				
			||||||
      </p>
 | 
					      </p>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </component>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
| 
						 | 
					@ -73,7 +74,7 @@ import { state, getters, mutations } from "@/store"; // Import your custom store
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "item",
 | 
					  name: "item",
 | 
				
			||||||
  data: function () {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      isThumbnailInView: false,
 | 
					      isThumbnailInView: false,
 | 
				
			||||||
      isMaximized: false,
 | 
					      isMaximized: false,
 | 
				
			||||||
| 
						 | 
					@ -98,27 +99,12 @@ export default {
 | 
				
			||||||
    selected() {
 | 
					    selected() {
 | 
				
			||||||
      return state.selected;
 | 
					      return state.selected;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    req() {
 | 
					 | 
				
			||||||
      return state.req;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    jwt() {
 | 
					 | 
				
			||||||
      return state.jwt;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    selectedCount() {
 | 
					 | 
				
			||||||
      return getters.selectedCount();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    isClicked() {
 | 
					    isClicked() {
 | 
				
			||||||
      if (state.user.singleClick || !this.allowedView) {
 | 
					      if (state.user.singleClick || !this.allowedView) {
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return !this.isMaximized;
 | 
					      return !this.isMaximized;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    allowedView() {
 | 
					 | 
				
			||||||
      return state.user.viewMode != "gallery" && state.user.viewMode != "normal";
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    singleClick() {
 | 
					 | 
				
			||||||
      return this.readOnly == undefined && state.user.singleClick;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    isSelected() {
 | 
					    isSelected() {
 | 
				
			||||||
      return this.selected.indexOf(this.index) !== -1;
 | 
					      return this.selected.indexOf(this.index) !== -1;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -181,18 +167,18 @@ export default {
 | 
				
			||||||
    toggleClick() {
 | 
					    toggleClick() {
 | 
				
			||||||
      this.isMaximized = this.isClicked;
 | 
					      this.isMaximized = this.isClicked;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    humanSize: function () {
 | 
					    humanSize() {
 | 
				
			||||||
      return this.type == "invalid_link"
 | 
					      return this.type == "invalid_link"
 | 
				
			||||||
        ? "invalid link"
 | 
					        ? "invalid link"
 | 
				
			||||||
        : getHumanReadableFilesize(this.size);
 | 
					        : getHumanReadableFilesize(this.size);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    humanTime: function () {
 | 
					    humanTime() {
 | 
				
			||||||
      if (this.readOnly == undefined && state.user.dateFormat) {
 | 
					      if (this.readOnly == undefined && state.user.dateFormat) {
 | 
				
			||||||
        return fromNow(this.modified, state.user.locale).format("L LT");
 | 
					        return fromNow(this.modified, state.user.locale).format("L LT");
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return fromNow(this.modified, state.user.locale);
 | 
					      return fromNow(this.modified, state.user.locale);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    dragStart: function () {
 | 
					    dragStart() {
 | 
				
			||||||
      if (getters.selectedCount() === 0) {
 | 
					      if (getters.selectedCount() === 0) {
 | 
				
			||||||
        mutations.addSelected(this.index);
 | 
					        mutations.addSelected(this.index);
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
| 
						 | 
					@ -203,7 +189,7 @@ export default {
 | 
				
			||||||
        mutations.addSelected(this.index);
 | 
					        mutations.addSelected(this.index);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    dragOver: function (event) {
 | 
					    dragOver(event) {
 | 
				
			||||||
      if (!this.canDrop) return;
 | 
					      if (!this.canDrop) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      event.preventDefault();
 | 
					      event.preventDefault();
 | 
				
			||||||
| 
						 | 
					@ -217,7 +203,7 @@ export default {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      el.style.opacity = 1;
 | 
					      el.style.opacity = 1;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    drop: async function (event) {
 | 
					    async drop(event) {
 | 
				
			||||||
      if (!this.canDrop) return;
 | 
					      if (!this.canDrop) return;
 | 
				
			||||||
      event.preventDefault();
 | 
					      event.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -276,11 +262,11 @@ export default {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      action(overwrite, rename);
 | 
					      action(overwrite, rename);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    itemClick: function (event) {
 | 
					    itemClick(event) {
 | 
				
			||||||
      if (this.singleClick && !state.multiple) this.open();
 | 
					      if (this.singleClick && !state.multiple) this.open();
 | 
				
			||||||
      else this.click(event);
 | 
					      else this.click(event);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    click: function (event) {
 | 
					    click(event) {
 | 
				
			||||||
      if (!this.singleClick && getters.selectedCount() !== 0) event.preventDefault();
 | 
					      if (!this.singleClick && getters.selectedCount() !== 0) event.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      setTimeout(() => {
 | 
					      setTimeout(() => {
 | 
				
			||||||
| 
						 | 
					@ -321,7 +307,7 @@ export default {
 | 
				
			||||||
        mutations.resetSelected();
 | 
					        mutations.resetSelected();
 | 
				
			||||||
      mutations.addSelected(this.index);
 | 
					      mutations.addSelected(this.index);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    open: function () {
 | 
					    open() {
 | 
				
			||||||
      this.$router.push({ path: this.url });
 | 
					      this.$router.push({ path: this.url });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,7 +52,7 @@ import FileList from "./FileList.vue";
 | 
				
			||||||
import { files as api } from "@/api";
 | 
					import { files as api } from "@/api";
 | 
				
			||||||
import buttons from "@/utils/buttons";
 | 
					import buttons from "@/utils/buttons";
 | 
				
			||||||
import * as upload from "@/utils/upload";
 | 
					import * as upload from "@/utils/upload";
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "copy",
 | 
					  name: "copy",
 | 
				
			||||||
| 
						 | 
					@ -102,7 +102,7 @@ export default {
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
          .catch((e) => {
 | 
					          .catch((e) => {
 | 
				
			||||||
            buttons.done("copy");
 | 
					            buttons.done("copy");
 | 
				
			||||||
            showError(e);
 | 
					            notify.showError(e);
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,7 @@
 | 
				
			||||||
import { files as api } from "@/api";
 | 
					import { files as api } from "@/api";
 | 
				
			||||||
import buttons from "@/utils/buttons";
 | 
					import buttons from "@/utils/buttons";
 | 
				
			||||||
import { state, getters, mutations } from "@/store";
 | 
					import { state, getters, mutations } from "@/store";
 | 
				
			||||||
import { showError,showSuccess } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "delete",
 | 
					  name: "delete",
 | 
				
			||||||
| 
						 | 
					@ -59,7 +59,7 @@ export default {
 | 
				
			||||||
        if (!this.isListing) {
 | 
					        if (!this.isListing) {
 | 
				
			||||||
          await api.remove(state.route.path);
 | 
					          await api.remove(state.route.path);
 | 
				
			||||||
          buttons.success("delete");
 | 
					          buttons.success("delete");
 | 
				
			||||||
          showSuccess("Deleted item successfully")
 | 
					          showSuccess("Deleted item successfully");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          this.currentPrompt?.confirm();
 | 
					          this.currentPrompt?.confirm();
 | 
				
			||||||
          this.closeHovers();
 | 
					          this.closeHovers();
 | 
				
			||||||
| 
						 | 
					@ -79,11 +79,11 @@ export default {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await Promise.all(promises);
 | 
					        await Promise.all(promises);
 | 
				
			||||||
        buttons.success("delete");
 | 
					        buttons.success("delete");
 | 
				
			||||||
        showSuccess("Deleted item successfully")
 | 
					        showSuccess("Deleted item successfully");
 | 
				
			||||||
        mutations.setReload(true); // Handle reload as needed
 | 
					        mutations.setReload(true); // Handle reload as needed
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        buttons.done("delete");
 | 
					        buttons.done("delete");
 | 
				
			||||||
        showError(e);
 | 
					        notify.showError(e);
 | 
				
			||||||
        if (this.isListing) mutations.setReload(true); // Handle reload as needed
 | 
					        if (this.isListing) mutations.setReload(true); // Handle reload as needed
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { users as api } from "@/api";
 | 
					import { users as api } from "@/api";
 | 
				
			||||||
import { showSuccess,showError } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
import buttons from "@/utils/buttons";
 | 
					import buttons from "@/utils/buttons";
 | 
				
			||||||
import { state, mutations, getters } from "@/store";
 | 
					import { state, mutations, getters } from "@/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,12 +40,12 @@ export default {
 | 
				
			||||||
      event.preventDefault();
 | 
					      event.preventDefault();
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        await api.remove(this.user.id);
 | 
					        await api.remove(this.user.id);
 | 
				
			||||||
        this.$router.push({ path: "/settings/users" });
 | 
					        this.$router.push({ path: "/settings",hash:"#users-main" });
 | 
				
			||||||
        showSuccess(this.$t("settings.userDeleted"));
 | 
					        notify.showSuccess(this.$t("settings.userDeleted"));
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        e.message === "403"
 | 
					        e.message === "403"
 | 
				
			||||||
          ? showError(this.$t("errors.forbidden"), false)
 | 
					          ? notify.showError(this.$t("errors.forbidden"), false)
 | 
				
			||||||
          : showError(e);
 | 
					          : notify.showError(e);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    closeHovers() {
 | 
					    closeHovers() {
 | 
				
			||||||
| 
						 | 
					@ -80,7 +80,7 @@ export default {
 | 
				
			||||||
        mutations.setReload(true); // Handle reload as needed
 | 
					        mutations.setReload(true); // Handle reload as needed
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        buttons.done("delete");
 | 
					        buttons.done("delete");
 | 
				
			||||||
        showError(e);
 | 
					        notify.showError(e);
 | 
				
			||||||
        if (this.isListing) mutations.setReload(true); // Handle reload as needed
 | 
					        if (this.isListing) mutations.setReload(true); // Handle reload as needed
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,6 @@
 | 
				
			||||||
import { state, mutations } from "@/store";
 | 
					import { state, mutations } from "@/store";
 | 
				
			||||||
import url from "@/utils/url";
 | 
					import url from "@/utils/url";
 | 
				
			||||||
import { files } from "@/api";
 | 
					import { files } from "@/api";
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "file-list",
 | 
					  name: "file-list",
 | 
				
			||||||
| 
						 | 
					@ -86,7 +85,7 @@ export default {
 | 
				
			||||||
      // content.
 | 
					      // content.
 | 
				
			||||||
      let uri = event.currentTarget.dataset.url;
 | 
					      let uri = event.currentTarget.dataset.url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      files.fetch(uri).then(this.fillOptions).catch(showError);
 | 
					      files.fetch(uri).then(this.fillOptions);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    touchstart(event) {
 | 
					    touchstart(event) {
 | 
				
			||||||
      let url = event.currentTarget.dataset.url;
 | 
					      let url = event.currentTarget.dataset.url;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -75,7 +75,6 @@ import { getHumanReadableFilesize } from "@/utils/filesizes";
 | 
				
			||||||
import { formatTimestamp } from "@/utils/moment";
 | 
					import { formatTimestamp } from "@/utils/moment";
 | 
				
			||||||
import { files as api } from "@/api";
 | 
					import { files as api } from "@/api";
 | 
				
			||||||
import { state, getters, mutations } from "@/store"; // Import your custom store
 | 
					import { state, getters, mutations } from "@/store"; // Import your custom store
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "info",
 | 
					  name: "info",
 | 
				
			||||||
| 
						 | 
					@ -146,12 +145,8 @@ export default {
 | 
				
			||||||
        link = state.route.path;
 | 
					        link = state.route.path;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
      const hash = await api.checksum(link, algo);
 | 
					      const hash = await api.checksum(link, algo);
 | 
				
			||||||
      event.target.innerHTML = hash;
 | 
					      event.target.innerHTML = hash;
 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        showError(e);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,7 +52,7 @@ import FileList from "./FileList.vue";
 | 
				
			||||||
import { files as api } from "@/api";
 | 
					import { files as api } from "@/api";
 | 
				
			||||||
import buttons from "@/utils/buttons";
 | 
					import buttons from "@/utils/buttons";
 | 
				
			||||||
import * as upload from "@/utils/upload";
 | 
					import * as upload from "@/utils/upload";
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "move",
 | 
					  name: "move",
 | 
				
			||||||
| 
						 | 
					@ -95,7 +95,7 @@ export default {
 | 
				
			||||||
          })
 | 
					          })
 | 
				
			||||||
          .catch((e) => {
 | 
					          .catch((e) => {
 | 
				
			||||||
            buttons.done("move");
 | 
					            buttons.done("move");
 | 
				
			||||||
            showError(e);
 | 
					            notify.showError(e);
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -121,7 +121,6 @@ export default {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      action(overwrite, rename);
 | 
					      action(overwrite, rename);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,7 +39,6 @@
 | 
				
			||||||
import { files as api } from "@/api";
 | 
					import { files as api } from "@/api";
 | 
				
			||||||
import url from "@/utils/url";
 | 
					import url from "@/utils/url";
 | 
				
			||||||
import { getters, mutations, state } from "@/store"; // Import your custom store
 | 
					import { getters, mutations, state } from "@/store"; // Import your custom store
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "new-dir",
 | 
					  name: "new-dir",
 | 
				
			||||||
| 
						 | 
					@ -87,7 +86,6 @@ export default {
 | 
				
			||||||
      uri += encodeURIComponent(this.name) + "/";
 | 
					      uri += encodeURIComponent(this.name) + "/";
 | 
				
			||||||
      uri = uri.replace("//", "/");
 | 
					      uri = uri.replace("//", "/");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
      await api.post(uri);
 | 
					      await api.post(uri);
 | 
				
			||||||
      if (this.redirect) {
 | 
					      if (this.redirect) {
 | 
				
			||||||
        this.$router.push({ path: uri });
 | 
					        this.$router.push({ path: uri });
 | 
				
			||||||
| 
						 | 
					@ -95,9 +93,6 @@ export default {
 | 
				
			||||||
        const res = await api.fetch(url.removeLastDir(uri) + "/");
 | 
					        const res = await api.fetch(url.removeLastDir(uri) + "/");
 | 
				
			||||||
        mutations.updateRequest(res);
 | 
					        mutations.updateRequest(res);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        showError(e);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      mutations.closeHovers();
 | 
					      mutations.closeHovers();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -73,12 +73,8 @@ export default {
 | 
				
			||||||
      uri += encodeURIComponent(this.name);
 | 
					      uri += encodeURIComponent(this.name);
 | 
				
			||||||
      uri = uri.replace("//", "/");
 | 
					      uri = uri.replace("//", "/");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
      await api.post(uri);
 | 
					      await api.post(uri);
 | 
				
			||||||
      this.$router.push({ path: uri });
 | 
					      this.$router.push({ path: uri });
 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        showError(e);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      mutations.closeHovers();
 | 
					      mutations.closeHovers();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -98,7 +98,6 @@ export default {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
 | 
					      newLink = url.removeLastDir(oldLink) + "/" + encodeURIComponent(this.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
      await api.move([{ from: oldLink, to: newLink }]);
 | 
					      await api.move([{ from: oldLink, to: newLink }]);
 | 
				
			||||||
      if (!this.isListing) {
 | 
					      if (!this.isListing) {
 | 
				
			||||||
        this.$router.push({ path: newLink });
 | 
					        this.$router.push({ path: newLink });
 | 
				
			||||||
| 
						 | 
					@ -106,9 +105,6 @@ export default {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      mutations.setReload(true);
 | 
					      mutations.setReload(true);
 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        showError(e);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      mutations.closeHovers();
 | 
					      mutations.closeHovers();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -120,7 +120,7 @@
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { showSuccess, showError } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
import { state, getters, mutations } from "@/store";
 | 
					import { state, getters, mutations } from "@/store";
 | 
				
			||||||
import { share as api, pub as pub_api } from "@/api";
 | 
					import { share as api, pub as pub_api } from "@/api";
 | 
				
			||||||
import { fromNow } from "@/utils/moment";
 | 
					import { fromNow } from "@/utils/moment";
 | 
				
			||||||
| 
						 | 
					@ -173,7 +173,6 @@ export default {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  async beforeMount() {
 | 
					  async beforeMount() {
 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
    const links = await api.get(this.url);
 | 
					    const links = await api.get(this.url);
 | 
				
			||||||
    this.links = links;
 | 
					    this.links = links;
 | 
				
			||||||
    this.sort();
 | 
					    this.sort();
 | 
				
			||||||
| 
						 | 
					@ -181,14 +180,11 @@ export default {
 | 
				
			||||||
    if (this.links.length === 0) {
 | 
					    if (this.links.length === 0) {
 | 
				
			||||||
      this.listing = false;
 | 
					      this.listing = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      showError(e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {
 | 
					  mounted() {
 | 
				
			||||||
    this.clip = new Clipboard(".copy-clipboard");
 | 
					    this.clip = new Clipboard(".copy-clipboard");
 | 
				
			||||||
    this.clip.on("success", () => {
 | 
					    this.clip.on("success", () => {
 | 
				
			||||||
      showSuccess(this.$t("success.linkCopied"));
 | 
					      notify.showSuccess(this.$t("success.linkCopied"));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  beforeUnmount() {
 | 
					  beforeUnmount() {
 | 
				
			||||||
| 
						 | 
					@ -198,7 +194,6 @@ export default {
 | 
				
			||||||
    async submit() {
 | 
					    async submit() {
 | 
				
			||||||
      let isPermanent = !this.time || this.time === 0;
 | 
					      let isPermanent = !this.time || this.time === 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
      let res = null;
 | 
					      let res = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (isPermanent) {
 | 
					      if (isPermanent) {
 | 
				
			||||||
| 
						 | 
					@ -215,22 +210,15 @@ export default {
 | 
				
			||||||
      this.password = "";
 | 
					      this.password = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.listing = true;
 | 
					      this.listing = true;
 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        showError(e);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async deleteLink(event, link) {
 | 
					    async deleteLink(event, link) {
 | 
				
			||||||
      event.preventDefault();
 | 
					      event.preventDefault();
 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
      await api.remove(link.hash);
 | 
					      await api.remove(link.hash);
 | 
				
			||||||
      this.links = this.links.filter((item) => item.hash !== link.hash);
 | 
					      this.links = this.links.filter((item) => item.hash !== link.hash);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (this.links.length === 0) {
 | 
					      if (this.links.length === 0) {
 | 
				
			||||||
        this.listing = false;
 | 
					        this.listing = false;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        showError(e);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    humanTime(time) {
 | 
					    humanTime(time) {
 | 
				
			||||||
      return fromNow(time, state.user.locale);
 | 
					      return fromNow(time, state.user.locale);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -68,7 +68,7 @@ export default {
 | 
				
			||||||
      handleFiles(event);
 | 
					      handleFiles(event);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleFiles = (event) => {
 | 
					    const handleFiles = async (event) => {
 | 
				
			||||||
      mutations.closeHovers();
 | 
					      mutations.closeHovers();
 | 
				
			||||||
      const files = event.target.files;
 | 
					      const files = event.target.files;
 | 
				
			||||||
      if (!files) return;
 | 
					      if (!files) return;
 | 
				
			||||||
| 
						 | 
					@ -94,21 +94,20 @@ export default {
 | 
				
			||||||
      if (conflict) {
 | 
					      if (conflict) {
 | 
				
			||||||
        mutations.showHover({
 | 
					        mutations.showHover({
 | 
				
			||||||
          name: "replace",
 | 
					          name: "replace",
 | 
				
			||||||
          action: (event) => {
 | 
					          action: async (event) => {
 | 
				
			||||||
            event.preventDefault();
 | 
					            event.preventDefault();
 | 
				
			||||||
            mutations.closeHovers();
 | 
					            mutations.closeHovers();
 | 
				
			||||||
            upload.handleFiles(uploadFiles, path, false);
 | 
					            await upload.handleFiles(uploadFiles, path, false);
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          confirm: (event) => {
 | 
					          confirm: async (event) => {
 | 
				
			||||||
            event.preventDefault();
 | 
					            event.preventDefault();
 | 
				
			||||||
            mutations.closeHovers();
 | 
					            mutations.closeHovers();
 | 
				
			||||||
            upload.handleFiles(uploadFiles, path, true);
 | 
					            await upload.handleFiles(uploadFiles, path, true);
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        return;
 | 
					      } else {
 | 
				
			||||||
 | 
					        await upload.handleFiles(uploadFiles, path, true);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					 | 
				
			||||||
      upload.handleFiles(uploadFiles, path, true);
 | 
					 | 
				
			||||||
      mutations.setReload(true);
 | 
					      mutations.setReload(true);
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,7 @@
 | 
				
			||||||
        type="text"
 | 
					        type="text"
 | 
				
			||||||
        v-model="user.username"
 | 
					        v-model="user.username"
 | 
				
			||||||
        id="username"
 | 
					        id="username"
 | 
				
			||||||
 | 
					        @input="emitUpdate"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,22 +19,24 @@
 | 
				
			||||||
        :placeholder="passwordPlaceholder"
 | 
					        :placeholder="passwordPlaceholder"
 | 
				
			||||||
        v-model="user.password"
 | 
					        v-model="user.password"
 | 
				
			||||||
        id="password"
 | 
					        id="password"
 | 
				
			||||||
 | 
					        @input="emitUpdate"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <p>
 | 
					    <p>
 | 
				
			||||||
      <label for="scope">{{ $t("settings.scope") }}</label>
 | 
					      <label for="scope">{{ $t("settings.scope") }}</label>
 | 
				
			||||||
      <input
 | 
					      <input
 | 
				
			||||||
        :disabled="createUserDirData"
 | 
					        :disabled="createUserDir"
 | 
				
			||||||
        :placeholder="scopePlaceholder"
 | 
					        :placeholder="scopePlaceholder"
 | 
				
			||||||
        class="input input--block"
 | 
					        class="input input--block"
 | 
				
			||||||
        type="text"
 | 
					        type="text"
 | 
				
			||||||
        v-model="user.scope"
 | 
					        v-model="user.scope"
 | 
				
			||||||
        id="scope"
 | 
					        id="scope"
 | 
				
			||||||
 | 
					        @input="emitUpdate"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
    <p class="small" v-if="displayHomeDirectoryCheckbox">
 | 
					    <p class="small" v-if="displayHomeDirectoryCheckbox">
 | 
				
			||||||
      <input type="checkbox" v-model="createUserDirData" />
 | 
					      <input type="checkbox" v-model="createUserDir" />
 | 
				
			||||||
      {{ $t("settings.createUserHomeDirectory") }}
 | 
					      {{ $t("settings.createUserHomeDirectory") }}
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,31 +46,32 @@
 | 
				
			||||||
        class="input input--block"
 | 
					        class="input input--block"
 | 
				
			||||||
        id="locale"
 | 
					        id="locale"
 | 
				
			||||||
        v-model:locale="user.locale"
 | 
					        v-model:locale="user.locale"
 | 
				
			||||||
 | 
					        @input="emitUpdate"
 | 
				
			||||||
      ></languages>
 | 
					      ></languages>
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <p v-if="!isDefault">
 | 
					    <p v-if="!isDefault">
 | 
				
			||||||
      <input
 | 
					      <input
 | 
				
			||||||
        type="checkbox"
 | 
					        type="checkbox"
 | 
				
			||||||
        :disabled="user.perm.admin"
 | 
					        :disabled="user.perm?.admin"
 | 
				
			||||||
        v-model="user.lockPassword"
 | 
					        v-model="user.lockPassword"
 | 
				
			||||||
 | 
					        @input="emitUpdate"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      {{ $t("settings.lockPassword") }}
 | 
					      {{ $t("settings.lockPassword") }}
 | 
				
			||||||
    </p>
 | 
					    </p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <permissions :perm="user.perm" />
 | 
					    <permissions :perm="localUser.perm" />
 | 
				
			||||||
    <commands v-if="isExecEnabled" v-model:commands="user.commands" />
 | 
					    <commands v-if="isExecEnabled" v-model:commands="user.commands" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div v-if="!isDefault">
 | 
					    <div v-if="!isDefault">
 | 
				
			||||||
      <h3>{{ $t("settings.rules") }}</h3>
 | 
					      <h3>{{ $t("settings.rules") }}</h3>
 | 
				
			||||||
      <p class="small">{{ $t("settings.rulesHelp") }}</p>
 | 
					      <p class="small">{{ $t("settings.rulesHelp") }}</p>
 | 
				
			||||||
      <rules v-model:rules="user.rules" />
 | 
					      <rules v-model:rules="user.rules" @input="emitUpdate" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { state } from "@/store"
 | 
					 | 
				
			||||||
import Languages from "./Languages.vue";
 | 
					import Languages from "./Languages.vue";
 | 
				
			||||||
import Rules from "./Rules.vue";
 | 
					import Rules from "./Rules.vue";
 | 
				
			||||||
import Permissions from "./Permissions.vue";
 | 
					import Permissions from "./Permissions.vue";
 | 
				
			||||||
| 
						 | 
					@ -75,35 +79,51 @@ import Commands from "./Commands.vue";
 | 
				
			||||||
import { enableExec } from "@/utils/constants";
 | 
					import { enableExec } from "@/utils/constants";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "user",
 | 
					  name: "UserForm",
 | 
				
			||||||
  data() {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      createUserDirData: false,
 | 
					 | 
				
			||||||
      originalUserScope: "/",
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
    Permissions,
 | 
					    Permissions,
 | 
				
			||||||
    Languages,
 | 
					    Languages,
 | 
				
			||||||
    Rules,
 | 
					    Rules,
 | 
				
			||||||
    Commands,
 | 
					    Commands,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  props: [ "createUserDir", "isNew", "isDefault"],
 | 
					  data() {
 | 
				
			||||||
  created() {
 | 
					    return {
 | 
				
			||||||
    this.originalUserScope = state.user.scope;
 | 
					      createUserDir: false,
 | 
				
			||||||
    this.createUserDirData = this.createUserDir;
 | 
					      originalUserScope: ".",
 | 
				
			||||||
 | 
					      localUser: { ...this.user },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  props: {
 | 
				
			||||||
 | 
					    user: Object, // Define user as a prop
 | 
				
			||||||
 | 
					    isDefault: Boolean,
 | 
				
			||||||
 | 
					    isNew: Boolean,
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  watch: {
 | 
				
			||||||
 | 
					    user: {
 | 
				
			||||||
 | 
					      handler(newUser) {
 | 
				
			||||||
 | 
					        console.log("UserForm: user changed", newUser);
 | 
				
			||||||
 | 
					        this.localUser = { ...newUser };  // Watch for changes in the parent and update the local copy
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      immediate: true,
 | 
				
			||||||
 | 
					      deep: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "user.perm.admin": function (newValue) {
 | 
				
			||||||
 | 
					      if (newValue) {
 | 
				
			||||||
 | 
					        this.user.lockPassword = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    createUserDir(newVal) {
 | 
				
			||||||
 | 
					      this.user.scope = newVal ? "" : this.originalUserScope;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    user() {
 | 
					 | 
				
			||||||
      return state.user;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    passwordPlaceholder() {
 | 
					    passwordPlaceholder() {
 | 
				
			||||||
      return this.isNew ? "" : this.$t("settings.avoidChanges");
 | 
					      return this.isNew ? "" : this.$t("settings.avoidChanges");
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    scopePlaceholder() {
 | 
					    scopePlaceholder() {
 | 
				
			||||||
      return this.createUserDir
 | 
					      return this.createUserDir
 | 
				
			||||||
        ? this.$t("settings.userScopeGenerationPlaceholder")
 | 
					        ? this.$t("settings.userScopeGenerationPlaceholder")
 | 
				
			||||||
        : "";
 | 
					        : "./";
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    displayHomeDirectoryCheckbox() {
 | 
					    displayHomeDirectoryCheckbox() {
 | 
				
			||||||
      return this.isNew && this.createUserDir;
 | 
					      return this.isNew && this.createUserDir;
 | 
				
			||||||
| 
						 | 
					@ -112,10 +132,5 @@ export default {
 | 
				
			||||||
      return enableExec; // Removed arrow function
 | 
					      return enableExec; // Removed arrow function
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  watch: {
 | 
					 | 
				
			||||||
    createUserDirData(newVal) {
 | 
					 | 
				
			||||||
      state.user.scope = newVal ? "" : this.originalUserScope;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -44,35 +44,6 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <!-- Section for logged-in users -->
 | 
					  <!-- Section for logged-in users -->
 | 
				
			||||||
  <div v-if="isLoggedIn" class="sidebar-scroll-list">
 | 
					  <div v-if="isLoggedIn" class="sidebar-scroll-list">
 | 
				
			||||||
    <!-- Buttons visible if user has create permission -->
 | 
					 | 
				
			||||||
    <div v-if="user.perm?.create">
 | 
					 | 
				
			||||||
      <!-- New Folder button -->
 | 
					 | 
				
			||||||
      <button
 | 
					 | 
				
			||||||
        @click="showHover('newDir')"
 | 
					 | 
				
			||||||
        class="action"
 | 
					 | 
				
			||||||
        :aria-label="$t('sidebar.newFolder')"
 | 
					 | 
				
			||||||
        :title="$t('sidebar.newFolder')"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <i class="material-icons">create_new_folder</i>
 | 
					 | 
				
			||||||
        <span>{{ $t("sidebar.newFolder") }}</span>
 | 
					 | 
				
			||||||
      </button>
 | 
					 | 
				
			||||||
      <!-- New File button -->
 | 
					 | 
				
			||||||
      <button
 | 
					 | 
				
			||||||
        @click="showHover('newFile')"
 | 
					 | 
				
			||||||
        class="action"
 | 
					 | 
				
			||||||
        :aria-label="$t('sidebar.newFile')"
 | 
					 | 
				
			||||||
        :title="$t('sidebar.newFile')"
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <i class="material-icons">note_add</i>
 | 
					 | 
				
			||||||
        <span>{{ $t("sidebar.newFile") }}</span>
 | 
					 | 
				
			||||||
      </button>
 | 
					 | 
				
			||||||
      <!-- Upload button -->
 | 
					 | 
				
			||||||
      <button id="upload-button" @click="uploadFunc" class="action">
 | 
					 | 
				
			||||||
        <i class="material-icons">file_upload</i>
 | 
					 | 
				
			||||||
        <span>Upload file</span>
 | 
					 | 
				
			||||||
      </button>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div v-if="isLoggedIn" class="sources card">
 | 
					    <div v-if="isLoggedIn" class="sources card">
 | 
				
			||||||
      <span>Sources</span>
 | 
					      <span>Sources</span>
 | 
				
			||||||
      <div class="inner-card">
 | 
					      <div class="inner-card">
 | 
				
			||||||
| 
						 | 
					@ -138,7 +109,6 @@ import { files } from "@/api";
 | 
				
			||||||
import ProgressBar from "@/components/ProgressBar.vue";
 | 
					import ProgressBar from "@/components/ProgressBar.vue";
 | 
				
			||||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
 | 
					import { getHumanReadableFilesize } from "@/utils/filesizes";
 | 
				
			||||||
import { state, getters, mutations } from "@/store"; // Import your custom store
 | 
					import { state, getters, mutations } from "@/store"; // Import your custom store
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "SidebarGeneral",
 | 
					  name: "SidebarGeneral",
 | 
				
			||||||
| 
						 | 
					@ -192,13 +162,13 @@ export default {
 | 
				
			||||||
      this.hoverText = "Quick Toggles"; // Reset to default hover text
 | 
					      this.hoverText = "Quick Toggles"; // Reset to default hover text
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    toggleClick() {
 | 
					    toggleClick() {
 | 
				
			||||||
      mutations.updateUser({ singleClick: !state.user.singleClick });
 | 
					      mutations.updateCurrentUser({ singleClick: !state.user.singleClick });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    toggleDarkMode() {
 | 
					    toggleDarkMode() {
 | 
				
			||||||
      mutations.toggleDarkMode();
 | 
					      mutations.toggleDarkMode();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    toggleSticky() {
 | 
					    toggleSticky() {
 | 
				
			||||||
      mutations.updateUser({ stickySidebar: !state.user.stickySidebar });
 | 
					      mutations.updateCurrentUser({ stickySidebar: !state.user.stickySidebar });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async updateUsage() {
 | 
					    async updateUsage() {
 | 
				
			||||||
      if (!getters.isLoggedIn()) {
 | 
					      if (!getters.isLoggedIn()) {
 | 
				
			||||||
| 
						 | 
					@ -209,21 +179,16 @@ export default {
 | 
				
			||||||
      if (this.disableUsedPercentage) {
 | 
					      if (this.disableUsedPercentage) {
 | 
				
			||||||
        return usageStats;
 | 
					        return usageStats;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
      let usage = await files.usage(path);
 | 
					      let usage = await files.usage(path);
 | 
				
			||||||
      usageStats = {
 | 
					      usageStats = {
 | 
				
			||||||
        used: getHumanReadableFilesize(usage.used / 1024),
 | 
					        used: getHumanReadableFilesize(usage.used / 1024),
 | 
				
			||||||
        total: getHumanReadableFilesize(usage.total / 1024),
 | 
					        total: getHumanReadableFilesize(usage.total / 1024),
 | 
				
			||||||
        usedPercentage: Math.round((usage.used / usage.total) * 100),
 | 
					        usedPercentage: Math.round((usage.used / usage.total) * 100),
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
      } catch (error) {
 | 
					
 | 
				
			||||||
        showError("Error fetching usage", error);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      mutations.setUsage(usageStats);
 | 
					      mutations.setUsage(usageStats);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    showHover(value) {
 | 
					
 | 
				
			||||||
      return mutations.showHover(value);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    navigateTo(path) {
 | 
					    navigateTo(path) {
 | 
				
			||||||
      const hashIndex = path.indexOf("#");
 | 
					      const hashIndex = path.indexOf("#");
 | 
				
			||||||
      if (hashIndex !== -1) {
 | 
					      if (hashIndex !== -1) {
 | 
				
			||||||
| 
						 | 
					@ -241,9 +206,7 @@ export default {
 | 
				
			||||||
    help() {
 | 
					    help() {
 | 
				
			||||||
      mutations.showHover("help");
 | 
					      mutations.showHover("help");
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    uploadFunc() {
 | 
					
 | 
				
			||||||
      mutations.showHover("upload");
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    // Logout the user
 | 
					    // Logout the user
 | 
				
			||||||
    logout: auth.logout,
 | 
					    logout: auth.logout,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,28 +7,38 @@
 | 
				
			||||||
    @click="setView(setting.id + '-main')"
 | 
					    @click="setView(setting.id + '-main')"
 | 
				
			||||||
    :class="{ 'active-settings': active(setting.id + '-main') }"
 | 
					    :class="{ 'active-settings': active(setting.id + '-main') }"
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    <div class="card-wrapper">{{ setting.label }}</div>
 | 
					    <div v-if="shouldShow(setting)" class="card-wrapper">{{ setting.label }}</div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { state, getters, mutations } from "@/store";
 | 
					import { state, getters, mutations } from "@/store";
 | 
				
			||||||
import { settings } from "@/utils/constants";
 | 
					import { settings } from "@/utils/constants";
 | 
				
			||||||
 | 
					import { router } from "@/router";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "SidebarSettings",
 | 
					  name: "SidebarSettings",
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      settings // Initialize the settings array in data
 | 
					      settings, // Initialize the settings array in data
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    currentHash: () => getters.currentHash(),
 | 
					    currentHash: () => getters.currentHash(),
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
 | 
					    shouldShow(setting) {
 | 
				
			||||||
 | 
					      const perm = setting?.perm || {};
 | 
				
			||||||
 | 
					      // Check if all keys in setting.perm exist in state.user.perm and have truthy values
 | 
				
			||||||
 | 
					      return Object.keys(perm).every((key) => state.user.perm[key]);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    active: (view) => state.activeSettingsView === view,
 | 
					    active: (view) => state.activeSettingsView === view,
 | 
				
			||||||
    setView(view) {
 | 
					    setView(view) {
 | 
				
			||||||
 | 
					      if (state.route.path != "/settings") {
 | 
				
			||||||
 | 
					        router.push({ path: "/settings", hash: "#" + view }, () => {});
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
        mutations.setActiveSettingsView(view);
 | 
					        mutations.setActiveSettingsView(view);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { version, commitSHA } from "@/utils/constants";
 | 
					import { version, commitSHA } from "@/utils/constants";
 | 
				
			||||||
import { state, getters, mutations } from "@/store"; // Import your custom store
 | 
					import { getters, mutations } from "@/store"; // Import your custom store
 | 
				
			||||||
import SidebarGeneral from "./General.vue";
 | 
					import SidebarGeneral from "./General.vue";
 | 
				
			||||||
import SidebarSettings from "./Settings.vue";
 | 
					import SidebarSettings from "./Settings.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,6 +43,7 @@ export default {
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    version: () => version,
 | 
					    version: () => version,
 | 
				
			||||||
 | 
					    commitSHA: () => commitSHA,
 | 
				
			||||||
    isDarkMode: () => getters.isDarkMode(),
 | 
					    isDarkMode: () => getters.isDarkMode(),
 | 
				
			||||||
    isLoggedIn: () => getters.isLoggedIn(),
 | 
					    isLoggedIn: () => getters.isLoggedIn(),
 | 
				
			||||||
    isSettings: () => getters.isSettings(),
 | 
					    isSettings: () => getters.isSettings(),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -92,7 +92,9 @@ main > div {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.breadcrumbs {
 | 
					.breadcrumbs {
 | 
				
			||||||
  height: 3em;
 | 
					  overflow-x: auto;
 | 
				
			||||||
 | 
					  height: auto;
 | 
				
			||||||
 | 
					  min-height: 3em;
 | 
				
			||||||
  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
 | 
					  border-bottom: 1px solid rgba(0, 0, 0, 0.05);
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
| 
						 | 
					@ -162,23 +164,25 @@ button:disabled {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#popup-notification {
 | 
					#popup-notification {
 | 
				
			||||||
  color: white;
 | 
					  border-radius: 1em;
 | 
				
			||||||
 | 
					  color: #fff;
 | 
				
			||||||
  position: fixed;
 | 
					  position: fixed;
 | 
				
			||||||
  max-width: 90vw;
 | 
					  max-width: 90vw;
 | 
				
			||||||
  height: 4em;
 | 
					  height: 4em;
 | 
				
			||||||
  bottom: 0;
 | 
					  bottom: 0;
 | 
				
			||||||
  right: -20em; /* Start off-screen */
 | 
					  right: -20em;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  padding: 1em;
 | 
					  padding: 0.5em;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  transition: right 1s ease; /* Animate the 'right' property */
 | 
					  transition: right 1s ease;
 | 
				
			||||||
  z-index: 5;
 | 
					  z-index: 5;
 | 
				
			||||||
 | 
					  margin: 1em;
 | 
				
			||||||
 | 
					  height: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#popup-notification-content {
 | 
					#popup-notification-content {
 | 
				
			||||||
  color: white;
 | 
					  color: white;
 | 
				
			||||||
  padding: 0;
 | 
					  padding: 1em;
 | 
				
			||||||
  padding-left: .5em;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#popup-notification.success {
 | 
					#popup-notification.success {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -103,6 +103,10 @@
 | 
				
			||||||
  border-color: var(--divider) !important;
 | 
					  border-color: var(--divider) !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dark-mode #listingView.gallery .item .text {
 | 
				
			||||||
 | 
					  text-shadow: 0 0 2px black;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Listing item modified text */
 | 
					/* Listing item modified text */
 | 
				
			||||||
.dark-mode #listingView .item .modified {
 | 
					.dark-mode #listingView .item .modified {
 | 
				
			||||||
  color: var(--textSecondary);
 | 
					  color: var(--textSecondary);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,7 @@ body.rtl #listingView {
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  user-select: none;
 | 
					  user-select: none;
 | 
				
			||||||
 | 
					  overflow:hidden;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#listingView .item div:last-of-type {
 | 
					#listingView .item div:last-of-type {
 | 
				
			||||||
| 
						 | 
					@ -140,7 +141,7 @@ body.rtl #listingView {
 | 
				
			||||||
  display:flex;
 | 
					  display:flex;
 | 
				
			||||||
  min-width: 12em;
 | 
					  min-width: 12em;
 | 
				
			||||||
  min-height: 12em;
 | 
					  min-height: 12em;
 | 
				
			||||||
  text-shadow: 0 0 2px black;
 | 
					  text-shadow: 0 0 2px white;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#listingView.gallery .item div:last-of-type {
 | 
					#listingView.gallery .item div:last-of-type {
 | 
				
			||||||
| 
						 | 
					@ -407,25 +408,3 @@ body.rtl #listingView {
 | 
				
			||||||
#listingView.list .header .active {
 | 
					#listingView.list .header .active {
 | 
				
			||||||
  font-weight: bold;
 | 
					  font-weight: bold;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
#listingView #multiple-selection {
 | 
					 | 
				
			||||||
  position: fixed;
 | 
					 | 
				
			||||||
  bottom: -4em;
 | 
					 | 
				
			||||||
  left: 0;
 | 
					 | 
				
			||||||
  z-index: 99999;
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  background-color: var(--blue);
 | 
					 | 
				
			||||||
  height: 4em;
 | 
					 | 
				
			||||||
  padding: 0.5em 0.5em 0.5em 1em;
 | 
					 | 
				
			||||||
  justify-content: space-between;
 | 
					 | 
				
			||||||
  transition: .2s ease bottom;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#listingView #multiple-selection.active {
 | 
					 | 
				
			||||||
  bottom: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#listingView #multiple-selection p,
 | 
					 | 
				
			||||||
#listingView #multiple-selection i {
 | 
					 | 
				
			||||||
  color: var(--item-selected);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -43,6 +43,8 @@
 | 
				
			||||||
  padding: .5em;
 | 
					  padding: .5em;
 | 
				
			||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
  animation: .2s opac forwards;
 | 
					  animation: .2s opac forwards;
 | 
				
			||||||
 | 
					  margin-bottom: 0.5em;
 | 
				
			||||||
 | 
					  border-radius: 1em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@keyframes opac {
 | 
					@keyframes opac {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -174,6 +174,7 @@
 | 
				
			||||||
    "video": "Video"
 | 
					    "video": "Video"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "settings": {
 | 
					  "settings": {
 | 
				
			||||||
 | 
					    "UserManagement": "User Management",
 | 
				
			||||||
    "admin": "Admin",
 | 
					    "admin": "Admin",
 | 
				
			||||||
    "administrator": "Administrator",
 | 
					    "administrator": "Administrator",
 | 
				
			||||||
    "allowCommands": "Execute commands",
 | 
					    "allowCommands": "Execute commands",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -175,7 +175,7 @@
 | 
				
			||||||
    "avoidChanges": "(değişiklikleri önlemek için boş bırakın)",
 | 
					    "avoidChanges": "(değişiklikleri önlemek için boş bırakın)",
 | 
				
			||||||
    "branding": "Marka",
 | 
					    "branding": "Marka",
 | 
				
			||||||
    "brandingDirectoryPath": "Marka dizin yolu",
 | 
					    "brandingDirectoryPath": "Marka dizin yolu",
 | 
				
			||||||
    "brandingHelp": "Adını değiştirerek, logoyu değiştirerek, özel stiller ekleyerek ve hatta GitHub'a harici bağlantıları devre dışı bırakarak Filebrowser örneğinizin görünüşünü ve hissini özelleştirebilirsiniz.\nÖzel marka bilinci oluşturma hakkında daha fazla bilgi için lütfen {0} sayfasına göz atın.",
 | 
					    "brandingHelp": "Adını değiştirerek, logoyu değiştirerek, özel stiller ekleyerek ve hatta GitHub'a harici bağlantıları devre dışı bırakarak FileBrowser örneğinizin görünüşünü ve hissini özelleştirebilirsiniz.\nÖzel marka bilinci oluşturma hakkında daha fazla bilgi için lütfen {0} sayfasına göz atın.",
 | 
				
			||||||
    "changePassword": "Şifre Değiştir",
 | 
					    "changePassword": "Şifre Değiştir",
 | 
				
			||||||
    "commandRunner": "Komut satırı",
 | 
					    "commandRunner": "Komut satırı",
 | 
				
			||||||
    "commandRunnerHelp": "Burada, adlandırılmış olaylarda yürütülen komutları ayarlayabilirsiniz. Her satıra bir tane yazmalısınız. {0} ve {1} ortam değişkenleri, {1}'ye göre {0} olacak şekilde kullanılabilir olacaktır. Bu özellik ve mevcut ortam değişkenleri hakkında daha fazla bilgi için lütfen {2}'yi okuyun.",
 | 
					    "commandRunnerHelp": "Burada, adlandırılmış olaylarda yürütülen komutları ayarlayabilirsiniz. Her satıra bir tane yazmalısınız. {0} ve {1} ortam değişkenleri, {1}'ye göre {0} olacak şekilde kullanılabilir olacaktır. Bu özellik ve mevcut ortam değişkenleri hakkında daha fazla bilgi için lütfen {2}'yi okuyun.",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,5 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { showSuccess, showError, closePopUp } from "./message.js";
 | 
					import * as notify from "./message.js";
 | 
				
			||||||
export {
 | 
					export {
 | 
				
			||||||
    showSuccess,
 | 
					    notify,
 | 
				
			||||||
    showError,
 | 
					 | 
				
			||||||
    closePopUp,
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,20 +1,35 @@
 | 
				
			||||||
 | 
					import { mutations, state } from "@/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function showPopup(type, message) {
 | 
					export function showPopup(type, message) {
 | 
				
			||||||
    const [popup, popupContent] = getElements();
 | 
					    const [popup, popupContent] = getElements();
 | 
				
			||||||
 | 
					    if (popup == undefined) {
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    popup.classList.remove('success', 'error'); // Clear previous types
 | 
					    popup.classList.remove('success', 'error'); // Clear previous types
 | 
				
			||||||
    popup.classList.add(type);
 | 
					    popup.classList.add(type);
 | 
				
			||||||
    popupContent.textContent = message;
 | 
					    popupContent.textContent = message;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Start animation: bring the popup into view
 | 
					 | 
				
			||||||
    popup.style.right = '1em';
 | 
					    popup.style.right = '1em';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // don't hide for actions
 | 
				
			||||||
 | 
					    if (type == "action") {
 | 
				
			||||||
 | 
					        popup.classList.add("success");
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Start animation: bring the popup into view
 | 
				
			||||||
    // Automatically hide after 10 seconds
 | 
					    // Automatically hide after 10 seconds
 | 
				
			||||||
    setTimeout(() => {
 | 
					    setTimeout(() => {
 | 
				
			||||||
        closePopUp()
 | 
					        closePopUp()
 | 
				
			||||||
    }, 10000);
 | 
					    }, 10000)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function closePopUp() {
 | 
					export function closePopUp() {
 | 
				
			||||||
    const [popup, popupContent] = getElements();
 | 
					    const [popup, popupContent] = getElements();
 | 
				
			||||||
 | 
					    if (popupContent == undefined) {
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (popupContent.textContent == "Multiple Selection Enabled" && state.multiple) {
 | 
				
			||||||
 | 
					        mutations.setMultiple(false)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    popup.style.right = '-50em'; // Slide out
 | 
					    popup.style.right = '-50em'; // Slide out
 | 
				
			||||||
    popupContent.textContent = "no content";
 | 
					    popupContent.textContent = "no content";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -22,13 +37,11 @@ export function closePopUp() {
 | 
				
			||||||
function getElements() {
 | 
					function getElements() {
 | 
				
			||||||
    const popup = document.getElementById('popup-notification');
 | 
					    const popup = document.getElementById('popup-notification');
 | 
				
			||||||
    if (!popup) {
 | 
					    if (!popup) {
 | 
				
			||||||
        console.error('Popup notification element not found');
 | 
					 | 
				
			||||||
        return [null, null];
 | 
					        return [null, null];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const popupContent = popup.querySelector('#popup-notification-content');
 | 
					    const popupContent = popup.querySelector('#popup-notification-content');
 | 
				
			||||||
    if (!popupContent) {
 | 
					    if (!popupContent) {
 | 
				
			||||||
        console.error('Popup notification content element not found');
 | 
					 | 
				
			||||||
       return [null, null];
 | 
					       return [null, null];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,3 +56,7 @@ export function showError(message) {
 | 
				
			||||||
    showPopup('error', message);
 | 
					    showPopup('error', message);
 | 
				
			||||||
    console.error(message)
 | 
					    console.error(message)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function showMultipleSelection() {
 | 
				
			||||||
 | 
					    showPopup("action","Multiple Selection Enabled");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -70,6 +70,14 @@ const routes = [
 | 
				
			||||||
        name: "Settings",
 | 
					        name: "Settings",
 | 
				
			||||||
        component: Settings,
 | 
					        component: Settings,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        path: "users/:id",
 | 
				
			||||||
 | 
					        name: "User",
 | 
				
			||||||
 | 
					        component: Settings,
 | 
				
			||||||
 | 
					        meta: {
 | 
				
			||||||
 | 
					          requiresAdmin: true,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import { state } from "./state.js";
 | 
					import { state } from "./state.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getters = {
 | 
					export const getters = {
 | 
				
			||||||
  isResizableView: () => (state.user.viewMode == "gallery" || state.user.viewMode == "normal" ) && getters.currentView() == "listingView" ,
 | 
					  isCardView: () => (state.user.viewMode == "gallery" || state.user.viewMode == "normal" ) && getters.currentView() == "listingView" ,
 | 
				
			||||||
  currentHash: () => state.route.hash.replace("#", ""),
 | 
					  currentHash: () => state.route.hash.replace("#", ""),
 | 
				
			||||||
  isMobile: () => state.isMobile,
 | 
					  isMobile: () => state.isMobile,
 | 
				
			||||||
  isLoading: () => Object.keys(state.loading).length > 0,
 | 
					  isLoading: () => Object.keys(state.loading).length > 0,
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,7 @@ export const getters = {
 | 
				
			||||||
  isFiles: () => state.route.name === "Files",
 | 
					  isFiles: () => state.route.name === "Files",
 | 
				
			||||||
  isListing: () => getters.isFiles() && state.req.isDir,
 | 
					  isListing: () => getters.isFiles() && state.req.isDir,
 | 
				
			||||||
  selectedCount: () => Array.isArray(state.selected) ? state.selected.length : 0,
 | 
					  selectedCount: () => Array.isArray(state.selected) ? state.selected.length : 0,
 | 
				
			||||||
 | 
					  getFirstSelected: () => state.req.items[state.selected[0]],
 | 
				
			||||||
  isSingleFileSelected: () => getters.selectedCount() === 1 && !state.req.items[state.selected[0]]?.isDir,
 | 
					  isSingleFileSelected: () => getters.selectedCount() === 1 && !state.req.items[state.selected[0]]?.isDir,
 | 
				
			||||||
  selectedDownloadUrl() {
 | 
					  selectedDownloadUrl() {
 | 
				
			||||||
    let selectedItem = state.selected[0]
 | 
					    let selectedItem = state.selected[0]
 | 
				
			||||||
| 
						 | 
					@ -77,7 +78,7 @@ export const getters = {
 | 
				
			||||||
    return { dirs, files };
 | 
					    return { dirs, files };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  isSidebarVisible: () => {
 | 
					  isSidebarVisible: () => {
 | 
				
			||||||
    let visible = state.showSidebar || getters.isStickySidebar()
 | 
					    let visible = (state.showSidebar || getters.isStickySidebar()) && state.user.username != "publicUser"
 | 
				
			||||||
    if (getters.currentView() == "settings") {
 | 
					    if (getters.currentView() == "settings") {
 | 
				
			||||||
      visible = !getters.isMobile();
 | 
					      visible = !getters.isMobile();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@ import { state } from "./state.js";
 | 
				
			||||||
import router from "@/router";
 | 
					import router from "@/router";
 | 
				
			||||||
import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js
 | 
					import { emitStateChanged } from './eventBus'; // Import the function from eventBus.js
 | 
				
			||||||
import { users } from "@/api";
 | 
					import { users } from "@/api";
 | 
				
			||||||
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const mutations = {
 | 
					export const mutations = {
 | 
				
			||||||
  setGallerySize: (value) => {
 | 
					  setGallerySize: (value) => {
 | 
				
			||||||
| 
						 | 
					@ -31,13 +32,13 @@ export const mutations = {
 | 
				
			||||||
    emitStateChanged();
 | 
					    emitStateChanged();
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  toggleDarkMode() {
 | 
					  toggleDarkMode() {
 | 
				
			||||||
    mutations.updateUser({ "darkMode": !state.user.darkMode });
 | 
					    mutations.updateCurrentUser({ "darkMode": !state.user.darkMode });
 | 
				
			||||||
    emitStateChanged();
 | 
					    emitStateChanged();
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  toggleSidebar() {
 | 
					  toggleSidebar() {
 | 
				
			||||||
    if (state.user.stickySidebar) {
 | 
					    if (state.user.stickySidebar) {
 | 
				
			||||||
      localStorage.setItem("stickySidebar", "false");
 | 
					      localStorage.setItem("stickySidebar", "false");
 | 
				
			||||||
      mutations.updateUser({ "stickySidebar": false }); // turn off sticky when closed
 | 
					      mutations.updateCurrentUser({ "stickySidebar": false }); // turn off sticky when closed
 | 
				
			||||||
      state.showSidebar = false;
 | 
					      state.showSidebar = false;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      state.showSidebar = !state.showSidebar;
 | 
					      state.showSidebar = !state.showSidebar;
 | 
				
			||||||
| 
						 | 
					@ -99,22 +100,18 @@ export const mutations = {
 | 
				
			||||||
    state.reload = value;
 | 
					    state.reload = value;
 | 
				
			||||||
    emitStateChanged();
 | 
					    emitStateChanged();
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  setUser: (value) => {
 | 
					  setCurrentUser: (value) => {
 | 
				
			||||||
    if (value === null) {
 | 
					    state.user = value;
 | 
				
			||||||
      state.user = null;
 | 
					    // If value is null or undefined, emit state change and exit early
 | 
				
			||||||
 | 
					    if (!value) {
 | 
				
			||||||
      emitStateChanged();
 | 
					      emitStateChanged();
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    // Ensure locale exists and is valid
 | 
				
			||||||
    let locale = value.locale;
 | 
					    if (!value.locale) {
 | 
				
			||||||
    if (locale === "") {
 | 
					      value.locale = i18n.detectLocale();  // Default to detected locale if missing
 | 
				
			||||||
      value.locale = i18n.detectLocale();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    let previousUser = state.user
 | 
					 | 
				
			||||||
    state.user = value;
 | 
					 | 
				
			||||||
    if (state.user != previousUser && state.user.username != "publicUser") {
 | 
					 | 
				
			||||||
      users.update(state.user);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    // Emit state change after setting the user and locale
 | 
				
			||||||
    emitStateChanged();
 | 
					    emitStateChanged();
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  setJWT: (value) => {
 | 
					  setJWT: (value) => {
 | 
				
			||||||
| 
						 | 
					@ -127,6 +124,11 @@ export const mutations = {
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  setMultiple: (value) => {
 | 
					  setMultiple: (value) => {
 | 
				
			||||||
    state.multiple = value;
 | 
					    state.multiple = value;
 | 
				
			||||||
 | 
					    if (value == true) {
 | 
				
			||||||
 | 
					      notify.showMultipleSelection()
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      notify.closePopUp()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    emitStateChanged();
 | 
					    emitStateChanged();
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  addSelected: (value) => {
 | 
					  addSelected: (value) => {
 | 
				
			||||||
| 
						 | 
					@ -144,22 +146,38 @@ export const mutations = {
 | 
				
			||||||
    mutations.setMultiple(false);
 | 
					    mutations.setMultiple(false);
 | 
				
			||||||
    emitStateChanged();
 | 
					    emitStateChanged();
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  updateUser: (value) => {
 | 
					  updateCurrentUser: (value) => {
 | 
				
			||||||
    if (typeof value !== "object") return;
 | 
					    // Ensure the input is a valid object
 | 
				
			||||||
    if (state.user === null) {
 | 
					    if (typeof value !== "object" || value === null) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Initialize state.user if it's null
 | 
				
			||||||
 | 
					    if (!state.user) {
 | 
				
			||||||
      state.user = {};
 | 
					      state.user = {};
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    let previousUser = state.user;
 | 
					
 | 
				
			||||||
 | 
					    // Store previous state for comparison
 | 
				
			||||||
 | 
					    const previousUser = { ...state.user };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Merge the new values into the current user state
 | 
				
			||||||
    state.user = { ...state.user, ...value };
 | 
					    state.user = { ...state.user, ...value };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Handle locale change
 | 
				
			||||||
    if (state.user.locale !== previousUser.locale) {
 | 
					    if (state.user.locale !== previousUser.locale) {
 | 
				
			||||||
      state.user.locale = i18n.detectLocale();
 | 
					      state.user.locale = i18n.detectLocale();
 | 
				
			||||||
      i18n.setLocale(state.user.locale);
 | 
					      i18n.setLocale(state.user.locale);
 | 
				
			||||||
      i18n.default.locale = state.user.locale;
 | 
					      i18n.default.locale = state.user.locale;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Update localStorage if stickySidebar exists
 | 
				
			||||||
 | 
					    if ('stickySidebar' in state.user) {
 | 
				
			||||||
      localStorage.setItem("stickySidebar", state.user.stickySidebar);
 | 
					      localStorage.setItem("stickySidebar", state.user.stickySidebar);
 | 
				
			||||||
    if (state.user != previousUser) {
 | 
					 | 
				
			||||||
      users.update(state.user);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    // Update users if there's any change in state.user
 | 
				
			||||||
 | 
					    if (JSON.stringify(state.user) !== JSON.stringify(previousUser)) {
 | 
				
			||||||
 | 
					      users.update(state.user,Object.keys(value));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Emit state change event
 | 
				
			||||||
    emitStateChanged();
 | 
					    emitStateChanged();
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  updateRequest: (value) => {
 | 
					  updateRequest: (value) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,7 @@ export function parseToken(token) {
 | 
				
			||||||
  localStorage.setItem("jwt", token);
 | 
					  localStorage.setItem("jwt", token);
 | 
				
			||||||
  mutations.setJWT(token);
 | 
					  mutations.setJWT(token);
 | 
				
			||||||
  mutations.setSession(generateRandomCode(8));
 | 
					  mutations.setSession(generateRandomCode(8));
 | 
				
			||||||
  mutations.setUser(data.user);
 | 
					  mutations.setCurrentUser(data.user);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function validateLogin() {
 | 
					export async function validateLogin() {
 | 
				
			||||||
| 
						 | 
					@ -89,7 +89,7 @@ export async function signupLogin(username, password) {
 | 
				
			||||||
export function logout() {
 | 
					export function logout() {
 | 
				
			||||||
  document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
 | 
					  document.cookie = "auth=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
 | 
				
			||||||
  mutations.setJWT("");
 | 
					  mutations.setJWT("");
 | 
				
			||||||
  mutations.setUser(null);
 | 
					  mutations.setCurrentUser(null);
 | 
				
			||||||
  localStorage.setItem("jwt", null);
 | 
					  localStorage.setItem("jwt", null);
 | 
				
			||||||
  router.push({ path: "/login" });
 | 
					  router.push({ path: "/login" });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
const name = window.FileBrowser.Name || "File Browser";
 | 
					const name = window.FileBrowser.Name || "FileBrowser Quantum";
 | 
				
			||||||
const disableExternal = window.FileBrowser.DisableExternal;
 | 
					const disableExternal = window.FileBrowser.DisableExternal;
 | 
				
			||||||
const disableUsedPercentage = window.FileBrowser.DisableUsedPercentage;
 | 
					const disableUsedPercentage = window.FileBrowser.DisableUsedPercentage;
 | 
				
			||||||
const baseURL = window.FileBrowser.BaseURL;
 | 
					const baseURL = window.FileBrowser.BaseURL;
 | 
				
			||||||
| 
						 | 
					@ -20,9 +20,13 @@ const origin = window.location.origin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const settings = [
 | 
					const settings = [
 | 
				
			||||||
  { id: 'profile', label: 'Profile Management', component: 'ProfileSettings' },
 | 
					  { id: 'profile', label: 'Profile Management', component: 'ProfileSettings' },
 | 
				
			||||||
  { id: 'shares', label: 'Share Management', component: 'SharesSettings' },
 | 
					  {
 | 
				
			||||||
  { id: 'global', label: 'Global', component: 'GlobalSettings' },
 | 
					    id: 'shares', label: 'Share Management', component: 'SharesSettings', perm: {
 | 
				
			||||||
  { id: 'user-defaults', label: 'User Defaults', component: 'UserDefaultSettings' },
 | 
					      share: true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  { id: 'global', label: 'Global', component: 'GlobalSettings', perm: { admin: true } },
 | 
				
			||||||
 | 
					  { id: 'users', label: 'User Management', component: 'UserManagement', perm: { admin: true } },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					import { state, mutations, getters } from "@/store"
 | 
				
			||||||
 | 
					import { files as api } from "@/api";
 | 
				
			||||||
 | 
					import { notify } from "@/notify"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function download() {
 | 
				
			||||||
 | 
					    if (getters.isSingleFileSelected()) {
 | 
				
			||||||
 | 
					      api.download(null, getters.selectedDownloadUrl());
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    mutations.showHover({
 | 
				
			||||||
 | 
					      name: "download",
 | 
				
			||||||
 | 
					      confirm: (format) => {
 | 
				
			||||||
 | 
					        mutations.closeHovers();
 | 
				
			||||||
 | 
					        let files = [];
 | 
				
			||||||
 | 
					        if (state.selected.length > 0) {
 | 
				
			||||||
 | 
					          for (let i of state.selected) {
 | 
				
			||||||
 | 
					            files.push(state.req.items[i].url);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          files.push(state.route.path);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					          api.download(format, ...files);
 | 
				
			||||||
 | 
					          notify.showSuccess("download started");
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          notify.showError("error downloading", e);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -101,7 +101,7 @@ export function scanFiles(dt) {
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function handleFiles(files, base, overwrite = false) {
 | 
					export async function handleFiles(files, base, overwrite = false) {
 | 
				
			||||||
  for (const file of files) {
 | 
					  for (const file of files) {
 | 
				
			||||||
    const id = state.upload.id;
 | 
					    const id = state.upload.id;
 | 
				
			||||||
    let path = base;
 | 
					    let path = base;
 | 
				
			||||||
| 
						 | 
					@ -123,8 +123,7 @@ export function handleFiles(files, base, overwrite = false) {
 | 
				
			||||||
      overwrite,
 | 
					      overwrite,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Upload the file using your API
 | 
					    await api.post(item.path, item.file, item.overwrite, (event) => {
 | 
				
			||||||
    api.post(item.path, item.file, item.overwrite, (event) => {
 | 
					 | 
				
			||||||
      console.log(`Upload progress: ${Math.round((event.loaded / event.total) * 100)}%`);
 | 
					      console.log(`Upload progress: ${Math.round((event.loaded / event.total) * 100)}%`);
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .then(response => {
 | 
					    .then(response => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,8 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					import { state } from "@/store";
 | 
				
			||||||
 | 
					import { router } from "@/router";
 | 
				
			||||||
const errors = {
 | 
					const errors = {
 | 
				
			||||||
  0: {
 | 
					  0: {
 | 
				
			||||||
    icon: "cloud_off",
 | 
					    icon: "cloud_off",
 | 
				
			||||||
| 
						 | 
					@ -30,13 +31,26 @@ const errors = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "errors",
 | 
					  name: "errors",
 | 
				
			||||||
  components: {
 | 
					  components: {},
 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  props: ["errorCode", "showHeader"],
 | 
					  props: ["errorCode", "showHeader"],
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    info() {
 | 
					    info() {
 | 
				
			||||||
      return errors[this.errorCode] ? errors[this.errorCode] : errors[500];
 | 
					      return errors[this.errorCode] ? errors[this.errorCode] : errors[500];
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  mounted() {
 | 
				
			||||||
 | 
					    window.addEventListener("keydown", this.keyEvent);
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    keyEvent(event) {
 | 
				
			||||||
 | 
					      const { key } = event;
 | 
				
			||||||
 | 
					      if (key == "Backspace") {
 | 
				
			||||||
 | 
					        // go back
 | 
				
			||||||
 | 
					        let currentPath = state.route.path.replace(/\/+$/, "");
 | 
				
			||||||
 | 
					        let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
 | 
				
			||||||
 | 
					        router.push({ path: newPath });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,7 @@
 | 
				
			||||||
    <i v-on:click="closePopUp" class="material-icons">close</i>
 | 
					    <i v-on:click="closePopUp" class="material-icons">close</i>
 | 
				
			||||||
    <div id="popup-notification-content">no info</div>
 | 
					    <div id="popup-notification-content">no info</div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <fileSelection> </fileSelection>
 | 
					  <ContextMenu></ContextMenu>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import editorBar from "./bars/EditorBar.vue";
 | 
					import editorBar from "./bars/EditorBar.vue";
 | 
				
			||||||
| 
						 | 
					@ -33,16 +33,16 @@ import listingBar from "./bars/ListingBar.vue";
 | 
				
			||||||
import Prompts from "@/components/prompts/Prompts.vue";
 | 
					import Prompts from "@/components/prompts/Prompts.vue";
 | 
				
			||||||
import Sidebar from "@/components/sidebar/Sidebar.vue";
 | 
					import Sidebar from "@/components/sidebar/Sidebar.vue";
 | 
				
			||||||
import Search from "@/components/Search.vue";
 | 
					import Search from "@/components/Search.vue";
 | 
				
			||||||
import fileSelection from "@/components/FileSelection.vue";
 | 
					import ContextMenu from "@/components/ContextMenu.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { closePopUp } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
import { enableExec } from "@/utils/constants";
 | 
					import { enableExec } from "@/utils/constants";
 | 
				
			||||||
import { state, getters, mutations } from "@/store";
 | 
					import { state, getters, mutations } from "@/store";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "layout",
 | 
					  name: "layout",
 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
    fileSelection,
 | 
					    ContextMenu,
 | 
				
			||||||
    Search,
 | 
					    Search,
 | 
				
			||||||
    defaultBar,
 | 
					    defaultBar,
 | 
				
			||||||
    editorBar,
 | 
					    editorBar,
 | 
				
			||||||
| 
						 | 
					@ -72,7 +72,7 @@ export default {
 | 
				
			||||||
      return getters.isSidebarVisible() && getters.isStickySidebar();
 | 
					      return getters.isSidebarVisible() && getters.isStickySidebar();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    closePopUp() {
 | 
					    closePopUp() {
 | 
				
			||||||
      return closePopUp;
 | 
					      return notify.closePopUp;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    progress() {
 | 
					    progress() {
 | 
				
			||||||
      return getters.progress(); // Access getter directly from the store
 | 
					      return getters.progress(); // Access getter directly from the store
 | 
				
			||||||
| 
						 | 
					@ -130,6 +130,9 @@ export default {
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style>
 | 
					<style>
 | 
				
			||||||
 | 
					#layout-container {
 | 
				
			||||||
 | 
					  padding-bottom: 30% !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
main {
 | 
					main {
 | 
				
			||||||
  -ms-overflow-style: none; /* Internet Explorer 10+ */
 | 
					  -ms-overflow-style: none; /* Internet Explorer 10+ */
 | 
				
			||||||
  scrollbar-width: none; /* Firefox */
 | 
					  scrollbar-width: none; /* Firefox */
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div id="login" :class="{ recaptcha: recaptcha, 'dark-mode': isDarkMode }">
 | 
					  <div id="login" :class="{ recaptcha: recaptcha, 'dark-mode': isDarkMode }">
 | 
				
			||||||
    <form @submit="submit">
 | 
					    <form @submit="submit">
 | 
				
			||||||
      <img :src="logoURL" alt="File Browser" />
 | 
					      <img :src="logoURL" alt="FileBrowser Quantum" />
 | 
				
			||||||
      <h1>{{ name }}</h1>
 | 
					      <h1>{{ name }}</h1>
 | 
				
			||||||
      <div v-if="error !== ''" class="wrong">{{ error }}</div>
 | 
					      <div v-if="error !== ''" class="wrong">{{ error }}</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,6 +42,7 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					import router from "@/router";
 | 
				
			||||||
import { state } from "@/store";
 | 
					import { state } from "@/store";
 | 
				
			||||||
import { signupLogin, login } from "@/utils/auth";
 | 
					import { signupLogin, login } from "@/utils/auth";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -113,7 +114,7 @@ export default {
 | 
				
			||||||
          await signupLogin(this.username, this.password);
 | 
					          await signupLogin(this.username, this.password);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        await login(this.username, this.password, captcha);
 | 
					        await login(this.username, this.password, captcha);
 | 
				
			||||||
        this.$router.push({ path: redirect });
 | 
					        router.push({ path: redirect });
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        console.error(e);
 | 
					        console.error(e);
 | 
				
			||||||
        if (e.message == 409) {
 | 
					        if (e.message == 409) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="dashboard">
 | 
					  <div class="dashboard" style="padding-bottom: 30vh">
 | 
				
			||||||
    <div class="settings-views">
 | 
					    <div v-if="isRootSettings" class="settings-views">
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        v-for="setting in settings"
 | 
					        v-for="setting in settings"
 | 
				
			||||||
        :key="setting.id + '-main'"
 | 
					        :key="setting.id + '-main'"
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,12 @@
 | 
				
			||||||
        @click="!active(setting.id + '-main') && setView(setting.id + '-main')"
 | 
					        @click="!active(setting.id + '-main') && setView(setting.id + '-main')"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <!-- Dynamically render the component based on the setting -->
 | 
					        <!-- Dynamically render the component based on the setting -->
 | 
				
			||||||
        <component :is="setting.component"></component>
 | 
					        <component v-if="shouldShow(setting)" :is="setting.component"></component>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div v-else class="settings-views">
 | 
				
			||||||
 | 
					      <div class="active">
 | 
				
			||||||
 | 
					        <UserSettings />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,17 +38,17 @@
 | 
				
			||||||
import { state, getters, mutations } from "@/store";
 | 
					import { state, getters, mutations } from "@/store";
 | 
				
			||||||
import { settings } from "@/utils/constants";
 | 
					import { settings } from "@/utils/constants";
 | 
				
			||||||
import GlobalSettings from "@/views/settings/Global.vue";
 | 
					import GlobalSettings from "@/views/settings/Global.vue";
 | 
				
			||||||
import UserDefaultSettings from "@/views/settings/UserDefaults.vue";
 | 
					 | 
				
			||||||
import UserColumnSettings from "@/views/settings/UserColumn.vue";
 | 
					 | 
				
			||||||
import ProfileSettings from "@/views/settings/Profile.vue";
 | 
					import ProfileSettings from "@/views/settings/Profile.vue";
 | 
				
			||||||
import SharesSettings from "@/views/settings/Shares.vue";
 | 
					import SharesSettings from "@/views/settings/Shares.vue";
 | 
				
			||||||
 | 
					import UserManagement from "@/views/settings/Users.vue";
 | 
				
			||||||
 | 
					import UserSettings from "@/views/settings/User.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "settings",
 | 
					  name: "settings",
 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
 | 
					    UserManagement,
 | 
				
			||||||
 | 
					    UserSettings,
 | 
				
			||||||
    GlobalSettings,
 | 
					    GlobalSettings,
 | 
				
			||||||
    UserDefaultSettings,
 | 
					 | 
				
			||||||
    UserColumnSettings,
 | 
					 | 
				
			||||||
    ProfileSettings,
 | 
					    ProfileSettings,
 | 
				
			||||||
    SharesSettings,
 | 
					    SharesSettings,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -53,6 +58,12 @@ export default {
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
 | 
					    isRootSettings() {
 | 
				
			||||||
 | 
					      return state.route.path == "/settings";
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    newUserPage() {
 | 
				
			||||||
 | 
					      return state.route.path == "/settings/users/new";
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    loading() {
 | 
					    loading() {
 | 
				
			||||||
      return getters.isLoading();
 | 
					      return getters.isLoading();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -67,6 +78,14 @@ export default {
 | 
				
			||||||
    mutations.setActiveSettingsView(getters.currentHash());
 | 
					    mutations.setActiveSettingsView(getters.currentHash());
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
 | 
					    shouldShow(setting) {
 | 
				
			||||||
 | 
					      if (state.isMobile) {
 | 
				
			||||||
 | 
					        const perm = setting?.perm || {};
 | 
				
			||||||
 | 
					        // Check if all keys in setting.perm exist in state.user.perm and have truthy values
 | 
				
			||||||
 | 
					        return Object.keys(perm).every((key) => state.user.perm[key]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return this.active(setting.id + "-main");
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    active(id) {
 | 
					    active(id) {
 | 
				
			||||||
      return state.activeSettingsView === id;
 | 
					      return state.activeSettingsView === id;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -88,6 +107,7 @@ export default {
 | 
				
			||||||
.settings-views {
 | 
					.settings-views {
 | 
				
			||||||
  max-width: 1000px;
 | 
					  max-width: 1000px;
 | 
				
			||||||
  padding-bottom: 35vh;
 | 
					  padding-bottom: 35vh;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.settings-views > .active > .card {
 | 
					.settings-views > .active > .card {
 | 
				
			||||||
  border-style: solid;
 | 
					  border-style: solid;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -119,20 +119,6 @@
 | 
				
			||||||
              readOnly
 | 
					              readOnly
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
            </item>
 | 
					            </item>
 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div :class="{ active: multiple }" id="multiple-selection">
 | 
					 | 
				
			||||||
              <p>{{ $t("files.multipleSelectionEnabled") }}</p>
 | 
					 | 
				
			||||||
              <div
 | 
					 | 
				
			||||||
                @click="setMultipleFalse"
 | 
					 | 
				
			||||||
                tabindex="0"
 | 
					 | 
				
			||||||
                role="button"
 | 
					 | 
				
			||||||
                :title="$t('files.clear')"
 | 
					 | 
				
			||||||
                :aria-label="$t('files.clear')"
 | 
					 | 
				
			||||||
                class="action"
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
                <i class="material-icons">clear</i>
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
| 
						 | 
					@ -149,7 +135,7 @@
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { showSuccess } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
import { getHumanReadableFilesize } from "@/utils/filesizes";
 | 
					import { getHumanReadableFilesize } from "@/utils/filesizes";
 | 
				
			||||||
import { pub as api } from "@/api";
 | 
					import { pub as api } from "@/api";
 | 
				
			||||||
import { fromNow } from "@/utils/moment";
 | 
					import { fromNow } from "@/utils/moment";
 | 
				
			||||||
| 
						 | 
					@ -184,15 +170,14 @@ export default {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  created() {
 | 
					  created() {
 | 
				
			||||||
    const hash = state.route.params.path.at(-1);
 | 
					    this.hash = state.route.params.path.at(0);
 | 
				
			||||||
    this.hash = hash;
 | 
					 | 
				
			||||||
    this.fetchData();
 | 
					    this.fetchData();
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {
 | 
					  mounted() {
 | 
				
			||||||
    window.addEventListener("keydown", this.keyEvent);
 | 
					    window.addEventListener("keydown", this.keyEvent);
 | 
				
			||||||
    this.clip = new Clipboard(".copy-clipboard");
 | 
					    this.clip = new Clipboard(".copy-clipboard");
 | 
				
			||||||
    this.clip.on("success", () => {
 | 
					    this.clip.on("success", () => {
 | 
				
			||||||
      showSuccess(this.$t("success.linkCopied"));
 | 
					      notify.showSuccess(this.$t("success.linkCopied"));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  beforeUnmount() {
 | 
					  beforeUnmount() {
 | 
				
			||||||
| 
						 | 
					@ -226,10 +211,19 @@ export default {
 | 
				
			||||||
      return "insert_drive_file";
 | 
					      return "insert_drive_file";
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    link() {
 | 
					    link() {
 | 
				
			||||||
      return api.getDownloadURL(state.req);
 | 
					      return api.getDownloadURL({
 | 
				
			||||||
 | 
					        hash: this.hash,
 | 
				
			||||||
 | 
					        path: window.location.pathname,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    inlineLink() {
 | 
					    inlineLink() {
 | 
				
			||||||
      return api.getDownloadURL(state.req, true);
 | 
					      return api.getDownloadURL(
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          hash: this.hash,
 | 
				
			||||||
 | 
					          path: window.location.pathname,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        true
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    humanSize() {
 | 
					    humanSize() {
 | 
				
			||||||
      if (state.req.isDir) {
 | 
					      if (state.req.isDir) {
 | 
				
			||||||
| 
						 | 
					@ -262,7 +256,7 @@ export default {
 | 
				
			||||||
      // Reset view information.
 | 
					      // Reset view information.
 | 
				
			||||||
      if (!getters.isLoggedIn()) {
 | 
					      if (!getters.isLoggedIn()) {
 | 
				
			||||||
        let userData = await api.getPublicUser();
 | 
					        let userData = await api.getPublicUser();
 | 
				
			||||||
        mutations.setUser(userData);
 | 
					        mutations.setCurrentUser(userData);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      mutations.setReload(false);
 | 
					      mutations.setReload(false);
 | 
				
			||||||
      mutations.resetSelected();
 | 
					      mutations.resetSelected();
 | 
				
			||||||
| 
						 | 
					@ -324,3 +318,8 @@ export default {
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					.share {
 | 
				
			||||||
 | 
					  padding-bottom: 35vh;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,11 +15,11 @@
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import url from "@/utils/url"
 | 
					import url from "@/utils/url";
 | 
				
			||||||
import router from "@/router";
 | 
					import router from "@/router";
 | 
				
			||||||
import { state, mutations, getters } from "@/store";
 | 
					import { state, mutations, getters } from "@/store";
 | 
				
			||||||
import { files as api } from "@/api";
 | 
					import { files as api } from "@/api";
 | 
				
			||||||
import Action from "@/components/header/Action.vue";
 | 
					import Action from "@/components/Action.vue";
 | 
				
			||||||
import css from "@/utils/css";
 | 
					import css from "@/utils/css";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
| 
						 | 
					@ -296,7 +296,8 @@ export default {
 | 
				
			||||||
      mutations.closeHovers();
 | 
					      mutations.closeHovers();
 | 
				
			||||||
      const currentIndex = this.viewModes.indexOf(state.user.viewMode);
 | 
					      const currentIndex = this.viewModes.indexOf(state.user.viewMode);
 | 
				
			||||||
      const nextIndex = (currentIndex + 1) % this.viewModes.length;
 | 
					      const nextIndex = (currentIndex + 1) % this.viewModes.length;
 | 
				
			||||||
      mutations.updateUser({ viewMode: this.viewModes[nextIndex] });
 | 
					      const newView = this.viewModes[nextIndex];
 | 
				
			||||||
 | 
					      mutations.updateCurrentUser({ "viewMode": newView });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    preventDefault(event) {
 | 
					    preventDefault(event) {
 | 
				
			||||||
      // Wrapper around prevent default.
 | 
					      // Wrapper around prevent default.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,9 +30,9 @@ import { state, mutations } from "@/store";
 | 
				
			||||||
import { eventBus } from "@/store/eventBus";
 | 
					import { eventBus } from "@/store/eventBus";
 | 
				
			||||||
import buttons from "@/utils/buttons";
 | 
					import buttons from "@/utils/buttons";
 | 
				
			||||||
import url from "@/utils/url";
 | 
					import url from "@/utils/url";
 | 
				
			||||||
import { showError, showSuccess } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Action from "@/components/header/Action.vue";
 | 
					import Action from "@/components/Action.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "editorBar",
 | 
					  name: "editorBar",
 | 
				
			||||||
| 
						 | 
					@ -108,10 +108,10 @@ export default {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        eventBus.emit("handleEditorValueRequest", "data");
 | 
					        eventBus.emit("handleEditorValueRequest", "data");
 | 
				
			||||||
        buttons.success(button);
 | 
					        buttons.success(button);
 | 
				
			||||||
        showSuccess("File Saved!");
 | 
					        notify.showSuccess("File Saved!");
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        buttons.done(button);
 | 
					        buttons.done(button);
 | 
				
			||||||
        showError("Error saving file: ", e);
 | 
					        notify.showError("Error saving file: ", e);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    close() {
 | 
					    close() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,8 +26,7 @@
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { state, mutations, getters } from "@/store";
 | 
					import { state, mutations, getters } from "@/store";
 | 
				
			||||||
import Action from "@/components/header/Action.vue";
 | 
					import Action from "@/components/Action.vue";
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "listingView",
 | 
					  name: "listingView",
 | 
				
			||||||
| 
						 | 
					@ -79,11 +78,7 @@ export default {
 | 
				
			||||||
      const currentIndex = this.viewModes.indexOf(state.user.viewMode);
 | 
					      const currentIndex = this.viewModes.indexOf(state.user.viewMode);
 | 
				
			||||||
      const nextIndex = (currentIndex + 1) % this.viewModes.length;
 | 
					      const nextIndex = (currentIndex + 1) % this.viewModes.length;
 | 
				
			||||||
      const newView = this.viewModes[nextIndex];
 | 
					      const newView = this.viewModes[nextIndex];
 | 
				
			||||||
      try {
 | 
					      mutations.updateCurrentUser({ "viewMode": newView });
 | 
				
			||||||
        mutations.updateUser({ viewMode: newView });
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        showError(e);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,9 +5,9 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					import { router } from "@/router";
 | 
				
			||||||
import { eventBus } from "@/store/eventBus";
 | 
					import { eventBus } from "@/store/eventBus";
 | 
				
			||||||
import { state, mutations, getters } from "@/store";
 | 
					import { state, getters } from "@/store";
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					 | 
				
			||||||
import { files as api } from "@/api";
 | 
					import { files as api } from "@/api";
 | 
				
			||||||
import url from "@/utils/url";
 | 
					import url from "@/utils/url";
 | 
				
			||||||
import ace from "ace-builds/src-min-noconflict/ace.js";
 | 
					import ace from "ace-builds/src-min-noconflict/ace.js";
 | 
				
			||||||
| 
						 | 
					@ -62,7 +62,8 @@ export default {
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted: function () {
 | 
					  mounted: function () {
 | 
				
			||||||
    // this is empty content string "empty-file-x6OlSil" which is used to represent empty text file
 | 
					    // this is empty content string "empty-file-x6OlSil" which is used to represent empty text file
 | 
				
			||||||
    const fileContent = state.req.content == "empty-file-x6OlSil" ? "" : state.req.content || "";
 | 
					    const fileContent =
 | 
				
			||||||
 | 
					      state.req.content == "empty-file-x6OlSil" ? "" : state.req.content || "";
 | 
				
			||||||
    this.editor = ace.edit("editor", {
 | 
					    this.editor = ace.edit("editor", {
 | 
				
			||||||
      value: fileContent,
 | 
					      value: fileContent,
 | 
				
			||||||
      showPrintMargin: false,
 | 
					      showPrintMargin: false,
 | 
				
			||||||
| 
						 | 
					@ -79,31 +80,36 @@ export default {
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    handleEditorValueRequest() {
 | 
					    handleEditorValueRequest() {
 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
      api.put(state.route.path, this.editor.getValue());
 | 
					      api.put(state.route.path, this.editor.getValue());
 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        showError(e);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    back() {
 | 
					    back() {
 | 
				
			||||||
      let uri = url.removeLastDir(state.route.path) + "/";
 | 
					      let uri = url.removeLastDir(state.route.path) + "/";
 | 
				
			||||||
      this.$router.push({ path: uri });
 | 
					      this.$router.push({ path: uri });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    keyEvent(event) {
 | 
					    keyEvent(event) {
 | 
				
			||||||
      if (!event.ctrlKey && !event.metaKey) {
 | 
					      const { key, ctrlKey, metaKey } = event;
 | 
				
			||||||
 | 
					      if (getters.currentPromptName() != null) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      if (key == "Backspace") {
 | 
				
			||||||
      if (String.fromCharCode(event.which).toLowerCase() !== "s") {
 | 
					        // go back
 | 
				
			||||||
 | 
					        let currentPath = state.route.path.replace(/\/+$/, "");
 | 
				
			||||||
 | 
					        let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
 | 
				
			||||||
 | 
					        router.push({ path: newPath });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!ctrlKey && !metaKey) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      switch (key.toLowerCase()) {
 | 
				
			||||||
 | 
					        case "s":
 | 
				
			||||||
          event.preventDefault();
 | 
					          event.preventDefault();
 | 
				
			||||||
          this.save();
 | 
					          this.save();
 | 
				
			||||||
    },
 | 
					          break;
 | 
				
			||||||
    close() {
 | 
					
 | 
				
			||||||
      mutations.replaceRequest({});
 | 
					        default:
 | 
				
			||||||
      let uri = url.removeLastDir(state.route.path) + "/";
 | 
					          // No action for other keys
 | 
				
			||||||
      this.$router.push({ path: uri });
 | 
					          return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div style="padding-bottom: 5em">
 | 
					  <div style="padding-bottom: 35vh">
 | 
				
			||||||
    <div v-if="loading">
 | 
					    <div v-if="loading">
 | 
				
			||||||
      <h2 class="message delayed">
 | 
					      <h2 class="message delayed">
 | 
				
			||||||
        <div class="spinner">
 | 
					        <div class="spinner">
 | 
				
			||||||
| 
						 | 
					@ -100,8 +100,7 @@
 | 
				
			||||||
            v-bind:type="item.type"
 | 
					            v-bind:type="item.type"
 | 
				
			||||||
            v-bind:size="item.size"
 | 
					            v-bind:size="item.size"
 | 
				
			||||||
            v-bind:path="item.path"
 | 
					            v-bind:path="item.path"
 | 
				
			||||||
          >
 | 
					          />
 | 
				
			||||||
          </item>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div v-if="numFiles > 0">
 | 
					        <div v-if="numFiles > 0">
 | 
				
			||||||
          <div class="header-items">
 | 
					          <div class="header-items">
 | 
				
			||||||
| 
						 | 
					@ -120,8 +119,7 @@
 | 
				
			||||||
            v-bind:type="item.type"
 | 
					            v-bind:type="item.type"
 | 
				
			||||||
            v-bind:size="item.size"
 | 
					            v-bind:size="item.size"
 | 
				
			||||||
            v-bind:path="item.path"
 | 
					            v-bind:path="item.path"
 | 
				
			||||||
          >
 | 
					          />
 | 
				
			||||||
          </item>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
| 
						 | 
					@ -129,7 +127,7 @@
 | 
				
			||||||
          type="file"
 | 
					          type="file"
 | 
				
			||||||
          id="upload-input"
 | 
					          id="upload-input"
 | 
				
			||||||
          @change="uploadInput($event)"
 | 
					          @change="uploadInput($event)"
 | 
				
			||||||
          getMultiple
 | 
					          multiple
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <input
 | 
					        <input
 | 
				
			||||||
          style="display: none"
 | 
					          style="display: none"
 | 
				
			||||||
| 
						 | 
					@ -137,34 +135,21 @@
 | 
				
			||||||
          id="upload-folder-input"
 | 
					          id="upload-folder-input"
 | 
				
			||||||
          @change="uploadInput($event)"
 | 
					          @change="uploadInput($event)"
 | 
				
			||||||
          webkitdirectory
 | 
					          webkitdirectory
 | 
				
			||||||
          getMultiple
 | 
					          multiple
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div :class="{ active: getMultiple }" id="multiple-selection">
 | 
					 | 
				
			||||||
          <p>{{ $t("files.multipleSelectionEnabled") }}</p>
 | 
					 | 
				
			||||||
          <div
 | 
					 | 
				
			||||||
            @click="this.setMultiple(false)"
 | 
					 | 
				
			||||||
            tabindex="0"
 | 
					 | 
				
			||||||
            role="button"
 | 
					 | 
				
			||||||
            :title="$t('files.clear')"
 | 
					 | 
				
			||||||
            :aria-label="$t('files.clear')"
 | 
					 | 
				
			||||||
            class="action"
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <i class="material-icons">clear</i>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
 | 
					import download from "@/utils/download";
 | 
				
			||||||
import { files as api } from "@/api";
 | 
					import { files as api } from "@/api";
 | 
				
			||||||
 | 
					import { router } from "@/router";
 | 
				
			||||||
import * as upload from "@/utils/upload";
 | 
					import * as upload from "@/utils/upload";
 | 
				
			||||||
import css from "@/utils/css";
 | 
					import css from "@/utils/css";
 | 
				
			||||||
import throttle from "@/utils/throttle";
 | 
					import throttle from "@/utils/throttle";
 | 
				
			||||||
import { state, mutations, getters } from "@/store";
 | 
					import { state, mutations, getters } from "@/store";
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import Item from "@/components/files/ListingItem.vue";
 | 
					import Item from "@/components/files/ListingItem.vue";
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
| 
						 | 
					@ -178,15 +163,39 @@ export default {
 | 
				
			||||||
      columnWidth: 250 + state.user.gallerySize * 50,
 | 
					      columnWidth: 250 + state.user.gallerySize * 50,
 | 
				
			||||||
      dragCounter: 0,
 | 
					      dragCounter: 0,
 | 
				
			||||||
      width: window.innerWidth,
 | 
					      width: window.innerWidth,
 | 
				
			||||||
 | 
					      lastSelected: {}, // Add this to track the currently focused item
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  watch: {
 | 
					  watch: {
 | 
				
			||||||
    gallerySize() {
 | 
					    gallerySize() {
 | 
				
			||||||
      this.columnWidth = 250 + state.user.gallerySize * 50; // Update columnWidth based on new gallery size\
 | 
					      this.columnWidth = 250 + state.user.gallerySize * 50;
 | 
				
			||||||
      this.colunmsResize();
 | 
					      this.colunmsResize();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
 | 
					    lastFolderIndex() {
 | 
				
			||||||
 | 
					      const allItems = [...this.items.dirs, ...this.items.files];
 | 
				
			||||||
 | 
					      for (let i = 0; i < allItems.length; i++) {
 | 
				
			||||||
 | 
					        if (!allItems[i].isDir) {
 | 
				
			||||||
 | 
					          return i - 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (allItems.length > 0) {
 | 
				
			||||||
 | 
					        return allItems.length;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return null; // Return null if there are no files
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    numColumns() {
 | 
				
			||||||
 | 
					      if (!getters.isCardView()) {
 | 
				
			||||||
 | 
					        return 1;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      let columns = Math.floor(
 | 
				
			||||||
 | 
					        document.querySelector("main").offsetWidth / this.columnWidth
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      if (columns === 0) columns = 1;
 | 
				
			||||||
 | 
					      return columns;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    // Create a computed property that references the Vuex state
 | 
					    // Create a computed property that references the Vuex state
 | 
				
			||||||
    gallerySize() {
 | 
					    gallerySize() {
 | 
				
			||||||
      return state.user.gallerySize;
 | 
					      return state.user.gallerySize;
 | 
				
			||||||
| 
						 | 
					@ -270,6 +279,7 @@ export default {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {
 | 
					  mounted() {
 | 
				
			||||||
 | 
					    this.lastSelected = state.selected;
 | 
				
			||||||
    // Check the columns size for the first time.
 | 
					    // Check the columns size for the first time.
 | 
				
			||||||
    this.colunmsResize();
 | 
					    this.colunmsResize();
 | 
				
			||||||
    // Add the needed event listeners to the window and document.
 | 
					    // Add the needed event listeners to the window and document.
 | 
				
			||||||
| 
						 | 
					@ -278,66 +288,238 @@ export default {
 | 
				
			||||||
    window.addEventListener("resize", this.windowsResize);
 | 
					    window.addEventListener("resize", this.windowsResize);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!state.user.perm?.create) return;
 | 
					    if (!state.user.perm?.create) return;
 | 
				
			||||||
    document.addEventListener("dragover", this.preventDefault);
 | 
					    this.$el.addEventListener("dragover", this.preventDefault);
 | 
				
			||||||
    document.addEventListener("dragenter", this.dragEnter);
 | 
					    this.$el.addEventListener("dragenter", this.dragEnter);
 | 
				
			||||||
    document.addEventListener("dragleave", this.dragLeave);
 | 
					    this.$el.addEventListener("dragleave", this.dragLeave);
 | 
				
			||||||
    document.addEventListener("drop", this.drop);
 | 
					    this.$el.addEventListener("drop", this.drop);
 | 
				
			||||||
 | 
					    this.$el.addEventListener("contextmenu", this.openContext);
 | 
				
			||||||
 | 
					    this.$el.addEventListener("click", this.clickClear);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  beforeUnmount() {
 | 
					  beforeUnmount() {
 | 
				
			||||||
    // Remove event listeners before destroying this page.
 | 
					    // Remove event listeners before destroying this page.
 | 
				
			||||||
    window.removeEventListener("keydown", this.keyEvent);
 | 
					    window.removeEventListener("keydown", this.keyEvent);
 | 
				
			||||||
    window.removeEventListener("scroll", this.scrollEvent);
 | 
					    window.removeEventListener("scroll", this.scrollEvent);
 | 
				
			||||||
    window.removeEventListener("resize", this.windowsResize);
 | 
					    window.removeEventListener("resize", this.windowsResize);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (state.user && !state.user.perm?.create) return;
 | 
					 | 
				
			||||||
    document.removeEventListener("dragover", this.preventDefault);
 | 
					 | 
				
			||||||
    document.removeEventListener("dragenter", this.dragEnter);
 | 
					 | 
				
			||||||
    document.removeEventListener("dragleave", this.dragLeave);
 | 
					 | 
				
			||||||
    document.removeEventListener("drop", this.drop);
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    base64(name) {
 | 
					    base64(name) {
 | 
				
			||||||
      return window.btoa(unescape(encodeURIComponent(name)));
 | 
					      return window.btoa(unescape(encodeURIComponent(name)));
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    keyEvent(event) {
 | 
					    // Helper method to select the first item if nothing is selected
 | 
				
			||||||
      // Esc!
 | 
					    selectFirstItem() {
 | 
				
			||||||
      if (event.keyCode === 27) {
 | 
					 | 
				
			||||||
      mutations.resetSelected();
 | 
					      mutations.resetSelected();
 | 
				
			||||||
 | 
					      const allItems = [...this.items.dirs, ...this.items.files];
 | 
				
			||||||
 | 
					      if (allItems.length > 0) {
 | 
				
			||||||
 | 
					        mutations.addSelected(allItems[0].index);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Del!
 | 
					    // Helper method to select an item by index
 | 
				
			||||||
      if (event.keyCode === 46) {
 | 
					    selectItem(index) {
 | 
				
			||||||
        if (!state.user.perm.delete || state.selected.length === 0) return;
 | 
					      mutations.resetSelected();
 | 
				
			||||||
        mutations.showHover("delete");
 | 
					      mutations.addSelected(index);
 | 
				
			||||||
      }
 | 
					    },
 | 
				
			||||||
 | 
					    // Helper method to handle selection based on arrow keys
 | 
				
			||||||
 | 
					    navigateKeboardArrows(arrowKey) {
 | 
				
			||||||
 | 
					      let selectedIndex = state.selected.length > 0 ? state.selected[0] : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // F2!
 | 
					      if (selectedIndex === null) {
 | 
				
			||||||
      if (event.keyCode === 113) {
 | 
					        // If nothing is selected, select the first item
 | 
				
			||||||
        if (!state.user.perm.rename || state.selected.length !== 1) return;
 | 
					        this.selectFirstItem();
 | 
				
			||||||
        mutations.showHover("rename");
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Ctrl is pressed
 | 
					 | 
				
			||||||
      if (!event.ctrlKey && !event.metaKey) {
 | 
					 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      let key = String.fromCharCode(event.which).toLowerCase();
 | 
					      const allItems = [...this.items.dirs, ...this.items.files]; // Combine files and directories
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      switch (key) {
 | 
					      // Find the current index of the selected item
 | 
				
			||||||
        case "f":
 | 
					      let currentIndex = allItems.findIndex((item) => item.index === selectedIndex);
 | 
				
			||||||
          event.preventDefault();
 | 
					
 | 
				
			||||||
          mutations.showHover("search");
 | 
					      // If no item is selected, select the first item
 | 
				
			||||||
 | 
					      if (currentIndex === -1) {
 | 
				
			||||||
 | 
					        // Check if there are any items to select
 | 
				
			||||||
 | 
					        if (allItems.length > 0) {
 | 
				
			||||||
 | 
					          currentIndex = 0;
 | 
				
			||||||
 | 
					          this.selectItem(allItems[currentIndex].index);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      let newSelected = null;
 | 
				
			||||||
 | 
					      const fileSelected = currentIndex > this.lastFolderIndex;
 | 
				
			||||||
 | 
					      const nextIsDir = currentIndex - this.numColumns <= this.lastFolderIndex;
 | 
				
			||||||
 | 
					      const folderSelected = currentIndex <= this.lastFolderIndex;
 | 
				
			||||||
 | 
					      const nextIsFile = currentIndex + this.numColumns > this.lastFolderIndex;
 | 
				
			||||||
 | 
					      const nextHopExists = currentIndex + this.numColumns < allItems.length;
 | 
				
			||||||
 | 
					      const thisColumnNum =
 | 
				
			||||||
 | 
					        ((currentIndex - this.lastFolderIndex - 1) % this.numColumns) + 1;
 | 
				
			||||||
 | 
					      const lastFolderColumn = (this.lastFolderIndex % this.numColumns) + 1;
 | 
				
			||||||
 | 
					      const thisColumnNum2 = (currentIndex + 1) % this.numColumns;
 | 
				
			||||||
 | 
					      let firstRowColumnPos = this.lastFolderIndex + thisColumnNum2;
 | 
				
			||||||
 | 
					      let newPos = currentIndex - lastFolderColumn;
 | 
				
			||||||
 | 
					      switch (arrowKey) {
 | 
				
			||||||
 | 
					        case "ArrowUp":
 | 
				
			||||||
 | 
					          if (currentIndex - this.numColumns < 0) {
 | 
				
			||||||
 | 
					            // do nothing
 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (!getters.isCardView) {
 | 
				
			||||||
 | 
					            newSelected = allItems[currentIndex - 1].index;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          // do normal move
 | 
				
			||||||
 | 
					          if (!(fileSelected && nextIsDir)) {
 | 
				
			||||||
 | 
					            newSelected = allItems[currentIndex - this.numColumns].index;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // complex logic to move from files to folders
 | 
				
			||||||
 | 
					          if (lastFolderColumn < thisColumnNum) {
 | 
				
			||||||
 | 
					            newPos -= this.numColumns;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          newSelected = allItems[newPos].index;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case "ArrowDown":
 | 
				
			||||||
 | 
					          if (currentIndex >= allItems.length) {
 | 
				
			||||||
 | 
					            // do nothing - last item
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (!getters.isCardView) {
 | 
				
			||||||
 | 
					            newSelected = allItems[currentIndex + 1].index;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (!nextHopExists) {
 | 
				
			||||||
 | 
					            // do nothing - next item is out of bounds
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (!(folderSelected && nextIsFile)) {
 | 
				
			||||||
 | 
					            newSelected = allItems[currentIndex + this.numColumns].index;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          // complex logic for moving from folders to files
 | 
				
			||||||
 | 
					          if (firstRowColumnPos <= this.lastFolderIndex) {
 | 
				
			||||||
 | 
					            firstRowColumnPos += this.numColumns;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          newSelected = allItems[firstRowColumnPos].index;
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case "ArrowLeft":
 | 
				
			||||||
 | 
					          if (currentIndex > 0) {
 | 
				
			||||||
 | 
					            newSelected = allItems[currentIndex - 1].index;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case "ArrowRight":
 | 
				
			||||||
 | 
					          if (currentIndex < allItems.length - 1) {
 | 
				
			||||||
 | 
					            newSelected = allItems[currentIndex + 1].index;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (newSelected != null) {
 | 
				
			||||||
 | 
					        this.selectItem(newSelected);
 | 
				
			||||||
 | 
					        setTimeout(() => {
 | 
				
			||||||
 | 
					          // Find the element with class "item" and aria-selected="true"
 | 
				
			||||||
 | 
					          const element = document.querySelector('.item[aria-selected="true"]');
 | 
				
			||||||
 | 
					          // Scroll the element into view if it exists
 | 
				
			||||||
 | 
					          if (element) {
 | 
				
			||||||
 | 
					            element.scrollIntoView({
 | 
				
			||||||
 | 
					              behavior: "smooth",
 | 
				
			||||||
 | 
					              block: "end",
 | 
				
			||||||
 | 
					              inline: "nearest",
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }, 50);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    keyEvent(event) {
 | 
				
			||||||
 | 
					      const { key, ctrlKey, metaKey, which } = event;
 | 
				
			||||||
 | 
					      // Check if the key is alphanumeric
 | 
				
			||||||
 | 
					      const isAlphanumeric = /^[a-z0-9]$/i.test(key);
 | 
				
			||||||
 | 
					      const noModifierKeys = !ctrlKey && !metaKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (isAlphanumeric && noModifierKeys) {
 | 
				
			||||||
 | 
					        this.alphanumericKeyPress(key); // Call the alphanumeric key press function
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // Handle the space bar key
 | 
				
			||||||
 | 
					      if (key === " ") {
 | 
				
			||||||
 | 
					        event.preventDefault();
 | 
				
			||||||
 | 
					        if (getters.currentPromptName() == "search") {
 | 
				
			||||||
 | 
					          mutations.closeHovers();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          mutations.showHover("search");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (getters.currentPromptName() != null) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      let currentPath = state.route.path.replace(/\/+$/, ""); // Remove trailing slashes
 | 
				
			||||||
 | 
					      let newPath = currentPath.substring(0, currentPath.lastIndexOf("/"));
 | 
				
			||||||
 | 
					      // Handle key events using a switch statement
 | 
				
			||||||
 | 
					      switch (key) {
 | 
				
			||||||
 | 
					        case "Enter":
 | 
				
			||||||
 | 
					          if (this.selectedCount === 1) {
 | 
				
			||||||
 | 
					            router.push({ path: getters.getFirstSelected().url });
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case "Backspace":
 | 
				
			||||||
 | 
					          // go back
 | 
				
			||||||
 | 
					          router.push({ path: newPath });
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case "Escape":
 | 
				
			||||||
 | 
					          mutations.resetSelected();
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case "Delete":
 | 
				
			||||||
 | 
					          if (!state.user.perm.delete || state.selected.length === 0) return;
 | 
				
			||||||
 | 
					          mutations.showHover("delete");
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case "F2":
 | 
				
			||||||
 | 
					          if (!state.user.perm.rename || state.selected.length !== 1) return;
 | 
				
			||||||
 | 
					          mutations.showHover("rename");
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        case "ArrowUp":
 | 
				
			||||||
 | 
					        case "ArrowDown":
 | 
				
			||||||
 | 
					        case "ArrowLeft":
 | 
				
			||||||
 | 
					        case "ArrowRight":
 | 
				
			||||||
 | 
					          event.preventDefault();
 | 
				
			||||||
 | 
					          this.navigateKeboardArrows(key);
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        default:
 | 
				
			||||||
 | 
					          // Handle keys with ctrl or meta keys
 | 
				
			||||||
 | 
					          if (!ctrlKey && !metaKey) return;
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const charKey = String.fromCharCode(which).toLowerCase();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      switch (charKey) {
 | 
				
			||||||
        case "c":
 | 
					        case "c":
 | 
				
			||||||
        case "x":
 | 
					        case "x":
 | 
				
			||||||
          this.copyCut(event, key);
 | 
					          this.copyCut(event, charKey);
 | 
				
			||||||
          break;
 | 
					          break;
 | 
				
			||||||
        case "v":
 | 
					        case "v":
 | 
				
			||||||
          this.paste(event);
 | 
					          this.paste(event);
 | 
				
			||||||
          break;
 | 
					          break;
 | 
				
			||||||
        case "a":
 | 
					        case "a":
 | 
				
			||||||
          event.preventDefault();
 | 
					          event.preventDefault();
 | 
				
			||||||
 | 
					          this.selectAll();
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case "s":
 | 
				
			||||||
 | 
					          event.preventDefault();
 | 
				
			||||||
 | 
					          download();
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Helper method to select all files and directories
 | 
				
			||||||
 | 
					    selectAll() {
 | 
				
			||||||
      for (let file of this.items.files) {
 | 
					      for (let file of this.items.files) {
 | 
				
			||||||
        if (state.selected.indexOf(file.index) === -1) {
 | 
					        if (state.selected.indexOf(file.index) === -1) {
 | 
				
			||||||
          mutations.addSelected(file.index);
 | 
					          mutations.addSelected(file.index);
 | 
				
			||||||
| 
						 | 
					@ -348,11 +530,50 @@ export default {
 | 
				
			||||||
          mutations.addSelected(dir.index);
 | 
					          mutations.addSelected(dir.index);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
          break;
 | 
					    },
 | 
				
			||||||
        case "s":
 | 
					    alphanumericKeyPress(key) {
 | 
				
			||||||
          event.preventDefault();
 | 
					      // Convert the key to uppercase to match the case-insensitive search
 | 
				
			||||||
          document.getElementById("download-button").click();
 | 
					      const searchLetter = key.toLowerCase();
 | 
				
			||||||
          break;
 | 
					      const currentSelected = getters.getFirstSelected();
 | 
				
			||||||
 | 
					      let currentName = null;
 | 
				
			||||||
 | 
					      let findNextWithName = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (currentSelected != undefined) {
 | 
				
			||||||
 | 
					        currentName = currentSelected.name.toLowerCase();
 | 
				
			||||||
 | 
					        if (currentName.startsWith(searchLetter)) {
 | 
				
			||||||
 | 
					          findNextWithName = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // Combine directories and files (assuming they are stored in this.items.dirs and this.items.files)
 | 
				
			||||||
 | 
					      const allItems = [...this.items.dirs, ...this.items.files];
 | 
				
			||||||
 | 
					      let foundPrevious = false;
 | 
				
			||||||
 | 
					      let firstFound = null;
 | 
				
			||||||
 | 
					      // Iterate over all items to find the first one where the name starts with the searchLetter
 | 
				
			||||||
 | 
					      for (let i = 0; i < allItems.length; i++) {
 | 
				
			||||||
 | 
					        const itemName = allItems[i].name.toLowerCase();
 | 
				
			||||||
 | 
					        if (!itemName.startsWith(searchLetter)) {
 | 
				
			||||||
 | 
					          continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (firstFound == null) {
 | 
				
			||||||
 | 
					          firstFound = allItems[i].index;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (!findNextWithName) {
 | 
				
			||||||
 | 
					          // return first you find
 | 
				
			||||||
 | 
					          this.selectItem(allItems[i].index);
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (itemName == currentName) {
 | 
				
			||||||
 | 
					          foundPrevious = true;
 | 
				
			||||||
 | 
					          continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (foundPrevious) {
 | 
				
			||||||
 | 
					          this.selectItem(allItems[i].index);
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // select the first item again
 | 
				
			||||||
 | 
					      if (firstFound != null) {
 | 
				
			||||||
 | 
					        this.selectItem(firstFound);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    preventDefault(event) {
 | 
					    preventDefault(event) {
 | 
				
			||||||
| 
						 | 
					@ -395,23 +616,17 @@ export default {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      mutations.setLoading("listing", true);
 | 
					      mutations.setLoading("listing", true);
 | 
				
			||||||
      let action = (overwrite, rename) => {
 | 
					      let action = (overwrite, rename) => {
 | 
				
			||||||
        api
 | 
					        api.copy(items, overwrite, rename).then(() => {
 | 
				
			||||||
          .copy(items, overwrite, rename)
 | 
					 | 
				
			||||||
          .then(() => {
 | 
					 | 
				
			||||||
          mutations.setLoading("listing", false);
 | 
					          mutations.setLoading("listing", false);
 | 
				
			||||||
          })
 | 
					        });
 | 
				
			||||||
          .catch(showError);
 | 
					 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (this.clipboard.key === "x") {
 | 
					      if (this.clipboard.key === "x") {
 | 
				
			||||||
        action = (overwrite, rename) => {
 | 
					        action = (overwrite, rename) => {
 | 
				
			||||||
          api
 | 
					          api.move(items, overwrite, rename).then(() => {
 | 
				
			||||||
            .move(items, overwrite, rename)
 | 
					 | 
				
			||||||
            .then(() => {
 | 
					 | 
				
			||||||
            this.clipboard = {};
 | 
					            this.clipboard = {};
 | 
				
			||||||
            mutations.setLoading("listing", false);
 | 
					            mutations.setLoading("listing", false);
 | 
				
			||||||
            })
 | 
					          });
 | 
				
			||||||
            .catch(showError);
 | 
					 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -440,12 +655,8 @@ export default {
 | 
				
			||||||
      action(false, false);
 | 
					      action(false, false);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    colunmsResize() {
 | 
					    colunmsResize() {
 | 
				
			||||||
      let columns = Math.floor(
 | 
					 | 
				
			||||||
        document.querySelector("main").offsetWidth / this.columnWidth
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      let items = css(["#listingView .item", "#listingView .item"]);
 | 
					      let items = css(["#listingView .item", "#listingView .item"]);
 | 
				
			||||||
      if (columns === 0) columns = 1;
 | 
					      items.style.width = `calc(${100 / this.numColumns}% - 1em)`;
 | 
				
			||||||
      items.style.width = `calc(${100 / columns}% - 1em)`;
 | 
					 | 
				
			||||||
      if (state.user.viewMode == "gallery") {
 | 
					      if (state.user.viewMode == "gallery") {
 | 
				
			||||||
        items.style.height = `${this.columnWidth / 20}em`;
 | 
					        items.style.height = `${this.columnWidth / 20}em`;
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
| 
						 | 
					@ -483,34 +694,44 @@ export default {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      let files = await upload.scanFiles(dt);
 | 
					      let files = await upload.scanFiles(dt);
 | 
				
			||||||
 | 
					      const folderUpload = !!files[0].webkitRelativePath;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const uploadFiles = [];
 | 
				
			||||||
 | 
					      for (let i = 0; i < files.length; i++) {
 | 
				
			||||||
 | 
					        const file = files[i];
 | 
				
			||||||
 | 
					        const fullPath = folderUpload ? file.webkitRelativePath : undefined;
 | 
				
			||||||
 | 
					        uploadFiles.push({
 | 
				
			||||||
 | 
					          file, // File object directly
 | 
				
			||||||
 | 
					          name: file.name,
 | 
				
			||||||
 | 
					          size: file.size,
 | 
				
			||||||
 | 
					          isDir: false,
 | 
				
			||||||
 | 
					          fullPath,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      let items = state.req.items;
 | 
					      let items = state.req.items;
 | 
				
			||||||
      let path = getters.getRoutePath();
 | 
					      let path = getters.getRoutePath();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (el !== null && el.classList.contains("item") && el.dataset.dir === "true") {
 | 
					      if (el !== null && el.classList.contains("item") && el.dataset.dir === "true") {
 | 
				
			||||||
        path = el.__vue__.url;
 | 
					        path = el.__vue__.url;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
        items = (await api.fetch(path)).items;
 | 
					        items = (await api.fetch(path)).items;
 | 
				
			||||||
        } catch (error) {
 | 
					 | 
				
			||||||
          showError(error);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const conflict = upload.checkConflict(files, items);
 | 
					      const conflict = upload.checkConflict(uploadFiles, items);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (conflict) {
 | 
					      if (conflict) {
 | 
				
			||||||
        mutations.showHover({
 | 
					        mutations.showHover({
 | 
				
			||||||
          name: "replace",
 | 
					          name: "replace",
 | 
				
			||||||
          confirm: (event) => {
 | 
					          confirm: async (event) => {
 | 
				
			||||||
            event.preventDefault();
 | 
					            event.preventDefault();
 | 
				
			||||||
            mutations.closeHovers();
 | 
					            mutations.closeHovers();
 | 
				
			||||||
            upload.handleFiles(files, path, true);
 | 
					            await upload.handleFiles(uploadFiles, path, true);
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
        return;
 | 
					      } else {
 | 
				
			||||||
 | 
					        await upload.handleFiles(uploadFiles, path);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      mutations.setReload(true);
 | 
				
			||||||
      upload.handleFiles(files, path);
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    uploadInput(event) {
 | 
					    uploadInput(event) {
 | 
				
			||||||
      mutations.closeHovers();
 | 
					      mutations.closeHovers();
 | 
				
			||||||
| 
						 | 
					@ -564,6 +785,7 @@ export default {
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    setMultiple(val) {
 | 
					    setMultiple(val) {
 | 
				
			||||||
      mutations.setMultiple(val == true);
 | 
					      mutations.setMultiple(val == true);
 | 
				
			||||||
 | 
					      showMultipleSelection();
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    openSearch() {
 | 
					    openSearch() {
 | 
				
			||||||
      this.currentPrompt = "search";
 | 
					      this.currentPrompt = "search";
 | 
				
			||||||
| 
						 | 
					@ -584,6 +806,23 @@ export default {
 | 
				
			||||||
        document.getElementById("upload-input").click();
 | 
					        document.getElementById("upload-input").click();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    openContext(event) {
 | 
				
			||||||
 | 
					      event.preventDefault();
 | 
				
			||||||
 | 
					      mutations.showHover({
 | 
				
			||||||
 | 
					        name: "ContextMenu",
 | 
				
			||||||
 | 
					        props: {
 | 
				
			||||||
 | 
					          posX: event.clientX,
 | 
				
			||||||
 | 
					          posY: event.clientY,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    clickClear() {
 | 
				
			||||||
 | 
					      const sameAsBefore = state.selected == this.lastSelected;
 | 
				
			||||||
 | 
					      if (sameAsBefore && !state.multiple) {
 | 
				
			||||||
 | 
					        mutations.resetSelected();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this.lastSelected = state.selected;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -198,19 +198,26 @@ export default {
 | 
				
			||||||
      this.$router.replace({ path: this.nextLink });
 | 
					      this.$router.replace({ path: this.nextLink });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    key(event) {
 | 
					    key(event) {
 | 
				
			||||||
      if (this.currentPrompt !== null) {
 | 
					      if (getters.currentPromptName() != null) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (event.which === 13 || event.which === 39) {
 | 
					      const { key } = event;
 | 
				
			||||||
        // right arrow
 | 
					
 | 
				
			||||||
        if (this.hasNext) this.next();
 | 
					      switch (key) {
 | 
				
			||||||
      } else if (event.which === 37) {
 | 
					        case "ArrowRight":
 | 
				
			||||||
        // left arrow
 | 
					          if (this.hasNext) {
 | 
				
			||||||
        if (this.hasPrevious) this.prev();
 | 
					            this.next();
 | 
				
			||||||
      } else if (event.which === 27) {
 | 
					          }
 | 
				
			||||||
        // esc
 | 
					          break;
 | 
				
			||||||
 | 
					        case "ArrowLeft":
 | 
				
			||||||
 | 
					          if (this.hasPrevious) {
 | 
				
			||||||
 | 
					            this.prev();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case ("Escape", "Backspace"):
 | 
				
			||||||
          this.close();
 | 
					          this.close();
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async updatePreview() {
 | 
					    async updatePreview() {
 | 
				
			||||||
| 
						 | 
					@ -222,13 +229,9 @@ export default {
 | 
				
			||||||
      this.name = decodeURIComponent(dirs[dirs.length - 1]);
 | 
					      this.name = decodeURIComponent(dirs[dirs.length - 1]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!this.listing) {
 | 
					      if (!this.listing) {
 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
        const path = url.removeLastDir(state.route.path);
 | 
					        const path = url.removeLastDir(state.route.path);
 | 
				
			||||||
        const res = await api.fetch(path);
 | 
					        const res = await api.fetch(path);
 | 
				
			||||||
        this.listing = res.items;
 | 
					        this.listing = res.items;
 | 
				
			||||||
        } catch (e) {
 | 
					 | 
				
			||||||
          showError(e);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.previousLink = "";
 | 
					      this.previousLink = "";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -86,7 +86,7 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { showSuccess, showError } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
import { state, mutations, getters } from "@/store";
 | 
					import { state, mutations, getters } from "@/store";
 | 
				
			||||||
import { settings as api } from "@/api";
 | 
					import { settings as api } from "@/api";
 | 
				
			||||||
import { enableExec } from "@/utils/constants";
 | 
					import { enableExec } from "@/utils/constants";
 | 
				
			||||||
| 
						 | 
					@ -140,9 +140,9 @@ export default {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        mutations.setSettings(this.selectedSettings);
 | 
					        mutations.setSettings(this.selectedSettings);
 | 
				
			||||||
        await api.update(state.settings);
 | 
					        await api.update(state.settings);
 | 
				
			||||||
        showSuccess(this.$t("settings.settingsUpdated"));
 | 
					        notify.showSuccess(this.$t("settings.settingsUpdated"));
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        showError(e);
 | 
					        notify.showError(e);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="card" id="profile-main" :class="{ active: active }">
 | 
					  <div class="card" :class="{ active: active }">
 | 
				
			||||||
    <div class="card-title">
 | 
					    <div class="card-title">
 | 
				
			||||||
      <h2>{{ $t("settings.profileSettings") }}</h2>
 | 
					      <h2>{{ $t("settings.profileSettings") }}</h2>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
| 
						 | 
					@ -97,7 +97,7 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { showSuccess, showError } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
import { state, mutations } from "@/store";
 | 
					import { state, mutations } from "@/store";
 | 
				
			||||||
import { users } from "@/api";
 | 
					import { users } from "@/api";
 | 
				
			||||||
import Languages from "@/components/settings/Languages.vue";
 | 
					import Languages from "@/components/settings/Languages.vue";
 | 
				
			||||||
| 
						 | 
					@ -174,9 +174,9 @@ export default {
 | 
				
			||||||
        newUserSettings.id = state.user.id;
 | 
					        newUserSettings.id = state.user.id;
 | 
				
			||||||
        newUserSettings.password = this.password;
 | 
					        newUserSettings.password = this.password;
 | 
				
			||||||
        await users.update(newUserSettings, ["password"]);
 | 
					        await users.update(newUserSettings, ["password"]);
 | 
				
			||||||
        showSuccess(this.$t("settings.passwordUpdated"));
 | 
					        notify.showSuccess(this.$t("settings.passwordUpdated"));
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        showError(e);
 | 
					        notify.showError(e);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async updateSettings(event) {
 | 
					    async updateSettings(event) {
 | 
				
			||||||
| 
						 | 
					@ -203,13 +203,13 @@ export default {
 | 
				
			||||||
          "dateFormat",
 | 
					          "dateFormat",
 | 
				
			||||||
          "gallerySize",
 | 
					          "gallerySize",
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
        mutations.updateUser(data);
 | 
					        mutations.updateCurrentUser(data);
 | 
				
			||||||
        if (shouldReload) {
 | 
					        if (shouldReload) {
 | 
				
			||||||
          location.reload();
 | 
					          location.reload();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        showSuccess(this.$t("settings.settingsUpdated"));
 | 
					        notify.showSuccess(this.$t("settings.settingsUpdated"));
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        showError(e);
 | 
					        notify.showError(e);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    updateViewMode(updatedMode) {
 | 
					    updateViewMode(updatedMode) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <errors v-if="error" :errorCode="error.status" />
 | 
					  <errors v-if="error" :errorCode="error.status" />
 | 
				
			||||||
  <div class="card" id="shares-main" :class="{ active: active }">
 | 
					  <div class="card" :class="{ active: active }">
 | 
				
			||||||
    <div class="card-title">
 | 
					    <div class="card-title">
 | 
				
			||||||
      <h2>{{ $t("settings.shareManagement") }}</h2>
 | 
					      <h2>{{ $t("settings.shareManagement") }}</h2>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
| 
						 | 
					@ -55,7 +55,7 @@
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { showSuccess, showError } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
import { share as api, users } from "@/api";
 | 
					import { share as api, users } from "@/api";
 | 
				
			||||||
import { state, mutations, getters } from "@/store";
 | 
					import { state, mutations, getters } from "@/store";
 | 
				
			||||||
import { fromNow } from "@/utils/moment";
 | 
					import { fromNow } from "@/utils/moment";
 | 
				
			||||||
| 
						 | 
					@ -95,7 +95,7 @@ export default {
 | 
				
			||||||
  mounted() {
 | 
					  mounted() {
 | 
				
			||||||
    this.clip = new Clipboard(".copy-clipboard");
 | 
					    this.clip = new Clipboard(".copy-clipboard");
 | 
				
			||||||
    this.clip.on("success", () => {
 | 
					    this.clip.on("success", () => {
 | 
				
			||||||
      showSuccess(this.$t("success.linkCopied"));
 | 
					      notify.showSuccess(this.$t("success.linkCopied"));
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  beforeUnmount() {
 | 
					  beforeUnmount() {
 | 
				
			||||||
| 
						 | 
					@ -126,9 +126,9 @@ export default {
 | 
				
			||||||
          try {
 | 
					          try {
 | 
				
			||||||
            api.remove(link.hash);
 | 
					            api.remove(link.hash);
 | 
				
			||||||
            this.links = this.links.filter((item) => item.hash !== link.hash);
 | 
					            this.links = this.links.filter((item) => item.hash !== link.hash);
 | 
				
			||||||
            showSuccess(this.$t("settings.shareDeleted"));
 | 
					            notify.showSuccess(this.$t("settings.shareDeleted"));
 | 
				
			||||||
          } catch (e) {
 | 
					          } catch (e) {
 | 
				
			||||||
            showError(e);
 | 
					            notify.showError(e);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <errors v-if="error" :errorCode="error.status" />
 | 
					  <errors v-if="error" :errorCode="error.status" />
 | 
				
			||||||
  <form @submit="save" id="user-main" class="card">
 | 
					  <form @submit="save" class="card active">
 | 
				
			||||||
    <div class="card-title">
 | 
					    <div class="card-title">
 | 
				
			||||||
      <h2 v-if="user.id === 0">{{ $t("settings.newUser") }}</h2>
 | 
					      <h2 v-if="user.id === 0">{{ $t("settings.newUser") }}</h2>
 | 
				
			||||||
      <h2 v-else>{{ $t("settings.user") }} {{ user.username }}</h2>
 | 
					      <h2 v-else>{{ $t("settings.user") }} {{ user.username }}</h2>
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,7 @@ import { mutations, state } from "@/store";
 | 
				
			||||||
import { users as api, settings } from "@/api";
 | 
					import { users as api, settings } from "@/api";
 | 
				
			||||||
import UserForm from "@/components/settings/UserForm.vue";
 | 
					import UserForm from "@/components/settings/UserForm.vue";
 | 
				
			||||||
import Errors from "@/views/Errors.vue";
 | 
					import Errors from "@/views/Errors.vue";
 | 
				
			||||||
import { showSuccess, showError } from "@/notify";
 | 
					import { notify } from "@/notify";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "user",
 | 
					  name: "user",
 | 
				
			||||||
| 
						 | 
					@ -49,12 +49,13 @@ export default {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      error: null,
 | 
					      error: null,
 | 
				
			||||||
      originalUser: null,
 | 
					      originalUser: null,
 | 
				
			||||||
      user: { perm: { admin: false } },
 | 
					      user: {
 | 
				
			||||||
 | 
					        scope: ".",
 | 
				
			||||||
 | 
					        username: "",
 | 
				
			||||||
 | 
					        perm: { admin: false },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      showDelete: false,
 | 
					      showDelete: false,
 | 
				
			||||||
      createUserDir: false,
 | 
					      createUserDir: false,
 | 
				
			||||||
      loading: false, // Replaces Vuex state `loading`
 | 
					 | 
				
			||||||
      currentPrompt: null, // Replaces Vuex getter `currentPrompt`
 | 
					 | 
				
			||||||
      currentPromptName: null, // Replaces Vuex getter `currentPromptName`
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  created() {
 | 
					  created() {
 | 
				
			||||||
| 
						 | 
					@ -65,14 +66,14 @@ export default {
 | 
				
			||||||
      return state.settings;
 | 
					      return state.settings;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    isNew() {
 | 
					    isNew() {
 | 
				
			||||||
      return state.route.path === "/settings/users/new";
 | 
					      return state.route.path.startsWith("/settings/users/new");
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  watch: {
 | 
					 | 
				
			||||||
    $route: "fetchData",
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    async fetchData() {
 | 
					    async fetchData() {
 | 
				
			||||||
 | 
					      if (!state.route.path.startsWith("/settings")) {
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      mutations.setLoading("users", true);
 | 
					      mutations.setLoading("users", true);
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        if (this.isNew) {
 | 
					        if (this.isNew) {
 | 
				
			||||||
| 
						 | 
					@ -87,11 +88,13 @@ export default {
 | 
				
			||||||
            id: 0,
 | 
					            id: 0,
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          const id = state.route.params.id;
 | 
					          const id = Array.isArray(state.route.params.id)
 | 
				
			||||||
 | 
					            ? state.route.params.id.join("")
 | 
				
			||||||
 | 
					            : state.route.params.id;
 | 
				
			||||||
          this.user = { ...(await api.get(id)) };
 | 
					          this.user = { ...(await api.get(id)) };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        showError(e);
 | 
					        notify.showError(e);
 | 
				
			||||||
        this.error = e;
 | 
					        this.error = e;
 | 
				
			||||||
      } finally {
 | 
					      } finally {
 | 
				
			||||||
        mutations.setLoading("users", false);
 | 
					        mutations.setLoading("users", false);
 | 
				
			||||||
| 
						 | 
					@ -101,27 +104,19 @@ export default {
 | 
				
			||||||
      mutations.showHover({ name: "deleteUser", props: { user: this.user } });
 | 
					      mutations.showHover({ name: "deleteUser", props: { user: this.user } });
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async save(event) {
 | 
					    async save(event) {
 | 
				
			||||||
 | 
					      let user = this.user
 | 
				
			||||||
      event.preventDefault();
 | 
					      event.preventDefault();
 | 
				
			||||||
      let user = {
 | 
					 | 
				
			||||||
        ...this.originalUser,
 | 
					 | 
				
			||||||
        ...this.user,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        if (this.isNew) {
 | 
					        if (this.isNew) {
 | 
				
			||||||
          const loc = await api.create(user);
 | 
					          const loc = await api.create(user);
 | 
				
			||||||
          this.$router.push({ path: loc });
 | 
					          this.$router.push({ path: loc });
 | 
				
			||||||
          showSuccess(this.$t("settings.userCreated"));
 | 
					          notify.showSuccess(this.$t("settings.userCreated"));
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          await api.update(user);
 | 
					          await api.update(user);
 | 
				
			||||||
          if (user.id === state.user.id) {
 | 
					          notify.showSuccess(this.$t("settings.userUpdated"));
 | 
				
			||||||
            consoel.log("set user");
 | 
					 | 
				
			||||||
            mutations.setUser(user);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          showSuccess(this.$t("settings.userUpdated"));
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        showError(e);
 | 
					        notify.showError(e);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,106 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <errors v-if="error" :errorCode="error.status" />
 | 
					 | 
				
			||||||
  <div v-if="isExecEnabled" class="card" id="userColumn-main">
 | 
					 | 
				
			||||||
    <form @submit.prevent="save">
 | 
					 | 
				
			||||||
      <div class="card-title">
 | 
					 | 
				
			||||||
        <h2>{{ $t("settings.commandRunner") }}</h2>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div class="card-content">
 | 
					 | 
				
			||||||
        <i18n path="settings.commandRunnerHelp" tag="p" class="small">
 | 
					 | 
				
			||||||
          <code>FILE</code>
 | 
					 | 
				
			||||||
          <code>SCOPE</code>
 | 
					 | 
				
			||||||
          <a
 | 
					 | 
				
			||||||
            class="link"
 | 
					 | 
				
			||||||
            target="_blank"
 | 
					 | 
				
			||||||
            href="https://filebrowser.org/configuration/command-runner"
 | 
					 | 
				
			||||||
            >{{ $t("settings.documentation") }}</a
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
        </i18n>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div
 | 
					 | 
				
			||||||
          v-for="(command, index) in settings.commands"
 | 
					 | 
				
			||||||
          :key="index"
 | 
					 | 
				
			||||||
          class="collapsible"
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <input :id="command.name" type="checkbox" />
 | 
					 | 
				
			||||||
          <label :for="command.name">
 | 
					 | 
				
			||||||
            <p>{{ capitalize(command.name) }}</p>
 | 
					 | 
				
			||||||
            <i class="material-icons">arrow_drop_down</i>
 | 
					 | 
				
			||||||
          </label>
 | 
					 | 
				
			||||||
          <div class="collapse">
 | 
					 | 
				
			||||||
            <textarea
 | 
					 | 
				
			||||||
              class="input input--block input--textarea"
 | 
					 | 
				
			||||||
              v-model.trim="command.value"
 | 
					 | 
				
			||||||
            ></textarea>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div class="card-action">
 | 
					 | 
				
			||||||
        <input class="button button--flat" type="submit" :value="$t('buttons.update')" />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </form>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
import { showSuccess } from "@/notify";
 | 
					 | 
				
			||||||
import { state, getters } from "@/store";
 | 
					 | 
				
			||||||
import { settings as api } from "@/api";
 | 
					 | 
				
			||||||
import { enableExec } from "@/utils/constants";
 | 
					 | 
				
			||||||
//import UserForm from "@/components/settings/UserForm.vue";
 | 
					 | 
				
			||||||
//import Rules from "@/components/settings/Rules.vue";
 | 
					 | 
				
			||||||
import Errors from "@/views/Errors.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  name: "settings",
 | 
					 | 
				
			||||||
  components: {
 | 
					 | 
				
			||||||
    //UserForm,
 | 
					 | 
				
			||||||
    //Rules,
 | 
					 | 
				
			||||||
    Errors,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data: function () {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      error: null,
 | 
					 | 
				
			||||||
      originalSettings: null,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  computed: {
 | 
					 | 
				
			||||||
    settings() {
 | 
					 | 
				
			||||||
      return state.settings;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    loading() {
 | 
					 | 
				
			||||||
      return getters.isLoading();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    user() {
 | 
					 | 
				
			||||||
      return state.user;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    isExecEnabled: () => enableExec,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  methods: {
 | 
					 | 
				
			||||||
    updateRules(updatedRules) {
 | 
					 | 
				
			||||||
      this.settings.rules = updatedRules;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    capitalize(name, where = "_") {
 | 
					 | 
				
			||||||
      if (where === "caps") where = /(?=[A-Z])/;
 | 
					 | 
				
			||||||
      let splitted = name.split(where);
 | 
					 | 
				
			||||||
      name = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      for (let i = 0; i < splitted.length; i++) {
 | 
					 | 
				
			||||||
        name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + " ";
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return name.slice(0, -1);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    async save() {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        await api.update(state.settings);
 | 
					 | 
				
			||||||
        showSuccess(this.$t("settings.settingsUpdated"));
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        showError(e);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,87 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
  <errors v-if="error" :errorCode="error.status" />
 | 
					 | 
				
			||||||
  <div class="card" id="user-defaults-main">
 | 
					 | 
				
			||||||
    <div class="card-title">
 | 
					 | 
				
			||||||
      <h2>{{ $t("settings.userDefaults") }}</h2>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="card-content">
 | 
					 | 
				
			||||||
      <p class="small">{{ $t("settings.defaultUserDescription") }}</p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <user-form
 | 
					 | 
				
			||||||
        :isNew="false"
 | 
					 | 
				
			||||||
        :isDefault="true"
 | 
					 | 
				
			||||||
        :user="settings.defaults"
 | 
					 | 
				
			||||||
        @update:user="updateUser"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="card-action">
 | 
					 | 
				
			||||||
      <input class="button button--flat" type="submit" :value="$t('buttons.update')" />
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script>
 | 
					 | 
				
			||||||
import { showSuccess } from "@/notify";
 | 
					 | 
				
			||||||
import { state, getters } from "@/store";
 | 
					 | 
				
			||||||
import { settings as api } from "@/api";
 | 
					 | 
				
			||||||
import { enableExec } from "@/utils/constants";
 | 
					 | 
				
			||||||
import UserForm from "@/components/settings/UserForm.vue";
 | 
					 | 
				
			||||||
//import Rules from "@/components/settings/Rules.vue";
 | 
					 | 
				
			||||||
import Errors from "@/views/Errors.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default {
 | 
					 | 
				
			||||||
  name: "settings",
 | 
					 | 
				
			||||||
  components: {
 | 
					 | 
				
			||||||
    UserForm,
 | 
					 | 
				
			||||||
    //Rules,
 | 
					 | 
				
			||||||
    Errors,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  data: function () {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      error: null,
 | 
					 | 
				
			||||||
      originalSettings: null,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  computed: {
 | 
					 | 
				
			||||||
    settings() {
 | 
					 | 
				
			||||||
      return state.settings;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    loading() {
 | 
					 | 
				
			||||||
      return getters.isLoading();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    user() {
 | 
					 | 
				
			||||||
      return state.user;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    isExecEnabled: () => enableExec,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  methods: {
 | 
					 | 
				
			||||||
    updateRules(updatedRules) {
 | 
					 | 
				
			||||||
      state.settings.rules = updatedRules;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    updateUser(updatedUser) {
 | 
					 | 
				
			||||||
      state.settings.defaults = updatedUser;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    capitalize(name, where = "_") {
 | 
					 | 
				
			||||||
      if (where === "caps") where = /(?=[A-Z])/;
 | 
					 | 
				
			||||||
      let splitted = name.split(where);
 | 
					 | 
				
			||||||
      name = "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      for (let i = 0; i < splitted.length; i++) {
 | 
					 | 
				
			||||||
        name += splitted[i].charAt(0).toUpperCase() + splitted[i].slice(1) + " ";
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return name.slice(0, -1);
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    async save() {
 | 
					 | 
				
			||||||
      try {
 | 
					 | 
				
			||||||
        await api.update(state.settings);
 | 
					 | 
				
			||||||
        showSuccess(this.$t("settings.settingsUpdated"));
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					 | 
				
			||||||
        showError(e);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,6 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <errors v-if="error" :errorCode="error.status" />
 | 
					  <errors v-if="error" :errorCode="error.status" />
 | 
				
			||||||
  <div class="card" id="users-main">
 | 
					  <div class="card">
 | 
				
			||||||
    <div class="card-title">
 | 
					    <div class="card-title">
 | 
				
			||||||
      <h2>{{ $t("settings.users") }}</h2>
 | 
					      <h2>{{ $t("settings.users") }}</h2>
 | 
				
			||||||
      <router-link to="/settings/users/new"
 | 
					      <router-link to="/settings/users/new"
 | 
				
			||||||
| 
						 | 
					@ -36,12 +36,12 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
import { state, mutations, getters } from "@/store";
 | 
					import { state, mutations, getters } from "@/store";
 | 
				
			||||||
import { getAllUsers } from "@/api/users";
 | 
					import { getAllUsers } from "@/api/users";
 | 
				
			||||||
import Errors from "@/views/Errors.vue";
 | 
					import Errors from "@/views/Errors.vue";
 | 
				
			||||||
import { showError } from "@/notify";
 | 
					
 | 
				
			||||||
mutations.setLoading("users", true);
 | 
					 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
  name: "users",
 | 
					  name: "users",
 | 
				
			||||||
  components: {
 | 
					  components: {
 | 
				
			||||||
| 
						 | 
					@ -54,18 +54,10 @@ export default {
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  async created() {
 | 
					  async created() {
 | 
				
			||||||
 | 
					    mutations.setLoading("users", true);
 | 
				
			||||||
    // Set loading state to true
 | 
					    // Set loading state to true
 | 
				
			||||||
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      // Fetch all users from the API
 | 
					 | 
				
			||||||
    this.users = await getAllUsers();
 | 
					    this.users = await getAllUsers();
 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      showError(e);
 | 
					 | 
				
			||||||
      // Handle errors
 | 
					 | 
				
			||||||
      this.error = e;
 | 
					 | 
				
			||||||
    } finally {
 | 
					 | 
				
			||||||
    mutations.setLoading("users", false);
 | 
					    mutations.setLoading("users", false);
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    settings() {
 | 
					    settings() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ test("redirect to login", async ({ page }) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test("login", async ({ authPage, page, context }) => {
 | 
					test("login", async ({ authPage, page, context }) => {
 | 
				
			||||||
  await authPage.goto();
 | 
					  await authPage.goto();
 | 
				
			||||||
  await expect(page).toHaveTitle(/Login - File Browser$/);
 | 
					  await expect(page).toHaveTitle(/Login - FileBrowser Quantum$/);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  await authPage.loginAs("fake", "fake");
 | 
					  await authPage.loginAs("fake", "fake");
 | 
				
			||||||
  await expect(authPage.wrongCredentials).toBeVisible();
 | 
					  await expect(authPage.wrongCredentials).toBeVisible();
 | 
				
			||||||
| 
						 | 
					@ -18,14 +18,14 @@ test("login", async ({ authPage, page, context }) => {
 | 
				
			||||||
  await authPage.loginAs();
 | 
					  await authPage.loginAs();
 | 
				
			||||||
  await expect(authPage.wrongCredentials).toBeHidden();
 | 
					  await expect(authPage.wrongCredentials).toBeHidden();
 | 
				
			||||||
  // await page.waitForURL("**/files/", { timeout: 5000 });
 | 
					  // await page.waitForURL("**/files/", { timeout: 5000 });
 | 
				
			||||||
  await expect(page).toHaveTitle(/.*Files - File Browser$/);
 | 
					  await expect(page).toHaveTitle(/.*Files - FileBrowser Quantum$/);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let cookies = await context.cookies();
 | 
					  let cookies = await context.cookies();
 | 
				
			||||||
  expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
 | 
					  expect(cookies.find((c) => c.name == "auth")?.value).toBeDefined();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // await authPage.logout();
 | 
					  // await authPage.logout();
 | 
				
			||||||
  // await page.waitForURL("**/login", { timeout: 5000 });
 | 
					  // await page.waitForURL("**/login", { timeout: 5000 });
 | 
				
			||||||
  // await expect(page).toHaveTitle(/Login - File Browser$/);
 | 
					  // await expect(page).toHaveTitle(/Login - FileBrowser Quantum$/);
 | 
				
			||||||
  // cookies = await context.cookies();
 | 
					  // cookies = await context.cookies();
 | 
				
			||||||
  // expect(cookies.find((c) => c.name == "auth")?.value).toBeUndefined();
 | 
					  // expect(cookies.find((c) => c.name == "auth")?.value).toBeUndefined();
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -3,15 +3,17 @@
 | 
				
			||||||
next 0.2.x release:
 | 
					next 0.2.x release:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Theme configuration from settings
 | 
					- Theme configuration from settings
 | 
				
			||||||
- Better media and file viewer support
 | 
					- File syncronization improvements
 | 
				
			||||||
 | 
					- right-click context menu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
initial 0.3.0 release :
 | 
					initial 0.3.0 release :
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- drop in replace backend db with pocketbas
 | 
					- database changes
 | 
				
			||||||
 | 
					- introduce jobs as replacement to runners.
 | 
				
			||||||
- Add Job status to the sidebar
 | 
					- Add Job status to the sidebar
 | 
				
			||||||
  - index status.
 | 
					  - index status.
 | 
				
			||||||
  - Job status from users
 | 
					  - Job status from users
 | 
				
			||||||
 | 
					  - upload status
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Future releases:
 | 
					Future releases:
 | 
				
			||||||
  - Replace http routes for gorilla/mux with pocketbase
 | 
					  - Replace http routes for gorilla/mux with pocketbase
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue