├── .github └── workflows │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── Makefile ├── README.md ├── app ├── app.go ├── datasource │ ├── dtype.go │ ├── folder.go │ ├── lmdb.go │ ├── text_file.go │ └── util.go ├── dtype.go ├── filter │ ├── dtype.go │ ├── file_ext.go │ ├── json.go │ └── none.go ├── handler.go ├── template.go ├── update.go └── util.go ├── go.mod ├── go.sum ├── main.go ├── static ├── css │ ├── bootstrap.min.css │ ├── bootstrap.min.css.map │ └── lightbox.min.css ├── images │ ├── close.png │ ├── loading.gif │ ├── logo.jpg │ ├── next.png │ └── prev.png └── js │ ├── bootstrap.bundle.min.js │ ├── bootstrap.bundle.min.js.map │ ├── jquery.min.js │ ├── js.cookie.min.js │ └── lightbox.min.js └── templates ├── base.html ├── compare.html └── list.html /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Binaries 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' # Tags like "v1.2.3" 7 | - '**' # Tags like "release/v1.2.3" 8 | 9 | jobs: 10 | goreleaser: 11 | name: Release Binaries 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v2 20 | 21 | - name: Set env 22 | id: app_info 23 | run: | 24 | echo ::set-output name=APP_NAME::${GITHUB_REF#refs/*/} 25 | echo ::set-output name=APP_BRANCH::${GITHUB_REF#refs/heads/} 26 | echo ::set-output name=APP_TAG::${GITHUB_REF#refs/tags/} 27 | echo ::set-output name=APP_COMMIT::${GITHUB_SHA} 28 | 29 | - name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v2 31 | with: 32 | distribution: goreleaser 33 | version: latest 34 | args: release --rm-dist 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | image_server* 2 | 3 | # Created by https://www.toptal.com/developers/gitignore/api/go,vim,macos,linux,windows,visualstudiocode,intellij+all 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,vim,macos,linux,windows,visualstudiocode,intellij+all 5 | 6 | ### Go ### 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | # vendor/ 22 | 23 | ### Go Patch ### 24 | /vendor/ 25 | /Godeps/ 26 | 27 | ### Intellij+all ### 28 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 29 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 30 | 31 | # User-specific stuff 32 | .idea/**/workspace.xml 33 | .idea/**/tasks.xml 34 | .idea/**/usage.statistics.xml 35 | .idea/**/dictionaries 36 | .idea/**/shelf 37 | 38 | # Generated files 39 | .idea/**/contentModel.xml 40 | 41 | # Sensitive or high-churn files 42 | .idea/**/dataSources/ 43 | .idea/**/dataSources.ids 44 | .idea/**/dataSources.local.xml 45 | .idea/**/sqlDataSources.xml 46 | .idea/**/dynamic.xml 47 | .idea/**/uiDesigner.xml 48 | .idea/**/dbnavigator.xml 49 | 50 | # Gradle 51 | .idea/**/gradle.xml 52 | .idea/**/libraries 53 | 54 | # Gradle and Maven with auto-import 55 | # When using Gradle or Maven with auto-import, you should exclude module files, 56 | # since they will be recreated, and may cause churn. Uncomment if using 57 | # auto-import. 58 | # .idea/artifacts 59 | # .idea/compiler.xml 60 | # .idea/jarRepositories.xml 61 | # .idea/modules.xml 62 | # .idea/*.iml 63 | # .idea/modules 64 | # *.iml 65 | # *.ipr 66 | 67 | # CMake 68 | cmake-build-*/ 69 | 70 | # Mongo Explorer plugin 71 | .idea/**/mongoSettings.xml 72 | 73 | # File-based project format 74 | *.iws 75 | 76 | # IntelliJ 77 | out/ 78 | 79 | # mpeltonen/sbt-idea plugin 80 | .idea_modules/ 81 | 82 | # JIRA plugin 83 | atlassian-ide-plugin.xml 84 | 85 | # Cursive Clojure plugin 86 | .idea/replstate.xml 87 | 88 | # Crashlytics plugin (for Android Studio and IntelliJ) 89 | com_crashlytics_export_strings.xml 90 | crashlytics.properties 91 | crashlytics-build.properties 92 | fabric.properties 93 | 94 | # Editor-based Rest Client 95 | .idea/httpRequests 96 | 97 | # Android studio 3.1+ serialized cache file 98 | .idea/caches/build_file_checksums.ser 99 | 100 | ### Intellij+all Patch ### 101 | # Ignores the whole .idea folder and all .iml files 102 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 103 | 104 | .idea/ 105 | 106 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 107 | 108 | *.iml 109 | modules.xml 110 | .idea/misc.xml 111 | *.ipr 112 | 113 | # Sonarlint plugin 114 | .idea/sonarlint 115 | 116 | ### Linux ### 117 | *~ 118 | 119 | # temporary files which can be created if a process still has a handle open of a deleted file 120 | .fuse_hidden* 121 | 122 | # KDE directory preferences 123 | .directory 124 | 125 | # Linux trash folder which might appear on any partition or disk 126 | .Trash-* 127 | 128 | # .nfs files are created when an open file is removed but is still being accessed 129 | .nfs* 130 | 131 | ### macOS ### 132 | # General 133 | .DS_Store 134 | .AppleDouble 135 | .LSOverride 136 | 137 | # Icon must end with two \r 138 | Icon 139 | 140 | 141 | # Thumbnails 142 | ._* 143 | 144 | # Files that might appear in the root of a volume 145 | .DocumentRevisions-V100 146 | .fseventsd 147 | .Spotlight-V100 148 | .TemporaryItems 149 | .Trashes 150 | .VolumeIcon.icns 151 | .com.apple.timemachine.donotpresent 152 | 153 | # Directories potentially created on remote AFP share 154 | .AppleDB 155 | .AppleDesktop 156 | Network Trash Folder 157 | Temporary Items 158 | .apdisk 159 | 160 | ### Vim ### 161 | # Swap 162 | [._]*.s[a-v][a-z] 163 | !*.svg # comment out if you don't need vector files 164 | [._]*.sw[a-p] 165 | [._]s[a-rt-v][a-z] 166 | [._]ss[a-gi-z] 167 | [._]sw[a-p] 168 | 169 | # Session 170 | Session.vim 171 | Sessionx.vim 172 | 173 | # Temporary 174 | .netrwhist 175 | # Auto-generated tag files 176 | tags 177 | # Persistent undo 178 | [._]*.un~ 179 | 180 | ### VisualStudioCode ### 181 | .vscode/* 182 | !.vscode/tasks.json 183 | !.vscode/launch.json 184 | *.code-workspace 185 | 186 | ### VisualStudioCode Patch ### 187 | # Ignore all local history of files 188 | .history 189 | .ionide 190 | 191 | ### Windows ### 192 | # Windows thumbnail cache files 193 | Thumbs.db 194 | Thumbs.db:encryptable 195 | ehthumbs.db 196 | ehthumbs_vista.db 197 | 198 | # Dump file 199 | *.stackdump 200 | 201 | # Folder config file 202 | [Dd]esktop.ini 203 | 204 | # Recycle Bin used on file shares 205 | $RECYCLE.BIN/ 206 | 207 | # Windows Installer files 208 | *.cab 209 | *.msi 210 | *.msix 211 | *.msm 212 | *.msp 213 | 214 | # Windows shortcuts 215 | *.lnk 216 | 217 | # End of https://www.toptal.com/developers/gitignore/api/go,vim,macos,linux,windows,visualstudiocode,intellij+all 218 | dist/ 219 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - binary: image_server 11 | env: 12 | - CGO_ENABLED=1 13 | goos: 14 | - linux 15 | goarch: 16 | - amd64 17 | ldflags: 18 | - -s -w -X main.Version=v{{.Version}} -X main.Build={{.ShortCommit}} 19 | archives: 20 | - replacements: 21 | darwin: Darwin 22 | linux: Linux 23 | windows: Windows 24 | 386: i386 25 | amd64: x86_64 26 | checksum: 27 | name_template: 'checksums.txt' 28 | snapshot: 29 | name_template: "{{ incpatch .Version }}-next" 30 | changelog: 31 | sort: asc 32 | filters: 33 | exclude: 34 | - '^docs:' 35 | - '^test:' 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | build: 4 | export GIN_MODE=release; \ 5 | go build -o image_server -ldflags "-X main.Version=`git describe --tags --abbrev=0 --dirty=-dev` -X main.Build=`git rev-parse --short HEAD`" main.go 6 | 7 | run: 8 | go run main.go 9 | 10 | clean: 11 | rm image_server -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image Server 2 | 3 | Image Server sets up a simple HTTP service, to make you view batch of images easier at a glance. 4 | 5 | Start the server, open your browser, and navigate to `http://your_ip:9420` to view your images. 6 | 7 | ## Start the server 8 | 9 | ### Current folder 10 | 11 | ```shell 12 | ImageServer 13 | ``` 14 | 15 | ### Specified folder 16 | 17 | ```shell 18 | image_server path/to/your/folder 19 | ``` 20 | 21 | ### URL list 22 | 23 | Each line should be a URL 24 | 25 | ```shell 26 | image_server path/to/your/list.txt 27 | ``` 28 | 29 | ### CSV / TSV file 30 | 31 | You should specify which column (by index, not name) is the image URL 32 | 33 | ```shell 34 | image_server --column 0 path/to/your/list.csv 35 | ``` 36 | 37 | ### JSON file 38 | 39 | One line per JSON object (ImageServer do not support human-friendly formatted JSON file), and you should specify how to 40 | get the image URL by JSONPath syntax 41 | 42 | ```shell 43 | image_server --filter "$.images[*]" path/to/your/json/file.json 44 | ``` 45 | 46 | ### LMDB file 47 | 48 | Keys will be separated by `/` to get virtual paths/folders 49 | 50 | ```shell 51 | image_server path/to/your/lmdb/database.lmdb 52 | ``` 53 | 54 | ### Parameters 55 | 56 | #### Change page size 57 | 58 | Default page size is 1000, you can change it by `--page` option 59 | 60 | ```shell 61 | image_server --page 100 path/to/your/folder 62 | ``` 63 | 64 | #### Change port 65 | 66 | Default port is 9420, you can change it by `--port` option 67 | 68 | ```shell 69 | image_server --port 2333 path/to/your/folder 70 | ``` 71 | 72 | ## Browse images in your browser 73 | 74 | ### Basic usage 75 | 76 | ``` 77 | http://your_ip:9420 78 | ``` 79 | 80 | ### Pagination 81 | 82 | ``` 83 | http://your_ip:9420?p=2333 84 | ``` 85 | 86 | ### Compare folders 87 | 88 | ``` 89 | http://your_ip:9420/path/to/foder/one?c=path/to/folder/two 90 | http://your_ip:9420/path/to/foder/one?c=path/to/folder/two&c=/path/to/another/folder 91 | ``` 92 | 93 | ## Web UI 94 | 95 | ### Navigation 96 | 97 | - ⏮ : Previous Neighborhood Folder 98 | - ⏫ : Parent Folder 99 | - ⏭ : Next Neighborhood Folder 100 | 101 | ### Pagination 102 | 103 | - 🔼 : Previous Page 104 | - 🔽 : Next Page 105 | -------------------------------------------------------------------------------- /app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "embed" 5 | "flag" 6 | "fmt" 7 | "html/template" 8 | "io/fs" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | 14 | "github.com/gin-gonic/gin" 15 | "haoyu.love/ImageServer/app/datasource" 16 | "haoyu.love/ImageServer/app/filter" 17 | ) 18 | 19 | var ( 20 | Root = "./" 21 | Port = flag.Int("port", 9420, "Listen Port") 22 | PageSize = flag.Int("page", 1000, "Page size") 23 | Column = flag.Int("column", 0, "Column") 24 | Filter = flag.String("filter", filter.PredefineDefault, "Filter") 25 | ) 26 | 27 | func InitFlag() { 28 | flag.Parse() 29 | if flag.NArg() == 0 { 30 | Root = "./" 31 | } else { 32 | Root = flag.Arg(0) 33 | } 34 | Root, _ = filepath.Abs(Root) 35 | 36 | if *Column < 0 { 37 | *Column = 0 38 | } 39 | 40 | // Ensure the root exists. 41 | if _, err := os.Stat(Root); os.IsNotExist(err) { 42 | panic(fmt.Sprintf("Path %s doesn't exists", Root)) 43 | } 44 | 45 | } 46 | 47 | func InitServer(assets embed.FS) *gin.Engine { 48 | // Select proper data source 49 | var data datasource.DataSource 50 | var flt filter.Filter 51 | 52 | if fileInfo, _ := os.Stat(Root); fileInfo.IsDir() { 53 | if filepath.Ext(Root) == ".lmdb" { 54 | flt = filter.NewNoFilter() 55 | data = datasource.NewLmdbDataSource(Root, &flt) 56 | } else { 57 | flt = filter.NewFileExtFilter(*Filter) 58 | data = datasource.NewFolderDataSource(Root, &flt) 59 | } 60 | } else { 61 | flt = filter.NewJsonFilter(*Filter) 62 | data = datasource.NewTextFileDataSource(Root, &flt, *Column) 63 | } 64 | 65 | handler := NewImageServerHandler(&data) 66 | 67 | // Router for the framework itself, such as static files 68 | frameworkRouter := gin.New() 69 | frameworkG := frameworkRouter.Group("/_") 70 | 71 | staticFiles, _ := fs.Sub(assets, "static") 72 | frameworkG.StaticFS("/", http.FS(staticFiles)) 73 | 74 | // The general router 75 | appRouter := gin.Default() 76 | templateFiles := template.Must( 77 | template.New("").Funcs(TemplateFunction).ParseFS(assets, "templates/*.html")) 78 | appRouter.SetHTMLTemplate(templateFiles) 79 | 80 | appRouter.GET("/*path", func(c *gin.Context) { 81 | path := c.Param("path") 82 | // Special handling for the static files 83 | if strings.HasPrefix(path, "/_/") { 84 | frameworkRouter.HandleContext(c) 85 | } else { 86 | handler.Handle(c) 87 | } 88 | }) 89 | return appRouter 90 | } 91 | -------------------------------------------------------------------------------- /app/datasource/dtype.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | type DataSource interface { 4 | GetFile(path string) ([]byte, error) 5 | GetFolder(path string) (FolderContent, error) 6 | GetNeighbor(current string) *Navigation 7 | Stat(filePath string) *FileStat 8 | } 9 | 10 | type FolderContent struct { 11 | Name string 12 | Folders []string 13 | Files []string 14 | } 15 | 16 | func (f *FolderContent) FilterTargetFile(target map[string]struct{}) { 17 | if len(target) > 0 { 18 | result := make([]string, 0) 19 | for _, file := range f.Files { 20 | if IsTargetFile(file, target) { 21 | result = append(result, file) 22 | } 23 | } 24 | f.Files = result 25 | } 26 | } 27 | 28 | type Navigation struct { 29 | Current string 30 | Prev string 31 | Next string 32 | Parent string 33 | } 34 | -------------------------------------------------------------------------------- /app/datasource/folder.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "path/filepath" 7 | "sort" 8 | "strings" 9 | 10 | "haoyu.love/ImageServer/app/filter" 11 | ) 12 | 13 | type FolderDataSource struct { 14 | root string 15 | filter *filter.Filter 16 | } 17 | 18 | func NewFolderDataSource(root string, flt *filter.Filter) *FolderDataSource { 19 | rootAbsolute, _ := filepath.Abs(root) 20 | ds := &FolderDataSource{root: rootAbsolute, filter: flt} 21 | return ds 22 | } 23 | 24 | func (ds *FolderDataSource) GetFile(filePath string) ([]byte, error) { 25 | currentAbs, _ := AbsolutePath(ds.root, filePath) 26 | data, err := os.ReadFile(currentAbs) 27 | return data, err 28 | } 29 | 30 | func (ds *FolderDataSource) GetFolder(current string) (content FolderContent, err error) { 31 | currentAbs, currentRelative := AbsolutePath(ds.root, current) 32 | 33 | content = FolderContent{ 34 | Name: currentRelative, 35 | Folders: []string{}, 36 | Files: []string{}, 37 | } 38 | 39 | // Ensure the root is a folder 40 | rootInfo, err := os.Stat(currentAbs) 41 | if nil != err { 42 | return 43 | } 44 | if !rootInfo.Mode().IsDir() { 45 | return 46 | } 47 | 48 | // Open the folder 49 | folder, err := os.Open(currentAbs) 50 | if nil != err { 51 | return 52 | } 53 | defer func() { _ = folder.Close() }() 54 | 55 | // Iter the folder 56 | fileInfo, err := folder.Readdir(-1) 57 | if nil != err { 58 | return 59 | } 60 | 61 | // Split result into folders and files 62 | for _, item := range fileInfo { 63 | // Filter out hidden files 64 | if strings.HasPrefix(item.Name(), ".") { 65 | continue 66 | } 67 | 68 | if item.IsDir() { 69 | content.Folders = append(content.Folders, path.Join(currentRelative, item.Name())) 70 | } else if (*ds.filter).Filter(item.Name()) { 71 | content.Files = append(content.Files, path.Join(currentRelative, item.Name())) 72 | } 73 | } 74 | 75 | // Sort to keep a static order 76 | sort.Strings(content.Folders) 77 | sort.Strings(content.Files) 78 | 79 | return 80 | } 81 | 82 | func (ds *FolderDataSource) GetNeighbor(current string) (nav *Navigation) { 83 | nav = &Navigation{} 84 | if current == "/" || current == "" { 85 | return 86 | } 87 | 88 | _, currentRelative := AbsolutePath(ds.root, current) 89 | nav.Current = currentRelative 90 | currentName := path.Base(current) 91 | 92 | baseAbs, baseRelative := AbsolutePath(ds.root, path.Dir(currentRelative)) 93 | nav.Parent = baseRelative 94 | 95 | folder, err := os.Open(baseAbs) 96 | if nil != err { 97 | return 98 | } 99 | defer func() { _ = folder.Close() }() 100 | 101 | fileInfo, err := folder.Readdir(-1) 102 | if nil != err { 103 | return 104 | } 105 | 106 | // Find all folders 107 | folders := make([]string, 0) 108 | for _, item := range fileInfo { 109 | // Filter out hidden files 110 | if strings.HasPrefix(item.Name(), ".") { 111 | continue 112 | } 113 | 114 | if item.IsDir() { 115 | folders = append(folders, item.Name()) 116 | } 117 | } 118 | 119 | // Sort to keep a static order 120 | sort.Strings(folders) 121 | 122 | for i, val := range folders { 123 | if val == currentName { 124 | if i-1 >= 0 { 125 | nav.Prev = path.Join(baseRelative, folders[i-1]) 126 | } 127 | if i+1 < len(folders) { 128 | nav.Next = path.Join(baseRelative, folders[i+1]) 129 | } 130 | return 131 | } 132 | } 133 | return 134 | } 135 | 136 | func (ds *FolderDataSource) Stat(filePath string) *FileStat { 137 | result := &FileStat{ 138 | Exists: false, 139 | IsFile: false, 140 | } 141 | 142 | fullPath, _ := AbsolutePath(ds.root, filePath) 143 | if !strings.HasPrefix(fullPath, ds.root) { 144 | return result 145 | } 146 | 147 | if fileInfo, err := os.Stat(fullPath); nil == err { 148 | result.Exists = true 149 | result.IsFile = !fileInfo.IsDir() 150 | } 151 | 152 | return result 153 | } 154 | -------------------------------------------------------------------------------- /app/datasource/lmdb.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "log" 5 | "path" 6 | "sort" 7 | 8 | "github.com/bmatsuo/lmdb-go/lmdb" 9 | "github.com/schollz/progressbar/v3" 10 | "haoyu.love/ImageServer/app/filter" 11 | ) 12 | 13 | type LmdbDataSource struct { 14 | root string 15 | filter *filter.Filter 16 | lmdbEnv *lmdb.Env 17 | lmdbDbi lmdb.DBI 18 | lmdbFileTree *Node 19 | } 20 | 21 | func NewLmdbDataSource(filePath string, flt *filter.Filter) *LmdbDataSource { 22 | ds := &LmdbDataSource{root: filePath, filter: flt} 23 | ds.scan() 24 | return ds 25 | } 26 | 27 | func (ds *LmdbDataSource) scan() { 28 | ds.lmdbFileTree = NewRoot() 29 | 30 | ds.lmdbEnv, _ = lmdb.NewEnv() 31 | _ = ds.lmdbEnv.SetMaxDBs(1) 32 | _ = ds.lmdbEnv.SetMapSize(1 << 42) 33 | err := ds.lmdbEnv.Open(ds.root, 0, 0644) 34 | 35 | if nil != err { 36 | panic(err) 37 | } 38 | err = ds.lmdbEnv.Update(func(txn *lmdb.Txn) (err error) { 39 | ds.lmdbDbi, err = txn.OpenRoot(lmdb.Create) 40 | return 41 | }) 42 | if nil != err { 43 | panic(err) 44 | } 45 | 46 | log.Printf("Scan database %s", ds.root) 47 | 48 | bar := progressbar.Default(-1, "Scanning") 49 | callback := func(itemPath string) { 50 | _ = bar.Add(1) 51 | } 52 | 53 | counter := 0 54 | _ = ds.lmdbEnv.View(func(txn *lmdb.Txn) (err error) { 55 | cur, err := txn.OpenCursor(ds.lmdbDbi) 56 | if err != nil { 57 | return err 58 | } 59 | defer cur.Close() 60 | 61 | for { 62 | k, _, err := cur.Get(nil, nil, lmdb.Next) 63 | if lmdb.IsNotFound(err) { 64 | return nil 65 | } 66 | if err != nil { 67 | return err 68 | } 69 | 70 | ds.lmdbFileTree.Add(string(k)) 71 | callback(string(k)) 72 | counter += 1 73 | } 74 | }) 75 | log.Printf("Scan Done! %d records scan", counter) 76 | } 77 | 78 | func (ds *LmdbDataSource) GetFile(filePath string) (data []byte, err error) { 79 | _ = ds.lmdbEnv.View(func(txn *lmdb.Txn) (err error) { 80 | data, err = txn.Get(ds.lmdbDbi, []byte(filePath[1:])) 81 | return nil 82 | }) 83 | 84 | return data, err 85 | } 86 | 87 | func (ds *LmdbDataSource) GetFolder(current string) (content FolderContent, err error) { 88 | content = FolderContent{ 89 | Name: current, 90 | Folders: []string{}, 91 | Files: []string{}, 92 | } 93 | 94 | node, err := ds.lmdbFileTree.GetChild(current) 95 | if nil != err { 96 | return content, err 97 | } 98 | 99 | basePath := node.GetAbsolutePath() 100 | for _, v := range node.Children { 101 | pa := path.Join(basePath, v.Name) 102 | if v.IsFile { 103 | content.Files = append(content.Files, pa) 104 | } else { 105 | content.Folders = append(content.Folders, pa) 106 | } 107 | } 108 | 109 | // Sort to keep a static order 110 | sort.Strings(content.Folders) 111 | sort.Strings(content.Files) 112 | 113 | return content, nil 114 | } 115 | 116 | func (ds *LmdbDataSource) GetNeighbor(current string) (nav *Navigation) { 117 | nav = &Navigation{} 118 | if "/" == current || "" == current { 119 | return 120 | } 121 | 122 | node, err := ds.lmdbFileTree.GetChild(current) 123 | if nil != err { 124 | return 125 | } 126 | nav.Current = node.GetAbsolutePath() 127 | 128 | parent := node.Parent 129 | if nil != node.Parent { 130 | nav.Parent = parent.GetAbsolutePath() 131 | } 132 | 133 | folders := make([]*Node, 0) 134 | for _, v := range parent.Children { 135 | if !v.IsFile { 136 | folders = append(folders, v) 137 | } 138 | } 139 | sort.Slice(folders, func(i, j int) bool { 140 | return folders[i].Name < folders[j].Name 141 | }) 142 | 143 | for i, val := range folders { 144 | if val.Name == node.Name { 145 | if i-1 >= 0 { 146 | nav.Prev = folders[i-1].GetAbsolutePath() 147 | } 148 | if i+1 < len(folders) { 149 | nav.Next = folders[i+1].GetAbsolutePath() 150 | } 151 | return 152 | } 153 | } 154 | return 155 | } 156 | 157 | func (ds *LmdbDataSource) Stat(filePath string) *FileStat { 158 | result := &FileStat{ 159 | Exists: false, 160 | IsFile: false, 161 | } 162 | 163 | if node, err := ds.lmdbFileTree.GetChild(filePath); nil == err { 164 | result.Exists = true 165 | result.IsFile = node.IsFile 166 | } 167 | 168 | return result 169 | } 170 | -------------------------------------------------------------------------------- /app/datasource/text_file.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "bufio" 5 | "encoding/csv" 6 | "io" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/schollz/progressbar/v3" 13 | "haoyu.love/ImageServer/app/filter" 14 | ) 15 | 16 | type TextFileDataSource struct { 17 | root string 18 | 19 | filter *filter.Filter 20 | column int 21 | data []string 22 | } 23 | 24 | func NewTextFileDataSource(root string, flt *filter.Filter, column int) *TextFileDataSource { 25 | ds := &TextFileDataSource{root: root, filter: flt, column: column} 26 | ds.scan() 27 | return ds 28 | } 29 | 30 | func (ds *TextFileDataSource) GetFile(_ string) ([]byte, error) { 31 | return nil, nil 32 | } 33 | 34 | func (ds *TextFileDataSource) GetFolder(_ string) (content FolderContent, err error) { 35 | content = FolderContent{ 36 | Name: "", 37 | Folders: []string{}, 38 | Files: ds.data, 39 | } 40 | return content, nil 41 | } 42 | 43 | func (ds *TextFileDataSource) GetNeighbor(_ string) (nav *Navigation) { 44 | nav = &Navigation{} 45 | return nav 46 | } 47 | 48 | func (ds *TextFileDataSource) scan() { 49 | log.Printf("Scan column %d of file %s", ds.column, ds.root) 50 | bar := progressbar.Default(-1, "Scanning") 51 | callback := func(path string) { 52 | _ = bar.Add(1) 53 | } 54 | 55 | ext := strings.ToLower(filepath.Ext(ds.root)) 56 | 57 | f, err := os.Open(ds.root) 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | 62 | defer func() { _ = f.Close() }() 63 | 64 | if ".csv" == ext || ".tsv" == ext { 65 | ds.data = ReadXsv(f, ext, ds.column, ds.filter, callback) 66 | } else { 67 | ds.data = ReadText(f, ds.filter, callback) 68 | } 69 | 70 | log.Printf("Done! %d records scan", len(ds.data)) 71 | } 72 | 73 | func (ds *TextFileDataSource) Stat(filePath string) *FileStat { 74 | result := &FileStat{ 75 | Exists: false, 76 | IsFile: false, 77 | } 78 | if "" == filePath || "/" == filePath { 79 | result.Exists = true 80 | } 81 | 82 | return result 83 | } 84 | 85 | func ReadXsv(f *os.File, ext string, column int, flt *filter.Filter, callback func(path string)) []string { 86 | result := make([]string, 0) 87 | 88 | csvReader := csv.NewReader(f) 89 | if ".tsv" == ext { 90 | csvReader.Comma = '\t' 91 | } 92 | 93 | for { 94 | rec, err := csvReader.Read() 95 | if err == io.EOF { 96 | break 97 | } 98 | if len(rec) < column { 99 | continue 100 | } 101 | result = append(result, (*flt).Extract(rec[column])...) 102 | 103 | if nil != callback { 104 | callback("") 105 | } 106 | } 107 | 108 | return result 109 | } 110 | 111 | func ReadText(f *os.File, flt *filter.Filter, callback func(path string)) []string { 112 | result := make([]string, 0) 113 | 114 | scanner := bufio.NewScanner(f) 115 | for scanner.Scan() { 116 | result = append(result, (*flt).Extract(scanner.Text())...) 117 | if nil != callback { 118 | callback("") 119 | } 120 | } 121 | 122 | return result 123 | } 124 | -------------------------------------------------------------------------------- /app/datasource/util.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | ) 7 | 8 | type FileStat struct { 9 | Exists bool 10 | IsFile bool 11 | } 12 | 13 | type Node struct { 14 | Name string 15 | IsFile bool 16 | Parent *Node 17 | Children map[string]*Node 18 | } 19 | 20 | func NewRoot() *Node { 21 | node := &Node{Name: "/", IsFile: false, Parent: nil, Children: make(map[string]*Node)} 22 | return node 23 | } 24 | 25 | func (node *Node) Add(name string) { 26 | namePart := strings.Split(name, "/") 27 | currNode := node 28 | for ith, k := range namePart { 29 | if ith == len(namePart)-1 { 30 | if _, ok := currNode.Children[k]; !ok { 31 | currNode.Children[k] = &Node{Name: k, IsFile: true, Parent: currNode, Children: make(map[string]*Node)} 32 | } else { 33 | currNode.Children[k].IsFile = true 34 | } 35 | } else { 36 | if _, ok := currNode.Children[k]; !ok { 37 | currNode.Children[k] = &Node{Name: k, IsFile: false, Parent: currNode, Children: make(map[string]*Node)} 38 | } 39 | currNode = currNode.Children[k] 40 | } 41 | } 42 | } 43 | 44 | func (node *Node) GetChild(name string) (*Node, error) { 45 | if strings.HasPrefix(name, "/") { 46 | name = name[1:] 47 | } 48 | current := node 49 | if "" != name { 50 | namePart := strings.Split(name, "/") 51 | for _, k := range namePart { 52 | if _, ok := current.Children[k]; !ok { 53 | return nil, nil 54 | } else { 55 | current = current.Children[k] 56 | } 57 | } 58 | } 59 | return current, nil 60 | } 61 | 62 | func (node *Node) GetAbsolutePath() (path string) { 63 | if node.Parent == nil { 64 | return node.Name 65 | } 66 | parent := node.Parent.GetAbsolutePath() 67 | if "/" == parent { 68 | return "/" + node.Name 69 | } 70 | return parent + "/" + node.Name 71 | } 72 | 73 | func IsTargetFile(file string, target ...map[string]struct{}) bool { 74 | ext := strings.ToLower(filepath.Ext(file)) 75 | for _, t := range target { 76 | if _, ok := t[ext]; ok { 77 | return true 78 | } 79 | } 80 | return false 81 | } 82 | 83 | func AbsolutePath(prefix, relative string) (string, string) { 84 | relativePart := strings.Trim(strings.TrimSpace(relative), "/") 85 | 86 | absolute := filepath.Join(prefix, relativePart) 87 | absolute, _ = filepath.Abs(absolute) 88 | relativeNew := "/" + relativePart 89 | 90 | return absolute, relativeNew 91 | } 92 | -------------------------------------------------------------------------------- /app/dtype.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | 7 | "haoyu.love/ImageServer/app/datasource" 8 | ) 9 | 10 | type Pagination struct { 11 | Current int 12 | Prev int 13 | Next int 14 | Total int 15 | Size int 16 | Url string 17 | Toc *datasource.FolderContent 18 | Content *[]datasource.FolderContent 19 | } 20 | 21 | func (p Pagination) URLPrev() string { 22 | if p.Prev <= 0 { 23 | return "#" 24 | } 25 | return p.offset(p.Prev) 26 | } 27 | 28 | func (p Pagination) URLNext() string { 29 | if p.Next <= 0 { 30 | return "#" 31 | } 32 | return p.offset(p.Next) 33 | } 34 | 35 | func (p Pagination) offset(page int) string { 36 | u, _ := url.Parse(p.Url) 37 | q, _ := url.ParseQuery(u.RawQuery) 38 | q.Set("p", strconv.Itoa(page)) 39 | u.RawQuery = q.Encode() 40 | return u.String() 41 | } 42 | -------------------------------------------------------------------------------- /app/filter/dtype.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | const ( 4 | PredefineDefault = "@DEFAULT" 5 | PredefineImageExt = "@IMAGE" 6 | PredefineVideoExt = "@VIDEO" 7 | PredefineAudioExt = "@AUDIO" 8 | ) 9 | 10 | type Filter interface { 11 | Filter(string) bool 12 | Extract(string) []string 13 | } 14 | -------------------------------------------------------------------------------- /app/filter/file_ext.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | DefaultAudioExt = []string{".mp3", ".wav", ".wma", ".ogg", ".flac"} 10 | DefaultImageExt = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".svg", ".webp", ".ico"} 11 | DefaultVideoExt = []string{".mp4", ".mkv", ".mov", ".wmv", ".flv", ".avi", ".rmvb", ".mpg", ".mpeg", ".m4v", ".3gp", ".3g2"} 12 | ) 13 | 14 | type FileExtFilter struct { 15 | ext map[string]interface{} 16 | } 17 | 18 | func NewFileExtFilter(ext string) *FileExtFilter { 19 | filter := &FileExtFilter{ext: make(map[string]interface{})} 20 | tmp := make([]string, 0) 21 | if PredefineDefault == ext || PredefineImageExt == ext { 22 | tmp = DefaultImageExt 23 | } else if PredefineVideoExt == ext { 24 | tmp = DefaultVideoExt 25 | } else if PredefineAudioExt == ext { 26 | tmp = DefaultAudioExt 27 | } else { 28 | tmp = strings.Split(strings.ToLower(ext), ",") 29 | } 30 | 31 | for _, v := range tmp { 32 | filter.ext[v] = struct{}{} 33 | } 34 | 35 | return filter 36 | } 37 | 38 | func (f *FileExtFilter) Filter(fileName string) bool { 39 | if 0 == len(f.ext) { 40 | return true 41 | } 42 | ext := strings.ToLower(filepath.Ext(fileName)) 43 | _, ok := f.ext[ext] 44 | return ok 45 | } 46 | 47 | func (f *FileExtFilter) Extract(line string) []string { 48 | return []string{line} 49 | } 50 | -------------------------------------------------------------------------------- /app/filter/json.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import "github.com/spyzhov/ajson" 4 | 5 | type JsonFilter struct { 6 | jsonPath string 7 | } 8 | 9 | func NewJsonFilter(jsonPath string) *JsonFilter { 10 | if "@DEFAULT" == jsonPath { 11 | jsonPath = "" 12 | } 13 | filter := &JsonFilter{jsonPath: jsonPath} 14 | return filter 15 | } 16 | 17 | func (f *JsonFilter) Filter(fileName string) bool { 18 | return true 19 | } 20 | 21 | func (f *JsonFilter) Extract(line string) []string { 22 | if "" == f.jsonPath { 23 | return []string{line} 24 | } 25 | 26 | var result []string 27 | 28 | root, err := ajson.Unmarshal([]byte(line)) 29 | if nil != err { 30 | return result 31 | } 32 | nodes, err := root.JSONPath(f.jsonPath) 33 | if nil != err { 34 | return result 35 | } 36 | for _, node := range nodes { 37 | s, err := node.GetString() 38 | if nil != err { 39 | continue 40 | } 41 | result = append(result, s) 42 | } 43 | 44 | return result 45 | } 46 | -------------------------------------------------------------------------------- /app/filter/none.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | type NoFilter struct { 4 | } 5 | 6 | func NewNoFilter() *NoFilter { 7 | filter := &NoFilter{} 8 | return filter 9 | } 10 | 11 | func (f *NoFilter) Filter(fileName string) bool { 12 | return true 13 | } 14 | 15 | func (f *NoFilter) Extract(line string) []string { 16 | return []string{line} 17 | } 18 | -------------------------------------------------------------------------------- /app/handler.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | 8 | "github.com/gabriel-vasile/mimetype" 9 | "github.com/gin-gonic/gin" 10 | "haoyu.love/ImageServer/app/datasource" 11 | ) 12 | 13 | type ImageServerHandler struct { 14 | data datasource.DataSource 15 | } 16 | 17 | func NewImageServerHandler(data *datasource.DataSource) *ImageServerHandler { 18 | handler := &ImageServerHandler{data: *data} 19 | return handler 20 | } 21 | 22 | func (handler *ImageServerHandler) Handle(c *gin.Context) { 23 | name := c.Param("path") 24 | 25 | fileInfo := handler.data.Stat(name) 26 | 27 | if !fileInfo.Exists { 28 | c.String(http.StatusNotFound, fmt.Sprintf("Path %s not found", name)) 29 | return 30 | } else { 31 | if fileInfo.IsFile { 32 | handler.processFile(c) 33 | } else { 34 | handler.processFolder(c) 35 | } 36 | } 37 | 38 | } 39 | 40 | func (handler *ImageServerHandler) processFile(c *gin.Context) { 41 | name := c.Param("path") 42 | content, err := handler.data.GetFile(name) 43 | if nil != err { 44 | c.String(http.StatusInternalServerError, "Internal Server Error: %s", err) 45 | } 46 | c.Data(http.StatusOK, mimetype.Detect(content).String(), content) 47 | } 48 | 49 | func (handler *ImageServerHandler) processFolder(c *gin.Context) { 50 | pageNumStr := c.DefaultQuery("p", "1") 51 | pageNum, err := strconv.Atoi(pageNumStr) 52 | if err != nil { 53 | pageNum = 1 54 | } 55 | 56 | folderNames := []string{c.Param("path")} 57 | for _, pa := range c.QueryArray("c") { 58 | stat := handler.data.Stat(pa) 59 | if !stat.Exists || stat.IsFile { 60 | continue 61 | } 62 | folderNames = append(folderNames, pa) 63 | } 64 | 65 | contents := make([]datasource.FolderContent, 0) 66 | for _, name := range folderNames { 67 | content, err := handler.data.GetFolder(name) 68 | if nil != err { 69 | c.String(http.StatusInternalServerError, "Internal Server Error: %s", err) 70 | } 71 | contents = append(contents, content) 72 | } 73 | 74 | aligned := DeduplicateFolderContent(&contents) 75 | pagination := Paginate(&aligned, &contents, *PageSize, pageNum, GetCurrentUrl(c)) 76 | 77 | navigation := datasource.Navigation{} 78 | if len(contents) == 1 { 79 | content := contents[0] 80 | navigation = *handler.data.GetNeighbor(contents[0].Name) 81 | 82 | c.HTML(http.StatusOK, "list.html", gin.H{ 83 | "content": content, 84 | "pagination": pagination, 85 | "navigation": navigation, 86 | }) 87 | } else { 88 | c.HTML(http.StatusOK, "compare.html", gin.H{ 89 | "contents": contents, 90 | "pagination": pagination, 91 | "navigation": navigation, 92 | "aligned": aligned, 93 | "columnWidth": 90. / len(contents), // Since the label column will take 10% of width 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/template.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "html/template" 7 | "net/url" 8 | "path" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | var TemplateFunction = template.FuncMap{ 14 | "pathToName": func(p string) string { 15 | return path.Base(p) 16 | }, 17 | "lastOne": func(arr []interface{}) interface{} { 18 | if len(arr) == 0 { 19 | return nil 20 | } 21 | return arr[len(arr)-1] 22 | }, 23 | "breadCrumb": func(root string) []string { 24 | crumb := make([]string, 0) 25 | root = strings.Trim(root, "/") 26 | if root != "" { 27 | sps := strings.Split(root, "/") 28 | for i := range sps { 29 | crumb = append(crumb, "/"+strings.Join(sps[:i+1], "/")) 30 | } 31 | } 32 | return crumb 33 | }, 34 | "stringToMD5": func(s string) string { 35 | hash := md5.Sum([]byte(s)) 36 | return hex.EncodeToString(hash[:]) 37 | }, 38 | "pageNum": func(rawUrl string, page int) string { 39 | if page <= 0 { 40 | return "#" 41 | } 42 | 43 | u, _ := url.Parse(rawUrl) 44 | q, _ := url.ParseQuery(u.RawQuery) 45 | q.Set("p", strconv.Itoa(page)) 46 | u.RawQuery = q.Encode() 47 | return u.String() 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /app/update.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | var UpdateAPI = "https://api.github.com/repos/jinyu121/ImageServer/releases/latest" 14 | 15 | func CheckUpdate(current string) { 16 | defer func() { 17 | if r := recover(); r != nil { 18 | log.Println("Failed to check update") 19 | } 20 | }() 21 | 22 | if gin.ReleaseMode != gin.Mode() { 23 | return 24 | } 25 | resp, err := http.Get(UpdateAPI) 26 | if nil != err { 27 | return 28 | } 29 | defer func() { _ = resp.Body.Close() }() 30 | 31 | body, err := io.ReadAll(resp.Body) 32 | if err != nil { 33 | return 34 | } 35 | var result map[string]interface{} 36 | err = json.Unmarshal(body, &result) 37 | if nil != err { 38 | return 39 | } 40 | 41 | if result["prerelease"].(bool) { 42 | return 43 | } 44 | tagName := result["tag_name"].(string) 45 | releaseURL := result["html_url"].(string) 46 | if tagName != current { 47 | fmt.Printf("New version %s is available, please update: %s\n", tagName, releaseURL) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/util.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "net/url" 7 | "path/filepath" 8 | "sort" 9 | 10 | "github.com/gin-gonic/gin" 11 | "haoyu.love/ImageServer/app/datasource" 12 | ) 13 | 14 | // Paginate paginates the given content in place, and returns the Pagination instance 15 | func Paginate( 16 | ref *datasource.FolderContent, 17 | contents *[]datasource.FolderContent, 18 | size int, current int, url string) Pagination { 19 | 20 | page := Pagination{Current: 1, Prev: -1, Next: -1, Total: 1, Size: size, Toc: ref, Url: url} 21 | 22 | numFolders, numFiles := len(ref.Folders), len(ref.Files) 23 | 24 | if size <= 0 { 25 | return page 26 | } 27 | // Ensure all element are not empty 28 | itemsCount := numFolders + numFiles 29 | if itemsCount == 0 { 30 | return page 31 | } 32 | 33 | // Calculate pagination 34 | page.Current = current 35 | page.Total = (itemsCount + size - 1) / size 36 | 37 | if page.Current < 1 { 38 | page.Current = 1 39 | } 40 | if page.Current > page.Total { 41 | page.Current = page.Total 42 | } 43 | offsetStart := (page.Current - 1) * page.Size 44 | offsetEnd := page.Current * page.Size 45 | if offsetEnd > itemsCount { 46 | offsetEnd = itemsCount 47 | } 48 | 49 | if page.Total > 1 { 50 | if page.Current > 1 { 51 | page.Prev = page.Current - 1 52 | } 53 | if page.Current < page.Total { 54 | page.Next = page.Current + 1 55 | } 56 | } 57 | 58 | // Limit folders and files 59 | if offsetStart < numFolders { 60 | if offsetEnd < numFolders { 61 | tmpStart := offsetStart 62 | tmpEnd := offsetEnd 63 | ref.Folders = ref.Folders[tmpStart:tmpEnd] 64 | } else { 65 | tmpStart := offsetStart 66 | tmpEnd := numFolders 67 | ref.Folders = ref.Folders[tmpStart:tmpEnd] 68 | 69 | tmpStart = 0 70 | tmpEnd = offsetEnd - numFolders 71 | ref.Files = ref.Files[tmpStart:tmpEnd] 72 | } 73 | } else { 74 | tmpStart := offsetStart - numFolders 75 | tmpEnd := offsetEnd - numFolders 76 | ref.Folders = make([]string, 0) 77 | ref.Files = ref.Files[tmpStart:tmpEnd] 78 | } 79 | 80 | // Align content 81 | AlignContent(contents, ref) 82 | page.Content = contents 83 | 84 | return page 85 | } 86 | 87 | // DeduplicateFolderContent merges the content of multiple FolderContent instances 88 | func DeduplicateFolderContent(contents *[]datasource.FolderContent) datasource.FolderContent { 89 | // Deduplicate 90 | folderSet := make(map[string]struct{}) 91 | fileSet := make(map[string]struct{}) 92 | for _, content := range *contents { 93 | for _, folder := range content.Folders { 94 | name := filepath.Base(folder) 95 | folderSet[name] = struct{}{} 96 | } 97 | for _, file := range content.Files { 98 | name := filepath.Base(file) 99 | fileSet[name] = struct{}{} 100 | } 101 | } 102 | 103 | // Sort 104 | folders := make([]string, 0, len(folderSet)) 105 | for folder := range folderSet { 106 | folders = append(folders, folder) 107 | } 108 | sort.Strings(folders) 109 | 110 | files := make([]string, 0, len(fileSet)) 111 | for file := range fileSet { 112 | files = append(files, file) 113 | } 114 | sort.Strings(files) 115 | 116 | return datasource.FolderContent{Name: "", Folders: folders, Files: files} 117 | } 118 | 119 | // AlignContent aligns the content of folders and files in-place according to the given content 120 | func AlignContent(contents *[]datasource.FolderContent, ref *datasource.FolderContent) { 121 | contents_ := *contents 122 | 123 | // Align 124 | for i := range contents_ { 125 | contents_[i].Folders = align(contents_[i].Folders, ref.Folders) 126 | contents_[i].Files = align(contents_[i].Files, ref.Files) 127 | } 128 | 129 | } 130 | 131 | // align the array to the given array of strings. 132 | // If something is not in the given array, a blank will be added in that place. 133 | func align(items, ref []string) []string { 134 | result := make([]string, len(ref)) 135 | tmp := make(map[string]string) 136 | for _, item := range items { 137 | name := filepath.Base(item) 138 | tmp[name] = item 139 | } 140 | for i, item := range ref { 141 | name := filepath.Base(item) 142 | if val, ok := tmp[name]; ok { 143 | result[i] = val 144 | } else { 145 | result[i] = "" 146 | } 147 | } 148 | return result 149 | } 150 | 151 | func GetIPAddress() []net.IP { 152 | result := make([]net.IP, 0) 153 | 154 | iFaces, err := net.Interfaces() 155 | if nil != err { 156 | return result 157 | } 158 | 159 | for _, face := range iFaces { 160 | if addresses, err := face.Addrs(); nil == err { 161 | for _, addr := range addresses { 162 | var ip net.IP 163 | switch v := addr.(type) { 164 | case *net.IPNet: 165 | ip = v.IP 166 | case *net.IPAddr: 167 | ip = v.IP 168 | default: 169 | continue 170 | } 171 | 172 | if !ip.IsUnspecified() && 173 | !ip.IsMulticast() && 174 | !ip.IsInterfaceLocalMulticast() && 175 | !ip.IsLinkLocalMulticast() && 176 | !ip.IsLinkLocalUnicast() { 177 | result = append(result, ip) 178 | } 179 | } 180 | } 181 | } 182 | 183 | sort.Slice(result, func(i, j int) bool { 184 | return bytes.Compare(result[i], result[j]) < 0 185 | }) 186 | 187 | return result 188 | } 189 | 190 | func GetCurrentUrl(c *gin.Context) string { 191 | p := c.Request.URL.Path 192 | q := c.Request.URL.Query() 193 | u, _ := url.Parse(p) 194 | u.RawQuery = q.Encode() 195 | return u.String() 196 | } 197 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module haoyu.love/ImageServer 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/bmatsuo/lmdb-go v1.8.0 7 | github.com/gabriel-vasile/mimetype v1.4.2 8 | github.com/gin-gonic/gin v1.9.0 9 | github.com/schollz/progressbar/v3 v3.13.1 10 | github.com/spyzhov/ajson v0.8.0 11 | ) 12 | 13 | require ( 14 | github.com/bytedance/sonic v1.8.0 // indirect 15 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 16 | github.com/gin-contrib/sse v0.1.0 // indirect 17 | github.com/go-playground/locales v0.14.1 // indirect 18 | github.com/go-playground/universal-translator v0.18.1 // indirect 19 | github.com/go-playground/validator/v10 v10.11.2 // indirect 20 | github.com/goccy/go-json v0.10.0 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect 23 | github.com/leodido/go-urn v1.2.1 // indirect 24 | github.com/mattn/go-isatty v0.0.17 // indirect 25 | github.com/mattn/go-runewidth v0.0.14 // indirect 26 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 27 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect 28 | github.com/modern-go/reflect2 v1.0.2 // indirect 29 | github.com/pelletier/go-toml/v2 v2.0.6 // indirect 30 | github.com/rivo/uniseg v0.2.0 // indirect 31 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 32 | github.com/ugorji/go/codec v1.2.9 // indirect 33 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect 34 | golang.org/x/crypto v0.5.0 // indirect 35 | golang.org/x/net v0.8.0 // indirect 36 | golang.org/x/sys v0.6.0 // indirect 37 | golang.org/x/term v0.6.0 // indirect 38 | golang.org/x/text v0.8.0 // indirect 39 | google.golang.org/protobuf v1.28.1 // indirect 40 | gopkg.in/yaml.v3 v3.0.1 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bmatsuo/lmdb-go v1.8.0 h1:ohf3Q4xjXZBKh4AayUY4bb2CXuhRAI8BYGlJq08EfNA= 2 | github.com/bmatsuo/lmdb-go v1.8.0/go.mod h1:wWPZmKdOAZsl4qOqkowQ1aCrFie1HU8gWloHMCeAUdM= 3 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 4 | github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= 5 | github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 6 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 7 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 8 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 13 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 14 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 15 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 16 | github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= 17 | github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= 18 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 19 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 20 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 21 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 22 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 23 | github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= 24 | github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= 25 | github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= 26 | github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 27 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 28 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 29 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 30 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 31 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 32 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 33 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= 34 | github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= 35 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 36 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 37 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 38 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 39 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 40 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 41 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 42 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 43 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 44 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 45 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 46 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 47 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 48 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 49 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 50 | github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= 51 | github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= 52 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 53 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 54 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 55 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 56 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 57 | github.com/schollz/progressbar/v3 v3.13.1 h1:o8rySDYiQ59Mwzy2FELeHY5ZARXZTVJC7iHD6PEFUiE= 58 | github.com/schollz/progressbar/v3 v3.13.1/go.mod h1:xvrbki8kfT1fzWzBT/UZd9L6GA+jdL7HAgq2RFnO6fQ= 59 | github.com/spyzhov/ajson v0.8.0 h1:sFXyMbi4Y/BKjrsfkUZHSjA2JM1184enheSjjoT/zCc= 60 | github.com/spyzhov/ajson v0.8.0/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzyqMuVA= 61 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 62 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 63 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 64 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 65 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 66 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 67 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 68 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 69 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 70 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 71 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 72 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 73 | github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= 74 | github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 75 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= 76 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 77 | golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= 78 | golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= 79 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 80 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 81 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 82 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 83 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 84 | golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= 85 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 86 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 87 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 88 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 89 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 90 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 91 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= 92 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 93 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 95 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 96 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 97 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 98 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 99 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | 12 | "github.com/gin-gonic/gin" 13 | "haoyu.love/ImageServer/app" 14 | ) 15 | 16 | var ( 17 | Version = "Unknown" 18 | Build = "Unknown" 19 | ) 20 | 21 | var ( 22 | //go:embed static templates 23 | assets embed.FS 24 | ) 25 | 26 | func main() { 27 | log.Println("ImageServer", Version, "Build", Build) 28 | 29 | if "Unknown" != Version { 30 | gin.SetMode(gin.ReleaseMode) 31 | 32 | // Only check updates in release mode 33 | go app.CheckUpdate(Version) 34 | } 35 | 36 | app.InitFlag() 37 | 38 | appRouter := app.InitServer(assets) 39 | 40 | go func() { 41 | srv := &http.Server{ 42 | Addr: fmt.Sprintf(":%d", *app.Port), 43 | Handler: appRouter, 44 | } 45 | // service connections 46 | if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { 47 | log.Fatalf("Error: %s\n", err) 48 | } 49 | }() 50 | 51 | listenOn := app.GetIPAddress() 52 | if len(listenOn) > 0 { 53 | log.Println("Listening on these addresses:") 54 | for _, addr := range listenOn { 55 | if addr.To4() != nil { 56 | log.Printf("\thttp://%s:%d\n", addr, *app.Port) 57 | } else { 58 | log.Printf("\thttp://[%s]:%d\n", addr, *app.Port) 59 | } 60 | } 61 | } 62 | 63 | quit := make(chan os.Signal) 64 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 65 | <-quit 66 | log.Println("Bye~") 67 | } 68 | -------------------------------------------------------------------------------- /static/css/lightbox.min.css: -------------------------------------------------------------------------------- 1 | .lb-loader,.lightbox{text-align:center;line-height:0;position:absolute;left:0}body.lb-disable-scrolling{overflow:hidden}.lightboxOverlay{position:absolute;top:0;left:0;z-index:9999;background-color:#000;filter:alpha(Opacity=80);opacity:.8;display:none}.lightbox{width:100%;z-index:10000;font-weight:400;outline:0}.lightbox .lb-image{display:block;height:auto;max-width:inherit;max-height:none;border-radius:3px;border:4px solid #fff}.lightbox a img{border:none}.lb-outerContainer{position:relative;width:250px;height:250px;margin:0 auto;border-radius:4px;background-color:#fff}.lb-outerContainer:after{content:"";display:table;clear:both}.lb-loader{top:43%;height:25%;width:100%}.lb-cancel{display:block;width:32px;height:32px;margin:0 auto;background:url(../images/loading.gif) no-repeat}.lb-nav{position:absolute;top:0;left:0;height:100%;width:100%;z-index:10}.lb-container>.nav{left:0}.lb-nav a{outline:0;background-image:url(data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==)}.lb-next,.lb-prev{height:100%;cursor:pointer;display:block}.lb-nav a.lb-prev{width:34%;left:0;float:left;background:url(../images/prev.png) left 48% no-repeat;filter:alpha(Opacity=0);opacity:0;-webkit-transition:opacity .6s;-moz-transition:opacity .6s;-o-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-prev:hover{filter:alpha(Opacity=100);opacity:1}.lb-nav a.lb-next{width:64%;right:0;float:right;background:url(../images/next.png) right 48% no-repeat;filter:alpha(Opacity=0);opacity:0;-webkit-transition:opacity .6s;-moz-transition:opacity .6s;-o-transition:opacity .6s;transition:opacity .6s}.lb-nav a.lb-next:hover{filter:alpha(Opacity=100);opacity:1}.lb-dataContainer{margin:0 auto;padding-top:5px;width:100%;border-bottom-left-radius:4px;border-bottom-right-radius:4px}.lb-dataContainer:after{content:"";display:table;clear:both}.lb-data{padding:0 4px;color:#ccc}.lb-data .lb-details{width:85%;float:left;text-align:left;line-height:1.1em}.lb-data .lb-caption{font-size:13px;font-weight:700;line-height:1em}.lb-data .lb-caption a{color:#4ae}.lb-data .lb-number{display:block;clear:left;padding-bottom:1em;font-size:12px;color:#999}.lb-data .lb-close{display:block;float:right;width:30px;height:30px;background:url(../images/close.png) top right no-repeat;text-align:right;outline:0;filter:alpha(Opacity=70);opacity:.7;-webkit-transition:opacity .2s;-moz-transition:opacity .2s;-o-transition:opacity .2s;transition:opacity .2s}.lb-data .lb-close:hover{cursor:pointer;filter:alpha(Opacity=100);opacity:1} -------------------------------------------------------------------------------- /static/images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinyu121/ImageServer/3a749007a7df28fa675815a5fe2aec1734e8bbb9/static/images/close.png -------------------------------------------------------------------------------- /static/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinyu121/ImageServer/3a749007a7df28fa675815a5fe2aec1734e8bbb9/static/images/loading.gif -------------------------------------------------------------------------------- /static/images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinyu121/ImageServer/3a749007a7df28fa675815a5fe2aec1734e8bbb9/static/images/logo.jpg -------------------------------------------------------------------------------- /static/images/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinyu121/ImageServer/3a749007a7df28fa675815a5fe2aec1734e8bbb9/static/images/next.png -------------------------------------------------------------------------------- /static/images/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jinyu121/ImageServer/3a749007a7df28fa675815a5fe2aec1734e8bbb9/static/images/prev.png -------------------------------------------------------------------------------- /static/js/bootstrap.bundle.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v5.1.3 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){"use strict";const t="transitionend",e=t=>{let e=t.getAttribute("data-bs-target");if(!e||"#"===e){let i=t.getAttribute("href");if(!i||!i.includes("#")&&!i.startsWith("."))return null;i.includes("#")&&!i.startsWith("#")&&(i=`#${i.split("#")[1]}`),e=i&&"#"!==i?i.trim():null}return e},i=t=>{const i=e(t);return i&&document.querySelector(i)?i:null},n=t=>{const i=e(t);return i?document.querySelector(i):null},s=e=>{e.dispatchEvent(new Event(t))},o=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(t):null,a=(t,e,i)=>{Object.keys(i).forEach((n=>{const s=i[n],r=e[n],a=r&&o(r)?"element":null==(l=r)?`${l}`:{}.toString.call(l).match(/\s([a-z]+)/i)[1].toLowerCase();var l;if(!new RegExp(s).test(a))throw new TypeError(`${t.toUpperCase()}: Option "${n}" provided type "${a}" but expected type "${s}".`)}))},l=t=>!(!o(t)||0===t.getClientRects().length)&&"visible"===getComputedStyle(t).getPropertyValue("visibility"),c=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),h=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?h(t.parentNode):null},d=()=>{},u=t=>{t.offsetHeight},f=()=>{const{jQuery:t}=window;return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},p=[],m=()=>"rtl"===document.documentElement.dir,g=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},"loading"===document.readyState?(p.length||document.addEventListener("DOMContentLoaded",(()=>{p.forEach((t=>t()))})),p.push(e)):e()},_=t=>{"function"==typeof t&&t()},b=(e,i,n=!0)=>{if(!n)return void _(e);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(i)+5;let r=!1;const a=({target:n})=>{n===i&&(r=!0,i.removeEventListener(t,a),_(e))};i.addEventListener(t,a),setTimeout((()=>{r||s(i)}),o)},v=(t,e,i,n)=>{let s=t.indexOf(e);if(-1===s)return t[!i&&n?t.length-1:0];const o=t.length;return s+=i?1:-1,n&&(s=(s+o)%o),t[Math.max(0,Math.min(s,o-1))]},y=/[^.]*(?=\..*)\.|.*/,w=/\..*/,E=/::\d+$/,A={};let T=1;const O={mouseenter:"mouseover",mouseleave:"mouseout"},C=/^(mouseenter|mouseleave)/i,k=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${T++}`||t.uidEvent||T++}function x(t){const e=L(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function D(t,e,i=null){const n=Object.keys(t);for(let s=0,o=n.length;sfunction(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};n?n=t(n):i=t(i)}const[o,r,a]=S(e,i,n),l=x(t),c=l[a]||(l[a]={}),h=D(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=L(r,e.replace(y,"")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(let a=o.length;a--;)if(o[a]===r)return s.delegateTarget=r,n.oneOff&&j.off(t,s.type,e,i),i.apply(r,[s]);return null}}(t,i,n):function(t,e){return function i(n){return n.delegateTarget=t,i.oneOff&&j.off(t,n.type,e),e.apply(t,[n])}}(t,i);u.delegationSelector=o?i:null,u.originalHandler=r,u.oneOff=s,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function I(t,e,i,n,s){const o=D(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function P(t){return t=t.replace(w,""),O[t]||t}const j={on(t,e,i,n){N(t,e,i,n,!1)},one(t,e,i,n){N(t,e,i,n,!0)},off(t,e,i,n){if("string"!=typeof e||!t)return;const[s,o,r]=S(e,i,n),a=r!==e,l=x(t),c=e.startsWith(".");if(void 0!==o){if(!l||!l[r])return;return void I(t,l,r,o,s?i:null)}c&&Object.keys(l).forEach((i=>{!function(t,e,i,n){const s=e[i]||{};Object.keys(s).forEach((o=>{if(o.includes(n)){const n=s[o];I(t,e,i,n.originalHandler,n.delegationSelector)}}))}(t,l,i,e.slice(1))}));const h=l[r]||{};Object.keys(h).forEach((i=>{const n=i.replace(E,"");if(!a||e.includes(n)){const e=h[i];I(t,l,r,e.originalHandler,e.delegationSelector)}}))},trigger(t,e,i){if("string"!=typeof e||!t)return null;const n=f(),s=P(e),o=e!==s,r=k.has(s);let a,l=!0,c=!0,h=!1,d=null;return o&&n&&(a=n.Event(e,i),n(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),h=a.isDefaultPrevented()),r?(d=document.createEvent("HTMLEvents"),d.initEvent(s,l,!0)):d=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach((t=>{Object.defineProperty(d,t,{get:()=>i[t]})})),h&&d.preventDefault(),c&&t.dispatchEvent(d),d.defaultPrevented&&void 0!==a&&a.preventDefault(),d}},M=new Map,H={set(t,e,i){M.has(t)||M.set(t,new Map);const n=M.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>M.has(t)&&M.get(t).get(e)||null,remove(t,e){if(!M.has(t))return;const i=M.get(t);i.delete(e),0===i.size&&M.delete(t)}};class B{constructor(t){(t=r(t))&&(this._element=t,H.set(this._element,this.constructor.DATA_KEY,this))}dispose(){H.remove(this._element,this.constructor.DATA_KEY),j.off(this._element,this.constructor.EVENT_KEY),Object.getOwnPropertyNames(this).forEach((t=>{this[t]=null}))}_queueCallback(t,e,i=!0){b(t,e,i)}static getInstance(t){return H.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,"object"==typeof e?e:null)}static get VERSION(){return"5.1.3"}static get NAME(){throw new Error('You have to implement the static method "NAME", for each component!')}static get DATA_KEY(){return`bs.${this.NAME}`}static get EVENT_KEY(){return`.${this.DATA_KEY}`}}const R=(t,e="hide")=>{const i=`click.dismiss${t.EVENT_KEY}`,s=t.NAME;j.on(document,i,`[data-bs-dismiss="${s}"]`,(function(i){if(["A","AREA"].includes(this.tagName)&&i.preventDefault(),c(this))return;const o=n(this)||this.closest(`.${s}`);t.getOrCreateInstance(o)[e]()}))};class W extends B{static get NAME(){return"alert"}close(){if(j.trigger(this._element,"close.bs.alert").defaultPrevented)return;this._element.classList.remove("show");const t=this._element.classList.contains("fade");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),j.trigger(this._element,"closed.bs.alert"),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=W.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}R(W,"close"),g(W);const $='[data-bs-toggle="button"]';class z extends B{static get NAME(){return"button"}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(t){return this.each((function(){const e=z.getOrCreateInstance(this);"toggle"===t&&e[t]()}))}}function q(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function F(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}j.on(document,"click.bs.button.data-api",$,(t=>{t.preventDefault();const e=t.target.closest($);z.getOrCreateInstance(e).toggle()})),g(z);const U={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${F(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${F(e)}`)},getDataAttributes(t){if(!t)return{};const e={};return Object.keys(t.dataset).filter((t=>t.startsWith("bs"))).forEach((i=>{let n=i.replace(/^bs/,"");n=n.charAt(0).toLowerCase()+n.slice(1,n.length),e[n]=q(t.dataset[i])})),e},getDataAttribute:(t,e)=>q(t.getAttribute(`data-bs-${F(e)}`)),offset(t){const e=t.getBoundingClientRect();return{top:e.top+window.pageYOffset,left:e.left+window.pageXOffset}},position:t=>({top:t.offsetTop,left:t.offsetLeft})},V={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode;for(;n&&n.nodeType===Node.ELEMENT_NODE&&3!==n.nodeType;)n.matches(e)&&i.push(n),n=n.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=["a","button","input","textarea","select","details","[tabindex]",'[contenteditable="true"]'].map((t=>`${t}:not([tabindex^="-"])`)).join(", ");return this.find(e,t).filter((t=>!c(t)&&l(t)))}},K="carousel",X={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},Y={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},Q="next",G="prev",Z="left",J="right",tt={ArrowLeft:J,ArrowRight:Z},et="slid.bs.carousel",it="active",nt=".active.carousel-item";class st extends B{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=V.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||navigator.maxTouchPoints>0,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return X}static get NAME(){return K}next(){this._slide(Q)}nextWhenVisible(){!document.hidden&&l(this._element)&&this.next()}prev(){this._slide(G)}pause(t){t||(this._isPaused=!0),V.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(s(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=V.findOne(nt,this._element);const e=this._getItemIndex(this._activeElement);if(t>this._items.length-1||t<0)return;if(this._isSliding)return void j.one(this._element,et,(()=>this.to(t)));if(e===t)return this.pause(),void this.cycle();const i=t>e?Q:G;this._slide(i,this._items[t])}_getConfig(t){return t={...X,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(K,t,Y),t}_handleSwipe(){const t=Math.abs(this.touchDeltaX);if(t<=40)return;const e=t/this.touchDeltaX;this.touchDeltaX=0,e&&this._slide(e>0?J:Z)}_addEventListeners(){this._config.keyboard&&j.on(this._element,"keydown.bs.carousel",(t=>this._keydown(t))),"hover"===this._config.pause&&(j.on(this._element,"mouseenter.bs.carousel",(t=>this.pause(t))),j.on(this._element,"mouseleave.bs.carousel",(t=>this.cycle(t)))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const t=t=>this._pointerEvent&&("pen"===t.pointerType||"touch"===t.pointerType),e=e=>{t(e)?this.touchStartX=e.clientX:this._pointerEvent||(this.touchStartX=e.touches[0].clientX)},i=t=>{this.touchDeltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this.touchStartX},n=e=>{t(e)&&(this.touchDeltaX=e.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((t=>this.cycle(t)),500+this._config.interval))};V.find(".carousel-item img",this._element).forEach((t=>{j.on(t,"dragstart.bs.carousel",(t=>t.preventDefault()))})),this._pointerEvent?(j.on(this._element,"pointerdown.bs.carousel",(t=>e(t))),j.on(this._element,"pointerup.bs.carousel",(t=>n(t))),this._element.classList.add("pointer-event")):(j.on(this._element,"touchstart.bs.carousel",(t=>e(t))),j.on(this._element,"touchmove.bs.carousel",(t=>i(t))),j.on(this._element,"touchend.bs.carousel",(t=>n(t))))}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=tt[t.key];e&&(t.preventDefault(),this._slide(e))}_getItemIndex(t){return this._items=t&&t.parentNode?V.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){const i=t===Q;return v(this._items,e,i,this._config.wrap)}_triggerSlideEvent(t,e){const i=this._getItemIndex(t),n=this._getItemIndex(V.findOne(nt,this._element));return j.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:n,to:i})}_setActiveIndicatorElement(t){if(this._indicatorsElement){const e=V.findOne(".active",this._indicatorsElement);e.classList.remove(it),e.removeAttribute("aria-current");const i=V.find("[data-bs-target]",this._indicatorsElement);for(let e=0;e{j.trigger(this._element,et,{relatedTarget:o,direction:d,from:s,to:r})};if(this._element.classList.contains("slide")){o.classList.add(h),u(o),n.classList.add(c),o.classList.add(c);const t=()=>{o.classList.remove(c,h),o.classList.add(it),n.classList.remove(it,h,c),this._isSliding=!1,setTimeout(f,0)};this._queueCallback(t,n,!0)}else n.classList.remove(it),o.classList.add(it),this._isSliding=!1,f();a&&this.cycle()}_directionToOrder(t){return[J,Z].includes(t)?m()?t===Z?G:Q:t===Z?Q:G:t}_orderToDirection(t){return[Q,G].includes(t)?m()?t===G?Z:J:t===G?J:Z:t}static carouselInterface(t,e){const i=st.getOrCreateInstance(t,e);let{_config:n}=i;"object"==typeof e&&(n={...n,...e});const s="string"==typeof e?e:n.slide;if("number"==typeof e)i.to(e);else if("string"==typeof s){if(void 0===i[s])throw new TypeError(`No method named "${s}"`);i[s]()}else n.interval&&n.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each((function(){st.carouselInterface(this,t)}))}static dataApiClickHandler(t){const e=n(this);if(!e||!e.classList.contains("carousel"))return;const i={...U.getDataAttributes(e),...U.getDataAttributes(this)},s=this.getAttribute("data-bs-slide-to");s&&(i.interval=!1),st.carouselInterface(e,i),s&&st.getInstance(e).to(s),t.preventDefault()}}j.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",st.dataApiClickHandler),j.on(window,"load.bs.carousel.data-api",(()=>{const t=V.find('[data-bs-ride="carousel"]');for(let e=0,i=t.length;et===this._element));null!==s&&o.length&&(this._selector=s,this._triggerArray.push(e))}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return rt}static get NAME(){return ot}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t,e=[];if(this._config.parent){const t=V.find(ut,this._config.parent);e=V.find(".collapse.show, .collapse.collapsing",this._config.parent).filter((e=>!t.includes(e)))}const i=V.findOne(this._selector);if(e.length){const n=e.find((t=>i!==t));if(t=n?pt.getInstance(n):null,t&&t._isTransitioning)return}if(j.trigger(this._element,"show.bs.collapse").defaultPrevented)return;e.forEach((e=>{i!==e&&pt.getOrCreateInstance(e,{toggle:!1}).hide(),t||H.set(e,"bs.collapse",null)}));const n=this._getDimension();this._element.classList.remove(ct),this._element.classList.add(ht),this._element.style[n]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const s=`scroll${n[0].toUpperCase()+n.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct,lt),this._element.style[n]="",j.trigger(this._element,"shown.bs.collapse")}),this._element,!0),this._element.style[n]=`${this._element[s]}px`}hide(){if(this._isTransitioning||!this._isShown())return;if(j.trigger(this._element,"hide.bs.collapse").defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,u(this._element),this._element.classList.add(ht),this._element.classList.remove(ct,lt);const e=this._triggerArray.length;for(let t=0;t{this._isTransitioning=!1,this._element.classList.remove(ht),this._element.classList.add(ct),j.trigger(this._element,"hidden.bs.collapse")}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(lt)}_getConfig(t){return(t={...rt,...U.getDataAttributes(this._element),...t}).toggle=Boolean(t.toggle),t.parent=r(t.parent),a(ot,t,at),t}_getDimension(){return this._element.classList.contains("collapse-horizontal")?"width":"height"}_initializeChildren(){if(!this._config.parent)return;const t=V.find(ut,this._config.parent);V.find(ft,this._config.parent).filter((e=>!t.includes(e))).forEach((t=>{const e=n(t);e&&this._addAriaAndCollapsedClass([t],this._isShown(e))}))}_addAriaAndCollapsedClass(t,e){t.length&&t.forEach((t=>{e?t.classList.remove(dt):t.classList.add(dt),t.setAttribute("aria-expanded",e)}))}static jQueryInterface(t){return this.each((function(){const e={};"string"==typeof t&&/show|hide/.test(t)&&(e.toggle=!1);const i=pt.getOrCreateInstance(this,e);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t]()}}))}}j.on(document,"click.bs.collapse.data-api",ft,(function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const e=i(this);V.find(e).forEach((t=>{pt.getOrCreateInstance(t,{toggle:!1}).toggle()}))})),g(pt);var mt="top",gt="bottom",_t="right",bt="left",vt="auto",yt=[mt,gt,_t,bt],wt="start",Et="end",At="clippingParents",Tt="viewport",Ot="popper",Ct="reference",kt=yt.reduce((function(t,e){return t.concat([e+"-"+wt,e+"-"+Et])}),[]),Lt=[].concat(yt,[vt]).reduce((function(t,e){return t.concat([e,e+"-"+wt,e+"-"+Et])}),[]),xt="beforeRead",Dt="read",St="afterRead",Nt="beforeMain",It="main",Pt="afterMain",jt="beforeWrite",Mt="write",Ht="afterWrite",Bt=[xt,Dt,St,Nt,It,Pt,jt,Mt,Ht];function Rt(t){return t?(t.nodeName||"").toLowerCase():null}function Wt(t){if(null==t)return window;if("[object Window]"!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function $t(t){return t instanceof Wt(t).Element||t instanceof Element}function zt(t){return t instanceof Wt(t).HTMLElement||t instanceof HTMLElement}function qt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof Wt(t).ShadowRoot||t instanceof ShadowRoot)}const Ft={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];zt(s)&&Rt(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"":e)})))}))},effect:function(t){var e=t.state,i={popper:{position:e.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="",t}),{});zt(n)&&Rt(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}},requires:["computeStyles"]};function Ut(t){return t.split("-")[0]}function Vt(t,e){var i=t.getBoundingClientRect();return{width:i.width/1,height:i.height/1,top:i.top/1,right:i.right/1,bottom:i.bottom/1,left:i.left/1,x:i.left/1,y:i.top/1}}function Kt(t){var e=Vt(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Xt(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&qt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function Yt(t){return Wt(t).getComputedStyle(t)}function Qt(t){return["table","td","th"].indexOf(Rt(t))>=0}function Gt(t){return(($t(t)?t.ownerDocument:t.document)||window.document).documentElement}function Zt(t){return"html"===Rt(t)?t:t.assignedSlot||t.parentNode||(qt(t)?t.host:null)||Gt(t)}function Jt(t){return zt(t)&&"fixed"!==Yt(t).position?t.offsetParent:null}function te(t){for(var e=Wt(t),i=Jt(t);i&&Qt(i)&&"static"===Yt(i).position;)i=Jt(i);return i&&("html"===Rt(i)||"body"===Rt(i)&&"static"===Yt(i).position)?e:i||function(t){var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox");if(-1!==navigator.userAgent.indexOf("Trident")&&zt(t)&&"fixed"===Yt(t).position)return null;for(var i=Zt(t);zt(i)&&["html","body"].indexOf(Rt(i))<0;){var n=Yt(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t)||e}function ee(t){return["top","bottom"].indexOf(t)>=0?"x":"y"}var ie=Math.max,ne=Math.min,se=Math.round;function oe(t,e,i){return ie(t,ne(e,i))}function re(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function ae(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const le={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=Ut(i.placement),l=ee(a),c=[bt,_t].indexOf(a)>=0?"height":"width";if(o&&r){var h=function(t,e){return re("number"!=typeof(t="function"==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:ae(t,yt))}(s.padding,i),d=Kt(o),u="y"===l?mt:bt,f="y"===l?gt:_t,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=te(o),_=g?"y"===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,E=oe(v,w,y),A=l;i.modifiersData[n]=((e={})[A]=E,e.centerOffset=E-w,e)}},effect:function(t){var e=t.state,i=t.options.element,n=void 0===i?"[data-popper-arrow]":i;null!=n&&("string"!=typeof n||(n=e.elements.popper.querySelector(n)))&&Xt(e.elements.popper,n)&&(e.elements.arrow=n)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function ce(t){return t.split("-")[1]}var he={top:"auto",right:"auto",bottom:"auto",left:"auto"};function de(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=!0===h?function(t){var e=t.x,i=t.y,n=window.devicePixelRatio||1;return{x:se(se(e*n)/n)||0,y:se(se(i*n)/n)||0}}(r):"function"==typeof h?h(r):r,u=d.x,f=void 0===u?0:u,p=d.y,m=void 0===p?0:p,g=r.hasOwnProperty("x"),_=r.hasOwnProperty("y"),b=bt,v=mt,y=window;if(c){var w=te(i),E="clientHeight",A="clientWidth";w===Wt(i)&&"static"!==Yt(w=Gt(i)).position&&"absolute"===a&&(E="scrollHeight",A="scrollWidth"),w=w,s!==mt&&(s!==bt&&s!==_t||o!==Et)||(v=gt,m-=w[E]-n.height,m*=l?1:-1),s!==bt&&(s!==mt&&s!==gt||o!==Et)||(b=_t,f-=w[A]-n.width,f*=l?1:-1)}var T,O=Object.assign({position:a},c&&he);return l?Object.assign({},O,((T={})[v]=_?"0":"",T[b]=g?"0":"",T.transform=(y.devicePixelRatio||1)<=1?"translate("+f+"px, "+m+"px)":"translate3d("+f+"px, "+m+"px, 0)",T)):Object.assign({},O,((e={})[v]=_?m+"px":"",e[b]=g?f+"px":"",e.transform="",e))}const ue={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:Ut(e.placement),variation:ce(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,de(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,de(Object.assign({},c,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}};var fe={passive:!0};const pe={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=Wt(e.elements.popper),c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener("scroll",i.update,fe)})),a&&l.addEventListener("resize",i.update,fe),function(){o&&c.forEach((function(t){t.removeEventListener("scroll",i.update,fe)})),a&&l.removeEventListener("resize",i.update,fe)}},data:{}};var me={left:"right",right:"left",bottom:"top",top:"bottom"};function ge(t){return t.replace(/left|right|bottom|top/g,(function(t){return me[t]}))}var _e={start:"end",end:"start"};function be(t){return t.replace(/start|end/g,(function(t){return _e[t]}))}function ve(t){var e=Wt(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function ye(t){return Vt(Gt(t)).left+ve(t).scrollLeft}function we(t){var e=Yt(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ee(t){return["html","body","#document"].indexOf(Rt(t))>=0?t.ownerDocument.body:zt(t)&&we(t)?t:Ee(Zt(t))}function Ae(t,e){var i;void 0===e&&(e=[]);var n=Ee(t),s=n===(null==(i=t.ownerDocument)?void 0:i.body),o=Wt(n),r=s?[o].concat(o.visualViewport||[],we(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Ae(Zt(r)))}function Te(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Oe(t,e){return e===Tt?Te(function(t){var e=Wt(t),i=Gt(t),n=e.visualViewport,s=i.clientWidth,o=i.clientHeight,r=0,a=0;return n&&(s=n.width,o=n.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=n.offsetLeft,a=n.offsetTop)),{width:s,height:o,x:r+ye(t),y:a}}(t)):zt(e)?function(t){var e=Vt(t);return e.top=e.top+t.clientTop,e.left=e.left+t.clientLeft,e.bottom=e.top+t.clientHeight,e.right=e.left+t.clientWidth,e.width=t.clientWidth,e.height=t.clientHeight,e.x=e.left,e.y=e.top,e}(e):Te(function(t){var e,i=Gt(t),n=ve(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ie(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ie(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+ye(t),l=-n.scrollTop;return"rtl"===Yt(s||i).direction&&(a+=ie(i.clientWidth,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Gt(t)))}function Ce(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?Ut(s):null,r=s?ce(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case mt:e={x:a,y:i.y-n.height};break;case gt:e={x:a,y:i.y+i.height};break;case _t:e={x:i.x+i.width,y:l};break;case bt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?ee(o):null;if(null!=c){var h="y"===c?"height":"width";switch(r){case wt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case Et:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ke(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.boundary,r=void 0===o?At:o,a=i.rootBoundary,l=void 0===a?Tt:a,c=i.elementContext,h=void 0===c?Ot:c,d=i.altBoundary,u=void 0!==d&&d,f=i.padding,p=void 0===f?0:f,m=re("number"!=typeof p?p:ae(p,yt)),g=h===Ot?Ct:Ot,_=t.rects.popper,b=t.elements[u?g:h],v=function(t,e,i){var n="clippingParents"===e?function(t){var e=Ae(Zt(t)),i=["absolute","fixed"].indexOf(Yt(t).position)>=0&&zt(t)?te(t):t;return $t(i)?e.filter((function(t){return $t(t)&&Xt(t,i)&&"body"!==Rt(t)})):[]}(t):[].concat(e),s=[].concat(n,[i]),o=s[0],r=s.reduce((function(e,i){var n=Oe(t,i);return e.top=ie(n.top,e.top),e.right=ne(n.right,e.right),e.bottom=ne(n.bottom,e.bottom),e.left=ie(n.left,e.left),e}),Oe(t,o));return r.width=r.right-r.left,r.height=r.bottom-r.top,r.x=r.left,r.y=r.top,r}($t(b)?b:b.contextElement||Gt(t.elements.popper),r,l),y=Vt(t.elements.reference),w=Ce({reference:y,element:_,strategy:"absolute",placement:s}),E=Te(Object.assign({},_,w)),A=h===Ot?E:y,T={top:v.top-A.top+m.top,bottom:A.bottom-v.bottom+m.bottom,left:v.left-A.left+m.left,right:A.right-v.right+m.right},O=t.modifiersData.offset;if(h===Ot&&O){var C=O[s];Object.keys(T).forEach((function(t){var e=[_t,gt].indexOf(t)>=0?1:-1,i=[mt,gt].indexOf(t)>=0?"y":"x";T[t]+=C[i]*e}))}return T}function Le(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations,l=i.allowedAutoPlacements,c=void 0===l?Lt:l,h=ce(n),d=h?a?kt:kt.filter((function(t){return ce(t)===h})):yt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ke(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[Ut(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const xe={name:"flip",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=Ut(g),b=l||(_!==g&&p?function(t){if(Ut(t)===vt)return[];var e=ge(t);return[be(t),e,be(e)]}(g):[ge(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(Ut(i)===vt?Le(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper,E=new Map,A=!0,T=v[0],O=0;O=0,D=x?"width":"height",S=ke(e,{placement:C,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),N=x?L?_t:bt:L?gt:mt;y[D]>w[D]&&(N=ge(N));var I=ge(N),P=[];if(o&&P.push(S[k]<=0),a&&P.push(S[N]<=0,S[I]<=0),P.every((function(t){return t}))){T=C,A=!1;break}E.set(C,P)}if(A)for(var j=function(t){var e=v.find((function(e){var i=E.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e,"break"},M=p?3:1;M>0&&"break"!==j(M);M--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function De(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Se(t){return[mt,_t,gt,bt].some((function(e){return t[e]>=0}))}const Ne={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ke(e,{elementContext:"reference"}),a=ke(e,{altBoundary:!0}),l=De(r,n),c=De(a,s,o),h=Se(l),d=Se(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":h,"data-popper-escaped":d})}},Ie={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=Lt.reduce((function(t,i){return t[i]=function(t,e,i){var n=Ut(t),s=[bt,mt].indexOf(n)>=0?-1:1,o="function"==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[bt,_t].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},Pe={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=Ce({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}},je={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ke(e,{boundary:l,rootBoundary:c,padding:d,altBoundary:h}),_=Ut(e.placement),b=ce(e.placement),v=!b,y=ee(_),w="x"===y?"y":"x",E=e.modifiersData.popperOffsets,A=e.rects.reference,T=e.rects.popper,O="function"==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,C={x:0,y:0};if(E){if(o||a){var k="y"===y?mt:bt,L="y"===y?gt:_t,x="y"===y?"height":"width",D=E[y],S=E[y]+g[k],N=E[y]-g[L],I=f?-T[x]/2:0,P=b===wt?A[x]:T[x],j=b===wt?-T[x]:-A[x],M=e.elements.arrow,H=f&&M?Kt(M):{width:0,height:0},B=e.modifiersData["arrow#persistent"]?e.modifiersData["arrow#persistent"].padding:{top:0,right:0,bottom:0,left:0},R=B[k],W=B[L],$=oe(0,A[x],H[x]),z=v?A[x]/2-I-$-R-O:P-$-R-O,q=v?-A[x]/2+I+$+W+O:j+$+W+O,F=e.elements.arrow&&te(e.elements.arrow),U=F?"y"===y?F.clientTop||0:F.clientLeft||0:0,V=e.modifiersData.offset?e.modifiersData.offset[e.placement][y]:0,K=E[y]+z-V-U,X=E[y]+q-V;if(o){var Y=oe(f?ne(S,K):S,D,f?ie(N,X):N);E[y]=Y,C[y]=Y-D}if(a){var Q="x"===y?mt:bt,G="x"===y?gt:_t,Z=E[w],J=Z+g[Q],tt=Z-g[G],et=oe(f?ne(J,K):J,Z,f?ie(tt,X):tt);E[w]=et,C[w]=et-Z}}e.modifiersData[n]=C}},requiresIfExists:["offset"]};function Me(t,e,i){void 0===i&&(i=!1);var n=zt(e);zt(e)&&function(t){var e=t.getBoundingClientRect();e.width,t.offsetWidth,e.height,t.offsetHeight}(e);var s,o,r=Gt(e),a=Vt(t),l={scrollLeft:0,scrollTop:0},c={x:0,y:0};return(n||!n&&!i)&&(("body"!==Rt(e)||we(r))&&(l=(s=e)!==Wt(s)&&zt(s)?{scrollLeft:(o=s).scrollLeft,scrollTop:o.scrollTop}:ve(s)),zt(e)?((c=Vt(e)).x+=e.clientLeft,c.y+=e.clientTop):r&&(c.x=ye(r))),{x:a.left+l.scrollLeft-c.x,y:a.top+l.scrollTop-c.y,width:a.width,height:a.height}}function He(t){var e=new Map,i=new Set,n=[];function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var Be={placement:"bottom",modifiers:[],strategy:"absolute"};function Re(){for(var t=arguments.length,e=new Array(t),i=0;ij.on(t,"mouseover",d))),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.add(Je),this._element.classList.add(Je),j.trigger(this._element,"shown.bs.dropdown",t)}hide(){if(c(this._element)||!this._isShown(this._menu))return;const t={relatedTarget:this._element};this._completeHide(t)}dispose(){this._popper&&this._popper.destroy(),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_completeHide(t){j.trigger(this._element,"hide.bs.dropdown",t).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._popper&&this._popper.destroy(),this._menu.classList.remove(Je),this._element.classList.remove(Je),this._element.setAttribute("aria-expanded","false"),U.removeDataAttribute(this._menu,"popper"),j.trigger(this._element,"hidden.bs.dropdown",t))}_getConfig(t){if(t={...this.constructor.Default,...U.getDataAttributes(this._element),...t},a(Ue,t,this.constructor.DefaultType),"object"==typeof t.reference&&!o(t.reference)&&"function"!=typeof t.reference.getBoundingClientRect)throw new TypeError(`${Ue.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`);return t}_createPopper(t){if(void 0===Fe)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let e=this._element;"parent"===this._config.reference?e=t:o(this._config.reference)?e=r(this._config.reference):"object"==typeof this._config.reference&&(e=this._config.reference);const i=this._getPopperConfig(),n=i.modifiers.find((t=>"applyStyles"===t.name&&!1===t.enabled));this._popper=qe(e,this._menu,i),n&&U.setDataAttribute(this._menu,"popper","static")}_isShown(t=this._element){return t.classList.contains(Je)}_getMenuElement(){return V.next(this._element,ei)[0]}_getPlacement(){const t=this._element.parentNode;if(t.classList.contains("dropend"))return ri;if(t.classList.contains("dropstart"))return ai;const e="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim();return t.classList.contains("dropup")?e?ni:ii:e?oi:si}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}_selectMenuItem({key:t,target:e}){const i=V.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",this._menu).filter(l);i.length&&v(i,e,t===Ye,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}static clearMenus(t){if(t&&(2===t.button||"keyup"===t.type&&"Tab"!==t.key))return;const e=V.find(ti);for(let i=0,n=e.length;ie+t)),this._setElementAttributes(di,"paddingRight",(e=>e+t)),this._setElementAttributes(ui,"marginRight",(e=>e-t))}_disableOverFlow(){this._saveInitialAttribute(this._element,"overflow"),this._element.style.overflow="hidden"}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t)[e];t.style[e]=`${i(Number.parseFloat(s))}px`}))}reset(){this._resetElementAttributes(this._element,"overflow"),this._resetElementAttributes(this._element,"paddingRight"),this._resetElementAttributes(di,"paddingRight"),this._resetElementAttributes(ui,"marginRight")}_saveInitialAttribute(t,e){const i=t.style[e];i&&U.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=U.getDataAttribute(t,e);void 0===i?t.style.removeProperty(e):(U.removeDataAttribute(t,e),t.style[e]=i)}))}_applyManipulationCallback(t,e){o(t)?e(t):V.find(t,this._element).forEach(e)}isOverflowing(){return this.getWidth()>0}}const pi={className:"modal-backdrop",isVisible:!0,isAnimated:!1,rootElement:"body",clickCallback:null},mi={className:"string",isVisible:"boolean",isAnimated:"boolean",rootElement:"(element|string)",clickCallback:"(function|null)"},gi="show",_i="mousedown.bs.backdrop";class bi{constructor(t){this._config=this._getConfig(t),this._isAppended=!1,this._element=null}show(t){this._config.isVisible?(this._append(),this._config.isAnimated&&u(this._getElement()),this._getElement().classList.add(gi),this._emulateAnimation((()=>{_(t)}))):_(t)}hide(t){this._config.isVisible?(this._getElement().classList.remove(gi),this._emulateAnimation((()=>{this.dispose(),_(t)}))):_(t)}_getElement(){if(!this._element){const t=document.createElement("div");t.className=this._config.className,this._config.isAnimated&&t.classList.add("fade"),this._element=t}return this._element}_getConfig(t){return(t={...pi,..."object"==typeof t?t:{}}).rootElement=r(t.rootElement),a("backdrop",t,mi),t}_append(){this._isAppended||(this._config.rootElement.append(this._getElement()),j.on(this._getElement(),_i,(()=>{_(this._config.clickCallback)})),this._isAppended=!0)}dispose(){this._isAppended&&(j.off(this._element,_i),this._element.remove(),this._isAppended=!1)}_emulateAnimation(t){b(t,this._getElement(),this._config.isAnimated)}}const vi={trapElement:null,autofocus:!0},yi={trapElement:"element",autofocus:"boolean"},wi=".bs.focustrap",Ei="backward";class Ai{constructor(t){this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}activate(){const{trapElement:t,autofocus:e}=this._config;this._isActive||(e&&t.focus(),j.off(document,wi),j.on(document,"focusin.bs.focustrap",(t=>this._handleFocusin(t))),j.on(document,"keydown.tab.bs.focustrap",(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,j.off(document,wi))}_handleFocusin(t){const{target:e}=t,{trapElement:i}=this._config;if(e===document||e===i||i.contains(e))return;const n=V.focusableChildren(i);0===n.length?i.focus():this._lastTabNavDirection===Ei?n[n.length-1].focus():n[0].focus()}_handleKeydown(t){"Tab"===t.key&&(this._lastTabNavDirection=t.shiftKey?Ei:"forward")}_getConfig(t){return t={...vi,..."object"==typeof t?t:{}},a("focustrap",t,yi),t}}const Ti="modal",Oi="Escape",Ci={backdrop:!0,keyboard:!0,focus:!0},ki={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"},Li="hidden.bs.modal",xi="show.bs.modal",Di="resize.bs.modal",Si="click.dismiss.bs.modal",Ni="keydown.dismiss.bs.modal",Ii="mousedown.dismiss.bs.modal",Pi="modal-open",ji="show",Mi="modal-static";class Hi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=V.findOne(".modal-dialog",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollBar=new fi}static get Default(){return Ci}static get NAME(){return Ti}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||j.trigger(this._element,xi,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isAnimated()&&(this._isTransitioning=!0),this._scrollBar.hide(),document.body.classList.add(Pi),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),j.on(this._dialog,Ii,(()=>{j.one(this._element,"mouseup.dismiss.bs.modal",(t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)}))})),this._showBackdrop((()=>this._showElement(t))))}hide(){if(!this._isShown||this._isTransitioning)return;if(j.trigger(this._element,"hide.bs.modal").defaultPrevented)return;this._isShown=!1;const t=this._isAnimated();t&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),this._focustrap.deactivate(),this._element.classList.remove(ji),j.off(this._element,Si),j.off(this._dialog,Ii),this._queueCallback((()=>this._hideModal()),this._element,t)}dispose(){[window,this._dialog].forEach((t=>j.off(t,".bs.modal"))),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new bi({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_getConfig(t){return t={...Ci,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Ti,t,ki),t}_showElement(t){const e=this._isAnimated(),i=V.findOne(".modal-body",this._dialog);this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.append(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&u(this._element),this._element.classList.add(ji),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,j.trigger(this._element,"shown.bs.modal",{relatedTarget:t})}),this._dialog,e)}_setEscapeEvent(){this._isShown?j.on(this._element,Ni,(t=>{this._config.keyboard&&t.key===Oi?(t.preventDefault(),this.hide()):this._config.keyboard||t.key!==Oi||this._triggerBackdropTransition()})):j.off(this._element,Ni)}_setResizeEvent(){this._isShown?j.on(window,Di,(()=>this._adjustDialog())):j.off(window,Di)}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(Pi),this._resetAdjustments(),this._scrollBar.reset(),j.trigger(this._element,Li)}))}_showBackdrop(t){j.on(this._element,Si,(t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&(!0===this._config.backdrop?this.hide():"static"===this._config.backdrop&&this._triggerBackdropTransition())})),this._backdrop.show(t)}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){if(j.trigger(this._element,"hidePrevented.bs.modal").defaultPrevented)return;const{classList:t,scrollHeight:e,style:i}=this._element,n=e>document.documentElement.clientHeight;!n&&"hidden"===i.overflowY||t.contains(Mi)||(n||(i.overflowY="hidden"),t.add(Mi),this._queueCallback((()=>{t.remove(Mi),n||this._queueCallback((()=>{i.overflowY=""}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;(!i&&t&&!m()||i&&!t&&m())&&(this._element.style.paddingLeft=`${e}px`),(i&&!t&&!m()||!i&&t&&m())&&(this._element.style.paddingRight=`${e}px`)}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}static jQueryInterface(t,e){return this.each((function(){const i=Hi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===i[t])throw new TypeError(`No method named "${t}"`);i[t](e)}}))}}j.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',(function(t){const e=n(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),j.one(e,xi,(t=>{t.defaultPrevented||j.one(e,Li,(()=>{l(this)&&this.focus()}))}));const i=V.findOne(".modal.show");i&&Hi.getInstance(i).hide(),Hi.getOrCreateInstance(e).toggle(this)})),R(Hi),g(Hi);const Bi="offcanvas",Ri={backdrop:!0,keyboard:!0,scroll:!1},Wi={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"},$i="show",zi=".offcanvas.show",qi="hidden.bs.offcanvas";class Fi extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get NAME(){return Bi}static get Default(){return Ri}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||j.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._backdrop.show(),this._config.scroll||(new fi).hide(),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add($i),this._queueCallback((()=>{this._config.scroll||this._focustrap.activate(),j.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(j.trigger(this._element,"hide.bs.offcanvas").defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.remove($i),this._backdrop.hide(),this._queueCallback((()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.scroll||(new fi).reset(),j.trigger(this._element,qi)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_getConfig(t){return t={...Ri,...U.getDataAttributes(this._element),..."object"==typeof t?t:{}},a(Bi,t,Wi),t}_initializeBackDrop(){return new bi({className:"offcanvas-backdrop",isVisible:this._config.backdrop,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:()=>this.hide()})}_initializeFocusTrap(){return new Ai({trapElement:this._element})}_addEventListeners(){j.on(this._element,"keydown.dismiss.bs.offcanvas",(t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}))}static jQueryInterface(t){return this.each((function(){const e=Fi.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t]||t.startsWith("_")||"constructor"===t)throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}j.on(document,"click.bs.offcanvas.data-api",'[data-bs-toggle="offcanvas"]',(function(t){const e=n(this);if(["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this))return;j.one(e,qi,(()=>{l(this)&&this.focus()}));const i=V.findOne(zi);i&&i!==e&&Fi.getInstance(i).hide(),Fi.getOrCreateInstance(e).toggle(this)})),j.on(window,"load.bs.offcanvas.data-api",(()=>V.find(zi).forEach((t=>Fi.getOrCreateInstance(t).show())))),R(Fi),g(Fi);const Ui=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]),Vi=/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i,Ki=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i,Xi=(t,e)=>{const i=t.nodeName.toLowerCase();if(e.includes(i))return!Ui.has(i)||Boolean(Vi.test(t.nodeValue)||Ki.test(t.nodeValue));const n=e.filter((t=>t instanceof RegExp));for(let t=0,e=n.length;t{Xi(t,r)||i.removeAttribute(t.nodeName)}))}return n.body.innerHTML}const Qi="tooltip",Gi=new Set(["sanitize","allowList","sanitizeFn"]),Zi={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Ji={AUTO:"auto",TOP:"top",RIGHT:m()?"left":"right",BOTTOM:"bottom",LEFT:m()?"right":"left"},tn={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:{"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},popperConfig:null},en={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},nn="fade",sn="show",on="show",rn="out",an=".tooltip-inner",ln=".modal",cn="hide.bs.modal",hn="hover",dn="focus";class un extends B{constructor(t,e){if(void 0===Fe)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this._config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return tn}static get NAME(){return Qi}static get Event(){return en}static get DefaultType(){return Zi}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){if(this._isEnabled)if(t){const e=this._initializeOnDelegatedTarget(t);e._activeTrigger.click=!e._activeTrigger.click,e._isWithActiveTrigger()?e._enter(null,e):e._leave(null,e)}else{if(this.getTipElement().classList.contains(sn))return void this._leave(null,this);this._enter(null,this)}}dispose(){clearTimeout(this._timeout),j.off(this._element.closest(ln),cn,this._hideModalHandler),this.tip&&this.tip.remove(),this._disposePopper(),super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");if(!this.isWithContent()||!this._isEnabled)return;const t=j.trigger(this._element,this.constructor.Event.SHOW),e=h(this._element),i=null===e?this._element.ownerDocument.documentElement.contains(this._element):e.contains(this._element);if(t.defaultPrevented||!i)return;"tooltip"===this.constructor.NAME&&this.tip&&this.getTitle()!==this.tip.querySelector(an).innerHTML&&(this._disposePopper(),this.tip.remove(),this.tip=null);const n=this.getTipElement(),s=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t));return t})(this.constructor.NAME);n.setAttribute("id",s),this._element.setAttribute("aria-describedby",s),this._config.animation&&n.classList.add(nn);const o="function"==typeof this._config.placement?this._config.placement.call(this,n,this._element):this._config.placement,r=this._getAttachment(o);this._addAttachmentClass(r);const{container:a}=this._config;H.set(n,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(a.append(n),j.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=qe(this._element,n,this._getPopperConfig(r)),n.classList.add(sn);const l=this._resolvePossibleFunction(this._config.customClass);l&&n.classList.add(...l.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>{j.on(t,"mouseover",d)}));const c=this.tip.classList.contains(nn);this._queueCallback((()=>{const t=this._hoverState;this._hoverState=null,j.trigger(this._element,this.constructor.Event.SHOWN),t===rn&&this._leave(null,this)}),this.tip,c)}hide(){if(!this._popper)return;const t=this.getTipElement();if(j.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented)return;t.classList.remove(sn),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach((t=>j.off(t,"mouseover",d))),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1;const e=this.tip.classList.contains(nn);this._queueCallback((()=>{this._isWithActiveTrigger()||(this._hoverState!==on&&t.remove(),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),j.trigger(this._element,this.constructor.Event.HIDDEN),this._disposePopper())}),this.tip,e),this._hoverState=""}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){if(this.tip)return this.tip;const t=document.createElement("div");t.innerHTML=this._config.template;const e=t.children[0];return this.setContent(e),e.classList.remove(nn,sn),this.tip=e,this.tip}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),an)}_sanitizeAndSetContent(t,e,i){const n=V.findOne(i,t);e||!n?this.setElementContent(n,e):n.remove()}setElementContent(t,e){if(null!==t)return o(e)?(e=r(e),void(this._config.html?e.parentNode!==t&&(t.innerHTML="",t.append(e)):t.textContent=e.textContent)):void(this._config.html?(this._config.sanitize&&(e=Yi(e,this._config.allowList,this._config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){const t=this._element.getAttribute("data-bs-original-title")||this._config.title;return this._resolvePossibleFunction(t)}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){return e||this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_getOffset(){const{offset:t}=this._config;return"string"==typeof t?t.split(",").map((t=>Number.parseInt(t,10))):"function"==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return"function"==typeof t?t.call(this._element):t}_getPopperConfig(t){const e={placement:t,modifiers:[{name:"flip",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...e,..."function"==typeof this._config.popperConfig?this._config.popperConfig(e):this._config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add(`${this._getBasicClassPrefix()}-${this.updateAttachment(t)}`)}_getAttachment(t){return Ji[t.toUpperCase()]}_setListeners(){this._config.trigger.split(" ").forEach((t=>{if("click"===t)j.on(this._element,this.constructor.Event.CLICK,this._config.selector,(t=>this.toggle(t)));else if("manual"!==t){const e=t===hn?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,i=t===hn?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT;j.on(this._element,e,this._config.selector,(t=>this._enter(t))),j.on(this._element,i,this._config.selector,(t=>this._leave(t)))}})),this._hideModalHandler=()=>{this._element&&this.hide()},j.on(this._element.closest(ln),cn,this._hideModalHandler),this._config.selector?this._config={...this._config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){const t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");(t||"string"!==e)&&(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?dn:hn]=!0),e.getTipElement().classList.contains(sn)||e._hoverState===on?e._hoverState=on:(clearTimeout(e._timeout),e._hoverState=on,e._config.delay&&e._config.delay.show?e._timeout=setTimeout((()=>{e._hoverState===on&&e.show()}),e._config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?dn:hn]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=rn,e._config.delay&&e._config.delay.hide?e._timeout=setTimeout((()=>{e._hoverState===rn&&e.hide()}),e._config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=U.getDataAttributes(this._element);return Object.keys(e).forEach((t=>{Gi.has(t)&&delete e[t]})),(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).container=!1===t.container?document.body:r(t.container),"number"==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),a(Qi,t,this.constructor.DefaultType),t.sanitize&&(t.template=Yi(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){const t={};for(const e in this._config)this.constructor.Default[e]!==this._config[e]&&(t[e]=this._config[e]);return t}_cleanTipClass(){const t=this.getTipElement(),e=new RegExp(`(^|\\s)${this._getBasicClassPrefix()}\\S+`,"g"),i=t.getAttribute("class").match(e);null!==i&&i.length>0&&i.map((t=>t.trim())).forEach((e=>t.classList.remove(e)))}_getBasicClassPrefix(){return"bs-tooltip"}_handlePopperPlacementChange(t){const{state:e}=t;e&&(this.tip=e.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(e.placement)))}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null)}static jQueryInterface(t){return this.each((function(){const e=un.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(un);const fn={...un.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:''},pn={...un.DefaultType,content:"(string|element|function)"},mn={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"};class gn extends un{static get Default(){return fn}static get NAME(){return"popover"}static get Event(){return mn}static get DefaultType(){return pn}isWithContent(){return this.getTitle()||this._getContent()}setContent(t){this._sanitizeAndSetContent(t,this.getTitle(),".popover-header"),this._sanitizeAndSetContent(t,this._getContent(),".popover-body")}_getContent(){return this._resolvePossibleFunction(this._config.content)}_getBasicClassPrefix(){return"bs-popover"}static jQueryInterface(t){return this.each((function(){const e=gn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}g(gn);const _n="scrollspy",bn={offset:10,method:"auto",target:""},vn={offset:"number",method:"string",target:"(string|element)"},yn="active",wn=".nav-link, .list-group-item, .dropdown-item",En="position";class An extends B{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,j.on(this._scrollElement,"scroll.bs.scrollspy",(()=>this._process())),this.refresh(),this._process()}static get Default(){return bn}static get NAME(){return _n}refresh(){const t=this._scrollElement===this._scrollElement.window?"offset":En,e="auto"===this._config.method?t:this._config.method,n=e===En?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),V.find(wn,this._config.target).map((t=>{const s=i(t),o=s?V.findOne(s):null;if(o){const t=o.getBoundingClientRect();if(t.width||t.height)return[U[e](o).top+n,s]}return null})).filter((t=>t)).sort(((t,e)=>t[0]-e[0])).forEach((t=>{this._offsets.push(t[0]),this._targets.push(t[1])}))}dispose(){j.off(this._scrollElement,".bs.scrollspy"),super.dispose()}_getConfig(t){return(t={...bn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}}).target=r(t.target)||document.documentElement,a(_n,t,vn),t}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){const t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),i=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=i){const t=this._targets[this._targets.length-1];this._activeTarget!==t&&this._activate(t)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(let e=this._offsets.length;e--;)this._activeTarget!==this._targets[e]&&t>=this._offsets[e]&&(void 0===this._offsets[e+1]||t`${e}[data-bs-target="${t}"],${e}[href="${t}"]`)),i=V.findOne(e.join(","),this._config.target);i.classList.add(yn),i.classList.contains("dropdown-item")?V.findOne(".dropdown-toggle",i.closest(".dropdown")).classList.add(yn):V.parents(i,".nav, .list-group").forEach((t=>{V.prev(t,".nav-link, .list-group-item").forEach((t=>t.classList.add(yn))),V.prev(t,".nav-item").forEach((t=>{V.children(t,".nav-link").forEach((t=>t.classList.add(yn)))}))})),j.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:t})}_clear(){V.find(wn,this._config.target).filter((t=>t.classList.contains(yn))).forEach((t=>t.classList.remove(yn)))}static jQueryInterface(t){return this.each((function(){const e=An.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(window,"load.bs.scrollspy.data-api",(()=>{V.find('[data-bs-spy="scroll"]').forEach((t=>new An(t)))})),g(An);const Tn="active",On="fade",Cn="show",kn=".active",Ln=":scope > li > .active";class xn extends B{static get NAME(){return"tab"}show(){if(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains(Tn))return;let t;const e=n(this._element),i=this._element.closest(".nav, .list-group");if(i){const e="UL"===i.nodeName||"OL"===i.nodeName?Ln:kn;t=V.find(e,i),t=t[t.length-1]}const s=t?j.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null;if(j.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==s&&s.defaultPrevented)return;this._activate(this._element,i);const o=()=>{j.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),j.trigger(this._element,"shown.bs.tab",{relatedTarget:t})};e?this._activate(e,e.parentNode,o):o()}_activate(t,e,i){const n=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?V.children(e,kn):V.find(Ln,e))[0],s=i&&n&&n.classList.contains(On),o=()=>this._transitionComplete(t,n,i);n&&s?(n.classList.remove(Cn),this._queueCallback(o,t,!0)):o()}_transitionComplete(t,e,i){if(e){e.classList.remove(Tn);const t=V.findOne(":scope > .dropdown-menu .active",e.parentNode);t&&t.classList.remove(Tn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}t.classList.add(Tn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),u(t),t.classList.contains(On)&&t.classList.add(Cn);let n=t.parentNode;if(n&&"LI"===n.nodeName&&(n=n.parentNode),n&&n.classList.contains("dropdown-menu")){const e=t.closest(".dropdown");e&&V.find(".dropdown-toggle",e).forEach((t=>t.classList.add(Tn))),t.setAttribute("aria-expanded",!0)}i&&i()}static jQueryInterface(t){return this.each((function(){const e=xn.getOrCreateInstance(this);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t]()}}))}}j.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',(function(t){["A","AREA"].includes(this.tagName)&&t.preventDefault(),c(this)||xn.getOrCreateInstance(this).show()})),g(xn);const Dn="toast",Sn="hide",Nn="show",In="showing",Pn={animation:"boolean",autohide:"boolean",delay:"number"},jn={animation:!0,autohide:!0,delay:5e3};class Mn extends B{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get DefaultType(){return Pn}static get Default(){return jn}static get NAME(){return Dn}show(){j.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),this._element.classList.remove(Sn),u(this._element),this._element.classList.add(Nn),this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.remove(In),j.trigger(this._element,"shown.bs.toast"),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this._element.classList.contains(Nn)&&(j.trigger(this._element,"hide.bs.toast").defaultPrevented||(this._element.classList.add(In),this._queueCallback((()=>{this._element.classList.add(Sn),this._element.classList.remove(In),this._element.classList.remove(Nn),j.trigger(this._element,"hidden.bs.toast")}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this._element.classList.contains(Nn)&&this._element.classList.remove(Nn),super.dispose()}_getConfig(t){return t={...jn,...U.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},a(Dn,t,this.constructor.DefaultType),t}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}),this._config.delay)))}_onInteraction(t,e){switch(t.type){case"mouseover":case"mouseout":this._hasMouseInteraction=e;break;case"focusin":case"focusout":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){j.on(this._element,"mouseover.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"mouseout.bs.toast",(t=>this._onInteraction(t,!1))),j.on(this._element,"focusin.bs.toast",(t=>this._onInteraction(t,!0))),j.on(this._element,"focusout.bs.toast",(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=Mn.getOrCreateInstance(this,t);if("string"==typeof t){if(void 0===e[t])throw new TypeError(`No method named "${t}"`);e[t](this)}}))}}return R(Mn),g(Mn),{Alert:W,Button:z,Carousel:st,Collapse:pt,Dropdown:hi,Modal:Hi,Offcanvas:Fi,Popover:gn,ScrollSpy:An,Tab:xn,Toast:Mn,Tooltip:un}})); 7 | //# sourceMappingURL=bootstrap.bundle.min.js.map -------------------------------------------------------------------------------- /static/js/js.cookie.min.js: -------------------------------------------------------------------------------- 1 | /*! js-cookie v2.2.1 | MIT */ 2 | 3 | !function(a){var b;if("function"==typeof define&&define.amd&&(define(a),b=!0),"object"==typeof exports&&(module.exports=a(),b=!0),!b){var c=window.Cookies,d=window.Cookies=a();d.noConflict=function(){return window.Cookies=c,d}}}(function(){function a(){for(var a=0,b={};a0)){var b=this;a('
').appendTo(a("body")),this.$lightbox=a("#lightbox"),this.$overlay=a("#lightboxOverlay"),this.$outerContainer=this.$lightbox.find(".lb-outerContainer"),this.$container=this.$lightbox.find(".lb-container"),this.$image=this.$lightbox.find(".lb-image"),this.$nav=this.$lightbox.find(".lb-nav"),this.containerPadding={top:parseInt(this.$container.css("padding-top"),10),right:parseInt(this.$container.css("padding-right"),10),bottom:parseInt(this.$container.css("padding-bottom"),10),left:parseInt(this.$container.css("padding-left"),10)},this.imageBorderWidth={top:parseInt(this.$image.css("border-top-width"),10),right:parseInt(this.$image.css("border-right-width"),10),bottom:parseInt(this.$image.css("border-bottom-width"),10),left:parseInt(this.$image.css("border-left-width"),10)},this.$overlay.hide().on("click",function(){return b.end(),!1}),this.$lightbox.hide().on("click",function(c){"lightbox"===a(c.target).attr("id")&&b.end()}),this.$outerContainer.on("click",function(c){return"lightbox"===a(c.target).attr("id")&&b.end(),!1}),this.$lightbox.find(".lb-prev").on("click",function(){return 0===b.currentImageIndex?b.changeImage(b.album.length-1):b.changeImage(b.currentImageIndex-1),!1}),this.$lightbox.find(".lb-next").on("click",function(){return b.currentImageIndex===b.album.length-1?b.changeImage(0):b.changeImage(b.currentImageIndex+1),!1}),this.$nav.on("mousedown",function(a){3===a.which&&(b.$nav.css("pointer-events","none"),b.$lightbox.one("contextmenu",function(){setTimeout(function(){this.$nav.css("pointer-events","auto")}.bind(b),0)}))}),this.$lightbox.find(".lb-loader, .lb-close").on("click",function(){return b.end(),!1})}},b.prototype.start=function(b){function c(a){d.album.push({alt:a.attr("data-alt"),link:a.attr("href"),title:a.attr("data-title")||a.attr("title")})}var d=this,e=a(window);e.on("resize",a.proxy(this.sizeOverlay,this)),this.sizeOverlay(),this.album=[];var f,g=0,h=b.attr("data-lightbox");if(h){f=a(b.prop("tagName")+'[data-lightbox="'+h+'"]');for(var i=0;ik||g.height>j)&&(g.width/k>g.height/j?(i=k,h=parseInt(g.height/(g.width/i),10),f.width(i),f.height(h)):(h=j,i=parseInt(g.width/(g.height/h),10),f.width(i),f.height(h))),c.sizeContainer(f.width(),f.height())},g.src=this.album[b].link,this.currentImageIndex=b},b.prototype.sizeOverlay=function(){var b=this;setTimeout(function(){b.$overlay.width(a(document).width()).height(a(document).height())},0)},b.prototype.sizeContainer=function(a,b){function c(){d.$lightbox.find(".lb-dataContainer").width(g),d.$lightbox.find(".lb-prevLink").height(h),d.$lightbox.find(".lb-nextLink").height(h),d.$overlay.focus(),d.showImage()}var d=this,e=this.$outerContainer.outerWidth(),f=this.$outerContainer.outerHeight(),g=a+this.containerPadding.left+this.containerPadding.right+this.imageBorderWidth.left+this.imageBorderWidth.right,h=b+this.containerPadding.top+this.containerPadding.bottom+this.imageBorderWidth.top+this.imageBorderWidth.bottom;e!==g||f!==h?this.$outerContainer.animate({width:g,height:h},this.options.resizeDuration,"swing",function(){c()}):c()},b.prototype.showImage=function(){this.$lightbox.find(".lb-loader").stop(!0).hide(),this.$lightbox.find(".lb-image").fadeIn(this.options.imageFadeDuration),this.updateNav(),this.updateDetails(),this.preloadNeighboringImages(),this.enableKeyboardNav()},b.prototype.updateNav=function(){var a=!1;try{document.createEvent("TouchEvent"),a=!!this.options.alwaysShowNavOnTouchDevices}catch(a){}this.$lightbox.find(".lb-nav").show(),this.album.length>1&&(this.options.wrapAround?(a&&this.$lightbox.find(".lb-prev, .lb-next").css("opacity","1"),this.$lightbox.find(".lb-prev, .lb-next").show()):(this.currentImageIndex>0&&(this.$lightbox.find(".lb-prev").show(),a&&this.$lightbox.find(".lb-prev").css("opacity","1")),this.currentImageIndex1&&this.options.showImageNumberLabel){var c=this.imageCountLabel(this.currentImageIndex+1,this.album.length);this.$lightbox.find(".lb-number").text(c).fadeIn("fast")}else this.$lightbox.find(".lb-number").hide();this.$outerContainer.removeClass("animating"),this.$lightbox.find(".lb-dataContainer").fadeIn(this.options.resizeDuration,function(){return a.sizeOverlay()})},b.prototype.preloadNeighboringImages=function(){if(this.album.length>this.currentImageIndex+1){(new Image).src=this.album[this.currentImageIndex+1].link}if(this.currentImageIndex>0){(new Image).src=this.album[this.currentImageIndex-1].link}},b.prototype.enableKeyboardNav=function(){this.$lightbox.on("keyup.keyboard",a.proxy(this.keyboardAction,this)),this.$overlay.on("keyup.keyboard",a.proxy(this.keyboardAction,this))},b.prototype.disableKeyboardNav=function(){this.$lightbox.off(".keyboard"),this.$overlay.off(".keyboard")},b.prototype.keyboardAction=function(a){var b=a.keyCode;27===b?(a.stopPropagation(),this.end()):37===b?0!==this.currentImageIndex?this.changeImage(this.currentImageIndex-1):this.options.wrapAround&&this.album.length>1&&this.changeImage(this.album.length-1):39===b&&(this.currentImageIndex!==this.album.length-1?this.changeImage(this.currentImageIndex+1):this.options.wrapAround&&this.album.length>1&&this.changeImage(0))},b.prototype.end=function(){this.disableKeyboardNav(),a(window).off("resize",this.sizeOverlay),this.$lightbox.fadeOut(this.options.fadeDuration),this.$overlay.fadeOut(this.options.fadeDuration),this.options.disableScrolling&&a("body").removeClass("lb-disable-scrolling")},new b}); 15 | //# sourceMappingURL=lightbox.min.map -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {{define "header"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ImageServer 11 | 12 | 13 |
14 | 33 |
34 | {{end}} 35 | 36 | {{define "breadcrumb"}} 37 |
38 |
39 | 47 |
48 |
49 | {{end}} 50 | 51 | {{define "footer"}} 52 | 53 | 54 | 55 | 56 | 57 | 58 | {{end}} -------------------------------------------------------------------------------- /templates/compare.html: -------------------------------------------------------------------------------- 1 | {{template "header" .}} 2 | {{$data := .}} 3 |
4 |
5 |
6 | 7 | 8 | 9 | 10 | {{range .contents}} 11 | 14 | {{end}} 15 | 16 | 17 | 18 | {{range $i, $_ := .aligned.Folders }} 19 | 20 | 21 | {{range $_, $content := $data.contents}} 22 | 24 | {{end}} 25 | 26 | {{end}} 27 | {{range $i, $filename := .aligned.Files }} 28 | 29 | 30 | {{range $_, $content := $data.contents}} 31 | 41 | {{end}} 42 | 43 | {{end}} 44 | 45 | 46 |
# 12 | 🗂 {{ .Name }} 13 |
🗂{{ index $content.Folders $i }} 23 |
{{ pathToName $filename}} 32 | {{$filePath := index $content.Files $i}} 33 | {{if $filePath}} 34 | 36 | 37 | 38 | {{else}} 39 | {{end}} 40 |
47 |
48 |
49 |
50 | 51 | {{template "footer" .}} -------------------------------------------------------------------------------- /templates/list.html: -------------------------------------------------------------------------------- 1 | {{template "header" .}} 2 | {{template "breadcrumb" .navigation}} 3 |
4 | {{ if .content.Folders }} 5 |
6 |
7 |
8 | {{range .content.Folders}} 9 | 25 | {{end}} 26 |
27 |
28 |
29 | {{end}} 30 | 31 | {{ if .content.Files }} 32 |
33 |
34 |
35 | {{range .content.Files}} 36 | 44 | {{end}} 45 |
46 |
47 |
48 | {{end}} 49 | 50 |
51 | {{template "footer" .}} --------------------------------------------------------------------------------