├── .gitignore ├── demo.gif ├── screenshot.png ├── assets ├── preloader.gif ├── default-skin.png ├── default-skin.svg ├── photoswipe.css ├── photoswipe-ui-default.min.js ├── default-skin.css └── photoswipe.min.js ├── config.json ├── main.go ├── go.mod ├── makefile ├── config.go ├── tools └── gen_assets.go ├── go.sum ├── readme.md ├── server.go └── watcher.go /.gitignore: -------------------------------------------------------------------------------- 1 | /*.xz 2 | /bilder 3 | /tmp 4 | /access.log 5 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgeller/bilder/HEAD/demo.gif -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgeller/bilder/HEAD/screenshot.png -------------------------------------------------------------------------------- /assets/preloader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgeller/bilder/HEAD/assets/preloader.gif -------------------------------------------------------------------------------- /assets/default-skin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fgeller/bilder/HEAD/assets/default-skin.png -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bilder-dir": "tmp", 3 | "access-log": "access.log", 4 | "reload-delay-seconds": 2, 5 | "addr": ":8111" 6 | } 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | conf := mustParseConfig() 5 | albums := make(chan []album, 1) 6 | w := newWatcher(conf.ReloadDelaySeconds, conf.BilderDir, conf.URLPathPrefix, albums) 7 | s := newServer(conf.Addr, conf.BilderDir, conf.AccessLog, albums) 8 | 9 | go w.start() 10 | s.serve() 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fgeller/bilder 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/gorilla/handlers v1.4.2 7 | github.com/nfnt/resize v0.0.0-20160109112512-4d93a29130b1 8 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 9 | github.com/oliamb/cutter v0.2.2 10 | github.com/satori/go.uuid v1.2.0 11 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | export SHELL:=/bin/bash -O extglob -c 2 | ARTIFACT = bilder 3 | OS = $(shell uname | tr [:upper:] [:lower:]) 4 | 5 | all: dev 6 | 7 | build: GOOS ?= ${OS} 8 | build: GOARCH ?= amd64 9 | build: clean 10 | GOOS=${GOOS} GOARCH=${GOARCH} CGO_ENABLED=0 go build -o ${ARTIFACT} -a *.go 11 | 12 | release: GOOS ?= ${OS} 13 | release: GOARCH ?= amd64 14 | release: build 15 | tar Jcf ${ARTIFACT}-`git rev-parse HEAD | cut -c-7`-${GOOS}-${GOARCH}.xz ${ARTIFACT} 16 | 17 | linux-release: 18 | GOOS=linux $(MAKE) release 19 | 20 | darwin-release: 21 | GOOS=darwin $(MAKE) release 22 | 23 | releases: linux-release darwin-release 24 | 25 | run: 26 | ./${ARTIFACT} -config config.json 27 | 28 | clean: 29 | rm -f ${ARTIFACT} 30 | 31 | dev: build 32 | ./${ARTIFACT} -config config.json 33 | 34 | .PHONY: assets 35 | assets: 36 | go run tools/gen_assets.go 37 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "io/ioutil" 7 | "log" 8 | ) 9 | 10 | type config struct { 11 | BilderDir string `json:"bilder-dir"` 12 | URLPathPrefix string `json:"url-path-prefix"` 13 | AccessLog string `json:"access-log"` 14 | Addr string `json:"addr"` 15 | ReloadDelaySeconds int `json:"reload-delay-seconds"` 16 | } 17 | 18 | var defaultConfig = config{ 19 | BilderDir: "bilder", 20 | Addr: "0.0.0.0:8173", 21 | ReloadDelaySeconds: 10, 22 | } 23 | 24 | func mustParseConfig() config { 25 | f := flag.String("config", "", "JSON config file for bilder.") 26 | flag.Parse() 27 | 28 | if *f == "" { 29 | return defaultConfig 30 | } 31 | 32 | byts, err := ioutil.ReadFile(*f) 33 | if err != nil { 34 | log.Fatalf("Failed to read config file %#v err=%v", *f, err) 35 | } 36 | 37 | var c config 38 | if err := json.Unmarshal(byts, &c); err != nil { 39 | log.Fatalf("Failed to unmarshal contents of %#v as config, err=%v", *f, err) 40 | } 41 | 42 | if c.BilderDir == "" { 43 | c.BilderDir = defaultConfig.BilderDir 44 | } 45 | 46 | if c.Addr == "" { 47 | c.Addr = defaultConfig.Addr 48 | } 49 | 50 | if c.ReloadDelaySeconds == 0 { 51 | c.ReloadDelaySeconds = defaultConfig.ReloadDelaySeconds 52 | } 53 | 54 | return c 55 | } 56 | -------------------------------------------------------------------------------- /assets/default-skin.svg: -------------------------------------------------------------------------------- 1 | default-skin 2 -------------------------------------------------------------------------------- /tools/gen_assets.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "text/template" 10 | ) 11 | 12 | type asset struct { 13 | ContentType string 14 | Content []byte 15 | } 16 | 17 | func main() { 18 | var ( 19 | assetsFileName = "assets.go" 20 | assetsTmpl = `package main 21 | 22 | type asset struct { 23 | ContentType string 24 | Content []byte 25 | } 26 | 27 | var assets = map[string]asset{} 28 | 29 | func init() { 30 | {{ range $n, $a := . }} 31 | assets[{{ printf "%q" $n }}] = asset{ 32 | ContentType: {{ printf "%q" $a.ContentType }}, 33 | Content: {{ printf "%#v" $a.Content }}, 34 | } 35 | {{ end }} 36 | } 37 | ` 38 | ) 39 | 40 | as := loadAssets() 41 | tmpl := template.Must(template.New("assets").Parse(assetsTmpl)) 42 | fh, err := os.OpenFile(assetsFileName, os.O_TRUNC|os.O_WRONLY, 0644) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | defer fh.Close() 47 | 48 | if err := tmpl.Execute(fh, as); err != nil { 49 | log.Fatal(err) 50 | } 51 | log.Printf("wrote %v assets to %#v", len(as), assetsFileName) 52 | } 53 | 54 | func loadAssets() map[string]asset { 55 | contents, err := ioutil.ReadDir("assets") 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | assets := map[string]asset{} 61 | for _, fi := range contents { 62 | if fi.IsDir() { 63 | continue 64 | } 65 | a := asset{ContentType: detectContentType(fi.Name())} 66 | a.Content, err = ioutil.ReadFile(filepath.Join("assets", fi.Name())) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | assets[fi.Name()] = a 71 | } 72 | 73 | return assets 74 | } 75 | 76 | func detectContentType(n string) string { 77 | switch { 78 | case strings.HasSuffix(n, ".css"): 79 | return "text/css; charset=utf-8" 80 | case strings.HasSuffix(n, ".js"): 81 | return "application/javascript" 82 | case strings.HasSuffix(n, ".html"): 83 | return "text/html; charset=utf-8" 84 | case strings.HasSuffix(n, ".svg"): 85 | return "image/svg+xml" 86 | default: 87 | return "application/octet-stream" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gorilla/handlers v0.0.0-20160721043455-801d6e3b0089 h1:/BEl5Rbd91qvjppKo3tdTZ3Akr7PeaymsT3sNy6+kh0= 2 | github.com/gorilla/handlers v0.0.0-20160721043455-801d6e3b0089/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 3 | github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg= 4 | github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= 5 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 6 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 7 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 8 | github.com/nfnt/resize v0.0.0-20160109112512-4d93a29130b1 h1:Gi7SMyKr6jDlZzNhBrTMD/1zFiHsd5NIQw2uAXDf3Jk= 9 | github.com/nfnt/resize v0.0.0-20160109112512-4d93a29130b1/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 10 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 11 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 12 | github.com/oliamb/cutter v0.2.1-0.20150813164310-6bb26911f524 h1:FlUtwnomEvJ4y29aVOeP/n7jG1Zv2zPZ3duQTeXOBlM= 13 | github.com/oliamb/cutter v0.2.1-0.20150813164310-6bb26911f524/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU= 14 | github.com/oliamb/cutter v0.2.2 h1:Lfwkya0HHNU1YLnGv2hTkzHfasrSMkgv4Dn+5rmlk3k= 15 | github.com/oliamb/cutter v0.2.2/go.mod h1:4BenG2/4GuRBDbVm/OPahDVqbrOemzpPiG5mi1iryBU= 16 | github.com/satori/go.uuid v1.1.1-0.20160713180306-0aa62d5ddceb h1:T3X8vQXH3/kJq4cD1P2pjIuz0Tfu2Vh10OCh9Rf6Hkw= 17 | github.com/satori/go.uuid v1.1.1-0.20160713180306-0aa62d5ddceb/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 18 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 19 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 20 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 21 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # bilder - web app to host photo albums. 2 | 3 | - Albums are directories with JPEG images that can be managed via rsync/scp. 4 | - It finds new albums and reloads their configuration and contents dynamically. 5 | - Thumbnails are generated automatically (filename_thumb.jpg). 6 | - Basic auth can be enabled per album. 7 | - Comes as a single binary. 8 | 9 | You can either download a [release](https://github.com/fgeller/bilder/releases) or get it via 10 | 11 | ``` 12 | $ go get github.com/fgeller/bilder 13 | ``` 14 | 15 | [Here](https://felix.geller.io/bilder/b/kitties)'s a live demo to click around. 16 | 17 | A screenshot: 18 | 19 | ![bilder demo](screenshot.png) 20 | 21 | A screencast: 22 | 23 | ![bilder demo](demo.gif) 24 | 25 | ## Configuration 26 | 27 | You can configure bilder via an optional JSON file. You can pass its location to **bilder** on startup: 28 | 29 | ``` 30 | $ bilder -config /path/to/your/config.json 31 | ``` 32 | 33 | It currently supports the following options: 34 | 35 | + `addr` *default:* `0.0.0.0:8173`: This is the address that bilder will serve on. 36 | + `url-path-prefix` *default:* `""`: This is a prefix that can be added to the assets' paths that are loaded from the browser. This allows bilder to run behind a proxy like nginx (e.g. if you want to use nginx to terminate the HTTPS connection). Consider the path of the demo linked above: [https://geller.io/bilder/b/kitties](https://geller.io/bilder/b/kitties). In this case nginx proxy passes to bilder under the `/bilder` path which we would set `url-path-prefix` to: 37 | ``` 38 | location /bilder/ { 39 | proxy_pass http://localhost:8173/; 40 | } 41 | ``` 42 | + `bilder-dir` *default:* `"bilder"`: This is the path of the folder that bilder scans for album directories. In the following example, this directory would contain a single album `kitties`: 43 | ``` 44 | $ find bilder 45 | bilder 46 | bilder/kitties 47 | bilder/kitties/happy.jpg 48 | ``` 49 | + `reload-delay-seconds` *default:* `10`: The time in seconds to wait between scans of `bilder-dir`. 50 | + `access-log` *default:* `""`: When set to a file name, bilder logs requests against the `/b` path in combined log format to the set file. 51 | 52 | This is the JSON file that is used for the [demo](https://geller.io/bilder/b/kitties): 53 | ``` 54 | { "bilder-dir": "/home/fgeller/var/bilder", "url-path-prefix": "/bilder", "addr": "0.0.0.0:8173" } 55 | ``` 56 | 57 | ### Albums 58 | 59 | Each sub-directory of the `bilder-dir` directory is considered an album if it contains JPG images. 60 | Only JPG images are currently supported. 61 | You can add more information about the album by adding a `bilder.json` to the directory. 62 | It currently supports the following options: 63 | 64 | + `user` *default:* `""`, `pass` *default:* `""`: If both are non-empty strings, bilder will use them as credentials to enable basic authentication for this album. 65 | + `title` *default:* `""`: Title that should be set for the album, defaults to the directory name. 66 | + `captions` *default:* `null`: Map object from file name to caption string (consider the demo example below). 67 | + `sort-order` *default:* `""`: Identifies sort order for images, supported: `ModTime` (newest first), `Name` (by file name, default). 68 | 69 | This is the `bilder.json` file in the `kitties` directory of the [demo](https://geller.io/bilder/b/kitties): 70 | ``` 71 | { 72 | "title": "Kitties", 73 | "captions": { 74 | "cat-eyes.jpg": "looking", 75 | "mini-monster.jpg": "rooooar!", 76 | "yawning.jpg": "Boring!" 77 | } 78 | } 79 | ``` 80 | 81 | ## Credits 82 | 83 | All images in the demos are free images from [pixabay](https://pixabay.com/). 84 | 85 | bilder uses the following libraries: 86 | 87 | + @dimsemenov's [PhotoSwipe](https://github.com/dimsemenov/PhotoSwipe) for rendering the album. 88 | + @nfnt's [resize](https://github.com/nfnt/resize) to generate thumbnails. 89 | + @oliamb's [cutter](https://github.com/oliamb/cutter) to crop thumbnails to a centered square. 90 | + @satori's [go.uuid](https://github.com/satori/go.uuid) to generate a random session ID. 91 | + @gorilla's [handlers](https://github.com/gorilla/handlers) for logging requests. 92 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/gorilla/handlers" 12 | uuid "github.com/satori/go.uuid" 13 | ) 14 | 15 | var ( 16 | cookieBaseName = "session-a2bb9-" 17 | nada = struct{}{} 18 | ) 19 | 20 | type syncFile struct { 21 | sync.Mutex 22 | os.File 23 | f *os.File 24 | } 25 | 26 | func (sf *syncFile) Write(p []byte) (n int, err error) { 27 | sf.Lock() 28 | defer sf.Unlock() 29 | return sf.f.Write(p) 30 | } 31 | 32 | type server struct { 33 | http.Server 34 | sync.RWMutex 35 | addr string 36 | albumUpdates <-chan []album 37 | dir string 38 | accessLog string 39 | logFile *syncFile 40 | albums map[string]authHandler 41 | } 42 | 43 | func newServer(ad, d, al string, au <-chan []album) *server { 44 | return &server{addr: ad, dir: d, accessLog: al, albumUpdates: au} 45 | } 46 | 47 | func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 48 | if r.URL.Path == "" { 49 | http.Error(w, "404 page not found", 404) 50 | return 51 | } 52 | 53 | sep := strings.Index(r.URL.Path, "/") 54 | an := r.URL.Path 55 | if sep > 0 { 56 | an = r.URL.Path[:sep] 57 | } 58 | 59 | s.RLock() 60 | h, ok := s.albums[an] 61 | s.RUnlock() 62 | 63 | if !ok { 64 | http.Error(w, "404 page not found", 404) 65 | return 66 | } 67 | 68 | p := strings.TrimPrefix(r.URL.Path, an) 69 | r.URL.Path = p 70 | h.ServeHTTP(w, r) 71 | } 72 | 73 | func assetsHandler(w http.ResponseWriter, r *http.Request) { 74 | p := r.URL.Path[len("/a/"):] 75 | a, ok := assets[p] 76 | if !ok { 77 | log.Printf("Got asset request %v but not available.\n", r.URL.Path) 78 | http.Error(w, "404 page not found", 404) 79 | return 80 | } 81 | 82 | w.Header().Set("Content-Type", a.ContentType) 83 | w.Write(a.Content) 84 | } 85 | 86 | type authHandler struct { 87 | sync.Mutex 88 | handler http.Handler 89 | name string 90 | user, pass string 91 | sessions map[string]struct{} 92 | authEnabled bool 93 | } 94 | 95 | func (h authHandler) isAuthed(sid string) bool { 96 | h.Lock() 97 | _, ok := h.sessions[sid] 98 | h.Unlock() 99 | return ok 100 | } 101 | 102 | func (h authHandler) newSession() string { 103 | sid := uuid.NewV1().String() 104 | h.Lock() 105 | h.sessions[sid] = nada 106 | h.Unlock() 107 | return sid 108 | } 109 | 110 | func (h authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 111 | if !h.authEnabled { 112 | h.handler.ServeHTTP(w, r) 113 | return 114 | } 115 | 116 | cookie, err := r.Cookie(cookieBaseName + h.name) 117 | if err == nil && cookie != nil && h.isAuthed(cookie.Value) { 118 | h.handler.ServeHTTP(w, r) 119 | return 120 | } 121 | 122 | u, p, ok := r.BasicAuth() 123 | if !(ok && u == h.user && p == h.pass) { 124 | w.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"") 125 | http.Error(w, "Not Authorized", http.StatusUnauthorized) 126 | return 127 | } 128 | 129 | sid := h.newSession() 130 | http.SetCookie(w, &http.Cookie{Name: cookieBaseName + h.name, Value: sid, MaxAge: 0}) 131 | h.handler.ServeHTTP(w, r) 132 | } 133 | 134 | func (s *server) listenForUpdates() { 135 | for as := range s.albumUpdates { 136 | s.RLock() 137 | oldHandlers := s.albums 138 | s.RUnlock() 139 | hs := make(map[string]authHandler) 140 | for _, a := range as { 141 | oh, oldExists := oldHandlers[a.name] 142 | sess := map[string]struct{}{} 143 | if oldExists { 144 | sess = oh.sessions 145 | } 146 | h := authHandler{ 147 | handler: http.FileServer(http.Dir(filepath.Join(s.dir, a.name))), 148 | name: a.name, 149 | user: a.user, 150 | pass: a.pass, 151 | sessions: sess, 152 | authEnabled: a.hasAuth(), 153 | } 154 | if s.logFile != nil { 155 | h.handler = handlers.CombinedLoggingHandler(s.logFile, h.handler) 156 | } 157 | hs[a.name] = h 158 | } 159 | 160 | s.Lock() 161 | s.albums = hs 162 | s.Unlock() 163 | } 164 | } 165 | 166 | func (s *server) serve() { 167 | go s.listenForUpdates() 168 | 169 | if s.accessLog != "" { 170 | lf, err := os.OpenFile(s.accessLog, os.O_WRONLY|os.O_CREATE, 0644) 171 | if err != nil { 172 | log.Printf("Cannot open access log %#v with write access, err=%v", s.accessLog, err) 173 | } else { 174 | s.logFile = &syncFile{f: lf} 175 | defer lf.Close() 176 | } 177 | } 178 | 179 | mux := http.NewServeMux() 180 | if len(assets) == 0 { 181 | log.Printf("Serving assets for assets directory.") 182 | mux.Handle("/a/", http.StripPrefix("/a/", http.FileServer(http.Dir("assets")))) 183 | } else { 184 | log.Printf("Serving assets from memory.") 185 | mux.HandleFunc("/a/", assetsHandler) 186 | } 187 | 188 | mux.Handle("/b/", http.StripPrefix("/b/", s)) 189 | 190 | s.Addr = s.addr 191 | s.Handler = mux 192 | 193 | log.Printf("Serving on http://" + s.addr) 194 | log.Fatal(s.ListenAndServe()) 195 | } 196 | -------------------------------------------------------------------------------- /assets/photoswipe.css: -------------------------------------------------------------------------------- 1 | /*! PhotoSwipe main CSS by Dmitry Semenov | photoswipe.com | MIT license */ 2 | /* 3 | Styles for basic PhotoSwipe functionality (sliding area, open/close transitions) 4 | */ 5 | /* pswp = photoswipe */ 6 | .pswp { 7 | display: none; 8 | position: absolute; 9 | width: 100%; 10 | height: 100%; 11 | left: 0; 12 | top: 0; 13 | overflow: hidden; 14 | -ms-touch-action: none; 15 | touch-action: none; 16 | z-index: 1500; 17 | -webkit-text-size-adjust: 100%; 18 | /* create separate layer, to avoid paint on window.onscroll in webkit/blink */ 19 | -webkit-backface-visibility: hidden; 20 | outline: none; } 21 | .pswp * { 22 | -webkit-box-sizing: border-box; 23 | box-sizing: border-box; } 24 | .pswp img { 25 | max-width: none; } 26 | 27 | /* style is added when JS option showHideOpacity is set to true */ 28 | .pswp--animate_opacity { 29 | /* 0.001, because opacity:0 doesn't trigger Paint action, which causes lag at start of transition */ 30 | opacity: 0.001; 31 | will-change: opacity; 32 | /* for open/close transition */ 33 | -webkit-transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1); 34 | transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1); } 35 | 36 | .pswp--open { 37 | display: block; } 38 | 39 | .pswp--zoom-allowed .pswp__img { 40 | /* autoprefixer: off */ 41 | cursor: -webkit-zoom-in; 42 | cursor: -moz-zoom-in; 43 | cursor: zoom-in; } 44 | 45 | .pswp--zoomed-in .pswp__img { 46 | /* autoprefixer: off */ 47 | cursor: -webkit-grab; 48 | cursor: -moz-grab; 49 | cursor: grab; } 50 | 51 | .pswp--dragging .pswp__img { 52 | /* autoprefixer: off */ 53 | cursor: -webkit-grabbing; 54 | cursor: -moz-grabbing; 55 | cursor: grabbing; } 56 | 57 | /* 58 | Background is added as a separate element. 59 | As animating opacity is much faster than animating rgba() background-color. 60 | */ 61 | .pswp__bg { 62 | position: absolute; 63 | left: 0; 64 | top: 0; 65 | width: 100%; 66 | height: 100%; 67 | background: #000; 68 | opacity: 0; 69 | -webkit-transform: translateZ(0); 70 | transform: translateZ(0); 71 | -webkit-backface-visibility: hidden; 72 | will-change: opacity; } 73 | 74 | .pswp__scroll-wrap { 75 | position: absolute; 76 | left: 0; 77 | top: 0; 78 | width: 100%; 79 | height: 100%; 80 | overflow: hidden; } 81 | 82 | .pswp__container, 83 | .pswp__zoom-wrap { 84 | -ms-touch-action: none; 85 | touch-action: none; 86 | position: absolute; 87 | left: 0; 88 | right: 0; 89 | top: 0; 90 | bottom: 0; } 91 | 92 | /* Prevent selection and tap highlights */ 93 | .pswp__container, 94 | .pswp__img { 95 | -webkit-user-select: none; 96 | -moz-user-select: none; 97 | -ms-user-select: none; 98 | user-select: none; 99 | -webkit-tap-highlight-color: transparent; 100 | -webkit-touch-callout: none; } 101 | 102 | .pswp__zoom-wrap { 103 | position: absolute; 104 | width: 100%; 105 | -webkit-transform-origin: left top; 106 | -ms-transform-origin: left top; 107 | transform-origin: left top; 108 | /* for open/close transition */ 109 | -webkit-transition: -webkit-transform 333ms cubic-bezier(0.4, 0, 0.22, 1); 110 | transition: transform 333ms cubic-bezier(0.4, 0, 0.22, 1); } 111 | 112 | .pswp__bg { 113 | will-change: opacity; 114 | /* for open/close transition */ 115 | -webkit-transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1); 116 | transition: opacity 333ms cubic-bezier(0.4, 0, 0.22, 1); } 117 | 118 | .pswp--animated-in .pswp__bg, 119 | .pswp--animated-in .pswp__zoom-wrap { 120 | -webkit-transition: none; 121 | transition: none; } 122 | 123 | .pswp__container, 124 | .pswp__zoom-wrap { 125 | -webkit-backface-visibility: hidden; } 126 | 127 | .pswp__item { 128 | position: absolute; 129 | left: 0; 130 | right: 0; 131 | top: 0; 132 | bottom: 0; 133 | overflow: hidden; } 134 | 135 | .pswp__img { 136 | position: absolute; 137 | width: auto; 138 | height: auto; 139 | top: 0; 140 | left: 0; } 141 | 142 | /* 143 | stretched thumbnail or div placeholder element (see below) 144 | style is added to avoid flickering in webkit/blink when layers overlap 145 | */ 146 | .pswp__img--placeholder { 147 | -webkit-backface-visibility: hidden; } 148 | 149 | /* 150 | div element that matches size of large image 151 | large image loads on top of it 152 | */ 153 | .pswp__img--placeholder--blank { 154 | background: #222; } 155 | 156 | .pswp--ie .pswp__img { 157 | width: 100% !important; 158 | height: auto !important; 159 | left: 0; 160 | top: 0; } 161 | 162 | /* 163 | Error message appears when image is not loaded 164 | (JS option errorMsg controls markup) 165 | */ 166 | .pswp__error-msg { 167 | position: absolute; 168 | left: 0; 169 | top: 50%; 170 | width: 100%; 171 | text-align: center; 172 | font-size: 14px; 173 | line-height: 16px; 174 | margin-top: -8px; 175 | color: #CCC; } 176 | 177 | .pswp__error-msg a { 178 | color: #CCC; 179 | text-decoration: underline; } 180 | -------------------------------------------------------------------------------- /assets/photoswipe-ui-default.min.js: -------------------------------------------------------------------------------- 1 | /*! PhotoSwipe Default UI - 4.1.3 - 2019-01-08 2 | * http://photoswipe.com 3 | * Copyright (c) 2019 Dmitry Semenov; */ 4 | !function(a,b){"function"==typeof define&&define.amd?define(b):"object"==typeof exports?module.exports=b():a.PhotoSwipeUI_Default=b()}(this,function(){"use strict";var a=function(a,b){var c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v=this,w=!1,x=!0,y=!0,z={barsSize:{top:44,bottom:"auto"},closeElClasses:["item","caption","zoom-wrap","ui","top-bar"],timeToIdle:4e3,timeToIdleOutside:1e3,loadingIndicatorDelay:1e3,addCaptionHTMLFn:function(a,b){return a.title?(b.children[0].innerHTML=a.title,!0):(b.children[0].innerHTML="",!1)},closeEl:!0,captionEl:!0,fullscreenEl:!0,zoomEl:!0,shareEl:!0,counterEl:!0,arrowEl:!0,preloaderEl:!0,tapToClose:!1,tapToToggleControls:!0,clickToCloseNonZoomable:!0,shareButtons:[{id:"facebook",label:"Share on Facebook",url:"https://www.facebook.com/sharer/sharer.php?u={{url}}"},{id:"twitter",label:"Tweet",url:"https://twitter.com/intent/tweet?text={{text}}&url={{url}}"},{id:"pinterest",label:"Pin it",url:"http://www.pinterest.com/pin/create/button/?url={{url}}&media={{image_url}}&description={{text}}"},{id:"download",label:"Download image",url:"{{raw_image_url}}",download:!0}],getImageURLForShare:function(){return a.currItem.src||""},getPageURLForShare:function(){return window.location.href},getTextForShare:function(){return a.currItem.title||""},indexIndicatorSep:" / ",fitControlsWidth:1200},A=function(a){if(r)return!0;a=a||window.event,q.timeToIdle&&q.mouseUsed&&!k&&K();for(var c,d,e=a.target||a.srcElement,f=e.getAttribute("class")||"",g=0;g-1&&(c.onTap(),d=!0);if(d){a.stopPropagation&&a.stopPropagation(),r=!0;var h=b.features.isOldAndroid?600:30;s=setTimeout(function(){r=!1},h)}},B=function(){return!a.likelyTouchDevice||q.mouseUsed||screen.width>q.fitControlsWidth},C=function(a,c,d){b[(d?"add":"remove")+"Class"](a,"pswp__"+c)},D=function(){var a=1===q.getNumItemsFn();a!==p&&(C(d,"ui--one-slide",a),p=a)},E=function(){C(i,"share-modal--hidden",y)},F=function(){return y=!y,y?(b.removeClass(i,"pswp__share-modal--fade-in"),setTimeout(function(){y&&E()},300)):(E(),setTimeout(function(){y||b.addClass(i,"pswp__share-modal--fade-in")},30)),y||H(),!1},G=function(b){b=b||window.event;var c=b.target||b.srcElement;return a.shout("shareLinkClick",b,c),!!c.href&&(!!c.hasAttribute("download")||(window.open(c.href,"pswp_share","scrollbars=yes,resizable=yes,toolbar=no,location=yes,width=550,height=420,top=100,left="+(window.screen?Math.round(screen.width/2-275):100)),y||F(),!1))},H=function(){for(var a,b,c,d,e,f="",g=0;g"+a.label+"",q.parseShareButtonOut&&(f=q.parseShareButtonOut(a,f));i.children[0].innerHTML=f,i.children[0].onclick=G},I=function(a){for(var c=0;c=.95&&v.showControls()});var a;l("onPinchClose",function(b){x&&b<.9?(v.hideControls(),a=!0):a&&!x&&b>.9&&v.showControls()}),l("zoomGestureEnded",function(){a=!1,a&&!x&&v.showControls()})},S=[{name:"caption",option:"captionEl",onInit:function(a){e=a}},{name:"share-modal",option:"shareEl",onInit:function(a){i=a},onTap:function(){F()}},{name:"button--share",option:"shareEl",onInit:function(a){h=a},onTap:function(){F()}},{name:"button--zoom",option:"zoomEl",onTap:a.toggleDesktopZoom},{name:"counter",option:"counterEl",onInit:function(a){g=a}},{name:"button--close",option:"closeEl",onTap:a.close},{name:"button--arrow--left",option:"arrowEl",onTap:a.prev},{name:"button--arrow--right",option:"arrowEl",onTap:a.next},{name:"button--fs",option:"fullscreenEl",onTap:function(){c.isFullscreen()?c.exit():c.enter()}},{name:"preloader",option:"preloaderEl",onInit:function(a){m=a}}],T=function(){var a,c,e,f=function(d){if(d)for(var f=d.length,g=0;g-1&&(q[e.option]?(b.removeClass(a,"pswp__element--disabled"),e.onInit&&e.onInit(a)):b.addClass(a,"pswp__element--disabled"))}};f(d.children);var g=b.getChildByClass(d,"pswp__top-bar");g&&f(g.children)};v.init=function(){b.extend(a.options,z,!0),q=a.options,d=b.getChildByClass(a.scrollWrap,"pswp__ui"),l=a.listen,R(),l("beforeChange",v.update),l("doubleTap",function(b){var c=a.currItem.initialZoomLevel;a.getZoomLevel()!==c?a.zoomTo(c,b,333):a.zoomTo(q.getDoubleTapZoom(!1,a.currItem),b,333)}),l("preventDragEvent",function(a,b,c){var d=a.target||a.srcElement;d&&d.getAttribute("class")&&a.type.indexOf("mouse")>-1&&(d.getAttribute("class").indexOf("__caption")>0||/(SMALL|STRONG|EM)/i.test(d.tagName))&&(c.prevent=!1)}),l("bindEvents",function(){b.bind(d,"pswpTap click",A),b.bind(a.scrollWrap,"pswpTap",v.onGlobalTap),a.likelyTouchDevice||b.bind(a.scrollWrap,"mouseover",v.onMouseOver)}),l("unbindEvents",function(){y||F(),t&&clearInterval(t),b.unbind(document,"mouseout",L),b.unbind(document,"mousemove",K),b.unbind(d,"pswpTap click",A),b.unbind(a.scrollWrap,"pswpTap",v.onGlobalTap),b.unbind(a.scrollWrap,"mouseover",v.onMouseOver),c&&(b.unbind(document,c.eventK,v.updateFullscreen),c.isFullscreen()&&(q.hideAnimationDuration=0,c.exit()),c=null)}),l("destroy",function(){q.captionEl&&(f&&d.removeChild(f),b.removeClass(e,"pswp__caption--empty")),i&&(i.children[0].onclick=null),b.removeClass(d,"pswp__ui--over-close"),b.addClass(d,"pswp__ui--hidden"),v.setIdle(!1)}),q.showAnimationDuration||b.removeClass(d,"pswp__ui--hidden"),l("initialZoomIn",function(){q.showAnimationDuration&&b.removeClass(d,"pswp__ui--hidden")}),l("initialZoomOut",function(){b.addClass(d,"pswp__ui--hidden")}),l("parseVerticalMargin",P),T(),q.shareEl&&h&&i&&(y=!0),D(),Q(),M(),N()},v.setIdle=function(a){k=a,C(d,"ui--idle",a)},v.update=function(){x&&a.currItem?(v.updateIndexIndicator(),q.captionEl&&(q.addCaptionHTMLFn(a.currItem,e),C(e,"caption--empty",!a.currItem.title)),w=!0):w=!1,y||F(),D()},v.updateFullscreen=function(d){d&&setTimeout(function(){a.setScrollOffset(0,b.getScrollY())},50),b[(c.isFullscreen()?"add":"remove")+"Class"](a.template,"pswp--fs")},v.updateIndexIndicator=function(){q.counterEl&&(g.innerHTML=a.getCurrentIndex()+1+q.indexIndicatorSep+q.getNumItemsFn())},v.onGlobalTap=function(c){c=c||window.event;var d=c.target||c.srcElement;if(!r)if(c.detail&&"mouse"===c.detail.pointerType){if(I(d))return void a.close();b.hasClass(d,"pswp__img")&&(1===a.getZoomLevel()&&a.getZoomLevel()<=a.currItem.fitRatio?q.clickToCloseNonZoomable&&a.close():a.toggleDesktopZoom(c.detail.releasePoint))}else if(q.tapToToggleControls&&(x?v.hideControls():v.showControls()),q.tapToClose&&(b.hasClass(d,"pswp__img")||I(d)))return void a.close()},v.onMouseOver=function(a){a=a||window.event;var b=a.target||a.srcElement;C(d,"ui--over-close",I(b))},v.hideControls=function(){b.addClass(d,"pswp__ui--hidden"),x=!1},v.showControls=function(){x=!0,w||v.update(),b.removeClass(d,"pswp__ui--hidden")},v.supportsFullscreen=function(){var a=document;return!!(a.exitFullscreen||a.mozCancelFullScreen||a.webkitExitFullscreen||a.msExitFullscreen)},v.getFullscreenAPI=function(){var b,c=document.documentElement,d="fullscreenchange";return c.requestFullscreen?b={enterK:"requestFullscreen",exitK:"exitFullscreen",elementK:"fullscreenElement",eventK:d}:c.mozRequestFullScreen?b={enterK:"mozRequestFullScreen",exitK:"mozCancelFullScreen",elementK:"mozFullScreenElement",eventK:"moz"+d}:c.webkitRequestFullscreen?b={enterK:"webkitRequestFullscreen",exitK:"webkitExitFullscreen",elementK:"webkitFullscreenElement",eventK:"webkit"+d}:c.msRequestFullscreen&&(b={enterK:"msRequestFullscreen",exitK:"msExitFullscreen",elementK:"msFullscreenElement",eventK:"MSFullscreenChange"}),b&&(b.enter=function(){return j=q.closeOnScroll,q.closeOnScroll=!1,"webkitRequestFullscreen"!==this.enterK?a.template[this.enterK]():void a.template[this.enterK](Element.ALLOW_KEYBOARD_INPUT)},b.exit=function(){return q.closeOnScroll=j,document[this.exitK]()},b.isFullscreen=function(){return document[this.elementK]}),b}};return a}); -------------------------------------------------------------------------------- /assets/default-skin.css: -------------------------------------------------------------------------------- 1 | /*! PhotoSwipe Default UI CSS by Dmitry Semenov | photoswipe.com | MIT license */ 2 | /* 3 | 4 | Contents: 5 | 6 | 1. Buttons 7 | 2. Share modal and links 8 | 3. Index indicator ("1 of X" counter) 9 | 4. Caption 10 | 5. Loading indicator 11 | 6. Additional styles (root element, top bar, idle state, hidden state, etc.) 12 | 13 | */ 14 | /* 15 | 16 | 1. Buttons 17 | 18 | */ 19 | /* 407 | 408 | 409 | 410 |
411 |
412 |
413 |
414 |
415 |
416 |
417 | 418 | 421 | 423 | 425 |
426 |
427 |
428 | 429 | 430 | 431 | 439 | 597 | 598 | 599 | ` 600 | ) 601 | -------------------------------------------------------------------------------- /assets/photoswipe.min.js: -------------------------------------------------------------------------------- 1 | /*! PhotoSwipe - v4.1.3 - 2019-01-08 2 | * http://photoswipe.com 3 | * Copyright (c) 2019 Dmitry Semenov; */ 4 | !function(a,b){"function"==typeof define&&define.amd?define(b):"object"==typeof exports?module.exports=b():a.PhotoSwipe=b()}(this,function(){"use strict";var a=function(a,b,c,d){var e={features:null,bind:function(a,b,c,d){var e=(d?"remove":"add")+"EventListener";b=b.split(" ");for(var f=0;f0&&(g=parseInt(g[1],10),g>=1&&g<8&&(d.isOldIOSPhone=!0))}var h=f.match(/Android\s([0-9\.]*)/),i=h?h[1]:0;i=parseFloat(i),i>=1&&(i<4.4&&(d.isOldAndroid=!0),d.androidVersion=i),d.isMobileOpera=/opera mini|opera mobi/i.test(f)}for(var j,k,l=["transform","perspective","animationName"],m=["","webkit","Moz","ms","O"],n=0;n<4;n++){c=m[n];for(var o=0;o<3;o++)j=l[o],k=c+(c?j.charAt(0).toUpperCase()+j.slice(1):j),!d[j]&&k in b&&(d[j]=k);c&&!d.raf&&(c=c.toLowerCase(),d.raf=window[c+"RequestAnimationFrame"],d.raf&&(d.caf=window[c+"CancelAnimationFrame"]||window[c+"CancelRequestAnimationFrame"]))}if(!d.raf){var p=0;d.raf=function(a){var b=(new Date).getTime(),c=Math.max(0,16-(b-p)),d=window.setTimeout(function(){a(b+c)},c);return p=b+c,d},d.caf=function(a){clearTimeout(a)}}return d.svg=!!document.createElementNS&&!!document.createElementNS("http://www.w3.org/2000/svg","svg").createSVGRect,e.features=d,d}};e.detectFeatures(),e.features.oldIE&&(e.bind=function(a,b,c,d){b=b.split(" ");for(var e,f=(d?"detach":"attach")+"Event",g=function(){c.handleEvent.call(c)},h=0;hb-1?a-b:a<0?b+a:a},Ba={},Ca=function(a,b){return Ba[a]||(Ba[a]=[]),Ba[a].push(b)},Da=function(a){var b=Ba[a];if(b){var c=Array.prototype.slice.call(arguments);c.shift();for(var d=0;df.currItem.fitRatio?ya||(mc(f.currItem,!1,!0),ya=!0):ya&&(mc(f.currItem),ya=!1)),Ga(ea,pa.x,pa.y,s))},Ia=function(a){a.container&&Ga(a.container.style,a.initialPosition.x,a.initialPosition.y,a.initialZoomLevel,a)},Ja=function(a,b){b[E]=u+a+"px, 0px"+v},Ka=function(a,b){if(!i.loop&&b){var c=m+(ta.x*ra-a)/ta.x,d=Math.round(a-tb.x);(c<0&&d>0||c>=ac()-1&&d<0)&&(a=tb.x+d*i.mainScrollEndFriction)}tb.x=a,Ja(a,n)},La=function(a,b){var c=ub[a]-sa[a];return oa[a]+na[a]+c-c*(b/t)},Ma=function(a,b){a.x=b.x,a.y=b.y,b.id&&(a.id=b.id)},Na=function(a){a.x=Math.round(a.x),a.y=Math.round(a.y)},Oa=null,Pa=function(){Oa&&(e.unbind(document,"mousemove",Pa),e.addClass(a,"pswp--has_mouse"),i.mouseUsed=!0,Da("mouseUsed")),Oa=setTimeout(function(){Oa=null},100)},Qa=function(){e.bind(document,"keydown",f),N.transform&&e.bind(f.scrollWrap,"click",f),i.mouseUsed||e.bind(document,"mousemove",Pa),e.bind(window,"resize scroll orientationchange",f),Da("bindEvents")},Ra=function(){e.unbind(window,"resize scroll orientationchange",f),e.unbind(window,"scroll",r.scroll),e.unbind(document,"keydown",f),e.unbind(document,"mousemove",Pa),N.transform&&e.unbind(f.scrollWrap,"click",f),V&&e.unbind(window,p,f),clearTimeout(O),Da("unbindEvents")},Sa=function(a,b){var c=ic(f.currItem,qa,a);return b&&(da=c),c},Ta=function(a){return a||(a=f.currItem),a.initialZoomLevel},Ua=function(a){return a||(a=f.currItem),a.w>0?i.maxSpreadZoom:1},Va=function(a,b,c,d){return d===f.currItem.initialZoomLevel?(c[a]=f.currItem.initialPosition[a],!0):(c[a]=La(a,d),c[a]>b.min[a]?(c[a]=b.min[a],!0):c[a]1?1:a.fitRatio,c=a.container.style,d=b*a.w,e=b*a.h;c.width=d+"px",c.height=e+"px",c.left=a.initialPosition.x+"px",c.top=a.initialPosition.y+"px"},Ha=function(){if(ea){var a=ea,b=f.currItem,c=b.fitRatio>1?1:b.fitRatio,d=c*b.w,e=c*b.h;a.width=d+"px",a.height=e+"px",a.left=pa.x+"px",a.top=pa.y+"px"}}},Xa=function(a){var b="";i.escKey&&27===a.keyCode?b="close":i.arrowKeys&&(37===a.keyCode?b="prev":39===a.keyCode&&(b="next")),b&&(a.ctrlKey||a.altKey||a.shiftKey||a.metaKey||(a.preventDefault?a.preventDefault():a.returnValue=!1,f[b]()))},Ya=function(a){a&&(Y||X||fa||T)&&(a.preventDefault(),a.stopPropagation())},Za=function(){f.setScrollOffset(0,e.getScrollY())},$a={},_a=0,ab=function(a){$a[a]&&($a[a].raf&&I($a[a].raf),_a--,delete $a[a])},bb=function(a){$a[a]&&ab(a),$a[a]||(_a++,$a[a]={})},cb=function(){for(var a in $a)$a.hasOwnProperty(a)&&ab(a)},db=function(a,b,c,d,e,f,g){var h,i=Ea();bb(a);var j=function(){if($a[a]){if(h=Ea()-i,h>=d)return ab(a),f(c),void(g&&g());f((c-b)*e(h/d)+b),$a[a].raf=H(j)}};j()},eb={shout:Da,listen:Ca,viewportSize:qa,options:i,isMainScrollAnimating:function(){return fa},getZoomLevel:function(){return s},getCurrentIndex:function(){return m},isDragging:function(){return V},isZooming:function(){return aa},setScrollOffset:function(a,b){sa.x=a,M=sa.y=b,Da("updateScrollOffset",sa)},applyZoomPan:function(a,b,c,d){pa.x=b,pa.y=c,s=a,Ha(d)},init:function(){if(!j&&!k){var c;f.framework=e,f.template=a,f.bg=e.getChildByClass(a,"pswp__bg"),J=a.className,j=!0,N=e.detectFeatures(),H=N.raf,I=N.caf,E=N.transform,L=N.oldIE,f.scrollWrap=e.getChildByClass(a,"pswp__scroll-wrap"),f.container=e.getChildByClass(f.scrollWrap,"pswp__container"),n=f.container.style,f.itemHolders=y=[{el:f.container.children[0],wrap:0,index:-1},{el:f.container.children[1],wrap:0,index:-1},{el:f.container.children[2],wrap:0,index:-1}],y[0].el.style.display=y[2].el.style.display="none",Wa(),r={resize:f.updateSize,orientationchange:function(){clearTimeout(O),O=setTimeout(function(){qa.x!==f.scrollWrap.clientWidth&&f.updateSize()},500)},scroll:Za,keydown:Xa,click:Ya};var d=N.isOldIOSPhone||N.isOldAndroid||N.isMobileOpera;for(N.animationName&&N.transform&&!d||(i.showAnimationDuration=i.hideAnimationDuration=0),c=0;c=ac())&&(m=0),f.currItem=_b(m),(N.isOldIOSPhone||N.isOldAndroid)&&(va=!1),a.setAttribute("aria-hidden","false"),i.modal&&(va?a.style.position="fixed":(a.style.position="absolute",a.style.top=e.getScrollY()+"px")),void 0===M&&(Da("initialLayout"),M=K=e.getScrollY());var l="pswp--open ";for(i.mainClass&&(l+=i.mainClass+" "),i.showHideOpacity&&(l+="pswp--animate_opacity "),l+=G?"pswp--touch":"pswp--notouch",l+=N.animationName?" pswp--css_animation":"",l+=N.svg?" pswp--svg":"",e.addClass(a,l),f.updateSize(),o=-1,ua=null,c=0;cda.min.x?a=da.min.x:ada.min.y?b=da.min.y:b=h&&(o+=ua+(ua>0?-h:h),c=h);for(var d=0;d0?(b=y.shift(),y[h-1]=b,o++,Ja((o+2)*ta.x,b.el.style),f.setContent(b,m-c+d+1+1)):(b=y.pop(),y.unshift(b),o--,Ja(o*ta.x,b.el.style),f.setContent(b,m+c-d-1-1));if(ea&&1===Math.abs(ua)){var e=_b(z);e.initialZoomLevel!==s&&(ic(e,qa),mc(e),Ia(e))}ua=0,f.updateCurrZoomItem(),z=m,Da("afterChange")}}},updateSize:function(b){if(!va&&i.modal){var c=e.getScrollY();if(M!==c&&(a.style.top=c+"px",M=c),!b&&xa.x===window.innerWidth&&xa.y===window.innerHeight)return;xa.x=window.innerWidth,xa.y=window.innerHeight,a.style.height=xa.y+"px"}if(qa.x=f.scrollWrap.clientWidth,qa.y=f.scrollWrap.clientHeight,Za(),ta.x=qa.x+Math.round(qa.x*i.spacing),ta.y=qa.y,Ka(ta.x*ra),Da("beforeResize"),void 0!==o){for(var d,g,j,k=0;k2&&(j=Aa(j)),g=_b(j),g&&(x||g.needsUpdate||!g.bounds)?(f.cleanSlide(g),f.setContent(d,j),1===k&&(f.currItem=g,f.updateCurrZoomItem(!0)),g.needsUpdate=!1):d.index===-1&&j>=0&&f.setContent(d,j),g&&g.container&&(ic(g,qa),mc(g),Ia(g));x=!1}t=s=f.currItem.initialZoomLevel,da=f.currItem.bounds,da&&(pa.x=da.center.x,pa.y=da.center.y,Ha(!0)),Da("resize")},zoomTo:function(a,b,c,d,f){b&&(t=s,ub.x=Math.abs(b.x)-pa.x,ub.y=Math.abs(b.y)-pa.y,Ma(oa,pa));var g=Sa(a,!1),h={};Va("x",g,h,a),Va("y",g,h,a);var i=s,j={x:pa.x,y:pa.y};Na(h);var k=function(b){1===b?(s=a,pa.x=h.x,pa.y=h.y):(s=(a-i)*b+i,pa.x=(h.x-j.x)*b+j.x,pa.y=(h.y-j.y)*b+j.y),f&&f(b),Ha(1===b)};c?db("customZoomTo",0,1,c,d||e.easing.sine.inOut,k):k(1)}},fb=30,gb=10,hb={},ib={},jb={},kb={},lb={},mb=[],nb={},ob=[],pb={},qb=0,rb=ma(),sb=0,tb=ma(),ub=ma(),vb=ma(),wb=function(a,b){return a.x===b.x&&a.y===b.y},xb=function(a,b){return Math.abs(a.x-b.x)-1)&&(b(a)?a:Cb(a.parentNode,b)))},Db={},Eb=function(a,b){return Db.prevent=!Cb(a.target,i.isClickableElement),Da("preventDragEvent",a,b,Db),Db.prevent},Fb=function(a,b){return b.x=a.pageX,b.y=a.pageY,b.id=a.identifier,b},Gb=function(a,b,c){c.x=.5*(a.x+b.x),c.y=.5*(a.y+b.y)},Hb=function(a,b,c){if(a-Q>50){var d=ob.length>2?ob.shift():{};d.x=b,d.y=c,ob.push(d),Q=a}},Ib=function(){var a=pa.y-f.currItem.initialPosition.y;return 1-Math.abs(a/(qa.y/2))},Jb={},Kb={},Lb=[],Mb=function(a){for(;Lb.length>0;)Lb.pop();return F?(la=0,mb.forEach(function(a){0===la?Lb[0]=a:1===la&&(Lb[1]=a),la++})):a.type.indexOf("touch")>-1?a.touches&&a.touches.length>0&&(Lb[0]=Fb(a.touches[0],Jb),a.touches.length>1&&(Lb[1]=Fb(a.touches[1],Kb))):(Jb.x=a.pageX,Jb.y=a.pageY,Jb.id="",Lb[0]=Jb),Lb},Nb=function(a,b){var c,d,e,g,h=0,j=pa[a]+b[a],k=b[a]>0,l=tb.x+b.x,m=tb.x-nb.x;return c=j>da.min[a]||jda.min[a]&&(c=i.panEndFriction,h=da.min[a]-j,d=da.min[a]-oa[a]),(d<=0||m<0)&&ac()>1?(g=l,m<0&&l>nb.x&&(g=nb.x)):da.min.x!==da.max.x&&(e=j)):(j0)&&ac()>1?(g=l,m>0&&lf.currItem.fitRatio&&(pa[a]+=b[a]*c)):(void 0!==g&&(Ka(g,!0),$=g!==nb.x),da.min.x!==da.max.x&&(void 0!==e?pa.x=e:$||(pa.x+=b.x*c)),void 0!==g)},Ob=function(a){if(!("mousedown"===a.type&&a.button>0)){if($b)return void a.preventDefault();if(!U||"mousedown"!==a.type){if(Eb(a,!0)&&a.preventDefault(),Da("pointerDown"),F){var b=e.arraySearch(mb,a.pointerId,"id");b<0&&(b=mb.length),mb[b]={x:a.pageX,y:a.pageY,id:a.pointerId}}var c=Mb(a),d=c.length;_=null,cb(),V&&1!==d||(V=ha=!0,e.bind(window,p,f),S=ka=ia=T=$=Y=W=X=!1,ga=null,Da("firstTouchStart",c),Ma(oa,pa),na.x=na.y=0,Ma(kb,c[0]),Ma(lb,kb),nb.x=ta.x*ra,ob=[{x:kb.x,y:kb.y}],Q=P=Ea(),Sa(s,!0),zb(),Ab()),!aa&&d>1&&!fa&&!$&&(t=s,X=!1,aa=W=!0,na.y=na.x=0,Ma(oa,pa),Ma(hb,c[0]),Ma(ib,c[1]),Gb(hb,ib,vb),ub.x=Math.abs(vb.x)-pa.x,ub.y=Math.abs(vb.y)-pa.y,ba=ca=yb(hb,ib))}}},Pb=function(a){if(a.preventDefault(),F){var b=e.arraySearch(mb,a.pointerId,"id");if(b>-1){var c=mb[b];c.x=a.pageX,c.y=a.pageY}}if(V){var d=Mb(a);if(ga||Y||aa)_=d;else if(tb.x!==ta.x*ra)ga="h";else{var f=Math.abs(d[0].x-kb.x)-Math.abs(d[0].y-kb.y);Math.abs(f)>=gb&&(ga=f>0?"h":"v",_=d)}}},Qb=function(){if(_){var a=_.length;if(0!==a)if(Ma(hb,_[0]),jb.x=hb.x-kb.x,jb.y=hb.y-kb.y,aa&&a>1){if(kb.x=hb.x,kb.y=hb.y,!jb.x&&!jb.y&&wb(_[1],ib))return;Ma(ib,_[1]),X||(X=!0,Da("zoomGestureStarted"));var b=yb(hb,ib),c=Vb(b);c>f.currItem.initialZoomLevel+f.currItem.initialZoomLevel/15&&(ka=!0);var d=1,e=Ta(),g=Ua();if(c1&&(d=1),c=e-d*(e/3);else c>g&&(d=(c-g)/(6*e),d>1&&(d=1),c=g+d*e);d<0&&(d=0),ba=b,Gb(hb,ib,rb),na.x+=rb.x-vb.x,na.y+=rb.y-vb.y,Ma(vb,rb),pa.x=La("x",c),pa.y=La("y",c),S=c>s,s=c,Ha()}else{if(!ga)return;if(ha&&(ha=!1,Math.abs(jb.x)>=gb&&(jb.x-=_[0].x-lb.x),Math.abs(jb.y)>=gb&&(jb.y-=_[0].y-lb.y)),kb.x=hb.x,kb.y=hb.y,0===jb.x&&0===jb.y)return;if("v"===ga&&i.closeOnVerticalDrag&&!Bb()){na.y+=jb.y,pa.y+=jb.y;var k=Ib();return T=!0,Da("onVerticalDrag",k),Fa(k),void Ha()}Hb(Ea(),hb.x,hb.y),Y=!0,da=f.currItem.bounds;var l=Nb("x",jb);l||(Nb("y",jb),Na(pa),Ha())}}},Rb=function(a){if(N.isOldAndroid){if(U&&"mouseup"===a.type)return;a.type.indexOf("touch")>-1&&(clearTimeout(U),U=setTimeout(function(){U=0},600))}Da("pointerUp"),Eb(a,!1)&&a.preventDefault();var b;if(F){var c=e.arraySearch(mb,a.pointerId,"id");if(c>-1)if(b=mb.splice(c,1)[0],navigator.msPointerEnabled){var d={4:"mouse",2:"touch",3:"pen"};b.type=d[a.pointerType],b.type||(b.type=a.pointerType||"mouse")}else b.type=a.pointerType||"mouse"}var g,h=Mb(a),j=h.length;if("mouseup"===a.type&&(j=0),2===j)return _=null,!0;1===j&&Ma(lb,h[0]),0!==j||ga||fa||(b||("mouseup"===a.type?b={x:a.pageX,y:a.pageY,type:"mouse"}:a.changedTouches&&a.changedTouches[0]&&(b={x:a.changedTouches[0].pageX,y:a.changedTouches[0].pageY,type:"touch"})),Da("touchRelease",a,b));var k=-1;if(0===j&&(V=!1,e.unbind(window,p,f),zb(),aa?k=0:sb!==-1&&(k=Ea()-sb)),sb=1===j?Ea():-1,g=k!==-1&&k<150?"zoom":"swipe",aa&&j<2&&(aa=!1,1===j&&(g="zoomPointerUp"),Da("zoomGestureEnded")),_=null,Y||X||fa||T)if(cb(),R||(R=Sb()),R.calculateSwipeSpeed("x"),T){var l=Ib();if(lf.currItem.fitRatio&&Tb(R))}},Sb=function(){var a,b,c={lastFlickOffset:{},lastFlickDist:{},lastFlickSpeed:{},slowDownRatio:{},slowDownRatioReverse:{},speedDecelerationRatio:{},speedDecelerationRatioAbs:{},distanceOffset:{},backAnimDestination:{},backAnimStarted:{},calculateSwipeSpeed:function(d){ob.length>1?(a=Ea()-Q+50,b=ob[ob.length-2][d]):(a=Ea()-P,b=lb[d]),c.lastFlickOffset[d]=kb[d]-b,c.lastFlickDist[d]=Math.abs(c.lastFlickOffset[d]),c.lastFlickDist[d]>20?c.lastFlickSpeed[d]=c.lastFlickOffset[d]/a:c.lastFlickSpeed[d]=0,Math.abs(c.lastFlickSpeed[d])<.1&&(c.lastFlickSpeed[d]=0),c.slowDownRatio[d]=.95,c.slowDownRatioReverse[d]=1-c.slowDownRatio[d],c.speedDecelerationRatio[d]=1},calculateOverBoundsAnimOffset:function(a,b){c.backAnimStarted[a]||(pa[a]>da.min[a]?c.backAnimDestination[a]=da.min[a]:pa[a]fb&&(h||b.lastFlickOffset.x>20)?d=-1:g<-fb&&(h||b.lastFlickOffset.x<-20)&&(d=1)}var j;d&&(m+=d,m<0?(m=i.loop?ac()-1:0,j=!0):m>=ac()&&(m=i.loop?0:ac()-1,j=!0),j&&!i.loop||(ua+=d,ra-=d,c=!0));var k,l=ta.x*ra,n=Math.abs(l-tb.x);return c||l>tb.x==b.lastFlickSpeed.x>0?(k=Math.abs(b.lastFlickSpeed.x)>0?n/Math.abs(b.lastFlickSpeed.x):333,k=Math.min(k,400),k=Math.max(k,250)):k=333,qb===m&&(c=!1),fa=!0,Da("mainScrollAnimStart"),db("mainScroll",tb.x,l,k,e.easing.cubic.out,Ka,function(){cb(),fa=!1,qb=-1,(c||qb!==m)&&f.updateCurrItem(),Da("mainScrollAnimComplete")}),c&&f.updateCurrItem(!0),c},Vb=function(a){return 1/ca*a*t},Wb=function(){var a=s,b=Ta(),c=Ua();sc&&(a=c);var d,g=1,h=ja;return ia&&!S&&!ka&&s1||navigator.msMaxTouchPoints>1),f.likelyTouchDevice=G,r[A]=Ob,r[B]=Pb,r[C]=Rb,D&&(r[D]=r[C]),N.touch&&(q+=" mousedown",p+=" mousemove mouseup",r.mousedown=r[A],r.mousemove=r[B],r.mouseup=r[C]),G||(i.allowPanToNext=!1)}}});var Xb,Yb,Zb,$b,_b,ac,bc,cc=function(b,c,d,g){Xb&&clearTimeout(Xb),$b=!0,Zb=!0;var h;b.initialLayout?(h=b.initialLayout,b.initialLayout=null):h=i.getThumbBoundsFn&&i.getThumbBoundsFn(m);var j=d?i.hideAnimationDuration:i.showAnimationDuration,k=function(){ab("initialZoom"),d?(f.template.removeAttribute("style"),f.bg.removeAttribute("style")):(Fa(1),c&&(c.style.display="block"),e.addClass(a,"pswp--animated-in"),Da("initialZoom"+(d?"OutEnd":"InEnd"))),g&&g(),$b=!1};if(!j||!h||void 0===h.x)return Da("initialZoom"+(d?"Out":"In")),s=b.initialZoomLevel,Ma(pa,b.initialPosition),Ha(),a.style.opacity=d?0:1,Fa(1),void(j?setTimeout(function(){k()},j):k());var n=function(){var c=l,g=!f.currItem.src||f.currItem.loadError||i.showHideOpacity;b.miniImg&&(b.miniImg.style.webkitBackfaceVisibility="hidden"),d||(s=h.w/b.w,pa.x=h.x,pa.y=h.y-K,f[g?"template":"bg"].style.opacity=.001,Ha()),bb("initialZoom"),d&&!c&&e.removeClass(a,"pswp--animated-in"),g&&(d?e[(c?"remove":"add")+"Class"](a,"pswp--animate_opacity"):setTimeout(function(){e.addClass(a,"pswp--animate_opacity")},30)),Xb=setTimeout(function(){if(Da("initialZoom"+(d?"Out":"In")),d){var f=h.w/b.w,i={x:pa.x,y:pa.y},l=s,m=ja,n=function(b){1===b?(s=f,pa.x=h.x,pa.y=h.y-M):(s=(f-l)*b+l,pa.x=(h.x-i.x)*b+i.x,pa.y=(h.y-M-i.y)*b+i.y),Ha(),g?a.style.opacity=1-b:Fa(m-b*m)};c?db("initialZoom",0,1,j,e.easing.cubic.out,n,k):(n(1),Xb=setTimeout(k,j+20))}else s=b.initialZoomLevel,Ma(pa,b.initialPosition),Ha(),Fa(1),g?a.style.opacity=1:Fa(1),Xb=setTimeout(k,j+20)},d?25:90)};n()},dc={},ec=[],fc={index:0,errorMsg:'
The image could not be loaded.
',forceProgressiveLoading:!1,preload:[1,1],getNumItemsFn:function(){return Yb.length}},gc=function(){return{center:{x:0,y:0},max:{x:0,y:0},min:{x:0,y:0}}},hc=function(a,b,c){var d=a.bounds;d.center.x=Math.round((dc.x-b)/2),d.center.y=Math.round((dc.y-c)/2)+a.vGap.top,d.max.x=b>dc.x?Math.round(dc.x-b):d.center.x,d.max.y=c>dc.y?Math.round(dc.y-c)+a.vGap.top:d.center.y,d.min.x=b>dc.x?0:d.center.x,d.min.y=c>dc.y?a.vGap.top:d.center.y},ic=function(a,b,c){if(a.src&&!a.loadError){var d=!c;if(d&&(a.vGap||(a.vGap={top:0,bottom:0}),Da("parseVerticalMargin",a)),dc.x=b.x,dc.y=b.y-a.vGap.top-a.vGap.bottom,d){var e=dc.x/a.w,f=dc.y/a.h;a.fitRatio=e1&&(c=1),a.initialZoomLevel=c,a.bounds||(a.bounds=gc())}if(!c)return;return hc(a,a.w*c,a.h*c),d&&c===a.initialZoomLevel&&(a.initialPosition=a.bounds.center),a.bounds}return a.w=a.h=0,a.initialZoomLevel=a.fitRatio=1,a.bounds=gc(),a.initialPosition=a.bounds.center,a.bounds},jc=function(a,b,c,d,e,g){b.loadError||d&&(b.imageAppended=!0,mc(b,d,b===f.currItem&&ya),c.appendChild(d),g&&setTimeout(function(){b&&b.loaded&&b.placeholder&&(b.placeholder.style.display="none",b.placeholder=null)},500))},kc=function(a){a.loading=!0,a.loaded=!1;var b=a.img=e.createEl("pswp__img","img"),c=function(){a.loading=!1,a.loaded=!0,a.loadComplete?a.loadComplete(a):a.img=null,b.onload=b.onerror=null,b=null};return b.onload=c,b.onerror=function(){a.loadError=!0,c()},b.src=a.src,b},lc=function(a,b){if(a.src&&a.loadError&&a.container)return b&&(a.container.innerHTML=""),a.container.innerHTML=i.errorMsg.replace("%url%",a.src),!0},mc=function(a,b,c){if(a.src){b||(b=a.container.lastChild);var d=c?a.w:Math.round(a.w*a.fitRatio),e=c?a.h:Math.round(a.h*a.fitRatio);a.placeholder&&!a.loaded&&(a.placeholder.style.width=d+"px",a.placeholder.style.height=e+"px"),b.style.width=d+"px",b.style.height=e+"px"}},nc=function(){if(ec.length){for(var a,b=0;b=0,e=Math.min(c[0],ac()),g=Math.min(c[1],ac());for(b=1;b<=(d?g:e);b++)f.lazyLoadItem(m+b);for(b=1;b<=(d?e:g);b++)f.lazyLoadItem(m-b)}),Ca("initialLayout",function(){f.currItem.initialLayout=i.getThumbBoundsFn&&i.getThumbBoundsFn(m)}),Ca("mainScrollAnimComplete",nc),Ca("initialZoomInEnd",nc),Ca("destroy",function(){for(var a,b=0;b=0&&(void 0!==Yb[a]&&Yb[a])},allowProgressiveImg:function(){return i.forceProgressiveLoading||!G||i.mouseUsed||screen.width>1200},setContent:function(a,b){i.loop&&(b=Aa(b));var c=f.getItemAt(a.index);c&&(c.container=null);var d,g=f.getItemAt(b);if(!g)return void(a.el.innerHTML="");Da("gettingData",b,g),a.index=b,a.item=g;var h=g.container=e.createEl("pswp__zoom-wrap");if(!g.src&&g.html&&(g.html.tagName?h.appendChild(g.html):h.innerHTML=g.html),lc(g),ic(g,qa),!g.src||g.loadError||g.loaded)g.src&&!g.loadError&&(d=e.createEl("pswp__img","img"),d.style.opacity=1,d.src=g.src,mc(g,d),jc(b,g,h,d,!0));else{if(g.loadComplete=function(c){if(j){if(a&&a.index===b){if(lc(c,!0))return c.loadComplete=c.img=null,ic(c,qa),Ia(c),void(a.index===m&&f.updateCurrZoomItem());c.imageAppended?!$b&&c.placeholder&&(c.placeholder.style.display="none",c.placeholder=null):N.transform&&(fa||$b)?ec.push({item:c,baseDiv:h,img:c.img,index:b,holder:a,clearPlaceholder:!0}):jc(b,c,h,c.img,fa||$b,!0)}c.loadComplete=null,c.img=null,Da("imageLoadComplete",b,c)}},e.features.transform){var k="pswp__img pswp__img--placeholder";k+=g.msrc?"":" pswp__img--placeholder--blank";var l=e.createEl(k,g.msrc?"img":"");g.msrc&&(l.src=g.msrc),mc(g,l),h.appendChild(l),g.placeholder=l}g.loading||kc(g),f.allowProgressiveImg()&&(!Zb&&N.transform?ec.push({item:g,baseDiv:h,img:g.img,index:b,holder:a}):jc(b,g,h,g.img,!0,!0))}Zb||b!==m?Ia(g):(ea=h.style,cc(g,d||g.img)),a.el.innerHTML="",a.el.appendChild(h)},cleanSlide:function(a){a.img&&(a.img.onload=a.img.onerror=null),a.loaded=a.loading=a.img=a.imageAppended=!1}}});var oc,pc={},qc=function(a,b,c){var d=document.createEvent("CustomEvent"),e={origEvent:a,target:a.target,releasePoint:b,pointerType:c||"touch"};d.initCustomEvent("pswpTap",!0,!0,e),a.target.dispatchEvent(d)};za("Tap",{publicMethods:{initTap:function(){Ca("firstTouchStart",f.onTapStart),Ca("touchRelease",f.onTapRelease),Ca("destroy",function(){pc={},oc=null})},onTapStart:function(a){a.length>1&&(clearTimeout(oc),oc=null)},onTapRelease:function(a,b){if(b&&!Y&&!W&&!_a){var c=b;if(oc&&(clearTimeout(oc),oc=null,xb(c,pc)))return void Da("doubleTap",c);if("mouse"===b.type)return void qc(a,b,"mouse");var d=a.target.tagName.toUpperCase();if("BUTTON"===d||e.hasClass(a.target,"pswp__single-tap"))return void qc(a,b);Ma(pc,c),oc=setTimeout(function(){qc(a,b),oc=null},300)}}}});var rc;za("DesktopZoom",{publicMethods:{initDesktopZoom:function(){L||(G?Ca("mouseUsed",function(){f.setupDesktopZoom()}):f.setupDesktopZoom(!0))},setupDesktopZoom:function(b){rc={};var c="wheel mousewheel DOMMouseScroll";Ca("bindEvents",function(){e.bind(a,c,f.handleMouseWheel)}),Ca("unbindEvents",function(){rc&&e.unbind(a,c,f.handleMouseWheel)}),f.mouseZoomedIn=!1;var d,g=function(){f.mouseZoomedIn&&(e.removeClass(a,"pswp--zoomed-in"),f.mouseZoomedIn=!1),s<1?e.addClass(a,"pswp--zoom-allowed"):e.removeClass(a,"pswp--zoom-allowed"),h()},h=function(){d&&(e.removeClass(a,"pswp--dragging"),d=!1)};Ca("resize",g),Ca("afterChange",g),Ca("pointerDown",function(){f.mouseZoomedIn&&(d=!0,e.addClass(a,"pswp--dragging"))}),Ca("pointerUp",h),b||g()},handleMouseWheel:function(a){if(s<=f.currItem.fitRatio)return i.modal&&(!i.closeOnScroll||_a||V?a.preventDefault():E&&Math.abs(a.deltaY)>2&&(l=!0,f.close())),!0;if(a.stopPropagation(),rc.x=0,"deltaX"in a)1===a.deltaMode?(rc.x=18*a.deltaX,rc.y=18*a.deltaY):(rc.x=a.deltaX,rc.y=a.deltaY);else if("wheelDelta"in a)a.wheelDeltaX&&(rc.x=-.16*a.wheelDeltaX),a.wheelDeltaY?rc.y=-.16*a.wheelDeltaY:rc.y=-.16*a.wheelDelta;else{if(!("detail"in a))return;rc.y=a.detail}Sa(s,!0);var b=pa.x-rc.x,c=pa.y-rc.y;(i.modal||b<=da.min.x&&b>=da.max.x&&c<=da.min.y&&c>=da.max.y)&&a.preventDefault(),f.panTo(b,c)},toggleDesktopZoom:function(b){b=b||{x:qa.x/2+sa.x,y:qa.y/2+sa.y};var c=i.getDoubleTapZoom(!0,f.currItem),d=s===c;f.mouseZoomedIn=!d,f.zoomTo(d?f.currItem.initialZoomLevel:c,b,333),e[(d?"remove":"add")+"Class"](a,"pswp--zoomed-in")}}});var sc,tc,uc,vc,wc,xc,yc,zc,Ac,Bc,Cc,Dc,Ec={history:!0,galleryUID:1},Fc=function(){return Cc.hash.substring(1)},Gc=function(){sc&&clearTimeout(sc),uc&&clearTimeout(uc)},Hc=function(){var a=Fc(),b={};if(a.length<5)return b;var c,d=a.split("&");for(c=0;c-1&&(yc=yc.substring(0,b),"&"===yc.slice(-1)&&(yc=yc.slice(0,-1))),setTimeout(function(){j&&e.bind(window,"hashchange",f.onHashChange)},40)}},onHashChange:function(){return Fc()===yc?(Ac=!0,void f.close()):void(vc||(wc=!0,f.goTo(Hc().pid),wc=!1))},updateURL:function(){Gc(),wc||(zc?sc=setTimeout(Ic,800):Ic())}}}),e.extend(f,eb)};return a}); --------------------------------------------------------------------------------